refactor(words): refine drawer Editable — hover pencil + textarea + icon-only controls

User feedback on the previous DRAWER-EDITABLE pass: the "Edit / Save /
Cancel" labeled buttons were too heavy. The reference design is the
Notion / Linear inline-edit pattern — resting state shows only the
preview text, a small pencil reveals on hover, click opens a textarea
with compact icon-only ✓ / ✕ controls beneath.

Drawer refactor:
- `activationMode="none"` + `submitMode="none"` — only the explicit
  triggers drive state. Removes accidental commits on blur / Enter
  (Enter must be a newline in a textarea anyway).
- `Editable.Input` swapped to a textarea via the `child` snippet:
  `{#snippet child({ props })}<textarea {...props} rows={2}></textarea>`.
  ALT and CAPTION can run long; a single-line input was wrong.
- `EditTrigger` / `SubmitTrigger` / `CancelTrigger` now hold only an
  icon (Pencil / Check / X) with `decorative` flag + `aria-label` on
  the trigger button for screen readers.
- Wrapper attribute `data-words-drawer-editable` scopes all the
  custom CSS to these two Editables — other Editable instances in
  the app keep the canonical wrapper look.

CSS (`words.css`, scoped to `[data-editable][data-words-drawer-editable]`):
- Pencil hidden by default (opacity 0); revealed on `:hover` /
  `:focus-within` of the editable row.
- Edit mode flips layout to column: textarea fills the row, the
  Control bar sits beneath, right-aligned.
