feat(words): use Editable for ALT/CAPTION in image drawer panel

User-suggested polish: replace the read-only `<p>` 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 — `<img alt="…">` in
the editor and the preview both updated. Same flow for caption.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 66225404e1
commit ffe2c56f42

@ -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 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}
<div data-words-drawer-section-subtitle>Source</div>
<p data-words-drawer-readonly-text>{block.src}</p>
<div data-words-drawer-section-subtitle>Alt</div>
<p data-words-drawer-readonly-text data-empty={!block.alt}>
{block.alt || '(missing)'}
</p>
{#if block.caption}
<Editable
value={block.alt ?? ''}
placeholder="Describe the image…"
activationMode="click"
submitMode="both"
size="sm"
variant="ghost"
color="primary"
onValueCommit={(next) =>
s.applyCommand({
type: 'updateBlock',
blockIndex,
patch: { alt: next }
})}
>
<Editable.Area>
<Editable.Preview />
<Editable.Input />
</Editable.Area>
<Editable.Control>
<Editable.EditTrigger>Edit</Editable.EditTrigger>
<Editable.SubmitTrigger>Save</Editable.SubmitTrigger>
<Editable.CancelTrigger>Cancel</Editable.CancelTrigger>
</Editable.Control>
</Editable>
<div data-words-drawer-section-subtitle>Caption</div>
<p data-words-drawer-readonly-text>{block.caption}</p>
{/if}
<Editable
value={block.caption ?? ''}
placeholder="Optional caption shown below the image"
activationMode="click"
submitMode="both"
size="sm"
variant="ghost"
color="primary"
onValueCommit={(next) =>
s.applyCommand({
type: 'updateBlock',
blockIndex,
patch: { caption: next || undefined }
})}
>
<Editable.Area>
<Editable.Preview />
<Editable.Input />
</Editable.Area>
<Editable.Control>
<Editable.EditTrigger>Edit</Editable.EditTrigger>
<Editable.SubmitTrigger>Save</Editable.SubmitTrigger>
<Editable.CancelTrigger>Cancel</Editable.CancelTrigger>
</Editable.Control>
</Editable>
{#if block.status}
<div data-words-drawer-section-subtitle>Status</div>
<p data-words-drawer-readonly-text data-tone={block.status}>

@ -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;

Loading…
Cancel
Save

Powered by TurnKey Linux.