Two fixes from feedback:
- The active/hovered-block marker belonged in the GUTTER, not as an overlay
on the block. `[data-words-block-outline]` is now a thin (3px) vertical
accent rail pinned to the gutter column (left margin, just left of where
the text begins) spanning the block's height — only top/height track the
block. Replaces the tinted fill that landed on the block content.
- Preview is read-only, but `WordsActivate` (click-to-select → the dotted
active outline) still ran, so blocks could be selected in preview. Add an
`enabled` prop (false in preview): no click-activation and the active stamp
is cleared. `<Words>` passes `enabled={mode === 'edit'}`.
Verified in-browser: the gutter rail sits 8px left of the block text (not
over it), spanning the block height; clicking a block in preview no longer
stamps `data-words-active`. Check clean for the touched files.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three issues found while testing the menu bar:
- Preview "did nothing": clicking Preview does switch to read-only
(contenteditable=false), but a read-only surface looks identical to an
editable one, so the change was invisible. Now `[data-mode='preview']`
collapses the bar's formatting groups, leaving just the view controls
(inspector + Edit/Preview) — the bar visibly simplifies.
- Gutter active block: the dashed outline read poorly. Replace it with a
faint primary-tinted fill + a 3px accent bar on the left edge (toward the
grip), so the block the handle acts on is obvious.
- Bold / insert "don't work" with no caret: a selection-dependent command
fired from the menu bar (outside the editable) no-op'd when the editor
was never focused — `toggleMark` bailed on a null selection, and
`insertBlockOfType` silently fell back to the last block. Add a TipTap
`.focus()`-style guard `ensureCaret()` in the provider: when `selection`
is null it drops a collapsed caret at the document start, so marks arm
(stored mark → next typed text) and inserts land predictably after the
first block. Wired into `runCommandName` (marks / align) and
`insertBlockOfType`. The provider freezes the selection across blur, so
this only fires on a never-focused editor.
Verified in-browser: Preview hides the format groups (toggle stays); the
hovered block shows the tinted fill + accent; a fresh editor's Bold now
arms + places a caret. Words tests 477/477 pass; check clean for the
touched files.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The editor had no persistent formatting bar — commands lived only in the
bubble (selection), slash (/) and drawer. Add a docked menu bar at the top
of `<Words>`, built from the existing (previously unused) soma
`Words.Toolbar` / `Words.CommandButton` primitives, the same way the bubble
composes them.
- words-menubar.svelte (new): role=toolbar with six groups — history
(undo/redo) · block style (Text/H1-3/Quote/Code via a DropdownMenu) ·
marks (B/I/U/S/code) · lists · align · insert (image/table/divider via
`api.insertBlockOfType`). Marks/align are `Words.CommandButton`s rendered
as eidos `Button`s (range restored before the command runs, active state
reflected). Right side: an Edit/Preview toggle + an inspector toggle.
- words.svelte: mounts the bar; owns `mode` ('edit'|'preview') mapped to the
provider's `readonly` (preview = contenteditable off, editing chrome —
bubble/gutter/drag — suppressed, bar stays visible). Unifies the inspector
open state (drawer open / sidebar visibility) behind one toggle the bar
drives; the floating drawer button is gone (folded into the bar).
- types.ts: `toolbar?: boolean` prop (default true).
- words.css: `[data-words-menubar]` fixed-height top bar (`--_words-bar-h`),
group dividers, active-control accent (`data-active` + `aria-pressed`), and
offsets the docked inspector panels below the bar.
- demo: drop the now-duplicate Edit/Preview toggle + serialized-HTML preview
swap (the bar owns preview as read-only); keep the inspector picker.
Verified in-browser: bar renders (role=toolbar, 44px, 6 groups, 18 controls);
Edit→Preview flips contenteditable to false, disables format buttons and hides
the chrome while the bar persists; Bold applies to a selection from the bar
(onpointerdown range-restore holds across the out-of-editor click); the
inspector toggle hides/shows the sidebar; align-left reads active for a
left-aligned block; no console errors; check clean for the touched files.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gutter grip rendered (cursor: grab) but the drag-to-move
functionality was lost when the soma-DragDrop migration was abandoned —
clicking it only opened the action menu (Move up / Move down). Restore
the reorder as native HTML5 drag-drop:
- words-block-drag-type.ts: private MIME `application/x-words-block` the
grip writes (its top-level index) and the controller reads, so block
reorders are told apart from native text drags.
- words-block-gutter.svelte: the grip's DropdownMenu.Trigger is now
`draggable`; `ondragstart` stamps the block index on the transfer. The
menu still opens on click (the dropdown opens on click, not
pointerdown — no collision with the drag gesture).
- words-block-drag.svelte: the drop sink. Listens on the contenteditable
for dragover/drop, paints a horizontal indicator at the nearest
inter-block seam, and repositions via the engine's `moveBlockTo`
(target is post-removal: `to = from < n ? n - 1 : n`). Bails unless the
drag carries our private type, leaving native text drag untouched.
A document-capture `dragend` clears the indicator wherever the drag ends.
- words.svelte: mount the controller next to the gutter.
- words.css: `[data-words-block-drop-line]` accent indicator.
Reorder is top-level only (the gutter is top-level only), mirroring the
gutter's `[data-words-node='block']` block detection exactly.
Verified in-browser: grip is draggable=true; dragstart writes index;
dragover paints the indicator + preventDefaults; drop reorders correctly
downward (0→end) and upward (14→1) with no block lost; native text drags
are not hijacked; no console errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two loose ends from the rich-paste work:
- The block drag-handle menu was hard-coded English (Inspect / Move up /
Duplicate / Insert below / Paste below / Delete) inside a Spanish editor.
Route every label through the langs system like the inspector already
does: new langs-menu.ts (idlangref catalog + en/es bundle), registered by
the eidos <Words> wrapper (`components.words.menu`); the gutter reads them
via ActiveEidos.require().langs.ts(). Verified the menu renders in Spanish
(Inspeccionar / Subir / Bajar / Duplicar / Insertar debajo / Pegar debajo
/ Eliminar) under the ES locale.
- README serialization doctrine: the "Si algun dia hace falta pegar HTML
externo" section ANTICIPATED exactly this feature (per-block parseHtml,
clipboard best-effort, persistence stays JSON). Mark it IMPLEMENTED
(2026-06-02) with the actual modules, fix the table's HTML Import cell,
and correct the now-stale "paste HTML is rejected" line — HTML now parses
to blocks; only content-free HTML logs unsupported-html. importContent
unchanged (JSON + plain text); HTML enters only via paste.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A discoverable, click-driven paste — the answer to "where do I paste?".
The block drag-handle menu gains a "Paste below" item that reads the
clipboard and inserts its content as new blocks right after that block,
no Ctrl+V needed.
- provider.pasteAfterBlock(index): reads the async Clipboard API
(navigator.clipboard.read, falling back to readText), parses text/html
into rich blocks via the inert-document parser (else splits plain text
into paragraphs), and inserts them at top-level index+1. No-op when the
clipboard is empty / unreadable / permission-denied. Exposed on the
provider snippet api.
- insertBlocks gains an optional `at` index: insert as siblings at an
explicit top-level position (ignoring the caret) for the menu paste.
Command carries `at`.
- eidos block gutter: "Paste below" DropdownMenu item -> api.pasteAfterBlock.
Tests: insert-blocks 7/7 (added explicit-index case). Reuses the
browser-verified parseWordsHtml + insertBlocks core.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Paste previously flattened everything to plain text (the signed import
doctrine deferred rich paste). Now the editor parses clipboard text/html
into the V2 model, the inverse of serialize-html.ts:
- parse-html.ts: parseWordsHtml(domRoot) -> WordsBlock[]. Maps p / h1-6 /
blockquote / pre / ul-ol-li (incl. checkboxes) / hr / img / figure /
table, with inline marks (bold/italic/underline/strike/code, colour +
background from style) and links. URLs sanitised; unknown blocks fall
back to a paragraph, unknown inline tags are transparent.
- insert-blocks.ts: insertBlocks(state, blocks) drops blocks at the caret.
Single pasted paragraph inline-merges (a phrase stays in the sentence);
multiple/block-level paste splits the host; a nested or non-inline caret
degrades to a structure-preserving plain-text insert; a trailing
paragraph guarantees a caret home after a terminal table/image. New
insertBlocks command + dispatch.
- provider onpaste: parse text/html in an INERT document
(createHTMLDocument off the active dom — no scripts run, no resources
load), then applyCommand insertBlocks. Image-files / single-URL /
plain-text paths unchanged; plain text is still the fallback.
Tests: insert-blocks 6/6 (merge/split/replace/append/no-op), parse-html
10/10 browser (marks, lists, tables, colour, javascript: URL sanitised).
Browser-verified: pasting an h2 + a bold/italic paragraph + a list inserts
the heading, the formatted paragraph and the list at the caret. Updated
the provider's unsupported-paste test (real HTML now parses; only
content-free fragments log unsupported-html).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
normalizePastedPlainText strips NUL chars from pasted text, but the regex
held a LITERAL 0x00 byte (/<NUL>/g) instead of the escape /\0/g. Runtime
behaviour was correct, but the raw NUL made ripgrep treat the whole file
as BINARY — so input.ts silently dropped out of every codebase content
search. Swap the byte for the \0 escape: identical at runtime (verified
input.test.ts 18/18), clean ASCII source, searchable again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two inspector-input issues:
- The image alt field (eidos <TextArea>) used the GLOBAL neutral skin
(transparent fill, lighter border) while the URL/caption inputs use the
words palette — so the textarea looked unframed and out of place.
Retokenise [data-textarea] in the inspector to --words-border / --words-bg
/ radius-2 / words font-size; now it reads identically to the text inputs
(verified both render border rgb(232,232,232) + fill rgb(249,249,249)).
- Every field drew an accent border AND a box-shadow ring on focus — two
stacked frames. Drop the border-accent change; the single accent ring is
the focus signal, the border stays structural. Applied to text inputs,
NumberField and TextArea so all inspector fields focus identically.
Scoped to the words inspector (not the global eidos input recipe).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The radius control was a 0-48 slider plus a none/full-only toggle — the
preset toggle 'didn't work' for anything between, and a bare slider value
is invisible until the block also has a border or background. Replace both
with one attached ToggleGroup of named steps: None / S / M / L / Full →
0 / 4 / 8 / 16 / 9999 px (render.ts already emits border-radius from
border.radius). Verified the control renders [Ninguno, S, M, L, Completo].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The floating selection menu now carries text + background colour, applied
to the selection like bold/italic. A 'Color' (A) dropdown opens Text +
Background swatch grids; each swatch is a Words.CommandButton whose
color:#hex / bgcolor:#hex command the provider already parses into a
parametric toggleMark on the live selection — reusing the same
range-restoring path the mark buttons use, no bespoke selection wiring.
The 'Default' chip clears the mark (empty value).
- words-bubble.svelte: colour DropdownMenu (Baseline / Highlighter icons),
9-hue palette + clear, marked data-words-bubble-color.
- words-provider: add [data-words-bubble-color] to the bubble's
keep-selection-alive closed surface so interacting with the dropdown
doesn't collapse the model selection.
- words.css: swatch-grid styling (hue from inline --words-swatch; clear
chip is a diagonal strike).
Verified: selecting a word → A → blue swatch recolours it (rendered
rgb(0,145,255), model carries {type:'color',value:'#0091ff'}); bubble +
selection stay alive so multiple swatches can be applied in a row.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three demo/chrome fixes:
- Drawer inspector: the open toggle (SlidersHorizontal) stayed mounted
while the panel was open, colliding with the panel's own X close in the
same corner. Render it only while closed; the panel's header X closes it.
- Preview columns: serialize as <div data-words-columns><div data-words-column>,
which no preview rule matched, so they stacked. Mirror the editor's row flex.
- Preview headings: were --font-size-2xl/xl/lg at weight 700 — louder than
the editor. Reference the SAME recipe tokens (xl, lg+4, lg; semibold;
heading line-height + primary color) so Preview reads like Edit. Verified
editor + preview both render h1=28px/600, h2=24px/600.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inserting an image into a column (slash menu or inspector "Add block")
left an empty trailing paragraph beside it. An image is a placeholder
configured through the inspector (URL panel), not a block you type after,
so the trailing paragraph was pure noise — the reported bug.
insertIntoColumn / insertIntoCell / insertBlockInColumn now skip the
trailing paragraph for images and return a null (caret-free) selection:
the atomic image has no inline text to host a caret, and the provider
auto-activates it for the URL panel. A divider keeps its trailing
paragraph (it separates typed content); table/callout keep theirs too
(they carry inline text, so the caret stays in the block).
5 new engine tests pin the behavior across both insert paths + the cell
case; divider regression guarded. Browser-verified: image into a column
yields [image] only, no empty text block.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P5m made the model accept blocks in cells, but the atomic-insert path
(`insertAtomicBlock`) only special-cased columns — with the caret in a cell it
fell through to the top-level split, dropping the block NEXT TO the table
instead of inside the cell. So "blocks in cells" was model-only, not reachable.
Adds an `insertIntoCell` branch mirroring `insertIntoColumn`, one level deeper
(container path [tableIdx, rowIdx, cellIdx, blockInCell, ...]): empty-cell
paragraph → replace in place (+ trailing paragraph); non-empty → split the
cell's block around the inserted one. So insert-callout / divider / image (and
any atomic insert) with the caret in a cell now land inside that cell.
New test `insert-block-types.test.ts` (3/3): inserts into the cell (single
top-level table preserved), callout lands inside, non-empty cell splits
(para("a") · divider · para("b")). npm run check: 0 new errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the long-open "export HTML roundtrip?" question as export-only — the
architecturally-correct, industry-aligned choice (TipTap/ProseMirror, Lexical,
Slate, Notion all keep JSON as the canonical lossless persistence format and
treat HTML/MD as projections).
- README "Serializacion e intercambio" doctrine: JSON = truth; HTML/MD =
export-only projections; import = JSON + plain text. Explains the
state-complete-HTML trap that JSON avoids, why markdown is the most lossy
(GFM cells are inline-only — block cells from P5m can't roundtrip), and the
conditional `parseHtml` per-block path (parseDOM/importDOM pattern) IF
paste-from-external is ever needed — as clipboard interop, never persistence.
- continue.md: the "NO firmada" note flipped to FIRMADA with the rationale.
Docs only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POLISH-3. Replaces the bare Inspector-mode row in the Words demo with a real
topbar: a document breadcrumb (FileText icon + the doc's first-heading title),
a "Saved / Editing…" indicator that flips on document change and settles after
a short idle, and the Inspector-mode + Edit/Preview toggles.
Preview mode renders the document as a clean, read-only reading view via the
export-only `serializeHtml` serializer (bare semantic HTML with its own
typography — headings, blockquote, code, lists, table, links). Showcases the
export path now that table cells serialize block content (<td><p>…</p></td>).
Also exposes the export-only serializers on the public soma barrel
(`serializeHtml`, `serializeMarkdown`, `renderWordsPlainText`) — apps need them
for preview / copy / download.
Browser-verified: topbar renders; Edit↔Preview swaps the editor for the
read-only article (table + headings render); Saved badge shows. npm run check:
0 new errors (1 pre-existing grafito baseline).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P5m. A table cell now holds full BLOCKS (paragraphs, lists, images, nested
tables) like Callout / Column, not just inline text. A fresh cell seeds one
empty paragraph; the existing generic block machinery (path walker, render,
validate, normalize) handles cells the same way it handles callout / column.
Atomic model change across the engine:
- types: TableCell.children: WordsInline[] -> WordsBlock[].
- factories / table-ops / extra-ops: cells seed [paragraph], not [text].
- normalize: normalizeTableCells recurses each child block (mirrors callout);
empty cell seeds a paragraph.
- built-ins table spec: render -> renderBlock per child; validate ->
validateBlock per child; HTML -> blockToHtml per child (<td><p>...</p></td>);
Markdown -> flatten blocks to inline joined with <br> (GFM cells are
inline-only; lossy, consistent with markdown being export-only).
- serialize-text + selection-walkers: cell text recurses block children.
- delete-ops: inline cross-cell merge (mergeTablePrev/Next) replaced by
recurseIntoTableMerge — treats the cell's blocks as a sub-document, so
backspace merges blocks WITHIN a cell; at a cell's first block it is a no-op
(cells never merge across the fixed grid — matches mainstream editors).
Caret paths into a cell are one level deeper now ([t,r,c,blockIdx,...]). All
table test fixtures wrapped (inline -> paragraph) and 13 assertions updated to
the new behavior (<p> wrappers, deeper paths, no cross-cell merge).
Browser-verified: cells render block content (path 14.0.0.0), table renders
clean. npm run check: 0 words errors (1 pre-existing grafito baseline). Words
tests: 452/452.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1. Image was missing from the per-column "Add block" menu — the inspector's
`columnInsertableSpecs` filter excluded `image` outright. Removed the
exclusion: the image spec's `create()` builds an empty-src placeholder and
`insertIntoActiveColumn` auto-activates it, so the inspector's image panel
immediately prompts for the URL / Upload — the canonical placeholder-on-
insert flow (same one the gutter inserter and slash menu use). Verified:
menu now lists "Imagen"; selecting it adds a placeholder image to the column
and switches the inspector to the image panel (URL + Upload + alt + caption).
2. Drawer inspector mode "only shifted" instead of hiding/sliding. The panel is
parked off the right edge via translateX(100%) when closed, but the frame's
overflow was `visible`, so the closed panel just sat outside the right border
(visible) and toggling merely slid it sideways. Added `overflow-x: clip` to
`[data-words][data-inspector='drawer']` so the closed panel is genuinely
hidden and the open one emerges from the edge. X-axis only: the block-gutter
grip (left margin, inside the frame) and the vertically-overflowing bubble
toolbar stay visible. Verified: closed panel clipped/hidden; open panel slides
in inside the frame's right edge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bleed-through: the portaled [data-dropdown-menu-content] had z-index:auto, so
any positioned page element with a positive z-index (e.g. a selected
[data-toggle-group-item], z-index 1) painted THROUGH it. In the Words inspector
the Add-block menu showed the Typography toggle chips ("Predeterminada" /
"Normal") bleeding through. Added `content-z: 80` to the dropdown-menu recipe
(matches `select`, its sibling click-triggered overlay) + `z-index:
var(--dropdown-menu-content-z, 80)` on content + sub-content; regenerated
base.css. The soma floating layer reads this computed z-index and mirrors it
onto the positioner wrapper. The rest of the menu family (context-menu /
menubar / navigation-menu) shares the same latent gap — deferred.
Canonization (finishes the dom.raf rollout — zero raw requestAnimationFrame
left in src/uix):
- tabs-indicator.svelte: requestAnimationFrame/cancelAnimationFrame +
new MutationObserver + new ResizeObserver -> eidos.dom.raf /
observeMutation / observeResize (disposer pattern via ActiveEidos.require).
- tabs.svelte: new ResizeObserver -> eidos.dom.observeResize.
- resize-observer.svelte.ts: requestFrame/cancelFrame (raw handle) ->
dom.raf (disposer); fixed the stale "Debounces via requestAnimationFrame"
comment.
Browser-verified in the Words demo: column Add-block menu is opaque and on top
(elementFromPoint at the chip centers returns menu items, inMenu:true); the
menu options are collapsed + localized (one Encabezado, one Lista con viñetas);
empty-column delete works (column persists addressable with 0 children).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two bugs in the column inspector's "Add block" flow:
1. Wrong options + untranslated. The menu rendered
`defaultWordsSchema.insertable()` — the flattened menu ENTRIES, which
expand heading → "Heading 1/2/3" and list → "Bulleted/Numbered/Check"
with hard-coded English labels. Now it lists one entry per block KIND
from `schema.all()` (paragraph / heading / quote / code / list / table /
divider / callout), labelled via `blockKindLabel(type)` → fully
localized ("Texto", "Encabezado", "Lista con viñetas", …). The exact
level / list-kind is tuned afterward in that block's own panel. Inserts
the kind's default via `spec.menu[0].create()`. Excludes `columns`
(no columns-in-columns) and `image` (insertable:false — needs a URL).
2. Couldn't leave a column empty. `deleteChildInColumn` re-seeded an empty
paragraph when the last block was removed ("must stay typable"). But an
empty column is a valid, supported state — the engine renders the
wrapper with `data-words-id` so it stays clickable, and neither the
validator nor `normalizeColumns` re-seeds it (only the doc root + lists
do). Removed the re-seed; deleting the last block now leaves the column
genuinely empty.
Also added the 5 missing column-panel aria bundle entries
(select/delete/move-up/move-down/add-block-to-column-n) so the block-list
buttons read in ES instead of the English fallback.
Verified in the browser: menu shows 8 localized kinds (1 heading, 1 list,
no "Heading 1"); deleting the last column block → child count 0, no reseed,
column still clickable; aria-labels localized. npm run check 1 pre-existing
error (grafito, not Words) · words soma 452/452 · prettier clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `raf(callback, node?): ActiveDomFrameCleanup` to the ActiveDom surface —
a one-shot animation frame that returns an idempotent **disposer** (the same
`() => void` shape as `listen` / `observe*`), so an `$effect` can
`return dom.raf(...)` and Svelte cancels the pending frame on teardown. It
wraps the existing `requestFrame` / `cancelFrame` (which already resolve the
instance's `targetWindow` — iframe / popup / happy-dom safe), adding no new
scheduling logic. New `ActiveDomFrameCleanup` type exported from the barrel;
`raf` also implemented on the disabled-dom stub (throws, like `requestFrame`).
Why: the doctrine is "all DOM activity via ActiveDom". `listen`/`observe*`
already returned disposers; the animation frame was the gap — `requestFrame`
exposes a raw numeric handle (per-call bookkeeping + leak risk), and layout
components were falling back to the GLOBAL `requestAnimationFrame`, which
targets the wrong window in iframe/popup contexts (the exact bug getWindow/
getDocument fix elsewhere). `raf` closes it.
Migrated the 3 raw `requestAnimationFrame` sites the Words audit surfaced —
words-block-gutter (reposition), words-bubble-menu + words-slash-menu
(overlay position) — to `dom.raf(...)`. Bubble/slash keep their `tick()`
fallback for no-rAF environments.
Documented the decision + rationale as a dated Backlog entry at the end of
`src/arts/adom/README.md` (and listed `raf` in the API + imperative-actions
sections). Notes the kept distinction: `raf` is for layout frames, NOT the
`$timer` lifecycle scheduler; low-level requestFrame/cancelFrame stays for
consumers that already hold the handle (drawer/slider/splitter/floating/
focus-scope). Remaining raw rAF in other eidos components (tabs-indicator…)
left for when those are touched — flagged in the backlog.
Gates: npm run check 1 error (pre-existing grafito, not adom/Words) · soma
words + adom 479/479 · prettier clean · browser smoke: gutter repositions,
bubble menu positions, no console errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- D1 (soma) block-tree.ts:104 — replace `doc as never` (which disabled all
type checking on the descend container) with `doc as unknown as {...}`, the
honest cast that still asserts the permissive descend-node shape
childStacks() reads.
- D2 (eidos) — extract the duplicated ~25-line "file picker → FileReader →
data URL" flow (copy-pasted in words-block-gutter.svelte and
words-block-panel.svelte) into a shared `pickImageFile(doc, onPick)` helper
in words-image-file.ts. Both consumers now call it; zero inline FileReader
blocks remain. Kept as a plain helper (not ActiveDom) — a transient
<input type=file> + FileReader is not instrumented by active-dom, so a
direct Document touch is correct; the caller resolves the right Document.
Gates: npm run check 1 error (pre-existing grafito, not Words) · words soma
452/452 · prettier clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Full audit of the Words component (3 parallel explore agents + deterministic
gates) surfaced 4 carried-over defects, all verified and fixed:
- R2 (soma) engine/operations/visual.ts:147 — the `node as Record` cast in
updateBlockAtPath failed because WordsNode includes WordsDocument (no index
signature). Use the double cast `as unknown as Record`, consistent with the
sibling op at line 118. Clears the only Words type error in `npm run check`.
- R1 (morfo) components/words.ts — the `data-words-node` enum was conflated
with `data-words-block`: it listed block TYPES (paragraph/heading/quote/
code/image) that nothing emits on that attr, and OMITTED the real
structural kinds the engine writes (`block`/`inline`). Aligned to the
engine's WordsDomNodeKind. eidos-lint invalid selectors 2 → 0; verified in
the browser that top-level blocks carry node='block' and the active-block
outline still paints.
- R3 (eidos) langs-inspector.ts — `LABEL_COLUMN_BLOCKS` had no bundle entry,
so the column "Blocks" panel title fell back to English in ES. Added
label.column-blocks + label.add-block. Verified: panel now reads "Bloques".
- D3 (morfo) components/words.ts — declared `expression: 'family-default'`
to make the no-sema-pack choice explicit; `morfo:vocabulary` no longer
warns about the missing pack/expression.
Gates: npm run check 1 error (pre-existing, web/routes/temas/grafito — not
Words) · words soma+morfo 514/514 · eidos-lint words invalid 0 · prettier
clean · morfo:vocabulary exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reconcile the theming reference with the engine as already committed
(color-model redesign, scaling axis, P2 fixes) so the doc is internally
consistent:
- TOC: add §23 (scaling), §24 (P2 corrections), §25 (color model); fix
the §21 entry to its resolved heading.
- §4: intent->scale mapping was stale (risk->amber not orange,
loss->plum not purple); note intents auto-derive via
CANONICAL_INTENT_SCALES (identity = step 9, cross-ref §25); correct
token counts (31-scale palette = 744 --scale-* tokens; 9 roles = 216
primitives).
- §3 / anti-pattern G: "30 escalas" -> 31; reconcile "never add a scale"
with §25.7 (a brand theme brings its own palette).
- §21 / §22: mark the two-level color RFC as resolved in §25 (anchor
model discarded); drop the stale present-tense "vigente" claim.
- README reference table: add §23 / §24 / §25 rows.
Docs only -- no engine or demo changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Rename theme untitled-ui -> Grafito: route /temas/grafito, theme ids
grafito-light/dark, grafitoConfig, brand/hero/footer/index.
- Palette: Grafito now carries the full 32-scale library (spreads the
framework's 31 Radix scales + brand carbon, overriding the brand-tuned
gray/violet/blue + intents). Each directly usable as --scale-{name}-{step}.
- Roles: hierarchy explicit (primary->carbon, secondary->violet,
tertiary->blue); the 6 intents auto-derive from the palette by convention
(step 9). THEME_SCALES derived from palette keys, labelled by COLOR NAME.
- Demo Color section -> 3 groups (Paleta / Roles jerarquia / Intents
auto-derivados) + scaling control in the topbar.
Verified in browser: 32 distinct scales emitted, intents auto-derive
(affirm=teal, risk=amber, loss=plum), no validation error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two audit P2 quality defects, fixed at engine level.
P2-2 - on-solid text illegible on light solids:
The `contrast` slot defaulted to `--color-content-on-solid` (white) for every
role. On light solids (amber/yellow, risk=orange ~2.3:1) white is sub-AA. The
engine now picks by WCAG contrast (gamma-linearized) of the role step-9: when
white fails (<3:1) it uses `--color-content-on-solid-contrast` (a dark, new
OPTIONAL `content.onSolidContrast` semantic, #1c1917 in base). Only risk flips
to dark in base (5.89:1); purple/red/teal/green keep white (convention, >=3:1).
An explicit `slots.contrast` override is still honored verbatim.
P2-4 - opaque tinted soft surfaces:
The soft variant tint (Button + Badge `{role}-soft-bg`) was opaque (step-1 track
+ opaque color-mix hover) so it did not composite over non-uniform backgrounds.
New derived tokens `--color-{role}-surface` (= a2) + `--color-{role}-surface-hover`
(= a3) are translucent by construction (compositing-inverse alpha). Button/Badge
soft consume them. Toast/Tabs untouched - they are cards, opacity is correct.
- config-types: optional onSolidContrast + CONTENT_COLOR_OPTIONAL_KEYS.
- config: content keySet allows the optional key; validator value-checks it.
- render-css: wcagContrastRatio/wcagRelativeLuminance; luminance pick; emit
on-solid-contrast + surface/surface-hover.
- contract: on-solid-contrast + surface tokens per role.
- themes/base: onSolidContrast #1c1917 (light + dark).
- recipes/base: Button + Badge soft-bg -> surface, soft-bg-hover -> surface-hover.
- docs: THEMING.md section 24 + section 22 item 2; audit P2-2/P2-4 resolved.
Verified: npm run check (0 new errors), vitest eidos (0 new regressions),
browser runtime (risk 5.89:1 dark text, surfaces translucent rgba).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the synthetic step-9-at-opacity alpha ramp with a real compositing inverse: each --scale-X-aN is the translucent color that, painted over the scale's background (white for light scales, black for dark, by step-1 luminance), reproduces the opaque solid step N (Radix model). Computed in render-css.ts for ANY theme (base + custom), so alpha is consistent-by-construction with the solid scale and cannot drift. alphaScales opt-in still wins verbatim; non-hex solids fall back to the legacy color-mix ramp. Verified: --scale-purple-a9 = rgb(92 0 173 / 0.6941) over white = #8e4ec6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Alpha steps (--scale-*-aN) are unconsumed today (only a test + README ref). The step-9-at-opacity synthesis is a valid 'accent-at-opacity' ramp default, not a bug; it is not a Radix-style reproduction of the solid scale. Documented the model, its limitation, and the already-wired alphaScales opt-in in appendColorAlphaScaleDeclarations. A build-time compositing-inverse generator is deferred until alpha is actually consumed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The runtime token-override API is setCssVariables() / clearCssVariables(); setOverrides() never existed. A dev copying §19 would hit a runtime error. (audit P2-5/A1)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add 40 recipes.banner.{intent}-{slot} forwarders (= var(--color-{intent}-{slot}))
- banner.css reads --banner-{intent}-{slot} instead of foundation --color-{intent}-* directly
- Banner is now retintable per-component via recipes.banner.* like button/badge; keeps data-intent (no API change)
- Regenerated base.css; recipe contract balanced (40 declared = 40 consumed), no visual regression
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Active-block addressing by materialized-path id (words-activate.svelte +
engine/block-tree.ts): click sets the active block explicitly; inspector
follows it, immune to selection churn. Breadcrumb navigates ancestry.
Inspector controls:
- ToggleGroups (font / weight / align / border) always keep one option
selected; the default value stores as undefined to keep the model clean.
- Border gains a `none` chip that clears the border (BorderChipValue).
- Image full-width is a Switch; image src + column width/gap validate on
commit (sanitizeWordsUrl, CSS-length/flex checks) with a [data-invalid] ring.
- Corner-radius + border-width are slider-only (WordsNumRow `sliderOnly`):
the paired numeric stepper was redundant.
- Numeric fields clamp to their range (line-height ≤ 2.5, font-size 12-40).
Inspector chrome:
- Unified field frame + single accent focus ring (no inset outline over text,
no ghost/black-border mix).
- Section dividers + uppercase accordion headers; styled breadcrumb row.
- Depth-tied accent (primary/affirm/loss/risk) shared by header mark and the
canvas active-block ring.
- Selection ring is an ::after pseudo-element with a fixed radius, so a block
the user rounded no longer rounds its selection indicator.
- Raw hex/rgba replaced by --color-*-solid tokens (theme-aware, portal-safe).
Columns: empty on insert, gap control, per-column width; Add-block removed
from the Columns BLOCK panel (lives in the column inspector). Shift+Enter
soft break renders via white-space: pre-wrap.
npm run check: 1 pre-existing baseline error (visual.ts:147). Words soma
452/452. Verified end-to-end in the browser.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The provider had a "smart" rule in syncSelectionFromDom that kept the
atomic-block highlight (and therefore the Inspector's panel) sticky
while the caret moved to a different block AS LONG AS that next
block was inside the same TOP-LEVEL wrapper (the rationale: editing
the trailing paragraph after an image-in-column shouldn't kick the
user out of the image inspector).
In practice users got the opposite of what they expected: they clicked
on something new, the inspector didn't follow. They thought the
editor was broken.
Drop the smart rule. Every real selection change now also clears the
atomic-block highlight — the inspector follows the caret, period. If
the user wants the atomic's panel back they click the figure again
(which `handleAtomicClick` re-selects).
We still skip the clear when `sameWordsSelection` reports no-op so
the synthetic selectionchange echo that fires right after
`selectAtomicBlock` (which doesn't move the DOM caret) can't wipe
the highlight a tick after being set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The floating in-canvas `+` overlay for column inserts was fundamentally
at odds with contenteditable semantics — it needed pointer-events:none
to let caret/text events pass through, but that ALSO disabled CSS
:hover detection, and any visible variant covered the column's auto-
trailing paragraph (the escape hatch the engine adds after atomic
inserts), hijacking clicks intended for typing.
Adopt React Bricks's pattern: container-block child insertion goes
through the sidebar/inspector, not via floating overlays on the canvas.
- New "Añadir bloque" dropdown per column in the Columns inspector
panel (`words-block-panel.svelte`). Lists every insertable block
type the engine knows (paragraph, headings, lists, image, divider,
callout, etc.). Picking one dispatches `insertBlockInColumn`,
appending it to the chosen column.
- Inspector lives outside the contenteditable, so the dropdown
doesn't fight focus traps, doesn't cover content, doesn't need
pointer-events gymnastics. Zero of the bug classes we hit.
- Deleted `words-column-inserter.svelte` and its mount in
`words.svelte`. The `insertBlockInColumn` engine op stays — it's
the right primitive, just driven from a different surface now.
- Added `ARIA_ADD_BLOCK_TO_COLUMN_N` and `LABEL_ADD_BLOCK` to the
inspector langs catalog.
Top-level block insertion (between rows) keeps using the canonical
gutter handle `⋮⋮ → Insert below` and the slash menu (for in-flow
keyboard users).
Inside-column editing keeps working as before: click on the block,
type. Enter at end of a paragraph creates a new paragraph below
within the column.
Breaking: anyone who imported `words-column-inserter.svelte`
directly is broken. Nobody outside this folder did.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The wrap was rect-matched to the whole column rect, which meant the
`+` trigger (positioned at the bottom of the wrap, pointer-events:auto
on hover) overlapped the column's TRAILING paragraph — the empty
escape-hatch the engine auto-adds after atomic inserts (image,
divider). Clicking on that paragraph to start typing hit the `+`
trigger and opened the dropdown instead.
Fix: for non-empty columns, position the wrap as a thin 28px strip at
the column's bottom (where the `+` lives) instead of covering the
whole column. The trailing paragraph above the strip is now clickable.
Empty columns keep the full-column wrap (so the centered `+`
invitation remains).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`pointer-events: none` on the inserter wrap (required so caret/text
events pass through to the contenteditable below) ALSO disabled CSS
`:hover` detection — the browser doesn't dispatch `mouseover` to
pointer-events:none elements, so the wrap's `:hover` styles never
fired and the `+` trigger stayed at opacity 0 forever.
Fix: track hover via JS — listen to `mouseenter`/`mouseleave` on the
column elements directly, stash the key in `hoveredColumnKey`, expose
to CSS via `data-hovered` on the wrap. CSS now uses
`[data-words-column-inserter][data-hovered]` instead of `:hover`.
Effect: hovering a non-empty column now reveals the `+` at the bottom
of the column, allowing users to add more blocks below the existing
content. Same fix applied to the empty-column reveal-on-hover (the
opacity-1 boost when fully hovered).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three composing bugs were blocking the `+`→pick-block→type flow:
1. **Path 5-segmentos en `insertBlockInColumn`** — the op passed the
path TO the text node (`[colsIdx, colIdx, innerIdx, 0]`) to
`pointFromInlineTextOffset`, which expects the path to the
CONTAINER block. The helper then appended its own inline-index,
producing a stale 5-segment path that no `[data-words-path]`
selector matches. Fix: pass `[colsIdx, colIdx, innerIdx]` for
heading/paragraph/quote/code/callout, `[colsIdx, colIdx, innerIdx, 0]`
for list (the first list-item IS the container).
2. **Selectionchange echo overwriting the model** — `api.focus()`
landed the caret at doc-start (browser default for focusing a
contenteditable without a prior selection), fired selectionchange,
`syncSelectionFromDom` overwrote the model's intended post-insert
selection with `0.0` before `restoreDomSelection` could run. Fix:
capture `intended = api.selection` immediately after the op, then
re-apply via `setTimeout(0)` AFTER the echo handler has settled.
3. **DropdownMenu focus trap intercepted `api.focus()`** — the
FocusScope's capture-phase `focusin` bouncer kept yanking focus
back inside the menu. Fix: `trapFocus={false}` on Content.
Also expose `api.restoreCaret()` (provider line 1274) for any consumer
that needs to force-write the model selection to DOM after awaiting
enough ticks for new blocks to mount.
Verified end-to-end in Chrome: empty column → click `+` → pick
Heading → "Title" stub selected → typing immediately replaces with
user's text. Inspector switches to Heading panel. Doc title and rest
of the doc untouched.
CONTINUE.md P0 — RESUELTO.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three architectural pieces for inserting blocks into a column slot from
an overlay button, plus the missing DOM-selection sync that any
imperative consumer of `applyCommand` needs.
- Engine op `insertBlockInColumn` returns `{document, selection,
activeMarks}` in one transaction (Tiptap-style). Atomic blocks land
with a trailing paragraph escape hatch + caret there; text-bearing
blocks select any stub text ("Title", "List item") so the next
keystroke replaces it Notion-style.
- Provider `applyCommandWithOptions` now schedules `restoreDomSelection`
via tick when the command changes the model selection (typing-batch
excluded — the browser already placed the caret). Was the hidden gap:
overlay buttons, drag-drop, slash menu, the new column inserter, all
updated the model but the DOM caret stayed wherever the user last
clicked, breaking subsequent text editing.
- `words-column-inserter.svelte` rebuilt around a busy guard with a
hard 250ms safety timeout (the previous pendingInsert + onCloseAuto
Focus pattern could leave the `+` button dead forever if the
dropdown's teardown swallowed the close callback).
Plus type sync: `WordsProviderSnippetProps` now declares
`selectedBlockPath`, the second arg of `selectAtomicBlock`, and
`setSelection` — they were exposed by the runtime but missing from
the type, breaking typecheck on eidos consumers.
Demo carries a `columns` block in the initial doc as a permanent test
fixture for column-related fixes.
**Known issue documented in CONTINUE.md P0:** typing inside a `columns`
block does NOT insert — selection sync (`syncSelectionFromDom`) isn't
mapping nested paths (`12.0.0.0`) to the model correctly. The inserter
flow is wired correctly; once the path encoding for nested selections
lands, the full Notion-style "click + → pick Heading → type" flow
works end to end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lands the eidos `<WordsColorRow>` (Text / Background pickers inside the
block inspector) on top of the new base-Block words engine + drops 3
obsolete audit MDs.
ColorPicker fixes surfaced while wiring it into the inspector — all
documented in `soma/components/color-picker/README.md` §Integration
pitfalls:
1. Eidos wrapper now declares `format = $bindable('hex')` (matches
soma's default). Without it, `bind:format={undefined}` threw
`props_invalid_value` on every mount → render loop.
2. Eidos wrapper now forwards `ref` to `ColorPickerProvider.create`.
Without it, `attachRef` was never built and any `runtime.trigger`
targeting `provider` threw `SomaRuntimeTargetError`.
3. `triggerClose` falls back to the picker provider's own DOM when
`runtime.partRef('content')` returns null (content is registered on
the Popover's runtime, not the picker's — re-exported part).
4. WordsColorRow draft-pattern: `$effect` reads draft inside `untrack`
so it doesn't overwrite mid-drag value, and `onValueChange` catches
the `Clear` programmatic transition (`onValueChangeEnd` doesn't fire
on Clear).
CSS: `contain: layout style` on `[data-color-picker-area]` and
`scrollbar-gutter: stable` on the picker popover content — stops the
area from shaking during drag when the trigger's ValueText hex changes
width or the popover scrollbar toggles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per the user-directed rework. The engine is rebuilt around a single
base `Block` carrying the common properties (id, align, margin,
padding, background, text color, border), with each specialized block
(paragraph/heading/quote/code/list/table/image/divider/callout — plus
list-item/table-row/table-cell) EXTENDING it. The old `visual` sidecar
+ per-type `Pick<>` whitelist + loose `textAlign` are gone; common
props are first-class and universal.
Naming: dropped the `v2` noise everywhere — `operations-v2/`→
`operations/`, `types-v2`/`render-v2`/`validate-v2`/`serialize-*-v2`→
clean names, `*V2` symbols→bare, `WordsEvalIntent`→`WordsIntent`.
Sema: removed entirely from the editor — deleted `sema/components/
words.ts` + `engine/sema-parity`, dropped the registration from the
sema barrel + the demo layout, no `runtime.trigger`/semantic events.
Visual layer: deleted `eidos/components/words/*` (the bespoke chrome)
and stubbed the demo — both rebuilt next on the new model (F2 render
by components, F3 gutters/bubble/drawer, all eidos).
Engine tests 400/400 green; `npm run check` 0 errors (5142 files).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The editor read as "poor" largely from border clutter — hairlines
everywhere. Strip incidental borders and let tonal surfaces + spacing
carry the structure:
- Toolbar: drop the bottom hairline (its surface tone separates it).
- Drawer: drop the seam line (both sides) — tonal separation only.
- Drawer panels: drop inter-panel divider lines (scope accent + spacing).
- Code block: drop the outline — the overlay surface fill defines it.
- Table: no outer box, no vertical lines — light horizontal row rules
only + airier cells.
- Drawer insert items: borderless soft tonal tiles (hover deepens with
the accent) instead of bordered boxes.
Kept the intentional lines (quote accent bar, the silver margin rule).
Verified in browser: no console errors; table is horizontal-rule-only,
insert tiles borderless.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The editor used UI-cramped metrics (14px / 1.25 line-height / 8px block
gap) which read as a form field, not a document. Give the CONTENT (not
the chrome) a reading scale: body 14→16px, line-height 1.25→1.6, block
gap space-2→space-3 (headings derive 1.5x for top margin). Chrome keeps
its smaller --_words-font-size.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Decouple band width from text inset: `--_words-rail-width` (1.25rem,
the thin band) + `--_words-rail-gap` (1.25rem, breathing room) →
text is no longer glued to the band edge.
- Narrow the band: 2.5rem → 1.25rem (20px).
- Lighter tones: rail-bg #ededed → #f5f5f5, rail-border #cfcfcf →
#e3e3e3. Eidos CSS regenerated.
Verified: band 20px + 1px #e3e3e3 edge, then 20px air before text at 40px.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User-directed editor rework, step 1 (left gutter):
- Remove the "+" inserter button from the block-handle gutter. Its
action moves into the drag-handle menu as a new "Insert below" item
(folded into `act('insertBelow')`); the standalone `insertBelow`
helper is gone. The separate seam inserter (words-block-inserter)
is untouched.
- Turn the left gutter into a soft silver margin band: narrow it
(`--_words-gutter-size` 3.5rem→2.5rem) and paint it via a
background-gradient on [data-words-content] — `--words-rail-bg`
#ededed band with a 1px, slightly stronger `--words-rail-border`
#cfcfcf right edge. Background only, so the contenteditable text /
selection is undisturbed. Recipe tokens softened + eidos CSS
regenerated.
Verified in browser: gutter shows only the grip on hover (no "+");
the menu reads Insert below / Move up / Move down / Duplicate / Delete
and "Insert below" inserts a paragraph (7→8 blocks) and closes the
menu; the silver band renders at 40px with the 1px border. npm run
check: 0 errors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds an H1/H2/H3 toggle to the drawer's Block panel, shown only when
the caret is in a heading (currentBlock === 'heading'). The toggle
reflects currentHeadingLevel and dispatches the structured
`setBlock` command with the numeric level via applyCommand:
applyCommand({ type: 'setBlock', block: 'heading', level })
runCommand / WordsCommandName has no per-level id (heading-1/2/3 are
slash/insert-menu ids), so the structured command carries the level;
`as const` keeps each level a 1|2|3 literal.
Verified in browser: caret in a heading shows "Heading level"; the
toggle reflects the active level and converts bidirectionally
(H1→H3→H2) with the toggle updating. npm run check: 0 errors.
Also closes the stale "drag handle outside the rail over <code>" P1:
it does not reproduce — the rail was removed in the gutter redesign
(5450d5f7) and the handle now anchors to contentRect.left+8 for every
block type (verified: identical left over code and paragraph). Both
P1 items marked resolved in continue.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The drawer image panel already had a corner-radius slider and
shadow/border toggles. Adds the controls that were still missing so
the V2 `block.visual` sidecar is fully editable from the UI:
- shadow blur slider (shown when shadow is on) → visual.shadow.blur
- shadow color picker → visual.shadow.color
- border color picker (shown when border is on) → visual.borderColor
- background color picker (always, with clear) → visual.background
All route through a small reusable `imageColorField` snippet (preset
swatches + native <input type=color> + optional clear), mirroring the
format panel's text-color UI, and dispatch `setBlockVisual`. The
engine already merges + renders these via visualToStyle onto the
<figure>.
Verified in browser: panel renders all four new controls when an image
is selected; clicking a Background swatch updates the figure
(background-color:#22c55e) while preserving radius/border/shadow
(shallow-merge intact). npm run check: 0 errors (26 pre-existing
warnings, none new).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Removes now-dead selectors left after V2 dropped table striped/compact
and cell tone/header (D2). Nothing emits these attrs anymore:
- `[data-command='table-cell-tone-*']` toolbar rules
- `table[data-words-table-striped]` / `[data-words-table-compact]` rules
- `[data-words-cell-tone='muted'|'accent']` cell + focus-within rules
- the dead `[data-words-cell-header]` alternates (headers render as <th>);
`tr:hover td:not([data-words-cell-header])` simplified to `td`
README updated: tables list only the live formatting (header row/column,
cell align, vertical align); HTML/Markdown documented as export-only,
import as JSON + plain text. ARCHITECTURE_PROPOSAL keeps its mentions —
they document the decision to drop these, not stale features.
Verified in browser: sample 2x2 table renders, <th> keeps the accent
tint + weight via :where(th), cell align/vertical intact, no console
errors. CSS + Markdown only — no type surface touched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Deletes the V1 rich-text engine and the extensions/ tree. The public
document model flips from WordsDocument to WordsDocumentV2 (D1: V2 puro,
sin legacy — no backward-compat importer, V1-persisted docs no longer
load). Per D2, table striped/compact/cell-tone are dropped entirely
(props, commands, morfo attrs).
- Delete engine/{document,operations,commands,history,input,render,
find-replace,serialize-{html,json,markdown,text},dom-selection,index}
and the whole extensions/ tree (registry + image + table).
- Complete the V2 ops under operations-v2/: port commands, history,
input, find-replace, render-dom, serialize-text, extra-ops,
insert-block-types, markdown shortcuts, factories, dom-selection;
land R4A.2-R4A.8 (text/delete/marks-ops/paragraph-ops/delete-ops/
block-format/list-ops/link-ops/table-ops/selection-walkers).
- Rewire provider, exports, types, morfo/words.ts, eidos word
components, soma words.svelte, the content-test harness, and the
/uix/components/words demo to V2.
- Import accepts JSON (JSON.parse + validateWordsDocument gate) and
plain text only; HTML and Markdown remain export-only (lossy, no
faithful inverse parser).
Verify: npm run check 0 errors; words scope 455/456 (lone failure is the
sema-parity 5s timeout flake under parallel load — 6/6 isolated);
morfo:vocabulary exit 0; browser smoke of /uix/components/words green
(sample renders, text + JSON import round-trip, invalid JSON rejected
without mutating the doc).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>