mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 15:16:35 +03:00
Enrich design context Product, Brand, and Color sections.
Add product conversion/principles/surface copy and leading-surface badge; Brand voice and principles blocks; Color ink chips plus as-dealt/edited provenance tags. Extend visual-cues schema. Polish font-pair preview cells 24–25 and sync gallery replica. AI-assisted. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -18,20 +18,41 @@ const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
|
||||
const form = $('#picker-form');
|
||||
const shell = $('[data-dcx-shell]');
|
||||
|
||||
/* Seed context (the chat half of the interview) rides in on cues.json as an
|
||||
optional `context` block. Fetched at load, before the server can exit. */
|
||||
/* Seed context (the chat half of the interview) and the dealt palettes ride
|
||||
in on cues.json. Fetched at load, before the server can exit: the context
|
||||
block feeds the chat-sourced pages, and the palette map is what the
|
||||
provenance tags compare committed values against. */
|
||||
let seedContext = null;
|
||||
let seedModes = null;
|
||||
let seedPalettes = null;
|
||||
fetch('/cues.json')
|
||||
.then((response) => (response.ok ? response.json() : null))
|
||||
.then((data) => { seedContext = data?.context || null; })
|
||||
.then((data) => {
|
||||
seedContext = data?.context || null;
|
||||
seedModes = Array.isArray(data?.modes) ? data.modes : null;
|
||||
seedPalettes = data?.palette || null;
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
/* The winning cue's dealt value for one role, read the way the deck's own
|
||||
createState reads it in palette-picker.js: the pixel-snapped value when
|
||||
the search landed, the planned hex otherwise, uppercased. A palette
|
||||
source that is not a cue in cues.json (a seed-deck card, a custom
|
||||
palette) has no entry here and returns nothing, which is what turns the
|
||||
provenance tag off. */
|
||||
const seedHexFor = (source, role) => {
|
||||
const slot = seedPalettes?.[source]?.[role];
|
||||
if (!slot) return '';
|
||||
return String(slot.snapped || slot.hex || '').toUpperCase();
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
Snapshot — everything the document renders, read once.
|
||||
============================================================ */
|
||||
|
||||
const ROLES = ['primary', 'secondary', 'tertiary', 'neutral'];
|
||||
const SURFACE_ORDER = ['persuade', 'operate', 'read', 'experience'];
|
||||
const SURFACE_LABELS = { persuade: 'Landing page', operate: 'Tool', read: 'Docs', experience: 'Portfolio' };
|
||||
const PER_SURFACE = ['color-strategy', 'boundary-style', 'corner-style', 'depth-style'];
|
||||
|
||||
const fieldValue = (name) => {
|
||||
@@ -112,6 +133,7 @@ function takeSnapshot() {
|
||||
|
||||
return {
|
||||
context: seedContext,
|
||||
suggestedModes: seedModes,
|
||||
surfaces,
|
||||
palette,
|
||||
paletteSource: fieldValue('palette-source'),
|
||||
@@ -177,6 +199,14 @@ const callout = (name, body, accent = false, extra = '') => `
|
||||
const list = (items) => `
|
||||
<ul class="dcx-list">${items.map((item) => `<li>${item}</li>`).join('')}</ul>`;
|
||||
|
||||
const principlesList = (items) => `
|
||||
<ol class="dcx-principles">${items.map((item) => {
|
||||
if (item && typeof item === 'object' && item.title) {
|
||||
return `<li><strong>${escapeHtml(item.title)}</strong>${item.detail ? ` — ${escapeHtml(item.detail)}` : ''}</li>`;
|
||||
}
|
||||
return `<li>${escapeHtml(item)}</li>`;
|
||||
}).join('')}</ol>`;
|
||||
|
||||
const chips = (items) => `
|
||||
<div class="dcx-chips">${items.map((item) => `<span class="dcx-chip dcx-chip--muted">${escapeHtml(item)}</span>`).join('')}</div>`;
|
||||
|
||||
@@ -333,16 +363,37 @@ function buildAudience(s, name) {
|
||||
drawing on top, dressed in the committed palette, then the label and goal
|
||||
the tile carried. */
|
||||
function surfaceCards(s, body) {
|
||||
return `<div class="dcx-surfaces" data-count="${s.surfaces.length}">${s.surfaces.map((surface) => `
|
||||
return `<div class="dcx-surfaces" data-count="${s.surfaces.length}">${s.surfaces.map((surface, index) => `
|
||||
<article class="dcx-surface-card">
|
||||
${proofHtml(surfaceBoard(surface.mode) || surfaceTilePreview(surface.mode), { kind: 'board' })}
|
||||
<div class="dcx-surface-copy">
|
||||
<h3 class="dcx-surface-name">${escapeHtml(surface.label)}</h3>
|
||||
<h3 class="dcx-surface-name">${escapeHtml(surface.label)}${index === 0 ? ' <span class="dcx-default-mark">leading</span>' : ''}</h3>
|
||||
${body(surface)}
|
||||
</div>
|
||||
</article>`).join('')}</div>`;
|
||||
}
|
||||
|
||||
function surfaceProvenanceNote(s) {
|
||||
if (!Array.isArray(s.suggestedModes) || !s.suggestedModes.length) return '';
|
||||
const chosen = s.surfaces.map((surface) => surface.mode);
|
||||
const suggestedSet = new Set(s.suggestedModes);
|
||||
const chosenSet = new Set(chosen);
|
||||
const label = (mode) => s.surfaces.find((surface) => surface.mode === mode)?.label
|
||||
|| SURFACE_LABELS[mode]
|
||||
|| mode;
|
||||
const added = chosen.filter((mode) => !suggestedSet.has(mode));
|
||||
const removed = s.suggestedModes.filter((mode) => !chosenSet.has(mode));
|
||||
let tail = 'you kept the suggested set';
|
||||
if (added.length && removed.length) {
|
||||
tail = `you added ${added.map(label).join(', ')} and dropped ${removed.map(label).join(', ')}`;
|
||||
} else if (added.length) {
|
||||
tail = `you added ${added.map(label).join(', ')}`;
|
||||
} else if (removed.length) {
|
||||
tail = `you dropped ${removed.map(label).join(', ')}`;
|
||||
}
|
||||
return note(`Suggested from <code>PRODUCT.md</code>: ${s.suggestedModes.map(label).join(', ')}; ${tail}.`);
|
||||
}
|
||||
|
||||
/* Display labels for the PRODUCT.md platform value; an unrecognized value
|
||||
renders as written rather than being dropped. */
|
||||
const PLATFORM_LABELS = { web: 'Web', ios: 'iOS', android: 'Android', adaptive: 'Adaptive' };
|
||||
@@ -367,16 +418,27 @@ function buildProduct(s, name) {
|
||||
].filter(Boolean).join('');
|
||||
parts.push(block('Positioning', `<div class="dcx-callout-pair">${cells}</div>`));
|
||||
}
|
||||
if (product.conversion) {
|
||||
parts.push(block('Primary conversion', callout('The one action', escapeHtml(product.conversion), true)));
|
||||
}
|
||||
if (Array.isArray(product.clarities) && product.clarities.length) {
|
||||
parts.push(block('What must be clear first', list(product.clarities.map(escapeHtml))));
|
||||
}
|
||||
if (Array.isArray(product.principles) && product.principles.length) {
|
||||
parts.push(block('Product principles', principlesList(product.principles)));
|
||||
}
|
||||
if (product.operatingContext) {
|
||||
parts.push(block('Operating context', `<p class="dcx-prose">${escapeHtml(product.operatingContext)}</p>`));
|
||||
}
|
||||
parts.push(block('Surfaces', surfaceCards(s, (surface) => `
|
||||
<p class="dcx-surface-goal">${surface.goal ? escapeHtml(surface.goal) : ''}</p>
|
||||
${surface.examples.length ? chips(surface.examples) : ''}`)
|
||||
+ note('Chosen on the questionnaire’s first screen, drawn in the committed palette; every per-surface answer in this document is keyed to this set.')));
|
||||
const surfaceMap = product.surfaces && typeof product.surfaces === 'object' ? product.surfaces : {};
|
||||
parts.push(block('Surfaces', surfaceCards(s, (surface) => {
|
||||
const specific = typeof surfaceMap[surface.mode] === 'string' ? surfaceMap[surface.mode].trim() : '';
|
||||
return `
|
||||
<p class="dcx-surface-goal">${escapeHtml(specific || surface.goal || '')}</p>
|
||||
${surface.examples.length ? chips(surface.examples) : ''}`;
|
||||
})
|
||||
+ surfaceProvenanceNote(s)
|
||||
+ note('Chosen on the questionnaire’s first screen, drawn in the committed palette; the leading surface owns every bare answer key in the sections that follow.')));
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
@@ -392,6 +454,26 @@ function buildBrand(s, name) {
|
||||
: (Array.isArray(brand.words) && brand.words.length
|
||||
? callout(brand.words.join(' · '), 'Three words, voice, and tone were confirmed in chat, before the browser questionnaire. <code>PRODUCT.md · Brand Personality</code> is the durable copy.', true)
|
||||
: fromChat('Three words, voice, and tone were confirmed', '<code>PRODUCT.md · Brand Personality</code>'))));
|
||||
/* Voice: say / not wording pairs the agent derived from Brand Personality
|
||||
and Brand Commitments at cues-write time. Concrete lines to write with,
|
||||
never adjectives, and no interview question stands behind the field.
|
||||
Pairs missing either half are dropped rather than rendered lopsided. */
|
||||
const voicePairs = (Array.isArray(brand.voice) ? brand.voice : [])
|
||||
.filter((pair) => pair && typeof pair === 'object' && pair.say && pair.not);
|
||||
if (voicePairs.length) {
|
||||
parts.push(block('Voice', `<div class="dcx-voice">${voicePairs.map((pair) => `
|
||||
<div class="dcx-voice-pair">
|
||||
<div class="dcx-voice-cell dcx-voice-cell--say"><span class="dcx-voice-tag">Say</span><p>${escapeHtml(pair.say)}</p></div>
|
||||
<div class="dcx-voice-cell dcx-voice-cell--not"><span class="dcx-voice-tag">Not</span><p>${escapeHtml(pair.not)}</p></div>
|
||||
</div>`).join('')}</div>`
|
||||
+ note('Derived from Brand Personality and Brand Commitments in <code>PRODUCT.md</code>: wording to write with beside wording to refuse.')));
|
||||
}
|
||||
/* Principles: PRODUCT.md's own list, in the prototype's numbered anatomy,
|
||||
folded into two columns. */
|
||||
if (Array.isArray(brand.principles) && brand.principles.length) {
|
||||
parts.push(block('Principles', `<ol class="dcx-principles dcx-principles--cols">${brand.principles.map((line) => `<li>${escapeHtml(line)}</li>`).join('')}</ol>`
|
||||
+ note('From <code>PRODUCT.md</code>’s principles section; the durable copy lives there.')));
|
||||
}
|
||||
if (Array.isArray(brand.commitments) && brand.commitments.length) {
|
||||
parts.push(block('Commitments', list(brand.commitments.map(escapeHtml))));
|
||||
}
|
||||
@@ -449,25 +531,40 @@ function buildColor(s, name) {
|
||||
+ note(`Committed on the palette screen${s.paletteSource ? ` from the <code>${escapeHtml(s.paletteSource)}</code> cue` : ''}, roles in the order you arranged them. Hover to fan; click to copy the hex.`)));
|
||||
|
||||
/* One full-width band per role: the swatch at real size with both value
|
||||
notations, the role's job, and a copy affordance. */
|
||||
parts.push(block('Roles and values', `<div class="dcx-swatches">${s.palette.map((entry) => `
|
||||
notations, the role's job, the ink that survives on it, where the value
|
||||
came from, and a copy affordance. The provenance tag compares the
|
||||
committed value against the winning cue's dealt value and stays away
|
||||
when the source is not a cue; the ink chip is a pure derivation and
|
||||
always renders. */
|
||||
const colorContext = s.context?.color || {};
|
||||
parts.push(block('Roles and values', `<div class="dcx-swatches">${s.palette.map((entry) => {
|
||||
const inkHex = contrastInkHex(entry.hex);
|
||||
const seedHex = seedHexFor(s.paletteSource, entry.role.toLowerCase());
|
||||
const provenance = seedHex ? (seedHex === entry.hex ? 'as dealt' : 'edited') : '';
|
||||
return `
|
||||
<div class="dcx-swatch" data-role="${entry.role.toLowerCase()}">
|
||||
<button class="dcx-swatch-chip" type="button" data-copy-color="${entry.hex}" data-color-name="${escapeHtml(entry.role)}"
|
||||
style="--swatch:${entry.hex}; --swatch-ink:${inkFor(entry.hex)};" aria-label="Copy ${escapeHtml(entry.role)} ${entry.hex}">
|
||||
<span class="dcx-swatch-hex">${entry.hex}</span>
|
||||
<span class="dcx-swatch-hex">${entry.hex}</span>${provenance ? `
|
||||
<span class="dcx-swatch-provenance" data-provenance="${provenance === 'edited' ? 'edited' : 'as-dealt'}">${provenance}</span>` : ''}
|
||||
<span class="dcx-swatch-copy-hint">Copy</span>
|
||||
</button>
|
||||
<div class="dcx-swatch-meta">
|
||||
<h3>${escapeHtml(entry.role)}</h3>
|
||||
<p>${escapeHtml(ROLE_STORY[entry.role] || '')}</p>
|
||||
<code>${escapeHtml(formatOklch(entry.hex))}</code>
|
||||
<span class="dcx-ink-pair"><span class="dcx-ink-tag">Ink</span><span class="dcx-ink-sample" style="--ink-ground:${entry.hex}; --ink-text:${inkHex};">${inkHex}</span></span>
|
||||
<span class="dcx-swatch-actions">
|
||||
<button class="dcx-edit" type="button" data-edit-color="${entry.role.toLowerCase()}">Edit color</button>
|
||||
<input class="dcx-native-color" type="color" value="${entry.hex}" data-color-input-for="${entry.role.toLowerCase()}"
|
||||
tabindex="-1" aria-label="Pick a new ${escapeHtml(entry.role)} color" />
|
||||
</span>
|
||||
</div>
|
||||
</div>`).join('')}</div>`));
|
||||
</div>`;
|
||||
}).join('')}</div>`
|
||||
+ (Array.isArray(colorContext.assetLocks) && colorContext.assetLocks.length
|
||||
? note(`From the assets: ${colorContext.assetLocks.map(escapeHtml).join(' · ')}.`)
|
||||
: '')));
|
||||
|
||||
/* Every chosen surface gets its artboard back, remapped by the strategy
|
||||
that surface answered with — the strategy screen's preview, kept. */
|
||||
|
||||
@@ -95,6 +95,8 @@ const LOREM_DOCS = {
|
||||
|
||||
const LOREM_INDEX = {
|
||||
stops: ['Lorem', 'Ipsum', 'Dolor', 'Amet'],
|
||||
caption:
|
||||
'Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit sed do eiusmod tempor.',
|
||||
};
|
||||
|
||||
/* The desktop artboard sets three cards and the phone two, so a fourth would
|
||||
@@ -433,7 +435,7 @@ function fillBoard(board) {
|
||||
fill('[data-type-note-label]', LOREM_DOCS.note);
|
||||
fill('[data-type-note-body]', LOREM.note);
|
||||
fill('[data-type-crumb]', LOREM_DOCS.crumb);
|
||||
fill('[data-type-caption]', LOREM.caption);
|
||||
fill('[data-type-caption]', LOREM_INDEX.caption);
|
||||
fill('[data-type-chart-title]', LOREM_APP.chartTitle);
|
||||
const fillGalleryCaptions = (root, items) => {
|
||||
if (!root) return;
|
||||
|
||||
@@ -794,6 +794,57 @@ body.dcx-open { overflow: hidden; background: linear-gradient(180deg, var(--ks-l
|
||||
|
||||
.dcx-principles strong { color: var(--ks-champagne); font-weight: 600; }
|
||||
|
||||
/* ============================================================
|
||||
Brand voice and principles: the say / not wording pairs and
|
||||
the two-column principles list. Rendered only when the agent
|
||||
passed brand.voice or brand.principles; every value is a dcx
|
||||
or ks token already in this file.
|
||||
============================================================ */
|
||||
.dcx-voice { display: grid; gap: 12px; }
|
||||
|
||||
/* One pair per row: the line to write beside the line to refuse. */
|
||||
.dcx-voice-pair {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dcx-voice-cell {
|
||||
border: 1px solid var(--ks-rule);
|
||||
background: var(--panel-bg);
|
||||
border-radius: 10px;
|
||||
padding: 14px 18px;
|
||||
}
|
||||
|
||||
.dcx-voice-cell--say { border-color: var(--accent-line); background: var(--accent-wash); }
|
||||
|
||||
.dcx-voice-tag {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.62rem;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ks-text-faint);
|
||||
}
|
||||
|
||||
.dcx-voice-cell p {
|
||||
margin: 0;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.6;
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
.dcx-voice-cell--say p { color: var(--ks-text); }
|
||||
|
||||
/* The numbered principle anatomy above, folded into two columns. The
|
||||
counters keep DOM order, so the first row reads 01 then 02. */
|
||||
.dcx-principles--cols {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
|
||||
gap: 0 clamp(24px, 4vw, 48px);
|
||||
}
|
||||
|
||||
/* Plain bullet list. */
|
||||
.dcx-list {
|
||||
list-style: none;
|
||||
@@ -1734,6 +1785,48 @@ body.dcx-open { overflow: hidden; background: linear-gradient(180deg, var(--ks-l
|
||||
color: var(--ks-text-faint);
|
||||
}
|
||||
|
||||
/* The ink that survives on this role, from the same contrast pick the
|
||||
artboards use: the hex drawn in itself on a mini sample of the role
|
||||
color. The sample is a span, not a code element, so the meta column's
|
||||
code rule cannot outrank the sample's own colors. */
|
||||
.dcx-ink-pair {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dcx-ink-tag {
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.62rem;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ks-text-faint);
|
||||
}
|
||||
|
||||
.dcx-ink-sample {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--ks-rule);
|
||||
background: var(--ink-ground);
|
||||
color: var(--ink-text);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
/* Where the committed value came from, at the hex's shoulder on the chip.
|
||||
Inherits the chip's own contrast ink. */
|
||||
.dcx-swatch-provenance {
|
||||
margin-left: auto;
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.62rem;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Font pair — the fonttrio anatomy from the font screen.
|
||||
============================================================ */
|
||||
|
||||
@@ -6733,7 +6733,7 @@ body.picker-page {
|
||||
}
|
||||
|
||||
.picker-preview-type--read .ps-docs-note-copy b {
|
||||
color: var(--pvs-accent-text);
|
||||
color: var(--pv-primary);
|
||||
font-family: var(--pt-body);
|
||||
font-size: var(--pt-micro);
|
||||
font-weight: 600;
|
||||
@@ -6864,6 +6864,11 @@ body.picker-page {
|
||||
|
||||
.picker-preview-type--experience .ps-index {
|
||||
gap: var(--ps-gap-sm);
|
||||
grid-template-rows: auto minmax(0, 1fr) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.picker-preview-type--experience .ps-desktop :is(.ps-index-rail, .ps-index-meta, .ps-nav-action) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.picker-preview-type--experience .ps-index > .pt-headline {
|
||||
@@ -6906,17 +6911,21 @@ body.picker-page {
|
||||
title on desktop; the type board repeats that mark so the font-pair preview
|
||||
reads like cells 19–20 beside it. Handset captions stay text-only. */
|
||||
.picker-preview-type--experience .ps-desktop .ps-index-cap {
|
||||
align-content: start;
|
||||
align-content: center;
|
||||
gap: calc(var(--pt-base) * 0.42);
|
||||
}
|
||||
|
||||
.picker-preview-type--experience .ps-desktop .ps-index-row--flip .ps-index-cap {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.picker-preview-type--experience .ps-index-avatar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.picker-preview-type--experience .ps-desktop .ps-index-avatar {
|
||||
display: block;
|
||||
width: calc(var(--pt-base) * 4.8);
|
||||
width: calc(var(--pt-base) * 3.4);
|
||||
margin-bottom: calc(var(--pt-base) * 0.12);
|
||||
aspect-ratio: 1;
|
||||
border: 1px solid color-mix(in oklab, var(--pv-secondary) 55%, var(--pv-neutral));
|
||||
@@ -6952,8 +6961,16 @@ body.picker-page {
|
||||
margin-top: calc(var(--pt-base) * 0.3);
|
||||
color: var(--pvs-copy);
|
||||
font-family: var(--pt-body);
|
||||
font-size: var(--pt-micro);
|
||||
line-height: 1.55;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.picker-preview-type--experience .ps-desktop .ps-index-note {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.picker-preview-type--experience .ps-phone-body .ps-index-note {
|
||||
font-size: var(--pt-micro);
|
||||
}
|
||||
|
||||
.ps-index-rail {
|
||||
@@ -7017,6 +7034,14 @@ body.picker-page {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.picker-preview-type--experience .ps-phone-body .ps-index-cap {
|
||||
padding-inline: 10px;
|
||||
}
|
||||
|
||||
.picker-preview-type--experience .ps-phone :is(.ps-index-meta, .ps-nav-action) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
The structural boards: the four bodies drawn as wireframes.
|
||||
|
||||
|
||||
@@ -464,6 +464,9 @@ In the same write, add a top-level `context` object carrying the chat half of th
|
||||
"platform": "[bare value from PRODUCT.md Platform: web, ios, android, or adaptive]",
|
||||
"positioning": { "not": "[what it is not, from PRODUCT.md Positioning]", "this": "[what it is instead]" },
|
||||
"clarities": ["[one line per item of PRODUCT.md's what-must-be-clear-first list]"],
|
||||
"conversion": "[primary conversion from PRODUCT.md Product Purpose, bare action: book a consultation]",
|
||||
"principles": [{ "title": "[principle name from PRODUCT.md Design Principles]", "detail": "[one clause: what it means for design]" }],
|
||||
"surfaces": { "persuade": "[what this surface is for this product, one line]", "operate": "[...]", "read": "[...]", "experience": "[...]" },
|
||||
"operatingContext": "[one line from PRODUCT.md Operating Context]"
|
||||
},
|
||||
"audience": {
|
||||
@@ -477,9 +480,12 @@ In the same write, add a top-level `context` object carrying the chat half of th
|
||||
"brand": {
|
||||
"words": ["[word]"],
|
||||
"personality": "[one sentence from PRODUCT.md Brand Personality]",
|
||||
"principles": ["[one line per principle from PRODUCT.md Product Principles, or the legacy Design Principles heading]"],
|
||||
"voice": [{ "say": "[a concrete line the product would write; 2 to 4 pairs, wording examples, never adjectives]", "not": "[the same message written the way the product refuses to sound]" }],
|
||||
"commitments": ["[one line per commitment from PRODUCT.md Brand Commitments]"]
|
||||
},
|
||||
"assets": ["[asset name: what Step 2 read off it]"],
|
||||
"color": { "assetLocks": ["[one short color fact an asset fixes, e.g. Primary locked from the logo mark; only when an asset names one]"] },
|
||||
"interview": {
|
||||
"colorStrategy": "[Q1 pick]", "hueAnchor": "[Q1 anchor]",
|
||||
"typeDirection": "[Q2 pick]", "motionEnergy": "[Q3 pick]",
|
||||
@@ -491,7 +497,9 @@ In the same write, add a top-level `context` object carrying the chat half of th
|
||||
|
||||
Quote the user's answers, not paraphrases of them; the document labels interview fields as the questions they answered. A missing block renders as a pointer to where that truth lives (PRODUCT.md), so an old `cues.json` without `context` still produces a complete document.
|
||||
|
||||
The optionality is field by field, and the document omits the block of any field that does not arrive, so fill a field only when its PRODUCT.md section or interview answer exists. A legacy PRODUCT.md without Positioning, Platform, Operating Context, or Brand Commitments yields a context without those fields, never an invented value. `product.clarities` carries PRODUCT.md's "What must be clear first" list under a shorter key. `interview.references` and `interview.antiReference` also accept their older shapes, plain strings, which render as the bare pills and single-name callout they always did.
|
||||
The optionality is field by field, and the document omits the block of any field that does not arrive, so fill a field only when its PRODUCT.md section or interview answer exists. A legacy PRODUCT.md without Positioning, Platform, Operating Context, or Brand Commitments yields a context without those fields, never an invented value. `product.clarities` carries PRODUCT.md's "What must be clear first" list under a shorter key. `product.conversion` names the single action the product most wants. `product.principles` carries PRODUCT.md's Design Principles, one `{ title, detail }` entry per line. `product.surfaces` maps each mode the run might choose to what that surface is for this product, not the generic tile copy. Only include keys for surfaces that exist in the product; the document reads the map for whichever surfaces the questionnaire chose. `interview.references` and `interview.antiReference` also accept their older shapes, plain strings, which render as the bare pills and single-name callout they always did.
|
||||
|
||||
Three of the additions are derived at write time rather than asked: `brand.principles` copies the PRODUCT.md principles list (the current Product Principles heading or the legacy Design Principles one), `brand.voice` distills Brand Personality and Brand Commitments into two to four say / not pairs, each half a concrete line of wording the product would or would not publish, never an adjective, and `color.assetLocks` records color facts the provided assets fix (one short line each, written only when Step 2 actually read such a fact off an asset). None of the three adds an interview question, and all three are omitted rather than invented when their source is missing.
|
||||
|
||||
Five of the questions are then answered per surface rather than once for the whole run, because the answer that suits a marketing page rarely suits the tool it sells: `color-strategy`, `motion-energy` (how much movement there is), `boundary-style` (how sections are separated), `corner-style` (how round shapes are), and `depth-style` (how far off the page things sit). Each of the five comes back twice over. The bare key holds the leading surface's answer, which is the first chosen tile in tile order and the one every later screen previews. Alongside it is one `<key>-<mode>` key for every surface chosen, `<mode>` being `persuade`, `operate`, `read`, or `experience`. Surfaces the user never opened are included too, holding the default for their kind; a surface nobody chose returns nothing at all.
|
||||
|
||||
|
||||
@@ -397,16 +397,22 @@ const FONT_PAIR_READ_DOCS_LAYOUT_CSS = [
|
||||
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: start !important;',
|
||||
' 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) * 4.8) !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;',
|
||||
@@ -434,6 +440,10 @@ const FONT_PAIR_EXPERIENCE_LAYOUT_CSS = [
|
||||
'}',
|
||||
'#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;',
|
||||
@@ -443,17 +453,24 @@ const FONT_PAIR_EXPERIENCE_LAYOUT_CSS = [
|
||||
' 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-index-stop--on {',
|
||||
' padding: 0.55em 0.95em !important;',
|
||||
' border-radius: var(--pvs-radius-bar) !important;',
|
||||
' background-color: var(--pvs-cta) !important;',
|
||||
' color: var(--pv-neutral) !important;',
|
||||
' font-weight: 600 !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 {',
|
||||
@@ -461,6 +478,9 @@ const FONT_PAIR_EXPERIENCE_LAYOUT_CSS = [
|
||||
'}',
|
||||
].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 = [
|
||||
{
|
||||
@@ -518,6 +538,11 @@ const EXPERIMENTS = [
|
||||
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'],
|
||||
@@ -528,6 +553,7 @@ const EXPERIMENTS = [
|
||||
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',
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user