Add screen 02 palette picker: cue deck, sampling rings, band tools

Deck of cue cards + seed palettes with scroll-snap browsing, draggable
color-sampling rings with loupe, four-band panel (copy hex+OKLCH, tint
strip, native custom color, card-local reset), /palettes.json route.

AI-assisted (agent-implemented, maintainer-directed).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Abdul Wahab
2026-09-01 10:02:05 +05:00
co-authored by Cursor
parent ba7f3c8891
commit c19c349fc6
7 changed files with 1839 additions and 172 deletions
+20
View File
@@ -10,6 +10,8 @@ import { readFile, mkdir, stat, writeFile } from 'node:fs/promises';
import net from 'node:net';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { extractRegister } from './context.mjs';
import { SEEDS } from './palette.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const pickerDir = path.join(scriptDir, 'picker');
const answersPath = path.resolve(process.cwd(), '.impeccable/design-interview/answers.json');
@@ -168,6 +170,14 @@ if (options.help) {
process.exit(0);
}
let register = 'brand';
try {
const product = await readFile(path.resolve(process.cwd(), 'PRODUCT.md'), 'utf8');
register = extractRegister(product) || register;
} catch {
// A missing or unreadable PRODUCT.md keeps the brand preview default.
}
const port = await findOpenPort(options.port);
let completed = false;
let timeout;
@@ -212,6 +222,16 @@ async function handleRequest(request, response) {
await serveFile(response, options.cuesDir, 'cues.json', ['.json']);
return;
}
if (requestPath === '/palettes.json') {
sendJson(response, 200, {
seeds: SEEDS.map(({ id, oklch, mood }) => ({ id, oklch, mood })),
});
return;
}
if (requestPath === '/context.json') {
sendJson(response, 200, { register });
return;
}
if (requestPath.startsWith('/cues/')) {
const cueName = requestPath.slice('/cues/'.length);
if (!cueName || cueName.includes('/')) {