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 }
174 Commits (50d56059d2f734d3e54d661b2fa2d2e509d85e95)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
50d56059d2 |
fix(card): motion prop (state-domain pilot) + nested-interactive guard
- `motion` prop: the Card is the state-domain pilot. Becoming `selected` plays the preset once via `data-motion-state` (mount-guarded — animates on the transition, not on mount/deselect). `card-emerge` yields to the motion system when `motion` is set (otherwise toggling `data-motion-state` re-triggers the mount fade). - Nested-interactive guard: clicking a control inside an interactive Card (a Footer button, a link) must not select the Card. `handleActivate`/`handleKeydown` bail when the event originates from an interactive descendant, and the `:active` press-scale is suppressed via `:not(:has(button:active, …))` — the press cascade reaches the ancestor `<button>`, which `stopPropagation` can't touch. - Demo: `motion` chip control on the live Card. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
b2b1f4ffcf |
demo(popover): showcase the `spring-pop` JS spring tier via a `motion` control
Add a `motion` chip-picker (slide-fade · scale-fade · fade · spring-pop,
default spring-pop) on the live `Popover.Content`, plus the prop in the
generated code snippet. Demonstrates the spring engine tier woken in
|
4 months ago |
|
|
54268717b4 |
feat(motion): `<Motion>` wrapper — content animate-in/out primitive, (b) step 3
The ergonomic companion to `motionAttrs`: wrap any content to animate it IN and
OUT. `<Motion motion="scale-fade">…</Motion>` plays the preset's enter on mount
and its exit on removal (Svelte `out:` flips to `data-state='closed'` + retains
the node for the eidos-declared duration, like `<Cascade.Item>`). Rides the
existing state-presets — no new mechanism, no JS engine.
- morfo `motion.ts` (scope ['eidos']): one Provider part declaring `data-state`.
- eidos `motion/`: component (spreads `motionAttrs` + `out:` retention) + types +
index + README (incl. the `<Motion>` vs `motionAttrs` table + the content-domain
boundary).
- badge demo: a `<Motion>` show/hide example next to the prop showcase.
Caught + fixed a real collision: `data-motion` is already the reduced-motion prefs
attr (`[data-motion='reduce']`), so the wrapper emits NO part-presence marker (a
prefs `closest('[data-motion]')` would have matched it) — only the functional
`data-animation-style` + `data-state` from `motionAttrs`.
Verified at runtime: enter (data-state=open → `scale-in, fade-in`, no data-motion)
and exit (Hide → retained with data-state=closed → `scale-out, fade-out`). `check`
clean for the morfo + component + demo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
0a6d112cde |
feat(motion): pilot the universal `motion` prop on a real component (Badge) — (b) step 2
First real consumer of `motionAttrs`: `<Badge>` gains a `motion` prop so a
designer can apply any registered preset as the badge's entrance — `<Badge
motion="scale-fade">` plays the preset's enter on mount, zero per-component
machinery (the component just spreads `{...motionAttrs(motion)}`).
Badge is the clean content-domain pilot: pure eidos primitive, no morfo events,
no own `data-state` (so the helper's presentation `data-state="open"` doesn't
clash — unlike Card, which has selected/idle + a built-in emerge). Additive +
non-breaking: `motion` defaults to undefined (no animation).
- badge/types.ts: `motion?: MotionPresetName`.
- badge/badge.svelte: spread `motionAttrs(motion)` on the span.
- badge demo: a "Motion" showcase (fade / scale-fade / slide-fade + Replay) —
the prop demonstrated per the testbed doctrine.
Verified at runtime: the three motion badges render `data-animation-style` +
`data-state="open"` with the right enter `animation-name` (fade-in / scale-in,
fade-in / slide-from-bottom, scale-in, fade-in). `check` clean for badge. (A
fresh badge screenshot was blocked by preview-infra flakiness this round; the
enter keyframes themselves were confirmed visually earlier via the cascade.)
Establishes the pattern: any component becomes animation-selectable by accepting
`motion` + spreading the helper. RFC §D.12 (b).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
7d31cd3fd7 |
feat(eidos): dialog/drawer modal-plane + h3 title role + framework-Button trigger/close + density testbed
- dialog + drawer adopt data-depth="modal" (surface·border·base-shadow from the plane); recipes pruned of orphan content-bg/border/shadow. The bundle's on-surface font fixes the Times New Roman of portaled content (Decisión 8). - Dialog.Close inherits the dialog size via context.ts, capped at md (sm→sm, md/lg/xl/full→md; full is layout, not a bigger control — Radix/Mantine/MD3). THEMING §5 container→part norm. - Dialog title → h3 typography role (--dialog-title-* points at --style-h3-*, override knob kept). - Dialog trigger + close compose the framework <Button> via the asChild `child` snippet (the trigger was a passthrough that rendered soma's native <button>); variant/color/size defer to the canonical Button defaults (solid/primary) — no hardcoded ghost/neutral. - demo layout (web/routes/uix/+layout@.svelte): "Display" control — density·scaling·border-width applied to :root (reaches portaled overlays) to observe the interrelated theming axes live. - update the depth-plane test (z is token-only; on-surface font) + the archetype audit hand-off. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
b8637976a0 |
feat(uix): elevation archetype (A1) + coherence audit + popover/dialog/option-list polish
Coherence audit (ARCHETYPE_COHERENCE_AUDIT_2026-06-19): 8 archetypes + 8 closed decisions, the §13 construction contract, per-component deviation matrix (88/117 deviate). Master guides (soma/COMPONENT_GUIDE, eidos/components/README) point to it. A1 — elevation bundle via `data-depth`: the plane paints surface·border·shadow·halo (added `border` to the depth bundle). z-index is NOT painted — stacking is a positioning concern owned by the floating-wrapper. Popover migrated to the overlay plane (panel + arrow consume it); recipe orphans pruned. Popover fixes: arrow un-clipped via an inner [data-popover-viewport] (Radix-style; the absolute arrow escapes the scroll clip), sized via density tokens (not soma's hardcoded 10×5), border-seam overlap keyed on data-side, hover-bounce guard in scheduleHoverOpen (ignore hover-open when already open). Dialog.Close composes IconButton/Button (neutral) — closes the Lote ② debt. Option lists: density-driven padding/radius via --space-* + concentric radius, ScrollArea adoption, emerge open/close events (combobox/select/command), and the canonical option-highlight in archetypes.css. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
e16c169f37 |
feat(eidos): color-field leading value swatch + full-width coherence
- <ColorField.Input swatch> renders an opt-in leading colour chip showing
the field's current value, sized to the control icon (--icon-size-{size})
via a concrete token — never 1em, so it cannot recompute/jitter.
- field-segment-state: drop the active-segment font-weight 600; bold widened
the ch-based segment ~0.5px, resizing the highlight on every increment. The
accent background already marks the active segment.
- color-field stays full-width like the whole field/select family — width is
container-driven, not field-driven (no per-component exception).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
8f84b36f6d |
feat(eidos): field-family font-scaling coherence (input 1:1 + label step-down)
- Field font scale: input 1:1 (size N -> --font-size-N, md now 16px), label one step below (10/12/14/16/18). Recipes: field, spin-field, date/time/color-field, search-field, password-field, select, editable, tags-input. Segmented-field labels via CSS calc; generic Field label via label-font-size tokens. - Picker triggers (date/time/range): box now scales off the local --_X-field-height + font:inherit so the 1em icon follows the field font; added the missing xs size variant to time-picker + time-range-picker. - color-field/time-field commit-set fires + sounds on increment (compare by ColorValue.hex; time-field passes fallbackTarget for the double-registered 'input' part). - color-field format-select: reverted the segment-active tint back to its focus ring. - Docs: THEMING.md field font rule; COMPONENT_COHERENCE_AUDIT.md. words / palabras / chronos untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
6b29e7dc44 |
feat(eidos): tie icon scale to type scale + button icons on tokens
Icon scale large steps now track the type scale (validated by eye in a study harness at /uix/icon-scale-study): --icon-size-xl 24→32, --icon-size-xxl 48→52, so the icon keeps its optical weight beside its paired text. 0 impact — nothing consumed those steps yet. button: icon-size per size references var(--icon-size-X) (mapped by value → same rendered px, now density-scalable) instead of hardcoded px. Docs: THEMING.md §5 documents the type↔icon correlation; CONTINUE-icon-type-scale.md captures the pending 1:1 work (size = font = icon per name, dropping the font step-down) for the next session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
4 months ago |
|
|
e5303f8217 |
fix(eidos): button tonal variants render bg/border + variants×colors gallery
The recipe defined per-color tonal tokens inconsistently — only fulfill/loss had
`track`/`border`, so soft/surface/outline rendered with no background or border
for the other 6 colors (primary/secondary/neutral/affirm/risk/threat):
`palette-track`/`palette-border` referenced undefined `--button-{color}-track`/
`-border`, which collapsed the background/border declarations. Added the missing
track+border to all 6 (mapped to `var(--color-{role}-track|border)` like
fulfill/loss); the recipe is now uniform (7 slots per color). Verified all 8
colors × soft/surface/outline in dark + light.
Demo: new "Gallery" tab — a 6 variants × 8 colors matrix of real <Button>s
(hierarchy via `color`, evaluative palette via `intent`).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
4 months ago |
|
|
e7a3d9085b |
fix(eidos): QrCode logo visibility + demo content presets
The logo overlay rendered invisible. Inside a viewBox-scaled foreignObject a fixed px padding (var(--space-1)) equals that many *modules*, overflowing the cleared centre and collapsing the content box; inline-size/block-size also don't size an <svg> element. Fix: proportional inline padding + physical width/height. The logo now renders and still decodes at ECC H. Dropped the now-orphan --qr-code-overlay-padding token + regenerated base.css. Demo: content presets (URL / vCard / WiFi / unicode) prove the encoder takes any UTF-8 string, not just URLs (each verified decoding via jsQR); logo on by default; quiet-zone chips fixed to canonical [4, 8, 16] (was [2, …], below the spec's 4-module minimum). README documents the quiet zone as modules, not pixels — the canonical unit; for px layout spacing, pad the container in CSS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
4 months ago |
|
|
e0579f7e24 |
feat(eidos): add QrCode — themeable QR with own zero-dep encoder
Own QR encoder in $libs/qr (ISO/IEC 18004: segment analysis → Reed-Solomon over GF(256) → matrix layout → 8-mask penalty selection), no npm dependency. Eidos <QrCode> renders the module matrix as one SVG path with square/rounded/dots cell shapes; function patterns stay square (via the encoder's `reserved` mask) so every shape scans. Optional logo clears the centre + auto-boosts ECC to H. <QrCode.DownloadTrigger> exports PNG/SVG, baking resolved colours into the clone first (a serialized SVG loses the document's CSS custom properties). Fixed-tone default colours for scannability; themeable via color/background. morfo qr-code (scope: eidos; parts Provider/Pattern/Overlay/ DownloadTrigger) + 'path' added to MorfoElement; recipe with 11 --qr-code-* tokens; canonical demo + nav entry. 11 encoder unit tests; verified end-to-end via jsQR round-trip + browser. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
4 months ago |
|
|
4add3ed58f |
feat(uix): RadioCards orientation as ResponsiveProp + first responsive-prop example
orientation is now breakpoint-aware like size: `orientation={{ base:'vertical',
md:'horizontal' }}` resolves via Eidos.resolve and forwards a concrete value to
the soma (keyboard nav handles both axes). Documented in the README (new
"Responsive orientation" section + example) and the demo (a "responsive" chip +
note + API row) — the first consumer-facing example of the ResponsiveProp object
syntax in the repo. Browser-verified: flips at md (768px). check 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
4 months ago |
|
|
f59396b51a |
feat(uix): RadioCards — card-style single-select reusing RadioGroup
New eidos component built ON TOP of RadioGroup with zero duplicated behaviour. RadioCards reuses RadioGroup's soma wholesale (selection, roving focus, role=radio, native <form> via HiddenInput) and adds only the card chrome: icon · title · description + an always-present corner radio. Vertical, horizontal and grid layouts. - morfo: scope:['eidos'], thin contract for presentation parts only (Provider marker, Item-card, Indicator, Icon, Content, Title, Description). The interactive contract + commit-select stay in radio-group, reused via its soma. No new soma, no new sema. - eidos: `radio-cards` recipe (card chrome) + wrappers + radio-cards.css. radio-group.css is NOT loaded here, only its foundation tokens — the card owns its visual, reusing --_radio-group-palette-* (inlined, since a :root token referencing the palette is invalid-at-computed-value). - demo: canonical structure (always-on stage + trace, tabs, chip/switch controls, Untitled-UI example patterns) + nav entry + README. check 0 · eidos-lint 0 invalid. Browser-verified: select (click + keyboard roving), disabled, the three layouts, controls drive the preview. Follow-up: icon/circle sizes are still px (mirroring radio-group's own size tokens) — making the control-size scale density-aware is a separate, family-wide change deferred per request. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
4 months ago |
|
|
3c059ff087 |
refactor(eidos): share number-field + css-field visual via spin-field
NumberField and CssField are the same visual (a bordered field + input +
increment/decrement triggers + scrubber, split/stacked layouts, sizes/
variants/colors, themeable glyphs); only their value model differs. They
were two cloned recipes + CSS that drifted — a refinement to one (square
flush buttons, divider, contrast) didn't reach the other.
Unify into ONE shared source (the toggle-group structural-identity pattern):
- New eidos/components/spin-field: recipe key `spin-field` (`--spin-field-*`)
+ spin-field.css with all the stepper-field rules, selecting `[data-spin-
field*]`. Loaded via the foundation @import in index.css.
- number-field + css-field morfos declare structural identity (`data-spin-
field*` presence attrs on each part). The Provider emits them via syncAttrs;
the sub-parts emit them in their soma `props` getter (number-field's soma
hardcodes sub-part attrs rather than syncing the morfo).
- Removed the `number-field` / `css-field` recipe keys; their CSS files are
now stubs. A theme tints one component by scoping `[data-number-field] {
--spin-field-… }`.
- css-field thereby adopts number-field's refined steppers (square, flush,
divider) — the drift fix the user asked for, now structural (no clone).
Verified bit-for-bit in browser: number-field identical to baseline (split
flush, stacked symmetric xs..xl, RTL, glyph token/children override);
css-field now square/flush/divider. eidos-lint invalid 0; recipe contract
passes (no orphans, loads-once); check + morfo:check clean for these.
Docs: THEMING sections 33 (glyph tokens now `--spin-field-*`) + 34 (the
shared layer); number-field / css-field READMEs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
4 months ago |
|
|
bd9d924577 |
feat(eidos): themeable stepper glyphs + css-field compact steppers
NumberField and CssField steppers gain a `stacked` compact layout and themeable default glyphs. - Glyphs render from recipe tokens via `:empty::before` (split plus/minus, stacked up/down triangles); passing children overrides per instance, a theme overrides the token. 4 tokens per component, unicode-escaped. - CssField gains the `steppers` prop (split | stacked); its stacked layout mirrors NumberField (self-contained — the split layout left untouched). - NumberField split: square buttons flush to the field edge with an inner divider and higher hover/focus contrast (control-radius 0). - Stacked rows use minmax(0,1fr) + a height-proportional glyph so the two arrows stay symmetric and uncut at xs. - Demos: steppers controls + stepper-layout / sizes / themed-glyph showcases; code snippets use the themed defaults. - Docs: THEMING section 33, number-field and css-field READMEs. Note: also sweeps in the pre-existing palabras WIP already in the tree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
4 months ago |
|
|
7da7285d94 |
feat(uix): cropper, image-picker, image-adjustments + reusable zoom-pan layer
Three new components across all 4 layers (morfo/soma/sema/eidos) + demos + langs + READMEs: - ImageAdjustments — reusable image-filter sliders subcomponent. - ImagePicker — composes file-upload + image + image-adjustments (fill modes, rotation). - Cropper — Ark image-cropper anatomy: normalized 0-1 crop rect, rect + round shapes, 8 resize handles, fixedSize move-only (avatar), canvas Blob output (5-arg drawImage for SVG safety), wheel/pan zoom, two-zone gesture (selection moves the area, the background pans to set focus; img draggable=false guards native DnD per zag). New reusable soma/layers/zoom-pan: cursor-centered zoom, clamped pan, transform + viewport-frac-to-content-frac mapping. Image-viewer / diagram-pan / map can reuse it. sema: add 'zoom' verb to the handle family (direct manipulation, documented extension). slider: track visible at zero (gray, not white), thumb lifts on drag. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
4 months ago |
|
|
b3ef470646 |
feat(uix): FloatPanel — draggable/resizable floating panel
New general-purpose overlay component across all 4 layers (morfo/soma/sema/eidos) plus a shared stacking layer and i18n catalog. - Drag (2D free, anchored→free) + resize with device-pixel-snapped, GPU-composited 1:1 movement; elevation lifts on pointerdown for the pickup metaphor. - Stages: minimize / maximize / restore (StageTrigger, computed maximized geometry so restore needs no saved-rect bookkeeping). - Composable header buttons: Controls + Action (custom) + Close + StageTrigger. - Keyboard a11y (the industry gap): grab-mode move/resize on the focusable header and a se-corner ResizeGrip (pointer + keyboard), with debounced live-region announcements, bounds/min-max cues, aria-roledescription/keyshortcuts. - Stacking registry (bring-to-front/topmost), Presence motion preset (scale-fade), es/en langs catalog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
4 months ago |
|
|
7ff1738a12 |
feat(uix): add CssField — validated CSS-value spinbutton + sium CSS helpers
New 4-layer UIX component for CSS dimension values (px/rem/%/vh/…) modeled on number-field: allowedUnits whitelist, optional increment/decrement triggers, pointer scrubber, keyboard stepping, wheel, Field integration. Validation is ambivalent — built-in pure sium functions by default, or delegate to a sium schema via the validate prop (untilFix feedback on invalid units). sium: parseCssValue/formatCssValue/isCssValue/cssValue + CSS unit vocabulary in arts/sium/types/css.ts; css_value issue code + es/en messages; tests. morfo/soma/sema/eidos: css-field across all layers; recipe tokens + interactive demo + sidebar link + READMEs. Verified: check 0 errors; sium 395, soma css-field 9, morfo+sema 214 tests; morfo:check + eidos-lint clean; browser render/behavior/sema-trace confirmed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
4 months ago |
|
|
d68d2c4513 |
palabras: grip+drag+inspector y fixes de borrado de bloque
Estado funcional del editor palabras (verificado en navegador) como punto de retorno antes de revertir el piloto del wrapper. - Grip: framework <Button> draggable copiado del gutter de Words (ghost/xs/iconOnly + dots svg). dragstart escribe el indice en el MIME canonico; click abre el inspector via onPress; cursor grab/grabbing. - Drag vertical: <WordsBlockDrag> acepta `dropZone` opcional y escucha dragover/drop en toda la fila (rail+canvas) en vez de solo en content, asi un arrastre recto hacia abajo sobre el carril ya engancha. - Borrado de bloque (palabras-panel): mueve el activo al bloque superviviente leyendo el documento YA actualizado (cubre el caso del ultimo bloque, que deja un parrafo en blanco), cierra el popup, y el grip se re-ancla en vez de quedar fantasma. - active-eidos: getter `timers` (ActiveTimers) sobre el uix service. - Piloto wrapper: paragraphSpec.render emite <div data-words-node=block> envolviendo un <p data-words-inner> (deja tests del render del motor por consolidar/revertir). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
4 months ago |
|
|
27c8214d98 |
feat(demos): expose the `shape` control in Badge/Button/Card/TagsInput demos
The shape prop was usable but invisible — add a live `shape` chip control (rounded/continuous/cut/scoop) to each demo, wired to the live preview + code snippet, plus a "shape families" patterns row in Badge and a tag-shape control on TagsInput items. The family system is now discoverable, not just documented. Verified live (Chrome 146): badge cut -> bevel, button continuous -> squircle, both flow from the control to the rendered component. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
f8056f438b |
fix(demos): eliminate each_key_duplicate crash in 22 component demos
The event-trace inspector in each demo keyed its `{#each}` by a
`Date.now()` timestamp (`entry.at` / `h.at`). A single interaction can
stamp `data-event` on several elements within the same millisecond, so
two trace entries share the key and Svelte throws `each_key_duplicate`
(reported on dropdown-menu).
The trace is an ephemeral, 3-item, text-only log with no transitions or
stateful children, so positional reconciliation is correct: drop the
timestamp key (the each becomes unkeyed). The `fmtTime(...at)` display is
left intact.
Swept all 22 demos carrying the pattern (21 keyed by `entry.at` + announce
by `h.at`): dropdown-menu, context-menu, menubar, navigation-menu, listbox,
grid-list, table, tree-view, tree-grid, feed, command, carousel, drag-drop,
clipboard, announce, alert-dialog, button, color-field, link-preview,
range-calendar, time-field, time-range-field.
Verified: `as \w+ (\w+.at)` -> 0 occurrences site-wide; `npm run check`
0 errors; browser repro on dropdown-menu + table (burst of events, no
each_key_duplicate, console clean).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
e97fdc53c2 |
fix(demos): drop fake soma snippet from 26 eidos-only component demos
Eidos-native components have NO soma layer, yet each demo rendered a
misleading "soma" code-snippet block ("n/a · eidos-native — equivalent
markup shown") fed by an orphan `somaSnippet` derived. Sweep the fix
already verified on text/heading across all 26:
layout primitives box flex grid stack container auto-grid wrap group
section aspect-ratio float
typography/inline code code-block kbd mark highlight link badge separator
visual leaves avatar banner skeleton spinner icon display scroll-frames
For each: remove the `somaSnippet` declaration + the soma `data-uix-code`
block, leaving only the real eidos snippet (and dropping its now-unneeded
inline margin-top).
Skipped (correctly): card / avatar-group / image (soma mentioned only in
prose, no fake code block — image reads a real ImageProvider) and all
genuinely soma-backed components.
Verified: `npm run check` 0 errors; 0 orphan somaSnippet refs in swept
files; browser spot-check (box, scroll-frames) shows one eidos code
block, zero soma badges.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
8e162221c3 |
fix(demos): typography demos fill live width + drop fake soma snippet
Text and Heading are eidos-native (no soma layer), but their demos:
1. wrapped the live preview in a content-sized box (no inline-size),
so `align` had no room to render inside the centered stage area;
2. rendered a "soma" code-snippet block ("n/a · eidos-native") with
non-real equivalent markup, muddying the layer story.
Fix both: the live wrapper now `inline-size: 100%` (fills up to its
max-inline-size cap, 36/38rem) so alignment is visible; and the soma
code block + its now-orphan `somaSnippet` derived are removed, leaving
only the real eidos snippet.
Verified in browser: text box 576px / heading box 608px (filling the
918px stage); align=end renders; one eidos code block, zero soma badges.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
29db54f49b |
feat(scroll-frames): video init-seek + start/end segment scrubbing
The video mode now paints a frame immediately: on `loadedmetadata` it seeks to the initial scroll position instead of waiting for the first scroll (the progress-0 loop guard used to leave some browsers black). New `start` / `end` props (seconds, clamped to the real bounds) scrub only a SEGMENT of a clip — progress 0->1 maps to `[start, end]`, `end` defaults to the full duration. Time-based seek means frame rate is irrelevant and duration stays browser-authoritative (read from `loadedmetadata`, never passed by hand). Demo: same-origin `/demos/video.mp4` sample (external URLs fail on cross-origin range requests), live `start` / `end` / `smooth` controls, API rows + snippet parity, README segment example. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
ba74b6b416 |
feat(scroll-frames): adapt to framework + demo; add genie-class motion presets
ScrollFrames (eidos) rebuilt as a first-class component -- it was draft WIP using
bare native APIs.
- No native globals: window scroll/resize -> dom.listen + dom.observeResize;
requestAnimationFrame -> dom.raf (disposer); window.innerHeight / devicePixelRatio
-> dom.getWindow(node); new Image() -> dom.getDocument().createElement('img').
- lang="ts" + typed props (types.ts) + index.ts + data-attrs + recipe CSS (no
scoped <style>), registered in index.css.
- New `root` prop: drive progress from a scroll container instead of the window
(makes it embeddable + demoable).
- Morfo (morfo/components/scroll-frames.ts): scope ['eidos'], 4 parts, 0 events.
- Demo (uix/components/scroll-frames): locked 6-tab template, self-contained
generated frames (canvas -> data URL), scrollable stage via `root`. + README.
- Fixes the 12 errors the earlier draft pushed (branch back to the 1 pre-existing
grafito error).
Motion demo (temas/animations): "genie y cia" section -- 4 expressive JS presets
played via eidos.motion.enter: genie (waapi warp), flip-3d (rotateY), swing
(pendulum), elastic (overshoot spring).
Verify: npm run check -> 1 pre-existing error (grafito), 0 new; recipe-css +
component-api contracts clean (3 pre-existing words failures); morfo:check
scroll-frames PASS (1/108 fail = pre-existing words).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
fff17c58dc |
words: add the editor menu bar (formatting toolbar + Edit/Preview + inspector)
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>
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
c4be6dbe1b |
fix(words): silence body event spam + slim top toolbar + reposition handle/inserter
Four user-requested fixes to the Words editor surface — the body should not emit perceptual cues during editing, the top toolbar must not duplicate the drawer, and the gutter widgets need clearer anchor points. EV-A (events spam): - Move `contact-focus` event target from `content` to `provider` in the morfo + sema cascade selector. Focus is a Words-component-level signal; firing it from the content body conflates "user entered the editor" with "user is editing the body". The wasFocused guard in soma already throttles to one emit per real focus transition. - Expand `isInsideWordsTool` selector list to cover the four companion overlays added after the original list was written: `data-words-drawer`, `data-words-block-handle`, `data-words-block-handle-menu`, `data-words-block-inserter`, `data-words-image-float-bar`. Before this fix every click on a companion overlay fired commit-save-content + contact-focus on the blur/refocus cycle (two perceptual cues per tool interaction). EV-B (toolbar trim): - Demo's full / formatting / minimal presets + custom config now host only GLOBAL actions: history (undo/redo), insert (creates new blocks), link (selection-bound flow), tools (find/replace + clear), find-replace in its own group. text-menu / block-menu / align-menu / list-menu / table-menu moved out because the drawer already owns contextual formatting per the HIER-2 split. EV-C (block-handle drag UX): - Drop the `setDragImage(hoverBlockEl)` call. The browser now uses its default snapshot (the grip button itself) as the drag ghost — the ghost travels with the cursor while the bar in the gutter stays fixed as a visual anchor. New `data-dragging` attr + `[data-words-block-handle][data-dragging]` CSS rule fades the static anchor to 0.35 opacity so it reads as "drag origin" while the ghost is the moving part. EV-D (inserter at block bottom): - Seam positions are now pinned to the BOTTOM EDGE of the preceding block (`a.bottom`) instead of the midpoint of the gap between two blocks. The "+" reads as "insert AFTER this block" anchored to that block's lower edge, per spec — el botón de añadir bloque debe aparecer en el límite inferior del área en relación al bloque. - Tighten the "cursor inside block band" check to a half-open interval `[top, bottom)` so the exact bottom-edge pixel belongs to the seam below (otherwise the seam at `y === bottom` is shadowed by the block and the inserter never snaps). Verification: dev server, /uix/components/words, DOM probe confirms handle centered in the rail column (left=48 inside the 32-wide rail starting at ~46), inserter snaps to block 1's bottom (`top=569.94px` when block 1 bottom = 570px). 366/366 tests pass in morfo + sema + soma/components/words. `npm run check` still 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
4cd54e2111 |
feat(words): drawer `side` prop (left | right) + overlays auto-mirror
The companion drawer was hard-coded to the right column. Now
`<Words.Drawer side="left">` flips the editor grid so the drawer
takes the start column; the floating overlays (block handle grip,
block inserter "+") automatically render on the OPPOSITE margin so
they don't crash with the drawer's gutter. Default stays `'right'`
so existing consumers see no change.
Wiring:
- Eidos `<Words.Drawer>` gains `side?: 'left' | 'right'` (default
'right'). The value is passthrough'd as `data-side={side}` on the
rendered drawer root so CSS + sibling overlays can read it from
the DOM.
- `words.css`:
- `[data-words]:has([data-words-drawer][data-side='left'])`
flips `grid-template-columns` + `grid-template-areas` so the
drawer column sits at the inline-start.
- `[data-words-drawer][data-side='left']` swaps the visual
seam from `border-inline-start` to `border-inline-end`.
- `[data-words-block-inserter][data-side='left']
[data-words-block-inserter-button]` overrides `left: -28px`
to `right: -28px` so the "+" lands on the block's right
margin.
- Block-handle (`words-block-handle.svelte`): new `detectDrawerSide()`
reads `[data-words-drawer]@data-side` once per measurement. The
grip's `position.left` becomes `rect.right + 4` when drawer is
on the left (was always `rect.left - 28`).
- Block-inserter (`words-block-inserter.svelte`): same `$derived`
read of the drawer's `data-side`, propagated as `data-side` on
the inserter overlay element so the CSS rule above can flip the
"+" button.
- Demo: new `let drawerSide = $state<'left' | 'right'>('right')` +
a chip control "drawer side: right | left". `<Words.Drawer
side={drawerSide} />`.
Verified `npm run check` clean (pre-existing errors only).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
71c24dca9b |
refactor(words): drop vertical toolbar orientation
The companion drawer + new floating overlays (image bar, block handle,
block inserter) assume a horizontal toolbar above the content. The
vertical layout was a CSS-grid two-column repositioning that fights
that geometry and was the cause of the new design breaking when the
user switched to vertical.
`horizontal` is the only orientation now. Cuts:
Morfo (`morfo/components/words.ts`)
- Toolbar + ToolbarGroup parts: `data-orientation` becomes a literal
`'horizontal'` (no longer reads from `propRef('orientation')`).
- Same for the toolbar's `aria-orientation`.
Soma (`soma/components/words/types.ts`, `exports.ts`, provider)
- Removed `WordsToolbarOrientation` type entirely.
- Removed `orientation?` from WordsToolbarProps and WordsToolbarGroupProps.
- Provider opts no longer read or pass through `orientation`.
Eidos (`eidos/components/words/{types,words.svelte,words-toolbar.svelte,words.css}`)
- `WordsProps.toolbarOrientation` removed.
- `<Words.Toolbar>` / `<Words.ToolbarGroup>` no longer accept or
forward `orientation`.
- Six chunks of CSS that targeted `[data-orientation='vertical']`
deleted (toolbar layout, content grid placement, status, find-replace,
link-editor, placeholder).
Demo (`web/routes/uix/components/words/+page.svelte`)
- `orientations` const, `orientation` state, the chip-control UI,
the `toolbarOrientation={orientation}` prop, and the corresponding
line in the generated code snippet all removed.
- Recipe-selector entry for `[data-orientation]` deleted.
- A11y row text simplified.
Verified: `npm run check` clean (only pre-existing 6 errors unrelated);
soma words tests 152/152 pass. Eidos vertical CSS is gone end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
9b1a964c69 |
feat(words): "+ between blocks" hover-zone inserter (POLISH-2b)
Fixes the wedge case the user just hit: two atomic blocks back-to-back (image, table, code) leave no caret position between them, so there's no way to insert a paragraph by clicking. Now: hover near the seam between any two top-level blocks and a "+" appears on the left margin with a faint accent line spanning the seam. Click "+" → a new paragraph is dropped at that index and the caret lands at the start so the user starts typing immediately. Same overlay pattern as the block-handle: single component watches mousemove inside `[data-words-content]`, computes the boundaries of all top-level blocks (plus "before first" and "after last" seams), snaps to the closest seam within 18px of the cursor's Y, and renders a fixed-positioned strip with the "+" button and accent line. New engine surface: - `insertBlockAt(state, blockIndex, block)` in `operations.ts` — splices the block in, normalizes, drops caret at `[blockIndex, 0]`. `blockIndex === children.length` appends to the tail. - `insertBlock` case added to `WordsCommand` + dispatcher in `commands.ts`. The command surface keeps `block` as a loose JSON record so consumers don't need to import `WordsBlock` — the reducer casts on the boundary and `normalizeDocument` validates. Verified in browser: hover at the seam between code (index 3) and image (index 4) → "+" appears at the left margin → click → doc becomes 0:heading | 1:paragraph | 2:quote | 3:code | 4:paragraph | 5:image; caret in the new paragraph ready for input. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
66225404e1 |
feat(words): drag handle + block action menu per block (POLISH-2a)
Notion-style :: grip rendered to the left of the hovered block via a single overlay component (`Words.BlockHandle`). Click reveals a small popover with Move up / Move down / Duplicate / Delete. Each fires via `snippet.applyCommand` so the discrete imperatives bypass the sema runtime (no extra event noise on top of what the operation already implies for downstream consumers). Backed by three new generic operations on the engine: - `moveBlockAt(state, blockIndex, direction)` — swap with neighbour. - `duplicateBlockAt(state, blockIndex)` — JSON-clone insert-after. - `deleteBlockAt(state, blockIndex)` — already existed; now wired to the menu's Delete entry. The three operations are exposed as `moveBlock` / `duplicateBlock` / `deleteBlock` commands in `WordsCommand` so any future block-toolbar can reuse them without per-feature engine plumbing. Also: add `applyCommand` / `selectAtomicBlock` / `clearSelectedBlock` to `WordsProviderSnippetProps` — they were emitted by the provider but missing from the type. Float bar + block handle both consume `snippet.applyCommand`. Verified in browser: hover over a block renders the grip (24×24, opacity 0.45 → 1 on hover, z-index popover); click opens menu with all 4 actions correctly labelled; Duplicate on the heading at index 0 produced a new heading at index 1 and pushed the rest down. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
5c10ab74b3 |
feat(words): floating image control bar + generic updateBlock/deleteBlock commands (POLISH-1a)
Quick-action toolbar that hovers above the selected image, mirroring
the pattern in Notion / Linear / Figma. Lets the user align L/C/R,
delete, or dismiss the image without having to expand the Image panel
of the drawer.
New eidos component (spike — no morfo/soma yet, refactor canon if it
sticks): src/uix/eidos/components/words/words-image-float-bar.svelte
- Position: fixed, calculated from the selected figure's
getBoundingClientRect on every selection / scroll / resize /
figure size change (ResizeObserver + window listeners).
- Visible only when snippet.selectedBlockIndex points to an image
block; null position otherwise so the component reflows away.
- 5 actions: align left / align center / align right / delete /
dismiss. Divider between layout and destructive actions.
- Calls snippet.applyCommand for align / delete instead of running
through runCommand string-name path (commands target an
arbitrary block index, no string lookup needed).
Engine — new generic commands so feature panels stop reaching for
hacky JSON re-imports to mutate single blocks:
- WordsCommand union gains 'updateBlock' (blockIndex + patch)
and 'deleteBlock' (blockIndex).
- updateBlockAt(state, idx, patch) — shallow-merge patch into the
block, normalize doc, clamp selection. No-op when patch matches
current values.
- deleteBlockAt(state, idx) — remove block, fall back to empty
paragraph when doc would be empty, place caret on the next block.
- applyCommand exposed on provider snippetProps so any consumer
(eidos panels, future block toolbars, tests) can drive arbitrary
WordsCommand without needing a string-name route.
CSS additions: float bar styling (position fixed, accent border,
shadow), action chips with hover + active + risk + ghost variants,
divider.
Demo: <Words.ImageFloatBar snippet={snippetProps} /> mounted next to
<Words.Drawer />.
Verified visually: clicking the demo image now (a) outlines the image
(COLOR-1 highlight), (b) opens the drawer in IMAGE mode, (c) shows
the float bar hovering above the figure with "align center" active
(default), trash + dismiss available.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
9a5f3c2359 |
refactor(words): drawer canon — morfo part + soma provider + eidos consumes via provider (DRAWER-CANON)
Closes the architectural gap from the DRAWER spike: the companion
drawer now follows the same morfo/soma/eidos pattern as every other
Words part (Toolbar/BubbleMenu/SlashMenu/LinkEditor/Status/etc.).
DRAWER-CANON-1 — morfo + soma:
- wordsMorfo gains a 'Drawer' part (kebab 'drawer', archetype 'group',
defaultElement 'aside', role 'complementary'). Declares data-words-
drawer-mode (default/format/image/table/code/list), data-open,
data-disabled, aria-label. New event 'commit-toggle-drawer' fires
on collapse/expand for sema/telemetry hooks.
- New WordsDrawerProvider in words-provider.svelte: owns `open`
bindable state, derives `mode` from parent provider's currentBlock
+ selection (same logic as the spike, now in soma where it
belongs), exposes snippetProps {mode, open, disabled, snippet,
toggle, setOpen}. `toggle()` and `setOpen()` emit the event.
- WordsDrawerProps + WordsDrawerSnippetProps + WordsDrawerMode types
added to types.ts and re-exported via exports.ts.
- soma/components/words/components/words-drawer.svelte (the soma
component) mounts the provider and forwards merged props to either
the `child` snippet or the default <aside> render.
DRAWER-CANON-2 — eidos refactor:
- eidos words-drawer.svelte rewritten as a consumer of
<Words.Drawer>. The shell (aside + header + collapse button) and
the per-mode body now live inside the children snippet that
receives the soma's drawer snippet props. Mode derivation removed
from eidos (it's now in soma). Local open $state removed (now
bindable via the soma component).
- Demo +page.svelte: `<Words.Drawer snippet={snippetProps} />`
collapses to plain `<Words.Drawer />` — the soma provider hands the
parent snippet down internally.
DRAWER-CANON-3 — verify:
- 214/214 tests pass in soma+morfo scope (no regressions).
- npx tsc --noEmit clean for words area.
- Visual verification: drawer still renders with DOCUMENT/OUTLINE/
STATS/SUGGESTIONS in default mode; context-switching to table /
code / list / format still works; aside element now carries
role='complementary', aria-label from morfo translationRef,
data-words-drawer-mode reflecting current panel.
The eidos drawer now passes the same canon checks as every other
Words part: morfo declares its contract, soma owns the state, eidos
only paints. The schema validator can verify the drawer's attrs, the
eidos lint can check selector usage, sema can react to the toggle
event.
Demo header chips reflect the new contract: PARTS 19→20, EVENTS
11→12.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
23f90a775c |
feat(words): companion drawer right-rail (concept A+D) — shell + default panel (DRAWER-1)
A radical disposition shift: instead of N fragmented popovers (bubble
menu + slash menu + link editor + heading picker + language picker +
table popover + image controls) we mount ONE contextual right-rail
that changes its panel based on what the user is editing.
Spike-1 surface (this commit) ships:
- New eidos component <Words.Drawer> (words-drawer.svelte) — a
collapsible right sidebar with mode detection derived from snippet
props (currentBlock + selection). 6 modes mapped: default | format
| image | table | code | list.
- Default panel (visible when no contextual mode active):
- OUTLINE — flat list of h1/h2/h3 headings, indented by level
- STATS — word count + char count + reading time (200wpm)
- SUGGESTIONS — writer-coaching warnings derived from the document
model: missing alt text, long paragraphs (>600 chars), heading
skip patterns (h3 without h2 above). Empty state shows "Nothing
flagged" in affirm color.
- Contextual panels (format/image/table/code/list) render a
placeholder "coming soon" message — implemented in DRAWER-2.
- Eidos CSS:
- Provider host gains a grid-template-areas layout when a drawer is
present: 'toolbar drawer / content drawer / status drawer'.
Existing children (toolbar/content/placeholder/status) get
explicit grid-area assignments.
- Drawer collapses to 2.25rem (just the chevron) when closed.
16rem default open.
- Outline items, stats grid, warning list each have their own
discrete styling.
Demo (+page.svelte):
- New `showDrawer` state (default true).
- Children snippet refactored to capture full snippetProps via @const
destructure (kept selectedLink working).
- <Words.Drawer snippet={snippetProps} /> mounted inside the children
block alongside the other parts.
Visual verification:
- Drawer renders as a dedicated right column with the editor content
on the left, no layout overlap.
- DOCUMENT header + OUTLINE shows "H1 ActiveUIX Words", STATS shows
73 words / 508 chars / 1 min reading, SUGGESTIONS shows "Nothing
flagged" (sample doc has no warnings).
- Collapse button toggles open/closed via animated inline-size
transition.
Next:
- DRAWER-2: format/image/table/code/list contextual panels wired to
runCommand.
- DRAWER-3: polish (mobile drawer-as-bottom-sheet, transitions, A/B
toggle in demo to compare classic toolbar vs companion drawer).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
cf9cb0b127 |
demo(words): add image block to sample document + fix readPlainText (F3.11)
- Sample document gains an image block between the code block and the table, showing figure/img/figcaption render in action. - readPlainText demo helper gets an 'image' branch returning alt text (was crashing the SSR with 'Cannot read properties of undefined (reading map)' because image has no .children). Demo screenshot confirms: - Image renders inside a figure with border + padding - Caption shows centered + italic below - img max-inline-size cap keeps the image inside the editor frame - Word/char count in status bar reflects alt + caption text F3 MVP visually verified end-to-end: - Block parses from markdown / HTML in the document model - Renders correctly as figure/img/figcaption in the contenteditable - Eidos styling (border, caption italic, alignment data-attr) applies - Status bar counts include image alt text Pending (UX surfaces, deferred to a follow-up session): - F3.7 slash menu /image entry (open a URL prompt) - F3.8 paste/drop handler with onUploadImage callback for file uploads - F3.7/F3.8 require new svelte components (insert dialog, drop overlay) and provider event handlers — they're isolatable from this commit's surface and are the next sub-sprint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
f25bf1558e |
demo(words): enrich status bar with char count + selection + dynamic link/commit (F4.5b)
Before: 'Optional editor · 64 words · none'
After: 'Optional · 64 words · 453 chars · none' (no link, no commit)
'Required · 64 words · 453 chars · range · https://...' (selection + link)
'Optional · 64 words · 453 chars · none · save' (after commit)
- Drops the redundant 'editor' suffix; the surface is already an editor.
- Adds char count from plainText.length — useful for size limits.
- Adds the existing selectionState (none/collapsed/range) inline.
- Splits link/commit into separate optional segments instead of
collapsing them into one slot.
Demo-only change in +page.svelte. The eidos Words.Status wrapper
still passes children through unchanged — enriching the eidos default
status snippet itself would require Words provider to expose
wordCount/charCount, which is a larger refactor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
85b8a56ab0 |
feat(words): add toolbarLayout grouped|inline (responsive)
New optional prop on <Words> for the toolbar layout strategy:
- 'grouped' (default): each family (text/block/list/align/insert/table/
link/tools) becomes a single trigger button that opens a popover with
its commands. Compact, recommended for narrow viewports.
- 'inline': every command in every family renders flat in the toolbar,
with the toolbar group separators marking family boundaries. High
discoverability, recommended for wide editors.
Tools that own their own panel UI (LinkEditor, FindReplace,
HeadingPicker, CodeLanguagePicker) remain popovers/dropdowns in both
layouts by design — they are not "inlineable".
Accepts ResponsiveProp<WordsToolbarLayout> for breakpoint-aware
switching, e.g. `toolbarLayout={{ base: 'grouped', md: 'inline' }}`.
Implementation:
- types.ts: new WordsToolbarLayout = 'grouped' | 'inline'; new
toolbarLayout?: ResponsiveProp<WordsToolbarLayout> on WordsProps.
- index.ts: export WordsToolbarLayout type.
- words.svelte: resolved via eidos.resolve(toolbarLayout, 'grouped').
New helper flattenFamilyToLeafItems(family, customItems) returns
the default leaf items per family for the inline expansion.
Render refactored: leaf rendering extracted to a Svelte snippet
reusable by both layouts, so the conditional only chooses between
<ToolbarFamilyMenu> (grouped) and {#each leafs as leaf}{@render
leafItem(leaf, snippetProps)}{/each} (inline).
- demo: toolbarLayout state added + wired to <Words>.
Verified visually in browser (preview + screenshot):
- grouped: 9 family menu triggers + 1 sibling tool = 10 toolbar buttons
- inline: 0 family triggers + 28 command buttons + 5 tools = 33 buttons
Tests: 161/161 pass in src/uix/{contracts,soma/components/words}.
TypeScript: 0 new errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
9c6375c96e |
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage
Token Scope Contract universal — no más excepciones arquitectónicas.
Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar,
toggle-group) ahora están dentro del contrato via dos extensiones nuevas.
TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts):
- `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite
selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para
componentes con data-color cascadeado per-part. Consumer: select.
- `composition: { foreignRecipe: { targetSelector, tokens } }` sibling
key — overrides cross-recipe scoped a la cascade del host. Consumer:
toggle-group modifica `--toggle-palette-*` en sus items.
Migraciones:
- select: 3 private `_accent-{track,border,text}` con parts: ['trigger',
'content'] + 7 cascades color:X each. Removed orphan `_accent-solid`
(CSS no consumía).
- avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con
matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques
CSS × 2 partes.
- toggle-group: composition block con 8 palette tokens × 4 colors.
Reemplaza 4 bloques CSS per-color.
Bug toggle-group post-composition (encontrado y arreglado):
Tras la composition migration, el cascade del toggle-group seguía roto
porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`,
etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es
sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía
undefined. Fix: inlined derivation expressions directamente en
`[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost),
referenciando palette tokens en su propio scope local.
Validador + emisor + contract:
- `validateRecipeComposition` valida el shape `{ targetSelector, tokens }`
y rechaza composition entries con scope='root'.
- `stripCompositionKey` + `emitComposition` separan el pipeline.
- `appendRecipeContractTokens` skip-list para `composition` (no aparece
como fake `--{c}-composition` knob).
- `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts
filtran composition en todos los iteradores.
Documentación:
- THEMING.md §18 reescrito como "Cobertura universal de TSC". §7
extendido con subsecciones "Multi-part scope" y "Cross-recipe
composition" + ejemplos completos. TOC actualizado.
- eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18.
- CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal).
- CONTINUE.md reescrito al estado actual de la sesión.
Working tree también incluye sprint Words en paralelo (multiple authors):
slash menu, find/replace regex, code language picker, table audit,
toolbar family menu, code highlight engine.
Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`:
6 errores pre-existentes (lib/_demo, soma/components/internal,
web/routes/active) no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
312c8740ca |
refactor(date-picker): rename kind 'date' to 'day' for trio consistency
Audit caught `data-kind="date"` violation — the date-picker / date-range-picker morfos declare `data-kind ∈ [day, month, year]` but the public `kind` prop accepted `'date' | 'month' | 'year'` and defaulted to `'date'`. Rename prop value `'date' → 'day'` so the trio is consistent (all named after the grid cell granularity). **Rationale (book-pure)**: - The trio `day` / `month` / `year` is internally consistent — all three name the granularity of the grid cell the calendar view shows. - `'date'` was awkward in the trio: month and year pickers also pick "dates", just at coarser granularities. - The morfo enum was already canonical (`['day', 'month', 'year']`); this aligns the prop with the contract. **Surface touched** (all `'date' → 'day'` for the kind value): soma components: - date-picker / date-range-picker / date-field / date-range-field: provider type (DatePickerKind / DateRangePickerKind), default value, test fixtures, types.ts JSDoc. demos (web/routes/uix/components/): - date-picker / date-range-picker / date-range-field: kind state, selector chips, doc table. `'date'` literals that DON'T refer to kind (kept untouched): - ValueProfile = 'date' | 'datetime' | 'zoned' (different concept). - SegmentLock = 'none' | 'year' | 'monthYear' | 'date'. - CSS `input[type='date']` (HTML attribute). **Result**: - `npm run morfo:check`: 1 issue / 1 component → **0 issues**. All 107 routed morfos validate against their demo DOM ✓ - `npm run check`: 0 errors ✓ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
f0440d647a |
docs(dialog-demo): update prose for polymorphic close
The dialog demo prose described "Six events" with the per-event close-*
shape that was collapsed into a single polymorphic close in commit
|
5 months ago |
|
|
e6eee766ee |
Improve words editor tooling
|
5 months ago |