go-doc/data/docs/go-bananas-0005-shared-token-contract.md
2026-10-07 15:05:48 +02:00

125 lines
5.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- go-doc:generated:begin source=go-bananas:docs/adr/0005-shared-token-contract.md signature=7d8b696ba971c1cbd2e19854d26de304 generated=2026-10-05 18:14 UTC -->
Imported from docs/adr/0005-shared-token-contract.md in the go-bananas repository.
---
# 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.
<!-- go-doc:generated:end -->