From e23ca6e8d15d1d7ff6eb001f3844bde248518760 Mon Sep 17 00:00:00 2001 From: fen Date: Thu, 10 Sep 2026 11:08:02 -0500 Subject: [PATCH] file upload preview in editor on /new (#171) - images render fitted into the editor area (object-fit contain), editor hidden - text files load content into the editor; title placeholder convention (Python.py, Text.txt fallback) - title auto-fills only when blank; never overwrites a typed title - external JS + CSS classes only, CSP-safe (no inline styles/scripts) --- internal/web/static/app.css | 13 ++++++ internal/web/static/new.js | 76 +++++++++++++++++++++++++++++++++ internal/web/templates/new.html | 1 + 3 files changed, 90 insertions(+) diff --git a/internal/web/static/app.css b/internal/web/static/app.css index 98b9b7e..6db9158 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -744,6 +744,19 @@ button[type="submit"]:focus-visible, background: var(--surface-2); } .dropzone.dragover { border-width: 2px; } +/* #171: uploaded-file preview replaces the editor area; image fitted inside */ +.file-preview { + flex: 1; + min-width: 0; + min-height: 0; + max-width: 100%; + max-height: 100%; + object-fit: contain; + border-radius: 8px; +} +.editor-wrap.previewing .editor, +.editor-wrap.previewing .gutter { display: none; } + .file-chip { display: flex; align-items: center; gap: 10px; border: 1px solid var(--border); border-radius: var(--radius); diff --git a/internal/web/static/new.js b/internal/web/static/new.js index 6e81f19..bee7ce8 100644 --- a/internal/web/static/new.js +++ b/internal/web/static/new.js @@ -230,6 +230,64 @@ function humanSize(n) { return n + ' B'; } +// #171: text-file extensions -> language key for title placeholder +// convention (e.g. Python.py); fallback Text. +const LANG_BY_EXT = { + py: 'python', go: 'go', js: 'javascript', mjs: 'javascript', ts: 'typescript', + tsx: 'typescript', rs: 'rust', c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', hpp: 'cpp', + java: 'java', cs: 'csharp', sh: 'bash', bash: 'bash', sql: 'sql', yml: 'yaml', + yaml: 'yaml', json: 'json', html: 'html', htm: 'html', css: 'css', xml: 'xml', + php: 'php', rb: 'ruby', pl: 'perl', lua: 'lua', toml: 'toml', ini: 'ini', + diff: 'diff', md: 'markdown', txt: 'text', log: 'text', +}; +const TEXT_EXTS = new Set(Object.keys(LANG_BY_EXT)); + +function extOf(name) { + const i = name.lastIndexOf('.'); + return i >= 0 ? name.slice(i + 1).toLowerCase() : ''; +} + +// #171: when a file is attached it takes over the main editing area. +// Images render fitted into the editor area; text files load their +// content into the editor. Title auto-fills only if still blank (house +// rule: never overwrite a typed title). +const IMAGE_RE = /^image\//; +let previewURL = null; + +async function showFileInEditor(file) { + const wrap = document.querySelector('.editor-wrap'); + const img = $('file-preview'); + if (IMAGE_RE.test(file.type)) { + if (previewURL) URL.revokeObjectURL(previewURL); + previewURL = URL.createObjectURL(file); + img.src = previewURL; + img.alt = file.name; + wrap.classList.add('previewing'); + img.classList.remove('hidden'); + return; + } + wrap.classList.remove('previewing'); + img.classList.add('hidden'); + if (!TEXT_EXTS.has(extOf(file.name))) return; // unknown binary: leave editor alone + try { + const text = await file.text(); + if (!content.value.trim()) { + content.value = text; + updateGutter(); + guessLang(); + } + } catch (e) {} +} + +function clearPreview() { + const wrap = document.querySelector('.editor-wrap'); + const img = $('file-preview'); + wrap.classList.remove('previewing'); + img.classList.add('hidden'); + img.removeAttribute('src'); + if (previewURL) { URL.revokeObjectURL(previewURL); previewURL = null; } +} + function setAttachedFile(file) { if (!file) return clearAttachedFile(); if (file.size > MAX_FILE_BYTES) { @@ -239,6 +297,23 @@ function setAttachedFile(file) { attachedFile = file; renderFileChip(); setHidden('file-text-note', false); + // #171: title auto-fill — images take the file name; text files use the + // language-placeholder convention (e.g. Python.py, fallback Text.txt). + // Only when the title is still blank; never overwrite a typed title. + if (!$('title').value.trim()) { + if (IMAGE_RE.test(file.type)) { + $('title').value = file.name; + } else { + const lang = LANG_BY_EXT[extOf(file.name)]; + if (lang) { + const fn = defaultFilename(lang); + if (fn) $('title').value = fn; + } else if (TEXT_EXTS.has(extOf(file.name))) { + $('title').value = defaultFilename('text') || 'Text.txt'; + } + } + } + showFileInEditor(file); } function clearAttachedFile() { @@ -246,6 +321,7 @@ function clearAttachedFile() { $('file-input').value = ''; renderFileChip(); setHidden('file-text-note', true); + clearPreview(); } function renderFileChip() { diff --git a/internal/web/templates/new.html b/internal/web/templates/new.html index 877a5d3..9333744 100644 --- a/internal/web/templates/new.html +++ b/internal/web/templates/new.html @@ -26,6 +26,7 @@
1
+