From d76154d15d9732878214ac0f3ec7572e1ad5f7cf Mon Sep 17 00:00:00 2001 From: khannurien Date: Tue, 8 Sep 2026 12:43:59 +0000 Subject: [PATCH] v3: keep pasted description images out of the dump kind and the card teasers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dump modal's window-level paste listener looked at the clipboard files before it looked at the event target, so an image pasted into the "Why?" field was claimed twice: the editor uploaded it as an inline attachment, and the modal switched itself to file mode with that same image as the dump. Writing a URL dump with an illustrated description was impossible. The target guard that already existed for text pastes now runs first and covers files too — fields own their paste, and the URL input keeps its own handler for the image-to-file-dump shortcut. Pasting onto the modal body, where nothing else is listening, still starts a file dump. Card teasers are line-clamped boxes, which an embedded image blows apart. Inline markdown — the mode the dump and journal cards already ask for — now drops img nodes, so a description image shows up on the detail page and nowhere else. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TiAPtJZeCYYk8rKehtLUQU --- src/components/DumpCreateModal.tsx | 8 ++++++-- src/components/Markdown.tsx | 20 +++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/components/DumpCreateModal.tsx b/src/components/DumpCreateModal.tsx index 3f104ec..4a628ef 100644 --- a/src/components/DumpCreateModal.tsx +++ b/src/components/DumpCreateModal.tsx @@ -186,6 +186,12 @@ export function DumpCreateModal( // Paste handler useEffect(() => { const handler = (e: ClipboardEvent) => { + // Fields own their paste: the comment editor uploads pasted images as + // inline attachments, and the URL input has its own file handler below. + // Without this guard, pasting an image into the description would flip + // the whole modal over to a file dump. + const tag = (e.target as HTMLElement).tagName; + if (tag === "INPUT" || tag === "TEXTAREA") return; const pastedFile = e.clipboardData?.files[0]; if (pastedFile) { setMode("file"); @@ -194,8 +200,6 @@ export function DumpCreateModal( clearErrors("root"); return; } - const tag = (e.target as HTMLElement).tagName; - if (tag === "INPUT" || tag === "TEXTAREA") return; const text = e.clipboardData?.getData("text") ?? ""; try { const u = new URL(normalizeUrl(text)); diff --git a/src/components/Markdown.tsx b/src/components/Markdown.tsx index 3395a59..7bb83a0 100644 --- a/src/components/Markdown.tsx +++ b/src/components/Markdown.tsx @@ -16,10 +16,10 @@ function preprocessMentions(text: string): string { return text.replace(/(?["components"] = { +type Components = React.ComponentProps["components"]; + +// Static components objects — defined once at module scope to avoid recreation on every render +const MARKDOWN_COMPONENTS: Components = { a: ({ href, children: linkChildren }) => { if (href?.startsWith("/users/")) { return {linkChildren}; @@ -32,6 +32,14 @@ const MARKDOWN_COMPONENTS: React.ComponentProps< }, }; +// Inline renderings are line-clamped teasers inside cards, where an embedded +// image blows the layout apart. Drop images there — the full description is +// one click away on the detail page. +const INLINE_MARKDOWN_COMPONENTS: Components = { + ...MARKDOWN_COMPONENTS, + img: () => null, +}; + export function Markdown( { children, className, inline = false }: MarkdownProps, ) { @@ -45,7 +53,9 @@ export function Markdown( > {processed}