Files
pbakaus_impeccable/picker/pages/index.astro
T
2026-09-01 10:02:40 +05:00

1287 lines
60 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import Picker from '../layouts/Picker.astro';
import Artboard from '../components/Artboard.astro';
import QuestionScreen from '../components/QuestionScreen.astro';
import iconPacks from '../data/icon-packs.json';
const roles = [
['primary', 'Primary'],
['secondary', 'Secondary'],
['tertiary', 'Tertiary'],
['neutral', 'Neutral'],
];
// Musical intervals, smallest step first. The ratio is the multiplier between
// one size and the next; the description says what that much contrast is for,
// since the number alone tells nobody which to pick.
const scales = [
['minor-second', 1.067, 'Minor Second', 'Almost no contrast. For dense tools where weight and color carry hierarchy instead of size.'],
['major-second', 1.125, 'Major Second', 'A quiet step. Long documents and data screens that need many levels close together.'],
['minor-third', 1.2, 'Minor Third', 'The workhorse. Clear order without shouting, and it survives on small screens.'],
['major-third', 1.25, 'Major Third', 'Confident separation. A safe default when the page has both marketing and interface on it.'],
['perfect-fourth', 1.333, 'Perfect Fourth', 'Real jumps between levels. Landing pages where the headline has to lead.'],
['augmented-fourth', 1.414, 'Augmented Fourth', 'Sharp contrast. Editorial layouts built from few sizes used decisively.'],
['perfect-fifth', 1.5, 'Perfect Fifth', 'Display type against small text, with little in between. Poster thinking.'],
['golden-ratio', 1.618, 'Golden Ratio', 'The widest range here. Beautiful with one big statement, awkward with six levels.'],
];
// Exponent above the 16px base: P sits at the base, H1 six steps up.
const scaleRows = [['H1', 6], ['H2', 5], ['H3', 4], ['H4', 3], ['H5', 2], ['H6', 1], ['P', 0]];
// Only the row copy is rendered here. The glyphs are 109KB of markup for sets
// the run will use one of, so they are fetched when the screen first opens.
const iconRows = iconPacks.packs.map((pack) => ({
id: pack.id,
name: pack.name,
license: pack.license,
url: pack.url,
note: pack.note,
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
// whose answer cannot be read on a handset, so it takes the frame alone.
const questions = [
{
id: '06',
step: 'Motion',
slug: 'motion',
name: 'motion-energy',
title: 'How much movement should it have?',
legend: 'Motion energy',
cta: 'Select motion energy',
previewClass: 'picker-preview-motion',
phone: false,
options: [
{
value: 'restrained',
title: 'Restrained',
desc: 'State changes only. Things are already changed by the time you see them.',
},
{
value: 'responsive',
title: 'Responsive',
desc: 'Feedback and transitions on whatever you touch. Nothing is orchestrated.',
},
{
value: 'choreographed',
title: 'Choreographed',
desc: 'Entrances are staged and scrolling drives sequences.',
},
],
},
{
id: '07',
step: 'Layout',
slug: 'layout',
name: 'layout-structure',
title: 'How strict should the layout feel?',
legend: 'Layout structure',
cta: 'Select layout structure',
previewClass: 'picker-strategy-preview picker-preview-layout',
options: [
{
value: 'simple-grid',
title: 'Simple grid',
desc: 'Everything lines up. Equal columns and one rhythm down the page.',
},
{
value: 'balanced',
title: 'Balanced',
desc: 'An aligned grid with a few deliberate breaks that carry the emphasis.',
},
{
value: 'editorial',
title: 'Editorial',
desc: 'Uneven columns, with one of them clearly leading.',
},
{
value: 'freeform',
title: 'Freeform',
desc: 'Blocks step out of the grid and overlap where it pays off.',
},
],
},
{
id: '08',
step: 'Boundaries',
slug: 'boundaries',
name: 'boundary-style',
title: 'How should sections be separated?',
legend: 'Boundary style',
cta: 'Select boundary style',
previewClass: 'picker-preview-boundaries',
options: [
{
value: 'open-space',
title: 'Open space',
desc: 'Nothing between sections but room. Spacing does the dividing.',
},
{
value: 'thin-dividers',
title: 'Thin dividers',
desc: 'A hairline marks where one section ends and the next starts.',
},
{
value: 'surface-changes',
title: 'Surface changes',
desc: 'Bands of tone. Sections are told apart by the ground they sit on.',
},
{
value: 'cards-and-panels',
title: 'Cards and panels',
desc: 'Content sits inside containers with edges you can see.',
},
],
},
{
id: '09',
step: 'Corners',
slug: 'corners',
name: 'corner-style',
title: 'How round should shapes be?',
legend: 'Corner style',
cta: 'Select corner style',
previewClass: 'picker-preview-corners',
options: [
{
value: 'sharp',
title: 'Sharp',
desc: 'Square corners throughout. Precise, drafted, architectural.',
},
{
value: 'slightly-soft',
title: 'Slightly soft',
desc: 'Just enough radius to take the edge off without being noticed.',
},
{
value: 'friendly',
title: 'Friendly',
desc: 'Cards, buttons, and images are clearly rounded.',
},
{
value: 'pill',
title: 'Pill-like',
desc: 'Buttons and tags go fully round.',
},
],
},
{
id: '10',
step: 'Depth',
slug: 'depth',
name: 'depth-style',
title: 'How much depth should the surface have?',
legend: 'Depth style',
cta: 'Select depth style',
previewClass: 'picker-preview-depth',
options: [
{
value: 'flat',
title: 'Flat',
desc: 'One plane. Nothing casts a shadow.',
},
{
value: 'layered',
title: 'Layered',
desc: 'Tone stacks the page. Still no shadows anywhere.',
},
{
value: 'soft-lift',
title: 'Soft lift',
desc: 'Cards and controls sit a little off the page.',
},
{
value: 'floating',
title: 'Floating',
desc: 'Panels are well above the surface, with the lift to prove it.',
},
],
},
];
---
<Picker>
<main class="picker-shell">
<div class="picker-hero-art" style="background-image: url('assets/hero-dark.jpg')" aria-hidden="true"></div>
<form id="picker-form">
<section class="picker-screen" data-screen="01" data-active aria-labelledby="picker-start-title">
<div class="picker-container">
<div class="picker-start">
<div class="picker-brand">
<span class="picker-brand-logo" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M5 2.5 L13.5 2.5 L5.5 21.5 L5 21.5 Q2.5 21.5 2.5 19 L2.5 5 Q2.5 2.5 5 2.5 Z"></path>
<path d="M16.5 2.5 L19 2.5 Q21.5 2.5 21.5 5 L21.5 19 Q21.5 21.5 19 21.5 L8.5 21.5 Z"></path>
</svg>
</span>
<span class="picker-brand-name">Impeccable</span>
</div>
<h1 id="picker-start-title" class="picker-title">Let's build your<br />design system.</h1>
<div class="picker-actions">
<button class="ks-button ks-button-primary" type="button" data-advance="next">
Start
<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 class="picker-key-hint">
<div class="picker-key-cluster" aria-hidden="true">
<div class="picker-key-row picker-key-row-top">
<kbd class="picker-keycap">↑</kbd>
</div>
<div class="picker-key-row">
<kbd class="picker-keycap">←</kbd>
<kbd class="picker-keycap">↓</kbd>
<kbd class="picker-keycap">→</kbd>
</div>
</div>
<p class="picker-key-caption">Arrow keys to move</p>
</div>
</div>
</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" data-surface-label="Landing page" data-strategies="restrained committed full-palette drenched" data-strategy-default="committed" checked />
<span class="picker-mode-preview picker-mode-preview--exact" aria-hidden="true">
<span class="picker-preview">
<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-image"></span>
<span class="pv-mini-card"><i></i><span></span></span>
<span class="pv-mini-card"><i></i><span></span></span>
</span>
<span class="pv-tabbar"><i></i><i></i><i></i><i></i></span>
</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" data-surface-label="App UI" data-strategies="restrained committed full-palette" data-strategy-default="restrained" />
<span class="picker-mode-preview picker-mode-preview--exact" aria-hidden="true">
<span class="picker-preview picker-preview--ops">
<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="po-body">
<span class="po-side">
<span class="po-item"><i class="po-dot"></i><i class="po-line"></i></span>
<span class="po-item po-item--on"><i class="po-dot"></i><i class="po-line"></i></span>
<span class="po-item"><i class="po-dot"></i><i class="po-line"></i></span>
</span>
<span class="po-main">
<span class="po-chart">
<i style="--h: 19.23cqh"></i>
<i style="--h: 26.17cqh"></i>
<i style="--h: 16cqh"></i>
<i class="po-peak" style="--h: 30.7cqh"></i>
<i style="--h: 22.3cqh"></i>
</span>
<span class="po-divider"></span>
<span class="po-rows">
<span class="po-row">
<i class="po-dot"></i>
<i class="po-cell" style="--w: 15.24cqh"></i>
<i class="po-cell" style="--w: 12.76cqh"></i>
<i class="po-cell" style="--w: 12.76cqh"></i>
</span>
<span class="po-row po-row--on">
<i class="po-dot"></i>
<i class="po-cell" style="--w: 15.24cqh"></i>
<i class="po-cell" style="--w: 12.76cqh"></i>
<i class="po-cell" style="--w: 12.61cqh"></i>
</span>
<span class="po-row">
<i class="po-dot"></i>
<i class="po-cell" style="--w: 15.24cqh"></i>
<i class="po-cell" style="--w: 12.76cqh"></i>
<i class="po-cell" style="--w: 12.61cqh"></i>
</span>
</span>
</span>
<span class="po-panel">
<span class="po-heading"></span>
<span class="po-field"><i class="po-chev"></i></span>
<span class="po-switch"><i class="po-label"></i><i class="po-toggle po-toggle--on"></i></span>
<span class="po-switch"><i class="po-label"></i><i class="po-toggle"></i></span>
</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="po-phone-body">
<span class="po-chart">
<i style="--h: 17.34cqh"></i>
<i style="--h: 25.42cqh"></i>
<i style="--h: 15.33cqh"></i>
<i class="po-peak" style="--h: 33.57cqh"></i>
<i style="--h: 23.93cqh"></i>
</span>
<span class="po-field"><i class="po-chev"></i></span>
<span class="po-prow">
<i class="po-dot"></i>
<i class="po-label"></i>
<i class="po-toggle po-toggle--on"></i>
</span>
<span class="po-prow">
<i class="po-dot"></i>
<i class="po-label"></i>
<i class="po-chev po-chev--next"></i>
</span>
</span>
<span class="pv-tabbar"><i></i><i></i><i></i><i></i></span>
</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" data-surface-label="Docs" data-strategies="restrained committed full-palette" data-strategy-default="restrained" />
<span class="picker-mode-preview picker-mode-preview--exact" aria-hidden="true">
<span class="picker-preview picker-preview--docs">
<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="pd-body">
<span class="pd-side">
<span class="pd-item"><i></i><span></span></span>
<span class="pd-item pd-item--on"><i></i><span></span></span>
<span class="pd-item"><i></i><span></span></span>
<span class="pd-item"><i></i><span></span></span>
</span>
<span class="pd-main">
<span class="pd-heading" style="--w:95%"></span>
<span class="pd-para pd-para--lede"><i style="--w:71%"></i><i style="--w:41%"></i></span>
<span class="pd-para"><i></i><i></i><i style="--w:72%"></i></span>
<span class="pd-para"><i></i><i></i><i style="--w:43%"></i></span>
<span class="pd-note">
<i class="pd-note-dot"></i>
<span class="pd-note-lines"><i style="--w:85%"></i><i style="--w:62%"></i></span>
</span>
</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="pd-phone-body">
<span class="pd-crumb"></span>
<span class="pd-heading" style="--w:98.5%"></span>
<span class="pd-para pd-para--lede"><i style="--w:71%"></i><i style="--w:55%"></i></span>
<span class="pd-para"><i></i><i></i><i></i><i style="--w:69%"></i></span>
<span class="pd-para"><i></i><i></i><i style="--w:57%"></i></span>
<span class="pd-note">
<i class="pd-note-dot"></i>
<span class="pd-note-lines"><i style="--w:92%"></i><i style="--w:63%"></i></span>
</span>
</span>
<span class="pv-tabbar"><i></i><i></i><i></i><i></i></span>
</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" data-surface-label="Portfolio" data-strategies="restrained committed drenched" data-strategy-default="restrained" />
<span class="picker-mode-preview picker-mode-preview--exact" aria-hidden="true">
<span class="picker-preview picker-preview--gallery">
<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="pg-body">
<span class="pg-row">
<span class="pv-image"></span>
<span class="pg-cap">
<i class="pg-cap-title" style="--w:33.61%"></i>
<i></i>
<i style="--w:65.3%"></i>
</span>
</span>
<span class="pg-row pg-row--flip">
<span class="pg-cap">
<i class="pg-cap-title" style="--w:36.64%"></i>
<i></i>
<i style="--w:66.07%"></i>
</span>
<span class="pv-image"></span>
</span>
<span class="pg-rail">
<i class="pg-arrow"></i>
<span class="pg-track">
<i class="pg-track-on" style="--w:20.31cqh"></i>
<i style="--w:9.14cqh"></i>
<i style="--w:8.98cqh"></i>
<i style="--w:12.44cqh"></i>
</span>
<i class="pg-arrow pg-arrow--next"></i>
</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="pg-phone-body">
<span class="pv-image"></span>
<span class="pg-cap">
<i class="pg-cap-title" style="--w:30.18%"></i>
<i style="--w:79.68%"></i>
<i style="--w:55.6%"></i>
</span>
<span class="pv-image"></span>
<span class="pg-cap">
<i class="pg-cap-title" style="--w:30.18%"></i>
<i style="--w:79.68%"></i>
<i style="--w:55.6%"></i>
</span>
</span>
<span class="pv-tabbar"><i></i><i></i><i></i></span>
</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">
<h2 id="picker-palette-title" class="picker-question-title">Choose a color palette</h2>
<div class="picker-palette-grid">
<div class="picker-deck-column" aria-label="Palette sources">
<div class="picker-deck-stage">
<div class="picker-deck-scroller" data-deck-scroll aria-label="Browse palette cards">
<div data-deck-points></div>
</div>
<div class="picker-card-layer" data-deck-cards></div>
<div class="picker-loupe" data-loupe aria-hidden="true">
<canvas width="80" height="80"></canvas>
</div>
</div>
<template data-cue-card>
<article class="picker-card">
<div class="picker-card-face picker-cue-face">
<img alt="" />
{roles.map(([role, label]) => (
<button class="picker-ring" type="button" data-role={role} role="slider" aria-label={`${label} color picker`}></button>
))}
</div>
</article>
</template>
<template data-seed-card>
<article class="picker-card">
<div class="picker-card-face picker-seed-face">
{roles.map(() => <span></span>)}
</div>
</article>
</template>
<div class="picker-deck-controls">
<button class="picker-icon-button" type="button" data-deck-prev aria-label="Previous card">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m12.5 4.5-5 5.5 5 5.5"></path></svg>
</button>
<p class="picker-deck-count" data-deck-count aria-live="polite">Loading palettes</p>
<button class="picker-icon-button" type="button" data-deck-next aria-label="Next card">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m7.5 4.5 5 5.5-5 5.5"></path></svg>
</button>
</div>
<p class="picker-ring-guide" data-ring-guide>
<span>Drag the color rings to sample from the image.</span>
<span>Use arrow keys for precise adjustments.</span>
</p>
</div>
<div class="picker-palette-subgrid">
<fieldset class="picker-palette-panel">
<legend>Chosen colors</legend>
<button class="picker-icon-button picker-tip picker-reset" type="button" data-reset data-tip="Reset to this card's colors" aria-label="Reset to this card's colors">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M4.6 7.2A6.2 6.2 0 1 1 4 11.4M4.6 7.2V3.8m0 3.4H8"></path></svg>
</button>
<p
class="picker-palette-hint"
data-palette-hint
data-idle="Drag the rings on the image to pull colors from it, or edit any swatch directly."
data-primary="Your main brand color: buttons, links, the color people remember. Pick something with presence."
data-secondary="Supports the primary: section accents, hovers, secondary buttons. Adjacent, not identical."
data-tertiary="The rare accent: badges, highlights, one detail per screen. The most saturated of the four."
data-neutral="Backgrounds and large surfaces: most of every page. Quiet, near-white or near-black."
aria-live="polite"
>Drag the rings on the image to pull colors from it, or edit any swatch directly.</p>
<div class="picker-bands">
{roles.map(([role, label]) => (
<div class="picker-band-item" data-band-item={role}>
<div class="picker-band" data-band={role} tabindex="0">
<button class="picker-band-grip picker-tip" type="button" data-grip={role} data-tip="Drag to reorder" aria-label={`Reorder the ${label} color`}>
<svg viewBox="0 0 20 20" aria-hidden="true">
<circle cx="7" cy="5" r="1.5"></circle><circle cx="13" cy="5" r="1.5"></circle>
<circle cx="7" cy="10" r="1.5"></circle><circle cx="13" cy="10" r="1.5"></circle>
<circle cx="7" cy="15" r="1.5"></circle><circle cx="13" cy="15" r="1.5"></circle>
</svg>
</button>
<output data-band-hex={role}>#000000</output>
<div class="picker-band-tools" role="group" aria-label={`${label} color actions`}>
<button class="picker-band-tool picker-tip" type="button" data-copy-color={role} data-tip="Copy hex + OKLCH" aria-label={`Copy ${label} hex and OKLCH`}>
<svg viewBox="0 0 20 20" aria-hidden="true"><rect x="6.5" y="6.5" width="9" height="9"></rect><path d="M4.5 13.5h-1v-10h10v1"></path></svg>
</button>
<button class="picker-band-tool picker-tip" type="button" data-edit-tints={role} data-tip="Edit tints" aria-expanded="false" aria-label={`Edit ${label} tints`}>
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4 14.5-.4 2 2-.4L15 6.7 13.3 5 4 14.5Z"></path><path d="m11.9 6.4 1.7 1.7"></path></svg>
</button>
<button class="picker-band-tool picker-tip" type="button" data-custom-color={role} data-tip="Custom color" aria-label={`Choose custom ${label} color`}>
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 4v12M4 10h12"></path></svg>
</button>
</div>
<input class="picker-native-color" type="color" value="#000000" data-color-input={role} tabindex="-1" aria-label={`Custom ${label} color`} />
</div>
<div class="picker-band-foot">
<h2>{label}</h2>
<div class="picker-tint-strip" data-tints={role} hidden>
{Array.from({ length: 7 }, () => <button class="picker-tint" type="button" data-tint></button>)}
<button class="picker-tint-close" type="button" data-close-tints aria-label="Close tints">×</button>
</div>
</div>
</div>
))}
</div>
<p class="picker-band-status" data-reorder-status role="status" aria-live="polite"></p>
<div class="picker-preview" aria-hidden="true">
<div class="pv-desktop">
<div class="pv-nav">
<div class="pv-logo"></div>
<div class="pv-nav-bars"><i></i><i></i><i></i></div>
<div class="pv-pill"></div>
</div>
<div class="pv-hero">
<div class="pv-hero-copy">
<div class="pv-eyebrow"></div>
<div class="pv-headline"><i></i><i></i></div>
<div class="pv-copy-bar"></div>
<div class="pv-actions"><i></i><i></i></div>
</div>
<div class="pv-image"></div>
</div>
</div>
<div class="pv-phone">
<div class="pv-phone-top">
<div class="pv-logo"></div>
<div class="pv-avatar"></div>
</div>
<div class="pv-phone-body">
<div class="pv-image"></div>
<div class="pv-mini-card"><i></i><span></span></div>
<div class="pv-mini-card"><i></i><span></span></div>
</div>
<div class="pv-tabbar"><i></i><i></i><i></i><i></i></div>
</div>
</div>
</fieldset>
<div class="picker-actions-stack">
<button class="ks-button ks-button-primary picker-select" type="button" data-select-palette data-advance="next" disabled>
Select this palette
<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>
<button class="picker-back" type="button" data-advance="prev">Back</button>
</div>
</div>
</div>
<input type="hidden" name="palette-source" />
{roles.map(([role]) => <input type="hidden" name={`palette-${role}`} />)}
</div>
</div>
</section>
<section class="picker-screen" data-screen="03" data-step="Color strategy" aria-hidden="true" aria-labelledby="picker-strategy-title">
<div class="picker-container">
<div class="picker-strategy">
<h2 id="picker-strategy-title" class="picker-question-title">How much color<br />should this design use?</h2>
<div class="picker-strategy-grid">
<div class="picker-type-rail">
<fieldset class="picker-strategy-options picker-strategy-choices picker-type-options">
<legend>Color strategy</legend>
<label class="picker-strategy-option">
<input type="radio" name="color-strategy" value="restrained" checked />
<span class="picker-strategy-copy">
<span class="picker-strategy-title">Restrained</span>
<span class="picker-strategy-desc">Mostly neutral. The primary appears in a few deliberate places.</span>
</span>
</label>
<label class="picker-strategy-option">
<input type="radio" name="color-strategy" value="committed" />
<span class="picker-strategy-copy">
<span class="picker-strategy-title">Committed</span>
<span class="picker-strategy-desc">One strong color does real work across the page.</span>
</span>
</label>
<label class="picker-strategy-option">
<input type="radio" name="color-strategy" value="full-palette" />
<span class="picker-strategy-copy">
<span class="picker-strategy-title">Full palette</span>
<span class="picker-strategy-desc">All four colors on duty, each with a clear job.</span>
</span>
</label>
<label class="picker-strategy-option">
<input type="radio" name="color-strategy" value="drenched" />
<span class="picker-strategy-copy">
<span class="picker-strategy-title">Drenched</span>
<span class="picker-strategy-desc">The page itself is the color. Everything else sits on top.</span>
</span>
</label>
</fieldset>
{/* Every chosen surface is colored separately and leaves its own
answer, so each has a field to leave it in. They stay disabled
until their tile is chosen, which is what keeps a surface
nobody asked for out of the answers. */}
{surfaces.map(({ value }) => (
<input type="hidden" name={`color-strategy-${value}`} data-surface-strategy={value} disabled />
))}
<div class="picker-bands picker-bands--compact" data-band-scope="strategy">
{roles.map(([role, label]) => (
<div class="picker-band-item" data-band-item={role}>
<div class="picker-band" data-band={role}>
<button class="picker-band-grip picker-tip" type="button" data-grip={role} data-tip="Drag to reorder" aria-label={`Reorder the ${label} color`}>
<svg viewBox="0 0 20 20" aria-hidden="true">
<circle cx="7" cy="5" r="1.5"></circle><circle cx="13" cy="5" r="1.5"></circle>
<circle cx="7" cy="10" r="1.5"></circle><circle cx="13" cy="10" r="1.5"></circle>
<circle cx="7" cy="15" r="1.5"></circle><circle cx="13" cy="15" r="1.5"></circle>
</svg>
</button>
<output data-band-hex={role}>#000000</output>
</div>
<div class="picker-band-foot">
<h2>{label}</h2>
</div>
</div>
))}
</div>
<p class="picker-band-status" data-reorder-status role="status" aria-live="polite"></p>
</div>
{/* The stage the chosen surfaces are drawn on. Its contents are
lifted from the tiles on screen 01b, so it is filled by script;
the ratio is the artboard's so the CTA sits where it sits on
every other screen. */}
<div class="picker-strategy-stage" data-strategy-stage>
<div class="picker-surface-tabs" data-surface-tabs role="tablist" aria-label="Surface being colored" hidden></div>
</div>
</div>
<div class="picker-actions-stack">
<button class="ks-button ks-button-primary" type="button" data-advance="next">
Select this strategy
<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>
<button class="picker-back" type="button" data-advance="prev">Back</button>
</div>
</div>
</div>
</section>
<section class="picker-screen" data-screen="04" data-step="Typeface" aria-hidden="true" aria-labelledby="picker-type-title">
<div class="picker-container">
<div class="picker-type">
<h2 id="picker-type-title" class="picker-question-title">Choose a font pair</h2>
<div class="picker-type-grid">
<div class="picker-type-rail">
<fieldset class="picker-strategy-options picker-type-options" data-font-options>
<legend>Font pairs</legend>
</fieldset>
<div class="picker-type-controls">
<div class="picker-type-scroll">
<button class="picker-icon-button" type="button" data-list-scroll="-1" aria-label="Scroll font pairs up">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 12.5 5.5-5 5.5 5"></path></svg>
</button>
<button class="picker-icon-button" type="button" data-list-scroll="1" aria-label="Scroll font pairs down">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 7.5 5.5 5 5.5-5"></path></svg>
</button>
</div>
<button class="picker-type-custom" type="button" data-font-custom-open>
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 4.5v11M4.5 10h11"></path></svg>
Custom font
</button>
</div>
</div>
<Artboard mode="type" class="picker-strategy-preview picker-preview-type" typeHook />
</div>
<template data-pair-card>
<label class="picker-strategy-option picker-type-option">
<input type="radio" name="font-pair" />
<span class="picker-type-copy">
<span class="picker-type-sample-heading"><span data-pair-heading></span><span class="picker-type-role">headings</span></span>
<span class="picker-type-sample-body"><span data-pair-body></span><span class="picker-type-role">body text</span></span>
<span class="picker-type-desc" data-pair-why></span>
</span>
</label>
</template>
<input type="hidden" name="font-heading" />
<input type="hidden" name="font-body" />
<input type="hidden" name="font-heading-source" />
<input type="hidden" name="font-body-source" />
<div class="picker-actions-stack">
<button class="ks-button ks-button-primary" type="button" data-advance="next">
Select this pair
<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>
<button class="picker-back" type="button" data-advance="prev">Back</button>
</div>
</div>
</div>
</section>
<section class="picker-screen" data-screen="05" data-step="Type scale" aria-hidden="true" aria-labelledby="picker-scale-title">
<div class="picker-container">
<div class="picker-scale">
<div class="picker-question-head">
<h2 id="picker-scale-title" class="picker-question-title">Choose a type scale</h2>
<p class="picker-question-lede">
Create a polished, professional experience with text that stays consistent and easy to read everywhere.
</p>
</div>
<div class="picker-scale-grid">
<div class="picker-type-rail">
<fieldset class="picker-strategy-options picker-type-options picker-scale-options" data-scale-options>
<legend>Type scale</legend>
{scales.map(([slug, ratio, name, description]) => (
<label class="picker-strategy-option">
<input
type="radio"
name="type-scale"
value={slug}
data-ratio={ratio}
data-scale-name={name}
checked={slug === 'major-third'}
/>
<span class="picker-strategy-copy">
<span class="picker-strategy-title">
<b class="picker-scale-ratio">{ratio.toFixed(3)}</b>
{name}
</span>
<span class="picker-strategy-desc">{description}</span>
</span>
</label>
))}
</fieldset>
<div class="picker-type-controls">
<div class="picker-type-scroll">
<button class="picker-icon-button" type="button" data-list-scroll="-1" aria-label="Scroll type scales up">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 12.5 5.5-5 5.5 5"></path></svg>
</button>
<button class="picker-icon-button" type="button" data-list-scroll="1" aria-label="Scroll type scales down">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 7.5 5.5 5 5.5-5"></path></svg>
</button>
</div>
<p class="picker-scale-note">Values at a 16px base, specimen scaled to fit</p>
</div>
</div>
<div class="picker-scale-sheet" data-scale-sheet aria-hidden="true">
{scaleRows.map(([tag, step]) => (
<div class="ts-row" data-scale-row={step}>
<p class="ts-sample" data-scale-sample></p>
<span class="ts-tag">{tag}</span>
<span class="ts-values">
<b data-scale-px></b>
<i data-scale-rem></i>
</span>
</div>
))}
</div>
</div>
<input type="hidden" name="type-scale-ratio" />
<div class="picker-actions-stack">
<button class="ks-button ks-button-primary" type="button" data-advance="next">
Select this scale
<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>
<button class="picker-back" type="button" data-advance="prev">Back</button>
</div>
</div>
</div>
</section>
{questions.map((question) => <QuestionScreen {...question} />)}
<section class="picker-screen" data-screen="11" data-step="Icons" aria-hidden="true" aria-labelledby="picker-icons-title">
<div class="picker-container">
<div class="picker-icons">
<h2 id="picker-icons-title" class="picker-question-title">Choose an icon set</h2>
<div class="picker-icons-grid">
<div class="picker-type-rail">
<fieldset class="picker-strategy-options picker-type-options picker-icon-options" data-icon-options>
<legend>Icon set</legend>
{iconRows.map((pack, index) => (
<div class="picker-icon-row">
<label class="picker-strategy-option">
<input
type="radio"
name="icon-pack"
value={pack.id}
data-pack-name={pack.name}
data-pack-license={pack.license}
data-pack-url={pack.url}
checked={index === 0}
/>
<span class="picker-strategy-copy">
<span class="picker-strategy-title">{pack.name}</span>
<span class="picker-icon-meta">{pack.meta}</span>
<span class="picker-strategy-desc">{pack.note}</span>
</span>
</label>
<a
class="picker-icon-out"
href={pack.url}
target="_blank"
rel="noreferrer"
aria-label={`Open the ${pack.name} site in a new tab`}
>
<svg viewBox="0 0 20 20" aria-hidden="true">
<path d="M11 4.5h4.5V9M15.5 4.5 9.5 10.5M13 11.5v3.5a.5.5 0 0 1-.5.5h-7a.5.5 0 0 1-.5-.5v-7a.5.5 0 0 1 .5-.5H9"></path>
</svg>
</a>
</div>
))}
</fieldset>
<div class="picker-type-scroll">
<button class="picker-icon-button" type="button" data-list-scroll="-1" aria-label="Scroll icon sets up">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 12.5 5.5-5 5.5 5"></path></svg>
</button>
<button class="picker-icon-button" type="button" data-list-scroll="1" aria-label="Scroll icon sets down">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 7.5 5.5 5 5.5-5"></path></svg>
</button>
</div>
</div>
<div class="picker-icon-sheet" data-icon-sheet aria-hidden="true">
<div class="picker-icon-field" data-icon-field></div>
<div class="picker-icon-strip">
<span class="picker-icon-strip-label">At 16px</span>
<div class="picker-icon-strip-row" data-icon-strip></div>
</div>
</div>
</div>
<input type="hidden" name="icon-pack-name" />
<input type="hidden" name="icon-pack-license" />
<input type="hidden" name="icon-pack-url" />
<div class="picker-actions-stack">
<button class="ks-button ks-button-primary" type="button" data-advance="next">
Select this set
<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>
<button class="picker-back" type="button" data-advance="prev">Back</button>
</div>
</div>
</div>
</section>
<section class="picker-screen" data-screen="12" data-step="Review" aria-hidden="true" aria-labelledby="picker-finish-copy">
<div class="picker-container">
<div class="picker-placeholder">
<p id="picker-finish-copy">Your visual direction is ready.</p>
<div class="picker-actions-stack">
<button class="ks-button ks-button-primary" type="submit">
Finish
<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>
<button class="picker-back" type="button" data-advance="prev">Back</button>
</div>
</div>
</div>
</section>
<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>
</div>
<p class="picker-progress-label">
<span class="picker-progress-index"><b data-progress-index>1</b> / 11</span>
<span class="picker-progress-name" data-progress-name></span>
</p>
</div>
</form>
</main>
<dialog class="picker-modal" data-font-modal aria-labelledby="picker-font-modal-title">
<div class="picker-modal-inner">
<header class="picker-modal-head">
<h2 id="picker-font-modal-title">Add a custom font</h2>
<p>Link a hosted stylesheet or upload a face. Nothing is checked until you finish.</p>
</header>
{[['heading', 'Heading'], ['body', 'Body']].map(([role, label]) => (
<fieldset class="picker-modal-field">
<legend>{label}</legend>
<label class="picker-modal-label" for={`custom-${role}-url`}>Stylesheet or font URL</label>
<input
class="picker-modal-input"
id={`custom-${role}-url`}
type="url"
inputmode="url"
placeholder="https://example.com/typeface.css"
data-custom-url={role}
/>
<p class="picker-modal-or">or</p>
<div class="picker-modal-file-block">
<div class="picker-modal-file">
<input
class="picker-modal-file-input"
id={`custom-${role}-file`}
type="file"
accept=".woff2,.woff,.ttf,.otf,font/woff2,font/woff,font/ttf,font/otf,application/font-woff,application/font-woff2,application/x-font-ttf,application/x-font-otf"
multiple
data-custom-file={role}
/>
<label class="picker-modal-file-button" for={`custom-${role}-file`}>Choose font files</label>
</div>
<ul class="picker-modal-file-list" data-custom-file-list={role} hidden></ul>
</div>
</fieldset>
))}
<p class="picker-modal-status" data-custom-status aria-live="polite"></p>
<p class="picker-modal-hint" id="picker-font-modal-hint" data-custom-hint aria-live="polite"></p>
<div class="picker-modal-actions">
<button class="picker-back" type="button" data-font-custom-close>Cancel</button>
<button
class="ks-button ks-button-secondary"
type="button"
data-font-custom-save
aria-describedby="picker-font-modal-hint"
disabled
>Use these fonts</button>
</div>
</div>
</dialog>
<aside class="picker-width-gate" aria-labelledby="picker-width-title">
<div>
<h2 id="picker-width-title">This window is too narrow.</h2>
<p>Open this link in your browser at least 1200px wide.</p>
<div class="picker-copy-link">
<code data-copy-url-value aria-label="Picker URL"></code>
<button class="picker-copy-link-button" type="button" aria-label="Copy link" data-copy-url>
<svg class="picker-copy-link-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<svg class="picker-copy-link-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M20 6 9 17l-5-5"></path>
</svg>
<span>Copy link</span>
</button>
</div>
<p class="picker-copy-status" data-copy-status aria-live="polite">Paste it into your browser to continue.</p>
</div>
</aside>
<script>
import '../scripts/palette-picker.js';
const $ = (selector, root = document) => root.querySelector(selector);
const form = $('#picker-form');
const copyButton = $('[data-copy-url]');
const status = $('[data-copy-status]');
const screens = [...form.querySelectorAll('.picker-screen')];
let current = screens.findIndex((screen) => screen.hasAttribute('data-active'));
const url = new URL(location.href);
url.hostname = 'localhost';
const link = url.href.replace(/\/$/, '');
$('[data-copy-url-value]').textContent = link;
const controls = () => [...screens[current].querySelectorAll(
'button:not([disabled]), input:not([disabled]):not([type="hidden"]):not([tabindex="-1"]), select:not([disabled]), textarea:not([disabled]), a[href]',
)];
// Neither the start screen nor the review is a question, so the stepper
// 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);
progress.dataset.step = String(step);
$('[data-progress-index]').textContent = String(step);
$('[data-progress-name]').textContent = name || '';
};
// The palette screen and the strategy screen are the one pair in the run
// that shows the same objects twice, so they are the pair whose change is
// worth animating: the four colors and the test page travel between the two
// positions instead of being redrawn. Everywhere else there is nothing to
// carry, and capturing a screen that is mid-animation reads worse than a
// plain swap does.
const MORPH = new Set(['02', '03']);
const morphs = (from, to) => (
typeof document.startViewTransition === 'function'
&& !matchMedia('(prefers-reduced-motion: reduce)').matches
&& MORPH.has(from?.dataset.screen)
&& MORPH.has(to?.dataset.screen)
);
const goTo = (index) => {
const target = screens[index];
if (!target) return;
if (morphs(screens[current], target)) {
document.startViewTransition(() => swap(index));
return;
}
swap(index);
};
const swap = (index) => {
const target = screens[index];
screens.forEach((screen, screenIndex) => {
const active = screenIndex === index;
screen.toggleAttribute('data-active', active);
if (active) screen.removeAttribute('aria-hidden');
else screen.setAttribute('aria-hidden', 'true');
});
current = index;
const id = target.dataset.screen;
form.dataset.current = id;
setProgress(index, target.dataset.step);
document.dispatchEvent(new CustomEvent('picker:screenchange', {
detail: { screen: id },
}));
controls()[0]?.focus();
};
form.onclick = (e) => {
const control = e.target.closest('[data-advance]');
if (control) goTo(current + (control.dataset.advance === 'prev' ? -1 : 1));
};
document.addEventListener('keydown', (e) => {
if (e.defaultPrevented) return;
if (e.target instanceof Element && e.target.closest('input, textarea, select, [role="slider"]')) return;
if (e.key === 'Enter') {
if (current === 0 && matchMedia('(max-width: 1199px)').matches) return;
const control = current === 0 ? $('[data-advance="next"]', screens[0]) : document.activeElement;
if (controls().includes(control)) {
e.preventDefault();
control.click();
}
return;
}
const d = { ArrowUp: -1, ArrowLeft: -1, ArrowDown: 1, ArrowRight: 1 };
if (!(e.key in d)) return;
e.preventDefault();
const items = controls();
const focused = items.indexOf(document.activeElement);
const index = focused < 0 ? 0 : (focused + d[e.key] + items.length) % items.length;
items[index]?.focus();
});
form.onsubmit = async (e) => {
e.preventDefault();
const answers = {};
for (const [name, value] of new FormData(form)) {
if (!(name in answers)) answers[name] = value;
else answers[name] = Array.isArray(answers[name]) ? [...answers[name], value] : [answers[name], value];
}
await fetch('/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(answers),
});
};
copyButton.onclick = async () => {
try {
await navigator.clipboard.writeText(link);
copyButton.classList.add('is-copied');
copyButton.setAttribute('aria-label', 'Link copied');
status.textContent = 'Copied. Paste it into your browser to continue.';
setTimeout(() => {
copyButton.classList.remove('is-copied');
copyButton.setAttribute('aria-label', 'Copy link');
}, 1200);
} catch {
status.textContent = 'Copy failed. Select the link above and copy it instead.';
}
};
form.dataset.current = screens[current].dataset.screen;
setProgress(current, screens[current].dataset.step);
</script>
</Picker>