|
|
# Soma Architecture
|
|
|
|
|
|
Documento de referencia arquitectonica para `src/uix/soma`.
|
|
|
|
|
|
`soma` es la capa de primitives headless del sistema UIX. Implementa
|
|
|
behavior, accesibilidad y composicion de partes; la presentacion visual
|
|
|
es responsabilidad de eidos. Cada componente se describe primero como
|
|
|
**morfo** (contrato declarativo) y soma lo materializa mediante clases
|
|
|
concretas de estado que registran sus partes en `SomaRuntime`.
|
|
|
|
|
|
## 1. Proposito
|
|
|
|
|
|
`soma` existe para dar una base comun sobre la que construir interfaces complejas sin repetir la misma logica de:
|
|
|
|
|
|
- contexto compartido
|
|
|
- control de foco
|
|
|
- teclado y puntero
|
|
|
- `aria-*`
|
|
|
- `data-*`
|
|
|
- sincronizacion de estado
|
|
|
- integracion con servicios transversales
|
|
|
- animaciones de entrada/salida
|
|
|
- posicionamiento flotante
|
|
|
|
|
|
Su objetivo no es ser una capa visual ni de producto. `soma` define primitives reutilizables y predecibles; la capa visual decide el look and feel.
|
|
|
|
|
|
## 2. Arquitectura de capas
|
|
|
|
|
|
```
|
|
|
soma → headless: behavior, accesibilidad, data-* contracts, context, servicios
|
|
|
eidos → visual: tokens, CSS, temas, recipes, reacciones a data-event-*
|
|
|
events → percepcion: sound/haptic/hold y dispatch de ocurrencias semanticas
|
|
|
app → producto: composicion final, contenido, logica de negocio
|
|
|
```
|
|
|
|
|
|
Cada capa tiene responsabilidades estrictas:
|
|
|
|
|
|
### soma aporta
|
|
|
|
|
|
- comportamiento (keyboard, focus, dismiss, scroll lock)
|
|
|
- accesibilidad (ARIA, roles, live regions)
|
|
|
- contratos `data-*` estables y validados
|
|
|
- contexto y composicion de partes
|
|
|
- servicios de runtime (`langs`, `format`, `logger`)
|
|
|
- sistema de animaciones (presence, data-starting/ending-style, onComplete)
|
|
|
- posicionamiento flotante (@floating-ui)
|
|
|
|
|
|
### La capa visual aporta
|
|
|
|
|
|
- apariencia (tokens, colores, tipografia, spacing)
|
|
|
- tono visual (temas light/dark, variantes)
|
|
|
- decisiones de diseno opinionated (sizes, recipes)
|
|
|
- motion CSS y reacciones visuales a `data-event-*`
|
|
|
- responsive design
|
|
|
|
|
|
### La capa visual NUNCA
|
|
|
|
|
|
- importa state classes internas de soma
|
|
|
- depende de estructura DOM incidental
|
|
|
- accede a propiedades privadas
|
|
|
- duplica behavior que soma ya resuelve
|
|
|
- usa `data-*` fuera de los contratos publicados
|
|
|
|
|
|
La frontera es los `data-*` attrs y las CSS variables que soma expone.
|
|
|
|
|
|
## 3. Principios de diseno
|
|
|
|
|
|
### 3.1 El desarrollador no necesita conocer los internos
|
|
|
|
|
|
Los layers, el sistema reactivo, el floating engine — son implementacion interna. El desarrollador de componentes interactua con:
|
|
|
|
|
|
- clases concretas de estado + `SomaRuntime`
|
|
|
- `Soma` class para servicios
|
|
|
- Barrel imports jerárquicos (`import { Dialog } from '$soma/components'`)
|
|
|
- subpaths explicitos cuando necesita helpers publicos (`$soma/provider`, `$soma/keyboard`, `$soma/runtime.svelte`)
|
|
|
|
|
|
### 3.2 Un patron, no tres
|
|
|
|
|
|
Todo componente sigue el mismo patron:
|
|
|
|
|
|
1. State class concreta registra sus partes con `SomaRuntime.part(...)`
|
|
|
2. Wrapper `.svelte` fino convierte props → Active/State
|
|
|
3. Props derivados via `$derived.by` + `runtimePart.assert`
|
|
|
4. Contexto para comunicacion padre-hijo
|
|
|
|
|
|
Los roots sin DOM usan `ProviderOpts` (ref opcional), las partes con DOM usan
|
|
|
`WithRefOpts`. Las unicas excepciones son partes declarativas que pueden vivir
|
|
|
fuera de su provider (`AnnounceRegion`, `FeedSentinel`): si encuentran un
|
|
|
provider reutilizan su `runtime`; si no, crean un runtime propio desde el
|
|
|
`Soma` del scope actual.
|
|
|
|
|
|
### 3.3 Layers como behaviors, no como wrappers
|
|
|
|
|
|
Los layers se instancian en el constructor del Provider y exponen `.props` para merge. No hay nesting de componentes wrapper en template.
|
|
|
|
|
|
```ts
|
|
|
// Correcto: behaviors integrados
|
|
|
readonly focusScope = FocusScope.use({...});
|
|
|
readonly dismissal = Dismissal.use({...});
|
|
|
readonly props = $derived.by(() => this.runtimePart.assert({
|
|
|
...this.runtimePart.props,
|
|
|
...this.focusScope.props,
|
|
|
...this.dismissal.props,
|
|
|
}));
|
|
|
```
|
|
|
|
|
|
```svelte
|
|
|
<!-- Incorrecto: nesting de wrappers (patron terra) -->
|
|
|
<ScrollLock>
|
|
|
<FocusScope>
|
|
|
<DismissibleLayer>
|
|
|
{content}
|
|
|
</DismissibleLayer>
|
|
|
</FocusScope>
|
|
|
</ScrollLock>
|
|
|
```
|
|
|
|
|
|
### 3.4 Soma es una clase, no una configuracion
|
|
|
|
|
|
`Soma` es la identidad runtime del framework. No es un archivo de configuracion — es el objeto raiz que provee servicios via context.
|
|
|
|
|
|
```ts
|
|
|
// En un Provider:
|
|
|
readonly soma = Soma.require();
|
|
|
const dir = this.soma.prefs.getDir();
|
|
|
```
|
|
|
|
|
|
### 3.5 Los data-\* son contrato publico
|
|
|
|
|
|
Los `data-*` attrs son la frontera entre soma y la capa visual. Cambiarlos es breaking change.
|
|
|
|
|
|
Convencion (obligatoria, sin excepciones):
|
|
|
|
|
|
- provider: `data-{component}` (no `data-{component}-provider`, **no `data-soma-*`**)
|
|
|
- parte: `data-{component}-{part}`
|
|
|
- estado: `data-state`, `data-disabled`, `data-side`, `data-align`, `data-orientation`
|
|
|
- animacion: `data-starting-style`, `data-ending-style`
|
|
|
- nesting: `data-nested`, `data-nested-open`
|
|
|
|
|
|
Los nombres los emite el compilador de morfo que consume `SomaRuntime`.
|
|
|
`createAttrs(morfo)` queda como helper tipado para `querySelector` y tooling,
|
|
|
no como sistema de registro ni escritura DOM. Cualquier selector CSS, cadena
|
|
|
en README o snippet debe coincidir exactamente con esos nombres generados. El
|
|
|
validador de contratos (`assertContract`) solo verifica valores enumerados, no
|
|
|
nombres ni presencia — la consistencia de nombres es responsabilidad del autor
|
|
|
del componente (checklist item 27).
|
|
|
|
|
|
### 3.6 La accesibilidad base no se delega
|
|
|
|
|
|
soma resuelve ARIA por defecto. El consumidor no necesita añadir `role`, `aria-modal`, `aria-expanded`, `aria-controls`, etc. — el Provider los genera.
|
|
|
|
|
|
Texto funcional se resuelve via `langs.ts()` con idlangref. El catalogo propio del componente vive en `morfo.translations` y se referencia con `v.translationRef(...)`; texto compartido como close/cancel/save vive en `common.*` y se referencia con `v.commonRef(...)` o un idlangref absoluto. `langs.ts` por componente queda como comodidad opcional para constantes imperativas, no como catalogo canonico.
|
|
|
|
|
|
### 3.7 DOM global via ActiveDom
|
|
|
|
|
|
Soma usa el `ActiveDom` del scope para escrituras gestionadas por UIX,
|
|
|
listeners de `document/window`, queries globales, foco imperativo y scroll de
|
|
|
ventana. No existe fachada `soma/events`: `dom.listen(...)` es la superficie
|
|
|
canonica para registrar listeners con cleanup.
|
|
|
|
|
|
Las lecturas locales de un elemento propio (`contains`, `closest`,
|
|
|
`getBoundingClientRect`, `clientWidth`, `scrollTop`) no se envuelven en
|
|
|
`ActiveDom`; son parte del comportamiento local del componente.
|
|
|
|
|
|
### 3.8 Props documentadas obligatoriamente
|
|
|
|
|
|
Todas las props de todos los componentes llevan JSDoc en `types.ts`. Cada prop: descripcion, `@default`, notas de comportamiento.
|
|
|
|
|
|
### 3.9 Comparacion con referencias
|
|
|
|
|
|
Cada componente se compara con ark-ui, bits-ui y radix-ui antes de implementar. Se documentan las props que otros tienen y soma no, con justificacion.
|
|
|
|
|
|
## 3.bis Arquitectura cerrada (post-2026-04-25)
|
|
|
|
|
|
El reparto de responsabilidades entre Morfo, Soma, Sema y ADom esta cerrado en
|
|
|
seis piezas con responsabilidades disjuntas:
|
|
|
|
|
|
```
|
|
|
Morfo declara
|
|
|
SomaRuntime transcribe (vive en soma/)
|
|
|
Provider aporta sources, targets y handlers
|
|
|
Effects sincronizan attrs derivados
|
|
|
EngineSemantic despacha senales a canales perceptivos
|
|
|
VisualChannel materializa la senal en el DOM (data-event*, hold, cleanup)
|
|
|
ADom aplica mutaciones DOM (commit estructural)
|
|
|
```
|
|
|
|
|
|
### SomaRuntime — la pieza nueva
|
|
|
|
|
|
`SomaRuntime` es la pieza que faltaba entre `Morfo` (declaracion) y
|
|
|
`Provider` (ejecucion). Lee el morfo y produce el comportamiento.
|
|
|
|
|
|
Una instancia por componente:
|
|
|
|
|
|
```ts
|
|
|
readonly soma = Soma.require();
|
|
|
readonly runtime = this.soma.runtime(morfo, {
|
|
|
states: { open: () => this.opts.open.current },
|
|
|
props: { disabled: () => this.opts.disabled.current },
|
|
|
parts: { content: () => this.contentId.current },
|
|
|
events: {
|
|
|
open: () => {
|
|
|
this.opts.open.current = true;
|
|
|
},
|
|
|
'close-cancel': () => {
|
|
|
this.opts.open.current = false;
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
```
|
|
|
|
|
|
API V1:
|
|
|
|
|
|
- `runtime.part(part, opts)` — unica API publica para registrar una parte.
|
|
|
Devuelve el handle (`props`, `resolveProps`, `assert`) y, con
|
|
|
`syncAttrs: true`, sincroniza attrs derivados por morfo via `dom.apply`.
|
|
|
- `runtime.partProps(part)` — devuelve `{ id, ref, marker, data-archetype? }`. Solo identidad estatica (el `data-archetype` es classification cross-component, nunca cambia).
|
|
|
- `runtime.keydown(part, event)` — dispatch de teclas declaradas en `morfo.keyboard`.
|
|
|
- `runtime.trigger(eventName)` — orquesta la secuencia perceptiva + state.
|
|
|
|
|
|
### Provider en el modelo nuevo
|
|
|
|
|
|
El provider deja de tener helpers locales de resolucion de morfo. Solo
|
|
|
aporta:
|
|
|
|
|
|
- getters reactivos para `states`, `props`, `parts`
|
|
|
- handlers sincronos para los `events`
|
|
|
- glue de layers ortogonales (Presence, Dismissal, ScrollLock)
|
|
|
|
|
|
Cada part-provider conserva el handle devuelto por `runtime.part(...)`:
|
|
|
|
|
|
```ts
|
|
|
readonly runtimePart = runtime.part('trigger', {
|
|
|
id: opts.id,
|
|
|
ref: opts.ref,
|
|
|
owner: this,
|
|
|
syncAttrs: true
|
|
|
});
|
|
|
|
|
|
readonly props = $derived.by(() => this.runtimePart.props);
|
|
|
```
|
|
|
|
|
|
### Tres operaciones que cubren todos los escenarios
|
|
|
|
|
|
```ts
|
|
|
// Cambio estructural sin senal
|
|
|
provider.commitState(change);
|
|
|
|
|
|
// Cambio estructural con senal
|
|
|
provider.commitState(change, event);
|
|
|
|
|
|
// Senal sin cambio estructural
|
|
|
provider.emitEvent(event);
|
|
|
```
|
|
|
|
|
|
Internamente:
|
|
|
|
|
|
```ts
|
|
|
async commitState(change, event?) {
|
|
|
if (event) await this.soma.events?.emit(event);
|
|
|
this.soma.dom.apply(change);
|
|
|
}
|
|
|
|
|
|
emitEvent(event) {
|
|
|
void this.soma.events?.emit(event);
|
|
|
}
|
|
|
```
|
|
|
|
|
|
### La secuencia de `runtime.trigger(eventName)`
|
|
|
|
|
|
```
|
|
|
1. prewrite imperativo (transient markers como data-last-action)
|
|
|
2. await events.emit(event)
|
|
|
3. handler sincrono del provider muta state
|
|
|
4. effects derivan y aplican attrs estructurales (data-state, aria-*)
|
|
|
```
|
|
|
|
|
|
Los effects del runtime escuchan los sources reactivos y reaplican attrs cada
|
|
|
vez que el estado cambia. ADom es el unico escritor de attrs mutables.
|
|
|
|
|
|
### Reglas operativas
|
|
|
|
|
|
- `partProps(part)` solo emite identidad estatica. Lo mutable lo escribe ADom.
|
|
|
- Lo que `dom.apply` escribe, Svelte no lo renderiza desde `partProps`.
|
|
|
- Event handlers son sincronos. Async va antes del trigger.
|
|
|
- Guards (`if (disabled) return`) van en el call-site, no dentro del handler.
|
|
|
- `events`/`VisualChannel` pueden usar `ActiveDom` a traves del projector
|
|
|
inyectado; `ActiveDom` no conoce `events`.
|
|
|
- `morfo.events.commits` es descriptivo, no ejecutable. El smoke valida.
|
|
|
|
|
|
### Pilotaje (orden incremental)
|
|
|
|
|
|
1. **Toggle** — primer caso, solo `partProps`.
|
|
|
2. **Collapsible** — anade `keydown`.
|
|
|
3. **Toast** — primer test real de `trigger()` con `intent`.
|
|
|
4. **Dialog** — al final, cuando layers + portal ya esten validados.
|
|
|
|
|
|
Ver tambien:
|
|
|
|
|
|
- [src/uix/morfo/README.md](../morfo/README.md) — declaracion, archetypes, regla 2-de-3
|
|
|
- [src/uix/sema/README.md](../sema/README.md) — contrato de `emit`, vocabulario de verbs
|
|
|
- [src/uix/adom/README.md](../adom/README.md) — `dom.apply`
|
|
|
- [src/uix/eidos/README.md](../eidos/README.md) — qué consume eidos del DOM
|
|
|
- [src/uix/README.md](../README.md) §2.bis — vista cross-layer
|
|
|
|
|
|
### Cross-layer hooks que soma emite por la regla 2-de-3
|
|
|
|
|
|
Soma escribe al DOM no solo lo que necesita; escribe también lo que sema y
|
|
|
eidos van a consumir. La regla "2-de-3" justifica qué entra al morfo y por
|
|
|
tanto qué emite el runtime:
|
|
|
|
|
|
- **`data-archetype`** — emitido por `partProps` cuando la parte declara
|
|
|
archetype. Eidos lo usa para selectores transversales
|
|
|
(`[data-archetype=trigger] { ... }`); sema puede asociar verbs por
|
|
|
archetype.
|
|
|
- **`data-event*`** — emitido por `events.emit` (a través del VisualChannel)
|
|
|
durante un hold configurable (240ms emerge/commit/handle, 600ms alert/sustain
|
|
|
por defecto). Eidos lo usa para tintar transiciones de eventos
|
|
|
(`[data-event^=dismiss]`).
|
|
|
- **`data-{component}` / `data-{component}-{part}`** — los markers
|
|
|
estructurales clásicos. Eidos los usa para selectores per-componente.
|
|
|
|
|
|
## 4. Modelo de componente
|
|
|
|
|
|
La forma base de soma es `Componente.Parte`:
|
|
|
|
|
|
```ts
|
|
|
import { Dialog } from '$soma/components';
|
|
|
|
|
|
Dialog.Provider; // root — crea contexto
|
|
|
Dialog.Trigger; // accion — abre/cierra
|
|
|
Dialog.Content; // contenido — layers integrados
|
|
|
Dialog.Overlay; // fondo — presence
|
|
|
Dialog.Title; // metadata ARIA
|
|
|
Dialog.Description; // metadata ARIA
|
|
|
Dialog.Close; // accion — cierra
|
|
|
```
|
|
|
|
|
|
### Provider (root)
|
|
|
|
|
|
Crea el estado central, lo registra en context, gestiona presence para content y overlay. Puede o no renderizar DOM:
|
|
|
|
|
|
- **Con DOM** (Collapsible, Accordion): usa `WithRefOpts`, renderiza `<div>`
|
|
|
- **Sin DOM** (Dialog, Popover): usa `ProviderOpts`, solo renderiza children
|
|
|
|
|
|
### Subcomponentes
|
|
|
|
|
|
Leen estado del root via `.require()`. No reimplementan logica — derivan props, ARIA, data-\*, events del estado del padre.
|
|
|
|
|
|
### Portal
|
|
|
|
|
|
Componente interno (`components/internal/portal.svelte`). Renderiza hijos en otro nodo DOM. El contexto de Svelte se preserva.
|
|
|
|
|
|
### Picker composition (shared state across providers)
|
|
|
|
|
|
Pickers (`DatePicker`, `DateRangePicker`, `TimePicker`, y futuros `TimeRangePicker`) no reimplementan Popover / Field / Calendar — los **componen** con estado compartido. El root wrapper crea tres (o más) Providers que apuntan a los mismos `writableActive` refs:
|
|
|
|
|
|
```ts
|
|
|
// Root wrapper
|
|
|
const sharedValue = writableActive(() => value, (v) => (value = v));
|
|
|
const sharedPlaceholder = writableActive(() => placeholder, (v) => (placeholder = v));
|
|
|
const sharedOpen = writableActive(() => open, (v) => (open = v));
|
|
|
|
|
|
{Name}PickerProvider.create({ value: sharedValue, placeholder: sharedPlaceholder, open: sharedOpen, ...config });
|
|
|
PopoverProvider.create({ open: sharedOpen, ... });
|
|
|
{Base}FieldProvider.create({ value: sharedValue, placeholder: sharedPlaceholder, ...config });
|
|
|
// Calendar/RangeCalendar/slider providers are created in their own wrapper (DatePicker.Calendar, TimePicker.HourSlider, …)
|
|
|
```
|
|
|
|
|
|
El picker expone **solo** wrappers únicos para `Provider`, `Trigger`, y el puente al calendario/slider. Los demás exports re-exportan desde los componentes compuestos — sus `data-*` nativos (`data-popover-*`, `data-date-field-*`, `data-calendar-*`, `data-slider-*`) siguen siendo la API de estilo autoritativa. El picker sólo añade atributos de identidad (`data-{picker}-trigger`, `data-{picker}-calendar`) en sus propios wrappers.
|
|
|
|
|
|
**Auto-close / auto-anchor**: el PickerProvider expone `handleSelect()` que el wrapper del calendario llama al completarse una selección. Los range pickers re-anclan `placeholder` para que el mes final caiga en la columna más a la derecha visible, evitando mostrar al usuario un mes que no contiene su selección.
|
|
|
|
|
|
Ver A27 en `COMPONENT_GUIDE.md` para el checklist completo.
|
|
|
|
|
|
## 5. Runtime parts
|
|
|
|
|
|
```ts
|
|
|
readonly soma = Soma.require();
|
|
|
readonly runtime = this.soma.runtime(accordionMorfo, {});
|
|
|
const runtimePart = this.runtime.part('provider', {
|
|
|
id: opts.id,
|
|
|
ref: opts.ref,
|
|
|
owner: this,
|
|
|
context: AccordionProvider.ctx
|
|
|
});
|
|
|
|
|
|
readonly props = $derived.by(() =>
|
|
|
runtimePart.assert({
|
|
|
...runtimePart.props,
|
|
|
'data-state': this.state
|
|
|
})
|
|
|
);
|
|
|
```
|
|
|
|
|
|
`SomaRuntime.part()` centraliza lo mecanico de cada parte:
|
|
|
|
|
|
```ts
|
|
|
interface SomaRuntimePart {
|
|
|
readonly attachment: RefAttachment | undefined;
|
|
|
readonly props: Record<string, unknown>;
|
|
|
resolveProps(bindings?): Record<string, unknown>;
|
|
|
assert<P extends Record<string, unknown>>(props: P): P;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
`syncAttrs: true` activa la escritura imperativa por `uix.dom` para partes
|
|
|
cuyos sources ya estan declarados en el runtime. Si un provider sigue
|
|
|
componiendo attrs en render props, no activa `syncAttrs`.
|
|
|
|
|
|
Dos interfaces de opts:
|
|
|
|
|
|
- `ProviderOpts` — `{ id: Active<string>; ref?: State<HTMLElement | null> }` — para roots sin DOM
|
|
|
- `WithRefOpts` — `{ id: Active<string>; ref: State<HTMLElement | null> }` — para parts con DOM
|
|
|
|
|
|
## 6. Layers
|
|
|
|
|
|
`layers/` contiene solo clases de comportamiento (`.svelte.ts`). Son infraestructura consumida por Providers, nunca directamente por el consumidor.
|
|
|
|
|
|
### Inventario
|
|
|
|
|
|
| Layer | API | Responsabilidad |
|
|
|
| ----------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
|
| `Presence` | `new Presence(opts)` | Mount/unmount con animaciones. `isPresent`, `transitionAttrs`, `onComplete`. |
|
|
|
| `FocusScope` | `FocusScope.use(opts)` | Focus trap, loop, auto-focus, restore. Singleton manager con stack. |
|
|
|
| `Dismissal` | `Dismissal.use(opts)` | Escape + click-outside. Registry global. Behaviors: close, ignore, defer. |
|
|
|
| `TextSelection` | `TextSelection.use(opts)` | Previene selection overflow durante drag. |
|
|
|
| `ScrollLock` | `new ScrollLock(initial?, delay?)` | Body scroll lock con refcount. Soporta delay para animaciones. |
|
|
|
| `ResizeObserver$` | `new ResizeObserver$(getter, cb)` | ResizeObserver con lifecycle Svelte. |
|
|
|
| `Floating*` | `FloatingProvider.create()`, `FloatingContent.create(opts)`, etc. | Posicionamiento relativo a anchor via @floating-ui. |
|
|
|
| `Gesture.base` | `Gesture.base(opts)` | Pointer tracking + axis lock + velocity. |
|
|
|
| `Gesture.drag` | `Gesture.drag(opts)` | Base + progress + snap points + dismiss. |
|
|
|
| `Gesture.resize` | `Gesture.resize(opts)` | Base + delta + min/max constraints. |
|
|
|
| `SafePolygon` | `new SafePolygon(opts)` | Hover-gap corridor between trigger↔content. |
|
|
|
|
|
|
`layers/floating/placement.ts` es la fuente unica para `Side`, `Align`,
|
|
|
`Boundary`, `SIDE_OPTIONS` y `ALIGN_OPTIONS`. `floating/types.ts` consume esa
|
|
|
fuente y no importa del runtime `floating.svelte.ts`, evitando ciclos entre
|
|
|
tipos y clases.
|
|
|
|
|
|
### Cobertura P0 actual
|
|
|
|
|
|
Los tests por componente estan creciendo desde las piezas de mayor riesgo. A
|
|
|
2026-05-15 existen tests directos para:
|
|
|
|
|
|
- `$libs/datagrid/table-core.svelte.test.ts` — motor de data grid.
|
|
|
- `$libs/forms/form-core.svelte.test.ts` — motor de formularios + Standard Schema.
|
|
|
- `dialog/dialog-provider.svelte.test.ts` — open/close, fallback target y
|
|
|
eventos morfo.
|
|
|
- `drawer/drawer-provider.svelte.test.ts` — direccion logica, dismiss y modo
|
|
|
persistent.
|
|
|
- `command/command-provider.svelte.test.ts` — filtro, visibilidad, navegación
|
|
|
y selección.
|
|
|
- `combobox/combobox-provider.svelte.test.ts` — selección, `inputValue` y
|
|
|
navegación sin items disabled.
|
|
|
- `select/select-provider.svelte.test.ts` — selección single/multiple y
|
|
|
navegación sin items disabled.
|
|
|
- `popover/popover-provider.svelte.test.ts` — toggle, hover timers y señales
|
|
|
runtime.
|
|
|
- `toast/toaster.svelte.test.ts` — overflow, dismiss/remove y promesas.
|
|
|
- `calendar/calendar-provider.svelte.test.ts` — placeholder, meses, selección
|
|
|
y flags.
|
|
|
- `range-calendar/range-calendar-provider.svelte.test.ts` — placeholder,
|
|
|
orden de endpoints y limites min/max.
|
|
|
- `date-field/date-field-provider.svelte.test.ts` — helpers UI de segmentos,
|
|
|
lectura DOM, navegación por `ActiveDom`, validación y commit segmentado.
|
|
|
- `date-picker/date-picker-provider.svelte.test.ts` — registro runtime y
|
|
|
cierre controlado por `closeOnDateSelect`.
|
|
|
- `date-range-picker/date-range-picker-provider.svelte.test.ts` — registro
|
|
|
runtime y cierre controlado por `closeOnRangeSelect`.
|
|
|
- `time-picker/time-picker-provider.svelte.test.ts` — registro runtime,
|
|
|
valores de placeholder y escritura de sliders al valor compartido.
|
|
|
- `time-range-picker/time-range-picker-provider.svelte.test.ts` — valores por
|
|
|
endpoint, escritura de sliders y cierre al completar rango.
|
|
|
- `time-range-field/time-range-field-provider.svelte.test.ts` — validación de
|
|
|
orden/min/max, validación custom y foco de label via `ActiveDom`.
|
|
|
- `time-field/time-field-provider.svelte.test.ts` — validación, sincronización
|
|
|
de segmentos 12h y commit solo cuando los segmentos renderizados estan
|
|
|
completos.
|
|
|
- `date-range-field/date-range-field-provider.svelte.test.ts` — validación de
|
|
|
orden/min/max, validación custom y foco de label via `ActiveDom`.
|
|
|
- `virtual-list/virtual-list-provider.svelte.test.ts` — cálculo de ventana,
|
|
|
`scrollToIndex` y compensación anti-jump via `ActiveDom`.
|
|
|
- `virtual-grid/virtual-grid-provider.svelte.test.ts` — cálculo de ventana 2D
|
|
|
y `scrollToCell` via `ActiveDom`.
|
|
|
- `number-field/number-field-provider.svelte.test.ts` — parsing localizable,
|
|
|
teclado spinbutton, triggers, props ARIA y scrubber.
|
|
|
- `file-upload/file-upload-provider.svelte.test.ts` — aceptación/rechazo,
|
|
|
dropzone, input oculto, items, progress y acciones remove/clear.
|
|
|
- `color-field/color-field-provider.svelte.test.ts` — commit por segmentos,
|
|
|
edición hex por teclado, selector de formato, input oculto y foco via
|
|
|
`ActiveDom`.
|
|
|
- `color-picker/color-picker-provider.svelte.test.ts` — helpers de canales,
|
|
|
trigger/value/hidden, area 2D, slider de canal y swatches.
|
|
|
- `navigation-menu/navigation-menu-provider.svelte.test.ts` — timers UIX,
|
|
|
enlace item/trigger/content, foco por teclado y props de list/link.
|
|
|
- `tree-grid/tree-grid-provider.svelte.test.ts` — expansión, selección/rango,
|
|
|
filas visibles, navegación y props de row/cell/header/expand trigger.
|
|
|
- `dropdown-menu/dropdown-menu-provider.svelte.test.ts` — trigger open/close,
|
|
|
scoping de items, selección, checkbox/radio groups, submenu y group/separator.
|
|
|
- `context-menu/context-menu-provider.svelte.test.ts` — apertura por
|
|
|
`contextmenu`, anchor virtual, scoping de items, selección, checkbox/radio,
|
|
|
submenu y group/separator.
|
|
|
- `menubar/menubar-provider.svelte.test.ts` — coordinación de menús hermanos,
|
|
|
hover-follow, navegación horizontal y cambio de menú desde content.
|
|
|
- `listbox/listbox-provider.svelte.test.ts` — props ARIA root, selección,
|
|
|
navegación, typeahead, indicador de item y grupos.
|
|
|
- `tree-view/tree-view-provider.svelte.test.ts` — expansión/selección,
|
|
|
navegación root, typeahead, props de ramas/hojas y partes auxiliares.
|
|
|
- `accordion/accordion-provider.svelte.test.ts` — modos single/multiple,
|
|
|
eventos `open/close` por item, navegación de triggers y header/content.
|
|
|
- `checkbox/checkbox-provider.svelte.test.ts` — commits check/uncheck,
|
|
|
indeterminate, hidden input y sincronización con `CheckboxGroup`.
|
|
|
- `radio-group/radio-group-provider.svelte.test.ts` — roving tabindex,
|
|
|
selección síncrona, navegación con auto-select, hidden input y guardas.
|
|
|
- `switch/switch-provider.svelte.test.ts` — `commit-toggle` runtime,
|
|
|
keyboard, hidden input, integración con `FieldProvider` y thumb.
|
|
|
- `toggle/toggle-provider.svelte.test.ts` — `commit-toggle` runtime, attrs DOM
|
|
|
via `ActiveDom`, integración con `FieldProvider` y guardas.
|
|
|
- `toggle-group/toggle-group-provider.svelte.test.ts` — modos single/multiple,
|
|
|
roving tabindex y navegación de items saltando disabled.
|
|
|
- `tabs/tabs-provider.svelte.test.ts` — registros trigger/content, activacion
|
|
|
automatic/manual, navegacion roving, fallback tab stop y partes auxiliares.
|
|
|
- `slider/slider-provider.svelte.test.ts` — contrato provider/range/thumb/tick,
|
|
|
snapping/clamping, drag pointer confirmado y teclado de thumb.
|
|
|
- `progress/progress-provider.svelte.test.ts` y
|
|
|
`meter/meter-provider.svelte.test.ts` — `data-value/data-min/data-max`
|
|
|
emitidos por morfo/runtime, estados derivados e indicador sincronizado.
|
|
|
- `collapsible/collapsible-provider.svelte.test.ts` — refs cruzadas
|
|
|
trigger/content, eventos `expand/collapse` y guardas disabled.
|
|
|
- `clipboard/clipboard-provider.svelte.test.ts` — copia via `uix.clipboard`,
|
|
|
reset con `uix.timers`, labels traducidos/override y errores `onError`.
|
|
|
- `tooltip/tooltip-provider.svelte.test.ts` — delay/close con `uix.timers`,
|
|
|
skip-delay de grupo, focus/blur, disabled y partes trigger/content/arrow.
|
|
|
- `field/field-provider.svelte.test.ts` — wiring de partes/ARIA, input
|
|
|
guardado por disabled/readonly e integración con parent Form.
|
|
|
- `search-field/search-field-provider.svelte.test.ts` — props root/input/clear,
|
|
|
input/submit/clear, foco y OR-merge con Field.
|
|
|
- `pagination/pagination-provider.svelte.test.ts` — rangos/ellipsis, clamp
|
|
|
reactivo, slice, triggers prev/next, item selected y disabled global.
|
|
|
- `breadcrumb/breadcrumb-provider.svelte.test.ts` — label/labelledby,
|
|
|
separator, link current con mirror al item y ellipsis decorativo/interactivo.
|
|
|
- `toolbar/toolbar-provider.svelte.test.ts` — roving focus, focusin, group
|
|
|
single/multiple y separator.
|
|
|
- `stepper/stepper-provider.svelte.test.ts` — conteo/estado de steps,
|
|
|
attrs runtime, selección lineal/no lineal, teclado y prev/next/completed.
|
|
|
- `scroll-area/scroll-area-provider.svelte.test.ts` — medición de overflow,
|
|
|
viewport, visibilidad/timers de scrollbar, track click, thumb y corner.
|
|
|
- `splitter/splitter-provider.svelte.test.ts` — registro de paneles, clamp,
|
|
|
collapse/expand y resize por pointer/teclado con attrs ARIA/data.
|
|
|
- `pin-input/pin-input-provider.svelte.test.ts` — props del input oculto,
|
|
|
merge con Field, patrón/paste, completion y estado de celdas/caret.
|
|
|
- `rating-group/rating-group-provider.svelte.test.ts` — slider attrs,
|
|
|
merge con Field, half hover/click, clearable y teclado LTR/RTL.
|
|
|
- `tag-group/tag-group-provider.svelte.test.ts` — labels/root grid, roving
|
|
|
por DOM renderizado, selección/removal, link y remove button.
|
|
|
- `announce/announce-provider.svelte.test.ts` — alternancia polite/assertive,
|
|
|
timers UIX, regiones ARIA declarativas y uso standalone.
|
|
|
- `feed/feed-provider.svelte.test.ts` — attrs APG feed/article/title,
|
|
|
navegación PageUp/PageDown, threads anidados y sentinel IntersectionObserver.
|
|
|
- `tags-input/tags-input-provider.svelte.test.ts` — add/paste/blur,
|
|
|
navegación de tags, delete/clear triggers y props de input/control.
|
|
|
- `editable/editable-provider.svelte.test.ts` — activación preview/trigger,
|
|
|
foco via `ActiveDom`, commit/cancel por teclado/blur y guards.
|
|
|
- `alert-dialog/alert-dialog-provider.svelte.test.ts` — contratos
|
|
|
action/cancel, labels traducidos/override y cierre delegado a Dialog.
|
|
|
- `carousel/carousel-provider.svelte.test.ts` — detección de slides,
|
|
|
navegación trigger/teclado, props de partes y autoplay via `uix.timers`.
|
|
|
- `grid-list/grid-list-provider.svelte.test.ts` — merge con Field,
|
|
|
selección, navegación/typeahead, celdas focusables y checkbox de fila.
|
|
|
- `link-preview/link-preview-provider.svelte.test.ts` — delays open/close
|
|
|
con `uix.timers`, touch guard, content hover y props Floating/Arrow.
|
|
|
- `table/table-provider.svelte.test.ts` — root/section/header/cell props
|
|
|
desde `$libs/datagrid`, selección de fila y disclosure `RowDetail`.
|
|
|
- `form/form-provider.svelte.test.ts` — submit inválido con foco via
|
|
|
`ActiveDom`, attrs runtime y partes Submit/Reset/ErrorSummary.
|
|
|
- `toast/toast-provider.svelte.test.ts` — viewport hotkey via `ActiveDom`,
|
|
|
runtime por item, ARIA/parts, auto-dismiss y close action.
|
|
|
- `drag-drop/drag-drop-provider.svelte.test.ts` — ruta de teclado
|
|
|
start/nav/drop, filtros `accept`, prevención de drag, cancel y preview.
|
|
|
|
|
|
La guardia `*-provider.svelte.ts` sin test directo devuelve
|
|
|
`NO_MISSING_PROVIDER_TESTS`: todos los providers Soma activos tienen cobertura
|
|
|
directa. `table-core`, `form-core` y el scorer de Command ya no viven dentro
|
|
|
de Soma: se consumen desde `$libs/datagrid`, `$libs/forms` y `$libs/strings`.
|
|
|
|
|
|
### Convencion
|
|
|
|
|
|
- `.use(opts)` → lifecycle auto-gestionado (watch/$effect internos). Constructor privado.
|
|
|
- `new X(opts)` → lifecycle manual. El consumidor controla.
|
|
|
- `.props` → objeto para spread en el Provider.
|
|
|
|
|
|
### Animaciones (Presence)
|
|
|
|
|
|
Lifecycle:
|
|
|
|
|
|
```
|
|
|
OPENING:
|
|
|
open=true → shouldRender=true + data-starting-style
|
|
|
→ next rAF: data-starting-style removed (triggers CSS transition)
|
|
|
→ getAnimations().finished → onComplete(true)
|
|
|
|
|
|
CLOSING:
|
|
|
open=false → data-ending-style (element stays in DOM!)
|
|
|
→ getAnimations().finished
|
|
|
→ shouldRender=false + data-ending-style removed → onComplete(false)
|
|
|
```
|
|
|
|
|
|
- `forceMount` mantiene el elemento en DOM siempre (para transiciones CSS)
|
|
|
- `onComplete` usa `getAnimations()` API, no eventos `transitionend`/`animationend`
|
|
|
- Run ID cancellation previene callbacks stale en toggle rapido
|
|
|
|
|
|
## 7. Soma class (component runtime scope)
|
|
|
|
|
|
Soma reads `ActiveUix` from context and exposes services to components.
|
|
|
Components import Soma internals through relative paths, never from
|
|
|
`$active-app` and never through their own `$soma/*` public alias. Nestable:
|
|
|
child `<Soma portalTo="#modals">` overrides parent.
|
|
|
|
|
|
```ts
|
|
|
class Soma {
|
|
|
static create(opts?: SomaOptions): Soma; // factory + context set
|
|
|
static get(): Soma | undefined; // safe read
|
|
|
static require(): Soma; // throws if not found
|
|
|
|
|
|
readonly uix: ActiveUix;
|
|
|
readonly portalTo: string | HTMLElement | undefined;
|
|
|
|
|
|
// Service accessors (delegate to ActiveUix)
|
|
|
get langs(): ActiveLangs;
|
|
|
get nums(): ActiveNumbers | undefined;
|
|
|
get money(): ActiveCurrency | undefined;
|
|
|
get dates(): ActiveDates | undefined;
|
|
|
get units(): ActiveUnits | undefined;
|
|
|
get prefs(): ActiveUixPrefsView;
|
|
|
get logger(): EngineLogger;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
### Service access from components
|
|
|
|
|
|
Components access services through Soma, never through App directly:
|
|
|
|
|
|
```ts
|
|
|
const soma = Soma.get();
|
|
|
soma?.langs.ts('#?common.buttons.close|Close'); // translation via idlangref
|
|
|
soma?.prefs.getDir(); // effective direction from uix.prefs.direction
|
|
|
soma?.money?.format(1099); // currency formatting
|
|
|
soma?.dates?.getDateOrder(); // DMY / MDY / YMD
|
|
|
soma?.dates?.getHourCycle(); // 12 | 24 (numeric — not '12h' / '24h')
|
|
|
soma?.portalTo; // portal target
|
|
|
```
|
|
|
|
|
|
### Date / time types and formatting
|
|
|
|
|
|
Soma imports date-related symbols from `$libs/days`, the canonical date library. Components **never** import from `$lib/util/dates` (legacy) or `@internationalized/date` directly. There is no Soma re-export façade for the date domain.
|
|
|
|
|
|
- Value types: `CalendarDate`, `CalendarDateTime`, `Time`, `ZonedDateTime`
|
|
|
- Types: `DateValue`, `TimeValue`, `DateRange`, `DateMatcher`, `Month`, `WeekStartsOn`, `HourCycle`, `TimeGranularity`, `DateOrder`, `Granularity`, `SegmentPart`, `EditableTimeSegmentPart`, `TimeSegmentObj`, `SegmentValueObj`, `DayPeriod`, …
|
|
|
- Queries: `isSameDay`, `hasTime`, `isZonedDateTime`, `isTimeBefore`, `isTimeAfter`, `today`, `now`, `startOfMonth`, `endOfMonth`, `getLastFirstDayOfWeek`, `getNextLastDayOfWeek`, …
|
|
|
- Operations: `dateValueToDate`, `convertTimeValueToDateValue`, `convertTimeValueToTime`, `toCalendarDate`, `toZoned`, …
|
|
|
- Parsing: `parseDate`, `parseDateTime`, `parseTime`
|
|
|
- Formatting: `DateFormatter`, `getCachedDateFormat`, `getPlaceholder`, `getDefaultDate`, `getDefaultTime`, `inferGranularity`, `inferTimeGranularity`, `getDefaultHourCycle`, `resolveDateOrder(locale)`, `resolveHourCycle(locale)`
|
|
|
- **Segments (dias/segments.ts)**: constants (`DATE_SEGMENT_PARTS`, `EDITABLE_TIME_SEGMENT_PARTS`, …), type guards (`isDateSegmentPart`, `isEditableTimeSegmentPart`, `isDateAndTimeSegmentObj`, …), pure helpers (`initializeSegmentValues`, `initializeTimeSegmentValues`, `getValueFromSegments`, `getTimeValueFromSegments`, `areAllSegmentsFilled`, `createSegmentContent`, `createTimeSegmentContent`, `getOptsByGranularity`, `getOptsByTimeGranularity`).
|
|
|
|
|
|
`HourCycle` is canonically the numeric form `12 | 24` across the whole framework, matching `Intl.DateTimeFormat`'s `hour12` resolved option. String forms like `'12h'`/`'24h'` are legacy and must not appear in new code.
|
|
|
|
|
|
**`soma/datetime/` holds only UI-level helpers** (screen-reader announcer, DOM segment navigation, `SegmentState` shape with `lastKeyZero`/`hasLeftFocus`/`updating`, `isAcceptableSegmentKey` using KEYS, description-element DOM writers). It must not re-export `$libs/days` symbols — consumers import from `$libs/days` directly. Extending `$libs/days` is the default for new date/time helpers; adding to `soma/datetime/` is only correct when the helper is genuinely UI-specific.
|
|
|
|
|
|
### Static method convention (project-wide)
|
|
|
|
|
|
All classes that use Svelte context follow the same pattern:
|
|
|
|
|
|
| Method | Returns | Use when |
|
|
|
| ---------------- | ----------------------- | --------------------------------- |
|
|
|
| `X.create(opts)` | instance | Creating + registering in context |
|
|
|
| `X.get()` | instance or `undefined` | Parent/context is optional |
|
|
|
| `X.require()` | instance (throws) | Parent/context is required |
|
|
|
|
|
|
This applies to `App`, `Soma`, and all state classes that use context. No
|
|
|
standalone functions. No `from()`. No `ctx` exposed.
|
|
|
|
|
|
### Texto funcional
|
|
|
|
|
|
Las traducciones propias del componente se declaran en el morfo:
|
|
|
|
|
|
```ts
|
|
|
export const drawerMorfo = {
|
|
|
name: 'Drawer',
|
|
|
kebab: 'drawer',
|
|
|
translations: {
|
|
|
trigger: { es: 'Abrir cajon', en: 'Open drawer' }
|
|
|
},
|
|
|
parts: [
|
|
|
{
|
|
|
name: 'Trigger',
|
|
|
kebab: 'trigger',
|
|
|
aria: [{ attr: 'aria-label', value: v.translationRef('trigger', 'Open drawer') }]
|
|
|
}
|
|
|
]
|
|
|
} as const satisfies Morfo;
|
|
|
```
|
|
|
|
|
|
Las traducciones compartidas no se duplican por componente:
|
|
|
|
|
|
```ts
|
|
|
value: v.commonRef('buttons.close', 'Close'); // #?common.buttons.close|Close
|
|
|
```
|
|
|
|
|
|
`ActiveUix` conecta el registro de morfos con `ActiveLangs`. Cuando el
|
|
|
provider crea `createSomaRuntime(morfo, sources)` o `soma.runtime(morfo,
|
|
|
sources)`, `registerMorfo(morfo)` registra el contrato `data-*` y publica
|
|
|
`morfo.translations` bajo `components.{kebab}`.
|
|
|
|
|
|
`commonLangs` en `src/uix/langs.ts` aporta los defaults de `common.*`.
|
|
|
`ActiveUix` los registra sin pisar hojas existentes, de modo que el
|
|
|
integrador puede pasar sus propias traducciones y UIX solo completa lo que
|
|
|
falte.
|
|
|
|
|
|
No existe catálogo global por componente. `ActiveUix` conecta el registro de
|
|
|
morfos; cada componente publica sus textos cuando su morfo se registra.
|
|
|
|
|
|
## 8. Sistema reactivo
|
|
|
|
|
|
Capa fina sobre runes de Svelte 5 que permite pasar estado reactivo por referencia entre clases.
|
|
|
|
|
|
- `state<T>(initial)` → `State<T>` (mutable, `.current`)
|
|
|
- `readableActive(() => value)` → `Active<T>` (readonly derived)
|
|
|
- `writableActive(getter, setter)` → `State<T>` (two-way binding)
|
|
|
|
|
|
Los wrappers `.svelte` convierten props normales a `Active`/`State` con estas funciones. Esta conversion es la frontera entre el mundo de props de Svelte y el mundo de clases reactivas de soma.
|
|
|
|
|
|
## 9. Contratos data-\*
|
|
|
|
|
|
Los `data-*` son API publica formal, validados con `assertContract()`.
|
|
|
|
|
|
Convencion:
|
|
|
|
|
|
```
|
|
|
data-dialog → provider (sin -provider, sin -root)
|
|
|
data-dialog-trigger → parte
|
|
|
data-dialog-content → parte
|
|
|
data-state="open|closed" → estado
|
|
|
data-disabled → flag
|
|
|
data-side="top|right|bottom|left" → posicion flotante
|
|
|
data-align="start|center|end" → alineacion
|
|
|
data-starting-style → animacion de entrada (1 frame)
|
|
|
data-ending-style → animacion de salida (persiste)
|
|
|
data-nested → es hijo de otro del mismo tipo
|
|
|
data-nested-open → tiene un hijo abierto
|
|
|
data-dragging → gesture drag activo
|
|
|
data-highlighted → item con virtual focus (aria-activedescendant)
|
|
|
data-resizing → splitter resize activo
|
|
|
```
|
|
|
|
|
|
CSS variables expuestas:
|
|
|
|
|
|
```
|
|
|
--floating-transform-origin
|
|
|
--floating-available-width
|
|
|
--floating-available-height
|
|
|
--floating-anchor-width
|
|
|
--floating-anchor-height
|
|
|
--dialog-depth
|
|
|
--dialog-nested-count
|
|
|
--drawer-progress → 0-1 drag progress
|
|
|
--drawer-offset-x / y → drag offset in px
|
|
|
--toast-swipe-move-x / y → toast swipe offset
|
|
|
```
|
|
|
|
|
|
## 10. IDs
|
|
|
|
|
|
Los IDs se generan con contexto de componente:
|
|
|
|
|
|
```
|
|
|
soma-dialog-c12
|
|
|
soma-dialog-trigger-c13
|
|
|
soma-dialog-content-c14
|
|
|
```
|
|
|
|
|
|
Pattern: `soma-{component}-{part}-{uid}`. Descriptivos e inspeccionables.
|
|
|
|
|
|
## 11. Barrel exports
|
|
|
|
|
|
### Componentes (jerárquico)
|
|
|
|
|
|
```ts
|
|
|
// $soma/components/index.ts
|
|
|
export * as Collapsible from './collapsible';
|
|
|
export * as Dialog from './dialog';
|
|
|
export * as Popover from './popover';
|
|
|
```
|
|
|
|
|
|
Consumo:
|
|
|
|
|
|
```ts
|
|
|
import { Dialog, Popover } from '$soma/components';
|
|
|
Dialog.Provider; // no SomaDialogProvider, no TerraDialogProvider
|
|
|
Dialog.Trigger;
|
|
|
```
|
|
|
|
|
|
### Internal
|
|
|
|
|
|
```ts
|
|
|
import { Portal, Arrow, VisuallyHidden, Soma } from '$soma/components/internal';
|
|
|
```
|
|
|
|
|
|
## 12. Fronteras externas
|
|
|
|
|
|
`soma` distingue entre:
|
|
|
|
|
|
- internos: `layers/`, `reactive/`, `dom/`, `provider/` — helpers propios
|
|
|
- externos: `@floating-ui/dom`, `runed`, `tabbable` — dependencias npm
|
|
|
|
|
|
Si una dependencia tiene API inestable o podria cambiar, se accede a traves de una frontera formal (como `layers/floating/` wrappea @floating-ui). Las dependencias estables (runed, svelte) se importan directamente.
|
|
|
|
|
|
## 13. Estructura del directorio
|
|
|
|
|
|
```
|
|
|
src/uix/soma/
|
|
|
├── SOMA_ARCHITECTURE.md ← this document
|
|
|
├── COMPONENT_GUIDE.md ← step-by-step implementation guide
|
|
|
├── README.md ← API reference
|
|
|
├── runtime.svelte.ts ← SomaRuntime (morfo interpreter)
|
|
|
├── errors.ts ← typed runtime/context errors
|
|
|
├── core/
|
|
|
│ └── soma.svelte.ts ← Soma class (root instance)
|
|
|
├── reactive/ ← reactive system
|
|
|
├── provider/ ← context + opts bridge
|
|
|
├── props/ ← mergeProps, composeHandlers
|
|
|
├── keyboard/ ← KEYS, directional
|
|
|
├── dom/ ← DOM utilities, focus
|
|
|
├── css/ ← styleToString, cssToStyleObj
|
|
|
├── id/ ← createId, useId
|
|
|
├── types/ ← shared types + service interfaces
|
|
|
├── layers/ ← behavior layers (classes only)
|
|
|
│ ├── presence.svelte.ts
|
|
|
│ ├── focus-scope.svelte.ts
|
|
|
│ ├── dismissal.svelte.ts
|
|
|
│ ├── text-selection.svelte.ts
|
|
|
│ ├── scroll-lock.svelte.ts
|
|
|
│ ├── resize-observer.svelte.ts
|
|
|
│ └── floating/
|
|
|
├── datetime/ ← UI-only helpers (announcer, segment DOM nav,
|
|
|
│ segment UI-state shapes, segment-key predicates,
|
|
|
│ description-element writers). NO date math,
|
|
|
│ NO re-exports of days — import `$libs/days`
|
|
|
│ directly.
|
|
|
├── components/
|
|
|
│ ├── internal/ ← Portal, Arrow, VisuallyHidden, <Soma>
|
|
|
│ ├── {name}/ ← each headless component
|
|
|
│ │ ├── {name}-provider.svelte.ts ← state classes (NOT {name}.svelte.ts)
|
|
|
│ │ ├── types.ts ← public props + canonical field shapes
|
|
|
│ │ ├── langs.ts ← optional idlangref constants for imperative strings
|
|
|
│ │ ├── exports.ts
|
|
|
│ │ ├── index.ts
|
|
|
│ │ └── components/
|
|
|
│ │ ├── {name}.svelte ← root wrapper
|
|
|
│ │ ├── {name}-trigger.svelte
|
|
|
│ │ └── ...
|
|
|
│ └── index.ts ← hierarchical barrel
|
|
|
└── index.ts ← root scope only (`Soma`)
|
|
|
```
|
|
|
|
|
|
### File naming convention
|
|
|
|
|
|
- State class: `{name}-provider.svelte.ts` — NOT `{name}.svelte.ts`
|
|
|
- Avoids Vite module resolution ambiguity with `{name}.svelte` wrapper
|
|
|
- Reflects what's inside: provider/state classes
|
|
|
- Root wrapper: `{name}.svelte` in `components/` subdirectory
|
|
|
- Export name: always `Provider`, never `Root`
|
|
|
|
|
|
## 14. Anti-patterns
|
|
|
|
|
|
Avoid in soma:
|
|
|
|
|
|
- Complex logic inside wrapper `.svelte` — belongs in Provider
|
|
|
- Props drilling when context is the correct pattern
|
|
|
- `data-*` attrs outside of contract
|
|
|
- Inventing part names without checking reference library anatomies (ark-ui, bits-ui, radix-ui)
|
|
|
- Nesting layers as component wrappers in templates
|
|
|
- Inline `z-index: auto` that overrides CSS
|
|
|
- Coupling primitives to app libraries
|
|
|
- Product copy inside the primitive
|
|
|
- Speculative abstractions ("just in case")
|
|
|
- One-line files that only re-export (merge into parent)
|
|
|
- Redundant naming prefixes (SomaDialog, DialogLayerState)
|
|
|
- Dummy refs to satisfy a type — use `ProviderOpts` for no-DOM roots
|
|
|
- **State class file named same as wrapper** — `select.svelte.ts` + `components/select.svelte` causes Vite module duplication. Always use `{name}-provider.svelte.ts`
|
|
|
- **Event handlers not in props** — defining onclick as a class method but not including it in the props derived object
|
|
|
- **getContext in event handlers** — getContext only works during initialization. Capture references in constructor
|
|
|
- **Exporting as Root** — always `Provider`, never `Root`
|
|
|
- **Skipping reference library comparison** — mandatory step, no exceptions
|
|
|
- **Comments in Spanish** — all code comments in English
|
|
|
- **Standalone context functions** — no `createX()`, `getX()`, `useX()` as loose functions. Use `X.create()`, `X.get()`, `X.require()` static methods
|
|
|
- **Importing from `$lib/ext/app`** in components — components access services through `Soma`, never App directly
|
|
|
- **`from()` as factory name** — use `create()` consistently
|
|
|
- **Re-implementing date/time helpers inside soma** — extend `$libs/days` (A23). Importing from `$lib/util/dates` (legacy vendored) or `@internationalized/date` directly is forbidden; use `$libs/days`.
|
|
|
- **Re-export façades over days** — a soma module whose only job is to forward `$libs/days` symbols is dead weight. Consumers import from `$libs/days` directly.
|
|
|
- **UI-level helpers in dias, or date math in `soma/datetime/`** — dias is pure (no DOM, no Svelte, no KEYS); `soma/datetime/` is UI-only (screen-reader announcer, DOM segment navigation, `SegmentState` shapes, KEYS-based predicates). No crossover
|
|
|
- **`readonlySegments` without a concrete value anchor** — A24: warn via `soma?.logger.warn` when `value` is undefined. Range components split into `startReadonlySegments` / `endReadonlySegments` (A25)
|
|
|
- **`keydown.preventDefault()` as the only guard on contenteditable segments** — IME/paste/drop bypass keydown. Always add `onbeforeinput: e => e.preventDefault()` (A26)
|
|
|
- **Time placeholders as `'––'`** — use `createSegmentContent` / `createTimeSegmentContent` from `dias/segments.ts`; time parts render as `hh`/`mm`/`ss` (A28)
|
|
|
- **Pickers that reimplement field/calendar/popover** — compose via shared `writableActive` refs (A27). Only `Provider`, `Trigger`, and the calendar/slider bridge are unique parts
|
|
|
- **Demo pages as galleries of canned snippets** — every soma demo must be an interactive testbed wiring every public prop to a live control, including a Field-integration section (A29)
|
|
|
- **`HourCycle` as `'12h' \| '24h'`** — canonical form is numeric `12 \| 24` (matches `Intl.DateTimeFormat.hour12`). String forms are legacy
|
|
|
|
|
|
## 15. Estado actual y deuda histórica
|
|
|
|
|
|
Soma ha pasado por varias fases. La forma actual (rama `active-uix`,
|
|
|
post-2026-05-08):
|
|
|
|
|
|
- **Provider inheritance dropped** — los providers ya no heredan de un
|
|
|
base abstracto; son clases concretas. La mecanica DOM comun vive en
|
|
|
`SomaRuntime.part(...)`, y los casos que necesitan eventos semanticos
|
|
|
usan el mismo `SomaRuntime` para `trigger`/`keydown`.
|
|
|
- **SomaRuntime cachea** la compilación del morfo (`compileMorfo` por
|
|
|
WeakMap) y registra los `effects` que sincronizan `state → attrs` vía
|
|
|
`dom.apply`.
|
|
|
- **Naming**: `Provider` (nunca `Root`); child providers referencian al
|
|
|
padre como `provider`, nunca `root`. El export de un componente
|
|
|
multi-parte sigue la forma compound `Toggle.Provider + Toggle.Trigger
|
|
|
- ...`.
|
|
|
- **Data-attr naming**: `data-{component}` (provider) y
|
|
|
`data-{component}-{kebab}` (sub-parts). Sin prefijo `data-soma-*`. El
|
|
|
compiler emite estos via `compiled.parts.attrs`.
|
|
|
- **State files**: `{name}-provider.svelte.ts` (explicit, no ambiguity).
|
|
|
- **IDs**: descriptive (`soma-dialog-trigger-c13`).
|
|
|
|
|
|
## 16. Regla de estabilidad
|
|
|
|
|
|
Un componente de soma se considera estable cuando:
|
|
|
|
|
|
- su API publica esta clara y documentada con JSDoc
|
|
|
- sus `data-*` estan registrados y validados con `assertContract`
|
|
|
- el wrapper y el Provider siguen el patron general
|
|
|
- su accesibilidad base esta resuelta (ARIA, roles, keyboard)
|
|
|
- sus props se han comparado con ark-ui, bits-ui y radix-ui
|
|
|
- tiene demo page funcional en `web/routes/uix/components/{componente}/`
|
|
|
- no depende de hacks locales, z-index hardcodeados, ni demo CSS para sostenerse
|
|
|
- compila con 0 errores (`svelte-check`)
|
|
|
|
|
|
## 17. New component checklist
|
|
|
|
|
|
See `COMPONENT_GUIDE.md` for the full step-by-step process (27 general steps + 4 date/time specific, with rules A1–A29). Summary:
|
|
|
|
|
|
```
|
|
|
[ ] 1. Compare with ark-ui, bits-ui, radix-ui — feature table
|
|
|
[ ] 2. Verify membership criteria
|
|
|
[ ] 3. Define parts + attrs + morfo.translations when the component owns text
|
|
|
[ ] 4. Create types.ts (props + canonical field shapes)
|
|
|
[ ] 5. Create langs.ts only for imperative idlangref constants, not as the catalog
|
|
|
[ ] 6. Create {name}-provider.svelte.ts (concrete state classes, no Provider inheritance)
|
|
|
[ ] 7. Create wrapper .svelte files (thin)
|
|
|
[ ] 8. Create exports.ts + index.ts
|
|
|
[ ] 9. Create interactive demo page + link in index (A29)
|
|
|
[ ] 10. README.md with anatomy, ARIA, data-attrs, comparison table
|
|
|
[ ] 11. svelte-check + test in browser
|
|
|
[ ] 12. Date/time components: only consume date/time domain via `$libs/days`,
|
|
|
`onbeforeinput` on contenteditable, readonly-without-value warning,
|
|
|
picker composition pattern (A23–A28)
|
|
|
```
|