Add TypeScript theme generator for doki-master-theme palettes

Generates one Gitea-compatible CSS theme file per doki-master-theme
definition (88 themes). Each file contains a gitea-theme-meta-info
block and a :root block with the full set of CSS custom properties
derived from the character's colour palette.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 17:38:38 +00:00
commit f2baf869c9
12 changed files with 1388 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
interface RGB { r: number; g: number; b: number; }
interface HSL { h: number; s: number; l: number; }
function parseHex(hex: string): RGB {
const h = hex.replace('#', '').slice(0, 6);
return {
r: parseInt(h.slice(0, 2), 16),
g: parseInt(h.slice(2, 4), 16),
b: parseInt(h.slice(4, 6), 16),
};
}
function toHex(rgb: RGB): string {
const clamp = (n: number) => Math.max(0, Math.min(255, Math.round(n)));
const hex = (n: number) => clamp(n).toString(16).padStart(2, '0');
return `#${hex(rgb.r)}${hex(rgb.g)}${hex(rgb.b)}`.toUpperCase();
}
function rgbToHsl(rgb: RGB): HSL {
const r = rgb.r / 255;
const g = rgb.g / 255;
const b = rgb.b / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const l = (max + min) / 2;
if (max === min) return { h: 0, s: 0, l };
const d = max - min;
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
let h: number;
if (max === r) {
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
} else if (max === g) {
h = ((b - r) / d + 2) / 6;
} else {
h = ((r - g) / d + 4) / 6;
}
return { h, s, l };
}
function hue2rgb(p: number, q: number, t: number): number {
if (t < 0) t += 1;
if (t > 1) t -= 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;
}
function hslToRgb(hsl: HSL): RGB {
const { h, s, l } = hsl;
if (s === 0) {
const v = Math.round(l * 255);
return { r: v, g: v, b: v };
}
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
return {
r: Math.round(hue2rgb(p, q, h + 1 / 3) * 255),
g: Math.round(hue2rgb(p, q, h) * 255),
b: Math.round(hue2rgb(p, q, h - 1 / 3) * 255),
};
}
export function stripAlpha(hex: string): string {
return '#' + hex.replace('#', '').slice(0, 6);
}
export function normalizeHex(hex: string): string {
let h = hex.replace('#', '');
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
return '#' + h.slice(0, 6).toUpperCase();
}
export function scaleLightness(hex: string, delta: number): string {
const rgb = parseHex(stripAlpha(hex));
const hsl = rgbToHsl(rgb);
hsl.l = Math.max(0, Math.min(1, hsl.l + delta));
return toHex(hslToRgb(hsl));
}
export function withAlpha(hex: string, alpha: number): string {
const base = normalizeHex(hex).replace('#', '');
const a = Math.max(0, Math.min(255, Math.round(alpha)));
return '#' + base + a.toString(16).padStart(2, '0').toUpperCase();
}
export function mix(a: string, b: string, weight: number): string {
const ra = parseHex(stripAlpha(a));
const rb = parseHex(stripAlpha(b));
return toHex({
r: ra.r + (rb.r - ra.r) * weight,
g: ra.g + (rb.g - ra.g) * weight,
b: ra.b + (rb.b - ra.b) * weight,
});
}
export function desaturate(hex: string): string {
const rgb = parseHex(stripAlpha(hex));
const hsl = rgbToHsl(rgb);
hsl.s = 0;
return toHex(hslToRgb(hsl));
}
+26
View File
@@ -0,0 +1,26 @@
import type { ThemeConfig } from './types.js';
export function renderThemeCSS(theme: ThemeConfig, variables: Record<string, string>): string {
const isDark = theme.colorScheme === 'dark';
const lines: string[] = [];
lines.push('gitea-theme-meta-info {');
lines.push(` --theme-display-name: "${theme.displayName}";`);
lines.push(` --theme-color-scheme: "${theme.colorScheme}";`);
lines.push('}');
lines.push('');
lines.push(':root {');
for (const [name, value] of Object.entries(variables)) {
lines.push(` ${name}: ${value};`);
}
// Native CSS properties at the end (not custom properties)
lines.push(` accent-color: var(--color-accent);`);
lines.push(` color-scheme: ${isDark ? 'dark' : 'light'};`);
lines.push('}');
lines.push('');
return lines.join('\n');
}
+67
View File
@@ -0,0 +1,67 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import type { DokiDefinition, DokiColors, ColorTemplate, ThemeConfig } from './types.js';
function loadJson<T>(path: string): T {
return JSON.parse(readFileSync(path, 'utf8')) as T;
}
export function resolvePalette(definitionPath: string, dokiRepoRoot: string): DokiColors {
const templatesDir = join(dokiRepoRoot, 'templates');
const base = loadJson<ColorTemplate>(join(templatesDir, 'base.colors.template.json'));
const def = loadJson<DokiDefinition>(definitionPath);
const variantTemplate = loadJson<ColorTemplate>(
join(templatesDir, def.dark ? 'dark.colors.template.json' : 'light.colors.template.json')
);
const overrides = def.overrides?.editorScheme?.colors ?? {};
const resolved: DokiColors = {
...base.colors,
...variantTemplate.colors,
...def.colors,
...overrides,
} as DokiColors;
return resolved;
}
function sanitize(s: string): string {
return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
}
export function loadTheme(definitionPath: string, dokiRepoRoot: string): ThemeConfig {
const def = loadJson<DokiDefinition>(definitionPath);
const palette = resolvePalette(definitionPath, dokiRepoRoot);
// Normalise path separators so we can split reliably
const normPath = definitionPath.replace(/\\/g, '/');
const parts = normPath.split('/');
const filename = parts[parts.length - 1];
// Strip .master.definition.json then split on dots
const stem = filename.replace('.master.definition.json', '');
const nameParts = stem.split('.');
const hasVariant = nameParts.length > 1;
const variant = hasVariant ? nameParts[nameParts.length - 1] : null;
// Series directory is 4 levels up when a variant dir exists, 2 levels up otherwise
const series = hasVariant ? parts[parts.length - 4] : parts[parts.length - 2];
const internalName = ['doki', sanitize(series), sanitize(def.displayName), variant ? sanitize(variant) : null]
.filter(Boolean)
.join('-');
const displayName = variant
? `${def.group}: ${def.displayName} (${variant})`
: `${def.group}: ${def.displayName}`;
return {
internalName,
displayName,
colorScheme: def.dark ? 'dark' : 'light',
palette,
};
}
+431
View File
@@ -0,0 +1,431 @@
import { normalizeHex, scaleLightness, withAlpha, mix, desaturate } from './colorMath.js';
import type { ThemeConfig, DokiColors } from './types.js';
const FALLBACK = '#FF00FF'; // magenta — visually obvious if a key is missing
function get(p: DokiColors, key: string, fallback = FALLBACK): string {
const v = p[key];
if (!v) {
console.warn(` warning: missing palette key "${key}", using fallback`);
return fallback;
}
return normalizeHex(v);
}
// Primary scale deltas (dark theme: dark-N lighter, light-N darker; light theme: reversed)
const PRIMARY_DARK_DELTAS = [0.10, 0.17, 0.24, 0.30, 0.40, 0.52, 0.62];
const PRIMARY_LIGHT_DELTAS = [0.10, 0.17, 0.24, 0.30, 0.40, 0.52, 0.62];
// Secondary scale deltas
const SECONDARY_DARK_DELTAS = [0.04, 0.08, 0.14, 0.20, 0.26, 0.31, 0.36, 0.40, 0.43, 0.46, 0.50, 0.53, 0.55];
const SECONDARY_LIGHT_DELTAS = [0.04, 0.08, 0.14, 0.20];
const ALPHA_STEPS = [
[10, 0x19], [20, 0x33], [30, 0x4b], [40, 0x66], [50, 0x80],
[60, 0x99], [70, 0xb3], [80, 0xcc], [90, 0xe1],
] as const;
const DARK_NAMED_COLORS = {
red: '#CC4848', redLight: '#D15A5A', redDark1: '#C23636', redDark2: '#AD3030',
orange: '#CC580C', orangeLight: '#F6A066', orangeDark1: '#F38236', orangeDark2: '#F16E17',
yellow: '#CC9903', yellowLight: '#EAAF03', yellowDark1: '#B88A03', yellowDark2: '#A37A02',
olive: '#91A313', oliveLight: '#ABC016', oliveDark1: '#839311', oliveDark2: '#74820F',
green: '#87AB63', greenLight: '#93B373', greenDark1: '#7A9E55', greenDark2: '#6C8C4C',
teal: '#00918A', tealLight: '#00B6AD', tealDark1: '#00837C', tealDark2: '#00746E',
blue: '#3A8AC6', blueLight: '#4E96CC', blueDark1: '#347CB3', blueDark2: '#2E6E9F',
violet: '#906AE1', violetLight: '#9B79E4', violetDark1: '#7B4EDB', violetDark2: '#6733D6',
purple: '#B259D0', purpleLight: '#BA6AD5', purpleDark1: '#A742C9', purpleDark2: '#9834B9',
pink: '#D22E8B', pinkLight: '#D74397', pinkDark1: '#BE297D', pinkDark2: '#A9246F',
brown: '#A47252', brownLight: '#B08061', brownDark1: '#94674A', brownDark2: '#835B42',
black: '#202225', blackLight: '#45484E', blackDark1: '#2E3033', blackDark2: '#292B2E',
};
const LIGHT_NAMED_COLORS = {
red: '#DB2828', redLight: '#E45E5E', redDark1: '#C82121', redDark2: '#B11E1E',
orange: '#F2711C', orangeLight: '#F59555', orangeDark1: '#E6630D', orangeDark2: '#CC580C',
yellow: '#FBBD08', yellowLight: '#FCCE46', yellowDark1: '#E5AC04', yellowDark2: '#CC9903',
olive: '#B5CC18', oliveLight: '#D3E942', oliveDark1: '#A3B816', oliveDark2: '#91A313',
green: '#21BA45', greenLight: '#46DE6A', greenDark1: '#1EA73E', greenDark2: '#1A9537',
teal: '#00B5AD', tealLight: '#08FFF4', tealDark1: '#00A39C', tealDark2: '#00918A',
blue: '#2185D0', blueLight: '#51A5E3', blueDark1: '#1E78BB', blueDark2: '#1A6AA6',
violet: '#6435C9', violetLight: '#8B67D7', violetDark1: '#5A30B5', violetDark2: '#502AA1',
purple: '#A333C8', purpleLight: '#BB64D8', purpleDark1: '#932EB4', purpleDark2: '#8229A0',
pink: '#E03997', pinkLight: '#E86BB1', pinkDark1: '#DB228A', pinkDark2: '#C21E7B',
brown: '#A5673F', brownLight: '#C58B66', brownDark1: '#955D39', brownDark2: '#845232',
black: '#1D2328', blackLight: '#4B5B68', blackDark1: '#2C3339', blackDark2: '#131619',
};
const SERIES_16 = {
'--color-series-16-0': '#7DB233',
'--color-series-16-1': '#499A37',
'--color-series-16-2': '#CE4751',
'--color-series-16-3': '#8F9121',
'--color-series-16-4': '#AC32A6',
'--color-series-16-5': '#7445E9',
'--color-series-16-6': '#C67D28',
'--color-series-16-7': '#4DB392',
'--color-series-16-8': '#AA4D30',
'--color-series-16-9': '#2A6F84',
'--color-series-16-10': '#C45327',
'--color-series-16-11': '#3D965C',
'--color-series-16-12': '#792A93',
'--color-series-16-13': '#439D73',
'--color-series-16-14': '#103AAD',
'--color-series-16-15': '#982E85',
};
export function buildVariables(theme: ThemeConfig): Record<string, string> {
const p = theme.palette;
const isDark = theme.colorScheme === 'dark';
const primary = get(p, 'accentColor');
const secondary = get(p, 'borderColor', get(p, 'secondaryBackground'));
const body = get(p, 'baseBackground');
const fg = get(p, 'foregroundColor');
const headerColor = get(p, 'headerColor');
const contrastColor = get(p, 'contrastColor');
const inactiveBackground = get(p, 'inactiveBackground', get(p, 'secondaryBackground'));
const named = isDark ? DARK_NAMED_COLORS : LIGHT_NAMED_COLORS;
const sign = isDark ? 1 : -1;
const vars: Record<string, string> = {};
// ─── Section 1: is-dark-theme ────────────────────────────────────────────────
vars['--is-dark-theme'] = isDark ? 'true' : 'false';
// ─── Section 2: Primary color system ─────────────────────────────────────────
vars['--color-primary'] = primary;
vars['--color-primary-contrast'] = get(p, 'accentContrastColor', '#FFFFFF');
PRIMARY_DARK_DELTAS.forEach((d, i) => {
vars[`--color-primary-dark-${i + 1}`] = scaleLightness(primary, sign * d);
});
PRIMARY_LIGHT_DELTAS.forEach((d, i) => {
vars[`--color-primary-light-${i + 1}`] = scaleLightness(primary, -sign * d);
});
ALPHA_STEPS.forEach(([pct, alpha]) => {
vars[`--color-primary-alpha-${pct}`] = withAlpha(primary, alpha);
});
vars['--color-primary-hover'] = 'var(--color-primary-dark-1)';
vars['--color-primary-active'] = 'var(--color-primary-dark-2)';
// ─── Section 3: Secondary color system ───────────────────────────────────────
vars['--color-secondary'] = secondary;
SECONDARY_DARK_DELTAS.forEach((d, i) => {
vars[`--color-secondary-dark-${i + 1}`] = scaleLightness(secondary, sign * d);
});
SECONDARY_LIGHT_DELTAS.forEach((d, i) => {
vars[`--color-secondary-light-${i + 1}`] = scaleLightness(secondary, -sign * d);
});
ALPHA_STEPS.forEach(([pct, alpha]) => {
vars[`--color-secondary-alpha-${pct}`] = withAlpha(secondary, alpha);
});
vars['--color-secondary-button'] = 'var(--color-secondary-dark-4)';
vars['--color-secondary-hover'] = isDark ? 'var(--color-secondary-dark-3)' : 'var(--color-secondary-dark-5)';
vars['--color-secondary-active'] = isDark ? 'var(--color-secondary-dark-2)' : 'var(--color-secondary-dark-6)';
// ─── Section 4: Console colors ────────────────────────────────────────────────
vars['--color-console-fg'] = fg;
vars['--color-console-fg-subtle'] = get(p, 'lineNumberColor');
vars['--color-console-bg'] = contrastColor;
vars['--color-console-border'] = secondary;
vars['--color-console-hover-bg'] = scaleLightness(contrastColor, isDark ? 0.05 : -0.05);
vars['--color-console-active-bg'] = scaleLightness(contrastColor, isDark ? 0.10 : -0.10);
vars['--color-console-menu-bg'] = mix(contrastColor, body, 0.5);
vars['--color-console-menu-border'] = secondary;
vars['--color-console-link'] = get(p, 'lineNumberColor');
// ─── Section 5: Named semantic colors (12 hues) ───────────────────────────────
vars['--color-red'] = named.red;
vars['--color-red-light'] = named.redLight;
vars['--color-red-dark-1'] = named.redDark1;
vars['--color-red-dark-2'] = named.redDark2;
vars['--color-orange'] = named.orange;
vars['--color-orange-light'] = named.orangeLight;
vars['--color-orange-dark-1'] = named.orangeDark1;
vars['--color-orange-dark-2'] = named.orangeDark2;
vars['--color-yellow'] = named.yellow;
vars['--color-yellow-light'] = named.yellowLight;
vars['--color-yellow-dark-1'] = named.yellowDark1;
vars['--color-yellow-dark-2'] = named.yellowDark2;
vars['--color-olive'] = named.olive;
vars['--color-olive-light'] = named.oliveLight;
vars['--color-olive-dark-1'] = named.oliveDark1;
vars['--color-olive-dark-2'] = named.oliveDark2;
vars['--color-green'] = named.green;
vars['--color-green-light'] = named.greenLight;
vars['--color-green-dark-1'] = named.greenDark1;
vars['--color-green-dark-2'] = named.greenDark2;
vars['--color-teal'] = named.teal;
vars['--color-teal-light'] = named.tealLight;
vars['--color-teal-dark-1'] = named.tealDark1;
vars['--color-teal-dark-2'] = named.tealDark2;
vars['--color-blue'] = named.blue;
vars['--color-blue-light'] = named.blueLight;
vars['--color-blue-dark-1'] = named.blueDark1;
vars['--color-blue-dark-2'] = named.blueDark2;
vars['--color-violet'] = named.violet;
vars['--color-violet-light'] = named.violetLight;
vars['--color-violet-dark-1'] = named.violetDark1;
vars['--color-violet-dark-2'] = named.violetDark2;
vars['--color-purple'] = named.purple;
vars['--color-purple-light'] = named.purpleLight;
vars['--color-purple-dark-1'] = named.purpleDark1;
vars['--color-purple-dark-2'] = named.purpleDark2;
vars['--color-pink'] = named.pink;
vars['--color-pink-light'] = named.pinkLight;
vars['--color-pink-dark-1'] = named.pinkDark1;
vars['--color-pink-dark-2'] = named.pinkDark2;
vars['--color-brown'] = named.brown;
vars['--color-brown-light'] = named.brownLight;
vars['--color-brown-dark-1'] = named.brownDark1;
vars['--color-brown-dark-2'] = named.brownDark2;
vars['--color-black'] = named.black;
vars['--color-black-light'] = named.blackLight;
vars['--color-black-dark-1'] = named.blackDark1;
vars['--color-black-dark-2'] = named.blackDark2;
// ─── Section 6: ANSI colors ───────────────────────────────────────────────────
const ansiBlack = contrastColor;
const ansiRed = get(p, 'terminal.ansiRed', named.red);
const ansiGreen = get(p, 'terminal.ansiGreen', named.green);
const ansiYellow = get(p, 'terminal.ansiYellow', named.yellow);
const ansiBlue = get(p, 'terminal.ansiBlue', named.blue);
const ansiMagenta = get(p, 'terminal.ansiMagenta', get(p, 'accentColor'));
const ansiCyan = get(p, 'terminal.ansiCyan', named.teal);
vars['--color-ansi-black'] = ansiBlack;
vars['--color-ansi-red'] = ansiRed;
vars['--color-ansi-green'] = ansiGreen;
vars['--color-ansi-yellow'] = ansiYellow;
vars['--color-ansi-blue'] = ansiBlue;
vars['--color-ansi-magenta'] = ansiMagenta;
vars['--color-ansi-cyan'] = ansiCyan;
vars['--color-ansi-white'] = 'var(--color-console-fg-subtle)';
vars['--color-ansi-bright-black'] = scaleLightness(ansiBlack, 0.12);
vars['--color-ansi-bright-red'] = scaleLightness(ansiRed, 0.08);
vars['--color-ansi-bright-green'] = scaleLightness(ansiGreen, 0.08);
vars['--color-ansi-bright-yellow'] = scaleLightness(ansiYellow, 0.08);
vars['--color-ansi-bright-blue'] = scaleLightness(ansiBlue, 0.08);
vars['--color-ansi-bright-magenta'] = scaleLightness(ansiMagenta, 0.08);
vars['--color-ansi-bright-cyan'] = scaleLightness(ansiCyan, 0.08);
vars['--color-ansi-bright-white'] = 'var(--color-console-fg)';
// ─── Section 7: Chart/series colors (fixed) ───────────────────────────────────
Object.assign(vars, SERIES_16);
// ─── Section 8: Utility colors ────────────────────────────────────────────────
const grey = desaturate(secondary);
vars['--color-grey'] = grey;
vars['--color-grey-light'] = scaleLightness(grey, 0.08);
vars['--color-gold'] = isDark ? '#B1983B' : '#A1882B';
vars['--color-white'] = '#FFFFFF';
// ─── Section 9: Diff colors ───────────────────────────────────────────────────
const diffInserted = get(p, 'diff.inserted', '#1B3B1C');
const diffDeleted = get(p, 'diff.deleted', '#565656');
const diffModified = get(p, 'diff.modified', '#203952');
// Mix diff color at ~80% alpha over body background
const diffAddedRowBg = mix(body, diffInserted, 0.8);
const diffRemovedRowBg = mix(body, diffDeleted, 0.8);
const diffMovedRowBg = mix(body, diffModified, 0.8);
vars['--color-diff-added-fg'] = ansiGreen;
vars['--color-diff-added-linenum-bg'] = scaleLightness(diffAddedRowBg, -0.05);
vars['--color-diff-added-row-bg'] = diffAddedRowBg;
vars['--color-diff-added-row-border'] = scaleLightness(diffAddedRowBg, -0.10);
vars['--color-diff-added-word-bg'] = scaleLightness(diffAddedRowBg, -0.15);
vars['--color-diff-removed-fg'] = named.red;
vars['--color-diff-removed-linenum-bg'] = scaleLightness(diffRemovedRowBg, -0.05);
vars['--color-diff-removed-row-bg'] = diffRemovedRowBg;
vars['--color-diff-removed-row-border'] = scaleLightness(diffRemovedRowBg, -0.10);
vars['--color-diff-removed-word-bg'] = scaleLightness(diffRemovedRowBg, -0.15);
vars['--color-diff-moved-row-bg'] = diffMovedRowBg;
vars['--color-diff-moved-row-border'] = scaleLightness(diffMovedRowBg, 0.15);
vars['--color-diff-inactive'] = scaleLightness(body, isDark ? -0.05 : 0.05);
// ─── Section 10: Status/alert colors ─────────────────────────────────────────
const errorText = get(p, 'errorColor', get(p, 'stopColor', '#FF5555'));
vars['--color-error-border'] = withAlpha(errorText, 0x66);
vars['--color-error-bg'] = withAlpha(errorText, 0x1a);
vars['--color-error-bg-active'] = withAlpha(errorText, 0x33);
vars['--color-error-bg-hover'] = withAlpha(errorText, 0x26);
vars['--color-error-text'] = errorText;
const successText = ansiGreen;
vars['--color-success-border'] = withAlpha(successText, 0x66);
vars['--color-success-bg'] = withAlpha(successText, 0x1a);
vars['--color-success-text'] = successText;
const warningText = ansiYellow;
vars['--color-warning-border'] = withAlpha(warningText, 0x66);
vars['--color-warning-bg'] = withAlpha(warningText, 0x1a);
vars['--color-warning-text'] = warningText;
const infoText = ansiBlue;
vars['--color-info-border'] = withAlpha(infoText, 0x66);
vars['--color-info-bg'] = withAlpha(infoText, 0x1a);
vars['--color-info-text'] = infoText;
const priorityText = named.violet;
vars['--color-priority-border'] = withAlpha(priorityText, 0x66);
vars['--color-priority-bg'] = withAlpha(priorityText, 0x1a);
vars['--color-priority-text'] = priorityText;
// ─── Section 11: Badge colors ─────────────────────────────────────────────────
vars['--color-red-badge'] = named.red;
vars['--color-red-badge-bg'] = withAlpha(named.red, 0x1a);
vars['--color-red-badge-hover-bg'] = withAlpha(named.red, 0x4d);
vars['--color-green-badge'] = named.green;
vars['--color-green-badge-bg'] = withAlpha(named.green, 0x1a);
vars['--color-green-badge-hover-bg'] = withAlpha(named.green, 0x4d);
vars['--color-yellow-badge'] = named.yellow;
vars['--color-yellow-badge-bg'] = withAlpha(named.yellow, 0x1a);
vars['--color-yellow-badge-hover-bg'] = withAlpha(named.yellow, 0x4d);
vars['--color-orange-badge'] = named.orange;
vars['--color-orange-badge-bg'] = withAlpha(named.orange, 0x1a);
vars['--color-orange-badge-hover-bg'] = withAlpha(named.orange, 0x4d);
// ─── Section 12: Brand colors (fixed) ─────────────────────────────────────────
vars['--color-git'] = '#F05133';
vars['--color-logo'] = '#609926';
// ─── Section 13: Semantic / target-based colors ───────────────────────────────
const boxBody = contrastColor;
const boxBodyHighlight = isDark
? scaleLightness(boxBody, 0.04)
: withAlpha(primary, 0x10);
vars['--color-body'] = body;
vars['--color-box-header'] = isDark ? contrastColor : headerColor;
vars['--color-box-body'] = contrastColor;
vars['--color-box-body-highlight'] = boxBodyHighlight;
vars['--color-text-dark'] = isDark ? get(p, 'selectionForeground') : fg;
vars['--color-text'] = fg;
vars['--color-text-light'] = scaleLightness(fg, isDark ? -0.08 : 0.08);
vars['--color-text-light-1'] = get(p, 'buttonFont');
vars['--color-text-light-2'] = get(p, 'lineNumberColor');
vars['--color-text-light-3'] = get(p, 'disabledColor');
vars['--color-footer'] = 'var(--color-nav-bg)';
vars['--color-timeline'] = secondary;
vars['--color-input-text'] = 'var(--color-text-dark)';
vars['--color-input-background'] = get(p, 'textEditorBackground');
vars['--color-input-toggle-background'] = secondary;
vars['--color-input-border'] = 'var(--color-secondary-dark-1)';
// Subtle overlay tint — fixed neutral values that work on any background
vars['--color-light'] = isDark ? '#F3F3F406' : '#00001706';
vars['--color-light-border'] = isDark ? '#F3F3F428' : '#0000171D';
vars['--color-hover'] = isDark ? withAlpha(fg, 0x19) : '#00001708';
vars['--color-hover-opaque'] = scaleLightness(body, isDark ? 0.06 : -0.05);
vars['--color-active'] = isDark ? withAlpha(fg, 0x24) : '#00001714';
vars['--color-menu'] = inactiveBackground;
vars['--color-card'] = inactiveBackground;
vars['--color-markup-table-row'] = isDark ? withAlpha(fg, 0x0f) : withAlpha(primary, 0x0a);
vars['--color-markup-code-block'] = isDark ? withAlpha(fg, 0x12) : withAlpha(primary, 0x10);
vars['--color-markup-code-inline'] = isDark ? withAlpha(fg, 0x28) : withAlpha(primary, 0x12);
vars['--color-button'] = get(p, 'buttonColor', contrastColor);
vars['--color-code-bg'] = get(p, 'codeBlock');
vars['--color-shadow'] = isDark ? withAlpha(body, 0x58) : '#00001726';
vars['--color-shadow-opaque'] = isDark ? contrastColor : scaleLightness(body, -0.12);
vars['--color-secondary-bg'] = get(p, 'secondaryBackground');
vars['--color-expand-button'] = get(p, 'highlightColor');
vars['--color-placeholder-text'] = 'var(--color-text-light-3)';
vars['--color-editor-line-highlight'] = isDark ? 'var(--color-secondary-alpha-40)' : 'var(--color-secondary-alpha-30)';
vars['--color-editor-selection'] = isDark ? 'var(--color-primary-alpha-50)' : 'var(--color-primary-alpha-30)';
vars['--color-project-column-bg'] = isDark ? 'var(--color-secondary-light-2)' : 'var(--color-secondary-light-4)';
vars['--color-caret'] = isDark ? 'var(--color-text)' : 'var(--color-text-dark)';
vars['--color-reaction-bg'] = isDark ? withAlpha(fg, 0x12) : '#0000170A';
vars['--color-reaction-hover-bg'] = isDark ? 'var(--color-primary-light-4)' : 'var(--color-primary-light-5)';
vars['--color-reaction-active-bg'] = isDark ? 'var(--color-primary-light-5)' : 'var(--color-primary-light-6)';
vars['--color-tooltip-text'] = isDark ? '#FAFAFA' : '#FBFDFF';
vars['--color-tooltip-bg'] = isDark ? '#0B0B0CF0' : '#000017F0';
vars['--color-nav-bg'] = headerColor;
vars['--color-nav-hover-bg'] = 'var(--color-secondary-light-1)';
vars['--color-nav-text'] = 'var(--color-text)';
vars['--color-secondary-nav-bg'] = scaleLightness(headerColor, isDark ? 0.02 : -0.02);
vars['--color-label-text'] = 'var(--color-text)';
vars['--color-label-bg'] = withAlpha(secondary, 0x4b);
vars['--color-label-hover-bg'] = withAlpha(secondary, 0xa0);
vars['--color-label-active-bg'] = withAlpha(secondary, 0xff);
vars['--color-accent'] = 'var(--color-primary-light-1)';
vars['--color-small-accent'] = isDark ? 'var(--color-primary-light-5)' : 'var(--color-primary-light-6)';
vars['--color-highlight-fg'] = get(p, 'searchForeground', get(p, 'selectionForeground'));
vars['--color-highlight-bg'] = get(p, 'searchBackground', withAlpha(primary, 0x66));
vars['--color-overlay-backdrop'] = '#080808C0';
vars['--color-danger'] = 'var(--color-red)';
vars['--color-transparency-grid-light'] = isDark ? '#2A2A2A' : '#FAFAFA';
vars['--color-transparency-grid-dark'] = isDark ? '#1A1A1A' : '#E2E2E2';
vars['--color-workflow-edge-hover'] = scaleLightness(secondary, isDark ? 0.2 : -0.2);
// ─── Section 14: Syntax highlighting ──────────────────────────────────────────
const syntaxComment = get(p, 'comments');
const diffRemovedSolid = normalizeHex(diffRemovedRowBg);
const diffAddedSolid = normalizeHex(diffAddedRowBg);
vars['--color-syntax-keyword'] = get(p, 'keywordColor');
vars['--color-syntax-bool'] = ansiCyan;
vars['--color-syntax-control'] = ansiYellow;
vars['--color-syntax-name'] = ansiYellow;
vars['--color-syntax-type'] = get(p, 'classNameColor');
vars['--color-syntax-number'] = get(p, 'constantColor', get(p, 'keyColor'));
vars['--color-syntax-operator'] = get(p, 'keywordColor');
vars['--color-syntax-regexp'] = ansiMagenta;
vars['--color-syntax-string'] = get(p, 'stringColor');
vars['--color-syntax-comment'] = syntaxComment;
vars['--color-syntax-invalid'] = errorText;
vars['--color-syntax-link'] = 'var(--color-primary)';
vars['--color-syntax-tag'] = get(p, 'htmlTagColor', get(p, 'keywordColor'));
vars['--color-syntax-attribute'] = ansiMagenta;
vars['--color-syntax-property'] = get(p, 'keyColor');
vars['--color-syntax-variable'] = ansiYellow;
vars['--color-syntax-string-special'] = ansiYellow;
vars['--color-syntax-escape'] = ansiYellow;
vars['--color-syntax-entity'] = ansiMagenta;
vars['--color-syntax-preproc'] = ansiGreen;
vars['--color-syntax-preproc-file'] = get(p, 'constantColor', get(p, 'keyColor'));
vars['--color-syntax-decorator'] = ansiGreen;
vars['--color-syntax-namespace'] = fg;
vars['--color-syntax-name-pseudo'] = ansiMagenta;
vars['--color-syntax-comment-special'] = scaleLightness(syntaxComment, 0.08);
vars['--color-syntax-text'] = isDark ? fg : 'inherit';
vars['--color-syntax-text-alt'] = scaleLightness(fg, -0.10);
vars['--color-syntax-punctuation'] = isDark ? fg : 'inherit';
vars['--color-syntax-whitespace'] = get(p, 'disabledColor');
vars['--color-syntax-diff-fg'] = isDark ? '#FFFFFF' : '#000000';
vars['--color-syntax-deleted-bg'] = diffRemovedSolid;
vars['--color-syntax-inserted-bg'] = diffAddedSolid;
vars['--color-syntax-emph'] = ansiYellow;
vars['--color-syntax-strong'] = ansiYellow;
vars['--color-syntax-heading'] = ansiYellow;
vars['--color-syntax-subheading'] = get(p, 'stringColor');
vars['--color-syntax-output'] = syntaxComment;
vars['--color-syntax-prompt'] = ansiYellow;
vars['--color-syntax-traceback'] = errorText;
vars['--color-syntax-matching-bracket-bg'] = withAlpha(named.teal, 0x48);
vars['--color-syntax-nonmatching-bracket-bg'] = withAlpha(named.red, 0x48);
return vars;
}
+107
View File
@@ -0,0 +1,107 @@
export interface DokiDefinition {
id: string;
name: string;
displayName: string;
dark: boolean;
author: string;
group: string;
stickers?: unknown;
overrides?: {
editorScheme?: {
colors?: Partial<DokiColors>;
};
};
colors: DokiColors;
characterId: string;
}
export interface DokiColors {
// Backgrounds
baseBackground: string;
textEditorBackground: string;
secondaryBackground: string;
headerColor: string;
contrastColor: string;
codeBlock: string;
foldedTextBackground?: string;
inactiveBackground?: string;
inactiveBackgroundDarker?: string;
lightEditorColor?: string;
nonProjectFileScopeColor?: string;
testScopeColor?: string;
breakpointColor?: string;
breakpointActiveColor?: string;
highlightColor: string;
caretRow: string;
// Foreground / text
foregroundColor: string;
buttonFont: string;
selectionForeground: string;
lineNumberColor: string;
disabledColor: string;
unusedColor?: string;
infoForeground?: string;
// Selection / states
selectionBackground: string;
selectionBackgroundTransparent?: string;
selectionInactive?: string;
identifierHighlight?: string;
searchBackground?: string;
searchForeground?: string;
popupMask?: string;
// Accent / brand
accentColor: string;
accentColorTransparent?: string;
accentColorLessTransparent?: string;
accentColorMoreTransparent?: string;
accentContrastColor?: string;
accentColorOverride?: string;
startColor?: string;
stopColor?: string;
borderColor: string;
buttonColor?: string;
// Syntax
comments: string;
stringColor: string;
keywordColor: string;
classNameColor: string;
htmlTagColor?: string;
keyColor: string;
constantColor?: string;
errorColor?: string;
// Diff
"diff.inserted"?: string;
"diff.deleted"?: string;
"diff.modified"?: string;
"diff.conflict"?: string;
// Terminal / ANSI
"terminal.ansiRed"?: string;
"terminal.ansiGreen"?: string;
"terminal.ansiYellow"?: string;
"terminal.ansiBlue"?: string;
"terminal.ansiMagenta"?: string;
"terminal.ansiCyan"?: string;
// Index signature for icon colors and other optional keys
[key: string]: string | undefined;
}
export interface ColorTemplate {
type: "COLOR";
name: string;
extends?: string;
colors: Partial<DokiColors>;
}
export interface ThemeConfig {
internalName: string;
displayName: string;
colorScheme: "dark" | "light";
palette: DokiColors;
}