From 3fa280d21878ae391674a21758199df3d2d8c3b5 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 3 Oct 2025 09:04:28 -0500 Subject: chore: app -> desktop --- packages/desktop/scripts/vite-theme-plugin.ts | 163 ++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 packages/desktop/scripts/vite-theme-plugin.ts (limited to 'packages/desktop/scripts') diff --git a/packages/desktop/scripts/vite-theme-plugin.ts b/packages/desktop/scripts/vite-theme-plugin.ts new file mode 100644 index 000000000..1241ffcd7 --- /dev/null +++ b/packages/desktop/scripts/vite-theme-plugin.ts @@ -0,0 +1,163 @@ +import type { Plugin } from "vite" +import { readdir, readFile, writeFile } from "fs/promises" +import { join, resolve } from "path" + +interface ThemeDefinition { + $schema?: string + defs?: Record + theme: Record +} + +interface ResolvedThemeColor { + dark: string + light: string +} + +class ColorResolver { + private colors: Map = new Map() + private visited: Set = new Set() + + constructor(defs: Record = {}, theme: Record = {}) { + Object.entries(defs).forEach(([key, value]) => { + this.colors.set(key, value) + }) + Object.entries(theme).forEach(([key, value]) => { + this.colors.set(key, value) + }) + } + + resolveColor(key: string, value: any): ResolvedThemeColor { + if (this.visited.has(key)) { + throw new Error(`Circular reference detected for color ${key}`) + } + + this.visited.add(key) + + try { + if (typeof value === "string") { + if (value === "none") return { dark: value, light: value } + if (value.startsWith("#")) { + return { dark: value.toLowerCase(), light: value.toLowerCase() } + } + const resolved = this.resolveReference(value) + return { dark: resolved, light: resolved } + } + if (typeof value === "object" && value !== null) { + const dark = this.resolveColorValue(value.dark || value.light || "#000000") + const light = this.resolveColorValue(value.light || value.dark || "#FFFFFF") + return { dark, light } + } + return { dark: "#000000", light: "#FFFFFF" } + } finally { + this.visited.delete(key) + } + } + + private resolveColorValue(value: any): string { + if (typeof value === "string") { + if (value === "none") return value + if (value.startsWith("#")) { + return value.toLowerCase() + } + return this.resolveReference(value) + } + return value + } + + private resolveReference(ref: string): string { + const colorValue = this.colors.get(ref) + if (colorValue === undefined) { + throw new Error(`Color reference '${ref}' not found`) + } + if (typeof colorValue === "string") { + if (colorValue === "none") return colorValue + if (colorValue.startsWith("#")) { + return colorValue.toLowerCase() + } + return this.resolveReference(colorValue) + } + return colorValue + } +} + +function kebabCase(str: string): string { + return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase() +} + +function parseTheme(themeData: ThemeDefinition): Record { + const resolver = new ColorResolver(themeData.defs, themeData.theme) + const colors: Record = {} + Object.entries(themeData.theme).forEach(([key, value]) => { + colors[key] = resolver.resolveColor(key, value) + }) + return colors +} + +async function loadThemes(): Promise>> { + const themesDir = resolve(__dirname, "../../tui/internal/theme/themes") + const files = await readdir(themesDir) + const themes: Record> = {} + + for (const file of files) { + if (!file.endsWith(".json")) continue + + const themeName = file.replace(".json", "") + const themeData: ThemeDefinition = JSON.parse(await readFile(join(themesDir, file), "utf-8")) + + themes[themeName] = parseTheme(themeData) + } + + return themes +} + +function generateCSS(themes: Record>): string { + let css = `/* Auto-generated theme CSS - Do not edit manually */\n:root {\n` + + const defaultTheme = themes["opencode"] || Object.values(themes)[0] + if (defaultTheme) { + Object.entries(defaultTheme).forEach(([key, color]) => { + const cssVar = `--theme-${kebabCase(key)}` + css += ` ${cssVar}: ${color.light};\n` + }) + } + css += `}\n\n` + + Object.entries(themes).forEach(([themeName, colors]) => { + css += `[data-theme="${themeName}"][data-dark="false"] {\n` + Object.entries(colors).forEach(([key, color]) => { + const cssVar = `--theme-${kebabCase(key)}` + css += ` ${cssVar}: ${color.light};\n` + }) + css += `}\n\n` + + css += `[data-theme="${themeName}"][data-dark="true"] {\n` + Object.entries(colors).forEach(([key, color]) => { + const cssVar = `--theme-${kebabCase(key)}` + css += ` ${cssVar}: ${color.dark};\n` + }) + css += `}\n\n` + }) + + return css +} + +export function generateThemeCSS(): Plugin { + return { + name: "generate-theme-css", + async buildStart() { + try { + console.log("Generating theme CSS...") + const themes = await loadThemes() + const css = generateCSS(themes) + + const outputPath = resolve(__dirname, "../src/assets/theme.css") + await writeFile(outputPath, css) + + console.log(`✅ Generated theme CSS with ${Object.keys(themes).length} themes`) + console.log(` Output: ${outputPath}`) + } catch (error) { + throw new Error(`Theme CSS generation failed: ${error}`) + } + }, + } +} -- cgit v1.2.3