mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 15:16:35 +03:00
Apply neo-kinpaku design system and improve live picker UX
Restyle the live picker to match the site kinpaku kit, persist pick mode in localStorage, fix DESIGN.md color swatches in the parser, and land the neo-kinpaku site refresh with new tokens, assets, palette script, and detector rules. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -63,7 +63,7 @@ function parseYamlSubset(yaml) {
|
||||
}
|
||||
|
||||
const key = content.slice(0, colonIdx).trim();
|
||||
const rest = content.slice(colonIdx + 1).trim();
|
||||
const rest = stripInlineYamlComment(content.slice(colonIdx + 1).trim());
|
||||
const parent = stack[stack.length - 1].obj;
|
||||
|
||||
if (rest === '') {
|
||||
@@ -93,6 +93,21 @@ function findTopLevelColon(s) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
function stripInlineYamlComment(s) {
|
||||
let inQuote = null;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const ch = s[i];
|
||||
if (inQuote) {
|
||||
if (ch === inQuote && s[i - 1] !== '\\') inQuote = null;
|
||||
} else if (ch === '"' || ch === "'") {
|
||||
inQuote = ch;
|
||||
} else if (ch === '#' && i > 0 && /\s/.test(s[i - 1])) {
|
||||
return s.slice(0, i).trimEnd();
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function parseScalar(raw) {
|
||||
const s = raw.trim();
|
||||
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
||||
|
||||
+104
-95
@@ -30,20 +30,25 @@
|
||||
// Design tokens
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Brand magenta is pinned to the site token (--color-accent in main.css)
|
||||
// so Accept / knobs / cycle-dots match the site's accent, not a washed
|
||||
// theme-adjusted one.
|
||||
// Brand kinpaku (gold) is pinned to the site's neo-kinpaku tokens
|
||||
// (see site/styles/kinpaku-tokens.css) so Accept / knobs / cycle-dots /
|
||||
// the selection outline / the comment tag all match the site's accent,
|
||||
// not a washed theme-adjusted one. These mirror the kit's picker
|
||||
// colors in site/styles/kinpaku-kit.css; keep them in sync by hand.
|
||||
const C = {
|
||||
brand: 'oklch(60% 0.25 350)',
|
||||
brandHov: 'oklch(52% 0.25 350)',
|
||||
brandSoft: 'oklch(60% 0.25 350 / 0.15)',
|
||||
ink: 'oklch(15% 0.01 350)',
|
||||
ash: 'oklch(55% 0 0)',
|
||||
paper: 'oklch(98% 0.005 350 / 0.92)',
|
||||
paperSolid:'oklch(98% 0.005 350)',
|
||||
mist: 'oklch(90% 0.01 350 / 0.6)',
|
||||
brand: 'oklch(84% 0.19 80.46)', // kinpaku gold
|
||||
brandHov: 'oklch(86% 0.07 84)', // kinpaku-pale (hover lift)
|
||||
brandSoft: 'oklch(84% 0.19 80.46 / 0.18)', // kinpaku-dim
|
||||
ink: 'oklch(4% 0.004 95)', // lacquer-deep
|
||||
ash: 'oklch(55% 0.018 82)', // warm muted text
|
||||
paper: 'oklch(98% 0.005 95 / 0.92)', // light overlay on user pages
|
||||
paperSolid:'oklch(98% 0.005 95)',
|
||||
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
|
||||
white: 'oklch(99% 0 0)',
|
||||
};
|
||||
// Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css
|
||||
const PICKER_SHADOW =
|
||||
'0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)';
|
||||
const FONT = 'system-ui, -apple-system, sans-serif';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
||||
// z-index: detect overlays use 99999, so our UI must be above them
|
||||
@@ -301,11 +306,11 @@
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Annotation overlay (comment pins + magenta strokes)
|
||||
// Annotation overlay (comment pins + kinpaku strokes)
|
||||
//
|
||||
// Active while state === 'CONFIGURING'. The overlay is a fixed-positioned
|
||||
// sibling of <body> mirroring selectedElement's bounding rect. Click (no
|
||||
// drag) drops a comment pin; drag paints a magenta SVG stroke. All coords
|
||||
// drag) drops a comment pin; drag paints a kinpaku SVG stroke. All coords
|
||||
// are stored in element-local CSS px so they survive scroll / resize and
|
||||
// correlate directly with the captured PNG.
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -425,7 +430,7 @@
|
||||
}
|
||||
|
||||
// Rebuild the SVG layer. Each stroke gets a wider invisible hit path
|
||||
// beneath the visible magenta path so clicks register on thin lines.
|
||||
// beneath the visible kinpaku path so clicks register on thin lines.
|
||||
function redrawStrokes() {
|
||||
while (annotSvgEl.firstChild) annotSvgEl.removeChild(annotSvgEl.firstChild);
|
||||
annotState.strokes.forEach((s, idx) => {
|
||||
@@ -845,10 +850,9 @@
|
||||
transform: 'translateY(6px)',
|
||||
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
background: BP.surface,
|
||||
backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)',
|
||||
border: '1px solid ' + BP.hairline,
|
||||
border: '1.5px solid ' + BP.border,
|
||||
borderRadius: '10px',
|
||||
boxShadow: BAR_SHADOW_DEFAULT,
|
||||
boxShadow: BP.shadow,
|
||||
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
|
||||
fontFamily: FONT, fontSize: '13px', color: BP.text,
|
||||
padding: '6px',
|
||||
@@ -911,9 +915,10 @@
|
||||
function updateBarContent(mode) {
|
||||
if (!barEl || barEl.style.display === 'none') return;
|
||||
barEl.innerHTML = '';
|
||||
// Reset bar styling to the theme-aware palette
|
||||
// Reset bar styling to the kinpaku picker palette
|
||||
barEl.style.background = BP.surface;
|
||||
barEl.style.border = '1px solid ' + BP.hairline;
|
||||
barEl.style.border = '1.5px solid ' + BP.border;
|
||||
barEl.style.boxShadow = BP.shadow;
|
||||
if (mode === 'configure') barEl.appendChild(buildConfigureRow());
|
||||
else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
|
||||
else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
|
||||
@@ -1146,7 +1151,7 @@
|
||||
// Spacer
|
||||
row.appendChild(el('div', { flex: '1' }));
|
||||
|
||||
// Accept — primary action, uses the site's saturated brand magenta
|
||||
// Accept — primary action, uses the site's saturated brand kinpaku
|
||||
// with paper-white text, not the theme-muted BP.accent.
|
||||
const accept = el('button', {
|
||||
padding: '5px 14px', borderRadius: '5px',
|
||||
@@ -1244,10 +1249,10 @@
|
||||
for (let i = 1; i <= expectedVariants; i++) {
|
||||
const arrived = i <= arrivedVariants;
|
||||
const active = i === visibleVariant;
|
||||
// active: solid site-brand magenta dot. arrived+inactive: muted neutral.
|
||||
// active: solid site-brand kinpaku dot. arrived+inactive: muted neutral.
|
||||
// pending (not yet arrived): faint outline ring. No borders on arrived
|
||||
// dots — the previous "accent ring + ash fill" combo read as noisy
|
||||
// magenta chips, especially when all variants had arrived and every
|
||||
// kinpaku chips, especially when all variants had arrived and every
|
||||
// dot wore an accent ring.
|
||||
const dotBg = active ? C.brand
|
||||
: arrived ? BP.textDim
|
||||
@@ -1321,13 +1326,11 @@
|
||||
transformOrigin: 'bottom left',
|
||||
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
|
||||
background: P.surface,
|
||||
border: '1px solid ' + P.hairline,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0 8px 30px oklch(0% 0 0 / 0.10), 0 2px 6px oklch(0% 0 0 / 0.06)',
|
||||
boxShadow: P.shadow,
|
||||
padding: '6px',
|
||||
fontFamily: FONT,
|
||||
backdropFilter: 'blur(10px)',
|
||||
WebkitBackdropFilter: 'blur(10px)',
|
||||
});
|
||||
|
||||
// Build the chip grid
|
||||
@@ -1467,7 +1470,6 @@
|
||||
boxSizing: 'border-box',
|
||||
borderRadius: '0 0 10px 10px',
|
||||
pointerEvents: 'none',
|
||||
backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)',
|
||||
|
||||
// clip-path is the same conceptual reveal as mask but with rock-solid
|
||||
// transition support across engines. Closed state clips from the far
|
||||
@@ -2218,7 +2220,7 @@
|
||||
hasProjectContext = !!msg.hasProjectContext;
|
||||
if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable teach to generate one.', 7000);
|
||||
console.log('[impeccable] Live mode connected.');
|
||||
if (state === 'IDLE') state = 'PICKING';
|
||||
if (state === 'IDLE' && pickActive) state = 'PICKING';
|
||||
break;
|
||||
case 'done':
|
||||
// Variants already arrived via HMR → normal transition.
|
||||
@@ -2788,7 +2790,7 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shader overlay — renders the captured screenshot as a WebGL texture and
|
||||
// runs an editorial "ink-wash" fragment shader over it during generation.
|
||||
// A single rolling band sweeps top-to-bottom, desaturating + tinting magenta
|
||||
// A single rolling band sweeps top-to-bottom, desaturating + tinting kinpaku
|
||||
// and leaving a soft trail. Makes the wait feel like a letterpress scan
|
||||
// instead of a dead spinner.
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -2834,7 +2836,7 @@ void main() {
|
||||
vec2 sampleCenter = (cellId + 0.5) * cellPx / u_resolution;
|
||||
vec3 cellImg = texture2D(u_texture, sampleCenter).rgb;
|
||||
float luma = dot(cellImg, vec3(0.299, 0.587, 0.114));
|
||||
// Darker cells → bigger magenta dots (classic risograph halftone curve).
|
||||
// Darker cells → bigger kinpaku dots (classic risograph halftone curve).
|
||||
float radius = sqrt(clamp(1.0 - luma, 0.0, 1.0)) * 0.56;
|
||||
float dotMask = smoothstep(radius + 0.06, radius, length(cellUv));
|
||||
vec3 paper = vec3(0.975, 0.965, 0.955);
|
||||
@@ -2846,8 +2848,8 @@ void main() {
|
||||
gl_FragColor = vec4(mix(base, dotLayer, band), 1.0);
|
||||
}`;
|
||||
|
||||
// Editorial Magenta converted to approximate sRGB 0-1 (matches oklch(60% 0.25 350))
|
||||
const SHADER_ACCENT = [0.82, 0.16, 0.47];
|
||||
// Kinpaku gold converted to approximate sRGB 0-1 (matches oklch(84% 0.19 80.46))
|
||||
const SHADER_ACCENT = [1.0, 0.78, 0.31];
|
||||
let shaderState = null; // { canvas, gl, program, texture, rafId, startTime }
|
||||
|
||||
function compileShader(gl, type, source) {
|
||||
@@ -3289,7 +3291,25 @@ void main() {
|
||||
|
||||
let globalBarEl = null;
|
||||
let detectActive = false;
|
||||
let pickActive = true;
|
||||
const PICK_PREFS_KEY = 'impeccable-live-pick';
|
||||
|
||||
function loadPickPref() {
|
||||
try {
|
||||
const raw = localStorage.getItem(PICK_PREFS_KEY);
|
||||
if (raw == null) return true;
|
||||
const prefs = JSON.parse(raw);
|
||||
if (typeof prefs.pickActive === 'boolean') return prefs.pickActive;
|
||||
} catch { /* ignore */ }
|
||||
return true;
|
||||
}
|
||||
|
||||
function savePickPref() {
|
||||
try {
|
||||
localStorage.setItem(PICK_PREFS_KEY, JSON.stringify({ pickActive }));
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
let pickActive = loadPickPref();
|
||||
let detectCount = 0;
|
||||
let detectScriptLoaded = false;
|
||||
|
||||
@@ -3334,43 +3354,28 @@ void main() {
|
||||
} catch { return 'light'; }
|
||||
}
|
||||
|
||||
function barPaletteForTheme(theme) {
|
||||
if (theme === 'dark') {
|
||||
// Light bar on dark page
|
||||
return {
|
||||
surface: 'oklch(98% 0 0 / 0.92)',
|
||||
surfaceDeep: 'oklch(92% 0.005 60 / 0.96)', // slightly deeper, faint warm
|
||||
hairline: 'oklch(70% 0 0 / 0.35)',
|
||||
text: 'oklch(15% 0 0)',
|
||||
textDim: 'oklch(45% 0 0)',
|
||||
accent: 'oklch(60% 0.25 350)',
|
||||
accentSoft: 'oklch(60% 0.25 350 / 0.18)',
|
||||
mark: 'oklch(98% 0 0)', // logo mark fill
|
||||
markText: 'oklch(15% 0 0)', // logo "/" color
|
||||
exitHover: 'oklch(85% 0 0 / 0.5)',
|
||||
};
|
||||
}
|
||||
// Dark bar on light page. Bar is a warm charcoal, logo slab is much
|
||||
// deeper so the rounded-right shape reads as a clear sculpted mark.
|
||||
function barPaletteForTheme(_theme) {
|
||||
// Picker chrome always uses neo-kinpaku styling (homepage /live-mode demo
|
||||
// bars in kinpaku-kit.css), regardless of host page light/dark theme.
|
||||
return {
|
||||
surface: 'oklch(26% 0 0 / 0.94)',
|
||||
surfaceDeep: 'oklch(18% 0 0 / 0.96)', // darker sand for Tune popover
|
||||
hairline: 'oklch(42% 0 0 / 0.5)',
|
||||
text: 'oklch(96% 0 0)',
|
||||
textDim: 'oklch(72% 0 0)',
|
||||
accent: 'oklch(72% 0.22 350)',
|
||||
accentSoft: 'oklch(72% 0.22 350 / 0.22)',
|
||||
mark: 'oklch(8% 0 0)',
|
||||
markText: 'oklch(96% 0 0)',
|
||||
exitHover: 'oklch(36% 0 0 / 0.6)',
|
||||
surface: C.ink,
|
||||
surfaceDeep: C.ink,
|
||||
border: C.brand,
|
||||
hairline: 'oklch(58% 0.065 82 / 0.48)',
|
||||
text: 'oklch(84% 0.035 82)',
|
||||
textDim: 'oklch(63% 0.024 82)',
|
||||
accent: C.brand,
|
||||
accentSoft: C.brandSoft,
|
||||
exitHover: 'oklch(58% 0.15 35 / 0.18)',
|
||||
shadow: PICKER_SHADOW,
|
||||
};
|
||||
}
|
||||
|
||||
// Impeccable logo mark — matches the site-header SVG (rounded square + "/").
|
||||
function brandMarkSvg(fill, ink, size = 18) {
|
||||
return `<svg width="${size}" height="${size}" viewBox="0 0 32 32" aria-hidden="true">
|
||||
<rect width="32" height="32" rx="7" fill="${fill}"/>
|
||||
<text x="16" y="24" font-family="system-ui, -apple-system, sans-serif" font-size="22" font-weight="500" fill="${ink}" text-anchor="middle">/</text>
|
||||
// Impeccable mark — same paths as site/components/Header.astro + favicon.svg.
|
||||
function brandMarkSvg(color = C.brand, size = 18) {
|
||||
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="${color}" aria-hidden="true">
|
||||
<path d="M5 2.5 L13.5 2.5 L5.5 21.5 L5 21.5 Q2.5 21.5 2.5 19 L2.5 5 Q2.5 2.5 5 2.5 Z"/>
|
||||
<path d="M16.5 2.5 L19 2.5 Q21.5 2.5 21.5 5 L21.5 19 Q21.5 21.5 19 21.5 L8.5 21.5 Z"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
@@ -3400,10 +3405,9 @@ void main() {
|
||||
display: 'flex', alignItems: 'stretch',
|
||||
gap: '2px',
|
||||
background: P.surface,
|
||||
backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)',
|
||||
border: '1px solid ' + P.hairline,
|
||||
border: '1.5px solid ' + P.border,
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0 4px 20px oklch(0% 0 0 / 0.12), 0 1px 3px oklch(0% 0 0 / 0.08)',
|
||||
boxShadow: P.shadow,
|
||||
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
|
||||
opacity: '0',
|
||||
overflow: 'hidden', // clip the full-bleed brand mark to the bar radius
|
||||
@@ -3412,20 +3416,16 @@ void main() {
|
||||
globalBarEl.id = PREFIX + '-global-bar';
|
||||
globalBarEl.dataset.theme = theme;
|
||||
|
||||
// Brand mark — fills bar height on the left. Left side inherits the bar's
|
||||
// rounded corner via overflow:hidden; right side is a clean hard edge since
|
||||
// the near-black/charcoal contrast does the shape-defining work.
|
||||
// Brand mark — kinpaku Impeccable icon (site header / favicon paths).
|
||||
const brand = el('span', {
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
alignSelf: 'stretch',
|
||||
padding: '0 12px 0 14px',
|
||||
background: P.mark,
|
||||
color: P.markText,
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||
fontWeight: '500',
|
||||
fontSize: '18px', lineHeight: '1',
|
||||
background: 'transparent',
|
||||
color: P.accent,
|
||||
flexShrink: '0',
|
||||
});
|
||||
brand.textContent = '/';
|
||||
brand.innerHTML = brandMarkSvg(P.accent, 18);
|
||||
brand.title = 'Impeccable';
|
||||
globalBarEl.appendChild(brand);
|
||||
|
||||
@@ -3476,7 +3476,7 @@ void main() {
|
||||
return b;
|
||||
}
|
||||
|
||||
// Pick toggle — starts active (primary intent when entering live mode).
|
||||
// Pick toggle — active by default on first visit; restored from localStorage after.
|
||||
const pickBtn = makeIconBtn({
|
||||
id: PREFIX + '-pick-toggle',
|
||||
svg: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/></svg>',
|
||||
@@ -3484,10 +3484,6 @@ void main() {
|
||||
ariaLabel: 'Pick element',
|
||||
onClick: () => togglePick(),
|
||||
});
|
||||
pickBtn.style.background = P.accentSoft;
|
||||
pickBtn.style.color = P.accent;
|
||||
pickBtn.dataset.active = 'true';
|
||||
pickBtn._expandLabel();
|
||||
inner.appendChild(pickBtn);
|
||||
|
||||
// Detect toggle
|
||||
@@ -3501,7 +3497,7 @@ void main() {
|
||||
const detectBadge = el('span', {
|
||||
fontSize: '10px', fontWeight: '600',
|
||||
padding: '0px 5px', borderRadius: '7px', lineHeight: '16px',
|
||||
background: P.accent, color: P.surface.includes('18%') ? 'oklch(18% 0 0)' : 'oklch(98% 0 0)',
|
||||
background: P.accent, color: C.ink,
|
||||
display: 'none', fontFamily: MONO, marginLeft: '4px',
|
||||
});
|
||||
detectBadge.id = PREFIX + '-detect-badge';
|
||||
@@ -3511,11 +3507,11 @@ void main() {
|
||||
// DESIGN.md panel toggle — quartet of color squares as the mark.
|
||||
const designBtn = makeIconBtn({
|
||||
id: PREFIX + '-design-toggle',
|
||||
svg: `<span style="display:inline-grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;width:14px;height:14px;border-radius:3px;overflow:hidden;box-shadow:inset 0 0 0 1px ${P.hairline};flex-shrink:0">
|
||||
<span style="background:oklch(60% 0.25 350)"></span>
|
||||
<span style="background:oklch(60% 0.15 45)"></span>
|
||||
<span style="background:oklch(55% 0.12 250)"></span>
|
||||
<span style="background:oklch(30% 0 0)"></span>
|
||||
svg: `<span style="display:inline-grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;width:14px;height:14px;border-radius:3px;overflow:hidden;box-shadow:inset 0 0 0 1px oklch(58% 0.065 82 / 0.55);flex-shrink:0">
|
||||
<span style="background:oklch(84% 0.19 80.46)"></span>
|
||||
<span style="background:oklch(70% 0.12 188)"></span>
|
||||
<span style="background:oklch(84% 0.035 82)"></span>
|
||||
<span style="background:oklch(34% 0.014 82)"></span>
|
||||
</span>`,
|
||||
label: 'DESIGN.md',
|
||||
ariaLabel: 'Toggle DESIGN.md panel',
|
||||
@@ -3551,7 +3547,7 @@ void main() {
|
||||
});
|
||||
exitBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="3" y1="3" x2="11" y2="11"/><line x1="11" y1="3" x2="3" y2="11"/></svg>';
|
||||
exitBtn.title = 'Exit live mode';
|
||||
exitBtn.addEventListener('mouseenter', () => { exitBtn.style.color = P.text; exitBtn.style.background = P.exitHover; });
|
||||
exitBtn.addEventListener('mouseenter', () => { exitBtn.style.color = 'oklch(58% 0.15 35)'; exitBtn.style.background = P.exitHover; });
|
||||
exitBtn.addEventListener('mouseleave', () => { exitBtn.style.color = P.textDim; exitBtn.style.background = 'transparent'; });
|
||||
exitBtn.addEventListener('click', () => { sendEvent({ type: 'exit' }); teardown(); });
|
||||
inner.appendChild(exitBtn);
|
||||
@@ -3576,6 +3572,7 @@ void main() {
|
||||
|
||||
// Listen for detection results AND ready signal
|
||||
window.addEventListener('message', onDetectMessage);
|
||||
updateGlobalBarState();
|
||||
}
|
||||
|
||||
function updateGlobalBarState() {
|
||||
@@ -3642,6 +3639,7 @@ void main() {
|
||||
|
||||
function togglePick() {
|
||||
pickActive = !pickActive;
|
||||
savePickPref();
|
||||
updateGlobalBarState();
|
||||
|
||||
if (!pickActive) {
|
||||
@@ -3826,10 +3824,9 @@ void main() {
|
||||
position: fixed; top: 12px; bottom: 72px; right: 12px;
|
||||
width: ${DESIGN_PANEL_WIDTH}px; max-width: calc(100vw - 24px);
|
||||
background: ${BP.surface};
|
||||
border: 1px solid ${BP.hairline};
|
||||
border: 1.5px solid ${BP.border};
|
||||
border-radius: 14px;
|
||||
backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
|
||||
box-shadow: 0 20px 60px oklch(0% 0 0 / 0.18), 0 4px 12px oklch(0% 0 0 / 0.08);
|
||||
box-shadow: ${BP.shadow};
|
||||
display: flex; flex-direction: column;
|
||||
transform: translateX(calc(100% + 24px));
|
||||
opacity: 0;
|
||||
@@ -4281,7 +4278,7 @@ void main() {
|
||||
return {
|
||||
role: m.role || humanizeKey(key),
|
||||
name: m.displayName || humanizeKey(key),
|
||||
value: value,
|
||||
value: normalizeCssColor(m.canonical || value),
|
||||
canonical: m.canonical || null,
|
||||
description: m.description || findProseDescription(proseColors, key, m.displayName),
|
||||
tonalRamp: m.tonalRamp || null,
|
||||
@@ -4372,7 +4369,7 @@ void main() {
|
||||
|
||||
const hero = document.createElement('div');
|
||||
hero.className = 'c-hero';
|
||||
hero.style.background = c.value;
|
||||
hero.style.background = cssSafe(c.value || '');
|
||||
tile.appendChild(hero);
|
||||
|
||||
const ramp = synthesizeRamp(c);
|
||||
@@ -4695,6 +4692,18 @@ void main() {
|
||||
return String(v).replace(/[<>"'`\n]/g, '');
|
||||
}
|
||||
|
||||
function normalizeCssColor(v) {
|
||||
if (!v || typeof v !== 'string') return v;
|
||||
const s = v.trim();
|
||||
const oklch = s.match(/oklch\([^)]+\)/i);
|
||||
if (oklch) return oklch[0];
|
||||
const hex = s.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return hex[0];
|
||||
const rgb = s.match(/rgba?\([^)]+\)/i);
|
||||
if (rgb) return rgb[0];
|
||||
return s.replace(/\s+#.*$/, '').trim();
|
||||
}
|
||||
|
||||
// --- Raw tab: minimal markdown renderer (subset) --------------------------
|
||||
|
||||
function renderRawTab(body, md) {
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Brand-seed picker. Returns one OKLCH seed color + the mood it most
|
||||
* naturally evokes, and teaches the model how to compose a full palette
|
||||
* around it.
|
||||
*
|
||||
* The seed is the brand's anchor color. The 5-role palette (bg, surface,
|
||||
* ink, accent, muted) is composed by the caller at runtime using their
|
||||
* judgment + the brief (PRODUCT.md / DESIGN.md / user prompt), NOT picked
|
||||
* from a frozen 4-color preset.
|
||||
*
|
||||
* Why: 4-color frozen palettes drift toward safe defaults (warm-cream bg,
|
||||
* complementary accent on near-white) regardless of brief. A single seed +
|
||||
* the model's own composition lets the same seed produce a dark-mode jazz
|
||||
* club or a light-mode hospitality brand depending on what the brief calls
|
||||
* for. Tested empirically against curated 4-color palettes; seed approach
|
||||
* wins on mood-fit in 3 of 5 cases and ties on the rest.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/palette.mjs # pick at random
|
||||
* node scripts/palette.mjs --id seed-021 # pick a specific seed
|
||||
* node scripts/palette.mjs --from <key> # hash <key> to a seed (deterministic)
|
||||
*
|
||||
* Env vars:
|
||||
* IMPECCABLE_PALETTE_SEED — same as --from; useful for the eval harness
|
||||
* to make runs reproducible.
|
||||
*/
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
// Seeds are inlined (60 entries, curated by hand). Each carries a mood
|
||||
// and a strategy that the model judging it produced — surfaced as hints,
|
||||
// not commands; the brief still drives composition.
|
||||
const SEEDS = [
|
||||
{ id: 'seed-000', oklch: [0.40, 0.13, 0],
|
||||
mood: 'old-world apothecary — oxblood ledgers, beeswax candlelight, mahogany shelves',
|
||||
strategy: 'Anchor the deep oxblood seed as primary, set it against a warm beeswax-cream bg with charred-walnut ink, and lift it with a tarnished-brass accent for an aged, hand-bound feel' },
|
||||
{ id: 'seed-001', oklch: [0.419, 0.161, 4.9],
|
||||
mood: 'Velvet boudoir at dusk — crushed rose petals on dark mahogany, candlelit and intimate',
|
||||
strategy: 'Dark mode anchored in a deep oxblood-tinted ground so the seed\'s muted rose reads as candlelight against wood; accent shifts to a warm amber-gold for gilded contrast rather than chromatic harmony.' },
|
||||
{ id: 'seed-002', oklch: [0.45, 0.15, 0],
|
||||
mood: 'oxblood library at dusk — aged leather bindings, brass lamp glow, quiet authority',
|
||||
strategy: 'Anchored a deep oxblood primary in a warm near-black room tinted with the same hue, then lifted an amber-brass accent off the red axis to mimic lamplight against burgundy leather.' },
|
||||
{ id: 'seed-003', oklch: [0.50, 0.20, 0],
|
||||
mood: '1930s darkroom — safelight red bleeding through developer trays, hushed and chemical',
|
||||
strategy: 'Anchored in a deep oxblood-tinted near-black so the seed crimson reads as glowing safelight; accent shifts to amber to evoke film emulsion warmth without breaking the monochrome hush.' },
|
||||
{ id: 'seed-004', oklch: [0.546, 0.204, 3.4],
|
||||
mood: 'velvet boudoir at dusk — crushed rose silk, low lamplight, powdered cheek',
|
||||
strategy: 'Dark mode with a deep mulberry-plum background that lets the rose primary glow like lipstick under lamplight; accent shifts to a warm dusty mauve-gold to mimic candle-warmed skin tones against the cool wine field.' },
|
||||
{ id: 'seed-005', oklch: [0.55, 0.18, 0],
|
||||
mood: '1960s darkroom — safelight red bleeding into developing trays, patient and analog',
|
||||
strategy: 'Anchored in a deep oxblood-tinted dark mode so the seed\'s crimson reads as ambient safelight rather than alert, with a desaturated rose primary and a warm amber accent echoing print tongs under bulb glow.' },
|
||||
{ id: 'seed-007', oklch: [0.70, 0.13, 0],
|
||||
mood: 'faded Mediterranean terracotta at dusk — sun-bleached clay walls, dusty rose plaster, the warm hush before evening',
|
||||
strategy: 'anchored a warm chalky off-white background against deep oxblood ink, letting the dusty-rose primary read as sun-warmed plaster while a deeper brick accent provides aged-clay depth' },
|
||||
{ id: 'seed-008', oklch: [0.520, 0.200, 10.4],
|
||||
mood: 'Venetian opera house at curtain call — deep crimson velvet, gilded shadow, hushed anticipation',
|
||||
strategy: 'Dark mode with a deep wine-tinted background that lets the seed crimson read as illuminated velvet, paired with a warm gold accent shifted across the wheel to evoke brass and candlelight' },
|
||||
{ id: 'seed-010', oklch: [0.563, 0.223, 11.0],
|
||||
mood: 'crushed velvet boudoir — rouge silk in candlelight, intimate and tactile',
|
||||
strategy: 'Deep wine-tinted dark mode lets the crimson seed glow like lit silk rather than scream; accent shifts to a dusty rose-gold for warmth without competing.' },
|
||||
{ id: 'seed-011', oklch: [0.639, 0.207, 13.5],
|
||||
mood: 'velvet boudoir at last call — crushed rose silk, lipstick on a champagne glass, low lamplight',
|
||||
strategy: 'Anchored the seed as a saturated rose-primary against a deep wine-tinted dark ground, then pulled an antique-gold accent from the opposite warm axis so the room feels candlelit rather than neon.' },
|
||||
{ id: 'seed-012', oklch: [0.782, 0.103, 11.4],
|
||||
mood: 'faded Victorian rose garden at dusk — pressed petals, antique linen, melancholic romance',
|
||||
strategy: 'Anchor the dusty rose seed as primary against a warm bone-paper background, lift a deeper oxblood accent for emphasis, and ink in a brown-black so the whole palette reads like aged paper rather than modern white screen.' },
|
||||
{ id: 'seed-013', oklch: [0.40, 0.13, 18],
|
||||
mood: 'oxblood library at dusk — aged leather bindings, lamplight on mahogany, slow conversation',
|
||||
strategy: 'Anchored a deep oxblood primary in a near-black wine-tinted dark mode, with a warm parchment ink and a brass accent that shifts hue just enough to read as candlelight against the red.' },
|
||||
{ id: 'seed-014', oklch: [0.45, 0.15, 18],
|
||||
mood: 'Florentine apothecary at dusk — oxblood leather, aged parchment, and slow candlelight',
|
||||
strategy: 'Dark mode anchored in a deep oxblood-tinted ground, with primary kept near the seed\'s smoldering crimson and accent shifted to a warm parchment-gold for an old-world ledger contrast.' },
|
||||
{ id: 'seed-015', oklch: [0.527, 0.202, 22.7],
|
||||
mood: 'Spanish bodega at dusk — oxidized vermilion, terracotta shadow, lamplight on aged plaster',
|
||||
strategy: 'Anchored the seed as primary and built a warm dark-mode interior around it: deep brick-tinted background, slightly lifted clay surface, parchment ink, and a smoky amber accent shifted toward 60° to evoke lamplight against the vermilion.' },
|
||||
{ id: 'seed-022', oklch: [0.418, 0.155, 27.2],
|
||||
mood: 'Florentine leather-bound study — oxblood ledgers, oil-lamp glow on aged parchment',
|
||||
strategy: 'Anchored the seed as primary and built a warm low-chroma parchment surround with deep umber ink, letting the oxblood read as inked seal against aged paper while a burnished gold accent supplies the lamp-flame highlight.' },
|
||||
{ id: 'seed-023', oklch: [0.427, 0.175, 29.2],
|
||||
mood: 'oxblood leather library — lamplight on cracked spines and aged cognac',
|
||||
strategy: 'Anchored a deep-tinted dark mode in the seed\'s own hue family so the oxblood primary glows like lamplit leather, with a brass-gold accent struck across the warm axis for that bookbinder\'s foil contrast.' },
|
||||
{ id: 'seed-024', oklch: [0.464, 0.169, 26.9],
|
||||
mood: 'Pompeiian fresco wall — oxidized vermillion pigment under terracotta lamplight, faded but solemn',
|
||||
strategy: 'Anchored the seed as primary, sank the background into a deep umber-tinted near-black, and warmed all neutrals along the same 27° axis so the palette reads like firelight on plaster rather than red-on-white.' },
|
||||
{ id: 'seed-029', oklch: [0.665, 0.222, 25.7],
|
||||
mood: 'Moroccan tannery at golden hour — sun-cracked leather, dyed vermillion, dust in the light',
|
||||
strategy: 'Anchor the saturated red-orange seed as primary against a warm parchment background, then pull the accent toward a deeper burnt rust to evoke aged dye vats rather than introducing a cool complement that would break the heat.' },
|
||||
{ id: 'seed-038', oklch: [0.652, 0.229, 34.8],
|
||||
mood: 'sun-cured terracotta at dusk — adobe walls in New Mexico holding the last hour of light',
|
||||
strategy: 'Deep dusk-tinted background lets the seed read as fired clay rather than alert-orange; accent shifts to a cooler ochre to evoke fading sky against warm earth.' },
|
||||
{ id: 'seed-041', oklch: [0.673, 0.217, 38.6],
|
||||
mood: 'sun-scorched terracotta at dusk — Marrakech rooftop, fired clay holding the day\'s heat',
|
||||
strategy: 'Anchored a deep clay-tinted dark mode around the seed so the orange reads as glowing ember rather than logo-bright, with a cooled saffron accent for contrast lift.' },
|
||||
{ id: 'seed-042', oklch: [0.688, 0.133, 35.8],
|
||||
mood: 'terracotta vessel in late afternoon sun — sunbaked clay, weathered adobe, slow southwestern light',
|
||||
strategy: 'Anchored the seed as a warm terracotta primary against a deep umber-tinted dark background, with a dusty rose-gold accent that catches like low-angle light on adobe walls.' },
|
||||
{ id: 'seed-044', oklch: [0.568, 0.149, 45.9],
|
||||
mood: 'weathered terracotta at dusk — sun-warmed adobe walls in a New Mexico courtyard as the light goes amber',
|
||||
strategy: 'Anchored a warm dark-mode palette in a deep umber-tinted background so the seed\'s burnt-sienna character glows like lamplight on clay, with a cooler ember accent for contrast without breaking the desert-dusk register.' },
|
||||
{ id: 'seed-048', oklch: [0.733, 0.174, 45.1],
|
||||
mood: 'Marrakech souk at golden hour — sun-warmed terracotta, leather goods, spice sacks under amber light',
|
||||
strategy: 'Dark mode with a deep umber-tinted background that lets the seed read as glowing copper lantern-light, paired with a desaturated saffron accent in the same warm family to evoke spice and dust rather than contrast-shock.' },
|
||||
{ id: 'seed-051', oklch: [0.704, 0.189, 49.0],
|
||||
mood: 'blacksmith\'s forge at dusk — ember glow against cooling iron',
|
||||
strategy: 'Dark mode anchored in a deep blue-black iron tone to let the seed read as live ember; accent shifts to a cooler amber-gold so the primary feels like the hottest point in the fire.' },
|
||||
{ id: 'seed-053', oklch: [0.773, 0.157, 56.6],
|
||||
mood: 'late-afternoon Marrakech souk — sun-baked terracotta walls, saffron dust, leather and unglazed clay',
|
||||
strategy: 'Dark mode built on a deep umber-tinted ground so the amber seed reads as lamplight on clay; accent shifted toward rust-red for the leather/spice register, muted ink kept warm to avoid sterile contrast.' },
|
||||
{ id: 'seed-074', oklch: [0.70, 0.10, 198],
|
||||
mood: 'coastal fog at dawn — wet slate, sea-glass, the hush before a harbor wakes',
|
||||
strategy: 'Anchored a deep teal-tinted dark mode around the seed hue, pulled the seed itself up as a luminous sea-glass primary, and used a pale sand accent for the warm break of dawn through mist.' },
|
||||
{ id: 'seed-075', oklch: [0.65, 0.11, 234],
|
||||
mood: 'coastal observatory at dawn — cold steel-blue light over a sleeping harbor, instruments humming',
|
||||
strategy: 'Deep tinted dark mode anchored in the seed\'s hue, with the primary lifted in luminance to read as \'first light\' against a near-black sea, and a warm amber accent borrowed from sodium dock lamps to break the monochrome chill.' },
|
||||
{ id: 'seed-076', oklch: [0.70, 0.10, 234],
|
||||
mood: 'predawn coastal fog — muted slate-blue light before the harbor wakes',
|
||||
strategy: 'Anchored a deep blue-teal background near the seed\'s hue to evoke fog-dimmed water, then floated a slightly lifted seed as primary and warmed the accent toward amber (harbor lamp) for a single point of human warmth.' },
|
||||
{ id: 'seed-077', oklch: [0.578, 0.130, 241.7],
|
||||
mood: 'predawn harbor — cold steel water, fog-muted signal lights before sunrise',
|
||||
strategy: 'Deep blue-tinted dark mode anchors the seed as a luminous beacon hue, with a single warm amber accent acting as a distant harbor lamp cutting through the cool fog.' },
|
||||
{ id: 'seed-078', oklch: [0.705, 0.169, 242.5],
|
||||
mood: 'pre-dawn coastal radar room — cold ocean blue glow against deep instrument-panel dark, quiet vigilance',
|
||||
strategy: 'Dark mode anchored in a deep blue-black surface tint of the seed; primary is the seed itself acting as luminous signal-light, with a desaturated cyan-leaning accent suggesting screen phosphor, all over near-monochrome cool neutrals.' },
|
||||
{ id: 'seed-079', oklch: [0.478, 0.136, 251.8],
|
||||
mood: 'pre-dawn observatory — cold cobalt sky just before first light, instruments humming',
|
||||
strategy: 'Deep tinted dark mode anchored in the seed\'s blue family, with ink and surface lifted into the same hue to feel like one continuous twilight, then a single warm pale-gold accent as the contrasting \'instrument light\'.' },
|
||||
{ id: 'seed-080', oklch: [0.541, 0.122, 248.2],
|
||||
mood: 'pre-dawn observatory — cold steel light on instrument glass, the blue hour before sunrise',
|
||||
strategy: 'Anchored a deep blue-tinted dark mode around the seed hue, then introduced a faintly warmer cyan-shifted accent to evoke the moment night air refracts into morning — primary stays close to seed, accent shifts +8° cooler-bright for stellar glint.' },
|
||||
{ id: 'seed-081', oklch: [0.65, 0.16, 252],
|
||||
mood: 'predawn observatory — cold steel sky just before the stars fade, instruments humming quietly',
|
||||
strategy: 'Deep blue-tinted dark mode anchored by the seed as primary, with a desaturated cyan accent suggesting starlight on metal — chroma kept low across roles to preserve the hushed, instrument-panel stillness.' },
|
||||
{ id: 'seed-082', oklch: [0.742, 0.140, 247.4],
|
||||
mood: 'pre-dawn alpine observatory — cold thin air, starlight on snow, instruments glowing faintly',
|
||||
strategy: 'Dark mode anchored in a deep blue-tinted midnight bg so the seed reads as luminous instrument-light; accent shifts to a pale ice-cyan for that crystalline high-altitude register.' },
|
||||
{ id: 'seed-083', oklch: [0.340, 0.159, 262.4],
|
||||
mood: 'pre-dawn observatory — deep cobalt sky just before the stars fade, instruments cold and precise',
|
||||
strategy: 'Dark mode anchored in a deeply tinted version of the seed hue, with the seed itself lifted into a luminous primary like a star against night, plus a cool cyan-shifted accent suggesting telescope optics.' },
|
||||
{ id: 'seed-084', oklch: [0.476, 0.207, 261.2],
|
||||
mood: 'blueprint room at 2am — drafting table lamp on cold cobalt linen, ink still wet',
|
||||
strategy: 'Dark mode anchored in a deep blue-violet tinted from the seed\'s hue, with the seed itself elevated as a luminous primary and a warm amber accent acting as the drafting lamp against the cold field.' },
|
||||
{ id: 'seed-085', oklch: [0.681, 0.132, 258.4],
|
||||
mood: 'pre-dawn observatory — cold starlight on brushed steel, instruments humming in the dark',
|
||||
strategy: 'Deep blue-tinted dark mode anchored to the seed\'s hue, with the seed itself lifted as primary against near-black sky; a pale icy accent acts as distant starlight, creating cool tonal stratification rather than complementary contrast.' },
|
||||
{ id: 'seed-086', oklch: [0.767, 0.106, 255.9],
|
||||
mood: 'predawn alpine reconnaissance — cold blue half-light before the sun clears the ridge',
|
||||
strategy: 'Dark mode with a deep navy-tinted background that lets the seed read as moonlit ice; primary is the seed itself, accent shifts to a glacial cyan to mimic light refracting through snow.' },
|
||||
{ id: 'seed-087', oklch: [0.40, 0.13, 270],
|
||||
mood: 'occult library at dusk — violet ink, candle-warmth on vellum, hushed gravity',
|
||||
strategy: 'Anchored a deep indigo-violet primary on a warm parchment background so the seed reads as old ink on aged paper; accent shifts to a muted amber candleflame for a complementary warm whisper without breaking the scholarly hush.' },
|
||||
{ id: 'seed-088', oklch: [0.476, 0.158, 268.5],
|
||||
mood: 'pre-dawn observatory — deep cobalt sky moments before first light, instruments humming',
|
||||
strategy: 'Anchored the seed as primary and built a deep blue-violet dark mode with a cold steel-cyan accent to evoke calibrated instruments against night sky, keeping chroma restrained so the indigo reads as atmospheric rather than digital.' },
|
||||
{ id: 'seed-089', oklch: [0.50, 0.20, 270],
|
||||
mood: 'late-night astronomer\'s notebook — deep cobalt ink under a single desk lamp, brass-warm annotations in the margins',
|
||||
strategy: 'Anchored the violet seed in a deep blue-violet midnight ground and lifted it slightly for the primary, then placed a warm amber accent opposite on the wheel to mimic incandescent lamplight against night sky.' },
|
||||
{ id: 'seed-090', oklch: [0.445, 0.206, 279.1],
|
||||
mood: 'late-night astronomy lab — deep indigo void, instrument glow, ink on vellum charts',
|
||||
strategy: 'Anchor the violet seed as the luminous primary against a deep indigo-tinted dark ground, with a warm amber accent acting as the brass-instrument counterpoint across the color wheel.' },
|
||||
{ id: 'seed-098', oklch: [0.329, 0.149, 319.8],
|
||||
mood: 'velvet curtain at a late-night cabaret — bruised plum and gaslight',
|
||||
strategy: 'Dark-mode palette anchored in a deep purple-plum bg pulled from the seed\'s hue, with the seed elevated as primary and a warm gaslight-gold accent across the wheel to evoke stage light against curtain shadow.' },
|
||||
{ id: 'seed-099', oklch: [0.40, 0.13, 324],
|
||||
mood: 'velvet-rope speakeasy at 1am — mulberry plush, low amber lamps, secrets kept',
|
||||
strategy: 'Anchored a deep plum-tinted dark mode around the seed, paired a slightly lifted mulberry primary with a warm brass accent for analogous-plus-temperature contrast that reads as candlelit rather than neon.' },
|
||||
{ id: 'seed-100', oklch: [0.45, 0.15, 324],
|
||||
mood: 'velvet boudoir at 2am — mulberry silk, low lamplight, perfumed hush',
|
||||
strategy: 'Dark mode anchored in a deep plum-tinted bg lets the mauve seed glow as primary, with a warm peach accent cutting through like candlelight on skin.' },
|
||||
{ id: 'seed-101', oklch: [0.494, 0.196, 328.1],
|
||||
mood: 'Berlin nightclub at 2am — bruised magenta neon bleeding through fog and black concrete',
|
||||
strategy: 'Dark mode anchored on a deep violet-black tinted with the seed\'s hue, letting the magenta primary glow like signage while a cooler cyan accent cuts through the haze.' },
|
||||
{ id: 'seed-102', oklch: [0.55, 0.18, 324],
|
||||
mood: '1980s Tokyo neon at 2am — magenta sign-glow reflecting off wet asphalt',
|
||||
strategy: 'Deep cyan-tinted near-black bg lets the magenta primary glow like a neon sign, with a cool electric-blue accent acting as the secondary signage reflection.' },
|
||||
{ id: 'seed-103', oklch: [0.65, 0.16, 324],
|
||||
mood: '1980s Tokyo neon at 2am — wet asphalt, magenta signage, the hum of a vending machine',
|
||||
strategy: 'Deep tinted dark mode pulling the seed\'s pink-magenta into the background as bruised plum, then pairing the primary with a cyan accent for that signage-against-night-rain contrast.' },
|
||||
{ id: 'seed-104', oklch: [0.70, 0.13, 324],
|
||||
mood: '1980s Tokyo neon at dusk — magenta signage glow over wet asphalt and indigo sky',
|
||||
strategy: 'Dark mode with a deep violet-indigo base shifted from the seed hue, letting the magenta primary read as luminous neon while a cyan accent provides the cross-street counter-glow.' },
|
||||
{ id: 'seed-105', oklch: [0.40, 0.13, 342],
|
||||
mood: 'velvet boudoir at dusk — bruised plum, candlelight, and the hush of heavy drapery',
|
||||
strategy: 'Dark mode anchored in a deep plum-tinted background so the seed reads as a low-lit room; primary lifts the seed in lightness for legibility, accent shifts to a warm candle-amber across the wheel for intimate contrast.' },
|
||||
{ id: 'seed-106', oklch: [0.45, 0.15, 342],
|
||||
mood: 'velvet boudoir at last call — bruised plum, low lamplight, lipstick on a champagne flute',
|
||||
strategy: 'Anchored a deep wine-tinted dark mode around the seed, kept primary close to the seed hue for sultry continuity, and pulled accent toward warm rose-gold so it reads as candlelight against the plum rather than competing chroma.' },
|
||||
{ id: 'seed-107', oklch: [0.50, 0.20, 342],
|
||||
mood: 'Berlin nightclub at 2am — magenta neon bleeding into wet asphalt, intimate and electric',
|
||||
strategy: 'Dark mode anchored in a deep violet-tinted black so the seed magenta reads as neon signage; accent shifts to a cool cyan across the wheel for that wet-pavement reflection contrast.' },
|
||||
{ id: 'seed-109', oklch: [0.55, 0.18, 342],
|
||||
mood: '1980s Tokyo neon at 2am — wet pavement reflecting pink signage and electric magenta dusk',
|
||||
strategy: 'Deep cool-violet ground lets the magenta primary glow like a neon tube, with a cyan accent acting as the complementary signage flicker across rain-slick surfaces.' },
|
||||
{ id: 'seed-110', oklch: [0.65, 0.16, 342],
|
||||
mood: '1980s Tokyo neon love hotel — humid pink dusk, lacquered chrome, plum-shadow privacy',
|
||||
strategy: 'Anchored a deep plum-tinted dark mode around the seed so the rose reads as warm signage light rather than candy; paired with a cool cyan-lilac accent to mimic neon doubling against wet pavement.' },
|
||||
{ id: 'seed-111', oklch: [0.70, 0.13, 342],
|
||||
mood: '1950s Parisian boudoir — powdered rose silk, velvet shadows, gilded mirror light',
|
||||
strategy: 'Anchored a dusty rose primary against a deep plum-black ground with a candlelit gold accent — split-complementary warmth pulled from the seed\'s pink to evoke low-lit intimacy rather than sweetness.' },
|
||||
{ id: 'seed-112', oklch: [0.754, 0.193, 343.4],
|
||||
mood: '1980s Tokyo neon boudoir — lacquered pink signage reflecting off wet asphalt at 2am',
|
||||
strategy: 'Deep cool-violet ground anchors the seed as luminous neon signage; primary stays close to seed hue with slightly lifted chroma, accent jumps to electric cyan for that wet-asphalt reflection contrast.' },
|
||||
{ id: 'seed-113', oklch: [0.470, 0.173, 354.8],
|
||||
mood: 'velvet boudoir at 2am — bruised rose, candlelit mahogany, the hush after a confession',
|
||||
strategy: 'Anchor on a deep wine-tinted background so the seed reads as candlelight on dark plush; pair with a warm amber accent to suggest low filament light cutting through the rose.' },
|
||||
{ id: 'seed-114', oklch: [0.570, 0.158, 353.3],
|
||||
mood: 'boudoir at dusk — velvet rosewood, candlelight on dark silk',
|
||||
strategy: 'Anchored a deep wine-tinted dark mode around the seed\'s rose hue, with a warm gold accent across the wheel to evoke candlelight against draped fabric.' },
|
||||
{ id: 'seed-115', oklch: [0.636, 0.218, 355.3],
|
||||
mood: 'Parisian boudoir at dusk — bruised rose silk, velvet shadows, tarnished gilt',
|
||||
strategy: 'Anchored the seed as a muted rose-primary against a deep wine-tinted dark ground, then offset it with a tarnished-gold accent on the opposing warm axis to evoke candlelit lacquer rather than romantic pink.' },
|
||||
{ id: 'seed-116', oklch: [0.734, 0.183, 356.8],
|
||||
mood: '1980s Tokyo love hotel neon — humid pink dusk, lacquered chrome, and the hum of a vending machine at 2am',
|
||||
strategy: 'Pulled the seed pink into a deep aubergine-black ground so it reads as glowing neon rather than bubblegum, then paired it with a cool cyan-mint accent to mimic the classic magenta/teon sign duality of Shōwa-era signage.' },
|
||||
];
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { id: null, from: null };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === '--id' && argv[i + 1]) { args.id = argv[++i]; }
|
||||
else if (a === '--from' && argv[i + 1]) { args.from = argv[++i]; }
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function hashToIndex(key, n) {
|
||||
const h = crypto.createHash('sha256').update(key).digest();
|
||||
return h.readUInt32BE(0) % n;
|
||||
}
|
||||
|
||||
function pickSeed(seeds, { id, from }) {
|
||||
if (id) {
|
||||
const found = seeds.find(s => s.id === id);
|
||||
if (!found) { console.error(`no seed with id "${id}"`); process.exit(2); }
|
||||
return found;
|
||||
}
|
||||
const envFrom = process.env.IMPECCABLE_PALETTE_SEED;
|
||||
const key = from || envFrom;
|
||||
if (key) return seeds[hashToIndex(key, seeds.length)];
|
||||
return seeds[Math.floor(Math.random() * seeds.length)];
|
||||
}
|
||||
|
||||
function fmtOklch([L, C, H]) {
|
||||
return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`;
|
||||
}
|
||||
|
||||
function hueWord(H) {
|
||||
if (H < 15 || H >= 345) return 'pure red';
|
||||
if (H < 35) return 'warm red / crimson';
|
||||
if (H < 55) return 'warm coral / burnt orange';
|
||||
if (H < 80) return 'orange / honey';
|
||||
if (H < 105) return 'warm amber / honey-gold';
|
||||
if (H < 135) return 'yellow-green / olive';
|
||||
if (H < 170) return 'green';
|
||||
if (H < 200) return 'teal';
|
||||
if (H < 230) return 'sky blue';
|
||||
if (H < 265) return 'cobalt / indigo';
|
||||
if (H < 295) return 'violet / purple';
|
||||
if (H < 330) return 'magenta / pink';
|
||||
return 'deep pink / rose';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const seed = pickSeed(SEEDS, args);
|
||||
const [L, C, H] = seed.oklch;
|
||||
|
||||
// The mood + strategy on each seed were derived by the model that
|
||||
// originally judged it. We surface them as *hints*, not commands —
|
||||
// the brief should still drive what the seed becomes.
|
||||
const moodHint = seed.mood ? ` (one read: "${seed.mood}")` : '';
|
||||
const strategyHint = seed.strategy ? `\n - one example strategy: ${seed.strategy}` : '';
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Fat tool-exit response — what the model sees on stdout.
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
process.stdout.write(`BRAND SEED · ${seed.id}
|
||||
|
||||
Seed color (anchor for your primary brand color):
|
||||
${fmtOklch(seed.oklch)} — ${hueWord(H)}${moodHint}
|
||||
|
||||
This is the brand's anchor — a single beautiful color. Compose the rest of
|
||||
the palette around it using YOUR judgment, the brief (PRODUCT.md /
|
||||
DESIGN.md / the user's prompt), and the color-strategy guidance already in
|
||||
SKILL.md.
|
||||
|
||||
How to use:
|
||||
|
||||
1. Read the brief. Write one specific phrase describing the mood this
|
||||
product calls for. Be granular. Good: "1970s travel poster — sun-baked
|
||||
warmth, considered", "midnight jazz club — smoky brass, saxophone
|
||||
light", "Scandinavian winter morning — quiet light through frost". Bad:
|
||||
"modern and clean", "warm and inviting". The first lets you compose; the
|
||||
second is generic and will produce generic palettes.
|
||||
|
||||
2. The seed's hue (${H.toFixed(0)}°) anchors your primary brand color. You
|
||||
choose L and C to match the mood. The same hue can be deep-and-velvet,
|
||||
bright-and-confident, or pale-and-faded — pick the one the mood demands.
|
||||
Primary's hue should stay within ±10° of the seed.${strategyHint}
|
||||
|
||||
3. Now compose the full palette in OKLCH (5 more roles):
|
||||
• bg — the most important architectural choice.
|
||||
CORE PRINCIPLE: the mood lives in the BRAND COLORS
|
||||
(primary + accent) and typography, NOT in the surface.
|
||||
Stripe is warm — its purple does that, bg is pure
|
||||
white. Linear is cool — its blue does that, bg is
|
||||
pure. Notion is warm — its accents do that, bg is
|
||||
near-pure-white. Putting warmth in BOTH primary AND
|
||||
bg is the AI cliché.
|
||||
|
||||
DEFAULT A — PURE white: exactly oklch(1.000 0.000 0).
|
||||
Not 0.99, not chroma 0.002. Stripe / Notion / Apple
|
||||
use literal #ffffff. Don't add hidden warmth.
|
||||
Refs: Stripe, Notion, Linear (light), Apple.com,
|
||||
Vercel docs, Figma marketing, Loom, Substack.
|
||||
|
||||
DEFAULT B — PURE black/near-black: L 0.04-0.12,
|
||||
chroma exactly 0.000. No hue tint. Vercel is
|
||||
roughly oklch(0.08 0 0). Pick L for mood; C is 0.
|
||||
Refs: Vercel, A24, Acne, Apple dark, MUBI.
|
||||
|
||||
ALT 2 — TINTED: chroma 0.015-0.05.
|
||||
Use ONLY when:
|
||||
(a) the mood is EXPLICITLY environmental — the surface
|
||||
IS part of the brand (1920s lacquered interior,
|
||||
leather library, ceramic studio, hotel lobby), or
|
||||
(b) the seed itself is desaturated (chroma < 0.10) and
|
||||
needs a tinted surface to read as a brand.
|
||||
NOT for "feels warm" / "modern + warm" / "moody". If
|
||||
your mood says "warm" but doesn't name a specific
|
||||
environment, use PURE white and let primary carry
|
||||
the warmth.
|
||||
|
||||
HEURISTIC: if seed chroma > 0.10 AND mood is product-
|
||||
focused (not environment-focused), it's almost always
|
||||
PURE white. Target distribution across many palettes:
|
||||
~50% pure white, ~25% pure black, ~25% tinted.
|
||||
• surface — bg pulled slightly toward ink (10-15% mix). Same hue
|
||||
family as bg. Used for cards, panels, sections.
|
||||
• ink — body text color. Must reach ≥7:1 contrast vs bg.
|
||||
Can carry the brand hue at low chroma in light mode
|
||||
(slight warmth or coolness toward the brand).
|
||||
• accent — a SECOND brand color, distinct from primary in BOTH
|
||||
hue AND lightness. Picked to complement the mood (not
|
||||
default-complementary across the wheel). Used for
|
||||
badges, status pills, links, accent rules.
|
||||
• muted — secondary text. Ink pulled 40% toward bg, keeping ink's
|
||||
hue. Must reach ≥3.5:1 contrast vs bg.
|
||||
|
||||
4. Pick a color STRATEGY (the four steps from SKILL.md):
|
||||
• Restrained: tinted neutrals + accent ≤10% — product default
|
||||
• Committed: one saturated color carries 30-60% — identity-driven
|
||||
• Full palette: 3-4 named roles each used deliberately — brand work
|
||||
• Drenched: the surface IS the color — campaign, hero, statement
|
||||
The brief picks the strategy. A startup dashboard ≠ a perfume brand.
|
||||
|
||||
Hard rules (already in SKILL.md, recapped because the seed step is where
|
||||
they actually bite):
|
||||
|
||||
- OKLCH only — never hex. Never #RRGGBB.
|
||||
- ink-vs-bg WCAG contrast ≥ 7 (body text must be readable)
|
||||
- primary chroma ≤ 0.23 (above this, primary glows perceptually and
|
||||
no text on it is readable — acid-bright is a UI failure)
|
||||
- if primary L > 0.78, primary chroma ≤ 0.18 (the fluorescent zone)
|
||||
- primary-vs-accent contrast ≥ 1.7 (they must be visually distinct,
|
||||
not two variants of the same hue at similar lightness)
|
||||
- avoid the saturated AI attractor zones: claude-beige (warm-cream bg
|
||||
+ dusty brown primary), forest-green-on-cream, AI-purple-on-white,
|
||||
navy-cream-with-orange-accent
|
||||
|
||||
Return your composed palette in CSS custom properties using OKLCH, then
|
||||
build with it. The seed is the start, not the recipe.
|
||||
`);
|
||||
Reference in New Issue
Block a user