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>
Per the doctrine update in GUIA §13: eidos for multi-part components
exposes BOTH a flat default that auto-composes Provider/Trigger/Content
AND the compound parts for advanced cases. Soma + morfo remain the
universal composable foundation underneath.
Why: with the previous "compound only" rule for multi-part, eidos
collapsible was a pure pass-through that added nothing operational —
the consumer wrote the same import + same shape as soma. The flat
default makes eidos earn its layer by collapsing the 90% case to a
single call:
// 90% case — flat ergonomic
<Collapsible bind:open>
{#snippet trigger()}Show details{/snippet}
<p>Body content.</p>
</Collapsible>
// Advanced — compound (parts in different subtrees, multi-trigger,
// conditional content)
<header><Collapsible.Provider bind:open>
<Collapsible.Trigger>Show</Collapsible.Trigger>
</Collapsible.Provider></header>
<aside>
<Collapsible.Content>...</Collapsible.Content>
</aside>
The architectural virtue: if the flat doesn't fit, a developer drops
to soma + morfo and composes freely. No vendor lock-in in the visual
layer; eidos is the design system convention, soma is the universal
primitive.
Files:
- src/uix/eidos/components/collapsible/collapsible.svelte (NEW): flat
default that auto-composes Provider + Trigger + Content via a
`trigger` snippet for the toggle label and `children` for the body.
- src/uix/eidos/components/collapsible/types.ts: adds
`CollapsibleFlatProps = SomaCollapsibleProps & { trigger?: Snippet }`.
- src/uix/eidos/components/collapsible/index.ts: exports `default`
(flat) plus named `Provider`/`Trigger`/`Content` (compound).
- src/uix/eidos/components/collapsible/{trigger,content,provider}.svelte:
fixed children destructuring so snippets propagate cleanly to soma
(was previously spreading $props() blindly which can lose snippet
bindings in Svelte 5).
- src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md: NEW §13 "Doctrina del API:
soma compound, eidos opinionado" formalises the two-shape rule and
the architectural virtue (soma+morfo as universal foundation).
Renumbered §13/§14 → §14/§15.
- web/routes/collapsible/+page.svelte: live preview now uses the flat
form; eidos snippet shows the flat shape; soma snippet keeps the
compound shape; Notes tab updated.
- src/uix/eidos/components/collapsible/README.md: rewrote API section
with both shapes + the "if neither fits, use soma+morfo" escape.
Tests 218/218 green. npm run check baseline 39.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two real bugs reported on the collapsible demo:
1. **Dark mode broken across all migrated components.**
- The docs site layout sets `document.documentElement.dataset.theme =
'light'|'dark'` — those are the selector names the docs CSS uses.
- The eidos themes (themes/base/{light,dark}.css) only respond to
`[data-theme='base-light'|'base-dark']`. Different namespace.
- Result: docs grays swap in dark mode but eidos doctrinal palette
(primary/secondary/neutral/affirm/fulfill/risk/threat/loss) stays
in light values, so component text renders dark on dark.
- Fix: each eidos theme now accepts BOTH selectors. `[data-theme=
'base-light'], [data-theme='light'], :root { ... }` and equivalent
for dark. Existing consumers that use `base-*` keep working; the
short `light/dark` aliases handle the docs site convention.
2. **Collapsible trigger jumps from center to left when expanded.**
- The preview-stage uses `display: flex; justify-content: center;
align-items: center;` — natural for buttons/switches that don't
grow.
- `Collapsible.Provider` is `display: block` so it shrink-fits its
content as a flex item. Closed = trigger-width (centered visually);
open = trigger + content width (longer, so the visual center
anchors trigger at the left edge).
- Fix: collapsible demo overrides the preview-stage style with
`justify-content: flex-start; align-items: flex-start` and wraps
the provider in a `max-inline-size: 32rem` panel. Disclosure
ergonomics need anchor-left + grow-down, not center.
- Also swapped the body's `--gray-50` (docs site token) for
`--color-surface-raised` + `--color-content-primary` (eidos
doctrinal tokens) so the panel inherits the dark-mode swap.
npm run check holds at the 39-error baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third component migrated to the eidos wrapper subdirectory pattern,
following the new "check air baseline first" discipline.
Air baseline recovered from commit 0a391408^:
- air/components/collapsible: 3 wrappers (Provider/Trigger/Content)
with `class` passthrough, `bind:open`, `onOpenChange`, manual
`air.interaction.play('expansion', enter|exit)` perceptual emit.
- No size, intent, color, or chevron slot.
Migration decisions (presented BEFORE coding per scope-approval rule):
- Two doctrinal events (`expand` + `collapse`, both emerge family,
sequence='pre', no intent — emerge is transitional). Replaces air's
manual `air.interaction.play` with `runtime.trigger('expand'|
'collapse')` which atravieses visual + sound + vibra channels.
- No intent/color/size — air didn't have them and disclosure is
non-evaluative per guide §3.1.
- Multi-part API: Provider + Trigger + Content (no flat default per
doctrine §10).
Files:
- src/uix/morfo/components/collapsible.ts: scope expanded to ['soma',
'sema'], events array added with two emerge events.
- src/uix/soma/components/collapsible/collapsible-provider.svelte.ts:
runtime.events handlers `expand`/`collapse` mutate state; toggle()
fires runtime.trigger(next ? 'expand' : 'collapse').
- src/uix/eidos/components/collapsible/{collapsible.css,
collapsible-provider.svelte, collapsible-trigger.svelte,
collapsible-content.svelte, types.ts, index.ts, README.md}.
- src/uix/eidos/components/collapsible.css (legacy flat) deleted.
- src/uix/eidos/index.css: import path updated to subdirectory.
- web/routes/collapsible/+page.svelte: full interactive testbed with
tabs (Live, Soma, Morfo, Sema, Eidos, Notes) mirroring switch's
structure. Sema tab shows per-channel signature cards for both
expand and collapse events.
Disciplina formalizada en CLAUDE.md hand-off + memory entry: "Check
air baseline first" before any future eidos migration. Switch shipped
without this check and lost ResponsiveProp<Size> (regression). Don't
repeat with collapsible/dialog/drawer/popover/toast/avatar.
npm run check holds at the 39-error baseline. Tests 218/218 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Architectural correction. Morfo was importing Svelte runes, $adom, and
$uix/sema — all violations of "morfo is pure declarative DNA". The
runtime that interprets a CompiledMorfo lives where it executes: soma.
Move:
- src/uix/morfo/runtime.svelte.ts → src/uix/soma/runtime.svelte.ts
- src/uix/morfo/runtime.svelte.test.ts → src/uix/soma/runtime.svelte.test.ts
Rename (consistent with the move; "soma's runtime", per the doctrine
"morfo declares, soma executes"):
- class MorfoRuntime → SomaRuntime
- createMorfoRuntime() → createSomaRuntime()
- type MorfoRuntimeSources → SomaRuntimeSources
- error prefixes [morfo-runtime] → [soma-runtime]
Sema is now genuinely ornamental:
- ActiveUix.semantic returns EngineSemantic | undefined (no longer
throws when no engine is configured). Components in SSR / headless
tests / audio-disabled environments stay functional.
- SomaRuntime.trigger() skips the emit step AND the target-resolution
check when no engine is present — only requires a DOM target when
something will actually be emitted.
- Soma.semantic getter returns | undefined too (was non-optional).
Consumers updated:
- src/uix/active-uix/active-uix.svelte.ts — imports createSomaRuntime
from $soma; semantic getter returns | undefined
- src/uix/active-uix/types.ts — interface SomaRuntime, semantic | undefined
- 8 providers (toggle, switch, collapsible, dialog, drawer, popover,
toast, avatar) — type SomaRuntime, import path $soma
- src/uix/soma/index.ts — re-exports createSomaRuntime + types
- src/uix/morfo/index.ts — drops the runtime re-exports (morfo is pure)
- src/uix/sema/signal.ts — JSDoc reference updated
- src/uix/soma/core/soma.svelte.ts — semantic getter | undefined
Test added: SomaRuntime.trigger() succeeds without a semantic engine
(skips emit + target check, runs handler directly).
Docs aligned to the new shape:
- active_architecture.md — runtime locations, sema as ornamental
- soma/SOMA_ARCHITECTURE.md, soma/README.md — name + import paths
- morfo/README.md — file listing now flags morfo as PURE TS, points
at soma/runtime.svelte.ts for the interpreter
- sema/README.md — SomaRuntime references
- README.md — root architecture diagram
- CLAUDE.md — hand-off note
Tests 218/218 green. npm run check holds at the 39-error baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4 (docs alignment with the canonical guide):
- active_architecture.md — Dialog example refactored to the new event
shape (target inside semantic, explicit verb + sequence). Verbs
table grouped by family with the post-canon vocabulary (select/
toggle moved to commit, edit removed from handle, new verbs added).
Section 10 (Estado actual) updated to 2026-05-08 with current
progress: doctrina cerrada, eidos en producción, SoundChannel
eager-init, switch migrated, etc. "Para profundizar" §14 points at
the canonical guide.
- morfo/README.md — Step 5.5 added covering the doctrinal event
shape with all fields (semantic.family, .verb, .target, .sequence,
.intent) and the two naming shapes ({verb}-{variant} and
{family}-{verb}).
- sema/README.md — verbs section restructured to Record-by-family
matching the runtime; documents the verbs that moved between
families per the canon. Naming shapes section updated with both
forms and validateEventName signature.
- CLAUDE.md — "Session hand-off" rewritten with the post-canon
state. Pointer at GUIA_IMPLEMENTACION_SEMAUIX.md as the canonical
source. Pending list refreshed: switch is the immediate next
component, persistence/a11ySemantic/polymorphism deferred until
real consumers appear.
Switch migration to the eidos wrapper pattern:
- morfo/components/switch.ts — adds events: [{ commit-toggle }] with
the doctrinal shape (verb=toggle, sequence=post, intent fromProp
binding to the 4 supported intents). New data-* (data-color with
the 6-token Toggle/Switch subset, data-readonly, data-invalid,
data-required) and corresponding aria-*. scope expanded to
['soma', 'sema'].
- soma/components/switch/types.ts — adds intent, color, readonly,
invalid, aria-labelledby props with full JSDoc. Subset constrained
per guide §3.1 (Toggle/Switch row).
- soma/components/switch/switch-provider.svelte.ts — runtime-direct,
Field integration via FieldProvider.get(), isDisabled/isReadonly/
isRequired/isInvalid OR-merge derivations, intent ↔ color
resolution (intent wins when evaluative), runtime.trigger(
'commit-toggle') in onclick.
- soma/components/switch/components/switch.svelte — wires the new
props through bindProps.
- eidos/components/switch/{switch.css, switch.svelte, types.ts,
index.ts, README.md} — full subdirectory pattern, mirroring
toggle. Recipe drops legacy success/warning/danger/info selectors;
declares the 6 doctrinal data-color values directly. Wrapper
auto-renders Switch.Thumb inside the track and exposes an optional
`thumb` snippet for in-thumb glyphs.
- eidos/components/switch.css (legacy flat) deleted; eidos/index.css
updated to import the subdirectory recipe.
- web/routes/switch/+page.svelte — interactive testbed with all soma
props as live controls + tabs (Live, Soma, Morfo, Sema, Eidos,
Notes). Mirrors toggle's structure.
npm run check holds at the 39-error baseline. Tests 217/217 green.
Pending:
- Migrate collapsible / dialog / drawer / popover / toast / avatar
to the eidos wrapper pattern.
- Persistence + holds-by-intent — defer until first signal.warn /
signal.alert consumer.
- a11ySemantic + polymorphic events — defer until concrete need.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The new doctrinal guide at src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md
supersedes src/docs/sema-implementation-guide.md and locks the
canonical vocabulary, event shape and color tokens. This commit aligns
the codebase to that canon in three concrete moves.
Phase 1 — sema/verbs.ts vocabulary:
- Restructured from flat array to family-keyed Record per the guide.
- Moved select / toggle / acknowledge → commit (these fix state, not
contact). Removed edit from handle (it's shift.enter-mode now).
- Added new verbs per the canon: contact (tap, focus, trigger,
release), commit (complete, restore, expire, set, remove, reorder),
signal (remind), handle (rotate, scroll, reorder), shift (route,
step, return, context), sustain (start, loading, waiting, syncing,
processing, streaming, pending, retrying, end).
- validateEventName now recognises both naming shapes:
{verb}-{variant} (head=verb) and {family}-{verb} (head=family).
- New helper familyForVerb. Tests rewritten — 16/16 green.
Phase 2 — morfo event shape:
- target moved from event-level into event.semantic. Adds optional
semantic.verb (advisory, validated against SEMA_VERBS[family]) and
optional semantic.sequence ('pre' | 'coincident' | 'post').
- compile.ts and schema.ts updated. The 22 events across toggle,
toast, popover, drawer, dialog refactored to the new shape with
explicit verb + sequence.
- Polymorphic capability (allowedFamilies + defaultSemantic) and
a11ySemantic deferred — no current consumer needs them; will land
when the first concrete usage appears.
Phase 5 — color tokens (doctrinal 8):
- themes/base/light.css and dark.css rewritten to expose primary,
secondary, neutral, affirm, fulfill, risk, threat, loss. Renames
success→fulfill, warning→risk, danger→threat (same hex). New
primitives for secondary (slate-blue), affirm (teal-mint, low
activation), loss (deep violet-grave, posterior). info palette
deleted entirely — per guide, "info no es intent, es signal.announce
+ neutral".
- _static.css legacy compatibility aliases (--color-bg, --color-success,
--color-error, --color-accent, ...) deleted. Clean cut, no
transition. Consumers reference doctrinal palettes directly.
- 29 consumers (token files + recipes) migrated:
- All --*-color-info-* declarations dropped.
- Component palettes extended with secondary / affirm / loss.
- switch + checkbox recipes updated to use [data-color='affirm'|
'risk'|'threat'|'secondary'] (was 'success'|'warning'|'danger'|
'info').
- toast tokens + recipe renamed to affirm/fulfill/risk/threat/loss
(was success/error/warning/info).
- meter, progress, mark, badge, button tokens renamed.
- Contracts (contracts/contract.css + contracts/components/*)
updated.
- focus-ring-color-error in themes now points to --color-risk-border
(validation errors are signal.warn + risk per doctrina, not threat).
Phase 4 (partial):
- src/docs/sema-implementation-guide.md deleted (superseded by the
canonical guide).
Tests: 217/217 green across uix. npm run check holds at the 39-error
baseline (all pre-existing in unrelated test files; none introduced).
Pending in follow-up commits:
- Update active_architecture.md, sema/README.md, morfo/README.md,
CLAUDE.md to reflect the canon.
- Switch component migration to the eidos wrapper pattern.
- Persistence field (untilAction / untilFix / stateBound) and
holds-by-intent — defer until the first signal.warn / signal.alert
consumer appears.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Deleted 60 obsolete documents that no longer reflect the active-uix
state:
- 47 per-component audit logs (*-audit.md, audit_N.md, AUDIT.md)
- 5 global soma audits (AUDIT_1, codex_audit, soma-audit-04-20/21,
audit-prompt template)
- 5 closed studies (soma/study, morfo/study, morfo/DESIGN, morfo/
PROVIDER_STUDY_2026-04-23, uix/CONTINUITY_2026-04-24)
- 2 superseded refactor proposals (refactor_claude, refactor_code)
- 1 stale bug log (form/BUG-onchange-onblur-hang — repro path
src/routes/test/sium/kitchen-sink no longer exists)
Updated the remaining operational references to drop dead links and
reflect the post-cleanup architecture:
- src/uix/README.md — rewritten links section, dropped pointers to
terra/air/CONTINUITY/refactor docs, updated adom/dom paths
- src/uix/active_architecture.md — header now dated 2026-05-08, lists
the major shifts since 2026-04-26 (provider inheritance dropped,
eidos in production, doctrina parte IV, sound eager-init); pendings
section reflects the eidos migration roadmap; "see also" purged of
deleted files
- src/uix/morfo/README.md — file tree updated (removed DESIGN/study,
added compile/runtime/PERMUTATION_RUNNER); see-also pruned
- src/uix/sema/README.md — SoundChannel section now describes the
eager-init pattern (constructor-time, not lazy); pointer to
sema-implementation-guide added
- src/uix/soma/README.md — drops terra/air mentions, declares the
actual dependency direction (eidos -> soma, not the reverse)
- src/uix/soma/SOMA_ARCHITECTURE.md — §15 rewritten as "Estado actual
y deuda histórica" reflecting Provider-inheritance-drop; demo path
updated to web/routes/
- src/uix/soma/COMPONENT_GUIDE.md — A23 dates section now points to
$libs/days (no external/dates façade); demo paths swapped from
/test/soma/* to web/routes/
- src/uix/morfo/PERMUTATION_RUNNER.md — paths swapped to web/routes/
with a relocation note
- CLAUDE.md — Refactor Documents section replaced with Reference
Documents pointing to per-layer READMEs
npm run check holds at baseline (39 pre-existing errors, none new).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper +
types + index + README) replacing the flat CSS-only form. Pattern is
documented in eidos/components/README.md and the toggle README.
Shared types live in eidos/lib/types.ts. First export is `Size` (8 values
xxs..xxl + full); components narrow with `Extract<Size, ...>` per the
per-component-subset doctrine. No `Eidos` prefix on types — module path
already conveys the layer.
API doctrine:
- soma stays compound (Toggle.Provider) for symmetry with multi-part
- eidos exports both default + Provider so single-part components
accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>`
(compound-style consumers)
SoundChannel eager-init fixes the autoplay race: AudioContext is created
+ resumed synchronously on the first user gesture (capture-phase
listener registered in the constructor), avoiding the previous race
where the first emit() scheduled the resume in a microtask outside the
gesture window.
Demo page (web/routes/toggle/+page.svelte) restructured so the live
preview renders ALWAYS above the tablist — Sema-tab Play buttons can
fire on the real toggle. Motion preview amplifies scale ×8 visually
only; doctrinal values stay in the <dl>.
Conventions 7-13 added to src/docs/sema-implementation-guide.md
covering: directory structure, wrapper composition, no Eidos prefix,
soma compound vs eidos flat, iconOnly sr-only body, sound eager-init,
docs-preview amplification.
CLAUDE.md gets a session hand-off block listing where things stand and
next concrete steps (migrate switch/collapsible/dialog/drawer/popover/
toast/avatar; wire topbar sound mute to masterGain; rename theme
tokens to drop the success/warning/danger fallback aliases).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes Phase A of the soma cleanup. Every soma component that was
already runtime-aware (Toggle, Switch, Collapsible, Avatar from prior
commits + the four in this commit) now lives without the `Provider<T>`
inheritance.
Pattern applied to each (8 classes total across the four components):
- Drop `extends Provider<...>`. Each class is plain.
- Drop module-level `createAttrs(morfo)` and `registerContract(morfo)`
side-effect calls.
- Drop `super(opts, ...)`. Constructors inline:
root: this.opts = opts; XxxProvider.ctx.set(this); …
child: this.opts = opts; this.provider = XxxProvider.require(); …
- Drop `assertProps()` wrapping. Eidos `lint.ts` covers contract
validation at design time; morfo type system covers enum mismatches
at compile time.
- `props` derived: `{ ...runtime.partProps(part), …overrides }`.
- For sub-parts that own a per-instance prop (Title.level,
Toast.Action.altText), pass the source through `registerPart(...,
{ props: { x: () => opts.x.current } })` so resolution stays local
to that part's effect.
- For ref-tracking callbacks (Dialog/Drawer/Popover content/overlay,
Drawer trigger/handle), use `registerPart(..., { onRefChange })`
instead of the legacy `super(opts, spec, undefined, onRefChange)`.
Component-specific notes:
- Dialog: 7 classes; root tracks parent + nesting via context lookup
BEFORE `ctx.set(this)`. `dismissWith(action)` API kept identical.
- Drawer: 8 classes; gesture/snap/handle plumbing untouched. The
drawer morfo doesn't declare `value:` sources for `data-state`,
`data-side`, `data-dragging`, etc., so those stay as inline
overrides in each part's `props` (component-specific computed
state, not morfo contract). To be lifted into the morfo as a
follow-up if it pays off in eidos selectors.
- Popover: 8 classes; floating layer (`FloatingProvider`/Anchor/
Content/Arrow) wires through unchanged.
- Toast: 7 classes; multi-runtime structure preserved — root
runtime carries Viewport, each Item creates its own runtime so
`partRef('item')` resolves to that toast's element. Title /
Description / Action / Close inside an Item register on the
Item's runtime, not the root's.
- alert-dialog: small drive-by — `this.dialog.soma?.langs.ts(…)`
became `Soma.get()?.langs.ts(…)` because `DialogProvider.soma` is
no longer an instance field.
Verified: 209/209 UIX tests green; TS clean for all migrated files
(remaining repo-wide TS errors pre-existing, unrelated).
After this commit:
- 8 soma components are runtime-direct (toggle, switch, collapsible,
avatar, dialog, drawer, popover, toast).
- ~52 components still use `Provider<T>` via the legacy `baseProps +
resolveMorfoProps` path. Those will migrate as Fase B —
opportunistically (each time one is touched for another reason)
rather than in a sweep, per the user's standing preference for
small, surgical commits.
- The `Provider<T>` base class stays as transitional infrastructure
until Phase B drains. Final deletion lands in Phase C.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same migration shape as toggle / collapsible. After 4 pilots the
pattern is consistent enough to extract a tiny base class — that's a
follow-up commit.
Switch (root + Thumb) — exercises `runtime.keydown` for action
dispatch (Enter / Space → 'toggle' action that calls
`preventDefault()` before flipping state).
Avatar (root + Image + Fallback) — exercises a multi-part component
where one child (Image) drives a watch.pre side effect to preload the
image and update parent state, and another (Fallback) reads the
parent's `status` reactively.
Per-class diff in both files:
- drop `extends Provider<...>`
- drop module-level `createAttrs(morfo)` and `registerContract(morfo)`
- drop `super(opts, ...)` calls; root inlines `ctx.set(this)`,
children inline `this.opts = opts; this.provider = Parent.require()`
- drop `this.assertProps(...)` wrapping in `props` derived
- drop `WithRefOpts` extension; inline `id: Active<string>; ref:
State<HTMLElement | null>` per child opts shape (more opt-in)
Avatar's `AvatarFallbackOpts` keeps `ref: State<HTMLElement | null>`
required because the fallback always renders DOM. `AvatarImageOpts` and
the root `AvatarOpts` derive from props via `OptsFromProps`.
Verified: 209/209 UIX tests green, TS clean for both files.
After this commit the migration sample is:
- toggle (single part, no ctx)
- switch (single part + ctx + actions/keyboard)
- collapsible (3 parts + ctx + cross-part id sources via partRef)
- avatar (3 parts + ctx + side-effect watch.pre + multi-state)
Common shape across all four:
- `readonly opts: T;` declaration + `this.opts = opts` assignment
- root: `XxxProvider.ctx.set(this)` (when ctx present)
- root: `Soma.require()` + `soma.uix.runtime(morfo, sources)` +
`runtime.registerPart('provider', { id, ref })`
- children: `this.provider = XxxProvider.require()` +
`parent.runtime.registerPart(part, { id, ref })`
- `props` derived → `{ ...runtime.partProps(part), ...customProps }`
The 60+ unmigrated components still use the legacy `Provider<T>` base
(baseProps + resolveMorfoProps path). That class stays put until those
components are also runtime-aware.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second pilot. Collapsible exercises the multi-part case Toggle didn't:
3 providers (root + Trigger + Content), one shared morfo runtime
created by the root, and Svelte context wiring so children can find
the parent. The migration shape mirrors Toggle but factors in the
extra plumbing.
What changed:
- All three classes (`CollapsibleProvider`, `CollapsibleTriggerProvider`,
`CollapsibleContentProvider`) drop `extends Provider<...>`.
- Root: `CollapsibleProvider.ctx.set(this)` is called manually in the
constructor (replaces the implicit `super(opts, spec, ctx)` setup).
The `context()` helper from `\$soma/provider/context` is independent
of the `Provider` base class and remains unchanged.
- Children: just store `this.opts = opts` and look up the parent via
`CollapsibleProvider.require()`. No `super(opts, spec)` call.
- Drop module-level `createAttrs(collapsibleMorfo)` and
`registerContract(collapsibleMorfo)` — same dead-side-effect pattern
Toggle had.
- Drop `assertProps` in every `props` derived. Eidos linter + morfo
types cover what the runtime contract validation did.
What stayed identical:
- Public surface — `CollapsibleProvider.create(opts)`,
`CollapsibleProvider.require()`, `state.props`, `state.toggle()`.
- The shared-runtime pattern: root creates `uix.runtime(morfo, sources)`
once; children call `parent.runtime.registerPart('trigger' | 'content',
{ id, ref })`. This is what the morfo runtime was designed for.
- The `triggerId` / `contentId` cross-part state sources for
`partRef` resolution. Children publish their id via direct assign
(per the "id registration: direct assign" rule, not \$effect).
- All 209 UIX tests green.
Note on `WithRefOpts`: the `Provider` base used `extends WithRefOpts,
StateProps<...>, ActiveProps<...>` to compose the opts type. Without
the base, the migrated classes inline `id: Active<string>; ref:
State<HTMLElement | null>` directly. Tradeoff: a couple of extra lines
of opts shape per class, but the type composition is now opt-in
(children that don't need a ref can omit it) instead of mandated by
inheritance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First soma component to live without the `Provider` base class. Toggle
already used `uix.runtime(toggleMorfo, sources)` for the morfo-derived
attrs since an earlier refactor; this commit removes the residual
inheritance + dev-time validation scaffolding that was no longer
pulling its weight.
What changed:
- `ToggleProvider` no longer extends `Provider<ToggleOpts>`. It is a
plain class that owns `opts`, the field hookup, and the runtime.
- Drop the module-level `createAttrs(toggleMorfo)` (dead — return
value was unused) and `registerContract(toggleMorfo)` (the contract
registry is only read by `assertContract`, which we no longer call
for this component).
- Drop `super(opts, { morfo: toggleMorfo, part: 'provider' })`. The
runtime's `registerPart('provider', { id, ref })` already attaches
the ref via `attachRef` internally.
- Drop `this.assertProps(...)` wrapping in the `props` derived. The
eidos linter (`scripts/eidos-lint.ts`) already validates emitted
selectors against the morfo's `cssSelectors` contract at design
time, and the morfo type system catches enum mismatches at
compile time. Runtime contract validation was belt-and-suspenders.
What stayed identical:
- Public surface (`ToggleProvider.create(opts)`, `state.props`,
`state.snippetProps`).
- `OptsFromProps`/`bindProps` from `\$soma/provider/opts` — those
helpers are independent of the `Provider` class and remain useful.
- The wrapper `toggle.svelte` is untouched; it still consumes
`state.props` and `state.snippetProps` as before.
- All 209 UIX tests green.
The `Provider` base class is intentionally not deprecated. The other
60+ soma components still depend on it. Toggle proves the pattern
works for runtime-only components; future migrations follow per
concrete need (a 2nd consumer of the new pattern justifies extracting
shared bits, per the "no premature abstraction" rule).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Provider base class re-implemented every helper from the morfo
resolver verbatim — 9 functions plus its own `resolveMorfoProps` walk.
The runtime already routes through `compileMorfo` + `evalAttrPlan`
since Phase 2a; the Provider is now wired the same way.
What changed in soma/provider/provider.svelte.ts:
- Drop the local `findMorfoPart`, `shouldEmitMorfoEntry`,
`resolveMorfoPrimitiveSource`, `resolveMorfoSource`,
`resolveMorfoDataValue`, `resolveMorfoAriaValue` (~75 LOC of literal
duplication of resolver.ts).
- Drop the manual `resolveMorfoProps` walk (~28 LOC). New body is
seven lines: spread `compiledPart.staticAttrs`, then `evalAttrPlan`
each `dynamicAttrs` plan and skip undefined results.
- Drop the `createAttrs(spec.morfo)` call from the constructor; read
`compiled.parts.attrs[part]` directly. No semantic change.
- Drop the `findMorfoPart` walk for the part metadata; read
`compiled.parts.byKebab.get(part)` (O(1)).
- `_morfoPartMeta: MorfoPart | undefined` becomes
`_compiledPart: CompiledPart | undefined`. Same role, richer info.
Net diff: provider.svelte.ts goes 287 → 149 lines (-138).
What stayed identical: the public API (`baseProps` getter, `assertProps`
helper, `resolveMorfoProps(bindings)` signature, the dual constructor
overload for legacy subclasses without a morfo spec) plus the
behavioral contract — same context registration, same data-attr
contract validation, same prop snapshot shape for subclass
`\$derived.by` blocks. Subclasses are not touched.
Side effect: `resolveMorfoProps` now emits ARIA boolean values as the
literal strings 'true'/'false' (via `evalAttrPlan`'s `aria-boolean`
mode) instead of JS booleans. Aligns the Provider with the morfo
runtime's behavior (resolver.ts already returned strings; only
provider.svelte.ts had been emitting booleans). Svelte stringifies
both identically when spread onto an element, so the rendered DOM is
unchanged. All 209 UIX tests still green.
Also re-export the compile.ts surface (`compileMorfo`, `evalAttrPlan`,
`CompiledMorfo`, `CompiledPart`, `AttrPlan`, `KeyboardPlan`,
`ActionPlan`, contracts, etc.) from `\$uix/morfo` so consumers don't
have to dig into the file. Fix a stale comment that still pointed at
the deleted `\$uix/lib/types`.
Phase 2b proper (kernel rewrite with the 8 abstractions:
ComponentInstance / PartScope / PartRegistry / SourceRegistry /
ActionDispatcher / SemanticEmission / LayoutFacts / BehaviorRegistry)
is parked on purpose. None of those abstractions has two concrete
consumers today; deduplicating against the existing compiler is the
honest first move and unblocks any future kernel work without churn.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The runtime is now the first production caller of `compileMorfo`. Each
per-tick effect previously walked the raw morfo tree; now it reads
pre-resolved plans off the compiled output.
What changed in runtime.svelte.ts:
- Single `compileMorfo(morfo)` call at the top (cached by WeakMap, so
multiple component instances of the same morfo share the result).
- `findMorfoPart` linear scan → `compiled.parts.byKebab.get(...)` O(1).
- `createAttrs(morfo)` walk → `compiled.parts.attrs` (pre-built).
- `findEvent` linear scan → `compiled.actions.byName.get(...)` O(1).
- `resolvePartAttrs` per-effect walk over `data` + `aria` arrays →
`{ ...staticAttrs, ...evalAttrPlan(plan, bindings) for plan in dynamicAttrs }`.
`staticAttrs` (literals + role) are pre-resolved at compile time;
only `dynamicAttrs` is re-evaluated per tick.
- Keyboard matching: `matchesKeySpec(specString, event)` re-parsed
the string every keystroke. Now the compiler emits `KeyboardPlan`
with a precomputed `ParsedKey`; matching is a flat field compare.
- Event dispatch uses `ActionPlan` (compiled view) instead of raw
`MorfoEvent`. `event.target.target` partRef-unwrap → `action.target`.
What was added in compile.ts:
- `evalAttrPlan(plan, bindings)` — evaluates one AttrPlan: short-circuits
on falsy condition, then branches once on `mode` instead of on
`source.kind` × `values.length` × `value.kind` per tick. Reuses the
pure helpers in resolver.ts (`shouldEmitMorfoEntry`,
`resolveMorfoSource`).
- Bug fix in `parseKey`: do not trim the literal key segment. `' '`
(Space) was being collapsed to empty string. The pre-existing
`matchesKeySpec` had a comment warning about this; the refactor
needed to preserve the same invariant. Caught by an existing
runtime test (Switch keyboard with Enter+Space).
Verified: 209/209 UIX tests green (morfo + soma + sema + eidos +
active-uix). The remaining 2 TS errors in runtime.svelte.test.ts
(lines 366 and 385) are pre-existing test casts and unrelated.
resolver.ts is no longer called from runtime.svelte.ts directly except
for the two pure helpers `shouldEmitMorfoEntry` (used by evalAttrPlan
and by keydown's condition check) and `MorfoBindings` (the binding
shape). The non-pure walk functions (`findMorfoPart`,
`resolvePartAttrs`) remain in resolver.ts for any consumer that still
imports them, but the runtime no longer does.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The form component still imported from paths that were deleted in the
active-uix cleanup:
- form.svelte: \$lib/sium/svelte (SiumProvider)
- form-auto-fields: \$lib/sium/core (Schema, SchemaKind, SiumShape)
\$langs (LangInstance)
Changes:
- form.svelte: drop the SiumProvider context fallback. The form
receives the schema via the `schema` prop (or none, for forms with
only `defaults`). The old fallback let a parent context inject the
schema implicitly; that was a single-call-site coupling and the
new ecosystem has no equivalent context yet. Re-introduce it later
via a soma-level scope only if a second consumer asks for it
(2-of-3 rule).
- form-auto-fields.svelte:
\$lib/sium/core → \$sium (Schema, SchemaKind, SiumShape are
re-exported from the package barrel)
\$langs (LangInstance) → \$lang (ActiveLang). The CompatibleLang
Pick<…, 'ts' | 't'> shape stays
identical since ActiveLang
carries the same surface.
Verified: svelte-check errors went 46 → 42 (the 4 form errors are gone;
remaining 42 are pre-existing in other components unrelated to the
cleanup). No form tests exist anymore (both were deleted in earlier
phases as casualties of the same import drift).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When src/uix/lib/types.ts went down with the rest of src/uix/lib/ in
phase A of the cleanup, the Layer and PartRef cross-layer primitives
went with it. Three files still imported them via `'../lib/types'`:
- src/uix/sema/types.ts
- src/uix/morfo/types.ts
- src/uix/morfo/compile.ts
Move the two types to src/uix/types.ts (top-level shared, sibling to
all UIX layers — no layer "owns" a cross-layer primitive), translate
the doc comment to English, and update the three importers to
`'../types'`. No semantic change.
Verified: svelte-check went from 49 errors to 46 (the 3 caused by the
deletion are gone); the remaining 46 are pre-existing and unrelated.
Vitest: morfo + eidos + sema = 136/136 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove src/uix/soma/components/form/components/form.svelte.test.ts
— same casualty as the auto-fields test removed in phase B: imports
from \$langs and \$lib/sium/* (both deleted).
- Update arts/session/ssr.ts JSDoc example: \$lib/app → \$active-app.
Note: form.svelte and form-auto-fields.svelte themselves still import
from \$lib/sium/* and \$langs. They will not compile against the new
ecosystem and need a rewrite or deletion as a separate decision —
flagged but not changed here.
Also note: src/arts/cache/test/engine-cache.test.ts fails with
"defineActiveCache is not a function". The test file is untracked
(was never committed); the failure predates this branch and is not
caused by the cleanup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removed in this commit:
- src/uix/air/ — old visual layer (~2.5k files)
- src/uix/terra/ — reference primitives (~765 files)
- src/lib/ — legacy root: actx, ling, logr, ext, glob,
util/dias, util/funcs, sium status notes,
uiux (~334 files). Replaced by src/arts/,
src/libs/, src/svrs/.
- src/uix/soma/components/form/components/form-auto-fields.svelte.test.ts
— only file in the codebase still importing
from \$uix/air. Form auto-fields will be
rebuilt with soma primitives if/when needed.
Verified post-deletion: zero matches for `from '\$lib(?!s)`, `\$glob`,
`\$langs`, `@/ling`, `@/logr`, `@/glob`, `@/actx`, `@/uiux`, `\$terra`,
`\$uix/(air|lib|terra)` across the remaining src/. The new ecosystem
(arts/, libs/, svrs/, uix/{active-uix, soma, sema, eidos, morfo}) is
self-contained.
Aliases in svelte.config.js / vite.config.ts still reference the
deleted paths — that's the next commit (phase C).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`src/uix/lib/` (13 files): orphaned DOM utility layer; replaced by
`src/libs/dom/` in the new ecosystem. Zero inbound imports.
`src/routes/` (459 files): test routes for the dead terra/air/soma demos,
plus a few stragglers under sema/eidos/sium. Demos will be re-authored
fresh under `web/routes/` once the SvelteKit config is updated.
No production code paths were touched — only the legacy reference
implementation and its test pages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>