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 }
266 Commits (0ca0f01f115775af18eb3aa25e68c4486d75a0fb)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
e69e0cba80 |
sema: namespace signal attrs under data-event-* (channel never touches state)
Supersedes the save/restore patch (
|
5 months ago |
|
|
b66039adb1 |
sema: VisualChannel preserves structural data-intent on cleanup
The channel writes `data-intent` from `signal.intent` during the hold,
but `data-intent` is also the structural attr the morfo of components
like Toast.Item writes from `v.propRef('intent')`. Removing it on
cleanup erased the structural value — the toast lost its tinting after
the announce signal expired.
Fix: save the prior `data-intent` value before overwrite, restore it on
cleanup. General principle — a channel should clean only what it wrote.
Other signal attrs (`data-event*`, `data-event-family`) are signal-
specific so no collision.
Tests: structural data-intent survives the hold; mismatched signal vs
structural intent (edge case) restores the structural value.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
4f0108f839 |
eidos: V1.5 — Toast component recipe
First per-component recipe in eidos/components/. Composes tokens + archetypes + intent palette into a finished Toast visual: - viewport stack layout (no positioning — integrator's call) - card with intent-tinted left border (reuses --eidos-tint-*) - absolute close button so layout doesn't depend on a header wrapper - typography for title / description, secondary-button style for action - swipe state (data-swipe='move'/'cancel') translates the card - loading state adds an accent stripe via inset shadow Toast.Item carries archetype='item' which sets cursor:pointer + user- select:none in archetypes.css; the recipe overrides those because a notification is passive, not selectable. Possible future morfo decision: introduce a 'card' or 'notification' archetype. The eidos test page drops its :global([data-toast-*]) scoped CSS — the recipe now suffices. Stale '~13 ms / 1 rAF' text replaced with the correct per-family hold (600ms for alert). Wired via @import './components/toast.css' from eidos/index.css. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
3d9a834886 |
docs: align cross-layer docs with channel-based Sema
Cleanup of stale references to the pre-refactor Sema model across architectural docs: - EngineSemantic → SemanticEngine (registry + dispatch) + VisualChannel (built-in materializer); engine no longer "depends on Dom" - "1 rAF + 1 hold frame (~13ms)" → perceptually-anchored hold (240ms emerge/commit/handle, 600ms alert/sustain, 120ms contact) - Promise semantics: sequential strict — cleanup BEFORE resolve, so structural commit lands AFTER perceptual feedback completes - Toast cadence trace updated: prewrite folded into VisualChannel.handle; CSS uses @keyframes (animation), not transition Touches: src/uix/README.md, src/uix/active_architecture.md, src/uix/sema/README.md, src/uix/eidos/README.md, src/uix/morfo/README.md, src/uix/soma/SOMA_ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
b4ceb1bb09 |
docs: SOMA_ARCHITECTURE — update sema timing reference after channel refactor
The "data-event*" bullet still mentioned the old "~13ms" timing from before the perceptual anchoring fix and the channel refactor. Updated to reflect current behavior: VisualChannel writes the attrs for a configurable hold (240ms emerge/commit/handle, 600ms alert/sustain by default per family, configurable per signal or globally). No code changes — single doc fix. |
6 months ago |
|
|
5871cbca6e |
sema: refactor into channel modules — engine becomes registry + dispatch
Reorganización estructural según el spec acordado: cada canal perceptivo
vive como módulo simétrico dentro de `chans/`. El engine queda mínimo
(registry + dispatch); la lógica de escribir `data-event*` al DOM,
mantener hold y retirar pasa al `VisualChannel`. La API pública que ven
los providers (`semantic.emit(signal)`) se mantiene idéntica.
Estructura nueva
src/uix/sema/
├── engine.ts registry + dispatch (ya no conoce DOM ni hold)
├── signal.ts SemanticSignal + nuevo campo opcional `id?`
├── exports.ts barrel actualizado (chans + drop publish + drop perception)
└── chans/
├── types.ts interfaz Channel { id, handle, dispose? }
├── visual.ts VisualChannel — escribe data-event*, hold, cleanup
├── visual.test.ts 9 tests del canal aislado
├── sound.ts SoundChannel placeholder (V1 no-op)
└── vibra.ts VibraChannel placeholder (V1 no-op)
Cambios al engine
- Constructor: `new SemanticEngine(opts?)`. `opts.visual` controla el
built-in (false / VisualChannelOptions / Channel custom). `opts.dom`
desaparece — ya no es responsabilidad del engine.
- Métodos: `register(channel)`, `getChannel(id)`, `emit(signal)`,
`destroy()`. Nada más.
- emit despacha a TODOS los canales registrados:
- Canales no-visuales (sound, vibra, futuros): fire-and-forget. Errores
se loguean pero no propagan (Sema es ornamental).
- Canal visual: el único cuya Promise se awaitea — comparte plano DOM
con el commit estructural posterior.
- emit genera el `id` de la ocurrencia y lo pasa a todos los canales,
garantizando coherencia cross-canal para tooling futuro.
Cambios al VisualChannel (lógica heredada del emit() anterior)
- 5 atributos: `data-event` + `data-event-id` + `data-event-phase`
(siempre) + `data-event-family` y `data-intent` (opcionales si vienen
en el signal). Los opcionales son la proyección al DOM de la metadata
semántica que Eidos consume.
- Hold defaults internos por familia (no exportados):
emerge/commit/handle: 240ms · alert/sustain: 600ms · contact: 120ms
Justificación técnica (rangos típicos de CSS transitions), no escala
perceptiva universal. Override per signal vía `signal.hold` o global
vía `new SemanticEngine({ visual: { defaultHold } })`.
- Semántica secuencial estricta: cleanup ANTES del resolve.
Eliminado
- `src/uix/sema/perception.ts` — escalas perceptivas no son concepto
cross-canal. La tabla de defaults por familia se movió al VisualChannel
como detalle interno con justificación técnica.
- `engine.publish()` y todo el sistema legacy:
- `SemanticEventDecl`, `SemanticComponentContract`, `SemanticPublishContext`,
`PublishedSemanticEvent`, `SemanticEventFilter`
- `onEvent()` y subscribers
- `applyPrewrites()` interno
- dependencia opcional de `ActiveDom` por construcción
- Tests de `publish()` reemplazados por tests del nuevo registry/dispatch.
Bootstrap (App + defaults)
- `src/lib/ext/app/app.svelte.ts:52` — `new SemanticEngine({ dom: this.dom })`
→ `new SemanticEngine()`
- `src/lib/ext/app/defaults.ts:45` — mismo cambio
Tests
- emit.test.ts ajustado: sin createActiveDom, sin SEMA_PERCEPTION/HOLD_DEFAULTS,
test "throws without dom" eliminado, números literales (240, 600).
Añadido test "resolves immediately when visual: false". 7 tests.
- engine.test.ts reescrito: register / getChannel / emit dispatch a múltiples
canales / id propagation / id override / fire-and-forget para non-visual /
error en canal no aborta dispatch / visual: false desactiva built-in /
custom Channel reemplaza built-in / destroy. 13 tests.
- chans/visual.test.ts nuevo: 5 attrs vs 3 / signal.hold respetado /
family default fallback / global default fallback / constructor defaultHold /
cleanup / sequential strict (resolve tras cleanup) / id field. 9 tests.
- 39 tests sema verdes en total.
Documentación
- src/uix/sema/README.md — reescrito alrededor del modelo de canales
- src/uix/eidos/events.css — comentario actualizado, referencia a chans/visual.ts
- src/uix/active_architecture.md — sección Sema reescrita
Componentes NO requieren cambios — la API `semantic.emit(signal)` es
idéntica externamente. Toast / runtime.trigger / providers funcionan igual.
Verificación
- 121/121 tests focused suite (sema 39 + adom + lib/dom + morfo + soma/morfo)
- 66/66 morfo-check
- 68/68 smoke routes
- svelte-check: 155 errors (baseline, sin nuevos)
Pendientes en doc del spec marcados como "no-objetivos":
- Implementación real de SoundChannel / VibraChannel (placeholders V1)
- Arbitraje propio del VisualChannel (deuda heredada del emit anterior)
- Cancelación si el target se desconecta durante el hold
|
6 months ago |
|
|
e664b59c96 |
sema: perceptually-anchored hold + sequential strict emit semantics
Two architectural fixes surfaced by the eidos demo:
1. The previous default hold (1 rAF + 1 hold frame, ~30ms) was a
technical timing decision — invisible to humans. Signals must be
anchored to perceptual scales so they can actually be perceived.
2. The previous lifecycle resolved emit AFTER 1 rAF (caller's structural
change ran in parallel with cleanup, ~30ms later). That's an implicit
parallelism between event and state. The author has no way to say
"wait for the announcement to be perceived before changing state".
Sequential strict semantics (NEW)
- emit writes the signal, awaits the full author-specified hold, cleans
up, then resolves. State change happens AFTER cleanup — strictly
sequential.
- `signal.hold` is in milliseconds anchored to `SEMA_PERCEPTION` scales:
subliminal 50ms below conscious threshold
glimpse 120ms minimum perceptible without effort
brief 240ms comfortable acknowledgement
noticed 600ms sustained signal
insistent 1200ms demands attention
persistent 3000ms until acknowledged
- Defaults per family in `SEMA_HOLD_DEFAULTS`:
emerge: brief sustain: noticed
contact: glimpse commit: brief
alert: noticed handle: brief
- Authors override per event via `SemanticSignal.hold` based on:
- their CSS animation duration (so it completes before cleanup)
- the perceptual scale they want
- critical attention requirements (use insistent/persistent)
Files
- src/uix/sema/perception.ts (new) — SEMA_PERCEPTION + SEMA_HOLD_DEFAULTS
+ SEMA_OBSERVE_FRAME_MS constants with rationale anchored on Bloch's
law, Material/Apple HIG motion guidelines, toast UX literature.
- src/uix/sema/signal.ts — `hold` JSDoc updated to explain ms semantics +
defaults + override pattern.
- src/uix/sema/engine.ts — emit() refactored from rAF chain to
setTimeout(holdMs); cleanup happens BEFORE resolve (sequential strict);
`nextFrame()` helper removed; `resolveHoldMs(signal)` added with the
3-tier fallback (signal.hold → family default → SEMA_PERCEPTION.brief).
- src/uix/sema/exports.ts — re-exports SEMA_PERCEPTION + SEMA_HOLD_DEFAULTS
+ SEMA_OBSERVE_FRAME_MS + SemaPerception type.
- src/uix/sema/emit.test.ts — rewritten around fakeTimers + setTimeout
(no more rAF queue); new test verifies cleanup runs BEFORE resolve.
7/7 tests green.
- src/uix/sema/README.md — new "Duración del signal" section with the
full perception scale + per-family defaults tables + override pattern.
- src/uix/eidos/events.css — comment updated to explain the new hold
windows; rule `[data-event^='dismiss']` etc. clarified.
- src/uix/active_architecture.md — replaced "~13ms" prose with explicit
perception-anchored scales; added explanation of "perceptual by design,
not framerate".
- src/uix/soma/components/toast/toast-provider.svelte.ts — new $effect
that fires `present` + `announce` events when item ref attaches
(one-shot guarded), so toasts emit signals on appear. The flash on
/test/eidos is now visible thanks to (a) the perceptual hold and (b)
the wired events.
Channel parallelism note
- Hold is the DOM channel duration only.
- Future sound / vibra / motion engines subscribe to sema events
independently; each owns its own timing relative to the event id.
- Sema does not coordinate cross-channel sync — that's an integrator
decision per app.
Verification
- 22/22 sema tests green.
- 22/22 morfo runtime tests green.
- 66/66 morfo-check + 68/68 smoke (verified before commit).
- Probe shows announce signal on toast appear lasting ~600ms (alert
family default) — visible in DevTools attribute panel.
- svelte-check unchanged at 155.
Pre-existing failure in src/uix/air/internal/canvas/layout.test.ts
(`bun:test` import) is in the dead-branch air layer and unrelated.
|
6 months ago |
|
|
44a499d052 |
eidos: V1 minimum viable — tokens + archetype rules + event reactions
First eidos code. Closes the cross-layer cycle that's been documented but
unproven: an actual CSS layer that consumes the morfo + sema contracts
and applies styles transversally without enumerating components.
src/uix/eidos/
- tokens.css — motion durations (quick/fast/base) + easings (standard /
emphasized / decelerate) + opacity scale (hover/disabled/faded) +
intent tints (neutral/affirm/fulfill/risk/threat) + focus ring tokens.
~30 lines. No theming dimension yet — when it arrives, these are the
base theme.
- archetypes.css — transversal selectors keyed by `data-archetype`.
Covers trigger, overlay, content, indicator, thumb, track, close,
action, item, option. Plus a universal `:focus-visible` ring rule that
applies to every archetype-classified element. Single attribute
selector specificity — components can override freely.
- events.css — reactions to perceptual signals during their ~13ms life.
`[data-event^="dismiss"]` fades out, `[data-event^="announce"]` pulses
with intent-tinted box-shadow, `[data-last-action]` tints exit
animation by causa.
- index.css — entrypoint that imports the above in cascade order.
Consumer: `import '$uix/eidos/index.css'`.
src/routes/test/eidos/+page.svelte (demo)
- Sets up Soma + App context inline (independent of /test/soma layout).
- Imports `$uix/eidos/index.css` and demonstrates the cross-layer story:
- Toggle + Switch buttons share trigger styles via `[data-archetype="trigger"]`
without per-component CSS.
- Collapsible expands/collapses with state-driven attrs.
- Toast intent buttons fire `data-event="announce" data-intent="risk|threat|affirm"`,
which eidos's events.css briefly tints via box-shadow during the signal window.
- Component-specific layout (positioning, switch thumb transform) lives in
the page's `<style>` block — eidos provides only the cross-component
baseline, NOT per-component recipes.
What this proves
- The runtime emits `data-archetype` correctly in browser (not just in
unit tests).
- A single CSS rule can target ALL triggers across all components
without enumeration. Add a new component with `archetype: 'trigger'`
and it inherits the styling automatically.
- Sema's `data-event*` signals live in the DOM long enough for CSS to
react (the ~13ms window measured by `scripts/probe-toast-dismiss.ts`
is observable as a brief box-shadow flash on toasts).
- The cross-layer cycle Morfo declara → Soma transcribe → Sema emite →
Dom aplica → Eidos lee runs end-to-end.
What V1 deliberately excludes
- Theming dimension (no `[data-theme]` overrides yet).
- Per-component recipes (each component will get its own CSS later).
- Keyframes / @starting-style / animation primitives.
- Sound / vibra (those are separate engines that subscribe to sema).
Verification
- /test/eidos returns 200; manual browser check shows triggers share
hover dim + focus ring across components, toasts flash intent tint on
appear.
- 68/68 smoke routes (smoke only scans /test/soma — eidos route is
outside that scope, manually verified).
- svelte-check unchanged at 155 (pure CSS additions).
|
6 months ago |
|
|
760575c419 |
docs: src/uix/active_architecture.md — cross-layer vision document
Synthesizes the four-layer architecture (morfo · soma · sema · eidos) into
a single readable doc. Where the per-layer READMEs are operational
references, this is the "north star" for understanding the why and how
the pieces articulate.
Structure (14 sections):
1. The thesis in one line — four layers with explicit contracts
2. The problem solved — what conventional frameworks mix together
3. The four layers — Morfo (DNA), Soma (behavior), Sema (semantic
channel), Eidos (visual)
4. How they articulate — the transcription chain (six pieces, one
responsibility each)
5. The causal chain of an interaction — concrete walkthrough of a Toast
dismiss with timing
6. The primitives that pass between layers — DOM attrs catalog +
archetypes + verbs vocabularies
7. Hard rules — the 12 invariants that keep the system coherent,
including the 2-of-3 rule
8. The autoría / transcripción distinction — the lens for deciding what
lives where
9. What this architecture is NOT — explicit non-goals
10. Current state (2026-04-26) — what's built, designed, pending
11. Acknowledged risks — over-layering, name discipline, responsibility
invasion, lack of precedent
12. Why it may be worth it — explainability, less drift, validation,
extensibility
13. The summary phrase — "Morfo declara · MorfoRuntime transcribe ·
Provider aporta · Effects sincronizan · Semantic emite · Dom aplica
· Eidos lee"
14. For deeper reading — links to per-layer docs
Also adds a top-of-page pointer in src/uix/README.md so the vision doc is
the obvious entry point for new readers.
No code changes — pure documentation.
|
6 months ago |
|
|
28f93ab089 |
docs: cross-layer articulation — eidos README + 2-of-3 rule across all layer docs
Closes the documentation loop on the cross-layer extension pass: morfo
now articulates between soma, sema, and (future) eidos. The "2-of-3 rule"
formalizes when an extension to morfo is justified vs when it should
stay as provider logic.
src/uix/eidos/README.md (new)
- Documents eidos's role and what it consumes from morfo + sema BEFORE
any code exists, so the contract is preparedly clean when implementation
starts.
- Catalogs which morfo fields eidos reads (parts, archetype, states,
data values, events, prewrite, focus, supportsNesting) and which it
ignores (computed state, runtime internals, layers).
- Documents the DOM-as-channel pattern: sema writes data-event* on emit;
eidos reacts to selectors like `[data-event^="dismiss"]`.
- Establishes the boundary with `air` (dead branch reference, not base).
src/uix/README.md (top-level)
- §8 Reglas de dependencia: adds the 2-of-3 rule table making the
morfo-extension contract explicit, plus a list of canonical vocabularies
(archetypes, verbs).
- §10 Reading order: includes eidos README + lib/dom + clarifies which
layers are dead branches.
src/uix/morfo/README.md
- New "Archetypes" section documenting the 24-verb vocabulary, the
Provider-as-trigger vs Provider-as-container distinction, and the rule
for adding new archetypes (≥2 components share the role).
- New "The 2-of-3 rule" section with the same table as the top-level,
listing which extensions did/didn't make it past the rule and why.
- `parts[].archetype` mentioned in the "What morfo contains" list.
src/uix/sema/README.md
- New "Vocabulario canónico de verbs" section listing SEMA_VERBS by
family and the `{verb}-{variant}` composite naming convention.
- Documents `validateEventName()` as advisory tooling.
src/uix/soma/SOMA_ARCHITECTURE.md
- partProps documentation now mentions data-archetype emission.
- New "Cross-layer hooks que soma emite por la regla 2-de-3" section
listing the data-* attrs soma writes that sema and eidos consume.
- Reading-order links updated.
No code changes — all docs.
|
6 months ago |
|
|
2e062f216b |
sema: canonical action verb vocabulary (SEMA_VERBS)
Cross-component verb names that morfo `events[].name` should preferably
align with. Lets sema / sound / vibra engines subscribe by verb instead
of by component-specific event name, and lets eidos write transversal
selectors like `[data-event^="dismiss"]`.
Vocabulary (24 verbs, grouped by family alignment)
- emerge: present, dismiss, open, close, expand, collapse
- commit: commit, cancel, confirm, submit, reset, fail
- alert: announce, alert
- contact: activate, select, toggle
- handle: acknowledge, edit, drag, resize
- sustain: tick, progress
Composite event names follow `{verb}-{variant}` (e.g. `commit-save`,
`dismiss-outside`). The verb is the head; the rest is component-specific
nuance the validator extracts and reports separately.
`validateEventName(name)` returns `{ name, head, matchesCanonical, variant }`.
Advisory — doesn't reject morfos, just surfaces drift for tooling /
review (a future `sema:vocabulary` script can use it to flag composite
event names whose head isn't canonical).
Existing event names already align well
- Toast: `present`, `announce`, `dismiss` — all canonical bare verbs.
- Dialog: `open`, `close-save`, `close-cancel`, `close-dismiss`,
`close-dismiss-outside`, `close-after-fail` — all canonical heads
(`open`, `close`) with variants.
Tests: 10 unit tests covering vocabulary integrity, isSemaVerb guard,
validateEventName splitting/matching, and a snapshot of current dialog +
toast event names against the catalog (all match).
This is the second extension justified under the "2-of-3 rule": verbs
are consumed by sema (subscription routing) AND eidos (selector
generation), with the original morfo authoring as the third producer.
|
6 months ago |
|
|
aba18117ba |
morfo: catalog `archetype` across 65 components (~265 part declarations)
Bulk classification pass that gives every part a cross-component archetype where one applies. Done with `scripts/catalog-archetypes.ts` — a one-shot mechanical mapping based on a kebab → archetype table, plus three manual overrides for components where Provider IS the interactive element. Coverage - 440 part declarations examined. - ~265 received an archetype (provider, trigger, content, overlay, viewport, item, indicator, thumb, track, label, title, description, close, action, header, image, fallback, arrow, separator, group, input, segment). - ~175 left without one — genuinely component-specific (Slider.Range, PinInput.Segment internals, color-picker channel-slider, etc.). Manual overrides (Provider IS the trigger) - Toggle, Switch, Checkbox: their `Provider` part is a `<button>` that the user actually interacts with, not a root container. Archetype set to `'trigger'` instead of the default `'provider'` so eidos can style them alongside Dialog.Trigger / Popover.Trigger / etc. Script (scripts/catalog-archetypes.ts) - Walks every `*.ts` under `src/uix/morfo/components/`. - Inserts `archetype: '...'` directly after the matching `kebab:` line when a mapping exists. - Idempotent — skips parts that already declare an archetype. - Indent-aware — only operates on part kebabs (≥2 tabs), never the morfo root kebab. Bug discovered + fixed mid-pass when radio-group's root kebab `'radio-group'` matched the `'radio-group' → 'group'` alias and inserted at the wrong level (cleaned up by hand). - Reports per-file counts + the list of unmapped kebabs at the end so future passes can extend the mapping. Tests - Updated `omits data-archetype when not declared` test to use a tiny inline morfo (Toggle's Provider now has archetype 'trigger', so it can't be the "no archetype" fixture anymore). Verification - 22/22 runtime tests, full focused suite green. - 66/66 morfo-check (every component still validates against its DOM — archetype is additive, doesn't change emitted attrs apart from the new `data-archetype="..."` which morfo-check doesn't enforce yet). - 68/68 smoke routes. - svelte-check unchanged at 155. |
6 months ago |
|
|
bf394873e0 |
morfo: add `archetype` field — cross-layer part categorization
First step toward making morfo articulate the three layers (soma, sema,
eidos) instead of just serving soma. Archetype is a small canonical
vocabulary that lets a part declare its conceptual role beyond its
component-specific kebab name.
Articulation purpose:
- Eidos can style transversally — `[data-archetype=trigger] { ... }`
applies to every Trigger across all 60+ components without enumeration.
- Sema can map archetypes to canonical action verbs (`trigger` may fire
`activate`, `item` may fire `select`).
- Docs can categorize parts cross-component for discovery.
The "2-of-3 rule" justifies the extension: at least two layers consume it.
Vocabulary
24 archetypes covering the common cross-component roles:
provider, trigger, content, overlay, viewport, item, option, indicator,
thumb, track, label, title, description, close, action, header, image,
fallback, arrow, separator, group, input, segment, preview.
Designed to stay small. New archetype only added when at least two
existing components share the role. Genuinely-unique parts (Slider.Range,
PinInput.Segment) omit the field.
Implementation
- `MorfoPart.archetype?: MorfoArchetype` — optional union type.
- `ARCHETYPE_VOCABULARY` exported as `as const satisfies` array for
enumeration tooling.
- `partShallowSchema` (sium validator) gets `archetype: optional(union(...))`.
- MorfoRuntime caches `partMeta` in PartRegistration so `partProps` can
read `archetype` without re-walking the morfo tree per render.
- `runtime.partProps(part)` emits `data-archetype="..."` when declared.
Static identity (never mutates), so it ships through partProps not
through `dom.apply` — Svelte renders it once on first paint.
Tests
2 new tests:
- partProps emits data-archetype when morfo declares one (inline test
morfo to avoid coupling to catalog state).
- partProps omits data-archetype when not declared (Toggle, pre-catalog).
Verification
- 22/22 runtime tests green.
- svelte-check unchanged at 155.
- No morfo currently declares archetype yet — catalog pass follows in the
next commit. All existing morfos remain valid (field is optional).
|
6 months ago |
|
|
e7e32a8355 |
soma: Avatar migrated to runtime + V2 props bridge
First component of the bulk migration phase (step 2 of the 1-3-2 plan).
Pattern is now well-rehearsed across Toggle / Collapsible / Toast / Switch:
- morfo: add `value` sources to data entries that were enum-only
- provider: replace manual Opts interface with `OptsFromProps<...>`,
create runtime in constructor, register parts, drop manual data-* /
aria attrs from props (runtime emits via dom.apply)
- wrapper: replace per-prop `readableActive(() => x)` list with one
`bindProps({ ... })` call
- keep manual: HTML primitives (style, src, etc.) and event handlers
Avatar specifics
- 3 parts (provider/image/fallback) all share `data-status` from the root's
internal `status` State (idle/loading/loaded/error). morfo's `propRef('status')`
reads from runtime sources where the root provider exposes it.
- `status` is not a public Props field — it's an internal $state in the
wrapper. Manually intersected into `AvatarOpts` rather than derived from
AvatarProps.
- Image's preload logic, src/crossorigin/referrerpolicy passthrough, and
the style-based show/hide on each part stay in the provider's render —
none of these are morfo-managed.
- Wrapper cast uses `as unknown as Parameters<typeof X.create>[0]` for
parts whose Opts have only `id`+`ref` (TS can't extract the specific
fields from the generic `Record<string, Active<unknown>>` return of
`bindProps`). Toggle/Switch don't need this because their richer Opts
shape provides enough overlap.
Bulk migration paused at this point — Progress / Meter need a design
decision around `aria-label` priority logic (the legacy provider has
"consumer-passed wins over translated default", which conflicts with the
"dom.apply is the only writer of mutable attrs" rule). Either:
(a) Drop aria-label from those morfos and keep provider-managed (legacy
pattern preserved, morfo loses one declaration).
(b) Extend the resolver with a priority chain (propRef → translationRef
fallback) so the morfo can express "consumer wins, translation
otherwise".
Verification
- 66/66 morfo-check (Avatar passes; all data-status emitted by runtime).
- 68/68 smoke routes.
- svelte-check unchanged at 155.
|
6 months ago |
|
|
55a5ad2647 |
soma: runtime.keydown() + Switch keyboard pilot
V1.5 of MorfoRuntime — adds the keyboard dispatch piece and validates it
against Switch (the simplest component with a real `morfo.keyboard` block).
src/uix/soma/morfo/runtime.svelte.ts
- New `runtime.keydown(part, event): boolean` walks `partMeta.keyboard`,
matches `event.key` plus modifier flags against each entry's spec
("Enter", " ", "Shift+Tab", "Ctrl+A", …), and dispatches to the matching
handler from `sources.actions[entry.action]`. Returns whether a handler ran.
- New `sources.actions: Record<string, KeyboardActionHandler>` source map.
Handlers receive the raw `KeyboardEvent` so they can `preventDefault()`
selectively. The runtime never calls preventDefault on its own.
- Modifier matching is exact: `Shift+Tab` does NOT match plain Tab; `Tab`
does NOT match Shift+Tab. Each entry's `condition` (`prop-truthy` etc.)
is also honored.
- Key-spec parsing keeps the literal key untrimmed (the spacebar is `" "`,
trimming would collapse it to empty). Modifier segments are trimmed
individually.
Toast pilot — keyboard wiring NOT included
Toast Item declares `{ key: 'Escape', action: 'close' }` in morfo, but the
legacy Toast doesn't actually wire Escape either. Out of scope for this
commit; a follow-up can add `events.close` + the keydown listener on the
Item element.
Switch pilot — first consumer
- `morfo/components/switch.ts`: added `value` sources to `data-state` /
`data-disabled` (same pattern Toggle/Collapsible/Toast needed for the
runtime to manage them).
- `soma/components/switch/switch-provider.svelte.ts`:
- Manual `interface SwitchOpts` (10 lines) replaced with `OptsFromProps<
SwitchProps, ManagedKeys, 'checked'>` (5 lines).
- The custom `onkeydown` block (`if (e.key === KEYS.ENTER) preventDefault;
toggle()`) collapses to `(e) => this.runtime.keydown('provider', e)`.
The morfo's `Enter` and ` ` (spacebar) both dispatch to action `'toggle'`,
handled by `actions.toggle: (e) => { e.preventDefault(); this.toggle() }`.
- props derivation uses `runtime.partProps('provider')` for identity,
keeping only the HTML `disabled` attr and event handlers manual.
- `soma/components/switch/components/switch.svelte`: 8 lines of
`readableActive(() => x)` collapsed to a single `bindProps({ ... })` call
(the V2 helper from commit
|
6 months ago |
|
|
eb0f128c3e |
soma: V2 props bridge (OptsFromProps + bindProps) — Toggle refactor
Eliminates the triplication that every component used to suffer:
1. Props (autoral, in types.ts) — public consumer API
2. Opts (mechanical) — provider's reactive contract
3. wrapper's `readableActive(() => x)` list (mechanical)
After V2 only (1) is hand-written. (2) is derived; (3) collapses to one
`bindProps({ ... })` call.
src/uix/soma/reactive/opts.ts (new)
- `OptsFromProps<P, Managed, StateKey>` type maps a Props interface to the
matching Opts: `id` + `ref` baked in; `Managed` selects which keys reach
the provider (HTML passthroughs stay out); `StateKey` is the bindable
subset (the rest become `Active`). `Exclude<P[K], undefined>` strips the
optionality the wrapper resolves via Svelte destructure defaults.
- `bindProps(config)` runtime helper: each entry is either a bare getter
(→ `readableActive`) or a `{ get, set }` object (→ `writableActive`).
Detection is structural (no marker function — avoids collision with
Svelte's `$bindable` rune in `<script>` blocks).
- `WritableSpec<T>` defaults to `unknown` for storage but
`PropsConfigEntry<T>` defaults to `any` for variance — heterogeneous bag
literals are bivariant under `any`, contravariant under `unknown`.
Toggle migration as the proof
- `morfo/components/toggle.ts`: rename `propRef('ariaLabel')` → `propRef('aria-label')`
so the morfo and the public Props key match. Bracket notation in the
provider handles the kebab-cased key.
- `soma/components/toggle/toggle-provider.svelte.ts`:
- `interface ToggleOpts { ... }` (15 lines hand-rolled) replaced by
`type ToggleOpts = OptsFromProps<ToggleProps, ManagedKeys, 'pressed'>` (10 lines)
- sources read `opts['aria-label'].current`
- `soma/components/toggle/components/toggle.svelte`:
- 11 lines of `name: readableActive(() => name)` collapsed to a single
`bindProps({ ... })` call where each line is just `name: () => name` or
`pressed: { get: () => pressed, set: (v) => (pressed = v) }`
Tests
- `opts.svelte.test.ts` — 4 unit tests verifying readable/writable
detection, mixed configs, and kebab-keyed entries (`'aria-label'`).
Verification
- 111/111 soma tests green.
- 66/66 morfo-check (Toggle's emitted DOM unchanged).
- 68/68 smoke routes.
- svelte-check unchanged at 155 (no new errors).
Recipe for migrating other components:
1. List `Managed` keys (everything in Props that isn't HTML passthrough).
2. List `StateKey` subset (anything bindable in the wrapper).
3. Replace the manual Opts interface with `OptsFromProps<...>`.
4. Wrap the wrapper's prop forwarding in `bindProps({ ... })`.
|
6 months ago |
|
|
6d805a4529 |
gitignore: ignore .kilo/ (Kilo IDE plugin scratch dir)
|
6 months ago |
|
|
ddde4601b0 |
scripts: probe-toast-dismiss — verify the runtime.trigger contract end-to-end
One-shot Playwright probe that drives the live Toast demo, installs a
MutationObserver on the toast item, clicks dismiss, and reports the full
attribute timeline.
Used to confirm that `runtime.trigger('dismiss')` actually fires the
`data-event*` perceptual signal in the browser. The signal lives ~13ms
in the DOM (1 awaited rAF + 1 hold frame + cleanup), which is too brief
for DevTools' attribute panel to refresh — this probe captures the full
sequence with millisecond timestamps so the contract is observable.
Run:
1. `npm run dev`
2. `node --import tsx/esm scripts/probe-toast-dismiss.ts`
Output ends with "Signal observed: ✅ YES" when the runtime is wired
correctly. Useful as a regression probe after touching the trigger
sequence or the Toast wiring.
|
6 months ago |
|
|
14aa218658 |
soma: Toast pilot — multi-instance runtime + trigger() integration
Commit B of the Toast pilot. The runtime.trigger() contract from commit A
now has its first real consumer: all three dismiss paths route through
`runtime.trigger('dismiss')`, which emits the perceptual signal one frame
before the toaster mutates state.
Multi-instance architecture validated
Toast is the first component with multiple per-instance subtrees: each
toast in the queue is its own scope with its own children. Two runtime
instances coexist per Toast tree:
ToastProvider (root)
└── runtime instance A — registers `viewport` only
ToastItemProvider (one per active toast in queue)
└── runtime instance B — registers `item` + its children
(`title`, `description`, `action`, `close`)
`partRef('item')` resolves to THIS toast's element because the lookup
happens on THIS Item's runtime, not a shared one. `runtime.trigger('dismiss')`
on Item-B targets Item-B's DOM element. Per-toast isolation by construction.
dismiss wired through runtime.trigger
Three call sites now go through the same path instead of calling
`toaster.dismiss(id)` directly:
- ToastCloseProvider.onclick (user clicks the × button)
- ToastItemProvider startTimer setTimeout callback (auto-dismiss)
- ToastItemProvider onpointerup (swipe gesture meets threshold)
The runtime fires `data-event="dismiss"` on the item, awaits one rAF for
CSS to observe it, then calls the dismiss handler that mutates
`toast.dismissing = true`. The item's effect re-derives `data-state="closed"`
from `isOpen = !dismissing` and writes it via dom.apply on the next tick.
intent-driven role / aria-live still works
The morfo's `mapRef(propRef('intent'), { neutral: 'status', risk: 'alert', … })`
for `role` and `aria-live` is resolved by the runtime per-Item: each toast
reads its own `intent` from `opts.toast.current.intent` via the per-Item
runtime sources. Risk/threat toasts get `role=alert` + `aria-live=assertive`;
the others get `role=status` + `aria-live=polite`. No code in the provider
selects this — the morfo declares it, the resolver computes it.
Per-part props
Action's `altText` is per-instance per-action — passed to the runtime via
`registerPart('action', { props: { altText: () => opts.altText.current } })`.
Validates that the runtime's per-part source override (introduced in
commit
|
6 months ago |
|
|
2b39a2423d |
soma + app: runtime.trigger() contract + Sema wired into App/Soma
Commit A of the Toast pilot: introduce the trigger() contract end-to-end
without migrating any provider yet. Toast's actual migration follows in a
separate commit so that bugs (if any) can be attributed to one layer at a
time.
App layer
- New `AppSemantic` contract in `$lib/ext/app/types`: a single-method
interface (`emit(signal): Promise<void>`) implemented by SemanticEngine
and accepted by the App constructor.
- App now owns `app.semantic`, defaulting to a fresh SemanticEngine wired
to `app.dom` when the consumer doesn't pass one. Fallback App also gets
`fallbackSemantic` so context-less calls don't throw.
- Soma re-exposes it as `soma.semantic` so providers reach it the same
way they reach `soma.dom` / `soma.langs`.
MorfoRuntime
- New `runtime.trigger(eventName): Promise<void>` with the full sequence
the architecture closed:
1. apply `prewrite` attrs imperatively (transient markers)
2. await `semantic.emit({ target, name, family, intent? })` — resolves
the intent from `event.semantic.intent` (literal SemaIntent or
SemaIntentBinding via `resolveSemaIntent`)
3. invoke the provider's handler from `sources.events[name]`
4. effects on the affected parts re-derive structural attrs and
write them via `dom.apply` (automatic — not awaited here)
- New optional `sources.semantic` and `sources.events` on the runtime.
Without `semantic`, step 2 is skipped. Without an events handler for
the name, step 3 is skipped.
- `morfo.events.commits` is intentionally NOT executed by the runtime —
it remains descriptive metadata documenting what an external observer
will see (per the architecture's "state is the source of truth" rule).
Tests
- 8 new trigger unit tests using the toast and dialog morfos:
- throws on undeclared event
- throws when target part has no DOM
- emits with intent resolved from prop (toastMorfo.announce)
- falls back to declared default intent when prop missing
- omits intent for transitional families (toastMorfo.present)
- prewrite applied before semantic.emit (dialogMorfo.close-cancel)
- handler invoked only after semantic.emit resolves
- skips semantic.emit when no semantic source provided
Verification
- 84/84 focused unit tests green (full adom/sema/lib/dom/morfo/soma).
- 66/66 morfo-check.
- 68/68 smoke routes.
- svelte-check unchanged at 155 (no new errors introduced).
Next (commit B): migrate Toast to use the contract — wire close.onclick,
swipe gesture, auto-timer, and Escape keydown to runtime.trigger().
|
6 months ago |
|
|
53d6539abe |
soma: Collapsible pilot — multi-part runtime migration
Second pilot of MorfoRuntime V1, validating the multi-part case left
unproven by Toggle. Collapsible has three providers (provider/trigger/
content), shares root state across them, and uses partRef cross-references
(`aria-controls` ↔ `aria-labelledby`).
Note on scope
The morfo doesn't declare `keyboard` — the Trigger is a `<button>` and
relies on native Enter/Space via onclick. So this pilot does NOT exercise
`runtime.keydown`; that's deferred to the next pilot with a real keyboard
contract (Tabs, Accordion, Listbox).
What this pilot validates
- Single runtime instance shared across three part-providers, created at
the root and accessed by children via the Provider context.
- Cross-part state source: all three parts read `open` and `disabled`
from the root provider's opts via `states` / `props` source maps.
- Cross-part id resolution: `parts.trigger`/`parts.content` getters read
from the root's reactive `triggerId`/`contentId` State; partRef on
Trigger and Content resolves once the corresponding sibling registers.
- Conditional emission with partRef: `aria-controls` (when Content present)
and `aria-labelledby` (when Trigger present) toggle correctly as siblings
mount/unmount.
- Effect timing across multiple parts: each part's effect runs in its own
component scope, cleans up on unmount.
Morfo update
Same fix as Toggle: data entries got explicit `value` sources. `data-state`
references `stateRef('open')`; `data-disabled` references `propRef` with
`prop-truthy` condition. Without this the runtime had no way to source
the values (the legacy provider hard-coded them).
Provider migration
- CollapsibleProvider creates the runtime in its constructor with shared
states/props/parts sources and registers the 'provider' part.
- CollapsibleTriggerProvider and CollapsibleContentProvider call
`provider.runtime.registerPart(...)` for their slot, and publish their
id to the root's State so partRef resolves.
- Each part's `props` derivation collapses from ~6 attrs (data-state,
data-disabled, role, aria-*, type, etc.) to just `runtime.partProps()`
+ the few primitive attrs not in the morfo (`disabled`, `onclick`,
`hidden`).
Verification
- 76/76 focused tests green.
- 66/66 morfo-check (Collapsible's emitted DOM matches the morfo).
- 68/68 smoke routes 200 OK.
- svelte-check unchanged.
|
6 months ago |
|
|
62bcb1268f |
soma/morfo: fix conditional attr cleanup in resolvePartAttrs
Real bug surfaced by user testing the Toggle pilot: setting `invalid` to true emitted `data-invalid` and `aria-invalid` correctly, but flipping it back to false left the attrs on the DOM. The red border from CSS based on `[data-invalid]` stayed lit. Cause: `resolvePartAttrs` skipped declared attrs whose condition was falsy. The map handed to `dom.apply` lacked the key, so dom.apply (which only iterates the keys it receives) had no instruction to remove the attr from the previous tick. The function's JSDoc claimed it emitted `undefined` for non-matching attrs. The implementation didn't match. Now it does: - Conditional attrs with falsy condition are emitted as `undefined` → `dom.apply` removes them from the DOM and Svelte ignores them at render. - Data entries without a `value` source are still skipped entirely (they are documentation in the morfo, not runtime-managed attrs). Same fix covers all conditional attrs: data-disabled, data-readonly, data-required, data-invalid, aria-disabled, aria-readonly, aria-required, aria-invalid, aria-label — all toggled on/off cleanly now. Regression test added covering the invalid true→false→true cycle. Verification: 5/5 runtime tests, 76/76 focused suite, 66/66 morfo-check, 68/68 smoke. |
6 months ago |
|
|
8ede535305 |
soma: MorfoRuntime V1 + Toggle pilot
First piloted slice of the closed architecture: a runtime that interprets
the morfo declaration and writes mutable attrs to the DOM via dom.apply,
removing the need for the provider to hand-roll resolveMorfoProps bindings
on every part.
src/uix/soma/morfo/
- resolver.ts — pure morfo → attrs resolver, extracted from Provider so
both legacy Provider (still in place) and the new runtime can share it
- runtime.svelte.ts — `createMorfoRuntime(morfo, sources)` with V1 surface:
- registerPart(part, opts) — registers per-instance id/ref/sources and
sets up a $effect that reapplies attrs whenever sources change
- partProps(part) — returns ONLY identity (id + marker + ref attachment),
never mutable attrs (those go through dom.apply)
- runtime.svelte.test.ts — 4 tests verifying initial application, reactive
reapplication, and unknown-part rejection
ARIA stateRef refinement
The shared resolver now stringifies ARIA stateRef values to 'true'/'false'.
Boolean false on aria-* must mean "explicitly unpressed/unexpanded", not
"attribute absent" — the previous boolean return wired into dom.apply's
`false → removeAttribute` semantics, which is correct for data-* presence
flags but wrong for ARIA. Distinct contracts; resolver decides at the
boundary, not dom.apply.
Toggle morfo
Added explicit `value` sources to all data entries. The legacy provider
hard-coded data-state etc.; the runtime needs the morfo to declare where
each attr's value comes from. Now data-state references stateRef('on') and
data-disabled / data-readonly / data-required / data-invalid reference
their corresponding propRef with prop-truthy conditions.
Toggle provider migration
- ToggleProvider creates a MorfoRuntime in its constructor with Soma's dom.
- registerPart('provider', { id, ref }) wires the runtime to the button.
- props derivation now returns only:
- runtime.partProps('provider') — id + data-toggle + ref attachment
- disabled (HTML attr; not in morfo by design — primitive button concern)
- onclick handler
- All morfo-declared attrs (data-state, aria-pressed, data-disabled,
aria-disabled, aria-label, etc.) are written by the runtime's effect via
dom.apply.
Verification
- Unit tests: 4/4 morfo runtime + full focused suite green.
- morfo:check: 66/66 — Toggle's emitted DOM matches the morfo declaration
end-to-end now that data-* sources are pinned.
- smoke: 68/68 routes 200 OK.
- svelte-check: no new errors.
V2 (deferred — not part of this pilot):
- runtime.keydown(part, event) for morfo.keyboard dispatch.
- runtime.trigger(eventName) — prewrite + semantic.emit + handler chain
(Toast will be the first component to exercise this).
- OptsFromProps + bindProps helper to absorb the props bridge boilerplate.
|
6 months ago |
|
|
02fa963cb8 |
adom + sema: dom.apply + semantic.emit contract skeleton
The minimum primitives the closed architecture needs. No MorfoRuntime yet,
no provider migration — just the two operations that everything else will
compose on top of.
adom (only writer of mutable attrs):
- types StructuralChange + DomAttrValue
- applyChange(change) writes attrs (string/number → setAttribute, true →
presence flag, false/null/undefined → removeAttribute)
- removeAttrs(target, names) targeted cleanup
- ActiveDom exposes apply() + remove() — same surface every consumer uses
sema (perceptual signal channel):
- new SemanticSignal type (runtime payload distinct from declaration-time
SemaEvent): { target, name, intent?, family?, hold? }
- SemanticEngine accepts dom by construction (DI; not imported hard from
$uix/adom inside sema)
- new emit(signal) async method with the agreed lifecycle:
1. write data-event* via dom.apply
2. await one rAF (CSS gets a frame to observe)
3. resolve Promise (caller can dom.apply structural change)
4. hold N frames (default 1, configurable per signal)
5. clear data-event* via dom.remove
- legacy publish() flow untouched for backward compatibility
Tests: 12/12 green; full adom+sema+lib/dom suite 50/50 green;
svelte-check delta: -12 errors (no regressions in touched files).
|
6 months ago |
|
|
13caaf215f |
docs: closed architecture (Morfo + MorfoRuntime + Provider + Effects + Sema + Dom)
Reflect the architectural decisions reached on 2026-04-25 across the
layer-level READMEs:
- src/uix/README.md
- rewrite ADom section: no longer a "broker semántico"; only DOM mutation surface
- rewrite Sema section: vocabulary + EngineSemantic with Promise-returning emit
- new §2.bis "Cómo se ejecuta un componente": six-piece chain with disjoint
responsibilities (Morfo declares, Runtime transcribes, Provider supplies,
Effects sync, Semantic emits, Dom applies)
- update §8 dependency rules to match the closed design
- new one-line summary in §9
- src/uix/morfo/README.md
- new "How morfo gets executed" section: maps each morfo field to its runtime
executor; documents trigger() sequence and provider responsibilities
- src/uix/sema/README.md
- rewrite around the Promise contract: emit() resolves after 1 rAF
- document lifecycle (id → write signal → wait frame → resolve → hold → cleanup)
- error policy and the three composition scenarios with dom.apply
- src/uix/soma/SOMA_ARCHITECTURE.md
- new §3.bis "Arquitectura cerrada" introducing MorfoRuntime as the missing
piece between Morfo (declaration) and Provider (execution)
- documents API V1, three commit operations, trigger() sequence, operational
rules, and pilot order (Toggle → Collapsible → Toast → Dialog)
No code changes; this commit pins the architecture before implementation.
|
6 months ago |
|
|
d87421580d |
adom + lib/dom: foundation for morfo runtime
- Split lib/dom (pure) from adom (reactive runtime). - Move resize-observer and responsive to plain .ts (no runes needed). - Fold elements.ts utilities into core.ts. - Tabbable helpers consolidated in tabbable.ts via npm `tabbable`. - BodyScrollLock: explicit MANAGED_PROPERTIES tracking instead of stringly setAttribute. - RovingFocusGroup: pure getTabIndex reader, separate initializeDefaultTabStop. - DOMContext: drop redundant canUseDom, getRootNode without optional chaining. - ActiveDom: viewport tracking moved to its own module, viewport readonly facade. - New READMEs at lib/dom and adom documenting the layer pyramid. Foundation commit before introducing MorfoRuntime. |
6 months ago |
|
|
a0a1485b9f |
Refactor sema and add shared dom runtime
|
6 months ago |
|
|
f62ede4a23 |
sema: add runtime foundation and dialog integration
|
6 months ago |
|
|
51dba711e9 |
sema: spec v0.3.1 + cleanup (drop sema_pre + accordion audit_1)
Adds the full Sema specification at `src/uix/sema/sema-spec-v0.3.1.md`, replacing the earlier `sema_pre.md` working draft. The spec is now the source of truth for the perceptual layer — 6 families × 5 intents = 22 canonical events, 4 channels, the MorfoSema extension for cross-layer action contracts, and the SemaPort protocol between headless and Sema. ## Key architectural decisions codified - Tripartite resolution: morfo-sema (contract) / `.csem` (integrator overrides) / `sema-map.json` (canonical vocabulary). - `MorfoSema` minimal shape (7 fields, 5 optional with defaults). - `SemaEventLabel` typed union — 22 events, closed set. - `prewrite` makes the "data-last-action BEFORE data-state" convention explicit and validated. - `commits` declares structural effect, NOT preconditions — validity remains the provider's responsibility. - `keyboard.action` relaxed: only acquires Sema semantics when it matches a name in `sema.actions[]`. - `sound` disabled by default; opt-in only. - Caps: 200 ms blocking / 80 ms under accessibility reductions. - 4 arbitration regimes: `replace | collapse | lock | queue`. ## Cleanups - `src/uix/sema/sema_pre.md` removed — superseded by v0.3.1. - `src/uix/soma/components/accordion/audit_1.md` removed — historical artifact, consolidated into the canonical audits at `src/uix/soma/AUDIT_1.md` and the two recent `soma-audit-*.md` files. ## Next steps (from §14) 1. Extend `Morfo` type with `sema?: MorfoSema`. 2. Extend sium validator for the 7 new validations of §5.7. 3. Stub `SemaPort` (interface + no-op + test port). 4. Implement the engine runtime. 5. Implement `.csem` PostCSS plugin. 6. Define `sema-map.json` with the canonical values. 7. Validate end-to-end with 4 real components (Button, Input, Dialog, Toast). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
6 months ago |
|
|
0d8de2869f |
soma test: split forms demo into dedicated switch/checkbox/radio-group pages
The combined `/test/soma/forms` page covered three separate components
(Switch, Checkbox, RadioGroup) in one route. Each deserves its own demo
page — matching the convention of every other component having a
dedicated `/test/soma/{kebab}` route, and letting the permutation runner
actually exercise them (the runner navigates per morfo-kebab, not per
merged page, so the combined demo left the 3 morfos SKIPPED).
## New demos
- `/test/soma/switch` — basic + disabled variants. `data-perm-step="0"`
toggles the basic switch. Axes: `dir, disabled`.
- `/test/soma/checkbox` — basic + indeterminate + group variants. Steps
0-2 toggle the first checkbox + first two group items. Seeds
`?perm.checked=true` / `?perm.value=a,b`. Axes: `dir`.
- `/test/soma/radio-group` — active + disabled variants. Steps 0-2 select
each option. Seeds `?perm.value=option-2` / `?perm.orientation`.
Axes: `dir, orientation`.
Each demo reuses the CSS from the original combined page (via `:global`
selectors for `data-switch`, `data-checkbox`, `data-radio-group-item`).
All three have `aria-label` on the Provider — the DEV-mode A4 warning
added in batch 10 of audit 2026-04-21 requires it.
The combined `/test/soma/forms` page is kept in place (pre-existing
integration demo showing composition); future batches may decide
whether to drop it or repurpose it.
## Coverage delta
- Before: 63/66 demos · 131 variants · 202 steps.
- After: 66/66 demos · 139 variants · 228 steps. (smoke: 68 routes)
Coverage complete — every morfo now has at least one instrumented demo
under the permutation runner.
## Verification
- `npm run check`: 0 errors in new demo files.
- `npm run smoke`: 68/68 routes (was 65).
- `npm run morfo:check`: 66/66 morfos.
- `npm run perm:check`: 66/66 demos · 228 steps · 139 variants.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
6 months ago |
|
|
db3ad3497f |
soma test: complete permutation runner instrumentation (63/66 demos)
Bulk-instruments the remaining 49 demo pages with `<meta name="perm-axes" content="dir" />`, bringing total coverage from 15/66 demos to 63/66. Only checkbox, radio-group, and switch remain skipped — they have no demo directory at `/test/soma/` (pre-existing gap, unrelated). ## Runner change `scripts/permutation-check.ts` now allows meta-only demos (no `data-perm-step` annotations) to participate: if a demo declares any axis via the meta tag, the runner still visits its baseline + RTL variants and runs the `[initial seed]` morfo validation at each, even without a click cycle. This gives passive LTR/RTL coverage to demos where a click-driven perm sequence doesn't apply naturally (leaf components like Avatar, Progress, Meter; grid/list components; pickers with inline controls). Key change in `probeDemo()`: a demo is "instrumented" if EITHER the meta tag OR at least one `data-perm-step` is present. Step-only demos get the v1 click cycle at baseline; meta-only demos get the v3 axis matrix with passive `[initial seed]` validation at each variant; declaring both is the full treatment. ## Bulk instrumentation For each of the 49 remaining demos, added: ```svelte <svelte:head> <title>… · Soma</title> <meta name="perm-axes" content="dir" /> </svelte:head> ``` Baseline axis (`dir`) only. Demos that benefit from additional axes (`orientation`, `disabled`, etc.) opt in incrementally — the bulk edit doesn't touch their `$state` seeds, only adds the meta tag. ## Coverage delta | Metric | Before | After | |--------|-------:|------:| | Instrumented demos | 15/66 | **63/66** | | URL variants | 35 | **131** | | Step validations | 106 | **202** | ~4× increase in URL variants visited per CI run. Most new demos contribute 2 variants each (ltr + rtl), validating morfo under direction flip. Fully-instrumented demos (tabs, toolbar, toggle-group, listbox) still contribute 3-4 variants with click cycles. ## Deferred - `context-menu` — right-click activation needs a `data-perm-mode='contextmenu'` runner extension. - `navigation-menu` — hover-driven, same as Tooltip. Tooltip was instrumented via `data-perm-mode="focus"` + long settle; NavigationMenu could follow the same pattern in a targeted batch. - `color-picker` — complex pointer state machine on Area / ChannelSlider. Meta-only covers the baseline structure. ## Verification - `npm run check`: 0 errors in soma/morfo/test-route scope. - `npm run smoke`: 65/65 routes. - `npm run morfo:check`: 66/66 morfos. - `npm run perm:check`: 63/63 instrumented demos · 202 steps · 131 variants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
6 months ago |
|
|
b68b831320 |
soma test: instrument 7 more demos for permutation runner (composite + toggleables batch)
Adds `data-perm-step` + `<meta name="perm-axes">` + perms seed to: - **accordion** — expand item-1 → switch to item-2. `?perm.value=item-1` seeds the Provider's value array. `dir` axis. - **listbox** — select apple (first) → select apricot. `?perm.orientation` flips the layout, already wired. Items in a loop use conditional `data-perm-step` by index. `dir,orientation` axes. - **tree-view** — select readme → select license under the expanded docs branch. `?perm.expanded=docs&perm.selected=readme` seed. `dir` axis. - **combobox** — open via trigger (Combobox.Trigger). `dir` axis only — typing / selection is left to v4 per-component value enumeration. - **menubar** — open File menu → close via Escape (mode=key, 500ms settle). `dir` axis. - **toggle** — press Bold toggle (inside Field.Provider integration example). `dir` axis. - **toggle-group** — press left → press center (single mode). Added `aria-label="Text alignment"` to silence the A4 DEV warning that now fires (shipped in batch 10 of audit 2026-04-21). `?perm.value=bold` preselects. `dir,orientation` axes. ## Deferred in this batch - **context-menu** — needs `contextmenu` event (right-click). Requires a `data-perm-mode='contextmenu'` extension to the runner. Future batch. - **navigation-menu** — hover-driven only. Same future-batch treatment as Tooltip has with `data-perm-mode="focus"` + long settle. ## Coverage delta - Before: 8 demos · 19 variants · 62 steps. - After: 15 demos · 35 variants · 106 steps. ~1.7× jump in a single batch. Tabs still leads with 4 variants (adds `orientation` + `disabled` axes); ToggleGroup, Toolbar, Listbox each have 3 variants. ## Verification - `npm run check`: 0 errors in soma/morfo/test-route scope. - `npm run smoke`: 65/65 routes. - `npm run morfo:check`: 66/66 morfos. - `npm run perm:check`: 15/15 demos · 106 steps · 35 variants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
6 months ago |
|
|
1e8500bfac |
soma test: instrument 5 overlay demos for permutation runner
Adds `data-perm-step` + `<meta name="perm-axes">` + perms seed to the 5 overlay demos sharing Dialog's open/close shape: - alert-dialog — trigger open → cancel. `?perm.open=true` seeds. `disabled` deliberately NOT in axes: disabling the Provider blocks the Trigger click path the runner depends on. - popover — trigger open → close button. `?perm.open=true` seeds open1. - tooltip — focus trigger (mode=focus, 1s settle — tooltip opens after 700ms delay). No close step: tooltip closes on blur. - drawer — trigger open → close button (500ms settle for exit animation). `?perm.open=true` seeds basicOpen. - dropdown-menu — trigger open → Escape close (mode=key, 500ms settle). Second step targets the Menu.Content selector because the Content only mounts after the Trigger step fires (runner handles dynamic step discovery). All five declare `dir` as the only axis (universal RTL variant). The other orientation / disabled axes don't apply to these overlay shapes. ## Coverage delta - Before: 3 instrumented demos · 9 variants · 34 steps. - After: 8 instrumented demos · 19 variants · 62 steps. 2x jump in per-CI-run validations with zero infra changes — just applying the v1-v3 convention to additional demos. ## Verification - `npm run check`: 0 errors in soma/morfo/test-route scope. - `npm run smoke`: 65/65 routes. - `npm run morfo:check`: 66/66 morfos. - `npm run perm:check`: 8/8 demos · 62 steps · 19 variants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
6 months ago |
|
|
545d47148c |
morfo: permutation runner v3 — per-demo axis matrix via `<meta name="perm-axes">`
v2 ran every demo under a hard-coded `ltr` + `rtl` matrix. v3 lets each demo declare which additional axes it honours — `orientation`, `disabled`, `readonly`, `invalid`, `loading` — and the runner adds ONE variant per declared axis (axis-at-a-time, not cartesian, so the matrix stays bounded at O(N) instead of O(2^N)). ## Convention ```svelte <svelte:head> <meta name="perm-axes" content="dir,orientation,disabled" /> </svelte:head> ``` `dir` is universal (always runs). The other axes are opt-in: they only produce variants when the demo both lists them in the meta AND wires the corresponding state from `Perms.getOr(...)`. Without the meta, only `ltr` + `rtl` run. ## Runner changes - New `discoverAxes(base, morfoKebab)` pass: loads the baseline URL, reads `<meta name="perm-axes">`, returns the declared axis list. - `buildVariants(declaredAxes)` composes `ltr` + `rtl` + one `?perm.X=Y` variant per declared axis from the `AXIS_FLIP` map. - `[initial seed]` step validates morfo at each URL before any click, so URL-seeded state alone is exercised (catches seed regressions even when the click cycle never fires). ## Demo opt-in (this commit) - `dialog` — `perm-axes="dir"` (no orientation / disabled / loading axes apply to Dialog). - `tabs` — `perm-axes="dir,orientation,disabled"` + wires `orientation` + `disabled` to the first `Tabs.Provider`. - `toolbar` — `perm-axes="dir,orientation"` + wires `orientation` to the first `Toolbar.Provider`. Result: 3 demos, 9 URL variants, 34 step validations per CI run (v2 was 22 steps across 6 variants). ## Docs `src/uix/morfo/PERMUTATION_RUNNER.md` v3 section: supported axis table, 3-step opt-in recipe, roadmap updated (v4 = morfo-inferred value cycling, v5 = MutationObserver ordering for Sema). ## Verification - `npm run check`: 0 errors in soma/morfo/test-route scope. - `npm run smoke`: 65/65 routes. - `npm run morfo:check`: 66/66 morfos. - `npm run perm:check`: 3/3 demos · 34 steps · 9 variants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
6 months ago |
|
|
575748f079 |
morfo: permutation runner v2 — URL-driven state seeds + RTL variant
v1 shipped click-based state cycles. v2 adds URL-seeded initial state so
the runner can reach any state the component exposes without walking a
click sequence, and runs every instrumented demo TWICE in CI (LTR + RTL)
to catch direction-specific regressions like the slider thumb transform
that landed this week.
## New test-route infrastructure
- `src/routes/test/soma/_perms.svelte.ts` — parses `?perm.X=Y` query
params into a typed `Perms` record (naive coercion: `'true'` → boolean,
digits → number, else string). Exposed via `Perms.getOr({})` context.
Underscore prefix so SvelteKit does not route it.
- `+layout.svelte` — parses perms once at mount, sets context, honours
`?perm.dir=rtl` and `?perm.locale=en` by routing through
`App.setDir` / `App.setLocale` so the existing control strip stays in
sync.
## Demo opt-in
Three demos seed initial state from URL params (v1 + v2 combined):
- `dialog` — `?perm.open=true` lands open.
- `tabs` — `?perm.value=tab-2` lands on that tab.
- `toolbar` — `?perm.format=bold,italic` + `?perm.align=center` preselect
toggles.
## Runner extension
`scripts/permutation-check.ts` now runs each instrumented demo under a
matrix of URL variants (currently `ltr` + `rtl`), validates morfo at the
URL-seeded state BEFORE any clicks (logged as `[initial seed]`), then
runs the v1 click cycle. Output format updated to show per-variant
blocks.
Result: 3 instrumented demos × 2 variants = 22 step validations per CI
run. RTL seeding reproduces the specific state machine the slider
`translate(50%, -50%)` bug needed to fail — had v2 been in place that
day, the regression would have fired before shipping.
## Docs
`src/uix/morfo/PERMUTATION_RUNNER.md` gains the v2 section (layout
plumbing, demo opt-in, URL variant table, `[initial seed]` step
semantics) and marks v1 + v2 as shipped 2026-04-22. v3 (morfo-inferred
per-component axis matrix) and v4 (MutationObserver ordering for Sema)
remain on the roadmap.
## Verification
- `npm run check`: 0 errors in soma/morfo/test-route scope.
- `npm run smoke`: 65/65 routes.
- `npm run morfo:check`: 66/66 morfos.
- `npm run perm:check`: 3/3 demos · 22 steps across 2 variants.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
6 months ago |
|
|
61e4ba6d26 |
morfo: add permutation runner (A37) — third validation layer for state transitions
## Why
`morfo:check` validates single-state DOM at page load; `smoke` catches
hydration-time errors. Neither ejercices state TRANSITIONS — and that is
where every reactivity bug we caught this week actually lived:
- toolbar A35 loop (mount-time, not caught by either)
- form A36 microtask loop (mount-time with onChange)
- slider RTL transform (static value, off-by-thumb-width)
All passed the existing CI. All would have failed a "click, re-validate"
pass.
## What
- **`scripts/permutation-check.ts`** — Playwright runner. For each demo,
collects `data-perm-step="N"` elements, executes them in ascending
order (re-discovering after each step so portal-gated controls get
counted), and re-validates the component's morfo + captures any
`pageerror` / `console.error` between steps.
- **`data-perm-*` authoring convention** — step index, human label, mode
(click / focus / type / key), settle delay, skip-validate flag. Demos
opt in incrementally; the runner SKIPs annotation-less pages without
failing.
- **`src/uix/morfo/PERMUTATION_RUNNER.md`** — full design doc covering
what it validates, the annotation convention, example, failure format,
and v2–v4 roadmap (URL-driven states → morfo-inferred cycles →
MutationObserver ordering for Sema).
- **COMPONENT_GUIDE A37 + checklist item 40** — instrumentation is now a
ship-gate rule; doc lists coverage targets (overlays / toggleables /
composite roving / forms / RTL) and explicit skips (alerts, file
pickers).
- **`npm run perm:check`** — pipeline entry, exit codes 0/1/2
parallelling `morfo:check`.
## Instrumented demos (v1 seed)
- dialog — 2 steps: open via trigger → close via Close button. Step 1 is
inside `{#if open}` + Portal; runner's dynamic re-discovery handles it.
- tabs — 3 steps: initial tab 1 → switch to tab 2 → switch to tab 3.
- toolbar — 3 steps: toggle bold / italic / underline GroupItems
(exercises the A35 pattern that previously looped).
Result: 8/8 permutations pass; 63 demos SKIPPED pending instrumentation.
## Verification
- `npm run check`: 0 errors in soma/morfo/scripts scope.
- `npm run smoke`: 65/65 routes.
- `npm run morfo:check`: 66/66 morfos.
- `npm run perm:check`: 3/3 instrumented demos, 8/8 permutations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
6 months ago |
|
|
6efbfeb795 |
sium: color demo widgets inline + queue CDX-212 (services registry)
- color demo now registers a ColorPicker-based widget inline so kind:color fields render properly instead of showing [object Object] in the fallback text input. Temporary — CDX-212 moves defaults to Soma.services.sium.widgets where they belong. - CDX-212 shard drafted with full v2.3 design (Soma services registry, SiumService.widgets, 3-layer override: instance > subtree > global). Includes all conversation context up to 2026-04-21 so tomorrow's session resumes without paginating chat history. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
6 months ago |
|
|
a722d443b2 |
sium: migrate 6 remaining demos to App+Soma+extend pattern + specimen-book design
Closes the v2.2 demo sweep. All 8 sium test pages (booking, color, kitchen-sink,
login, settings, signup, survey, tree) now share:
- App-level langs: each page does `App.get().langs.extend('demo', {...})` to add
its own namespace. siumLangs + componentLangs already registered globally by
/test/sium/+layout.svelte. Locale switch goes through `app.setLocale()` so
Form.AutoFields sees translated sium.errors.* without per-page plumbing.
- Specimen-book design language: shared CSS in /test/sium/_design.css. Pages
contribute markup only — masthead with eyebrow + schema-name + shape-hint +
locale-switch, hairline rules, description, two-column .specimen layout with
numbered PLATE debug panels.
- No inline <style> blocks. No local createLangs. No ad-hoc helper functions.
Each demo went from ~98 v1-pattern lines (with 25 lines of inline CSS) to ~110
v2.2-pattern lines (zero CSS, all imports via canonical libs). Login was already
migrated as the pilot last turn.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
6 months ago |
|
|
8a4bbb4aa6 |
sium: v2.0 → v2.2 — Issue idlangref, SiumProvider, progressive forms, plurals
Multi-phase delivery closing fase-9 through fase-13 (v2.0 → v2.2).
sium core + langs:
- Issue shape unified to idlangref: { path, code, message: '#?sium.errors.X|fallback', params }
- Dropped MESSAGES registry, resolveIssue, message-resolver (CDX-201, GLM-201, CDX-202)
- Split min/max codes by runtime context: min_value/max_value (numeric, no plural) +
min_length/max_length (string/array, p()-pluralized via count:actual). ISSUE_CODES: 15 → 17.
- BREAKING: Issue.code === 'min' | 'max' consumers must migrate to the 4 new codes.
- toStandardIssues now preserves code + params at the SS bridge (was silently dropping
them, which collapsed interpolation and plurals downstream).
- New core/introspect.ts: serializeSchema, countLeafFields, walkSchema (WeakSet cycle
detection + maxDepth).
sium svelte:
- SiumProvider class + <Sium.Provider {schema}> component (scoped context, schema-only).
- Lang is NOT a Sium concern — orthogonal layer consumed directly from Soma.langs.
soma form integration:
- Form.Provider reads SiumProvider context, creates form internally, exposes bind:form.
- Form.AutoFields translates errors at render via lang.ts / interpolateTemplate.
- form.issues now carries structured Issue data (code + params) alongside form.errors.
- validationBehaviour gains 'progressive' mode + becomes default: quiet until first
invalid submit, onChange until a submit succeeds, quiet again.
- AutoFields wires onblur → form.setFieldTouched, enabling onBlur mode end-to-end.
- createForm hardened with $effect.root fallback; onChange/onBlur hang fixed via
untrack() in runValidate's async branch.
- Form.AutoFields now part of the Form namespace barrel (<Form.AutoFields>).
dias + colors (public API completions):
- dias: isCalendarDate / isCalendarDateTime / isTime / isZonedDateTime type guards +
parseDateSafe / parseDateTimeSafe / parseTimeSafe non-throwing wrappers.
- colors: isColorValue(unknown): value is ColorValue type guard.
Demos + docs:
- /test/sium/+layout.svelte mounts App + Soma with siumLangs registered globally.
Locale switch propagates via app.setLocale() so AutoFields sees translated errors.
- Design language extracted to /test/sium/_design.css + loaded by layout (specimen-book
aesthetic: Fraunces + Geist Mono, hairline rules, cinnabar accent, § sections,
numbered PLATE debug panels).
- Kitchen-sink demo gets live validation-mode switcher (4 modes) via child FormShell
remount with {#key mode}.
- README / GUIDE / INTRO / COOKBOOK aligned to v2.2. New IMPLEMENTATION_STATUS.md as
single-page snapshot of everything delivered and accepted.
- Coordination shards (CDX-201..211, GLM-201..204) live under lib/sium/status for
reproducibility and audit trail.
Tests: 378/378 green across sium core, langs, schema, primitives, refines, combinators,
standard-schema, introspect, form.svelte, form-auto-fields.svelte.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
6 months ago |
|
|
2627dc2094 |
soma: fix slider thumb alignment + document morfo in README
## Slider thumb misalignment `SliderThumbProvider.thumbStyle` used `translate(50%, -50%)` for both LTR and RTL. For LTR that pushed the thumb RIGHT by half its width from the `left: X%` anchor — visible symptoms in the time-picker / time-range-picker demos: - thumb never reached `left: 0` (centre at value=0 was offset by thumbWidth/2) - thumb overshot the track on the right edge at value=max - drag felt off by the same amount throughout Fix: use `translate(-50%, -50%)` for LTR (thumb centre aligns with anchor) and keep `translate(50%, -50%)` for RTL (anchor is `right: X%`, thumb must shift right by half its width to centre-align). Matches the logic already used by `SliderTickProvider` which chose `tx = isRtl ? '50%' : '-50%'`. Browser probe confirms: value=0 now centres at 0%, value=30/59 at ~50.8%, style attribute reports `translate(-50%, -50%)` in LTR. ## soma README documents morfo Added §4b "Morfo — contrato declarativo cross-layer" covering: why morfo exists (6-place drift without it), how providers consume it (`createAttrs(morfo)` + `registerContract(morfo)`), the mandatory `as const satisfies Morfo` authoring pattern for literal-typed inference, the 3-layer validation pipeline (schema / assertContract / morfo-check), and the boundary (what does NOT go in morfo — props, prose, langs, state machines, visual recipes). Updated the §4 structure listing so `attrs/create-attrs.ts` shows the new signature. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
6 months ago |
|
|
50821aae44 |
soma: P3 cleanup — LinkPreview a11y section + TagGroup key API consistency
- LinkPreview README: elevate the "intentionally hidden from AT" note to its own `## Accessibility` section with the three reasons (duplicate content, hover-only activation, focus stays on trigger). Consumers reaching for an AT-visible preview are pointed at Popover / Tooltip. - TagGroup `handleItemKeydown`: vertical nav now calls `getDirectionalKeys(dir, 'vertical')` (returns UP/DOWN absolute) instead of hardcoded `KEYS.ARROW_DOWN/UP`. Functionally identical — vertical keys don't flip under RTL — but keeps the shape symmetric with the horizontal branch and matches the convention used across the library. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
6 months ago |
|
|
8d4f53a73f |
soma/morfo: typed createAttrs + 6 audit batches + form hang fix + A36
## morfo: typed createAttrs
- `types.ts`: all array fields are now `readonly` so morfos can be authored
with `as const satisfies Morfo` without casts.
- `createAttrs` is a generic `<const M extends MorfoShape>` that infers the
emitted attr map from the morfo's literal shape. Typos like `attrs.trigerr`
are now compile errors; autocomplete lists every valid part.
- All 66 morfos migrated: `export const xMorfo = { ... } as const satisfies Morfo`.
- Two latent bugs the new typing caught: `dateRangeFieldAttrs.root` and
`timeRangeFieldAttrs.root` — legacy from the root→provider refactor, fixed
to `.provider`.
- Docs (morfo/README, COMPONENT_GUIDE) mandate `as const satisfies Morfo`;
the older `: Morfo` annotation widens literals and loses autocomplete.
## Audit batches 1–7 (soma-audit-2026-04-20)
Batch 1 — Keyboard a11y (A14/A12):
- Tabs: `isFallbackTabStop(el)` using `untrack` → first trigger gets
`tabindex=0` when no tab matches (WCAG 2.1.1 was failing).
- RatingGroup: `dir` prop + `getDirectionalKeys(dir, 'horizontal')` for RTL.
- Accordion P1-1/P1-6 dismissed: roving/labelledby already correct per APG.
Batch 2 — A33 reactive collections: Select `labelRegistry` and NavigationMenu
`triggerRefs` migrated to `SvelteMap`.
Batch 3 — A31 O(N²) fix: RadioGroup lifts `hasSelection` + `isFallbackTabStop`
to the provider with `untrack`-wrapped DOM walk. Per-item derivations drop
from O(N²) to O(1) per trigger.
Batch 4 — Morfo aria sync: toast / time-field / date-field / tabs /
tag-group / navigation-menu declare every aria the provider emits
(14 entries across 6 files).
Batch 5 — Accessible naming props: Tabs.List, ToggleGroup, TreeView,
NavigationMenu expose `aria-label?` / `aria-labelledby?` with explicit
"required unless labelledby is set" JSDoc.
Batch 6 — A30 cleanup: Checkbox `setLabelId` and 5 trigger-ref providers
(select/tooltip/link-preview/dropdown-menu/context-menu) migrated from
`$effect(() => parent.ref = opts.ref)` to the `Provider` base's
`onRefChange` callback (fires via attachRef inside `untrack`).
Batch 7 — A11 wrapper thinning: stepper/tree-view wrappers removed
`ctx.get()`, providers expose proxy derivations; time/date-range-field
inputs use `untrack(() => …)` for one-shot id registration; menubar
content's 40-line keyboard nav moved to `MenubarMenuProvider.onContentKeydown`;
color-picker area + channel-slider pointer state machines moved to the
providers with `onpointerdown/move/up` emitted via `props`.
form-auto-fields deferred with justification (reflective schema renderer,
not a state-holding wrapper).
## Form hang fix (A36)
`runValidate` entered the `isPromiseLike` branch even for sync schemas
because sium's Standard-Schema adapter declares `validate: async`. The
fallback `return { errors, issues }` read both reactive vars inside the
validation `$effect` while the `.then(...)` microtask wrote them
→ microtask-mediated loop, silent to Svelte's sync effect-depth guard.
Symptom: kitchen-sink hung the tab on mount with onChange / onBlur.
Fix: `return untrack(() => ({ errors, issues }))`. Regression locked by
two new 5s-timeout tests in `form-auto-fields.svelte.test.ts`.
Pattern codified as A36 in COMPONENT_GUIDE; CLAUDE.md count updated to
A1–A36 / 39 items.
## Verification
- `npm run check`: 0 errors in soma/morfo scope (sema demo pre-existing
errors untouched).
- `npx vitest run src/uix/soma src/uix/morfo`: all tests green
(94 + 7 = 101 tests, inc. 2 new form regression tests).
- `npm run smoke`: 65/65 routes.
- `npx tsx scripts/morfo-check.ts`: 66/66 morfos.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
6 months ago |
|
|
e1837e8727 |
docs: add A35 (ref+parent-write effect loop) + update CLAUDE.md
A35 codifies the toolbar incident (2026-04-19): per-item `$effect` that reads `opts.ref.current` and writes provider state read back by per-item `props` $derived triggers `effect_update_depth_exceeded` via the attachment reapply cycle. Register in the constructor (A30-style); use `$effect` for cleanup only; wrap DOM walks in `untrack` when called from `$derived`. - COMPONENT_GUIDE: §A35 rule, checklist item 38 - CLAUDE.md: checklist count updated (A1–A35, 38 items), verification mention of morfo-check + morfo kebab naming note Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
6 months ago |
|
|
91257e8526 |
soma/morfo: rename root part → provider + fix toolbar effect loop
Naming coherence across the part layer: the root part's `kebab` field now
matches its `name: 'Provider'`. The `createAttrs` special case still emits
`data-{component}` (no suffix) for the provider part — the DOM contract is
unchanged, only the authoring name changes.
Affected:
- createAttrs special-case: 'root' → 'provider'
- 66 morfos: kebab 'root' → 'provider'
- 77 soma provider files: `attrs.root` → `attrs.provider`, constructor
part arg 'root' → 'provider'
- Docs: A2 rule rewritten, morfo/README, soma/README example updates
- Test / schema / morfo-check: updated for new kebab
Toolbar fix: remove `itemsVersion` counter and per-item `$effect` blocks
that called `notifyItemsChanged()`. The pattern was an A30 violation —
each item's $effect read `ref.current`/`disabled` and wrote back to the
provider, which cascaded through `firstTabStop` → per-item `props`
$derived → DOM reattach → effect re-run, triggering
`effect_update_depth_exceeded` on page load. `isTabStop` now uses
`untrack` around the DOM walk so per-item `props` derivations only
depend on `lastFocusedElement`.
Verification: 7/7 morfo tests, 65/65 smoke, 66/66 morfo-check, no effect
loops detected across any demo page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
6 months ago |
|
|
0222af7a0b |
morfo: enrich 65 components with aria + keyboard + focus contracts
Each morfo now declares what its provider actually emits (via agent-based
extraction from provider code). Before: parts + data only. After: parts +
data + ARIA per part + keyboard per part + focus policy for overlays.
Contract source: provider's `readonly props = $derived.by(...)` blocks,
classified into the v.* value kinds — literal, stateRef, partRef, propRef,
translationRef. Conditions extracted from ternary / guard patterns.
Focus policies added on overlay components: popover, drawer, alert-dialog,
dropdown-menu, context-menu, select, date-picker, date-range-picker,
time-picker, time-range-picker, color-picker. Context-menu uses
return:'previous' (no discrete trigger). Tooltip, link-preview, combobox,
command omit focus (no trap).
Tag-group Link variant: morfo-check tolerates sibling parts sharing a
physical element (Link IS Item in the DOM) via allPartAttrs cross-scope.
README.md added — developer guide: authoring a morfo step-by-step, the
v.* builders, severity rules, validation layers, polymorphism handling
via defaultElement + role, Sema alignment (data-last-action, transition
markers).
Verify:
npm run check — 5 errs, all pre-existing in sium/examples.
vitest run src/uix/morfo — 7/7 pass.
morfo:check — 64/66 pass (2 flaky Playwright navigation timeouts,
re-run clean when isolated).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
6 months ago |
|
|
8a5b402dd3 |
morfo: port 65 components + sium.lazy + morfo-check passes 66/66
All 65 soma components now consume their morfo as the cross-layer contract.
createAttrs(morfo) and registerContract(morfo) overloads removed — the
Morfo form is now mandatory. Each component provider has a 3-line morfo
import + wire-up block instead of 20+ lines of inline declarations.
Additions:
sium/core/lazy.ts — lazy() combinator for recursive schemas (unblocks
morfo's eventual lazy part-tree validation; tests included).
sium/examples/ — booking, color, login, settings, signup, survey, tree
form demos.
Route demos at src/routes/test/sium/{booking,color,login,settings,
signup,survey,tree}.
Tooling:
morfo-check.ts — now tolerates sibling parts sharing a physical element
(Link variant of Item, etc.). Queries via allPartAttrs cross-scope.
morfo-vocabulary-check.ts — 28 WARN (legitimate per-component
vocabularies or subsets like switch missing "indeterminate"). All
reviewed, none are bugs.
Fixes caught by morfo-check:
table: data-sorted / data-pinned marked severity:'optional' (only
emitted when sorting/pinning active, not always).
tree-grid: data-expanded on Row severity:'optional' (branches only).
menubar: data-menubar-value declared on Trigger.
splitter: data-splitter-panel-id declared on Panel.
Verify:
npm run check — 0 morfo errors (4 pre-existing in sium/examples,
unrelated).
npx vitest run src/uix/morfo — 7/7 tests pass.
npm run smoke — 65/65 routes pass.
npm run morfo:check — 66/66 morfos validate against real DOM.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
6 months ago |
|
|
8c16ffac81 |
morfo: cross-layer contract + 14 new WAI-ARIA components + Dialog wired
Morfo (src/uix/morfo/): machine-readable contract of a component's public DOM surface, shared between soma/sema/eidos. Shape v5 consolidated across three independent AI reviews (Gemini, Grok, ChatGPT) plus Sema alignment. Types, sium-based validator with cross-field invariants, dialog.ts as the first canonical morfo with data-last-action for Sema-causal exits, 7 passing unit tests. createAttrs(morfo) and registerContract(morfo) now consume a Morfo — legacy inline signature removed. Dialog provider refactored to derive parts + data contract from dialogMorfo. The other 65 soma components need porting (svelte-check lists them) — tracked work. Tooling: scripts/smoke-check.mjs — Playwright smoke over 65 soma demo routes scripts/morfo-check.ts — validates emitted DOM vs morfo scripts/morfo-vocabulary-check.ts — canonical vocabulary consistency npm run smoke / morfo:check / morfo:vocabulary WAI-ARIA APG components added (100% APG coverage now): Announce, Avatar, Clipboard, DragDrop, Feed, GridList, Meter, Progress, SearchField, TagGroup, TreeGrid + Table.RowDetail/Trigger (detail panel pattern; hierarchical Table rows deprecated — use TreeGrid instead). air: Link, Banner. Sema prep: src/uix/sema/sema_pre.md documents the semantic layer's DOM requirements. Morfo already provides everything Sema needs (data-last-action patterns, transition markers, enumerable state values, cross-component consistency). Sema implementation deferred. COMPONENT_GUIDE updated: items 37-39 (translation namespace grep, DOM topology vs .require() audit, npm run smoke is part of done) + A34 rule with incident log. WAI-ARIA pattern listed as soma membership criterion. Pagination translation bug fixed (soma.pagination.page -> idlangref PAGINATION_LANGS.PAGE). Table demo namespace fix (soma.table.* -> components.table.*). Avatar demo now offline-safe (SVG data URL). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
6 months ago |
|
|
3b704286ba |
sium: v1.0 — schema core, domain types, i18n, Svelte adapter, AutoFields
Ship v1 of sium, the Schema-In-Use Module — soma's native validation layer.
Core (PR1): issue/result helpers, Schema<I,O> factory with ~standard + ~sium
wiring, 5 primitives (string/number/boolean/literal/enumOf), 3 modifiers
(optional/nullable/defaulted), 4 combinators (object/array/union/discriminated),
pipe + 4 step constructors (refine/transform/codec/meta), 7 refines
(min/max/length/regex/email/url/integer), barrel.
Domain types (PR2b): colorValue, dateValue, dateRange, timeValue + 15 segments
(year/month/day, hour/minute/second/dayPeriod, red/green/blue/hue/saturation/
lightness/brightness/alpha). All segments are kind:'number' + meta.channel —
keeps SchemaKind vocabulary tight at 14 entries.
i18n (PR2a): 15 stable issue codes, MESSAGES registry with English fallbacks,
siumLangs LangNode (es + en) for ext/lang integration, pure template resolver
with {{name}} syntax.
Svelte adapter (PR3): resolveIssue/resolveIssues bridge, createForm reactive
with $state/$derived + locale change reactivity + sync/async detection.
Form.AutoFields (PR4): Soma renderer driven by schema introspection. Widget
registry with 4-level priority (meta.widget → channel:X → kind:X → built-in).
Contract supports Component | Snippet widgets — compound widgets like
ColorPicker/Calendar compose inline without wrappers.
Polish (PR5): COOKBOOK with 12 recipes, GUIDE with vs-Zod comparison and
practical examples, kitchen-sink demo (src/routes/test/sium) with locale
switcher and live schema introspection panel, v1 readiness audit report,
doc consistency audit report, isColorValue hardening (rejects NaN + invalid
hex) after production bug report.
Coordination: all work tracked via protocol v2.1 (state.json + sharded orders
+ audit.mjs runner) in src/lib/sium/status/. 340+ tests, npm run check clean
(0 errors, 0 warnings against 28-budget).
Deferred to v1.1 (README §11.2): timeRange, lazy/recursive schemas, tuple/
record/intersection, bundle measurement, additional locales, jsDate coerce.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
6 months ago |
|
|
72a32e0176 |
soma: 10 new components + reactivity audit + guide rules A30–A33
Components shipped: - AlertDialog (thin Dialog specialisation with Escape=close default) - Breadcrumb (+ interactive Ellipsis composing DropdownMenu) - Carousel (gesture + autoplay + indicators) - Listbox (roving tabindex + typeahead + Ctrl+A multi-select) - NavigationMenu (hover-intent + data-motion direction + Indicator CSS vars) - PinInput (Field-aware segmented input with paste transformer) - RatingGroup (half-step + clearable + keyboard digits) - Toggle (standalone two-state with Field OR-merge) - VirtualGrid (2D windowing for spreadsheets / thumbnail grids) - VirtualList (fixed + dynamic heights, ResizeObserver anti-jump, WindowViewport) Guide rules (src/uix/soma/COMPONENT_GUIDE.md): - A30 + item 32: id registration via direct assign, never $effect — the $effect(() => parent.id = opts.id) pattern causes reactive loops that freeze the page. Ref: PinInput + Listbox incidents. - A31 + item 33: per-entity $derived must not call provider methods that read global state — lift to single provider $derived, per-entity derivations pointer-compare. Ref: Command (2026-04-17), Listbox rovingTarget. - A32 + items 34–35: present comparison table in conversation before declaring done. Every ❌/⚠️ gets an explicit decision (implement / defer to v2 / drop); deferred features become their own README section. - A33 + item 36: reactive collections use SvelteMap / SvelteSet from svelte/reactivity — $state(new Map()) only tracks field reassignment, not per-entry .set/.get/.has reads inside $derived. Reactivity audits + fixes: - VirtualList sizeCache → SvelteMap (dynamic heights were silently stuck). - Form touched + registry → SvelteMap (isTouched / isDirty / onBlur validation $derived never re-ran with plain Map). - Combobox labelRegistry → SvelteMap (removed clone-and-reassign workaround, O(N)→O(1) per registration). Other fixes this pass: - ColorField/ColorPicker RGB co-increment bug (HSV round-trip was lossy). - 24 state_referenced_locally warnings swept across date/time/color field+picker wrappers (removed `void X;` placeholders that Svelte 5 flagged, added svelte-ignore for intentional mount-time reads). - AlertDialog Content wrapper forces escapeKeydownBehavior=close (overrides Dialog's alertdialog→ignore default → matches WAI-ARIA). - Breadcrumb Ellipsis interactive prop renders <button aria-haspopup> so DropdownMenu composition works without manual trigger plumbing. - VirtualList Viewport drops `contain: strict` (was breaking programmatic scrollTo in some engines). - Memory entries: feedback_id_effect_assign_directly, feedback_scope_approval, feedback_svelte_reactivity_collections. Sium library (delegated session, landed in tree): - Core schema + primitives + combinators + pipe + refines + modifiers. - Langs module with idlangref resolver. - Svelte form adapter. - Protocol docs + status ledger for Codex coordination. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
6 months ago |
|
|
16f706439e |
soma: tier 3 dates + tier 4 time (partial), dias/segments, docs
Tier 3 (complete):
DateField, DatePicker, DateRangeField, DateRangePicker
Tier 4 (partial):
TimeField, TimePicker (TimeRangeField pending)
Core:
$lib/util/dias/segments.ts — pure domain primitives for segmented
date/time inputs (constants, guards, initializers, reconciliation,
content generation, Intl opts)
soma/external/dates re-exports everything (no façades inside soma)
soma/datetime/ slimmed to UI-only helpers (announcer, DOM segment nav,
SegmentState, KEYS predicates, description writers)
Conventions established (A23–A29):
A23 — never re-implement date helpers; extend dias
A24 — readonlySegments without value anchor logs warning
A25 — range components split readonly per-endpoint
A26 — contenteditable segments use onbeforeinput to block IME/paste
A27 — picker composition pattern with shared writableActive refs
A28 — time placeholders render as hh/mm/ss via dias/segments
A29 — demo pages are interactive testbeds, not static galleries
Demo pages: Material Design 3 styled, every public prop wired to a live
control, Field integration + state readout. Day-period uses real radio
group in 12h mode. Auto-anchor on range pickers keeps the end month
visible. 12h sliders map 0↔12 / 13-23↔1-11 correctly.
Docs: COMPONENT_GUIDE.md + SOMA_ARCHITECTURE.md updated with A23–A29,
checklist items 28–31, picker composition section, dias boundary,
directory structure note for soma/datetime/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
6 months ago |