Learn / UI architecture
The layers
Anatomy — what parts exist. A Slots.define contract
(foldkit-mixins), e.g. DialogSlots, is the anatomy value: named slots
carrying capability, events, attributes, requirements, and visibility.
Slots.describe renders a contract inspectable; Slots.withCapability
selects the slots one capability fits. Every adapted widget publishes a
full typed anatomy, not the minimum currently needed.
Capability — what each part can do. Base -> Interactive ->
{ Container, Focusable -> TextInput, Draggable }, Base -> Collection,
checked by Behavior.forSlots when the behavior is defined, not when it
attaches. A capability is added only when a behavior needs to require it:
taxonomy without a consumer is dead weight. (Today's set is exactly
Base, Interactive, Container, Focusable, TextInput, Draggable,
Collection — nothing else has earned its place.)
Pattern — what those parts must do together. An A11y.pattern per
widget with an ARIA contract, plus a Patterns.catalog entry
{ name, pattern, slots, tier, roles, floor }. CI validates every entry's
slots against its pattern, so an adapter cannot drift from the contract it
claims — and a custom view built from primitives validates against the
same pattern. Timing and positioning behaviors without an ARIA contract of
their own (Anchor, HoverIntent) get none; they borrow their content's.
State — what the widget owns. A Bundle, a Submodel, or a primitive;
never a Behavior. A Behavior that "needs state" means a Submodel is
missing. State transitions are Messages through update; element effects
are Mounts; I/O is Message -> Command.
Behavior — reusable interaction attached to compatible anatomy:
Behavior.forSlots(Slots)<Input, Message>({ slot: Behavior.slot({
requires, attributes, mount }) }), applied with Behavior.attach
(which returns a new view; the original is untouched). Cross-component by
construction: a tooltip behavior asks only "does this slot satisfy
Interactive/Focusable?", never "is this a Button?".
Style — how parts look. Style.forSlots(Slots)({ slot: piece })
with classes, inline declarations, rules, and states, compiled to
deterministic classes in named layers (Layers.standard). A slot style is
layered when compiled, so views and the stylesheet share one value.
Recipe — the default design policy. Style.recipeFor(Slots)({ base,
variants, defaults, compound }) over semantic theme tokens, .extended
rather than copied (IconButton extends Button, it does not redeclare
it). Recipes ship for the common contracts (Recipes.Button, ...);
Segmented shows the shape for the rest.
Theme / Knobs — system-wide policy. Tokens are semantic values;
knobs (Theme.tokens' density/radius factors) are global
transformations; variants are component-local decisions; state is runtime.
Recipes talk only to tokens, so re-knobbing recolors the suite.
Widget — the assembled default. Either an upstream @foldkit/ui
component adapted through its toView seam, or — where upstream exposes
no seam — a view fork transcribed from the pinned source with the seam
added and behavior staying upstream (documented per fork, deleted if
upstream gains the seam). Every widget registers anatomy, pattern,
recipe, tier, and platform floor in the catalog.
Block — application-level composition (SettingsPage = Form +
Surface + Field + Tabs + Remote), teaching architecture rather than
shipping a lump of view code. Blocks arrive last, when the layers below
make them boring.
The resolver rules (the whole merge semantics)
Base attributes and ChildAttributes survive by identity; classes are
additive; inline declarations merge per property; each event and scalar
attribute has exactly one owner (a second owner throws
mixins:event-conflict, never silently doubles); Key/InnerHTML cannot
come from a mixin; all mounts compose. Diagnostics carry codes
(mixins:unknown-slot, a11y:missing-slot, ...), never bare strings.
Platform floor
For every widget, declare what the browser already guarantees
(Popover.pattern.floor, Patterns.catalog floor) instead of
reimplementing it in JS: native controls, form submission, top-layer,
focus trap, escape/outside dismiss, inert. Theme.breakpointWidths and
Style.responsive keep viewport/container/preference conditions in data,
not ad-hoc CSS.