Skip to content

sparkles:ui theme — Feature Requirements (THM)

Status: planned · Date: 2026-07-29 · Scope: the unified, runtime-swappable design language — syntax highlighting rules, semantic widget slots, glyph sets and chrome metrics in one value, gated by terminal capabilities.

Design & rationale

A "theme" today means three unrelated things in this repository: an ordered list of dotted selectors mapping syntax labels to text styles; a semantic Slot → Visual palette with scalar chrome metrics; and a terminal Theme carrying 16-color roles, box-drawing charsets and status glyphs. They share no vocabulary, they are configured separately, and two different enums are even named BorderStyle with different meanings.

That split is wrong for the goal: an application should express its whole design language as one value the user can swap at runtime. Whether a code keyword is mauve, whether a popup border is rounded or square, and whether a table uses heavy or light box-drawing are all the same kind of decision.

The unification is cheap because the syntax rules' value type is already a base text-style type — the merged theme needs no syntax types at all, so placing it in sparkles:ui does not drag a highlighting engine into the toolkit. It goes the other way: sparkles:syntax consumes the theme.

IMPORTANT

The syntax channel is opaque to sparkles:ui. The toolkit carries the rules as plain data — dotted selector strings paired with text styles — and cannot resolve them. Resolution needs a label vocabulary (LabelSet), which is a syntax-highlighting concept; it and ResolvedTheme/resolveTheme stay in sparkles:syntax. Moving them would invert the dependency and close a cycle.

Alternative considered and rejected: putting the unified theme in sparkles:base, which would keep sparkles:syntax free of any UI dependency for future non-UI consumers. Every value in the merged theme is already base-typed, so it would work. Rejected because the design language belongs with the toolkit that defines the slots and metrics, and sparkles:ui's dependency set (base + input) is nearly as small as base's own. Recorded here rather than only in a plan so it is not relitigated.

Channels

ChannelContentReplaces
syntaxordered rules mapping dotted label selectors to text styles; resolved once against a label vocabulary into an O(1) tablesparkles.syntax.theme
slotssemantic role → resolved appearance (fg/bg with alpha, attributes, border, shadow, font role/scale)sparkles.ui.style.Palette
glyphsbox-drawing charsets and border presets, table glyphs, status marks, tree guides, meter and spinner framescore_cli.ui.theme + table glyphs
metricsscalar chrome — corner radius, paddings, gaps, border widths, shadow offsets, font scales, arrow sizePalette's scalar fields

Requirements

IDRequirementStatusTraces to
THM1A widget must reference a semantic slot, never a concrete color; resolution to concrete appearance happens during display-list construction, so the tree stays presentation-free.fullstyle.d Slot; display_list.d
THM2The slot vocabulary must cover general design-system roles — surfaces, borders, text emphasis levels, semantic status (error/warn/info/success), interactive accents, selection, focus, muted/disabled — not only the roles one consumer happened to need first.partialstyle.d Slot
THM3Slot resolution must yield a Visual carrying foreground, background, alpha, text attributes, border, radius, shadow, font role and font scale, so every backend reads one resolved value.fullstyle.d Visual, resolveVisual
THM4Chrome metrics must live in the theme, not be hardcoded in views, so a theme can restyle spacing and border weight without code changes.fullstyle.d Palette scalar fields
THM5Where the theme is mirrored by a stylesheet, the D values and the CSS must be asserted in lockstep by tests, so drift is a build failure rather than a visual regression.fulltwoslash paletteLockstep/metricsLockstep
THM6A single Theme value must carry all four channels — syntax rules, slots, glyphs and metrics — so an application expresses its design language once.partialui/theme.d Theme (glyph channel declared, filled with the component move)
THM7Themes must be swappable at runtime: re-resolving rebuilds every derived table and repaints. Observable criterion: a frame rendered after swapping to theme B must be byte-identical to a cold render started with B — which fails if any derived table or resolved color survives the swap.partialproposed Theme.resolve
THM8Glyph and color-tier selection must be capability-gated through the target's declared capabilities (TGT5), not from per-call flags threaded by hand. A target reporting no Unicode support gets the ASCII charset; the color tier follows the declared depth.not startedTGT5; base.term_caps (the TUI adapter's input)
THM9The theme must be plain data, loadable from a file (and serializable back) without code changes, so users can ship their own. Parsing formats are out of scope here; the requirement is that the value contains no code.researchedproposed theme file format

NOTE

THM2 is the one substantive widening. The shipped slot set was designed for a single overlay feature and has no interaction-state axis and no general accent/selection/focus roles. A design language for a whole application needs them, and the widget catalog in widgets.md cannot be expressed without them.

WARNING

THM8 must not gate on a terminal snapshot directly. Only the TUI target has a TermCaps; the GPU and HTML targets have none and always have Unicode, and one process can render several targets in the same run (the parity harness does exactly that). Gating the theme on isTerminal would ship an ASCII-bordered GUI the first time stdout is not a tty. The authored theme is one value; capability application is per-target, and the terminal snapshot is merely the TUI adapter's input to its declared capability set.

NOTE

The glyph channel arrives with the component move, not before: its content (BorderStyle presets, table glyphs, status marks) is currently defined in terms of core_cli.ui's own box and table types. sparkles:ui declares the channel with its own plain-data glyph types; the core_cli.ui.theme values are mapped onto them when those components move, and the old module retires then. Folding it in earlier would mean either ui → core-cli (forbidden by PKG1) or a partial component move smuggled into the theme work. See migration.md.

Milestones

MilestoneScopeStatusRequirements
H0Slot vocabulary widened to general design-system rolesnot startedTHM2
H1Single Theme value with all four channels; syntax rules move innot startedTHM6
H2Capability gating of glyphs and color tiernot startedTHM8
H3Runtime swap across every backend, with derived tables rebuiltnot startedTHM7
H4File-loadable themesnot startedTHM9

Module coverage

Source fileRequirements
libs/ui/src/sparkles/ui/style.dTHM1THM5
libs/ui/src/sparkles/ui/theme.dTHM6THM9
libs/ui/src/sparkles/ui/display_list.dTHM1, THM3

Relationship to existing specs

PieceRole in theming
sparkles:syntax label vocabularythe selector space the syntax channel resolves against
sparkles.base.term_stylethe underlying text-style and color value types
sparkles.base.term_capsthe capability snapshot gating glyphs and color tier (THM8)
widgets.md WGTthe consumers of slots; THM2's widened vocabulary serves them
backends.md TGTper-target degradation of chrome the theme requests

Overview · Widgets · Backends