You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/eidos/components/words/words-drawer.svelte

617 lines
19 KiB

feat(words): companion drawer right-rail (concept A+D) — shell + default panel (DRAWER-1) A radical disposition shift: instead of N fragmented popovers (bubble menu + slash menu + link editor + heading picker + language picker + table popover + image controls) we mount ONE contextual right-rail that changes its panel based on what the user is editing. Spike-1 surface (this commit) ships: - New eidos component <Words.Drawer> (words-drawer.svelte) — a collapsible right sidebar with mode detection derived from snippet props (currentBlock + selection). 6 modes mapped: default | format | image | table | code | list. - Default panel (visible when no contextual mode active): - OUTLINE — flat list of h1/h2/h3 headings, indented by level - STATS — word count + char count + reading time (200wpm) - SUGGESTIONS — writer-coaching warnings derived from the document model: missing alt text, long paragraphs (>600 chars), heading skip patterns (h3 without h2 above). Empty state shows "Nothing flagged" in affirm color. - Contextual panels (format/image/table/code/list) render a placeholder "coming soon" message — implemented in DRAWER-2. - Eidos CSS: - Provider host gains a grid-template-areas layout when a drawer is present: 'toolbar drawer / content drawer / status drawer'. Existing children (toolbar/content/placeholder/status) get explicit grid-area assignments. - Drawer collapses to 2.25rem (just the chevron) when closed. 16rem default open. - Outline items, stats grid, warning list each have their own discrete styling. Demo (+page.svelte): - New `showDrawer` state (default true). - Children snippet refactored to capture full snippetProps via @const destructure (kept selectedLink working). - <Words.Drawer snippet={snippetProps} /> mounted inside the children block alongside the other parts. Visual verification: - Drawer renders as a dedicated right column with the editor content on the left, no layout overlap. - DOCUMENT header + OUTLINE shows "H1 ActiveUIX Words", STATS shows 73 words / 508 chars / 1 min reading, SUGGESTIONS shows "Nothing flagged" (sample doc has no warnings). - Collapse button toggles open/closed via animated inline-size transition. Next: - DRAWER-2: format/image/table/code/list contextual panels wired to runCommand. - DRAWER-3: polish (mobile drawer-as-bottom-sheet, transitions, A/B toggle in demo to compare classic toolbar vs companion drawer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
<script lang="ts">
/**
refactor(words): drawer canon — morfo part + soma provider + eidos consumes via provider (DRAWER-CANON) Closes the architectural gap from the DRAWER spike: the companion drawer now follows the same morfo/soma/eidos pattern as every other Words part (Toolbar/BubbleMenu/SlashMenu/LinkEditor/Status/etc.). DRAWER-CANON-1 — morfo + soma: - wordsMorfo gains a 'Drawer' part (kebab 'drawer', archetype 'group', defaultElement 'aside', role 'complementary'). Declares data-words- drawer-mode (default/format/image/table/code/list), data-open, data-disabled, aria-label. New event 'commit-toggle-drawer' fires on collapse/expand for sema/telemetry hooks. - New WordsDrawerProvider in words-provider.svelte: owns `open` bindable state, derives `mode` from parent provider's currentBlock + selection (same logic as the spike, now in soma where it belongs), exposes snippetProps {mode, open, disabled, snippet, toggle, setOpen}. `toggle()` and `setOpen()` emit the event. - WordsDrawerProps + WordsDrawerSnippetProps + WordsDrawerMode types added to types.ts and re-exported via exports.ts. - soma/components/words/components/words-drawer.svelte (the soma component) mounts the provider and forwards merged props to either the `child` snippet or the default <aside> render. DRAWER-CANON-2 — eidos refactor: - eidos words-drawer.svelte rewritten as a consumer of <Words.Drawer>. The shell (aside + header + collapse button) and the per-mode body now live inside the children snippet that receives the soma's drawer snippet props. Mode derivation removed from eidos (it's now in soma). Local open $state removed (now bindable via the soma component). - Demo +page.svelte: `<Words.Drawer snippet={snippetProps} />` collapses to plain `<Words.Drawer />` — the soma provider hands the parent snippet down internally. DRAWER-CANON-3 — verify: - 214/214 tests pass in soma+morfo scope (no regressions). - npx tsc --noEmit clean for words area. - Visual verification: drawer still renders with DOCUMENT/OUTLINE/ STATS/SUGGESTIONS in default mode; context-switching to table / code / list / format still works; aside element now carries role='complementary', aria-label from morfo translationRef, data-words-drawer-mode reflecting current panel. The eidos drawer now passes the same canon checks as every other Words part: morfo declares its contract, soma owns the state, eidos only paints. The schema validator can verify the drawer's attrs, the eidos lint can check selector usage, sema can react to the toggle event. Demo header chips reflect the new contract: PARTS 19→20, EVENTS 11→12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
* Words.Drawer — companion right-rail eidos wrapper.
feat(words): companion drawer right-rail (concept A+D) — shell + default panel (DRAWER-1) A radical disposition shift: instead of N fragmented popovers (bubble menu + slash menu + link editor + heading picker + language picker + table popover + image controls) we mount ONE contextual right-rail that changes its panel based on what the user is editing. Spike-1 surface (this commit) ships: - New eidos component <Words.Drawer> (words-drawer.svelte) — a collapsible right sidebar with mode detection derived from snippet props (currentBlock + selection). 6 modes mapped: default | format | image | table | code | list. - Default panel (visible when no contextual mode active): - OUTLINE — flat list of h1/h2/h3 headings, indented by level - STATS — word count + char count + reading time (200wpm) - SUGGESTIONS — writer-coaching warnings derived from the document model: missing alt text, long paragraphs (>600 chars), heading skip patterns (h3 without h2 above). Empty state shows "Nothing flagged" in affirm color. - Contextual panels (format/image/table/code/list) render a placeholder "coming soon" message — implemented in DRAWER-2. - Eidos CSS: - Provider host gains a grid-template-areas layout when a drawer is present: 'toolbar drawer / content drawer / status drawer'. Existing children (toolbar/content/placeholder/status) get explicit grid-area assignments. - Drawer collapses to 2.25rem (just the chevron) when closed. 16rem default open. - Outline items, stats grid, warning list each have their own discrete styling. Demo (+page.svelte): - New `showDrawer` state (default true). - Children snippet refactored to capture full snippetProps via @const destructure (kept selectedLink working). - <Words.Drawer snippet={snippetProps} /> mounted inside the children block alongside the other parts. Visual verification: - Drawer renders as a dedicated right column with the editor content on the left, no layout overlap. - DOCUMENT header + OUTLINE shows "H1 ActiveUIX Words", STATS shows 73 words / 508 chars / 1 min reading, SUGGESTIONS shows "Nothing flagged" (sample doc has no warnings). - Collapse button toggles open/closed via animated inline-size transition. Next: - DRAWER-2: format/image/table/code/list contextual panels wired to runCommand. - DRAWER-3: polish (mobile drawer-as-bottom-sheet, transitions, A/B toggle in demo to compare classic toolbar vs companion drawer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
*
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
* After HIER-1/2/3 the drawer renders a STACK of panels from
* most-specific to most-general. E.g. inside a table cell with
* text selected the user sees: Format → Cell → Row → Table —
* each panel scoped to its level so every relevant action is
* reachable from one surface (instead of guessing which popover/
* menu owns it).
feat(words): companion drawer right-rail (concept A+D) — shell + default panel (DRAWER-1) A radical disposition shift: instead of N fragmented popovers (bubble menu + slash menu + link editor + heading picker + language picker + table popover + image controls) we mount ONE contextual right-rail that changes its panel based on what the user is editing. Spike-1 surface (this commit) ships: - New eidos component <Words.Drawer> (words-drawer.svelte) — a collapsible right sidebar with mode detection derived from snippet props (currentBlock + selection). 6 modes mapped: default | format | image | table | code | list. - Default panel (visible when no contextual mode active): - OUTLINE — flat list of h1/h2/h3 headings, indented by level - STATS — word count + char count + reading time (200wpm) - SUGGESTIONS — writer-coaching warnings derived from the document model: missing alt text, long paragraphs (>600 chars), heading skip patterns (h3 without h2 above). Empty state shows "Nothing flagged" in affirm color. - Contextual panels (format/image/table/code/list) render a placeholder "coming soon" message — implemented in DRAWER-2. - Eidos CSS: - Provider host gains a grid-template-areas layout when a drawer is present: 'toolbar drawer / content drawer / status drawer'. Existing children (toolbar/content/placeholder/status) get explicit grid-area assignments. - Drawer collapses to 2.25rem (just the chevron) when closed. 16rem default open. - Outline items, stats grid, warning list each have their own discrete styling. Demo (+page.svelte): - New `showDrawer` state (default true). - Children snippet refactored to capture full snippetProps via @const destructure (kept selectedLink working). - <Words.Drawer snippet={snippetProps} /> mounted inside the children block alongside the other parts. Visual verification: - Drawer renders as a dedicated right column with the editor content on the left, no layout overlap. - DOCUMENT header + OUTLINE shows "H1 ActiveUIX Words", STATS shows 73 words / 508 chars / 1 min reading, SUGGESTIONS shows "Nothing flagged" (sample doc has no warnings). - Collapse button toggles open/closed via animated inline-size transition. Next: - DRAWER-2: format/image/table/code/list contextual panels wired to runCommand. - DRAWER-3: polish (mobile drawer-as-bottom-sheet, transitions, A/B toggle in demo to compare classic toolbar vs companion drawer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
*
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
* Modes are derived in soma (`WordsDrawerProvider.modes`); eidos
* just paints them.
feat(words): companion drawer right-rail (concept A+D) — shell + default panel (DRAWER-1) A radical disposition shift: instead of N fragmented popovers (bubble menu + slash menu + link editor + heading picker + language picker + table popover + image controls) we mount ONE contextual right-rail that changes its panel based on what the user is editing. Spike-1 surface (this commit) ships: - New eidos component <Words.Drawer> (words-drawer.svelte) — a collapsible right sidebar with mode detection derived from snippet props (currentBlock + selection). 6 modes mapped: default | format | image | table | code | list. - Default panel (visible when no contextual mode active): - OUTLINE — flat list of h1/h2/h3 headings, indented by level - STATS — word count + char count + reading time (200wpm) - SUGGESTIONS — writer-coaching warnings derived from the document model: missing alt text, long paragraphs (>600 chars), heading skip patterns (h3 without h2 above). Empty state shows "Nothing flagged" in affirm color. - Contextual panels (format/image/table/code/list) render a placeholder "coming soon" message — implemented in DRAWER-2. - Eidos CSS: - Provider host gains a grid-template-areas layout when a drawer is present: 'toolbar drawer / content drawer / status drawer'. Existing children (toolbar/content/placeholder/status) get explicit grid-area assignments. - Drawer collapses to 2.25rem (just the chevron) when closed. 16rem default open. - Outline items, stats grid, warning list each have their own discrete styling. Demo (+page.svelte): - New `showDrawer` state (default true). - Children snippet refactored to capture full snippetProps via @const destructure (kept selectedLink working). - <Words.Drawer snippet={snippetProps} /> mounted inside the children block alongside the other parts. Visual verification: - Drawer renders as a dedicated right column with the editor content on the left, no layout overlap. - DOCUMENT header + OUTLINE shows "H1 ActiveUIX Words", STATS shows 73 words / 508 chars / 1 min reading, SUGGESTIONS shows "Nothing flagged" (sample doc has no warnings). - Collapse button toggles open/closed via animated inline-size transition. Next: - DRAWER-2: format/image/table/code/list contextual panels wired to runCommand. - DRAWER-3: polish (mobile drawer-as-bottom-sheet, transitions, A/B toggle in demo to compare classic toolbar vs companion drawer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
*/
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
import { ChevronRight, ChevronLeft, ChevronDown, ChevronUp } from '$uix/eidos/components/icon';
refactor(words): drawer canon — morfo part + soma provider + eidos consumes via provider (DRAWER-CANON) Closes the architectural gap from the DRAWER spike: the companion drawer now follows the same morfo/soma/eidos pattern as every other Words part (Toolbar/BubbleMenu/SlashMenu/LinkEditor/Status/etc.). DRAWER-CANON-1 — morfo + soma: - wordsMorfo gains a 'Drawer' part (kebab 'drawer', archetype 'group', defaultElement 'aside', role 'complementary'). Declares data-words- drawer-mode (default/format/image/table/code/list), data-open, data-disabled, aria-label. New event 'commit-toggle-drawer' fires on collapse/expand for sema/telemetry hooks. - New WordsDrawerProvider in words-provider.svelte: owns `open` bindable state, derives `mode` from parent provider's currentBlock + selection (same logic as the spike, now in soma where it belongs), exposes snippetProps {mode, open, disabled, snippet, toggle, setOpen}. `toggle()` and `setOpen()` emit the event. - WordsDrawerProps + WordsDrawerSnippetProps + WordsDrawerMode types added to types.ts and re-exported via exports.ts. - soma/components/words/components/words-drawer.svelte (the soma component) mounts the provider and forwards merged props to either the `child` snippet or the default <aside> render. DRAWER-CANON-2 — eidos refactor: - eidos words-drawer.svelte rewritten as a consumer of <Words.Drawer>. The shell (aside + header + collapse button) and the per-mode body now live inside the children snippet that receives the soma's drawer snippet props. Mode derivation removed from eidos (it's now in soma). Local open $state removed (now bindable via the soma component). - Demo +page.svelte: `<Words.Drawer snippet={snippetProps} />` collapses to plain `<Words.Drawer />` — the soma provider hands the parent snippet down internally. DRAWER-CANON-3 — verify: - 214/214 tests pass in soma+morfo scope (no regressions). - npx tsc --noEmit clean for words area. - Visual verification: drawer still renders with DOCUMENT/OUTLINE/ STATS/SUGGESTIONS in default mode; context-switching to table / code / list / format still works; aside element now carries role='complementary', aria-label from morfo translationRef, data-words-drawer-mode reflecting current panel. The eidos drawer now passes the same canon checks as every other Words part: morfo declares its contract, soma owns the state, eidos only paints. The schema validator can verify the drawer's attrs, the eidos lint can check selector usage, sema can react to the toggle event. Demo header chips reflect the new contract: PARTS 19→20, EVENTS 11→12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
import * as Words from '$soma/components/words';
feat(words): companion drawer right-rail (concept A+D) — shell + default panel (DRAWER-1) A radical disposition shift: instead of N fragmented popovers (bubble menu + slash menu + link editor + heading picker + language picker + table popover + image controls) we mount ONE contextual right-rail that changes its panel based on what the user is editing. Spike-1 surface (this commit) ships: - New eidos component <Words.Drawer> (words-drawer.svelte) — a collapsible right sidebar with mode detection derived from snippet props (currentBlock + selection). 6 modes mapped: default | format | image | table | code | list. - Default panel (visible when no contextual mode active): - OUTLINE — flat list of h1/h2/h3 headings, indented by level - STATS — word count + char count + reading time (200wpm) - SUGGESTIONS — writer-coaching warnings derived from the document model: missing alt text, long paragraphs (>600 chars), heading skip patterns (h3 without h2 above). Empty state shows "Nothing flagged" in affirm color. - Contextual panels (format/image/table/code/list) render a placeholder "coming soon" message — implemented in DRAWER-2. - Eidos CSS: - Provider host gains a grid-template-areas layout when a drawer is present: 'toolbar drawer / content drawer / status drawer'. Existing children (toolbar/content/placeholder/status) get explicit grid-area assignments. - Drawer collapses to 2.25rem (just the chevron) when closed. 16rem default open. - Outline items, stats grid, warning list each have their own discrete styling. Demo (+page.svelte): - New `showDrawer` state (default true). - Children snippet refactored to capture full snippetProps via @const destructure (kept selectedLink working). - <Words.Drawer snippet={snippetProps} /> mounted inside the children block alongside the other parts. Visual verification: - Drawer renders as a dedicated right column with the editor content on the left, no layout overlap. - DOCUMENT header + OUTLINE shows "H1 ActiveUIX Words", STATS shows 73 words / 508 chars / 1 min reading, SUGGESTIONS shows "Nothing flagged" (sample doc has no warnings). - Collapse button toggles open/closed via animated inline-size transition. Next: - DRAWER-2: format/image/table/code/list contextual panels wired to runCommand. - DRAWER-3: polish (mobile drawer-as-bottom-sheet, transitions, A/B toggle in demo to compare classic toolbar vs companion drawer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
import type {
refactor(words): drawer canon — morfo part + soma provider + eidos consumes via provider (DRAWER-CANON) Closes the architectural gap from the DRAWER spike: the companion drawer now follows the same morfo/soma/eidos pattern as every other Words part (Toolbar/BubbleMenu/SlashMenu/LinkEditor/Status/etc.). DRAWER-CANON-1 — morfo + soma: - wordsMorfo gains a 'Drawer' part (kebab 'drawer', archetype 'group', defaultElement 'aside', role 'complementary'). Declares data-words- drawer-mode (default/format/image/table/code/list), data-open, data-disabled, aria-label. New event 'commit-toggle-drawer' fires on collapse/expand for sema/telemetry hooks. - New WordsDrawerProvider in words-provider.svelte: owns `open` bindable state, derives `mode` from parent provider's currentBlock + selection (same logic as the spike, now in soma where it belongs), exposes snippetProps {mode, open, disabled, snippet, toggle, setOpen}. `toggle()` and `setOpen()` emit the event. - WordsDrawerProps + WordsDrawerSnippetProps + WordsDrawerMode types added to types.ts and re-exported via exports.ts. - soma/components/words/components/words-drawer.svelte (the soma component) mounts the provider and forwards merged props to either the `child` snippet or the default <aside> render. DRAWER-CANON-2 — eidos refactor: - eidos words-drawer.svelte rewritten as a consumer of <Words.Drawer>. The shell (aside + header + collapse button) and the per-mode body now live inside the children snippet that receives the soma's drawer snippet props. Mode derivation removed from eidos (it's now in soma). Local open $state removed (now bindable via the soma component). - Demo +page.svelte: `<Words.Drawer snippet={snippetProps} />` collapses to plain `<Words.Drawer />` — the soma provider hands the parent snippet down internally. DRAWER-CANON-3 — verify: - 214/214 tests pass in soma+morfo scope (no regressions). - npx tsc --noEmit clean for words area. - Visual verification: drawer still renders with DOCUMENT/OUTLINE/ STATS/SUGGESTIONS in default mode; context-switching to table / code / list / format still works; aside element now carries role='complementary', aria-label from morfo translationRef, data-words-drawer-mode reflecting current panel. The eidos drawer now passes the same canon checks as every other Words part: morfo declares its contract, soma owns the state, eidos only paints. The schema validator can verify the drawer's attrs, the eidos lint can check selector usage, sema can react to the toggle event. Demo header chips reflect the new contract: PARTS 19→20, EVENTS 11→12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
DrawerSnippetProps,
DrawerProps,
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
ProviderSnippetProps,
WordsDrawerMode
feat(words): companion drawer right-rail (concept A+D) — shell + default panel (DRAWER-1) A radical disposition shift: instead of N fragmented popovers (bubble menu + slash menu + link editor + heading picker + language picker + table popover + image controls) we mount ONE contextual right-rail that changes its panel based on what the user is editing. Spike-1 surface (this commit) ships: - New eidos component <Words.Drawer> (words-drawer.svelte) — a collapsible right sidebar with mode detection derived from snippet props (currentBlock + selection). 6 modes mapped: default | format | image | table | code | list. - Default panel (visible when no contextual mode active): - OUTLINE — flat list of h1/h2/h3 headings, indented by level - STATS — word count + char count + reading time (200wpm) - SUGGESTIONS — writer-coaching warnings derived from the document model: missing alt text, long paragraphs (>600 chars), heading skip patterns (h3 without h2 above). Empty state shows "Nothing flagged" in affirm color. - Contextual panels (format/image/table/code/list) render a placeholder "coming soon" message — implemented in DRAWER-2. - Eidos CSS: - Provider host gains a grid-template-areas layout when a drawer is present: 'toolbar drawer / content drawer / status drawer'. Existing children (toolbar/content/placeholder/status) get explicit grid-area assignments. - Drawer collapses to 2.25rem (just the chevron) when closed. 16rem default open. - Outline items, stats grid, warning list each have their own discrete styling. Demo (+page.svelte): - New `showDrawer` state (default true). - Children snippet refactored to capture full snippetProps via @const destructure (kept selectedLink working). - <Words.Drawer snippet={snippetProps} /> mounted inside the children block alongside the other parts. Visual verification: - Drawer renders as a dedicated right column with the editor content on the left, no layout overlap. - DOCUMENT header + OUTLINE shows "H1 ActiveUIX Words", STATS shows 73 words / 508 chars / 1 min reading, SUGGESTIONS shows "Nothing flagged" (sample doc has no warnings). - Collapse button toggles open/closed via animated inline-size transition. Next: - DRAWER-2: format/image/table/code/list contextual panels wired to runCommand. - DRAWER-3: polish (mobile drawer-as-bottom-sheet, transitions, A/B toggle in demo to compare classic toolbar vs companion drawer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
} from '$soma/components/words';
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
import { SvelteMap } from 'svelte/reactivity';
feat(words): companion drawer right-rail (concept A+D) — shell + default panel (DRAWER-1) A radical disposition shift: instead of N fragmented popovers (bubble menu + slash menu + link editor + heading picker + language picker + table popover + image controls) we mount ONE contextual right-rail that changes its panel based on what the user is editing. Spike-1 surface (this commit) ships: - New eidos component <Words.Drawer> (words-drawer.svelte) — a collapsible right sidebar with mode detection derived from snippet props (currentBlock + selection). 6 modes mapped: default | format | image | table | code | list. - Default panel (visible when no contextual mode active): - OUTLINE — flat list of h1/h2/h3 headings, indented by level - STATS — word count + char count + reading time (200wpm) - SUGGESTIONS — writer-coaching warnings derived from the document model: missing alt text, long paragraphs (>600 chars), heading skip patterns (h3 without h2 above). Empty state shows "Nothing flagged" in affirm color. - Contextual panels (format/image/table/code/list) render a placeholder "coming soon" message — implemented in DRAWER-2. - Eidos CSS: - Provider host gains a grid-template-areas layout when a drawer is present: 'toolbar drawer / content drawer / status drawer'. Existing children (toolbar/content/placeholder/status) get explicit grid-area assignments. - Drawer collapses to 2.25rem (just the chevron) when closed. 16rem default open. - Outline items, stats grid, warning list each have their own discrete styling. Demo (+page.svelte): - New `showDrawer` state (default true). - Children snippet refactored to capture full snippetProps via @const destructure (kept selectedLink working). - <Words.Drawer snippet={snippetProps} /> mounted inside the children block alongside the other parts. Visual verification: - Drawer renders as a dedicated right column with the editor content on the left, no layout overlap. - DOCUMENT header + OUTLINE shows "H1 ActiveUIX Words", STATS shows 73 words / 508 chars / 1 min reading, SUGGESTIONS shows "Nothing flagged" (sample doc has no warnings). - Collapse button toggles open/closed via animated inline-size transition. Next: - DRAWER-2: format/image/table/code/list contextual panels wired to runCommand. - DRAWER-3: polish (mobile drawer-as-bottom-sheet, transitions, A/B toggle in demo to compare classic toolbar vs companion drawer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
refactor(words): drawer canon — morfo part + soma provider + eidos consumes via provider (DRAWER-CANON) Closes the architectural gap from the DRAWER spike: the companion drawer now follows the same morfo/soma/eidos pattern as every other Words part (Toolbar/BubbleMenu/SlashMenu/LinkEditor/Status/etc.). DRAWER-CANON-1 — morfo + soma: - wordsMorfo gains a 'Drawer' part (kebab 'drawer', archetype 'group', defaultElement 'aside', role 'complementary'). Declares data-words- drawer-mode (default/format/image/table/code/list), data-open, data-disabled, aria-label. New event 'commit-toggle-drawer' fires on collapse/expand for sema/telemetry hooks. - New WordsDrawerProvider in words-provider.svelte: owns `open` bindable state, derives `mode` from parent provider's currentBlock + selection (same logic as the spike, now in soma where it belongs), exposes snippetProps {mode, open, disabled, snippet, toggle, setOpen}. `toggle()` and `setOpen()` emit the event. - WordsDrawerProps + WordsDrawerSnippetProps + WordsDrawerMode types added to types.ts and re-exported via exports.ts. - soma/components/words/components/words-drawer.svelte (the soma component) mounts the provider and forwards merged props to either the `child` snippet or the default <aside> render. DRAWER-CANON-2 — eidos refactor: - eidos words-drawer.svelte rewritten as a consumer of <Words.Drawer>. The shell (aside + header + collapse button) and the per-mode body now live inside the children snippet that receives the soma's drawer snippet props. Mode derivation removed from eidos (it's now in soma). Local open $state removed (now bindable via the soma component). - Demo +page.svelte: `<Words.Drawer snippet={snippetProps} />` collapses to plain `<Words.Drawer />` — the soma provider hands the parent snippet down internally. DRAWER-CANON-3 — verify: - 214/214 tests pass in soma+morfo scope (no regressions). - npx tsc --noEmit clean for words area. - Visual verification: drawer still renders with DOCUMENT/OUTLINE/ STATS/SUGGESTIONS in default mode; context-switching to table / code / list / format still works; aside element now carries role='complementary', aria-label from morfo translationRef, data-words-drawer-mode reflecting current panel. The eidos drawer now passes the same canon checks as every other Words part: morfo declares its contract, soma owns the state, eidos only paints. The schema validator can verify the drawer's attrs, the eidos lint can check selector usage, sema can react to the toggle event. Demo header chips reflect the new contract: PARTS 19→20, EVENTS 11→12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
let {
open = $bindable(undefined),
defaultOpen = true,
disabled = false,
children: customChildren,
...rest
}: Omit<DrawerProps, 'child'> = $props();
feat(words): companion drawer right-rail (concept A+D) — shell + default panel (DRAWER-1) A radical disposition shift: instead of N fragmented popovers (bubble menu + slash menu + link editor + heading picker + language picker + table popover + image controls) we mount ONE contextual right-rail that changes its panel based on what the user is editing. Spike-1 surface (this commit) ships: - New eidos component <Words.Drawer> (words-drawer.svelte) — a collapsible right sidebar with mode detection derived from snippet props (currentBlock + selection). 6 modes mapped: default | format | image | table | code | list. - Default panel (visible when no contextual mode active): - OUTLINE — flat list of h1/h2/h3 headings, indented by level - STATS — word count + char count + reading time (200wpm) - SUGGESTIONS — writer-coaching warnings derived from the document model: missing alt text, long paragraphs (>600 chars), heading skip patterns (h3 without h2 above). Empty state shows "Nothing flagged" in affirm color. - Contextual panels (format/image/table/code/list) render a placeholder "coming soon" message — implemented in DRAWER-2. - Eidos CSS: - Provider host gains a grid-template-areas layout when a drawer is present: 'toolbar drawer / content drawer / status drawer'. Existing children (toolbar/content/placeholder/status) get explicit grid-area assignments. - Drawer collapses to 2.25rem (just the chevron) when closed. 16rem default open. - Outline items, stats grid, warning list each have their own discrete styling. Demo (+page.svelte): - New `showDrawer` state (default true). - Children snippet refactored to capture full snippetProps via @const destructure (kept selectedLink working). - <Words.Drawer snippet={snippetProps} /> mounted inside the children block alongside the other parts. Visual verification: - Drawer renders as a dedicated right column with the editor content on the left, no layout overlap. - DOCUMENT header + OUTLINE shows "H1 ActiveUIX Words", STATS shows 73 words / 508 chars / 1 min reading, SUGGESTIONS shows "Nothing flagged" (sample doc has no warnings). - Collapse button toggles open/closed via animated inline-size transition. Next: - DRAWER-2: format/image/table/code/list contextual panels wired to runCommand. - DRAWER-3: polish (mobile drawer-as-bottom-sheet, transitions, A/B toggle in demo to compare classic toolbar vs companion drawer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
const MODE_LABELS: Record<WordsDrawerMode, string> = {
feat(words): companion drawer right-rail (concept A+D) — shell + default panel (DRAWER-1) A radical disposition shift: instead of N fragmented popovers (bubble menu + slash menu + link editor + heading picker + language picker + table popover + image controls) we mount ONE contextual right-rail that changes its panel based on what the user is editing. Spike-1 surface (this commit) ships: - New eidos component <Words.Drawer> (words-drawer.svelte) — a collapsible right sidebar with mode detection derived from snippet props (currentBlock + selection). 6 modes mapped: default | format | image | table | code | list. - Default panel (visible when no contextual mode active): - OUTLINE — flat list of h1/h2/h3 headings, indented by level - STATS — word count + char count + reading time (200wpm) - SUGGESTIONS — writer-coaching warnings derived from the document model: missing alt text, long paragraphs (>600 chars), heading skip patterns (h3 without h2 above). Empty state shows "Nothing flagged" in affirm color. - Contextual panels (format/image/table/code/list) render a placeholder "coming soon" message — implemented in DRAWER-2. - Eidos CSS: - Provider host gains a grid-template-areas layout when a drawer is present: 'toolbar drawer / content drawer / status drawer'. Existing children (toolbar/content/placeholder/status) get explicit grid-area assignments. - Drawer collapses to 2.25rem (just the chevron) when closed. 16rem default open. - Outline items, stats grid, warning list each have their own discrete styling. Demo (+page.svelte): - New `showDrawer` state (default true). - Children snippet refactored to capture full snippetProps via @const destructure (kept selectedLink working). - <Words.Drawer snippet={snippetProps} /> mounted inside the children block alongside the other parts. Visual verification: - Drawer renders as a dedicated right column with the editor content on the left, no layout overlap. - DOCUMENT header + OUTLINE shows "H1 ActiveUIX Words", STATS shows 73 words / 508 chars / 1 min reading, SUGGESTIONS shows "Nothing flagged" (sample doc has no warnings). - Collapse button toggles open/closed via animated inline-size transition. Next: - DRAWER-2: format/image/table/code/list contextual panels wired to runCommand. - DRAWER-3: polish (mobile drawer-as-bottom-sheet, transitions, A/B toggle in demo to compare classic toolbar vs companion drawer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
default: 'Document',
format: 'Format',
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
block: 'Block',
cell: 'Cell',
row: 'Row',
feat(words): companion drawer right-rail (concept A+D) — shell + default panel (DRAWER-1) A radical disposition shift: instead of N fragmented popovers (bubble menu + slash menu + link editor + heading picker + language picker + table popover + image controls) we mount ONE contextual right-rail that changes its panel based on what the user is editing. Spike-1 surface (this commit) ships: - New eidos component <Words.Drawer> (words-drawer.svelte) — a collapsible right sidebar with mode detection derived from snippet props (currentBlock + selection). 6 modes mapped: default | format | image | table | code | list. - Default panel (visible when no contextual mode active): - OUTLINE — flat list of h1/h2/h3 headings, indented by level - STATS — word count + char count + reading time (200wpm) - SUGGESTIONS — writer-coaching warnings derived from the document model: missing alt text, long paragraphs (>600 chars), heading skip patterns (h3 without h2 above). Empty state shows "Nothing flagged" in affirm color. - Contextual panels (format/image/table/code/list) render a placeholder "coming soon" message — implemented in DRAWER-2. - Eidos CSS: - Provider host gains a grid-template-areas layout when a drawer is present: 'toolbar drawer / content drawer / status drawer'. Existing children (toolbar/content/placeholder/status) get explicit grid-area assignments. - Drawer collapses to 2.25rem (just the chevron) when closed. 16rem default open. - Outline items, stats grid, warning list each have their own discrete styling. Demo (+page.svelte): - New `showDrawer` state (default true). - Children snippet refactored to capture full snippetProps via @const destructure (kept selectedLink working). - <Words.Drawer snippet={snippetProps} /> mounted inside the children block alongside the other parts. Visual verification: - Drawer renders as a dedicated right column with the editor content on the left, no layout overlap. - DOCUMENT header + OUTLINE shows "H1 ActiveUIX Words", STATS shows 73 words / 508 chars / 1 min reading, SUGGESTIONS shows "Nothing flagged" (sample doc has no warnings). - Collapse button toggles open/closed via animated inline-size transition. Next: - DRAWER-2: format/image/table/code/list contextual panels wired to runCommand. - DRAWER-3: polish (mobile drawer-as-bottom-sheet, transitions, A/B toggle in demo to compare classic toolbar vs companion drawer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
table: 'Table',
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
'list-item': 'List item',
list: 'List',
feat(words): companion drawer right-rail (concept A+D) — shell + default panel (DRAWER-1) A radical disposition shift: instead of N fragmented popovers (bubble menu + slash menu + link editor + heading picker + language picker + table popover + image controls) we mount ONE contextual right-rail that changes its panel based on what the user is editing. Spike-1 surface (this commit) ships: - New eidos component <Words.Drawer> (words-drawer.svelte) — a collapsible right sidebar with mode detection derived from snippet props (currentBlock + selection). 6 modes mapped: default | format | image | table | code | list. - Default panel (visible when no contextual mode active): - OUTLINE — flat list of h1/h2/h3 headings, indented by level - STATS — word count + char count + reading time (200wpm) - SUGGESTIONS — writer-coaching warnings derived from the document model: missing alt text, long paragraphs (>600 chars), heading skip patterns (h3 without h2 above). Empty state shows "Nothing flagged" in affirm color. - Contextual panels (format/image/table/code/list) render a placeholder "coming soon" message — implemented in DRAWER-2. - Eidos CSS: - Provider host gains a grid-template-areas layout when a drawer is present: 'toolbar drawer / content drawer / status drawer'. Existing children (toolbar/content/placeholder/status) get explicit grid-area assignments. - Drawer collapses to 2.25rem (just the chevron) when closed. 16rem default open. - Outline items, stats grid, warning list each have their own discrete styling. Demo (+page.svelte): - New `showDrawer` state (default true). - Children snippet refactored to capture full snippetProps via @const destructure (kept selectedLink working). - <Words.Drawer snippet={snippetProps} /> mounted inside the children block alongside the other parts. Visual verification: - Drawer renders as a dedicated right column with the editor content on the left, no layout overlap. - DOCUMENT header + OUTLINE shows "H1 ActiveUIX Words", STATS shows 73 words / 508 chars / 1 min reading, SUGGESTIONS shows "Nothing flagged" (sample doc has no warnings). - Collapse button toggles open/closed via animated inline-size transition. Next: - DRAWER-2: format/image/table/code/list contextual panels wired to runCommand. - DRAWER-3: polish (mobile drawer-as-bottom-sheet, transitions, A/B toggle in demo to compare classic toolbar vs companion drawer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
code: 'Code',
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
image: 'Image'
feat(words): companion drawer right-rail (concept A+D) — shell + default panel (DRAWER-1) A radical disposition shift: instead of N fragmented popovers (bubble menu + slash menu + link editor + heading picker + language picker + table popover + image controls) we mount ONE contextual right-rail that changes its panel based on what the user is editing. Spike-1 surface (this commit) ships: - New eidos component <Words.Drawer> (words-drawer.svelte) — a collapsible right sidebar with mode detection derived from snippet props (currentBlock + selection). 6 modes mapped: default | format | image | table | code | list. - Default panel (visible when no contextual mode active): - OUTLINE — flat list of h1/h2/h3 headings, indented by level - STATS — word count + char count + reading time (200wpm) - SUGGESTIONS — writer-coaching warnings derived from the document model: missing alt text, long paragraphs (>600 chars), heading skip patterns (h3 without h2 above). Empty state shows "Nothing flagged" in affirm color. - Contextual panels (format/image/table/code/list) render a placeholder "coming soon" message — implemented in DRAWER-2. - Eidos CSS: - Provider host gains a grid-template-areas layout when a drawer is present: 'toolbar drawer / content drawer / status drawer'. Existing children (toolbar/content/placeholder/status) get explicit grid-area assignments. - Drawer collapses to 2.25rem (just the chevron) when closed. 16rem default open. - Outline items, stats grid, warning list each have their own discrete styling. Demo (+page.svelte): - New `showDrawer` state (default true). - Children snippet refactored to capture full snippetProps via @const destructure (kept selectedLink working). - <Words.Drawer snippet={snippetProps} /> mounted inside the children block alongside the other parts. Visual verification: - Drawer renders as a dedicated right column with the editor content on the left, no layout overlap. - DOCUMENT header + OUTLINE shows "H1 ActiveUIX Words", STATS shows 73 words / 508 chars / 1 min reading, SUGGESTIONS shows "Nothing flagged" (sample doc has no warnings). - Collapse button toggles open/closed via animated inline-size transition. Next: - DRAWER-2: format/image/table/code/list contextual panels wired to runCommand. - DRAWER-3: polish (mobile drawer-as-bottom-sheet, transitions, A/B toggle in demo to compare classic toolbar vs companion drawer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
};
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
// Per-panel expand/collapse map. Default heuristic: first mode in
// the stack (most specific) is open, the rest collapsed. User
// explicit toggles override the default and persist across context
// changes.
const panelOpenOverrides = new SvelteMap<WordsDrawerMode, boolean>();
function isPanelOpen(mode: WordsDrawerMode, modes: readonly WordsDrawerMode[]): boolean {
const override = panelOpenOverrides.get(mode);
if (override !== undefined) return override;
// Default: only the top of the stack is open.
return modes[0] === mode;
}
function togglePanel(mode: WordsDrawerMode, modes: readonly WordsDrawerMode[]) {
panelOpenOverrides.set(mode, !isPanelOpen(mode, modes));
}
feat(words): companion drawer right-rail (concept A+D) — shell + default panel (DRAWER-1) A radical disposition shift: instead of N fragmented popovers (bubble menu + slash menu + link editor + heading picker + language picker + table popover + image controls) we mount ONE contextual right-rail that changes its panel based on what the user is editing. Spike-1 surface (this commit) ships: - New eidos component <Words.Drawer> (words-drawer.svelte) — a collapsible right sidebar with mode detection derived from snippet props (currentBlock + selection). 6 modes mapped: default | format | image | table | code | list. - Default panel (visible when no contextual mode active): - OUTLINE — flat list of h1/h2/h3 headings, indented by level - STATS — word count + char count + reading time (200wpm) - SUGGESTIONS — writer-coaching warnings derived from the document model: missing alt text, long paragraphs (>600 chars), heading skip patterns (h3 without h2 above). Empty state shows "Nothing flagged" in affirm color. - Contextual panels (format/image/table/code/list) render a placeholder "coming soon" message — implemented in DRAWER-2. - Eidos CSS: - Provider host gains a grid-template-areas layout when a drawer is present: 'toolbar drawer / content drawer / status drawer'. Existing children (toolbar/content/placeholder/status) get explicit grid-area assignments. - Drawer collapses to 2.25rem (just the chevron) when closed. 16rem default open. - Outline items, stats grid, warning list each have their own discrete styling. Demo (+page.svelte): - New `showDrawer` state (default true). - Children snippet refactored to capture full snippetProps via @const destructure (kept selectedLink working). - <Words.Drawer snippet={snippetProps} /> mounted inside the children block alongside the other parts. Visual verification: - Drawer renders as a dedicated right column with the editor content on the left, no layout overlap. - DOCUMENT header + OUTLINE shows "H1 ActiveUIX Words", STATS shows 73 words / 508 chars / 1 min reading, SUGGESTIONS shows "Nothing flagged" (sample doc has no warnings). - Collapse button toggles open/closed via animated inline-size transition. Next: - DRAWER-2: format/image/table/code/list contextual panels wired to runCommand. - DRAWER-3: polish (mobile drawer-as-bottom-sheet, transitions, A/B toggle in demo to compare classic toolbar vs companion drawer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
function countWordsInText(text: string): number {
const trimmed = text.trim();
return trimmed ? trimmed.split(/\s+/).length : 0;
}
refactor(words): drawer canon — morfo part + soma provider + eidos consumes via provider (DRAWER-CANON) Closes the architectural gap from the DRAWER spike: the companion drawer now follows the same morfo/soma/eidos pattern as every other Words part (Toolbar/BubbleMenu/SlashMenu/LinkEditor/Status/etc.). DRAWER-CANON-1 — morfo + soma: - wordsMorfo gains a 'Drawer' part (kebab 'drawer', archetype 'group', defaultElement 'aside', role 'complementary'). Declares data-words- drawer-mode (default/format/image/table/code/list), data-open, data-disabled, aria-label. New event 'commit-toggle-drawer' fires on collapse/expand for sema/telemetry hooks. - New WordsDrawerProvider in words-provider.svelte: owns `open` bindable state, derives `mode` from parent provider's currentBlock + selection (same logic as the spike, now in soma where it belongs), exposes snippetProps {mode, open, disabled, snippet, toggle, setOpen}. `toggle()` and `setOpen()` emit the event. - WordsDrawerProps + WordsDrawerSnippetProps + WordsDrawerMode types added to types.ts and re-exported via exports.ts. - soma/components/words/components/words-drawer.svelte (the soma component) mounts the provider and forwards merged props to either the `child` snippet or the default <aside> render. DRAWER-CANON-2 — eidos refactor: - eidos words-drawer.svelte rewritten as a consumer of <Words.Drawer>. The shell (aside + header + collapse button) and the per-mode body now live inside the children snippet that receives the soma's drawer snippet props. Mode derivation removed from eidos (it's now in soma). Local open $state removed (now bindable via the soma component). - Demo +page.svelte: `<Words.Drawer snippet={snippetProps} />` collapses to plain `<Words.Drawer />` — the soma provider hands the parent snippet down internally. DRAWER-CANON-3 — verify: - 214/214 tests pass in soma+morfo scope (no regressions). - npx tsc --noEmit clean for words area. - Visual verification: drawer still renders with DOCUMENT/OUTLINE/ STATS/SUGGESTIONS in default mode; context-switching to table / code / list / format still works; aside element now carries role='complementary', aria-label from morfo translationRef, data-words-drawer-mode reflecting current panel. The eidos drawer now passes the same canon checks as every other Words part: morfo declares its contract, soma owns the state, eidos only paints. The schema validator can verify the drawer's attrs, the eidos lint can check selector usage, sema can react to the toggle event. Demo header chips reflect the new contract: PARTS 19→20, EVENTS 11→12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
function imageBlockAtCaret(s: ProviderSnippetProps):
| {
readonly src: string;
readonly alt?: string;
readonly caption?: string;
readonly status?: 'pending' | 'error';
}
| undefined {
fix(words): image block selection — click figure activates image drawer mode (COLOR-1) Image blocks are contenteditable=false so the text caret can never live inside them. The drawer never entered image mode and the user had no visible "selected image" state. Fixed with a separate atomic- block selection layer: - New provider state `selectedBlockIndex` (number | undefined) for atomic block selection. Distinct from text selection. - `currentBlock` derived now short-circuits to the atomic block's type when `selectedBlockIndex` is set, so the drawer enters image mode whenever the user has selected an image. - New methods `selectAtomicBlock(blockIndex)` + `clearSelectedBlock()` on the provider. selectAtomicBlock also focuses the editor root so keyboard handlers (esc, delete) work on the atomic block. - `syncSelectionFromDom` clears the atomic-block highlight only when the text selection actually moved (was clearing on every selectionchange echo, which wiped the highlight a tick after the click landed). - Provider onclick handler detects clicks on figure[data-words-block= 'image'][data-words-path], decodes the path, calls selectAtomicBlock(blockIndex). - Render passes `selectedBlockIndex` through findOpts; image block render adds `data-words-block-selected` attr when the index matches. - Eidos CSS paints a 3px accent outline + glow on the selected figure. Image panel's imageBlockAtCaret() now falls back to selectedBlockIndex first, then text-selection anchor. Result: clicking an image in the editor now highlights it (accent outline + glow), the drawer enters IMAGE mode and shows its source/alt/caption/status panel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
// Atomic block selection wins (image is contenteditable=false so
// the text selection rarely points at it). Falls back to the
// caret's anchor block when the user hasn't atomic-clicked.
const blockIndex =
s.selectedBlockIndex !== undefined
? s.selectedBlockIndex
: s.selection?.anchor.path[0];
feat(words): drawer contextual panels — format/image/table/code/list (DRAWER-2) The companion drawer now switches its body content based on where the caret is. Five context modes wired to runCommand on the snippet provider, all reading current state to show active highlights: - format: B/I/U/S/code mark toggles (data-active reflects activeMarks), align L/C/R/J (data-active reflects currentTextAlign), Add link (URL prompt) + Remove (canUnlink-gated), Clear formatting. - image: read-only view of src, alt (italic + "(missing)" placeholder when empty), caption, status (pending → accent tint, error → threat tint). Edit-image flow deferred to a follow-up (needs a new updateImage operation; for the spike, view-only is enough to validate the panel works). - table: Rows +/- · Columns +/- · Row/Col header toggles · Striped / Compact toggles · Cell tone Plain/Muted/Accent. Every toggle reads current* state so the user sees which buttons are currently applied. - code: Plain + TS/JS/CSS/HTML/JSON/Bash/Py language chips, data-active reflects currentCodeLanguage. - list: Bullet/Numbered/Check kind chips · ⇤ Outdent / ⇥ Indent. All buttons use onmousedown preventDefault so the editor selection isn't dropped when clicking a drawer action (would otherwise lose the target paragraph / table cell / list item). CSS adds a generic .data-words-drawer-action chip (with hover, active, ghost, full, risk tones) + readonly-text style with pending/error tints. Reused across all panels, so adding new panels later means markup-only, no CSS extension. Verified in browser: - Click table cell → drawer shows TABLE panel with Striped active (matches sample's striped=true) and Plain tone active (matches default cell tone). - Click code block → drawer shows CODE panel with TS chip active (matches sample's language='ts'). - Click list item → drawer shows LIST panel with kind + indent actions. Concept A+D validated visually: ONE surface replaces what would otherwise be bubble menu + slash menu + heading picker + language picker + table popover + image controls + link editor. The contextual switch happens automatically; the user just moves the caret. DRAWER-3 (polish): mobile bottom-sheet layout, A/B toggle in demo controls, image-edit flow — deferred to next session if needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
if (blockIndex === undefined) return undefined;
const block = s.document.children[blockIndex];
if (!block || block.type !== 'image') return undefined;
return block;
}
const CODE_LANGUAGES = [
{ id: '', label: 'Plain' },
{ id: 'ts', label: 'TS' },
{ id: 'js', label: 'JS' },
{ id: 'css', label: 'CSS' },
{ id: 'html', label: 'HTML' },
{ id: 'json', label: 'JSON' },
{ id: 'bash', label: 'Bash' },
{ id: 'python', label: 'Py' }
] as const;
feat(words): parametric color marks — foreground + background pickers (COLOR-3) Marks now support parametric values via colon syntax: - color:#hex — foreground text color - bgcolor:#hex — highlight / background color Existing boolean marks (bold/italic/underline/strike/code) are unchanged. Empty value (color:, bgcolor:) clears that slot. Engine - WordsMark union extended with `color:\${string}` and `bgcolor:\${string}` template literal variants. - isWordsMark accepts the parametric form (validates against #hex 3/4/6/8 digits). - normalizeMarks de-duplicates by prefix for parametric marks (only one color, one bgcolor per inline survives). - toggleMarkSet REPLACES same-prefix entries instead of toggling them; empty value strips the slot. - New helpers markValue() and markPrefix() for reading parametric data. Serializers - HTML out: wrapMarks collects color/bgcolor into a single `<span style="color:..;background-color:..">` instead of nested spans, then wraps with the boolean-mark tags. - HTML in: not extended this pass — round-trip via style attr is a follow-up. The marks survive in the document model + render OK. - Markdown out: silently drops parametric marks (no MD syntax for inline color; HTML round-trip is the supported path). DOM render - createWordsTextAttrs splits marks into booleans (→ data-words-marks) and parametrics (→ inline `style` attr) so the contenteditable paints the colors directly. Drawer Format panel - New "Foreground" + "Background" sections with: - 10-swatch palette (red/orange/amber/green/cyan/blue/violet/pink/ white/grey) — hand-picked editorial set - Native <input type=color> for free hex pick (no preset constraint) - "×" clear button (only when a value is set for that slot) - Active swatch outline when the current value matches the preset - runCommand passes the `color:#hex` / `bgcolor:#hex` string straight through commandFromName → toggleMark. WordsCommandName extended with `color:\${string}` and `bgcolor:\${string}` for type-safe consumer calls. 152/152 tests pass. Verified visually: clicking the red swatch on selected "ActiveUI" text in the demo produces `<span data-words-marks="bold" style="color:#ef4444">ActiveUI</span>` in the rendered HTML, text paints red in the editor, the swatch shows active outline + clear (×) button appears. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
/** Hand-picked palette — keeps the picker compact while covering
* most editorial intents. Free hex still available via the input
* next to the chips. */
const COLOR_PRESETS = [
'#ef4444', // red
'#f97316', // orange
'#eab308', // amber
'#22c55e', // green
'#06b6d4', // cyan
'#3b82f6', // blue
'#8b5cf6', // violet
'#ec4899', // pink
'#ffffff', // white
'#a1a1aa' // grey
] as const;
/** Read the parametric mark value for a given prefix from the
* active marks. Returns '' when not set. */
function currentParametricMark(
marks: readonly string[],
prefix: 'color' | 'bgcolor'
): string {
const found = marks.find((m) => m.startsWith(`${prefix}:`));
return found ? found.slice(prefix.length + 1) : '';
}
feat(words): companion drawer right-rail (concept A+D) — shell + default panel (DRAWER-1) A radical disposition shift: instead of N fragmented popovers (bubble menu + slash menu + link editor + heading picker + language picker + table popover + image controls) we mount ONE contextual right-rail that changes its panel based on what the user is editing. Spike-1 surface (this commit) ships: - New eidos component <Words.Drawer> (words-drawer.svelte) — a collapsible right sidebar with mode detection derived from snippet props (currentBlock + selection). 6 modes mapped: default | format | image | table | code | list. - Default panel (visible when no contextual mode active): - OUTLINE — flat list of h1/h2/h3 headings, indented by level - STATS — word count + char count + reading time (200wpm) - SUGGESTIONS — writer-coaching warnings derived from the document model: missing alt text, long paragraphs (>600 chars), heading skip patterns (h3 without h2 above). Empty state shows "Nothing flagged" in affirm color. - Contextual panels (format/image/table/code/list) render a placeholder "coming soon" message — implemented in DRAWER-2. - Eidos CSS: - Provider host gains a grid-template-areas layout when a drawer is present: 'toolbar drawer / content drawer / status drawer'. Existing children (toolbar/content/placeholder/status) get explicit grid-area assignments. - Drawer collapses to 2.25rem (just the chevron) when closed. 16rem default open. - Outline items, stats grid, warning list each have their own discrete styling. Demo (+page.svelte): - New `showDrawer` state (default true). - Children snippet refactored to capture full snippetProps via @const destructure (kept selectedLink working). - <Words.Drawer snippet={snippetProps} /> mounted inside the children block alongside the other parts. Visual verification: - Drawer renders as a dedicated right column with the editor content on the left, no layout overlap. - DOCUMENT header + OUTLINE shows "H1 ActiveUIX Words", STATS shows 73 words / 508 chars / 1 min reading, SUGGESTIONS shows "Nothing flagged" (sample doc has no warnings). - Collapse button toggles open/closed via animated inline-size transition. Next: - DRAWER-2: format/image/table/code/list contextual panels wired to runCommand. - DRAWER-3: polish (mobile drawer-as-bottom-sheet, transitions, A/B toggle in demo to compare classic toolbar vs companion drawer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
</script>
refactor(words): drawer canon — morfo part + soma provider + eidos consumes via provider (DRAWER-CANON) Closes the architectural gap from the DRAWER spike: the companion drawer now follows the same morfo/soma/eidos pattern as every other Words part (Toolbar/BubbleMenu/SlashMenu/LinkEditor/Status/etc.). DRAWER-CANON-1 — morfo + soma: - wordsMorfo gains a 'Drawer' part (kebab 'drawer', archetype 'group', defaultElement 'aside', role 'complementary'). Declares data-words- drawer-mode (default/format/image/table/code/list), data-open, data-disabled, aria-label. New event 'commit-toggle-drawer' fires on collapse/expand for sema/telemetry hooks. - New WordsDrawerProvider in words-provider.svelte: owns `open` bindable state, derives `mode` from parent provider's currentBlock + selection (same logic as the spike, now in soma where it belongs), exposes snippetProps {mode, open, disabled, snippet, toggle, setOpen}. `toggle()` and `setOpen()` emit the event. - WordsDrawerProps + WordsDrawerSnippetProps + WordsDrawerMode types added to types.ts and re-exported via exports.ts. - soma/components/words/components/words-drawer.svelte (the soma component) mounts the provider and forwards merged props to either the `child` snippet or the default <aside> render. DRAWER-CANON-2 — eidos refactor: - eidos words-drawer.svelte rewritten as a consumer of <Words.Drawer>. The shell (aside + header + collapse button) and the per-mode body now live inside the children snippet that receives the soma's drawer snippet props. Mode derivation removed from eidos (it's now in soma). Local open $state removed (now bindable via the soma component). - Demo +page.svelte: `<Words.Drawer snippet={snippetProps} />` collapses to plain `<Words.Drawer />` — the soma provider hands the parent snippet down internally. DRAWER-CANON-3 — verify: - 214/214 tests pass in soma+morfo scope (no regressions). - npx tsc --noEmit clean for words area. - Visual verification: drawer still renders with DOCUMENT/OUTLINE/ STATS/SUGGESTIONS in default mode; context-switching to table / code / list / format still works; aside element now carries role='complementary', aria-label from morfo translationRef, data-words-drawer-mode reflecting current panel. The eidos drawer now passes the same canon checks as every other Words part: morfo declares its contract, soma owns the state, eidos only paints. The schema validator can verify the drawer's attrs, the eidos lint can check selector usage, sema can react to the toggle event. Demo header chips reflect the new contract: PARTS 19→20, EVENTS 11→12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
<Words.Drawer bind:open {defaultOpen} {disabled} {...rest}>
{#snippet children(d: DrawerSnippetProps)}
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
{@const { modes, snippet, toggle: toggleDrawer } = d}
refactor(words): drawer canon — morfo part + soma provider + eidos consumes via provider (DRAWER-CANON) Closes the architectural gap from the DRAWER spike: the companion drawer now follows the same morfo/soma/eidos pattern as every other Words part (Toolbar/BubbleMenu/SlashMenu/LinkEditor/Status/etc.). DRAWER-CANON-1 — morfo + soma: - wordsMorfo gains a 'Drawer' part (kebab 'drawer', archetype 'group', defaultElement 'aside', role 'complementary'). Declares data-words- drawer-mode (default/format/image/table/code/list), data-open, data-disabled, aria-label. New event 'commit-toggle-drawer' fires on collapse/expand for sema/telemetry hooks. - New WordsDrawerProvider in words-provider.svelte: owns `open` bindable state, derives `mode` from parent provider's currentBlock + selection (same logic as the spike, now in soma where it belongs), exposes snippetProps {mode, open, disabled, snippet, toggle, setOpen}. `toggle()` and `setOpen()` emit the event. - WordsDrawerProps + WordsDrawerSnippetProps + WordsDrawerMode types added to types.ts and re-exported via exports.ts. - soma/components/words/components/words-drawer.svelte (the soma component) mounts the provider and forwards merged props to either the `child` snippet or the default <aside> render. DRAWER-CANON-2 — eidos refactor: - eidos words-drawer.svelte rewritten as a consumer of <Words.Drawer>. The shell (aside + header + collapse button) and the per-mode body now live inside the children snippet that receives the soma's drawer snippet props. Mode derivation removed from eidos (it's now in soma). Local open $state removed (now bindable via the soma component). - Demo +page.svelte: `<Words.Drawer snippet={snippetProps} />` collapses to plain `<Words.Drawer />` — the soma provider hands the parent snippet down internally. DRAWER-CANON-3 — verify: - 214/214 tests pass in soma+morfo scope (no regressions). - npx tsc --noEmit clean for words area. - Visual verification: drawer still renders with DOCUMENT/OUTLINE/ STATS/SUGGESTIONS in default mode; context-switching to table / code / list / format still works; aside element now carries role='complementary', aria-label from morfo translationRef, data-words-drawer-mode reflecting current panel. The eidos drawer now passes the same canon checks as every other Words part: morfo declares its contract, soma owns the state, eidos only paints. The schema validator can verify the drawer's attrs, the eidos lint can check selector usage, sema can react to the toggle event. Demo header chips reflect the new contract: PARTS 19→20, EVENTS 11→12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
<header data-words-drawer-header>
<button
type="button"
data-words-drawer-collapse
aria-label={d.open ? 'Collapse drawer' : 'Expand drawer'}
aria-expanded={d.open}
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
onclick={(e) => toggleDrawer(e.currentTarget as HTMLElement)}
refactor(words): drawer canon — morfo part + soma provider + eidos consumes via provider (DRAWER-CANON) Closes the architectural gap from the DRAWER spike: the companion drawer now follows the same morfo/soma/eidos pattern as every other Words part (Toolbar/BubbleMenu/SlashMenu/LinkEditor/Status/etc.). DRAWER-CANON-1 — morfo + soma: - wordsMorfo gains a 'Drawer' part (kebab 'drawer', archetype 'group', defaultElement 'aside', role 'complementary'). Declares data-words- drawer-mode (default/format/image/table/code/list), data-open, data-disabled, aria-label. New event 'commit-toggle-drawer' fires on collapse/expand for sema/telemetry hooks. - New WordsDrawerProvider in words-provider.svelte: owns `open` bindable state, derives `mode` from parent provider's currentBlock + selection (same logic as the spike, now in soma where it belongs), exposes snippetProps {mode, open, disabled, snippet, toggle, setOpen}. `toggle()` and `setOpen()` emit the event. - WordsDrawerProps + WordsDrawerSnippetProps + WordsDrawerMode types added to types.ts and re-exported via exports.ts. - soma/components/words/components/words-drawer.svelte (the soma component) mounts the provider and forwards merged props to either the `child` snippet or the default <aside> render. DRAWER-CANON-2 — eidos refactor: - eidos words-drawer.svelte rewritten as a consumer of <Words.Drawer>. The shell (aside + header + collapse button) and the per-mode body now live inside the children snippet that receives the soma's drawer snippet props. Mode derivation removed from eidos (it's now in soma). Local open $state removed (now bindable via the soma component). - Demo +page.svelte: `<Words.Drawer snippet={snippetProps} />` collapses to plain `<Words.Drawer />` — the soma provider hands the parent snippet down internally. DRAWER-CANON-3 — verify: - 214/214 tests pass in soma+morfo scope (no regressions). - npx tsc --noEmit clean for words area. - Visual verification: drawer still renders with DOCUMENT/OUTLINE/ STATS/SUGGESTIONS in default mode; context-switching to table / code / list / format still works; aside element now carries role='complementary', aria-label from morfo translationRef, data-words-drawer-mode reflecting current panel. The eidos drawer now passes the same canon checks as every other Words part: morfo declares its contract, soma owns the state, eidos only paints. The schema validator can verify the drawer's attrs, the eidos lint can check selector usage, sema can react to the toggle event. Demo header chips reflect the new contract: PARTS 19→20, EVENTS 11→12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
>
{#if d.open}
<ChevronRight size="sm" decorative />
{:else}
<ChevronLeft size="sm" decorative />
{/if}
</button>
{#if d.open}
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
<span data-words-drawer-title>{MODE_LABELS[modes[0]]}</span>
{#if modes.length > 1}
<span data-words-drawer-trail aria-hidden="true">
{#each modes.slice(1) as m, i (m)}
<span data-words-drawer-trail-sep>·</span>
<span data-words-drawer-trail-item>{MODE_LABELS[m]}</span>
{/each}
</span>
{/if}
feat(words): companion drawer right-rail (concept A+D) — shell + default panel (DRAWER-1) A radical disposition shift: instead of N fragmented popovers (bubble menu + slash menu + link editor + heading picker + language picker + table popover + image controls) we mount ONE contextual right-rail that changes its panel based on what the user is editing. Spike-1 surface (this commit) ships: - New eidos component <Words.Drawer> (words-drawer.svelte) — a collapsible right sidebar with mode detection derived from snippet props (currentBlock + selection). 6 modes mapped: default | format | image | table | code | list. - Default panel (visible when no contextual mode active): - OUTLINE — flat list of h1/h2/h3 headings, indented by level - STATS — word count + char count + reading time (200wpm) - SUGGESTIONS — writer-coaching warnings derived from the document model: missing alt text, long paragraphs (>600 chars), heading skip patterns (h3 without h2 above). Empty state shows "Nothing flagged" in affirm color. - Contextual panels (format/image/table/code/list) render a placeholder "coming soon" message — implemented in DRAWER-2. - Eidos CSS: - Provider host gains a grid-template-areas layout when a drawer is present: 'toolbar drawer / content drawer / status drawer'. Existing children (toolbar/content/placeholder/status) get explicit grid-area assignments. - Drawer collapses to 2.25rem (just the chevron) when closed. 16rem default open. - Outline items, stats grid, warning list each have their own discrete styling. Demo (+page.svelte): - New `showDrawer` state (default true). - Children snippet refactored to capture full snippetProps via @const destructure (kept selectedLink working). - <Words.Drawer snippet={snippetProps} /> mounted inside the children block alongside the other parts. Visual verification: - Drawer renders as a dedicated right column with the editor content on the left, no layout overlap. - DOCUMENT header + OUTLINE shows "H1 ActiveUIX Words", STATS shows 73 words / 508 chars / 1 min reading, SUGGESTIONS shows "Nothing flagged" (sample doc has no warnings). - Collapse button toggles open/closed via animated inline-size transition. Next: - DRAWER-2: format/image/table/code/list contextual panels wired to runCommand. - DRAWER-3: polish (mobile drawer-as-bottom-sheet, transitions, A/B toggle in demo to compare classic toolbar vs companion drawer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
{/if}
refactor(words): drawer canon — morfo part + soma provider + eidos consumes via provider (DRAWER-CANON) Closes the architectural gap from the DRAWER spike: the companion drawer now follows the same morfo/soma/eidos pattern as every other Words part (Toolbar/BubbleMenu/SlashMenu/LinkEditor/Status/etc.). DRAWER-CANON-1 — morfo + soma: - wordsMorfo gains a 'Drawer' part (kebab 'drawer', archetype 'group', defaultElement 'aside', role 'complementary'). Declares data-words- drawer-mode (default/format/image/table/code/list), data-open, data-disabled, aria-label. New event 'commit-toggle-drawer' fires on collapse/expand for sema/telemetry hooks. - New WordsDrawerProvider in words-provider.svelte: owns `open` bindable state, derives `mode` from parent provider's currentBlock + selection (same logic as the spike, now in soma where it belongs), exposes snippetProps {mode, open, disabled, snippet, toggle, setOpen}. `toggle()` and `setOpen()` emit the event. - WordsDrawerProps + WordsDrawerSnippetProps + WordsDrawerMode types added to types.ts and re-exported via exports.ts. - soma/components/words/components/words-drawer.svelte (the soma component) mounts the provider and forwards merged props to either the `child` snippet or the default <aside> render. DRAWER-CANON-2 — eidos refactor: - eidos words-drawer.svelte rewritten as a consumer of <Words.Drawer>. The shell (aside + header + collapse button) and the per-mode body now live inside the children snippet that receives the soma's drawer snippet props. Mode derivation removed from eidos (it's now in soma). Local open $state removed (now bindable via the soma component). - Demo +page.svelte: `<Words.Drawer snippet={snippetProps} />` collapses to plain `<Words.Drawer />` — the soma provider hands the parent snippet down internally. DRAWER-CANON-3 — verify: - 214/214 tests pass in soma+morfo scope (no regressions). - npx tsc --noEmit clean for words area. - Visual verification: drawer still renders with DOCUMENT/OUTLINE/ STATS/SUGGESTIONS in default mode; context-switching to table / code / list / format still works; aside element now carries role='complementary', aria-label from morfo translationRef, data-words-drawer-mode reflecting current panel. The eidos drawer now passes the same canon checks as every other Words part: morfo declares its contract, soma owns the state, eidos only paints. The schema validator can verify the drawer's attrs, the eidos lint can check selector usage, sema can react to the toggle event. Demo header chips reflect the new contract: PARTS 19→20, EVENTS 11→12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
</header>
feat(words): companion drawer right-rail (concept A+D) — shell + default panel (DRAWER-1) A radical disposition shift: instead of N fragmented popovers (bubble menu + slash menu + link editor + heading picker + language picker + table popover + image controls) we mount ONE contextual right-rail that changes its panel based on what the user is editing. Spike-1 surface (this commit) ships: - New eidos component <Words.Drawer> (words-drawer.svelte) — a collapsible right sidebar with mode detection derived from snippet props (currentBlock + selection). 6 modes mapped: default | format | image | table | code | list. - Default panel (visible when no contextual mode active): - OUTLINE — flat list of h1/h2/h3 headings, indented by level - STATS — word count + char count + reading time (200wpm) - SUGGESTIONS — writer-coaching warnings derived from the document model: missing alt text, long paragraphs (>600 chars), heading skip patterns (h3 without h2 above). Empty state shows "Nothing flagged" in affirm color. - Contextual panels (format/image/table/code/list) render a placeholder "coming soon" message — implemented in DRAWER-2. - Eidos CSS: - Provider host gains a grid-template-areas layout when a drawer is present: 'toolbar drawer / content drawer / status drawer'. Existing children (toolbar/content/placeholder/status) get explicit grid-area assignments. - Drawer collapses to 2.25rem (just the chevron) when closed. 16rem default open. - Outline items, stats grid, warning list each have their own discrete styling. Demo (+page.svelte): - New `showDrawer` state (default true). - Children snippet refactored to capture full snippetProps via @const destructure (kept selectedLink working). - <Words.Drawer snippet={snippetProps} /> mounted inside the children block alongside the other parts. Visual verification: - Drawer renders as a dedicated right column with the editor content on the left, no layout overlap. - DOCUMENT header + OUTLINE shows "H1 ActiveUIX Words", STATS shows 73 words / 508 chars / 1 min reading, SUGGESTIONS shows "Nothing flagged" (sample doc has no warnings). - Collapse button toggles open/closed via animated inline-size transition. Next: - DRAWER-2: format/image/table/code/list contextual panels wired to runCommand. - DRAWER-3: polish (mobile drawer-as-bottom-sheet, transitions, A/B toggle in demo to compare classic toolbar vs companion drawer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
refactor(words): drawer canon — morfo part + soma provider + eidos consumes via provider (DRAWER-CANON) Closes the architectural gap from the DRAWER spike: the companion drawer now follows the same morfo/soma/eidos pattern as every other Words part (Toolbar/BubbleMenu/SlashMenu/LinkEditor/Status/etc.). DRAWER-CANON-1 — morfo + soma: - wordsMorfo gains a 'Drawer' part (kebab 'drawer', archetype 'group', defaultElement 'aside', role 'complementary'). Declares data-words- drawer-mode (default/format/image/table/code/list), data-open, data-disabled, aria-label. New event 'commit-toggle-drawer' fires on collapse/expand for sema/telemetry hooks. - New WordsDrawerProvider in words-provider.svelte: owns `open` bindable state, derives `mode` from parent provider's currentBlock + selection (same logic as the spike, now in soma where it belongs), exposes snippetProps {mode, open, disabled, snippet, toggle, setOpen}. `toggle()` and `setOpen()` emit the event. - WordsDrawerProps + WordsDrawerSnippetProps + WordsDrawerMode types added to types.ts and re-exported via exports.ts. - soma/components/words/components/words-drawer.svelte (the soma component) mounts the provider and forwards merged props to either the `child` snippet or the default <aside> render. DRAWER-CANON-2 — eidos refactor: - eidos words-drawer.svelte rewritten as a consumer of <Words.Drawer>. The shell (aside + header + collapse button) and the per-mode body now live inside the children snippet that receives the soma's drawer snippet props. Mode derivation removed from eidos (it's now in soma). Local open $state removed (now bindable via the soma component). - Demo +page.svelte: `<Words.Drawer snippet={snippetProps} />` collapses to plain `<Words.Drawer />` — the soma provider hands the parent snippet down internally. DRAWER-CANON-3 — verify: - 214/214 tests pass in soma+morfo scope (no regressions). - npx tsc --noEmit clean for words area. - Visual verification: drawer still renders with DOCUMENT/OUTLINE/ STATS/SUGGESTIONS in default mode; context-switching to table / code / list / format still works; aside element now carries role='complementary', aria-label from morfo translationRef, data-words-drawer-mode reflecting current panel. The eidos drawer now passes the same canon checks as every other Words part: morfo declares its contract, soma owns the state, eidos only paints. The schema validator can verify the drawer's attrs, the eidos lint can check selector usage, sema can react to the toggle event. Demo header chips reflect the new contract: PARTS 19→20, EVENTS 11→12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
{#if d.open}
<div data-words-drawer-body>
{#if customChildren}
{@render customChildren(d)}
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
{:else}
{#each modes as mode, i (mode)}
{@const panelOpen = isPanelOpen(mode, modes)}
<section
data-words-drawer-panel
data-mode={mode}
data-open={panelOpen ? '' : undefined}
data-depth={i}
>
feat(words): drawer contextual panels — format/image/table/code/list (DRAWER-2) The companion drawer now switches its body content based on where the caret is. Five context modes wired to runCommand on the snippet provider, all reading current state to show active highlights: - format: B/I/U/S/code mark toggles (data-active reflects activeMarks), align L/C/R/J (data-active reflects currentTextAlign), Add link (URL prompt) + Remove (canUnlink-gated), Clear formatting. - image: read-only view of src, alt (italic + "(missing)" placeholder when empty), caption, status (pending → accent tint, error → threat tint). Edit-image flow deferred to a follow-up (needs a new updateImage operation; for the spike, view-only is enough to validate the panel works). - table: Rows +/- · Columns +/- · Row/Col header toggles · Striped / Compact toggles · Cell tone Plain/Muted/Accent. Every toggle reads current* state so the user sees which buttons are currently applied. - code: Plain + TS/JS/CSS/HTML/JSON/Bash/Py language chips, data-active reflects currentCodeLanguage. - list: Bullet/Numbered/Check kind chips · ⇤ Outdent / ⇥ Indent. All buttons use onmousedown preventDefault so the editor selection isn't dropped when clicking a drawer action (would otherwise lose the target paragraph / table cell / list item). CSS adds a generic .data-words-drawer-action chip (with hover, active, ghost, full, risk tones) + readonly-text style with pending/error tints. Reused across all panels, so adding new panels later means markup-only, no CSS extension. Verified in browser: - Click table cell → drawer shows TABLE panel with Striped active (matches sample's striped=true) and Plain tone active (matches default cell tone). - Click code block → drawer shows CODE panel with TS chip active (matches sample's language='ts'). - Click list item → drawer shows LIST panel with kind + indent actions. Concept A+D validated visually: ONE surface replaces what would otherwise be bubble menu + slash menu + heading picker + language picker + table popover + image controls + link editor. The contextual switch happens automatically; the user just moves the caret. DRAWER-3 (polish): mobile bottom-sheet layout, A/B toggle in demo controls, image-edit flow — deferred to next session if needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
<button
type="button"
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
data-words-drawer-panel-header
aria-expanded={panelOpen}
onclick={() => togglePanel(mode, modes)}
feat(words): drawer contextual panels — format/image/table/code/list (DRAWER-2) The companion drawer now switches its body content based on where the caret is. Five context modes wired to runCommand on the snippet provider, all reading current state to show active highlights: - format: B/I/U/S/code mark toggles (data-active reflects activeMarks), align L/C/R/J (data-active reflects currentTextAlign), Add link (URL prompt) + Remove (canUnlink-gated), Clear formatting. - image: read-only view of src, alt (italic + "(missing)" placeholder when empty), caption, status (pending → accent tint, error → threat tint). Edit-image flow deferred to a follow-up (needs a new updateImage operation; for the spike, view-only is enough to validate the panel works). - table: Rows +/- · Columns +/- · Row/Col header toggles · Striped / Compact toggles · Cell tone Plain/Muted/Accent. Every toggle reads current* state so the user sees which buttons are currently applied. - code: Plain + TS/JS/CSS/HTML/JSON/Bash/Py language chips, data-active reflects currentCodeLanguage. - list: Bullet/Numbered/Check kind chips · ⇤ Outdent / ⇥ Indent. All buttons use onmousedown preventDefault so the editor selection isn't dropped when clicking a drawer action (would otherwise lose the target paragraph / table cell / list item). CSS adds a generic .data-words-drawer-action chip (with hover, active, ghost, full, risk tones) + readonly-text style with pending/error tints. Reused across all panels, so adding new panels later means markup-only, no CSS extension. Verified in browser: - Click table cell → drawer shows TABLE panel with Striped active (matches sample's striped=true) and Plain tone active (matches default cell tone). - Click code block → drawer shows CODE panel with TS chip active (matches sample's language='ts'). - Click list item → drawer shows LIST panel with kind + indent actions. Concept A+D validated visually: ONE surface replaces what would otherwise be bubble menu + slash menu + heading picker + language picker + table popover + image controls + link editor. The contextual switch happens automatically; the user just moves the caret. DRAWER-3 (polish): mobile bottom-sheet layout, A/B toggle in demo controls, image-edit flow — deferred to next session if needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
>
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
<span data-words-drawer-panel-title>{MODE_LABELS[mode]}</span>
{#if panelOpen}
<ChevronUp size="xs" decorative />
{:else}
<ChevronDown size="xs" decorative />
{/if}
feat(words): drawer contextual panels — format/image/table/code/list (DRAWER-2) The companion drawer now switches its body content based on where the caret is. Five context modes wired to runCommand on the snippet provider, all reading current state to show active highlights: - format: B/I/U/S/code mark toggles (data-active reflects activeMarks), align L/C/R/J (data-active reflects currentTextAlign), Add link (URL prompt) + Remove (canUnlink-gated), Clear formatting. - image: read-only view of src, alt (italic + "(missing)" placeholder when empty), caption, status (pending → accent tint, error → threat tint). Edit-image flow deferred to a follow-up (needs a new updateImage operation; for the spike, view-only is enough to validate the panel works). - table: Rows +/- · Columns +/- · Row/Col header toggles · Striped / Compact toggles · Cell tone Plain/Muted/Accent. Every toggle reads current* state so the user sees which buttons are currently applied. - code: Plain + TS/JS/CSS/HTML/JSON/Bash/Py language chips, data-active reflects currentCodeLanguage. - list: Bullet/Numbered/Check kind chips · ⇤ Outdent / ⇥ Indent. All buttons use onmousedown preventDefault so the editor selection isn't dropped when clicking a drawer action (would otherwise lose the target paragraph / table cell / list item). CSS adds a generic .data-words-drawer-action chip (with hover, active, ghost, full, risk tones) + readonly-text style with pending/error tints. Reused across all panels, so adding new panels later means markup-only, no CSS extension. Verified in browser: - Click table cell → drawer shows TABLE panel with Striped active (matches sample's striped=true) and Plain tone active (matches default cell tone). - Click code block → drawer shows CODE panel with TS chip active (matches sample's language='ts'). - Click list item → drawer shows LIST panel with kind + indent actions. Concept A+D validated visually: ONE surface replaces what would otherwise be bubble menu + slash menu + heading picker + language picker + table popover + image controls + link editor. The contextual switch happens automatically; the user just moves the caret. DRAWER-3 (polish): mobile bottom-sheet layout, A/B toggle in demo controls, image-edit flow — deferred to next session if needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
</button>
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
{#if panelOpen}
<div data-words-drawer-panel-body>
{#if mode === 'default'}
{@render defaultPanel(snippet)}
{:else if mode === 'format'}
{@render formatPanel(snippet)}
{:else if mode === 'block'}
{@render blockPanel(snippet)}
{:else if mode === 'cell'}
{@render cellPanel(snippet)}
{:else if mode === 'row'}
{@render rowPanel(snippet)}
{:else if mode === 'table'}
{@render tablePanel(snippet)}
{:else if mode === 'list-item'}
{@render listItemPanel(snippet)}
{:else if mode === 'list'}
{@render listPanel(snippet)}
{:else if mode === 'code'}
{@render codePanel(snippet)}
{:else if mode === 'image'}
{@render imagePanel(snippet)}
{/if}
</div>
refactor(words): drawer canon — morfo part + soma provider + eidos consumes via provider (DRAWER-CANON) Closes the architectural gap from the DRAWER spike: the companion drawer now follows the same morfo/soma/eidos pattern as every other Words part (Toolbar/BubbleMenu/SlashMenu/LinkEditor/Status/etc.). DRAWER-CANON-1 — morfo + soma: - wordsMorfo gains a 'Drawer' part (kebab 'drawer', archetype 'group', defaultElement 'aside', role 'complementary'). Declares data-words- drawer-mode (default/format/image/table/code/list), data-open, data-disabled, aria-label. New event 'commit-toggle-drawer' fires on collapse/expand for sema/telemetry hooks. - New WordsDrawerProvider in words-provider.svelte: owns `open` bindable state, derives `mode` from parent provider's currentBlock + selection (same logic as the spike, now in soma where it belongs), exposes snippetProps {mode, open, disabled, snippet, toggle, setOpen}. `toggle()` and `setOpen()` emit the event. - WordsDrawerProps + WordsDrawerSnippetProps + WordsDrawerMode types added to types.ts and re-exported via exports.ts. - soma/components/words/components/words-drawer.svelte (the soma component) mounts the provider and forwards merged props to either the `child` snippet or the default <aside> render. DRAWER-CANON-2 — eidos refactor: - eidos words-drawer.svelte rewritten as a consumer of <Words.Drawer>. The shell (aside + header + collapse button) and the per-mode body now live inside the children snippet that receives the soma's drawer snippet props. Mode derivation removed from eidos (it's now in soma). Local open $state removed (now bindable via the soma component). - Demo +page.svelte: `<Words.Drawer snippet={snippetProps} />` collapses to plain `<Words.Drawer />` — the soma provider hands the parent snippet down internally. DRAWER-CANON-3 — verify: - 214/214 tests pass in soma+morfo scope (no regressions). - npx tsc --noEmit clean for words area. - Visual verification: drawer still renders with DOCUMENT/OUTLINE/ STATS/SUGGESTIONS in default mode; context-switching to table / code / list / format still works; aside element now carries role='complementary', aria-label from morfo translationRef, data-words-drawer-mode reflecting current panel. The eidos drawer now passes the same canon checks as every other Words part: morfo declares its contract, soma owns the state, eidos only paints. The schema validator can verify the drawer's attrs, the eidos lint can check selector usage, sema can react to the toggle event. Demo header chips reflect the new contract: PARTS 19→20, EVENTS 11→12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
{/if}
feat(words): drawer contextual panels — format/image/table/code/list (DRAWER-2) The companion drawer now switches its body content based on where the caret is. Five context modes wired to runCommand on the snippet provider, all reading current state to show active highlights: - format: B/I/U/S/code mark toggles (data-active reflects activeMarks), align L/C/R/J (data-active reflects currentTextAlign), Add link (URL prompt) + Remove (canUnlink-gated), Clear formatting. - image: read-only view of src, alt (italic + "(missing)" placeholder when empty), caption, status (pending → accent tint, error → threat tint). Edit-image flow deferred to a follow-up (needs a new updateImage operation; for the spike, view-only is enough to validate the panel works). - table: Rows +/- · Columns +/- · Row/Col header toggles · Striped / Compact toggles · Cell tone Plain/Muted/Accent. Every toggle reads current* state so the user sees which buttons are currently applied. - code: Plain + TS/JS/CSS/HTML/JSON/Bash/Py language chips, data-active reflects currentCodeLanguage. - list: Bullet/Numbered/Check kind chips · ⇤ Outdent / ⇥ Indent. All buttons use onmousedown preventDefault so the editor selection isn't dropped when clicking a drawer action (would otherwise lose the target paragraph / table cell / list item). CSS adds a generic .data-words-drawer-action chip (with hover, active, ghost, full, risk tones) + readonly-text style with pending/error tints. Reused across all panels, so adding new panels later means markup-only, no CSS extension. Verified in browser: - Click table cell → drawer shows TABLE panel with Striped active (matches sample's striped=true) and Plain tone active (matches default cell tone). - Click code block → drawer shows CODE panel with TS chip active (matches sample's language='ts'). - Click list item → drawer shows LIST panel with kind + indent actions. Concept A+D validated visually: ONE surface replaces what would otherwise be bubble menu + slash menu + heading picker + language picker + table popover + image controls + link editor. The contextual switch happens automatically; the user just moves the caret. DRAWER-3 (polish): mobile bottom-sheet layout, A/B toggle in demo controls, image-edit flow — deferred to next session if needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
</section>
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
{/each}
feat(words): drawer contextual panels — format/image/table/code/list (DRAWER-2) The companion drawer now switches its body content based on where the caret is. Five context modes wired to runCommand on the snippet provider, all reading current state to show active highlights: - format: B/I/U/S/code mark toggles (data-active reflects activeMarks), align L/C/R/J (data-active reflects currentTextAlign), Add link (URL prompt) + Remove (canUnlink-gated), Clear formatting. - image: read-only view of src, alt (italic + "(missing)" placeholder when empty), caption, status (pending → accent tint, error → threat tint). Edit-image flow deferred to a follow-up (needs a new updateImage operation; for the spike, view-only is enough to validate the panel works). - table: Rows +/- · Columns +/- · Row/Col header toggles · Striped / Compact toggles · Cell tone Plain/Muted/Accent. Every toggle reads current* state so the user sees which buttons are currently applied. - code: Plain + TS/JS/CSS/HTML/JSON/Bash/Py language chips, data-active reflects currentCodeLanguage. - list: Bullet/Numbered/Check kind chips · ⇤ Outdent / ⇥ Indent. All buttons use onmousedown preventDefault so the editor selection isn't dropped when clicking a drawer action (would otherwise lose the target paragraph / table cell / list item). CSS adds a generic .data-words-drawer-action chip (with hover, active, ghost, full, risk tones) + readonly-text style with pending/error tints. Reused across all panels, so adding new panels later means markup-only, no CSS extension. Verified in browser: - Click table cell → drawer shows TABLE panel with Striped active (matches sample's striped=true) and Plain tone active (matches default cell tone). - Click code block → drawer shows CODE panel with TS chip active (matches sample's language='ts'). - Click list item → drawer shows LIST panel with kind + indent actions. Concept A+D validated visually: ONE surface replaces what would otherwise be bubble menu + slash menu + heading picker + language picker + table popover + image controls + link editor. The contextual switch happens automatically; the user just moves the caret. DRAWER-3 (polish): mobile bottom-sheet layout, A/B toggle in demo controls, image-edit flow — deferred to next session if needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
{/if}
refactor(words): drawer canon — morfo part + soma provider + eidos consumes via provider (DRAWER-CANON) Closes the architectural gap from the DRAWER spike: the companion drawer now follows the same morfo/soma/eidos pattern as every other Words part (Toolbar/BubbleMenu/SlashMenu/LinkEditor/Status/etc.). DRAWER-CANON-1 — morfo + soma: - wordsMorfo gains a 'Drawer' part (kebab 'drawer', archetype 'group', defaultElement 'aside', role 'complementary'). Declares data-words- drawer-mode (default/format/image/table/code/list), data-open, data-disabled, aria-label. New event 'commit-toggle-drawer' fires on collapse/expand for sema/telemetry hooks. - New WordsDrawerProvider in words-provider.svelte: owns `open` bindable state, derives `mode` from parent provider's currentBlock + selection (same logic as the spike, now in soma where it belongs), exposes snippetProps {mode, open, disabled, snippet, toggle, setOpen}. `toggle()` and `setOpen()` emit the event. - WordsDrawerProps + WordsDrawerSnippetProps + WordsDrawerMode types added to types.ts and re-exported via exports.ts. - soma/components/words/components/words-drawer.svelte (the soma component) mounts the provider and forwards merged props to either the `child` snippet or the default <aside> render. DRAWER-CANON-2 — eidos refactor: - eidos words-drawer.svelte rewritten as a consumer of <Words.Drawer>. The shell (aside + header + collapse button) and the per-mode body now live inside the children snippet that receives the soma's drawer snippet props. Mode derivation removed from eidos (it's now in soma). Local open $state removed (now bindable via the soma component). - Demo +page.svelte: `<Words.Drawer snippet={snippetProps} />` collapses to plain `<Words.Drawer />` — the soma provider hands the parent snippet down internally. DRAWER-CANON-3 — verify: - 214/214 tests pass in soma+morfo scope (no regressions). - npx tsc --noEmit clean for words area. - Visual verification: drawer still renders with DOCUMENT/OUTLINE/ STATS/SUGGESTIONS in default mode; context-switching to table / code / list / format still works; aside element now carries role='complementary', aria-label from morfo translationRef, data-words-drawer-mode reflecting current panel. The eidos drawer now passes the same canon checks as every other Words part: morfo declares its contract, soma owns the state, eidos only paints. The schema validator can verify the drawer's attrs, the eidos lint can check selector usage, sema can react to the toggle event. Demo header chips reflect the new contract: PARTS 19→20, EVENTS 11→12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
</div>
{/if}
{/snippet}
</Words.Drawer>
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
{#snippet defaultPanel(s: ProviderSnippetProps)}
{@const wordCount = countWordsInText(s.plainText)}
{@const charCount = s.plainText.length}
{@const readingTimeMin = Math.max(1, Math.round(wordCount / 200))}
{@const outline = s.document.children
.map((block, blockIndex) => ({ block, blockIndex }))
.filter(({ block }) => block.type === 'heading')
.map(({ block, blockIndex }) => ({
blockIndex,
level: block.type === 'heading' ? block.level : 1,
text:
block.type === 'heading'
? block.children.map((i) => (i.type === 'text' ? i.text : '')).join('')
: ''
}))
.filter((e) => e.text.trim())}
{@const warnings = collectWarnings(s)}
<div data-words-drawer-section data-flush>
<h4 data-words-drawer-section-title>Outline</h4>
{#if outline.length === 0}
<p data-words-drawer-empty>No headings yet</p>
{:else}
<ol data-words-drawer-outline>
{#each outline as entry (entry.blockIndex)}
<li data-words-drawer-outline-item data-level={entry.level}>
<span data-words-drawer-outline-bullet>H{entry.level}</span>
<span data-words-drawer-outline-text>{entry.text}</span>
</li>
{/each}
</ol>
{/if}
</div>
<div data-words-drawer-section data-flush>
<h4 data-words-drawer-section-title>Stats</h4>
<dl data-words-drawer-stats>
<div><dt>Words</dt><dd>{wordCount}</dd></div>
<div><dt>Chars</dt><dd>{charCount}</dd></div>
<div><dt>Reading</dt><dd>{readingTimeMin} min</dd></div>
</dl>
</div>
<div data-words-drawer-section data-flush>
<h4 data-words-drawer-section-title>
Suggestions
{#if warnings.length > 0}
<span data-words-drawer-section-count>{warnings.length}</span>
{/if}
</h4>
{#if warnings.length === 0}
<p data-words-drawer-empty data-tone="affirm">Nothing flagged</p>
{:else}
<ul data-words-drawer-warnings>
{#each warnings as w (w.id)}
<li data-severity={w.severity}>{w.message}</li>
{/each}
</ul>
{/if}
</div>
{/snippet}
{#snippet formatPanel(s: ProviderSnippetProps)}
<div data-words-drawer-actions>
{#each [
{ id: 'bold', label: 'B', title: 'Bold' },
{ id: 'italic', label: 'I', title: 'Italic' },
{ id: 'underline', label: 'U', title: 'Underline' },
{ id: 'strike', label: 'S', title: 'Strikethrough' },
{ id: 'code', label: '<>', title: 'Inline code' }
] as mark (mark.id)}
<button
type="button"
data-words-drawer-action
data-active={s.activeMarks.includes(mark.id) ? '' : undefined}
title={mark.title}
aria-pressed={s.activeMarks.includes(mark.id)}
onmousedown={(e) => e.preventDefault()}
onclick={() => s.runCommand(mark.id)}
>
{mark.label}
</button>
{/each}
</div>
feat(words): parametric color marks — foreground + background pickers (COLOR-3) Marks now support parametric values via colon syntax: - color:#hex — foreground text color - bgcolor:#hex — highlight / background color Existing boolean marks (bold/italic/underline/strike/code) are unchanged. Empty value (color:, bgcolor:) clears that slot. Engine - WordsMark union extended with `color:\${string}` and `bgcolor:\${string}` template literal variants. - isWordsMark accepts the parametric form (validates against #hex 3/4/6/8 digits). - normalizeMarks de-duplicates by prefix for parametric marks (only one color, one bgcolor per inline survives). - toggleMarkSet REPLACES same-prefix entries instead of toggling them; empty value strips the slot. - New helpers markValue() and markPrefix() for reading parametric data. Serializers - HTML out: wrapMarks collects color/bgcolor into a single `<span style="color:..;background-color:..">` instead of nested spans, then wraps with the boolean-mark tags. - HTML in: not extended this pass — round-trip via style attr is a follow-up. The marks survive in the document model + render OK. - Markdown out: silently drops parametric marks (no MD syntax for inline color; HTML round-trip is the supported path). DOM render - createWordsTextAttrs splits marks into booleans (→ data-words-marks) and parametrics (→ inline `style` attr) so the contenteditable paints the colors directly. Drawer Format panel - New "Foreground" + "Background" sections with: - 10-swatch palette (red/orange/amber/green/cyan/blue/violet/pink/ white/grey) — hand-picked editorial set - Native <input type=color> for free hex pick (no preset constraint) - "×" clear button (only when a value is set for that slot) - Active swatch outline when the current value matches the preset - runCommand passes the `color:#hex` / `bgcolor:#hex` string straight through commandFromName → toggleMark. WordsCommandName extended with `color:\${string}` and `bgcolor:\${string}` for type-safe consumer calls. 152/152 tests pass. Verified visually: clicking the red swatch on selected "ActiveUI" text in the demo produces `<span data-words-marks="bold" style="color:#ef4444">ActiveUI</span>` in the rendered HTML, text paints red in the editor, the swatch shows active outline + clear (×) button appears. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
<div data-words-drawer-section-subtitle>Foreground</div>
<div data-words-drawer-color-picker>
{#each COLOR_PRESETS as preset (preset)}
<button
type="button"
data-words-drawer-color-swatch
data-active={currentParametricMark(s.activeMarks, 'color') === preset ? '' : undefined}
style="background-color:{preset}"
title={preset}
aria-label={`Set foreground to ${preset}`}
onmousedown={(e) => e.preventDefault()}
fix(words): color picker uses applyCommand (no sema) + onchange instead of oninput Two related fixes to the color picker UX: 1. Swatches now call applyCommand({type:'toggleMark', mark:'color:#x'}) directly instead of runCommand('color:#x'). runCommand emits the 'commit-set-format' sema event (which fires sound + visual feedback on the editor surface). For discrete color picks the sema event is noise — applyCommand bypasses the trigger and just mutates the doc. 2. Native <input type=color> uses `onchange` instead of `oninput`. `oninput` fires continuously while the user drags the OS color picker — each fire was queueing a separate command + sema event, producing a horror cascade of sound / visual flashes on every pixel of the slider drag. `onchange` only fires when the user releases / commits the picker (closes the OS popup), so we get exactly one command per intent. Same pattern applied to both Foreground and Background pickers + the clear (×) buttons. Sema events are still emitted by the rest of the drawer's action chips (run via runCommand) because those are user-facing intent actions (Bold, Italic, etc.) where the perceptual feedback aligns with the user's notion of "I just did a thing". Color slider drags are NOT in that category — they're continuous parameter tuning. Verified: clicking the red swatch on selected "ActiveUI" produces the expected red <span> in the document AND the trace shows no new commit-set-format event (was previously firing on every swatch click, drowning audio + animation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
onclick={() => s.applyCommand({ type: 'toggleMark', mark: `color:${preset}` as never })}
feat(words): parametric color marks — foreground + background pickers (COLOR-3) Marks now support parametric values via colon syntax: - color:#hex — foreground text color - bgcolor:#hex — highlight / background color Existing boolean marks (bold/italic/underline/strike/code) are unchanged. Empty value (color:, bgcolor:) clears that slot. Engine - WordsMark union extended with `color:\${string}` and `bgcolor:\${string}` template literal variants. - isWordsMark accepts the parametric form (validates against #hex 3/4/6/8 digits). - normalizeMarks de-duplicates by prefix for parametric marks (only one color, one bgcolor per inline survives). - toggleMarkSet REPLACES same-prefix entries instead of toggling them; empty value strips the slot. - New helpers markValue() and markPrefix() for reading parametric data. Serializers - HTML out: wrapMarks collects color/bgcolor into a single `<span style="color:..;background-color:..">` instead of nested spans, then wraps with the boolean-mark tags. - HTML in: not extended this pass — round-trip via style attr is a follow-up. The marks survive in the document model + render OK. - Markdown out: silently drops parametric marks (no MD syntax for inline color; HTML round-trip is the supported path). DOM render - createWordsTextAttrs splits marks into booleans (→ data-words-marks) and parametrics (→ inline `style` attr) so the contenteditable paints the colors directly. Drawer Format panel - New "Foreground" + "Background" sections with: - 10-swatch palette (red/orange/amber/green/cyan/blue/violet/pink/ white/grey) — hand-picked editorial set - Native <input type=color> for free hex pick (no preset constraint) - "×" clear button (only when a value is set for that slot) - Active swatch outline when the current value matches the preset - runCommand passes the `color:#hex` / `bgcolor:#hex` string straight through commandFromName → toggleMark. WordsCommandName extended with `color:\${string}` and `bgcolor:\${string}` for type-safe consumer calls. 152/152 tests pass. Verified visually: clicking the red swatch on selected "ActiveUI" text in the demo produces `<span data-words-marks="bold" style="color:#ef4444">ActiveUI</span>` in the rendered HTML, text paints red in the editor, the swatch shows active outline + clear (×) button appears. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
></button>
{/each}
<input
type="color"
data-words-drawer-color-input
value={currentParametricMark(s.activeMarks, 'color') || '#000000'}
aria-label="Pick custom foreground color"
fix(words): color picker uses applyCommand (no sema) + onchange instead of oninput Two related fixes to the color picker UX: 1. Swatches now call applyCommand({type:'toggleMark', mark:'color:#x'}) directly instead of runCommand('color:#x'). runCommand emits the 'commit-set-format' sema event (which fires sound + visual feedback on the editor surface). For discrete color picks the sema event is noise — applyCommand bypasses the trigger and just mutates the doc. 2. Native <input type=color> uses `onchange` instead of `oninput`. `oninput` fires continuously while the user drags the OS color picker — each fire was queueing a separate command + sema event, producing a horror cascade of sound / visual flashes on every pixel of the slider drag. `onchange` only fires when the user releases / commits the picker (closes the OS popup), so we get exactly one command per intent. Same pattern applied to both Foreground and Background pickers + the clear (×) buttons. Sema events are still emitted by the rest of the drawer's action chips (run via runCommand) because those are user-facing intent actions (Bold, Italic, etc.) where the perceptual feedback aligns with the user's notion of "I just did a thing". Color slider drags are NOT in that category — they're continuous parameter tuning. Verified: clicking the red swatch on selected "ActiveUI" produces the expected red <span> in the document AND the trace shows no new commit-set-format event (was previously firing on every swatch click, drowning audio + animation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
onchange={(e) => {
feat(words): parametric color marks — foreground + background pickers (COLOR-3) Marks now support parametric values via colon syntax: - color:#hex — foreground text color - bgcolor:#hex — highlight / background color Existing boolean marks (bold/italic/underline/strike/code) are unchanged. Empty value (color:, bgcolor:) clears that slot. Engine - WordsMark union extended with `color:\${string}` and `bgcolor:\${string}` template literal variants. - isWordsMark accepts the parametric form (validates against #hex 3/4/6/8 digits). - normalizeMarks de-duplicates by prefix for parametric marks (only one color, one bgcolor per inline survives). - toggleMarkSet REPLACES same-prefix entries instead of toggling them; empty value strips the slot. - New helpers markValue() and markPrefix() for reading parametric data. Serializers - HTML out: wrapMarks collects color/bgcolor into a single `<span style="color:..;background-color:..">` instead of nested spans, then wraps with the boolean-mark tags. - HTML in: not extended this pass — round-trip via style attr is a follow-up. The marks survive in the document model + render OK. - Markdown out: silently drops parametric marks (no MD syntax for inline color; HTML round-trip is the supported path). DOM render - createWordsTextAttrs splits marks into booleans (→ data-words-marks) and parametrics (→ inline `style` attr) so the contenteditable paints the colors directly. Drawer Format panel - New "Foreground" + "Background" sections with: - 10-swatch palette (red/orange/amber/green/cyan/blue/violet/pink/ white/grey) — hand-picked editorial set - Native <input type=color> for free hex pick (no preset constraint) - "×" clear button (only when a value is set for that slot) - Active swatch outline when the current value matches the preset - runCommand passes the `color:#hex` / `bgcolor:#hex` string straight through commandFromName → toggleMark. WordsCommandName extended with `color:\${string}` and `bgcolor:\${string}` for type-safe consumer calls. 152/152 tests pass. Verified visually: clicking the red swatch on selected "ActiveUI" text in the demo produces `<span data-words-marks="bold" style="color:#ef4444">ActiveUI</span>` in the rendered HTML, text paints red in the editor, the swatch shows active outline + clear (×) button appears. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
const v = (e.currentTarget as HTMLInputElement).value;
fix(words): color picker uses applyCommand (no sema) + onchange instead of oninput Two related fixes to the color picker UX: 1. Swatches now call applyCommand({type:'toggleMark', mark:'color:#x'}) directly instead of runCommand('color:#x'). runCommand emits the 'commit-set-format' sema event (which fires sound + visual feedback on the editor surface). For discrete color picks the sema event is noise — applyCommand bypasses the trigger and just mutates the doc. 2. Native <input type=color> uses `onchange` instead of `oninput`. `oninput` fires continuously while the user drags the OS color picker — each fire was queueing a separate command + sema event, producing a horror cascade of sound / visual flashes on every pixel of the slider drag. `onchange` only fires when the user releases / commits the picker (closes the OS popup), so we get exactly one command per intent. Same pattern applied to both Foreground and Background pickers + the clear (×) buttons. Sema events are still emitted by the rest of the drawer's action chips (run via runCommand) because those are user-facing intent actions (Bold, Italic, etc.) where the perceptual feedback aligns with the user's notion of "I just did a thing". Color slider drags are NOT in that category — they're continuous parameter tuning. Verified: clicking the red swatch on selected "ActiveUI" produces the expected red <span> in the document AND the trace shows no new commit-set-format event (was previously firing on every swatch click, drowning audio + animation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
s.applyCommand({ type: 'toggleMark', mark: `color:${v}` as never });
feat(words): parametric color marks — foreground + background pickers (COLOR-3) Marks now support parametric values via colon syntax: - color:#hex — foreground text color - bgcolor:#hex — highlight / background color Existing boolean marks (bold/italic/underline/strike/code) are unchanged. Empty value (color:, bgcolor:) clears that slot. Engine - WordsMark union extended with `color:\${string}` and `bgcolor:\${string}` template literal variants. - isWordsMark accepts the parametric form (validates against #hex 3/4/6/8 digits). - normalizeMarks de-duplicates by prefix for parametric marks (only one color, one bgcolor per inline survives). - toggleMarkSet REPLACES same-prefix entries instead of toggling them; empty value strips the slot. - New helpers markValue() and markPrefix() for reading parametric data. Serializers - HTML out: wrapMarks collects color/bgcolor into a single `<span style="color:..;background-color:..">` instead of nested spans, then wraps with the boolean-mark tags. - HTML in: not extended this pass — round-trip via style attr is a follow-up. The marks survive in the document model + render OK. - Markdown out: silently drops parametric marks (no MD syntax for inline color; HTML round-trip is the supported path). DOM render - createWordsTextAttrs splits marks into booleans (→ data-words-marks) and parametrics (→ inline `style` attr) so the contenteditable paints the colors directly. Drawer Format panel - New "Foreground" + "Background" sections with: - 10-swatch palette (red/orange/amber/green/cyan/blue/violet/pink/ white/grey) — hand-picked editorial set - Native <input type=color> for free hex pick (no preset constraint) - "×" clear button (only when a value is set for that slot) - Active swatch outline when the current value matches the preset - runCommand passes the `color:#hex` / `bgcolor:#hex` string straight through commandFromName → toggleMark. WordsCommandName extended with `color:\${string}` and `bgcolor:\${string}` for type-safe consumer calls. 152/152 tests pass. Verified visually: clicking the red swatch on selected "ActiveUI" text in the demo produces `<span data-words-marks="bold" style="color:#ef4444">ActiveUI</span>` in the rendered HTML, text paints red in the editor, the swatch shows active outline + clear (×) button appears. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
}}
/>
{#if currentParametricMark(s.activeMarks, 'color')}
<button
type="button"
data-words-drawer-color-clear
title="Clear foreground"
aria-label="Clear foreground color"
onmousedown={(e) => e.preventDefault()}
fix(words): color picker uses applyCommand (no sema) + onchange instead of oninput Two related fixes to the color picker UX: 1. Swatches now call applyCommand({type:'toggleMark', mark:'color:#x'}) directly instead of runCommand('color:#x'). runCommand emits the 'commit-set-format' sema event (which fires sound + visual feedback on the editor surface). For discrete color picks the sema event is noise — applyCommand bypasses the trigger and just mutates the doc. 2. Native <input type=color> uses `onchange` instead of `oninput`. `oninput` fires continuously while the user drags the OS color picker — each fire was queueing a separate command + sema event, producing a horror cascade of sound / visual flashes on every pixel of the slider drag. `onchange` only fires when the user releases / commits the picker (closes the OS popup), so we get exactly one command per intent. Same pattern applied to both Foreground and Background pickers + the clear (×) buttons. Sema events are still emitted by the rest of the drawer's action chips (run via runCommand) because those are user-facing intent actions (Bold, Italic, etc.) where the perceptual feedback aligns with the user's notion of "I just did a thing". Color slider drags are NOT in that category — they're continuous parameter tuning. Verified: clicking the red swatch on selected "ActiveUI" produces the expected red <span> in the document AND the trace shows no new commit-set-format event (was previously firing on every swatch click, drowning audio + animation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
onclick={() => s.applyCommand({ type: 'toggleMark', mark: 'color:' as never })}
feat(words): parametric color marks — foreground + background pickers (COLOR-3) Marks now support parametric values via colon syntax: - color:#hex — foreground text color - bgcolor:#hex — highlight / background color Existing boolean marks (bold/italic/underline/strike/code) are unchanged. Empty value (color:, bgcolor:) clears that slot. Engine - WordsMark union extended with `color:\${string}` and `bgcolor:\${string}` template literal variants. - isWordsMark accepts the parametric form (validates against #hex 3/4/6/8 digits). - normalizeMarks de-duplicates by prefix for parametric marks (only one color, one bgcolor per inline survives). - toggleMarkSet REPLACES same-prefix entries instead of toggling them; empty value strips the slot. - New helpers markValue() and markPrefix() for reading parametric data. Serializers - HTML out: wrapMarks collects color/bgcolor into a single `<span style="color:..;background-color:..">` instead of nested spans, then wraps with the boolean-mark tags. - HTML in: not extended this pass — round-trip via style attr is a follow-up. The marks survive in the document model + render OK. - Markdown out: silently drops parametric marks (no MD syntax for inline color; HTML round-trip is the supported path). DOM render - createWordsTextAttrs splits marks into booleans (→ data-words-marks) and parametrics (→ inline `style` attr) so the contenteditable paints the colors directly. Drawer Format panel - New "Foreground" + "Background" sections with: - 10-swatch palette (red/orange/amber/green/cyan/blue/violet/pink/ white/grey) — hand-picked editorial set - Native <input type=color> for free hex pick (no preset constraint) - "×" clear button (only when a value is set for that slot) - Active swatch outline when the current value matches the preset - runCommand passes the `color:#hex` / `bgcolor:#hex` string straight through commandFromName → toggleMark. WordsCommandName extended with `color:\${string}` and `bgcolor:\${string}` for type-safe consumer calls. 152/152 tests pass. Verified visually: clicking the red swatch on selected "ActiveUI" text in the demo produces `<span data-words-marks="bold" style="color:#ef4444">ActiveUI</span>` in the rendered HTML, text paints red in the editor, the swatch shows active outline + clear (×) button appears. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
>×</button>
{/if}
</div>
<div data-words-drawer-section-subtitle>Background</div>
<div data-words-drawer-color-picker>
{#each COLOR_PRESETS as preset (preset)}
<button
type="button"
data-words-drawer-color-swatch
data-active={currentParametricMark(s.activeMarks, 'bgcolor') === preset ? '' : undefined}
style="background-color:{preset}"
title={preset}
aria-label={`Set background to ${preset}`}
onmousedown={(e) => e.preventDefault()}
fix(words): color picker uses applyCommand (no sema) + onchange instead of oninput Two related fixes to the color picker UX: 1. Swatches now call applyCommand({type:'toggleMark', mark:'color:#x'}) directly instead of runCommand('color:#x'). runCommand emits the 'commit-set-format' sema event (which fires sound + visual feedback on the editor surface). For discrete color picks the sema event is noise — applyCommand bypasses the trigger and just mutates the doc. 2. Native <input type=color> uses `onchange` instead of `oninput`. `oninput` fires continuously while the user drags the OS color picker — each fire was queueing a separate command + sema event, producing a horror cascade of sound / visual flashes on every pixel of the slider drag. `onchange` only fires when the user releases / commits the picker (closes the OS popup), so we get exactly one command per intent. Same pattern applied to both Foreground and Background pickers + the clear (×) buttons. Sema events are still emitted by the rest of the drawer's action chips (run via runCommand) because those are user-facing intent actions (Bold, Italic, etc.) where the perceptual feedback aligns with the user's notion of "I just did a thing". Color slider drags are NOT in that category — they're continuous parameter tuning. Verified: clicking the red swatch on selected "ActiveUI" produces the expected red <span> in the document AND the trace shows no new commit-set-format event (was previously firing on every swatch click, drowning audio + animation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
onclick={() => s.applyCommand({ type: 'toggleMark', mark: `bgcolor:${preset}` as never })}
feat(words): parametric color marks — foreground + background pickers (COLOR-3) Marks now support parametric values via colon syntax: - color:#hex — foreground text color - bgcolor:#hex — highlight / background color Existing boolean marks (bold/italic/underline/strike/code) are unchanged. Empty value (color:, bgcolor:) clears that slot. Engine - WordsMark union extended with `color:\${string}` and `bgcolor:\${string}` template literal variants. - isWordsMark accepts the parametric form (validates against #hex 3/4/6/8 digits). - normalizeMarks de-duplicates by prefix for parametric marks (only one color, one bgcolor per inline survives). - toggleMarkSet REPLACES same-prefix entries instead of toggling them; empty value strips the slot. - New helpers markValue() and markPrefix() for reading parametric data. Serializers - HTML out: wrapMarks collects color/bgcolor into a single `<span style="color:..;background-color:..">` instead of nested spans, then wraps with the boolean-mark tags. - HTML in: not extended this pass — round-trip via style attr is a follow-up. The marks survive in the document model + render OK. - Markdown out: silently drops parametric marks (no MD syntax for inline color; HTML round-trip is the supported path). DOM render - createWordsTextAttrs splits marks into booleans (→ data-words-marks) and parametrics (→ inline `style` attr) so the contenteditable paints the colors directly. Drawer Format panel - New "Foreground" + "Background" sections with: - 10-swatch palette (red/orange/amber/green/cyan/blue/violet/pink/ white/grey) — hand-picked editorial set - Native <input type=color> for free hex pick (no preset constraint) - "×" clear button (only when a value is set for that slot) - Active swatch outline when the current value matches the preset - runCommand passes the `color:#hex` / `bgcolor:#hex` string straight through commandFromName → toggleMark. WordsCommandName extended with `color:\${string}` and `bgcolor:\${string}` for type-safe consumer calls. 152/152 tests pass. Verified visually: clicking the red swatch on selected "ActiveUI" text in the demo produces `<span data-words-marks="bold" style="color:#ef4444">ActiveUI</span>` in the rendered HTML, text paints red in the editor, the swatch shows active outline + clear (×) button appears. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
></button>
{/each}
<input
type="color"
data-words-drawer-color-input
value={currentParametricMark(s.activeMarks, 'bgcolor') || '#ffff00'}
aria-label="Pick custom background color"
fix(words): color picker uses applyCommand (no sema) + onchange instead of oninput Two related fixes to the color picker UX: 1. Swatches now call applyCommand({type:'toggleMark', mark:'color:#x'}) directly instead of runCommand('color:#x'). runCommand emits the 'commit-set-format' sema event (which fires sound + visual feedback on the editor surface). For discrete color picks the sema event is noise — applyCommand bypasses the trigger and just mutates the doc. 2. Native <input type=color> uses `onchange` instead of `oninput`. `oninput` fires continuously while the user drags the OS color picker — each fire was queueing a separate command + sema event, producing a horror cascade of sound / visual flashes on every pixel of the slider drag. `onchange` only fires when the user releases / commits the picker (closes the OS popup), so we get exactly one command per intent. Same pattern applied to both Foreground and Background pickers + the clear (×) buttons. Sema events are still emitted by the rest of the drawer's action chips (run via runCommand) because those are user-facing intent actions (Bold, Italic, etc.) where the perceptual feedback aligns with the user's notion of "I just did a thing". Color slider drags are NOT in that category — they're continuous parameter tuning. Verified: clicking the red swatch on selected "ActiveUI" produces the expected red <span> in the document AND the trace shows no new commit-set-format event (was previously firing on every swatch click, drowning audio + animation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
onchange={(e) => {
feat(words): parametric color marks — foreground + background pickers (COLOR-3) Marks now support parametric values via colon syntax: - color:#hex — foreground text color - bgcolor:#hex — highlight / background color Existing boolean marks (bold/italic/underline/strike/code) are unchanged. Empty value (color:, bgcolor:) clears that slot. Engine - WordsMark union extended with `color:\${string}` and `bgcolor:\${string}` template literal variants. - isWordsMark accepts the parametric form (validates against #hex 3/4/6/8 digits). - normalizeMarks de-duplicates by prefix for parametric marks (only one color, one bgcolor per inline survives). - toggleMarkSet REPLACES same-prefix entries instead of toggling them; empty value strips the slot. - New helpers markValue() and markPrefix() for reading parametric data. Serializers - HTML out: wrapMarks collects color/bgcolor into a single `<span style="color:..;background-color:..">` instead of nested spans, then wraps with the boolean-mark tags. - HTML in: not extended this pass — round-trip via style attr is a follow-up. The marks survive in the document model + render OK. - Markdown out: silently drops parametric marks (no MD syntax for inline color; HTML round-trip is the supported path). DOM render - createWordsTextAttrs splits marks into booleans (→ data-words-marks) and parametrics (→ inline `style` attr) so the contenteditable paints the colors directly. Drawer Format panel - New "Foreground" + "Background" sections with: - 10-swatch palette (red/orange/amber/green/cyan/blue/violet/pink/ white/grey) — hand-picked editorial set - Native <input type=color> for free hex pick (no preset constraint) - "×" clear button (only when a value is set for that slot) - Active swatch outline when the current value matches the preset - runCommand passes the `color:#hex` / `bgcolor:#hex` string straight through commandFromName → toggleMark. WordsCommandName extended with `color:\${string}` and `bgcolor:\${string}` for type-safe consumer calls. 152/152 tests pass. Verified visually: clicking the red swatch on selected "ActiveUI" text in the demo produces `<span data-words-marks="bold" style="color:#ef4444">ActiveUI</span>` in the rendered HTML, text paints red in the editor, the swatch shows active outline + clear (×) button appears. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
const v = (e.currentTarget as HTMLInputElement).value;
fix(words): color picker uses applyCommand (no sema) + onchange instead of oninput Two related fixes to the color picker UX: 1. Swatches now call applyCommand({type:'toggleMark', mark:'color:#x'}) directly instead of runCommand('color:#x'). runCommand emits the 'commit-set-format' sema event (which fires sound + visual feedback on the editor surface). For discrete color picks the sema event is noise — applyCommand bypasses the trigger and just mutates the doc. 2. Native <input type=color> uses `onchange` instead of `oninput`. `oninput` fires continuously while the user drags the OS color picker — each fire was queueing a separate command + sema event, producing a horror cascade of sound / visual flashes on every pixel of the slider drag. `onchange` only fires when the user releases / commits the picker (closes the OS popup), so we get exactly one command per intent. Same pattern applied to both Foreground and Background pickers + the clear (×) buttons. Sema events are still emitted by the rest of the drawer's action chips (run via runCommand) because those are user-facing intent actions (Bold, Italic, etc.) where the perceptual feedback aligns with the user's notion of "I just did a thing". Color slider drags are NOT in that category — they're continuous parameter tuning. Verified: clicking the red swatch on selected "ActiveUI" produces the expected red <span> in the document AND the trace shows no new commit-set-format event (was previously firing on every swatch click, drowning audio + animation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
s.applyCommand({ type: 'toggleMark', mark: `bgcolor:${v}` as never });
feat(words): parametric color marks — foreground + background pickers (COLOR-3) Marks now support parametric values via colon syntax: - color:#hex — foreground text color - bgcolor:#hex — highlight / background color Existing boolean marks (bold/italic/underline/strike/code) are unchanged. Empty value (color:, bgcolor:) clears that slot. Engine - WordsMark union extended with `color:\${string}` and `bgcolor:\${string}` template literal variants. - isWordsMark accepts the parametric form (validates against #hex 3/4/6/8 digits). - normalizeMarks de-duplicates by prefix for parametric marks (only one color, one bgcolor per inline survives). - toggleMarkSet REPLACES same-prefix entries instead of toggling them; empty value strips the slot. - New helpers markValue() and markPrefix() for reading parametric data. Serializers - HTML out: wrapMarks collects color/bgcolor into a single `<span style="color:..;background-color:..">` instead of nested spans, then wraps with the boolean-mark tags. - HTML in: not extended this pass — round-trip via style attr is a follow-up. The marks survive in the document model + render OK. - Markdown out: silently drops parametric marks (no MD syntax for inline color; HTML round-trip is the supported path). DOM render - createWordsTextAttrs splits marks into booleans (→ data-words-marks) and parametrics (→ inline `style` attr) so the contenteditable paints the colors directly. Drawer Format panel - New "Foreground" + "Background" sections with: - 10-swatch palette (red/orange/amber/green/cyan/blue/violet/pink/ white/grey) — hand-picked editorial set - Native <input type=color> for free hex pick (no preset constraint) - "×" clear button (only when a value is set for that slot) - Active swatch outline when the current value matches the preset - runCommand passes the `color:#hex` / `bgcolor:#hex` string straight through commandFromName → toggleMark. WordsCommandName extended with `color:\${string}` and `bgcolor:\${string}` for type-safe consumer calls. 152/152 tests pass. Verified visually: clicking the red swatch on selected "ActiveUI" text in the demo produces `<span data-words-marks="bold" style="color:#ef4444">ActiveUI</span>` in the rendered HTML, text paints red in the editor, the swatch shows active outline + clear (×) button appears. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
}}
/>
{#if currentParametricMark(s.activeMarks, 'bgcolor')}
<button
type="button"
data-words-drawer-color-clear
title="Clear background"
aria-label="Clear background color"
onmousedown={(e) => e.preventDefault()}
fix(words): color picker uses applyCommand (no sema) + onchange instead of oninput Two related fixes to the color picker UX: 1. Swatches now call applyCommand({type:'toggleMark', mark:'color:#x'}) directly instead of runCommand('color:#x'). runCommand emits the 'commit-set-format' sema event (which fires sound + visual feedback on the editor surface). For discrete color picks the sema event is noise — applyCommand bypasses the trigger and just mutates the doc. 2. Native <input type=color> uses `onchange` instead of `oninput`. `oninput` fires continuously while the user drags the OS color picker — each fire was queueing a separate command + sema event, producing a horror cascade of sound / visual flashes on every pixel of the slider drag. `onchange` only fires when the user releases / commits the picker (closes the OS popup), so we get exactly one command per intent. Same pattern applied to both Foreground and Background pickers + the clear (×) buttons. Sema events are still emitted by the rest of the drawer's action chips (run via runCommand) because those are user-facing intent actions (Bold, Italic, etc.) where the perceptual feedback aligns with the user's notion of "I just did a thing". Color slider drags are NOT in that category — they're continuous parameter tuning. Verified: clicking the red swatch on selected "ActiveUI" produces the expected red <span> in the document AND the trace shows no new commit-set-format event (was previously firing on every swatch click, drowning audio + animation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
onclick={() => s.applyCommand({ type: 'toggleMark', mark: 'bgcolor:' as never })}
feat(words): parametric color marks — foreground + background pickers (COLOR-3) Marks now support parametric values via colon syntax: - color:#hex — foreground text color - bgcolor:#hex — highlight / background color Existing boolean marks (bold/italic/underline/strike/code) are unchanged. Empty value (color:, bgcolor:) clears that slot. Engine - WordsMark union extended with `color:\${string}` and `bgcolor:\${string}` template literal variants. - isWordsMark accepts the parametric form (validates against #hex 3/4/6/8 digits). - normalizeMarks de-duplicates by prefix for parametric marks (only one color, one bgcolor per inline survives). - toggleMarkSet REPLACES same-prefix entries instead of toggling them; empty value strips the slot. - New helpers markValue() and markPrefix() for reading parametric data. Serializers - HTML out: wrapMarks collects color/bgcolor into a single `<span style="color:..;background-color:..">` instead of nested spans, then wraps with the boolean-mark tags. - HTML in: not extended this pass — round-trip via style attr is a follow-up. The marks survive in the document model + render OK. - Markdown out: silently drops parametric marks (no MD syntax for inline color; HTML round-trip is the supported path). DOM render - createWordsTextAttrs splits marks into booleans (→ data-words-marks) and parametrics (→ inline `style` attr) so the contenteditable paints the colors directly. Drawer Format panel - New "Foreground" + "Background" sections with: - 10-swatch palette (red/orange/amber/green/cyan/blue/violet/pink/ white/grey) — hand-picked editorial set - Native <input type=color> for free hex pick (no preset constraint) - "×" clear button (only when a value is set for that slot) - Active swatch outline when the current value matches the preset - runCommand passes the `color:#hex` / `bgcolor:#hex` string straight through commandFromName → toggleMark. WordsCommandName extended with `color:\${string}` and `bgcolor:\${string}` for type-safe consumer calls. 152/152 tests pass. Verified visually: clicking the red swatch on selected "ActiveUI" text in the demo produces `<span data-words-marks="bold" style="color:#ef4444">ActiveUI</span>` in the rendered HTML, text paints red in the editor, the swatch shows active outline + clear (×) button appears. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
>×</button>
{/if}
</div>
feat(words): drawer hierarchical mode stack — Format → Cell → Row → Table (HIER 1-3) The drawer now renders the FULL hierarchy of scopes that apply at the caret, instead of only the most-specific one. Inside a table cell with text selected the user sees four panels stacked: Format ← active selection's inline marks/links Cell ← tone, vertical align, cell text-align Row ← +/- row, row header toggle Table ← striped, compact, +/- column, col header Each panel is independently collapsible. Default: the leaf (most specific) is open, the rest are collapsed but accessible via the chevron next to their header. User-explicit toggles persist across context changes. HIER-1 — soma: - WordsDrawerMode union expanded: split 'table' → 'cell' | 'row' | 'table'; split 'list' → 'list-item' | 'list'; added 'block' for the paragraph/heading/quote text-align scope. - WordsDrawerProvider gains `modes: readonly DrawerMode[]` derived that walks the caret context outward (most-specific first). The legacy `mode` field is kept as the top of the stack for consumers that just want the leaf. - snippetProps surfaces both `modes` (stack) and `mode` (leaf). - Morfo `data-words-drawer-mode` values updated to the new union. HIER-2 — eidos panel split: - Table panel decomposed into three: cell-scope (tone + vertical + text-align), row-scope (+/- row, row header), table-scope (+/- col, col header, striped, compact). - List panel decomposed into list-item-scope (indent/outdent) + list-scope (kind). - New 'block' panel hosts text-align when caret is in a heading / paragraph / quote outside a table cell. - Each panel is a `<section data-words-drawer-panel data-depth=N>` with its own toggleable header + body snippet. HIER-3 — per-panel collapse: - SvelteMap stores user-explicit open/closed overrides per mode. isPanelOpen() falls back to "open if leaf, else collapsed" when no override exists. - Drawer header now shows leaf title in bold + breadcrumb of deeper panels in muted text (Format · Cell · Row · Table). - Visual depth cue: border-inline-start of each panel renders at decreasing strength as depth grows (leaf solid, deeper at 60% / 30% accent border). Visual verification: selected text inside a sample table cell shows 4 panels stacked (Format open, Cell/Row/Table collapsed), header breadcrumb "FORMAT · Cell · Row · Table". Click on any collapsed header expands it. 214/214 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
<div data-words-drawer-actions data-flush>
<button
type="button"
data-words-drawer-action
data-tone="full"
onmousedown={(e) => e.preventDefault()}
onclick={() => {
const url = window.prompt('Link URL:');
if (url) s.insertLink(url);
}}
>
Add link
</button>
{#if s.canUnlink}
<button
type="button"
data-words-drawer-action
onmousedown={(e) => e.preventDefault()}
onclick={() => s.unlink()}
>
Remove
</button>
{/if}
<button
type="button"
data-words-drawer-action
data-tone="ghost"
onmousedown={(e) => e.preventDefault()}
onclick={() => s.runCommand('clear-formatting')}
>
Clear
</button>
</div>
{/snippet}
{#snippet blockPanel(s: ProviderSnippetProps)}
<div data-words-drawer-section-subtitle>Text align</div>
<div data-words-drawer-actions>
{#each [
{ id: 'align-left', label: '⫷', title: 'Left' },
{ id: 'align-center', label: '☰', title: 'Center' },
{ id: 'align-right', label: '⫸', title: 'Right' },
{ id: 'align-justify', label: '☷', title: 'Justify' }
] as a (a.id)}
<button
type="button"
data-words-drawer-action
data-active={s.currentTextAlign === a.id.replace('align-', '') ? '' : undefined}
title={a.title}
onmousedown={(e) => e.preventDefault()}
onclick={() => s.runCommand(a.id)}
>
{a.label}
</button>
{/each}
</div>
{/snippet}
{#snippet cellPanel(s: ProviderSnippetProps)}
<div data-words-drawer-section-subtitle>Tone</div>
<div data-words-drawer-actions>
{#each [
{ id: 'table-cell-tone-default', label: 'Plain' },
{ id: 'table-cell-tone-muted', label: 'Muted' },
{ id: 'table-cell-tone-accent', label: 'Accent' }
] as t (t.id)}
<button
type="button"
data-words-drawer-action
data-active={s.currentTableCellTone === t.id.replace('table-cell-tone-', '') ? '' : undefined}
onclick={() => s.runCommand(t.id)}
>{t.label}</button>
{/each}
</div>
<div data-words-drawer-section-subtitle>Vertical</div>
<div data-words-drawer-actions>
{#each [
{ id: 'table-cell-vertical-top', label: '⬆ Top' },
{ id: 'table-cell-vertical-middle', label: '◆ Mid' },
{ id: 'table-cell-vertical-bottom', label: '⬇ Bot' }
] as v (v.id)}
<button
type="button"
data-words-drawer-action
data-active={s.currentTableCellVerticalAlign === v.id.replace('table-cell-vertical-', '') ? '' : undefined}
onclick={() => s.runCommand(v.id)}
>{v.label}</button>
{/each}
</div>
<div data-words-drawer-section-subtitle>Text align</div>
<div data-words-drawer-actions>
{#each [
{ id: 'align-left', label: '⫷' },
{ id: 'align-center', label: '☰' },
{ id: 'align-right', label: '⫸' },
{ id: 'align-justify', label: '☷' }
] as a (a.id)}
<button
type="button"
data-words-drawer-action
data-active={s.currentTextAlign === a.id.replace('align-', '') ? '' : undefined}
onclick={() => s.runCommand(a.id)}
>{a.label}</button>
{/each}
</div>
{/snippet}
{#snippet rowPanel(s: ProviderSnippetProps)}
<div data-words-drawer-actions>
<button type="button" data-words-drawer-action onclick={() => s.runCommand('insert-table-row')}>+ Row</button>
<button type="button" data-words-drawer-action data-tone="risk" onclick={() => s.runCommand('delete-table-row')}>− Row</button>
<button
type="button"
data-words-drawer-action
data-active={s.currentTableHeaderRowActive ? '' : undefined}
onclick={() => s.runCommand('toggle-table-header-row')}
>Header</button>
</div>
{/snippet}
{#snippet tablePanel(s: ProviderSnippetProps)}
<div data-words-drawer-section-subtitle>Columns</div>
<div data-words-drawer-actions>
<button type="button" data-words-drawer-action onclick={() => s.runCommand('insert-table-column')}>+ Col</button>
<button type="button" data-words-drawer-action data-tone="risk" onclick={() => s.runCommand('delete-table-column')}>− Col</button>
<button
type="button"
data-words-drawer-action
data-active={s.currentTableHeaderColumnActive ? '' : undefined}
onclick={() => s.runCommand('toggle-table-header-column')}
>Header</button>
</div>
<div data-words-drawer-section-subtitle>Appearance</div>
<div data-words-drawer-actions>
<button
type="button"
data-words-drawer-action
data-active={s.currentTableStriped ? '' : undefined}
onclick={() => s.runCommand('toggle-table-striped')}
>Striped</button>
<button
type="button"
data-words-drawer-action
data-active={s.currentTableCompact ? '' : undefined}
onclick={() => s.runCommand('toggle-table-compact')}
>Compact</button>
</div>
{/snippet}
{#snippet listItemPanel(s: ProviderSnippetProps)}
<div data-words-drawer-actions>
<button type="button" data-words-drawer-action onclick={() => s.runCommand('decrease-indent')}>⇤ Outdent</button>
<button type="button" data-words-drawer-action onclick={() => s.runCommand('increase-indent')}>⇥ Indent</button>
</div>
{/snippet}
{#snippet listPanel(s: ProviderSnippetProps)}
<div data-words-drawer-section-subtitle>Kind</div>
<div data-words-drawer-actions>
<button type="button" data-words-drawer-action onclick={() => s.runCommand('unordered-list')}>Bullet</button>
<button type="button" data-words-drawer-action onclick={() => s.runCommand('ordered-list')}>Numbered</button>
<button type="button" data-words-drawer-action onclick={() => s.runCommand('check-list')}>Check</button>
</div>
{/snippet}
{#snippet codePanel(s: ProviderSnippetProps)}
<div data-words-drawer-actions>
{#each CODE_LANGUAGES as lang (lang.id)}
<button
type="button"
data-words-drawer-action
data-active={s.currentCodeLanguage === lang.id ? '' : undefined}
onclick={() => s.setCodeLanguage(lang.id || undefined)}
>{lang.label}</button>
{/each}
</div>
{/snippet}
{#snippet imagePanel(s: ProviderSnippetProps)}
{@const block = imageBlockAtCaret(s)}
{#if block}
<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}
<div data-words-drawer-section-subtitle>Caption</div>
<p data-words-drawer-readonly-text>{block.caption}</p>
{/if}
{#if block.status}
<div data-words-drawer-section-subtitle>Status</div>
<p data-words-drawer-readonly-text data-tone={block.status}>
{block.status === 'pending' ? 'Uploading…' : 'Upload failed'}
</p>
{/if}
{:else}
<p data-words-drawer-empty>Place caret on an image to edit it.</p>
{/if}
{/snippet}
refactor(words): drawer canon — morfo part + soma provider + eidos consumes via provider (DRAWER-CANON) Closes the architectural gap from the DRAWER spike: the companion drawer now follows the same morfo/soma/eidos pattern as every other Words part (Toolbar/BubbleMenu/SlashMenu/LinkEditor/Status/etc.). DRAWER-CANON-1 — morfo + soma: - wordsMorfo gains a 'Drawer' part (kebab 'drawer', archetype 'group', defaultElement 'aside', role 'complementary'). Declares data-words- drawer-mode (default/format/image/table/code/list), data-open, data-disabled, aria-label. New event 'commit-toggle-drawer' fires on collapse/expand for sema/telemetry hooks. - New WordsDrawerProvider in words-provider.svelte: owns `open` bindable state, derives `mode` from parent provider's currentBlock + selection (same logic as the spike, now in soma where it belongs), exposes snippetProps {mode, open, disabled, snippet, toggle, setOpen}. `toggle()` and `setOpen()` emit the event. - WordsDrawerProps + WordsDrawerSnippetProps + WordsDrawerMode types added to types.ts and re-exported via exports.ts. - soma/components/words/components/words-drawer.svelte (the soma component) mounts the provider and forwards merged props to either the `child` snippet or the default <aside> render. DRAWER-CANON-2 — eidos refactor: - eidos words-drawer.svelte rewritten as a consumer of <Words.Drawer>. The shell (aside + header + collapse button) and the per-mode body now live inside the children snippet that receives the soma's drawer snippet props. Mode derivation removed from eidos (it's now in soma). Local open $state removed (now bindable via the soma component). - Demo +page.svelte: `<Words.Drawer snippet={snippetProps} />` collapses to plain `<Words.Drawer />` — the soma provider hands the parent snippet down internally. DRAWER-CANON-3 — verify: - 214/214 tests pass in soma+morfo scope (no regressions). - npx tsc --noEmit clean for words area. - Visual verification: drawer still renders with DOCUMENT/OUTLINE/ STATS/SUGGESTIONS in default mode; context-switching to table / code / list / format still works; aside element now carries role='complementary', aria-label from morfo translationRef, data-words-drawer-mode reflecting current panel. The eidos drawer now passes the same canon checks as every other Words part: morfo declares its contract, soma owns the state, eidos only paints. The schema validator can verify the drawer's attrs, the eidos lint can check selector usage, sema can react to the toggle event. Demo header chips reflect the new contract: PARTS 19→20, EVENTS 11→12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
<script context="module" lang="ts">
import type { ProviderSnippetProps as PSP } from '$soma/components/words';
type Warning = {
readonly id: string;
readonly severity: 'info' | 'warn';
readonly message: string;
};
export function collectWarnings(s: PSP): readonly Warning[] {
const out: Warning[] = [];
s.document.children.forEach((block, index) => {
if (block.type === 'image' && !block.alt?.trim()) {
out.push({
id: `alt-${index}`,
severity: 'warn',
message: `Image at block ${index + 1} has no alt text`
});
}
if (block.type === 'paragraph') {
const text = block.children
.map((i) => (i.type === 'text' ? i.text : ''))
.join('');
if (text.length > 600) {
out.push({
id: `long-${index}`,
severity: 'info',
message: `Paragraph ${index + 1} is long (${text.length} chars) — consider splitting`
});
}
}
});
const headingLevels = s.document.children
.filter((b) => b.type === 'heading')
.map((b) => (b.type === 'heading' ? b.level : 0));
for (let i = 1; i < headingLevels.length; i++) {
if (headingLevels[i] - headingLevels[i - 1] > 1) {
out.push({
id: `skip-${i}`,
severity: 'warn',
message: `Heading h${headingLevels[i]} appears without an h${headingLevels[i - 1] + 1} above — structure skip`
});
break;
}
}
return out;
}
</script>

Powered by TurnKey Linux.