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 }
615 Commits (b1b1e39dc0fd4f9dfe0dea654b9f7eff4b33ac8a)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
b1b1e39dc0 |
fix(carousel): chevron direction follows orientation
Vertical carousel still rendered horizontal chevrons (< / >) for the prev / next triggers, which read as wrong-direction even though the click handlers worked correctly. The user complaint "varios clicks" was the visual mismatch making each click look like it did nothing — value WAS advancing index 0→1→2 on each click but the icons pointed sideways in a vertical rail. - Added `orientation` to the eidos carousel context so the prev / next trigger wrappers can pick the right chevron without passing props. - Re-exported `Direction` + `Orientation` from soma's carousel module so the eidos context can type-check them. - Prev trigger now uses `direction='up'` in vertical, `'left'` in horizontal. Next trigger mirrors with `'down'` / `'right'`. Verified: index goes 0→1→2 on successive clicks; chevrons now show `^` (top) and `v` (bottom) in vertical orientation. 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 |
|
|
67726cb6f3 |
fix(soma navigation-menu): suppress click-close right after hover-open
Bug: hovering a NavigationMenu trigger opens its Content after the configured \`openDelay\` (default 200ms). If the user then clicked the trigger — naturally, after the open is visible — the click handler saw \`isOpen=true\` and called \`closeNow()\`, shutting the menu the user just opened. Net effect: the first click appears to open AND close the menu in one go. Repro (commit before fix): - hover for ≥200ms (or any delay long enough to trigger \`scheduleOpen\`) - menu opens - click the trigger - menu closes within milliseconds Fix: track \`lastOpenedAt\` per NavigationMenuTriggerProvider via a root \`\$effect\` that stamps the timestamp whenever \`isOpen\` transitions to true (regardless of whether the open came from hover, click, or keyboard). The click handler checks \`Date.now() - lastOpenedAt < 250\` and silently ignores the close-toggle in that window — treating the click as "commit the open" intent, not "toggle shut". Two-click toggle behavior is preserved: a click ≥250ms after the open still closes the menu (verified: openBefore=true, stillOpenAfterClick= true, closedAfter2ndClick=true). The 250ms threshold is slightly above the default openDelay (200ms) so the protection covers the typical hover-then-click sequence without making the toggle feel sluggish. 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 |
|
|
fd34fa535c |
fix(eidos navigation-menu): Content floats as popover, not inline
Bug: Content panels were rendering inline below their Triggers, pushing the bar's flex layout. They should float anchored to the Item the same way Radix / bits-ui mega-menus do. Two changes: 1. **Item** — was `flex-direction: column`, which made Content stack below Trigger in the flex flow. Switched to inline-flex with `align-items: center`. `position: relative` stays so it remains the anchor element. Vertical orientation gets `display: flex`. 2. **Content** — added `position: absolute; inset-block-start: calc(100% + var(--space-1)); inset-inline-start: 0; z-index: 50`. The panel now sits 4px below the trigger row, left-aligned to the Item (extends via `min-inline-size: 14rem` past the trigger), and stacks above the rest of the page. Vertical orientation flips the anchor: `inset-block-start: 0; inset-inline-start: calc(100% + var(--space-1))` so the panel opens to the right of the trigger column. Verified in browser: contentTop=623, triggerBottom=619, position=absolute, z-index=50, bar layout no longer pushed. 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 |
|
|
830025b4ad |
fix(eidos listbox): add max-block-size + overflow-y auto
The Listbox container grew unbounded when items overflowed — long lists just stretched the parent layout. Adds: - Default `max-block-size: min(60vh, 22rem)` via `--_listbox-max-height` token (consumer can override via inline style). - `overflow-y: auto` + `overscroll-behavior: contain` so the listbox becomes its own scroll context. - Horizontal orientation switches axes: removes the block cap and uses `overflow-x: auto` instead so a single-row list never clips. Verified: with 8 items at md size, container caps at 352px with scrollHeight 378px — scrollbar present and working. 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 |
|
|
0afcabee8b |
feat(eidos): port LinkPreview (Radix HoverCard equivalent)
#128. Eidos wrapper + types + recipe + canonical-depth demo. Trigger renders as `<a>` (navigational anchor); on hover/focus after `openDelay` (default 700ms) the Content panel reveals with SafePolygon bridging trigger and content so the pointer can travel without dismissing. Non-modal, no-op on touch (link follows directly). Reference parity: | Feature | Radix HoverCard | bits-ui LinkPreview | ark-ui HoverCard | this port | |---|---|---|---|---| | open with bind: | ✓ | ✓ | ✓ | ✓ | | openDelay (700ms) / closeDelay (300ms) | ✓ | ✓ | ✓ | ✓ | | disabled | ✗ | ✓ | ✓ | ✓ | | Trigger as `<a>` | ✓ | ✓ | ✗ (`<button>`) | ✓ | | Portal + Arrow | ✓ | ✓ | ✓ | ✓ | | Floating layer (side/align/offsets/collision) | ✓ | ✓ | ✓ | ✓ | | customAnchor | ✗ | ✓ | ✗ | ✓ | | ContentStatic | ✗ | ✓ | ✗ | ⚠ deferred | | lazyMount | ✗ | ✗ | ✓ | ⚠ deferred | Naming choice: `LinkPreview` (matches air + bits-ui), not `HoverCard`. Trigger semantic is navigational anchor — for a button-triggered hover-card use `<Popover>` with `openDelay` or pass `child` snippet to bypass the default `<a>`. Variant set: only `size: 'xs'..'xl'` (no ControlVariant — LinkPreview is a surface, not actionable). Defer color/intent per the canon rule that surfaces are uncolored. Parts shipped: Provider, Trigger, Portal, Content, Arrow. Recipe: `link-preview.css` with anchor underline (subtle, intensifies on hover/open), content panel chrome (border + surface bg + shadow), arrow fill matching panel bg, slide+fade entrance per `data-side`, reduced-motion skip. Demo: 6 tabs (Live/API/Morfo/Sema/Recipe/A11y) at canonical depth per DEMO_AUTHORING_GUIDE — Live wires every Provider+Content prop with the right chip set, eidos+soma snippets generated, all 6 ARIA contract rows + keyboard + pointer tables in A11y tab. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
b414ec2b85 |
docs(CONTINUE): refresh for next session — #118 + #120 + #127 closed
Updates CONTINUE.md to reflect the segunda-mitad sprint of 2026-05-23: the 7 commits between |
5 months ago |
|
|
0b55be5c36 |
refactor(combobox + search-field): ClearButton/Clear render <Button>
Closes #120. The two icon-only clear affordances now render as eidos `<Button>` via soma's `child` snippet, retiring the per-recipe chrome (border, padding, focus ring, hover, dimensions) in favour of button.css. The X glyph is provided through Button's `icon` snippet slot; `iconOnly` collapses the body and keeps the label sr-only. Both narrow `variant` to ControlVariant (`'surface' | 'outline' | 'ghost'`) per DEMO_AUTHORING_GUIDE §12.7 — same 3-set as DropdownMenu.Trigger and AlertDialog.Cancel. Default `variant='ghost'` (subtle X that reveals chrome on hover) which is the canonical clear affordance. Defaults: - Combobox.ClearButton: ghost · xs · iconOnly · color=neutral - SearchField.ClearTrigger: ghost · sm · iconOnly · color=neutral Per-component CSS gutted to the empty-state visibility rule only — `[data-…][data-empty] [data-…-clear-…] { opacity: 0; pointer-events: none }` stays because hiding the X when there's nothing to clear is component-specific behavior, not Button chrome. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
32598d7cf3 |
fix(alert-dialog): narrow Action.variant to canonical SelectionVariant (3)
Stops the chip-count drift introduced in \`c98510af\`. AlertDialog.Action
was exposing all 6 ButtonVariant chips, breaking the 3-chip rhythm the
rest of UIX follows. Per DEMO_AUTHORING_GUIDE §12.7 every chip-group
must enumerate the full union the type declares — so the fix is to
narrow the *type*, not truncate the chips.
\`AlertDialogActionVariant = Extract<ButtonVariant, SelectionVariant>\`
imports the canonical set from \`eidos/lib/types.ts\` (`solid | outline |
ghost`), which is the right shape for a primary action button:
- keeps 'solid' as the CTA default
- 3-chip parity with Cancel (ControlVariant), DropdownMenu.Trigger
(ControlVariant), and the rest of the form-control surface
- no invented vocab — uses the existing canon set named for exactly
this use case ("selection chips / toggles" semantically maps to
"selection / action buttons")
Demo array drops from 6 to 3 chips; API row updated to reflect the
narrowing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
c98510aff7 |
demo(alert-dialog): expose variant/size chips for Action and Cancel
Surfaces the Button visual props that AlertDialog.Action and
AlertDialog.Cancel now forward (post `c6f063dc`) so the demo isn't
lying about the component's API:
- Action: 6 chips (full ButtonVariant — primary CTA), 5 size chips.
Intent is NOT exposed as a separate chip because it inherits from
the dialog's own `intent` prop (already a Live control above).
- Cancel: 3 chips (ControlVariant — narrowed per
DEMO_AUTHORING_GUIDE §12.7), 5 size chips. Always neutral palette
regardless of dialog intent.
Live preview wires both bindings into the rendered Action/Cancel; API
tables list the new props with type, default, and the snippet shape
(`Snippet<[{ props; intent }]>` for Action's `child`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
c6f063dc6a |
refactor(alert-dialog): Action/Cancel render <Button>; intent flows via snippet
#120 second pass. AlertDialog.Action and AlertDialog.Cancel now render eidos `<Button>` internally via soma's `child` snippet, retiring the per-recipe Toggle-token chrome and the CSS ancestor cascade (`[data-dialog-content][data-color='risk'] [data-alert-dialog-action]`). Intent propagation: soma's `AlertDialogActionProvider` exposes a new `dialogIntent` derived getter that reads `DialogProvider.opts.intent`. The Action component passes it to the `child` snippet alongside `props` (via `WithChildNoChildrenSnippetProps` so plain `children` stays a regular Snippet). The eidos Action wrapper forwards it to Button as `intent={intentOverride ?? dialogIntent}` — so `<AlertDialog intent="risk">` automatically paints a red Action without any cascade selector. Defaults: - Action: variant='solid', intent inherits from dialog - Cancel: variant='outline', intent='neutral', color='neutral' so it reads as gray-outlined regardless of the dialog's intent (canonical "this is the safe choice" cue) Cancel narrows variant to ControlVariant (3 chips: surface/outline/ghost) per DEMO_AUTHORING_GUIDE §12.7. Action keeps the full ButtonVariant because it's the primary call-to-action. alert-dialog.css gutted to a doc-only stub; all chrome now comes from button.css. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
a2709476f4 |
style(menu-indicator): align with Checkbox / RadioGroup visual
Menu CheckboxItem / RadioItem now mirror the standalone Checkbox /
RadioGroup chrome:
- off: outlined box / circle (transparent fill, neutral border)
- on : filled solid accent + white ✓ glyph (checkbox) or white inner
dot (radio) — same shape as `<SvgCheck>` and RadioGroup.Indicator
The check glyph uses `mask-image` with the same `M5 12.5L10 17.5L19 7`
path as `<SvgCheck>` so the menu item reads visually identical to a
standalone `<Checkbox checked>`. Background-color carries the glyph
colour via `--_menu-indicator-glyph` (`--color-content-on-solid`),
matching how Checkbox sets `--_checkbox-palette-text` over the solid
accent fill.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
4b5bd9f35d |
refactor(dropdown-menu): remove unused modal; Trigger→Button; canon vocab fixes
Three related fixes touching the menu surface: 1. **Remove `modal` from DropdownMenu** (#127). The prop was declared in soma but never wired to anything — no overlay, no pointer-events blocker, no scrim. ark-ui / Headless-UI / Chakra all treat menu as non-modal by convention; Dialog/Drawer/AlertDialog cover blocking semantics. Drop it from soma (types, provider opts, component, test fixture), the eidos doc comment, the menubar consumer, and the dropdown-menu demo (state, control, snippet, API table, parity table). 2. **DropdownMenu.Trigger now renders `<Button>` internally** (#120 first consumer). Default variant='surface', size='md', intent='neutral'. `variant` is narrowed to ControlVariant (`'surface' | 'outline' | 'ghost'`) per DEMO_AUTHORING_GUIDE §12.7 so the trigger stays consistent with the form-control surface (Field/Select/Combobox) instead of inheriting Button's wider 6-variant set. The eidos demo exposes variant/size/intent chips that match the canonical narrowed vocab (Intent: neutral · affirm · fulfill · risk · threat · loss). Trigger's per-recipe CSS dropped — chrome comes from button.css. 3. **Menu-item unchecked indicators** (follow-up to #118). CheckboxItem and RadioItem now paint an empty outlined box / circle even when unchecked, so the user can tell at a glance which rows are checkable. Checked state fills with the accent color via the same `::before`. The reserved padding slot (26px) keeps row text from shifting when state flips. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
0e9628b8f0 |
refactor(eidos): extract shared menu-item indicator partial
Unify the four pseudo-element indicator recipes — context-menu and
dropdown-menu × {checkbox-item, radio-item} — into a single partial at
`lib/menu-indicator.css`. The four selectors paint identical geometry
(0.875rem rounded square or circle at the leading inset) so a single
source keeps them in lockstep.
Tokens are exposed as private vars (`--_menu-indicator-{size,radius,
color,inset,mask}`) so a consumer can override per-component without
re-implementing the recipe.
Import order matters: the partial must load AFTER the menu recipes so
its `padding-inline-start: calc(...)` wins over the row rule's
`padding-inline` shorthand at equal specificity. Verified in browser
on both /uix/components/dropdown-menu and /uix/components/context-menu:
checked checkbox-items render 14×14 purple rounded squares, radio-items
render circles, and all rows reserve the same 26px slot so toggling
state doesn't shift text.
Out of scope: Checkbox / RadioGroup / Select.ItemIndicator — those use
`[data-*-indicator]` DOM parts with SVG markers and xs/sm/md/lg/xl size
scales, an architecturally different pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
dc03773ca5 |
docs(CONTINUE): refresh for next session — Button + canonical demos done; #118 #120 open
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
a804c39208 |
feat(eidos): Button + 9 demo rebuilds + picker recipe polish
Build: - New <Button> compound (morfo + soma + sema + eidos): 6 variants (solid/soft/surface/outline/ghost/plain), 8 colors, 5 sizes, optional icon/endIcon snippets, loading state with built-in Spinner, asChild Slot pattern, intent-driven sema (commit-action with fromProp:intent). Demo rebuilds (canonical depth per DEMO_AUTHORING_GUIDE): - context-menu, dropdown-menu, pin-input, alert-dialog, toggle-group: full per-part Morfo contracts, reference-parity tables, ARIA contract tables, keyboard tables, composition controls. - color-field, time-field, time-picker, time-range-picker, color-picker, month-grid, year-grid: same canonical treatment; per-part data/aria/ keyboard iteration of raw morfo, reference parity vs react-aria/ ark-ui/Chakra/bits-ui/Mantine. Picker recipe polish: - PickerShell.Clear/Cancel/Close refactored to use <Button size="xs"> (affects 5 pickers — date / date-range / time / time-range / color). - TimePicker + TimeRangePicker: popover condensed (min-width 18rem -> 14 / 32 -> 28), per-channel accent (hour=primary, minute=secondary, second=neutral; end variant muted), ruler off by default, hour ruler cleaned to 0/6/12/18/23. - ColorPicker: typography token --_color-picker-trigger-font-size that scales with data-size (10/12/14px); autoFormat default true; Area recipe override for [data-x-channel='hue'] with rainbow gradient + saturation/brightness Y overlays. - ValueText eidos wrapper: fix children snippet bug that suppressed soma's displayValue fallback. - date-field.css: segment editing affordance (cursor + hover + active ring) via new --_date-field-accent-element token across 8 color cascades. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
ec7cdd60b4 |
fix(date-range-field): canonical demo + recipe specificity + DateField overlay
User audit (2026-05-23) caught two real defects in this component:
1. Demo was a 386-line skeleton — switches used the made-up
`data-control-kind="switch"` attribute, props like granularity /
locale / hideTimeZone / per-endpoint readonlySegments were not
wired, API / Morfo / Recipe / A11y tabs were one-sentence stubs.
Canonical reference demos are 700–1200 lines.
2. Recipe rule `[data-date-range-field-input][data-active]
[data-date-field]` matched nothing — soma overlays
`data-date-field-input` on the SAME element as
`data-date-range-field-input` (no nested `[data-date-field]`).
Fixes:
- Eidos `<DateRangeField>` root now overlays
`data-date-field=""` on top of `data-date-range-field`. Lets the
full `date-field.css` cascade (chrome, size, variant, color)
apply inside the range with zero recipe duplication. Documented
in the wrapper comment.
- `date-range-field.css` simplified to layout-only:
[data-date-range-field][data-date-field] (specificity bump
beats date-field's base `display: inline-flex` rule)
grid: 1fr · auto · 1fr × 2 rows
label spans the full first row
separator via `::before` in the middle grid cell
endpoints placed in columns 1 and 3 via `data-endpoint='start' / 'end'`
No recipe duplication; date-field paints input chrome.
- Demo rebuilt to canonical depth (1008 lines, +622):
Live tab: 6 layered subsections (value shape / locale + dir /
flags / per-endpoint readonly segments / eidos chrome /
actions), 11 switches in canonical `<span data-uix-switch>`
style with state-text labels, 9 chip groups (value profile,
kind, granularity, hourCycle, hideTimeZone, locale × 6
locales, dir, size × 5, variant × 3, color × 6), reset /
clear actions, reactive soma + eidos snippets reflecting
every control.
API tab: full prop table per part (Root with 21 props, Label,
Input, Segment), reference parity table vs Bits UI / Ark UI
/ react-aria / Chakra v3.
Morfo tab: header table + parts overview + per-part data /
aria / keyboard tables (driven by raw morfo, cast to a
narrowed shape) + events table.
Sema tab: passive justification + delegation table showing
per-endpoint DateField runtimes.
Recipe tab: selector classification (morfo / eidos) + chrome
attribution.
A11y tab: per-segment keyboard + ARIA contract tables.
- Demo `{#each segments}` keyed by `(i)` (was already fixed, kept).
Lesson registered in
`C:\Users\dev\.claude\projects\G--dev-svelte-vicen\memory\feedback_demos_must_be_canonical_depth.md`
and referenced from `MEMORY.md`: ALWAYS read DEMO_AUTHORING_GUIDE.md
+ a reference demo (date-field 716L, drawer 737L, date-range-picker
1238L) BEFORE writing. Compare against reference libs first.
Verified visually at /uix/components/date-range-field — DOM has
both endpoints side-by-side, em-dash separator in middle column,
date-field chrome applied to each input, canonical control style
across all 6 subsections.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
18edecba5b |
fix(date-range-field demo): key each segments by index, not seg.part
Soma's segments array uses `'literal'` as the part for every visual
separator (e.g. the `/` between dd / mm / yyyy). Keying the {#each}
by `seg.part` produced duplicate keys → `each_key_duplicate` runtime
error at the second `literal` separator. Switch to positional key
`(i)` — segments don't reorder, so positional is stable and correct.
Snippet template strings updated to match so the visible code in
the Live tab reflects the right pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
754754d4a4 |
feat(date-range-field): port from soma to eidos (Tier 2 sprint, 1/3)
Two-endpoint date entry. Mirrors DateField × 2 with shared validation and a single label naming the whole range. Each Input scopes its own DateField context internally, so segments inside re-use the existing date-field recipe automatically. Eidos surface: - variant / color / size — same vocabulary as <DateField>, cascaded via data-* on the root to both endpoint inputs and their segments - Re-uses DateField.Segment (zero duplication) Recipe (date-range-field.css): - Label + control row stack - Em-dash separator rendered via `::before` on the end Input (no manual `<span>—</span>` needed) - Endpoint-aware tint via `data-active` on the holding input Morfo: - scope: ['soma'] → ['soma', 'sema', 'eidos'] - apg URL: Spinbutton (per segment) - Passive at this contract layer (events come from the two endpoint DateField runtimes); README documents the classification Sidebar nav: 'Date range field' added under Forms (next to Date field). Tier 2 sprint progress: 1/3 (date-range-field next: time-range-field, range-calendar). Checks: svelte-check 0 errors, component:audit 91/91 PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
2d8599b62f |
fix(dropdown-menu, context-menu): keep menu open on CheckboxItem / RadioItem activation
Soma's MenuCheckboxItem / MenuRadioItem default `closeOnSelect=true`
(matches Radix Primitives), but for toggle-state items that's the
wrong UX default — the user wants to flip multiple checkboxes or
switch a radio choice without re-opening the menu each time.
Override the default in the eidos wrappers for both DropdownMenu and
ContextMenu (4 components total: CheckboxItem + RadioItem in each).
Activating Item (plain command) still closes by default; only the
state-toggle parts stay open.
Consumers who genuinely want close-on-toggle can pass
`closeOnSelect={true}` explicitly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
207774714a |
feat(pin-input): port from soma to eidos (Tier 1 sprint, 5/5 — DONE)
Multi-cell OTP / verification code input. Two parts (Provider, Cell) with a snippet-driven cell iteration. Architecture: - A transparent absolutely-positioned <input> captures every keystroke, paste and IME event (soma). - Visible bordered cells follow the hidden input's caret via data-active / data-filled emitted by the morfo. - Single-input approach (shadcn / Bits family) — preserves paste distribution, autocomplete='one-time-code' for SMS autofill on iOS/Android, and IME quality. Eidos surface: - size: 'sm' | 'md' | 'lg' | 'xl' (responsive), cascades cell dimension + glyph size tokens to all cells - Recipe paints square bordered cells with focus ring tracking data-active, subtle bg lift on data-filled, threat-toned border when [data-invalid] cascades from the provider Morfo: - scope: ['soma'] → ['soma', 'sema', 'eidos'] - apg URL added (Textbox pattern — OTP has no formal ARIA pattern) - Passive at the morfo layer (no events array; perceptual feedback via the commit family base when onComplete fires) - README documents the passive classification Comparativa: aligned with shadcn / Bits UI (single hidden input). Ark and Chakra use one input per cell which loses paste / IME quality. Sidebar nav: 'Pin input' added under Forms (next to Tags input). === Sprint Tier 1 — DONE === 1. toggle-group |
5 months ago |
|
|
21eb8a6264 |
feat(context-menu): port from soma to eidos (Tier 1 sprint, 4/5)
14-part compound — DropdownMenu surface minus CheckboxGroup. Same visual recipe vocabulary; the only differences vs DropdownMenu are the trigger surface (a `<span>` wrapping right-clickable content rather than a `<button>` toggle) and cursor-anchored positioning (soma's floating layer handles that). Parts: Provider · Trigger · Content · Arrow · Item · Group · GroupHeading · Separator · CheckboxItem · RadioGroup · RadioItem · Sub · SubTrigger · SubContent. Recipe (context-menu.css): - Panel chrome + item rows identical to dropdown-menu - Trigger gets a subtle `data-state='open'` dashed outline as a "this region summoned the menu" affordance - Recipe duplication over aliasing — each component evolves freely without CSS cascade coupling Sidebar nav: 'Context menu' added under Overlays. Morfo scope: ['soma'] → ['soma', 'sema', 'eidos']. Sprint progress: 4/5 (toggle-group, alert-dialog, dropdown-menu, context-menu). Last is pin-input. Checks: svelte-check 0 errors, component:audit 91/91 PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
f19b5c7270 |
docs(uix): add Toggle group · Alert dialog · Dropdown menu to sidebar nav
Wires the three Tier 1 sprint components into the IA: - 'Toggle group' under Controls (next to Toggle) - 'Alert dialog' under Overlays (next to Dialog) - 'Dropdown menu' under Overlays (next to Popover) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
4e477e637b |
feat(dropdown-menu): port from soma to eidos (Tier 1 sprint, 3/5)
Full 15-part compound — pass-through wrappers over soma with the canonical visual recipe. Parts: Provider · Trigger · Content · Arrow · Item · Group · GroupHeading · Separator · CheckboxItem · CheckboxGroup · RadioGroup · RadioItem · Sub · SubTrigger · SubContent. API parity with Bits UI (richest reference): CheckboxGroup + RadioGroup binding arrays / scalars, nested Sub-menus, Arrow pointer. Radix Primitives + Ark UI lack CheckboxGroup; Chakra v3 + Radix Themes lack Arrow. Recipe (dropdown-menu.css): - Uniform chrome across Content + SubContent (panel shadow, border, padding, max-height) - Items share one set of tokens (hover/focus highlight, disabled opacity, indicator slot via `::before` for Checkbox/Radio data-state='checked') - SubTrigger gets a chevron via `::after` - GroupHeading: muted uppercase, micro-tracked - Separator: 1px subtle divider - Enter animation on `data-state='open'` No `variant` / `color` / `size` knobs at the eidos layer — menus are themselves the visual variant of "list of actions" within an app. Per-item destructive coloring deferred to next pass (cascade via item's `data-color` attr). Morfo scope: ['soma'] → ['soma', 'sema', 'eidos']. Sound comes from the family bases (`emerge` for open/close, `commit` for select) — no per-component cascade needed. Demo: 6-tab template, items hooked to `lastSelected` tracker, all three groupings demoed (Items / CheckboxGroup / RadioGroup / Sub). Checks: svelte-check 0 errors, component:audit 91/91 PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
d5630a528f |
feat(alert-dialog): port from soma to eidos (Tier 1 sprint, 2/5)
Modal Dialog variant with role=alertdialog, forced modal=true, and click-outside disabled. Soma delegates to the Dialog runtime (variant='alertdialog') so DOM markers are Dialog's (data-dialog-*) — eidos reuses Dialog's recipe for chrome and adds two alert-dialog-specific buttons: Action + Cancel. Eidos shape: - Provider, Content, Action, Cancel — alert-dialog-specific wrappers - Trigger, Overlay, Title, Description, Header, Footer, Portal — re-exported from Dialog's eidos barrel (zero duplication) Recipe (alert-dialog.css): - Only styles Action + Cancel - Buttons consume Toggle palette tokens (same "interactive button" vocabulary used by <Toggle> and <ToggleGroup.Item>) - Auto-color cascade: Action picks up the dialog's data-color so a risk/threat-flagged dialog gets a red/amber Action button without manual recipe code - Cancel stays neutral so the safe-choice contrast is preserved Morfo: - scope: ['soma'] → ['soma', 'sema', 'eidos'] - The morfo declares Action + Cancel parts only (Provider is virtual, other parts inherited via Dialog runtime delegation) - Classified passive at this contract level (Dialog owns the events); README documents this in §"Passive justification" Demo: 6-tab canonical template with intent / size / position chips, soma + eidos snippets, references comparison table vs Radix Primitives / Bits UI / Ark UI / Chakra v3 / Radix Themes / shadcn. Checks: svelte-check 0 errors, component:audit 91/91 PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
61ffbade46 |
feat(toggle-group): port from soma to eidos (Tier 1 sprint, 1/5)
First component in the soma→eidos porting sprint. Two-part compound (Provider + Item), modeled on radio-group's structure. Eidos surface: - variant: 'solid' | 'outline' | 'ghost' — mirrors <Toggle> - color: 7 tones, propagated to items via data-color cascade - size: xs/sm/md/lg/xl (responsive) - attached: boolean — segmented control affordance (shared borders, active item z-indexed above neighbors) - block: boolean — items grow to fill the inline-size - iconOnly: per-item flag — square aspect, sr-only label Items consume the Toggle palette token vocabulary (--toggle-*) so a standalone <Toggle> and a <ToggleGroup.Item> at the same variant × color × size are visually identical — single source of truth for the "toggle button" recipe. Morfo scope fix: ['soma'] → ['soma', 'sema', 'eidos']. Adds apg URL (WAI-ARIA Button pattern) so the audit's A-1.4 rule passes. The commit-toggle event's sound comes from the `commit` family base — no per-component cascade needed. Demo: canonical 6-tab template (Live / API / Morfo / Sema / Recipe / A11y) with every prop exposed as a chip/switch, soma + eidos snippets update live, sema "▶ play" button on the commit-toggle event. Reference comparison vs Radix Primitives / Bits UI / Ark UI / Chakra v3 / Mantine / Radix Themes / shadcn documented in README §Comparativa. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
4324cb489d |
fix(splitter): register sema scope + cascade so handle-pick/drag actually sound
Two missing registrations meant the splitter sema cascade from the
previous commit never reached the engine — handle-pick and handle-drag
were silent, only commit-set sounded (because the `commit` family has
a sound payload baked into its SEMA_MAP base, while `handle` only has
haptic and depends on the cascade for sound).
Fixes:
1. morfo scope: ['soma'] → ['soma', 'sema', 'eidos']. Without 'sema'
the per-component cascade rules in src/uix/sema/components/splitter.ts
are never honored.
2. web/routes/uix/+layout@.svelte: add splitterSema to the
`components: [...]` array passed to defineEngineSemantic. The
sema engine only loads cascade packs it's explicitly given, so
forgetting this line silently drops all splitter cascade rules.
Verified in browser at /uix/components/splitter via Live tab (real
pointer interaction, AudioContext.createOscillator counter):
handle-pick → 2 oscillators (handle.pickup.air, gain 0.10)
handle-drag → 16 oscillators across 8 throttled emits — the
dynamic resolveSplitterDragSound curve is playing
per-frame as expected
commit-set → 2 oscillators (handle.snap.chime, gain 0.14)
Note: the Sema tab's static "▶ play" button still shows 0 oscillators
for handle-drag — same as Slider's demo. By design: that button calls
engine.emit() directly without the per-emit `signal.overrides` that
soma injects during real drag. The dynamic curve IS the payload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
59505e6da2 |
feat(splitter): sema + dynamic drag-signal sound infrastructure
Splitter previously had no sema bindings — handle-pick / handle-drag /
commit-set fired silently. Closes the asymmetry with Slider and Drawer.
Sema (src/uix/sema/components/splitter.ts):
- handle-pick → sound('handle.pickup.air', gain 0.10) + tick haptic
- handle-drag → channels: ['sound', 'haptic'] (channels-only; per-emit
signal.overrides from soma own the dynamic curve so cascade doesn't
clobber them)
- commit-set → sound('handle.snap.chime', gain 0.14) + tap haptic
Sound resolver (src/uix/sema/sounds.ts):
- resolveSplitterDragSound — heavier sonic register than slider since
splitters move whole panels, not a thumb:
pitch 480..820 Hz (slider: 760..1280)
centroid 2400..4800 (slider: 3600..6800) — less brilliant
roughness 0.006 (slider: 0.003) — woody texture, not metallic
envelope attack 16 / decay 72 / duration 76 — perceptual mass
gain 0.025 + velocity*0.03 — restrained ceiling, long swipes ok
Soma drag-signal infrastructure (splitter-provider.svelte.ts):
- Mirrors slider's frame-throttled signal pipeline:
dragSignalFrame / pendingDragPercent / lastDragSignalAt / lastDragPercent
DRAG_SIGNAL_MS = 72ms throttle (≈14 Hz emit cadence)
- dragSignalOverrides(percent01) computes velocity01 from frame delta,
builds SemaSignatureOverride with sound (via resolveSplitterDragSound)
and haptic (tick scaled by velocity).
- queueHandleDrag / cancelQueuedHandleDrag wrap requestFrame so each
pointermove only schedules one trigger per throttle window.
- onpointerdown calls resetDragSignal(startSizeA/100) to anchor the
velocity baseline at grab time.
- onpointerup / onlostpointercapture / unmount $effect all cancel the
queued frame so we don't fire post-release.
Verified in browser at /uix/components/splitter:
handle-pick → 1 event (family: handle)
handle-drag → frame-throttled across 6 pointermoves
commit-set → 1 event (family: commit, intent: neutral)
Checks: svelte-check 0 errors, 133/133 vitest (splitter + sema),
component:audit 91/91 PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
b504c1c4b7 |
feat(eidos foundation): vertebrate typography via named-style aliases + R-2.7
Single source of truth for typography values that the recipe layer
consumes. The foundation aliases `--font-ui` and `--leading-ui` (read
by ~30 recipe tokens in `lib/recipes/base.ts`) now derive from the
canonical `label` named style instead of carrying duplicate literals:
--style-label-font-family: var(--font-family-primary);
--style-label-line-height: 1.25;
--font-ui: var(--style-label-font-family, var(--font-family-primary));
--leading-ui: var(--style-label-line-height, 1.25);
Chain: typography.ts styles → --style-{name}-* → --leading-ui / --font-ui
→ recipe tokens → component CSS. Editing
`STATIC_TYPOGRAPHY.styles.label.lineHeight` now propagates to every
recipe in one go.
Why not push recipes to consume `--style-{name}-*` directly:
- t-shirt sizes (xs/sm/md/lg/xl) don't map to four semantic buckets
- per-component matices (description/caption/hint) need their own
color / weight / letter-spacing
- ref libraries (Radix Themes, Mantine, MUI, Chakra) all keep
numerical scale for component internals; semantic layer is only for
user-facing typography primitives (`<Text variant="body2">`)
Audit rule R-2.7 (warn): detects literal font-size / font-weight /
line-height / letter-spacing in eidos component CSS. Escape valves:
var(...), numeric identities (0/0px/1), keywords (inherit/initial/
unset), or trailing `/* literal: <reason> */` comment. Current run
flags 6 components with letter-spacing/font-size literals (all
intentional micro-tracking and em-relative; can be annotated case by
case).
Documentation:
- src/uix/eidos/README.md § "Vertebración tipográfica" — two-layer
architecture rationale, alias chain diagram, comparison vs Radix
Themes / Chakra / Mantine / MUI, escape valves
- web/routes/uix/lib/COMPONENT_AUDIT_GUIDE.md § 4.11 — pointer to
R-2.7 + cross-link to the foundation doc
Verified end-to-end in browser at /uix/components/field:
--font-ui → 'Instrument Sans', system-ui, sans-serif
--style-label-font-family → 'Instrument Sans', system-ui, sans-serif
--leading-ui → 1.25
--style-label-line-height → 1.25
computed [data-field-label].line-height → 17.5px (= 14 × 1.25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
c703e2e7ff |
fix(scroll-area): split mount-vs-visibility — the real type='scroll' / 'hover' bug
Root cause: soma/scroll-area-scrollbar.svelte gated the entire <div> on
`state.shouldShow`. For type='hover' / 'scroll', `shouldShow` starts
false, so the bar never reached the DOM. With no DOM node:
- bind:this never fired → scrollbar's `ref` stayed null
- `requestFrame(cb, ref.current)` ran with a null element, so the
`mounted` flag never flipped
- the hover / drag listeners had no element to attach to
- the recipe's `data-state` opacity transition had no element to
animate
- `shouldShow` could therefore never become true → deadlock
Fix — split the concern:
- `shouldMount` (new derived) — true whenever the axis overflows OR
type='always'. Drives the `{#if}` in the part component.
- `shouldShow` — drives `data-state="visible|hidden"`. The recipe
transitions opacity.
The bar now lives in the DOM whenever it could ever be needed, fades
in/out via CSS, and stays a stable hit target for hover / click /
drag. Dropped the `mounted` flag and the requestFrame mount-frame
scheduling — they only existed to suppress a flash that the opacity
transition handles cleanly.
Also dropped the inline `border-radius: inherit` on the thumb so the
recipe's `--scroll-area-thumb-radius` (the new `radius` prop) wins.
Without this the prop was silently overridden to `0px`.
Test updates:
- removed `cancelFrame` assertion (no mount-frame to cancel)
- added `shouldMount` assertion
Verified in browser at /uix/components/scroll-area:
- type='hover' + vertical: bar mounts hidden, fades in on root hover,
fades out after delay
- type='scroll' + horizontal: bar mounts hidden, fades in on scroll,
thumb tracks scrollLeft, fades out after 600ms
- type='always' + both: both bars + corner visible at all times
- size=lg, radius=full: 12px-wide bar, fully rounded thumb
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
e11d3539ef |
feat(scroll-area): add size/radius/scrollbars props + fix scroll-type race
Reference-library parity (Radix Themes, Chakra v3): - size: 'xs' | 'sm' | 'md' | 'lg' (Chakra t-shirt scale) → thickness via --scroll-area-scrollbar-size. - radius: 'none' | 'sm' | 'md' | 'lg' | 'full' (Radix Themes) → thumb curvature via --scroll-area-thumb-radius. - scrollbars: 'vertical' | 'horizontal' | 'both' (Radix Themes) → recipe-level sugar that auto-mounts Scrollbar / Thumb / Corner from the root. Explicit composition still supported (omit the prop). Bug fix — type='scroll' + horizontal sometimes stuck hidden: - `mounted` on ScrollAreaScrollbarProvider is now $state. Previously a plain field, so the `shouldShow` derived didn't invalidate when the requestFrame mount-flag flipped during the first frame. - The reveal-on-scroll $effect now skips its first run via a `firstRun` flag. That first run was the subscription pass with scrollTop/scrollLeft still 0 — it was eagerly calling show() and setting `visible=true` before `mounted` flipped. Subsequent real scrolls then assigned visible=true again (no-op), so shouldShow didn't re-run and the bar stayed hidden until the hide timer expired. Now the bar only reveals on genuine scroll deltas. Demo: chip controls for size + radius, switched to scrollbars prop on the root (drops the explicit Scrollbar/Thumb children), eidos snippet shows the recipe-sugar form, soma snippet still shows the explicit Radix-style composition. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
560ce8f66c |
fix(scroll-area, drawer): horizontal scrollbar visibility + drawer drag affordance
- scroll-area soma: defer the mount-frame request to `$effect` so SSR doesn't hit `dom::window_required` during constructor evaluation. - scroll-area recipe: scrollbars now `position: absolute` (overlay style) so the horizontal track lands at the viewport's block-end instead of flowing below it. `:has` rules reserve corresponding edges when both axes mount; corner switched to absolute as well. - drawer demo: drop the ↑↓←→ glyph pop-outs and `overflow: visible` during dragging. The handle's own pill grip is the affordance — the same shape the splitter uses, no extra chrome. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
bd2e40366a |
feat(layout): Layout Batch 3 — scroll-area, separator, splitter + splitter sema rewire
3 new layout primitives close the air port set (15 / 15 done).
`npm run component:audit`: 91 / 91 PASS, all 3 new PASS with 0 errors.
**scroll-area** — eidos compound wrapper over the existing soma:
`<ScrollArea><ScrollArea.Viewport>{content}</ScrollArea.Viewport>
<ScrollArea.Scrollbar orientation="vertical"><ScrollArea.Thumb />
</ScrollArea.Scrollbar><ScrollArea.Corner /></ScrollArea>`. Mirrors
Radix Primitives / Bits UI shape. Demo 667 lines.
**separator** — visual primitive (`scope: ['eidos']`) with conditional
ARIA: `role="separator"` + `aria-orientation` when not decorative,
`aria-hidden="true"` when decorative. New morfo
`src/uix/morfo/components/separator.ts`. Demo 645 lines.
**splitter** — eidos compound over existing soma:
`<Splitter orientation="horizontal"><Splitter.Panel>{a}</Splitter.Panel>
<Splitter.ResizeTrigger /><Splitter.Panel>{b}</Splitter.Panel>
</Splitter>`. Demo 824 lines.
**Splitter sema rewire — match Slider's handle/drag/commit shape:**
The morfo previously declared only `commit-resize`. Direct-manipulation
primitives (Slider, Splitter) should share the same event vocabulary so
audio/haptic projections feel coherent across the family. Renamed +
expanded to mirror Slider:
- `handle-pick` (family: handle, verb: pick, sequence: coincident,
target: resize-trigger) — fires on pointerdown when user grabs the
trigger.
- `handle-drag` (family: handle, verb: drag, sequence: coincident,
target: resize-trigger) — fires on each pointermove frame.
- `commit-set` (family: commit, verb: set, intent: neutral,
sequence: post, target: provider) — fires on pointerup. The runtime
`commit-set` handler is wired to call the consumer's `onResizeEnd`
callback, so sema and JS callback land at the same moment (parity
with Slider's `onValueCommit`).
Soma provider triggers these via `this.runtime.trigger(name)` in
`onpointerdown` / `onpointermove` / `notifyResizeEnd` (the latter
unchanged externally — internally now routes through the runtime
trigger).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
a08a0a9890 |
fix(aspect-ratio): fill inline axis by default + panel demo uses solid colors
Two bugs surfaced when verifying the panel demo visually: 1. **AspectRatio shrunk to content width inside grid/flex containers.** Grid items default to `width: max-content`, so AspectRatio without an explicit width was sizing itself to its child's intrinsic content instead of filling the slot. The ratio still applied but against the collapsed width, producing a tiny box. Fix: recipe sets `inline-size: var(--box-width, 100%)` so the default is "fill the slot". `width=…` from Box still wins; `maxWidth=…` constrains the upper bound exactly as before. Block-level containers are unaffected (100% of the line box matches their natural width). 2. **Panel demo gradient invisible in dark mode.** The demo used `--color-primary-track` + `--color-affirm-track` which are surface tokens (near-black in dark theme). Swapped to `*-solid` tokens with `--color-content-on-solid` text for high contrast in both themes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
d421f1fb5d |
feat(typography): 8 primitives — Heading, Display, Code, CodeBlock, Kbd, Mark, Highlight, Link
Full canon for the rest of the typography family, following Text
(commit `03531709`) as template. Each primitive ships morfo (passive,
scope eidos, 0 events) + full eidos set (svelte/css/types/index +
README with required sections) + canon 6-tab demo (≥400 lines each).
**Heading** — semantic h1..h6 via `level` prop, visual style decoupled
via `style` prop (defaults to match level but `<Heading level={3}
style="h1">` allowed). Demo 594 lines.
**Display** — always-h1 page-title primitive. Always uses
`style="hero"`. Tracking via literal em values until foundation emits
hero-scale tracking tokens. Demo 546 lines.
**Code** — inline code via `<code>`. `variant: 'plain' | 'soft' |
'outline'` for subtle background chrome. Demo 423 lines.
**CodeBlock** — multi-line `<pre><code>` with optional title +
copy button (button utility, NOT a sema verb — kept out of morfo per
2-of-3 rule). Demo 402 lines.
**Kbd** — keyboard shortcut chrome over `style="code"`. Demo 418 lines.
**Mark** — text highlight via `<mark>`. Palette uses canonical
`ColorRole` intents (default `fulfill` ≈ legacy yellow). Demo 402 lines.
**Highlight** — substring highlighter inside Text. Scans children for
`query` and wraps matches. Same intent palette as Mark. Demo 407 lines.
**Link** — text link via `<a>`. `variant: 'default' | 'subtle' |
'plain'` for underline style, `intent: ColorRole` for color. Passive
on the morfo — hover/active is CSS-only, navigation belongs to the
consumer (router). Demo 448 lines.
**Architectural deviations from air documented per README's
Decisiones section:**
- Heading: semantic level vs visual style decoupling
- Code split into inline `<Code>` + block `<CodeBlock>` (was one
component with `variant='block'` in air)
- Mark/Highlight palette swapped from literal hue names to canonical
ColorRole intents (theme contract)
- Link palette opened to full ColorRole; `quiet`/`emphasized` air
variants dropped in favour of composable patterns
`npm run component:audit`: **90 / 90 PASS**, 0 NEEDS-WORK.
svelte-check: 0 errors, 0 new warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
0353170982 |
feat(typography): Text — POC consuming new --style-* foundation tokens
First typography primitive consuming the named-style tokens added in
`015bd761`. Sets the canon for the remaining typography ports
(Heading, Display, Code, CodeBlock, Kbd, Mark, Highlight, Link).
API:
- `style: 'body' | 'prose' | 'label' | 'caption'` (default `body`).
Selects which `--style-{name}-*` token set the recipe consumes.
- Per-axis overrides via `size`, `family`, `weight`, `color`, `align`.
Each writes a local `--_text-{axis}` CSS variable that wins over
the named style's default via the cascade.
- `italic`, `underline` shorthand booleans.
- `truncate` (single-line ellipsis) + `clamp={N}` (multi-line via
CSS `-webkit-line-clamp`).
- `as` prop switches the tag (default `'span'` — inline composable).
- `tagStyle` escape hatch for one-off inline CSS that the prop
surface doesn't cover (separate from the `style` named-style prop).
Architectural choices documented in README:
- Sizes map to eidos `xxs..xxxl` (not air's `1..12`).
- Families use the eidos slot names (`primary/secondary/mono/display`)
not air's domain aliases (`ui/prose/code`) — coherence with the
foundation contract.
- Colors are restricted to the content-role slots
(`--color-content-*`) to preserve contrast guarantees. Free colors
via `tagStyle`.
- Canvas line-count measurement (air's `text` + bindable `lineCount`)
deferred; CSS line-clamp covers the visual-truncate use case.
Sidebar nav gains a `Typography` group with Text as its first entry.
`npm run component:audit`: 82 / 82 PASS, 0 NEEDS-WORK. svelte-check 0
errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |