mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-19 09:36:59 +03:00
Track questionnaire previews gallery and color strategy experiments.
Un-ignore tmp/questionaire for the gallery toolchain, baseline HTML, and the improved replica with drenched, committed, and full-palette color fixes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+2
-1
@@ -123,7 +123,8 @@ talks/
|
||||
.astro/
|
||||
|
||||
# Local-only scratch for exploratory scripts, parked pages, and unused asset candidates.
|
||||
tmp/
|
||||
tmp/*
|
||||
!tmp/questionaire/
|
||||
|
||||
# Isolated ablation staging (impeccable-evals copies the built staged skill here
|
||||
# per rule, removes one rule, points a worker at it). Throwaway; never committed.
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Allowlist only the gallery toolchain and HTML outputs; keep captures and diag local.
|
||||
*
|
||||
!.gitignore
|
||||
!build-gallery.mjs
|
||||
!build-gallery-replica.mjs
|
||||
!capture-previews.mjs
|
||||
!gallery-cells.mjs
|
||||
!previews-gallery.html
|
||||
!previews-gallery-improved.html
|
||||
@@ -0,0 +1,149 @@
|
||||
/* Build an untouched replica of previews-gallery.html, then apply color-only
|
||||
experiments to selected strategy iframes. The source gallery stays baseline. */
|
||||
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const SOURCE = path.join(HERE, 'previews-gallery.html');
|
||||
const OUTPUT = path.join(HERE, 'previews-gallery-improved.html');
|
||||
|
||||
const OPS_PREVIEW =
|
||||
'#picker-form .picker-strategy-stage > .picker-preview.picker-preview--ops';
|
||||
|
||||
/** @type {{ id: string, cell: string, css: string }[]} */
|
||||
const EXPERIMENTS = [
|
||||
{
|
||||
id: 'landing-drenched-color-roles',
|
||||
cell: 'strategy--persuade--drenched',
|
||||
css: [
|
||||
'html[data-cell-screen="03"][data-cell-surface="persuade"][data-cell-option="drenched"]',
|
||||
'#picker-form .picker-strategy-stage > .picker-preview[data-surface="persuade"] {',
|
||||
' --pv-secondary: var(--pkc-secondary) !important;',
|
||||
' --pv-tertiary: var(--pkc-tertiary) !important;',
|
||||
' --pv-bone: color-mix(in oklab, var(--pkc-p-ink) 55%, var(--pkc-primary)) !important;',
|
||||
' --pv-strong: var(--pkc-p-ink) !important;',
|
||||
' --pv-secondary-wash: color-mix(in oklab, var(--pkc-p-ink) 16%, var(--pkc-primary)) !important;',
|
||||
'}',
|
||||
'html[data-cell-screen="03"][data-cell-surface="persuade"][data-cell-option="drenched"]',
|
||||
'#picker-form .picker-strategy-stage > .picker-preview[data-surface="persuade"] .pv-image {',
|
||||
' background: linear-gradient(',
|
||||
' 135deg,',
|
||||
' color-mix(in oklab, var(--pkc-p-ink) 16%, var(--pkc-primary)),',
|
||||
' color-mix(in oklab, var(--pkc-p-ink) 28%, var(--pkc-primary))',
|
||||
' ) !important;',
|
||||
' border-color: color-mix(in oklab, var(--pkc-p-ink) 40%, var(--pkc-primary)) !important;',
|
||||
'}',
|
||||
'html[data-cell-screen="03"][data-cell-surface="persuade"][data-cell-option="drenched"]',
|
||||
'#picker-form .picker-strategy-stage > .picker-preview[data-surface="persuade"] .pv-actions i:last-child {',
|
||||
' border-color: color-mix(in oklab, var(--pkc-p-ink) 45%, var(--pkc-primary)) !important;',
|
||||
' background: color-mix(in oklab, var(--pkc-p-ink) 12%, var(--pkc-primary)) !important;',
|
||||
'}',
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
id: 'ops-committed-color-visibility',
|
||||
cell: 'strategy--operate--committed',
|
||||
css: [
|
||||
'html[data-cell-screen="03"][data-cell-surface="operate"][data-cell-option="committed"]',
|
||||
`${OPS_PREVIEW} {`,
|
||||
' /* Committed: primary owns the paper and the selection wash, not a 5% trace.',
|
||||
' Structure stays neutral like Restrained so accents still pop. */',
|
||||
' --pv-neutral: color-mix(in oklab, var(--pkc-neutral) 84%, var(--pkc-primary)) !important;',
|
||||
' --po-wash: color-mix(in oklab, var(--pkc-primary) 24%, var(--pkc-neutral)) !important;',
|
||||
'}',
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
id: 'ops-full-palette-color-roles',
|
||||
cell: 'strategy--operate--full-palette',
|
||||
css: [
|
||||
'html[data-cell-screen="03"][data-cell-surface="operate"][data-cell-option="full-palette"]',
|
||||
`${OPS_PREVIEW} {`,
|
||||
' /* Full palette wireframe never assigns secondary; spread all four roles. */',
|
||||
' --po-wash: color-mix(in oklab, var(--pkc-secondary) 22%, var(--pkc-neutral)) !important;',
|
||||
'}',
|
||||
'html[data-cell-screen="03"][data-cell-surface="operate"][data-cell-option="full-palette"]',
|
||||
`${OPS_PREVIEW} .po-row--on .po-dot {`,
|
||||
' background: var(--pkc-tertiary) !important;',
|
||||
'}',
|
||||
'html[data-cell-screen="03"][data-cell-surface="operate"][data-cell-option="full-palette"]',
|
||||
`${OPS_PREVIEW} .po-field {`,
|
||||
' border-color: var(--pkc-secondary) !important;',
|
||||
'}',
|
||||
'html[data-cell-screen="03"][data-cell-surface="operate"][data-cell-option="full-palette"]',
|
||||
`${OPS_PREVIEW} .po-chart i:nth-child(2) {`,
|
||||
' background: color-mix(in oklab, var(--pkc-secondary) 78%, var(--pkc-neutral)) !important;',
|
||||
'}',
|
||||
'html[data-cell-screen="03"][data-cell-surface="operate"][data-cell-option="full-palette"]',
|
||||
`${OPS_PREVIEW} .po-chart i:nth-child(3) {`,
|
||||
' background: color-mix(in oklab, var(--pkc-tertiary) 68%, var(--pkc-neutral)) !important;',
|
||||
'}',
|
||||
].join('\n'),
|
||||
},
|
||||
];
|
||||
|
||||
const experiment = String.raw`
|
||||
<script data-gallery-replica-experiment="strategy-color-fixes">
|
||||
(function () {
|
||||
var specs = ${JSON.stringify(EXPERIMENTS.map(({ id, cell, css }) => ({ id, cell, css })))};
|
||||
|
||||
specs.forEach(function (spec) {
|
||||
var frame = document.querySelector('iframe[data-cell="' + spec.cell + '"]');
|
||||
if (!frame) return;
|
||||
|
||||
function apply() {
|
||||
var doc = frame.contentDocument;
|
||||
if (!doc || !doc.head) return;
|
||||
|
||||
var style = doc.querySelector('[data-experiment="' + spec.id + '"]');
|
||||
if (!style) {
|
||||
style = doc.createElement('style');
|
||||
style.setAttribute('data-experiment', spec.id);
|
||||
style.textContent = spec.css;
|
||||
doc.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
frame.addEventListener('load', apply);
|
||||
apply();
|
||||
});
|
||||
})();
|
||||
</script>`;
|
||||
|
||||
const html = await readFile(SOURCE, 'utf8');
|
||||
if (html.includes('data-gallery-replica-experiment')) {
|
||||
throw new Error('Source gallery already contains the replica experiment');
|
||||
}
|
||||
if (!html.includes('</body>')) {
|
||||
throw new Error('Source gallery has no closing body tag');
|
||||
}
|
||||
|
||||
for (const { cell } of EXPERIMENTS) {
|
||||
if (!html.includes('data-cell="' + cell + '"')) {
|
||||
throw new Error('Target cell not found: ' + cell);
|
||||
}
|
||||
}
|
||||
|
||||
let replica = html.replace('</body>', experiment + '\n</body>');
|
||||
replica = replica.replace(
|
||||
'<title>Questionnaire previews, ground-truth gallery</title>',
|
||||
'<title>Questionnaire previews (improved experiment)</title>',
|
||||
);
|
||||
replica = replica.replace(
|
||||
'<h1>Questionnaire previews</h1>',
|
||||
'<h1>Questionnaire previews (improved experiment)</h1>',
|
||||
);
|
||||
replica = replica.replace(
|
||||
/<p class="note">Ground-truth extraction[\s\S]*?<\/p>/,
|
||||
'<p class="note">Replica of the ground-truth gallery with color-only experiments on '
|
||||
+ '<strong>Landing page + Drenched</strong>, '
|
||||
+ '<strong>App UI + Committed</strong>, and '
|
||||
+ '<strong>App UI + Full palette</strong>. '
|
||||
+ 'Baseline: <a href="previews-gallery.html">previews-gallery.html</a>. '
|
||||
+ 'Restrained is unchanged (reference).</p>',
|
||||
);
|
||||
|
||||
await writeFile(OUTPUT, replica);
|
||||
console.log('wrote previews-gallery-improved.html (' + (replica.length / 1024 / 1024).toFixed(1) + ' MB)');
|
||||
@@ -0,0 +1,545 @@
|
||||
|
||||
/* Plan 6 builder. Reads capture.json and writes previews-gallery.html: one
|
||||
self-contained page with a sticky control bar (palette presets 1-4, per
|
||||
role custom color inputs, font presets 1-4, jump navigation) over 86 cells,
|
||||
each an iframe rebuilt from the captured ground truth. Zero runtime network
|
||||
requests: all CSS resources are data URIs and every captured <img> is
|
||||
neutralized. */
|
||||
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { CELL_LIST, FONT_PRESETS, PALETTE_PRESETS, SECTIONS } from './gallery-cells.mjs';
|
||||
|
||||
const OUT_DIR = '/Users/abdulwahab/impeccable/tmp/questionaire';
|
||||
const BLANK_GIF = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
||||
|
||||
const neutralizeImages = (html) =>
|
||||
html.replace(/(<img\b[^>]*?\bsrc=")[^"]*(")/g, '$1' + BLANK_GIF + '$2');
|
||||
|
||||
/* One external form-state block per cell: every radio group the picker CSS
|
||||
can read, except the group whose radios already live inside the captured
|
||||
section markup, plus the committed palette fields. surface-modes checkboxes
|
||||
are added except on screen 01b, whose section carries its own. */
|
||||
function externalInputs(cell) {
|
||||
const parts = [];
|
||||
if (cell.screen !== '01b') {
|
||||
for (const s of ['persuade', 'operate', 'read', 'experience']) {
|
||||
parts.push('<input type="checkbox" name="surface-modes" value="' + s + '"' + (s === cell.surface ? ' checked' : '') + '>');
|
||||
}
|
||||
}
|
||||
parts.push('<input type="hidden" name="palette-source" value="zinc-dawn">');
|
||||
for (const role of ['primary', 'secondary', 'tertiary', 'neutral']) {
|
||||
parts.push('<input type="hidden" name="palette-' + role + '" value="' + PALETTE_PRESETS[0][role] + '">');
|
||||
}
|
||||
const sectionGroup = SECTIONS.find((s) => s.id === cell.section).group;
|
||||
for (const [group, value] of Object.entries(cell.formState)) {
|
||||
if (group === sectionGroup) continue;
|
||||
parts.push('<input type="radio" name="' + group + '" value="' + value + '" checked>');
|
||||
}
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
/* The standalone document each cell iframe runs. Same placeholder scheme as
|
||||
tmp/motion-previews-premium.html. */
|
||||
const IFRAME_TEMPLATE = [
|
||||
'<!doctype html>',
|
||||
'<html lang="en" class="dark" data-cell-screen="__SCREEN__" data-cell-surface="__SURFACE__" data-cell-option="__OPTION__">',
|
||||
'<head><meta charset="utf-8">',
|
||||
'__CSS__',
|
||||
'<style data-origin="standalone-override">',
|
||||
'html, body { margin: 0; padding: 0; overflow: hidden; }',
|
||||
'.picker-question-title, .picker-question-head, .picker-type-rail, .picker-actions-stack, .picker-actions,',
|
||||
'.picker-back, .picker-surface-tabs, .picker-modes-note, .picker-deck-column, .picker-bands,',
|
||||
'.picker-band-status, .picker-palette-hint, .picker-reset, .picker-ring-guide, .picker-loupe,',
|
||||
'.picker-type-controls, .picker-type-scroll, .picker-icon-options, .picker-progress, .picker-key-hint,',
|
||||
'#picker-form legend { display: none !important; }',
|
||||
'main.picker-shell, #picker-form, .picker-screen, .picker-container, .picker-strategy, .picker-strategy-grid,',
|
||||
'.picker-modes, .picker-modes-grid, .picker-palette, .picker-palette-grid, .picker-palette-subgrid,',
|
||||
'.picker-palette-panel, .picker-type, .picker-type-grid, .picker-icons, .picker-icons-grid {',
|
||||
' display: block !important; padding: 0 !important; margin: 0 !important; border: 0 !important;',
|
||||
' max-width: none !important; min-height: 0 !important; gap: 0 !important; justify-items: start !important;',
|
||||
' height: auto !important; overflow: visible !important; contain: none !important; background: transparent !important;',
|
||||
'}',
|
||||
'__STAGE_CSS__',
|
||||
'<\/style>',
|
||||
'<\/head>',
|
||||
'<body class="picker-page" style="--pk-foil: url(__FOIL__)">',
|
||||
'<main class="picker-shell">',
|
||||
'<form id="picker-form" data-current="__SCREEN__">',
|
||||
'<div hidden data-origin="external-form-state">__EXTERNAL__<\/div>',
|
||||
'__SECTION__',
|
||||
'<\/form>',
|
||||
'<\/main>',
|
||||
'<script>__SCRIPT__<\/script>',
|
||||
'<\/body><\/html>',
|
||||
].join('\n');
|
||||
|
||||
/* Runs inside every cell. Applies the cell form state, shows the cell's
|
||||
surface board, exposes the repaint hooks, and (screen 06 only) carries the
|
||||
premium file's motion route plotting and hover replay verbatim. */
|
||||
const IFRAME_SCRIPT = [
|
||||
'var FORM_STATE = __FORM_STATE_JSON__;',
|
||||
'var CELL = __CELL_JSON__;',
|
||||
'Object.keys(FORM_STATE).forEach(function (group) {',
|
||||
' var value = FORM_STATE[group];',
|
||||
' var inputs = document.querySelectorAll(\'input[name="\' + group + \'"]\');',
|
||||
' Array.prototype.forEach.call(inputs, function (input) {',
|
||||
' input.disabled = false;',
|
||||
' input.checked = input.value === value;',
|
||||
' });',
|
||||
'});',
|
||||
'if (CELL.surface) {',
|
||||
' Array.prototype.forEach.call(document.querySelectorAll(\'input[name="surface-modes"]\'), function (input) {',
|
||||
' input.checked = input.value === CELL.surface;',
|
||||
' });',
|
||||
' Array.prototype.forEach.call(document.querySelectorAll(\'.picker-screen [data-surface]\'), function (node) {',
|
||||
' var active = node.getAttribute(\'data-surface\') === CELL.surface;',
|
||||
' if (active) node.removeAttribute(\'hidden\'); else node.setAttribute(\'hidden\', \'\');',
|
||||
' node.setAttribute(\'aria-hidden\', active ? \'false\' : \'true\');',
|
||||
' });',
|
||||
'}',
|
||||
'window.__applyPalette = function (vars) {',
|
||||
' var targets = document.querySelectorAll(\'[data-artboard], .picker-mode-preview--exact > .picker-preview, .picker-palette-panel .picker-preview\');',
|
||||
' Array.prototype.forEach.call(targets, function (node) {',
|
||||
' Object.keys(vars).forEach(function (key) { node.style.setProperty(\'--pv-\' + key, vars[key]); });',
|
||||
' });',
|
||||
' Array.prototype.forEach.call(document.querySelectorAll(\'[data-surface-stage]\'), function (stage) {',
|
||||
' Object.keys(vars).forEach(function (key) { stage.style.setProperty(\'--pkc-\' + key, vars[key]); });',
|
||||
' });',
|
||||
'};',
|
||||
'window.__applyFonts = function (font) {',
|
||||
' Array.prototype.forEach.call(document.querySelectorAll(\'[data-type-stage]\'), function (stage) {',
|
||||
' stage.style.setProperty(\'--pt-heading\', font.heading);',
|
||||
' stage.style.setProperty(\'--pt-body\', font.body);',
|
||||
' stage.style.setProperty(\'--pt-heading-weight\', font.headingWeight);',
|
||||
' });',
|
||||
'};',
|
||||
'if (CELL.screen === \'06\') {',
|
||||
' var MOTION_STOPS = {',
|
||||
' \'--mtr-nav1\': \'.ps-nav-bars i:nth-child(1)\',',
|
||||
' \'--mtr-nav2\': \'.ps-nav-bars i:nth-child(2)\',',
|
||||
' \'--mtr-cta1\': \'.ps-actions i:first-child\',',
|
||||
' \'--mtr-cta2\': \'.ps-actions i:last-child\',',
|
||||
' \'--mtr-card1\': \'.ps-gallery-item:nth-child(1) > i\',',
|
||||
' \'--mxi-work1\': \'.ps-index-row:nth-of-type(1) > .ps-image\',',
|
||||
' \'--mxi-work2\': \'.ps-index-row:nth-of-type(2) > .ps-image\',',
|
||||
' \'--mxi-rail\': \'.ps-index-arrow--next\',',
|
||||
' };',
|
||||
' var motionScenes = Array.prototype.slice.call(document.querySelectorAll(\'.picker-preview-motion\'));',
|
||||
' var plotMotionRoute = function (scene) {',
|
||||
' var boards = scene ? [scene] : motionScenes;',
|
||||
' boards.forEach(function (board) {',
|
||||
' var desk = board.querySelector(\'.ps-desktop\');',
|
||||
' if (!desk || !desk.clientWidth) return;',
|
||||
' var center = function (el) {',
|
||||
' var x = el.offsetWidth / 2;',
|
||||
' var y = el.offsetHeight / 2;',
|
||||
' for (var node = el; node && node !== desk; node = node.offsetParent) {',
|
||||
' x += node.offsetLeft;',
|
||||
' y += node.offsetTop;',
|
||||
' }',
|
||||
' return { x: (x / desk.clientWidth) * 100, y: (y / desk.clientHeight) * 100 };',
|
||||
' };',
|
||||
' Object.keys(MOTION_STOPS).forEach(function (name) {',
|
||||
' var el = desk.querySelector(MOTION_STOPS[name]);',
|
||||
' if (!el) return;',
|
||||
' var c = center(el);',
|
||||
' board.style.setProperty(name, c.x.toFixed(2) + \'cqw \' + c.y.toFixed(2) + \'cqh\');',
|
||||
' });',
|
||||
' var nav1 = desk.querySelector(\'.ps-nav-bars i:nth-child(1)\');',
|
||||
' board.style.setProperty(\'--mtr-entry\', center(nav1).x.toFixed(2) + \'cqw -8cqh\');',
|
||||
' });',
|
||||
' };',
|
||||
' motionScenes.forEach(function (scene) {',
|
||||
' new ResizeObserver(function () { plotMotionRoute(scene); }).observe(scene.querySelector(\'.ps-desktop\'));',
|
||||
' });',
|
||||
' plotMotionRoute();',
|
||||
' window.__replayMotion = function () {',
|
||||
' motionScenes.forEach(function (scene) {',
|
||||
' scene.getAnimations({ subtree: true }).forEach(function (animation) {',
|
||||
' animation.cancel();',
|
||||
' animation.play();',
|
||||
' });',
|
||||
' });',
|
||||
' };',
|
||||
'}',
|
||||
].join('\n');
|
||||
|
||||
/* The host page runtime: color math verbatim from picker/scripts/color.js
|
||||
(template literals rewritten to concatenation), fontStack verbatim from
|
||||
picker/scripts/palette-picker.js, plus the control bar wiring. */
|
||||
const HOST_RUNTIME = [
|
||||
'var PAYLOAD = JSON.parse(document.getElementById(\'gallery-payload\').textContent);',
|
||||
'',
|
||||
'/* ---- color.js, verbatim (concatenation instead of template literals) */',
|
||||
'var clamp = function (value) { return Math.min(1, Math.max(0, value)); };',
|
||||
'var linearize = function (value) { return value <= 0.04045 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4); };',
|
||||
'var gamma = function (value) { return value <= 0.0031308 ? 12.92 * value : 1.055 * Math.pow(value, 1 / 2.4) - 0.055; };',
|
||||
'var parseHex = function (hex) { return hex.match(/[\\da-f]{2}/gi).map(function (value) { return Number.parseInt(value, 16) / 255; }); };',
|
||||
'function toLinearRgb(lch) {',
|
||||
' var L = lch[0], C = lch[1], H = lch[2];',
|
||||
' var angle = H * Math.PI / 180;',
|
||||
' var a = C * Math.cos(angle);',
|
||||
' var b = C * Math.sin(angle);',
|
||||
' var l = Math.pow(L + 0.3963377774 * a + 0.2158037573 * b, 3);',
|
||||
' var m = Math.pow(L - 0.1055613458 * a - 0.0638541728 * b, 3);',
|
||||
' var s = Math.pow(L - 0.0894841775 * a - 1.291485548 * b, 3);',
|
||||
' return [',
|
||||
' 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,',
|
||||
' -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,',
|
||||
' -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,',
|
||||
' ];',
|
||||
'}',
|
||||
'function oklchToHex(lch) {',
|
||||
' var L = clamp(lch[0]);',
|
||||
' var C = Math.max(0, lch[1]);',
|
||||
' var hue = lch[2];',
|
||||
' var rgb = toLinearRgb([L, C, hue]);',
|
||||
' while (C > 0 && rgb.some(function (channel) { return channel < 0 || channel > 1; })) {',
|
||||
' C = Math.max(0, C - 0.005);',
|
||||
' rgb = toLinearRgb([L, C, hue]);',
|
||||
' }',
|
||||
' return \'#\' + rgb.map(function (channel) {',
|
||||
' return Math.round(clamp(gamma(channel)) * 255).toString(16).padStart(2, \'0\');',
|
||||
' }).join(\'\').toUpperCase();',
|
||||
'}',
|
||||
'function hexToOklch(hex) {',
|
||||
' var rgb = parseHex(hex).map(linearize);',
|
||||
' var red = rgb[0], green = rgb[1], blue = rgb[2];',
|
||||
' var l = Math.cbrt(0.4122214708 * red + 0.5363325363 * green + 0.0514459929 * blue);',
|
||||
' var m = Math.cbrt(0.2119034982 * red + 0.6806995451 * green + 0.1073969566 * blue);',
|
||||
' var s = Math.cbrt(0.0883024619 * red + 0.2817188376 * green + 0.6299787005 * blue);',
|
||||
' var L = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;',
|
||||
' var a = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;',
|
||||
' var b = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;',
|
||||
' var C = Math.hypot(a, b);',
|
||||
' return [L, C, C < 0.00001 ? 0 : (Math.atan2(b, a) * 180 / Math.PI + 360) % 360];',
|
||||
'}',
|
||||
'var INK_DARK_LUMINANCE = 0.0027;',
|
||||
'var INK_LIGHT_LUMINANCE = 0.9716;',
|
||||
'function relativeLuminance(hex) {',
|
||||
' var rgb = parseHex(hex).map(linearize);',
|
||||
' return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];',
|
||||
'}',
|
||||
'function ratio(a, b) {',
|
||||
' var hi = a > b ? a : b;',
|
||||
' var lo = a > b ? b : a;',
|
||||
' return (hi + 0.05) / (lo + 0.05);',
|
||||
'}',
|
||||
'function contrastInk(hex) {',
|
||||
' var swatch = relativeLuminance(hex);',
|
||||
' var against = function (ink) { return ratio(ink, swatch); };',
|
||||
' return against(INK_DARK_LUMINANCE) >= against(INK_LIGHT_LUMINANCE)',
|
||||
' ? \'var(--pk-ink-dark)\'',
|
||||
' : \'var(--pk-ink-light)\';',
|
||||
'}',
|
||||
'function contrastInkHex(hex) {',
|
||||
' return contrastInk(hex).includes(\'light\')',
|
||||
' ? oklchToHex([0.99, 0.008, 95])',
|
||||
' : oklchToHex([0.14, 0.018, 95]);',
|
||||
'}',
|
||||
'function readableOn(accent, ground, target) {',
|
||||
' if (target === undefined) target = 4.5;',
|
||||
' var groundLuminance = relativeLuminance(ground);',
|
||||
' if (ratio(relativeLuminance(accent), groundLuminance) >= target) return accent;',
|
||||
' var lch = hexToOklch(accent);',
|
||||
' var L = lch[0], C = lch[1], H = lch[2];',
|
||||
' var darker = ratio(INK_DARK_LUMINANCE, groundLuminance) >= ratio(INK_LIGHT_LUMINANCE, groundLuminance);',
|
||||
' var step = darker ? -0.015 : 0.015;',
|
||||
' for (var lightness = L + step; lightness > 0.03 && lightness < 1; lightness += step) {',
|
||||
' var candidate = oklchToHex([lightness, C, H]);',
|
||||
' if (ratio(relativeLuminance(candidate), groundLuminance) >= target) return candidate;',
|
||||
' }',
|
||||
' return darker ? \'#000000\' : \'#FFFFFF\';',
|
||||
'}',
|
||||
'',
|
||||
'/* ---- fontStack, verbatim from palette-picker.js lines 216-217 */',
|
||||
'var serifFamily = /serif|mincho|baskerville|bitter|marcellus|slab|antiqua|garamond|didot|bodoni/i;',
|
||||
'var fontStack = function (family) {',
|
||||
' return \'"\' + family.replaceAll(\'"\', \'\\\\"\') + \'", \' + (serifFamily.test(family) ? \'serif\' : \'sans-serif\');',
|
||||
'};',
|
||||
'',
|
||||
'/* ---- cell documents */',
|
||||
'var CSS_BLOCKS = PAYLOAD.cssSources.map(function (c) {',
|
||||
' return \'<style data-origin=\' + JSON.stringify(c.from) + \'>\\n\' + c.text + \'\\n<\' + \'/style>\';',
|
||||
'}).join(\'\\n\');',
|
||||
'var fill = function (text, key, value) { return text.split(key).join(value); };',
|
||||
'function buildDoc(cell) {',
|
||||
' var section = PAYLOAD.sections[cell.section];',
|
||||
' var html = cell.variantKey ? section.variants[cell.variantKey] : section.sectionHtml;',
|
||||
' var stageSel = cell.tileIndex ? \'.picker-mode-tile:nth-of-type(\' + cell.tileIndex + \')\' : section.stageSelector;',
|
||||
' var stageCss = stageSel + \' { width: \' + section.dims.width + \'px !important; height: \' + section.dims.height + \'px !important; }\';',
|
||||
' if (cell.tileIndex) {',
|
||||
' stageCss += \'\\n.picker-mode-tile:not(:nth-of-type(\' + cell.tileIndex + \')) { display: none !important; }\';',
|
||||
' }',
|
||||
' var script = fill(fill(PAYLOAD.iframeScript,',
|
||||
' \'__FORM_STATE_JSON__\', JSON.stringify(cell.formState)),',
|
||||
' \'__CELL_JSON__\', JSON.stringify({ section: cell.section, screen: cell.screen, surface: cell.surface, option: cell.option }));',
|
||||
' var doc = PAYLOAD.iframeTemplate;',
|
||||
' doc = fill(doc, \'__SCREEN__\', cell.screen);',
|
||||
' doc = fill(doc, \'__SURFACE__\', cell.surface || \'\');',
|
||||
' doc = fill(doc, \'__OPTION__\', cell.option || \'\');',
|
||||
' doc = fill(doc, \'__FOIL__\', PAYLOAD.foilDataUri);',
|
||||
' doc = fill(doc, \'__STAGE_CSS__\', stageCss);',
|
||||
' doc = fill(doc, \'__EXTERNAL__\', cell.externalHtml);',
|
||||
' doc = fill(doc, \'__CSS__\', CSS_BLOCKS);',
|
||||
' doc = fill(doc, \'__SECTION__\', html);',
|
||||
' doc = fill(doc, \'__SCRIPT__\', script);',
|
||||
' return doc;',
|
||||
'}',
|
||||
'',
|
||||
'/* ---- control bar state and repainting */',
|
||||
'var state = {',
|
||||
' colors: {',
|
||||
' primary: PAYLOAD.palettePresets[0].primary,',
|
||||
' secondary: PAYLOAD.palettePresets[0].secondary,',
|
||||
' tertiary: PAYLOAD.palettePresets[0].tertiary,',
|
||||
' neutral: PAYLOAD.palettePresets[0].neutral,',
|
||||
' },',
|
||||
' font: PAYLOAD.fontPresets[0],',
|
||||
'};',
|
||||
'function paletteVars() {',
|
||||
' var c = state.colors;',
|
||||
' return {',
|
||||
' primary: c.primary, secondary: c.secondary, tertiary: c.tertiary, neutral: c.neutral,',
|
||||
' \'n-ink\': contrastInk(c.neutral), \'p-ink\': contrastInk(c.primary), \'t-ink\': contrastInk(c.tertiary),',
|
||||
' \'p-on-n\': readableOn(c.primary, c.neutral), \'t-on-n\': readableOn(c.tertiary, c.neutral),',
|
||||
' \'t-on-p\': readableOn(c.tertiary, c.primary), \'p-on-p\': readableOn(c.primary, c.primary),',
|
||||
' \'t-on-t\': readableOn(c.tertiary, c.tertiary), \'p-on-i\': readableOn(c.primary, contrastInkHex(c.primary)),',
|
||||
' };',
|
||||
'}',
|
||||
'function applyToFrame(iframe) {',
|
||||
' var win = iframe.contentWindow;',
|
||||
' if (!win || !win.__applyPalette) return;',
|
||||
' win.__applyPalette(paletteVars());',
|
||||
' win.__applyFonts({',
|
||||
' heading: fontStack(state.font.heading.family),',
|
||||
' body: fontStack(state.font.body.family),',
|
||||
' headingWeight: String(state.font.heading.weight),',
|
||||
' });',
|
||||
'}',
|
||||
'function applyAll() {',
|
||||
' Array.prototype.forEach.call(document.querySelectorAll(\'iframe[data-cell]\'), applyToFrame);',
|
||||
'}',
|
||||
'',
|
||||
'/* ---- wire the frames */',
|
||||
'var cellById = {};',
|
||||
'PAYLOAD.cells.forEach(function (cell) { cellById[cell.id] = cell; });',
|
||||
'Array.prototype.forEach.call(document.querySelectorAll(\'iframe[data-cell]\'), function (iframe) {',
|
||||
' var cell = cellById[iframe.getAttribute(\'data-cell\')];',
|
||||
' iframe.addEventListener(\'load\', function () { applyToFrame(iframe); });',
|
||||
' iframe.srcdoc = buildDoc(cell);',
|
||||
' if (cell.section === \'motion\') {',
|
||||
' iframe.closest(\'.cell\').addEventListener(\'mouseenter\', function () {',
|
||||
' try { if (iframe.contentWindow.__replayMotion) iframe.contentWindow.__replayMotion(); } catch (e) {}',
|
||||
' });',
|
||||
' }',
|
||||
'});',
|
||||
'',
|
||||
'/* ---- control bar events */',
|
||||
'function markActive(groupSelector, active) {',
|
||||
' Array.prototype.forEach.call(document.querySelectorAll(groupSelector), function (button) {',
|
||||
' if (button === active) button.setAttribute(\'data-active\', \'\');',
|
||||
' else button.removeAttribute(\'data-active\');',
|
||||
' });',
|
||||
'}',
|
||||
'Array.prototype.forEach.call(document.querySelectorAll(\'[data-palette-preset]\'), function (button) {',
|
||||
' button.addEventListener(\'click\', function () {',
|
||||
' var preset = PAYLOAD.palettePresets[Number(button.getAttribute(\'data-palette-preset\'))];',
|
||||
' state.colors = { primary: preset.primary, secondary: preset.secondary, tertiary: preset.tertiary, neutral: preset.neutral };',
|
||||
' Array.prototype.forEach.call(document.querySelectorAll(\'input[data-role]\'), function (input) {',
|
||||
' input.value = state.colors[input.getAttribute(\'data-role\')];',
|
||||
' });',
|
||||
' markActive(\'[data-palette-preset]\', button);',
|
||||
' applyAll();',
|
||||
' });',
|
||||
'});',
|
||||
'Array.prototype.forEach.call(document.querySelectorAll(\'input[data-role]\'), function (input) {',
|
||||
' input.value = state.colors[input.getAttribute(\'data-role\')];',
|
||||
' input.addEventListener(\'input\', function () {',
|
||||
' state.colors[input.getAttribute(\'data-role\')] = input.value.toUpperCase();',
|
||||
' markActive(\'[data-palette-preset]\', null);',
|
||||
' applyAll();',
|
||||
' });',
|
||||
'});',
|
||||
'Array.prototype.forEach.call(document.querySelectorAll(\'[data-font-preset]\'), function (button) {',
|
||||
' button.addEventListener(\'click\', function () {',
|
||||
' state.font = PAYLOAD.fontPresets[Number(button.getAttribute(\'data-font-preset\'))];',
|
||||
' markActive(\'[data-font-preset]\', button);',
|
||||
' applyAll();',
|
||||
' });',
|
||||
'});',
|
||||
'',
|
||||
'/* ---- uniform downscaling: a cell shrinks as one image when its column is narrower than its captured stage */',
|
||||
'function fitScaler(scaler) {',
|
||||
' var w = parseFloat(scaler.getAttribute(\'data-width\'));',
|
||||
' var h = parseFloat(scaler.getAttribute(\'data-height\'));',
|
||||
' var scale = Math.min(1, scaler.clientWidth / w);',
|
||||
' scaler.firstElementChild.style.transform = \'scale(\' + scale + \')\';',
|
||||
' scaler.style.height = (h * scale) + \'px\';',
|
||||
'}',
|
||||
'var scalerObserver = new ResizeObserver(function (entries) {',
|
||||
' entries.forEach(function (entry) { fitScaler(entry.target); });',
|
||||
'});',
|
||||
'Array.prototype.forEach.call(document.querySelectorAll(\'.scaler\'), function (scaler) {',
|
||||
' fitScaler(scaler);',
|
||||
' scalerObserver.observe(scaler);',
|
||||
'});',
|
||||
].join('\n');
|
||||
|
||||
const HOST_CSS = [
|
||||
':root { color-scheme: dark; }',
|
||||
'body { margin: 0; padding: 0 40px 64px; background: oklch(0.07 0.006 95); color: #cfc9ba;',
|
||||
' font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }',
|
||||
'h1 { font-size: 18px; font-weight: 600; margin: 24px 0 6px; color: #efe9da; }',
|
||||
'p.note { margin: 0 0 20px; opacity: .7; }',
|
||||
'h2 { font-size: 15px; font-weight: 600; margin: 44px 0 14px; color: #efe9da; }',
|
||||
'.controls { position: sticky; top: 0; z-index: 10; background: #1a1712; border-bottom: 1px solid #363025;',
|
||||
' padding: 12px 40px; margin: 0 -40px 12px; display: flex; flex-direction: column; gap: 8px; }',
|
||||
'.controls .row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }',
|
||||
'.controls .lbl { font-size: 11px; letter-spacing: .14em; text-transform: uppercase; opacity: .6; width: 70px; flex: none; }',
|
||||
'.controls button { background: #2a251c; color: #efe9da; border: 1px solid #4a4232; padding: 6px 10px;',
|
||||
' cursor: pointer; font: inherit; font-size: 13px; }',
|
||||
'.controls button[data-active] { border-color: #c8b57c; }',
|
||||
'.controls .sw { display: inline-block; width: 11px; height: 11px; margin-right: 2px; vertical-align: -1px; }',
|
||||
'.controls label { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; opacity: .85; }',
|
||||
'.controls input[type="color"] { width: 30px; height: 22px; padding: 0; border: 1px solid #4a4232; background: none; }',
|
||||
'.controls a { color: #c8b57c; text-decoration: none; font-size: 13px; margin-right: 8px; }',
|
||||
'.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(var(--cell-min, 560px), 100%), 1fr));',
|
||||
' gap: 36px 28px; }',
|
||||
'figure.cell { margin: 0; min-width: 0; }',
|
||||
'figure.cell figcaption { margin: 0 0 8px; font-size: 13px; letter-spacing: .02em; opacity: .85; }',
|
||||
'figure.cell .scaler { position: relative; width: 100%; }',
|
||||
'figure.cell .clip { overflow: hidden; position: absolute; top: 0; left: 0; transform-origin: top left; }',
|
||||
'figure.cell iframe { display: block; border: 0; background: transparent; position: absolute; top: 0; left: 0; }',
|
||||
].join('\n');
|
||||
|
||||
async function main() {
|
||||
const capture = JSON.parse(await readFile(OUT_DIR + '/capture.json', 'utf8'));
|
||||
|
||||
/* Guard: everything the cells need must be in the capture, and no network
|
||||
URL may survive in the CSS. */
|
||||
for (const section of SECTIONS) {
|
||||
const got = capture.sections[section.id];
|
||||
if (!got) throw new Error('capture.json is missing section ' + section.id);
|
||||
if (!got.dims) throw new Error('capture.json is missing dims for ' + section.id);
|
||||
if (section.id === 'palette' || section.id === 'icons') {
|
||||
if (!got.variants || Object.keys(got.variants).length !== section.cells.length) {
|
||||
throw new Error('capture.json has incomplete variants for ' + section.id);
|
||||
}
|
||||
} else if (!got.sectionHtml) {
|
||||
throw new Error('capture.json is missing sectionHtml for ' + section.id);
|
||||
}
|
||||
}
|
||||
if (!capture.cssSources || capture.cssSources.length === 0) throw new Error('capture.json has no cssSources');
|
||||
for (const source of capture.cssSources) {
|
||||
if (/url\((['"]?)https?:/.test(source.text)) {
|
||||
throw new Error('a network url() survived in stylesheet ' + source.from + '; re-run the capture');
|
||||
}
|
||||
}
|
||||
|
||||
/* Neutralize captured <img> tags so no cell fetches anything. */
|
||||
const sections = {};
|
||||
for (const [id, section] of Object.entries(capture.sections)) {
|
||||
sections[id] = {
|
||||
screen: section.screen,
|
||||
stageSelector: section.stageSelector,
|
||||
dims: section.dims,
|
||||
sectionHtml: section.sectionHtml ? neutralizeImages(section.sectionHtml) : undefined,
|
||||
variants: section.variants
|
||||
? Object.fromEntries(Object.entries(section.variants).map(([k, v]) => [k, neutralizeImages(v)]))
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const cells = CELL_LIST.map((cell) => ({ ...cell, externalHtml: externalInputs(cell) }));
|
||||
|
||||
const payload = {
|
||||
viewport: capture.viewport,
|
||||
foilDataUri: capture.foilDataUri,
|
||||
cssSources: capture.cssSources,
|
||||
sections,
|
||||
cells,
|
||||
palettePresets: PALETTE_PRESETS,
|
||||
fontPresets: FONT_PRESETS,
|
||||
iframeTemplate: IFRAME_TEMPLATE,
|
||||
iframeScript: IFRAME_SCRIPT,
|
||||
};
|
||||
/* All '<' inside the JSON become \u003c so the payload can sit inside a
|
||||
script tag with no risk of a premature close tag. */
|
||||
const payloadJson = JSON.stringify(payload).replace(/</g, '\\u003c');
|
||||
|
||||
const swatches = (preset) =>
|
||||
['primary', 'secondary', 'tertiary', 'neutral']
|
||||
.map((role) => '<span class="sw" style="background:' + preset[role] + '"></span>')
|
||||
.join('');
|
||||
|
||||
const controls = [
|
||||
'<div class="controls">',
|
||||
'<div class="row"><span class="lbl">Palette</span>',
|
||||
...PALETTE_PRESETS.map((preset, index) =>
|
||||
'<button type="button" data-palette-preset="' + index + '"' + (index === 0 ? ' data-active' : '') + '>'
|
||||
+ swatches(preset) + ' ' + (index + 1) + ' · ' + preset.id + '</button>'),
|
||||
...['primary', 'secondary', 'tertiary', 'neutral'].map((role) =>
|
||||
'<label>' + role.charAt(0).toUpperCase() + role.slice(1)
|
||||
+ ' <input type="color" data-role="' + role + '"></label>'),
|
||||
'</div>',
|
||||
'<div class="row"><span class="lbl">Fonts</span>',
|
||||
...FONT_PRESETS.map((preset, index) =>
|
||||
'<button type="button" data-font-preset="' + index + '"' + (index === 0 ? ' data-active' : '') + '>'
|
||||
+ (index + 1) + ' · ' + preset.name + '</button>'),
|
||||
'</div>',
|
||||
'<div class="row"><span class="lbl">Jump to</span>',
|
||||
...SECTIONS.map((section) => '<a href="#section-' + section.id + '">' + section.title.replace(/ \(screen .*\)/, '') + '</a>'),
|
||||
'</div>',
|
||||
'</div>',
|
||||
].join('\n');
|
||||
|
||||
const sectionsHtml = SECTIONS.map((section) => {
|
||||
const dims = capture.sections[section.id].dims;
|
||||
const cellsHtml = cells.filter((cell) => cell.section === section.id).map((cell) => [
|
||||
'<figure class="cell">',
|
||||
'<figcaption>' + cell.caption + '</figcaption>',
|
||||
'<div class="scaler" data-width="' + dims.width + '" data-height="' + dims.height + '">',
|
||||
'<div class="clip" style="width:' + dims.width + 'px;height:' + dims.height + 'px">',
|
||||
'<iframe title="' + cell.caption.replace(/"/g, '"') + '" data-cell="' + cell.id
|
||||
+ '" data-section="' + cell.section + '" scrolling="no" style="width:'
|
||||
+ capture.viewport.width + 'px;height:' + capture.viewport.height + 'px"></iframe>',
|
||||
'</div>',
|
||||
'</div>',
|
||||
'</figure>',
|
||||
].join('\n')).join('\n');
|
||||
return '<section id="section-' + section.id + '">\n<h2>' + section.title + '</h2>\n<div class="grid" style="--cell-min:' + Math.ceil(dims.width) + 'px">\n'
|
||||
+ cellsHtml + '\n</div>\n</section>';
|
||||
}).join('\n');
|
||||
|
||||
const html = [
|
||||
'<!doctype html>',
|
||||
'<!-- impeccable-disable design-system-font, overused-font, single-font, flat-type-hierarchy, design-system-color, design-system-font-size, design-system-radius, side-tab, marquee, repeating-stripes-gradient -- standalone extraction fixture: verbatim ground-truth copy of the questionnaire previews (live-captured markup + built CSS); the host shell is minimal chrome around 86 iframes. Generated by tmp/questionaire/build-gallery.mjs; do not hand-edit. -->',
|
||||
'<html lang="en">',
|
||||
'<head>',
|
||||
'<meta charset="utf-8">',
|
||||
'<title>Questionnaire previews, ground-truth gallery</title>',
|
||||
'<style>',
|
||||
HOST_CSS,
|
||||
'</style>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
'<h1>Questionnaire previews</h1>',
|
||||
'<p class="note">Ground-truth extraction of every questionnaire preview except type scale and iconography: 75 cells, one per valid surface and option combination. The control bar repaints every cell through the picker\'s own custom-property machinery; hover a Motion cell to restart its scene from the first frame.</p>',
|
||||
controls,
|
||||
sectionsHtml,
|
||||
'<script type="application/json" id="gallery-payload">',
|
||||
payloadJson,
|
||||
'<\/script>',
|
||||
'<script>',
|
||||
HOST_RUNTIME,
|
||||
'<\/script>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
].join('\n');
|
||||
|
||||
await writeFile(OUT_DIR + '/previews-gallery.html', html);
|
||||
console.log('wrote previews-gallery.html (' + (html.length / 1024 / 1024).toFixed(1) + ' MB, ' + cells.length + ' cells)');
|
||||
}
|
||||
|
||||
main().catch((error) => { console.error(error); process.exit(1); });
|
||||
@@ -0,0 +1,246 @@
|
||||
|
||||
/* Plan 6 capture script. Boots the real picker for the hanazono-atelier
|
||||
project, walks the questionnaire, and writes ground truth for the previews
|
||||
gallery: per-screen section HTML, verbatim stylesheets with every url()
|
||||
resource inlined as a data URI, measured stage boxes, and one reference
|
||||
screenshot per cell. It writes ONLY capture.json and refs/*.png inside
|
||||
tmp/questionaire/. It never submits the picker form and it kills only the
|
||||
server it spawned. */
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { chromium } from 'playwright';
|
||||
import { CELL_LIST, SECTIONS, VIEWPORT, refName } from './gallery-cells.mjs';
|
||||
|
||||
const SERVER = '/Users/abdulwahab/impeccable/skill/scripts/picker-server.mjs';
|
||||
const PROJECT = '/Users/abdulwahab/hanazono-atelier';
|
||||
const OUT_DIR = '/Users/abdulwahab/impeccable/tmp/questionaire';
|
||||
const REFS_DIR = OUT_DIR + '/refs';
|
||||
|
||||
function startServer() {
|
||||
const child = spawn('node', [SERVER], { cwd: PROJECT, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
const url = new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error('picker-server did not print PICKER_URL within 30s')), 30000);
|
||||
let buffer = '';
|
||||
child.stdout.on('data', (chunk) => {
|
||||
buffer += String(chunk);
|
||||
const match = buffer.match(/PICKER_URL\s+(\S+)/);
|
||||
if (match) { clearTimeout(timer); resolve(match[1]); }
|
||||
});
|
||||
child.on('exit', (code) => reject(new Error('picker-server exited early (code ' + code + ')')));
|
||||
});
|
||||
return { child, url };
|
||||
}
|
||||
|
||||
async function fetchDataUri(absoluteUrl) {
|
||||
const res = await fetch(absoluteUrl);
|
||||
if (!res.ok) throw new Error('resource fetch failed: ' + absoluteUrl + ' (' + res.status + ')');
|
||||
const mime = (res.headers.get('content-type') || 'application/octet-stream').split(';')[0];
|
||||
const buf = Buffer.from(await res.arrayBuffer());
|
||||
return 'data:' + mime + ';base64,' + buf.toString('base64');
|
||||
}
|
||||
|
||||
/* Rewrite every url(...) in a stylesheet to a data URI so the gallery makes
|
||||
zero network requests. data: and fragment references are left alone. */
|
||||
async function inlineUrls(cssText, baseUrl) {
|
||||
const refs = [...new Set(
|
||||
[...cssText.matchAll(/url\((['"]?)([^)'"]+)\1\)/g)].map((m) => m[2]),
|
||||
)].filter((u) => !u.startsWith('data:') && !u.startsWith('#'));
|
||||
let out = cssText;
|
||||
for (const ref of refs) {
|
||||
const dataUri = await fetchDataUri(new URL(ref, baseUrl).href);
|
||||
for (const quoted of ['url(' + ref + ')', "url('" + ref + "')", 'url("' + ref + '")']) {
|
||||
out = out.split(quoted).join('url(' + dataUri + ')');
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await mkdir(REFS_DIR, { recursive: true });
|
||||
const { child, url: urlPromise } = startServer();
|
||||
process.on('exit', () => { try { child.kill(); } catch {} });
|
||||
const pickerUrl = await urlPromise;
|
||||
console.log('picker at', pickerUrl);
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: VIEWPORT });
|
||||
const capture = { viewport: VIEWPORT, pickerUrl, capturedAt: new Date().toISOString(), sections: {} };
|
||||
|
||||
try {
|
||||
await page.goto(pickerUrl);
|
||||
await page.waitForSelector('.picker-screen[data-screen="01"][data-active]');
|
||||
|
||||
const gotoScreen = async (id) => {
|
||||
await page.evaluate((screen) => {
|
||||
document.dispatchEvent(new CustomEvent('picker:goto', { detail: { screen } }));
|
||||
}, id);
|
||||
await page.waitForSelector('.picker-screen[data-screen="' + id + '"][data-active]');
|
||||
await page.waitForTimeout(400);
|
||||
};
|
||||
const sectionHtml = (id) => page.evaluate(
|
||||
(screen) => document.querySelector('.picker-screen[data-screen="' + screen + '"]').outerHTML, id);
|
||||
const dimsOf = (selector) => page.evaluate((q) => {
|
||||
const el = document.querySelector(q);
|
||||
if (!el) throw new Error('missing stage: ' + q);
|
||||
const r = el.getBoundingClientRect();
|
||||
return { width: r.width, height: r.height };
|
||||
}, selector);
|
||||
const setSurfaces = (values) => page.evaluate((wanted) => {
|
||||
for (const input of document.querySelectorAll('input[name="surface-modes"]')) {
|
||||
if (input.checked !== wanted.includes(input.value)) input.click();
|
||||
}
|
||||
}, values);
|
||||
const clickRadio = (group, value) => page.evaluate(([g, v]) => {
|
||||
const input = document.querySelector('input[name="' + g + '"][value="' + v + '"]');
|
||||
if (!input) throw new Error('missing radio ' + g + '=' + v);
|
||||
input.click();
|
||||
}, [group, value]);
|
||||
const clickTab = (screen, surface) => page.evaluate(([s, mode]) => {
|
||||
const tab = document.querySelector('.picker-screen[data-screen="' + s + '"] [data-surface-tab="' + mode + '"]');
|
||||
if (!tab) throw new Error('missing surface tab ' + mode + ' on screen ' + s);
|
||||
tab.click();
|
||||
}, [screen, surface]);
|
||||
const applyFormState = (formState) => page.evaluate((state) => {
|
||||
for (const [group, value] of Object.entries(state)) {
|
||||
const input = document.querySelector('input[name="' + group + '"][value="' + value + '"]');
|
||||
if (input && !input.checked) input.click();
|
||||
}
|
||||
}, formState);
|
||||
const neutralizeLiveImages = () => page.evaluate(() => {
|
||||
const blank = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
||||
for (const img of document.querySelectorAll('img')) img.src = blank;
|
||||
});
|
||||
const shoot = async (screen, stageSelector, file) => {
|
||||
await neutralizeLiveImages();
|
||||
const selector = '.picker-screen[data-screen="' + screen + '"] ' + stageSelector;
|
||||
await page.locator(selector).first().screenshot({ path: REFS_DIR + '/' + file });
|
||||
};
|
||||
const bySection = (id) => SECTIONS.find((s) => s.id === id);
|
||||
const cellsOf = (id) => CELL_LIST.filter((c) => c.section === id);
|
||||
|
||||
/* ---- Screen 01b: all four surfaces on, capture section + tile refs. */
|
||||
await gotoScreen('01b');
|
||||
await setSurfaces(['persuade', 'operate', 'read', 'experience']);
|
||||
await page.waitForTimeout(300);
|
||||
capture.sections.surfaces = {
|
||||
screen: '01b',
|
||||
stageSelector: bySection('surfaces').stageSelector,
|
||||
dims: await dimsOf('.picker-screen[data-screen="01b"] .picker-modes-grid .picker-mode-tile'),
|
||||
sectionHtml: await sectionHtml('01b'),
|
||||
};
|
||||
for (const cell of cellsOf('surfaces')) {
|
||||
await shoot('01b', '.picker-modes-grid .picker-mode-tile:nth-of-type(' + cell.tileIndex + ')', refName(cell));
|
||||
}
|
||||
console.log('captured surfaces');
|
||||
|
||||
/* ---- Screen 02: one visit per surface so the panel preview is that
|
||||
surface's drawing. The deck rests on the first cue card (zinc-dawn). */
|
||||
capture.sections.palette = {
|
||||
screen: '02',
|
||||
stageSelector: bySection('palette').stageSelector,
|
||||
dims: null,
|
||||
variants: {},
|
||||
};
|
||||
for (const cell of cellsOf('palette')) {
|
||||
await gotoScreen('01b');
|
||||
await setSurfaces([cell.surface]);
|
||||
await page.waitForTimeout(300);
|
||||
await gotoScreen('02');
|
||||
await page.waitForSelector('.picker-card-layer .picker-card');
|
||||
await page.waitForSelector('[data-select-palette]:not([disabled])');
|
||||
await page.waitForTimeout(400);
|
||||
capture.sections.palette.variants[cell.surface] = await sectionHtml('02');
|
||||
if (!capture.sections.palette.dims) {
|
||||
capture.sections.palette.dims = await dimsOf('.picker-screen[data-screen="02"] .picker-palette-panel .picker-preview');
|
||||
}
|
||||
await shoot('02', '.picker-palette-panel .picker-preview', refName(cell));
|
||||
}
|
||||
console.log('captured palette');
|
||||
|
||||
/* ---- Restore all four surfaces, then commit the palette. Clicking the
|
||||
select button advances to screen 03; it does not submit the form. */
|
||||
await gotoScreen('01b');
|
||||
await setSurfaces(['persuade', 'operate', 'read', 'experience']);
|
||||
await page.waitForTimeout(300);
|
||||
await gotoScreen('02');
|
||||
await page.waitForSelector('[data-select-palette]:not([disabled])');
|
||||
await page.click('[data-select-palette]');
|
||||
await page.waitForSelector('.picker-screen[data-screen="03"][data-active]');
|
||||
await page.waitForSelector('.picker-strategy-stage > .picker-preview[data-surface="persuade"]');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
/* ---- Stylesheets, verbatim and in document order, captured once. */
|
||||
const rawCss = await page.evaluate(async () => {
|
||||
const out = [];
|
||||
for (const sheet of document.styleSheets) {
|
||||
if (sheet.href) {
|
||||
const text = await fetch(sheet.href).then((r) => {
|
||||
if (!r.ok) throw new Error('css fetch failed: ' + sheet.href);
|
||||
return r.text();
|
||||
});
|
||||
out.push({ from: sheet.href, text });
|
||||
} else if (sheet.ownerNode && sheet.ownerNode.textContent) {
|
||||
out.push({ from: 'inline', text: sheet.ownerNode.textContent });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
});
|
||||
capture.cssSources = [];
|
||||
for (const source of rawCss) {
|
||||
const base = source.from === 'inline' ? pickerUrl : source.from;
|
||||
capture.cssSources.push({ from: source.from, text: await inlineUrls(source.text, base) });
|
||||
}
|
||||
capture.foilDataUri = await fetchDataUri(new URL('/assets/kinpaku-gold-leaf.jpg', pickerUrl).href);
|
||||
console.log('captured ' + capture.cssSources.length + ' stylesheets');
|
||||
|
||||
/* ---- The tab strips stay visible: dims and reference screenshots are
|
||||
taken on each section's preview element (previewSelector), which sits in
|
||||
the stage row below the strip and never contains it. Hiding the strip
|
||||
before measuring is what plan 6 got wrong: on screens 03 and 04 the
|
||||
collapsed tab row hands its height to the preview, which then measures
|
||||
taller than anything a visitor ever sees. */
|
||||
await page.evaluate(() => {
|
||||
const blank = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
||||
for (const img of document.querySelectorAll('img')) img.src = blank;
|
||||
});
|
||||
|
||||
/* ---- Screens 03 through 10: capture the section once on arrival, then
|
||||
per cell open the surface tab, click the option, settle, screenshot. */
|
||||
for (const sectionId of ['strategy', 'font-pair', 'motion', 'layout', 'boundaries', 'corners', 'depth']) {
|
||||
const section = bySection(sectionId);
|
||||
await gotoScreen(section.screen);
|
||||
if (sectionId === 'font-pair') {
|
||||
await page.waitForSelector('input[name="font-pair"]');
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
}
|
||||
await page.waitForTimeout(600);
|
||||
const measureSelector = section.previewSelector || section.stageSelector;
|
||||
capture.sections[sectionId] = {
|
||||
screen: section.screen,
|
||||
stageSelector: section.stageSelector,
|
||||
dims: await dimsOf('.picker-screen[data-screen="' + section.screen + '"] ' + measureSelector),
|
||||
sectionHtml: await sectionHtml(section.screen),
|
||||
};
|
||||
for (const cell of cellsOf(sectionId)) {
|
||||
await clickTab(section.screen, cell.surface);
|
||||
await applyFormState(cell.formState);
|
||||
await page.waitForTimeout(sectionId === 'motion' ? 700 : 350);
|
||||
await shoot(section.screen, measureSelector, refName(cell));
|
||||
}
|
||||
console.log('captured ' + sectionId);
|
||||
}
|
||||
|
||||
/* Screen 11 (iconography) left the gallery when its section left
|
||||
gallery-cells.mjs; capturing it here read a section that no longer
|
||||
exists and crashed the walk before capture.json was written. */
|
||||
await writeFile(OUT_DIR + '/capture.json', JSON.stringify(capture));
|
||||
console.log('wrote capture.json; sections: ' + Object.keys(capture.sections).join(', '));
|
||||
console.log('refs written: ' + CELL_LIST.length);
|
||||
} finally {
|
||||
await browser.close();
|
||||
child.kill();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => { console.error(error); process.exit(1); });
|
||||
@@ -0,0 +1,205 @@
|
||||
|
||||
/* Shared inventory for the previews gallery (plan-6). Used by
|
||||
capture-previews.mjs, build-gallery.mjs, and verify-previews-gallery.mjs.
|
||||
All values are quoted from the picker source and the hanazono-atelier run;
|
||||
see tmp/questionaire/plans/plan-6-previews-gallery.md for the anchors. */
|
||||
|
||||
export const VIEWPORT = { width: 1600, height: 1000 };
|
||||
|
||||
export const SURFACE_LABEL = {
|
||||
persuade: 'Landing page',
|
||||
operate: 'App UI',
|
||||
read: 'Docs',
|
||||
experience: 'Portfolio',
|
||||
};
|
||||
|
||||
export const TILE_INDEX = { persuade: 1, operate: 2, read: 3, experience: 4 };
|
||||
|
||||
export const PALETTE_PRESETS = [
|
||||
{ id: 'zinc-dawn', primary: '#175558', secondary: '#587570', tertiary: '#C9534C', neutral: '#E9EBEA' },
|
||||
{ id: 'lacquer-glow', primary: '#1E1914', secondary: '#6D5848', tertiary: '#D5842C', neutral: '#F1EBE2' },
|
||||
{ id: 'margin-stems', primary: '#5D4975', secondary: '#6A8E7A', tertiary: '#2E8562', neutral: '#EEEAE4' },
|
||||
{ id: 'clay-bench', primary: '#C65A36', secondary: '#9A7056', tertiary: '#EA3871', neutral: '#F3E6DD' },
|
||||
];
|
||||
|
||||
export const FONT_PRESETS = [
|
||||
{ id: 'cormorant-source', name: 'Cormorant + Source Sans 3', heading: { family: 'Cormorant Garamond', weight: 600 }, body: { family: 'Source Sans 3', weight: 400 } },
|
||||
{ id: 'playfair-work', name: 'Playfair + Work Sans', heading: { family: 'Playfair Display', weight: 600 }, body: { family: 'Work Sans', weight: 400 } },
|
||||
{ id: 'libre-plex', name: 'Libre Baskerville + IBM Plex Sans', heading: { family: 'Libre Baskerville', weight: 700 }, body: { family: 'IBM Plex Sans', weight: 400 } },
|
||||
{ id: 'source-serif-pair', name: 'Source Serif 4 + Source Sans 3', heading: { family: 'Source Serif 4', weight: 600 }, body: { family: 'Source Sans 3', weight: 400 } },
|
||||
];
|
||||
|
||||
/* Per-surface defaults for every radio group the picker CSS reads, plus the
|
||||
flat groups. Motion and layout are not asked of operate/read; the live
|
||||
picker parks their radios on the leading applicable surface (persuade). */
|
||||
export const DEFAULTS = {
|
||||
persuade: { 'color-strategy': 'full-palette', 'motion-energy': 'responsive', 'layout-structure': 'balanced', 'boundary-style': 'open-space', 'corner-style': 'slightly-soft', 'depth-style': 'soft-lift', 'type-scale': 'major-third', 'font-pair': 'cormorant-source', 'icon-pack': 'lucide' },
|
||||
operate: { 'color-strategy': 'full-palette', 'motion-energy': 'responsive', 'layout-structure': 'balanced', 'boundary-style': 'surface-changes', 'corner-style': 'slightly-soft', 'depth-style': 'flat', 'type-scale': 'major-third', 'font-pair': 'cormorant-source', 'icon-pack': 'lucide' },
|
||||
read: { 'color-strategy': 'full-palette', 'motion-energy': 'responsive', 'layout-structure': 'balanced', 'boundary-style': 'open-space', 'corner-style': 'slightly-soft', 'depth-style': 'flat', 'type-scale': 'major-third', 'font-pair': 'cormorant-source', 'icon-pack': 'lucide' },
|
||||
experience: { 'color-strategy': 'restrained', 'motion-energy': 'choreographed', 'layout-structure': 'simple-grid', 'boundary-style': 'open-space', 'corner-style': 'sharp', 'depth-style': 'flat', 'type-scale': 'major-third', 'font-pair': 'cormorant-source', 'icon-pack': 'lucide' },
|
||||
};
|
||||
|
||||
/* Sections in questionnaire order, cells fully enumerated. Captions are the
|
||||
authoritative labels from plan-6's inventory tables. */
|
||||
export const SECTIONS = [
|
||||
{
|
||||
id: 'surfaces', screen: '01b', title: 'Surfaces (screen 01b)', group: null,
|
||||
stageSelector: '.picker-mode-tile',
|
||||
cells: [
|
||||
{ surface: 'persuade', option: null, caption: 'Selected: Landing page' },
|
||||
{ surface: 'operate', option: null, caption: 'Selected: App UI' },
|
||||
{ surface: 'read', option: null, caption: 'Selected: Docs' },
|
||||
{ surface: 'experience', option: null, caption: 'Selected: Portfolio' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'palette', screen: '02', title: 'Color palette (screen 02)', group: null,
|
||||
stageSelector: '.picker-palette-panel .picker-preview',
|
||||
cells: [
|
||||
{ surface: 'persuade', option: null, caption: 'Selected: Landing page + Palette preview' },
|
||||
{ surface: 'operate', option: null, caption: 'Selected: App UI + Palette preview' },
|
||||
{ surface: 'read', option: null, caption: 'Selected: Docs + Palette preview' },
|
||||
{ surface: 'experience', option: null, caption: 'Selected: Portfolio + Palette preview' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'strategy', screen: '03', title: 'Color strategy (screen 03)', group: 'color-strategy',
|
||||
stageSelector: '.picker-strategy-stage',
|
||||
previewSelector: '.picker-strategy-stage > .picker-preview:not([hidden])',
|
||||
cells: [
|
||||
{ surface: 'persuade', option: 'restrained', caption: 'Selected: Landing page + Restrained' },
|
||||
{ surface: 'persuade', option: 'committed', caption: 'Selected: Landing page + Committed' },
|
||||
{ surface: 'persuade', option: 'full-palette', caption: 'Selected: Landing page + Full palette' },
|
||||
{ surface: 'persuade', option: 'drenched', caption: 'Selected: Landing page + Drenched' },
|
||||
{ surface: 'operate', option: 'restrained', caption: 'Selected: App UI + Restrained' },
|
||||
{ surface: 'operate', option: 'committed', caption: 'Selected: App UI + Committed' },
|
||||
{ surface: 'operate', option: 'full-palette', caption: 'Selected: App UI + Full palette' },
|
||||
{ surface: 'read', option: 'restrained', caption: 'Selected: Docs + Restrained' },
|
||||
{ surface: 'read', option: 'committed', caption: 'Selected: Docs + Committed' },
|
||||
{ surface: 'read', option: 'full-palette', caption: 'Selected: Docs + Full palette' },
|
||||
{ surface: 'experience', option: 'restrained', caption: 'Selected: Portfolio + Restrained' },
|
||||
{ surface: 'experience', option: 'committed', caption: 'Selected: Portfolio + Committed' },
|
||||
{ surface: 'experience', option: 'drenched', caption: 'Selected: Portfolio + Drenched' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'font-pair', screen: '04', title: 'Font pair (screen 04)', group: 'font-pair',
|
||||
stageSelector: '.picker-type-stage',
|
||||
previewSelector: '.picker-type-stage > .picker-artboard:not([hidden])',
|
||||
cells: [
|
||||
{ surface: 'persuade', option: null, caption: 'Selected: Landing page + Font pair' },
|
||||
{ surface: 'operate', option: null, caption: 'Selected: App UI + Font pair' },
|
||||
{ surface: 'read', option: null, caption: 'Selected: Docs + Font pair' },
|
||||
{ surface: 'experience', option: null, caption: 'Selected: Portfolio + Font pair' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'motion', screen: '06', title: 'Motion (screen 06)', group: 'motion-energy',
|
||||
stageSelector: '.picker-board-stage',
|
||||
previewSelector: '.picker-board-stage > .picker-artboard:not([hidden])',
|
||||
cells: [
|
||||
{ surface: 'persuade', option: 'restrained', caption: 'Selected: Landing page + Restrained' },
|
||||
{ surface: 'persuade', option: 'responsive', caption: 'Selected: Landing page + Responsive' },
|
||||
{ surface: 'persuade', option: 'choreographed', caption: 'Selected: Landing page + Choreographed' },
|
||||
{ surface: 'experience', option: 'restrained', caption: 'Selected: Portfolio + Restrained' },
|
||||
{ surface: 'experience', option: 'responsive', caption: 'Selected: Portfolio + Responsive' },
|
||||
{ surface: 'experience', option: 'choreographed', caption: 'Selected: Portfolio + Choreographed' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'layout', screen: '07', title: 'Layout (screen 07)', group: 'layout-structure',
|
||||
stageSelector: '.picker-board-stage',
|
||||
previewSelector: '.picker-board-stage > .picker-artboard:not([hidden])',
|
||||
cells: [
|
||||
{ surface: 'persuade', option: 'simple-grid', caption: 'Selected: Landing page + Simple grid' },
|
||||
{ surface: 'persuade', option: 'balanced', caption: 'Selected: Landing page + Balanced' },
|
||||
{ surface: 'persuade', option: 'freeform', caption: 'Selected: Landing page + Freeform' },
|
||||
{ surface: 'experience', option: 'simple-grid', caption: 'Selected: Portfolio + Simple grid' },
|
||||
{ surface: 'experience', option: 'balanced', caption: 'Selected: Portfolio + Balanced' },
|
||||
{ surface: 'experience', option: 'freeform', caption: 'Selected: Portfolio + Freeform' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'boundaries', screen: '08', title: 'Boundaries (screen 08)', group: 'boundary-style',
|
||||
stageSelector: '.picker-board-stage',
|
||||
previewSelector: '.picker-board-stage > .picker-artboard:not([hidden])',
|
||||
cells: [
|
||||
{ surface: 'persuade', option: 'open-space', caption: 'Selected: Landing page + Open space' },
|
||||
{ surface: 'persuade', option: 'thin-dividers', caption: 'Selected: Landing page + Thin dividers' },
|
||||
{ surface: 'persuade', option: 'surface-changes', caption: 'Selected: Landing page + Surface changes' },
|
||||
{ surface: 'persuade', option: 'cards-and-panels', caption: 'Selected: Landing page + Cards and panels' },
|
||||
{ surface: 'operate', option: 'thin-dividers', caption: 'Selected: App UI + Thin dividers' },
|
||||
{ surface: 'operate', option: 'surface-changes', caption: 'Selected: App UI + Surface changes' },
|
||||
{ surface: 'operate', option: 'cards-and-panels', caption: 'Selected: App UI + Cards and panels' },
|
||||
{ surface: 'read', option: 'open-space', caption: 'Selected: Docs + Open space' },
|
||||
{ surface: 'read', option: 'thin-dividers', caption: 'Selected: Docs + Thin dividers' },
|
||||
{ surface: 'read', option: 'surface-changes', caption: 'Selected: Docs + Surface changes' },
|
||||
{ surface: 'experience', option: 'open-space', caption: 'Selected: Portfolio + Open space' },
|
||||
{ surface: 'experience', option: 'thin-dividers', caption: 'Selected: Portfolio + Thin dividers' },
|
||||
{ surface: 'experience', option: 'surface-changes', caption: 'Selected: Portfolio + Surface changes' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'corners', screen: '09', title: 'Corners (screen 09)', group: 'corner-style',
|
||||
stageSelector: '.picker-board-stage',
|
||||
previewSelector: '.picker-board-stage > .picker-artboard:not([hidden])',
|
||||
cells: [
|
||||
{ surface: 'persuade', option: 'sharp', caption: 'Selected: Landing page + Sharp' },
|
||||
{ surface: 'persuade', option: 'slightly-soft', caption: 'Selected: Landing page + Slightly soft' },
|
||||
{ surface: 'persuade', option: 'friendly', caption: 'Selected: Landing page + Friendly' },
|
||||
{ surface: 'persuade', option: 'pill', caption: 'Selected: Landing page + Pill-like' },
|
||||
{ surface: 'operate', option: 'sharp', caption: 'Selected: App UI + Sharp' },
|
||||
{ surface: 'operate', option: 'slightly-soft', caption: 'Selected: App UI + Slightly soft' },
|
||||
{ surface: 'operate', option: 'friendly', caption: 'Selected: App UI + Friendly' },
|
||||
{ surface: 'operate', option: 'pill', caption: 'Selected: App UI + Pill-like' },
|
||||
{ surface: 'read', option: 'sharp', caption: 'Selected: Docs + Sharp' },
|
||||
{ surface: 'read', option: 'slightly-soft', caption: 'Selected: Docs + Slightly soft' },
|
||||
{ surface: 'read', option: 'friendly', caption: 'Selected: Docs + Friendly' },
|
||||
{ surface: 'read', option: 'pill', caption: 'Selected: Docs + Pill-like' },
|
||||
{ surface: 'experience', option: 'sharp', caption: 'Selected: Portfolio + Sharp' },
|
||||
{ surface: 'experience', option: 'slightly-soft', caption: 'Selected: Portfolio + Slightly soft' },
|
||||
{ surface: 'experience', option: 'friendly', caption: 'Selected: Portfolio + Friendly' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'depth', screen: '10', title: 'Depth (screen 10)', group: 'depth-style',
|
||||
stageSelector: '.picker-board-stage',
|
||||
previewSelector: '.picker-board-stage > .picker-artboard:not([hidden])',
|
||||
cells: [
|
||||
{ surface: 'persuade', option: 'flat', caption: 'Selected: Landing page + Flat' },
|
||||
{ surface: 'persuade', option: 'soft-lift', caption: 'Selected: Landing page + Soft lift' },
|
||||
{ surface: 'persuade', option: 'floating', caption: 'Selected: Landing page + Floating' },
|
||||
{ surface: 'operate', option: 'flat', caption: 'Selected: App UI + Flat' },
|
||||
{ surface: 'operate', option: 'soft-lift', caption: 'Selected: App UI + Soft lift' },
|
||||
{ surface: 'read', option: 'flat', caption: 'Selected: Docs + Flat' },
|
||||
{ surface: 'read', option: 'soft-lift', caption: 'Selected: Docs + Soft lift' },
|
||||
{ surface: 'experience', option: 'flat', caption: 'Selected: Portfolio + Flat' },
|
||||
{ surface: 'experience', option: 'soft-lift', caption: 'Selected: Portfolio + Soft lift' },
|
||||
{ surface: 'experience', option: 'floating', caption: 'Selected: Portfolio + Floating' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/* Flat list with ids, per-cell form state, and builder metadata. */
|
||||
export const CELL_LIST = [];
|
||||
for (const section of SECTIONS) {
|
||||
for (const cell of section.cells) {
|
||||
const id = section.id + '--' + (cell.surface ?? 'all') + '--' + (cell.option ?? 'default');
|
||||
const formState = { ...DEFAULTS[cell.surface ?? 'persuade'] };
|
||||
if (section.group && cell.option) formState[section.group] = cell.option;
|
||||
CELL_LIST.push({
|
||||
id,
|
||||
section: section.id,
|
||||
screen: section.screen,
|
||||
surface: cell.surface ?? null,
|
||||
option: cell.option ?? null,
|
||||
caption: cell.caption,
|
||||
variantKey: section.id === 'palette' ? cell.surface : section.id === 'icons' ? cell.option : null,
|
||||
tileIndex: section.id === 'surfaces' ? TILE_INDEX[cell.surface] : null,
|
||||
formState,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const refName = (cell) =>
|
||||
cell.section + '--' + (cell.surface ?? 'all') + '--' + (cell.option ?? 'default') + '.png';
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user