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>
The "I want to… use a runtime artifact" row in docs/README.md now routes
through the composition root (architecture/active-app.md) → the arts map
(arts/README.md) → the per-artifact README. Map/aliases were already complete
(9e48b80b). docs:check 0 errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the three superseded arts design-phase records into the E3 corpus
(already indexed in docs/decisions.md), verbatim-as-historical:
- docs/decisions/design-connection.md (from src/arts/connection/DESIGN_CONN.md)
- docs/decisions/design-session.md (from src/arts/session/DESIGN.md)
- docs/decisions/design-timer.md (from src/arts/timer/DESIGN_TIMR.md, kept Spanish)
Each gets a `status: historical` frontmatter block; the current reference for
each subsystem is its README.md. A stub at every old src/arts/* path keeps the
~30 source provenance citations (`DESIGN_TIMR §12.8`, `DESIGN.md §7.Q1`, …)
resolving. Bodies kept verbatim (no reformat). docs/decisions.md index + naming
note updated to the new paths. docs:check 0 errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ValueField: optionally-editable centre value composing NumberField, wired to
the knob's value (time-picker-hour-slider pattern); de-chromed to read as
text, width-locked to the max value's digit count, and its pointer/keyboard
events isolated from the rotate gesture so editing never rotates or resets
the dial.
- handle-drag: continuous rAF-throttled perceptual tick during rotation,
mirroring the Slider (morfo event + provider emit + sema rule).
- NumberField: first-class maxLength prop (native maxlength, unlimited when
unset); the knob caps the field to the max value's digit count.
- Rotation: publish --knob-start-angle / --knob-sweep so the arc reacts to the
sweep; snap to the nearest end inside the bottom dead zone so overshooting the
max lands on the max and never wraps around to the min.
- Register knobSema in the uix demo layout so the knob emits sound + haptic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the active-app reference into the book tree as the arts composition root,
the App-side analogue of active-uix.md:
- new docs/architecture/active-app.md (frontmatter + fixed relative paths); the
dated Spanish "Handoff 2026-05-13" heading becomes a timeless "Quick start".
- extract that Handoff block verbatim (ES) to docs/process/handoffs-2026-05.md,
alongside the other extracted arts/uix handoffs.
- stub src/arts/active-app/README.md pointing at the chapter (active-uix pattern).
- wire the chapter into the docs/README.md E1 map.
docs:check 0 errors (11 pre-existing warnings unrelated).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Translate the last Spanish README in the arts reconciliation (~1457 lines).
Prose only; all code blocks, ORCA_* constants, type names and the A1 API
corrections that landed in 9e48b80b are preserved verbatim. arts:check 0/0/22.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full Spanish -> English translation of sium/README.md (862 lines; faithful).
Code blocks, schema examples, issue codes and `#?sium.errors.*` idlangrefs kept
verbatim. Also fixed a stale ref: `/test/sium` -> `/active/docs/sium`. Carries
the A1 additions (cssLength/cssValue in Included Refines).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full Spanish -> English translation of cache/README.md (faithful; code blocks,
CACHE_* constants and scope/policy values kept verbatim). The mixed file's
Spanish sections (What It Solves, Keys, Scopes, Policies, Modes, Tags, Mutate,
Explain, Adapters, Events/Logger, Security, Roadmap) translated; the already-
English "Reacting to identity" section left as-is. Carries the A1 fixes
(defineActiveCache signature, ActiveEngine contract surface, demo URL).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full Spanish -> English translation of adom/README.md (faithful; code blocks and
the ActiveDom interface kept verbatim, incl. the Backlog chronicle entries).
Carries the A1 additions (prefersReducedMotion/writeProperty/removeProperty,
listen 4 overloads, standalone rune-helpers table, /active/docs/adom).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full Spanish -> English translation of connection/README.md (faithful; code
blocks, event/error constants and state diagrams kept verbatim). Carries the A1
fixes (required `{ timers }`, the 13 ActiveConnections getters, `/active/docs/conn`
demo, the no-own-scheduler paragraph).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full Spanish -> English translation of auth/README.md (faithful; code blocks,
route/error constants and the `$lib/server/*` SvelteKit-consumer imports kept
verbatim). Example Svelte UI strings translated (Validating.../Hello/Anonymous
session). Also fixed a stale ref: `/test/auth` -> `/active/docs/auth`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full Spanish -> English translation of format/README.md (faithful). Code blocks,
tables and es-ES formatter OUTPUT strings in comments (e.g. "hace 2 horas",
"lunes, 25 de mayo") kept verbatim — they demonstrate locale output, they are
data, not prose. Carries the A1 fixes (units/ path, formatAs/convertAs,
convertToDefault, $format/currency alias).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full Spanish -> English translation of prefs/README.md (faithful; all code /
text blocks and the `prefs::*` error strings kept verbatim). Carries the A1
signature fixes already landed (applyBrowserEnvironment/watchBrowserEnvironment,
createPrefsStorageBridge `engine`).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First A2 batch (Spanish -> English, faithful; code blocks + i18n example data
left untouched):
- clipboard: full translation (was fully Spanish).
- langs: light pass — the "Handoff 2026-05-12" Spanish paragraph only (the rest
was already English; example i18n records like 'Aceptar' are data, not prose).
- logger: light pass — the "Framework Logger Contract" + "Framework Diagnostics"
Spanish spans (rest already English).
No residual Spanish prose in the three files (verified by grep). arts:check 0/0/22.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second A1 batch — every fix verified against the cited code symbol, not the
plan (the plan repeatedly under- or mis-counted; verifying against code caught
both extra drift and already-fixed items):
- format: `unts/`->`units/` path typo; documented currency `formatAs`/`convertAs`
and units `convertToDefault`; fixed the `$formats/currency` alias (plural, no
such alias) -> `$format/currency` in format + currency READMEs.
- timer: FS diagram completed (11 -> the full 16 source files), "50 tests" -> 47,
reschedule behaviour/errors reconciled (from a prior uncommitted session; all
verified accurate against timer-handle.ts).
- bus: Layer-split diagram corrected (consts/errors/matching/diagnostics/contracts
live in libs/bus, not arts/bus; only EngineBus/EngineBusOptions + engine/svelte
in arts/bus; added silent-bus.ts); documented all 3 listener error modes
(log-and-continue/throw/collect); fixed stale preset names -> the real
cache-clear-on-*/connections-*/perm-invalidate-*/session-auto-refresh/standard.
- connection: `createEngineConnections()` -> `{ timers }` (required); documented
the 6 missing ActiveConnections getters (13-getter surface); conn-chat demo
(nonexistent script + npm task) -> real `/active/docs/conn`; fixed a paragraph
that contradicted the required-timers reality.
- sium: documented `cssLength`/`cssValue`/`CSS_LENGTH_REGEX` refines (were absent);
verified the "codes" item was already covered (siumLangs has all 26).
- prefs: `createPrefsStorageBridge({ prefs, storage, key })` -> `{ engine,
storage, onError?, skipHydrate? }` (prefs->engine, key was phantom);
`applyBrowserEnvironment(prefs,...)` -> `(engine,...)`;
`watchBrowserEnvironment(prefs,...)` -> `(apply,...)` (first arg is a callback).
- http: `DEFAULT_RETRY`/`DEFAULT_TIMEOUT` -> `HTTP_DEFAULT_*`.
- motion: documented the missing `resolve`/`has`/`list`/`exit`; noted `JsDriver
'svelte'` is reserved/unimplemented (README + engine-motion.ts JSDoc, which
falsely listed it as running).
- color: types.ts JSDoc `ActiveEidos.setCssVariables` -> `applyColorScheme`; bare
`THEMING.md` ref -> `../../uix/eidos/THEMING.md`.
- systemic (not in the plan): `defineActive*`/`defineEngine*` imported from
`$active-app/services` (the schema-contract module, which does NOT export them)
-> `$active-app/service-factories`, in clipboard + auth + storage + active-app.
arts:check: 0 errors, 0 warnings, 22 arts. Note: bus/connection/format/prefs/sium/
timer carried prior uncommitted arts-reconciliation edits from an earlier session,
folded in here (all coherent, same initiative).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the provider→recipe direction of the CSS-var drift stumble the
architecture-faithful way: the morfo is the source of truth. A component now
declares the functional custom properties its soma provider writes and its eidos
recipe reads:
cssVars: [{ name: 'progress', … }, { name: 'angle', … }] // → --knob-progress, --knob-angle
- types: `MorfoCssVar` + optional `cssVars` on `Morfo` (additive; the 133
existing morfos are unaffected).
- schema: validates `cssVars` (array of `{ name, description? }`).
- compile: exposes `contracts.cssVars` as full names `--{kebab}-{name}`.
- knob morfo declares its two contract vars.
- guard (recipe-css-contract.test.ts): enumerates morfos via `import.meta.glob`
and verifies BOTH sides honour each declaration — the provider writes
`--{kebab}-{name}` AND the recipe reads it — so renaming one side without the
other fails loudly. Proven non-vacuous (a bogus cssVar is flagged on both
sides).
Why A1 and not a grep cross-check (A2): in the CSS a provider-written functional
var and a consumer-override alias are indistinguishable, and providers publish
hook vars the recipe doesn't consume — a grep gives ~17 false positives (proven
earlier). The morfo declaration is what disambiguates.
npm run check 59 (baseline, 0 in touched files); eidos recipe-css-contract 24/24;
morfo suite green (only the pre-existing dialog-role test fails); docs:check 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Empirically confirmed the grep approach can't distinguish a provider-written
functional var from a consumer-override alias (identical in CSS), and providers
publish API-hook vars the recipe doesn't consume (17 such, none read by var()).
Only A1 (morfo cssVars declaration) can close the provider→recipe direction.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the recipe→theme direction of the soma→eidos CSS-var drift stumble. New
check in recipe-css-contract.test.ts: every NO-FALLBACK `var(--x)` in a
component recipe must resolve to a token declared somewhere in the eidos CSS
tree (foundation + shared layers + the component's own self-declarations). A
`var(--x, default)` stays exempt — those are runtime-optional by construction
(provider/floating vars whose fallback IS the default), matching the existing
`collectOwnPublicVariables` rule.
This is the exact class that silently broke the Knob (`--color-neutral-content`,
`--state-hover`, `--color-neutral-bg` resolved to nothing, only visible in the
render). The guard immediately caught 3 MORE real phantoms — fixed, not silenced
(silencing would be the anti-pattern of a guard exempting the gap it exists to
catch):
- card-group: `--color-content-default` → `--color-content-primary`
- link-preview: `--leading-body` → `--leading-normal`
- textarea: `--font-family-body` → `--font-family-primary`
Full eidos suite 290/290. The provider→recipe direction (renaming a
provider-written `--knob-angle` breaking the recipe) remains open as option A
(morfo `cssVars` declaration or a grep cross-check) — noted in STUMBLES.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a '### Authoring notes' subsection to soma.md §6 grounded in the real
defs — state<T>() vs $state, role optionality on a Provider, Without<>/
PrimitiveDivAttributes, and gesture pointermove/up ownership. Closes STUMBLES
#9; only #7 (soma->eidos CSS-var contract, design-open) remains.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Knob now built across all 5 layers, validating the exercise. #7 confirmed
first-hand: the Knob build hit phantom theme tokens + uncontracted provider
CSS vars (silent drift), exactly as predicted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The relocated Knob didn't render (only the number showed). Root causes, all
found by inspecting the loaded stylesheet (not guessing):
- A stray `*/` inside the header comment (`--scale-*/--primitive-*`) closed the
comment early and corrupted the FIRST rule, so the base `[data-knob]` rule
(which sets `--_knob-diameter`) never loaded → the control collapsed to height 0.
- Phantom tokens inherited from the Fable draft that don't exist in the theme:
`--color-neutral-content` (→ `--color-neutral-text`), `--state-hover`
(→ a `filter: brightness` hover); the pointer was transparent because its
fallback token didn't exist.
Design (per the user's reference image + directives):
- Value readout centred in the dial's hole (grid place-items).
- Donut arc: replaced an improvised hard-edged radial mask (aliased inner edge)
with an opaque face plate over the conic pie — a border-radius circle is
GPU-antialiased, so the inner ring is clean. Colours use theme-adaptive
tokens (surface-raised face, neutral-track ring, neutral-text value,
neutral-text-strong pointer).
- Value + label fonts scale with the knob via the same size coordinate as the
diameter (`--size-{k}-*` bundle), not an invented "one step below" rule.
Demo: value composed inside the Control (centre), plus a `needle` control that
conditionally composes `<Knob.Indicator>` (the compositional way to show/hide it).
Verified structurally (sizes, tokens, toggle) via DOM inspection; the screenshot
tool timed out all session, so the final pixel check is the user's. Recipe
contract green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two corrections after verifying the relocated Knob against the recipe contract:
1. The Knob was NEVER a contract violation — every `--knob-*` alias in the css
is consumed WITH a fallback (`var(--knob-x, default)`), which the
recipe-css-contract treats as runtime-optional, so no recipe-token
declaration is required. Removing `knob` from `WIP_TRACKS` keeps the full
eidos suite green (289) — it's now first-class, not an excluded WIP track.
2. But `size` was a visual no-op: the css hardcoded `--size-md-*` everywhere,
so `data-size` did nothing. Introduce private `--_knob-*` size vars selected
by `[data-size]` off the global `--size-{k}-*` bundle (public `--knob-*`
still overrides; the size-driven private is only the default; value readout
sits one step below). Browser-verified: control diameter now sm 60 / md 72 /
lg 88 px (was fixed 72 at any size).
npm run check unchanged at 59 (baseline); no console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Canonical-depth demo at web/routes/uix/components/knob mirroring the Slider
demo (Live / API / morfo / sema / recipe / a11y tabs; every public prop is a
live control; sema event trace + play buttons; soma/eidos code snippets).
Serves as the visual/behavioral verification the relocation lacked. Confirmed
live at /uix/components/knob:
- renders role=slider, aria-valuemin/max/now, aria-roledescription resolved
from the langs catalog ("dial giratorio" under the es locale);
- provider-published `--knob-angle` / `--knob-progress` drive the conic-gradient
arc + rotated indicator (angle = startAngle + progress*sweep, verified exact);
- keyboard loop works end-to-end (ArrowUp/PageUp → value 40→52, aria-valuenow +
CSS vars + bound ValueText all update); no console errors.
npm run check: 0 errors in the demo (total unchanged at the 59 baseline).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ships the net that batches A1-A3 / B3 land against, mirroring
scripts/docs-check.ts (textual invariants, no Svelte-graph imports):
- A-readme (error): every src/arts/{name}/ has a README.md. ethereal is
exempt via a self-cleaning README_PENDING set until batch A3 authors it.
- A-naming (error): the Engine*/Active* split holds — no Engine* root is
declared in a .svelte.ts (engines are stateless), and every reactive
`class Active*` root lives in a .svelte.ts (Active...Error exempt).
- A-index (warn): the src/arts/README.md Map lists every art. WARN, not
error — the four current gaps (bus, ethereal, orca, prefs) are batch B3's
worklist and drive to zero there.
Green now: 0 errors, 4 warnings (the B3 worklist), 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
User decided (mirroring F7's corpus-shape call): arts stay
E5-in-place-reconciled with only active-app promoted to an architecture
chapter; orca stays E5 for now; the DESIGN docs move to docs/decisions/
translated. B1/B2 locked; A1/A2 (drift + language) unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The docs reconciliation (F6 drift-fix + F7 corpus-libro) covered
active-uix (morfo/soma/sema/eidos) but left the active-app side — the 22
arts/ runtime artifacts — as E5-in-place, un-reconciled. This plan does
for arts what F6+F7 did for the UIX layers, grounded in a 22-agent
per-art audit (every drift verified against the code): ~40 doc-vs-code
mismatches across 15 arts, 12 READMEs needing ES->EN, ethereal missing a
README, 3 historical DESIGN docs, dead route/demo cross-refs. Batches
A0 (guard) / A1 (drift sweep) / A2 (ES->EN) / A3 (ethereal README) / B1
(active-app -> architecture chapter, the missing analogue of
active-uix) / B2 (DESIGN docs -> decisions/) / B3 (index+TOC). Three
structural open questions for the user (arts E5 vs book; orca chapter;
DESIGN docs move) mirror F7's corpus-shape decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Fable-built Knob lived entirely under `src/uix/active-uix/knob/` — the
wrong home for a multi-layer component — with guessed APIs (103 type errors),
missing sub-part wrappers, junk `mnt/user-data/outputs/` paths, and its angle
math inlined against a fabricated `Gesture.base` surface. Dismember it into the
canonical per-layer directories, against the real APIs, consuming the real
`Gesture.rotate` (STUMBLES S4):
- **morfo** → `morfo/components/knob.ts`. Applies the S3 `part-absent` condition
to `aria-label` (emit only when no Label part) — the file's own NOTE asked
for it once the condition existed.
- **langs** → `langs/components/knob.ts` (+ registered in the langs index).
- **sema** → `sema/components/knob.ts` (+ registered in the sema index); valid
nominal earcons + haptic kinds, `channels` declared, risk-commit composes
(`op: 'add'`) over the intent profile per doctrine.
- **soma** → `soma/components/knob/`: provider (rewired to `Gesture.rotate` in
the constructor — mapping `angleAbsolute` onto the [startAngle,endAngle]
sweep, folding [0,360) into a symmetric range), `state<T>()` for the label id,
the missing `KnobIndicatorProvider`, root + five sub-part wrappers, exports,
index, and a provider test (ARIA slider contract + gesture wiring + keyboard
stepping/commit + disabled guard + dblclick reset).
- **eidos** → `eidos/components/knob/`: recipe css, size-aware root wrapper
(`ActiveEidos.resolve`), five pass-through wrappers, types, namespace index.
The old `active-uix/knob/` (untracked, added mid-session) is removed. Knob is
added to `recipe-css-contract`'s WIP_TRACKS until its recipe tokens land (css
still hardcodes md sizes, no variants). `npm run check`: 0 errors in every knob
file (total back to the 59 pre-existing baseline, from 162). Tests: 4 knob
provider + no regressions across eidos/sema/langs (174).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ran the full 65-test port suite on WebKit + Firefox (UIX_CROSS_BROWSER=1),
not just chromium. That caught one real robustness gap: Firefox stores
scrollTop as a subpixel value (~59.93 for a 60px scroll), so ScrollState's
exact-equality assertion is relaxed to toBeCloseTo — the port faithfully
passes the browser's own scrollTop through.
New coverage:
- tabbable.ts WRAPPER (getTabbableCandidates / getTabbableEdges /
getTabbableFrom / getTabbableFromFocusable) — the surface soma's
focus-scope actually consumes, previously untested.
- resource: AbortController cancellation + onCleanup on rapid source change
(aborted request must not clobber state with an AbortError).
- FiniteStateMachine: from/to/event/args lifecycle metadata, send-arg
forwarding, and no-action-for-event (no transition).
All green on chromium + webkit + firefox. (Repo check baseline is ~214 due
to the unrelated in-progress Knob component; 0 errors in these files.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fourth gesture specialization alongside base/drag/resize, for radial controls
(Knob, and future AngleSlider / colour wheel / rotary time picker). The Knob
built by Fable (active-uix/knob) currently does its angle math inline against a
guessed `Gesture.base` API and flags the gap — this is the primitive it needs.
`GestureRotate extends GestureBase`, mirroring the existing peers (protected
onMove/onRelease/onCancelled overrides + its own opts callbacks):
- `angleAbsolute` — pointer angle around a centre, 0° = up (12 o'clock),
clockwise positive, [0, 360). Matches CSS `rotate()`, so it pipes straight
into a `--rotation` var (the convention the real Knob consumer uses).
- `angleDelta` — signed rotation since start, unwrapped across the 0/360 seam
(355°→5° is +10°, not −350°); multi-turn friendly.
- `angularVelocityDeg` — deg/ms via a dedicated VelocityTracker fed the
accumulated angle, so it reads d(angle)/dt (radius-independent).
- Detents (`AngularSnap`: fraction / degrees / '45deg' / '50%'), wrap-aware
distance, dismiss by accumulated degrees, `center: 'element' | {x,y}`
(captured once at pointerdown — no per-move layout read).
Deviations from the scoped plan, all deliberate: angle convention taken from
the real Knob consumer (`atan2(dx,-dy)`, not the plan's `atan2(dy,dx)`);
`referenceSize` omitted (no meaning for a radius-independent angle);
CW/CCW direction locking deferred to v1 as the plan itself noted.
Two release-time snap bugs (found by adversarial review) fixed before landing:
- angular velocity projection now scales for the deg/ms domain and caps below a
half-turn, instead of reusing the px-scaled `VelocityTracker.project` (a
1 deg/ms flick would otherwise project 400° and cross a full turn);
- the sequential-detent guard measures neighbour adjacency modulo the detent
count under `wrap`, so the first/last detents are treated as physically
adjacent (the linear `|i-current|>1` from drag broke across the seam).
Tests: 9 new cases (angle convention on all cardinals, seam unwrap, onRotate,
detent resolution, dismiss by degrees + no-dismiss under threshold,
single-pointer, cancel). `npm run check` = 0 errors in the gesture layer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The emit contract explains discrete events (one gesture → one emit → one
hold) but says nothing about direct-manipulation surfaces (Slider, Drawer,
future knob/swipe) where the pointer moves at sample rate while the value
updates continuously. Add a `## Continuous components (drag, swipe, hold)`
section after the emit contract, grounded entirely in the real Slider/Drawer
code (adversarially fact-checked, zero discrepancies):
- One door: `runtime.trigger()` is the sole path (there is no `emitEvent`).
Its Promise represents the whole occurrence incl. the visual hold — so
`void trigger()` is fire-and-forget (correct for high-frequency emits) and
`await trigger()` blocks until the hold ends (correct when a structural
change must observe the resolved signal, e.g. `close`).
- `coincident` vs `post` for a moving value: the move (handle-drag,
drag-progress) is `coincident` (signal + mutation indivisible); the commit
(commit-set) is `post` (handler settles, then the signal celebrates).
Slider is the worked example.
- Throttle continuous POINTER emits, not keyboard: pointer emits are coalesced
to animation frames + floored to a component-tuned interval via ActiveDom's
requestFrame (Slider DRAG_SIGNAL_MS, Drawer DRAG_PROGRESS_SIGNAL_MS);
keyboard commit-set fires once per keydown (already human-paced). Tuning ms
point to the code, never hard-coded here.
- The per-emit `overrides` payload owns pitch/gain/contour; a continuous
event's cascade rule sets only `channels` and MUST NOT override those
primitives, or it would clobber the live payload every frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A line-by-line audit of the ports against runed's original source found two
faithfulness regressions in unused F3 utilities:
- IsIdle seeded `lastActive` to 0 (epoch) instead of the construction
timestamp, so `Date.now() - lastActive` read ~56 years until first activity.
Now `$state(Date.now())`, matching runed.
- IsInViewport passed `threshold: undefined`, which the IntersectionObserver
spec coerces to 0 (fires at a single pixel). runed defaults it to 0.1;
restored (and rootMargin -> '0px').
Guarded IsIdle's baseline in its test. check=59.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
65 tests in real chromium covering every ported utility, so the removed
runed + tabbable deps can be relied on with confidence:
- tabbable-core: order, tabindex, disabled, real display/visibility, inert,
disabled fieldset, radio groups, contenteditable, details, shadow DOM.
- $libs/reactive runes: watch / watchOnce / watch.pre, Context, Previous,
Debounced/debounce, Throttled/throttle, interval, StateHistory,
FiniteStateMachine, resource, IsMounted, onCleanup.
- $adom DOM runes: ElementSize, ElementRect, ActiveElement, IsFocusWithin,
IsIdle, IsDocumentVisible, IsInViewport, onClickOutside, PressedKeys,
ScrollState, TextareaAutosize, AnimationFrames.
Patterns: $effect.root + flushSync for runes, vi.useFakeTimers for
debounce/throttle/interval/idle, vi.waitFor for real observers/rAF, and
rendered fixtures (context-probe, scroll-state-probe) for the two utilities
that need a component context. `npm run check` = 59 (baseline).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the runed + tabbable removal (F1+F2 in d0f41fb2). Ports runed's 11
DOM-reactive utilities into $adom, window-correct by resolving the window from
the node (getWindow) or an optional window/document — not runed's defaultWindow:
AnimationFrames, ActiveElement (+ activeElement singleton), IsDocumentVisible,
IsFocusWithin, IsInViewport, onClickOutside, PressedKeys, ScrollState,
TextareaAutosize, ElementRect, IsIdle.
Follows the committed ElementSize precedent (raw observers/listeners live in the
arts/adom implementation layer) rather than threading a `dom` param.
PersistedState is intentionally NOT ported: the $storage art already supersedes
it (its README names runed/PersistedState as the thing it replaces) — same call
as boolAttr being covered by boolToEmptyStrOrUndef.
`npm run check` = 59 (baseline, 0 in ported files); server tests unchanged
(pre-existing failures only). The runed + tabbable port is now complete.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>