| 1 |
# Alloy Tokens |
| 2 |
|
| 3 |
The design-token system for Alloy. Storage format, authoring discipline, extension rules, and how Alloy renders any theme downloaded from the future theme catalog. |
| 4 |
|
| 5 |
The rules these tokens implement live in [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md). This file is where the rules become storage, derivation, and gates. |
| 6 |
|
| 7 |
## Storage format: makeover |
| 8 |
|
| 9 |
Alloy consumes themes in the [makeover](https://git.sr.ht/~maxmj/makeover) TOML format, the same schema GoingsOn, Balanced Breakfast, audiofiles, and makenot.work already consume. A theme file has seven sections: |
| 10 |
|
| 11 |
```toml |
| 12 |
[meta] |
| 13 |
name = "Akari Dawn" |
| 14 |
variant = "light" # "light" | "dark" | "high-contrast" |
| 15 |
|
| 16 |
[surface] |
| 17 |
page = "#e4ded6" # main canvas |
| 18 |
raised = "#ede7de" # elevated / focused / popup base (lighter than page in light mode) |
| 19 |
sunken = "#cfc4b6" # recessed panels (darker than page in light mode) |
| 20 |
overlay = "#f0ece4" # modal / top-most temporary UI |
| 21 |
|
| 22 |
[content] |
| 23 |
primary = "#1a1816" # body text |
| 24 |
secondary = "#222d38" # section headers, labels |
| 25 |
muted = "#514b45" # disabled, placeholder, metadata |
| 26 |
|
| 27 |
[action] |
| 28 |
primary = "#8a4530" # the theme's primary action / brand accent |
| 29 |
|
| 30 |
[status] |
| 31 |
danger = "#6a2828" # error |
| 32 |
success = "#3a5830" # healthy / ok |
| 33 |
warning = "#b07840" # near-threshold |
| 34 |
info = "#304050" # informational / link / in-progress |
| 35 |
|
| 36 |
[line] |
| 37 |
border = "#cabeae" # single border tone; Alloy derives -subtle and -strong (see below) |
| 38 |
|
| 39 |
[category] |
| 40 |
one = "#8a4530" # categorical series colors (chart / syntax / tag rows) |
| 41 |
two = "#3a5830" |
| 42 |
three = "#304050" |
| 43 |
four = "#b07840" |
| 44 |
five = "#806080" |
| 45 |
six = "#305858" |
| 46 |
``` |
| 47 |
|
| 48 |
Alloy's default light theme is **[Akari Dawn](https://git.sr.ht/~maxmj/makeover/tree/main/item/themes/akari-dawn.toml)**; the default dark is **[Akari Night](https://git.sr.ht/~maxmj/makeover/tree/main/item/themes/akari-night.toml)**. Both are based on Shu Kutsuzawa's [Akari](https://github.com/cappyzawa/akari-theme) (MIT). Users pick a mode at startup (`--theme` flag or `$COLORFGBG` / OSC background query); users can also drop any makeover `.toml` into `~/.config/alloy/themes/` and pass its name to swap. |
| 49 |
|
| 50 |
**Why makeover as the storage format:** the future user-facing theme catalog (a website of downloadable `.toml` files) is only useful if every make-family app renders any downloaded theme the same way. Sharing the format across apps buys that with zero adapter code. |
| 51 |
|
| 52 |
## Alloy-specific derived tokens |
| 53 |
|
| 54 |
Alloy needs three token tiers makeover ships one of: `border-subtle`, `border`, `border-strong`. Rather than extend the schema (which would break existing consumers), Alloy computes the outer two from `line.border` at load time: |
| 55 |
|
| 56 |
``` |
| 57 |
border-subtle = mix(line.border, surface.page, 60%) # pulled toward surface — decorative divider |
| 58 |
border-strong = mix(line.border, content.primary, 65%) # pulled toward text — focus rings, selection |
| 59 |
``` |
| 60 |
|
| 61 |
Mix is in linear sRGB. The 65%-toward-text ratio for `border-strong` is calibrated to reach WCAG AA-UI (≥3.0:1) against `surface.page` even on soft-border themes (Akari's `line.border` is `#cabeae`; the mix produces `#7f786d` which lands at 3.27:1). Themes with crisper borders will overshoot; that's fine. |
| 62 |
|
| 63 |
The formulas live in `alloy_tui`, not in the theme file. Theme files stay minimal and cross-app compatible. |
| 64 |
|
| 65 |
## Authoring discipline: OKLCH mental model, hex on disk |
| 66 |
|
| 67 |
Themes on disk are hex. The design *reasoning* behind those hex values (Alloy-authored ones or curated ones like Akari) still uses OKLCH: |
| 68 |
|
| 69 |
- **Ramp discipline.** Surface tiers should follow monotonic L ordering per mode (light: `sunken < page < raised < overlay`; dark: inverted). Accents live in the mid-L / mid-C range where they read as color-carrying-information on either ramp. |
| 70 |
- **Tinted-not-neutral chrome.** Chrome tones should carry a hue tint (see [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md#core-principle-tinted-greyscale-chrome-color-as-information)); Akari's warm-clay palette is the canonical worked example. |
| 71 |
- **Accent-on-glyph, not on body text.** Confirmed by contrast math in the audit: accents that clear WCAG AA-text (4.5:1) on the theme's surface can be used freely; accents that only clear AA-UI (3.0:1) go on glyphs and edge markers, with body text staying at `content.primary`. |
| 72 |
|
| 73 |
Because storage is hex, OKLCH doesn't need to be preserved in the file; it's the mental model of the person authoring the theme. |
| 74 |
|
| 75 |
## Gate: the WCAG audit |
| 76 |
|
| 77 |
Any theme, downloaded or authored, is audited by [`tools/wcag_audit.py`](../tools/wcag_audit.py). It reads a makeover `.toml`, converts hex to linear sRGB to WCAG 2.1 relative luminance, and reports pass/fail for every affordance-carrying pair (text-on-surface, borders-on-surface, accents-on-surface, elevation deltas). Also computes `border-subtle` / `border-strong` via the same derivation Alloy uses at runtime. |
| 78 |
|
| 79 |
Run: |
| 80 |
|
| 81 |
``` |
| 82 |
python3 tools/wcag_audit.py ../Libraries/makeover/themes/akari-dawn.toml |
| 83 |
``` |
| 84 |
|
| 85 |
The audit is not a hard-fail gate: a theme with accents that only reach AA-UI on some surface tiers is still shippable if the accent-on-glyph rule is honored. The audit's job is to make trade-offs visible. |
| 86 |
|
| 87 |
## Verified contrast: Akari Dawn |
| 88 |
|
| 89 |
Full audit output at time of adoption. Method: OKLCH-agnostic; reads hex, computes WCAG 2.1 relative luminance directly. |
| 90 |
|
| 91 |
**Text on surfaces:** |
| 92 |
|
| 93 |
|
| 94 |
|
| 95 |
| `content.primary` | 10.31 | 13.25 | 14.40 | 15.03 | |
| 96 |
| `content.secondary` | 8.15 | 10.48 | 11.39 | 11.88 | |
| 97 |
| `content.muted` | 5.01 | 6.44 | 7.00 | 7.30 | |
| 98 |
|
| 99 |
All twelve pairings clear AA-text (4.5); most clear AAA (7). |
| 100 |
|
| 101 |
**Borders on surfaces:** |
| 102 |
|
| 103 |
|
| 104 |
|
| 105 |
| `border-strong` (derived) | 3.27 | 3.55 | 3.71 | |
| 106 |
| `line.border` | 1.37 | 1.49 | 1.55 | |
| 107 |
| `border-subtle` (derived) | 1.12 | 1.22 | 1.27 | |
| 108 |
|
| 109 |
`border-strong` clears AA-UI on all three surfaces; the derivation formula works for Akari's soft border. `line.border` and `border-subtle` are decorative dividers, not required to meet 3.0. |
| 110 |
|
| 111 |
**Accents on surfaces:** |
| 112 |
|
| 113 |
|
| 114 |
|
| 115 |
| `action.primary` (lantern `#8a4530`) | 5.28 | 5.74 | **4.11** | 5.99 | AA-text on three of four; AA-UI on `sunken`, glyph-safe on the darkest tier | |
| 116 |
| `status.danger` (`#6a2828`) | 8.07 | 8.78 | 6.28 | 9.15 | AAA everywhere | |
| 117 |
| `status.success` (`#3a5830`) | 6.00 | 6.53 | 4.67 | 6.81 | AA-text everywhere | |
| 118 |
| `status.warning` (`#b07840`) | **2.80** | **3.05** | **2.18** | **3.18** | Fails AA-text on all tiers; AA-UI only on `raised`/`overlay`. **Amber-on-cream is inherent to Akari's aesthetic**; per the accent-on-glyph rule, warning is expressed as glyph + edge marker only. | |
| 119 |
| `status.info` (`#304050`) | 7.96 | 8.65 | 6.19 | 9.03 | AAA / AA-text everywhere | |
| 120 |
|
| 121 |
The two failure modes are both Akari-inherent, not derivation bugs, and both survive Alloy's discipline: `action.primary` on `surface.sunken` still clears AA-UI (glyph-safe on the darkest recessed panel), and `status.warning`'s amber is a glyph-only accent per the same rule that governs `accent-syntax` in the previous Alloy palette. |
| 122 |
|
| 123 |
## Verified contrast: Akari Night |
| 124 |
|
| 125 |
Text on surfaces all AAA or AA-text. Borders derived at 7+ contrast (dark themes have more headroom because the border tone is against a light content.primary). Notable accent behavior: `status.danger` at `#d25046` lands at 3.3–4.1:1 across surfaces: AA-UI territory, glyph-safe under the same rule. Full audit reproducible via the script. |
| 126 |
|
| 127 |
## What's deferred |
| 128 |
|
| 129 |
- **Calibrated-display verification** on real hardware in real terminals (truecolor and 256-color). Pre-1.0 gate. |
| 130 |
- **256-color downgrade table** for terminals that report no truecolor. |
| 131 |
- **Figlet / Nerd-Font recipes** for the large numeric readouts a display font would otherwise carry. |
| 132 |
- **`alloy theme <name>` subcommand** (see [CONSOLE.md](CONSOLE.md)), which wraps theme swap. The console reads themes at startup today, via `--theme` or a guess from `$COLORFGBG`; swapping in place without a relaunch is what remains. |
| 133 |
|
| 134 |
## How this file is consumed |
| 135 |
|
| 136 |
The themes [makeover ships](https://git.sr.ht/~maxmj/makeover/tree/main/item/themes/) are the ground truth. `alloy_tui` at load time: |
| 137 |
|
| 138 |
1. Reads the selected theme file via the `makeover` crate. |
| 139 |
2. Computes the two derived tokens (`border-subtle`, `border-strong`) from `line.border`. |
| 140 |
3. Exposes the full token map as a runtime `Theme` of ratatui `Style` / `Color` values. |
| 141 |
4. Optionally emits a 256-color fallback table for terminals without truecolor. |
| 142 |
|
| 143 |
No hex values are hard-coded in Rust. Changing the theme file changes every authored Alloy surface after re-launch (or hot-swap once `alloy theme` ships). Third-party themes downloaded into `~/.config/alloy/themes/` work with zero adapter code. |
| 144 |
|