99 lines
3.9 KiB
JavaScript
99 lines
3.9 KiB
JavaScript
import { readFileSync } from 'node:fs'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { hexToRgb, stripAllComments } from './lib/css-utils.mjs'
|
|
|
|
// Cross-surface brand-accent guard.
|
|
//
|
|
// The product ships several independently-authored surfaces (the Vue console,
|
|
// the Electron splash, the Electron onboarding window). Issue #403 found the
|
|
// brand accent had fragmented into six different oranges across them. The Vue
|
|
// console is already token-guarded by check-webui-colors.mjs; this guard keeps
|
|
// the DESKTOP launch sequence (splash + onboarding) locked to the one canonical
|
|
// "strike" accent so it can't drift off again.
|
|
//
|
|
// Scope: only a SATURATED ORANGE (a brand-accent candidate) is checked — a hue
|
|
// in the orange band with real saturation. Danger-reds, greens, warm neutrals
|
|
// and paper whites are ignored, so the guard is about brand identity, not every
|
|
// colour. Backgrounds are intentionally out of scope (too many valid neutral
|
|
// shades to allowlist without false positives).
|
|
//
|
|
// The legacy gateway frontend (src/opensquilla/gateway/static/css) is NOT scanned
|
|
// here — its status (resync vs. freeze) is a separate, still-open decision.
|
|
const repoRoot = fileURLToPath(new URL('../../', import.meta.url))
|
|
|
|
// The canonical "strike" family — the Instrument accent and its documented
|
|
// hover / deep / secondary / light-theme siblings. Stored as normalized
|
|
// "r,g,b" so hex and rgb()/rgba() forms compare equal.
|
|
const CANONICAL = new Set([
|
|
'242,106,27', // #F26A1B accent (dark)
|
|
'255,122,46', // #FF7A2E accent-hover (dark)
|
|
'217,90,17', // #D95A11 accent-deep (dark) / onboarding hover
|
|
'255,138,76', // #FF8A4C accent-secondary
|
|
'186,77,15', // #BA4D0F accent (light)
|
|
'165,68,12', // #A5440C accent-hover (light)
|
|
'142,58,10', // #8E3A0A accent-deep (light)
|
|
'182,80,28', // #B6501C accent-secondary (light)
|
|
])
|
|
|
|
// Files that make up the desktop launch sequence.
|
|
const targets = [
|
|
'desktop/electron/src/boot.html',
|
|
'desktop/electron/src/main.ts',
|
|
]
|
|
|
|
// Is this rgb a saturated orange — i.e. a brand-accent candidate?
|
|
function isBrandOrange([r, g, b]) {
|
|
const rn = r / 255, gn = g / 255, bn = b / 255
|
|
const max = Math.max(rn, gn, bn), min = Math.min(rn, gn, bn)
|
|
const l = (max + min) / 2
|
|
const d = max - min
|
|
if (d === 0) return false
|
|
const s = d / (1 - Math.abs(2 * l - 1))
|
|
let hue
|
|
if (max === rn) hue = ((gn - bn) / d) % 6
|
|
else if (max === gn) hue = (bn - rn) / d + 2
|
|
else hue = (rn - gn) / d + 4
|
|
hue = ((hue * 60) + 360) % 360
|
|
// Orange band, well saturated, mid lightness — excludes red danger (<16),
|
|
// yellow (>46), and low-saturation warm taupes/papers.
|
|
return hue >= 16 && hue <= 46 && s >= 0.4 && l >= 0.18 && l <= 0.72
|
|
}
|
|
|
|
const hexRe = /#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b/g
|
|
const rgbRe = /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})/g
|
|
|
|
const failures = []
|
|
for (const rel of targets) {
|
|
let text
|
|
try {
|
|
text = readFileSync(repoRoot + rel, 'utf8')
|
|
} catch {
|
|
console.warn(`[cross-surface] skipped (not found): ${rel}`)
|
|
continue
|
|
}
|
|
// Comments are stripped so hexes named in them don't trip the guard.
|
|
const lines = stripAllComments(text).split('\n')
|
|
lines.forEach((line, i) => {
|
|
const found = []
|
|
for (const m of line.matchAll(hexRe)) found.push({ raw: m[0], rgb: hexToRgb(m[0]) })
|
|
for (const m of line.matchAll(rgbRe)) found.push({ raw: m[0], rgb: [+m[1], +m[2], +m[3]] })
|
|
for (const { raw, rgb } of found) {
|
|
if (!isBrandOrange(rgb)) continue
|
|
if (CANONICAL.has(rgb.join(','))) continue
|
|
failures.push(
|
|
`${rel}:${i + 1}: off-canonical brand orange ${raw} (rgb ${rgb.join(',')}); use the strike accent (#F26A1B family).`,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
console.error(
|
|
`Cross-surface accent guard: ${failures.length} off-canonical brand orange(s) — the desktop launch sequence must use the strike accent:\n` +
|
|
failures.join('\n'),
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log('Cross-surface accent guard passed.')
|