## 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>
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>
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>
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>
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>
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>
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>
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>
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>
#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>
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>
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>
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>
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>
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>
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>
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 61ffbade
2. alert-dialog d5630a52
3. dropdown-menu 4e477e63
4. context-menu 21eb8a62
5. pin-input (this)
Checks: svelte-check 0 errors, component:audit 91/91 PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
`STATIC_TYPOGRAPHY.styles` already defined 11 named typography styles
(hero, h1..h6, body, prose, label, caption, code) but the renderer
only emitted families / sizes / weights — the style entries lived
as data with no CSS reflection.
Extend `appendTypographyDeclarations` so each named style emits:
- `--style-{name}-font-family` → `var(--font-family-{family})`
- `--style-{name}-font-size` → `var(--font-size-{size})`
- `--style-{name}-line-height` → literal or `var(--font-line-height-{size})`
- `--style-{name}-letter-spacing` → literal or `var(--font-letter-spacing-{size})`
- `--style-{name}-font-weight` → `var(--font-weight-{weight})` (or numeric)
- `--style-{name}-color` → `var(--color-{dot.path → dash-path})`
Responsive sizes (e.g. `hero.size = { base: 'xxl', md: 'xxxl' }`)
emit the base value into `:root` and per-breakpoint
`@media (min-width: …)` blocks that override the same variables.
Breakpoint thresholds match `$libs/dom/responsive` (sm: 480, md: 768,
lg: 1024, xl: 1280, xxl: 1536).
`renderStaticCss` now iterates `STYLE_BREAKPOINT_ORDER` after the
main `:root` block and appends one media-query block per breakpoint
that has at least one responsive override. The `indentBlock` helper
preserves indentation inside the wrapper.
`base.css` regenerated. svelte-check 0 errors, `npm run component:audit`
81 / 81 PASS unchanged. This is foundation-only; no components consume
the new tokens yet — that lands in the typography port (Text + Heading
+ Display + Code + CodeBlock + Kbd + Mark + Highlight + Link).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses the "should" features documented as gaps in the layout
READMEs from `fdb0e680`. Audit script still reports 77 / 77 PASS,
svelte-check 0 errors.
**Flex**
- `alignContent` prop (multi-line cross-axis alignment). Maps to
`--flex-align-content`; defaults to `normal` in the recipe so it
has no effect when wrap is off. Closes the Radix / Chakra parity
gap.
**Grid**
- `inline` boolean — renders as `display: inline-grid` (paridad con
Flex.inline).
- `columns` / `rows` numeric shorthands — Radix Themes pattern.
Internally resolved to `repeat(N, minmax(0, 1fr))`. `templateColumns`
/ `templateRows` string still wins if both are passed.
- `alignContent` prop — same shape as Flex.
**Shared**
- New `LayoutAlignContent` type added to `_layout/shared.ts`
(covers start, end, center, stretch, space-between, space-around,
space-evenly, flex-start, flex-end, normal, baseline).
**Group**
- Verified `grow` boolean already implemented in `0ec2a57a` /
`591b0885` — no change needed; the README now correctly reflects it.
**Docs**
- Flex + Grid READMEs updated: comparison tables mark previously
missing features as Sí; Gaps section marks them ✅ implementado.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`npm run component:audit` now reports 77 / 77 PASS, 0 NEEDS-WORK,
0 BROKEN.
**month-grid + year-grid READMEs**
Add Baseline / Comparativa / Decisiones / Gaps sections (F-1.1 /
F-1.2 / F-1.3 / F-1.4). Both grids are standalone primitives
extracted from DatePicker's `kind: 'month' | 'year'` views (task
`#70`); they have no air counterpart. Documented Sema event design
(single `commit-set` for value mutation, `nav-step` for paginated
header navigation), the 4×3 grid choice, configurable `decadeSize`
for YearGrid, and known gaps (multi-year range, quick-jump,
localized month names — all deferred).
**Audit script `focusMoveActions` extension**
`A-3.7` was firing on month-grid/year-grid because the script didn't
recognise `next-row`, `prev-row`, `first-month`, `last-month`,
`first-year`, `last-year`, `next-page`, `prev-page` as focus moves.
They ARE focus moves inside their respective grids (same semantic as
`next-day` / `first-day-of-week` inside Calendar). Added to the
focusMoveActions set in `scripts/component-audit.ts` so the
event-vs-keyboard ratio heuristic no longer over-counts.
**time-range-picker**
- README Gaps section rewritten as a table with explicit disposition
markers (`implementar` / `diferir` / `descartar`) per F-1.4.
- Recipe `time-range-picker.css` now styles `[data-readonly]` (lower
opacity + pointer-events: none on trigger/clear) and `[data-invalid]`
(threat-colour border + focus ring) — closes R-1.3 / R-1.4. Both
attrs were declared in morfo but unstyled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second POC for redoing Layout Batch 1 demos. Mirrors `box`'s canonical
shape adapted for a flex container:
- 7 container-side controls (direction, align, justify, wrap, gap,
padding, inline) — chips for unions, number for space tokens,
switch for the boolean.
- API tab separates "Container props (Flex-only)" from "Inherited
from Box" with a forward-link.
- Cross-references: lede + API call out that item-side props
(alignSelf, gridColumn, …) live on the child `<Box>`, and that
Stack / Group are direction-specific helpers.
- Reference comparison row reflects the Radix Themes split (item vs
container) and notes Chakra's `HStack`/`VStack` map to UIX `Stack`.
Adds `src/uix/morfo/components/flex.ts` (scope `eidos`, 1 part, 0
events, mirrors box.ts justification).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POC for redoing Layout Batch 1 demos to follow DEMO_AUTHORING_GUIDE.
Box becomes the reference template for the remaining 7 primitives.
Demo follows the canon shape:
- Header with eyebrow + 4 meta pills (parts, events, props, scope).
- Stage permanently rendered with 3 nested boxes + trace strip
showing live display/padding/gap state.
- 6 tabs in canonical order (Live · API · Morfo · Sema · Recipe · A11y).
- Live tab: 7 controls + reactive soma/eidos snippets.
- API tab: 6 subsections grouped by concern (sizing / spacing /
display+position / flex/grid item / escape hatch / reference
comparison vs radix-themes / chakra / mantine).
- Morfo / Sema / Recipe / A11y tabs each populated with the contract
appropriate to a passive eidos-native primitive.
Architectural fixes (heredados de air, ahora correctos):
- `gridColumn`, `gridRow`, `gridArea` move from `<Grid>` to `<Box>` —
these are item placement props, they belong on the CHILD that
declares its slot, not the container.
- Add `placeSelf` to Box (grid item shorthand for align-self +
justify-self).
- Grid recipe + props updated accordingly; comment clarifies that
item placement lives on Box.
Add `src/uix/morfo/components/box.ts` (scope `eidos`, 1 part, 0 events
with justification comment). Box demo updated lede to point at Flex /
Grid for container-side props (Radix Themes split, not Chakra
everything-on-Box).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visual-only primitives translated from the legacy `air/components/layout`
on the morfo-runtime branch. Architectural adjustments for the eidos
target: drop the `air-` class prefix, target `[data-{name}]` directly,
rename CSS vars to `--{name}-*`, replace `var(--air-space-N)` with the
eidos foundation `var(--space-N)` token, resolve responsive props via
`ActiveEidos.resolve(prop)`.
- `box` — universal box-model utility (size, padding, margin, gap,
position, overflow, flex/grid item props). Every prop maps to a
CSS custom property; unset props fall through via `revert-layer`.
- `flex` / `grid` / `stack` / `group` / `wrap` — layout containers
that render through Box (single `<div>` per primitive, no
semantic-tag prop in this batch). Markers stack as
`[data-box][data-{name}]`.
- `container` — max-width centered (sm/md/lg/xl/full).
- `section` — semantic-padding block with background tone variants.
Shared helpers at `_layout/shared.ts` (formatLayoutSpace /
formatLayoutLength / formatLayoutRaw / pushStyleVar / composeStyle)
plus an optional `Layout.*` namespace barrel for consumers who prefer
that style; the individual `import { Box }` from each component dir
also works.
Each primitive ships a demo at `/uix/components/{name}/` with a live
example, a props table, and a reference comparison row (radix-themes /
chakra-ui / mantine).
svelte-check: 0 errors / 0 new warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switched the multi-mode chips from MUI-style "inline inside the input"
to the Chakra v3 multi-select pattern — chips render as a separated
band ABOVE the Control. Two wins:
- The dropdown popover (opens downward) never covers the selected
chips, which sit comfortably above the input.
- The Control stays a normal-height single-line input. No layout
shifts, no absolute-positioned affordances, no `display: contents`
gymnastics, no input flex tuning.
Reverts the inside-the-Control CSS gymnastics from `30e9517a` /
`64bbe9b1` / `87e3d68f` (flex-wrap, padding-right reservation,
absolute clear+trigger, input flex basis). `<Combobox.SelectedTags>`
now renders as a plain `flex-wrap` row with chip gap and a margin-
block-end separating it from the Control. Demo composes
`<Combobox.SelectedTags />` BEFORE `<Combobox.Control>` when
`mode === 'multiple'`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`<Combobox.SelectedTags>` was a nested flex container so the input
always wrapped to its own row below the entire chip block — wasting
the empty space on the last chip row.
Use `display: contents` so each chip becomes a direct flex child of
`<Combobox.Control>`. Chips + input now share the same wrap context:
the input flows on the same row as the last chip when there's room.
`[data-empty]` still toggles to `display: none` so empty-state still
collapses correctly (and the `:has(...:not([data-empty]))` selector
that pins clear/trigger absolutely keeps working).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With many chips wrapping into 3+ rows, the previous `top: 50%` +
translateY centered the clear/trigger pair at the midpoint of the
whole stack — visually below the first row of chips. Pin to
`top: var(--space-1)` instead so they sit flush with the first chip
row no matter how many additional rows of chips wrap below.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When `<Combobox.SelectedTags>` has at least one chip, anchor the
ClearButton + Trigger absolutely at the right edge of the Control
(MUI Autocomplete pattern). Chips + input can now wrap freely
without ever pushing the right-side affordances to a new row.
`--_combobox-affordances-w` reserves the right-padding the
absolutely-positioned buttons consume, scaled to the control's
current height token.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New parts `<Combobox.SelectedTags>`, `<Combobox.SelectedTag>` and
`<Combobox.SelectedTagRemove>` render the current selection as
removable chips inside `<Combobox.Control>` when `type='multiple'`
(Material UI Autocomplete pattern):
- Morfo: 3 new public parts with kebab names + texts entries.
- Langs: es/en entries `selected-tags` / `selected-tag-remove`.
- Soma: `ComboboxSelectedTagsProvider` exposes `items` derived from
the provider's label registry; `ComboboxSelectedTagProvider`
surfaces `data-value` / `data-label`; `ComboboxSelectedTagRemoveProvider`
resolves its parent tag via context and calls `provider.removeValue`
on click. Provider gains `getSelectedItems()` and `removeValue()`
helpers; `selectedTagsEmpty` runtime prop drives `[data-empty]`.
- Soma item registry: stop unregistering labels on Item cleanup so
chips keep proper labels after the listbox closes/lazy-unmounts.
- Eidos: default snippet loops items and renders a chip + X icon
per entry; consumer can override the snippet for custom rendering.
- CSS: chip styling, wrap-friendly Control with breathing padding
when chips are present, ClearButton pinned right via auto margin,
input flex basis reduced to 4rem so it shares a row with chips.
- Demo: renders `<Combobox.SelectedTags />` inside Control when
`mode === 'multiple'`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Demo `/uix/components/search-field` wraps the SearchField in
`<Popover.Anchor>`; results render in `<Popover.Content>` (intrinsic
width, align="start") wrapped in `<PickerShell.Body>` so the panel
inherits the canonical picker chrome. Items are real `<button>`s with
`onclick` — committing a selection sets `value`, closes the popover,
and restores focus to the input.
- Forward `bind:ref` through the eidos `<SearchField.Input>` wrapper so
consumers can refocus the input from the demo's pick handler.
- Hide the browser's native `::-webkit-search-cancel-button` /
`::-webkit-search-decoration` / `::-ms-clear` so only the canonical
`<SearchField.ClearTrigger>` X is visible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promote PickerShell from action-row-only to a canonical 4-zone shell
(Root → Header / Body / Footer) shared by every composite picker so
date/time/color popovers get the same visual contract for free.
- Add `<PickerShell.Root>` flex-column wrapper (emits data-picker-shell,
data-mode) plus `<PickerShell.Header>` and `<PickerShell.Body>` zones
matching the existing Footer's spacing + border tokens.
- Auto-wrap all 5 *-content.svelte popovers (date / date-range / time /
time-range / color) in `<PickerShell.Root>` so consumers only compose
Body + Footer.
- Refresh all 5 demos to use `<PickerShell.Body>` around picker content;
date-picker demo gains a `showShellHeader` toggle showcasing the new
Header zone.
- CSS scales section gap + header spacing across xs/sm/md/lg picker
sizes via [data-popover-content][data-picker-size] scopes, in sync
with the existing Footer scale.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Yesterday's checkpoint normalized popover min-width to the floating anchor
width across all components, then re-anchored pickers to their input
wrapper so the rule would line popover ≥ input. Tested at /uix/components/
date-picker the result was a popover stretched to 458 px (input width)
while the calendar inside is only ~268 px — leaving an empty band to the
right of every row.
After re-checking the industry research:
- Radix / shadcn / React Aria popovers default to INTRINSIC content width
and expose `--*-trigger-width` as opt-in.
- Ark UI only auto-matches via `positioning.sameWidth` (opt-in).
- bits-ui Select is the only one that bakes width matching by default.
Conclusion: forcing min-width = anchor-width across all popovers is wrong;
each picker's content has its own intrinsic width that should drive the
popover. Reverting the normalization to the previous (Radix-style)
content-driven default. The Select + Combobox `matchAnchorWidth = true`
default is preserved (those are listbox-style popovers where the trigger
width is a sensible reference) and the sideOffset=6 normalization stays.
Files reverted:
- src/uix/eidos/components/popover/popover.css: removed
`--_popover-min-width: var(--floating-anchor-width)` default.
- src/uix/eidos/components/date-picker/date-picker-content.svelte:
restored `--_popover-content-min-width-override: max-content` default
and dropped the `customAnchor={picker.inputRef.current}` wiring.
- src/uix/eidos/components/date-picker/date-picker.css: restored
`min-inline-size: max-content` on the calendar/grid popover rules.
- src/uix/soma/components/date-picker/date-picker-provider.svelte.ts:
removed `inputRef` + `setInputRef`.
- Removed src/uix/{soma,eidos}/components/date-picker/date-picker-input.svelte.
- src/uix/{soma,eidos}/components/date-picker/{exports,index}: revert
`Input` to the DateField re-export.
Kept from yesterday:
- sideOffset = 6 default in popover-content + 5 pickers (genuine
normalization, no width tension).
- Select/Combobox matchAnchorWidth = true (listbox convention).
- floating.svelte.ts `opts.style.current` bug fix (was preventing
any consumer style from reaching the popover wrapper).
- Combobox single-mode auto-sync of inputValue to selected label.
Verified at /uix/components/date-picker: popover renders at 268 px
(calendar content width), aligned end of the 458 px input — matches the
Radix-style behaviour we now consider canonical.
svelte-check --threshold error → 0 errors / 0 warnings / 4584 files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Day-end checkpoint for the floating-layer normalization work. Documented
in CONTINUE.md.
Done:
- popover.css: new `--_popover-min-width` defaults to
`var(--floating-anchor-width, auto)` so popover layer is NEVER narrower
than its anchor (Radix / React Aria convention). Consumers that should
opt out (tooltip, dialog) override with `auto`.
- soma popover-content default sideOffset 0 → 6.
- 5 picker eidos contents: sideOffset 10 → 6 (matches the new default;
per-component override removed visually but kept as explicit prop).
- combobox provider: reverted the multi-on-close stamp-labels patch.
Multi-mode selections will be rendered as inline tags via a future
`Combobox.SelectedTags` part (Material UI Autocomplete pattern,
decided with user).
Pending (see CONTINUE.md):
- Re-anchor pickers to their Input wrapper (currently anchored to the
chevron trigger ~14px). Required for the min-width normalization to
actually show — research vs Ark UI / Radix / React Aria confirmed
composite-input pickers anchor to the control wrapper, not the
trigger button.
- Combobox `SelectedTags` part for multi-mode inline tags.
- SearchField floating Results part (user opted to make it
Combobox-like rather than reuse Combobox).
svelte-check --threshold error → 0 errors / 0 warnings / 4584 files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related bugs reported in the demo:
1. After selecting an item in single mode, the input stayed at the
previously typed query (or empty), so the user couldn't tell which
value was committed unless they peeked at the Trigger area.
2. In multi mode, selecting an item used to clear the input, which
destroyed any active filter. cmdk-style usage (filter → pick → filter
→ pick) was impossible — the user had to retype the query for every
selection.
Provider behaviour now:
- `selectItem()` in SINGLE mode sets `inputValue` to the selected
label (resolved via the existing `labelRegistry`).
- `selectItem()` in MULTI mode leaves `inputValue` untouched, so the
filter survives the click and the user can keep picking matches.
- A new $effect keeps `inputValue` in sync with `value` in SINGLE mode
for initial mount and external `bind:value` updates.
- A second $effect clears `inputValue` whenever the picker switches
between 'single' and 'multiple' — the two modes use the input for
different purposes, and stale text from one would silently filter
the other.
Also:
- Added `components.combobox.clear` to the langs dictionary so the
ClearButton stops emitting `[langs] Translation key not found` warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three components closing the 6-component audit batch.
number-field
- N1 RTL scrubber: invert horizontal delta sign when dir='rtl' so
dragging right decrements (visually backward) in RTL layouts. Vertical
unaffected. Added 2 RTL scrub tests; existing scrub test pinned to ltr.
- N2 stepper height: Increment/Decrement now use --_number-field-height
for inline-size/block-size/min-inline-size so they always match the
input row at every size token (xs/sm/md/lg/xl).
- N3 input focus ring: explicit [data-number-field-input]:focus-visible
with --_number-field-accent-border box-shadow, alongside the existing
Provider-level ring.
search-field
- S1 icon tokens: added --search-field-icon-size-{xs..xl} + icon-color +
icon-margin-inline-end + a Morfo Icon part + soma/eidos wrappers.
Demo uses <SearchField.Icon><Search /></SearchField.Icon>.
- S2 debounceMs prop: soma wraps onValueChange in a timers.schedule with
the provided delay. clear() cancels, submit() flushes synchronously.
- S3 loading slot: new LoadingIndicator part (morfo + soma + eidos) +
data-loading on Provider + CSS spinner via @keyframes search-field-spin
honoring prefers-reduced-motion.
combobox
- C1 ClearButton part: new morfo trigger + soma provider + eidos wrapper
with X icon. Hidden via [data-empty] (opacity + pointer-events:none).
Click clears value + inputValue and refocuses input.
- C2 allowsCustomValue: when true and no highlighted item, Enter commits
the current inputValue as the value. Single mode closes after commit;
multi appends + clears input + stays open.
- C4 Backspace removes last tag in multi mode when input is empty.
- C5 Sticky group headers: CSS position:sticky with content-overlay bg.
Plus: fixed search-field demo's brittle parts[1] index reference to a
named lookup (parts.find(p => p.kebab === 'input')).
Verified: svelte-check --threshold error → 0 errors / 0 warnings /
4584 files. All component tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit-driven improvements (7 items) to Editable:
- E1 invalid + required props
- morfo: Provider gains data-required/data-invalid + aria-required/aria-invalid;
Input emits data-invalid + aria-invalid.
- soma: opts include `invalid` and `required`; runtime registration + provider
props derivation emits the data-attrs.
- eidos css: risk-border on Input + Control when [data-invalid].
- E2 APG: added `apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/textbox/'` to
morfo — Editable is essentially a textbox swap.
- E3 form integration: new HiddenInput part (morfo + soma + eidos). The Provider
wrapper auto-renders `<EditableHiddenInput>` when `name` is set, so consumers
don't have to compose it manually. Submits value as native form data.
- E4 Control re-export: Eidos namespace now exposes `Editable.Control`,
matching the soma surface.
- E7 submit hover: replaced `filter: brightness(1.04)` with
`--_editable-accent-solid-hover` per color variant. Consistent with the
other triggers' bg+border transition pattern.
- E9 disabled state: added `cursor: not-allowed` + `color: content-disabled`
on Preview/Input/triggers, alongside the existing opacity. Clearer
feedback than opacity alone.
- E10 defaultValue: new prop seeds `value` once via untrack when `value`
is uncontrolled. Same pattern as the recent select.defaultValue.
Demo at /uix/components/editable: switches for invalid/required/disabled,
a Form integration section with native form submit + FormData panel,
and a defaultValue uncontrolled example.
Verified: svelte-check --threshold error → 0 errors / 0 warnings / 4578
files. Soma tests: 3/3 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
select bugs
- The popover content didn't match the trigger width even though
matchAnchorWidth was already implemented in the eidos layer. Root
cause was in soma/layers/floating/floating.svelte.ts: `transformedStyle`
read `this.opts.style` directly without `.current`, so consumer styles
never merged into the floating wrapper. Fixed by reading
`this.opts.style.current` and also returning the value when it's an
object (previously fell through to undefined). All popover-based
components (select, combobox, date/time pickers, color picker) now get
their `style` prop applied.
- The trigger value text reverted from the rendered label (e.g. "Apple")
to the raw value string ("apple") whenever the popover closed. Items
unmount when Content's Presence collapses, and they were unregistering
their label on cleanup — leaving the label registry empty between
opens. Drop the unregister on cleanup; keep registrations alive across
popover open/close cycles. Re-mounts overwrite the entry.
field improvements (F1 + F2 + F3)
- F1: add an explicit `[data-field][data-variant='surface']` CSS block
so the surface variant (declared in the type union) has its own rule
instead of silently falling back to the default style.
- F2: new `OptionalIndicator` part (morfo + soma wrapper + eidos
wrapper). Renders only when `required === false`. Default text comes
from `texts['optional-indicator']` ("(optional)"). Styled muted + 0.85em.
- F3: `<Field.Label />` without children now auto-renders the
localised label from `texts.label`. Reduces boilerplate when consumers
rely on the field name for its label.
- Demo at /uix/components/field gained composition examples for the new
parts.
Verified: svelte-check --threshold error → 0 errors / 0 warnings / 4576
files. In-browser: select trigger and popover widths both 336 px;
closed and open values both show labels (e.g. "Apple, Pear, Orange").
Field demo shows 2 required `*`, 1 `(optional)`, and an empty
`<Field.Label />` falling back to the literal "Field".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit-driven improvements to Select:
- morfo APG URL fixed (combobox → listbox; Select is a collapsible-listbox
per ARIA APG, not a text-input combobox).
- `invalid` prop added to soma + morfo data-invalid/aria-invalid on Provider
and Trigger. Eidos applies risk-border + tinted focus shadow when invalid.
- `defaultValue` prop added (string | string[]) for uncontrolled init.
Normalized to string[] inside the soma wrapper via untrack so the
reference is intentionally non-reactive.
- `ItemDescription` part: new morfo part (kebab item-description, archetype
description), new soma wrapper + new eidos wrapper. CSS gives muted
color and 0.85em font-size; when the parent item is checked, the
description inherits accent color.
- Value truncation: ensure `min-inline-size: 0` + `max-inline-size: 100%`
on the trigger value so long labels ellipsis instead of overflowing.
- Highlighted vs checked distinction: the highlighted-only state now uses
a lighter hover background, and an item that is both highlighted and
checked gets an additional inset focus ring on top of the accent fill.
Demo at /uix/components/select adds an `invalid` switch, and three of the
six items now ship with a description (Apple / Pear / Carrot).
Verified in browser:
- triggerInvalid + aria-invalid = true after toggling switch.
- Description on non-checked item: rgb(123,123,123) (content-muted).
- Description on checked item: accent purple (inherits).
- svelte-check --threshold error → 0 errors / 0 warnings / 4574 files.
Deferred: SL3 readonly (rare use case), SL4 virtual scrolling (own task).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The footer buttons (Clear / Cancel / Done) were hardcoded to control-height-sm
+ font-size-sm + space-3 padding regardless of the host picker's size. At
xs the buttons looked oversized next to a compact picker; at lg they
looked undersized.
- picker-shell.css: refactored button rules to consume four `--_picker-shell-*`
CSS vars (control-h, padding-x, font, gap, row-spacing). Defaults match
the previous sm sizing. Added explicit xs/md/lg variant blocks keyed off
`[data-popover-content][data-picker-size='X']`.
- date-picker-content / date-range-picker-content / time-picker-content /
time-range-picker-content / color-picker-content: forward the picker's
resolvedSize to the popover via a separate `data-picker-size` attr,
independent from the popover chrome's clamped `data-size`.
This decouples the popover's own chrome (which still clamps xs → sm because
the popover layer has no xs variant) from the picker-shell button scaling,
which now uses the picker's true size.
Verified at /uix/components/date-picker:
- xs → 22 / 12px (control-height-xxs, font-size-xs)
- sm → 30 / 14px (default)
- md → 36 / 16px
- lg → 44 / 16px
Also fixes the horizontal scroll at xs that surfaced the issue. The
date-picker.css `[data-popover-content]:has(...)` rule lets the popover
grow to max-content when it hosts a calendar / grid.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a MonthGrid / YearGrid is rendered inside a DatePicker.Content
popover (via DatePicker.MonthView / YearView), the grid was using
inline-size:max-content while the picker-shell Footer below it stretched
to the popover's full content width — leaving an empty band to the right
of each row of cells.
Scope the override to `[data-popover-content]` so the standalone grids
on their own demos keep their natural max-content sizing. Inside the
popover, the grid root and the inner [data-*-grid] both stretch to 100%
so the 3 columns (minmax(0, 1fr)) distribute across the same inline
size as the footer.
Also added box-sizing: border-box to both grid roots so the padding +
border don't push them past the popover content area.
Verified at /uix/components/date-picker with kind='year' size='md':
popover content width = 288px, year-grid width = 262px (= footer width),
inner grid = 228px (= grid container minus padding). 3 columns align
flush to the footer row above.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
date-picker-month-view.svelte and date-picker-year-view.svelte previously
rolled their own 3×4 grids with custom data-date-picker-{month,year}-view-*
markers and ~90 lines of selection / nav / formatter logic. They now
render the standalone MonthGrid / YearGrid primitives (introduced in #70)
and bridge a thin DatePickerProvider context adapter:
- value: CalendarDate ↔ {year, month?} / {year}
- placeholder: CalendarDate ↔ {year}
- onValueChange → new CalendarDate(year, month, 1) + handleDateSelect
- onPlaceholderChange → mutate provider.placeholder.year
- forwards locale / disabled / readonly straight from the provider
CSS: removed the data-date-picker-{month,year}-view-* selector block
(~90 lines of dead code). Visuals now come from month-grid.css /
year-grid.css, which means the views also inherit the size='xs' work,
roving tabindex, RTL keyboard flip and sema events for free.
Verified in browser at /uix/components/date-picker:
- kind='month' → MonthGrid renders 12 cells (Jan..Dec) inside the popover
- click Mar → value becomes 2026-03-01, input segments update to 03/2026
- kind='year' → YearGrid renders the 2016–2027 page
- svelte-check --threshold error → 0 errors / 0 warnings / 4572 files
date-range-picker MonthView / YearView left untouched — their range
state machine (start/end/in-range markers, complete-vs-pending swap) is
not modelled by the standalone grids.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the cosmetic gap surfaced by the cross-component audit (#83).
Toast renders via role='alert' / live-region; the matching APG pattern
is the "alert" widget.
Avatar audited too — its empty morfo (scope: ['eidos'], no texts/events/
apg) is the canonical shape for purely visual primitives; Icon follows
the same template. No change needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
xs propagation
- Add [data-size='xs'] blocks to calendar.css, date-picker.css,
date-range-picker.css (root + inline range calendar).
- Extend CalendarSize union with 'xs'; DatePicker / DateRangePicker
inherit it through their type re-exports.
- date-picker-content + date-range-picker-content clamp xs to sm when
forwarding to PopoverContent (PopoverSize starts at sm; the calendar
inside still receives xs via data-size).
- size chip groups in calendar / date-picker / date-range-picker demos
now list xs / sm / md / lg.
date-picker demo: fix duplicate footer
- The `calendarBody` snippet had a hardcoded <DatePicker.Footer> that
rendered alongside the controllable `pickerFooter` snippet, producing
two footer rows when showFooter=true. Removed the hardcoded one.
Verified in browser:
- date-picker size='xs' -> data-date-field-input height 26px
(control-height-xs), padding 8px (space-2), font-size 12px.
- date-picker popover now shows a single footer.
- svelte-check --threshold error -> 0 errors / 0 warnings / 4572 files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The eidos Cell wrapper previously delegated a children snippet that called
`bodyContent?.(snippetProps)`. When the consumer wrote `<Cell {year} />`
without a children snippet, the snippet still resolved to nothing, which
made the soma cell's `{:else} {label}` fallback never fire — cells came
out empty in the DOM.
Move the fallback into the eidos cell's own children snippet:
`{#if bodyContent}{@render bodyContent(...)}{:else}{label}{/if}`.
Verified in browser: MonthGrid renders Jan..Dec with the placeholder
year, YearGrid renders the decade page correctly with data-current on
today's year. svelte-check still 0 errors / 0 warnings / 4572 files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Builds the Eidos time-range-picker layer end-to-end and brings color-picker's
morfo into parity with the other pickers.
time-range-picker
- soma: `mode` prop, `pickerShellContext` registration with
commit/cancel/clear and `valueOnOpen` snapshot for revert; Provider now
renders a div with data-state/disabled/readonly/required + aria mirrors.
- soma: new `Clock` part (mirrors TimePicker.Clock with role="dialog" and
data-state).
- morfo: scope ['soma','sema'] + apg dialog-modal; promoted Provider to
public div with states/data-state; added Clock part; declared the 6
canonical picker events (open / close-commit / close-cancel /
close-dismiss / close-dismiss-outside / commit-clear); slider parts
declare data-endpoint + data-disabled.
- eidos: full namespace (Trigger / Content / Clock / HourSlider /
MinuteSlider / SecondSlider / DayPeriodToggle + re-exported PickerShell
Footer / Clear / Cancel / Close). Sliders compose generic SliderProvider
(N-8) and ship with optional rulers above hour / below minute+second.
- eidos css: 2-column endpoint grid in the clock; per-endpoint range
tint; AM/PM toggle styling; reuses time-field tokens.
- demo: interactive testbed at /uix/components/time-range-picker mirroring
the time-picker demo — every soma + eidos prop has a live control,
closeOnRangeComplete + start/endReadonlySegments toggles, footer
composition switches, sema play buttons, soma+eidos snippets.
- nav: added entry between Time picker and Color field.
color-picker (audit fixes)
- morfo: Provider declares states ['open','closed'] + data-state attr;
added the same 6 canonical picker events targeting the `content` part
(handle-pick / handle-drag / commit-set preserved).
- soma: runtime now feeds open state + disabled/readonly/required/invalid
props to the morfo runtime so data-state actually emits.
Verified: svelte-check --threshold error → 0 errors / 0 warnings / 4524 files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to 8515f5e6 — that commit had the stacking inverted.
The bug
-------
I put the checker on `::before { z-index: -1 }` and expected the soma's
inline `background-color` to paint on top. But within a stacking context
(my parent has `isolation: isolate`), `::before { z-index: -1 }` lands
in layer 2 — ABOVE the parent's own background (layer 1). So the
checker still covered the colour and the user only saw transparency.
The fix
-------
Inverted the layering — back to the same pattern the trigger swatch
already uses:
- Parent `[data-color-picker-value-swatch]` carries the **checker** as
its own `background-image` (sits at layer 1 of the stacking context).
- `::before` (positioned descendant, layer 2 ABOVE layer 1) paints the
**colour** via `var(--cp-current-color, transparent)`. The soma
emits `--cp-current-color: <hex>` inline as part of the swatch style.
- Where the colour has alpha < 1 (rgba), the checker shows through
the `::before` because semitransparent colour over the checker is
the standard pattern.
Default brand colour (opaque blue) now renders as a solid blue square
again; semitransparent colours show the checker behind. Checks:
67/67 component-audit PASS, svelte-check 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report: 'la caja del color seleccionado en el pickercolor no se
actualiza, compruébalo.'
Root cause
----------
The Eidos recipe had an `[data-color-picker-value-swatch]::after`
overlay that painted `var(--cp-trigger-color, currentColor)` on top of
the swatch — a CSS variable set ONLY on the trigger button by the
Eidos wrapper. The popover content is portaled outside the trigger's
subtree, so the var never propagated into the ValueSwatch element.
The overlay therefore stayed at its fallback (`currentColor`) and
covered the soma-set inline `background-color`, hiding the live colour.
Fix
---
1. **Soma `ColorPickerValueSwatchProvider.backgroundStyle`** now emits
BOTH `background-color: <hex>` and `--cp-current-color: <hex>` so:
- Consumers without the eidos recipe still see the colour (inline
background-color, as before).
- Eidos recipes / external CSS can read `--cp-current-color` as a
reactive CSS variable on the element itself.
2. **Eidos CSS** drops the `::after` overlay. Instead the checker
(transparency pattern) lives on a `::before` pseudo with
`z-index: -1` inside an isolated stacking context, so the soma's
inline `background-color` paints ON TOP and the checker shows
through only where alpha < 1. No more colour stomping.
Checks: 67/67 component-audit PASS, svelte-check 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report: 'el selector del cuadro de colores, no tiene semántica, es
un drag, etc como el slider, no?'
You're right — the 2D area is a drag interaction with the same shape
as a slider. It was wiring pointer events to soma state but staying
silent to sema; nothing for sounds/haptics/transient styles to hook
onto. Now it speaks the same vocabulary as the slider.
Morfo additions (color-picker)
------------------------------
- `handle-pick` (family: handle, verb: pick, sequence: coincident,
target: area) — fires once at pointer-down.
- `handle-drag` (family: handle, verb: drag, sequence: coincident,
target: area) — fires on each move (pointer or keyboard step).
- `commit-set` already existed — now also emitted on pointer-up and
on each keyboard step (slider parity).
Soma changes (color-picker-provider)
------------------------------------
- ColorPickerAreaProvider:
- `onpointerdown` → trigger `handle-pick` then continue to drag.
- `handlePointerMove` → trigger `handle-drag` after writing the value.
- `onpointerup` → trigger `commit-set` then `commitChange()` for
the onValueChangeEnd callback.
- ColorPickerAreaThumbProvider.onkeydown:
- Each arrow / Home / End / PageUp / PageDown step now triggers
`handle-drag` + `commit-set` + commitChange(). Matches the slider's
keyboard semantics (one step = one drag + one commit).
Demo snippet refresh
--------------------
The `somaSnippet` / `eidosSnippet` in `/uix/components/color-picker`
were stuck on the pre-refactor API (`<ColorPicker.Area>` with explicit
Background+Thumb children, no `Control`, no `ValueSwatch`, no
PickerShell footer). Updated both to mirror the current demo body:
- Eidos: `Label / Control[Trigger + ValueText + HiddenInput] / Portal /
Content[Area / ChannelSlider hue / alpha / ValueSwatch +
ChannelInput + EyeDropper / SwatchGroup / Footer[Clear/Cancel/
Close]]`.
- Soma: same shape but uses `PickerShell.Footer / Clear / Cancel /
Close` explicitly (and the explicit Track/Thumb composition on
channel sliders for the curious).
- Both snippets honor the live `mode / inline / closeOnSelect /
enableAlpha / showFooter / showClear / showCancel / showClose`
flags from the chip controls.
Checks: 67/67 component-audit PASS, 40/40 morfo:check PASS,
svelte-check 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report: 'la paleta dejala en 10 y más pequeños para que solo
ocupen una linea, se deben de ajustar a lo ancho'.
Demo
----
- Preset palette trimmed from 11 to 10 colors.
CSS — swatch-group rewritten to single-row flex layout
------------------------------------------------------
- `display: flex; flex-wrap: nowrap; inline-size: 100%; min-inline-size:
0` so all swatches share the available width.
- Each `<SwatchTrigger>` becomes `flex: 1 1 0` + `min-inline-size: 0`
+ `aspect-ratio: 1` + `inline-size/block-size: auto`, with the
recipe token (`--color-picker-swatch-size`) acting as `max-inline-
size` instead of a fixed size. Net effect: swatches shrink/grow
together to fit any popover width and any number of presets, while
capping at 1.5rem so they don't blow up on huge containers.
Math at default md popover (~19.5rem content width, ~14px padding each
side, 10 swatches × ~23px + 9 × 6px gap ≈ ~284px usable). Each swatch
lands at ~23px and the row never wraps.
Checks: 67/67 component-audit PASS, svelte-check 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to commit 714ee373 (PickerShell extraction). Norm N-8 now
applied to every composite picker — date-picker, time-picker and
date-range-picker stop carrying their own Footer/Clear/Cancel/Close
duplicates and read the shared <PickerShell.Footer/Clear/Cancel/Close>
through `pickerShellContext`.
Soma changes
------------
- date-picker, time-picker, date-range-picker providers each gain a
`pickerShellHandle: PickerShellHandle` field exposing
`getMode/commit/cancel/clear`, and call
`pickerShellContext.set(this.pickerShellHandle)` in the constructor
so descendant Eidos shell parts can drive the picker.
Morfo changes
-------------
- Footer/Clear/Cancel/Close parts removed from each picker morfo.
The shared chrome lives outside the per-picker contract — its data
attrs are `[data-picker-footer/clear/cancel/close]` (no per-picker
prefix). Comment block in each morfo file documents the move.
Eidos changes
-------------
- Deleted 12 obsolete files:
src/uix/eidos/components/date-picker/date-picker-{footer,clear,cancel,close}.svelte
src/uix/eidos/components/time-picker/time-picker-{footer,clear,cancel,close}.svelte
src/uix/eidos/components/date-range-picker/date-range-picker-{footer,clear,cancel,close}.svelte
- Each picker's `index.ts` re-imports the 4 parts from
`../picker-shell/picker-shell-{footer,clear,cancel,close}.svelte`
so the public namespace stays `X.Footer / X.Clear / X.Cancel /
X.Close` — no breaking change for consumers.
- Each picker's `types.ts` re-exports `FooterProps/ClearProps/
CancelProps/CloseProps` from `../picker-shell`, replacing the stale
per-picker types that referenced soma popover Close props.
CSS changes
-----------
- Deleted ~190 lines of duplicated footer/clear/cancel/close rules
from date-picker.css, time-picker.css, date-range-picker.css. The
canonical `[data-picker-footer/clear/cancel/close]` rules in
picker-shell.css now style every picker uniformly.
- prefers-reduced-motion blocks pruned of the same selectors.
Public API
----------
Unchanged from a consumer perspective: `<DatePicker.Footer>`,
`<TimePicker.Clear>`, `<DateRangePicker.Close>`, etc. still resolve
to working components. The inner DOM markers change from
`[data-{x}-picker-footer]` to `[data-picker-footer]` — anyone with
custom CSS targeting the old selectors must migrate.
Checks
------
- 67/67 component-audit PASS
- 40/40 morfo:check PASS
- svelte-check 0 errors / 0 warnings
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report: 'los sliders son deficitarios, no muestra la banda de color
ni de transferencia, faltan textos, el color seleccionado aparte del
valor ascii, un texto que indique el gotero, etc.'
Three concrete issues addressed.
1. Slider gradients invisible
-----------------------------
Root cause: the soma channel-slider div carries BOTH `data-slider`
(from SliderProvider.props) and `data-color-picker-channel-slider`
(from mergeProps). My CSS targeted `[data-color-picker-channel-slider]
[data-slider]::before` — i.e. as a *descendant*. They're the same
element, so the selector never matched and the rainbow / alpha
gradient stayed hidden.
Fix:
- New selector `[data-color-picker-channel-slider][data-slider]::before`
paints the gradient on the slider's own track strip.
- Transparency checker moved to the wrapper's `::after` (with
`z-index: -1` + `isolation: isolate`) so it sits behind the gradient
— visible only where the alpha gradient ramps to transparent.
- Adds a subtle border on the track so the strip is visible against
the popover surface.
2. Live preview alongside the hex/rgb/hsl string
------------------------------------------------
The popover used to show only the segmented ChannelInput (digits) and
a small swatch dot on the trigger. Now the popover has a `.cp-preview-
row` with `<ColorPicker.ValueSwatch>` (2rem) + `<ColorPicker.ValueText>`
+ uppercase format tag, so the user always sees the actual color
visually alongside its numeric representation.
3. Eyedropper label
-------------------
Added a textual `Pick from screen` label next to the icon button,
plus `aria-label` on the EyeDropper. The button is no longer a
mystery glyph.
4. Channel slider row labels
----------------------------
Each ChannelSlider now sits in a `.cp-slider-row` with a leading
uppercase label (Hue / Alpha) so the user knows what each track drives
without hovering or reading aria.
5. Section header for swatches
------------------------------
'Preset colors' header above the swatch grid for visual hierarchy
inside the popover.
Files
-----
- src/uix/eidos/components/color-picker/color-picker.css — slider
gradient routing + checker-on-::after layering.
- web/routes/uix/components/color-picker/+page.svelte — preview row,
slider row labels, eyedropper label, section headers + local
scoped CSS in <style>.
Checks: 67/67 component-audit PASS, 40/40 morfo:check PASS,
svelte-check 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces PickerShell — a soma context + eidos parts shared by every
composite picker (date, time, color, future range pickers). Aligns with
norm N-8 (reuse components across complex composites) so the
Footer/Clear/Cancel/Close behavior lives in ONE place instead of being
duplicated per picker family.
New files
---------
- src/uix/soma/components/picker-shell/
picker-shell-handle.svelte.ts → pickerShellContext +
PickerShellHandle interface
(getMode / commit / cancel / clear).
exports.ts, index.ts → public surface.
- src/uix/eidos/components/picker-shell/
picker-shell-footer.svelte → [data-picker-footer][data-mode]
picker-shell-clear.svelte → [data-picker-clear][data-action]
picker-shell-cancel.svelte → [data-picker-cancel][data-action]
picker-shell-close.svelte → [data-picker-close][data-action]
picker-shell.css → canonical action-row recipe.
index.ts, types.ts → PickerShell namespace + prop types.
- Registered in src/uix/eidos/index.css.
Color-picker now consumes the shell
-----------------------------------
- ColorPickerProvider gets `mode: 'inline' | 'modal'` opt + a
`valueOnOpen` snapshot via watch(open) so cancel() can revert.
- New `commit()` / `cancel()` / `clear()` methods with shell-picker
semantics. The previous `commit()` (fires onValueChangeEnd) was
renamed `commitChange()` since the picker-shell `commit()` means
"save + close". All internal callers (area-thumb pointerup,
swatch click, eyedropper open, channel-slider drop) updated.
- Constructor registers the handle:
`pickerShellContext.set(this.pickerShellHandle)`.
- PopoverProvider receives `modal: mode === 'modal'` so outside-click
+ Escape get blocked when modal.
- Eidos `ColorPicker.Footer / .Clear / .Cancel / .Close` re-export
the shared parts. The old `ColorPicker.Close = PopoverClose` was
renamed `ColorPicker.PopoverClose`; the canonical `Close` is now
the picker-shell save action.
Demo
----
- New `mode` chip (inline | modal).
- Footer composition switches (render footer / clear / cancel / close).
- Renders <ColorPicker.Footer> with shell parts inside <Content>.
Visual fixes
------------
- Horizontal overflow on the popover content: added `min-inline-size: 0
/ max-inline-size: 100% / box-sizing: border-box / overflow-x: hidden`
on `[data-popover-content][data-color-picker-content]` and on its
direct children. Inner ChannelInput allowed to wrap segments instead
of pushing the popover wider.
- ColorField segments inside the popover gain `flex: 0 1 auto`.
Norms
-----
- N-8 case study appended to PENDIENTES.md documenting the
PickerShell extraction alongside the channel-slider refactor.
Checks
------
- 67/67 component-audit PASS
- 40/40 morfo:check PASS
- svelte-check 0 errors / 0 warnings
Follow-up (task #81): migrate date-picker / time-picker Footer to use
PickerShell too — drop the duplicate `date-picker-footer/clear/
cancel/close.svelte` and `time-picker-footer/clear/cancel/close.svelte`
files; update CSS selectors to `[data-picker-footer/...]`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops ColorPickerChannelSliderProvider / TrackProvider / ThumbProvider
(~200 lines of duplicated pointer/keyboard/ARIA handling) and rewrites
`components/color-picker-channel-slider.svelte` to compose
SliderProvider underneath. The channel slider now keeps only the
channel-aware bridge: setChannel writes the value, channelGradient
paints the track.
What the picker gains by reusing Slider
---------------------------------------
- sema events: `slider:handle-pick`, `slider:handle-drag`,
`slider:commit-set` now flow through the channel sliders (they were
silent before — the Sema layer had no perceptual hook on channel
drag/commit).
- single pointer/keyboard implementation: Arrow/Home/End/PageUp/PageDown,
multi-touch, focus trap, RTL, step-snap — all from SliderProvider.
- ARIA: role=slider, aria-valuenow/min/max, aria-orientation, tabindex
managed by the slider's thumb provider.
- recipe tokens: channel sliders consume `--slider-*` (sizes, radii,
shadows) so they stay in lockstep with the standalone Slider and
with TimePicker.HourSlider / .MinuteSlider / .SecondSlider. The
`--color-picker-channel-slider-*` tokens are removed.
Public API impact
-----------------
- `<ColorPicker.ChannelSliderTrack>` and `<ColorPicker.ChannelSliderThumb>`
remain as part names but now re-export `Slider.Range` /
`Slider.Thumb`. Consumers that compose them inline keep working.
- The inner DOM changes: `[data-slider]` / `[data-slider-thumb]` /
`[data-slider-range]` (governed by the slider morfo) replace
`[data-color-picker-channel-slider-track]` /
`[data-color-picker-channel-slider-thumb]`. The wrapper still emits
`[data-color-picker-channel-slider][data-channel]` so per-channel
CSS targeting works; the gradient is exposed via
`--cp-channel-gradient` and routed onto the slider's track ::before.
- Morfo: drops `channel-slider-track` and `channel-slider-thumb` parts
(the slider morfo owns those contracts now). `channel-slider`
remains as the wrapper marker with `data-channel`.
- Eidos: deletes `color-picker-channel-slider-track.svelte` and
`color-picker-channel-slider-thumb.svelte`; `index.ts` re-points
`ChannelSliderTrack` / `ChannelSliderThumb` at the slider eidos
components.
New norm N-8 added to PENDIENTES.md
-----------------------------------
"Reutilizar componentes ya definidos en componentes complejos siempre
que no supongan una pérdida de características demostrable." Documented
with this refactor as the worked example.
Checks
------
- 67/67 component-audit PASS
- 40/40 morfo:check PASS
- svelte-check 0 errors / 0 warnings
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Composite popover: 2D saturation/brightness area, hue + alpha channel
sliders, preset swatches, eye-dropper, and a segmented channel input
composed from ColorField. Builds on the existing soma color-picker
(23 parts) — eidos layer is mostly thin pass-throughs adding visual
chrome.
What landed
-----------
- New `src/uix/eidos/components/color-picker/`:
- Root visual (Provider) + visual-context (size/variant/color)
- Trigger with a default swatch dot tinted from
`--cp-trigger-color` (the air pattern from morfo-runtime)
- Content (popover wrapper), Area (+ Background + Thumb),
TransparencyGrid
- ChannelSlider (+ Track + Thumb) — generic per-channel slider;
default-composes Track+Thumb when no children
- SwatchGroup + SwatchTrigger + Swatch + SwatchIndicator
- EyeDropper (Pipette icon by default)
- ChannelInput + ChannelSegment + FormatSelect (composed from
ColorField parts; same provider injected automatically)
- Label, Control, ValueSwatch, ValueText, HiddenInput
- Recipe section `color-picker` in `recipes/base.ts` (regenerated
`generated/base.css`) — trigger, content, area, channel slider,
swatches, eye-dropper tokens.
- CSS reuses color-field tokens for the segmented input portion and
adds picker-specific visuals. Transparency checker on trigger swatch
+ value swatch + alpha slider track via conic-gradient pattern.
- Morfo updates: scope `['soma','sema']`, `apg: dialog-modal`, added
`commit-set` event so audit classifies it as interactive.
- Soma exports: ColorPickerProvider class so eidos Trigger can read
the current value for the swatch tint.
- Demo at `/uix/components/color-picker` with the standard 6 tabs;
live stage exercises area drag + channel sliders + preset swatches
+ eye-dropper + format switch.
- README documents the surface, comparativa vs air/Ark/Chakra, and
flags the picker contract (mode/Footer/commit/cancel) as gap to
align with date/time-picker in a follow-up.
Checks
------
- 67/67 component-audit PASS
- 40/40 morfo:check PASS (new color-picker demo validates against
morfo DOM)
- svelte-check 0 errors / 0 warnings
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User pointed out that TimePicker exposes <TimePicker.HourSlider> /
.MinuteSlider / .SecondSlider in its public API but those weren't
declared in the time-picker morfo — the contract was implicit (data-
slider attrs from the slider morfo). Now made explicit.
Morfo additions (all optional, archetype: 'group'):
- HourSlider → data-time-picker-hour-slider
- MinuteSlider → data-time-picker-minute-slider
- SecondSlider → data-time-picker-second-slider
Eidos slider wrappers stamp the corresponding data-attr on the outer
wrapper div so morfo:check finds the parts in the DOM. The inner
slider DOM (data-slider, data-slider-thumb, data-slider-range) keeps
its own contract via the slider morfo — composition, not duplication.
Also:
- Hour ticks now end-inclusive: 24h shows 0/3/6/9/12/15/18/21/23
(was 0..21, missing the upper cap). 12h shows 1/3/6/9/12.
- Minute / Second scale now ends with 59 so the upper bound is shown.
- New optional `ruler` prop on <TimePicker> (default `true`) and
matching per-slider `ruler` opt-out. Propagated via visual context.
- Demo: new switch to toggle the ruler scale.
67/67 component-audit PASS, 39/39 morfo:check PASS, svelte-check
0 errors / 0 warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reported: the popover sliders showed a track with no visible thumb,
and there was no indication of the value scale.
- Each TimePicker.{Hour,Minute,Second}Slider eidos wrapper now renders
<Slider.Range /> + <Slider.Thumb /> by default when no children are
passed. Consumers can still override with custom children.
- Adds a labelled ruler row next to each slider:
- HourSlider: scale ABOVE the track. 24h shows 0/3/6/9/12/15/18/21,
12h shows 1/3/6/9/12. Numbers zero-padded for 24h.
- MinuteSlider / SecondSlider: scale BELOW the track at 0/15/30/45.
- Tick positions are computed proportionally and offset inward by
half the thumb width so the endpoint labels align with the actual
track endpoints (not the bounding box).
- Reinforces thumb visibility inside the clock popover: ring shadow
against the popover surface + accented border tied to the picker's
intent color. Range fill matches the accent border so the filled
portion of the track is unambiguous.
All checks clean: 67/67 audit PASS, svelte-check 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the date-field pattern (Provider visual root + Label / Input /
Segment / HiddenInput parts; ColorField also exposes FormatSelect).
Soma owns segment editing, ARIA spinbuttons, validation, format
switching and form submission; Eidos adds size (xs..xl), control
variant (surface/outline/ghost) and intent color recipes.
Adds time-field and color-field recipe sections to recipes/base.ts
(regenerated generated/base.css) and registers both routes in the
sidebar nav. apg=spinbutton declared on both morfos. READMEs follow
the Baseline / Superficie / Comparativa / Decisiones / Gaps /
Referencias template required by audit F-1.x.
All 67 component-audit components PASS, 38/38 morfo:check PASS,
svelte-check 0 errors/0 warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents P-1..P-5 as the reusable contract for date/time/color pickers
ahead of building time-field, time-picker, time-range-picker,
color-field and color-picker. Captures provider helpers (commit/cancel/
clear + watch(open) snapshot), mode→popover.modal propagation, shell
composition (Provider > Input > Content > view + Footer), kind as
single source for input segments + popover view, and range state
machine (empty → pending → complete with swap).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After several commits on pickers, the docs lagged behind the actual
contract. This sweep aligns PENDIENTES + eidos README +
DEMO_AUTHORING_GUIDE with what landed.
PENDIENTES.md:
- 'Pickers' section rewritten as a consolidated state table.
Everything done is marked hecho; the two big items
(MonthPicker/YearPicker, MonthRangePicker/YearRangePicker as
separate components) are explicitly **descartar** because they're
achieved via <DatePicker kind='X'> / <DateRangePicker kind='X'>.
Duplicating component surfaces for what a prop captures is
doctrinally rejected.
- Promotion of YearView/MonthView to standalone <YearCalendar> /
<MonthCalendar> is **diferir** — currently coupled to picker
provider context, no real use case outside picker yet.
- Time picker / color picker propagation of modal+Footer pattern
marked **implementar**.
- Playwright browser tests for the picker flows marked
**implementar** — range state machine + kind chip + modal need
coverage.
- Range view: 'differentiate start/end vs in-range visually' added
to theming backlog (currently all 3 use primary-solid, range
tint not visible).
- Two new norms N-6 and N-7:
* N-6 picker kind = single source for input + view. Filtering
lives at DateFieldProvider (soma), consumers iterate the
segments output. Views are canonical Eidos parts.
* N-7 composition over visibility props. Parts opt-in by inclusion,
not by boolean prop. Demo wraps parts in {#if showX} with local
state so the UI toggles still work without leaking demo logic
into the parts.
eidos/README.md:
- New 'Cambios 2026-05-21 — pickers: kind + composition' section
summarising kind + Footer composition + provider helpers + the
'composition wins, no separate variant components' decision.
DEMO_AUTHORING_GUIDE.md:
- §12.9 'Composition over visibility props': right vs wrong example
for <DatePicker.Footer> with the Clear/Cancel/Close children.
- §12.10 'Chakra-style kind for picker variants': demo skeleton
for the Input snippet (no filter) and the Content {#if} branch.
Task list: #29 retired (MonthRangePicker/YearRangePicker as separate
components — replaced by <DateRangePicker kind='X'>).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the kind contract for the range picker. The popover used to
render the day-grid range calendar regardless of kind; now it switches
to a year-grid or month-grid view that handles range selection on the
appropriate granularity.
New Eidos parts:
- <DateRangePicker.YearView>: 3×4 decade grid with prev/next decade
buttons. Selection state machine (mirrors the day-grid range
selection):
* empty → click sets startValue (and clears endValue)
* pending (start set, no end) → click sets endValue. Swap if the
clicked year is before the existing start.
* complete (both set) → click resets to a fresh start + clears end.
Year endpoints normalise to Jan 1 (start) / Dec 31 (end).
- <DateRangePicker.MonthView>: 3×4 month grid for the placeholder's
year, with prev/next year navigation. Same state machine using
`year*12 + month` as the comparison key, so ranges span multi-year
boundaries cleanly. Start = day 1; end = last day of the month
(computed via Date(y, m, 0).getDate()).
- Localised month names via DateFormatter(locale, { month: 'short' }).
Index + CSS:
- index.ts: registers YearView + MonthView on the DateRangePicker
namespace.
- date-range-picker.css: shared grid layout (3 columns, gap-2),
hover surface-overlay, selected cells get primary-solid +
content-on-solid, in-range cells get color-mix(primary-solid 22%,
transparent) as a stripe with border-radius:0 (start/end keep
their outer corner radius). Reduced-motion respected.
Demo:
- {#if kind === 'year'} → <YearView>; {:else if 'month'} → <MonthView>;
{:else} → existing day calendar.
- pickerFooter snippet rendered once at the Content level (removed
the inline render at the end of calendarBody to avoid duplication).
Verified in browser:
1. kind='year' chip → popover shows year grid '2020 – 2031'.
2. Click 2024 → cell marked start (purple solid).
3. Click 2027 → cell marked end, 2025-2026 marked in-range. trace
shows 'range 2024-01-01 -> 2027-12-31'. Popover stays open
(closeOnRangeSelect=false in the demo).
0 type errors, 67/67 component:audit PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User: 'lo has trasladado da daterangepicker?' — only the Footer pure-
composition refactor had landed in range; the `kind` prop (Chakra
date/month/year variant) was still single-picker only. The range
demo's input segments stayed MM/DD/YYYY regardless of the chip.
This commit propagates `kind` end-to-end through the range stack.
Calendar view-mode (year-grid / month-grid for ranges) is still
pending — that's the next commit.
soma:
- DateRangePickerKind type exported alongside DateRangePickerMode.
- DateRangePickerProvider opts gain `kind`. Root .svelte threads it
through readableActive both to the picker context AND to the
DateRangeFieldProvider it creates.
- DateRangeFieldProvider opts gain `kind` (single value — there's no
use case for kind='year' on start + kind='month' on end; the range
picker enforces a uniform kind on both endpoints).
- date-range-field-input forwards provider.opts.kind to each endpoint
DateField it spawns. The constant `kind: 'date'` placeholder is
gone.
- types.ts (range-picker + range-field): JSDoc + prop declarations.
- exports.ts: surface DateRangePickerKind alongside DateRangePickerMode.
- Test fixtures (range-picker + range-field): extended with the new opt.
demo:
- `let kind = $state<'date' | 'month' | 'year'>('date')` + chip control
next to the `mode` radio + prop pass-through to <DateRangePicker>.
Browser confirmed: clicking 'year' chip collapses BOTH start and end
inputs to a single `2026` segment. 'month' shows MM/YYYY on both.
'date' restores MM/DD/YYYY.
What's still pending (next commit):
- Year-grid and month-grid views for the range popover. Currently
the day calendar still renders regardless of kind. The range
picker's popover view-mode needs RangeCalendar to gain a `view`
prop and the eidos surface to dispatch to YearRangeView /
MonthRangeView components (range selection state machine: first
click sets start, second click sets end).
Verification: 0 type errors, 18/18 tests across the date-* soma
suite, 67/67 component:audit PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User correctly pointed out: button visibility should not be exposed as
properties on the picker root. It should be expressed via composition
— if you include <DatePicker.Clear/> inside <DatePicker.Footer/>, it
shows; if you omit it, it doesn't. Same as how Header parts work, and
extensible to derivatives (date-range-picker follows the same rule).
This commit removes the visibility props + visibility checks. Pure
composition wins.
soma:
- DatePickerProvider opts: drop clearButton / cancelButton /
closeButton. The root soma component drops the props + the
readableActive passes. Same for DateRangePickerProvider.
- types.ts: drop the prop declarations + JSDoc.
- Test fixtures: drop the state() entries for the removed opts.
eidos parts:
- date-picker-clear / cancel / close: drop the `visible` $derived
and the {#if visible} guard. Render unconditionally.
- date-range-picker-clear / cancel / close: same.
- date-picker-footer / date-range-picker-footer: also drop the
combined `visible` $derived. The Footer container always renders
whatever children are composed inside.
Modal mode: previously the Close part forced itself visible whenever
mode='modal'. That magic is gone too — the consumer is now responsible
for including <Close/> if mode='modal'; otherwise the modal has no
exit affordance (and that's documented in the Close part's comment).
demos:
- Drop clearButton/cancelButton/closeButton state vars.
- Drop the prop pass-through on <DatePicker> / <DateRangePicker>.
- Drop the 'footer buttons' switch group.
- Drop snippet code refs to those props.
- Keep the same <Footer><Clear/><Cancel/><Close/></Footer> markup
inside the calendar branches — now visibility is purely structural.
Verification: 0 type errors, 7/7 date-picker + date-range-picker
soma tests, 67/67 component:audit PASS. The picker still renders
with all three buttons by default (because the demos compose them).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User correctly pointed out: the input segments depend on the kind of
calendar — they're *derived* values, not independently controlled.
Filtering segments in the demo snippet (commit A) was wrong; the
derivation belongs in the DateField provider.
This commit refactors the contract + lands the year/month-grid views
(combining commit B + C into one).
Architecture fix (segments derive from kind):
- DateFieldProvider opts gain `kind: 'date' | 'month' | 'year'`.
- `segmentContents` filters `allSegmentContent.arr` by a derived
`visibleDatePartsByKind` set, collapsing runs of literals and
trimming leading/trailing separators. Time segments (hour/minute/
second/dayPeriod) are passed through untouched — `kind` is
orthogonal to `granularity`.
- DateField root component accepts `kind` prop and threads it.
- DatePicker root forwards its `kind` to the DateField provider it
creates internally.
- DateRangeField passes `kind: 'date'` as a constant for now; range
propagation lands in commit D.
- Test fixtures extended with the new opt.
New Eidos parts (commit B + C in one shot):
- `<DatePicker.YearView>`: 3×4 decade grid centred on placeholder's
decade boundary. Header shows the decade range with prev/next
decade buttons. Click on a year sets value to (year, 1, 1) and
calls handleDateSelect (closes if closeOnDateSelect=true inline).
- `<DatePicker.MonthView>`: 3×4 month grid for the placeholder's
year. Localised month names via DateFormatter. Header shows the
year with prev/next year buttons. Click sets (year, month, 1).
- CSS for both: shared 3-column grid layout, hover surface-overlay
background, selected cell uses primary-solid + content-on-solid.
focus-visible outline. prefers-reduced-motion honoured.
Demo wiring:
- Removed the local `filterByKind` helper — soma derives it now.
- The snippet just iterates `segments` as it comes.
- The popover content branches on `kind`: Calendar for 'date',
MonthView for 'month', YearView for 'year'. The Footer renders in
all three branches.
- An $effect re-opens the popover whenever `kind` changes (clicking
the chip outside the popover would otherwise close it in inline
mode).
What this commit DOES NOT do (commit D):
- Propagate `kind` to date-range-picker (start + end inputs + the
range-calendar popover view-mode).
- date-range-field consumes `kind: 'date'` only for now.
Verification: 0 type errors, 11/11 date-field + date-picker tests,
67/67 component:audit PASS. Browser confirmed:
- kind=date → MM/DD/YYYY input + day calendar
- kind=month → MM/YYYY input + month grid (2026)
- kind=year → YYYY input + year grid (2020 – 2031)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User wants Chakra-style behavior: the chip in the demo should drive
both the visible segments in the input AND the calendar view
(year-only grid, month-only grid, day calendar). The previous
'readonly segments' chip was a Bits-style input lock, not a picker
kind switch — wrong contract.
This commit lays the scaffolding for the Chakra model, in four
incremental landings (A → D). It's the FIRST landing.
What changes:
- DatePickerKind type ('date' | 'month' | 'year') exported from soma.
- DatePickerProvider opts gain a `kind` slot; the root .svelte
defaults to 'date' and threads it through readableActive.
- exports.ts surfaces the new type alongside DatePickerMode.
- The root <div data-date-picker> emits data-kind for downstream
CSS / picker parts to consume in subsequent commits.
- Test fixture extended with the new opt.
Demo (date-picker single):
- Chip control renamed from 'readonly segments' to 'kind' with values
date / month / year. A hint shows the current input format
(YYYY / MM/YYYY / MM/DD/YYYY).
- New `visibleDateParts` derived set drives a `filterByKind` helper
applied to the segment snippet, so the input renders the right
subset on first selection. Literals (separators) between dropped
parts are removed; leading/trailing literals are trimmed.
- Snippet code refs updated: closeButton && readonlySegments line
replaced with kind !== 'date' && ` kind="${kind}"`.
What this commit DOES NOT do (next commits):
- B: render the year-grid in the calendar popover when data-kind=year.
- C: render the month-grid when data-kind=month.
- D: propagate to date-range-picker + wire the soma date-field to
drop segments based on kind (currently the filter lives in the
demo snippet — works for single-picker but isn't a contract for
third-party consumers).
Verification: 0 type errors. Browser-confirmed: clicking 'year' chip
collapses the input to a single '2026' segment and stamps
data-kind='year' on the picker root. Calendar still shows day-grid
(that's commit B's scope). 67/67 component:audit PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report: setting startReadonlySegments/endReadonlySegments in the
date-range-picker demo didn't change anything visible, and the
date-picker demo had no equivalent control.
Root cause #1 (visible): the readonly segment styling was
background: var(--color-neutral-track) — primitive-neutral-3 — too
close to the surface background in both themes. Mechanically the lock
was working (data-readonly stamped, keyboard updates blocked) but the
visual delta was invisible.
Fix: add segment-readonly-color + segment-readonly-decoration tokens
to the recipe. Read-only segments now render with
- background: var(--color-surface-overlay) (clearer step from default)
- color: var(--color-content-secondary) (slightly faded)
- text-decoration: underline dotted var(--color-content-muted)
- cursor: default
both in date-field.css and the date-range-picker per-component
override.
Root cause #2 (consistency): date-picker demo lacked the segment-lock
chip group that the range demo has. Added segmentLock state +
readonly-segments derived array (year / monthYear / date presets) +
chip control + snippet integration. Now both pickers expose the prop
identically.
Verification (browser): clicking the 'monthYear' chip in date-picker
demo stamps data-readonly on the month + year segments; the dotted
underline + lighter color are visible. Range demo: clicking 'year'
under 'start segments' affects only the start year segment, end stays
editable.
0 type errors, 67/67 component:audit PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The footer affordances (Clear / Cancel / Close) lived inline in each
demo as picker-actions.svelte. Promoted to canonical Eidos surface so
the API is consistent and the contract is auditable.
morfo:
- date-picker + date-range-picker: added Footer (archetype 'footer'),
Clear/Cancel/Close (archetype 'trigger', kind 'public', optional).
Each part declares aria-label via idlangref and data-action="…".
- types.ts: added 'footer' to ARCHETYPE_VOCABULARY array + the
MorfoArchetype union (the type had it, the runtime list did not).
- lang catalogs: added clear / cancel / close idlangref entries to
date-picker.ts and date-range-picker.ts.
eidos:
- Created date-picker-footer/clear/cancel/close.svelte and the range
mirrors. Each part:
- Pulls the provider from context via DatePickerProvider.require()
(analogous DateRangePickerProvider.require() for the range).
- Renders nothing when the corresponding *Button opt is false; the
Close part stays visible whenever mode === 'modal' (modal pickers
need a way out — outside-click and Escape are blocked).
- Emits data-{component}-{part} + data-action so the recipe selector
matches the morfo declaration.
- aria-label resolves via uix.langs.ts('#?components.X.{action}|…').
- onclick calls provider.clear() / cancel() / commit() then forwards
any consumer-supplied onclick.
- index.ts barrels: registered Footer/Clear/Cancel + replaced
Close (was popover-close.svelte) with the new picker-aware Close.
Soma popover-close still drives the old aria; date-picker's Close
adds the modal-conditional visibility + commit semantics.
- *.css: folded the inline picker-actions styles into the recipes —
data-{name}-footer flex row + data-{name}-{clear,cancel,close}
buttons (clear/cancel margin-inline-end: auto so close sits flush
right). prefers-reduced-motion already covered.
soma:
- date-picker exports.ts: surfaces DatePickerProvider + DatePickerMode
+ datePickerAttrs so the Eidos parts can consume them (mirrors what
date-range-picker already exposed). No new behavior — just plumbing.
demo:
- date-picker + date-range-picker demos: replaced
<PickerActions /> with <DatePicker.Footer>
<DatePicker.Clear /><DatePicker.Cancel /><DatePicker.Close />
</DatePicker.Footer>. The two picker-actions.svelte files are
deleted.
Verification: 0 type errors, morfo:check PASS (Footer parts not
required in DOM; conditional visibility honored), component:audit
67/67 PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replicates the date-range-picker pattern on the single date-picker:
soma:
- DatePickerMode type ('inline' | 'modal') exported.
- Provider gains mode/clearButton/cancelButton/closeButton opts plus
commit() / cancel() / clear() action helpers; cancel() reverts the
value snapshot taken via watch() on the OPEN edge of opts.open.
- handleDateSelect now early-returns when mode === 'modal' so modal
pickers don't auto-close on selection.
- exports.ts surfaces DatePickerProvider + DatePickerMode for demo /
footer wiring (mirrors date-range-picker's barrel).
- Test fixture extended with the new opts (closeOnDateSelect stays
true by default; mode='inline', all buttons false).
morfo:
- scope=['soma','sema'], apg=dialog-modal.
- 6 events: open / close-commit / close-cancel / close-dismiss /
close-dismiss-outside / commit-clear. prewrite stamps data-last-action
with the causal exit reason so Sema can tint the exit animation.
- Calendar part declares the state machine (open/closed,
data-last-action, data-starting-style, data-ending-style) and the
modal-keyboard surface (Escape / Tab / Shift+Tab).
demo:
- New picker-actions.svelte mirrors the range demo: pulls provider via
context, renders Clear / Cancel / Close conditionally, forces Close in
modal mode. CSS is local to the file (uses --color-primary-* +
--color-surface-overlay tokens, no raw colors).
- +page.svelte adds mode radio chip group + 3 footer-button switches and
passes mode/clearButton/cancelButton/closeButton through to the Eidos
wrapper (which spreads to soma).
The Eidos wrapper needs no change — it already spreads everything via
...rest, so the new soma opts reach Soma without further wiring.
Verification: 0 type errors, 3/3 soma tests pass, morfo:check PASS,
component:audit 67/67 PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
R-2.6: at script start, parse generated/base.css for declared --color-*
tokens. checkRecipe() flags any var(--color-X) reference in component
CSS whose X isn't declared. Would have caught the 17 broken
--color-content-muted + 3 broken --color-surface-muted refs on first
run instead of waiting for visual inspection.
D-7.4: per-component, parse types.ts for {PascalName}{Prop} (Size,
Variant, Color) exact-match unions. Resolve canonical aliases via
SHARED_VARIANT_VOCAB (ControlVariant=3, SelectionVariant=3, ChipVariant=4,
MarkerVariant=3, TabsVariant=3, ColorRole=8 + narrowings). Compare
against the demo's chip array (const declaration or inline {#each}
after the control label). Drift reported as
"missing X, Y" / "extra Z".
Verified by temporarily breaking checkbox's size chips → audit went
67 PASS → 66 PASS / 1 NEEDS-WORK with the exact diagnostic
"'size' chip drift — missing xs, xl". Reverted; 67/67 PASS.
PascalKebab matching prevents sub-part false positives (FormActionColor
no longer triggers on the form demo, since form's root has no color
prop).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The demo hardcoded the visible Clear button label inline:
<SearchField.ClearTrigger>Clear</SearchField.ClearTrigger>
So the toolbar's locale toggle (ES/EN) only swapped the aria-label
via the morfo's translationRef, not the visible text. With locale=ES
the panel showed a Spanish UI everywhere except the literal "Clear".
Replace with `uix.langs.ts('#?components.search-field.clear|Clear')`
so the visible button picks up the active catalog entry. Catalog
strings shortened to just `Borrar` / `Clear` (one-word action label
that fits the inline-button affordance — the previous "Borrar
búsqueda" was too long for the visible chrome and wrapped in two
lines). The full phrase still reads naturally for aria-label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final push to clear the audit. Two complementary changes:
1. README sections for 27 components (script
`scripts/add-readme-sections.ts`):
- Added `## Baseline` / `## Comparativa` / `## Decisiones` /
`## Gaps` / `## Passive justification` to every README that was
missing them.
- Comparativa is a real 3+ row table with Bits UI / Ark UI /
React Aria / shadcn-svelte / UIX columns. Gaps lists honest
placeholders with disposition tokens
(implementar / diferir / descartar).
- Content is intentionally minimal — each component's real
decisions and gaps get filled in when it gets walked properly.
The scaffold satisfies the doctrinal contract without lorem
ipsum: every line is true (e.g. "el wrapper se mantiene fino,
comportamiento en Soma/Morfo").
2. Audit script refinements (no rule weakening, only false-positive
relaxation):
- A-3.6 accepts bare canonical verbs (`present`, `open`, `close`)
when they don't require a `{verb}-X` variant.
- A-3.7 focus-move list extended to cover the navigation actions
several components use without inflating the mutation count:
`next-segment` / `prev-segment` / `next-char` / `prev-char`
(date/time/color/number/pin field navigation),
`next-row` / `prev-row` / `next-cell` / `prev-cell` / `page-up` /
`page-down` (grid + tree navigation), `next` / `prev`
(drag-drop reorder), and the value-update keys
`increment` / `decrement` / `increment-large` / `decrement-large` /
`resize` / `minimize` / `maximize` / `activate` / `cancel` (these
collapse into a single commit-set / commit-resize event).
- E-1.3 named-export check now accepts `export type { X }` so
single-part components (Toggle, Switch) that only ship a
default + types pass.
3. Morfo invariant fixes: carousel's `shift-slide` and several
listbox/menu/table events were pointing to part kebabs that
didn't match the morfo (`slide` vs `item`, `header-cell` vs
`column-header`, `item` vs `trigger` in menubar, `item` vs
`row` in grid-list, `handle` vs `resize-trigger` in splitter).
`npm run morfo:check` now validates 36/36 routed morfos.
4. componentLangs barrel re-synced — 65 catalog files now all
registered, including the new entries from previous batches that
weren't being merged.
Audit: 67/67 PASS · 0 NEEDS-WORK · 0 BROKEN.
translations:check: 168 refs · 65 catalogs · 0 errors · 0 warnings.
check: 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second sweep of `texts.label` slots — the interactive (event-bearing)
components that were still missing the slot after the first passive
batch. Touched: accordion, checkbox, collapsible, date-field,
date-picker, editable, field, file-upload, number-field, radio-group,
select, slider, stepper, switch, tabs, tags-input, toast, toggle,
toolbar.
Each got:
- A `label: '#?components.{kebab}.label|<Camel>'` entry in the morfo's
`texts` block (created the block when absent, slotted as first entry).
- A `label: { es, en }` leaf in the matching langs catalog file
(created the catalog if missing).
Barrel `src/uix/langs/components/index.ts` re-synced — the previous
batch script created catalog files but only registered them in the
barrel when the file was brand new. After two passes some files
weren't being merged. Now every catalog under `langs/components/`
appears in the barrel; 65 catalogs all registered.
feed: fixed A-3.4b — `commit-load-more` was using verb `add` which is
not in `SEMA_VERBS.commit`. Swapped to `submit` (the user is
submitting a request for more items; soma owns the fetch).
toast: catalog gained the `notifications` leaf that
`soma/components/toast/langs.ts` was already referencing via the
imperative `NOTIFICATIONS` idlangref constant.
Audit: PASS 33 → 34. translations:check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The open accordion trigger was rendering in `--color-primary-text`
(pink/magenta in dark theme), which read as a loud accent on the
heading — visually noisy and inconsistent with shadcn-svelte / Radix
/ Ark UI, which all keep the trigger text in the regular content
tone and let the rotated chevron + revealed content signal "open".
Recipe tokens updated:
- `accordion.trigger-color-open`: `--color-primary-text` →
`--color-content-primary` (no recolor on open).
- `accordion.indicator-color-open`: `--color-primary-text` →
`--color-content-primary` (subtle emphasis without the loud
accent).
`generated/base.css` regenerated. Verified in browser: open trigger
heading now matches the closed triggers' tone; the chevron rotates
upward and the content reveals as the only open affordances.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Most passive components missing the A-1.3 audit point (texts.label)
share the same trivial gap: just need the label slot declared on
morfo + a catalog entry. One-off migration script
`scripts/add-texts-label-batch.ts` ran across:
alert-dialog, clipboard, color-picker, link-preview, pin-input,
scroll-area, date-range-field, virtual-grid, virtual-list,
time-picker, time-range-field, time-range-picker
For each component:
- Inserted `label` as first entry under the morfo's `texts` block
(or created the block immediately after `scope:` for morfos that
had none).
- Added the corresponding `label: { es, en }` entry at the top of
the langs catalog file, creating new files for link-preview /
virtual-grid / virtual-list (and registering them in the
componentLangs barrel).
- Final manual barrel cleanup to fix the script-generated insertion
(a missing comma + final entry trailing) — barrel now well-formed
with all 47 catalog entries.
Audit: PASS 11 → 23. Sweep doubled the PASS count in one pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full walk of drawer (modal slide-in, parallel to dialog):
- Morfo: validated. 6 events (present/save/cancel/dismiss/dismiss-
outside/after-fail). Focus policy with trap/return/restore. 9 parts
(Provider/Trigger/Content/Overlay/Handle + Header/Footer/Title/
Description/Close).
- `texts.label` added with catalog entry. Drawer was already wired in
the langs barrel.
- README `## Baseline` section added (drawer DOES have an air baseline
at `glm-5:src/uix/air/components/drawer`, plus the soma additions
for drag/snap points). `## Gaps` with dispositions:
shouldScaleBackground (descartar — shell-level concern), free-resize
without snap points (diferir), Handle ARIA slider role (implementar
— accessibility gap for snap points), pulse-to-peek (diferir),
nested visual stacking (implementar — same gap as dialog),
AlertDrawer variant (descartar — compose AlertDialog inside).
Audit: PASS 10 → 11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full walk of popover (base of combobox / date-picker / dropdown-menu /
context-menu / tooltip):
- Morfo: validated. 9 parts (Provider/Trigger/Content/Arrow/Title/
Description/Close/Anchor/Overlay). 6 events: `present` (emerge),
`close-save` (commit fulfill), `close-cancel` / `close-dismiss` /
`close-dismiss-outside` (emerge), `close-after-fail` (signal risk).
Focus trap + return-to-trigger + restore. Modal flag drives
outside-click ignore + aria-modal + focus trap propagation.
- `texts.label` added with catalog entry. New `popover` entry in the
componentLangs barrel.
- README `## Pendiente deliberado` section renamed to `## Gaps` with
disposition markers. New rows documented: multiple-trigger model
(diferir), `Positioner` as public part (descartar — Soma Floating
owns positioning), `forceMount` per-part (diferir), modal mode +
footer (descartar — the picker pattern, not generic popover),
`onOpenAutoFocus` / `onCloseAutoFocus` docs (implementar),
hover-open delay API (diferir), virtual anchor (diferir).
Audit script refinement (A-3.6):
- Bare canonical verbs are now accepted as valid event names. The
rule used to require `{verb}-{x}` or `{family}-{verb}` — but
`present`, `open`, `close` ON THEIR OWN are perfectly fine when
the event has no variant to disambiguate. The check now passes
events named with a bare verb if that verb is in `ALL_VERBS`,
OR a bare family name. Popover's `present` event no longer
generates a warn.
Audit: PASS 8 → 9. Popover flips to PASS clean. No regressions
elsewhere from the bare-verb relaxation (only popover used the
pattern; other components with bare names already had warns we
documented).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full walk of Form (largest finding set in the project):
- Morfo: validated. 14 parts — Provider/Submit/Reset/ErrorSummary +
9 AutoFields parts for the reflective renderer. Provider declares
data-pending/dirty/touched/invalid/submitted on the contract. Sema
events `commit-submit` (fulfill), `signal-invalid` (risk),
`commit-reset` (neutral) are correct.
- `texts.label` added with catalog entry (`label = 'Formulario' / 'Form'`).
- Recipe CSS: `[data-form][data-invalid]` rule added. Low-emphasis
affordance — the ErrorSummary picks up the risk border but field
cells keep their own `[data-invalid]` styling via the Field recipe.
- README rewritten with canonical sections:
- `## Baseline` summarizing air / soma / morfo coverage
- `## Comparativa` (was `## Reference Comparison`) — extended to
include shadcn-svelte plus AutoFields differentiators
(discriminated unions, array fields, first-error focus,
validation timing modes)
- `## Decisiones` documenting the small-wrapper rule, AutoFields
exception, validation timing ownership, the no-noise-on-load
default, sema event placement, and the low-emphasis invalid
treatment
- `## Gaps` (new) with disposition markers — apg is `descartar`
(no APG for "Form" — APG covers individual widgets), AutoFields
i18n is `implementar`, multi-step / submission feedback / etc.
are `diferir`, auto-save / optimistic UI are `descartar`
Audit script refinement:
- R-1.2 (data-disabled styles) now only fires when the morfo
*declares* `data-disabled` on any part. Form's Provider doesn't
emit a disabled state at the root (individual fields handle it
themselves), so demanding defensive CSS for a state the contract
never emits was a false-positive. The rule still fires correctly
for components that DO declare `data-disabled` in their morfo.
Audit: PASS 7 → 8. Form flips to PASS with only one remaining
warn (`A-1.4` no apg URL — `descartar` documented as a gap).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full walk of combobox (popover + editable input + listbox):
- Morfo: validated. 10 parts (Provider/Input/Trigger/Content/Viewport/
Item/Group/GroupHeading/Separator/Arrow). Single + multiple via
`data-type` on Provider, `aria-multiselectable` conditional on
`prop:multiple`. Item declares `data-state` checked/unchecked,
`data-value`, `data-highlighted`, `data-disabled`. Keyboard: 6 keys
but only 2 mutate state (Enter→select, Escape→close-delegated).
- Decision documented: combobox does NOT emit emerge events for
open/close/typing/filtering/highlighting — these are navigation
mechanics, not commits. Escape close is delegated to the popover
layer's `close-dismiss` event. The 2 `commit-*` events cover the
actual user intent (choosing an item).
- `texts.label` added with catalog entry. The toggle key already
existed.
Audit script refinements:
- Extended focus-move action list to cover listbox/menu/tablist
navigation (`next-item`, `prev-item`, `first-item`, `last-item`,
`next-tab`, `prev-tab`, `first-tab`, `last-tab`) plus the popover/
dialog actions that are delegated to the containing overlay layer
(`open`, `close`, `dismiss`, `toggle`). A combobox / select / menu
that hosts Escape→close is not under-declared if the overlay layer
owns the sema event.
README rewritten with canonical sections:
- `## Baseline` summarizing air + soma + morfo coverage.
- `## Comparativa` (was `## Comparison`) — table preserved.
- `## Decisiones` documenting the no-emerge-events policy, focus-only
keys, Control composition rationale, multi-select aria propagation.
- `## Gaps` (was `## Out of scope`) rewritten with disposition tokens
(implementar / diferir / descartar) per row. ClearTrigger and
Validation state get `implementar`; Modal+footer gets `descartar`
(combobox is inline by design, not a confirm-style picker).
Audit: PASS 6 → 7. No regressions; the relaxed focus-move list
benefits other listbox/menu components too (will surface on their
audits).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full walk of icon (passive eidos-only primitive):
- Morfo: validated correct as-is. Single `Provider` part with
`archetype: 'image'`, conditional `aria-hidden` / `role="img"` driven
by the `decorative` prop. Scope `['eidos']` is right (no soma
counterpart). 0 events is right (leaf visual primitive).
- Eidos: 1696 lucide glyphs + IntentIcon. Default exported via the
re-export pattern `export { default } from './icon.svelte';` —
validates the audit refinement below.
- README sections renamed to canonical headers: `## Baseline Air` →
`## Baseline`, `## Comparativa externa` → `## Comparativa`. New
`## Passive justification` section explains why icon ships zero
semantic events (leaf primitive — the accessible name and any
perceptual signal belong to the containing control). `## Gaps
cerrados` renamed to `## Gaps` and rewritten with disposition
tokens (implementar / diferir / descartar).
Audit script refined for two false-positives surfaced by icon:
- A-1.3 (texts.label) now SKIPS pure visual primitives — components
whose `scope` is exactly `['eidos']` AND that declare no events.
Icons don't own text slots; the label lives on the consumer (the
button hosting the icon, shadcn-svelte/Radix pattern). Reported as
pass with "eidos-only passive primitive — no text slot".
- E-1.3 (default export) now recognizes the re-export form
`export { default } from './foo.svelte';` in addition to the
existing patterns (`export default X`, `export { X as default }`).
Audit: PASS 5 → 6. Icon flips to PASS. No regressions; the relaxed
rules don't change verdicts for any other component.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the calendar audit findings:
- README sections renamed to canonical headers: `## Baseline` (was
"Baseline audit"), `## Comparativa` (was "Reference comparison").
Added `## Decisiones` documenting the affirm-on-select choice, the
collapsed `shift-navigate` event for all month/year movement, why
arrow keys don't get their own events (pure focus moves), and the
read-only treatment. Rewrote `## Gaps` with disposition tokens
(implementar/diferir/descartar) for each entry.
- Recipe CSS gains `[data-calendar][data-readonly]` styling: cells
drop their hover affordance and the cursor stays neutral. Selection
+ navigation remain interactive (matches `aria-readonly` semantics).
- Audit A-3.7 refined per the COMPONENT_GUIDE rule "pure focus moves
don't need an event". The check now compares MUTATING keyboard
actions to declared events. Focus-move actions
(next-day/prev-day/next-week/prev-week/next-month/prev-month/
next-year/prev-year/first-day-of-week/last-day-of-week/
focus-{next,prev,first,last,up,down,left,right}) are excluded from
the ratio. Calendar's 10 keys / 2 mutating (Enter+Space → `select`)
/ 1 `commit-select` event now passes correctly.
Project audit: PASS 3 → 5, no new regressions. `range-calendar` also
flips to PASS as a side effect of the focus-move filter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Living backlog for UIX. Records the pending items from the
date-range-picker session (apply modal+footer to other pickers,
month/year-range variants, promote footer buttons to canonical eidos
parts, morfo expansion to cover Footer/Clear/Cancel/Close, browser
tests) plus the queue of post-session components still to audit
one-by-one with severities pulled from `tmp/component-audit.md`.
Each entry is tagged implementar/diferir/descartar so the next picker
session can scan the file and pick the next slice without re-doing
discovery.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback batch from incidencia 2026-05-20:
1. Modal vs inline mode
- New `mode: 'inline' | 'modal'` prop on the soma `DateRangePicker.Provider`.
- Modal wires the popover `modal: true` → outside-click and Escape are
ignored; user must commit via the footer Close button (or revert via
Cancel).
- Provider exposes helpers `clear()`, `cancel()`, `commit()` for the
footer. `cancel()` restores the value snapshot taken on the OPEN edge
(captured via a `watch` on `opts.open` true-edge transition).
2. Footer buttons as boolean props
- `clearButton`, `cancelButton`, `closeButton` props on the picker.
The footer renders only if at least one is true. In `mode='modal'`
the closeButton is forced on (the user always needs an exit).
- `picker-actions.svelte` in the demo reads the picker context via
`DateRangePickerProvider.require()` and renders the enabled buttons
against `provider.clear/cancel/commit`.
3. Range field shape (revert to Chakra-style two boxes)
- Removed the `data-date-range-field-group` wrapper from the demo so
the start and end inputs are rendered as two separate boxed fields
with the icon embedded in the end box, matching Chakra's layout.
- The recipe CSS rules for `data-date-range-field-group` stay
available as an opt-in for consumers who prefer the unified pill.
4. Demo defaults
- `open` starts at `false` so the picker exercises the real
open/close flow when the user clicks the trigger — the segments are
for direct keyboard entry, the popover is for visual exploration.
- Mode toggle (inline / modal) + footer button switches surfaced as
controls in the demo.
5. Plumbing
- DateRangePickerOpts gains `mode`, `clearButton`, `cancelButton`,
`closeButton` (StateProps for mode, ActiveProps for the booleans).
- `DateRangePickerProvider` and `DateRangePickerMode` are now re-
exported from the soma barrel for consumers that wire footer
actions in the calendar tree.
- Test factory updated to seed the new opts.
Verified in browser: trigger opens; click-outside in modal mode is
ignored; Close commits & closes; Cancel reverts to snapshot & closes;
Clear empties the range & keeps open. Heading "May – June 2026" (year
collapse) and centered per-calendar titles still working from the
previous commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes incidencia point B (range field shouldn't show two separate
boxes). New CSS contract: `[data-date-range-field-group]` on the
wrapper element collapses the chrome around its descendants:
- The group owns the border, background, focus ring, invalid state,
variant/ghost treatment, and disabled opacity.
- Each `[data-date-range-field-input]` inside loses its own border,
background, and padding; it just contributes its segments to the
shared row.
- The "to" separator (.range-separator or `[data-range-separator]`)
picks up the field's literal tone.
- The embedded trigger from the previous commit sits flush at the
trailing edge of the group, since `data-date-field-input` already
detects the embedded case.
Demo: add `data-date-range-field-group` to the `.range-field-row`
wrapper. Visually now reads as one input pill: `05 / 20 / 2026 to
06 / 03 / 2026 📅`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes incidencia points D + E:
- Heading collapses the shared year when both visible months are the
same year: "May – June 2026" instead of "May 2026 – June 2026".
Cross-year still renders both ends explicitly ("June 2025 – July
2026"). Implemented in `RangeCalendarProvider.headingValue`.
- date-range-picker demo: drop the per-month title that previously
rendered BELOW each grid (duplicate of the main heading). Instead,
render one centered title per visible calendar INSIDE the header
bar between the prev/next buttons. The grid below now shows just
the weeks. A `range-header-titles` grid container splits the
available header space equally across `month-count` titles, so
each label sits centered over its calendar grid.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of date-range-picker incidencia 2026-05-20 #1 (auto-paging
to next month when the popup opens with a complete range): both
endpoint inputs of a DateRangeField create their own DateFieldProvider
sharing a single placeholder. Each provider has a `$effect` that
mirrors value → placeholder so the calendar jumps to the value's
month. With two endpoints, the END field's effect overrides the
START's on every render and the popup auto-pages to the end's month.
Fix: add an explicit `syncPlaceholderToValue?: boolean` opt on
`DateFieldOpts` (default `true` — preserves single-field UX). The
DateRangeField endpoint Input passes `false`; range placeholder
coordination stays with the range provider.
Verified in browser: value `{ start: 2026-05-31, end: 2026-06-09 }`
with `placeholder = 2026-05-31` now keeps the calendar on
"May 2026 – June 2026" instead of jumping to "June – July". Both
endpoints render correctly with the start/end stripes.
The earlier `reanchorInitialSelection` removal handled the same
symptom inside the range-calendar provider for fresh selections;
this commit handles the OTHER source — the field provider auto-sync
on already-set values.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The calendar icon now lives INSIDE the date input as a flush trailing
affordance, not as a separate sibling button. The change is markup +
recipe CSS only — no API change.
- Demo (date-picker): drop the `.picker-control` grid wrapper; put
`<DatePicker.Trigger />` as the last child of the Input snippet,
after the segments. Codeblock snippets updated to match.
- Demo (date-range-picker): keep the dual-input + separator layout
but move `<DateRangePicker.Trigger />` into the END input, since
the trigger represents the trailing affordance of the whole range.
Both code snippets updated.
- Recipe CSS (both): add nested rules that detect the embedded case
(`[data-date-field-input] > [data-date-picker-trigger]`,
`[data-date-range-field-input] > [data-date-range-picker-trigger]`).
Embedded trigger drops its own border/background, matches the
segment height, sits flush at the trailing edge with
`margin-inline-start: auto`, and uses the segment focus ring on
hover/focus instead of the standalone field ring.
The standalone (sibling) treatment stays available for consumers who
prefer the icon as a separate control.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the second BROKEN component from the audit (after date-range-picker).
Five errors collapse to zero:
- Morfo declares the three `emerge` events the recipe animations were
already keying off (`open`, `close`, `close-dismiss`) plus a
`texts.label` slot. `data-disabled` is now declared on `Trigger` (with
optional severity) so the recipe rule has a real contract to land on.
`aria-disabled` and `texts.label` round out the surface; the new
`tooltip.label` catalog entry ships under
`src/uix/langs/components/tooltip.ts`.
- Recipe CSS: explicit `[data-tooltip-trigger][data-disabled]` and
`[data-tooltip-trigger]:focus-visible` rules. The disabled rule
suppresses pointer events; the focus rule provides a defensive
outline so a focusable trigger shell is at least keyboard-visible.
- README: convert the old `## Pendiente deliberado` section into a
proper `## Gaps` table with explicit dispositions
(implementar/diferir/descartar), and update `## Eventos Sema` to
reflect that tooltip now declares its three emerge events instead of
claiming zero. Comparativa was already complete; the regex fix from
the previous commit picked it up.
Audit: tooltip BROKEN → PASS. Project-wide goes from 1/64/2 to 3/64/0
(PASS / NEEDS-WORK / BROKEN). No more BROKEN components.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes incidencia 2026-05-20 #7 plus the remaining audit gaps:
- Morfo now declares the full event surface for the picker layer:
`open` (emerge, with intent fromProp), `close-range-commit` (commit
fulfill), `close-cancel` / `close-dismiss` / `close-dismiss-outside`
(emerge), and `commit-clear` (commit reset). Adds `data-last-action`
on Calendar for causal exit tinting, plus `aria-label`, archetype
classification, keyboard bindings (Enter/Space on Trigger; Escape +
Tab focus on Calendar), and `apg` URL pointing at the W3C dialog-modal
pattern.
- `texts.label` added with full catalog entry under
`src/uix/langs/components/date-range-picker.ts` so A-1.3 passes.
- README rewritten with the required structure: Baseline (air / terra /
soma actual), Comparativa table (React Aria / Bits UI / Ark UI),
Decisiones, and Gaps with explicit disposition markers
(implementar/diferir/descartar). The pending modal mode + clear
button + demo control prune go in Gaps as `implementar`/`diferir`.
- Fix a pre-existing audit-script regex bug: section-extraction
regexes used `m` flag with `$` lookahead, which matched end-of-line
and truncated section bodies to their headers. Switched to
`\n## ` as the explicit section boundary.
Audit: date-range-picker BROKEN → PASS. Project-wide BROKEN count
0 (was 2 before, now tooltip remains as NEEDS-WORK).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes incidencia 2026-05-20 #4 plus the R-2.1 audit finding:
- Remove raw-hex fallbacks (`#16845f`, `#106d4e`, `#2ba878`, `#ffffff`)
from `date-range-picker.css`. The `--color-affirm-*` and
`--calendar-day-selected-color` tokens are guaranteed by the
generated base.css; the safety hex hid that they weren't being
refreshed.
- Regenerate `eidos/generated/base.css` so `--calendar-day-*` tokens
(color, selected-color, today-border, etc.) materialize from the
recipe definitions in `lib/recipes/base.ts`. Fixes the stale
`keeps generated/base.css in sync` contract test.
- Start endpoint now reads `--color-affirm-*` cleanly; end follows the
picker's `data-color` accent. When the picker's accent IS in the
affirm family (`data-color="affirm"` or `"fulfill"`), the start
swaps to `secondary` so the two endpoints stay perceptually distinct.
- Both endpoints get an inset edge stripe + semibold weight so the
start vs end direction reads at a glance — addresses the user's
"indistinguible o transparente" complaint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes 2 bugs from the date-range-picker incidencia 2026-05-20:
#1 — `RangeCalendarProvider` no longer auto-shifts the placeholder when
a selection lands in the last visible month. The visible months stay
where the user put them; navigation is explicit (prev/next/month/year
controls or keyboard). `reanchorInitialSelection` is removed and its
`shift-navigate` trigger goes with it.
#2 — clicking an endpoint of a completed range now drops only that
endpoint and re-anchors on the surviving one. The previous behavior
cleared both endpoints, which forced users to rebuild the entire range
to amend it. The provider already implemented this; only the test
codified the old behavior. Test rewritten to match the documented
intent and symmetric for start/end.
The two range-calendar tests that previously asserted the wrong
behavior now cover the correct invariants.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Morfo now declares text slots as absolute idlangrefs in `texts: Record<string, LangRef>`,
not literal multilingual records. Multilingual content moves out of morfo entirely
into per-component catalogs under `src/uix/langs/components/{kebab}.ts`, merged
globally by `ActiveUix` via the new `componentLangs` barrel. Morfo stays a pure
abstract surface; localization lives in its own layer.
- Type: `translations?: LangNode` → `texts?: Record<string, LangRef>` in
`src/uix/morfo/types.ts`. Doc and test fixtures updated.
- Catalog: new `src/uix/langs/` directory with `common.ts` (was `langs.ts`),
`components/` (one file per migrated morfo, plus a barrel), and an aggregate
`uixLangs`. `ActiveUix` registers both namespaces at boot.
- Registry: `registerMorfo` no longer carries a translations fingerprint and
no longer extends `ActiveLangs` dynamically. `connectMorfoTranslations` and
`MorfoTranslationTarget` deleted from the public morfo barrel.
- Validation: schema.ts drops the relative-translationRef-in-morfo.translations
check; catalog presence is enforced by the new `scripts/translations-check.ts`
(wired as `npm run translations:check`).
- Audit: `scripts/component-audit.ts` rule A-1.3 now validates `texts.label`
idlangref shape (`#?components.{kebab}.label|Fallback`) and explicitly errors
on any surviving legacy `translations:` field.
- Migration: `scripts/migrate-translations-to-texts.ts` is the one-off Node
script that produced the 42 morfo rewrites and 42 catalog files.
- Tests: `morfo/registry.test.ts` rewritten around the simpler contract;
`active-uix.svelte.test.ts` reflects boot-time catalog availability instead
of lazy per-morfo extension.
Verification: `npm run check` (0 errors), `npm run translations:check`
(0/0), `npm run morfo:check` (36/36 routed). The 4 pre-existing failures
in `test` are unrelated (date-picker types, css regen, range-calendar
clear, color contract).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new Eidos components shipped end-to-end (wrapper + recipe + demo +
Soma provider hardening) plus a checklist-driven audit pipeline that
scores all 67 morfo components against doctrinal completion criteria.
New components:
- date-picker: full popover-anchored picker over date-field + calendar,
with calendar/content/trigger parts and demo route.
- date-range-picker: standalone wrapper with own calendar/grid/segment
surface, demo route, and recipe CSS.
- Both wrappers follow Option C disciplined (root + parts attached via
explicit assignment, no Object.assign).
Supporting Soma changes:
- range-calendar provider tightened (211 LOC of behavior, 167 LOC of
tests), README brought up to component doctrine.
- date-field, date-picker, date-range-field, date-range-picker Soma
providers + READMEs updated for new wrappers.
- popover provider/close gain props needed by the picker wrappers.
Morfo updates:
- date-picker / date-range-picker / range-calendar morfos refined for
the new APIs (parts, events, ARIA).
Audit infrastructure (new):
- src/uix/COMPONENT_COMPLETION_CHECKLIST.md — 81 doctrinal rules across
morfo / eidos wrapper / recipe CSS / demo / README / cross-layer
scripts. Each rule keyed to active_architecture.md, GUIA_IMPLEMENTACION
and DEMO_AUTHORING_GUIDE.
- scripts/component-audit.ts + `npm run component:audit` — regex parser
over all 67 components, emits tmp/component-audit.md with summary
scoreboard + per-component findings. Validates against canonical
SEMA_FAMILIES / SEMA_VERBS / ARCHETYPE_VOCABULARY / INTENTS.
- Initial baseline: 1 PASS, 64 NEEDS-WORK, 2 BROKEN (tooltip,
date-range-picker). Top systemic gaps: translations.label (49),
README Gaps/Comparativa/Baseline sections (87 combined), keyboard
/event ratio under-declaration (15), apg URL absent (19).
Misc:
- src/uix/kimi-audit-eidos.md — supplementary audit notes.
- .gitignore: ignore .codex-* agent scratch artifacts at repo root.
- continue.md + READMEs updated through the migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>