From f2baf869c9d9ae32c970e694f80e1eb76ca9c71a Mon Sep 17 00:00:00 2001 From: Codinget Date: Fri, 22 May 2026 17:38:38 +0000 Subject: [PATCH] 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 --- .gitignore | 4 + .gitmodules | 3 + doki-master-theme | 1 + generate.ts | 53 +++++ package-lock.json | 566 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 14 ++ src/colorMath.ts | 106 +++++++++ src/cssWriter.ts | 26 ++ src/palette.ts | 67 ++++++ src/themeBuilder.ts | 431 +++++++++++++++++++++++++++++++++ src/types.ts | 107 +++++++++ tsconfig.json | 10 + 12 files changed, 1388 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 160000 doki-master-theme create mode 100644 generate.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/colorMath.ts create mode 100644 src/cssWriter.ts create mode 100644 src/palette.ts create mode 100644 src/themeBuilder.ts create mode 100644 src/types.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4f5d4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +output/ +.vscode/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ce273f5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "doki-master-theme"] + path = doki-master-theme + url = https://github.com/doki-theme/doki-master-theme.git diff --git a/doki-master-theme b/doki-master-theme new file mode 160000 index 0000000..4467240 --- /dev/null +++ b/doki-master-theme @@ -0,0 +1 @@ +Subproject commit 4467240b45837ff473903d572790112fc2caa8ca diff --git a/generate.ts b/generate.ts new file mode 100644 index 0000000..96f9eb5 --- /dev/null +++ b/generate.ts @@ -0,0 +1,53 @@ +import { readdir, writeFile, mkdir } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; +import { loadTheme } from './src/palette.js'; +import { buildVariables } from './src/themeBuilder.js'; +import { renderThemeCSS } from './src/cssWriter.js'; + +const DOKI_REPO = resolve(process.env['DOKI_REPO'] ?? './doki-master-theme'); +const OUTPUT_DIR = resolve(process.env['OUTPUT_DIR'] ?? './output'); + +async function findDefinitionFiles(dir: string): Promise { + const entries = await readdir(dir, { recursive: true }); + return entries + .filter((f) => f.endsWith('.master.definition.json')) + .map((f) => join(dir, f)); +} + +async function main(): Promise { + await mkdir(OUTPUT_DIR, { recursive: true }); + + const definitionsDir = join(DOKI_REPO, 'definitions'); + const definitionFiles = await findDefinitionFiles(definitionsDir); + + if (definitionFiles.length === 0) { + console.error(`No definition files found in ${definitionsDir}`); + console.error('Make sure the doki-master-theme submodule is initialised:'); + console.error(' git submodule update --init'); + process.exit(1); + } + + let success = 0; + let errors = 0; + + for (const defPath of definitionFiles) { + try { + const theme = loadTheme(defPath, DOKI_REPO); + const variables = buildVariables(theme); + const css = renderThemeCSS(theme, variables); + + const outPath = join(OUTPUT_DIR, `theme-${theme.internalName}.css`); + await writeFile(outPath, css, 'utf8'); + + console.log(`✓ ${theme.internalName}`); + success++; + } catch (err) { + console.error(`✗ ${defPath}: ${err}`); + errors++; + } + } + + console.log(`\nGenerated ${success} theme${success !== 1 ? 's' : ''}. Errors: ${errors}.`); +} + +main(); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6386269 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,566 @@ +{ + "name": "gitea-doki-theme-generator", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gitea-doki-theme-generator", + "version": "1.0.0", + "devDependencies": { + "@types/node": "^22.15.21", + "tsx": "^4.19.4", + "typescript": "^5.8.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/tsx": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7eb6073 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "gitea-doki-theme-generator", + "version": "1.0.0", + "type": "module", + "scripts": { + "generate": "tsx generate.ts", + "generate:watch": "tsx watch generate.ts" + }, + "devDependencies": { + "tsx": "^4.19.4", + "typescript": "^5.8.3", + "@types/node": "^22.15.21" + } +} diff --git a/src/colorMath.ts b/src/colorMath.ts new file mode 100644 index 0000000..8d4fd4a --- /dev/null +++ b/src/colorMath.ts @@ -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)); +} diff --git a/src/cssWriter.ts b/src/cssWriter.ts new file mode 100644 index 0000000..b68ae7f --- /dev/null +++ b/src/cssWriter.ts @@ -0,0 +1,26 @@ +import type { ThemeConfig } from './types.js'; + +export function renderThemeCSS(theme: ThemeConfig, variables: Record): 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'); +} diff --git a/src/palette.ts b/src/palette.ts new file mode 100644 index 0000000..25fc1cd --- /dev/null +++ b/src/palette.ts @@ -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(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(join(templatesDir, 'base.colors.template.json')); + const def = loadJson(definitionPath); + + const variantTemplate = loadJson( + 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(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, + }; +} diff --git a/src/themeBuilder.ts b/src/themeBuilder.ts new file mode 100644 index 0000000..b2b6af0 --- /dev/null +++ b/src/themeBuilder.ts @@ -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 { + 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 = {}; + + // ─── 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; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..7557a28 --- /dev/null +++ b/src/types.ts @@ -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; + }; + }; + 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; +} + +export interface ThemeConfig { + internalName: string; + displayName: string; + colorScheme: "dark" | "light"; + palette: DokiColors; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..48c8a6c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "outDir": "./dist" + }, + "include": ["generate.ts", "src/**/*.ts"] +}