mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-16 08:06:24 +03:00
772 lines
38 KiB
Plaintext
772 lines
38 KiB
Plaintext
---
|
||
import Picker from '../layouts/Picker.astro';
|
||
|
||
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]];
|
||
---
|
||
|
||
<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="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>
|
||
|
||
<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">
|
||
<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-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>
|
||
|
||
<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-mini-card"><i></i><span></span></div>
|
||
<div class="pv-mini-card"><i></i><span></span></div>
|
||
<div class="pv-mini-chart"><i></i><i></i><i></i><i></i><i></i></div>
|
||
</div>
|
||
<div class="pv-tabbar"><i></i><i></i><i></i><i></i></div>
|
||
</div>
|
||
</div>
|
||
|
||
<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>
|
||
</div>
|
||
</fieldset>
|
||
</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>
|
||
</div>
|
||
|
||
<div class="picker-strategy-preview" data-strategy-preview aria-hidden="true">
|
||
<div class="ps-desktop">
|
||
<div class="ps-nav">
|
||
<div class="ps-brand-block"></div>
|
||
<div class="ps-nav-bars"><i></i><i></i><i></i><i></i></div>
|
||
<div class="ps-nav-action"></div>
|
||
</div>
|
||
<div class="ps-hero">
|
||
<div class="ps-hero-copy">
|
||
<div class="ps-eyebrow"></div>
|
||
<div class="ps-headline"><i></i><i></i></div>
|
||
<div class="ps-copy"><i></i></div>
|
||
<div class="ps-actions"><i></i><i></i></div>
|
||
</div>
|
||
<div class="ps-image"></div>
|
||
</div>
|
||
<div class="ps-proof">
|
||
<div class="ps-proof-item"><i></i><span></span></div>
|
||
<i class="ps-proof-divider" aria-hidden="true"></i>
|
||
<div class="ps-proof-item"><i></i><span></span></div>
|
||
<i class="ps-proof-divider" aria-hidden="true"></i>
|
||
<div class="ps-proof-item"><i></i><span></span></div>
|
||
<i class="ps-proof-divider" aria-hidden="true"></i>
|
||
<div class="ps-proof-item"><i></i><span></span></div>
|
||
</div>
|
||
<div class="ps-editorial">
|
||
<div class="ps-editorial-copy">
|
||
<strong></strong>
|
||
<span><i></i><i></i></span>
|
||
<em></em>
|
||
</div>
|
||
<div class="ps-gallery">
|
||
<div class="ps-gallery-item"><i></i><span><b></b><b></b></span></div>
|
||
<div class="ps-gallery-item"><i></i><span><b></b><b></b></span></div>
|
||
<div class="ps-gallery-item"><i></i><span><b></b><b></b></span></div>
|
||
<div class="ps-gallery-item"><i></i><span><b></b><b></b></span></div>
|
||
</div>
|
||
</div>
|
||
<div class="ps-footer">
|
||
<div class="ps-footer-links"><i></i><i></i><i></i><i></i></div>
|
||
<div class="ps-footer-mark"></div>
|
||
</div>
|
||
</div>
|
||
<div class="ps-phone">
|
||
<div class="ps-phone-top">
|
||
<div class="ps-brand-block"></div>
|
||
<div class="ps-nav-action"></div>
|
||
</div>
|
||
<div class="ps-phone-body">
|
||
<div class="ps-image"></div>
|
||
<div class="ps-headline"><i></i><i></i></div>
|
||
<div class="ps-copy"><i></i><i></i></div>
|
||
<div class="ps-actions"><i></i><i></i></div>
|
||
<div class="ps-proof">
|
||
<div class="ps-proof-item"><i></i><span></span></div>
|
||
<i class="ps-proof-divider" aria-hidden="true"></i>
|
||
<div class="ps-proof-item"><i></i><span></span></div>
|
||
</div>
|
||
<div class="ps-editorial-copy">
|
||
<strong></strong>
|
||
<span><i></i><i></i></span>
|
||
</div>
|
||
<div class="ps-gallery">
|
||
<div class="ps-gallery-item"><i></i><span><b></b><b></b></span></div>
|
||
<div class="ps-gallery-item"><i></i><span><b></b><b></b></span></div>
|
||
</div>
|
||
</div>
|
||
<div class="ps-phone-footer"><i></i><i></i><i></i></div>
|
||
</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>
|
||
|
||
<div class="picker-strategy-preview picker-preview-type" data-type-preview aria-hidden="true">
|
||
<div class="ps-desktop">
|
||
<div class="ps-nav">
|
||
<div class="ps-brand-block" data-type-brand></div>
|
||
<div class="ps-nav-bars"><i data-type-nav></i><i data-type-nav></i><i data-type-nav></i><i data-type-nav></i></div>
|
||
<div class="ps-nav-action" data-type-nav-action></div>
|
||
</div>
|
||
<div class="ps-hero">
|
||
<div class="ps-hero-copy">
|
||
<h2 class="pt-headline" data-type-headline></h2>
|
||
<p class="pt-body" data-type-body></p>
|
||
<div class="ps-actions"><i data-type-cta-primary></i><i data-type-cta-secondary></i></div>
|
||
</div>
|
||
<div class="ps-image"></div>
|
||
</div>
|
||
<div class="ps-proof">
|
||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||
<i class="ps-proof-divider" aria-hidden="true"></i>
|
||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||
<i class="ps-proof-divider" aria-hidden="true"></i>
|
||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||
<i class="ps-proof-divider" aria-hidden="true"></i>
|
||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||
</div>
|
||
<div class="ps-editorial">
|
||
<div class="ps-editorial-copy">
|
||
<strong data-type-section-title></strong>
|
||
<span><i data-type-section-body></i><i data-type-section-body></i></span>
|
||
<em data-type-section-link></em>
|
||
</div>
|
||
<div class="ps-gallery">
|
||
<div class="ps-gallery-item"><i></i><span><b data-type-gallery-title></b><b data-type-gallery-meta></b></span></div>
|
||
<div class="ps-gallery-item"><i></i><span><b data-type-gallery-title></b><b data-type-gallery-meta></b></span></div>
|
||
<div class="ps-gallery-item"><i></i><span><b data-type-gallery-title></b><b data-type-gallery-meta></b></span></div>
|
||
<div class="ps-gallery-item"><i></i><span><b data-type-gallery-title></b><b data-type-gallery-meta></b></span></div>
|
||
</div>
|
||
</div>
|
||
<div class="ps-footer">
|
||
<div class="ps-footer-links"><i data-type-footer-link></i><i data-type-footer-link></i><i data-type-footer-link></i><i data-type-footer-link></i></div>
|
||
<div class="ps-footer-mark" data-type-footer-mark></div>
|
||
</div>
|
||
</div>
|
||
<div class="ps-phone">
|
||
<div class="ps-phone-top">
|
||
<div class="ps-brand-block" data-type-brand></div>
|
||
<div class="ps-nav-action" data-type-menu-action></div>
|
||
</div>
|
||
<div class="ps-phone-body">
|
||
<div class="ps-image"></div>
|
||
<h2 class="pt-headline" data-type-headline></h2>
|
||
<p class="pt-body" data-type-body></p>
|
||
<div class="ps-actions"><i data-type-cta-primary></i><i data-type-cta-secondary></i></div>
|
||
<div class="ps-proof">
|
||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||
<i class="ps-proof-divider" aria-hidden="true"></i>
|
||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||
</div>
|
||
<div class="ps-editorial-copy">
|
||
<strong data-type-section-title></strong>
|
||
<span><i data-type-section-body></i><i data-type-section-body></i></span>
|
||
</div>
|
||
<div class="ps-gallery">
|
||
<div class="ps-gallery-item"><i></i><span><b data-type-gallery-title></b><b data-type-gallery-meta></b></span></div>
|
||
<div class="ps-gallery-item"><i></i><span><b data-type-gallery-title></b><b data-type-gallery-meta></b></span></div>
|
||
</div>
|
||
</div>
|
||
<div class="ps-phone-footer"><i data-type-footer-link></i><i data-type-footer-link></i><i data-type-footer-link></i></div>
|
||
</div>
|
||
</div>
|
||
</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" data-pair-heading></span>
|
||
<span class="picker-type-sample-body" data-pair-body></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">
|
||
A type scale is the short list of sizes every heading and paragraph is allowed to use, each one a
|
||
fixed ratio above the last. Setting it here means every screen inherits the same steps, so hierarchy
|
||
is decided once instead of re-guessed in each component.
|
||
</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 === 'minor-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>
|
||
|
||
<section class="picker-screen" data-screen="06" 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>
|
||
</div>
|
||
<p class="picker-progress-label">
|
||
<span class="picker-progress-index"><b data-progress-index>1</b> / 5</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>
|
||
|
||
<div class="picker-modal-actions">
|
||
<button class="ks-button ks-button-primary" type="button" data-font-custom-save>Use these fonts</button>
|
||
<button class="picker-back" type="button" data-font-custom-close>Cancel</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]',
|
||
)];
|
||
|
||
// The start screen is not a step, so the stepper counts from screen 02.
|
||
const progress = $('.picker-progress');
|
||
const setProgress = (index, name) => {
|
||
progress.dataset.step = String(index);
|
||
$('[data-progress-index]').textContent = String(Math.max(index, 1));
|
||
$('[data-progress-name]').textContent = name || '';
|
||
};
|
||
|
||
const goTo = (index) => {
|
||
const target = screens[index];
|
||
if (!target) return;
|
||
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>
|