Untrack the picker gallery toolchain; tmp is scratch again

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Abdul Wahab
2026-09-01 10:04:59 +05:00
co-authored by Cursor
parent ba999bd799
commit 4f069a39ef
8 changed files with 1 additions and 3995 deletions
+1 -2
View File
@@ -123,8 +123,7 @@ talks/
.astro/
# Local-only scratch for exploratory scripts, parked pages, and unused asset candidates.
tmp/*
!tmp/questionaire/
tmp/
# Isolated ablation staging (impeccable-evals copies the built staged skill here
# per rule, removes one rule, points a worker at it). Throwaway; never committed.
-9
View File
@@ -1,9 +0,0 @@
# 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
-719
View File
@@ -1,719 +0,0 @@
/* Build an untouched replica of previews-gallery.html, then apply 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';
const DOCS_PREVIEW =
'#picker-form .picker-strategy-stage > .picker-preview.picker-preview--docs';
const DOCS_SCOPE =
'html[data-cell-screen="03"][data-cell-surface="read"]';
const DOCS_WEIGHT_CSS = [
`${DOCS_SCOPE} ${DOCS_PREVIEW} {`,
' --pv-text: 1.65 !important;',
'}',
`${DOCS_SCOPE} ${DOCS_PREVIEW} .pd-item i {`,
' width: calc(4.07 * var(--pv-x)) !important;',
'}',
`${DOCS_SCOPE} ${DOCS_PREVIEW} .pd-item span {`,
' height: calc(1.53 * var(--pv-text) * var(--pv-x)) !important;',
'}',
`${DOCS_SCOPE} ${DOCS_PREVIEW} .pd-para i {`,
' height: calc(1.35 * var(--pv-text) * var(--pv-x)) !important;',
'}',
`${DOCS_SCOPE} ${DOCS_PREVIEW} .pd-note-dot {`,
' width: calc(4.07 * var(--pv-x)) !important;',
'}',
`${DOCS_SCOPE} ${DOCS_PREVIEW} .pd-note-lines i {`,
' height: calc(1 * var(--pv-text) * var(--pv-x)) !important;',
'}',
`${DOCS_SCOPE} ${DOCS_PREVIEW} .pd-phone-body .pd-para i {`,
' height: calc(1.37 * var(--pv-text) * var(--pv-x)) !important;',
'}',
`${DOCS_SCOPE} ${DOCS_PREVIEW} .pd-phone-body .pd-note-lines i {`,
' height: calc(0.83 * var(--pv-text) * var(--pv-x)) !important;',
'}',
`${DOCS_SCOPE} ${DOCS_PREVIEW} .pd-phone-body .pd-heading {`,
' height: calc(2.53 * var(--pv-text) * var(--pv-x)) !important;',
'}',
`${DOCS_SCOPE} ${DOCS_PREVIEW} .pd-phone-body .pd-note-dot {`,
' width: calc(2.64 * var(--pv-text) * var(--pv-x)) !important;',
'}',
].join('\n');
const DOCS_CARRY_SCOPE =
'html[data-cell-surface="read"]:is([data-cell-screen="08"], [data-cell-screen="09"], [data-cell-screen="10"])';
const DOCS_CARRY_ROOT = '[data-carry][data-surface="read"]';
const DOCS_CARRY_WEIGHT_CSS = [
`${DOCS_CARRY_SCOPE} ${DOCS_CARRY_ROOT} {`,
' --pvs-carry-weight: 1.65 !important;',
'}',
`${DOCS_CARRY_SCOPE} ${DOCS_CARRY_ROOT} .ps-docs-item > i {`,
' width: calc(var(--pt-base) * 0.34 * var(--pvs-carry-weight)) !important;',
'}',
`${DOCS_CARRY_SCOPE} ${DOCS_CARRY_ROOT} :is(`,
' .ps-docs-item b,',
' .ps-docs-crumb,',
' .ps-docs-note-copy b',
') {',
' height: calc(var(--pt-base) * 0.26 * var(--pvs-carry-weight)) !important;',
'}',
`${DOCS_CARRY_SCOPE} ${DOCS_CARRY_ROOT} :is(`,
' .ps-docs-main .pt-headline i,',
' .ps-phone-body .pt-headline i',
') {',
' height: calc(var(--pt-base) * 0.72 * var(--pvs-carry-weight)) !important;',
'}',
`${DOCS_CARRY_SCOPE} ${DOCS_CARRY_ROOT} .ps-docs-sub {`,
' height: calc(var(--pt-base) * 0.5 * var(--pvs-carry-weight)) !important;',
'}',
`${DOCS_CARRY_SCOPE} ${DOCS_CARRY_ROOT} :is(`,
' .ps-docs-lede i,',
' .ps-docs-para i,',
' .ps-docs-list li',
') {',
' height: calc(var(--pt-base) * 0.3 * var(--pvs-carry-weight)) !important;',
'}',
`${DOCS_CARRY_SCOPE} ${DOCS_CARRY_ROOT} .ps-docs-note-copy > span i {`,
' height: calc(var(--pt-base) * 0.26 * var(--pvs-carry-weight)) !important;',
'}',
`${DOCS_CARRY_SCOPE} ${DOCS_CARRY_ROOT} .ps-docs-list li::before {`,
' width: calc(var(--pt-base) * 0.3 * var(--pvs-carry-weight)) !important;',
'}',
].join('\n');
const DOCS_BOARD_CELLS = [
'boundaries--read--open-space',
'boundaries--read--thin-dividers',
'boundaries--read--surface-changes',
'corners--read--sharp',
'corners--read--slightly-soft',
'corners--read--friendly',
'corners--read--pill',
'depth--read--flat',
'depth--read--soft-lift',
];
const GALLERY_PREVIEW =
'#picker-form .picker-strategy-stage > .picker-preview.picker-preview--gallery:not([hidden])';
const LANDING_PREVIEW =
'#picker-form .picker-strategy-stage > .picker-preview[data-surface="persuade"]';
function cellScope(screen, option) {
return (
'html[data-cell-screen="03"][data-cell-surface="' + screen + '"][data-cell-option="' + option + '"]'
);
}
function drenchedRestoreCss(scope, preview) {
return [
scope,
`${preview} {`,
' --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;',
'}',
].join('\n');
}
function drenchedPanelCss(scope, preview, panel = '.pv-image') {
return [
scope,
`${preview} ${panel} {`,
' background: linear-gradient(',
' 135deg,',
' color-mix(in oklab, var(--pkc-p-ink) 16%, var(--pkc-primary)),',
' color-mix(in oklab, var(--pkc-secondary) 34%, var(--pkc-primary))',
' ) !important;',
' border-color: color-mix(in oklab, var(--pkc-secondary) 52%, var(--pkc-primary)) !important;',
'}',
scope,
`${preview} ${panel}::after {`,
' background: var(--pkc-tertiary) !important;',
'}',
].join('\n');
}
const LANDING_DRENCHED_CSS = [
drenchedRestoreCss(cellScope('persuade', 'drenched'), LANDING_PREVIEW),
drenchedPanelCss(cellScope('persuade', 'drenched'), LANDING_PREVIEW + ' .pv-desktop'),
cellScope('persuade', 'drenched'),
`${LANDING_PREVIEW} .pv-desktop .pv-actions i:last-child {`,
' border-color: color-mix(in oklab, var(--pkc-secondary) 68%, var(--pkc-p-ink)) !important;',
' background: color-mix(in oklab, var(--pkc-secondary) 18%, var(--pkc-primary)) !important;',
'}',
cellScope('persuade', 'drenched'),
`${LANDING_PREVIEW} .pv-desktop .pv-nav-bars i:nth-child(2) {`,
' background: color-mix(in oklab, var(--pkc-secondary) 72%, var(--pkc-p-ink)) !important;',
'}',
].join('\n');
function portfolioCapAvatarsCss(option, circle = 'neutral') {
const scope = cellScope('experience', option);
const capCircle =
circle === 'drenched'
? [
' border: 1px solid color-mix(in oklab, var(--pkc-secondary) 58%, var(--pkc-primary));',
' background: linear-gradient(',
' 135deg,',
' color-mix(in oklab, var(--pkc-secondary) 30%, var(--pkc-primary)),',
' color-mix(in oklab, var(--pkc-p-ink) 20%, var(--pkc-primary))',
' );',
].join('\n')
: circle === 'committed'
? [
' border: 1px solid color-mix(in oklab, var(--pkc-primary) 42%, var(--pkc-neutral));',
' background: linear-gradient(',
' 135deg,',
' color-mix(in oklab, var(--pkc-primary) 16%, var(--pkc-neutral)),',
' color-mix(in oklab, var(--pkc-primary) 30%, var(--pkc-neutral))',
' );',
].join('\n')
: [
' border: 1px solid color-mix(in oklab, var(--pv-secondary) 55%, var(--pv-neutral));',
' background: linear-gradient(',
' 135deg,',
' color-mix(in oklab, var(--pv-secondary) 24%, var(--pv-neutral)),',
' color-mix(in oklab, var(--pv-secondary) 46%, var(--pv-neutral))',
' );',
].join('\n');
return [
`${scope} ${GALLERY_PREVIEW} .pv-desktop .pg-cap {`,
' padding-top: 5.2cqh !important;',
' gap: 2.6cqh !important;',
'}',
`${scope} ${GALLERY_PREVIEW} .pv-desktop .pg-cap::before {`,
' content: "";',
' display: block;',
' width: calc(11.5 * var(--pv-x));',
' aspect-ratio: 1;',
' border-radius: 50%;',
capCircle,
'}',
].join('\n');
}
const PORTFOLIO_DRENCHED_COLOR_CSS = [
drenchedRestoreCss(cellScope('experience', 'drenched'), GALLERY_PREVIEW),
drenchedPanelCss(cellScope('experience', 'drenched'), GALLERY_PREVIEW),
cellScope('experience', 'drenched'),
`${GALLERY_PREVIEW} .pg-track i:nth-child(2) {`,
' background: color-mix(in oklab, var(--pkc-secondary) 78%, var(--pkc-p-ink)) !important;',
'}',
cellScope('experience', 'drenched'),
`${GALLERY_PREVIEW} .pg-cap-title {`,
' background: var(--pkc-tertiary) !important;',
'}',
cellScope('experience', 'drenched'),
`${GALLERY_PREVIEW} .pv-nav-bars i:nth-child(2) {`,
' background: color-mix(in oklab, var(--pkc-secondary) 72%, var(--pkc-p-ink)) !important;',
'}',
cellScope('experience', 'drenched'),
`${GALLERY_PREVIEW} .pv-tabbar i:nth-child(2) {`,
' background: color-mix(in oklab, var(--pkc-secondary) 70%, var(--pkc-p-ink)) !important;',
'}',
cellScope('experience', 'drenched'),
`${GALLERY_PREVIEW} .pv-tabbar i:nth-child(3) {`,
' background: color-mix(in oklab, var(--pkc-tertiary) 82%, var(--pkc-p-ink)) !important;',
'}',
].join('\n');
const DOCS_QUOTE_RESTRAINED_CSS = [
cellScope('read', 'restrained'),
`${DOCS_PREVIEW}:not([hidden]) {`,
' --pd-quote: var(--pd-ink) !important;',
'}',
cellScope('read', 'restrained'),
`${DOCS_PREVIEW}:not([hidden]) :is(.pd-note-dot, .pd-note-lines i) {`,
' background: var(--pd-ink) !important;',
'}',
].join('\n');
const FONT_PAIR_NO_NAV_ACTION_CELLS = [
'font-pair--persuade--default',
'font-pair--operate--default',
'font-pair--read--default',
];
const FONT_PAIR_NO_NAV_ACTION_CSS = [
'#picker-form .picker-preview-type .ps-nav-action {',
' display: none !important;',
'}',
'#picker-form .picker-preview-type .ps-nav-tail {',
' display: none !important;',
'}',
'#picker-form .picker-preview-type .ps-nav {',
' grid-template-columns: auto 1fr !important;',
'}',
'#picker-form .picker-preview-type {',
' --pvs-cta-text: var(--pv-neutral) !important;',
'}',
].join('\n');
const FONT_PAIR_PERSUADE_HERO_IMAGE_CSS = [
'#picker-form .picker-preview-type .ps-hero {',
' grid-template-columns: minmax(0, 1fr) 40% !important;',
'}',
].join('\n');
const FONT_PAIR_PERSUADE_GALLERY_CSS = [
'#picker-form .picker-preview-type .ps-gallery-item {',
' gap: var(--ps-gap-xs) !important;',
'}',
'#picker-form .picker-preview-type .ps-gallery-item > i {',
' max-width: none !important;',
' aspect-ratio: 3 / 2 !important;',
' border-radius: var(--pvs-radius-frame) !important;',
'}',
'#picker-form .picker-preview-type .ps-desktop .ps-gallery-item > i {',
' margin-inline: 0 !important;',
'}',
'#picker-form .picker-preview-type .ps-gallery-item [data-type-gallery-caption] {',
' color: var(--pvs-title) !important;',
' font-size: var(--pt-micro) !important;',
' line-height: 1.38 !important;',
' display: -webkit-box !important;',
' -webkit-box-orient: vertical !important;',
' -webkit-line-clamp: 2 !important;',
' overflow: hidden !important;',
' text-wrap: balance !important;',
' white-space: normal !important;',
'}',
].join('\n');
const FONT_PAIR_PERSUADE_GALLERY_JS = [
'var captions = [',
' "Lorem ipsum dolor sit amet consectetur",',
' "Dolor sit amet consectetur elit",',
' "Eiusmod tempor sed do eiusmod",',
'];',
'doc.querySelectorAll(".ps-gallery-item").forEach(function (item, index) {',
' var title = item.querySelector("[data-type-gallery-title]");',
' var meta = item.querySelector("[data-type-gallery-meta]");',
' var caption = item.querySelector("[data-type-gallery-caption]");',
' var text = captions[index] || "";',
' if (!text && title) {',
' text = title.textContent.trim();',
' if (meta) text = (text + " " + meta.textContent.trim()).trim();',
' }',
' if (!text) return;',
' if (caption) {',
' caption.textContent = text;',
' return;',
' }',
' if (!title) return;',
' title.removeAttribute("data-type-gallery-title");',
' title.setAttribute("data-type-gallery-caption", "");',
' title.textContent = text;',
' if (meta) meta.remove();',
'});',
].join(' ');
const FONT_PAIR_OPERATE_OPS_CSS = [
'#picker-form .picker-preview-type--ops {',
' --pvs-cta-text: var(--pv-neutral) !important;',
'}',
'#picker-form .picker-preview-type--ops .ps-ops-item--on {',
' padding-block: calc(var(--pt-base) * 0.45) !important;',
' padding-inline: calc(var(--pt-base) * 0.72) calc(var(--pt-base) * 0.58) !important;',
'}',
'#picker-form .picker-preview-type--ops .ps-ops-item--on b {',
' color: var(--pv-neutral) !important;',
'}',
'#picker-form .picker-preview-type--ops .ps-ops-field {',
' gap: calc(var(--pt-base) * 0.5) !important;',
' padding-block: calc(var(--pt-base) * 0.52) !important;',
' padding-inline: calc(var(--pt-base) * 0.68) calc(var(--pt-base) * 0.58) !important;',
'}',
'#picker-form .picker-preview-type--ops .ps-ops-field b {',
' flex: 1 1 auto !important;',
' line-height: 1.35 !important;',
'}',
'#picker-form .picker-preview-type--ops .ps-ops-chev {',
' transform: translateY(-12%) rotate(45deg) !important;',
' margin-inline-end: calc(var(--pt-base) * 0.08) !important;',
'}',
'#picker-form .picker-preview-type--ops .ps-ops-row--on {',
' padding-inline: calc(var(--pt-base) * 0.68) calc(var(--pt-base) * 0.58) !important;',
' border-radius: var(--pvs-radius-frame) !important;',
'}',
].join('\n');
const FONT_PAIR_PERSUADE_PHONE_SPACING_CSS = [
'#picker-form .picker-preview-type[data-surface="persuade"] .ps-phone-body {',
' gap: var(--ps-gap-md) !important;',
'}',
'#picker-form .picker-preview-type[data-surface="persuade"] .ps-phone-body :is(.pt-headline, .pt-body, .ps-actions, .ps-proof) {',
' margin-block-end: 0 !important;',
'}',
'#picker-form .picker-preview-type[data-surface="persuade"] .ps-phone-body .ps-editorial-copy {',
' gap: var(--ps-gap-sm) !important;',
'}',
].join('\n');
const FONT_PAIR_READ_PHONE_SPACING_CSS = [
'#picker-form .picker-preview-type--read .ps-phone-body {',
' gap: var(--ps-gap-md) !important;',
'}',
'#picker-form .picker-preview-type--read .ps-phone-body > * {',
' margin-block: 0 !important;',
'}',
'#picker-form .picker-preview-type--read .ps-phone-body .ps-docs-sub + .ps-docs-para {',
' margin-top: calc(var(--pt-base) * 0.45) !important;',
'}',
'#picker-form .picker-preview-type--read .ps-phone-body :is(.ps-docs-list, .ps-docs-note) {',
' margin-top: 0 !important;',
'}',
].join('\n');
const FONT_PAIR_READ_DOCS_LAYOUT_CSS = [
'#picker-form .picker-preview-type--read .ps-docs {',
' grid-template-columns: minmax(0, 21%) minmax(0, 1fr) !important;',
'}',
'#picker-form .picker-preview-type--read .ps-docs-rail {',
' padding-inline-end: calc(var(--pt-base) * 0.35) !important;',
'}',
'#picker-form .picker-preview-type--read .ps-docs-main {',
' padding: var(--ps-band-y) 5% 0 calc(var(--pt-base) * 0.8) !important;',
'}',
].join('\n');
const FONT_PAIR_READ_SECTION_TITLE_JS =
'doc.querySelectorAll("[data-type-section-title]").forEach(function (el) { if (el.textContent.trim() === "Lorem ipsum dolor sit") el.textContent = "Lorem ipsum dolor"; });';
const FONT_PAIR_READ_NOTA_CSS = [
'#picker-form .picker-preview-type--read .ps-docs-note-copy b {',
' color: var(--pv-primary) !important;',
'}',
].join('\n');
const FONT_PAIR_EXPERIENCE_CAP_AVATARS_CSS = [
'#picker-form .picker-preview-type--experience .ps-desktop .ps-index-cap {',
' align-content: center !important;',
'}',
'#picker-form .picker-preview-type--experience .ps-index-avatar {',
' display: none !important;',
'}',
'#picker-form .picker-preview-type--experience .ps-desktop .ps-index-avatar {',
' display: block !important;',
' width: calc(var(--pt-base) * 3.4) !important;',
' aspect-ratio: 1 !important;',
' border-radius: 50% !important;',
' border: 1px solid color-mix(in oklab, var(--pv-secondary) 55%, var(--pv-neutral)) !important;',
' background: linear-gradient(',
' 135deg,',
' color-mix(in oklab, var(--pv-secondary) 24%, var(--pv-neutral)),',
' color-mix(in oklab, var(--pv-secondary) 46%, var(--pv-neutral))',
' ) !important;',
'}',
].join('\n');
const FONT_PAIR_EXPERIENCE_CAP_AVATARS_JS = [
'doc.querySelectorAll("#picker-form .picker-preview-type--experience .ps-desktop .ps-index-cap").forEach(function (cap) {',
' if (cap.querySelector(".ps-index-avatar")) return;',
' var avatar = doc.createElement("i");',
' avatar.className = "ps-index-avatar";',
' avatar.setAttribute("aria-hidden", "true");',
' cap.insertBefore(avatar, cap.firstChild);',
'});',
].join(' ');
const FONT_PAIR_EXPERIENCE_LAYOUT_CSS = [
'#picker-form .picker-preview-type--experience .ps-desktop {',
' grid-template-rows: 9.4% minmax(0, 1fr) !important;',
'}',
'#picker-form .picker-preview-type--experience .ps-index {',
' gap: var(--ps-gap-sm) !important;',
' grid-template-rows: auto minmax(0, 1fr) minmax(0, 1fr) !important;',
'}',
'#picker-form .picker-preview-type--experience .ps-desktop :is(.ps-index-rail, .ps-index-meta, .ps-nav-action) {',
' display: none !important;',
'}',
'#picker-form .picker-preview-type--experience .ps-index > .pt-headline {',
' max-width: none !important;',
' font-size: var(--pt-title) !important;',
' letter-spacing: 0 !important;',
' white-space: nowrap !important;',
' text-wrap: nowrap !important;',
'}',
'#picker-form .picker-preview-type--experience .ps-desktop .ps-index-cap {',
' align-content: center !important;',
' gap: calc(var(--pt-base) * 0.42) !important;',
'}',
'#picker-form .picker-preview-type--experience .ps-desktop .ps-index-row--flip .ps-index-cap {',
' padding-left: 20px !important;',
'}',
'#picker-form .picker-preview-type--experience .ps-desktop .ps-index-avatar {',
' margin-bottom: calc(var(--pt-base) * 0.12) !important;',
'}',
'#picker-form .picker-preview-type--experience .ps-desktop .ps-index-note {',
' font-size: 16px !important;',
' white-space: pre-line !important;',
'}',
'#picker-form .picker-preview-type--experience .ps-phone-body .ps-index-cap {',
' padding-inline: 10px !important;',
'}',
'#picker-form .picker-preview-type--experience .ps-phone :is(.ps-index-meta, .ps-nav-action) {',
' display: none !important;',
'}',
'#picker-form .picker-preview-type--experience .ps-footer,',
'#picker-form .picker-preview-type--experience .ps-phone-footer {',
' display: none !important;',
'}',
].join('\n');
const FONT_PAIR_EXPERIENCE_CAPTION_JS =
'doc.querySelectorAll("[data-type-caption]").forEach(function (el) { el.textContent = "Lorem ipsum dolor sit amet,\\nconsectetur adipiscing elit sed do eiusmod tempor."; });';
/** @type {{ id: string, cells: string[], css: string }[]} */
const EXPERIMENTS = [
{
id: 'font-pair-hide-nav-action',
cells: FONT_PAIR_NO_NAV_ACTION_CELLS,
css: FONT_PAIR_NO_NAV_ACTION_CSS,
},
{
id: 'font-pair-persuade-hero-image',
cells: ['font-pair--persuade--default'],
css: FONT_PAIR_PERSUADE_HERO_IMAGE_CSS,
},
{
id: 'font-pair-persuade-proof-copy',
cells: ['font-pair--persuade--default'],
js: 'var proofs = doc.querySelectorAll("[data-type-proof]"); if (proofs[1]) proofs[1].textContent = "Sit amet elit dolor";',
},
{
id: 'font-pair-persuade-editorial-copy',
cells: ['font-pair--persuade--default'],
js: [
'doc.querySelectorAll(".ps-editorial-copy > em[data-type-section-link]").forEach(function (el) { el.remove(); });',
'var merged = "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.";',
'doc.querySelectorAll("[data-type-section-body]").forEach(function (p) {',
' if (p.hasAttribute("data-type-section-body-2")) { p.remove(); return; }',
' var second = p.nextElementSibling;',
' if (second && second.hasAttribute("data-type-section-body-2")) {',
' p.textContent = merged;',
' second.remove();',
' return;',
' }',
' p.textContent = merged;',
'});',
].join(' '),
},
{
id: 'font-pair-persuade-gallery-cards',
cells: ['font-pair--persuade--default'],
css: FONT_PAIR_PERSUADE_GALLERY_CSS,
js: FONT_PAIR_PERSUADE_GALLERY_JS,
},
{
id: 'font-pair-persuade-phone-spacing',
cells: ['font-pair--persuade--default'],
css: FONT_PAIR_PERSUADE_PHONE_SPACING_CSS,
},
{
id: 'font-pair-read-phone-spacing',
cells: ['font-pair--read--default'],
css: FONT_PAIR_READ_PHONE_SPACING_CSS,
},
{
id: 'font-pair-read-docs-layout',
cells: ['font-pair--read--default'],
css: FONT_PAIR_READ_DOCS_LAYOUT_CSS,
js: FONT_PAIR_READ_SECTION_TITLE_JS,
},
{
id: 'font-pair-read-nota-color',
cells: ['font-pair--read--default'],
css: FONT_PAIR_READ_NOTA_CSS,
},
{
id: 'font-pair-experience-cap-avatars',
cells: ['font-pair--experience--default'],
css: FONT_PAIR_EXPERIENCE_CAP_AVATARS_CSS,
js: FONT_PAIR_EXPERIENCE_CAP_AVATARS_JS,
},
{
id: 'font-pair-experience-layout',
cells: ['font-pair--experience--default'],
css: FONT_PAIR_EXPERIENCE_LAYOUT_CSS,
js: FONT_PAIR_EXPERIENCE_CAPTION_JS,
},
{
id: 'font-pair-operate-ops-chrome',
cells: ['font-pair--operate--default'],
css: FONT_PAIR_OPERATE_OPS_CSS,
},
{
id: 'landing-drenched-color-roles',
cells: ['strategy--persuade--drenched'],
css: LANDING_DRENCHED_CSS,
},
{
id: 'ops-committed-color-visibility',
cells: ['strategy--operate--committed'],
css: [
'html[data-cell-screen="03"][data-cell-surface="operate"][data-cell-option="committed"]',
`${OPS_PREVIEW} {`,
' --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',
cells: ['strategy--operate--full-palette'],
css: [
'html[data-cell-screen="03"][data-cell-surface="operate"][data-cell-option="full-palette"]',
`${OPS_PREVIEW} {`,
' --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'),
},
{
id: 'docs-restrained-quote-contrast',
cells: ['strategy--read--restrained'],
css: DOCS_QUOTE_RESTRAINED_CSS,
},
{
id: 'docs-strategy-wireframe-weight',
cells: [
'strategy--read--restrained',
'strategy--read--committed',
'strategy--read--full-palette',
],
css: DOCS_WEIGHT_CSS,
},
{
id: 'docs-board-carry-weight',
cells: DOCS_BOARD_CELLS,
css: DOCS_CARRY_WEIGHT_CSS,
},
{
id: 'portfolio-restrained-cap-avatars',
cells: ['strategy--experience--restrained'],
css: portfolioCapAvatarsCss('restrained', 'neutral'),
},
{
id: 'portfolio-committed-cap-avatars',
cells: ['strategy--experience--committed'],
css: portfolioCapAvatarsCss('committed', 'committed'),
},
{
id: 'portfolio-drenched-gallery',
cells: ['strategy--experience--drenched'],
css: PORTFOLIO_DRENCHED_COLOR_CSS + '\n' + portfolioCapAvatarsCss('drenched', 'drenched'),
},
];
const experiment = String.raw`
<script data-gallery-replica-experiment="strategy-fixes">
(function () {
var specs = ${JSON.stringify(EXPERIMENTS.map(({ id, cells, css, js }) => ({ id, cells, css: css || '', js: js || '' })))};
specs.forEach(function (spec) {
spec.cells.forEach(function (cellId) {
var frame = document.querySelector('iframe[data-cell="' + cellId + '"]');
if (!frame) return;
function apply() {
var doc = frame.contentDocument;
if (!doc || !doc.head) return;
if (spec.css) {
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);
}
}
if (spec.js) {
try {
eval(spec.js);
} catch (err) {
console.warn('[gallery-replica] ' + spec.id + ':', err);
}
}
}
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 { cells } of EXPERIMENTS) {
for (const cell of cells) {
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 experiments on '
+ '<strong>12 Drenched</strong>, '
+ '<strong>1415 App UI</strong> color roles, '
+ '<strong>1618 Docs</strong> strategy wireframe weight, and '
+ '<strong>4547 / 5962 / 7172 Docs</strong> board bar weight, and '
+ '<strong>1921 Portfolio</strong> caption circles (desktop), '
+ '<strong>12 / 21 Drenched</strong> full accent roles, '
+ 'Baseline: <a href="previews-gallery.html">previews-gallery.html</a>.</p>',
);
await writeFile(OUTPUT, replica);
console.log('wrote previews-gallery-improved.html (' + (replica.length / 1024 / 1024).toFixed(1) + ' MB)');
-566
View File
@@ -1,566 +0,0 @@
/* 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]); });',
' });',
' var roles = [\'primary\', \'secondary\', \'tertiary\', \'neutral\'];',
' var bandInk = { primary: \'p-ink\', secondary: \'s-ink\', tertiary: \'t-ink\', neutral: \'n-ink\' };',
' roles.forEach(function (role) {',
' var input = document.querySelector(\'[name="palette-\' + role + \'"]\');',
' if (input && vars[role]) input.value = vars[role];',
' });',
' Array.prototype.forEach.call(document.querySelectorAll(\'.picker-band[data-band]\'), function (band) {',
' var role = band.getAttribute(\'data-band\');',
' if (!vars[role]) return;',
' band.style.setProperty(\'--band-color\', vars[role]);',
' band.style.setProperty(\'--band-ink\', vars[bandInk[role]]);',
' var hex = band.querySelector(\'output[data-band-hex]\');',
' if (hex) hex.textContent = vars[role];',
' });',
'};',
'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),',
' \'s-ink\': contrastInk(c.secondary), \'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 figcaption .cell-num { display: inline-block; min-width: 2.2em; margin-right: 4px;',
' font-variant-numeric: tabular-nums; font-weight: 600; color: #c8b57c; opacity: 1; }',
'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, index) => ({
...cell,
number: index + 1,
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) + ' &middot; ' + 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) + ' &middot; ' + 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" data-cell-num="' + cell.number + '">',
'<figcaption><span class="cell-num">' + cell.number + '</span> ' + 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.number + '. ' + cell.caption.replace(/"/g, '&quot;') + '" data-cell="' + cell.id
+ '" data-cell-num="' + cell.number + '" 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); });
-246
View File
@@ -1,246 +0,0 @@
/* 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); });
-205
View File
@@ -1,205 +0,0 @@
/* 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