119 lines
5.5 KiB
Markdown
119 lines
5.5 KiB
Markdown
|
|
# ADR 0005: One token contract for every application, with a legacy alias layer
|
|||
|
|
|
|||
|
|
- **Status:** accepted
|
|||
|
|
- **Date:** 2026-10-05
|
|||
|
|
- **Revisit when:** an application adopts Tailwind v4 as its only styling mechanism, which
|
|||
|
|
would let the alias layer be dropped
|
|||
|
|
|
|||
|
|
## Context
|
|||
|
|
|
|||
|
|
Six applications consume design decisions today, each in its own dialect:
|
|||
|
|
|
|||
|
|
| Application | Mechanism | Palettes | Light/dark | Token indirection |
|
|||
|
|
| --- | --- | --- | --- | --- |
|
|||
|
|
| `go-doc` | Tailwind v4 `@theme` | 1, dark hardcoded | none | `--color-ink-*` |
|
|||
|
|
| `go-guess` | one 2800-line CSS file | 1, light hardcoded | none | 11 `:root` vars, mostly bypassed by ~120 hardcoded hexes |
|
|||
|
|
| `go-loose` | plain CSS | 4 × 2 modes | works | `[data-theme=X]` + `[data-mode=light]` |
|
|||
|
|
| `go-mad` | plain CSS, copied from `go-loose` | 1 × 2 modes | CSS present, JS dead | `[data-mode=X]` |
|
|||
|
|
| `go-places` | JS-injected `<style>` | 2 objects | broken | inline custom properties |
|
|||
|
|
| `go-tell` | plain CSS, **no custom properties at all** | 4, all dark | none | 340 hardcoded hexes |
|
|||
|
|
|
|||
|
|
Four problems follow from that, and only one of them is aesthetic:
|
|||
|
|
|
|||
|
|
1. **No shared vocabulary.** `--navy` in `go-guess` and `--surface` in `go-loose` mean
|
|||
|
|
unrelated things, so nothing can be shared and no token can be changed in one place.
|
|||
|
|
2. **No light mode in three of six.** A theme is the wrong level of abstraction to fix
|
|||
|
|
this: the missing axis is *appearance*, and a theme that is dark-only cannot express
|
|||
|
|
it.
|
|||
|
|
3. **Two incompatible theme mechanisms.** `go-loose` sets `data-theme` and `data-mode`
|
|||
|
|
as separate attributes; `go-tell` sets `data-theme` only, with the mode baked into
|
|||
|
|
each palette. An application can adopt one or the other, not both.
|
|||
|
|
4. **Tailwind is a different language.** A Tailwind application cannot consume a
|
|||
|
|
`:root` block; it needs `@theme`.
|
|||
|
|
|
|||
|
|
The brief was to consolidate, find the middle ground, and make that styling available to
|
|||
|
|
every application.
|
|||
|
|
|
|||
|
|
## Decision
|
|||
|
|
|
|||
|
|
Adopt `go-loose`'s two-attribute model as the contract, because it is the only
|
|||
|
|
mechanism that is both working and already copied by another application:
|
|||
|
|
|
|||
|
|
```html
|
|||
|
|
<html data-theme="tokyo-night" data-mode="dark">
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Two orthogonal axes. `data-theme` is the palette, `data-mode` is the appearance. Every
|
|||
|
|
theme therefore has both a light and a dark rendering, which fixes problem 2 without
|
|||
|
|
inventing a second concept.
|
|||
|
|
|
|||
|
|
Token names are namespaced with `--bn-`, and the rendered stylesheet emits a **legacy
|
|||
|
|
alias block** underneath:
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
:root[data-theme='tokyo-night'][data-mode='dark'] {
|
|||
|
|
--bn-bg: #1a1b26;
|
|||
|
|
--bn-surface: #20222f;
|
|||
|
|
/* ... */
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Compatibility with applications that already use these names. */
|
|||
|
|
:root[data-theme='tokyo-night'][data-mode='dark'] {
|
|||
|
|
--bg: var(--bn-bg);
|
|||
|
|
--surface: var(--bn-surface);
|
|||
|
|
--text: var(--bn-text);
|
|||
|
|
--muted: var(--bn-muted);
|
|||
|
|
--line: var(--bn-border);
|
|||
|
|
--accent: var(--bn-accent);
|
|||
|
|
--good: var(--bn-good);
|
|||
|
|
--danger: var(--bn-danger);
|
|||
|
|
--radius: var(--bn-radius);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The alias block is the middle ground made literal. `go-loose` and `go-mad` keep working
|
|||
|
|
unchanged, and the other four applications can migrate by deleting their own `:root`
|
|||
|
|
block and renaming nothing.
|
|||
|
|
|
|||
|
|
## Rationale
|
|||
|
|
|
|||
|
|
**Namespaced names, because the stylesheet is injected into someone else's page.** An
|
|||
|
|
unprefixed `--surface` on a host page is a collision waiting to happen, and the host's
|
|||
|
|
value wins or loses depending on order. `--bn-bg` cannot collide with anything.
|
|||
|
|
|
|||
|
|
**Aliases, because a rename is a coordinated six-application change.** Emitting both
|
|||
|
|
means an application adopts the contract by adding one `<link>`, with no call sites
|
|||
|
|
edited. That is the difference between a migration that happens and one that is
|
|||
|
|
proposed.
|
|||
|
|
|
|||
|
|
**Two axes rather than N themes.** Six applications currently have 13 palettes between
|
|||
|
|
them, several of which are the same palette twice. Splitting palette from appearance
|
|||
|
|
turns 13 one-dimensional choices into 7 palettes × 2 modes, and makes "give me a light
|
|||
|
|
version" a solvable problem instead of a request for a new theme.
|
|||
|
|
|
|||
|
|
**Attributes, not classes.** `[data-theme=X]` beats `.theme-x` because an attribute is
|
|||
|
|
one axis for two features, and because a consumer can set it in the `<head>` before
|
|||
|
|
first paint. A class list forces the two axes into an ordering the cascade then has to
|
|||
|
|
be told about.
|
|||
|
|
|
|||
|
|
**`color-mix`, not extra tokens.** `go-loose` and `go-mad` already derive hover and
|
|||
|
|
subtle states with `color-mix(in srgb, var(--bn-accent) 12%, var(--bn-surface))`. That
|
|||
|
|
is kept: it means one accent serves every interaction state, instead of a hand-tuned
|
|||
|
|
ladder that drifts between themes.
|
|||
|
|
|
|||
|
|
**`--bn-font-*` and `--bn-radius` join the set.** Both applications that have a scale
|
|||
|
|
disagree (`go-loose` uses 16px cards, `go-mad` 8px; `go-doc` uses Tailwind defaults).
|
|||
|
|
Consolidation without them would leave typography and geometry app-local.
|
|||
|
|
|
|||
|
|
## Consequences
|
|||
|
|
|
|||
|
|
- The rendered stylesheet grows by the alias block. It is small, it is compressible, and
|
|||
|
|
it is removed entirely once an application has renamed its own references.
|
|||
|
|
- `go-doc` needs a three-line Tailwind bridge, because `@theme` cannot read a runtime
|
|||
|
|
custom property. It becomes `@theme inline { --color-bn-bg: var(--bn-bg) }`.
|
|||
|
|
- `go-tell`'s four themes are palettes with the appearance baked in. They become four
|
|||
|
|
`data-theme` values with their dark values as the `dark` mode, and their light
|
|||
|
|
counterparts have to be authored. That work is the honest cost of the migration.
|
|||
|
|
- Appearance preference is per-application. Two applications on the same browser do not
|
|||
|
|
share a `data-mode` unless they agree on a storage key; the shipped components use one
|
|||
|
|
key per origin and document it.
|