Admins set the default mode, contrast and color on Settings > Display, shown as three labelled form rows below the Theme select. Logged-in users' choices are saved on their account and override only the parts they changed; anonymous visitors get the default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
66 lines
3.6 KiB
JavaScript
66 lines
3.6 KiB
JavaScript
// Generates tonalmine/stylesheets/palettes.css: one M3 color scheme per seed color,
|
|
// in light and dark. Run: cd tools/palettes && npm install && npm run build
|
|
// The same build bundles scheme.mjs into tonalmine/javascripts/scheme.js for the custom color picker
|
|
// (bundled with esbuild because material-color-utilities 0.4.0 has extensionless ESM imports).
|
|
import { writeFileSync, readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import { ROLES, kebab, paletteCss } from './scheme.mjs';
|
|
|
|
// The first palette is the default. Ids must match PALETTES in tonalmine/javascripts/theme.js
|
|
// and redmine_tonalmine/lib/redmine_tonalmine/appearance.rb.
|
|
// An optional third entry is a Material Theme Builder export folder (light.css, dark.css and the
|
|
// -mc/-hc contrast variants):
|
|
// its roles are copied as-is instead of generated, and the seed only drives success/warning.
|
|
const PALETTES = [
|
|
['purple', '#6750A4'],
|
|
['blue', '#0061A4'],
|
|
['green', '#386A20'],
|
|
['orange', '#8B5000'],
|
|
];
|
|
|
|
// Reads the roles from a Theme Builder export; fails if one is missing.
|
|
function exported(dir, dark, contrast) {
|
|
const file = `${dir}/${dark ? 'dark' : 'light'}${{ medium: '-mc', high: '-hc' }[contrast] || ''}.css`;
|
|
const vars = Object.fromEntries([...readFileSync(file, 'utf8').matchAll(/--md-sys-color-([a-z-]+):\s*([^;]+);/g)].map((m) => [m[1], m[2].trim()]));
|
|
return ROLES.map((r) => {
|
|
if (!vars[kebab(r)]) { console.error(`${file} is missing --md-sys-color-${kebab(r)}`); process.exit(1); }
|
|
return `--md-sys-color-${kebab(r)}: ${vars[kebab(r)]};`;
|
|
});
|
|
}
|
|
|
|
// Specificity: the default palette uses bare :root, the others add [data-palette], and every dark
|
|
// rule adds one more attribute, so "palette + mode" always beats "palette" and the default.
|
|
let css = `/*
|
|
* Generated by tools/palettes/build_palettes.mjs; do not edit by hand.
|
|
* M3 color roles (TonalSpot) per palette, light and dark, at contrast 0, 0.5 and 1.
|
|
* <html data-palette="..."> picks the palette, <html data-mode="light|dark"> forces a mode;
|
|
* without data-mode the OS setting (prefers-color-scheme) decides.
|
|
* <html data-contrast="medium|high"> picks the contrast level; without it, standard.
|
|
*/
|
|
|
|
:root { color-scheme: light; }
|
|
:root[data-mode="dark"] { color-scheme: dark; }
|
|
@media (prefers-color-scheme: dark) { :root:not([data-mode="light"]) { color-scheme: dark; } }
|
|
`;
|
|
PALETTES.forEach(([id, seed, dir], i) => {
|
|
const sel = i === 0 ? ':root' : `:root[data-palette="${id}"]`;
|
|
css += `
|
|
/* ${id} (${dir ? `export ${dir}, ` : ''}seed ${seed}) */
|
|
` + paletteCss(sel, seed, dir && ((dark, contrast) => exported(dir, dark, contrast)));
|
|
});
|
|
|
|
// Paths are relative to tools/palettes (npm runs scripts there); the bundle itself lives in .build/.
|
|
const theme = resolve(process.cwd(), '../../tonalmine');
|
|
const out = `${theme}/stylesheets/palettes.css`;
|
|
writeFileSync(out, css);
|
|
|
|
// Keep theme.js and the plugin's validation in sync: every palette id must appear in their PALETTES lists.
|
|
const js = readFileSync(`${theme}/javascripts/theme.js`, 'utf8');
|
|
const missing = PALETTES.map(([id]) => id).filter((id) => !js.includes(`'${id}'`));
|
|
if (missing.length) { console.error(`theme.js is missing palettes: ${missing.join(', ')}`); process.exit(1); }
|
|
const rb = readFileSync(resolve(theme, '../redmine_tonalmine/lib/redmine_tonalmine/appearance.rb'), 'utf8');
|
|
const rbIds = rb.match(/PALETTES = %w\[([^\]]*)\]/)[1].split(/\s+/);
|
|
const rbMissing = PALETTES.map(([id]) => id).filter((id) => !rbIds.includes(id));
|
|
if (rbMissing.length) { console.error(`appearance.rb is missing palettes: ${rbMissing.join(', ')}`); process.exit(1); }
|
|
console.log(`Wrote ${PALETTES.length} palettes to ${out}`);
|