42 KiB
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 layerssrc/lib/,src/uix/terra/,src/uix/air/and the oldsrc/routes/test tree were removed during the UIX refactor. The new ecosystem lives insrc/arts/,src/libs/,src/svrs/, andsrc/uix/{active-uix,soma,sema,eidos,morfo}. Demos are being re-authored underweb/routes/from scratch.
Build / Test Commands
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.jsdynamicCompileOptions). - 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+ optionalkeyboardevents[]— name +targetpartRef +semantic+ optionalprewrite+commitsfocus— 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 <component> 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 checkand 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/daysdirectly). - Provider naming: child providers reference the parent as
provider, neverroot. The exported component is<Xxx.Provider>.createAttrsstill uses the part kebab'provider'internally. - Data-attr naming:
data-{component}(provider) anddata-{component}-{kebab}(sub-parts). Neverdata-soma-*. The compiler emits these viacompiled.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-…. Migratingair-baselines means stripping theair-prefix, not replacing it. - DOM helpers:
$adomis the single public surface for DOM-related functionality at component / soma level. It re-exports all of$libs/domwholesale, plus the reactivecreateActiveDomruntime and stateful primitives (BodyScrollLock,DOMContext,RovingFocusGroup). Components and soma NEVER import from$libs/domdirectly — consume$adom. Onlyarts/adom/*(the implementation),arts/frontend(peer service fallback), and tests of$libs/dommay reach into it. This includes responsive types (ResponsiveProp<T>,Breakpoint,Breakpoints) — they travel with the runtime, so they live in$adom. - Iframe / popup correctness: when you have a
uix.dom, preferuix.dom.getDocument(node?)anduix.dom.getWindow(node?)over the bare functions of the same name. The instance methods respect the active-dom'stargetWindow; the bare functions fall back to the globaldocument/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
createActiveUixorattachActiveUix. - NEVER skip hooks (
--no-verify) or bypass signing unless explicitly asked. If a hook fails, fix the root cause. $reactivesurvives,$libdoes not. The$reactivealias points to the currentsrc/libs/reactive; the singular$libalias 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.
Per-layer references:
src/uix/morfo/README.mdsrc/uix/sema/README.mdsrc/uix/soma/SOMA_ARCHITECTURE.md+src/uix/soma/COMPONENT_GUIDE.mdsrc/uix/eidos/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.
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
partKebabdoesn't exist onmorfo.parts. - Compile-error if
matchers.eventNamedoesn't matchmorfo.events. - Type-checks
eventFamily/eventIntentagainst 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.
// 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:
{
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→intentREQUIRED inMorfoEventSemantic(discriminated union).forbiddenis 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.
{
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) consumesemaSelector(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
.cssrecipes are still raw CSS today — there is no CSS-side typed builder. For those,scripts/eidos-lint.tsremains as an opt-in safety net that classifies each[data-*]selector asmorfo-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'ensrc/uix/sema/types.ts, propagado aSemanticSignal,MorfoEventSemanticyTriggerOptions. - Tabla canónica
SEMA_HOLDS_BY_INTENTensrc/uix/sema/holds.ts(referencia, NO auto-aplicada — default conservador'transient'). EngineSemantic.emit()ahora devuelvePromise<string>(el id). Parapersistence !== 'transient'mantiene la proyección viva pasado el hold; exponeengine.clear(id),engine.clearTarget(target),engine.hasActive(id).SomaRuntime.trigger()devuelveTriggerResult { id?, persistence? }. Exponeruntime.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
persistenceexplí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
MorfoA11ySemanticinterface ensrc/uix/morfo/types.ts:requiresPersistentTrace?,requiresLiveRegion?,requiresFocusMove?,keyboardEquivalent?,reducedMotionFallback?: 'state' | 'text' | 'focus' | 'none'. Añadido comoa11ySemantic?opcional enMorfoEvent. Pasa por compile aActionPlan.a11ySemantic. - Helper de reduced-motion en
src/arts/adom/reduced-motion.svelte.ts(paralelo aviewport.svelte.ts):ReducedMotionTrackerreactivo del media query con SSR-safe fallback. Expuesto enActiveDom.prefersReducedMotion.matches. ActiveUix.announce(message, priority?, timeout?): live region compartida lazy-creada víadom.writeNode. No depende de soma. Polite/assertive son regiones separadas, cleanup endispose().Soma.runtime()auto-wiressources.announce = (m, p) => uix.announce(m, p). Tests usan fakeannounce.SomaRuntime.trigger()honra a11ySemantic después del emit: live region (conopts.message), focus move, reduced-motion fallback (incluye forzarchannels: []cuando fallback = 'state').- 6 morfos anotados (los mismos consumidores).
Polymorphic events (libro §5.3) — ADITIVO sobre el shape concreto:
- Morfo declara
family+intent+verbcomo default. Si añadeallowedFamilies: readonly SemaFamily[], los providers pueden override la family enruntime.trigger(name, { semantic: { family, intent?, verb? } }). El default family del morfo es IMPLÍCITAMENTE allowed. isPolymorphicSemantic(semantic)helper exportado de$uix/morfo.SomaRuntimePolymorphicErrorcuando 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 accedenevent.semantic.familydirectamente.
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— DONECaller 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). UsaFORM_LANGS.ERROR_SUMMARY_*.file-upload-provider:signal-warn-reject→ "1 file was rejected" / "{N} files were rejected". Nuevas entradasFILE_UPLOAD_LANGS.REJECT_SUMMARY_SINGLE/MULTI+ langs catalog.password-field-provider:signal-notify-caps-state→ "Caps Lock is on" (usaPASSWORD_FIELD_LANGS.CAPS_WARNINGque ya existía).dialog-provider.dismissWith(action, { message }): signatura aceptandomessageopcional, 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 viaclearTarget(input). Nueva entradatags-input.reject-warningen langs catalog. Provider gana método privadoemitWarnReject(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 entradatextarea.overflow-warningen langs catalog. Provider emite SOLO en transición INTO overflow (no en cada keystroke al cap) — eluntilFixproyecta 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 eventocloseconfamily: 'emerge', verb: 'close', allowedFamilies: ['emerge', 'commit', 'signal']. SINprewrite(el provider escribedata-last-actionimperativamente). - Validador relajado: el invariante "cada
values[]debe ser prewritten por algún event" cayó al sentido único "cada prewrite con value debe estar envalues[]". Razón: con polymorphism los values pueden ser escritos imperativamente. El comentario enschema.tsexplica. - Provider (
dialog-provider.svelte.ts):DISMISS_CAUSESmapa de acciones a{ lastAction, semantic }.dismissWith(action, opts)traduce adom.apply+runtime.trigger('close', { semantic, ... }).triggerClosees privado ahora. - Cascade sema (
sema/components/dialog.ts): selectoreseventName: '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íadata-last-actionpara tintar, no nombres de evento. - API pública preservada: consumidores externos no notan diferencia (
dismissWithmantiene 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 converb: '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
closeconfamily: 'emerge', allowedFamilies: ['emerge', 'commit', 'signal'], sin prewrite. Drag events (drag-start / drag-progress / drag-end / resize) intactos. - Provider:
DISMISS_CAUSESmap +dismissWith(action, opts?)traduce adom.apply(data-last-action)+runtime.trigger('close', { semantic }).triggerCloseprivado. 4 callsites internos migrados adismissWith(Content escape, Content keydown escape, Overlay click-outside, Close button). - Sema cascade (
sema/components/drawer.ts): selectoreventNamePrefix: '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 propactiontraducido 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 usabadrawerMorfo→ ahora usacolorPickerMorfo.runtime.svelte.test.ts: prewrite test usabadrawerMorfo→ ahora usacolorPickerMorfo.
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
prewritedeclarativo + sirven como fixtures de tests. - API pública intacta —
dismissWithmantiene 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 polymorphicclosedate-picker: 4 close-* → 1 polymorphicclosedate-range-picker: 4 close-* (incl.close-range-commit) → 1 polymorphicclosetime-picker: 4 close-* → 1 polymorphicclosetime-range-picker: 4 close-* → 1 polymorphicclose
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.tsconprewriteFixtureMorfosintético. compile.test.ts: migrado decolorPickerMorfo→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 viasemaSelector(dialogMorfo, 'content', matchers?)instead of hand-writing strings. The helper lives insrc/uix/morfo/selectors.tsand 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/contouronsound, 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
MorfoRuntimeinsrc/uix/morfo/runtime.svelte.tsis nowSomaRuntimeinsrc/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'suix.runtime(morfo, sources)factory stays unchanged; it importscreateSomaRuntimefrom$soma. - Sema is now genuinely ornamental —
ActiveUix.semanticreturnsEngineSemantic | undefined(no longer throws).SomaRuntime.trigger()skips theemitstep 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-keyedRecord.select/toggle/acknowledgemoved tocommit;editremoved fromhandle(now expressed asshift.enter-mode). New verbs added per the canon (tap, focus, release, complete, restore, remind, route, step, syncing, etc.).validateEventNamerecognises both{verb}-{variant}and{family}-{verb}shapes. - Morfo event shape (
src/uix/morfo/types.ts):targetmoved insidesemantic; added optionalverb(advisory, validated againstSEMA_VERBS[family]) andsequence: '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. Renamedsuccess → fulfill,warning → risk,danger → threat(same hex). New primitives forsecondary(slate-blue),affirm(teal-mint, low activation),loss(deep violet-grave, posterior).infopalette deleted entirely. Legacy aliases in_static.cssdeleted (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 insrc/uix/eidos/components/README.md. - Eidos has a shared lib at
src/uix/eidos/lib/types.ts. First type isSize('xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'full'). Components narrow withExtract<Size, 'sm' | 'md' | 'lg'>per the per-component-subset doctrine. - No
Eidosprefix on types. The path$uix/eidos/components/{x}already conveys layer. Public types areToggleProps,ToggleVariant,ToggleSize. - API doctrine: soma stays compound (
Toggle.Provider); eidos exports bothdefaultandProviderfor single-part components so<Toggle>works for the ergonomic case while<Toggle.Provider>stays available. - SoundChannel eager-init landed in
src/uix/sema/chans/sound.tsto fix the autoplay race: the AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener ondocument). - 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<dl>.
Mandatory checklist before any eidos component migration:
- 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 - Inventory air's props, slots, effects, perceptual emits.
- Build a comparison table: feature × (air had it | canon allows it |
eidos exposes it). Flag each gap as
regression,enhanced,dropped-by-canon, ornuevo. - Present the table BEFORE coding. Get explicit per-gap decision.
- Only then write the wrapper.
Switch was migrated WITHOUT this discipline and shipped a regression
(no ResponsiveProp<Size> for breakpoint-aware sizing). Don't repeat.
Next concrete steps:
- Migrate
collapsible → dialog → drawer → popover → toast → avatarto 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. - Persistence + holds-by-intent (guide §6) — defer until the
first concrete
signal.warn/signal.alertconsumer appears. Today every signal istransientwith a numeric hold. Need to addpersistence: 'transient' | 'untilAction' | 'untilFix' | 'stateBound'toSemanticSignal+ the holds-by-intent table per the canon. - a11ySemantic per event (guide §9) — defer until there's a reduced-motion runtime helper + live-region helper to consume the contract.
- Polymorphic events (guide §5.3,
allowedFamilies+defaultSemantic) — defer; no current component needs polymorphism. - 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.expressionnew field ('pack' | 'family-default' | 'delegated' | 'none') declares how each morfo with declared events materialises its perceptual signature. Schema-validated; lint enforces coverage. Seesrc/uix/morfo/types.ts+LIBRO_VARIACIONES_Y_EXTENSIONES.mdD.4.SemaFamilyis now 8 (addeddelegateper book cap. 29).SEMA_FAMILY_POLICYcarries two axes (intentRequirement+intentGuidance) — see updated section above.SEMA_VERBSextended withsignal.inform,commit.unselect, contextual verbs (apply,partial,block,move,upload,acknowledge,confirm).- 38 sema packs under
src/uix/sema/components/*.tscompose 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 atdefineEngineSemantic({ 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
fallbackTargetresolving 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/tosrc/uix/morfo/internal/— declares its INTERNAL status by file location. Apps never write<PickerShell>; the five composite pickers (date / date-range / time / time-range / color) re-export the shell parts under their own namespace. README added atsrc/uix/eidos/components/picker-shell/README.md. LIBRO_VARIACIONES_Y_EXTENSIONES.mdis the authoritative registry for project decisions vs the book canon. Sections D.4 (expressionfield), 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;sampleOverlayrejected permanently), D.8 (channels scope —sound+hapticare 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 eventstable 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.