681 lines
29 KiB
TypeScript
681 lines
29 KiB
TypeScript
/*
|
|
* Copyright (C) 2026 Fluxer Contributors
|
|
*
|
|
* This file is part of Fluxer.
|
|
*
|
|
* Fluxer is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Fluxer is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import {mkdirSync, writeFileSync} from 'node:fs';
|
|
import {dirname, join, relative} from 'node:path';
|
|
|
|
interface ColorFamily {
|
|
hue: number;
|
|
saturation: number;
|
|
useSaturationFactor: boolean;
|
|
}
|
|
|
|
interface ScaleStop {
|
|
name: string;
|
|
position?: number;
|
|
}
|
|
|
|
interface Scale {
|
|
family: string;
|
|
range: [number, number];
|
|
curve: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';
|
|
stops: Array<ScaleStop>;
|
|
}
|
|
|
|
interface TokenDef {
|
|
name?: string;
|
|
scale?: string;
|
|
value?: string;
|
|
family?: string;
|
|
hue?: number;
|
|
saturation?: number;
|
|
lightness?: number;
|
|
alpha?: number;
|
|
useSaturationFactor?: boolean;
|
|
}
|
|
|
|
interface Config {
|
|
families: Record<string, ColorFamily>;
|
|
scales: Record<string, Scale>;
|
|
tokens: {
|
|
root: Array<TokenDef>;
|
|
light: Array<TokenDef>;
|
|
coal: Array<TokenDef>;
|
|
};
|
|
}
|
|
|
|
const CONFIG: Config = {
|
|
families: {
|
|
neutralDark: {hue: 220, saturation: 13, useSaturationFactor: true},
|
|
neutralLight: {hue: 220, saturation: 10, useSaturationFactor: true},
|
|
brand: {hue: 242, saturation: 70, useSaturationFactor: true},
|
|
link: {hue: 210, saturation: 100, useSaturationFactor: true},
|
|
accentPurple: {hue: 270, saturation: 80, useSaturationFactor: true},
|
|
statusOnline: {hue: 142, saturation: 76, useSaturationFactor: true},
|
|
statusIdle: {hue: 45, saturation: 93, useSaturationFactor: true},
|
|
statusDnd: {hue: 0, saturation: 84, useSaturationFactor: true},
|
|
statusOffline: {hue: 218, saturation: 11, useSaturationFactor: true},
|
|
statusDanger: {hue: 1, saturation: 77, useSaturationFactor: true},
|
|
textCode: {hue: 340, saturation: 50, useSaturationFactor: true},
|
|
brandIcon: {hue: 38, saturation: 92, useSaturationFactor: true},
|
|
},
|
|
|
|
scales: {
|
|
darkSurface: {
|
|
family: 'neutralDark',
|
|
range: [5, 26],
|
|
curve: 'easeOut',
|
|
stops: [
|
|
{name: '--background-primary', position: 0},
|
|
{name: '--background-secondary', position: 0.16},
|
|
{name: '--background-secondary-lighter', position: 0.22},
|
|
{name: '--background-secondary-alt', position: 0.28},
|
|
{name: '--background-tertiary', position: 0.4},
|
|
{name: '--background-channel-header', position: 0.34},
|
|
{name: '--guild-list-foreground', position: 0.38},
|
|
{name: '--background-header-secondary', position: 0.5},
|
|
{name: '--background-header-primary', position: 0.5},
|
|
{name: '--background-textarea', position: 0.68},
|
|
{name: '--background-header-primary-hover', position: 0.85},
|
|
],
|
|
},
|
|
coalSurface: {
|
|
family: 'neutralDark',
|
|
range: [1, 12],
|
|
curve: 'easeOut',
|
|
stops: [
|
|
{name: '--background-primary', position: 0},
|
|
{name: '--background-secondary', position: 0.16},
|
|
{name: '--background-secondary-alt', position: 0.28},
|
|
{name: '--background-tertiary', position: 0.4},
|
|
{name: '--background-channel-header', position: 0.34},
|
|
{name: '--guild-list-foreground', position: 0.38},
|
|
{name: '--background-header-secondary', position: 0.5},
|
|
{name: '--background-header-primary', position: 0.5},
|
|
{name: '--background-textarea', position: 0.68},
|
|
{name: '--background-header-primary-hover', position: 0.85},
|
|
],
|
|
},
|
|
darkText: {
|
|
family: 'neutralDark',
|
|
range: [52, 96],
|
|
curve: 'easeInOut',
|
|
stops: [
|
|
{name: '--text-tertiary-secondary', position: 0},
|
|
{name: '--text-tertiary-muted', position: 0.2},
|
|
{name: '--text-tertiary', position: 0.38},
|
|
{name: '--text-primary-muted', position: 0.55},
|
|
{name: '--text-chat-muted', position: 0.55},
|
|
{name: '--text-secondary', position: 0.72},
|
|
{name: '--text-chat', position: 0.82},
|
|
{name: '--text-primary', position: 1},
|
|
],
|
|
},
|
|
lightSurface: {
|
|
family: 'neutralLight',
|
|
range: [86, 98.5],
|
|
curve: 'easeIn',
|
|
stops: [
|
|
{name: '--background-header-primary-hover', position: 0},
|
|
{name: '--background-header-primary', position: 0.12},
|
|
{name: '--background-header-secondary', position: 0.2},
|
|
{name: '--guild-list-foreground', position: 0.35},
|
|
{name: '--background-tertiary', position: 0.42},
|
|
{name: '--background-channel-header', position: 0.5},
|
|
{name: '--background-secondary-alt', position: 0.63},
|
|
{name: '--background-secondary', position: 0.74},
|
|
{name: '--background-secondary-lighter', position: 0.83},
|
|
{name: '--background-textarea', position: 0.88},
|
|
{name: '--background-primary', position: 1},
|
|
],
|
|
},
|
|
lightText: {
|
|
family: 'neutralLight',
|
|
range: [15, 60],
|
|
curve: 'easeOut',
|
|
stops: [
|
|
{name: '--text-primary', position: 0},
|
|
{name: '--text-chat', position: 0.08},
|
|
{name: '--text-secondary', position: 0.28},
|
|
{name: '--text-chat-muted', position: 0.45},
|
|
{name: '--text-primary-muted', position: 0.45},
|
|
{name: '--text-tertiary', position: 0.6},
|
|
{name: '--text-tertiary-secondary', position: 0.75},
|
|
{name: '--text-tertiary-muted', position: 0.85},
|
|
],
|
|
},
|
|
},
|
|
|
|
tokens: {
|
|
root: [
|
|
{scale: 'darkSurface'},
|
|
{scale: 'darkText'},
|
|
{
|
|
name: '--panel-control-bg',
|
|
value: `color-mix(
|
|
in srgb,
|
|
var(--background-secondary-alt) 80%,
|
|
hsl(220, calc(13% * var(--saturation-factor)), 2%) 20%
|
|
)`,
|
|
},
|
|
{name: '--panel-control-border', family: 'neutralDark', saturation: 30, lightness: 65, alpha: 0.45},
|
|
{name: '--panel-control-divider', family: 'neutralDark', saturation: 30, lightness: 55, alpha: 0.35},
|
|
{name: '--panel-control-highlight', value: 'hsla(0, 0%, 100%, 0.04)'},
|
|
{name: '--background-modifier-hover', family: 'neutralDark', lightness: 100, alpha: 0.05},
|
|
{name: '--background-modifier-selected', family: 'neutralDark', lightness: 100, alpha: 0.1},
|
|
{name: '--background-modifier-accent', family: 'neutralDark', saturation: 13, lightness: 80, alpha: 0.15},
|
|
{name: '--background-modifier-accent-focus', family: 'neutralDark', saturation: 13, lightness: 80, alpha: 0.22},
|
|
{name: '--control-button-normal-bg', value: 'transparent'},
|
|
{name: '--control-button-normal-text', value: 'var(--text-primary-muted)'},
|
|
{name: '--control-button-hover-bg', family: 'neutralDark', lightness: 22},
|
|
{name: '--control-button-hover-text', value: 'var(--text-primary)'},
|
|
{name: '--control-button-active-bg', family: 'neutralDark', lightness: 24},
|
|
{name: '--control-button-active-text', value: 'var(--text-primary)'},
|
|
{name: '--control-button-danger-text', hue: 1, saturation: 77, useSaturationFactor: true, lightness: 60},
|
|
{name: '--control-button-danger-hover-bg', hue: 1, saturation: 77, useSaturationFactor: true, lightness: 20},
|
|
{name: '--brand-primary', family: 'brand', lightness: 55},
|
|
{name: '--brand-secondary', family: 'brand', saturation: 60, lightness: 49},
|
|
{name: '--brand-primary-light', family: 'brand', saturation: 100, lightness: 84},
|
|
{name: '--brand-primary-fill', hue: 0, saturation: 0, lightness: 100},
|
|
{name: '--status-online', family: 'statusOnline', lightness: 40},
|
|
{name: '--status-idle', family: 'statusIdle', lightness: 50},
|
|
{name: '--status-dnd', family: 'statusDnd', lightness: 60},
|
|
{name: '--status-offline', family: 'statusOffline', lightness: 65},
|
|
{name: '--status-danger', family: 'statusDanger', lightness: 55},
|
|
{name: '--status-warning', value: 'var(--status-idle)'},
|
|
{name: '--text-warning', family: 'statusIdle', lightness: 55},
|
|
{name: '--plutonium', value: 'var(--brand-primary)'},
|
|
{name: '--plutonium-hover', value: 'var(--brand-secondary)'},
|
|
{name: '--plutonium-text', value: 'var(--text-on-brand-primary)'},
|
|
{name: '--plutonium-icon', family: 'brandIcon', lightness: 50},
|
|
{name: '--invite-verified-icon-color', value: 'var(--text-on-brand-primary)'},
|
|
{name: '--text-link', family: 'link', lightness: 70},
|
|
{name: '--text-on-brand-primary', hue: 0, saturation: 0, lightness: 98},
|
|
{name: '--text-code', family: 'textCode', lightness: 90},
|
|
{name: '--text-selection', hue: 210, saturation: 90, useSaturationFactor: true, lightness: 70, alpha: 0.35},
|
|
{name: '--markup-mention-text', value: 'var(--text-link)'},
|
|
{name: '--markup-mention-fill', value: 'color-mix(in srgb, var(--text-link) 20%, transparent)'},
|
|
{name: '--markup-mention-border', family: 'link', lightness: 70, alpha: 0.3},
|
|
{name: '--markup-jump-link-text', value: 'var(--text-link)'},
|
|
{name: '--markup-jump-link-fill', value: 'color-mix(in srgb, var(--text-link) 12%, transparent)'},
|
|
{name: '--markup-jump-link-hover-fill', value: 'color-mix(in srgb, var(--text-link) 20%, transparent)'},
|
|
{name: '--markup-everyone-text', hue: 250, saturation: 80, useSaturationFactor: true, lightness: 75},
|
|
{
|
|
name: '--markup-everyone-fill',
|
|
value: 'color-mix(in srgb, hsl(250, calc(80% * var(--saturation-factor)), 75%) 18%, transparent)',
|
|
},
|
|
{
|
|
name: '--markup-everyone-border',
|
|
hue: 250,
|
|
saturation: 80,
|
|
useSaturationFactor: true,
|
|
lightness: 75,
|
|
alpha: 0.3,
|
|
},
|
|
{name: '--markup-here-text', hue: 45, saturation: 90, useSaturationFactor: true, lightness: 70},
|
|
{
|
|
name: '--markup-here-fill',
|
|
value: 'color-mix(in srgb, hsl(45, calc(90% * var(--saturation-factor)), 70%) 18%, transparent)',
|
|
},
|
|
{name: '--markup-here-border', hue: 45, saturation: 90, useSaturationFactor: true, lightness: 70, alpha: 0.3},
|
|
{name: '--markup-interactive-hover-text', value: 'var(--text-link)'},
|
|
{name: '--markup-interactive-hover-fill', value: 'color-mix(in srgb, var(--text-link) 30%, transparent)'},
|
|
{
|
|
name: '--interactive-muted',
|
|
value: `color-mix(
|
|
in oklab,
|
|
hsl(228, calc(10% * var(--saturation-factor)), 35%) 100%,
|
|
hsl(245, calc(100% * var(--saturation-factor)), 80%) 40%
|
|
)`,
|
|
},
|
|
{
|
|
name: '--interactive-active',
|
|
value: `color-mix(
|
|
in oklab,
|
|
hsl(0, calc(0% * var(--saturation-factor)), 100%) 100%,
|
|
hsl(245, calc(100% * var(--saturation-factor)), 80%) 40%
|
|
)`,
|
|
},
|
|
{name: '--button-primary-fill', hue: 139, saturation: 55, useSaturationFactor: true, lightness: 44},
|
|
{name: '--button-primary-active-fill', hue: 136, saturation: 60, useSaturationFactor: true, lightness: 38},
|
|
{name: '--button-primary-text', hue: 0, saturation: 0, lightness: 100},
|
|
{name: '--button-secondary-fill', hue: 0, saturation: 0, lightness: 100, alpha: 0.1, useSaturationFactor: false},
|
|
{
|
|
name: '--button-secondary-active-fill',
|
|
hue: 0,
|
|
saturation: 0,
|
|
lightness: 100,
|
|
alpha: 0.15,
|
|
useSaturationFactor: false,
|
|
},
|
|
{name: '--button-secondary-text', hue: 0, saturation: 0, lightness: 100},
|
|
{name: '--button-secondary-active-text', value: 'var(--button-secondary-text)'},
|
|
{name: '--button-danger-fill', hue: 359, saturation: 70, useSaturationFactor: true, lightness: 54},
|
|
{name: '--button-danger-active-fill', hue: 359, saturation: 65, useSaturationFactor: true, lightness: 45},
|
|
{name: '--button-danger-text', hue: 0, saturation: 0, lightness: 100},
|
|
{name: '--button-danger-outline-border', value: '1px solid hsl(359, calc(70% * var(--saturation-factor)), 54%)'},
|
|
{name: '--button-danger-outline-text', hue: 0, saturation: 0, lightness: 100},
|
|
{name: '--button-danger-outline-active-fill', hue: 359, saturation: 65, useSaturationFactor: true, lightness: 48},
|
|
{name: '--button-danger-outline-active-border', value: 'transparent'},
|
|
{name: '--button-ghost-text', hue: 0, saturation: 0, lightness: 100},
|
|
{name: '--button-inverted-fill', hue: 0, saturation: 0, lightness: 100},
|
|
{name: '--button-inverted-text', hue: 0, saturation: 0, lightness: 0},
|
|
{name: '--button-outline-border', value: '1px solid hsla(0, 0%, 100%, 0.3)'},
|
|
{name: '--button-outline-text', hue: 0, saturation: 0, lightness: 100},
|
|
{name: '--button-outline-active-fill', value: 'hsla(0, 0%, 100%, 0.15)'},
|
|
{name: '--button-outline-active-border', value: '1px solid hsla(0, 0%, 100%, 0.4)'},
|
|
{name: '--theme-border', value: 'transparent'},
|
|
{name: '--theme-border-width', value: '0px'},
|
|
{name: '--bg-primary', value: 'var(--background-primary)'},
|
|
{name: '--bg-secondary', value: 'var(--background-secondary)'},
|
|
{name: '--bg-tertiary', value: 'var(--background-tertiary)'},
|
|
{name: '--bg-hover', value: 'var(--background-modifier-hover)'},
|
|
{name: '--bg-active', value: 'var(--background-modifier-selected)'},
|
|
{name: '--bg-code', family: 'neutralDark', lightness: 15, alpha: 0.8},
|
|
{name: '--bg-code-block', value: 'var(--background-secondary-alt)'},
|
|
{name: '--bg-blockquote', value: 'var(--background-secondary-alt)'},
|
|
{name: '--bg-table-header', value: 'var(--background-tertiary)'},
|
|
{name: '--bg-table-row-odd', value: 'var(--background-primary)'},
|
|
{name: '--bg-table-row-even', value: 'var(--background-secondary)'},
|
|
{name: '--border-color', family: 'neutralDark', lightness: 50, alpha: 0.2},
|
|
{name: '--border-color-hover', family: 'neutralDark', lightness: 50, alpha: 0.3},
|
|
{name: '--border-color-focus', hue: 210, saturation: 90, useSaturationFactor: true, lightness: 70, alpha: 0.45},
|
|
{name: '--accent-primary', value: 'var(--brand-primary)'},
|
|
{name: '--accent-success', value: 'var(--status-online)'},
|
|
{name: '--accent-warning', value: 'var(--status-idle)'},
|
|
{name: '--accent-danger', value: 'var(--status-dnd)'},
|
|
{name: '--accent-info', value: 'var(--text-link)'},
|
|
{name: '--accent-purple', family: 'accentPurple', lightness: 65},
|
|
{name: '--alert-note-color', family: 'link', lightness: 70},
|
|
{name: '--alert-tip-color', family: 'statusOnline', lightness: 45},
|
|
{name: '--alert-important-color', family: 'accentPurple', lightness: 65},
|
|
{name: '--alert-warning-color', family: 'statusIdle', lightness: 55},
|
|
{name: '--alert-caution-color', hue: 359, saturation: 75, useSaturationFactor: true, lightness: 60},
|
|
{name: '--shadow-sm', value: '0 1px 2px rgba(0, 0, 0, 0.1)'},
|
|
{name: '--shadow-md', value: '0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.1)'},
|
|
{name: '--shadow-lg', value: '0 4px 8px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1)'},
|
|
{name: '--shadow-xl', value: '0 10px 20px rgba(0, 0, 0, 0.15), 0 4px 8px rgba(0, 0, 0, 0.1)'},
|
|
{name: '--transition-fast', value: '100ms ease'},
|
|
{name: '--transition-normal', value: '200ms ease'},
|
|
{name: '--transition-slow', value: '300ms ease'},
|
|
{name: '--spoiler-overlay-color', value: 'rgba(0, 0, 0, 0.2)'},
|
|
{name: '--spoiler-overlay-hover-color', value: 'rgba(0, 0, 0, 0.3)'},
|
|
{name: '--scrollbar-thumb-bg', value: 'rgba(121, 122, 124, 0.4)'},
|
|
{name: '--scrollbar-thumb-bg-hover', value: 'rgba(121, 122, 124, 0.7)'},
|
|
{name: '--scrollbar-track-bg', value: 'transparent'},
|
|
{
|
|
name: '--user-area-divider-color',
|
|
value: 'color-mix(in srgb, var(--background-modifier-hover) 70%, transparent)',
|
|
},
|
|
],
|
|
|
|
light: [
|
|
{scale: 'lightSurface'},
|
|
{scale: 'lightText'},
|
|
{name: '--panel-control-bg', value: 'color-mix(in srgb, var(--background-secondary) 65%, hsl(0, 0%, 100%) 35%)'},
|
|
{name: '--panel-control-border', family: 'neutralLight', saturation: 25, lightness: 45, alpha: 0.25},
|
|
{name: '--panel-control-divider', family: 'neutralLight', saturation: 30, lightness: 35, alpha: 0.2},
|
|
{name: '--panel-control-highlight', value: 'hsla(0, 0%, 100%, 0.65)'},
|
|
{name: '--background-modifier-hover', family: 'neutralLight', saturation: 10, lightness: 10, alpha: 0.05},
|
|
{name: '--background-modifier-selected', family: 'neutralLight', saturation: 10, lightness: 10, alpha: 0.1},
|
|
{name: '--background-modifier-accent', family: 'neutralLight', saturation: 10, lightness: 40, alpha: 0.22},
|
|
{name: '--background-modifier-accent-focus', family: 'neutralLight', saturation: 10, lightness: 40, alpha: 0.32},
|
|
{name: '--control-button-normal-bg', value: 'transparent'},
|
|
{name: '--control-button-normal-text', family: 'neutralLight', lightness: 50},
|
|
{name: '--control-button-hover-bg', family: 'neutralLight', lightness: 88},
|
|
{name: '--control-button-hover-text', family: 'neutralLight', lightness: 20},
|
|
{name: '--control-button-active-bg', family: 'neutralLight', lightness: 85},
|
|
{name: '--control-button-active-text', family: 'neutralLight', lightness: 15},
|
|
{name: '--control-button-danger-text', hue: 359, saturation: 70, useSaturationFactor: true, lightness: 50},
|
|
{name: '--control-button-danger-hover-bg', hue: 359, saturation: 70, useSaturationFactor: true, lightness: 95},
|
|
{name: '--text-link', family: 'link', lightness: 45},
|
|
{name: '--text-code', family: 'textCode', lightness: 45},
|
|
{name: '--text-selection', hue: 210, saturation: 90, useSaturationFactor: true, lightness: 50, alpha: 0.2},
|
|
{name: '--markup-mention-border', family: 'link', lightness: 45, alpha: 0.4},
|
|
{name: '--markup-jump-link-fill', value: 'color-mix(in srgb, var(--text-link) 8%, transparent)'},
|
|
{name: '--markup-everyone-text', hue: 250, saturation: 70, useSaturationFactor: true, lightness: 45},
|
|
{
|
|
name: '--markup-everyone-fill',
|
|
value: 'color-mix(in srgb, hsl(250, calc(70% * var(--saturation-factor)), 45%) 12%, transparent)',
|
|
},
|
|
{
|
|
name: '--markup-everyone-border',
|
|
hue: 250,
|
|
saturation: 70,
|
|
useSaturationFactor: true,
|
|
lightness: 45,
|
|
alpha: 0.4,
|
|
},
|
|
{name: '--markup-here-text', hue: 40, saturation: 85, useSaturationFactor: true, lightness: 40},
|
|
{
|
|
name: '--markup-here-fill',
|
|
value: 'color-mix(in srgb, hsl(40, calc(85% * var(--saturation-factor)), 40%) 12%, transparent)',
|
|
},
|
|
{name: '--markup-here-border', hue: 40, saturation: 85, useSaturationFactor: true, lightness: 40, alpha: 0.4},
|
|
{name: '--status-online', family: 'statusOnline', saturation: 70, lightness: 40},
|
|
{name: '--status-idle', family: 'statusIdle', saturation: 90, lightness: 45},
|
|
{name: '--status-dnd', hue: 359, saturation: 70, useSaturationFactor: true, lightness: 50},
|
|
{name: '--status-offline', family: 'statusOffline', hue: 210, saturation: 10, lightness: 55},
|
|
{name: '--plutonium', value: 'var(--brand-primary)'},
|
|
{name: '--plutonium-hover', value: 'var(--brand-secondary)'},
|
|
{name: '--plutonium-text', value: 'var(--text-on-brand-primary)'},
|
|
{name: '--plutonium-icon', family: 'brandIcon', lightness: 45},
|
|
{name: '--invite-verified-icon-color', value: 'var(--brand-primary)'},
|
|
{name: '--border-color', family: 'neutralLight', lightness: 40, alpha: 0.15},
|
|
{name: '--border-color-hover', family: 'neutralLight', lightness: 40, alpha: 0.25},
|
|
{name: '--border-color-focus', hue: 210, saturation: 90, useSaturationFactor: true, lightness: 50, alpha: 0.4},
|
|
{name: '--bg-primary', value: 'var(--background-primary)'},
|
|
{name: '--bg-secondary', value: 'var(--background-secondary)'},
|
|
{name: '--bg-tertiary', value: 'var(--background-tertiary)'},
|
|
{name: '--bg-hover', value: 'var(--background-modifier-hover)'},
|
|
{name: '--bg-active', value: 'var(--background-modifier-selected)'},
|
|
{name: '--bg-code', family: 'neutralLight', saturation: 22, lightness: 90, alpha: 0.9},
|
|
{name: '--bg-code-block', value: 'var(--background-primary)'},
|
|
{name: '--bg-blockquote', value: 'var(--background-secondary-alt)'},
|
|
{name: '--bg-table-header', value: 'var(--background-tertiary)'},
|
|
{name: '--bg-table-row-odd', value: 'var(--background-primary)'},
|
|
{name: '--bg-table-row-even', value: 'var(--background-secondary)'},
|
|
{name: '--alert-note-color', family: 'link', lightness: 45},
|
|
{name: '--alert-tip-color', hue: 150, saturation: 80, useSaturationFactor: true, lightness: 35},
|
|
{name: '--alert-important-color', family: 'accentPurple', lightness: 50},
|
|
{name: '--alert-warning-color', family: 'statusIdle', saturation: 90, lightness: 45},
|
|
{name: '--alert-caution-color', hue: 358, saturation: 80, useSaturationFactor: true, lightness: 50},
|
|
{name: '--spoiler-overlay-color', value: 'rgba(0, 0, 0, 0.1)'},
|
|
{name: '--spoiler-overlay-hover-color', value: 'rgba(0, 0, 0, 0.15)'},
|
|
{name: '--button-secondary-fill', family: 'neutralLight', saturation: 10, lightness: 10, alpha: 0.1},
|
|
{name: '--button-secondary-active-fill', family: 'neutralLight', saturation: 10, lightness: 10, alpha: 0.15},
|
|
{name: '--button-secondary-text', family: 'neutralLight', lightness: 15},
|
|
{name: '--button-secondary-active-text', family: 'neutralLight', lightness: 10},
|
|
{name: '--button-ghost-text', family: 'neutralLight', lightness: 20},
|
|
{name: '--button-inverted-fill', hue: 0, saturation: 0, lightness: 100},
|
|
{name: '--button-inverted-text', hue: 0, saturation: 0, lightness: 10},
|
|
{name: '--button-outline-border', value: '1px solid hsla(220, calc(10% * var(--saturation-factor)), 40%, 0.3)'},
|
|
{name: '--button-outline-text', family: 'neutralLight', lightness: 20},
|
|
{name: '--button-outline-active-fill', family: 'neutralLight', saturation: 10, lightness: 10, alpha: 0.1},
|
|
{
|
|
name: '--button-outline-active-border',
|
|
value: '1px solid hsla(220, calc(10% * var(--saturation-factor)), 40%, 0.5)',
|
|
},
|
|
{name: '--button-danger-outline-border', value: '1px solid hsl(359, calc(70% * var(--saturation-factor)), 50%)'},
|
|
{name: '--button-danger-outline-text', hue: 359, saturation: 70, useSaturationFactor: true, lightness: 45},
|
|
{name: '--button-danger-outline-active-fill', hue: 359, saturation: 70, useSaturationFactor: true, lightness: 50},
|
|
{name: '--user-area-divider-color', family: 'neutralLight', lightness: 40, alpha: 0.2},
|
|
],
|
|
|
|
coal: [
|
|
{scale: 'coalSurface'},
|
|
{name: '--background-secondary', value: 'var(--background-primary)'},
|
|
{name: '--background-secondary-lighter', value: 'var(--background-primary)'},
|
|
{
|
|
name: '--panel-control-bg',
|
|
value: `color-mix(
|
|
in srgb,
|
|
var(--background-primary) 90%,
|
|
hsl(220, calc(13% * var(--saturation-factor)), 0%) 10%
|
|
)`,
|
|
},
|
|
{name: '--panel-control-border', family: 'neutralDark', saturation: 20, lightness: 30, alpha: 0.35},
|
|
{name: '--panel-control-divider', family: 'neutralDark', saturation: 20, lightness: 25, alpha: 0.28},
|
|
{name: '--panel-control-highlight', value: 'hsla(0, 0%, 100%, 0.06)'},
|
|
{name: '--background-modifier-hover', family: 'neutralDark', lightness: 100, alpha: 0.04},
|
|
{name: '--background-modifier-selected', family: 'neutralDark', lightness: 100, alpha: 0.08},
|
|
{name: '--background-modifier-accent', family: 'neutralDark', saturation: 10, lightness: 65, alpha: 0.18},
|
|
{name: '--background-modifier-accent-focus', family: 'neutralDark', saturation: 10, lightness: 70, alpha: 0.26},
|
|
{name: '--control-button-normal-bg', value: 'transparent'},
|
|
{name: '--control-button-normal-text', value: 'var(--text-primary-muted)'},
|
|
{name: '--control-button-hover-bg', family: 'neutralDark', lightness: 12},
|
|
{name: '--control-button-hover-text', value: 'var(--text-primary)'},
|
|
{name: '--control-button-active-bg', family: 'neutralDark', lightness: 14},
|
|
{name: '--control-button-active-text', value: 'var(--text-primary)'},
|
|
{name: '--scrollbar-thumb-bg', value: 'rgba(160, 160, 160, 0.35)'},
|
|
{name: '--scrollbar-thumb-bg-hover', value: 'rgba(200, 200, 200, 0.55)'},
|
|
{name: '--scrollbar-track-bg', value: 'rgba(0, 0, 0, 0.45)'},
|
|
{name: '--bg-primary', value: 'var(--background-primary)'},
|
|
{name: '--bg-secondary', value: 'var(--background-secondary)'},
|
|
{name: '--bg-tertiary', value: 'var(--background-tertiary)'},
|
|
{name: '--bg-hover', value: 'var(--background-modifier-hover)'},
|
|
{name: '--bg-active', value: 'var(--background-modifier-selected)'},
|
|
{name: '--bg-code', value: 'hsl(220, calc(13% * var(--saturation-factor)), 8%)'},
|
|
{name: '--bg-code-block', value: 'var(--background-secondary-alt)'},
|
|
{name: '--bg-blockquote', value: 'var(--background-secondary)'},
|
|
{name: '--bg-table-header', value: 'var(--background-tertiary)'},
|
|
{name: '--bg-table-row-odd', value: 'var(--background-primary)'},
|
|
{name: '--bg-table-row-even', value: 'var(--background-secondary)'},
|
|
{name: '--button-secondary-fill', value: 'hsla(0, 0%, 100%, 0.04)'},
|
|
{name: '--button-secondary-active-fill', value: 'hsla(0, 0%, 100%, 0.07)'},
|
|
{name: '--button-secondary-text', value: 'var(--text-primary)'},
|
|
{name: '--button-secondary-active-text', value: 'var(--text-primary)'},
|
|
{name: '--button-outline-border', value: '1px solid hsla(0, 0%, 100%, 0.08)'},
|
|
{name: '--button-outline-active-fill', value: 'hsla(0, 0%, 100%, 0.12)'},
|
|
{name: '--button-outline-active-border', value: '1px solid hsla(0, 0%, 100%, 0.16)'},
|
|
{
|
|
name: '--user-area-divider-color',
|
|
value: 'color-mix(in srgb, var(--background-modifier-hover) 80%, transparent)',
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
interface OutputToken {
|
|
type: 'tone' | 'literal';
|
|
name: string;
|
|
family?: string;
|
|
hue?: number;
|
|
saturation?: number;
|
|
lightness?: number;
|
|
alpha?: number;
|
|
useSaturationFactor?: boolean;
|
|
value?: string;
|
|
}
|
|
|
|
function clamp01(value: number): number {
|
|
return Math.min(1, Math.max(0, value));
|
|
}
|
|
|
|
function applyCurve(curve: Scale['curve'], t: number): number {
|
|
switch (curve) {
|
|
case 'easeIn':
|
|
return t * t;
|
|
case 'easeOut':
|
|
return 1 - (1 - t) * (1 - t);
|
|
case 'easeInOut':
|
|
if (t < 0.5) {
|
|
return 2 * t * t;
|
|
}
|
|
return 1 - 2 * (1 - t) * (1 - t);
|
|
default:
|
|
return t;
|
|
}
|
|
}
|
|
|
|
function buildScaleTokens(scale: Scale): Array<OutputToken> {
|
|
const lastIndex = Math.max(scale.stops.length - 1, 1);
|
|
const tokens: Array<OutputToken> = [];
|
|
|
|
for (let i = 0; i < scale.stops.length; i++) {
|
|
const stop = scale.stops[i];
|
|
let pos: number;
|
|
|
|
if (stop.position !== undefined) {
|
|
pos = clamp01(stop.position);
|
|
} else {
|
|
pos = i / lastIndex;
|
|
}
|
|
|
|
const eased = applyCurve(scale.curve, pos);
|
|
let lightness = scale.range[0] + (scale.range[1] - scale.range[0]) * eased;
|
|
lightness = Math.round(lightness * 1000) / 1000;
|
|
|
|
tokens.push({
|
|
type: 'tone',
|
|
name: stop.name,
|
|
family: scale.family,
|
|
lightness,
|
|
});
|
|
}
|
|
|
|
return tokens;
|
|
}
|
|
|
|
function expandTokens(defs: Array<TokenDef>, scales: Record<string, Scale>): Array<OutputToken> {
|
|
const tokens: Array<OutputToken> = [];
|
|
|
|
for (const def of defs) {
|
|
if (def.scale) {
|
|
const scale = scales[def.scale];
|
|
if (!scale) {
|
|
console.warn(`Warning: unknown scale "${def.scale}"`);
|
|
continue;
|
|
}
|
|
tokens.push(...buildScaleTokens(scale));
|
|
continue;
|
|
}
|
|
|
|
if (def.value !== undefined) {
|
|
tokens.push({
|
|
type: 'literal',
|
|
name: def.name!,
|
|
value: def.value.trim(),
|
|
});
|
|
} else {
|
|
tokens.push({
|
|
type: 'tone',
|
|
name: def.name!,
|
|
family: def.family,
|
|
hue: def.hue,
|
|
saturation: def.saturation,
|
|
lightness: def.lightness,
|
|
alpha: def.alpha,
|
|
useSaturationFactor: def.useSaturationFactor,
|
|
});
|
|
}
|
|
}
|
|
|
|
return tokens;
|
|
}
|
|
|
|
function formatNumber(value: number): string {
|
|
if (value === Math.floor(value)) {
|
|
return String(Math.floor(value));
|
|
}
|
|
let s = value.toFixed(2);
|
|
s = s.replace(/\.?0+$/, '');
|
|
return s;
|
|
}
|
|
|
|
function formatTone(token: OutputToken, families: Record<string, ColorFamily>): string {
|
|
const family = token.family ? families[token.family] : undefined;
|
|
|
|
let hue = 0;
|
|
let saturation = 0;
|
|
let lightness = 0;
|
|
let useFactor = false;
|
|
|
|
if (token.hue !== undefined) {
|
|
hue = token.hue;
|
|
} else if (family) {
|
|
hue = family.hue;
|
|
}
|
|
|
|
if (token.saturation !== undefined) {
|
|
saturation = token.saturation;
|
|
} else if (family) {
|
|
saturation = family.saturation;
|
|
}
|
|
|
|
if (token.lightness !== undefined) {
|
|
lightness = token.lightness;
|
|
}
|
|
|
|
if (token.useSaturationFactor !== undefined) {
|
|
useFactor = token.useSaturationFactor;
|
|
} else if (family) {
|
|
useFactor = family.useSaturationFactor;
|
|
}
|
|
|
|
let satStr: string;
|
|
if (useFactor) {
|
|
satStr = `calc(${formatNumber(saturation)}% * var(--saturation-factor))`;
|
|
} else {
|
|
satStr = `${formatNumber(saturation)}%`;
|
|
}
|
|
|
|
if (token.alpha === undefined) {
|
|
return `hsl(${formatNumber(hue)}, ${satStr}, ${formatNumber(lightness)}%)`;
|
|
}
|
|
|
|
return `hsla(${formatNumber(hue)}, ${satStr}, ${formatNumber(lightness)}%, ${formatNumber(token.alpha)})`;
|
|
}
|
|
|
|
function formatValue(token: OutputToken, families: Record<string, ColorFamily>): string {
|
|
if (token.type === 'tone') {
|
|
return formatTone(token, families);
|
|
}
|
|
return token.value!.trim();
|
|
}
|
|
|
|
function renderBlock(selector: string, tokens: Array<OutputToken>, families: Record<string, ColorFamily>): string {
|
|
const lines: Array<string> = [];
|
|
for (const token of tokens) {
|
|
lines.push(`\t${token.name}: ${formatValue(token, families)};`);
|
|
}
|
|
return `${selector} {\n${lines.join('\n')}\n}`;
|
|
}
|
|
|
|
function generateCSS(
|
|
cfg: Config,
|
|
rootTokens: Array<OutputToken>,
|
|
lightTokens: Array<OutputToken>,
|
|
coalTokens: Array<OutputToken>,
|
|
): string {
|
|
const header = `/*
|
|
* This file is auto-generated by scripts/GenerateColorSystem.ts.
|
|
* Do not edit directly — update the config in generate-color-system.ts instead.
|
|
*/`;
|
|
|
|
const blocks = [
|
|
renderBlock(':root', rootTokens, cfg.families),
|
|
renderBlock('.theme-light', lightTokens, cfg.families),
|
|
renderBlock('.theme-coal', coalTokens, cfg.families),
|
|
];
|
|
|
|
return `${header}\n\n${blocks.join('\n\n')}\n`;
|
|
}
|
|
|
|
function main() {
|
|
const scriptDir = import.meta.dirname;
|
|
const appDir = join(scriptDir, '..');
|
|
|
|
const rootTokens = expandTokens(CONFIG.tokens.root, CONFIG.scales);
|
|
const lightTokens = expandTokens(CONFIG.tokens.light, CONFIG.scales);
|
|
const coalTokens = expandTokens(CONFIG.tokens.coal, CONFIG.scales);
|
|
|
|
const cssPath = join(appDir, 'src', 'styles', 'generated', 'color-system.css');
|
|
|
|
mkdirSync(dirname(cssPath), {recursive: true});
|
|
|
|
const css = generateCSS(CONFIG, rootTokens, lightTokens, coalTokens);
|
|
writeFileSync(cssPath, css);
|
|
|
|
const relCSS = relative(appDir, cssPath);
|
|
console.log(`Wrote ${relCSS}`);
|
|
}
|
|
|
|
main();
|