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 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 17:38:38 +00:00
commit f2baf869c9
12 changed files with 1388 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
dist/
output/
.vscode/
+3
View File
@@ -0,0 +1,3 @@
[submodule "doki-master-theme"]
path = doki-master-theme
url = https://github.com/doki-theme/doki-master-theme.git
+1
Submodule doki-master-theme added at 4467240b45
+53
View File
@@ -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<string[]> {
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<void> {
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();
+566
View File
@@ -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"
}
}
}
+14
View File
@@ -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"
}
}
+106
View File
@@ -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));
}
+26
View File
@@ -0,0 +1,26 @@
import type { ThemeConfig } from './types.js';
export function renderThemeCSS(theme: ThemeConfig, variables: Record<string, string>): 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');
}
+67
View File
@@ -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<T>(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<ColorTemplate>(join(templatesDir, 'base.colors.template.json'));
const def = loadJson<DokiDefinition>(definitionPath);
const variantTemplate = loadJson<ColorTemplate>(
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<DokiDefinition>(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,
};
}
+431
View File
@@ -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<string, string> {
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<string, string> = {};
// ─── 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;
}
+107
View File
@@ -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<DokiColors>;
};
};
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<DokiColors>;
}
export interface ThemeConfig {
internalName: string;
displayName: string;
colorScheme: "dark" | "light";
palette: DokiColors;
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"outDir": "./dist"
},
"include": ["generate.ts", "src/**/*.ts"]
}