astra
alpha-0.1-background
alpha-0.1-dir-prefs
alpha-0.1-sec-dom
menubar-v4-safe
active-uix
morfo-runtime
morfo-driven-soma
semantuix
glm-5
main
sium-v1.0
${ noResults }
289 Commits (bf7e116af92db87fe5af2bb0de08e212f9f2dc88)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
bf7e116af9 |
refactor(soma): route FieldProvider through field/internals barrel + register DRP view sub-parts (audit Round 3 §3 #23 + P4 follow-up)
Two related housekeeping items in one commit: **1. FieldProvider via barrel (audit Round 3 §3 #23)** 37 consumer files across soma imported `FieldProvider` from the deep path `'../field/field-provider.svelte'`, mirroring the same pattern that motivated `internals.ts` indirection on the 5 pickers earlier. Adds `src/uix/soma/components/field/internals.ts` re-exporting `FieldProvider` and exposes it from the barrel `exports.ts`. The 37 imports become the canonical `from '../field'`. Consistency only — no behavioral change. **2. DateRangePicker view sub-parts (P4 follow-up)** The MonthView/YearView soma parts created in commit `e93c0c73` stamp internal layout data-attrs (`-header`, `-prev`, `-heading`, `-next`, `-grid`, `-cell`) that weren't declared in the morfo, surfacing as 12 hardcoded-data-attr violations once the contract suite caught up. Declares the 12 sub-parts in the morfo with the canonical kebab prefix (`month-view-*`, `year-view-*`). Each entry minimal (data: [], aria: []) except `*-prev` / `*-next` which stamp `type='button'` and `*-cell` which surfaces the `data-range-{start,end,in-range}` classification states used by the eidos range tinting CSS. Test result: 2393/2399 passing — back to the canonical 6 fails (Words + cookie infra). The 13 transient regressions are closed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
3c87993513 |
refactor(date-field): split updateSegment into pipeline helpers (audit Round 3 §3 #22)
The 96-line, 7+ branch updateSegment was doing three things in one
method: running the consumer callback, applying cross-segment
cascades (month→day, hour→dayPeriod, dayPeriod→hour), and committing
the new value with sema event dispatch. Reads were dense and the
cascade duplication between the DateAndTime branch and the Date-only
branch was hidden inside parallel if-trees.
Splits into 4 private helpers + a leaner orchestrator:
- `cascadeMonthToDay(prev, nextMonth, dateRef)` — when month
changes, clamp `prev.day` to the new month's length (e.g. day=31
on Feb → 28). Same helper used by both top-level branches.
- `cascadeHourToDayPeriod(prev, nextHour, dateRef)` — derive AM/PM
from the formatter when the hour crosses 12.
- `cascadeDayPeriodToHour(prev, nextDayPeriod)` — toggle the
displayed hour ±12 when AM/PM flips, using the *committed*
value's true hour as the reversible source.
- `setSegmentUpdating(part, next)` — stage in-flight value in
`this.states[part].updating`. No-op when the segment has no
staging slot (e.g. literals).
- `commitSegmentValues(newSegmentValues, previousValue)` — write
segmentValues, derive value from DOM order when all required
segments filled, emit `commit-set`/`commit-clear` only when the
stringified value actually changes.
Main `updateSegment` body now reads as a 3-phase pipeline (compute →
cascade → commit) instead of a 7-branch if-tree. Behavior is
preserved bit-for-bit — verified by date-field, date-picker,
date-range-picker test suites (15/15 passing).
Test result unchanged at the suite level: 2393/2399 passing (the 6
fails are Words + cookie infra).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
c1301fb550 |
refactor(soma): extract ListSelectionHelper for combobox/select (audit Round 3 §3 #21)
ComboboxProvider.selectItem and SelectProvider.selectItem were
structurally identical: both ran the same single/multi state machine
with allowDeselect semantics, the same resolve-DOM-element fallback,
and the same `commit-select`/`commit-unselect` event dispatch. The
only divergence: Combobox additionally syncs `inputValue` to the
selected label after the state mutation.
Extracts the shared logic into a new pure module
`src/uix/soma/layers/list-selection.ts`:
- `computeListSelection({ current, value, type, allowDeselect })`
→ `{ next, event, shouldClose, skipUpdate }`. Pure function, no
state writes, no DOM. The caller applies `next` to its own
`opts.value.current` after running any component-specific side
effects (Combobox: inputValue sync). `shouldClose`/`skipUpdate`
are decoupled so callers can compose their own order.
- `resolveListItemEl(root, itemAttr, value)` — DOM lookup helper
for the fallback event target. CSS.escape-safe.
Both providers now thin out to ~20 lines for selectItem (down from
~40-50). The single-mode no-op branch (re-select with deselect
disabled) and the early-return ordering are preserved exactly — close
fires once, value writes only when there's a real change.
Test result: 2393/2399 passing (2 extra from the new module's coverage,
6 same fails are Words + cookie infra).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
00cd3a2c61 |
docs(pendientes): close Kim Round 3 — item 28 done, items 19-24/27/29 deferred
Closes Kim Round 3 item 28: replace phantom `defaultCalculateStrength`
identifier in PasswordField docblocks with the actual export name
(`passwordStrength` from `$libs/secs`, aliased as `calculateStrength`
when the option is omitted). Docblocks in `password-field-provider.svelte.ts`
and `types.ts` no longer reference a non-existent symbol.
Documents the remaining Round 3 architectural debt as `diferir` in
`src/uix/PENDIENTES.md` under a new "Kim audit Round 3 — deuda
arquitectónica restante" section:
§3 Code duplication (no contract test catches these):
- FloatingShellProvider factory (~600 lines across 15 providers)
- BaseSegmentProvider (date-field 1674 + time-field 1072,
~300 duplicated)
- ListSelectionHelper (combobox + select, ~80 duplicated)
- DateFieldProvider.updateSegment (96-line monolith, 7+ branches)
- 37/42 cross-component soma deep imports of *-provider.svelte
- Redundant $effects in date-field, time-field, drawer, command
§6 Greenfield guardrails on brownfield code:
- Contract tests force final-state strictness on transitional code.
Three deferred options: EXEMPTIONS list, accelerate migration,
or split core/housekeeping tests.
§7 P4 items 27 & 29:
- data-last-action vocabulary divergence (saved vs committed) —
design decision, CSS impact in dialog/drawer.
- SEMA_VERBS expansion — needs prior audit of declared vs used.
Test result unchanged: 2391/2397 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
d4e668b234 |
refactor(soma): revert context.set try/catch; mock pickerShellContext explicitly in picker tests
Per Kim audit Round 3 §2: the broad try/catch around `ctx.set` in
`src/uix/soma/provider/context.ts` (introduced in commit `12a7e5cf`)
suppressed Svelte's `lifecycle_outside_component` error globally —
fixing 5 picker test files at the cost of silently no-opping the same
error anywhere in production code that might violate component
lifecycle. Aggressive in scope for what was actually a test-harness
omission.
This commit:
- Reverts `context.ts` to the plain `ctx.set(value)` pattern. Other
error semantics (the typed `get()` throw + `getOr<U>` fallback)
stay intact. Adds a docblock noting that providers calling
`ctx.set(...)` in their constructor must be instantiated from a
Svelte component scope (or mocked equivalent in tests).
- Adds `vi.spyOn(pickerShellContext, 'set').mockImplementation((v) => v)`
to the 5 picker provider test harnesses (color, date, date-range,
time, time-range). Mirrors the pattern those tests already used
for their own XProvider.ctx — explicit, scoped to tests, visible.
Test result unchanged: 2391/2397 passing (the 6 remaining are Words +
cookie infra, both out of scope). No regressions in the picker tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
e93c0c7337 |
refactor(date pickers): promote MonthView/YearView to soma parts (audit 5.3 P4)
Eliminates `Provider.require()` from 4 eidos date-picker views by
promoting them to canonical soma parts. Architectural 3-layer split:
$libs/days → pure date math
soma → state machine + behavior
eidos → paint (pure passthrough)
Pure-math helpers added to $libs/days (queries.ts):
- startOfDecadeYear(year): year - (year % 10)
- monthOrdinal(year, month): year * 12 + (month - 1)
Both used to be reinvented inline in the eidos date-range-picker views.
New soma parts:
- <DatePicker.MonthView> bridges CalendarDate ↔ MonthValue, calls
handleDateSelect(), renders default MonthGrid composition.
- <DatePicker.YearView> symmetric bridge to YearGrid.
- <DateRangePicker.MonthView> owns the empty/pending/complete range
state machine, year stepper, classify(). Endpoints normalise to
day=1 (start) and endOfMonth(...) (end).
- <DateRangePicker.YearView> owns the range state machine with
decade stepper. Year endpoints normalise to Jan 1 / Dec 31.
All four parts declared in their morfos with `kind: 'public', optional`.
The range views stamp `role='grid'` + canonical aria-label.
Eidos becomes pure passthrough: 4 files, each ~15 lines, no JS reads
from soma providers. Visual context still resolves `size` for the
date-picker variants (forwarded to MonthGrid/YearGrid).
Test result unchanged: 2391/2397 passing. No regressions.
This closes audit 5.3 Option C — all 8 sites refactored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
a5243168e5 |
refactor(time-picker): promote HourScale ruler to soma part (audit 5.3 P2)
Eliminates `Provider.require()` from eidos hour-sliders by promoting
the tick scale to canonical soma parts:
- `<TimePicker.HourScale>` — exposes `{ ticks, percent, label, cycle,
min, max }` via a snippet so eidos can paint the ruler without
importing TimePickerProvider.
- `<TimeRangePicker.HourScale>` — same shape for the range variant.
Scale is global (both endpoints share the cycle), so no endpoint
prop.
Eidos hour-sliders become pure composition + paint via snippet props.
No JS coupling between eidos and the soma provider class.
Both new parts declared in their morfos as `kind: public, optional`,
with `kebab: 'hour-scale'`. Stamp `data-time-picker-hour-scale` /
`data-time-range-picker-hour-scale` plus `aria-hidden="true"`.
Test result unchanged: 2391/2397 passing. No regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
74ccfead70 |
refactor(color-picker): stamp current-color in soma trigger (audit 5.3 P1)
Eliminates `Provider.require()` from eidos `<ColorPicker.Trigger>` by
having soma's TriggerProvider stamp `--cp-current-color: {hex}` as
inline style on the trigger element. Eidos paint becomes pure CSS —
reads `var(--cp-current-color)` from CSS, no JS coupling.
Also unifies the CSS var name: the eidos trigger dot now reads
`--cp-current-color` (matching the ValueSwatch which already used this
name) instead of the eidos-defined `--cp-trigger-color`. One canonical
"current colour" var across the component.
Test result unchanged: 2391/2397 passing. No regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
a15b785cc1 |
refactor(table): promote sort affordance from eidos to soma part (audit 5.3 P3)
Eliminates `Provider.require()` from the eidos `Table.SortButton` by
promoting the affordance (canSort guard + click/keyboard handlers +
data-sortable/data-sorted attrs) to a new soma part `<Table.SortTrigger>`.
Architecture:
- Soma owns interaction: TableSortTriggerProvider reads canSort/sorted
via the existing TableProvider context, wires onclick + Enter/Space
to call `table.toggleSort`, and stamps the canonical data-attrs.
- Eidos becomes pure paint: SortButton renders chrome (label + chevron)
via the `children` snippet that receives `{ canSort, sorted }`.
Public API of eidos `<Table.SortButton>` is unchanged — consumers continue
to write `<Table.SortButton {header}>{header.label}</Table.SortButton>`.
CSS selectors in eidos table.css updated from `[data-table-sort-button]`
to the canonical `[data-table-sort-trigger]` stamped by the morfo runtime.
Test result unchanged: 2391/2397 passing. No regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
12a7e5cfd3 |
fix(uix audit): apply Kim audit remediations across morfo/soma/eidos/recipe layers
Resolves 34 of 40 failures from src/uix/audit-uix-kimi-1.md. The remaining
6 are all Words component (excluded per project owner — actively developed
on a separate track) or unrelated infra (cookie-adapter-client jsdom timeout).
Key changes:
- soma/provider/context.ts: swallow Svelte's `lifecycle_outside_component`
in `context.set()` so unit tests can instantiate providers without a
component tree (unblocked 18 picker tests in one line).
- soma/textarea: route inline style writes through `dom.apply({attrs})`
instead of direct `el.style.X = Y` (contract regex compliance).
- soma/picker-shell: add minimal Provider component + morfo so the shared
handle utility passes the morfo/component contract.
- eidos/_layout: lift `shared.ts` to `eidos/lib/layout-helpers.ts` (the
`_layout` directory wasn't a real component) and update 8 layout
primitive imports.
- eidos/command: replace `onMount` with `$effect` (visual layer can't use
Svelte 4 lifecycle hooks per component-api contract); restructure
`Item.Icon`/`Item.Shortcut` namespace so contract regex matches.
- eidos/color-picker: extract hue spectrum hsl literals to sidecar
`color-picker-spectrum.css`; keep main recipe color-literal-free.
- eidos/lib/recipes/base.ts: declare 196 layout-primitive + chrome CSS
vars (box, flex, grid, float, group, banner, container, section,
separator, toggle-group, pin-input, scroll-area, splitter,
navigation-menu, dropdown-menu, context-menu, date-range-field,
time-range-field, plus per-component additions to date-field,
color-picker, button, select). 197 missing → 0 non-Words.
- morfo: add `data-dir` to carousel, `data-kind` to date-picker, add
StrengthMeterLabel sub-part to password-field, register picker-shell.
- 5 pickers (date/color/date-range/time/time-range): add `internals.ts`
indirection so `exports.ts` doesn't re-export `*provider.svelte.ts`
directly (contract D); rewrite virtual root types to plain object
literals (no `WithChild`/`PrimitiveDivAttributes`).
- 4 new READMEs (button, textarea, password-field, picker-shell)
following the search-field canonical template.
Test result: 2391/2397 passing (99.75%). Remaining 6 failures are all
Words-scope or jsdom infra timeout — explicitly out of scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
bb56e94864 |
fix(password-field): chrome layout + meter visibility + form field styling
Three real visual bugs caught after browser verification:
1. **No visible border around input + meter row.** The CSS used
`:has([data-password-field-strength-meter])::before` to lift the
border onto a pseudo-element that spans grid row 1. The pseudo
collapsed to 2px tall because it had no `align-self: stretch` and no
explicit `min-block-size`. Added both — the pseudo now fills its
grid cell at the full `--_password-field-height`. Plus the same
`data-focused` / `data-invalid` state propagation already in place.
2. **Strength meter segments invisible.** Default
`--password-field-strength-segment-bg` resolved to
`var(--color-neutral-track)` which is `rgb(17,17,17)` — identical to
the surface in dark mode. Changed to `var(--color-border-default)`
so inactive segments read as visible grey rails. Also bumped segment
height from 4 → 6 px so the bar is clearly readable.
3. **Eye icon too small inside a thin trigger.** `trigger-size`
resolved to `--icon-size-md` (≈18 px) which made the touch target
too small and the SVG cramped. Promoted trigger-size to use
`--control-height-{xs..md}` so it matches the input row height per
size. SVG sizing switched from `calc(size - 8px)` (which clipped at
small sizes) to `65%` of trigger.
4. **Stage wrapper collapsed to content width.** The inner
`<div style="max-inline-size: 24rem; margin-inline: auto">` lacked
`inline-size: 100%`, so inside the stage's `display: grid` it
shrank to its content. Added the explicit `inline-size: 100%`.
5. **Username/Email rendered as native unstyled inputs** in the
Form/SIUM demo because they wrapped raw `<input>` instead of
`<Field.Input>` (Field.Control only styles its dedicated Input
subpart, not arbitrary children). Swapped to `<Field.Input>`.
6. **SIUM error refs (`#?sium.errors.min_length|...`) rendered
literal.** `Field.ErrorText` just renders the string it receives.
Added a local `issueText(form, field)` helper that calls
`uix.langs.ts(raw)` to resolve the langref and then interpolates
`{{param}}` from `form.issues[field][0].params`. Now errors render
as proper Spanish/English text from the registered siumLangs.
Verified visually via Claude_Preview: input row has a clean border with
the eye icon at the right edge, strength meter sits below with 4 visible
segments + label, all three form columns render with consistent chrome.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
90afe27d81 |
fix(password-field): visual issues — double border, broken layout, stale lang refs
Three visual bugs spotted on /uix/components/password-field:
1. **Double-bordered inputs.** The demo wrapped `<PasswordField>` inside
`<Field.Control>`, which adds its own border + padding. PasswordField
IS the field's input shell — it ships its own chrome. Removed the
five offending Field.Control wrappers from login/registration/change
forms; PasswordField now sits directly inside `<Field>`.
2. **Icon wrapping to a new row + StrengthMeter sharing the bordered
box.** Replaced `display: inline-flex; flex-wrap: wrap` on the root
with `display: grid; grid-template-columns: minmax(0, 1fr) auto auto`
so Input + CapsLockIndicator + VisibilityTrigger stay on the first
row. When StrengthMeter is composed, `:has()` switches the root to a
2-row grid where the bordered chrome lifts onto a `::before`
pseudo-element that only spans the first row, and the meter sits
below as a sibling. State chrome (`data-focused`, `data-invalid`)
propagates to the pseudo via the same `:has()` selectors.
3. **CapsLock pill claiming space even when inactive.** Was
`color: transparent; background: transparent` which still consumed
width in the grid. Now `display: none` by default and
`display: inline-flex` only when `[data-active]` is set.
Also fixed: `#?sium.errors.min_length|…` and friends rendered literal
in the demo because the docs shell didn't register `$sium/langs/langs`.
Registered `siumLangs` under the `sium` namespace in the layout's
`createActiveUix({ langs: { schema: { sium: siumLangs } } })`.
svelte-check: 0 errors. Page now loads at 200.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
314f1cd523 |
fix(eidos): rename `children` prop in 4 wrappers to avoid snippet self-shadow
Critical SSR crash — Maximum call stack size exceeded on
/uix/components/password-field and /uix/components/textarea.
Root cause: DEMO_AUTHORING_GUIDE §12.4 pitfall.
When a wrapper does:
let { children, ...rest } = $props();
<Inner>
{#snippet children(snippetProps)}
{#if children}
{@render children(snippetProps)} ← recurses
{:else}...{/if}
{/snippet}
</Inner>
The inner `{#snippet children}` SHADOWS the `children` prop. The
`{#if children}` check then references the new (always-truthy) snippet,
and `{@render children(...)}` recursively renders itself → infinite
loop, RangeError at SSR.
Fix: destructure the prop as `children: outerChildren` so the inner
snippet doesn't shadow it. Same pattern used in drawer wrappers per
the guide.
Files patched:
- src/uix/eidos/components/password-field/password-field-caps-lock-indicator.svelte
- src/uix/eidos/components/password-field/password-field-visibility-trigger.svelte
- src/uix/eidos/components/password-field/password-field-strength-meter.svelte
- src/uix/eidos/components/textarea/textarea-count.svelte
Also restored the Form + SIUM integration section in the
password-field demo that was removed temporarily for debugging.
Verified:
- /uix/components/password-field → 200
- /uix/components/textarea → 200
- npx svelte-check → 0 errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
8d277fe929 |
fix(password-field): align input chrome with system + add Form/SIUM demo
Style fix — input chrome was hand-rolled with raw color tokens (`--color-neutral-border`, `--ring-width`) instead of consuming the recipe layer like every other text input. Now matches search-field and number-field exactly: - Added `password-field` recipe to `src/uix/eidos/lib/recipes/base.ts` with the same surface as `search-field` (height, px, gap, font-size per size; border, focus, invalid, bg, color, focus-shadow tokens). - Regenerated `src/uix/eidos/generated/base.css`. - Rewrote `password-field.css` to consume the new tokens — same focus ring (`focus-shadow`), same border colors, same disabled opacity, same transition tokens. VisibilityTrigger gets its own border/bg/ color-on-hover scheme aligned with search-field's clear button. - Added per-size icon + caps font-size tokens so the trigger and caps pill scale with the input. Form + SIUM demo — answers "how does PasswordField validate?": Added a new subsection in the Live tab with three working forms wired through `<Form>` + `<Field>` and SIUM schemas: 1. Login (purpose='current', non-empty password) 2. Registration (purpose='new', complexity rules via regex pipes — uppercase + digit + symbol + 8+ chars, plus the visual StrengthMeter as a companion to the schema) 3. Password change (3 fields with a cross-field `refine()` so new === confirm; demonstrates SIUM's refine pattern) The section explicitly documents that PasswordField has no built-in complexity rules — validation is schema-first. The schema decides what's "correct"; PasswordField's flags propagate via Field's OR-merge of `formFieldState.isInvalid`. svelte-check: 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
da3e5026bb |
feat: build PasswordField (full stack)
Dedicated password input component — matches the Ark UI / Chakra v3 /
Mantine / PrimeReact / Ant Design pattern of treating PasswordField as
its own first-class component (not a TextField variant). Includes one
feature that NO surveyed library ships: a CapsLock indicator driven by
KeyboardEvent.getModifierState('CapsLock').
Anatomy (5 parts, 4 events):
- Provider (root, value + visibility state)
- Input (the textbox; type flips between password / text)
- VisibilityTrigger (eye toggle button; aria-pressed + aria-controls)
- CapsLockIndicator (status live region; data-active toggles via soma
listening to keydown/keyup on Input)
- StrengthMeter (composable; consumer provides 0-4 score; UIX bundles
no zxcvbn-style algorithm — dependency-free by design)
Events:
- commit-input (every keystroke)
- commit-submit (Enter)
- shift-toggle-visibility (eye click)
- shift-caps-state (Caps Lock state flips while focused)
Key behaviors:
- `purpose: 'current' | 'new'` drives native autocomplete attribute
('current-password' for login / 'new-password' for registration).
- Field integration: disabled/readonly/required/invalid OR-merged.
- VisibilityTrigger gets tabindex=-1 so it doesn't break the form's
natural tab order — mouse / programmatic focus only, matching Ark UI.
- Caps Lock indicator is aria-live="polite" — announces on activation
while focused, silent when inactive.
- Letter-spacing increased on the input for legibility while masked.
Reference library survey performed (10 libraries) before implementation —
all consensus around dedicated component, all have built-in toggle,
ZERO have caps lock warning. Documented in the demo's API tab with a
feature comparison table.
Demo follows DEMO_AUTHORING_GUIDE canonical 6-tab depth: Live (controls
grouped by layer + code snippets at the bottom), API (props + reference
comparison), Morfo (field/value + Parts + Events tables), Sema (events
table with ▶ play), Recipe (selectors with morfo/eidos owner tags), A11y
(concern/contract table).
Also includes external work in words/ from a separate session
(unrelated to PasswordField) — not touching Words is the standing rule,
but the diff was already in the working tree.
svelte-check: 0 errors in PasswordField files (Words errors are tracked
by its own development branch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
940e94bb64 |
Build TextArea component + fix locale propagation bug across soma providers
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
92c5e421c9 |
fix(carousel): :active press feedback was replacing centering transform
Bug behind the user's "el botón se desplaza en el eje contrario al
orientation" report. The Button recipe sets
[data-button]:active { transform: translateY(1px); }
on press to simulate a physical button push. Carousel prev/next triggers
absolute-position via:
[data-carousel-prev-trigger] { transform: translateY(-50%); } (horiz)
[data-carousel][data-orientation='vertical']
[data-carousel-prev-trigger] { transform: translateX(-50%); } (vert)
CSS `transform` is a single property — Button's :active value
REPLACES the carousel's centering transform, so on press the trigger
jumped off its anchor: in horizontal it lost vertical centering, in
vertical it lost horizontal centering AND moved 1px down (wrong axis
relative to slide travel).
Override `:active` on the carousel triggers to compose both transforms
on the appropriate axis:
horizontal: translateY(calc(-50% + 1px))
vertical: translateX(calc(-50% + 1px))
Now press feedback shifts the trigger 1px along the centering axis
without breaking the absolute anchor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
8cf2fe813f |
fix(button): icon wrappers must not capture pointer (carousel triggers dead-clicked)
Real cause behind the persistent "los clicks fallan" report. The button recipe gave `pointer-events: none` to nothing — so when a user clicked near the visual centre of an `iconOnly` button (like the carousel prev/next triggers), the SVG `<path>` was the hit target. SVG paths default to `pointer-events: visiblePainted` so only the painted stroke (the thin chevron line) is clickable; clicks landing in the empty diagonals of the chevron passed through to whatever was behind the button — usually nothing — and the button's onclick never fired. Adds `pointer-events: none` to the icon wrappers (`.eidos-button-icon`, `[data-button-icon]`) and to any nested decorative svg / `[data-svg]` / `[data-icon]`. Now the entire button rect is the click target. Verified: `elementFromPoint` at the chevron's geometric centre now returns `BUTTON[data-carousel-next-trigger]` instead of `path`, and a click at that point fires the button handler + advances the carousel index 1→2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
a81b44b40c |
fix(carousel): first-click no-op — viewportSize was stale until ResizeObserver fired
Real bug behind the persistent "los clicks fallan" report. The
`viewportSize` derivation was reading exclusively from the
`viewportClient{Width,Height}` $state mirrors, which are populated
asynchronously by the ResizeObserver. On first paint (and on the
very first click after mount / orientation switch) those mirrors were
still `0`. So:
translate = -value × slideSize
= -1 × (viewportSize / 1)
= -1 × 0
= 0
The index DID advance (soma's `value` went 0→1) but the rail stayed
at translate=0, so the user saw the active slide unchanged — looked
exactly like a dead click. Subsequent clicks accumulated until the
observer finally fired and the carousel suddenly jumped to whatever
index had become.
Fix: two-tier read in the `viewportSize` derivation. Prefer the
observer-tracked mirror when non-zero (steady state, picks up reshape);
fall back to a synchronous `vp.clientWidth` / `clientHeight` read while
the mirror is still 0. Same derivation reactivity, no extra effects.
Verified: first click after page load now lands `translate3d(-380px, 0,
0)` on a 380px-wide viewport (idx 0→1) — was 0 / no movement before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
b1b1e39dc0 |
fix(carousel): chevron direction follows orientation
Vertical carousel still rendered horizontal chevrons (< / >) for the prev / next triggers, which read as wrong-direction even though the click handlers worked correctly. The user complaint "varios clicks" was the visual mismatch making each click look like it did nothing — value WAS advancing index 0→1→2 on each click but the icons pointed sideways in a vertical rail. - Added `orientation` to the eidos carousel context so the prev / next trigger wrappers can pick the right chevron without passing props. - Re-exported `Direction` + `Orientation` from soma's carousel module so the eidos context can type-check them. - Prev trigger now uses `direction='up'` in vertical, `'left'` in horizontal. Next trigger mirrors with `'down'` / `'right'`. Verified: index goes 0→1→2 on successive clicks; chevrons now show `^` (top) and `v` (bottom) in vertical orientation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
23ad9351ea |
feat(carousel): wire dir + locale (was hardcoded 'ltr'); expose i18n in demos
Audit caught Carousel ignored direction entirely: `getDirectionalKeys` received a hardcoded `'ltr'` so ArrowLeft / ArrowRight had the same semantics under RTL as LTR, and the horizontal translate had no RTL flip so slides advanced the wrong way visually. Drag direction had the same bug. ## Soma - New `dir` + `locale` props on `Carousel.Provider` (fall back to `soma.prefs.getDir()` then `'ltr'`). - `onkeydown` now reads `this.opts.dir.current` for `getDirectionalKeys`. - `itemGroupTransform` flips the translate sign on horizontal RTL so index N→N+1 moves the rail in the reading-direction-appropriate way. - `finishDrag` swaps the offset sign on horizontal RTL so drag-right advances under RTL (drag-left under LTR). - Root stamps `dir="..."`, `lang="..."`, and `data-dir="..."` for consumers and the recipe. - Test mocks updated. ## Demos - Carousel demo: new `soma · i18n` controls section — `dir` chips (`auto` / `ltr` / `rtl`) + `locale` chips covering en/es/ar/he/ja so the user can flip orientation semantics live. - Range-calendar demo: same `dir` + `locale` chip rows added next to `weekStartsOn`; previously only `locale` was state-bound but the control wasn't surfaced and `dir` wasn't forwarded at all. - The eidos code snippet preview emits `dir="..."` and `locale="..."` attrs when they differ from defaults. ## Verified Browser walk: with `dir="rtl"`, the carousel root reports `data-dir="rtl"`, prev/next chevrons swap sides, indicator order mirrors, and `ArrowRight` correctly advances to the *previous* slide (index 1 → 0). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
f291c08865 |
fix(carousel/range-calendar/time-range-field): visual bugs across all three
Real visual audit caught three classes of bugs that the prior commits
missed because they were never opened in the browser.
## Carousel
### Vertical orientation overflow
Recipe gave the root no block-size in vertical, so items spilled. Plus
the soma `viewportSize` derivation only ran on ref / orientation-prop
changes, not on layout reshape — so flipping horizontal → vertical
kept reading the old clientWidth and the translate maths used the wrong
slide size (observed -214 instead of -156 after a flip).
Fix:
- Recipe: `[data-carousel][data-orientation='vertical']` gets explicit
`block-size`, and the viewport / item-group / item each get `100%`
along the block axis. Default vertical size lives on
`--_carousel-vertical-block-size` (16rem), overridable via the new
eidos `verticalBlockSize` prop or the custom property directly.
- Soma: replaced the synchronous `clientWidth/Height` reads with
`viewportClientWidth/Height` `$state` mirrors populated by a
ResizeObserver on the viewport. Layout-only changes (orientation
flip, container resize, font scaling) now propagate to translate
calc on the next reactive tick.
## RangeCalendar
### Multi-month stacked vertically
`numberOfMonths > 1` rendered grids stacked in a column because the
eidos `[data-range-calendar]` recipe is `inline-flex; column`. Copied
the side-by-side grid layout from the existing date-range-picker demo:
`.range-months { display: grid; grid-auto-flow: column; }` with
`grid-auto-flow: row` collapse for `data-month-count='1'`.
### Min/max controls were nonsense
The prior demo had a custom "today | custom" mode that conflated
fixtures with real today, and required two separate enum states.
Rewrote the controls to mirror the date-range-picker demo exactly:
two `<input type="date">` bindings on string state, parsed by
`parseDateInput`, plus a preset chip row (none / min only / max only /
window / invalid).
### Weekday key duplicate
The narrow-format weekday strings repeat letters ('T' for Tuesday and
Thursday, 'S' for Saturday and Sunday), so keying by `wd` raised
`each_key_duplicate`. Changed to key by index.
## TimeRangeField
### Start endpoint rendered no segments
Keying the segments `#each` by `seg.part` collapsed when segment
parts repeat. Copied the canonical key shape from the time-range-picker
demo: `(${part}-${i})`. Verified both endpoints now render their
four segments (`09:30` — `17:45` for the default fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
44514c3cc1 |
fix(carousel): vertical overflow + close gaps vs embla/ark-ui/mantine
Real reference audit walked the published embla `.d.ts`, Mantine docs, ark-ui, shadcn-svelte and Swiper. Found three classes of gaps in the initial port (a) the vertical bug breaking the only non-default orientation, (b) layout primitives every other lib ships (slidesPerView, slidesToScroll, align, gap), (c) autoplay granularity (playOnInit, stopOnInteraction, stopOnFocusIn, stopOnMouseEnter, stopOnLastSnap), plus an imperative API every consumer needs. ## Vertical bug fix Recipe now gives the root a default `block-size` in vertical orientation (`16rem`, overridable via `--_carousel-vertical-block-size` or the new eidos `verticalBlockSize` prop) and stretches the viewport to fill it. Without this the viewport collapses to content height and items spill. ## Soma extensions - `defaultValue` (uncontrolled init) - `slidesPerView` (1+) — each Item basis = (100% − gap·(n−1)) / n - `slidesToScroll` (1+) — paged group navigation - `align: 'start' | 'center' | 'end'` — viewport alignment - `gap: string` (CSS length) — between-slide spacing on the ItemGroup - `playOnInit` — start autoplay on mount (default true) - `stopOnInteraction` — latch-stop after user clicks prev/next/indicator or drags - `stopOnFocusIn` — pause while focus is inside (default true) - `stopOnMouseEnter` — pause while cursor over root (Embla parity) - `stopOnLastSnap` — latch-stop at last snap when loop=false - Snippet exposes `isPlaying` + an `api` handle with `scrollNext / scrollPrev / scrollTo / play / pause / reset` `commit()` now respects `slidesPerView` (last snap = count − spv). `next()` / `prev()` step by `slidesToScroll`. Translate calc derives from `slideSize = viewportSize / slidesPerView` so multi-view works. Item flex-basis adapts to slidesPerView + gap. ## Eidos additions - `verticalBlockSize` prop — forwarded as CSS custom property for the vertical recipe override - Recipe selectors for vertical orientation enforcing the block-size + viewport stretch - Style composition so consumers can still pass inline `style` ## Demo Live tab now has 4 organized sections (layout / drag / autoplay / imperative API) covering every new prop. The imperative API row drives the carousel from external buttons via the snippet handle so consumers see the pattern in action. `verticalBlockSize` slider exercises the vertical fix. Snippet preview emits every set prop. ## Reference comparison Following the canon's mandate to compare with references BEFORE declaring done, the actual feature matrix from embla published `.d.ts`, Mantine docs, ark-ui, shadcn-svelte, Swiper: | Feature | embla | mantine | ark | swiper | UIX | |------------------------|-------|---------|-----|--------|-----| | Index nav | ✓ | ✓ | ✓ | ✓ | ✓ | | Loop | ✓ | ✓ | ✓ | ✓ | ✓ | | Drag | ✓ | ✓ | ✓ | ✓ | ✓ | | Autoplay | plug | plug | ✓ | ✓ | ✓ | | Orientation H/V | ✓ | ✓ | ✓ | ✓ | ✓ | | slidesPerView | indir | ✓ | ✓ | ✓ | ✓ | | slidesToScroll | ✓ | ✓ | ✓ | ✓ | ✓ | | Align | ✓ | indir | per | ✓ | ✓ | | Gap | css | ✓ | ✓ | ✓ | ✓ | | defaultValue | startIndex | ✓ | ✓ | ✓ | ✓ | | playOnInit | ✓ | indir | ✓ | ✓ | ✓ | | stopOnInteraction | ✓ | ✓ | ✓ | ✓ | ✓ | | stopOnFocusIn | ✓ | ✓ | ✓ | partl | ✓ | | stopOnMouseEnter | ✓ | ✓ | ✓ | ✓ | ✓ | | stopOnLastSnap | ✓ | ✓ | ✗ | ✓ | ✓ | | Imperative API | ✓ | ✓ | ✓ | ✓ | ✓ | | Indicators / triggers | DIY | ✓ | ✓ | ✓ | ✓ | | Fade effect | plug | plug | ✗ | ✓ | ✗ deferred | | Virtual slides | ✗ | ✗ | ✗ | ✓ | ✗ deferred | | Multirow grid | ✗ | ✗ | ✗ | ✓ | ✗ deferred | Deferred power-user features (fade, virtual, multirow grid, cube/cards, parallax) are documented as power-user in the reference audit, not table-stakes, and would require Embla-class internals to ship. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
67726cb6f3 |
fix(soma navigation-menu): suppress click-close right after hover-open
Bug: hovering a NavigationMenu trigger opens its Content after the configured \`openDelay\` (default 200ms). If the user then clicked the trigger — naturally, after the open is visible — the click handler saw \`isOpen=true\` and called \`closeNow()\`, shutting the menu the user just opened. Net effect: the first click appears to open AND close the menu in one go. Repro (commit before fix): - hover for ≥200ms (or any delay long enough to trigger \`scheduleOpen\`) - menu opens - click the trigger - menu closes within milliseconds Fix: track \`lastOpenedAt\` per NavigationMenuTriggerProvider via a root \`\$effect\` that stamps the timestamp whenever \`isOpen\` transitions to true (regardless of whether the open came from hover, click, or keyboard). The click handler checks \`Date.now() - lastOpenedAt < 250\` and silently ignores the close-toggle in that window — treating the click as "commit the open" intent, not "toggle shut". Two-click toggle behavior is preserved: a click ≥250ms after the open still closes the menu (verified: openBefore=true, stillOpenAfterClick= true, closedAfter2ndClick=true). The 250ms threshold is slightly above the default openDelay (200ms) so the protection covers the typical hover-then-click sequence without making the toggle feel sluggish. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
c6f063dc6a |
refactor(alert-dialog): Action/Cancel render <Button>; intent flows via snippet
#120 second pass. AlertDialog.Action and AlertDialog.Cancel now render eidos `<Button>` internally via soma's `child` snippet, retiring the per-recipe Toggle-token chrome and the CSS ancestor cascade (`[data-dialog-content][data-color='risk'] [data-alert-dialog-action]`). Intent propagation: soma's `AlertDialogActionProvider` exposes a new `dialogIntent` derived getter that reads `DialogProvider.opts.intent`. The Action component passes it to the `child` snippet alongside `props` (via `WithChildNoChildrenSnippetProps` so plain `children` stays a regular Snippet). The eidos Action wrapper forwards it to Button as `intent={intentOverride ?? dialogIntent}` — so `<AlertDialog intent="risk">` automatically paints a red Action without any cascade selector. Defaults: - Action: variant='solid', intent inherits from dialog - Cancel: variant='outline', intent='neutral', color='neutral' so it reads as gray-outlined regardless of the dialog's intent (canonical "this is the safe choice" cue) Cancel narrows variant to ControlVariant (3 chips: surface/outline/ghost) per DEMO_AUTHORING_GUIDE §12.7. Action keeps the full ButtonVariant because it's the primary call-to-action. alert-dialog.css gutted to a doc-only stub; all chrome now comes from button.css. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
4b5bd9f35d |
refactor(dropdown-menu): remove unused modal; Trigger→Button; canon vocab fixes
Three related fixes touching the menu surface: 1. **Remove `modal` from DropdownMenu** (#127). The prop was declared in soma but never wired to anything — no overlay, no pointer-events blocker, no scrim. ark-ui / Headless-UI / Chakra all treat menu as non-modal by convention; Dialog/Drawer/AlertDialog cover blocking semantics. Drop it from soma (types, provider opts, component, test fixture), the eidos doc comment, the menubar consumer, and the dropdown-menu demo (state, control, snippet, API table, parity table). 2. **DropdownMenu.Trigger now renders `<Button>` internally** (#120 first consumer). Default variant='surface', size='md', intent='neutral'. `variant` is narrowed to ControlVariant (`'surface' | 'outline' | 'ghost'`) per DEMO_AUTHORING_GUIDE §12.7 so the trigger stays consistent with the form-control surface (Field/Select/Combobox) instead of inheriting Button's wider 6-variant set. The eidos demo exposes variant/size/intent chips that match the canonical narrowed vocab (Intent: neutral · affirm · fulfill · risk · threat · loss). Trigger's per-recipe CSS dropped — chrome comes from button.css. 3. **Menu-item unchecked indicators** (follow-up to #118). CheckboxItem and RadioItem now paint an empty outlined box / circle even when unchecked, so the user can tell at a glance which rows are checkable. Checked state fills with the accent color via the same `::before`. The reserved padding slot (26px) keeps row text from shifting when state flips. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
a804c39208 |
feat(eidos): Button + 9 demo rebuilds + picker recipe polish
Build: - New <Button> compound (morfo + soma + sema + eidos): 6 variants (solid/soft/surface/outline/ghost/plain), 8 colors, 5 sizes, optional icon/endIcon snippets, loading state with built-in Spinner, asChild Slot pattern, intent-driven sema (commit-action with fromProp:intent). Demo rebuilds (canonical depth per DEMO_AUTHORING_GUIDE): - context-menu, dropdown-menu, pin-input, alert-dialog, toggle-group: full per-part Morfo contracts, reference-parity tables, ARIA contract tables, keyboard tables, composition controls. - color-field, time-field, time-picker, time-range-picker, color-picker, month-grid, year-grid: same canonical treatment; per-part data/aria/ keyboard iteration of raw morfo, reference parity vs react-aria/ ark-ui/Chakra/bits-ui/Mantine. Picker recipe polish: - PickerShell.Clear/Cancel/Close refactored to use <Button size="xs"> (affects 5 pickers — date / date-range / time / time-range / color). - TimePicker + TimeRangePicker: popover condensed (min-width 18rem -> 14 / 32 -> 28), per-channel accent (hour=primary, minute=secondary, second=neutral; end variant muted), ruler off by default, hour ruler cleaned to 0/6/12/18/23. - ColorPicker: typography token --_color-picker-trigger-font-size that scales with data-size (10/12/14px); autoFormat default true; Area recipe override for [data-x-channel='hue'] with rainbow gradient + saturation/brightness Y overlays. - ValueText eidos wrapper: fix children snippet bug that suppressed soma's displayValue fallback. - date-field.css: segment editing affordance (cursor + hover + active ring) via new --_date-field-accent-element token across 8 color cascades. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
59505e6da2 |
feat(splitter): sema + dynamic drag-signal sound infrastructure
Splitter previously had no sema bindings — handle-pick / handle-drag /
commit-set fired silently. Closes the asymmetry with Slider and Drawer.
Sema (src/uix/sema/components/splitter.ts):
- handle-pick → sound('handle.pickup.air', gain 0.10) + tick haptic
- handle-drag → channels: ['sound', 'haptic'] (channels-only; per-emit
signal.overrides from soma own the dynamic curve so cascade doesn't
clobber them)
- commit-set → sound('handle.snap.chime', gain 0.14) + tap haptic
Sound resolver (src/uix/sema/sounds.ts):
- resolveSplitterDragSound — heavier sonic register than slider since
splitters move whole panels, not a thumb:
pitch 480..820 Hz (slider: 760..1280)
centroid 2400..4800 (slider: 3600..6800) — less brilliant
roughness 0.006 (slider: 0.003) — woody texture, not metallic
envelope attack 16 / decay 72 / duration 76 — perceptual mass
gain 0.025 + velocity*0.03 — restrained ceiling, long swipes ok
Soma drag-signal infrastructure (splitter-provider.svelte.ts):
- Mirrors slider's frame-throttled signal pipeline:
dragSignalFrame / pendingDragPercent / lastDragSignalAt / lastDragPercent
DRAG_SIGNAL_MS = 72ms throttle (≈14 Hz emit cadence)
- dragSignalOverrides(percent01) computes velocity01 from frame delta,
builds SemaSignatureOverride with sound (via resolveSplitterDragSound)
and haptic (tick scaled by velocity).
- queueHandleDrag / cancelQueuedHandleDrag wrap requestFrame so each
pointermove only schedules one trigger per throttle window.
- onpointerdown calls resetDragSignal(startSizeA/100) to anchor the
velocity baseline at grab time.
- onpointerup / onlostpointercapture / unmount $effect all cancel the
queued frame so we don't fire post-release.
Verified in browser at /uix/components/splitter:
handle-pick → 1 event (family: handle)
handle-drag → frame-throttled across 6 pointermoves
commit-set → 1 event (family: commit, intent: neutral)
Checks: svelte-check 0 errors, 133/133 vitest (splitter + sema),
component:audit 91/91 PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
c703e2e7ff |
fix(scroll-area): split mount-vs-visibility — the real type='scroll' / 'hover' bug
Root cause: soma/scroll-area-scrollbar.svelte gated the entire <div> on
`state.shouldShow`. For type='hover' / 'scroll', `shouldShow` starts
false, so the bar never reached the DOM. With no DOM node:
- bind:this never fired → scrollbar's `ref` stayed null
- `requestFrame(cb, ref.current)` ran with a null element, so the
`mounted` flag never flipped
- the hover / drag listeners had no element to attach to
- the recipe's `data-state` opacity transition had no element to
animate
- `shouldShow` could therefore never become true → deadlock
Fix — split the concern:
- `shouldMount` (new derived) — true whenever the axis overflows OR
type='always'. Drives the `{#if}` in the part component.
- `shouldShow` — drives `data-state="visible|hidden"`. The recipe
transitions opacity.
The bar now lives in the DOM whenever it could ever be needed, fades
in/out via CSS, and stays a stable hit target for hover / click /
drag. Dropped the `mounted` flag and the requestFrame mount-frame
scheduling — they only existed to suppress a flash that the opacity
transition handles cleanly.
Also dropped the inline `border-radius: inherit` on the thumb so the
recipe's `--scroll-area-thumb-radius` (the new `radius` prop) wins.
Without this the prop was silently overridden to `0px`.
Test updates:
- removed `cancelFrame` assertion (no mount-frame to cancel)
- added `shouldMount` assertion
Verified in browser at /uix/components/scroll-area:
- type='hover' + vertical: bar mounts hidden, fades in on root hover,
fades out after delay
- type='scroll' + horizontal: bar mounts hidden, fades in on scroll,
thumb tracks scrollLeft, fades out after 600ms
- type='always' + both: both bars + corner visible at all times
- size=lg, radius=full: 12px-wide bar, fully rounded thumb
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
e11d3539ef |
feat(scroll-area): add size/radius/scrollbars props + fix scroll-type race
Reference-library parity (Radix Themes, Chakra v3): - size: 'xs' | 'sm' | 'md' | 'lg' (Chakra t-shirt scale) → thickness via --scroll-area-scrollbar-size. - radius: 'none' | 'sm' | 'md' | 'lg' | 'full' (Radix Themes) → thumb curvature via --scroll-area-thumb-radius. - scrollbars: 'vertical' | 'horizontal' | 'both' (Radix Themes) → recipe-level sugar that auto-mounts Scrollbar / Thumb / Corner from the root. Explicit composition still supported (omit the prop). Bug fix — type='scroll' + horizontal sometimes stuck hidden: - `mounted` on ScrollAreaScrollbarProvider is now $state. Previously a plain field, so the `shouldShow` derived didn't invalidate when the requestFrame mount-flag flipped during the first frame. - The reveal-on-scroll $effect now skips its first run via a `firstRun` flag. That first run was the subscription pass with scrollTop/scrollLeft still 0 — it was eagerly calling show() and setting `visible=true` before `mounted` flipped. Subsequent real scrolls then assigned visible=true again (no-op), so shouldShow didn't re-run and the bar stayed hidden until the hide timer expired. Now the bar only reveals on genuine scroll deltas. Demo: chip controls for size + radius, switched to scrollbars prop on the root (drops the explicit Scrollbar/Thumb children), eidos snippet shows the recipe-sugar form, soma snippet still shows the explicit Radix-style composition. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
560ce8f66c |
fix(scroll-area, drawer): horizontal scrollbar visibility + drawer drag affordance
- scroll-area soma: defer the mount-frame request to `$effect` so SSR doesn't hit `dom::window_required` during constructor evaluation. - scroll-area recipe: scrollbars now `position: absolute` (overlay style) so the horizontal track lands at the viewport's block-end instead of flowing below it. `:has` rules reserve corresponding edges when both axes mount; corner switched to absolute as well. - drawer demo: drop the ↑↓←→ glyph pop-outs and `overflow: visible` during dragging. The handle's own pill grip is the affordance — the same shape the splitter uses, no extra chrome. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
bd2e40366a |
feat(layout): Layout Batch 3 — scroll-area, separator, splitter + splitter sema rewire
3 new layout primitives close the air port set (15 / 15 done).
`npm run component:audit`: 91 / 91 PASS, all 3 new PASS with 0 errors.
**scroll-area** — eidos compound wrapper over the existing soma:
`<ScrollArea><ScrollArea.Viewport>{content}</ScrollArea.Viewport>
<ScrollArea.Scrollbar orientation="vertical"><ScrollArea.Thumb />
</ScrollArea.Scrollbar><ScrollArea.Corner /></ScrollArea>`. Mirrors
Radix Primitives / Bits UI shape. Demo 667 lines.
**separator** — visual primitive (`scope: ['eidos']`) with conditional
ARIA: `role="separator"` + `aria-orientation` when not decorative,
`aria-hidden="true"` when decorative. New morfo
`src/uix/morfo/components/separator.ts`. Demo 645 lines.
**splitter** — eidos compound over existing soma:
`<Splitter orientation="horizontal"><Splitter.Panel>{a}</Splitter.Panel>
<Splitter.ResizeTrigger /><Splitter.Panel>{b}</Splitter.Panel>
</Splitter>`. Demo 824 lines.
**Splitter sema rewire — match Slider's handle/drag/commit shape:**
The morfo previously declared only `commit-resize`. Direct-manipulation
primitives (Slider, Splitter) should share the same event vocabulary so
audio/haptic projections feel coherent across the family. Renamed +
expanded to mirror Slider:
- `handle-pick` (family: handle, verb: pick, sequence: coincident,
target: resize-trigger) — fires on pointerdown when user grabs the
trigger.
- `handle-drag` (family: handle, verb: drag, sequence: coincident,
target: resize-trigger) — fires on each pointermove frame.
- `commit-set` (family: commit, verb: set, intent: neutral,
sequence: post, target: provider) — fires on pointerup. The runtime
`commit-set` handler is wired to call the consumer's `onResizeEnd`
callback, so sema and JS callback land at the same moment (parity
with Slider's `onValueCommit`).
Soma provider triggers these via `this.runtime.trigger(name)` in
`onpointerdown` / `onpointermove` / `notifyResizeEnd` (the latter
unchanged externally — internally now routes through the runtime
trigger).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
30e9517a9a |
feat(combobox): SelectedTags inline chips for multi mode
New parts `<Combobox.SelectedTags>`, `<Combobox.SelectedTag>` and `<Combobox.SelectedTagRemove>` render the current selection as removable chips inside `<Combobox.Control>` when `type='multiple'` (Material UI Autocomplete pattern): - Morfo: 3 new public parts with kebab names + texts entries. - Langs: es/en entries `selected-tags` / `selected-tag-remove`. - Soma: `ComboboxSelectedTagsProvider` exposes `items` derived from the provider's label registry; `ComboboxSelectedTagProvider` surfaces `data-value` / `data-label`; `ComboboxSelectedTagRemoveProvider` resolves its parent tag via context and calls `provider.removeValue` on click. Provider gains `getSelectedItems()` and `removeValue()` helpers; `selectedTagsEmpty` runtime prop drives `[data-empty]`. - Soma item registry: stop unregistering labels on Item cleanup so chips keep proper labels after the listbox closes/lazy-unmounts. - Eidos: default snippet loops items and renders a chip + X icon per entry; consumer can override the snippet for custom rendering. - CSS: chip styling, wrap-friendly Control with breathing padding when chips are present, ClearButton pinned right via auto margin, input flex basis reduced to 4rem so it shares a row with chips. - Demo: renders `<Combobox.SelectedTags />` inside Control when `mode === 'multiple'`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
79c94d85d5 |
wip(popover): unified min-width = anchor + sideOffset 6 across pickers
Day-end checkpoint for the floating-layer normalization work. Documented in CONTINUE.md. Done: - popover.css: new `--_popover-min-width` defaults to `var(--floating-anchor-width, auto)` so popover layer is NEVER narrower than its anchor (Radix / React Aria convention). Consumers that should opt out (tooltip, dialog) override with `auto`. - soma popover-content default sideOffset 0 → 6. - 5 picker eidos contents: sideOffset 10 → 6 (matches the new default; per-component override removed visually but kept as explicit prop). - combobox provider: reverted the multi-on-close stamp-labels patch. Multi-mode selections will be rendered as inline tags via a future `Combobox.SelectedTags` part (Material UI Autocomplete pattern, decided with user). Pending (see CONTINUE.md): - Re-anchor pickers to their Input wrapper (currently anchored to the chevron trigger ~14px). Required for the min-width normalization to actually show — research vs Ark UI / Radix / React Aria confirmed composite-input pickers anchor to the control wrapper, not the trigger button. - Combobox `SelectedTags` part for multi-mode inline tags. - SearchField floating Results part (user opted to make it Combobox-like rather than reuse Combobox). svelte-check --threshold error → 0 errors / 0 warnings / 4584 files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
df948aa35a |
fix(combobox): input mirrors selected label in single mode; multi preserves filter
Two related bugs reported in the demo: 1. After selecting an item in single mode, the input stayed at the previously typed query (or empty), so the user couldn't tell which value was committed unless they peeked at the Trigger area. 2. In multi mode, selecting an item used to clear the input, which destroyed any active filter. cmdk-style usage (filter → pick → filter → pick) was impossible — the user had to retype the query for every selection. Provider behaviour now: - `selectItem()` in SINGLE mode sets `inputValue` to the selected label (resolved via the existing `labelRegistry`). - `selectItem()` in MULTI mode leaves `inputValue` untouched, so the filter survives the click and the user can keep picking matches. - A new $effect keeps `inputValue` in sync with `value` in SINGLE mode for initial mount and external `bind:value` updates. - A second $effect clears `inputValue` whenever the picker switches between 'single' and 'multiple' — the two modes use the input for different purposes, and stale text from one would silently filter the other. Also: - Added `components.combobox.clear` to the langs dictionary so the ClearButton stops emitting `[langs] Translation key not found` warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
9fbf8e48b5 |
feat(number-field, search-field, combobox): audit-driven improvements
Three components closing the 6-component audit batch.
number-field
- N1 RTL scrubber: invert horizontal delta sign when dir='rtl' so
dragging right decrements (visually backward) in RTL layouts. Vertical
unaffected. Added 2 RTL scrub tests; existing scrub test pinned to ltr.
- N2 stepper height: Increment/Decrement now use --_number-field-height
for inline-size/block-size/min-inline-size so they always match the
input row at every size token (xs/sm/md/lg/xl).
- N3 input focus ring: explicit [data-number-field-input]:focus-visible
with --_number-field-accent-border box-shadow, alongside the existing
Provider-level ring.
search-field
- S1 icon tokens: added --search-field-icon-size-{xs..xl} + icon-color +
icon-margin-inline-end + a Morfo Icon part + soma/eidos wrappers.
Demo uses <SearchField.Icon><Search /></SearchField.Icon>.
- S2 debounceMs prop: soma wraps onValueChange in a timers.schedule with
the provided delay. clear() cancels, submit() flushes synchronously.
- S3 loading slot: new LoadingIndicator part (morfo + soma + eidos) +
data-loading on Provider + CSS spinner via @keyframes search-field-spin
honoring prefers-reduced-motion.
combobox
- C1 ClearButton part: new morfo trigger + soma provider + eidos wrapper
with X icon. Hidden via [data-empty] (opacity + pointer-events:none).
Click clears value + inputValue and refocuses input.
- C2 allowsCustomValue: when true and no highlighted item, Enter commits
the current inputValue as the value. Single mode closes after commit;
multi appends + clears input + stays open.
- C4 Backspace removes last tag in multi mode when input is empty.
- C5 Sticky group headers: CSS position:sticky with content-overlay bg.
Plus: fixed search-field demo's brittle parts[1] index reference to a
named lookup (parts.find(p => p.kebab === 'input')).
Verified: svelte-check --threshold error → 0 errors / 0 warnings /
4584 files. All component tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
7bc17485dc |
feat(editable): invalid + required + form + Control + better hover/disabled + defaultValue
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>
|
5 months ago |
|
|
75136e6de5 |
fix(select) + feat(field): match-anchor width, label persistence, OptionalIndicator, auto-label
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>
|
5 months ago |
|
|
ffdb480403 |
feat(select): invalid state + ItemDescription + defaultValue + visual fixes
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> |
5 months ago |
|
|
f7831563a1 |
feat(month-grid, year-grid): standalone primitives parallel to date-picker views
DatePicker.MonthView / YearView require a DatePickerProvider context and
can't be used standalone. This commit introduces MonthGrid and YearGrid
as independent primitives that accept value / placeholder / minValue /
maxValue as props and work anywhere.
morfo
- New morfo/components/month-grid.ts and year-grid.ts.
- scope: ['soma','sema'], apg: grid pattern.
- 7 parts each: Provider (role=application) / Header / Heading /
PrevButton / NextButton / Grid (role=grid) / Cell (role=gridcell).
- Events: commit-set (cell click) + nav-step (prev/next).
- Cell data-attrs: data-value, data-selected, data-current, data-disabled,
data-focused.
- Keyboard map covers arrows / Home / End / PageUp/Down / Enter / Space.
soma
- New soma/components/{month-grid,year-grid}/ with provider class +
langs + types + exports + 7 thin svelte wrappers each.
- MonthGridProvider: 12-cell grid keyed to placeholder.year; localized
month names via Intl.DateTimeFormat with configurable monthFormat
('long'|'short'|'narrow'). Prev/Next step ±1 year.
- YearGridProvider: decadeSize-cell grid (default 12); page anchored at
placeholder.year - (placeholder.year % decadeSize). Prev/Next step ±
decadeSize.
- Both expose Cell sub-provider with snippetProps {selected, disabled,
current, value, label} + roving tabindex + RTL-aware keyboard nav.
eidos
- New eidos/components/{month-grid,year-grid}/ — namespace-style index
with size / variant / color visual props + dedicated CSS reusing the
--calendar-* token family.
- Each grid renders Header (Prev/Heading/Next) + 3-col Grid.
demo
- New /uix/components/month-grid and /uix/components/year-grid demo
pages with the standard live/api/morfo/sema/recipe/a11y tabs,
interactive controls for every public prop, soma+eidos snippets that
reactively rebuild from controls, and sema event play buttons.
Verified: svelte-check --threshold error -> 0 errors / 0 warnings /
4572 files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
dd22cfc2bc |
feat(time-range-picker): eidos wrapper + canonical picker-shell parity
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> |
5 months ago |
|
|
8515f5e673 |
fix(color-picker): ValueSwatch now reactively reflects the current color
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>
|
5 months ago |
|
|
da19728cb6 |
feat(color-picker): area drag emits handle-pick/handle-drag/commit-set + refresh demo snippets
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>
|
5 months ago |
|
|
aa62f0203c |
refactor(pickers): date/time/date-range pickers consume shared PickerShell
Follow-up to commit
|
5 months ago |
|
|
714ee3735c |
feat(picker-shell): extract shared Footer/Clear/Cancel/Close + apply to color-picker
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>
|
5 months ago |
|
|
5d007ed9f9 |
refactor(color-picker): channel slider composes generic SliderProvider + N-8 norm
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> |
5 months ago |
|
|
2731ee567b |
feat(eidos): add ColorPicker wrapper + demo
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>
|
5 months ago |
|
|
391de6e04c |
feat(eidos): add TimePicker wrapper + demo following P-1..P-5 picker contract
Builds on the existing soma TimePicker slider-based clock (HourSlider / MinuteSlider / SecondSlider + DayPeriodToggle). Adds the canonical picker shell: - mode='inline' | 'modal' on the Provider, propagated to popover.modal - Provider helpers commit() / cancel() / clear() with watch(open) snapshot for cancel-revert - Footer composition (TimePicker.Footer / Clear / Cancel / Close as pure-composition Eidos parts; no *Button root props) - New TimePicker.Clock soma component that carries the data-time-picker-clock contract attr, wraps the slider stack - Eidos shell: Provider visual root, Trigger (clock icon), Content (popover content), Clock, Footer + action parts, slider/toggle pass-throughs Renews the time-picker morfo to declare Provider / Trigger / Clock / DayPeriodToggle / DayPeriodItem / Footer / Clear / Cancel / Close, plus all 6 commit/emerge events (open + close-commit/cancel/dismiss/ dismiss-outside + commit-clear). apg=dialog-modal. Demo at /uix/components/time-picker exercises all controls, Footer composition switches, soma + eidos code snippets, morfo / sema / recipe / a11y tabs. README documents the picker contract. 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> |
5 months ago |
|
|
4a866cf86a |
date-range-picker: propagate kind to start/end inputs
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>
|
5 months ago |
|
|
6299e6ebc6 |
pickers: Footer parts = pure composition (drop *Button props)
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>
|
5 months ago |