/* * 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 . */ 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; } interface TokenDef { name?: string; scale?: string; value?: string; family?: string; hue?: number; saturation?: number; lightness?: number; alpha?: number; useSaturationFactor?: boolean; } interface Config { families: Record; scales: Record; tokens: { root: Array; light: Array; coal: Array; }; } 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 { const lastIndex = Math.max(scale.stops.length - 1, 1); const tokens: Array = []; 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, scales: Record): Array { const tokens: Array = []; 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 { 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 { if (token.type === 'tone') { return formatTone(token, families); } return token.value!.trim(); } function renderBlock(selector: string, tokens: Array, families: Record): string { const lines: Array = []; 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, lightTokens: Array, coalTokens: Array, ): 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();