# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. > **Branch note (`active-uix`):** the legacy layers `src/lib/`, `src/uix/terra/`, > `src/uix/air/` and the old `src/routes/` test tree were removed during the > UIX refactor. The new ecosystem lives in `src/arts/`, `src/libs/`, `src/svrs/`, > and `src/uix/{active-uix,soma,sema,eidos,morfo}`. Demos are being re-authored > under `web/routes/` from scratch. ## Build / Test Commands ```bash npm run dev # Start dev server (routes resolve from web/routes) npm run build # Production build (static via adapter-static) npm run test # Run all tests once npm run test:unit # Run tests in watch mode npx vitest run src/uix/morfo/compile.test.ts # Run a single test file npx vitest run -t "describe name" # Run tests matching a pattern npm run check # Type check with svelte-check npm run lint # Check formatting (Prettier) npm run format # Auto-format ``` ## Code Style - Tabs for indentation, single quotes, no trailing commas, 100 char print width (Prettier). - Comments in **English**. (Some legacy Spanish comments remain — don't add new ones.) - Svelte 5 runes mode is enforced globally (`svelte.config.js` `dynamicCompileOptions`). - DOM event handlers: lowercase (`onclick`, `onfocus`). User callbacks: camelCase (`onComplete`, `onInteractOutside`). - State: `$state()` without `#`. Derived: `readonly x = $derived.by(...)`. ## Path Aliases Aliases are kept in sync between `svelte.config.js` and `vite.config.ts`. Source of truth is `vite.config.ts` (single `aliases` const reused by both top-level resolve and the server-test project). | Alias | Target | | --- | --- | | `@` | `src/` | | `$active-app`, `$adom`, `$auth`, `$bus`, `$cache`, `$connection`, `$format`, `$frontend`, `$http`, `$lang`, `$logger`, `$orca`, `$perm`, `$prefs`, `$session`, `$sium`, `$storage`, `$timer` | `src/arts/{name}` | | `$libs`, `$locale`, `$reactive` | `src/libs`, `src/libs/locale`, `src/libs/reactive` | | `$svrs` | `src/svrs/` | | `$uix`, `$active-uix`, `$soma` | `src/uix`, `src/uix/active-uix`, `src/uix/soma` | ## Vitest Two-Project Structure `vite.config.ts` defines two test projects: - **client**: Browser tests via Playwright for `*.svelte.{test,spec}.{js,ts}`. - **server**: Node environment for `*.{test,spec}.{js,ts}` (excludes svelte tests). ## Architecture The UIX is layered around a declarative contract (morfo) that the other layers consume. Each consumer reads the morfo via `compileMorfo(morfo)`, never by walking the raw declaration. ``` arts/ → runtime artifacts: active-app, adom, perm, prefs, ... libs/ → pure helpers (zero-dep): reactive, days, dom, locale, ... svrs/ → server-authoritative engines uix/morfo → declarative contract: parts, data-attrs, ARIA, keyboard, events with semantic family/intent. Compiler at compile.ts emits CompiledMorfo (cached by WeakMap). uix/sema → semantic engine + channels (visual / sound / vibra). The visual channel writes data-event* during a hold window. State attrs (data-state, data-disabled, ...) are NEVER touched by sema — they belong to the morfo runtime. uix/soma → headless component layer. Each provider consumes morfo + sema via `runtime.trigger(eventName)` which sequences: prewrite → semantic.emit → handler → effect-driven state attrs. uix/eidos → CSS layer that selects against the data-attrs the morfo contract promises. lint.ts validates a CSS file against compileMorfo(morfo).contracts.cssSelectors. uix/active-uix → composition root. Two boot modes: - createActiveUix(options) — standalone (owns services) - attachActiveUix(activeApp) — attach to external app ``` ### morfo (`src/uix/morfo/`) Single source of truth for a component. A `Morfo` declares: - `parts[]` — kebab + archetype + `data` + `aria` + optional `keyboard` - `events[]` — name + `target` partRef + `semantic` + optional `prewrite` + `commits` - `focus` — initial / trap / return / restore (for overlay components) `compileMorfo(morfo)` returns a `CompiledMorfo` with pre-resolved `AttrPlan[]`, `KeyboardPlan[]`, `ActionPlan[]`, and `contracts.cssSelectors` (the closed set of selectors eidos is allowed to use). Cached by morfo identity. ### eidos linter `scripts/eidos-lint.ts ` and `scripts/eidos-lint-all.ts` classify every `[data-*]` selector in eidos CSS as morfo-backed / eidos-only / invalid. Use this to detect drift between the morfo contract and the eidos rules. ## Key Conventions - **Never modify the morfo declaration files** without updating the eidos selectors and provider sources that depend on them. - **Read before acting.** When told to read a file, read it. Don't paraphrase. - **Verify before reporting done.** Run `npm run check` and the relevant vitest scope. UI claims need a browser check; if you can't run a browser, say so. - **No backward-compat shims** when relocating code. Update consumers and delete the old path; don't leave a re-export. - **No re-export façades** between layers (e.g. soma must not re-export dias — consume `$libs/days` directly). - **Provider naming**: child providers reference the parent as `provider`, never `root`. The exported component is ``. `createAttrs` still uses the part kebab `'provider'` internally. - **Data-attr naming**: `data-{component}` (provider) and `data-{component}-{kebab}` (sub-parts). Never `data-soma-*`. The compiler emits these via `compiled.parts.attrs`; runtime queries match exactly. - **CSS token naming (eidos recipes)**: bare-prefixed by component, never by layer. Public tokens: `--{component}-…` (e.g. `--dialog-content-bg`, `--toggle-radius-md`). Internal recipe tokens: `--_{component}-…`. Never `--eidos-…`, `--air-…`, `--terra-…`, `--soma-…`. Migrating `air-` baselines means stripping the `air-` prefix, not replacing it. - **DOM helpers**: `$adom` is the single public surface for DOM-related functionality at component / soma level. It re-exports all of `$libs/dom` wholesale, plus the reactive `createActiveDom` runtime and stateful primitives (`BodyScrollLock`, `DOMContext`, `RovingFocusGroup`). Components and soma NEVER import from `$libs/dom` directly — consume `$adom`. Only `arts/adom/*` (the implementation), `arts/frontend` (peer service fallback), and tests of `$libs/dom` may reach into it. This includes responsive types (`ResponsiveProp`, `Breakpoint`, `Breakpoints`) — they travel with the runtime, so they live in `$adom`. - **Iframe / popup correctness**: when you have a `uix.dom`, prefer `uix.dom.getDocument(node?)` and `uix.dom.getWindow(node?)` over the bare functions of the same name. The instance methods respect the active-dom's `targetWindow`; the bare functions fall back to the global `document` / `window`, which is wrong in iframe / popup / happy-dom-test contexts. ## Critical Rules - **NEVER delete files without explicit instruction** ("delete", "remove", "borra", "elimina"). If ambiguous, ASK first. - **NEVER create fake translators / mocked services** in tests for components. Use real instances composed via `createActiveUix` or `attachActiveUix`. - **NEVER skip hooks (`--no-verify`) or bypass signing** unless explicitly asked. If a hook fails, fix the root cause. - **`$reactive` survives, `$lib` does not.** The `$reactive` alias points to the current `src/libs/reactive`; the singular `$lib` alias was removed in the cleanup phase and any code that resurfaces it is a regression. ## Reference Documents Architecture overview and motivations: [`src/uix/active_architecture.md`](src/uix/active_architecture.md). Per-layer references: - [`src/uix/morfo/README.md`](src/uix/morfo/README.md) - [`src/uix/sema/README.md`](src/uix/sema/README.md) - [`src/uix/soma/SOMA_ARCHITECTURE.md`](src/uix/soma/SOMA_ARCHITECTURE.md) + [`src/uix/soma/COMPONENT_GUIDE.md`](src/uix/soma/COMPONENT_GUIDE.md) - [`src/uix/eidos/README.md`](src/uix/eidos/README.md) + [`src/uix/eidos/components/README.md`](src/uix/eidos/components/README.md) The canonical guide for sema vocabulary, morfo event shape, color tokens, persistence and a11ySemantic lives in [`src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md`](src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md). That document is authoritative — when in doubt about families, intents, verbs, color subsets per component, or holds, that's the source. ## Sema cascade selectors must use the typed builder Cascade rules in `src/uix/sema/components/*.ts` MUST construct their `selector` via `semaSelector(morfo, partKebab, matchers?)` from `$uix/morfo`. **Never hand-write selector strings** that target morfo- emitted attrs (`[data-{component}]`, `[data-{component}-{part}]`, `[data-event="..."]`). The helper: - Compile-error if `partKebab` doesn't exist on `morfo.parts`. - Compile-error if `matchers.eventName` doesn't match `morfo.events`. - Type-checks `eventFamily`/`eventIntent` against the canonical sema unions. - Output is the same CSS string the cascade rule needs — zero runtime cost beyond concatenation. Hand-written selectors in cascade rules are an architecture violation: when morfo renames a part or event, the strings go stale silently. The helper wires the cascade to the morfo's contract so renames break at the type level. ```ts // good import { semaSelector } from '$uix/morfo' import { dialogMorfo } from '$uix/morfo/components/dialog' { selector: semaSelector(dialogMorfo, 'content', { eventFamily: 'commit' }), haptic: { kind: 'tap' } } // bad — string drifts silently if morfo renames { selector: '[data-dialog-content][data-event-family="commit"]', haptic: { kind: 'tap' } } ``` ## Sema: intent policy per family Per book canon there are **8 families**, encoded in `SEMA_FAMILY_POLICY` at `src/uix/sema/types.ts`. Each family declares two independent axes: ```ts { contact: { intentRequirement: 'optional', intentGuidance: 'discouraged' }, commit: { intentRequirement: 'required', intentGuidance: 'expected' }, signal: { intentRequirement: 'required', intentGuidance: 'expected' }, handle: { intentRequirement: 'optional', intentGuidance: 'contextual' }, emerge: { intentRequirement: 'optional', intentGuidance: 'contextual' }, shift: { intentRequirement: 'optional', intentGuidance: 'contextual' }, sustain: { intentRequirement: 'optional', intentGuidance: 'contextual' }, delegate: { intentRequirement: 'optional', intentGuidance: 'contextual' } } ``` - **`intentRequirement`** (`'required' | 'optional' | 'forbidden'`) — compile-time gate. `required` → `intent` REQUIRED in `MorfoEventSemantic` (discriminated union). `forbidden` is reserved for future use (no family uses it today). - **`intentGuidance`** (`'expected' | 'contextual' | 'discouraged'`) — doctrinal hint, not type-enforced. Drives lint warnings and editor tooltips. Type derivation: `SemaEvent` and `MorfoEventSemantic` discriminate over `intentRequirement`. Editing the const reshapes the discriminated union. Runtime enforcement: `validateSemaEvent` throws when an `intentRequirement: 'required'` family declares an event without intent. Optional families pass silently. **`delegate`** is the 8th family per book cap. 29 — reparto de iniciativa entre usuario y sistema. Structural; carries no intent of its own. Active channels empty (delegate composes with sustain / signal / commit for perceptual layering, doesn't own a base signature). The earlier flat `intentPolicy: 'allowed' | 'expected' | 'optional'` mixed type-requirement with doctrinal guidance. The split was applied in commit `00f0b740`. ## Sema: open channel registry + flat CSS-style cascade The perceptual layer is **NOT 4-channel-closed**. The framework ships 5 canonical channels (`motion`, `sound`, `color`, `presence`, `haptic`) declared in `SemaChannelSignatures`. Apps add more via TypeScript declaration merging. **The semantic tokens are `data-event-*`**: the engine stamps `data-event`, `data-event-family`, `data-event-intent`, `data-event-phase`, `data-event-id` on `signal.target` BEFORE the cascade resolves. Cascade rules use CSS selectors that target those tokens directly — same surface that eidos CSS already reads. Engine owns stamp/unstamp; VisualChannel only owns the hold. Cascade rules are **flat** (no `overrides: { eventLabel: ... }` middle layer): a single selector + per-channel deltas, like a CSS rule. ```ts { selector: '[data-dialog-content][data-event-intent="threat"]', sound: { sampleUrl: '/sounds/dialog-fail.wav' }, haptic: { kind: 'error', pattern: [50, 80, 50, 80, 50] } } ``` Resolution cascade (each layer overrides the previous): ``` 1. SEMA_MAP.families[F].base — per-channel signatures 2. SEMA_MAP.intents[I].deltas — valenced families only; numbers ADD (modificadores) 3. signal.overrides + signal.channels — per-event morfo overrides; numbers REPLACE 4. engineOpts.overrides.runtime — path-based globals; REPLACE 5a. engineOpts.components[].cascade — per-component packs (sema/components/{name}.ts) 5b. engineOpts.overrides.cascade — app-level rules; appended AFTER packs so they win on equal specificity ``` **Per-component packs live in `src/uix/sema/components/{name}.ts`** — symmetric to morfo / soma / eidos. Each exports a `Sema` with `cascade` rules + optional `preloadSamples` (WAVs decoded at boot). **Override semantics — numbers**: - intent.deltas (capa 2): numbers ADD (modificadores compositivos). - overrides (capas 4, 5, 6): numbers REPLACE (`color: red`, not "add red"). - Use `{ op: 'add' }` / `{ op: 'multiply' }` / `{ op: 'replace' }` for explicit semantics either way. **`intent.deltas` owns the perceptual signature of the intent** — the sound primitives `pitch`, `gain`, `contour` (and the haptic `intensity` when shaped per-intent) come from capa 2. **Cascade rules (capas 6a/6b) MUST NOT override these primitives** — doing so flattens the perceptual difference between `risk` / `threat` / `loss` (or `affirm` / `fulfill`). Cascade rules add character (`haptic.kind`, `haptic.pattern`, `sound.sampleUrl`, `motion.easing`, `presence.backdrop`) — not the intent's evaluative profile. If a per-component cascade rule needs to *shift* a numeric primitive (e.g. close-* in emerge goes descending), use `{ op: 'add', value: -150 }` so it composes on top of the intent rather than erasing it. `HapticChannel` (V1, real — Vibration API) is opt-in via `new EngineSemantic({ haptic: true })`. Sound and haptic both default off because they have audible / tactile side effects. Canonical sources: `src/uix/sema/sema-map.ts` (SEMA_MAP value + SemaChannelSignatures registry + Sema), `src/uix/sema/resolver.ts` (cascade implementation + CSS specificity), `src/uix/sema/stamp.ts` (stamp/unstamp), `src/uix/sema/components/*.ts` (per-component packs), `src/uix/sema/README.md`. ## Eidos drift defense — types over lint Selector drift between morfo's emitted attrs and the layers that style / react to them (eidos CSS, sema cascade) is defended **at compile time via typed builders**, not at lint time. - **Sema cascade rules** (`sema/components/*.ts`) consume `semaSelector(morfo, partKebab, matchers?)` from `$uix/morfo`. Renames in morfo break the cascade at type-check time. See "Sema cascade selectors must use the typed builder" above. - **Eidos plain `.css` recipes** are still raw CSS today — there is no CSS-side typed builder. For those, `scripts/eidos-lint.ts` remains as an **opt-in safety net** that classifies each `[data-*]` selector as `morfo-backed` / `eidos-only` / `invalid`. It is not the architectural contract; the contract is the morfo declaration. Rule: when a layer can consume the morfo via TypeScript (anything in `.ts` / `.svelte`), it MUST use the typed builder. Lint is for the remaining surface (plain CSS recipes) until those gain a builder of their own. Hand-written morfo-targeting selector strings in TypeScript files are an architecture violation, not a lint warning. ## Session hand-off — 2026-05-27 (persistence + a11ySemantic + polymorphic) Sprint cerrando los 3 ítems diferidos del hand-off 2026-05-08: persistence + holds-by-intent (libro §6), a11ySemantic per event (libro §9), polymorphic events (libro §5.3). Detalle completo en `src/docs/LIBRO_VARIACIONES_Y_EXTENSIONES.md` D.9 / D.10 / D.11. **Persistence (libro §6.1)** — separa `hold` (duración perceptible mínima) de `persistence` (lifecycle real): - Nuevo tipo `SignalPersistence = 'transient' | 'untilAction' | 'untilFix' | 'stateBound'` en `src/uix/sema/types.ts`, propagado a `SemanticSignal`, `MorfoEventSemantic` y `TriggerOptions`. - Tabla canónica `SEMA_HOLDS_BY_INTENT` en `src/uix/sema/holds.ts` (referencia, NO auto-aplicada — default conservador `'transient'`). - `EngineSemantic.emit()` ahora devuelve `Promise` (el id). Para `persistence !== 'transient'` mantiene la proyección viva pasado el hold; expone `engine.clear(id)`, `engine.clearTarget(target)`, `engine.hasActive(id)`. - `SomaRuntime.trigger()` devuelve `TriggerResult { id?, persistence? }`. Expone `runtime.clearSignal(id)`, `runtime.clearTarget(target)`, `runtime.partRef(part)` (helper que devuelve el HTMLElement registrado de un part — útil para que providers limpien sin trackear ids). - 6 morfos declaran `persistence` explícita: `announce.signal-alert` (untilAction), `dialog/drawer.close-after-fail` (transient explícito), `file-upload.signal-warn-reject` + `form.signal-warn-invalid` (untilFix), `password-field.signal-notify-caps-state` (stateBound). - 3 providers cabledados con `clearTarget(provider)`: form (limpia al validar / reset), file-upload (limpia antes de cada nueva ronda de accept/reject + en remove / clear), password-field (limpia indicator al apagarse caps lock). **a11ySemantic (libro §9.1)**: - Nuevo `MorfoA11ySemantic` interface en `src/uix/morfo/types.ts`: `requiresPersistentTrace?`, `requiresLiveRegion?`, `requiresFocusMove?`, `keyboardEquivalent?`, `reducedMotionFallback?: 'state' | 'text' | 'focus' | 'none'`. Añadido como `a11ySemantic?` opcional en `MorfoEvent`. Pasa por compile a `ActionPlan.a11ySemantic`. - Helper de reduced-motion en `src/arts/adom/reduced-motion.svelte.ts` (paralelo a `viewport.svelte.ts`): `ReducedMotionTracker` reactivo del media query con SSR-safe fallback. Expuesto en `ActiveDom.prefersReducedMotion.matches`. - `ActiveUix.announce(message, priority?, timeout?)`: live region compartida lazy-creada vía `dom.writeNode`. No depende de soma. Polite/assertive son regiones separadas, cleanup en `dispose()`. - `Soma.runtime()` auto-wires `sources.announce = (m, p) => uix.announce(m, p)`. Tests usan fake `announce`. - `SomaRuntime.trigger()` honra a11ySemantic después del emit: live region (con `opts.message`), focus move, reduced-motion fallback (incluye forzar `channels: []` cuando fallback = 'state'). - 6 morfos anotados (los mismos consumidores). **Polymorphic events (libro §5.3)** — ADITIVO sobre el shape concreto: - Morfo declara `family` + `intent` + `verb` como default. Si añade `allowedFamilies: readonly SemaFamily[]`, los providers pueden override la family en `runtime.trigger(name, { semantic: { family, intent?, verb? } })`. El default family del morfo es IMPLÍCITAMENTE allowed. - `isPolymorphicSemantic(semantic)` helper exportado de `$uix/morfo`. - `SomaRuntimePolymorphicError` cuando override no está en allowedFamilies. - Diseño aditivo (no variante separada con `defaultSemantic`) elegido para mantener backwards-compat con 29 morfos existentes y demos que acceden `event.semantic.family` directamente. **Tests**: 671/671 pasan en `src/uix/sema + morfo + soma + src/arts/adom/test`. 4 fallas pre-existentes en `src/uix/contracts.test.ts` (words component translation keys) — confirmadas en baseline antes de mis cambios via `git stash`. **`npm run check`**: 1 error pre-existente en `src/uix/soma/components/command/command-provider.svelte.ts:384` (`Cannot find name 'attrs'`) que ya estaba en main. **Pendientes deliberadamente fuera del sprint** (resueltos en la segunda mitad — ver hand-off 2026-05-27 #2 abajo): - ~~Extender persistence a tags-input + textarea~~ — DONE - ~~Caller messages para announce~~ — DONE (form / file-upload / password-field / dialog / drawer) - ~~Demo polymorphic real~~ — DONE (Dialog refactorizado a polymorphic close) ## Session hand-off — 2026-05-27 #2 (messages + persistence ext + Dialog polymorphic) Segunda mitad del sprint cerrando el backlog del hand-off anterior. 3 grupos de cambios. **M — Caller messages para live region**: Cada provider que dispara un evento con `a11ySemantic.requiresLiveRegion` ahora pasa `opts.message` con texto localizado. La live region (ya cabledada via `Soma.runtime() → sources.announce`) finalmente anuncia algo: - `form-provider`: `signal-warn-invalid` → "1 form error..." / "{N} form errors..." (count = `Object.keys(form.issues).length`). Usa `FORM_LANGS.ERROR_SUMMARY_*`. - `file-upload-provider`: `signal-warn-reject` → "1 file was rejected" / "{N} files were rejected". Nuevas entradas `FILE_UPLOAD_LANGS.REJECT_SUMMARY_SINGLE/MULTI` + langs catalog. - `password-field-provider`: `signal-notify-caps-state` → "Caps Lock is on" (usa `PASSWORD_FIELD_LANGS.CAPS_WARNING` que ya existía). - `dialog-provider.dismissWith(action, { message })`: signatura aceptando `message` opcional, forwarded al trigger. - `drawer-provider.dismissWith(action, { message })`: igual que dialog. **P — Persistence extendido a tags-input + textarea**: Misma doctrina §6.2 — signal+risk = untilFix: - `tags-input.signal-warn-reject` → persistence: `untilFix` + a11ySemantic + provider clear via `clearTarget(input)`. Nueva entrada `tags-input.reject-warning` en langs catalog. Provider gana método privado `emitWarnReject(target)` que centraliza clear + emit + message. Successful add también clearea (la adición VÁLIDA es el "fix" de un reject previo). - `textarea.signal-warn-count-overflow` → persistence: `untilFix` + a11ySemantic + provider clear en transición OUT-of-overflow. Nueva entrada `textarea.overflow-warning` en langs catalog. Provider emite SOLO en transición INTO overflow (no en cada keystroke al cap) — el `untilFix` proyecta persistente, no necesita re-anuncio. **POLY — Dialog refactor a polymorphic close**: Caso real de uso del feature de eventos polymorphic (§5.3). 5 eventos `close-*` colapsados en 1 evento `close`: - **Morfo** (`dialog.ts`): un evento `close` con `family: 'emerge', verb: 'close', allowedFamilies: ['emerge', 'commit', 'signal']`. SIN `prewrite` (el provider escribe `data-last-action` imperativamente). - **Validador relajado**: el invariante "cada `values[]` debe ser prewritten por algún event" cayó al sentido único "cada prewrite con value debe estar en `values[]`". Razón: con polymorphism los values pueden ser escritos imperativamente. El comentario en `schema.ts` explica. - **Provider** (`dialog-provider.svelte.ts`): `DISMISS_CAUSES` mapa de acciones a `{ lastAction, semantic }`. `dismissWith(action, opts)` traduce a `dom.apply` + `runtime.trigger('close', { semantic, ... })`. `triggerClose` es privado ahora. - **Cascade sema** (`sema/components/dialog.ts`): selectores `eventName: 'close-dismiss-outside'` → `eventName: 'close', state: { attr: 'data-last-action', value: 'dismissed-outside' }`. `eventNamePrefix: 'close-'` → `eventName: 'close', eventFamily: 'emerge'`. - **Eidos CSS** (`dialog.css`): NO cambió — ya leía `data-last-action` para tintar, no nombres de evento. - **API pública preservada**: consumidores externos no notan diferencia (`dismissWith` mantiene firma + comportamiento observable). - **Drawer / Popover**: NO refactorizados. Mismo patrón aplicable; defer porque cada uno requiere reescribir su cascade + decisión separada de timing. **Tests**: - `npx vitest run src/uix/sema src/uix/morfo src/uix/soma src/arts/adom/test`: **679/679 pass**. - `npm run check`: 1 error pre-existente (`command-provider.svelte.ts:384` — ya en baseline). - 4 fallas pre-existentes en `src/uix/contracts.test.ts` (words translations) — no relacionadas. **Pendientes a futuro** (cubierto en hand-off 2026-05-27 #3 abajo): - ~~Refactor analógico de Drawer + Popover a polymorphic close~~ — DONE - Otros `signal.warn-*` no incluidos: hay variantes con `verb: 'warn'` que podrían también beneficiarse de persistence untilFix (revisar caso por caso). ## Session hand-off — 2026-05-27 #3 (Drawer + Popover polymorphic close) Tercera tanda del sprint — completa el rollout del patrón polymorphic close (book §5.3) a los dos componentes hermanos de Dialog. Mismo refactor aplicado de forma sistemática. **Drawer**: - Morfo: 5 close-* → 1 polymorphic `close` con `family: 'emerge', allowedFamilies: ['emerge', 'commit', 'signal']`, sin prewrite. Drag events (drag-start / drag-progress / drag-end / resize) intactos. - Provider: `DISMISS_CAUSES` map + `dismissWith(action, opts?)` traduce a `dom.apply(data-last-action)` + `runtime.trigger('close', { semantic })`. `triggerClose` privado. 4 callsites internos migrados a `dismissWith` (Content escape, Content keydown escape, Overlay click-outside, Close button). - Sema cascade (`sema/components/drawer.ts`): selector `eventNamePrefix: 'close-'` → `eventName: 'close', eventFamily: 'emerge'`. - Test (`drawer-provider.svelte.test.ts`): `triggerClose('close-dismiss', ...)` → `dismissWith('dismiss')`; expect name='close'. **Popover**: - Morfo: igual refactor. 5 close-* → 1 polymorphic `close`. - Provider: igual patrón con DISMISS_CAUSES + dismissWith. Internal callsites (5 lugares: scheduleHoverClose, trigger toggle click, trigger toggle keydown, escape, outside) migrados a `dismissWith`. Close button: usa el prop `action` traducido a dismissWith. - Sema cascade (`sema/components/popover.ts`): mismo patrón — `eventName: 'close'` + `state: { attr: 'data-last-action', value: 'dismissed-outside' }` para la regla de dismiss-outside passive. - Test (`popover-provider.svelte.test.ts`): no había refs a close-* directos. Ningún cambio necesario. **Test fixtures migrados antes del refactor**: - `compile.test.ts`: prewrite test usaba `drawerMorfo` → ahora usa `colorPickerMorfo`. - `runtime.svelte.test.ts`: prewrite test usaba `drawerMorfo` → ahora usa `colorPickerMorfo`. **Resultado**: - 3 componentes overlay (Dialog / Drawer / Popover) usan el patrón polymorphic close de forma consistente. - 5 picker components (color-picker / date-picker / date-range-picker / time-picker / time-range-picker) mantienen el shape per-event. Son la canonical reference para `prewrite` declarativo + sirven como fixtures de tests. - API pública intacta — `dismissWith` mantiene signatura en los tres overlays. - Eidos CSS no tocó (ya leía `data-last-action`). **Tests**: 680/680 pass en `src/uix/sema + morfo + soma + src/arts/adom/test`. `npm run check`: 1 error pre-existente. **Pendientes a futuro** (picker family cubierto en hand-off 2026-05-27 #4): - ~~Picker family refactor~~ — DONE - Otros `signal-warn-*` no cubiertos (textarea / tags-input ya hechos; revisar caso por caso si emergen más). ## Session hand-off — 2026-05-27 #4 (Picker family polymorphic close) Cuarta tanda — completa el rollout polymorphic close al picker family (5 componentes). Más decoupling de tests. **Refactor de los 5 pickers**: - `color-picker`: 4 close-* → 1 polymorphic `close` - `date-picker`: 4 close-* → 1 polymorphic `close` - `date-range-picker`: 4 close-* (incl. `close-range-commit`) → 1 polymorphic `close` - `time-picker`: 4 close-* → 1 polymorphic `close` - `time-range-picker`: 4 close-* → 1 polymorphic `close` Cada morfo: `family: 'emerge', verb: 'close', target: v.partRef('calendar'|'clock'|'content'), persistence: 'transient', allowedFamilies: ['emerge', 'commit', 'signal']`. **Hallazgo importante**: los picker providers NO disparan los close events. Solo togglean `opts.open = false`. Los eventos estaban declarados pero **inertes** — su único consumidor era el schema validator y los compiler tests. El refactor es alineación doctrinal, no de comportamiento. **Sema cascade**: solo `color-picker` tiene un sema pack y no referenciaba close-* (solo handle-*). Cero updates necesarios en cascades. **Test fixtures decoupling**: - Nuevo `src/uix/morfo/test-fixtures.ts` con `prewriteFixtureMorfo` sintético. - `compile.test.ts`: migrado de `colorPickerMorfo` → `prewriteFixtureMorfo`. - `runtime.svelte.test.ts`: igual migración. - Beneficio: los tests validan el contrato del compiler/runtime sin depender del catálogo de componentes. Futuros refactors del catálogo no rompen estos tests. **Estado final del rollout polymorphic** (3 sprints combinados): | Componente | Polymorphic morfo | Provider cabledado | |---|---|---| | Dialog | ✓ | ✓ (`dismissWith` → `runtime.trigger`) | | Drawer | ✓ | ✓ | | Popover | ✓ | ✓ | | color-picker | ✓ | ✗ (toggle opts.open) | | date-picker | ✓ | ✗ | | date-range-picker | ✓ | ✗ | | time-picker | ✓ | ✗ | | time-range-picker | ✓ | ✗ | Único morfo con shape pre-polymorphic restante: el fixture sintético `prewriteFixtureMorfo` — vivo sólo para tests. **Tests**: 680/680 pass. `npm run check`: 1 error pre-existente. **Pendiente a futuro**: - Cabledar `runtime.trigger('close', { semantic })` en los providers picker cuando justifique disparar el evento (telemetría, sound on commit picker, etc.). ## Session hand-off — 2026-05-09 (selector discipline + lint reframing) - **Typed selector builder applied** in `src/uix/sema/components/dialog.ts`. Every cascade rule now constructs its selector via `semaSelector(dialogMorfo, 'content', matchers?)` instead of hand-writing strings. The helper lives in `src/uix/morfo/selectors.ts` and is exported from `$uix/morfo`. - **Eidos-lint reframed** from "architectural drift mechanism" to "opt-in safety net for plain CSS recipes". The architectural mechanism is compile-time typing. The lint script + scripts continue to work unchanged but are no longer the doctrinal source of safety. - **Intent.deltas signature ownership documented** — cascade rules MUST NOT override `pitch` / `gain` / `contour` on `sound`, because doing so collapses the per-intent perceptual difference. Captured both inline in the dialog cascade (NOTE comment on the intent block) and in the "Override semantics — numbers" rule above. - **Documentation updated**: - `src/uix/morfo/README.md` — new "Typed selector builder — `semaSelector`" section. - `src/uix/eidos/README.md` — replaced "Linter" section with "Defensa contra drift de selectores" framing the two-tier defense (compile-time builder for TS/Svelte, opt-in lint for plain CSS). - `src/uix/sema/README.md` — already documented the builder + the cascade discipline + the SEMA_FAMILY_POLICY shape (from the prior refactor). **Verification at hand-off**: `npm run check` reports 0 errors, 2 pre-existing Svelte warnings unrelated to this work. `npm run test` reports 1883/1883 tests green across 154 files (~22s). ## Session hand-off — 2026-05-08 (post-canon alignment) The codebase has been aligned to the canonical guide. Most recent architectural move: - **Runtime relocated + renamed** — what was `MorfoRuntime` in `src/uix/morfo/runtime.svelte.ts` is now `SomaRuntime` in `src/uix/soma/runtime.svelte.ts`. Reason: morfo must remain pure declarative TypeScript (DNA); the runtime imports Svelte runes, `$adom`, `$uix/sema`, and `$libs/reactive` — those are soma's territory. Per the doctrine "morfo declares, soma executes" the runtime now lives where it conceptually belongs. Active-uix's `uix.runtime(morfo, sources)` factory stays unchanged; it imports `createSomaRuntime` from `$soma`. - **Sema is now genuinely ornamental** — `ActiveUix.semantic` returns `EngineSemantic | undefined` (no longer throws). `SomaRuntime.trigger()` skips the `emit` step and the target-resolution check when no engine is present. Components stay functional in SSR / headless tests / audio-disabled environments. The earlier swept phases (still in effect): - **Sema verbs** (`src/uix/sema/verbs.ts`): restructured to family-keyed `Record`. `select`/`toggle`/`acknowledge` moved to `commit`; `edit` removed from `handle` (now expressed as `shift.enter-mode`). New verbs added per the canon (tap, focus, release, complete, restore, remind, route, step, syncing, etc.). `validateEventName` recognises both `{verb}-{variant}` and `{family}-{verb}` shapes. - **Morfo event shape** (`src/uix/morfo/types.ts`): `target` moved inside `semantic`; added optional `verb` (advisory, validated against `SEMA_VERBS[family]`) and `sequence: 'pre' | 'coincident' | 'post'`. All 22 events across toggle/toast/popover/drawer/dialog refactored to the new shape. - **Color tokens** (`src/uix/eidos/themes/base/{light,dark}.css`): doctrinal 8 tokens — `primary, secondary, neutral, affirm, fulfill, risk, threat, loss`. Renamed `success → fulfill`, `warning → risk`, `danger → threat` (same hex). New primitives for `secondary` (slate-blue), `affirm` (teal-mint, low activation), `loss` (deep violet-grave, posterior). `info` palette deleted entirely. Legacy aliases in `_static.css` deleted (clean cut, no transition). 29 consumers (recipe CSS + tokens) migrated to doctrinal names. Earlier in the session: - **Toggle is the eidos pilot** (`src/uix/eidos/components/toggle/`): recipe + Svelte wrapper + types + index + README. The pattern is documented in `src/uix/eidos/components/README.md`. - **Eidos has a shared lib** at `src/uix/eidos/lib/types.ts`. First type is `Size` (`'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'full'`). Components narrow with `Extract` per the per-component-subset doctrine. - **No `Eidos` prefix on types**. The path `$uix/eidos/components/{x}` already conveys layer. Public types are `ToggleProps`, `ToggleVariant`, `ToggleSize`. - **API doctrine**: soma stays compound (`Toggle.Provider`); eidos exports both `default` and `Provider` for single-part components so `` works for the ergonomic case while `` stays available. - **SoundChannel eager-init landed** in `src/uix/sema/chans/sound.ts` to fix the autoplay race: the AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener on `document`). - **Demo page (`web/routes/toggle/+page.svelte`)** has the live preview rendered ALWAYS above the tablist (so Sema-tab Play buttons can fire on the real toggle) and the motion preview amplifies scale ×8 visually only — doctrinal values stay in the `
`. **Mandatory checklist before any eidos component migration**: 1. Recover the air baseline from git: ``` git ls-tree -r 0a391408^ --name-only | Select-String "air.*{name}" git show 0a391408^:src/uix/air/components/{name}/types.ts git show 0a391408^:src/uix/air/components/{name}/{name}.svelte ``` 2. Inventory air's props, slots, effects, perceptual emits. 3. Build a comparison table: feature × (air had it | canon allows it | eidos exposes it). Flag each gap as `regression`, `enhanced`, `dropped-by-canon`, or `nuevo`. 4. Present the table BEFORE coding. Get explicit per-gap decision. 5. Only then write the wrapper. Switch was migrated WITHOUT this discipline and shipped a regression (no `ResponsiveProp` for breakpoint-aware sizing). Don't repeat. Next concrete steps: 1. Migrate `collapsible → dialog → drawer → popover → toast → avatar` to the eidos wrapper pattern. **For each: walk the air-baseline checklist above first.** Toggle / switch are templates for the subdirectory shape, but the per-component prop surface must come from air's recovery, not from copying toggle's props. 2. **Persistence + holds-by-intent** (guide §6) — defer until the first concrete `signal.warn` / `signal.alert` consumer appears. Today every signal is `transient` with a numeric hold. Need to add `persistence: 'transient' | 'untilAction' | 'untilFix' | 'stateBound'` to `SemanticSignal` + the holds-by-intent table per the canon. 3. **a11ySemantic per event** (guide §9) — defer until there's a reduced-motion runtime helper + live-region helper to consume the contract. 4. **Polymorphic events** (guide §5.3, `allowedFamilies` + `defaultSemantic`) — defer; no current component needs polymorphism. 5. The docs site's topbar prefs (sound mute toggle, theme switcher) are wired up but the sound mute does NOT yet drive `SoundChannel.masterGain`. Wiring belongs on layout.svelte's `$effect`. Baseline `npm run check` shows 39 pre-existing errors in test files unrelated to this work. Tests are 217/217 green across uix. ## Session hand-off — 2026-05-26 (sema canon + soma emission + READMEs) Sprint completing the alignment of the project with the book *Diseñando lo que ocurre*. Nine commits closed P1 #3, P1 #4 and P1 #5 of the audit-codex: ``` 504780ad expression field + sema coverage check 2d562f37 11 sema packs + doctrina samples (D.5/D.6/D.7) 826ca2bf D.8 channels scope doctrine 43b15372 picker-shell INTERNAL (P1 #5 closed) 66318c47 tree-view target → branch for emerge events adf89a08 batch A — soma emission for 7 packs-shipped components 0784cae5 batch B — 8 components with sema scope, no pack cbf76b66 batch C — drag-drop + virtual-list/grid 7afa7057 23 component READMEs with Sema events sections ``` **Architectural deltas from this sprint:** - **`Morfo.expression`** new field (`'pack' | 'family-default' | 'delegated' | 'none'`) declares how each morfo with declared events materialises its perceptual signature. Schema-validated; lint enforces coverage. See `src/uix/morfo/types.ts` + `LIBRO_VARIACIONES_Y_EXTENSIONES.md` D.4. - **`SemaFamily` is now 8** (added `delegate` per book cap. 29). `SEMA_FAMILY_POLICY` carries two axes (`intentRequirement` + `intentGuidance`) — see updated section above. - **`SEMA_VERBS`** extended with `signal.inform`, `commit.unselect`, contextual verbs (`apply`, `partial`, `block`, `move`, `upload`, `acknowledge`, `confirm`). - **38 sema packs** under `src/uix/sema/components/*.ts` compose 9 canonical tuning profiles (`form.commit.soft/subtle`, `form.toggle.silent`, `tooltip.silent`, `emerge.soft`, `emerge.exit/.soft`, `emerge.medium`, `emerge.dismiss.passive`, `tabs.select.soft`). Apps override at `defineEngineSemantic({ overrides: { cascade: [...] } })`. - **Soma emission cabled** in 18 components (toggle-group, menubar, navigation-menu, dropdown-menu, context-menu, tree-view, tree-grid, listbox, grid-list, table, feed, command, carousel, announce, clipboard, drag-drop, virtual-list, virtual-grid). Pattern: emit from the central state-mutator with `fallbackTarget` resolving the specific element so the cascade matches the right instance. - **`handle-scroll*` intentionally NOT emitted** in virtual-list / virtual-grid — family.handle activates haptic only; emitting on every pixel of scroll would buzz the device nonstop. Apps that want scroll-feedback wire their own throttled emit. Documented in both READMEs. - **picker-shell relocated** from `src/uix/morfo/components/` to `src/uix/morfo/internal/` — declares its INTERNAL status by file location. Apps never write ``; the five composite pickers (date / date-range / time / time-range / color) re-export the shell parts under their own namespace. README added at `src/uix/eidos/components/picker-shell/README.md`. - **`LIBRO_VARIACIONES_Y_EXTENSIONES.md`** is the authoritative registry for project decisions vs the book canon. Sections D.4 (`expression` field), D.5 (toggles soft-tuned packs), D.6 (menus/trees packs), D.7 (samples doctrine — `SOUND_LIBRARY` = resources, `SOUND_TUNINGS` = canon, packs compose tunings never samples; `sampleOverlay` rejected permanently), D.8 (channels scope — `sound` + `haptic` are the only canonical runtime channels; ARIA structural lives in morfo, ARIA dynamic in soma, visual in eidos — never canonized as channels). - **23 soma component READMEs** now include a `## Sema events` table with event / family / verb / target / intent / when + pack reference + doctrinal notes for the verb-rename corrections applied across the sprint. **Verification at hand-off**: `npm run morfo:vocabulary` EXIT 0 (clean). `npx vitest run src/uix/sema src/uix/morfo` 195/195. `npx vitest run src/uix/soma` 419/419. Pre-existing words/* type errors unchanged (separate dev track). **What's left from the audit-codex**: P2 (34 demos NEEDS-WORK) — explicitly out of scope (full refactor of demos + web routes pending separately). --- # Behavioral Guidelines These reduce common LLM coding mistakes. They bias toward caution over speed — for trivial tasks, use judgment. ## 1. Think before coding Don't assume. Don't hide confusion. Surface tradeoffs. - State assumptions explicitly. If uncertain, ask. - If multiple interpretations exist, present them — don't pick silently. - If a simpler approach exists, say so. Push back when warranted. - If something is unclear, stop. Name what's confusing. Ask. ## 2. Simplicity first Minimum code that solves the problem. Nothing speculative. - No features beyond what was asked. - No abstractions for single-use code. - No "flexibility" or "configurability" that wasn't requested. - No error handling for impossible scenarios. - If you write 200 lines and it could be 50, rewrite it. ## 3. Surgical changes Touch only what you must. Clean up only your own mess. - Don't "improve" adjacent code, comments, or formatting. - Don't refactor things that aren't broken. - Match existing style, even if you'd do it differently. - If you notice unrelated dead code, mention it — don't delete it. When your changes create orphans, remove the imports / variables / functions your changes made unused. Don't remove pre-existing dead code unless asked. ## 4. Goal-driven execution Define success criteria. Loop until verified. - "Add validation" → "Write tests for invalid inputs, then make them pass." - "Fix the bug" → "Write a test that reproduces it, then make it pass." - "Refactor X" → "Ensure tests pass before and after." Strong success criteria let you loop independently. Weak criteria require constant clarification.