astra
alpha-0.1-background
alpha-0.1-dir-prefs
alpha-0.1-sec-dom
menubar-v4-safe
active-uix
morfo-runtime
morfo-driven-soma
semantuix
glm-5
main
sium-v1.0
${ noResults }
899 Commits (6c8a7260ecb8d70c343fb055a373aa8cbe743537)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
329f30cf30 |
feat(words): text + background colour in the bubble menu
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>
|
4 months ago |
|
|
ed71ccf46b |
fix(words): drawer toggle overlap + faithful preview (columns, headings)
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> |
4 months ago |
|
|
65cf4bde35 |
fix(words): no trailing paragraph when inserting an image into a column/cell
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> |
4 months ago |
|
|
c4acfa4460 |
feat(words): insert blocks INTO table cells (complete P5m's UI path)
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>
|
4 months ago |
|
|
aac80f7630 |
docs(words): sign the export-only serialization decision (no roundtrip)
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> |
4 months ago |
|
|
30192bb853 |
feat(words): production-grade demo topbar (breadcrumb + Edit/Preview + Saved)
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> |
4 months ago |
|
|
aa9ffedfd8 |
feat(words): table cells hold blocks (TableCell.children → WordsBlock[])
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> |
4 months ago |
|
|
6ce4a0e922 |
fix(words): restore Image in column Add-block menu + make Drawer inspector hide/slide
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> |
4 months ago |
|
|
44fe111ca0 |
fix(eidos): dropdown-menu z-index + finish ActiveDom canonization (tabs, resize-observer)
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>
|
4 months ago |
|
|
64e13d1dd6 |
fix(words): column Add-block menu (one per kind, localized) + allow empty column
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>
|
4 months ago |
|
|
2d60703ab4 |
feat(adom): canonize dom.raf() frame scheduler + migrate Words overlays
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> |
4 months ago |
|
|
1186d794cd |
refactor(words): close audit debt D1 (honest cast) + D2 (dedup image picker)
- 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>
|
4 months ago |
|
|
49802e9257 |
fix(words): close 4 cross-layer audit findings (morfo/soma/eidos)
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> |
4 months ago |
|
|
8090efc833 |
docs(theming): align THEMING.md + README with current color/scaling engine
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> |
4 months ago |
|
|
89147d0e1b |
Grafito theme: rename from untitled-ui + 32-scale palette + Color 3-group
- 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>
|
4 months ago |
|
|
152d2ad960 |
Theming: Radix-parity palette + intent auto-derivation + color-model docs
Two-level color model settled (THEMING.md section 25), replacing the anchor RFC:
the palette is the source (scales, directly usable, designable); hierarchy roles
alias scales explicitly; intents auto-derive from the palette by the book's
canonical convention.
- Palette library expanded 12 -> 31 scales at Radix Colors parity (exact values):
radix-scales.ts (19 added: mauve/sage/olive/sand/tomato/ruby/crimson/plum/
violet/iris/indigo/jade/grass/brown/sky/mint/lime/gold/bronze) spread into
base.ts. Each directly usable as --scale-{name}-{step}.
- Intent auto-derivation: CANONICAL_INTENT_SCALES (neutral->gray, affirm->teal,
fulfill->green, risk->amber, threat->red, loss->plum) + completeColorRoleMap.
Intents omitted from a theme role map fill from the convention (identity =
step 9); slots derive normally; override optional. ColorRoleMap: hierarchy
required, intents optional.
- Validation: hierarchy roles required; omitted intents validate the canonical
scale exists in the palette.
- index: export ScalingKey / SCALING_KEYS.
- docs: THEMING.md section 25 (full color model + decisions), section 21 marked
resolved, COLOR_MODEL_RFC resolved (anchor rejected).
- test: base library asserts 31 scales x 12 steps.
Verified: npm run check (0 new errors), vitest eidos (0 new regressions),
browser (intents auto-derive: affirm=teal #0E9384, risk=amber #DC6803,
loss=plum #7A3AAD at step 9).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
4 months ago |
|
|
66ce4f354d |
Theming: on-solid contrast by luminance (P2-2) + translucent role surfaces (P2-4)
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>
|
4 months ago |
|
|
bceaef41e0 |
Theming: add `scaling` zoom axis (Radix parity), separate from density
Introduce a global zoom axis independent of density, in parity with Radix
Themes' `scaling` (90/95/100/105/110%). Scaling zooms px metrics INCLUDING
typography (font-size, icon-size, space, control-height); density only moves
layout rhythm + control height and leaves text fixed. The two axes compose
multiplicatively.
- config-types: SCALING_KEYS / ScalingKey / DEFAULT_SCALING; DensityPrimitiveSet
drops the dead `scale` + `contentScale` (kept spaceScale, controlScale).
- primitives/static: STATIC_SCALING (0.9..1.1).
- render-css: appendScaledMetricDeclarations wraps metrics in
calc(<raw>[ * var(--density-x-scale)] * var(--scaling)); appendScalingDeclarations
emits --scaling-{key} + --scaling default; renderScalingBlocks emits
[data-scaling] blocks. line-height/radius/border/shadow excluded.
- config + contract: prune the removed density scalars.
- active-eidos: `scaling` / `scalingSource` options, getScaling() on the
preference source, data-scaling projection + dispose cleanup.
- docs: THEMING.md section 23 + 20.1 reconcile; README density/scaling; SCALING_RFC.md.
Verified: npm run check (0 new errors), vitest eidos (0 new regressions),
browser cascade at 90/100/110 scales font/space/control x0.9/x1.1 and leaves
radius/border fixed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
4 months ago |
|
|
98d6799ad0 |
Theming: generate alpha scales via compositing inverse (P1-1)
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> |
4 months ago |
|
|
ef9f589d4c |
docs(theming): clarify synthetic alpha-scale model + alphaScales opt-in (P1-1)
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> |
4 months ago |
|
|
c8fa6e87ed |
docs(theming): fix §19 reference to nonexistent ActiveEidos.setOverrides()
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> |
4 months ago |
|
|
3adc93b2d1 |
Banner: route palette through recipe forwarders (P1-5)
- 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>
|
4 months ago |
|
|
218ccfcc5e |
Theming: untitled-ui demo theme, engine fixes, audit + P0/P1 fixes
- New untitled-ui demo theme + theming showcase route (web/routes/temas) - Engine: density tokens scale via calc(var(--density-*-scale)); contrast slot defaults to on-solid - Eidos theming audit (THEMING_AUDIT_2026-06-01.md) + two-tier color-model RFC (COLOR_MODEL_RFC.md) - P0: replace phantom foundation tokens in recipes + component CSS — shadows -> semantic scale (subtle/raised/overlay), add --space-7, --border-width-strong -> thick; neutral solid contrast -> step 12 (AA in light + dark) - P1: validate hex color values + per-theme semantic completeness; regression tests for contrast, density, dark output, and TSC multi-part/composition emission Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
4 months ago |
|
|
9324764675 |
feat(words): inspector UX overhaul — id-based active block, controls, chrome
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> |
4 months ago |
|
|
7be1d54f89 |
fix(words): inspector follows every click — drop sticky atomic-block selection
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> |
4 months ago |
|
|
e81b53e806 |
refactor(words)!: column block insertion moves to inspector (React Bricks pattern)
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> |
4 months ago |
|
|
eb293b4e50 |
fix(words): column inserter overlay only covers bottom strip on non-empty columns
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> |
4 months ago |
|
|
8cdc4e3b9e |
fix(words): column inserter `+` reveals on hover for non-empty columns
`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> |
4 months ago |
|
|
eb946148cd |
fix(words): column inserter — caret lands in new block, typing replaces stub
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>
|
4 months ago |
|
|
bcad6647a7 |
feat(words): column inserter + provider DOM-selection sync after commands
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>
|
4 months ago |
|
|
da8d03085b |
feat(words): inspector ColorPicker integration + 4 root-cause picker fixes
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>
|
4 months ago |
|
|
73636d07a9 |
refactor(words)!: F1 — rework engine to base-Block model, drop v2 + sema
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> |
4 months ago |
|
|
1bb16ff636 |
feat(words): border-light chrome restyle (declutter)
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> |
4 months ago |
|
|
8afe2728bb |
feat(words): comfortable reading scale for the content surface
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> |
4 months ago |
|
|
9e13d0083e |
fix(words): refine the left margin band per feedback
- 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> |
4 months ago |
|
|
540c35e679 |
feat(words): gutter rework — drop "+", move insert into drag menu, silver band
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>
|
4 months ago |
|
|
005239c732 |
feat(words): inline heading-level control in the Block drawer panel
Adds an H1/H2/H3 toggle to the drawer's Block panel, shown only when
the caret is in a heading (currentBlock === 'heading'). The toggle
reflects currentHeadingLevel and dispatches the structured
`setBlock` command with the numeric level via applyCommand:
applyCommand({ type: 'setBlock', block: 'heading', level })
runCommand / WordsCommandName has no per-level id (heading-1/2/3 are
slash/insert-menu ids), so the structured command carries the level;
`as const` keeps each level a 1|2|3 literal.
Verified in browser: caret in a heading shows "Heading level"; the
toggle reflects the active level and converts bidirectionally
(H1→H3→H2) with the toggle updating. npm run check: 0 errors.
Also closes the stale "drag handle outside the rail over <code>" P1:
it does not reproduce — the rail was removed in the gutter redesign
(
|
4 months ago |
|
|
cc4412b8e9 |
feat(words): complete image visual panel (R5 / POLISH-1b)
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> |
4 months ago |
|
|
4a0661a53b |
chore(words): sweep D2 orphans from eidos CSS + README
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> |
4 months ago |
|
|
10ed029b91 |
refactor(words)!: remove V1 engine — V2 is now the sole engine
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>
|
4 months ago |
|
|
5450d5f717 |
fix(words): gutter redesign — rail out, handle at block lower-left, "+" at block bottom-center
Per spec: "la banda de la derecha la eliminamos, el drag aparece con
el hover del block a la izquierda inferior y el insertar (+) en el
centro cuando el hover este en el final del block, la line debe de
ser mas solida sin blur"
Layout:
- Removed the dedicated rail grid column entirely. The editor host
grid drops from 4 columns (rail | gap | content | drawer) to 2
columns (content | drawer), or (drawer | content) when side=left.
- Removed `--_words-rail-bg/border/px/gap` CSS vars + the
`::before` pseudo-element that drew the silver band. The rail
recipe tokens (words.rail-bg / rail-border) become dead and can
be removed in a later sweep; left for now to avoid generated-css
test regen in the same commit.
Block handle (drag grip):
- Position math simplified: lives at the BLOCK'S lower-left corner
with a 4px inset. `top = rect.bottom - 28 - 4`,
`left = rect.left + 4`. No more rail-relative math, no
detectDrawerSide / cssToPx / rememberRailMetrics helpers — all
removed.
- CSS base opacity bumped from 0.75 → 1 (the grip is shown via
JS only on block hover; the visibility is binary, not faded).
Block inserter ("+"):
- findSeamForCursor rewritten: only triggers when the cursor is
INSIDE a top-level block AND in its bottom portion (last 25% of
block height, capped at 24px). When triggered, the seam anchors
at the block's bottom edge spanning the block's full width — CSS
centers the button via left:50% + translateX(-50%).
- Removed listBlockBoundaries + SEAM_RANGE_PX (dead code under the
new model — we don't compute inter-block seams anymore, we react
to the hovered block's bottom).
- Removed drawerSide flip + data-side attr (the "+" lives INSIDE
the block now, not in a side gutter — drawer side is irrelevant).
Line visual:
- Removed transparency: `background: var(--_words-accent-solid)`
(was color-mix(... 35%, transparent)). Opacity 0.9 always when
shown (no opacity-0 default + hover transition).
- Removed box-shadow halo on drop-mode state (was the blur effect
the spec calls out). Drop mode still grows the line block-size
from 2px to 3px so the drop target is unambiguous.
Verification: DOM probe at /uix/components/words confirms:
- Hover middle of block: handle visible at lower-left
(top=666 vs rect.bottom=698, left=66 vs rect.left=62), inserter
hidden.
- Hover bottom of block: handle still at lower-left, inserter
visible spanning block width (left=block.left, width=block.width),
CSS centers the button horizontally.
- `npm run check`: 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
74e54384ea |
feat(words): R4A.1 — foundation V2 ops (types + normalize + setBlockVisual + 5 block ops) + 31 tests
R4A.1 opens the operations layer of the V2 engine. Pure functions only;
no provider coupling. Lives in `engine/operations-v2/`. The full V1
op surface (~50 functions, ~3000 lines) splits across multiple R4A.*
sprints — this one establishes the foundation + the V2-exclusive
visual operation that unlocks POLISH-1b in later sprints.
operations-v2/types.ts (~55 lines):
- WordsEditorStateV2 = { document, selection, activeMarks } over V2
shapes. Reuses V1's `WordsSelection` (path + offset) verbatim — the
selection model doesn't change between V1 and V2.
- WordsOperationResultV2 = { state, changed }. Same shape as V1's
result so future provider migration can plug in directly.
- noOp + changed helpers for guard-clause and result construction.
operations-v2/normalize.ts (~250 lines):
- normalizeDocumentV2(doc, opts?) — defensive shape repair after
every operation. Returns same document REFERENCE when nothing
changed (cheap structural compare via mutated flag).
- Autogen `id` for blocks/items/rows/cells without one (delegates
to defaultIdGenerator from migrate-v2 → crypto.randomUUID + fallback).
- Seeds empty containers: empty document → 1 paragraph; empty list
→ 1 item; empty table → 1×1 cell; empty inline children → 1
empty text.
- Coalesces adjacent text inlines with identical marks (V1 invariant
preserved — keeps the model from fragmenting after edits).
- Strips marks from code block children (code is plain text only).
operations-v2/visual.ts (~115 lines):
- setBlockVisual(state, blockIndex, patch) — V2-EXCLUSIVE.
- Shallow-merges patch into block.visual. Defends:
* blockIndex must be valid.
* Every patch key must be in WORDS_VISUAL_KEYS_PER_TYPE[block.type]
(P8 whitelist enforced at runtime; unknown keys silently dropped).
* Patch values of `undefined` REMOVE the corresponding visual
property from the merged result (clears via undefined).
* Empty visual is omitted from the block (no `visual: {}` stored).
* No-op result when patch matches existing visual (avoids
spurious re-renders).
- This is the operation POLISH-1b (image radius slider + shadow
toggles + border controls) will call.
operations-v2/block.ts (~140 lines):
- updateBlockAt(state, blockIndex, patch) — shallow-merges block
fields, refuses to change `type`.
- deleteBlockAt(state, blockIndex) — removes block; normalizer
seeds an empty paragraph if it was the last.
- insertBlockAt(state, blockIndex, block) — inserts at index (clamps
to bounds).
- moveBlockAt(state, blockIndex, direction) — swap with neighbor.
- duplicateBlockAt(state, blockIndex) — inserts a copy with a fresh
autogen id (drops the original's id before normalize so they're
distinguishable).
- moveBlockToAt(state, fromIndex, toIndex) — arbitrary repositioning
for drag-drop.
operations-v2/index.ts: barrel export of the public surface.
Tests (31): normalize (autogen ids preserved when present, empty
containers seeded for doc/list/table/inline children, text coalescing
with marks-aware preservation, code stripping, idempotence via same
reference). setBlockVisual (set/merge/clear/empty-elision/per-type
whitelist enforcement/shadow on image vs paragraph/no-op on match).
Block ops (updateBlockAt with type-change refusal + invalid index
no-op; deleteBlockAt with seed-on-empty; insertBlockAt with clamping;
moveBlockAt with edge no-op; duplicateBlockAt with fresh id;
moveBlockToAt with from-equals-to no-op).
Two TS errors landed mid-write (over-clever readonly modifier on
function param + structural cast across discriminated union) — both
fixed with simpler types. Verification: 257/257 engine tests pass
(85 V1 + 50 V2 validator + 35 V2 migrator/parity + 56 V2 render/
serializers + 31 V2 ops). `npm run check`: 0 errors.
Next: R4A.2 — text + selection operations (insertText, deleteRange,
insertParagraph, deleteBackward, deleteForward, toggleMark, etc.).
~2500 lines of V1 to mechanically translate. Or pause R4 entirely
and tackle the pragmatic shortcut (backport visual to V1 image to
ship POLISH-1b sooner).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
194ca208f9 |
feat(words): R3 — render-v2 + HTML + MD serializers + 56 tests
R3 closes the pure-V2 output path. Take a WordsDocumentV2 and turn it
into either an abstract render tree (for Eidos/React/Vue/vanilla to
walk), an HTML string with inline styles (roundtrip-friendly), or a
Markdown string (lossy by design — visual.* dropped).
engine/render-v2.ts (~370 lines):
- Pure renderWordsDocumentV2(doc) → readonly WordsRenderElementV2[].
- WordsRenderNodeV2 is a superset of V1's WordsRenderNode (adds 'hr',
'div', 'section' tags for divider / callout). V1 stays untouched.
- Per-block dispatch for all 9 V2 types (paragraph/heading/quote/code/
list/table/image/divider/callout). Same node shape as V1 — Svelte
walker doesn't need changes.
- visualToStyle(visual) helper exports the central translation from
BlockVisual props to CSS declarations. Numbers get 'px', hex strings
pass through. borderColor without borderWidth defaults to 1px solid.
- Mark wrapping: parametric color/background marks combine into one
<span style="...">; boolean marks stack via <code>/<span> with
data-words-mark attr + inline style fallback.
- Callout renders as <div role="note" data-words-callout-intent="X">
with optional title block + nested children.
engine/serialize-html-v2.ts (~120 lines):
- Pure serializeHtmlV2(doc, { pretty? }) → string.
- Walks renderWordsDocumentV2 output and emits escaped HTML.
- Properly escapes &, <, > in text; &, <, >, " in attribute values.
- Void elements (img/hr/br/input/meta/link) emit self-closing form.
- Pretty mode (opt-in) indents nested elements with 2 spaces.
- Roundtrip-friendly: inline style="..." preserves visual.* lossless.
engine/serialize-markdown-v2.ts (~190 lines):
- Pure serializeMarkdownV2(doc) → string.
- LOSSY by design (D-Q2): visual.* dropped entirely; color/background
marks dropped (no MD vocabulary); underline mark dropped.
- Preserves structural semantics: headings (# ## ###), quotes (>),
code blocks (``` with language fence), lists (- / 1. / - [x] - [ ]
GFM task lists), tables (GFM with :--- / :---: / ---: alignment),
images (), dividers (---), callouts (GFM
admonitions via INTENT_TO_GFM_ADMONITION mapping).
- INTENT_TO_GFM_ADMONITION exported: P7 boundary mapping —
neutral→NOTE, affirm→TIP, fulfill→IMPORTANT, risk→WARNING,
threat→CAUTION, loss→CAUTION.
- Mark order in output: bold(**) outer → italic(*) → strike(~~) →
code(`) inner.
- Cells escape | as \| to avoid GFM table parser confusion.
Tests (56 new):
- render-v2.test.ts (23 tests): basic blocks, visual sidecar with
exact style strings, table headerRow/headerCol semantics, marks
rendering, path encoding, visualToStyle direct.
- serialize-html-v2.test.ts (12 tests): paragraph, escape semantics,
void elements, inline styles, callout role attr, pretty mode,
table th/td.
- serialize-markdown-v2.test.ts (21 tests): every block type, marks,
visual.* dropped (lossy), callout intent → admonition tag mapping
(all 6 SemaIntent values), link with title, table cell | escaping.
Important deferral: POLISH-1b (image radius/shadow/border slider)
needs the provider to write block.visual.*. Provider is V1 internally.
Deferred to R5 (after R4 migrates provider to V2). Pure render path
is ready when provider catches up.
Verification: 226/226 engine tests pass (85 V1 + 50 V2 validator +
35 V2 migrator/parity + 56 V2 render/serializers). `npm run check`:
0 errors. V1 model + render path UNTOUCHED — V2 lives in parallel.
Next: R4 — provider V2 migration. Operations rewritten over
WordsBlockV2. Runtime imageStatus sidecar wired at mount. Public API
preserved via adapter during transition.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
92f0432eb0 |
docs(continue): R2 cerrado — migrator + parity check + 35 tests
|
4 months ago |
|
|
6c70699ff7 |
feat(words): R2 — V1→V2 migrator + sema parity check + 35 tests
R2 closes the engine refactor non-breaking phase. The V2 model from
R1 is now reachable from any V1 document via a pure migrator, and
the engine's local SemaIntent copy stays in lockstep with the
canonical $uix/intent.INTENTS via a runtime assertion.
engine/migrate-v2.ts (~370 lines):
- Pure function migrateV1ToV2(input, opts?) → { document, runtimeState,
warnings }.
- Per-block migration paths for paragraph / heading / quote / code /
list / table / image / divider / callout. Each rejects invalid
values defensively (e.g. invalid heading level coerced to 1 with
warning).
- P4 enforcement on image: drops `image.status` from the content,
moves it to `runtimeState.imageStatus: Map<blockId, status>` keyed
by an autogen id. Caller (provider) restores into in-memory
sidecar on load.
- P1 sweep on table: drops `striped` / `compact` (presentation
tokens) with warnings. Drops `cell.tone` (design-system token)
with warning. Renames list.children → list.items silently.
- Marks reshape: V1 template-literal `'color:#hex'` /
`'bgcolor:#hex'` → V2 structured `{type:'color'|'background',
value:'#hex'}`. Already-structured marks pass through unchanged.
- Code blocks narrow to text-only: flattens any link inside code
into its text content with warning.
- Idempotent on V2 input: a V2 document round-trips with zero
warnings and exact-equal output (validates with
validateWordsDocument).
- Default id generator uses crypto.randomUUID() when available, with
Math.random + monotonic counter fallback. Tests inject a
deterministic sequential generator.
engine/sema-parity.ts (~60 lines):
- assertSemaIntentParity(canonicalIntents) throws SemaIntentParityError
if the engine's local WORDS_EVAL_INTENTS drifts from the canonical
list. Error names both the missing and extra values + points to
the file to update.
- Caller passes canonical list (avoids cross-package import in this
module). Provider mount calls it with $uix/intent.INTENTS.
engine/migrate-v2.test.ts (35 tests):
- Entry-point shape handling (null / non-object / non-array children).
- Per-block migration (paragraph, heading with level clamp, quote
with cite, code with text-only narrowing + link flattening, list
with kind coercion and indent-0 omission, table with token drops,
image with src validation).
- P4 enforcement: image.status → runtimeState.imageStatus keyed by
autogen id; respects caller-provided id.
- Marks reshape: template-literal → structured; structured
pass-through; invalid hex rejected; unknown boolean dropped.
- Idempotence on V2 input (paragraph, callout, divider).
- Callout intent: all 6 canonical SemaIntent values accepted;
invalid (e.g. 'warning') coerced to 'neutral' with warning.
- Post-condition: migrator output ALWAYS passes
validateWordsDocument on any V1 input.
- Sema parity: 5 tests including the real golden cross-check
against $uix/intent.INTENTS.
Verification: 170/170 engine tests pass (85 V1 + 50 V2 validator
+ 35 V2 migrator/parity). `npm run check`: 0 errors.
Next: R3 — visual sidecar implementation. Render emits inline
styles from block.visual.*; POLISH-1b (image radius/shadow/border
controls) becomes implementable under this model.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
d18b32dd93 |
docs(continue): update with R1 V2 architecture status + roadmap
R1 closed → audit + types-v2 + validator + 50 tests. Doctrine signed (P1-P8 + D-SEM-1/2/3 + Q1-Q6). Roadmap R2-R5 listed for next sessions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
9b13e3cf23 |
feat(words): R1 — V2 architecture proposal + types-v2 + validator + 50 tests
Architectural refactor proposal landed for review. R1 closes the
audit + type design + validator phase. V1 stays untouched in parallel.
ARCHITECTURE_PROPOSAL.md (~500 lines):
- 8 principles signed (P1 content↔design strict, P2 raw values only,
P3 renderer translates, P4 runtime state out, P5 framework-neutral
engine, P6 single source of truth, P7 sema intents canonical for
evaluative vocabulary, P8 per-property entry rules).
- D-SEM-1/2/3 + Q1-Q6 all signed with recommendations accepted by
user. Notable: CalloutBlock.intent uses SemaIntent canonical
(neutral|affirm|fulfill|risk|threat|loss), NOT web admonition
vocabulary (warning|info|danger|etc.) — those live as UI labels +
Markdown serializer mappings ONLY.
engine/types-v2.ts (~320 lines):
- WordsDocumentV2 with version '2.0.0'.
- BlockBase with optional id (autogen at load) + optional visual
sidecar.
- BlockVisual with RAW values only (numbers in px, hex color
strings). Per-type Pick<> subsets: ParagraphVisual / HeadingVisual
/ QuoteVisual / CodeVisual / ListVisual / TableVisual / ImageVisual
/ DividerVisual / CalloutVisual.
- 9 block types: paragraph, heading, quote, code, list, table,
image PLUS new divider (semantic <hr>) and callout (with
WordsEvalIntent + nestable block children).
- Marks refactored from V1 template-literal `color:${string}` to
structured `{type:'color'|'background', value:'#hex'}`. Type-safe.
- Image upload status REMOVED from the content model (P4): the
WordsImageBlockV2 no longer has `status`; it goes to runtime
sidecar `provider.runtime.imageStatus: Map<blockId, status>`.
- Table V2 drops `striped` + `compact` (presentation tokens). Adds
`headerRow` + `headerCol` (semantic accessibility). Per-cell and
per-row `visual` allowed (background for zebra; cell padding).
- Cell `tone: 'muted'|'accent'` REMOVED (was a design system token).
Replace with cell.visual.background hex if user wants a tinted
cell.
- WordsEvalIntent declared locally in engine to keep engine
framework-neutral. Parity with $uix/sema asserted at boot
(TODO in R2).
engine/validate-v2.ts (~520 lines):
- Pure function validateWordsDocument(input) → ValidationResult.
- 11 error codes: invalid-version, invalid-block-type,
invalid-block-shape, invalid-children, invalid-visual-key,
invalid-visual-value, invalid-mark, invalid-href,
invalid-eval-intent, invalid-enum-value, invalid-number,
duplicate-id, invalid-text-content, invalid-link-children.
- Enforces P2: hex regex on color fields, finite numbers on
dimension fields, rejects tokens / classes / var() refs / non-
canonical mark shapes.
- Enforces P8: per-type Pick<> at runtime via
WORDS_VISUAL_KEYS_PER_TYPE whitelist.
- Enforces P7: callout.intent must be one of the 6 canonical sema
intents; rejects web vocabulary (`warning`/`info`/etc.).
- Each error carries a JSON-pointer path (e.g.
/children/1/visual/background).
engine/validate-v2.test.ts (50 tests):
- Top-level shape, block type registry, marks (boolean + structured
+ V1 reject + token reject), visual properties (allowed +
rejected tokens/classes/var()/non-numeric + per-type Pick<>),
id uniqueness, heading levels, list kind + indent, code text-only
children, image src + width, callout SemaIntent + reject of web
vocabulary + nested blocks, divider, quote.cite URL parseability,
link nesting + URL parseability, table headerRow/headerCol +
per-cell visual whitelist + per-row visual.
Verification: 135/135 engine tests pass (85 V1 + 50 V2). V1
untouched. npm run check: 0 errors, 26 warnings (pre-existing).
Next: R2 — migrator V1→V2, runtime image-status sidecar, refactor
provider to consume V2 internally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
d126e16e65 |
fix(words): silence canvas truly, drag for list/table, no overlay hovers + day-end docs
EV-G final pass + EOD documentation. Words editor: - Stop emitting `contact-focus` from soma. Removing the per-component cascade rule earlier didn't fully silence the editor — the `contact` family carries a BASE sound signature in SEMA_MAP (pitch 800, gain 0.25) that fires even without a cascade rule. The trigger call is commented out (event no longer emitted; telemetry empty). Per spec: "sigue apareciendo el puto sonido, el ring". - Drag handle + inserter now cover lists AND tables. Selectors expanded from `[data-words-node="block"][data-words-path]` to also match `[data-words-node="list"]` and `[data-words-node="table"]`. Top-level lists / tables get the grip + the "+" seam under them. - Removed :hover from `[data-words-block-handle]` and `[data-words-block-inserter-button]`. Gutter overlays are AMBIENT — visual state via data-attrs only (`data-open`, `data-grabbed`, `data-dragging`). Per spec: "quítale el hover al botón de añadir bloque, si el handle no tiene este tampoco". - Rail tokens hoisted to recipe: `words.rail-bg = #d4d4d4`, `words.rail-border = #9a9a9a`. Fixed-tone (not theme-aware) — the rail emulates the physical margin of a notebook; same look in light/dark. Recipe-css-contract test was rejecting raw hex in words.css. Docs: - CLAUDE.md hand-off 2026-05-28 #2 documenting the 6 EV-* Words sprints + 5 architectural decisions with lasting effect (canvas silent, fixed-tone tokens for physical metaphors, drag predicate covers list/table, ambient overlays, insertParagraph empty-block guard). - continue.md (CONTINUE.md) rewritten for tomorrow's pickup: TL;DR of the two halves (toggle-group structural + Words UX), remaining bugs (drag handle outside rail on code blocks, heading inline level change), and the heredados (POLISH-1b, F2.3-F2.6, DRAGDROP-SOMA). Verification: 467/467 morfo + sema + eidos + soma/words tests pass. 85/85 engine pass. `npm run check`: 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
125c62cc20 |
fix(words): silence canvas, silver rail, direct undo/redo, popover widths, Enter empty
Six user-reported issues in one pass.
EV-F1 — canvas silence
Remove the contact-focus entry from the sema cascade. The event still
fires (telemetry / a11y can read the data-event* stamps) but no sound
/ haptic plays. Per spec: "no puede emitir eventos es demencial, una
vez que está trabajando con el no tiene sentido". The editor surface
is now perceptually silent on focus; only explicit commits (save,
format, set-link, etc.) make noise.
EV-F2 — rail visual
`--_words-rail-bg` becomes flat silver `#d4d4d4` and the dot
pattern is gone. New `--_words-rail-border` (`#9a9a9a`) lays a
1px hairline on the rail's inline-end so it reads as a separate
gutter ("paper edge" feel). Fixed-tone (not theme-aware) — the rail
is meant to look like the physical margin of a notebook regardless
of the editor theme. Left-side drawer flips the border to inline-
start.
EV-F3 — direct undo/redo in toolbar
Demo presets + custom config now use `['undo', 'redo', ...]` as
direct command strings instead of `['history-menu', ...]` (which
opened a popover to pick from two items — wasted real estate +
two clicks). `link-menu` and `tools-menu` stay as popovers because
they hold 2-3 items each. `insert-menu` is OUT — block construction
lives in the drawer's empty-state Insert panel and the slash menu.
`demoToolbarFamilyItems.insert` collapsed to `[]` accordingly.
EV-F4 — popover widths
Base `[data-words-toolbar-family-panel]` width 12.5rem → 16rem so
content fits without horizontal scroll. Per-family overrides:
tools → 17rem (descriptive labels), link → 20rem (URL input +
button grid). The default panel CSS no longer carries a max-block-
size + scroll — a vertical scroll inside these popovers is a
misdesign signal; size the panel, don't scroll it.
EV-F5 — link popover (covered by EV-F4)
The vertical scroll the user saw was a side-effect of the
12.5rem width forcing the link-editor input + button to wrap, then
the panel running out of vertical room. The new 20rem width
restores the natural one-row layout and no scroll appears.
EV-F6 — Enter on empty block no longer multiplies
`insertParagraph` engine op now guards the default case
(paragraph/heading): if the block is collapsed AND empty (children
length 0, both halves of split empty), Enter on a paragraph is a
no-op and Enter on a heading demotes it to a paragraph (canonical
Notion / Google Docs UX). For non-empty paragraphs/headings the
existing split-and-create-paragraph behavior stands.
Verification: 467/467 tests pass across morfo + sema + eidos +
soma/components/words. 85/85 engine tests pass (insertParagraph
guard non-regressive on existing fixtures). `npm run check` still 0
errors. DOM probe at /uix/components/words confirms toolbar = 5
items (undo, redo, Links, Tools, Find/replace), rail bg
`rgb(212,212,212)` + 1px `rgb(154,154,154)` right border, link
popover 320px wide with no scroll.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
ce95473261 |
feat(words): internal scroll for the content area + capture-phase scroll sync
The editor body was unbounded vertically — long documents pushed the
whole page instead of confining the scroll to the editor surface.
Per spec: "la parte de la edicion deberia tener un scroll".
Recipe:
- Three new tokens `content-max-block-size-sm/md/lg` (50vh / 60vh /
70vh) mirror the existing `content-min-block-size-*`. Generated CSS
regenerated.
CSS:
- `[data-words-content]` now carries `max-block-size:
var(--_words-content-max-block-size)` and `overflow-y: auto`. The
min-block-size baseline stays as the starting height for empty
editors. When content grows past the cap, the body scrolls
internally — the host card no longer balloons.
- `scroll-padding-block: 1rem` hint so caret-into-view nudges leave
breathing room.
Block-handle / block-inserter / image-float-bar:
- Switch scroll listeners from `window.addEventListener('scroll')` to
`document.addEventListener('scroll', { capture: true })`. Scroll
events don't bubble; the previous wiring only caught window scrolls.
Now the gutter overlays (handle position, "+" snap target, image
float bar anchor) re-measure when the user scrolls INSIDE the
content area too.
Verification: dev probe at /uix/components/words confirms
`max-block-size: 600px`, `overflow-y: auto`, content scrollHeight
(615px) > clientHeight (600px), `isInternallyScrollable: true` on a
1000px viewport (60vh of 1000 = 600). 467/467 tests pass across
eidos + morfo + sema + soma/components/words.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
4 months ago |