feat(words): inline heading-level control in the Block drawer panel

Adds an H1/H2/H3 toggle to the drawer's Block panel, shown only when
the caret is in a heading (currentBlock === 'heading'). The toggle
reflects currentHeadingLevel and dispatches the structured
`setBlock` command with the numeric level via applyCommand:

  applyCommand({ type: 'setBlock', block: 'heading', level })

runCommand / WordsCommandName has no per-level id (heading-1/2/3 are
slash/insert-menu ids), so the structured command carries the level;
`as const` keeps each level a 1|2|3 literal.

Verified in browser: caret in a heading shows "Heading level"; the
toggle reflects the active level and converts bidirectionally
(H1→H3→H2) with the toggle updating. npm run check: 0 errors.

Also closes the stale "drag handle outside the rail over <code>" P1:
it does not reproduce — the rail was removed in the gutter redesign
(5450d5f7) and the handle now anchors to contentRect.left+8 for every
block type (verified: identical left over code and paragraph). Both
P1 items marked resolved in continue.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
active-uix
dev 4 months ago
parent cc4412b8e9
commit 005239c732

@ -148,21 +148,31 @@ panel renderiza los 4 controles nuevos; click en swatch de Background
actualiza el figure (`background-color:#22c55e`) preservando
radius/border/shadow (shallow-merge correcto). `npm run check` 0 errors.
## Pendiente para mañana
### P1 — Drag handle FUERA del rail sobre bloques `<code>` (heredado)
El grip se va fuera de la rail al hacer hover sobre un code block.
Sin diagnosticar. Lugar: `eidos/components/words/words-block-handle.svelte
> measureFromEl()`. Pasos: comparar `getBoundingClientRect().left` de
`<pre>` vs `<p>`; si difieren, buscar padding/margin/transform en
`[data-words-content] :where(pre)` en `words.css`.
### ✅ P1 — Drag handle sobre `<code>` (NO reproduce)
Investigado: NO reproduce en el código actual. El "rail" al que se
refería el bug fue ELIMINADO en el rediseño de gutter (commit
`5450d5f7`, "rail out"), posterior al sprint EV donde se reportó.
`measureFromEl` (`words-block-handle.svelte`) ancla el handle a
`contentRect.left + 8` — block-type-agnostic. Verificado en browser:
sobre code y sobre paragraph el gutter cae en `left: 350.333` idéntico
(solo difiere `top`). No hay nada que arreglar; bug obsoleto.
### ✅ P1 — Heading inline level change (h1 ↔ h2 ↔ h3)
Añadido al Block panel del drawer
(`eidos/components/words/words-drawer.svelte > blockPanel`): cuando
`currentBlock === 'heading'`, un ToggleGroup H1/H2/H3 (value =
`currentHeadingLevel`) despacha `applyCommand({ type: 'setBlock',
block: 'heading', level })`. `runCommand` / `WordsCommandName` no tiene
id por-nivel (`heading-1/2/3` son ids de slash / insert-menu), por eso
se usa el comando estructurado `setBlock` con el nivel numérico (`as
const` mantiene cada `level` como literal 1|2|3). Verificado en
browser: caret en heading → el panel muestra "Heading level"; el toggle
refleja el nivel actual; el click convierte bidireccional (H1→H3→H2) y
el toggle se actualiza. `npm run check` 0 errors.
### P1 — Heading inline level change (h1 ↔ h2 ↔ h3) (heredado)
Cambiar nivel de heading inline sin abrir drawer. Recomendación:
empezar por el Block panel del drawer (verificar que el HeadingPicker
se muestra cuando `currentBlock === 'heading'`).
## Pendiente para mañana
### P2 — Heredados

@ -196,6 +196,15 @@
{ id: 'align-right', label: '⫸' },
{ id: 'align-justify', label: '☷' }
] as const satisfies readonly { id: WordsCommandName; label: string }[];
// Heading level uses the structured `setBlock` command (carries the
// numeric level) — `runCommand` / WordsCommandName has no per-level
// id. `as const` keeps each `level` a 1|2|3 literal for the command.
const HEADING_LEVEL_ACTIONS = [
{ level: 1, value: '1', label: 'H1', title: 'Heading 1' },
{ level: 2, value: '2', label: 'H2', title: 'Heading 2' },
{ level: 3, value: '3', label: 'H3', title: 'Heading 3' }
] as const;
</script>
<Words.Drawer bind:open {defaultOpen} {disabled} {...rest} data-side={side}>
@ -530,6 +539,26 @@
{/snippet}
{#snippet blockPanel(s: ProviderSnippetProps)}
{#if s.currentBlock === 'heading'}
<div data-words-drawer-section-subtitle>Heading level</div>
<ToggleGroup
type="single"
size="sm"
variant="ghost"
value={s.currentHeadingLevel ? [String(s.currentHeadingLevel)] : []}
onValueChange={(next) => {
const action = HEADING_LEVEL_ACTIONS.find((a) => a.value === next[0]);
if (action) s.applyCommand({ type: 'setBlock', block: 'heading', level: action.level });
}}
aria-label="Heading level"
>
{#each HEADING_LEVEL_ACTIONS as h (h.value)}
<ToggleGroup.Item value={h.value} title={h.title} onmousedown={(e) => e.preventDefault()}>
{h.label}
</ToggleGroup.Item>
{/each}
</ToggleGroup>
{/if}
<div data-words-drawer-section-subtitle>Text align</div>
<ToggleGroup
type="single"

Loading…
Cancel
Save

Powered by TurnKey Linux.