mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-13 06:36:26 +03:00
update
This commit is contained in:
+188
-5
@@ -39,6 +39,38 @@ const iconRows = iconPacks.packs.map((pack) => ({
|
||||
meta: `${pack.grid} grid / ${pack.weight} / ${pack.license}`,
|
||||
}));
|
||||
|
||||
// The four kinds of surface a design system can be asked to serve, straight
|
||||
// from the skill's modes. Values travel as `surface-modes` (multi-select);
|
||||
// the agent pre-checks what PRODUCT.md suggests via cues.json, and the
|
||||
// visitor corrects it. Persuade is the markup default so the answer can
|
||||
// never arrive empty on runs whose cues carry no hint.
|
||||
const surfaces = [
|
||||
{
|
||||
value: 'persuade',
|
||||
title: 'Persuade',
|
||||
goal: 'Grab attention and move people to act.',
|
||||
examples: ['Landing pages', 'Marketing', 'Campaigns', 'Pricing'],
|
||||
},
|
||||
{
|
||||
value: 'operate',
|
||||
title: 'Operate',
|
||||
goal: 'Feel familiar so work gets done fast.',
|
||||
examples: ['App UI', 'Dashboards', 'Admin', 'Settings'],
|
||||
},
|
||||
{
|
||||
value: 'read',
|
||||
title: 'Read',
|
||||
goal: 'Get out of the way of understanding.',
|
||||
examples: ['Docs', 'Articles', 'Guides', 'Changelogs'],
|
||||
},
|
||||
{
|
||||
value: 'experience',
|
||||
title: 'Experience',
|
||||
goal: 'Be the work itself.',
|
||||
examples: ['Portfolios', 'Galleries', 'Showcases'],
|
||||
},
|
||||
];
|
||||
|
||||
// The five questions that are answered by looking at the same wireframe page.
|
||||
// Each one only rewrites the artboard slots it owns, so by the last screen the
|
||||
// page is showing every decision made before it. Motion is the one question
|
||||
@@ -251,6 +283,157 @@ const questions = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="picker-screen" data-screen="01b" data-step="Surfaces" aria-hidden="true" aria-labelledby="picker-modes-title">
|
||||
<div class="picker-container">
|
||||
<div class="picker-modes">
|
||||
<h2 id="picker-modes-title" class="picker-question-title">What are we designing?</h2>
|
||||
|
||||
<fieldset class="picker-modes-grid">
|
||||
<legend>Surfaces</legend>
|
||||
|
||||
<label class="picker-mode-tile">
|
||||
<input type="checkbox" name="surface-modes" value="persuade" aria-label="Landing pages, marketing, campaigns, and pricing" checked />
|
||||
<span
|
||||
class="picker-mode-preview picker-preview"
|
||||
aria-hidden="true"
|
||||
style="--pv-primary: #195856; --pv-secondary: #5F7C7A; --pv-tertiary: #3A958B; --pv-neutral: #EDEFEF; --pv-n-ink: var(--pk-ink-dark);"
|
||||
>
|
||||
<span class="pv-desktop">
|
||||
<span class="pv-nav">
|
||||
<span class="pv-logo"></span>
|
||||
<span class="pv-nav-bars"><i></i><i></i><i></i></span>
|
||||
<span class="pv-pill"></span>
|
||||
</span>
|
||||
<span class="pv-hero">
|
||||
<span class="pv-hero-copy">
|
||||
<span class="pv-eyebrow"></span>
|
||||
<span class="pv-headline"><i></i><i></i></span>
|
||||
<span class="pv-copy-bar"></span>
|
||||
<span class="pv-actions"><i></i><i></i></span>
|
||||
</span>
|
||||
<span class="pv-image"></span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pv-phone">
|
||||
<span class="pv-phone-top">
|
||||
<span class="pv-logo"></span>
|
||||
<span class="pv-avatar"></span>
|
||||
</span>
|
||||
<span class="pv-phone-body">
|
||||
<span class="pv-mini-card"><i></i><span></span></span>
|
||||
<span class="pv-mini-card"><i></i><span></span></span>
|
||||
<span class="pv-mini-chart"><i></i><i></i><i></i><i></i><i></i></span>
|
||||
</span>
|
||||
<span class="pv-tabbar"><i></i><i></i><i></i><i></i></span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="picker-mode-copy">
|
||||
<span class="picker-mode-pills"><i>Landing pages</i><i>Marketing</i><i>Campaigns</i><i>Pricing</i></span>
|
||||
<span class="picker-mode-goal">You're <strong>convincing</strong> someone to act. Lead with proof and a clear next step.</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="picker-mode-tile">
|
||||
<input type="checkbox" name="surface-modes" value="operate" aria-label="App UI, dashboards, admin, and settings" />
|
||||
<span class="picker-mode-preview" aria-hidden="true">
|
||||
<span class="pm-desktop pm-split">
|
||||
<span class="pm-side"><i></i><i></i><i></i><i></i></span>
|
||||
<span class="pm-main">
|
||||
<span class="pm-cards"><i></i><i></i><i></i></span>
|
||||
<span class="pm-rows"><i></i><i></i><i></i><i></i></span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pm-phone">
|
||||
<span class="pm-phone-top"><i class="pm-dot"></i><i class="pm-bar pm-faint" style="--w:30%"></i></span>
|
||||
<span class="pm-phone-body">
|
||||
<span class="pm-cards pm-cards-2"><i></i><i></i></span>
|
||||
<span class="pm-rows"><i></i><i></i><i></i></span>
|
||||
</span>
|
||||
<span class="pm-tabbar"><i></i><i></i><i></i><i></i></span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="picker-mode-copy">
|
||||
<span class="picker-mode-pills"><i>App UI</i><i>Dashboards</i><i>Admin</i><i>Settings</i></span>
|
||||
<span class="picker-mode-goal"><strong>Familiarity</strong> comes first. Keep paths obvious and the layout predictable.</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="picker-mode-tile">
|
||||
<input type="checkbox" name="surface-modes" value="read" aria-label="Docs, articles, guides, and changelogs" />
|
||||
<span class="picker-mode-preview" aria-hidden="true">
|
||||
<span class="pm-desktop pm-landing">
|
||||
<span class="pm-topbar"><i class="pm-dot"></i><span class="pm-links"><i></i><i></i></span></span>
|
||||
<span class="pm-article">
|
||||
<i class="pm-bar pm-strong" style="--w:62%"></i>
|
||||
<i class="pm-bar pm-faint" style="--w:34%"></i>
|
||||
<i class="pm-bar" style="--w:100%"></i>
|
||||
<i class="pm-bar" style="--w:100%"></i>
|
||||
<i class="pm-bar" style="--w:90%"></i>
|
||||
<i class="pm-bar" style="--w:97%"></i>
|
||||
<i class="pm-bar" style="--w:58%"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pm-phone">
|
||||
<span class="pm-phone-body">
|
||||
<i class="pm-bar pm-strong" style="--w:74%"></i>
|
||||
<i class="pm-bar pm-faint" style="--w:40%"></i>
|
||||
<i class="pm-bar" style="--w:100%"></i>
|
||||
<i class="pm-bar" style="--w:100%"></i>
|
||||
<i class="pm-bar" style="--w:88%"></i>
|
||||
<i class="pm-bar" style="--w:96%"></i>
|
||||
<i class="pm-bar" style="--w:64%"></i>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="picker-mode-copy">
|
||||
<span class="picker-mode-pills"><i>Docs</i><i>Articles</i><i>Guides</i><i>Changelogs</i></span>
|
||||
<span class="picker-mode-goal">The job is to <strong>read and understand</strong>. Let type and structure do the work.</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="picker-mode-tile">
|
||||
<input type="checkbox" name="surface-modes" value="experience" aria-label="Portfolios, galleries, and showcases" />
|
||||
<span class="picker-mode-preview" aria-hidden="true">
|
||||
<span class="pm-desktop">
|
||||
<span class="pm-mosaic">
|
||||
<span class="pm-media"></span>
|
||||
<span class="pm-media"></span>
|
||||
<span class="pm-media"></span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pm-phone">
|
||||
<span class="pm-phone-body pm-fill">
|
||||
<span class="pm-media"></span>
|
||||
<span class="pm-media"></span>
|
||||
<i class="pm-bar pm-faint" style="--w:46%"></i>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="picker-mode-copy">
|
||||
<span class="picker-mode-pills"><i>Portfolios</i><i>Galleries</i><i>Showcases</i></span>
|
||||
<span class="picker-mode-goal">You're <strong>showcasing</strong> work, not pitching it. Let browsing feel like exploring the work up close.</span>
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<p class="picker-modes-note">
|
||||
Previews only help you decide. They will <strong>not be the final design</strong>.
|
||||
</p>
|
||||
|
||||
<div class="picker-actions-stack">
|
||||
<button class="ks-button ks-button-primary" type="button" data-modes-next data-advance="next">
|
||||
Continue
|
||||
<span class="ks-button-arrow" aria-hidden="true">
|
||||
<svg viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square">
|
||||
<path d="M0 4h14M10 0l4 4-4 4"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="picker-screen" data-screen="02" data-step="Palette" aria-hidden="true" aria-labelledby="picker-palette-title">
|
||||
<div class="picker-container">
|
||||
<div class="picker-palette">
|
||||
@@ -707,10 +890,10 @@ const questions = [
|
||||
|
||||
<div class="picker-progress" aria-hidden="true">
|
||||
<div class="picker-progress-track">
|
||||
<i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i>
|
||||
<i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i>
|
||||
</div>
|
||||
<p class="picker-progress-label">
|
||||
<span class="picker-progress-index"><b data-progress-index>1</b> / 10</span>
|
||||
<span class="picker-progress-index"><b data-progress-index>1</b> / 11</span>
|
||||
<span class="picker-progress-name" data-progress-name></span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -812,9 +995,9 @@ const questions = [
|
||||
)];
|
||||
|
||||
// Neither the start screen nor the review is a question, so the stepper
|
||||
// counts from screen 02 and fills on the last one, icons. Review holds it
|
||||
// full rather than counting past the end.
|
||||
const STEPS = 10;
|
||||
// counts from the surfaces screen and fills on the last one, icons.
|
||||
// Review holds it full rather than counting past the end.
|
||||
const STEPS = 11;
|
||||
const progress = $('.picker-progress');
|
||||
const setProgress = (index, name) => {
|
||||
const step = Math.min(Math.max(index, 1), STEPS);
|
||||
|
||||
@@ -1264,9 +1264,30 @@ document.addEventListener('picker:screenchange', (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Screen 01b: the surface tiles. Multi-select with a floor of one, so the
|
||||
// answer can never arrive empty; the continue button holds that line while
|
||||
// the markup's default (persuade) keeps scripted runs moving.
|
||||
const modeInputs = [...document.querySelectorAll('input[name="surface-modes"]')];
|
||||
const modesNext = document.querySelector('[data-modes-next]');
|
||||
const syncModesNext = () => {
|
||||
if (modesNext) modesNext.disabled = !modeInputs.some((input) => input.checked);
|
||||
};
|
||||
for (const input of modeInputs) input.addEventListener('change', syncModesNext);
|
||||
syncModesNext();
|
||||
|
||||
try {
|
||||
const get = (url) => fetch(url).then((response) => response.ok ? response.json() : Promise.reject());
|
||||
const [cueData, seedData] = await Promise.all([get('/cues.json'), get('/palettes.json')]);
|
||||
// The agent's reading of PRODUCT.md arrives as cues.modes and pre-checks
|
||||
// the surface tiles. Applied only when it names at least one real tile, so
|
||||
// a bad hint cannot uncheck everything.
|
||||
if (Array.isArray(cueData.modes)) {
|
||||
const wanted = new Set(cueData.modes);
|
||||
if (modeInputs.some((input) => wanted.has(input.value))) {
|
||||
for (const input of modeInputs) input.checked = wanted.has(input.value);
|
||||
syncModesNext();
|
||||
}
|
||||
}
|
||||
cards = [
|
||||
...cueData.cues.map((id) => ({ id, type: 'cue', palette: cueData.palette[id] })),
|
||||
...seedData.seeds.map((seed) => ({ ...seed, type: 'seed' })),
|
||||
|
||||
+501
-5
@@ -275,6 +275,7 @@ body.picker-page {
|
||||
/* The art belongs to the start screen and the finish. Every question in
|
||||
between is judged against a plain ground, because a photograph behind a
|
||||
palette or a wireframe is a second opinion nobody asked for. */
|
||||
.picker-shell:has(#picker-form[data-current="01b"]) .picker-hero-art,
|
||||
.picker-shell:has(#picker-form[data-current="02"]) .picker-hero-art,
|
||||
.picker-shell:has(#picker-form[data-current="03"]) .picker-hero-art,
|
||||
.picker-shell:has(#picker-form[data-current="04"]) .picker-hero-art,
|
||||
@@ -300,13 +301,13 @@ body.picker-page {
|
||||
transition: opacity 180ms var(--ks-ease);
|
||||
}
|
||||
|
||||
/* Ten steps hold the same total width five did: the row is a measure of how
|
||||
far along the interview is, and a wider one would start competing with the
|
||||
question. The ticks divide whatever that width is. */
|
||||
/* Eleven steps hold the same total width five did: the row is a measure of
|
||||
how far along the interview is, and a wider one would start competing with
|
||||
the question. The ticks divide whatever that width is. */
|
||||
.picker-progress-track {
|
||||
width: 232px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(11, minmax(0, 1fr));
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
@@ -325,7 +326,8 @@ body.picker-page {
|
||||
.picker-progress[data-step="7"] .picker-progress-track i:nth-child(-n + 7),
|
||||
.picker-progress[data-step="8"] .picker-progress-track i:nth-child(-n + 8),
|
||||
.picker-progress[data-step="9"] .picker-progress-track i:nth-child(-n + 9),
|
||||
.picker-progress[data-step="10"] .picker-progress-track i {
|
||||
.picker-progress[data-step="10"] .picker-progress-track i:nth-child(-n + 10),
|
||||
.picker-progress[data-step="11"] .picker-progress-track i {
|
||||
background: var(--ks-kinpaku);
|
||||
}
|
||||
|
||||
@@ -1222,6 +1224,500 @@ body.picker-page {
|
||||
background: var(--pv-primary);
|
||||
}
|
||||
|
||||
/* ── Screen 01b: which surfaces ──────────────────────────────
|
||||
Four tiles, one per mode the skill recognizes, chosen before any visual
|
||||
question so every later answer knows what kind of page it is dressing.
|
||||
Multi-select: a product is allowed to be an app with a landing page and
|
||||
docs. The previews are deliberately monochrome, drawn entirely from the
|
||||
theme's own neutral ramp; color has not been chosen yet, and a gray
|
||||
wireframe cannot pre-sell one. */
|
||||
.picker-modes {
|
||||
display: grid;
|
||||
gap: clamp(12px, 2svh, 26px);
|
||||
justify-items: center;
|
||||
padding-block: clamp(16px, 2.6svh, 36px) clamp(12px, 2svh, 32px);
|
||||
}
|
||||
|
||||
.picker-modes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
/* Both rows take the taller row's height, so a goal or chip row that wraps
|
||||
in one tile cannot leave the other three sitting shorter than it. */
|
||||
grid-template-rows: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
/* Narrow enough that a proportional preview fills most of its tile. A wider
|
||||
grid leaves the frames marooned in empty space. */
|
||||
width: min(100%, 760px);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.picker-modes-grid legend {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.picker-mode-tile {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: clamp(7px, 1.1svh, 14px);
|
||||
padding: clamp(9px, 1.4svh, 16px);
|
||||
background: var(--ks-lacquer-raised);
|
||||
border: 1px solid var(--ks-rule);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 180ms var(--ks-ease),
|
||||
background-color 180ms var(--ks-ease);
|
||||
}
|
||||
|
||||
.picker-mode-tile input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.picker-mode-tile:hover {
|
||||
border-color: color-mix(in oklab, var(--ks-patina) 45%, var(--ks-rule));
|
||||
}
|
||||
|
||||
.picker-mode-tile:has(input:checked) {
|
||||
background: color-mix(in oklab, var(--ks-patina) 9%, var(--ks-lacquer-raised));
|
||||
border-color: var(--ks-patina);
|
||||
}
|
||||
|
||||
.picker-mode-tile:has(input:focus-visible) {
|
||||
outline: 2px solid var(--ks-patina-deep);
|
||||
outline-offset: -2px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* The tile's little pair of frames: same anatomy as the palette screen's
|
||||
preview, drawn in the neutral ramp. Bars take their width from --w so each
|
||||
mode's silhouette is written in the markup, not in a pile of one-off
|
||||
classes. The three bar weights below are the whole drawing vocabulary. */
|
||||
.picker-mode-preview {
|
||||
--pm-strong: var(--ks-text-muted);
|
||||
--pm-mid: var(--ks-text-mute-deep);
|
||||
--pm-faint: color-mix(in oklab, var(--ks-text-mute-deep) 45%, var(--ks-graphite));
|
||||
--pm-fill: var(--ks-graphite-2);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
/* The height is the budget and the width follows it, at the same 16 / 7 the
|
||||
palette screen's preview uses. Letting the tile set the width instead
|
||||
stretched the desktop card to about five times its own ratio. */
|
||||
height: clamp(104px, 13svh, 152px);
|
||||
aspect-ratio: 16 / 7;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.pm-desktop,
|
||||
.pm-phone {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
background: var(--ks-graphite);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.pm-phone {
|
||||
height: 100%;
|
||||
aspect-ratio: 9 / 19;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.pm-phone > .pm-phone-body:first-child {
|
||||
grid-row: 1 / -1;
|
||||
}
|
||||
|
||||
.pm-landing {
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
.pm-bar {
|
||||
display: block;
|
||||
width: var(--w, 100%);
|
||||
height: 5px;
|
||||
background: var(--pm-mid);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pm-strong {
|
||||
height: 8px;
|
||||
background: var(--pm-strong);
|
||||
}
|
||||
|
||||
.pm-faint {
|
||||
background: var(--pm-faint);
|
||||
}
|
||||
|
||||
.pm-dot {
|
||||
width: 5px;
|
||||
aspect-ratio: 1;
|
||||
background: var(--pm-strong);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.pm-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.pm-links {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.pm-links i {
|
||||
width: 10px;
|
||||
height: 3px;
|
||||
background: var(--pm-mid);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pm-media {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background: var(--pm-fill);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Operate: rail plus work area, the one silhouette nobody mistakes for a
|
||||
landing page. */
|
||||
.pm-split {
|
||||
grid-template-columns: 26% minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.pm-side {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 7px;
|
||||
padding: 9px 8px;
|
||||
border-right: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.pm-side i {
|
||||
height: 5px;
|
||||
background: var(--pm-mid);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pm-side i:first-child {
|
||||
background: var(--pm-strong);
|
||||
}
|
||||
|
||||
.pm-main {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: 7px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.pm-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pm-cards-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.pm-cards i {
|
||||
height: 26px;
|
||||
background: var(--pm-fill);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pm-rows {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.pm-rows i {
|
||||
height: 5px;
|
||||
background: var(--pm-faint);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Read: one measured column of text and nothing competing with it. */
|
||||
.pm-article {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
padding: 8px 20%;
|
||||
}
|
||||
|
||||
/* Experience: the pictures are the page. */
|
||||
.pm-mosaic {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr 1fr;
|
||||
grid-template-rows: repeat(2, minmax(0, 1fr));
|
||||
gap: 5px;
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.pm-mosaic .pm-media:first-child {
|
||||
grid-row: 1 / -1;
|
||||
}
|
||||
|
||||
.pm-phone-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 7px;
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.pm-phone-body {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 5px;
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.pm-fill {
|
||||
align-content: stretch;
|
||||
grid-template-rows: 1fr 1fr auto;
|
||||
}
|
||||
|
||||
.pm-tabbar {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
border-top: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.pm-tabbar i {
|
||||
width: 4px;
|
||||
aspect-ratio: 1;
|
||||
background: var(--pm-mid);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.pm-tabbar i:first-child {
|
||||
background: var(--pm-strong);
|
||||
}
|
||||
|
||||
/* Persuade carries the palette screen's preview component itself, class and
|
||||
all, so it inherits that component's every color derivation. The tile also
|
||||
sets the four palette roles inline, because the palette screen has not been
|
||||
reached yet and the component would otherwise fall back to the picker's own
|
||||
dark chrome. Only the sizes below change: the component is authored for a
|
||||
box roughly three times this tall, and every rule here is scoped to the
|
||||
tile so screen 02 renders exactly as it did. */
|
||||
.picker-mode-preview .pv-desktop {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-phone {
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-nav {
|
||||
gap: 5px;
|
||||
padding: 4px 7px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-logo {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-nav-bars {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-nav-bars i {
|
||||
width: 9px;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-pill {
|
||||
width: 15px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-hero {
|
||||
gap: 7%;
|
||||
padding: 3px 8% 6px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-hero-copy {
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-eyebrow {
|
||||
width: 30%;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-headline {
|
||||
gap: 3px;
|
||||
margin-block: 1px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-headline i {
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-copy-bar {
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-actions {
|
||||
gap: 4px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-actions i {
|
||||
width: 18px;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
/* The frame is wide and short here, so a 4 / 3 plate would stand taller than
|
||||
the whole desktop: the image takes its height from the hero row instead of
|
||||
setting it. Its gradient and border are the component's own. */
|
||||
.picker-mode-preview .pv-image {
|
||||
align-self: stretch;
|
||||
aspect-ratio: auto;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-image::after {
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-phone-top {
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-avatar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-phone-body {
|
||||
gap: 3px;
|
||||
padding: 4px 4px 5px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-mini-card {
|
||||
gap: 2px;
|
||||
padding: 2px 3px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-mini-card i,
|
||||
.picker-mode-preview .pv-mini-card span {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-mini-card span {
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-mini-chart {
|
||||
gap: 2px;
|
||||
padding: 2px 3px;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-tabbar {
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
.picker-mode-preview .pv-tabbar i {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.picker-mode-copy {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.picker-mode-goal {
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* The examples name the tile now that the mode word is gone, so they lead the
|
||||
copy block and carry its strongest weight. Sized to keep a four-chip row
|
||||
within two lines at the narrowest tile the grid produces. */
|
||||
.picker-mode-pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.picker-mode-pills i {
|
||||
padding: 3px 7px;
|
||||
color: var(--ks-champagne);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
letter-spacing: 0.015em;
|
||||
border: 1px solid color-mix(in oklab, var(--ks-champagne) 24%, var(--ks-rule));
|
||||
border-radius: 2px;
|
||||
transition:
|
||||
color 180ms var(--ks-ease),
|
||||
border-color 180ms var(--ks-ease);
|
||||
}
|
||||
|
||||
.picker-mode-tile:hover .picker-mode-pills i {
|
||||
border-color: color-mix(in oklab, var(--ks-patina) 30%, var(--ks-rule));
|
||||
}
|
||||
|
||||
.picker-mode-tile:has(input:checked) .picker-mode-pills i {
|
||||
color: color-mix(in oklab, var(--ks-patina) 85%, var(--ks-champagne));
|
||||
border-color: color-mix(in oklab, var(--ks-patina) 55%, var(--ks-rule));
|
||||
}
|
||||
|
||||
/* The wording here is doing careful work: every preview in the interview is
|
||||
an isolating instrument, not a sketch of the deliverable, and this is the
|
||||
one place the visitor is told so before meeting the rest of them. */
|
||||
.picker-modes-note {
|
||||
max-width: 52ch;
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 1.08rem;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.picker-modes-note strong,
|
||||
.picker-mode-goal strong {
|
||||
color: var(--ks-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Screen 03 mirrors screen 02's rhythm: the same centered headline class,
|
||||
the same vertical gaps, options left and preview right. */
|
||||
.picker-strategy {
|
||||
|
||||
@@ -436,6 +436,8 @@ Done when: `fonts.json` is parseable, contains exactly six ranked pairs, every f
|
||||
|
||||
## Step 7: Launch the picker
|
||||
|
||||
Before launching, add a top-level `modes` array to `cues.json` naming the surface kinds this product already implies, judged from PRODUCT.md and the codebase: any of `persuade`, `operate`, `read`, `experience`. The picker's first question pre-checks those tiles as its starting point; the user corrects the set by hand, and the final selection returns in the answers as `surface-modes`. Omit the field when the product gives no clear signal; the picker then starts from `persuade` alone.
|
||||
|
||||
Tell the user in one line that the visual cues are ready at `.impeccable/visual-cues/` (name the count), then run `node {{scripts_path}}/picker-server.mjs` from the project root as a foreground command and parse its `PICKER_URL` line.
|
||||
|
||||
- **The harness has a browser tool**: open the URL with it and let the user drive. The tool is a viewport only; never drive the questionnaire yourself, because the answers are the user's.
|
||||
|
||||
Reference in New Issue
Block a user