;
assert>(props: P): P;
}
```
`syncAttrs: true` enables the imperative write via `uix.dom` for parts whose
sources are already declared on the runtime. A provider still composing attrs
in render props does not enable `syncAttrs`.
Two opts interfaces:
- `ProviderOpts` — `{ id: Active; ref?: State }` —
for DOM-less roots
- `WithRefOpts` — `{ id: Active; ref: State }` —
for parts with DOM
## 6. Layers
`layers/` contains behavior classes only (`.svelte.ts`). They are
infrastructure consumed by Providers, never directly by the consumer.
### Inventory
| Layer | API | Responsibility |
| ----------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `Presence` | `new Presence(opts)` | Animation-aware mount/unmount. `isPresent`, `transitionAttrs`, `onComplete`. |
| `FocusScope` | `FocusScope.use(opts)` | Focus trap, loop, auto-focus, restore. Singleton manager with a stack. |
| `Dismissal` | `Dismissal.use(opts)` | Escape + click-outside. Global registry. Behaviors: close, ignore, defer. |
| `TextSelection` | `TextSelection.use(opts)` | Prevents selection overflow during drag. |
| `ScrollLock` | `new ScrollLock(initial?, delay?)` | Body scroll lock with refcount. Supports a delay for animations. |
| `ResizeObserver$` | `new ResizeObserver$(getter, cb)` | ResizeObserver with Svelte lifecycle. |
| `Floating*` | `FloatingProvider.create()`, `FloatingContent.create(opts)`, etc. | Anchor-relative positioning — the in-house engine (`layers/floating` + `$ethereal`; `@floating-ui` is a parity-tests devDep). |
| `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)` (`floating/safe-polygon.ts`) | Hover-gap corridor between trigger↔content. |
| `Stacking` | module-level registry (`stacking.svelte.ts`) | Shared z-order of movable surfaces (FloatPanel): `bringToFront`, `data-topmost`/`data-behind`. |
| `AxialDrag` | `new AxialDrag(opts)` (`manipulation/`) | Single-axis drag with snap points + release state. |
| `ZoomPan` | `new ZoomPan(config)` | Scale + pan of content inside a fixed viewport (Cropper); pure state + math. |
| `ImageProvider` | `new ImageProvider(opts)` | Image load state (`idle/loading/loaded/error`) with delay. |
| `ListSelection` | pure functions (`list-selection.ts`) | The single/multi selection machine + `allowDeselect`, shared by Select/Combobox. |
`layers/floating/placement.ts` is the single source for `Side`, `Align`,
`Boundary`, `SIDE_OPTIONS` and `ALIGN_OPTIONS`. `floating/types.ts` consumes
that source and does not import from the `floating.svelte.ts` runtime,
avoiding cycles between types and classes.
### Provider test coverage
Every active Soma provider has a direct `*-provider.svelte.test.ts` — the
guard returns `NO_MISSING_PROVIDER_TESTS` when a provider ships without one,
so the live inventory is the test tree itself (one test file next to each
provider). The reusable engines live outside Soma and carry their own tests:
`$libs/datagrid` (table core), `$libs/forms` (form core + Standard Schema)
and `$libs/strings` (Command's scorer).
### Convention
- `.use(opts)` → self-managed lifecycle (internal watch/$effect). Private
constructor.
- `new X(opts)` → manual lifecycle. The consumer controls it.
- `.props` → an object to spread into the Provider.
### Animations (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` keeps the element in the DOM always (for CSS transitions)
- `onComplete` uses the `getAnimations()` API, not
`transitionend`/`animationend` events
- Run-ID cancellation prevents stale callbacks on fast toggles
- **JS-driver gating** (the `motion` option): a `spring` (pure rAF) does not
appear in `getAnimations()`. `Presence` receives `motion: EngineMotion`
(= `soma.motion`, relocated to `arts/motion`) and calls
`motion.run(node, phase)`; it awaits its `finished` ALONGSIDE
`getAnimations()` before unmounting. For CSS presets (or nodes without
`data-animation-style`), `run` returns an already-settled handle → the
declarative path is unchanged. (Replaces the old
`runMotion`/`eidos.motionRunner` hook.)
## 7. The Soma class (the 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: a
child `` overrides the 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;
get motion(): EngineMotion; // arts/motion — Presence's JS-driver gating
}
```
### 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(); // the app's direction — one link of the chain, see below
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
```
A component never takes its own direction from that accessor: the wrapper runs
`activeDir(() => dir, soma)` and the provider defaults once in `resolvedDir`
(§3.4) — [`canon/direction-contract.md`](../canon/direction-contract.md).
### 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** (the screen-reader
announcer, DOM segment navigation, the `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.
### The static-method convention (project-wide)
All classes using 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 every state class using context. No
standalone functions. No `from()`. No exposed `ctx`.
### Functional text
The component's own text slots are declared in the morfo as idlangrefs (the
multilingual catalog lives in `src/uix/langs/components/{kebab}.ts`):
```ts
export const drawerMorfo = {
name: 'Drawer',
kebab: 'drawer',
texts: {
trigger: '#?components.drawer.trigger|Open drawer'
},
parts: [
{
name: 'Trigger',
kebab: 'trigger',
aria: [{ attr: 'aria-label', value: v.translationRef('trigger', 'Open drawer') }]
}
]
} as const satisfies Morfo;
```
Shared translations are not duplicated per component:
```ts
value: v.commonRef('buttons.close', 'Close'); // #?common.buttons.close|Close
```
`ActiveUix` registers the `src/uix/langs/components/*` catalogs into
`ActiveLangs` under `components.{kebab}.*`. When the provider creates
`createSomaRuntime(morfo, sources)` or `soma.runtime(morfo, sources)`,
`registerMorfo(morfo)` compiles and registers the `data-*` contract; the
morfo only declares its slots (`morfo.texts`), not the catalog.
`commonLangs` in `src/uix/langs.ts` supplies the `common.*` defaults.
`ActiveUix` registers them without overwriting existing leaves, so the
integrator can pass their own translations and UIX only fills what is
missing.
There is no global per-component catalog. `ActiveUix` wires the morfo
registry; each component publishes its texts when its morfo registers.
## 8. The reactive system
A thin layer over Svelte 5 runes that lets reactive state be passed by
reference between classes.
- `state(initial)` → `State` (mutable, `.current`)
- `readableActive(() => value)` → `Active` (readonly derived)
- `writableActive(getter, setter)` → `State` (two-way binding)
Types:
```ts
type Active = { readonly current: T }; // readonly container
type State = { current: T }; // mutable container
type ActiveProps = { [K in keyof T]: Active };
type StateProps = { [K in keyof T]: State };
```
The `.svelte` wrappers convert plain props into `Active`/`State` with these
functions. That conversion is the boundary between Svelte's prop world and
soma's reactive-class world. Providers receive their options typed as
`StateProps<…>` / `ActiveProps<…>`.
## 8.bis Internal helpers
Infrastructure modules providers consume. Not consumer API; imported by
relative path inside soma.
### props — `mergeProps`
```ts
const merged = mergeProps(restProps, state.props);
```
- handlers (`onclick`, `onfocus`, …) → composed with `composeHandlers`
- `class` → merged with clsx
- `style` → merged (object + string)
- ARIA naming attrs (`ARIA_NAMING_ATTRS` — `aria-label`) → FIRST wins. The
framework-wide call shape puts the consumer's `restProps` first, so the
consumer's explicit label beats the morfo's default (two-class precedence,
A-85 — see `architecture/morfo.md` Step 4). Contract attrs keep last-wins:
the runtime must win on state/wiring or the component lies.
- `hidden: false` / `disabled: false` → removed (a Svelte fix)
- the rest → last wins
### provider — `context()`
`context(name)` wraps `$libs/reactive`'s `Context` with descriptive errors. Providers
don't touch it directly: they expose the static `create()` / `get()` /
`require()` methods (see §7).
```ts
const ctx = context('Accordion');
ctx.set(instance); // registers in Svelte context
ctx.get(); // reads — throws if missing
ctx.getOr(fallback); // reads with a fallback
```
### keyboard — `KEYS`, `getDirectionalKeys`
```ts
KEYS.ENTER; // 'Enter'
KEYS.ESCAPE; // 'Escape'
KEYS.ARROW_DOWN; // 'ArrowDown'
KEYS.SPACE; // ' '
const { nextKey, prevKey } = getDirectionalKeys('ltr', 'horizontal');
// nextKey: 'ArrowRight', prevKey: 'ArrowLeft'
IsUsingKeyboard.current; // boolean — keyboard vs pointer
```
### dom — focus, roving, scroll lock
```ts
focusWithoutScroll(element);
focusFirst(candidates);
getTabbableCandidates(container);
getTabbableEdges(container);
const roving = new RovingFocusGroup({ candidateAttr, rootNode, loop, orientation });
roving.handleKeydown(currentElement, event);
const lock = new ScrollLock();
lock.locked.current = true; // locks body scroll
```
### attrs — boolean helpers
```ts
boolToStr(true); // 'true'
boolToEmptyStrOrUndef(true); // ''
boolToEmptyStrOrUndef(false); // undefined
boolToTrueOrUndef(true); // true
boolToTrueOrUndef(false); // undefined
```
## 9. data-\* contracts
The `data-*` attrs are formal public API, validated with `assertContract()`.
Convention:
```
data-dialog → provider (no -provider, no -root)
data-dialog-trigger → part
data-dialog-content → part
data-state="open|closed" → state
data-disabled → flag
data-side="top|right|bottom|left" → floating position
data-align="start|center|end" → alignment
data-starting-style → enter animation (1 frame)
data-ending-style → exit animation (persists)
data-nested → is a child of another of the same type
data-nested-open → has an open child
data-dragging → gesture drag active
data-highlighted → item with virtual focus (aria-activedescendant)
data-resizing → splitter resize active
```
Exposed CSS variables:
```
--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
IDs are generated with component context:
```
soma-dialog-c12
soma-dialog-trigger-c13
soma-dialog-content-c14
```
Pattern: `soma-{component}-{part}-{uid}`. Descriptive and inspectable.
## 11. Barrel exports
### Components (hierarchical)
```ts
// $soma/components/index.ts
export * as Collapsible from './collapsible';
export * as Dialog from './dialog';
export * as Popover from './popover';
```
Consumption:
```ts
import { Dialog, Popover } from '$soma/components';
Dialog.Provider; // not SomaDialogProvider, not TerraDialogProvider
Dialog.Trigger;
```
### Internal
```ts
import { Portal, Arrow, VisuallyHidden, Soma } from '$soma/components/internal';
```
## 12. External boundaries
`soma` distinguishes between:
- internal: `layers/`, `reactive/`, `dom/`, `provider/` — its own helpers
- external: `svelte` only (`clsx` is imported in `props/props.ts` without being
declared — it resolves as a transitive of svelte; a debt pending decision)
Floating positioning stopped being an external dependency: it is the in-house
engine (`layers/floating` + `$ethereal`); `@floating-ui` survives only as a
devDependency for the parity tests. **`runed` and `tabbable` followed the same
path (2026-07)**: they are no longer npm dependencies. `runed`'s reactive runes
were ported into `$libs/reactive` (`Context`, `watch`, `Previous`, `Debounced`,
`FiniteStateMachine`, `resource`, …) and `$adom` (`ElementSize`, rebuilt on the
ActiveDom runtime); `tabbable`'s focus-order engine was ported into
`$libs/dom` (`tabbable-core`, consumed by the existing `tabbable.ts` wrapper and
surfaced through `$adom`). soma now depends on nobody but svelte. If a dependency
has an unstable API or could change, it is accessed through a formal boundary (as
`layers/floating/` does with the positioning engine).
## 13. Directory structure
```
src/uix/soma/
├── SOMA_ARCHITECTURE.md ← stub (this chapter lives in docs/architecture/)
├── COMPONENT_GUIDE.md ← stub (the guide lives in docs/guides/component-guide.md)
├── README.md ← stub (the soma chapter lives in docs/architecture/)
├── runtime.svelte.ts ← SomaRuntime (morfo interpreter)
├── errors.ts ← typed runtime/context errors
├── core/
│ └── soma.svelte.ts ← the Soma class (root instance)
├── reactive/ ← the reactive system
├── provider/ ← context + opts bridge
├── props/ ← mergeProps, composeHandlers
├── keyboard/ ← KEYS, directional
├── dom/ ← DOM utilities, focus
├── css/ ← styleToString, cssToStyleObj
├── id/ ← createId (useId → $active-uix/id)
├── 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,
│ ├── {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 the `{name}.svelte` wrapper
- Reflects what's inside: provider/state classes
- Root wrapper: `{name}.svelte` in the `components/` subdirectory
- Export name: always `Provider`, never `Root`
## 14. Anti-patterns
Avoid in soma:
- Complex logic inside the wrapper `.svelte` — it belongs in the Provider
- Props drilling when context is the correct pattern
- `data-*` attrs outside the contract
- Inventing part names without checking the reference-library anatomies
(ark-ui, bits-ui, radix-ui)
- Nesting layers as component wrappers in templates
- Inline `z-index: auto` overriding 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 the parent)
- Redundant naming prefixes (SomaDialog, DialogLayerState)
- Dummy refs to satisfy a type — use `ProviderOpts` for no-DOM roots
- **State class file named like the wrapper** — `select.svelte.ts` +
`components/select.svelte` causes Vite module duplication. Always
`{name}-provider.svelte.ts`
- **Event handlers not in props** — defining onclick as a class method but
not including it in the derived props object
- **getContext in event handlers** — getContext only works during
initialization. Capture references in the constructor
- **Exporting as Root** — always `Provider`, never `Root`
- **Skipping the reference-library comparison** — a mandatory step, no
exceptions
- **Comments in Spanish** — all code comments in English
- **Standalone context functions** — no `createX()`, `getX()`, `useX()` as
loose functions. Use the `X.create()`, `X.get()`, `X.require()` statics
- **Importing from `$lib/ext/app`** in components — components access
services through `Soma`, never App directly
- **`from()` as a 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 (the
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'`** — the canonical form is numeric
`12 \| 24` (matches `Intl.DateTimeFormat.hour12`). String forms are legacy
## 15. Current shape (standing decisions)
Soma has gone through several phases. Its current shape (post-2026-05-08):
- **Provider inheritance dropped** — providers no longer inherit from an
abstract base; they are concrete classes. The shared DOM mechanics live in
`SomaRuntime.part(...)`, and cases needing semantic events use the same
`SomaRuntime` for `trigger`/`keydown`.
- **SomaRuntime caches** the morfo compilation (`compileMorfo` by WeakMap)
and registers the `effects` that sync `state → attrs` via `dom.apply`.
- **Naming**: `Provider` (never `Root`); child providers reference the parent
as `provider`, never `root`. A multi-part component's export keeps the
compound shape `Toggle.Provider + Toggle.Trigger + ...`.
- **Data-attr naming**: `data-{component}` (provider) and
`data-{component}-{kebab}` (sub-parts). No `data-soma-*` prefix. The
compiler emits these via `compiled.parts.attrs`.
- **State files**: `{name}-provider.svelte.ts` (explicit, no ambiguity).
- **IDs**: descriptive (`soma-dialog-trigger-c13`).
## 16. The stability rule
A soma component is considered stable when:
- its public API is clear and JSDoc-documented
- its `data-*` are registered and validated with `assertContract`
- the wrapper and the Provider follow the general pattern
- its base accessibility is solved (ARIA, roles, keyboard)
- its props have been compared against ark-ui, bits-ui and radix-ui
- it has a working demo page at `web/routes/uix/components/{component}/`
- it doesn't depend on local hacks, hardcoded z-indexes or demo CSS to stand
- it compiles with 0 errors (`svelte-check`)
## 17. New component checklist
See [`component-guide.md`](../guides/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.texts 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)
```