From c833176b638eeb1cdc8b30d4aef632a25ede3777 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 14 Jul 2023 23:01:33 +0800 Subject: ... --- FrontEnd/tools/palette.ts | 271 ------------------------- FrontEnd/tools/theme-generator.ts | 403 ++++++++++++++++++++++++++++++++++++++ FrontEnd/tools/tsconfig.json | 4 +- 3 files changed, 405 insertions(+), 273 deletions(-) delete mode 100644 FrontEnd/tools/palette.ts create mode 100644 FrontEnd/tools/theme-generator.ts (limited to 'FrontEnd/tools') diff --git a/FrontEnd/tools/palette.ts b/FrontEnd/tools/palette.ts deleted file mode 100644 index 7b047ce1..00000000 --- a/FrontEnd/tools/palette.ts +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/env ts-node - -/** - * Color variable name scheme: - * has variant: --[prefix]-[name]-[variant]-color: [color]; - * no variant: --[prefix]-[name]-color: [color]; - * Variant scheme: - * [variant-prefix][level] - * eg. --cru-primary-color: [color]; --cru-primary-l1-color: [color]; - */ - -class HslColor { - constructor( - public h: number, - public s: number, - public l: number, - ) {} - - lighter(level: number): HslColor { - return new HslColor(this.h, this.s, this.l + level * 10); - } - - darker(level: number): HslColor { - return new HslColor(this.h, this.s, this.l - level * 10); - } - - toString(): string { - return `hsl(${this.h} ${this.s}% ${this.l}%)`; - } -} - -class CssVarColor { - constructor( - public name: string, - public cssVar: string, - ) {} - - toString(): string { - return `var(--${this.cssVar})`; - } -} - -class VariantColor { - constructor( - public color: HslColor, - public variant?: string | null, - ) {} - - toCssString(prefix: string, name: string): string { - const variantPart = this.variant == null ? "" : `-${this.variant}`; - return `--${prefix}-${name}${variantPart}-color: ${this.color.toString()};`; - } -} - -type LightnessVariantType = "lighter" | "darker"; - -interface LightnessVariant { - prefix: string; - type: LightnessVariantType; -} - -function generateLightnessVariantColors( - baseColor: HslColor, - lightnessVariant: LightnessVariant, - levels: number, -): VariantColor[] { - const result: VariantColor[] = []; - - for (let i = 1; i <= levels; i++) { - const color = - lightnessVariant.type === "lighter" - ? baseColor.lighter(i) - : baseColor.darker(i); - const colorVariant = `${lightnessVariant.prefix}${i}`; - result.push(new VariantColor(color, colorVariant)); - } - - return result; -} - -type ColorMode = "light" | "dark"; - -const themeVariantPrefixes = ["l", "d", "f", "b"] as const; - -type LightnessVariants = { - prefix: (typeof themeVariantPrefixes)[number]; - type: LightnessVariantType; -}[]; - -function generateThemeColorLightnessVariants( - mode: ColorMode, -): LightnessVariants { - return [ - { - prefix: "l", - type: "lighter", - }, - { - prefix: "d", - type: "darker", - }, - { - prefix: "f", - type: mode === "light" ? "lighter" : "darker", - }, - { - prefix: "b", - type: mode === "light" ? "darker" : "lighter", - }, - ]; -} - -class ColorGroup { - constructor( - public name: string, - public baseColor: HslColor, - ) {} - - generateVariantColors( - lightnessVariants: LightnessVariant[], - levels = 3, - ): VariantColor[] { - const result: VariantColor[] = [new VariantColor(this.baseColor)]; - - for (const lightnessVariant of lightnessVariants) { - result.push( - ...generateLightnessVariantColors( - this.baseColor, - lightnessVariant, - levels, - ), - ); - } - - return result; - } -} - -class ThemeColorGroup extends ColorGroup { - constructor(name: string, baseColor: HslColor) { - super(name, baseColor); - } - - generateColors(mode: ColorMode): VariantColor[] { - return super.generateVariantColors( - generateThemeColorLightnessVariants(mode), - ); - } - - generateCss(prefix: string, mode: ColorMode): string { - return this.generateColors(mode) - .map((c) => c.toCssString(prefix, this.name)) - .join("\n"); - } -} - -class VarColorGroup { - constructor( - public name: string, - public varName: string, - ) {} - - generateCss( - prefix: string, - variantPrefixes = themeVariantPrefixes, - levels = 3, - ): string { - const vs: string[] = []; - for (const v of variantPrefixes) { - for (let l = 1; l <= levels; l++) { - vs.push(`${v}${l}`); - } - } - let result = vs - .map( - (v) => - `--${prefix}-${this.name}-${v}-color: var(--${prefix}-${this.varName}-${v}-color);`, - ) - .join("\n"); - result = `--${prefix}-${this.name}-color: var(--${prefix}-${this.varName}-color);\n${result}`; - return result; - } -} - -const themeColorNames = [ - "primary", - "secondary", - "tertiary", - "danger", - "success", -] as const; - -type ThemeColorNames = (typeof themeColorNames)[number]; - -type ThemeColors = { - [key in ThemeColorNames]: HslColor; -}; - -// Config region begin -const prefix = "cru"; - -const themeColors: ThemeColors = { - primary: new HslColor(210, 100, 50), - secondary: new HslColor(40, 100, 50), - tertiary: new HslColor(160, 100, 50), - danger: new HslColor(0, 100, 50), - success: new HslColor(120, 100, 50), -}; - -// Config region end - -let output = ""; - -function indentText( - text: string, - level: number, - indentWidth = 2, - appendNewlines = 1, -): string { - const lines = text.split("\n"); - const indent = " ".repeat(level * indentWidth); - return ( - lines - .map((line) => (line.length === 0 ? "" : `${indent}${line}`)) - .join("\n") + "\n".repeat(appendNewlines) - ); -} - -function print(text: string, indent = 0, appendNewlines = 1) { - output += indentText(text, indent, 2, appendNewlines); -} - -function generateThemeColorCss(mode: ColorMode): string { - let output = ""; - for (const name of themeColorNames) { - const colorGroup = new ThemeColorGroup(name, themeColors[name]); - output += colorGroup.generateCss(prefix, mode); - } - return output; -} - -function generateThemeColorAliasCss(): string { - let result = ""; - for (const name of themeColorNames) { - const varColorGroup = new VarColorGroup("theme", name); - result += `.${prefix}-${name} {\n${indentText( - varColorGroup.generateCss(prefix), - 1, - )}\n}\n`; - } - return result; -} - -function main() { - print("/* Generated by palette.ts */\n"); - - print(":root {"); - print(generateThemeColorCss("light"), 1); - print("}\n"); - - print("@media (prefers-color-scheme: dark) {"); - print(":root {", 1); - print(generateThemeColorCss("dark"), 2); - print("}", 1); - print("}\n"); - - print(generateThemeColorAliasCss()); -} - -main(); -process.stdout.write(output); diff --git a/FrontEnd/tools/theme-generator.ts b/FrontEnd/tools/theme-generator.ts new file mode 100644 index 00000000..27dd5d1d --- /dev/null +++ b/FrontEnd/tools/theme-generator.ts @@ -0,0 +1,403 @@ +#!/usr/bin/env ts-node + +/** + * Color variable name scheme: + * has variant: --[prefix]-[name]-[variant]-color: [color]; + * no variant: --[prefix]-[name]-color: [color]; + * Variant scheme: + * [variant-prefix][level] + * eg. --cru-primary-color: [color]; --cru-primary-l1-color: [color]; + */ + +import { stdout } from "process"; + +interface CssSegment { + toCssString(): string; +} + +interface Color extends CssSegment { + readonly type: "hsl" | "css-var"; + toString(): string; +} + +class HslColor implements Color { + readonly type = "hsl"; + + constructor( + public h: number, + public s: number, + public l: number, + ) {} + + lighter(level: number): HslColor { + return new HslColor(this.h, this.s, this.l + level * 10); + } + + darker(level: number): HslColor { + return new HslColor(this.h, this.s, this.l - level * 10); + } + + toCssString(): string { + return this.toString(); + } + + toString(): string { + return `hsl(${this.h} ${this.s}% ${this.l}%)`; + } + + static readonly white = new HslColor(0, 0, 100); + static readonly black = new HslColor(0, 0, 0); +} + +class ColorVariable implements CssSegment { + constructor( + public prefix: string, + public name: string, + public variant?: string | null, + ) {} + + toString(): string { + const variantPart = this.variant == null ? "" : `-${this.variant}`; + return `--${this.prefix}-${this.name}${variantPart}-color`; + } + + toCssString(): string { + return this.toString(); + } +} + +class CssVarColor implements Color { + readonly type = "css-var"; + + constructor(public colorVariable: ColorVariable) {} + + toCssString(): string { + return this.toString(); + } + + toString(): string { + return `var(${this.colorVariable.toString()})`; + } +} + +class ColorVariableDefinition implements CssSegment { + constructor( + public name: ColorVariable, + public color: Color, + ) {} + + toCssString(): string { + return `${this.name.toCssString()}: ${this.color.toCssString()};`; + } +} + +type LightnessVariantType = "lighter" | "darker"; + +interface LightnessVariantInfo { + prefix: string; + type: LightnessVariantType; + levels: number; +} + +abstract class ColorGroup implements CssSegment { + abstract getColorVariables(): ColorVariableDefinition[]; + toCssString(): string { + return this.getColorVariables() + .map((c) => c.toCssString()) + .join("\n"); + } +} + +class LightnessVariantColorGroup extends ColorGroup { + constructor( + public prefix: string, + public name: string, + public baseColor: HslColor, + public lightnessVariants: LightnessVariantInfo[], + ) { + super(); + } + + getColorVariables(): ColorVariableDefinition[] { + const result: ColorVariableDefinition[] = [ + new ColorVariableDefinition( + new ColorVariable(this.prefix, this.name), + this.baseColor, + ), + ]; + + for (const lightnessVariant of this.lightnessVariants) { + for (let i = 1; i <= lightnessVariant.levels; i++) { + const color = + lightnessVariant.type === "lighter" + ? this.baseColor.lighter(i) + : this.baseColor.darker(i); + const colorVariant = `${lightnessVariant.prefix}${i}`; + result.push( + new ColorVariableDefinition( + new ColorVariable(this.prefix, this.name, colorVariant), + color, + ), + ); + } + } + + return result; + } +} + +class VarAliasColorGroup extends ColorGroup { + constructor( + public prefix: string, + public newName: string, + public oldName: string, + public variants: string[], + ) { + super(); + } + + getColorVariables(): ColorVariableDefinition[] { + const result = [ + new ColorVariableDefinition( + new ColorVariable(this.prefix, this.newName), + new CssVarColor(new ColorVariable(this.prefix, this.oldName)), + ), + ]; + for (const variant of this.variants) { + result.push( + new ColorVariableDefinition( + new ColorVariable(this.prefix, this.newName, variant), + new CssVarColor( + new ColorVariable(this.prefix, this.oldName, variant), + ), + ), + ); + } + return result; + } +} + +class GrayscaleColorGroup extends ColorGroup { + _delegate: LightnessVariantColorGroup; + + constructor( + public prefix: string, + public name: string, + public baseColor: HslColor, + public type: LightnessVariantType, + public levels = 3, + ) { + super(); + + this._delegate = new LightnessVariantColorGroup( + prefix, + name, + this.baseColor, + [{ prefix: "", type: this.type, levels }], + ); + } + + getColorVariables(): ColorVariableDefinition[] { + return this._delegate.getColorVariables(); + } + + static white(prefix: string, name: string, levels = 3): GrayscaleColorGroup { + return new GrayscaleColorGroup( + prefix, + name, + HslColor.white, + "darker", + levels, + ); + } + + static black(prefix: string, name: string, levels = 3): GrayscaleColorGroup { + return new GrayscaleColorGroup( + prefix, + name, + HslColor.black, + "lighter", + levels, + ); + } +} + +class CompositeColorGroup extends ColorGroup { + constructor(public groups: ColorGroup[]) { + super(); + } + + getColorVariables(): ColorVariableDefinition[] { + return this.groups + .map((g) => g.getColorVariables()) + .reduce((prev, curr) => prev.concat(curr), []); + } +} + +type ThemeColors = { name: string; color: HslColor }[]; + +type ColorMode = "light" | "dark"; + +class Theme { + static getDefaultThemeColorLightnessVariants( + mode: ColorMode, + levels = 3, + ): LightnessVariantInfo[] { + return [ + { + prefix: "l", + type: "lighter", + levels, + }, + { + prefix: "d", + type: "darker", + levels, + }, + { + prefix: "f", + type: mode === "light" ? "lighter" : "darker", + levels, + }, + { + prefix: "b", + type: mode === "light" ? "darker" : "lighter", + levels, + }, + ]; + } + + static getThemeColorAllVariants(): string[] { + const lightnessVariantInfos = + Theme.getDefaultThemeColorLightnessVariants("light"); + const result: string[] = []; + for (const { prefix, levels } of lightnessVariantInfos) { + for (let i = 1; i <= levels; i++) { + result.push(`${prefix}${i}`); + } + } + return result; + } + + constructor( + public prefix: string, + public themeColors: ThemeColors, + public levels = 3, + ) {} + + getThemeColorDefinitions(mode: ColorMode): ColorGroup { + const groups: ColorGroup[] = []; + for (const { name, color } of this.themeColors) { + const colorGroup = new LightnessVariantColorGroup( + this.prefix, + name, + color, + Theme.getDefaultThemeColorLightnessVariants(mode, this.levels), + ); + groups.push(colorGroup); + } + return new CompositeColorGroup(groups); + } + + getAliasColorDefinitions(name: string): ColorGroup { + return new VarAliasColorGroup( + this.prefix, + "theme", + name, + Theme.getThemeColorAllVariants(), + ); + } + + getGrayscaleDefinitions(mode: ColorMode): ColorGroup { + const textGroup = + mode === "light" + ? GrayscaleColorGroup.black(this.prefix, "text", this.levels) + : GrayscaleColorGroup.white(this.prefix, "text", this.levels); + const bgGroup = + mode === "light" + ? GrayscaleColorGroup.white(this.prefix, "bg", this.levels) + : GrayscaleColorGroup.black(this.prefix, "bg", this.levels); + const disabledGroup = + mode == "light" + ? new GrayscaleColorGroup( + this.prefix, + "disabled", + new HslColor(0, 0, 75), + "darker", + this.levels, + ) + : new GrayscaleColorGroup( + this.prefix, + "disabled", + new HslColor(0, 0, 25), + "lighter", + this.levels, + ); + return new CompositeColorGroup([textGroup, bgGroup, disabledGroup]); + } + + generateCss(print: (text: string, indent: number) => void): void { + print(":root {", 0); + print(this.getThemeColorDefinitions("light").toCssString(), 1); + print(this.getGrayscaleDefinitions("light").toCssString(), 1); + print("}", 0); + + print("", 0); + + print("@media (prefers-color-scheme: dark) {", 0); + print(":root {", 1); + print(this.getThemeColorDefinitions("dark").toCssString(), 2); + print(this.getGrayscaleDefinitions("dark").toCssString(), 2); + print("}", 1); + print("}", 0); + + print("", 0); + + for (const { name } of this.themeColors) { + print(`.${this.prefix}-${name} {`, 0); + print(this.getAliasColorDefinitions(name).toCssString(), 1); + print("}", 0); + + print("", 0); + } + } +} + +(function main() { + const prefix = "cru"; + const themeColors: ThemeColors = [ + { name: "primary", color: new HslColor(210, 100, 50) }, + { name: "secondary", color: new HslColor(40, 100, 50) }, + { name: "tertiary", color: new HslColor(160, 100, 50) }, + { name: "danger", color: new HslColor(0, 100, 50) }, + { name: "success", color: new HslColor(120, 100, 50) }, + ]; + + const theme = new Theme(prefix, themeColors); + + let output = ""; + + function indentText( + text: string, + level: number, + indentWidth = 2, + appendNewlines = 1, + ): string { + const lines = text.split("\n"); + const indent = " ".repeat(level * indentWidth); + return ( + lines + .map((line) => (line.length === 0 ? "" : `${indent}${line}`)) + .join("\n") + "\n".repeat(appendNewlines) + ); + } + + function print(text: string, indent = 0, appendNewlines = 1) { + output += indentText(text, indent, 2, appendNewlines); + } + + print("/* Generated by theme-generator.ts */\n"); + theme.generateCss(print); + + stdout.write(output); +})(); diff --git a/FrontEnd/tools/tsconfig.json b/FrontEnd/tools/tsconfig.json index 8c9b3c47..08f53190 100644 --- a/FrontEnd/tools/tsconfig.json +++ b/FrontEnd/tools/tsconfig.json @@ -1,6 +1,6 @@ { - // This is an alias to @tsconfig/node16: https://github.com/tsconfig/bases - "extends": "ts-node/node16/tsconfig.json", + // This is an alias to @tsconfig/node20: https://github.com/tsconfig/bases + "extends": "@tsconfig/node20/tsconfig.json", // Most ts-node options can be specified here using their programmatic names. "ts-node": { // It is faster to skip typechecking. -- cgit v1.2.3