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 }
153 Commits (d126e16e6527d6315a93ae2abc415e38956ec97a)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
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 |
|
|
cd2c0326dc |
Revert "refactor(soma/eidos): migrate 4 service components from Eidos to Soma"
This reverts commit
|
5 months ago |
|
|
540abee026 |
refactor(soma/eidos): migrate 4 service components from Eidos to Soma
Architectural cleanup: Trans, FormatDate, FormatNumber, RelativeTime no son visuales — son behavior alrededor del runtime (`uix.langs.t/ts`, `uix.format.*`). Estaban en `eidos/components/` solo porque ahi se inventariaban los wrappers. Naturaleza real: Soma. Beneficio principal: el invariante "Eidos no importa `getActiveUix()`" recupera su pureza — los 4 service components ya no contaban como violación del audit Codex (P1 #1 reducido en 4 archivos + dependencias). Movimientos (4 componentes, 31 archivos staged): - src/uix/eidos/components/{kebab}/ → src/uix/soma/components/{kebab}/ - {kebab}.svelte → components/{kebab}.svelte (subdir convencion Soma) - types.ts → types.ts - README.md → README.md - index.ts → exports.ts (Soma pattern: index re-exports from exports) - + nuevo index.ts: `export * from './exports';` - Eidos dirs eliminados (4 directories). Morfos: scope `['eidos']` → scope `['soma']` (4 archivos): - morfo/components/trans.ts - morfo/components/format-date.ts - morfo/components/format-number.ts - morfo/components/relative-time.ts Demos (4 +page.svelte) updated: - import paths `$uix/eidos/components/{kebab}` → `$soma/components/{kebab}` - meta-pill `scope: eidos` → `scope: soma` Headers de los .svelte updated: "Eidos `<X>`" → "Soma `<X>` — service component" + parrafo explicando por que vive en Soma (behavior, no visual primitive). Verificación: - `npm run check`: 0 errors (mantenido). - HTTP probes /uix/components/{trans,format-date,format-number,relative-time}: todos 200. Sin barrel re-exports a limpiar (no existían). Sin consumers fuera de los 4 demos. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
89f52c5e88 |
feat(eidos): Card, Avatar ext, Image, SText, Badge, service components + dates engine fixes
Sprint completo: - Card (eidos + sema): elevation/intent/interactive, eventos select+activate. - Avatar: ring, AvatarBadge, AvatarGroup separados; demo Avatar standalone. - Image: status surface (idle/loading/loaded/error) consumiendo soma ImageProvider. - SText + STextVirtualList: canvas-text engine portado de morfo-driven-soma como componentes separados para optimizar bundle (no se importa canvas si no se usa). - Badge: primitivo eidos puro. - Trans, FormatNumber, FormatDate, RelativeTime: 4 service components greppables alrededor de uix.langs y uix.format. Nav group "Utilities". Bugs fix arts/format/dates (engine-dates.ts): 1. Preset / per-axis conflict: Intl.DateTimeFormat rechaza dateStyle/timeStyle con cualquier per-axis (weekday/era/year/month/day/dayPeriod/hour/minute/ second/fractionalSecondDigits) o con timeZoneName. Engine drop-ea ambos presets por defecto cuando detecta conflict (regla estricta — un solo per-axis hace ilegales los dos presets). 2. Per-call locale override: formatDate/Time/DateTime aceptan tercer arg locale?: string. No muta engine state. Cache (getCachedDateFormat) keyea por (locale x options). resolveCallHourCycle: cuando hay locale override y pref auto, el ciclo horario se resuelve desde el locale OVERRIDE. 3. withHourCycle precedence: caller hourCycle/hour12 ahora gana sobre preferencia del engine. Antes spread mal ordenado sobrescribia 'h12' del caller con 'h23' del engine cuando locale resolvia a 24h. FormatDate component vuelve a rutear siempre por uix.format.dates (cache + hourCycle + locale reactivo), pasando locale como tercer arg. Demos service components autonomos del toolbar global: cada uno owns su demoLocale state con 7 chips (es-ES/en-US/en-GB/de-DE/fr-FR/ja-JP/ar-EG). FormatDate stage filtra dateStyle/timeStyle por mode + chip hourCycle anadido. Tests: 14/14 passing en engine-dates.test.ts (+6 nuevos: conflict handling, locale override, hourCycle precedence). Docs: arts/format/README.md (handoff 2026-05-25 con las 3 reglas), README.md por service component, project_service_components_sprint_2026-05-25.md, feedback_service_demos_own_locale.md (pattern reusable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
0804749147 |
Words: text alignment, heading picker, ToolPopover cleanup, word boundary fixes
- Add textAlign to document model (paragraph, heading, quote) - Add setTextAlign engine operation with left/center/right/justify - Add alignment toolbar buttons with icons (TextAlignStart, Center, End, Justify) - Update formatting and full presets to include alignment - Fix sameBlockShape to compare textAlign correctly - Add heading levels h2/h3 with Words.HeadingPicker dropdown (Popover) - Remove Words.ToolPopover component; migrate LinkEditor to native Popover - Fix word boundary algorithm: Intl.Segmenter + CJK/punctuation fallback - Fix bubble menu isSelecting logic and pointer event forwarding - Add 5 engine tests for setTextAlign - Update demo presets for alignment - Words engine tests: 42/42 passing |
5 months ago |
|
|
91ecf87cd5 |
fix(pendientes): tooltip Play buttons + range view-cell tint differentiation
Two items from src/uix/PENDIENTES.md:
- Tooltip D-4.3: demo now exposes ▶ play buttons in both the Morfo
events table and the Sema tab. Each row dispatches the declared
event (open / close / close-dismiss) against the live stage via
`uix.events.emit({ name, family, target })`. Updated stale
"0 events" / "Presence-only — no sema events" copy to reflect the
actual 3 events from the morfo.
- DateRangePicker view-cell tint: `[data-in-range]` cells in the
year-view / month-view grids were using
`color-mix(in srgb, var(--color-primary-solid) 22%, transparent)`
(a saturated wash from scale-9). Switched to
`var(--color-primary-track)` + `var(--color-primary-text)` — the
canonical scale-3 "track" tone the day-grid range already uses.
Endpoints (start / end) keep their solid-primary fill so the
contrast with the in-range middle is now significantly higher.
Test result unchanged: 2391/2397 passing. No regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
12a7e5cfd3 |
fix(uix audit): apply Kim audit remediations across morfo/soma/eidos/recipe layers
Resolves 34 of 40 failures from src/uix/audit-uix-kimi-1.md. The remaining
6 are all Words component (excluded per project owner — actively developed
on a separate track) or unrelated infra (cookie-adapter-client jsdom timeout).
Key changes:
- soma/provider/context.ts: swallow Svelte's `lifecycle_outside_component`
in `context.set()` so unit tests can instantiate providers without a
component tree (unblocked 18 picker tests in one line).
- soma/textarea: route inline style writes through `dom.apply({attrs})`
instead of direct `el.style.X = Y` (contract regex compliance).
- soma/picker-shell: add minimal Provider component + morfo so the shared
handle utility passes the morfo/component contract.
- eidos/_layout: lift `shared.ts` to `eidos/lib/layout-helpers.ts` (the
`_layout` directory wasn't a real component) and update 8 layout
primitive imports.
- eidos/command: replace `onMount` with `$effect` (visual layer can't use
Svelte 4 lifecycle hooks per component-api contract); restructure
`Item.Icon`/`Item.Shortcut` namespace so contract regex matches.
- eidos/color-picker: extract hue spectrum hsl literals to sidecar
`color-picker-spectrum.css`; keep main recipe color-literal-free.
- eidos/lib/recipes/base.ts: declare 196 layout-primitive + chrome CSS
vars (box, flex, grid, float, group, banner, container, section,
separator, toggle-group, pin-input, scroll-area, splitter,
navigation-menu, dropdown-menu, context-menu, date-range-field,
time-range-field, plus per-component additions to date-field,
color-picker, button, select). 197 missing → 0 non-Words.
- morfo: add `data-dir` to carousel, `data-kind` to date-picker, add
StrengthMeterLabel sub-part to password-field, register picker-shell.
- 5 pickers (date/color/date-range/time/time-range): add `internals.ts`
indirection so `exports.ts` doesn't re-export `*provider.svelte.ts`
directly (contract D); rewrite virtual root types to plain object
literals (no `WithChild`/`PrimitiveDivAttributes`).
- 4 new READMEs (button, textarea, password-field, picker-shell)
following the search-field canonical template.
Test result: 2391/2397 passing (99.75%). Remaining 6 failures are all
Words-scope or jsdom infra timeout — explicitly out of scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
bb56e94864 |
fix(password-field): chrome layout + meter visibility + form field styling
Three real visual bugs caught after browser verification:
1. **No visible border around input + meter row.** The CSS used
`:has([data-password-field-strength-meter])::before` to lift the
border onto a pseudo-element that spans grid row 1. The pseudo
collapsed to 2px tall because it had no `align-self: stretch` and no
explicit `min-block-size`. Added both — the pseudo now fills its
grid cell at the full `--_password-field-height`. Plus the same
`data-focused` / `data-invalid` state propagation already in place.
2. **Strength meter segments invisible.** Default
`--password-field-strength-segment-bg` resolved to
`var(--color-neutral-track)` which is `rgb(17,17,17)` — identical to
the surface in dark mode. Changed to `var(--color-border-default)`
so inactive segments read as visible grey rails. Also bumped segment
height from 4 → 6 px so the bar is clearly readable.
3. **Eye icon too small inside a thin trigger.** `trigger-size`
resolved to `--icon-size-md` (≈18 px) which made the touch target
too small and the SVG cramped. Promoted trigger-size to use
`--control-height-{xs..md}` so it matches the input row height per
size. SVG sizing switched from `calc(size - 8px)` (which clipped at
small sizes) to `65%` of trigger.
4. **Stage wrapper collapsed to content width.** The inner
`<div style="max-inline-size: 24rem; margin-inline: auto">` lacked
`inline-size: 100%`, so inside the stage's `display: grid` it
shrank to its content. Added the explicit `inline-size: 100%`.
5. **Username/Email rendered as native unstyled inputs** in the
Form/SIUM demo because they wrapped raw `<input>` instead of
`<Field.Input>` (Field.Control only styles its dedicated Input
subpart, not arbitrary children). Swapped to `<Field.Input>`.
6. **SIUM error refs (`#?sium.errors.min_length|...`) rendered
literal.** `Field.ErrorText` just renders the string it receives.
Added a local `issueText(form, field)` helper that calls
`uix.langs.ts(raw)` to resolve the langref and then interpolates
`{{param}}` from `form.issues[field][0].params`. Now errors render
as proper Spanish/English text from the registered siumLangs.
Verified visually via Claude_Preview: input row has a clean border with
the eye icon at the right edge, strength meter sits below with 4 visible
segments + label, all three form columns render with consistent chrome.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
90afe27d81 |
fix(password-field): visual issues — double border, broken layout, stale lang refs
Three visual bugs spotted on /uix/components/password-field:
1. **Double-bordered inputs.** The demo wrapped `<PasswordField>` inside
`<Field.Control>`, which adds its own border + padding. PasswordField
IS the field's input shell — it ships its own chrome. Removed the
five offending Field.Control wrappers from login/registration/change
forms; PasswordField now sits directly inside `<Field>`.
2. **Icon wrapping to a new row + StrengthMeter sharing the bordered
box.** Replaced `display: inline-flex; flex-wrap: wrap` on the root
with `display: grid; grid-template-columns: minmax(0, 1fr) auto auto`
so Input + CapsLockIndicator + VisibilityTrigger stay on the first
row. When StrengthMeter is composed, `:has()` switches the root to a
2-row grid where the bordered chrome lifts onto a `::before`
pseudo-element that only spans the first row, and the meter sits
below as a sibling. State chrome (`data-focused`, `data-invalid`)
propagates to the pseudo via the same `:has()` selectors.
3. **CapsLock pill claiming space even when inactive.** Was
`color: transparent; background: transparent` which still consumed
width in the grid. Now `display: none` by default and
`display: inline-flex` only when `[data-active]` is set.
Also fixed: `#?sium.errors.min_length|…` and friends rendered literal
in the demo because the docs shell didn't register `$sium/langs/langs`.
Registered `siumLangs` under the `sium` namespace in the layout's
`createActiveUix({ langs: { schema: { sium: siumLangs } } })`.
svelte-check: 0 errors. Page now loads at 200.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
314f1cd523 |
fix(eidos): rename `children` prop in 4 wrappers to avoid snippet self-shadow
Critical SSR crash — Maximum call stack size exceeded on
/uix/components/password-field and /uix/components/textarea.
Root cause: DEMO_AUTHORING_GUIDE §12.4 pitfall.
When a wrapper does:
let { children, ...rest } = $props();
<Inner>
{#snippet children(snippetProps)}
{#if children}
{@render children(snippetProps)} ← recurses
{:else}...{/if}
{/snippet}
</Inner>
The inner `{#snippet children}` SHADOWS the `children` prop. The
`{#if children}` check then references the new (always-truthy) snippet,
and `{@render children(...)}` recursively renders itself → infinite
loop, RangeError at SSR.
Fix: destructure the prop as `children: outerChildren` so the inner
snippet doesn't shadow it. Same pattern used in drawer wrappers per
the guide.
Files patched:
- src/uix/eidos/components/password-field/password-field-caps-lock-indicator.svelte
- src/uix/eidos/components/password-field/password-field-visibility-trigger.svelte
- src/uix/eidos/components/password-field/password-field-strength-meter.svelte
- src/uix/eidos/components/textarea/textarea-count.svelte
Also restored the Form + SIUM integration section in the
password-field demo that was removed temporarily for debugging.
Verified:
- /uix/components/password-field → 200
- /uix/components/textarea → 200
- npx svelte-check → 0 errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
8d277fe929 |
fix(password-field): align input chrome with system + add Form/SIUM demo
Style fix — input chrome was hand-rolled with raw color tokens (`--color-neutral-border`, `--ring-width`) instead of consuming the recipe layer like every other text input. Now matches search-field and number-field exactly: - Added `password-field` recipe to `src/uix/eidos/lib/recipes/base.ts` with the same surface as `search-field` (height, px, gap, font-size per size; border, focus, invalid, bg, color, focus-shadow tokens). - Regenerated `src/uix/eidos/generated/base.css`. - Rewrote `password-field.css` to consume the new tokens — same focus ring (`focus-shadow`), same border colors, same disabled opacity, same transition tokens. VisibilityTrigger gets its own border/bg/ color-on-hover scheme aligned with search-field's clear button. - Added per-size icon + caps font-size tokens so the trigger and caps pill scale with the input. Form + SIUM demo — answers "how does PasswordField validate?": Added a new subsection in the Live tab with three working forms wired through `<Form>` + `<Field>` and SIUM schemas: 1. Login (purpose='current', non-empty password) 2. Registration (purpose='new', complexity rules via regex pipes — uppercase + digit + symbol + 8+ chars, plus the visual StrengthMeter as a companion to the schema) 3. Password change (3 fields with a cross-field `refine()` so new === confirm; demonstrates SIUM's refine pattern) The section explicitly documents that PasswordField has no built-in complexity rules — validation is schema-first. The schema decides what's "correct"; PasswordField's flags propagate via Field's OR-merge of `formFieldState.isInvalid`. svelte-check: 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
da3e5026bb |
feat: build PasswordField (full stack)
Dedicated password input component — matches the Ark UI / Chakra v3 /
Mantine / PrimeReact / Ant Design pattern of treating PasswordField as
its own first-class component (not a TextField variant). Includes one
feature that NO surveyed library ships: a CapsLock indicator driven by
KeyboardEvent.getModifierState('CapsLock').
Anatomy (5 parts, 4 events):
- Provider (root, value + visibility state)
- Input (the textbox; type flips between password / text)
- VisibilityTrigger (eye toggle button; aria-pressed + aria-controls)
- CapsLockIndicator (status live region; data-active toggles via soma
listening to keydown/keyup on Input)
- StrengthMeter (composable; consumer provides 0-4 score; UIX bundles
no zxcvbn-style algorithm — dependency-free by design)
Events:
- commit-input (every keystroke)
- commit-submit (Enter)
- shift-toggle-visibility (eye click)
- shift-caps-state (Caps Lock state flips while focused)
Key behaviors:
- `purpose: 'current' | 'new'` drives native autocomplete attribute
('current-password' for login / 'new-password' for registration).
- Field integration: disabled/readonly/required/invalid OR-merged.
- VisibilityTrigger gets tabindex=-1 so it doesn't break the form's
natural tab order — mouse / programmatic focus only, matching Ark UI.
- Caps Lock indicator is aria-live="polite" — announces on activation
while focused, silent when inactive.
- Letter-spacing increased on the input for legibility while masked.
Reference library survey performed (10 libraries) before implementation —
all consensus around dedicated component, all have built-in toggle,
ZERO have caps lock warning. Documented in the demo's API tab with a
feature comparison table.
Demo follows DEMO_AUTHORING_GUIDE canonical 6-tab depth: Live (controls
grouped by layer + code snippets at the bottom), API (props + reference
comparison), Morfo (field/value + Parts + Events tables), Sema (events
table with ▶ play), Recipe (selectors with morfo/eidos owner tags), A11y
(concern/contract table).
Also includes external work in words/ from a separate session
(unrelated to PasswordField) — not touching Words is the standing rule,
but the diff was already in the working tree.
svelte-check: 0 errors in PasswordField files (Words errors are tracked
by its own development branch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
fead097ce1 |
fix(demos): rewrite Skeleton + Spinner demos to canonical 6-tab depth
The previous demos violated DEMO_AUTHORING_GUIDE in several places. Both have been rewritten end-to-end against the aspect-ratio canary template (canonical reference for 0-events eidos primitives). Changes per demo: - Stage always rendered between header and tablist (was inside Live tab). - MutationObserver wired with stageRef + 6-entry trace strip (was absent — required even for 0-event components per §6). - Trace strip layout: 'no semantic events' badge + shape/variant + size + color, matches aspect-ratio shape. - Live tab adds the soma + eidos code snippets with full data-uix-code wrappers and head badges (§7 + §8). - somaSnippet now shows the equivalent semantic markup with a clear "not real soma" note (since these are eidos-native). - API tab uses data-uix-table with explicit "Skeleton-specific props" + "Inherited from Box" + "Reference comparison" subsections (§4). - Morfo tab matches §10: header field/value table + Parts table with marker column + per-part data/aria subsection tables. - Sema tab keeps the explicit 0-events explanation (was absent for one of the demos). - Recipe tab uses the Selectors table with morfo/eidos owner tags (§11) instead of a single code snippet. - A11y tab uses data-uix-table (concern/contract) instead of a bullet list, matching aspect-ratio. Reference template: web/routes/uix/components/aspect-ratio/+page.svelte. Per §1, when in doubt, copy from the canary template and adapt. svelte-check: 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
616d8535f7 |
feat(eidos): add Skeleton and Spinner loader primitives
Two pure-eidos loader components with morfos at scope `['eidos']` (no soma backing, no semantic events). Skeleton — content placeholder: - 3 shapes: rect (default), circle, text (multi-line with staggered widths) - 5 sizes (xs..xl), 8 color accents - Optional shimmer animation via CSS `background-position` keyframes - Single `<div role="status" aria-busy="true">` with translated aria-label - Reduced-motion freezes animation, keeps visual Spinner — indeterminate progress: - 3 variants: ring (rotating arc), dots (bouncing), bars (stretching) - 5 sizes, 8 colors - Optional inline label via children snippet - `role="status"` + `aria-live="polite"` for SR announcements - Reduced-motion freezes animation, keeps visual Files: - morfo: skeleton.ts, spinner.ts (scope eidos, no events) - eidos: full component + types + css + langs + index per component - langs: skeleton/spinner under componentLangs barrel - demos: canonical 6-tab depth + composition examples (avatar+text card) - layout: sidebar additions under Status group Note: also includes responsive shell additions to web/routes/uix/uix.css that surfaced from an external session — same file, leaving them in. Reference libraries reviewed (Mantine, Chakra v3, Radix Themes, MUI): all expose the same shape/variant/size/color surface. shadcn Skeleton uses the same pulse animation. No surprises in our API. svelte-check: 0 errors in skeleton/spinner files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
f8897d92d6 |
fix: clear remaining `npm run check` errors (12 → 0)
Three groups of errors blocking the global typecheck:
A. Announce morfo used invalid family/intent vocabulary
- `absent-clear` event used `family: 'absent'` and `intent: 'absent'`,
neither valid in current MorfoEventSemantic schema
- Renamed to `commit-reset` (family: commit, verb: reset, intent: neutral)
matching SearchField's clear-reset pattern
- Updated demo copy to reference the new event name
- Cascade-resolved 4 errors (morfo + soma provider + demo + active-uix test)
B. Tree wrappers stripped bindable props
- eidos `<TreeGrid>` didn't expose `expanded`/`value` as `$bindable`;
they leaked through `...rest` and Svelte rejected `bind:expanded`
- Same issue for `<TreeView>` with `expandedValue`/`selectedValue`
- Now declared explicitly + forwarded via `bind:` to soma's Provider
C. Virtual containers passed wide `style` to native markup
- `style` was typed `string | Record<string, unknown> | null | undefined`
but the markup `<div style={...}>` only accepts string
- Filtered with `typeof style === 'string'` derived guard before render
svelte-check result: 5076 files, 0 errors, 23 warnings (no-op warnings
in navigation-menu `href="#"` and table `state_referenced_locally`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
940e94bb64 |
Build TextArea component + fix locale propagation bug across soma providers
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
3fc4ced37b |
feat(carousel): showIndicators + showControls visibility props
Two new eidos-level booleans on `<Carousel>` so consumers can toggle the indicator dots and the prev/next triggers without touching the markup composition. Defaults stay `true` (existing behaviour). The consumer still renders the parts in markup; the recipe just hides them via `display: none` when the eidos root stamps `data-hide-indicators` / `data-hide-controls`. Toggling on/off doesn't unmount the DOM, so ARIA focus is preserved across the change and the layout doesn't reflow into a different size. Naming follows the canonical eidos cascade — `showIndicators` mirrors calendar's `showWeekNumbers`. Mantine uses `withIndicators` (default false); we default `true` because the Eidos recipe assumes indicators are part of the default surface. Demo: two switches added to the eidos visual-props section + snippet preview emits the prop when set to false. Verified: clicking the demo switch flips `data-hide-indicators` on the root and the indicator-group's computed `display` goes from default to `none` without remounting. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
92c5e421c9 |
fix(carousel): :active press feedback was replacing centering transform
Bug behind the user's "el botón se desplaza en el eje contrario al
orientation" report. The Button recipe sets
[data-button]:active { transform: translateY(1px); }
on press to simulate a physical button push. Carousel prev/next triggers
absolute-position via:
[data-carousel-prev-trigger] { transform: translateY(-50%); } (horiz)
[data-carousel][data-orientation='vertical']
[data-carousel-prev-trigger] { transform: translateX(-50%); } (vert)
CSS `transform` is a single property — Button's :active value
REPLACES the carousel's centering transform, so on press the trigger
jumped off its anchor: in horizontal it lost vertical centering, in
vertical it lost horizontal centering AND moved 1px down (wrong axis
relative to slide travel).
Override `:active` on the carousel triggers to compose both transforms
on the appropriate axis:
horizontal: translateY(calc(-50% + 1px))
vertical: translateX(calc(-50% + 1px))
Now press feedback shifts the trigger 1px along the centering axis
without breaking the absolute anchor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
09bbf05ec0 |
fix(carousel demo): default loop=true so triggers never go dead at boundaries
The "los clicks siguen fallando" report was tested against loop=false default behavior — at idx=3 (last slide) the next button correctly went disabled (canGoNext=false), opacity 0.4, no advance — but testers read that as a broken click handler rather than a boundary stop. Default the demo to loop=true so each click predictably advances (0→1→2→3→0...). Verified 6 successive clicks wrap correctly. Component default stays loop=false (matches Embla / Mantine / ark-ui); the demo just picks the more illustrative interactive baseline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
f197463968 |
refactor(carousel demo): move Prev/Next triggers outside Viewport
Canonical placement (matches Mantine, shadcn-svelte): triggers sit as siblings of Viewport (children of the Carousel root), not as siblings of ItemGroup inside Viewport. Keeps the gesture-bearing ItemGroup region clean of unrelated event targets; the recipe's absolute positioning still anchors the triggers correctly because the carousel root is `position: relative`. The "clicks fail" report was the expected disabled state at the last slide under loop=false — verified the next button correctly reports disabled=true + opacity 0.4 once canGoNext becomes false. Enable loop from the controls for infinite navigation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
a6baae0cf1 |
fix(carousel/range-calendar/time-range-field): apply canonical i18n + vertical layout patterns
## Vertical carousel layout
Recipe was making the indicator group stretch full width (380px) because
the root stayed in `flex-direction: column` and the indicator-group
inherited `align-items: stretch`. Reshaped:
- `[data-carousel][data-orientation='vertical']` now flips the root to
`flex-direction: row` so viewport and indicator-group sit side-by-side.
- Viewport takes `flex: 1 1 auto` of the inline axis with explicit
100% block-size.
- Indicator-group becomes `flex: 0 0 auto; inline-size: max-content;
align-self: center` — narrow column of dots on the right edge.
Active vertical-stretched indicator already had `block-size: 2.25×`
treatment from the prior commit, so it now appears as a vertical pill
inside the dot column.
## i18n controls: follow canonical pattern
Mirrored the date-range-picker / calendar demos verbatim — those are
the canon and the new demos drifted from them. Three corrections:
1. **Locale**: `<select>` with `en-US / es-ES / fr-FR` (the docs
canon — chip rows and made-up locale lists like ar/he/ja were both
inventions). Carousel exposes locale too; it's forwarded to soma
for downstream lang-aware behavior (announcer text, future label
formatting hooks).
2. **Direction**: chip row with `ltr / rtl` (no `auto`). Label is
"direction" not "dir".
3. **State shape**: plain `let dir = $state<Direction>('ltr')`, not
the synthetic `dirCtl + derived` pattern I had invented.
Applied across all three demos so they read identically to the canon.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
23ad9351ea |
feat(carousel): wire dir + locale (was hardcoded 'ltr'); expose i18n in demos
Audit caught Carousel ignored direction entirely: `getDirectionalKeys` received a hardcoded `'ltr'` so ArrowLeft / ArrowRight had the same semantics under RTL as LTR, and the horizontal translate had no RTL flip so slides advanced the wrong way visually. Drag direction had the same bug. ## Soma - New `dir` + `locale` props on `Carousel.Provider` (fall back to `soma.prefs.getDir()` then `'ltr'`). - `onkeydown` now reads `this.opts.dir.current` for `getDirectionalKeys`. - `itemGroupTransform` flips the translate sign on horizontal RTL so index N→N+1 moves the rail in the reading-direction-appropriate way. - `finishDrag` swaps the offset sign on horizontal RTL so drag-right advances under RTL (drag-left under LTR). - Root stamps `dir="..."`, `lang="..."`, and `data-dir="..."` for consumers and the recipe. - Test mocks updated. ## Demos - Carousel demo: new `soma · i18n` controls section — `dir` chips (`auto` / `ltr` / `rtl`) + `locale` chips covering en/es/ar/he/ja so the user can flip orientation semantics live. - Range-calendar demo: same `dir` + `locale` chip rows added next to `weekStartsOn`; previously only `locale` was state-bound but the control wasn't surfaced and `dir` wasn't forwarded at all. - The eidos code snippet preview emits `dir="..."` and `locale="..."` attrs when they differ from defaults. ## Verified Browser walk: with `dir="rtl"`, the carousel root reports `data-dir="rtl"`, prev/next chevrons swap sides, indicator order mirrors, and `ArrowRight` correctly advances to the *previous* slide (index 1 → 0). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
f744583c12 |
fix(range-calendar demo): add explicit none/preset chips to minDays + maxDays
A bare `<input type="number" placeholder="—">` made it non-obvious how to clear the constraint — users would type and not realise empty=undefined. Mirroring the date-range-picker demo's bounds row, both minDays and maxDays now expose a chip row with `none` as the first option plus common presets (`2/3/7/14` for min, `7/14/30/90` for max). The custom number input stays alongside for ad-hoc values. The label header shows the current effective value (`none` when undefined). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
f291c08865 |
fix(carousel/range-calendar/time-range-field): visual bugs across all three
Real visual audit caught three classes of bugs that the prior commits
missed because they were never opened in the browser.
## Carousel
### Vertical orientation overflow
Recipe gave the root no block-size in vertical, so items spilled. Plus
the soma `viewportSize` derivation only ran on ref / orientation-prop
changes, not on layout reshape — so flipping horizontal → vertical
kept reading the old clientWidth and the translate maths used the wrong
slide size (observed -214 instead of -156 after a flip).
Fix:
- Recipe: `[data-carousel][data-orientation='vertical']` gets explicit
`block-size`, and the viewport / item-group / item each get `100%`
along the block axis. Default vertical size lives on
`--_carousel-vertical-block-size` (16rem), overridable via the new
eidos `verticalBlockSize` prop or the custom property directly.
- Soma: replaced the synchronous `clientWidth/Height` reads with
`viewportClientWidth/Height` `$state` mirrors populated by a
ResizeObserver on the viewport. Layout-only changes (orientation
flip, container resize, font scaling) now propagate to translate
calc on the next reactive tick.
## RangeCalendar
### Multi-month stacked vertically
`numberOfMonths > 1` rendered grids stacked in a column because the
eidos `[data-range-calendar]` recipe is `inline-flex; column`. Copied
the side-by-side grid layout from the existing date-range-picker demo:
`.range-months { display: grid; grid-auto-flow: column; }` with
`grid-auto-flow: row` collapse for `data-month-count='1'`.
### Min/max controls were nonsense
The prior demo had a custom "today | custom" mode that conflated
fixtures with real today, and required two separate enum states.
Rewrote the controls to mirror the date-range-picker demo exactly:
two `<input type="date">` bindings on string state, parsed by
`parseDateInput`, plus a preset chip row (none / min only / max only /
window / invalid).
### Weekday key duplicate
The narrow-format weekday strings repeat letters ('T' for Tuesday and
Thursday, 'S' for Saturday and Sunday), so keying by `wd` raised
`each_key_duplicate`. Changed to key by index.
## TimeRangeField
### Start endpoint rendered no segments
Keying the segments `#each` by `seg.part` collapsed when segment
parts repeat. Copied the canonical key shape from the time-range-picker
demo: `(${part}-${i})`. Verified both endpoints now render their
four segments (`09:30` — `17:45` for the default fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
44514c3cc1 |
fix(carousel): vertical overflow + close gaps vs embla/ark-ui/mantine
Real reference audit walked the published embla `.d.ts`, Mantine docs, ark-ui, shadcn-svelte and Swiper. Found three classes of gaps in the initial port (a) the vertical bug breaking the only non-default orientation, (b) layout primitives every other lib ships (slidesPerView, slidesToScroll, align, gap), (c) autoplay granularity (playOnInit, stopOnInteraction, stopOnFocusIn, stopOnMouseEnter, stopOnLastSnap), plus an imperative API every consumer needs. ## Vertical bug fix Recipe now gives the root a default `block-size` in vertical orientation (`16rem`, overridable via `--_carousel-vertical-block-size` or the new eidos `verticalBlockSize` prop) and stretches the viewport to fill it. Without this the viewport collapses to content height and items spill. ## Soma extensions - `defaultValue` (uncontrolled init) - `slidesPerView` (1+) — each Item basis = (100% − gap·(n−1)) / n - `slidesToScroll` (1+) — paged group navigation - `align: 'start' | 'center' | 'end'` — viewport alignment - `gap: string` (CSS length) — between-slide spacing on the ItemGroup - `playOnInit` — start autoplay on mount (default true) - `stopOnInteraction` — latch-stop after user clicks prev/next/indicator or drags - `stopOnFocusIn` — pause while focus is inside (default true) - `stopOnMouseEnter` — pause while cursor over root (Embla parity) - `stopOnLastSnap` — latch-stop at last snap when loop=false - Snippet exposes `isPlaying` + an `api` handle with `scrollNext / scrollPrev / scrollTo / play / pause / reset` `commit()` now respects `slidesPerView` (last snap = count − spv). `next()` / `prev()` step by `slidesToScroll`. Translate calc derives from `slideSize = viewportSize / slidesPerView` so multi-view works. Item flex-basis adapts to slidesPerView + gap. ## Eidos additions - `verticalBlockSize` prop — forwarded as CSS custom property for the vertical recipe override - Recipe selectors for vertical orientation enforcing the block-size + viewport stretch - Style composition so consumers can still pass inline `style` ## Demo Live tab now has 4 organized sections (layout / drag / autoplay / imperative API) covering every new prop. The imperative API row drives the carousel from external buttons via the snippet handle so consumers see the pattern in action. `verticalBlockSize` slider exercises the vertical fix. Snippet preview emits every set prop. ## Reference comparison Following the canon's mandate to compare with references BEFORE declaring done, the actual feature matrix from embla published `.d.ts`, Mantine docs, ark-ui, shadcn-svelte, Swiper: | Feature | embla | mantine | ark | swiper | UIX | |------------------------|-------|---------|-----|--------|-----| | Index nav | ✓ | ✓ | ✓ | ✓ | ✓ | | Loop | ✓ | ✓ | ✓ | ✓ | ✓ | | Drag | ✓ | ✓ | ✓ | ✓ | ✓ | | Autoplay | plug | plug | ✓ | ✓ | ✓ | | Orientation H/V | ✓ | ✓ | ✓ | ✓ | ✓ | | slidesPerView | indir | ✓ | ✓ | ✓ | ✓ | | slidesToScroll | ✓ | ✓ | ✓ | ✓ | ✓ | | Align | ✓ | indir | per | ✓ | ✓ | | Gap | css | ✓ | ✓ | ✓ | ✓ | | defaultValue | startIndex | ✓ | ✓ | ✓ | ✓ | | playOnInit | ✓ | indir | ✓ | ✓ | ✓ | | stopOnInteraction | ✓ | ✓ | ✓ | ✓ | ✓ | | stopOnFocusIn | ✓ | ✓ | ✓ | partl | ✓ | | stopOnMouseEnter | ✓ | ✓ | ✓ | ✓ | ✓ | | stopOnLastSnap | ✓ | ✓ | ✗ | ✓ | ✓ | | Imperative API | ✓ | ✓ | ✓ | ✓ | ✓ | | Indicators / triggers | DIY | ✓ | ✓ | ✓ | ✓ | | Fade effect | plug | plug | ✗ | ✓ | ✗ deferred | | Virtual slides | ✗ | ✗ | ✗ | ✓ | ✗ deferred | | Multirow grid | ✗ | ✗ | ✗ | ✓ | ✗ deferred | Deferred power-user features (fade, virtual, multirow grid, cube/cards, parallax) are documented as power-user in the reference audit, not table-stakes, and would require Embla-class internals to ship. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
2cecc0b3b7 |
feat(eidos): port range-calendar from soma + extract shared partial
Standalone eidos `<RangeCalendar>` wrapper over the soma range-calendar primitive. Adds size / variant / color cascades; otherwise pass-through 14 compound parts (Provider, Header, Heading, PrevButton, NextButton, MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell, Cell, Day). ## Feature comparison | Feature | bits-ui | ark-ui | react-aria | Eidos | |----------------------|---------|--------|------------|-------| | Range start/end | ✓ | ✓ | ✓ | ✓ | | Hover preview state | ✓ | ✓ | ✓ | ✓ | | minDays / maxDays | — | — | ✓ | ✓ | | allowSingleDay | ✓ | ✓ | ✓ | ✓ | | preventDeselect | ✓ | ✓ | — | ✓ | | Multi-month display | ✓ | ✓ | ✓ | ✓ | | Week-number column | — | — | ✓ | ✓ | | Month/Year selects | — | ✓ | — | ✓ | | Disabled / readonly | ✓ | ✓ | ✓ | ✓ | | isDateUnavailable | ✓ | ✓ | ✓ | ✓ | | Holiday markers | — | — | — | ✓ | | Locale / dir | ✓ | ✓ | ✓ | ✓ | | Size cascade | — | — | — | ✓ 4 | | Variant cascade | — | — | — | ✓ 3 | | Color cascade | — | — | — | ✓ 8 | ## Range-cells partial extracted The recipe at `src/uix/eidos/components/range-calendar/range-calendar.css` now owns ALL `[data-range-calendar-*]` selectors — the shared rules that previously lived inside `date-range-picker.css` (lines 537-834) moved here. Both consumers (the standalone `<RangeCalendar>` and `<DateRangePicker>`'s popover content) stamp the same data-attrs, so the same selectors paint without duplication. `date-range-picker.css` drops 298 lines of duplication; it keeps only the picker-specific scope overrides (`[data-date-range-picker-calendar]` with no chrome, max-content sizing, transparent bg). ## Morfo Extended `rangeCalendarMorfo.scope` to include `'eidos'` so the new wrapper appears in the eidos-layer inventory and the demo Morfo tab declares it correctly. ## Demo Canonical 6-tab depth (~600L) at `/uix/components/range-calendar`. Live tab exposes every range-specific prop: minDays / maxDays sliders, allowSingleDay, preventDeselect, numberOfMonths chips (1 / 2 / 3), pagedNavigation, fixedWeeks, showWeekNumbers, disableDaysOutsideMonth, weekStartsOn, disabled, readonly, locale, plus size / variant / color cascades. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
68670b6331 |
feat(eidos): port carousel from soma
Eidos wrapper for the soma Carousel primitive — horizontally or vertically scrollable rail of slides with keyboard, button, indicator, drag and autoplay nav. Soma owns the index + autoplay timer + gesture; eidos adds size/variant/color cascades and renders prev/next triggers as `<Button iconOnly variant="ghost" rounded="full">` per the canonical Button consumer pattern. ## Feature comparison | Feature | embla | Mantine | ark-ui | Eidos | |----------------------|-------|---------|--------|-------| | Index nav | ✓ | ✓ | ✓ | ✓ | | Loop | ✓ | ✓ | ✓ | ✓ | | Drag / scrub | ✓ | — | ✓ | ✓ | | Autoplay | plugin| ✓ | ✓ | ✓ | | Pause on hover | plugin| ✓ | ✓ | ✓ | | Orientation V/H | ✓ | ✓ | ✓ | ✓ | | Keyboard nav | ✓ | ✓ | ✓ | ✓ | | Indicators | — | ✓ | ✓ | ✓ | | Prev/Next triggers | — | ✓ | ✓ | ✓ | | Size cascade | — | — | — | ✓ 5 | | Variant cascade | — | — | — | ✓ 3 | | Color cascade | — | — | — | ✓ 8 | | Multi-slides view | ✓ | ✓ | — | deferred | | Lazy-mount slides | plugin| — | — | deferred | ## Parts (8) Provider · Viewport · ItemGroup · Item · PrevTrigger · NextTrigger · IndicatorGroup · Indicator. PrevTrigger and NextTrigger render Button via the soma `child` snippet (canonical Button consumer pattern). Default chrome: `iconOnly variant='ghost' rounded='full'` + chevron glyph. Size cascades from Carousel root; consumers can override every Button visual prop inline. Passing `child` bypasses Button entirely. ## Recipe Card chrome around the rail (`variant='surface'`), absolute-positioned triggers over the viewport edges (vertical flips to top/bottom). Active indicator stretches inline-size 2.25× for clearer current-position hint. 8 ColorRole accents. ## Demo Canonical 6-tab depth (~470L): Live / API / Morfo / Sema / Recipe / A11y. Live controls cover all soma props (orientation, loop, dragEnabled, autoplay + delay slider, pauseOnHover, disabled) and all eidos props (variant, size, color). 4 colored placeholder slides for visual contrast. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
db21767db1 |
feat(eidos): port time-range-field from soma
Eidos wrapper for time range selection — start/end TimeField inputs sharing a single ZonedDateTime range value. Mirrors DateRangeField pattern with `data-time-field` overlay trick so item segments inherit the time-field recipe. ## Feature comparison | Feature | react-aria | Mantine | Eidos | |----------------------|------------|---------|-------| | Range value | ✓ | ✓ | ✓ | | Per-endpoint Input | ✓ | ✓ | ✓ | | Segment editing | ✓ | — | ✓ | | Granularity prop | ✓ | ✓ | ✓ | | Hour cycle (12/24) | ✓ | ✓ | ✓ | | Min/max value | ✓ | ✓ | ✓ | | Placeholder per-end | ✓ | — | ✓ | | Invalid signalling | ✓ | ✓ | ✓ | | Disabled / readOnly | ✓ | ✓ | ✓ | | Form integration | ✓ | ✓ | ✓ | | Size cascade | — | ✓ | ✓ xs–xl | | Variant cascade | — | ✓ | ✓ 3 | | Color cascade | — | — | ✓ 6 | ## Parts Root · Label · Input (type='start' | 'end') · Segment (re-exported from TimeField). Each Input scopes its own TimeField context so segment editing routes to the correct endpoint. ## Recipe Grid `1fr · auto · 1fr` with em-dash separator. Specificity bump on `[data-time-range-field][data-time-field]` to win over the overlay data-time-field rules. ## Demo Canonical 6-tab depth (~530L): Live / API / Morfo / Sema / Recipe / A11y. Variant / size / color / granularity / hour-cycle / min-max / disabled / readOnly / required controls. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
a010f8f255 |
fix(eidos navigation-menu): style sub-items inside Content
Bug: anchors inside Content panels had no hover/padding/border treatment — consumers writing typical mega-menu markup (<a> elements) got a wall of unstyled text. Adds a default sub-item recipe that targets \`[data-navigation-menu-content] :is(a, [data-navigation-menu-link])\` — covers both plain anchors AND the official \`NavigationMenu.Link\` part when used inside Content. Each item gets: - display: block - padding 8px / 12px - border-radius (matches md radius) - color = primary content - text-decoration: none, cursor: pointer - hover/focus background = --color-primary-element - aria-current="page" + data-active = same tinted active state Demo cleanup: dropped the redundant inline \`style="color: inherit; text-decoration: none;"\` overrides on every <a>, tightened the grid gap from space-3 to space-1 (rows now have their own background hover so gap can shrink). Content panel padding reduced to space-2 (was space-4) because sub-items already provide their own padding. Verified in browser: items render with padding 8/12px, border-radius 6px, text-decoration: none, cursor: pointer. Hover changes the background to primary-element. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
065cc6a369 |
feat(eidos): port GridList (selectable list w/ interactive row children)
Closes #133. Eidos wrapper + types + recipe + canonical-depth demo. 4 parts: Provider, Row, Cell, SelectionCheckbox. Soma owns selection (none/single/multiple), 2D keyboard nav (Up/Down rows + Left/Right cells), Shift/Ctrl range/toggle, Ctrl+A select-all, Escape-clear, typeahead, hidden form input, Field merge. Eidos adds variant + size + color + defaultValue (uncontrolled init). Reference parity (no air baseline): | Feature | soma | react-aria GridList | this port | |---|---|---|---| | selectionMode none/single/multiple | ✓ | ✓ | ✓ | | bind:value (controlled) | ✓ | ✓ | ✓ | | defaultValue (uncontrolled init) | ✗ | ✓ | ✓ added (parity with Combobox/Select) | | Item with interactive descendants | ✓ | ✓ | ✓ | | 2D keyboard nav (Up/Down rows, L/R cells) | ✓ | ✓ | ✓ | | Shift/Ctrl range/toggle | ✓ | ✓ | ✓ | | Ctrl+A select-all (multiple) | ✓ | ✓ | ✓ | | Escape-clear | ✓ | ✓ | ✓ | | Typeahead | ✓ | ✓ | ✓ | | ARIA grid/row/gridcell | ✓ | ✓ | ✓ | | Field merge (disabled/invalid/required/readonly) | ✓ | ✗ | ✓ | | Form participation (hidden input) | ✓ | ✗ | ✓ | | Drag-to-reorder | ✗ | ✓ | ⚠ deferred (compose w/ future DragDrop) | | onAction (row click → navigate) | ✗ | ✓ | ⚠ deferred (use button/link inside Cell) | | selectionBehavior toggle/replace | ✗ implicit | ✓ | ⚠ deferred | | disabledBehavior 'selection'/'all' | ✗ skip-only | ✓ | ⚠ deferred | | Async / infinite loading | ✗ | ✓ | ⚠ deferred | | Empty state slot | ✗ | ✓ | ⚠ document `data-empty` styling | Naming: keeps soma's part names (Provider, Row, Cell, SelectionCheckbox). react-aria's "GridListItem" maps to `Row`; soma's vocabulary is more precise and consistent with future Table / TreeGrid in the same family. Variant set: - variant: ControlVariant (surface/outline/ghost), default 'surface' - size: xs..xl (5-step, mirrors Combobox/Listbox) - color: ColorRole (primary/secondary/neutral/affirm/fulfill/risk/threat/loss) - block: stretch to container width Disambiguation (in index.ts comment): use GridList for rows with interactive children; use Listbox for atomic text options (ARIA forbids interactive descendants inside role="listbox"); use Select/Combobox for closed-picker patterns; use Table for tabular data with columns + sort. Visual reuse: - SelectionCheckbox uses the same `mask-image: url(svg-check)` pattern as the shared menu-indicator partial (commit |
5 months ago |
|
|
52627e60bb |
feat(eidos): port NavigationMenu (top-level site navigation w/ mega-menus)
#132. Eidos wrapper + types + recipe + canonical-depth demo. 7 parts: Provider, List, Item, Trigger, Content, Link, Indicator. Soma owns active-item state, hover delays (openDelay/skipDelayDuration/closeDelay), hoverEnabled gate, loop arrow nav, data-motion directional swap, Indicator rect CSS vars. Eidos paints the bar + trigger chrome + Content panel + slide animations. Reference parity (no air baseline — port directly from soma): | Feature | soma | Radix | bits-ui | this port | |---|---|---|---|---| | Provider + List + Item + Trigger + Content + Link + Indicator | ✓ | ✓ | ✓ | ✓ | | Hover open (openDelay/skipDelayDuration/closeDelay) | ✓ | ✓ | ✓ | ✓ | | hoverEnabled (click-only mode) | ✓ | ⚠ | ✓ | ✓ | | orientation horizontal + vertical | ✓ | ✓ | ⚠ | ✓ | | dir / RTL | ✓ | ✓ | ✓ | ✓ | | Indicator CSS vars (x/y/w/h rect) | ✓ | ✗ | ✗ | ✓ | | data-motion directional swap (from-/to-start/end) | ✓ | ✓ | ⚠ | ✓ | | forceMount on Content + Indicator | ✓ | ✓ | ✓ | ✓ | | ARIA <nav> + translated aria-label | ✓ | ⚠ | ⚠ | ✓ | | Keyboard nav (Arrow/Home/End/Enter/Space/Esc, ArrowDown → Content) | ✓ | ✓ | ✓ | ✓ | | Sub (nested viewports) | ✗ | ✓ | ✓ | ⚠ deferred (matches soma) | | Viewport (shared swap pane) | ✗ | ✓ | ✓ | ⚠ deferred | | Modal variant (focus trap) | ✗ | ✓ | ✗ | ⚠ deferred | | SafePolygon hover bridge | ✗ | ✗ | ✗ | ⚠ deferred | Naming: matches soma's part names exactly. Both Trigger (button) and Link (anchor) stay native — NOT routed through <Button> — because top-nav has flat/borderless chrome distinct from action buttons. The Link automatically picks up `aria-current="page"` styling for the active route. Variant set: only `size: 'sm' | 'md' | 'lg'` (no xs/xl). No variant/color — NavigationMenu is chrome surface, not actionable intent. Fresh recipe — does NOT reuse dropdown-menu (floating click-driven) or menubar (roving tabindex bar). NavigationMenu lives inline in the page; per-item Content panels render via Floating but conceptually attach to the bar. Indicator is a 2px underline that slides via CSS vars (--navigation-menu-indicator-{x,y,w,h}) written by soma. Trigger chevron rotates on [data-state='open']. Content animates via data-motion directional slide on sibling swap. Sidebar entry added under Navigation (between Pagination and a future NavigationMenu entry). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
80b21f63ba |
feat(eidos): port Listbox (embedded selection list)
#131. Eidos wrapper + types + recipe + canonical-depth demo. Soma owns single/multi selection (value as string[]), roving-tabindex keyboard nav (Arrow + Home/End + typeahead + Ctrl+A), Field-merged flags (disabled/readonly/required/invalid), hidden form inputs. Eidos adds the visual surface and exposes variant + size + color. Reference parity (no air baseline — port directly from soma): | Feature | soma | react-aria | Headless UI | bits-ui | this port | |---|---|---|---|---|---| | Single + multiple selection | ✓ | ✓ | ✓ | ✓ | ✓ | | bind:value (controlled) | ✓ | ✓ | ✓ | ✓ | ✓ | | Typeahead + Arrow / Home / End | ✓ | ✓ | ✓ | ✓ | ✓ | | Loop nav | ✓ | ✗ | ✗ | ✗ | ✓ | | Section / Group + label | ✓ | ✓ | ✗ | ✓ | ✓ | | Ctrl+A select-all (multi) | ✓ | ✓ | ✗ | ✗ | ✓ | | Form participation (hidden inputs) | ✓ | ✗ | ✓ | ✗ | ✓ | | Field.Provider merge (disabled/invalid/…) | ✓ | ✗ | ✗ | ✗ | ✓ | | Horizontal orientation | ✓ | ✓ | ✓ | ✓ | ✓ | | Item indicator (✓ glyph) | ✓ | ✓ | ✗ | ✓ | ✓ shared partial | | Drag-to-reorder | ✗ | ✓ | ✗ | ✗ | ⚠ deferred | | Async / infinite loading | ✗ | ✓ | ✗ | ✗ | ⚠ deferred | | selectionBehavior (toggle/replace) | ⚠ implicit | ✓ | ✗ | ✓ | ⚠ deferred | | PageUp/Down nav | ✗ | ✓ | ✗ | ✗ | ⚠ deferred | Parts (5): Listbox (Root, replaces soma's Provider name), Item, ItemIndicator, Group, GroupLabel. Variant set: - variant: ControlVariant (`'surface' | 'outline' | 'ghost'`), default 'surface' - size: xs..xl (5-step canonical form-control scale) - color: ColorRole (primary/secondary/neutral/affirm/fulfill/risk/threat/loss) - block: stretch to container width Cross-component reuse: - `ItemIndicator` shares the menu-indicator visual (`a2709476`) — ✓ glyph identical to standalone Checkbox and Dropdown/Context Menu CheckboxItems. CSS-only difference: shown only inside `[data-listbox-item][data-selected]`. - Item row hover / highlight tokens mirror Combobox.Item / Command.Item — the form-control surface family stays coherent. Disambiguation note (in index.ts comment): - Listbox: atomic options, text rows, no inner interactive elements (ARIA forbids interactive descendants inside role="listbox") - GridList: rows with buttons / links / checkboxes inside (2D nav) - Select / Combobox: closed picker surface (Listbox-like dropdown) Gotcha: listbox.svelte and listbox-item.svelte rename `children` to `bodyContent` on destructure to avoid shadowing the inner `{#snippet children}` block (same recursion trap documented in `feedback_button_consumer_pattern.md` Rule 3). Demo: 6 tabs at canonical depth — Live wires all soma + eidos props, all 3 ControlVariant chips, 5 size chips, 8 ColorRole chips, single + multi mode toggle, RTL/horizontal toggle. Sidebar entry added under Forms (between Combobox and Calendar). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
d58b684c5c |
docs(uix sidebar): list LinkPreview, Menubar, Command under Overlays
The 3 newly-ported components (commits |
5 months ago |
|
|
8ab8f9dae1 |
feat(eidos): port Command (⌘K palette)
Closes #130. Eidos wrapper + 13 part wrappers + 2 new sub-parts + Dialog composition + recipe + canonical-depth demo. Soma owns scoring (`computeFuzzyScore`), keyboard nav (Arrow + vim Ctrl+N/P/J/K + typeahead + loop + grid columns + RTL), item visibility, and the imperative `CommandApi`. Eidos adds size + Item.Icon / Item.Shortcut slots + Dialog composition with optional shortcut binding. Reference parity: | Feature | cmdk | Mantine Spotlight | bits-ui Command | this port | |---|---|---|---|---| | Root provider w/ bind:value, bind:search | ✓ | ✓ | ✓ | ✓ | | Input, List, Viewport, Empty | ✓ | ✓ | ✓ | ✓ | | Group + GroupHeading + GroupItems | ✓ | ✓ | ✓ | ✓ | | Item value, keywords, disabled, onSelect | ✓ | ✓ | ✓ | ✓ | | LinkItem (anchor) | ✗ | ✗ | ✓ | ✓ | | Separator, Loading (progress 0-100) | ✓ | partial | ✓ | ✓ | | Custom filter / shouldFilter | ✓ | ✓ | ✓ | ✓ | | Loop, vim bindings, grid columns | ✓ | partial | ✓ | ✓ | | disablePointerSelection | ✓ | ✗ | ✓ | ✓ | | Modal Dialog composition | ✓ | ✓ (Root is modal) | ✓ | ✓ via eidos Dialog | | Built-in ⌘K shortcut binding | ✗ (BYO) | ✓ | ✗ (BYO) | ✓ optional | | Item.Icon / Item.Shortcut slots | BYO | leftSection/rightSection | BYO | ✓ NEW | Naming: `Command.Root` aliased as the default export (cmdk/bits-ui parity, eidos convention). Inner parts as named sub-properties: `Command.Input`, `Command.List`, `Command.Item`, `Command.Item.Icon`, `Command.Item.Shortcut`, `Command.Dialog`, … Variant set: only `size: 'sm' | 'md' | 'lg'` (drops xs/xl — palette UX needs comfortable hit targets, matches air baseline). Shortcut binding: cross-platform `mod` token (Cmd on macOS, Ctrl elsewhere). Parses `'mod+k'`, `'ctrl+shift+p'`, etc. Window keydown listener registered on mount, cleaned up on unmount. Pass `null`/ undefined to disable. Recipe: `command.css` paints palette container, input row, scrolling List/Viewport, item rows (with highlighted [data-selected] state), icon + shortcut slot layout, group heading, separator, empty state. Loading progress is a 2px bar under Input with `--_command-loading- progress` for the fill. Inside Command.Dialog the panel chrome comes from dialog.css. Demo: 6 tabs (Live/API/Morfo/Sema/Recipe/A11y) — Live wires every soma + eidos prop + Dialog mode with shortcut input. Two stages: inline palette + dialog opener with shortcut hint. Three code snippets (soma, eidos inline, eidos dialog). Gotcha note: command.svelte renames the children prop to `bodyContent` on destructure to avoid shadowing the inner `{#snippet children}` — same infinite-recursion trap noted in `feedback_button_consumer_pattern.md`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
9def877dbd |
feat(eidos): port Menubar (desktop File/Edit/View bar)
#129. Eidos wrapper + types + recipe + canonical-depth demo. Soma owns active-menu state, cross-menu ArrowLeft/Right navigation, hover-switch when active, focus return on Escape, typeahead, RTL. Eidos paints the bar surface and trigger states; per-menu Content reuses the dropdown-menu recipe (Item / Sub / Checkbox / Radio chrome + the shared menu-indicator partial from `a2709476`) because soma already re-exports the DropdownMenu parts for those slots. Reference parity: | Feature | Radix | bits-ui | ark-ui | this port | |---|---|---|---|---| | Root provider w/ active-menu binding | ✓ | ✓ | ✗ (Menu only) | ✓ | | Menu / Trigger / Content | ✓ | ✓ | ✓ | ✓ | | Item / Group / GroupHeading / Separator | ✓ | ✓ | ✓ | ✓ | | CheckboxItem / CheckboxGroup / RadioGroup / RadioItem | ✓ | ✓ | ✓ | ✓ | | Sub / SubTrigger / SubContent | ✓ | ✓ | ✓ | ✓ | | Cross-menu ArrowLeft/Right (with loop) | ✓ | ✓ | n/a | ✓ | | Auto-open on hover when active | ✓ | ✓ | n/a | ✓ | | Typeahead | ✓ | ✓ | ✓ | ✓ | | Arrow part | ✓ | ✓ | ✓ | ⚠ dropped — menubar idiom anchors to bar edge | Parts shipped (15): Provider, Menu, Trigger, Portal, Content, Item, Group, GroupHeading, Separator, Arrow (re-exported), CheckboxItem, CheckboxGroup, RadioGroup, RadioItem, Sub, SubTrigger, SubContent. The inner 12 (Item onwards) are re-exports of the eidos DropdownMenu wrappers so the menu-indicator partial applies automatically — no recipe duplication. Variant set: only `size: 'sm' | 'md' | 'lg'` (xs would crowd triggers, xl unnecessary for a chrome bar). Size cascades from Menubar root to per-menu Content via a Svelte context (`menubar-context.ts`, `getMenubarSizeContext` / `setMenubarSizeContext`). Cross-layer: eidos Menubar reuses eidos DropdownMenu internals — the recipe doesn't redefine Item/Sub/Checkbox/Radio. Menubar.Content stamps `data-size` AND inherits `data-dropdown-menu-content` (soma's MenubarContent specializes DropdownMenuContent), so dropdown-menu.css paints the panel chrome and our menubar.css only declares the bar surface + trigger states. Workaround note: menubar-content.svelte casts rest props through `Record<string, unknown>` to bypass a TS "union too complex" depth limit triggered by soma's inline-augmented MenuContentProps + HTML attribute union. Runtime types are correct. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |