Equivalent to TERRA_ARCHITECTURE.md. Documents: - 3-layer architecture (soma → visual layer → app) - Design principles (layers as behaviors, Soma class, Provider pattern) - data-* contracts and CSS variable conventions - Animation system lifecycle (Presence, data-starting/ending-style) - Anti-patterns and stability rules - Improvements over terra (comparison table) - Component checklist (including reference library comparison) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>semantuix
parent
b6d9f51c47
commit
b0d5407478
@ -0,0 +1,466 @@
|
||||
# Soma Architecture
|
||||
|
||||
Documento de referencia arquitectonica para `src/uix/soma`.
|
||||
|
||||
`soma` es la capa de primitives headless del sistema. Reemplaza a `terra` con un rediseño que elimina el boilerplate, resuelve los problemas de dispersion, y establece patrones mas claros para el desarrollador de componentes.
|
||||
|
||||
## 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 3 capas
|
||||
|
||||
```
|
||||
soma → headless: behavior, accesibilidad, data-* contracts, context, servicios
|
||||
capa visual → apariencia: tokens, CSS, temas, motion, sound, 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 (translator, 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 y sound semanticos
|
||||
- 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:
|
||||
|
||||
- `Provider` base class
|
||||
- `Soma` class para servicios
|
||||
- Barrel imports jerárquicos (`import { Dialog } from '$soma/components'`)
|
||||
|
||||
### 3.2 Un patron, no tres
|
||||
|
||||
Todo componente sigue el mismo patron:
|
||||
|
||||
1. State class extiende `Provider`
|
||||
2. Wrapper `.svelte` fino convierte props → Active/State
|
||||
3. Props derivados via `$derived.by` + `assertProps`
|
||||
4. Contexto para comunicacion padre-hijo
|
||||
|
||||
No hay excepciones: components sin DOM usan `ProviderOpts` (ref opcional), components con DOM usan `WithRefOpts`.
|
||||
|
||||
### 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.baseProps,
|
||||
...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.getOr();
|
||||
const label = this.soma?.translate('dialog.close', 'Close');
|
||||
const dateOrder = Soma.resolve(opts.dateOrder, () => this.soma?.format.current?.date?.getDateOrder(), 'DMY');
|
||||
```
|
||||
|
||||
### 3.5 Los data-* son contrato publico
|
||||
|
||||
Los `data-*` attrs son la frontera entre soma y la capa visual. Cambiarlos es breaking change.
|
||||
|
||||
Convencion:
|
||||
|
||||
- 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`
|
||||
|
||||
### 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 (close, cancel, etc.) se resuelve via `Soma.translate()` con fallbacks hardcodeados en ingles.
|
||||
|
||||
### 3.7 Props documentadas obligatoriamente
|
||||
|
||||
Todas las props de todos los componentes llevan JSDoc en `types.ts`. Cada prop: descripcion, `@default`, notas de comportamiento.
|
||||
|
||||
### 3.8 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.
|
||||
|
||||
## 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): extiende `Provider<WithRefOpts>`, renderiza `<div>`
|
||||
- **Sin DOM** (Dialog, Popover): extiende `Provider<ProviderOpts>`, solo renderiza children
|
||||
|
||||
### Subcomponentes
|
||||
|
||||
Leen estado del root via `ctx.get()`. 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.
|
||||
|
||||
## 5. Provider base class
|
||||
|
||||
```ts
|
||||
abstract class Provider<S extends ProviderOpts> {
|
||||
readonly opts: S;
|
||||
readonly attachment: RefAttachment | undefined;
|
||||
|
||||
protected constructor(opts, component, part, partAttr, ctx?, onRefChange?);
|
||||
|
||||
protected get baseProps(): { id, [partAttr], ...attachment };
|
||||
protected assertProps<P>(props: P): P; // valida data-* contract
|
||||
}
|
||||
```
|
||||
|
||||
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. |
|
||||
|
||||
### 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 (servicios)
|
||||
|
||||
```ts
|
||||
class Soma {
|
||||
static create(props: SomaProps): Soma;
|
||||
static get(): Soma;
|
||||
static getOr(): Soma | undefined;
|
||||
static resolve<T>(prop, fromConfig, fallback): Active<T>;
|
||||
|
||||
readonly dir: Active<Direction>;
|
||||
readonly translator: Active<SomaTranslator | undefined>;
|
||||
readonly logger: Active<SomaLogger | undefined>;
|
||||
readonly portalTo: Active<PortalTarget | undefined>;
|
||||
readonly format: Active<SomaFormat | undefined>;
|
||||
|
||||
translate(path: string, fallback: string): string;
|
||||
}
|
||||
```
|
||||
|
||||
### Resolucion por capas
|
||||
|
||||
1. prop explicita del componente
|
||||
2. valor en Soma (via context)
|
||||
3. fallback interno del primitive
|
||||
|
||||
`Soma.resolve()` implementa esta cadena como generico.
|
||||
|
||||
### Format namespace
|
||||
|
||||
```ts
|
||||
interface SomaFormat {
|
||||
locale: string;
|
||||
date?: SomaDateFormat;
|
||||
number?: SomaNumberFormat;
|
||||
currency?: SomaCurrencyFormat;
|
||||
unit?: SomaUnitFormat;
|
||||
}
|
||||
```
|
||||
|
||||
Un namespace, sin nesting artificial. Extensible sin contaminar el top level.
|
||||
|
||||
### Texto funcional
|
||||
|
||||
soma tiene texto propio solo para semantica funcional: close, cancel, next month, etc. Se resuelve via `Soma.translate()` con fallback en ingles. soma no depende de ningun runtime de i18n concreto.
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
CSS variables expuestas:
|
||||
|
||||
```
|
||||
--soma-floating-transform-origin
|
||||
--soma-floating-available-width
|
||||
--soma-floating-available-height
|
||||
--soma-floating-anchor-width
|
||||
--soma-floating-anchor-height
|
||||
--soma-dialog-depth
|
||||
--soma-dialog-nested-count
|
||||
```
|
||||
|
||||
## 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 ← este documento
|
||||
├── README.md ← referencia de API
|
||||
├── soma.svelte.ts ← Soma class (root instance)
|
||||
├── reactive/ ← sistema reactivo
|
||||
├── provider/ ← Provider base class + context
|
||||
├── props/ ← mergeProps, composeHandlers
|
||||
├── attrs/ ← createAttrs, contracts, helpers
|
||||
├── keyboard/ ← KEYS, directional
|
||||
├── dom/ ← DOM utilities, focus
|
||||
├── events/ ← addEventListener
|
||||
├── css/ ← styleToString, cssToStyleObj
|
||||
├── id/ ← createId, useId
|
||||
├── types/ ← shared types + service interfaces
|
||||
├── layers/ ← behavior layers (solo clases)
|
||||
│ ├── presence.svelte.ts
|
||||
│ ├── focus-scope.svelte.ts
|
||||
│ ├── dismissal.svelte.ts
|
||||
│ ├── text-selection.svelte.ts
|
||||
│ ├── scroll-lock.svelte.ts
|
||||
│ ├── resize-observer.svelte.ts
|
||||
│ └── floating/
|
||||
├── components/
|
||||
│ ├── internal/ ← Portal, Arrow, VisuallyHidden, Soma component
|
||||
│ ├── collapsible/
|
||||
│ ├── dialog/
|
||||
│ ├── popover/
|
||||
│ └── index.ts ← barrel jerárquico
|
||||
└── index.ts ← barrel principal
|
||||
```
|
||||
|
||||
## 14. Anti-patrones
|
||||
|
||||
Evitar en soma:
|
||||
|
||||
- logica compleja dentro del wrapper `.svelte`
|
||||
- props drilling cuando el patron correcto es contexto
|
||||
- `data-*` fuera de contrato
|
||||
- inventar nombres de parts sin revisar anatomias de referencia (ark-ui, bits-ui, radix-ui)
|
||||
- nesting de layers como componentes wrapper en templates
|
||||
- z-index inline con `auto` que sobreescribe CSS
|
||||
- acoplar primitives a librerias de app
|
||||
- meter copy de producto dentro del primitive
|
||||
- crear helpers, abstracciones o indirecciones "por si acaso"
|
||||
- archivos de 1 linea que solo reexportan (merge en el padre)
|
||||
- naming con prefijos redundantes (SomaDialog, DialogLayerState)
|
||||
- dummy refs para satisfacer un type — si no hay DOM, usa `ProviderOpts`
|
||||
|
||||
## 15. Mejoras sobre terra
|
||||
|
||||
| Aspecto | Terra | Soma |
|
||||
|---------|-------|------|
|
||||
| Layers | 5 niveles de nesting en template | Behaviors integrados en Provider |
|
||||
| Config | `config/` con funciones sueltas | `Soma` class con servicios |
|
||||
| Resolvers | 6 funciones especificas | 1 generico `Soma.resolve()` |
|
||||
| Animaciones | Deteccion basica de animationend | `getAnimations()` API + `data-starting/ending-style` |
|
||||
| Naming | Prefijos redundantes (TerraDialog) | Barrel jerárquico (`Dialog.Provider`) |
|
||||
| Archivos | 28 archivos en 10 dirs (layers) | 13 archivos en 2 dirs |
|
||||
| Provider | No base class | `Provider` abstract con baseProps, assertProps |
|
||||
| Tipos | Dispersos | `ProviderOpts` vs `WithRefOpts` |
|
||||
| IDs | Genericos (`soma-c12`) | Descriptivos (`soma-dialog-trigger-c13`) |
|
||||
| Format | 4 props sueltas en presentation | 1 namespace `SomaFormat` |
|
||||
|
||||
## 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 test page funcional en `/test/soma/[componente]`
|
||||
- no depende de hacks locales, z-index hardcodeados, ni demo CSS para sostenerse
|
||||
- compila con 0 errores (`svelte-check`)
|
||||
|
||||
## 17. Checklist de componente nuevo
|
||||
|
||||
```
|
||||
[ ] 1. Comparar con ark-ui, bits-ui, radix-ui — cuadro de features
|
||||
[ ] 2. Verificar criterio de pertenencia (composicion + behavior complejo)
|
||||
[ ] 3. Definir partes: Provider + sub-parts
|
||||
[ ] 4. Definir attrs: createAttrs({ component, parts })
|
||||
[ ] 5. Definir types.ts con JSDoc en todas las props
|
||||
[ ] 6. Implementar state classes extendiendo Provider
|
||||
[ ] 7. Integrar layers como behaviors (no nesting)
|
||||
[ ] 8. Implementar wrappers svelte (thin: props → Active → state → mergeProps → render)
|
||||
[ ] 9. IDs descriptivos: createId(uid, 'component-part')
|
||||
[ ] 10. Implementar exports.ts barrel
|
||||
[ ] 11. Añadir al barrel jerarquico en components/index.ts
|
||||
[ ] 12. Crear test page en /test/soma/[componente]
|
||||
[ ] 13. Verificar: svelte-check + dev server
|
||||
[ ] 14. Documentar gaps vs librerias de referencia
|
||||
```
|
||||
Loading…
Reference in new issue