skill
Adom Theme
Public Made by Adomby adom
The canonical Adom application theme spec: five schemes, exact tokens, font pairings, and the implementation contract.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
#!/usr/bin/env node
// Adom Studio — Dark 2026, re-grounded on the Adom brand.
//
// NAMING (John 2026-07-27): this design started life as "Adom Studio v2"; once
// approved it TOOK OVER the "Adom Studio" name and the old template-projected
// Studio was abandoned. Keeping the name means every existing reference — the
// v21 golden image's baked default, HD's scheme mapping, every user's
// settings.json — resolves to the new design with zero migration.
//
// John 2026-07-27: "Dark 2026 is a perfect theme. ... take Dark 2026 and just
// shift all the grays to be more to the dark teal ... the most important thing
// is the main background color that Adom Studio uses right now matches the main
// adom brand and when a vscode panel is next to a wiki panel, they look like the
// same brand. and the foreground text color we picked for adom studio is the
// right base text color. ... stay true to the color differences that Dark 2026
// setup cuz it did an excellent job so we don't want to ruin it."
//
// So this file changes NO relationships. It reads the resolved Dark 2026 theme
// (tools/reference-2026-dark.json, extracted verbatim from code-server's
// theme-defaults) and applies one mathematical re-grounding:
//
// NEUTRALS every gray is re-issued on the Adom slate hue (215°, the hue of
// #161b22) at a luminance produced by a two-anchor map in
// log(L + 0.05) space: 2026 bg #121314 -> #161b22 (Adom ground)
// 2026 fg #BBBEBF -> #c9d1d9 (Adom body text)
// Contrast ratios are functions of (L + 0.05), so a log-space affine
// map preserves 2026's contrast ARCHITECTURE while landing both
// anchors exactly. Between/beyond the anchors the scale interpolates,
// which keeps every 2026 step visibly the same step.
//
// ACCENT the 2026 product-blue family (#3994BC etc., hue 180-240 at real
// saturation) moves to the Adom teal (hue 177), same luminance map —
// focus rings, badges, buttons keep their exact prominence, in brand.
//
// SEMANTIC reds/greens/yellows/purples (errors, git, warnings) and the whole
// syntax palette are UNTOUCHED. Color-as-meaning is 2026's design;
// re-tinting it would "ruin it with color shifting".
//
// node tools/gen-adom-studio-v2.mjs [outDir]
import { readFileSync, writeFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const outDir = resolve(process.argv[2] ?? join(root, 'vscode-themes'));
const ref = JSON.parse(readFileSync(join(root, 'tools/reference-2026-dark.json'), 'utf8'));
// ── anchors ──────────────────────────────────────────────────────────────────
const BG_OLD = '#121314', BG_NEW = '#161b22'; // Adom brand ground (wiki-adjacent)
const FG_OLD = '#bbbebf', FG_NEW = '#c9d1d9'; // Adom Studio base text (John: "the right base text color")
const NEUTRAL_HUE = 215; // hue of #161b22 — the Adom slate cast
const NEUTRAL_SAT = 0.16; // in-family with #161b22 (.21) / #30363d (.12) / #8b949e (.09)
const TEAL_HUE = 177; // Adom accent #00b8b1
// ── color math ───────────────────────────────────────────────────────────────
const clamp = (x, a, b) => Math.min(b, Math.max(a, x));
function hexToRgb(h) { return [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16)); }
function rgbToHex(r) { return '#' + r.map((c) => clamp(Math.round(c), 0, 255).toString(16).padStart(2, '0')).join(''); }
function srgbToLin(c) { c /= 255; return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; }
function relLum(h) { const [r, g, b] = hexToRgb(h); return 0.2126 * srgbToLin(r) + 0.7152 * srgbToLin(g) + 0.0722 * srgbToLin(b); }
function rgbToHsl(h) {
let [r, g, b] = hexToRgb(h).map((c) => c / 255);
const mx = Math.max(r, g, b), mn = Math.min(r, g, b), l = (mx + mn) / 2;
if (mx === mn) return [0, 0, l];
const d = mx - mn;
const s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn);
let hh = mx === r ? (g - b) / d + (g < b ? 6 : 0) : mx === g ? (b - r) / d + 2 : (r - g) / d + 4;
return [hh * 60, s, l];
}
function hslToHex(h, s, l) {
h = ((h % 360) + 360) % 360 / 360;
const q = l < 0.5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q;
const f = (t) => {
t = ((t % 1) + 1) % 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
return rgbToHex([f(h + 1 / 3), f(h), f(h - 1 / 3)].map((c) => c * 255));
}
/** Color at (hue, sat) whose relative luminance ≈ target, solved by bisection on HSL lightness. */
function atLuminance(hue, sat, target) {
let lo = 0, hi = 1;
for (let i = 0; i < 40; i++) {
const mid = (lo + hi) / 2;
relLum(hslToHex(hue, sat, mid)) < target ? (lo = mid) : (hi = mid);
}
return hslToHex(hue, sat, (lo + hi) / 2);
}
// Two-anchor luminance map in log(L+0.05) space: bg->bg and fg->fg land exactly,
// everything else interpolates/extrapolates linearly, preserving step ORDER and
// (piecewise) the multiplicative structure contrast ratios live in. x0/x1 are the
// OLD (Dark 2026) anchors, shared by every re-grounding; only the ground (y0) moves.
const x0 = Math.log(relLum(BG_OLD) + 0.05), x1 = Math.log(relLum(FG_OLD) + 0.05);
// buildTheme re-grounds Dark 2026 onto ONE Adom ground. Adom Studio and Adom Studio
// Dark are the SAME brand-accurate treatment (same base text, same slate hue, same
// teal re-issue, same contrast-preserving map); they differ ONLY in the ground
// luminance. John 2026-08-01: "for Adom Studio Dark ... we use a lot of the mods we
// made for Adom Studio ... but we stay true to the teals ... based on Dark 2026." So
// Studio Dark stops being a template projection and becomes Dark 2026 re-grounded on
// the deeper #0d1117, exactly the way the flagship is on #161b22.
//
// fgNew moves the TEXT anchor the same way bgNew moves the ground. Adom Studio Bright
// (John 2026-08-01) is "Studio Dark with brighter text": same #0d1117 ground, but the
// whole neutral text scale lifted by re-anchoring fg onto #e6edf3. One anchor move
// brightens primary AND secondary text proportionally, so the hierarchy 2026 designed
// (active > primary > secondary) survives instead of flattening.
//
// syntaxMinRatio, when set, lifts code colours WITHIN their hue to at least that
// contrast ratio against the ground -- the 2026-07-25 keyword ruling (7:1 AAA, raise
// lightness only, never swap hue: "the colour IS the meaning").
function buildTheme({ bgNew, name, file, fgNew = FG_NEW, syntaxMinRatio = 0 }) {
const y0 = Math.log(relLum(bgNew) + 0.05), y1 = Math.log(relLum(fgNew) + 0.05);
const mapLum = (L) => {
const x = Math.log(L + 0.05);
const t = (x - x0) / (x1 - x0);
return clamp(Math.exp(y0 + t * (y1 - y0)) - 0.05, 0, 1);
};
const transformBase = (base) => {
const [h, s] = rgbToHsl(base);
const Lnew = mapLum(relLum(base));
// Near-neutral: re-issue on the Adom slate hue. Saturation eases off toward
// white/black extremes so near-black and near-white don't look dyed.
if (s < 0.22) {
const ease = clamp(1.6 * Math.min(Lnew, 1 - Lnew) + 0.55, 0, 1);
return atLuminance(NEUTRAL_HUE, NEUTRAL_SAT * ease, Lnew);
}
// 2026 product blue -> Adom teal, same luminance treatment, same saturation.
if (h >= 180 && h <= 245) return atLuminance(TEAL_HUE, s, Lnew);
// Semantic color (red/green/yellow/purple/orange): untouched.
return base;
};
const cache = new Map();
const transform = (value) => {
if (typeof value !== 'string' || !/^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(value)) return value;
const base = value.slice(0, 7).toLowerCase(), alpha = value.slice(7);
// Anchor equality is architecture: any surface 2026 made IDENTICAL to the
// ground/text anchors stays identical to ours (active tab == editor, etc.).
if (base === BG_OLD.toLowerCase()) return bgNew + alpha;
if (base === FG_OLD.toLowerCase()) return fgNew + alpha;
if (!cache.has(base)) cache.set(base, transformBase(base));
return cache.get(base) + alpha;
};
const colors = {};
for (const [k, v] of Object.entries(ref.colors)) colors[k] = transform(v);
// The anchors must land EXACTLY (float bisection could be one digit off).
colors['editor.background'] = bgNew;
colors['editor.foreground'] = fgNew;
// THE ONE-GROUND CONTRACT (John 2026-08-02, superseding the same-day tab
// contract): Claude chats paint sideBar.background in history-resumed mode and
// editor.background in fullEditor mode, so the ONLY way the active tab can flow
// into EVERY chat (and every file editor) is to unify the two grounds:
// sideBar takes the editor ground and text. The active tab keeps 2026's anchor
// equality (== editor ground == both chat modes == file bodies), and inactive
// tabs/strip keep their natural RAISED 2026 chrome tone, so the active tab
// reads as the sunken/merged one - exactly Dark 2026's architecture. The
// sidebar/editor boundary is carried by sideBar.border, not a fill change.
colors['sideBar.background'] = bgNew;
colors['sideBar.foreground'] = fgNew;
// Syntax lift (Bright only): raise lightness WITHIN the hue until the colour clears
// syntaxMinRatio against the ground. Hue and saturation are untouched -- semantics
// keep their colour, they just read better. 0 = verbatim (flagship + Dark).
const liftSyntax = (hex) => {
if (!syntaxMinRatio || !/^#[0-9a-fA-F]{6}$/.test(hex)) return hex;
const bgL = relLum(bgNew);
if ((relLum(hex) + 0.05) / (bgL + 0.05) >= syntaxMinRatio) return hex;
const [h, s] = rgbToHsl(hex);
// 2% overshoot so hex quantization can't land the result a hair UNDER the floor.
return atLuminance(h, s, syntaxMinRatio * 1.02 * (bgL + 0.05) - 0.05);
};
const tokenColors = !syntaxMinRatio ? ref.tokenColors : ref.tokenColors.map((tc) => {
const fg = tc.settings?.foreground;
if (typeof fg !== 'string') return tc;
return { ...tc, settings: { ...tc.settings, foreground: liftSyntax(fg) } };
});
const theme = {
name,
semanticHighlighting: ref.semanticHighlighting ?? true,
colors,
// Syntax stays 2026's verbatim (colour-as-meaning, not gray ambience), except
// the within-hue legibility lift when syntaxMinRatio asks for it.
tokenColors,
semanticTokenColors: ref.semanticTokenColors
};
writeFileSync(join(outDir, 'themes', file), JSON.stringify(theme, null, 2) + '\n');
// Sanity: the relationships John cares about survived.
const cr = (a, b) => { const [x, y] = [relLum(a.slice(0, 7)) + 0.05, relLum(b.slice(0, 7)) + 0.05].sort((p, q) => q - p); return x / y; };
const rel = (k1, k2) => `${k1}/${k2}: 2026=${cr(ref.colors[k1], ref.colors[k2]).toFixed(3)} new=${cr(colors[k1], colors[k2]).toFixed(3)}`;
console.log(`${name} <- Dark 2026, re-grounded onto ${bgNew}`);
console.log(' ' + rel('tab.inactiveBackground', 'editor.background'));
console.log(' ' + rel('editorWidget.background', 'editor.background'));
console.log(' ' + rel('input.background', 'editor.background'));
console.log(' one ground (activeTab==sideBar==editor):', colors['tab.activeBackground'] === colors['editor.background'] && colors['sideBar.background'] === colors['editor.background'], ' inactive raised + distinct:', colors['tab.inactiveBackground'] !== colors['tab.activeBackground']);
console.log(' chrome sample:', colors['tab.inactiveBackground'], ' overlay sample:', colors['editorWidget.background'], ' accent sample:', colors['tab.activeBorderTop']);
}
// -- emit both --
buildTheme({ bgNew: BG_NEW, name: 'Adom Studio', file: 'adom-studio-color-theme.json' });
// Adom Studio Dark: the SAME re-grounding, onto the deeper base ground (#0d1117).
// Was a plain template projection before; now brand-accurate like the flagship, just
// darker (John 2026-08-01: keep #0d1117, just make it brand-true).
buildTheme({ bgNew: '#0d1117', name: 'Adom Studio Dark', file: 'adom-studio-dark-color-theme.json' });
// 'Adom Studio Dark (Brighter Text)' (label renamed 2026-08-02, was 'Adom Studio Bright'; slug studio-bright is the
// frozen storage key and never changes): Studio Dark's ground with the text scale lifted (John 2026-08-01:
// "Adom Studio Dark as the basis ... just making the text brighter, even more contrast of
// the foreground text against the same background colors"). fg anchor #c9d1d9 -> #e6edf3
// lifts every neutral proportionally; syntax clears 7:1 AAA within-hue (2026-07-25 ruling).
buildTheme({ bgNew: '#0d1117', fgNew: '#e6edf3', syntaxMinRatio: 7.0, name: 'Adom Studio Dark (Brighter Text)', file: 'adom-studio-bright-color-theme.json' });