- Textarea has a real border + focus ring (was inheriting the ghost
  variant's transparent treatment, looked invisible against the drawer).
- Submit chip is filled accent (✓ stands out), Cancel chip is
  outlined neutral. Both 28×28 square.
- Preview placeholder ("Describe the image…") is italic + muted.

Verified in browser:
- Hover over ALT row → pencil appears at the right (opacity 0.7)
- Click pencil → textarea visible with current value preselected
- Below: purple ✓ + outlined ✕
- Type new value → click ✓ → `<img alt="...">` in the editor and the
  drawer preview both update; mode returns to preview.

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

@ -12,7 +12,15 @@
* Modes are derived in soma (`WordsDrawerProvider.modes`); eidos
* just paints them.
*/
import { ChevronRight, ChevronLeft, ChevronDown, ChevronUp } from '$uix/eidos/components/icon';
import {
ChevronRight,
ChevronLeft,
ChevronDown,
ChevronUp,
Pencil,
Check,
X
} from '$uix/eidos/components/icon';
import { Editable } from '$uix/eidos/components/editable';
import * as Words from '$soma/components/words';
import type {
@ -557,10 +565,11 @@
<p data-words-drawer-readonly-text>{block.src}</p>
<div data-words-drawer-section-subtitle>Alt</div>
<Editable
data-words-drawer-editable
value={block.alt ?? ''}
placeholder="Describe the image…"
activationMode="click"
submitMode="both"
activationMode="none"
submitMode="none"
size="sm"
variant="ghost"
color="primary"
@ -573,20 +582,31 @@
>
<Editable.Area>
<Editable.Preview />
<Editable.Input />
<Editable.Input>
{#snippet child({ props })}
<textarea {...props} rows={2}></textarea>
{/snippet}
</Editable.Input>
</Editable.Area>
<Editable.Control>
<Editable.EditTrigger>Edit</Editable.EditTrigger>
<Editable.SubmitTrigger>Save</Editable.SubmitTrigger>
<Editable.CancelTrigger>Cancel</Editable.CancelTrigger>
<Editable.EditTrigger aria-label="Edit alt text">
<Pencil size="xs" decorative />
</Editable.EditTrigger>
<Editable.SubmitTrigger aria-label="Save alt text">
<Check size="xs" decorative />
</Editable.SubmitTrigger>
<Editable.CancelTrigger aria-label="Cancel editing">
<X size="xs" decorative />
</Editable.CancelTrigger>
</Editable.Control>
</Editable>
<div data-words-drawer-section-subtitle>Caption</div>
<Editable
data-words-drawer-editable
value={block.caption ?? ''}
placeholder="Optional caption shown below the image"
activationMode="click"
submitMode="both"
activationMode="none"
submitMode="none"
size="sm"
variant="ghost"
color="primary"
@ -599,12 +619,22 @@
>
<Editable.Area>
<Editable.Preview />
<Editable.Input />
<Editable.Input>
{#snippet child({ props })}
<textarea {...props} rows={2}></textarea>
{/snippet}
</Editable.Input>
</Editable.Area>
<Editable.Control>
<Editable.EditTrigger>Edit</Editable.EditTrigger>
<Editable.SubmitTrigger>Save</Editable.SubmitTrigger>
<Editable.CancelTrigger>Cancel</Editable.CancelTrigger>
<Editable.EditTrigger aria-label="Edit caption">
<Pencil size="xs" decorative />
</Editable.EditTrigger>
<Editable.SubmitTrigger aria-label="Save caption">
<Check size="xs" decorative />
</Editable.SubmitTrigger>
<Editable.CancelTrigger aria-label="Cancel editing">
<X size="xs" decorative />
</Editable.CancelTrigger>
</Editable.Control>
</Editable>
{#if block.status}

@ -376,6 +376,136 @@
text-transform: uppercase;
}
/* Drawer-scoped Editable — Notion/Linear-style inline edit.
Resting state shows the preview with a faded pencil to its right
(revealed on hover or focus-within). Edit mode swaps to a textarea
(alt/caption can run long) with icon-only Submit (✓) and Cancel (✕)
beneath, right-aligned. Per the user's reference design. */
[data-editable][data-words-drawer-editable] {
display: flex;
inline-size: 100%;
max-inline-size: 100%;
align-items: center;
gap: 0.25rem;
}
[data-editable][data-words-drawer-editable] [data-editable-area] {
flex: 1 1 auto;
min-inline-size: 0;
}
[data-editable][data-words-drawer-editable] [data-editable-preview] {
inline-size: 100%;
min-block-size: auto;
white-space: normal;
line-height: 1.35;
color: var(--words-toolbar-color);
}
[data-editable][data-words-drawer-editable] [data-editable-preview][data-placeholder-shown] {
color: var(--words-status-color);
font-style: italic;
}
/* Pencil hides until the user signals intent (hover anywhere on the
editable row, focus-within, or focus on the preview itself). */
[data-editable][data-words-drawer-editable] [data-editable-edit-trigger] {
flex: 0 0 auto;
inline-size: 1.5rem;
block-size: 1.5rem;
min-inline-size: 1.5rem;
padding: 0;
border: 0;
background: transparent;
color: var(--words-status-color);
opacity: 0;
transition:
opacity 120ms ease,
background-color 120ms ease,
color 120ms ease;
}
[data-editable][data-words-drawer-editable]:hover [data-editable-edit-trigger],
[data-editable][data-words-drawer-editable]:focus-within [data-editable-edit-trigger] {
opacity: 0.7;
}
[data-editable][data-words-drawer-editable] [data-editable-edit-trigger]:hover {
opacity: 1;
background: var(--words-command-hover-bg);
color: var(--words-command-active-color);
}
/* Edit mode: stack vertically. Textarea fills the row, action chips
sit beneath right-aligned. */
[data-editable][data-words-drawer-editable][data-editing] {
flex-direction: column;
align-items: stretch;
}
[data-editable][data-words-drawer-editable][data-editing] [data-editable-area] {
inline-size: 100%;
}
[data-editable][data-words-drawer-editable] textarea[data-editable-input] {
inline-size: 100%;
min-block-size: 4.5rem;
resize: vertical;
padding: 0.4rem 0.5rem;
border: var(--words-border-width) solid var(--words-toolbar-border);
border-radius: var(--words-command-radius);
background: var(--words-bg, var(--words-toolbar-bg));
color: var(--words-toolbar-color);
font: inherit;
line-height: 1.35;
outline: none;
}
[data-editable][data-words-drawer-editable] textarea[data-editable-input]:focus-visible {
border-color: var(--_words-accent-border);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--_words-accent-solid) 22%, transparent);
}
[data-editable][data-words-drawer-editable][data-editing] [data-editable-control] {
display: inline-flex;
justify-content: flex-end;
gap: 0.25rem;
margin-block-start: 0.35rem;
}
/* Icon-only submit / cancel chips. Square, compact. */
[data-editable][data-words-drawer-editable] [data-editable-submit-trigger],
[data-editable][data-words-drawer-editable] [data-editable-cancel-trigger] {
flex: 0 0 auto;
inline-size: 1.75rem;
block-size: 1.75rem;
min-inline-size: 1.75rem;
padding: 0;
border-radius: var(--words-command-radius);
}
[data-editable][data-words-drawer-editable] [data-editable-submit-trigger] {
border: 0;
background: var(--_words-accent-solid);
color: var(--_words-accent-on);
}
[data-editable][data-words-drawer-editable] [data-editable-submit-trigger]:hover {
background: color-mix(in srgb, var(--_words-accent-solid) 85%, white 0%);
filter: brightness(0.95);
}
[data-editable][data-words-drawer-editable] [data-editable-cancel-trigger] {
border: var(--words-border-width) solid var(--words-toolbar-border);
background: transparent;
color: var(--words-status-color);
}
[data-editable][data-words-drawer-editable] [data-editable-cancel-trigger]:hover {
border-color: var(--words-threat-text);
color: var(--words-threat-text);
}
/* Drawer header trail — shows the breadcrumb of stacked modes after
the leaf so the user knows what scopes are stacked underneath. */
[data-words-drawer-trail] {

Loading…
Cancel
Save

Powered by TurnKey Linux.