// M3 color roles for one palette, shared by build_palettes.mjs (palettes.css) and the browser // (bundled as tonalmine/javascripts/scheme.js, global tonalmineScheme, for the custom color picker). import { Hct, SchemeTonalSpot, MaterialDynamicColors as C, hexFromArgb, argbFromHex, customColor } from '@material/material-color-utilities'; const CUSTOM = [['success', '#386A20'], ['warning', '#7C5800']]; export const ROLES = ['primary', 'onPrimary', 'primaryContainer', 'onPrimaryContainer', 'inversePrimary', 'secondary', 'onSecondary', 'secondaryContainer', 'onSecondaryContainer', 'tertiary', 'onTertiary', 'tertiaryContainer', 'onTertiaryContainer', 'error', 'onError', 'errorContainer', 'onErrorContainer', 'background', 'onBackground', 'surface', 'surfaceDim', 'surfaceBright', 'surfaceContainerLowest', 'surfaceContainerLow', 'surfaceContainer', 'surfaceContainerHigh', 'surfaceContainerHighest', 'onSurface', 'surfaceVariant', 'onSurfaceVariant', 'inverseSurface', 'inverseOnSurface', 'outline', 'outlineVariant', 'shadow', 'scrim', 'surfaceTint']; export const kebab = (s) => s.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()); const hex = (argb) => hexFromArgb(argb); // Contrast levels as Material Theme Builder has them; '' is standard (no data-contrast attribute). export const CONTRASTS = [['', 0], ['medium', 0.5], ['high', 1]]; // TonalSpot roles plus success/warning; `own` replaces the generated ROLES lines. // ponytail: success/warning use customColor, which has no contrast level (same tones at every level). function roles(seedHex, dark, level, own) { const seed = argbFromHex(seedHex); const scheme = new SchemeTonalSpot(Hct.fromInt(seed), dark, level); const lines = own || ROLES.map((r) => `--md-sys-color-${kebab(r)}: ${hex(C[r].getArgb(scheme))};`); for (const [name, value] of CUSTOM) { const c = customColor(seed, { name, value: argbFromHex(value), blend: true })[dark ? 'dark' : 'light']; lines.push(`--md-sys-color-${name}: ${hex(c.color)};`, `--md-sys-color-on-${name}: ${hex(c.onColor)};`, `--md-sys-color-${name}-container: ${hex(c.colorContainer)};`, `--md-sys-color-on-${name}-container: ${hex(c.onColorContainer)};`); } return lines.join(' '); } // Light, forced dark and OS dark rules for one palette selector, per contrast level. // `own(dark, contrast)` may return role lines to use instead of generated ones. // Each contrast rule adds an attribute, so it beats the same palette's standard rules. export function paletteCss(sel, seedHex, own) { return CONTRASTS.map(([id, level]) => { const s = id ? `${sel}[data-contrast="${id}"]` : sel; const r = (dark) => roles(seedHex, dark, level, own && own(dark, id)); return `${s} { ${r(false)} } ${s}[data-mode="dark"] { ${r(true)} } @media (prefers-color-scheme: dark) { ${s}:not([data-mode="light"]) { ${r(true)} } } `; }).join(''); }