The demos will be exposed through a NEW frame for the whole system, so drift
against the CURRENT page template is not fix-work. checkDemo results are
downgraded to warn at the call site and D-* rules are excluded from the
verdict count entirely — the data stays visible in the report, the component
matrix stops being painted by it. The D-* rules get rewritten against the new
frame when it lands.
Matrix: 102 -> 118 PASS / 16 NEEDS-WORK / 0 BROKEN. The 16 now decompose into
exactly three real families: the R-1.x focus/disabled census (9), the old
dossier-form READMEs (6, the reduced lote 6), and two contract items — field
A-3.1 and float-panel A-3.7 — which converge with the new clean-room audit
finding "field/float-panel infra-declarados".
Type baseline 57.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The "3 ajenos" census. What the 12 pre-existing failures really were: a few
genuine violations + several guards CRASHING on menu-dial's missing
exports.ts (ENOENT) and hiding their true violation lists. All addressed
except the two runtime-discipline files (see tail).
- knob: soma barrel export + the missing soma README (headless contract —
parts/ARIA/absolute angular keyboard/props/published CSS vars/sema table).
- menu-dial + onion-menu: joined SOMA_INTERNAL_PRIMITIVES — both are
documented provider-less bridges (keyboard dispatch + nav maths + emission;
the eidos root owns state and render). The scope guard now subtracts the
internal set from BOTH sides so a bridge may truthfully keep 'soma' in its
morfo scope without being a public namespace. Self-alias imports fixed;
the two morfo docblocks realigned to the implementation ("soma owns open"
was drift — the bridge files own dispatch/emission only; onion-menu's
stale "family-default, no pack" tail also corrected, S3b shipped its pack).
- Public barrels decoupled from provider implementation filenames (the
picker `internals.ts` house pattern) for the 6 offenders: calendar,
cropper, field-langs, gradient-builder, gradient-picker, image-adjustments.
- listSourceFiles now skips ACTIVE_DEV_TRACK paths (words/palabras/chronos)
— the track doctrine already said catalogue guards skip them; the
file-scanning guards just didn't.
- data-archetype added to the known morfo attrs (compiler-emitted from part
archetype declarations); the four field-overlay picker triggers
(color-picker, date-range-picker, time-picker, time-range-picker) declared
archetype 'trigger' while stamping 'field-trigger' at runtime — morfos
aligned to the date-picker/ntp precedent (contract now matches the DOM;
zero runtime change, the stamp already won).
- float-panel: the consumer-placed data-float-panel-no-drag opt-out marker
is now declared in the morfo (presence entry with the consumer-marker
note) — the NON_DRAG_SELECTOR contract stops being invisible.
Remaining tail (3 failures, deliberately deferred to a dedicated
runtime-discipline session with browser gesture verification):
float-panel-provider direct style writes + raw timer (drag/resize hot path
-> dom.apply/uix.timers migration) and media-player media-provider raw
listeners (-> dom.listen).
Gates: contracts 34/37 (was 25/37) · component-audit 102 PASS · vocab exit 0
· type baseline 57 · eidos-lint invalid 0 on the four touched pickers ·
compile + eidos contract tests 42/42.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- contracts.test: metrics joins SOMA_INTERNAL_PRIMITIVES (with picker-shell) —
its soma layer is a deliberate provider-less runtime bridge (documented
in-place: "no provider/state class"; morfo scope ['sema','eidos'] on
purpose), NOT a public component namespace. Classification fix, not an
exemption of debt: the barrel/exports/README guards stop mis-demanding a
canonical Provider shape it was designed not to have. Also fixes the bridge's
$soma self-alias import (the internals guard). Contracts: 13 -> 12 failures,
ZERO metrics mentions (the remaining 12 belong to knob/menu-dial/onion-menu).
- morfo: apg 'none — data display readout' (C5 form) — clears A-1.4; the
README's stale "A-1.4 N/A" audit note updated to point at the declaration.
- README upgraded to dossier form (Baseline Untitled-UI/Tremor/Chakra Stat,
Comparativa with the goodTrend decoupling as the differentiator, Decisiones
incl. the internal-bridge classification, Gaps with the D-1.2 demo union
routed to the cross-catalog demo census).
- Suite (the ficha's F-2): resolveDeltaIntent extracted to a pure helper
(delta-intent.ts, consumed by Metrics.Delta) + delta-intent.test.ts 4/4
(direction/valence decoupling, goodTrend flip, flat neutral, explicit
override) + metrics.svelte.test.ts (the live-update bridge: signal.notify
carries the prop-source intent; the morfo's requiresLiveRegion contract).
Gates: component-audit metrics clean except D-1.2 (demo census) · eidos-lint
invalid 0/class-hooks 0 · type baseline 57 · contracts 25 pass/12 pre-existing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The package's headline item — the generic Picker's transactional events —
turned out ALREADY PAID at checkpoint S3b (commit-set fulfill / commit-cancel
/ commit-reset declared, pack shipped, soma triggers wired, suite green): the
2026-07-07 ficha claim was stale and the ntp dossier had propagated it; the
dossier row is now corrected with the verification.
What this session actually adds:
- component-audit R-2.1 learns the `/* literal: <reason> */` escape valve
(same declarationWindow mechanism the R-4.x rules already use) — the tokens
doctrine's physically-fixed exception finally has its machine valve. Full
run confirms no other component silently passes (same 102/32).
- natural-time-picker.css: all 9 raw colors annotated line-by-line (5 skies +
knob ink + line/ticks/shadow — sky ARTWORK, not theme roles; identical in
light and dark). NOT tokenized, per the ficha's F-2 disposition. R-2.1
clears for ntp.
- soma/components/natural-time-picker/README.md: the headless contract — the
domain coordinator's state surface (minutes bridge, sliderValue, isDay,
moments, steppers, display) + the fully-delegated perceptual chain table
(TimeField / Slider / generic Picker / Popover).
- natural-time-picker-provider.svelte.test.ts: 12 tests green (band bridge
with wrap + seconds preservation, steppers incl. second non-rolling wrap,
moment jumps + exact-match chips, readonly lock, daylight window bounds,
12/24h display incl. the no-soma.dates fallback, granularity displayTime).
Gates: component-audit 102 PASS (ntp R-2.1 cleared) · type baseline 57 ·
new suite 12/12.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The initiative's real remaining scope (verified 2026-07-10 against the repo;
the "uncommitted work" memory was stale — builder landed in b3d0e290, picker
in f5eb8acb):
- contracts.test: remove gradient-builder/gradient-picker from
ACTIVE_DEV_TRACK. The catalogue guards found exactly ONE violation — the
missing soma barrel exports — now added (GradientBuilder, GradientPicker).
Post-graduation: same 13 pre-existing failures (knob/menu-dial/metrics/
onion-menu debris from other WIP), zero gradient mentions.
- apg declared on both morfos: builder cites slider (every stop + the angle
dial IS a slider thumb — the keyboard-a11y differentiator); picker cites
dialog-modal via the composed Popover (same rationale as date-picker/ntp).
- Both eidos READMEs upgraded to dossier form (Baseline / Comparativa /
Decisiones / Gaps) transplanting the CONTINUE.md competitive research
(OKLCH model shared with the token engine; keyboard-editable stops — the
field is mouse-only RGBA exporters). Stale "active dev track" recipe notes
corrected.
- gradient-builder demo: D-1.2 was a false negative — the tab union was the
correct v2 9-tab set, just formatted multi-line; joined to one line
(canary format). SSR smoke: 200 + 40 data-gradient-builder attrs.
Remaining on both: D-1.5/D-3.1/D-4.3 — the catalog-wide demo-census (fails
on 10/7 components INCLUDING the button canary), tracked in the dossiers'
Gaps, not gradient-specific.
Gates: component-audit 102 PASS · contracts 24 pass/13 pre-existing ·
vocab exit 0 · type baseline 57.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dialog-001 a11y-race fix (53b6f629) removed the literal role from dialog
Content on purpose — variant-dependent dialog/alertdialog, DialogContentProvider
is the sole authority — but the compile test kept asserting the static hoist on
that part, red on HEAD since then. Adjudicated: stale test, not a compiler
regression (mechanism verified intact via Title, which does declare a literal
role: heading).
- Re-point the hoist mechanism test at Title (heading).
- New guard: Content emits NO static role, locking dialog-001 against a
re-declared literal reintroducing the syncAttrs overwrite race.
Suite 35/35 (was 33+1 failed); type baseline 57.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Option (a) with riders: the ::before slop stands for bare markers (the
Material mdc-touch-target / Android touch-delegate / iOS hit-inset lineage).
Rider 1: dense coarse stacks must keep pitch >= --touch-target (WCAG excludes
overlapped area; bare boxes still hold AA via 2.5.8 Spacing worst-case).
Rider 2: the Field labeled-row task stays open as the ADDITIVE improvement —
the row becomes the real AAA target (2.5.5 Equivalent) and the slop remains a
harmless net. Chronicle updated in changelog §37; standing in reference §37;
handoff block resolved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- natural-time-picker: expression 'delegated' (canon S11; composed children
own every event — clears A-3.1 truthfully; the Picker-generico event debt
stays tracked in its dossier Gaps for the S9 package).
- s-text-virtual-list: drop archetype 'item' from Row (0-event display
surface; item pulled cursor/user-select/hover + a height floor fighting
the canvas-computed row heights, and flipped the classifier to
interactive -> spurious R-1.5). Verified live: rows keep canvas heights,
no overlap, text selectable. Machine now classifies passive (F-1.5
justification already in the dossier).
- time-range-field / field-langs / link-preview: apg declared (spinbutton /
C5 'none — rationale' x2) — clears the three A-1.4 warns.
- button: soma README realigned to the real types (loading/intent/color —
the old pending/pressed/expanded/href surface no longer exists) + the two
stale docblocks from the audit ficha (types aria-label layer leak, wrapper
data-state mention).
- password-field: morfo StrengthMeter comment aligned with types (default
$libs/secs scoring, overridable) + index.ts example updated to the
post-S6 Control composition.
- Affected dossiers' Gaps tables updated (implemented rows removed).
Verified: component-audit 102 PASS (A-3.1/R-1.5-artifact/A-1.4 x3 cleared),
vocab guard exit 0, type baseline 57, eidos contract tests 7/7, stvl checked
in browser. Note: compile.test.ts 'hoists role into staticAttrs' (dialog)
fails on HEAD pre-existing — unrelated to this pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The S7 doctrine already landed in P0/P1 (guide §5 classes, machine classifier
via morfo evidence, F-1.5 marker rule, canonical exemplars cited). This closes
the two missing artifacts:
- color-swatch: re-anchor the existing "Why a primitive" rationale under the
canonical ## Passive justification heading (machine rule F-1.5) with the
formal classification paragraph. Verified against the machine: the F-1.5
failure is gone from the audit report (remaining F-1.1..1.4 warnings are the
grandfathered dossier — the S4 pass).
- spin-field: layer README formalizing the shared-layer contract per guide §5
(consumer-stamped selectors, sizing sourced from Field's resolved control
privates, stepper-only recipe vocabulary, the two compound-specificity box
overrides, foundation loading, consumer list). radio-cards needs no F-1.5
(machine classifies it interactive); its canonical citation lives in the
morfo header (verified).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both were type-only: the assertions read morfo-declared keys (data-editing,
role) that DO reach the provider props at runtime through the renderProps()
spread, but the static type of runtimePart.assert erases them. Read them
through an explicit indexed cast instead; suites stay green (6/6). Baseline
drops 59 -> 57 — protocol number updated in the handoff, with the remaining
out-of-mandate errors inventoried (9 src/: file-upload x4, navigation-menu,
arts/connection x4; ~48 web/ demos).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The audit's directed verification confirmed the gap: panel sizes were
internal-only state (per-panel defaultSize seed + onResize/onResizeEnd
callbacks), so a consumer could not control or persist the layout. Add the
framework-norm bindable pair:
- `sizes` (bindable, % by panel registration order): seeds the initial
layout when provided, stays in sync while the user resizes, and an
external write re-applies the layout clamped to each panel's min/max
(collapse bookkeeping is not re-derived — parity with reference
setLayout APIs). Plumbed through the eidos wrapper (a spread can't
two-way bind, so it re-binds explicitly).
- `onResize` renamed to `onSizesChange` — with the bindable it forms the
norm pair (X + onXChange); keeping both names would alias one concept.
`onResizeEnd` stays (distinct commit moment, fires with sema
commit-set). Internal mutations funnel through emitSizesChange, which
syncs the binding and fires the callback at the same moment.
READMEs + demo updated (live sizes readout + programmatic 50/50 write,
API table). Provider test covers the pair: binding seeds over
defaultSize, drag syncs binding + callback, external write applies
clamped. Suite 4/4; browser-verified both directions (drag 50/50 -> 80/20
syncs the readout; the 50/50 chip re-applies the layout). Baseline 59.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
textarea (the audit-CONFIRMED violation): measureAutosize wrote
height:auto then synchronously read scrollHeight + getComputedStyle — a
forced reflow on EVERY keystroke (input / clear / mount). The read block
now defers through the context ActiveDom's dom.measure: write stays in
the event turn, the read runs post-layout in the coalesced pre-paint
frame (typing bursts coalesce to one measurement; stale pending reads
cancel), and the clamped height is written back inside the frame
(read->write, no mid-turn reflow). Verified live: style.height is 'auto'
synchronously after input (the old code showed px immediately) and the
grown px lands next frame.
Directed census over the audit candidates, violations fixed:
- scroll-area: the thumb's size/offset getters read the track's
clientWidth/Height inside the thumb props $derived — a forced reflow
per scroll tick. The scrollbar now mirrors its track size into $state
fed by a ResizeObserver; thumb math + drag read the mirrors. The
initial viewport measurement defers through dom.measure.
- virtual-list (both viewports) / virtual-grid: the initial
updateSize/updateScroll ran sync at ref-attach right after DOM
insertion — deferred through dom.measure (observers keep them fresh).
- carousel: the mount seed defers through dom.measure (lands pre-paint,
before any interaction), which closes the async-observer gap the
viewportSize $derived's live clientWidth fallback guarded — that
fallback (a render-time layout read) is removed.
Classified compliant (event-time reads before writes — D13 is a timing
rule): float-panel (drag already caches bounds; open seed is one-shot),
cropper, splitter.
Suites updated to the deferred contract and green (scroll-area 3/3,
carousel 4/4, virtual-list, virtual-grid); baseline 59.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add getWeekInfo(locale) -> { firstDay, weekend } to $libs/days (Intl
convention, 1 = Monday ... 7 = Sunday): firstDay mirrors the internal
getWeekStart (Intl weekInfo + region-table fallback — one source of truth,
now exported from the vendored port), weekend comes from Intl.Locale
weekInfo (getWeekInfo() method or the Safari weekInfo accessor) with the
DOCUMENTED Saturday/Sunday fallback. Cached per locale.
Calendar + RangeCalendar derive their weekend flag from it: the shared
helpers.isWeekend(date) hardcoded Sat/Sun against a fixed 'en-US' — wrong
for e.g. ar-SA (Fri/Sat) — and now takes the provider's effectiveLocale.
Verified live: data-weekend moves to Fri+Sat under ar-SA, stays Sat+Sun
for en-US / es-ES; firstDay verified (es-ES Monday-first, en-US
Sunday-first grids; explicit weekStartsOn still overrides).
Directed firstDay verification also caught getSimpleWeekNumber mixing
frames: an ABSOLUTE weekStartsOn (0 = Sunday) was passed to
getLastFirstDayOfWeek with the display locale, which re-interprets the
index relative to that locale's own week start — es-ES 'simple' week
numbers anchored the year on a Tuesday and ran one week behind. Anchor in
the fixed en-US frame (same convention as buildMonth's explicit branch);
the dead locale param is removed from both call sites.
Tests: getWeekInfo (es-ES / en-US / ar-SA / cache identity) + new calendar
helpers suite (locale weekend, grid week-start per locale, week-number
regression). Suites 15/15, baseline 59.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The range endpoint colors lived as private `--_calendar-range-*` tokens declared
in range-calendar.css AND redundantly re-tuned by date-range-picker (reaching into
a sibling's privates). Promote them to the calendar-surface PUBLIC API: 8
`--calendar-range-{start,end}-{solid,solid-hover,border,text}` tokens in the
`calendar` recipe (`:root` defaults), consumed by range-calendar via the public
name. DateRangePicker drops its 6 `--_calendar-range-*` blocks — the composed
range calendar already paints from range-calendar.css, so the drp declarations
were redundant.
Deliberate asymmetry (documented in range-calendar.css): START defaults live in
the recipe `:root` so a theme can override them via config; END binds to the live
`--_calendar-accent-*` on the host (the accent is a per-color private that only
cascades there), so its `:root` defaults are the nominal public default. Both stay
public per the S8 verdict, preserving the original "end may differ from accent"
capability.
Value-preserving: recipe-css-contract 25/25, baseline 59, eidos-lint invalid 0,
browser-verified identical across default/affirm/secondary/fulfill on the
standalone RangeCalendar and the DateRangePicker popover.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark S6 done (16 fields compose field.css + 199 orphaned recipe tokens pruned +
date/time pickers de-aliased onto the Field public tokens, commits 5eb92132..
d1670caa). Rewrite "lo que queda" to start at S8 calendar-surface; record the
two non-blocking follow-ups (TimeRangePicker endpoint data-size threading
task_d66356de, PickerProvider deferValue migration) and the S6 method lessons
(run the recipe-css-contract vitest scope after CSS refactors; --field-segment-
height only exists under [data-field][data-size]).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The date-range / time / time-range pickers referenced --date-field-* /
--time-field-* recipe tokens to paint their field-shaped parts (standalone
trigger box, wrap gap/font, embedded trigger). That is a cross-component alias
— the exact drift the S6 Field composition eliminates. Point every one at the
FIELD FAMILY public tokens instead (--field-control-* / --field-* / the B.1
--field-segment-height), or at the underlying primitive where Field has no
matching token (the wrap column-gap keeps the tighter segment rhythm). Each
substitution is computed-value-identical (both sides resolve to the same
primitive) — adversarially verified + browser-confirmed on all four pickers.
Also drop dead private declarations the compositions left behind
(--_time-field-padding-inline / --_time-field-segment-padding-inline, and
--_time-field-gap in time-picker) and, now that nothing aliases them, prune the
32 newly-orphaned date-field/time-field chrome tokens from the recipe +
generated CSS (recipe-css-contract orphan guard green, 25/25).
TimeRangePicker's embedded clock-trigger keeps sizing from the picker-local
--_time-field-height (not --field-segment-height): its composed endpoints don't
carry their own [data-size], so the field-family segment-height would pin to md.
That endpoint size-threading gap is pre-existing (DateRangePicker threads it,
TimeRangePicker doesn't) and tracked separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The S6 Field composition moved each field's chrome (border / radius / bg /
height / padding / font / label) into field.css, leaving the per-component
recipe tokens still emitting but unconsumed. recipe-css-contract.test.ts's
"does not leave declared public recipe variables orphaned" test went red with
167 tokens across spin/date/time/color/search/mask/password-field.
Remove the confirmed-orphaned keys from each recipe (mask-field emptied →
entry dropped entirely); regenerate generated/base.css. Value-preserving:
the removed tokens had zero consumers (the test is the oracle), browser-
confirmed identical rendering on search-field + password-field.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Password is the one field with content BELOW the control (the strength meter), so
unlike the collapsed text fields it needs the full canonical Field structure:
provider STACK > control BOX + supporting content. Added a `<PasswordField.Control>`
part (morfo + soma provider class + component + exports/types + eidos wrapper/index)
that carries `fieldRole('control')`; the Provider takes `fieldRole('provider')` (the
stack) and the input `fieldRole('input')`.
- field.css owns the control box chrome (border/radius/bg/height/padding/focus-within/
invalid/disabled) + the input reset. password-field.css keeps ONLY the affixes:
visibility trigger, caps-lock indicator, strength meter, and the input's mask-tracking.
- REMOVED the 2-row grid + `::before` chrome hack (it existed only to avoid `:has()`
layout flicker without a control element). Now the strength meter is a plain stack
sibling BELOW the control — appears/disappears with no `:has()`, no flicker.
- No tokens aliasing the Field base.
This is the canonical composition (why A over the de-alias compromise: composability
IS the S6 canon; the "API change" — wrapping row 1 in <Control> — aligns password to
how Field itself works, it doesn't invent API). Contained: the demo is the consumer,
updated here.
Baseline unchanged (59). Verified: Provider stack (flex column), Control box paints
from field.css (36px/border/pad/radius), input + eye affix in a row, strength meter
8px below as a sibling, :focus-within ring. Closes S6 field composition — all 16
fields now compose field.css.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Converts the shared `spin-field.css` layer (NumberField + CssField) to the
collapsed text-field pattern. Both morfos add `fieldRole('provider')` +
`fieldRole('control')` to the spin-field box (data-field + data-field-control
alongside the existing data-spin-field marker) and `fieldRole('input')` to the
bare input; both eidos wrappers import field.css.
spin-field.css drops its ENTIRE parallel control chrome (border/radius/bg/height/
padding/font/focus/invalid/disabled/variant/readonly + all `--spin-field-*` chrome
tokens) — field.css `[data-field-control]` owns it now. It keeps ONLY the stepper
affordances (increment/decrement triggers, scrubber, split + stacked layouts,
themeable glyphs), re-sourced off field's OWN resolved control tokens
(`--_field-control-height` / `--_field-control-padding-inline` / `--_field-control-gap`)
so the steppers always match the input row — no parallel size axis. Two box overrides
survive at `[data-spin-field][data-field-control]` (0,2,0, beats field.css regardless
of the global-vs-per-mount load order): `overflow: hidden` (edge steppers break out
of the padding then clip to the corners) and `gap: 0` (steppers butt flush via dividers).
Baseline unchanged (59). Verified in browser: number-field + css-field paint from
field.css (36px/border/radius/overflow), steppers 36px flush at the edges with
dividers, split layout intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same collapsed text-field pattern as the search-field pilot: mask-field is a single
inline-flex row box (input only, no affixes), so its Provider part collapses
`fieldRole('provider')` + `fieldRole('control')` and the `<input>` gets
`fieldRole('input')`. field.css owns the entire box chrome (border/radius/bg/height/
padding/hover/focus/invalid/disabled/variant/size) + the bare-input reset; mask-field.css
drops from 143 lines to a single rule — the input's `tabular-nums` (masked patterns are
digit-heavy, keep glyph columns aligned so fixed literals don't shift as the mask fills).
Baseline unchanged (59). Verified: box paints from field.css (36px/border/pad/radius,
flex row), input reset + tabular figures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pilot for the TEXT-field pattern (distinct from the segmented fields): a text field
is a single inline-flex ROW box (input + affixes), with no label/segment stack — it
IS-A control. So its morfo collapses BOTH Field roles onto the one box element:
`fieldRole('provider')` + `fieldRole('control')` (data-field + data-field-control),
and the `<input>` gets `fieldRole('input')`.
- field.css owns the box chrome (border/radius/bg/height/padding/gap), the input
reset, focus (`:focus-within`), invalid and disabled. search-field.css keeps ONLY
the search affixes (leading icon, loading spinner, clear-trigger empty-state) and
the native `<input type=search>` clear-button suppression. No tokens aliasing Field.
- field.css `[data-field-control]` now sets `flex-direction: row` explicitly: on a
collapsed box the provider's `flex-direction: column` (label/control stack) leaked
in and stacked the affixes vertically. A control is always a prefix/input/suffix
row, so this is a no-op for non-collapsed controls (verified date-field unaffected).
Baseline unchanged (59). Verified: box paints from field.css (36px/border/pad/radius),
input reset, `:focus-within` ring, icon + clear in a row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Applies the field-composition pattern to color-field (the last segmented field):
- color-field: chrome / label / invalid / disabled / focus come from field.css via
the fieldRole identity (data-field / data-field-control); color-field.css keeps
only its GENUINE specifics — the channel segments (3ch / hex 6ch), the vertical
divider separators, the leading value swatch, the format <Select>, and the
`gap: 0` control override (per-element spacing, so a flex gap would double the
dividers). No tokens aliasing the Field base. Imports field.css.
- ColorPicker: the in-popover value row IS a composed ColorField (it already renders
a ColorField provider element carrying data-field + threaded data-size), so it
only needed field.css imported for the chrome to resolve inside the portal.
Baseline unchanged (59). Verified in browser: color-field paint + label + swatch
(18px) + format select + 5 segments; picker value row paints with field chrome.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Applies the proven date-* pattern to the time family (exact structural parallel):
- time-field: chrome / label / invalid / disabled / focus come from field.css via
the fieldRole identity (data-field / data-field-control); time-field.css keeps
only segment specifics. No tokens aliasing the Field base. Imports field.css.
- time-range-field: COMPOSES two real <TimeField>s (soma TimeRangeField.Input →
TimeField.Provider > TimeField.Input per endpoint) bound to the range slice;
size/variant/color thread through a visual context; CSS is layout-only. (No
syncPlaceholderToValue opt-out needed — TimeField has no calendar to auto-page,
and the controlled placeholder keeps the endpoints from mutating each other.)
- TimePicker (single field): root carries data-field/data-time-field (A27);
imports field.css.
- TimeRangePicker: inherits the composed endpoints; imports field.css (no
double-box — its recipe never re-declared the endpoint box).
Baseline unchanged (59). Verified in browser: time-field paint + label, range
endpoints = composed TimeFields (nested provider>control, size threaded, value
renders), both pickers paint, no double-box.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the two remaining date-range-picker cleanups after the composition refactor:
- Size threading (functional): the eidos `<DateRangePicker.Input>` now reads the
picker's visual context and forwards data-size/variant/color to soma's Input
(= the composed DateRangeField.Input), so each endpoint's composed <DateField>
self-themes at the picker size. Verified reactive (md → lg resizes both endpoints).
- De-alias / prune (no-token-alias hygiene): removed the orphaned
`--_date-field-padding-inline` / `--_date-field-segment-padding-inline`
re-declarations (their consumers — the endpoint box + segment rules — are gone);
reduced the shared label rule to layout-only (colour/size come from
date-range-field.css); RETARGETED the endpoint-invalid rule from the now-borderless
endpoint wrapper to each inner `[data-field-control]` with Field's
`--field-control-border-invalid` (it was a no-op post-composition — invalid now
paints again); and re-based the embedded trigger on the Field `--field-segment-height`
token + shared segment-active tokens + foundation radius (no date-field aliases).
Residual (coupled to the pending date-field recipe-token pruning): the UNUSED
standalone trigger + the root wrap-stack still reference date-field recipe tokens
(height/gap/font-size/accent); they resolve correctly and retire with that pass.
Baseline unchanged (59). Verified: two boxes, embedded trigger sized/rounded,
invalid paints, size reactive.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Design decision (2026-07-08): the range picker keeps TWO separate composed
<DateField> boxes side by side — the border stays on each DateField, never on a
wrapper enclosing both. This overrides the reference-frameworks default (a
web-sourced sweep of React Aria / MUI X / Ark / Mantine / Bits UI found single-box
is the norm 5/5), which is the user's call.
The former `[data-date-range-field-group]` treatment moved the chrome onto a
consumer-supplied wrapper div to collapse the two endpoints into one box. It was
CSS-only, unused (no component part, no demo), and — after the composition
refactor moved each endpoint's box to its inner [data-field-control] — it targeted
the wrong element anyway. Removed outright. Verified: two endpoint boxes intact,
no enclosing box.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The range picker's segments are composed DateField segments (they carry
data-date-field-segment + the shared data-field-segment marker and read their
tokens from each endpoint's own [data-date-field]). The per-picker copy only
duplicated the Field base and re-added a focus font-weight the shared
field-segment-state layer deliberately drops (bold widens the ch-based segment,
jittering it on every increment). Removing it defers to date-field.css +
field-segment-state.css. Verified: segment box / padding / min-width / highlight
unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The date-* family now IS-a-Field then evolves, per the S6 mandate + the
no-token-alias doctrine — no more marker-overlay borrowing of date-field.css.
- date-field: chrome / label / invalid / disabled / focus come from field.css via
the fieldRole identity (data-field / data-field-control); date-field.css keeps
only segment specifics. No tokens aliasing the Field base.
- Field recipe: add --field-segment-height (size-axis, family level).
- DateField.Provider: expose syncPlaceholderToValue for controlled composition.
- date-range-field: COMPOSES two real <DateField>s (DateField.Provider >
DateField.Input per endpoint) bound to the range slice; size/variant/color thread
through a visual context; CSS is layout-only. Fixes the field.css-not-loaded
chrome regression.
- DatePicker (single field): root carries data-field (A27); imports field.css;
drops the dead date-field token re-declarations.
- DateRangePicker: inherits the composed endpoints; imports field.css; removes the
endpoint box that double-boxed the composed DateField.
Baseline unchanged (59). Verified in browser: paint, edit coordination, size
reactivity, no double-box.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the FieldLangs component: export it from the soma components
barrel and add its demo route to the docs sidebar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FieldLangs holds one value per language and switches the active editing language
through an adaptive in-control switcher, composing the shipped Field / Popover /
TextArea instead of reinventing them (picker composition pattern, A27).
- Coordinator (soma): values map + activeLang + required-per-language validity;
exposes the value/selection proxies the composed controls bind to; fires
shift-navigate (context change) as its only semantic event.
- Switcher (eidos, FieldLangs' own -- deliberately NOT a composed ToggleGroup,
whose commit-toggle is the wrong semantic for a language navigation and would
leak into the field): a segmented radiogroup for 3 or fewer languages, an own
Popover dropdown for more; per-language set/required dot; roving keyboard, RTL.
- Single-line Input and multiline Textarea (corner switcher, the field's default
floating label untouched); floatingLabel passthrough; switcher font one
typographic step below the control (label doctrine).
- morfo (provider/switcher/lang-option/menu/textarea parts + shift-navigate
event), i18n catalog, and an interactive demo.
Guards: svelte-check clean, morfo test, eidos-lint 0 invalid, component:audit PASS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the static `lazy` boolean with `showMask`, defaulting to `'focus'`.
A reference sweep (imask `lazy`, jQuery-inputmask `showMaskOnFocus`,
react-input-mask `alwaysShowMask`, ngx-mask `showMaskTyped`, PrimeReact) found
no library that shows the full skeleton on a blurred, empty field by default:
the ones that reveal it do so on focus. So the resting field stays clean, and
focusing reveals `(___) ___-____` with the caret on the first slot; blurring an
unfinished field drops the placeholder tail. `'always'` (skeleton even when
blurred) and `'never'` (progressive) are the opt-outs.
- provider: `effectiveLazy` derived from `showMask` + focus; `revealOnFocus`
(wires the already-exported `firstEditableCaret`) and `settleOnBlur`; `isEmpty`
now reads the unmasked value so a visible skeleton doesn't read as filled.
- `'focus'` composes with `Field.floatingLabel`: focus floats the label and
reveals the skeleton without the two competing for the empty control.
- demo: `showMask` chips replace the `lazy` switch; README documents the
decision + the reference sweep.
Verified all three modes in the browser; provider tests 31 pass (adds a
focus/blur reveal test); check clean for mask-field.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MaskField demo showed the composed `<Field floatingLabel>` case only as a
static example. Turn it into a toggle in the Field-integration section (default
on) so it reads as a real option, with a note that floatingLabel is a Field-
level mode (not a MaskField prop) that works because MaskField renders a real
<input>. Verified: toggling flips data-floating-label on the wrapping field.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The floated-label fill test targeted `[data-field-input]` — the base
Field.Input only — so `<Field floatingLabel>` didn't float when wrapping a
specialized field (MaskField/NumberField/PasswordField/… render their own
`<input>`). Widen it to any non-hidden text input / textarea inside:
`:has(:is(input, textarea):not([type='hidden']):not(:placeholder-shown))`.
Now the floating label works for any composed text field (the vertical anchor
already aligns via shared --size-*-control-height tokens). Segmented controls
(Date/Time, contenteditable) still float on focus only — deferred.
Demo: MaskField floating example added to its Field-integration section.
Verified in browser: resting placeholder centred, floats onto the border when
filled, mask still conforms the value.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Floating label (`<Field floatingLabel>`), the label typography doctrine, and
its propagation across the text-field family.
Floating label (Material outlined, text inputs only):
- CSS-only via :focus-within + :placeholder-shown — no new soma state. The
label starts as a placeholder inside the control and floats onto the top
border when focused/filled. Reuses Field.Label + Field.Input. Requires the
input to carry `placeholder=" "`. Segmented controls (Date/Time) not covered.
- Floated position is themeable via `--field-floating-label-top` (default 0 =
on the border) without touching the recipe.
Label weight doctrine ("label one step below the component"):
- The field VALUE is now `medium` and the label stays `regular` — one weight
step below (the size offset was already derived one step down). Base Field via
`--field-control-font-weight`; propagated to the specialized text fields
(number/css via spin-field, mask, password, search, textarea, date, time,
date-range, time-range, color) so every field value reads at medium.
Verified: recipe-css-contract + component-visual-attrs + api-contract (30),
eidos-lint 0 invalid, smoke, check 0 field errors. Browser: floating resting↔
floated transitions correctly; value 500 / label 400 across fields.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Native <label> accessible primitive with `for` association — the gap the
Field family left open (Field.Label is context-bound; nothing labelled a
control outside a Field). Ported from morfo-runtime:terra/label into the
current eidos-native leaf shape.
- Eidos-native leaf (scope: ['eidos'], no soma/sema): morfo is the contract
(one Provider part -> data-label); the wrapper renders <label> directly.
- Visual axes size/weight/color follow the Text typography mechanism
(--style-label-* base + inline --_label-* overrides via ActiveEidos.resolve),
not a prop-less shell — real selectable axes in the demo.
- Multi-click mousedown guard (Radix convention) inline in the wrapper;
composes the consumer's onmousedown.
- Complements Field.Label (auto-wired inside a Field); floating label is a
deferred Field concern, not a prop here.
Passes component:audit label (0 err / 0 warn), check (0 label errors),
eidos-lint (0 invalid), recipe-css-contract, smoke. Browser-verified: size/
weight/color change the label live; for-association focuses the control.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two behavior-preserving fixes that unblocked the repo-wide morfo tooling
(morfo:check / morfo:vocabulary / perm:check all threw at loadMorfos):
- data-intent is enum-valued, so the compiler writes it in `enum` mode and
`emit` is ignored; its presence on an enum is rejected by the schema
invariant (schema.ts, tested). Removed it — matches timeline / toast.
- Knob declares 5 events and ships a sema pack (knobSema) but scope was
[soma, eidos]; added 'sema' so declaration matches reality.
Verified: morfo:vocabulary exit 0, schema.test 14/14, morfo:check knob PASS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebuilds the GradientPicker on the generic Picker.Provider<Gradient>
transaction core (open / commit / cancel / clear / workingValue + a composed
Popover), like the natural-time-picker — the bespoke coordinator is now a thin
domain provider (morfo runtime + working-value CSS + the presets state).
- deferValue prop (live vs deferred draft-until-Listo). The trigger chip
reflects the COMMITTED value, so a deferred picker's chip stays put while you
edit the working draft.
- Clear restores the INITIAL bound value the picker was given, not a hardcoded
default gradient.
- No tabs: the editor IS the popover body; the presets grid opens in a panel
from a Layers button next to "Añadir degradado" (the Add <-> Remove toggle) —
ColorPicker-style "select a saved preset -> Remove", and it reclaims the
tab-header height.
- Add / Remove presets fire commit.fulfill / commit.loss firmas (new
gradient-picker sema pack, registered in the demo). Per the Button doctrine the
press is a neutral contact.activate and the consumer emits the RESULT commit;
the events target the REGISTERED `provider` part (targeting the unregistered
`content` part was also why the Clear commit-reset was silent).
- Stop color editor hides the remove-stop button for the endpoint stops
(first / last by position — they anchor 0% / 100%).
- maxPresets cap (default 10). StopList in a ScrollArea (autosize, slim
scrollbar with a gap so the rows never sit under it).
Composes the existing components AS-IS (Popover / Button / ScrollArea / the
GradientBuilder parts) without overriding their styles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GradientBuilder (this session):
- Stop color editor uses the canonical ColorField value row (swatch +
styled Select), not a native select; rails at 90% width + xl track.
- Composable footer: ColorPicker.Cancel / ColorPicker.Close + trash
remove-stop, as a sibling of PickerShell.Body.
- Surface-agnostic by default (variant='ghost'): no card, adapts to host.
- '+' add-stop on the kind-switch row; inserts between the selected stop
and the next; rail click-to-add re-enabled (insert at cursor).
- GradientBuilder.StopList composable stops list (rendered by default).
- presets prop (Gradient[]) rendered below the editor (flex, not tabs);
save-to-presets with a maxPresets cap; the action button flips
Save <-> Delete when a saved preset is selected (ColorPicker mechanic).
Also lands the in-progress gradient tree so HEAD is self-consistent
(langs/components/index.ts already imported ./gradient-picker, whose file
was untracked). gradient-builder + gradient-picker stay on ACTIVE_DEV_TRACK;
the GradientPicker 2-tab redesign is next (see gradient-builder/CONTINUE.md).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark the workstream done in the continuation handoff: orca translated (A2),
active-app promoted to an architecture chapter (B1), historical DESIGN docs
moved to decisions/ (B2), runtime-artifact shortcut rerouted (B3). Guards green
at close (arts:check 0/0/22, docs:check 0 err).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>