From ffe2c56f4287aaa8df9ff010a94a5f4feb11b29b Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 27 May 2026 22:33:36 +0200 Subject: [PATCH] feat(words): use Editable for ALT/CAPTION in image drawer panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-suggested polish: replace the read-only `

` previews for the image alt and caption fields with the canonical Editable component. Same inline-edit shape used in the dedicated Editable demo — preview text with a small "Edit" trigger that swaps to an input + Save/Cancel on activation. Closer to the Notion/Figma/Linear interaction the user expects out of a sidebar property panel. Wiring: - `imageBlockAtCaret` now returns `{ index, block }` so the panel can dispatch `applyCommand({ type:'updateBlock', blockIndex, patch })` with the right index instead of re-walking the selection. - ALT: `onValueCommit={(next) => s.applyCommand({ type:'updateBlock', blockIndex, patch: { alt: next } })}`. `placeholder` "Describe the image…" surfaces when alt is empty. - CAPTION: same shape; `patch: { caption: next || undefined }` so clearing the field drops the field entirely from the block. - `size="sm" variant="ghost" color="primary"` chosen to fit the drawer's compact density without competing with the image preview. - Status (pending/error) stays as the existing readonly tag — not user-editable. Also: add `selectedBlockIndex: number | undefined` to `WordsProviderSnippetProps`. The provider was already emitting it (consumed by `imageBlockAtCaret` and the image float bar) but the type definition didn't list it. Verified in browser: clicking the image enters image mode in the drawer; clicking ALT's Edit trigger reveals the input; typing a new value and clicking Save propagated end-to-end — `…` in the editor and the preview both updated. Same flow for caption. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/words/words-drawer.svelte | 78 +++++++++++++++---- src/uix/soma/components/words/types.ts | 8 ++ 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/uix/eidos/components/words/words-drawer.svelte b/src/uix/eidos/components/words/words-drawer.svelte index e91f19b1f..593e71aa2 100644 --- a/src/uix/eidos/components/words/words-drawer.svelte +++ b/src/uix/eidos/components/words/words-drawer.svelte @@ -13,6 +13,7 @@ * just paints them. */ import { ChevronRight, ChevronLeft, ChevronDown, ChevronUp } from '$uix/eidos/components/icon'; + import { Editable } from '$uix/eidos/components/editable'; import * as Words from '$soma/components/words'; import type { DrawerSnippetProps, @@ -65,10 +66,13 @@ function imageBlockAtCaret(s: ProviderSnippetProps): | { - readonly src: string; - readonly alt?: string; - readonly caption?: string; - readonly status?: 'pending' | 'error'; + readonly index: number; + readonly block: { + readonly src: string; + readonly alt?: string; + readonly caption?: string; + readonly status?: 'pending' | 'error'; + }; } | undefined { // Atomic block selection wins (image is contenteditable=false so @@ -81,7 +85,7 @@ if (blockIndex === undefined) return undefined; const block = s.document.children[blockIndex]; if (!block || block.type !== 'image') return undefined; - return block; + return { index: blockIndex, block }; } const CODE_LANGUAGES = [ @@ -545,18 +549,64 @@ {/snippet} {#snippet imagePanel(s: ProviderSnippetProps)} - {@const block = imageBlockAtCaret(s)} - {#if block} + {@const entry = imageBlockAtCaret(s)} + {#if entry} + {@const block = entry.block} + {@const blockIndex = entry.index}

Source

{block.src}

Alt
-

- {block.alt || '(missing)'} -

- {#if block.caption} -
Caption
-

{block.caption}

- {/if} + + s.applyCommand({ + type: 'updateBlock', + blockIndex, + patch: { alt: next } + })} + > + + + + + + Edit + Save + Cancel + + +
Caption
+ + s.applyCommand({ + type: 'updateBlock', + blockIndex, + patch: { caption: next || undefined } + })} + > + + + + + + Edit + Save + Cancel + + {#if block.status}
Status

diff --git a/src/uix/soma/components/words/types.ts b/src/uix/soma/components/words/types.ts index 617bcca56..6f9781e96 100644 --- a/src/uix/soma/components/words/types.ts +++ b/src/uix/soma/components/words/types.ts @@ -100,6 +100,14 @@ export type WordsCommandName = export type WordsProviderSnippetProps = { readonly document: WordsDocument; readonly selection: WordsSelection | null; + /** + * Index of an atomic block currently selected via `selectAtomicBlock` + * (e.g. click on an image). Coexists with `selection` but takes + * priority for block-scoped UIs (drawer image panel, float bar) + * because atomic blocks are `contenteditable=false` and the text + * selection rarely points at them. + */ + readonly selectedBlockIndex: number | undefined; readonly plainText: string; readonly isEmpty: boolean; readonly isFocused: boolean;