|
| 1 | +import { execFileSync } from 'node:child_process' |
| 2 | +import { readFileSync, writeFileSync } from 'node:fs' |
| 3 | +import { createRequire } from 'node:module' |
| 4 | +import { pathToFileURL } from 'node:url' |
| 5 | + |
| 6 | +const require = createRequire(import.meta.resolve('@pierre/diffs')) |
| 7 | + |
| 8 | +const IDS = [ |
| 9 | + 'pierre-dark-vibrant', |
| 10 | + 'pierre-dark-protanopia-deuteranopia', |
| 11 | + 'pierre-dark-tritanopia', |
| 12 | +] |
| 13 | + |
| 14 | +const ACHROMATOPSIA = 'achromatopsia' |
| 15 | + |
| 16 | +const ACCENTS = { |
| 17 | + primary: 'button.background', |
| 18 | + secondary: 'gitDecoration.conflictingResourceForeground', |
| 19 | + error: 'gitDecoration.deletedResourceForeground', |
| 20 | + info: 'gitDecoration.modifiedResourceForeground', |
| 21 | + success: 'gitDecoration.addedResourceForeground', |
| 22 | + warning: 'notificationsWarningIcon.foreground', |
| 23 | +} |
| 24 | + |
| 25 | +const OVERRIDES = { |
| 26 | + 'pierre-dark-vibrant': { primary: 'oklch(0.811 0.17 293.6)' }, |
| 27 | + 'pierre-dark-protanopia-deuteranopia': { primary: '#c4b5fd' }, |
| 28 | + 'pierre-dark-tritanopia': { primary: 'oklch(0.811 0.101 195)', info: 'oklch(0.62 0.205 276)' }, |
| 29 | + [ACHROMATOPSIA]: { primary: '#c4b5fd' }, |
| 30 | +} |
| 31 | + |
| 32 | +function channels (color) { |
| 33 | + const p3 = color.match(/color\(display-p3\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\)/) |
| 34 | + if (p3) return [+p3[1], +p3[2], +p3[3]] |
| 35 | + const hex = color.replace('#', '') |
| 36 | + const pairs = hex.length === 3 ? [...hex].map(c => c + c) : hex.match(/../g) |
| 37 | + return pairs.slice(0, 3).map(p => Number.parseInt(p, 16) / 255) |
| 38 | +} |
| 39 | + |
| 40 | +function linear (color) { |
| 41 | + return channels(color).map(v => (v <= 0.040_45 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4)) |
| 42 | +} |
| 43 | + |
| 44 | +function luminance (color) { |
| 45 | + const oklch = color.match(/oklch\(([\d.]+)/) |
| 46 | + if (oklch) return Number(oklch[1]) ** 3 |
| 47 | + const [r, g, b] = linear(color) |
| 48 | + return 0.2126 * r + 0.7152 * g + 0.0722 * b |
| 49 | +} |
| 50 | + |
| 51 | +/** Oklab's L, i.e. the lightness that survives when the chroma is dropped. */ |
| 52 | +function lightness (color) { |
| 53 | + const oklch = color.match(/oklch\(([\d.]+)/) |
| 54 | + if (oklch) return Number(oklch[1]) |
| 55 | + const [r, g, b] = linear(color) |
| 56 | + const l = Math.cbrt(0.412_221_470_8 * r + 0.536_332_536_3 * g + 0.051_445_992_9 * b) |
| 57 | + const m = Math.cbrt(0.211_903_498_2 * r + 0.680_699_545_1 * g + 0.107_396_956_6 * b) |
| 58 | + const s = Math.cbrt(0.088_302_461_9 * r + 0.281_718_837_6 * g + 0.629_978_700_5 * b) |
| 59 | + return 0.210_454_255_3 * l + 0.793_617_785 * m - 0.004_072_046_8 * s |
| 60 | +} |
| 61 | + |
| 62 | +/** |
| 63 | + * Achromatopsia sees no hue at all, so every color collapses to its own |
| 64 | + * lightness. Additions and deletions end up near-identical grays — the diff |
| 65 | + * switches to Pierre's literal +/- indicators to carry that signal instead. |
| 66 | + */ |
| 67 | +function achromatic (record) { |
| 68 | + const colors = Object.fromEntries( |
| 69 | + Object.entries(record.colors).map(([key, value]) => [key, `oklch(${lightness(value).toFixed(3)} 0 0)`]), |
| 70 | + ) |
| 71 | + // Graying moves an accent's lightness, so its foreground has to be picked |
| 72 | + // again against the gray rather than inherited from the color it replaced. |
| 73 | + for (const name of Object.keys(ACCENTS)) { |
| 74 | + colors[`on-${name}`] = lightness(colors[name]) > 0.55 ? colors.background : colors['on-background'] |
| 75 | + } |
| 76 | + return { dark: record.dark, colors } |
| 77 | +} |
| 78 | + |
| 79 | +function toV0 (theme, id) { |
| 80 | + const c = theme.colors |
| 81 | + const bg = c['editor.background'] |
| 82 | + const fg = c['editor.foreground'] |
| 83 | + // Foreground for text sitting on an accent fill: whichever of the theme's own |
| 84 | + // extremes contrasts with it. |
| 85 | + const on = accent => (luminance(accent) > 0.36 ? bg : fg) |
| 86 | + |
| 87 | + const accents = Object.fromEntries(Object.entries(ACCENTS).map(([name, key]) => [name, c[key]])) |
| 88 | + Object.assign(accents, OVERRIDES[id]) |
| 89 | + |
| 90 | + return { |
| 91 | + dark: theme.type === 'dark', |
| 92 | + colors: { |
| 93 | + ...accents, |
| 94 | + 'background': bg, |
| 95 | + 'surface': c['sideBar.background'], |
| 96 | + 'surface-tint': c['input.background'], |
| 97 | + 'surface-variant': c['editorIndentGuide.activeBackground'], |
| 98 | + 'divider': c['editorLineNumber.foreground'], |
| 99 | + ...Object.fromEntries(Object.entries(accents).map(([k, v]) => [`on-${k}`, on(v)])), |
| 100 | + 'on-background': fg, |
| 101 | + 'on-surface': fg, |
| 102 | + 'on-surface-variant': c['sideBar.foreground'], |
| 103 | + }, |
| 104 | + } |
| 105 | +} |
| 106 | + |
| 107 | +const load = id => require(`@pierre/theme/themes/${id}.json`) |
| 108 | + |
| 109 | +const themes = { |
| 110 | + ...Object.fromEntries(IDS.map(id => [id, toV0(load(id), id)])), |
| 111 | + [ACHROMATOPSIA]: achromatic(toV0(load('pierre-dark'), ACHROMATOPSIA)), |
| 112 | +} |
| 113 | + |
| 114 | +const manifest = new URL('../package.json', pathToFileURL(require.resolve('@pierre/theme/themes/pierre-dark.json'))) |
| 115 | +const { version } = JSON.parse(readFileSync(manifest, 'utf8')) |
| 116 | + |
| 117 | +const source = `/** |
| 118 | + * Pierre's editor themes mapped onto v0's palette — the colorblind-safe variants |
| 119 | + * matter most for reading a diff. Generated from @pierre/theme@${version} by |
| 120 | + * \`node scripts/gen-themes.mjs\`; edit that script, not this file. |
| 121 | + */ |
| 122 | +
|
| 123 | +import type { ThemeRecord } from '@vuetify/v0' |
| 124 | +
|
| 125 | +export const pierreThemes: Record<string, ThemeRecord> = ${JSON.stringify(themes, null, 2)} |
| 126 | +` |
| 127 | + |
| 128 | +const out = new URL('../src/lib/pierre-themes.ts', import.meta.url) |
| 129 | +writeFileSync(out, source) |
| 130 | +execFileSync('pnpm', ['lint:fix', 'src/lib/pierre-themes.ts'], { stdio: 'inherit' }) |
| 131 | +console.log(`wrote ${Object.keys(themes).length} themes to ${out.pathname}`) |
0 commit comments