`, 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.
## 5. Provider base class
```ts
abstract class Provider {
readonly opts: S;
readonly attachment: RefAttachment | undefined;
protected constructor(opts, component, part, partAttr, ctx?, onRefChange?);
protected get baseProps(): { id, [partAttr], ...attachment };
protected assertProps(props: P): P; // valida data-* contract
}
```
Dos interfaces de opts:
- `ProviderOpts` — `{ id: Active; ref?: State }` — para roots sin DOM
- `WithRefOpts` — `{ id: Active; ref: State }` — 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. |
### 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 App from context and exposes services to components. Components import from `$soma`, never from `$lib/ext/app`. Nestable: child `` 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 app: App;
readonly portalTo: string | HTMLElement | undefined;
// Service accessors (delegate to App)
get langs(): AppLangs;
get nums(): AppNums | undefined;
get money(): AppMoney | undefined;
get dates(): AppDates | undefined;
get units(): AppUnits | undefined;
get presentation(): AppPresentation;
get logger(): AppLogger;
}
```
### 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?.presentation.getDir(); // 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 all date-related symbols through a single boundary: `$soma/external/dates`. That module re-exports from `$lib/util/dias`, the canonical date library. Components **never** import from `$lib/util/dates` (legacy) or `@internationalized/date` directly.
- Value types: `CalendarDate`, `CalendarDateTime`, `Time`, `ZonedDateTime`
- Types: `DateValue`, `DateRange`, `DateMatcher`, `Month`, `WeekStartsOn`, `HourCycle`, `DateOrder`, `Granularity`
- Operations: `today`, `now`, `startOfMonth`, `endOfMonth`, `isSameDay`, `getDayOfWeek`, `getLastFirstDayOfWeek`, `getNextLastDayOfWeek`, etc.
- Parsing: `parseDate`, `parseDateTime`, `parseTime`
- Formatting: `DateFormatter`, `getCachedDateFormat`, `resolveDateOrder(locale)`, `resolveHourCycle(locale)`
`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.
### 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 `Provider` subclasses. No standalone functions. No `from()`. No `ctx` exposed.
### Texto funcional
Traducciones funcionales (close, cancel, etc.) se resuelven via `langs.ts()` con idlangref:
```ts
// drawer/langs.ts
export const DRAWER_LANGS = {
TRIGGER: '#?components.drawer.trigger|Open drawer',
CLOSE: '#?common.buttons.close|Close',
} as const;
// En el provider:
'aria-label': this.provider.soma?.langs.ts(DRAWER_LANGS.CLOSE),
```
Common keys (`close`, `open`, `cancel`, etc.) en `common.buttons.*`. Component-specific en `components.{name}.*`. soma exporta `componentLangs` desde `core/langs.ts` — el consumidor lo importa y extiende. soma no inyecta traducciones.
## 8. Sistema reactivo
Capa fina sobre runes de Svelte 5 que permite pasar estado reactivo por referencia entre clases.
- `state(initial)` → `State` (mutable, `.current`)
- `readableActive(() => value)` → `Active` (readonly derived)
- `writableActive(getter, setter)` → `State` (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:
```
--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
--drawer-progress → 0-1 drag progress
--drawer-offset-x / y → drag offset in px
--soma-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
├── core/
│ ├── soma.svelte.ts ← Soma class (root instance)
│ └── langs.ts ← componentLangs (default translations)
├── reactive/ ← reactive system
├── 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 (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/
├── external/
│ └── dates/ ← boundary with date library
├── components/
│ ├── internal/ ← Portal, Arrow, VisuallyHidden,
│ ├── {name}/ ← each headless component
│ │ ├── {name}-provider.svelte.ts ← state classes (NOT {name}.svelte.ts)
│ │ ├── types.ts ← public props + canonical field shapes
│ │ ├── langs.ts ← idlangref constants (if has translations)
│ │ ├── exports.ts
│ │ ├── index.ts
│ │ └── components/
│ │ ├── {name}.svelte ← root wrapper
│ │ ├── {name}-trigger.svelte
│ │ └── ...
│ └── index.ts ← hierarchical barrel
└── index.ts ← main barrel
```
### 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 subclasses
- 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
## 15. Improvements over terra
| Aspect | Terra | Soma |
| ------------- | -------------------------------- | ---------------------------------------------------- |
| Layers | 5-level template nesting | Behaviors integrated in Provider |
| Config | `config/` with loose functions | `Soma` class with services |
| Resolvers | 6 specific functions | 1 generic `Soma.resolve()` |
| Animations | Basic animationend detection | `getAnimations()` API + `data-starting/ending-style` |
| Naming | Redundant prefixes (TerraDialog) | Hierarchical barrel (`Dialog.Provider`) |
| Files | 28 files in 10 dirs (layers) | 13 files in 2 dirs |
| Provider | No base class | `Provider` abstract with baseProps, assertProps |
| Types | Scattered | `ProviderOpts` vs `WithRefOpts` |
| IDs | Generic (`soma-c12`) | Descriptive (`soma-dialog-trigger-c13`) |
| Format | 4 loose props in presentation | 1 namespace `SomaFormat` |
| State files | `{name}.svelte.ts` (ambiguous) | `{name}-provider.svelte.ts` (explicit) |
| Exports | Mixed Root/Provider | Always Provider |
| Documentation | README only | Architecture + Component Guide + JSDoc |
## 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. New component checklist
See `COMPONENT_GUIDE.md` for the full step-by-step process (25 steps with A1-A22 rules). Summary:
```
[ ] 1. Compare with ark-ui, bits-ui, radix-ui — feature table
[ ] 2. Verify membership criteria
[ ] 3. Define parts + attrs + contract (registerContract)
[ ] 4. Create types.ts (props + canonical field shapes)
[ ] 5. Create langs.ts (idlangref constants, if has translations)
[ ] 6. Create {name}-provider.svelte.ts (Provider subclasses)
[ ] 7. Create wrapper .svelte files (thin)
[ ] 8. Create exports.ts + index.ts
[ ] 9. Create test page + link in index
[ ] 10. svelte-check + test in browser
```