Add SOMA_ARCHITECTURE.md — architectural reference for soma framework

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
dev 6 months ago
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…
Cancel
Save

Powered by TurnKey Linux.