| 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://makenot.work/git/max/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://makenot.work/git/max/makeover/tree/main/themes/akari-dawn.toml)**; the default dark is **[Akari Night](https://makenot.work/git/max/makeover/tree/main/themes/akari-night.toml)**. Both are based on Shu Kutsuzawa's [Akari](https://github.com/cappyzawa/akari-theme) (MIT). A mode is picked in `alloy settings`, which writes two things: the console's own theme key, effective immediately, and `~/.config/alloy/mode`, which every other themed config follows at the next login via `alloy theme apply`. A console told nothing follows what it is drawn on instead, asking `$COLORFGBG` first and the mode file second. `--theme` overrides either for one run. Users can also drop any makeover `.toml` into `~/.config/alloy/themes/` and name it. |
| 49 |
|
| 50 |
There is no OSC background query. It is the only other way to learn a terminal's background, and it means writing an escape sequence and waiting for a reply before the first frame, which is not worth the stall. |
| 51 |
|
| 52 |
**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. |
| 53 |
|
| 54 |
## Alloy-specific derived tokens |
| 55 |
|
| 56 |
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: |
| 57 |
|
| 58 |
``` |
| 59 |
border-subtle = mix(line.border, surface.page, 60%) # pulled toward surface — decorative divider |
| 60 |
border-strong = mix(line.border, content.primary, 65%) # pulled toward text — focus rings, selection |
| 61 |
``` |
| 62 |
|
| 63 |
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. |
| 64 |
|
| 65 |
The formulas live in `alloy_tui`, not in the theme file. Theme files stay minimal and cross-app compatible. |
| 66 |
|
| 67 |
## Authoring discipline: OKLCH mental model, hex on disk |
| 68 |
|
| 69 |
Themes on disk are hex. The design *reasoning* behind those hex values (Alloy-authored ones or curated ones like Akari) still uses OKLCH: |
| 70 |
|
| 71 |
- **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. |
| 72 |
- **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. |
| 73 |
- **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`. |
| 74 |
|
| 75 |
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. |
| 76 |
|
| 77 |
## Gate: the WCAG audit |
| 78 |
|
| 79 |
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. |
| 80 |
|
| 81 |
Run: |
| 82 |
|
| 83 |
``` |
| 84 |
python3 tools/wcag_audit.py ../Libraries/makeover/themes/akari-dawn.toml |
| 85 |
``` |
| 86 |
|
| 87 |
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. |
| 88 |
|
| 89 |
## Verified contrast: Akari Dawn |
| 90 |
|
| 91 |
Method: OKLCH-agnostic; reads hex, computes WCAG 2.1 relative luminance directly. |
| 92 |
|
| 93 |
**Text on surfaces:** |
| 94 |
|
| 95 |
|
| 96 |
|
| 97 |
| `content.primary` | 10.31 | 13.25 | 14.40 | 15.03 | |
| 98 |
| `content.secondary` | 8.15 | 10.48 | 11.39 | 11.88 | |
| 99 |
| `content.muted` | 5.01 | 6.44 | 7.00 | 7.30 | |
| 100 |
|
| 101 |
All twelve pairings clear AA-text (4.5); most clear AAA (7). |
| 102 |
|
| 103 |
**Borders on surfaces:** |
| 104 |
|
| 105 |
|
| 106 |
|
| 107 |
| `border-strong` (derived) | 3.27 | 3.55 | 3.71 | |
| 108 |
| `line.border` | 1.37 | 1.49 | 1.55 | |
| 109 |
| `border-subtle` (derived) | 1.12 | 1.22 | 1.27 | |
| 110 |
|
| 111 |
`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. |
| 112 |
|
| 113 |
**Accents on surfaces:** |
| 114 |
|
| 115 |
|
| 116 |
|
| 117 |
| `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 | |
| 118 |
| `status.danger` (`#6a2828`) | 8.07 | 8.78 | 6.28 | 9.15 | AAA everywhere | |
| 119 |
| `status.success` (`#3a5830`) | 6.00 | 6.53 | 4.67 | 6.81 | AA-text everywhere | |
| 120 |
| `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. | |
| 121 |
| `status.info` (`#304050`) | 7.96 | 8.65 | 6.19 | 9.03 | AAA / AA-text everywhere | |
| 122 |
|
| 123 |
Both failure modes are Akari-inherent rather than 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. |
| 124 |
|
| 125 |
## Verified contrast: Akari Night |
| 126 |
|
| 127 |
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. |
| 128 |
|
| 129 |
## What's deferred |
| 130 |
|
| 131 |
- **Calibrated-display verification** on real hardware in real terminals (truecolor and 256-color). Pre-1.0 gate. |
| 132 |
- **256-color downgrade table** for terminals that report no truecolor. |
| 133 |
- **Figlet / block-element recipes** for the large numeric readouts a display font would otherwise carry. |
| 134 |
- **Swapping the desktop's mode without a re-login.** `alloy theme apply` ships and the settings row writes the mode file, but the configs it lays down are read once by programs already running, so the switch lands at the next login. The console itself re-themes immediately. |
| 135 |
|
| 136 |
## How this file is consumed |
| 137 |
|
| 138 |
The themes [makeover ships](https://makenot.work/git/max/makeover/tree/main/themes/) are the ground truth. `alloy_tui` at load time: |
| 139 |
|
| 140 |
1. Reads the selected theme file via the `makeover` crate. |
| 141 |
2. Computes the two derived tokens (`border-subtle`, `border-strong`) from `line.border`. |
| 142 |
3. Exposes the full token map as a runtime `Theme` of ratatui `Style` / `Color` values. |
| 143 |
4. Optionally emits a 256-color fallback table for terminals without truecolor. |
| 144 |
|
| 145 |
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. |
| 146 |
|