mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-21 18:47:02 +03:00
update
This commit is contained in:
+28
-28
@@ -191,7 +191,7 @@ const roles = [
|
||||
<h1 id="picker-strategy-title" class="picker-palette-title">How much color should this design use?</h1>
|
||||
|
||||
<div class="picker-strategy-grid">
|
||||
<fieldset class="picker-strategy-options">
|
||||
<fieldset class="picker-strategy-options picker-strategy-choices">
|
||||
<legend>Color strategy</legend>
|
||||
|
||||
<label class="picker-strategy-option">
|
||||
@@ -318,66 +318,66 @@ const roles = [
|
||||
<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">AC</div>
|
||||
<div class="ps-nav-bars"><i>Product</i><i>Stories</i><i>Pricing</i><i>About</i></div>
|
||||
<div class="ps-nav-action">Sign in</div>
|
||||
<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>Get started</i><i>Learn more</i></div>
|
||||
<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>4.9 rating</span></div>
|
||||
<div class="ps-proof-item"><i></i><span>12k members</span></div>
|
||||
<div class="ps-proof-item"><i></i><span>Est. 2019</span></div>
|
||||
<div class="ps-proof-item"><i></i><span>Free shipping</span></div>
|
||||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||||
</div>
|
||||
<div class="ps-editorial">
|
||||
<div class="ps-editorial-copy">
|
||||
<strong>Made to last</strong>
|
||||
<span><i>Every piece starts as a sketch and</i><i>earns its place in the collection.</i></span>
|
||||
<em>Read the story</em>
|
||||
<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>Morning set</b><b>From $48</b></span></div>
|
||||
<div class="ps-gallery-item"><i></i><span><b>Studio series</b><b>From $64</b></span></div>
|
||||
<div class="ps-gallery-item"><i></i><span><b>Archive No. 3</b><b>From $32</b></span></div>
|
||||
<div class="ps-gallery-item"><i></i><span><b>Limited run</b><b>From $96</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 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>Privacy</i><i>Terms</i><i>Contact</i><i>Careers</i></div>
|
||||
<div class="ps-footer-mark">© 2026 AC</div>
|
||||
<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">AC</div>
|
||||
<div class="ps-nav-action">Menu</div>
|
||||
<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>Get started</i><i>Learn more</i></div>
|
||||
<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>4.9 rating</span></div>
|
||||
<div class="ps-proof-item"><i></i><span>12k members</span></div>
|
||||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||||
</div>
|
||||
<div class="ps-editorial-copy">
|
||||
<strong>Made to last</strong>
|
||||
<span><i>Every piece starts as a sketch and</i><i>earns its place in the collection.</i></span>
|
||||
<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>Morning set</b><b>From $48</b></span></div>
|
||||
<div class="ps-gallery-item"><i></i><span><b>Studio series</b><b>From $64</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-phone-footer"><i>Privacy</i><i>Terms</i><i>Contact</i></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>
|
||||
|
||||
@@ -30,6 +30,29 @@ const FALLBACK_FONTS = {
|
||||
headline: 'Shape a clear visual direction',
|
||||
body: 'Choose a type system that gives the product a distinct and usable voice.',
|
||||
},
|
||||
preview: {
|
||||
brand: 'Im',
|
||||
nav: ['Skills', 'Detect', 'Docs', 'About'],
|
||||
navAction: 'Sign in',
|
||||
menuAction: 'Menu',
|
||||
ctaPrimary: 'Get started',
|
||||
ctaSecondary: 'Learn more',
|
||||
proof: ['23 commands', 'Open source', 'Any harness', 'Free to install'],
|
||||
sectionTitle: 'Design laws, not vibes',
|
||||
sectionBody: [
|
||||
'Shared vocabulary your agent can follow',
|
||||
'when shaping interfaces.',
|
||||
],
|
||||
sectionLink: 'Read the skill',
|
||||
gallery: [
|
||||
{ title: 'Audit', meta: 'Quality pass' },
|
||||
{ title: 'Polish', meta: 'Ship ready' },
|
||||
{ title: 'Bolder', meta: 'Turn it up' },
|
||||
{ title: 'Quieter', meta: 'Pull it back' },
|
||||
],
|
||||
footerLinks: ['GitHub', 'Changelog', 'CLI', 'Extension'],
|
||||
footerMark: '© Impeccable',
|
||||
},
|
||||
pairs: [
|
||||
{
|
||||
id: 'source-editorial',
|
||||
@@ -160,10 +183,48 @@ function syncCommittedPalette(target) {
|
||||
target.style.setProperty('--pv-p-ink', contrastInk(committed.primary));
|
||||
}
|
||||
|
||||
function isPreviewCopy(value) {
|
||||
return value
|
||||
&& typeof value.brand === 'string'
|
||||
&& Array.isArray(value.nav)
|
||||
&& value.nav.length === 4
|
||||
&& value.nav.every((entry) => typeof entry === 'string')
|
||||
&& typeof value.navAction === 'string'
|
||||
&& typeof value.menuAction === 'string'
|
||||
&& typeof value.ctaPrimary === 'string'
|
||||
&& typeof value.ctaSecondary === 'string'
|
||||
&& Array.isArray(value.proof)
|
||||
&& value.proof.length === 4
|
||||
&& value.proof.every((entry) => typeof entry === 'string')
|
||||
&& typeof value.sectionTitle === 'string'
|
||||
&& Array.isArray(value.sectionBody)
|
||||
&& value.sectionBody.length === 2
|
||||
&& value.sectionBody.every((entry) => typeof entry === 'string')
|
||||
&& typeof value.sectionLink === 'string'
|
||||
&& Array.isArray(value.gallery)
|
||||
&& value.gallery.length === 4
|
||||
&& value.gallery.every((entry) => (
|
||||
typeof entry.title === 'string'
|
||||
&& typeof entry.meta === 'string'
|
||||
))
|
||||
&& Array.isArray(value.footerLinks)
|
||||
&& value.footerLinks.length === 4
|
||||
&& value.footerLinks.every((entry) => typeof entry === 'string')
|
||||
&& typeof value.footerMark === 'string';
|
||||
}
|
||||
|
||||
function normalizeFontManifest(manifest) {
|
||||
return {
|
||||
...manifest,
|
||||
preview: { ...FALLBACK_FONTS.preview, ...manifest.preview },
|
||||
};
|
||||
}
|
||||
|
||||
function isFontManifest(value) {
|
||||
return value?.version === 1
|
||||
&& typeof value.specimen?.headline === 'string'
|
||||
&& typeof value.specimen?.body === 'string'
|
||||
&& (!value.preview || isPreviewCopy(value.preview))
|
||||
&& value.pairs?.length === 6
|
||||
&& value.pairs.every((pair) => (
|
||||
typeof pair.id === 'string'
|
||||
@@ -173,6 +234,7 @@ function isFontManifest(value) {
|
||||
&& typeof pair.body?.family === 'string'
|
||||
&& Number.isFinite(pair.body?.weight)
|
||||
&& typeof pair.why === 'string'
|
||||
&& (!pair.preview || isPreviewCopy(pair.preview))
|
||||
));
|
||||
}
|
||||
|
||||
@@ -198,19 +260,72 @@ function loadFontStylesheet(pairs) {
|
||||
document.head.append(link);
|
||||
}
|
||||
|
||||
function fillIndexed(root, selector, values) {
|
||||
if (!root) return;
|
||||
root.querySelectorAll(selector).forEach((node, index) => {
|
||||
node.textContent = values[index] ?? '';
|
||||
});
|
||||
}
|
||||
|
||||
function fillGallery(root, gallery) {
|
||||
if (!root) return;
|
||||
root.querySelectorAll('.ps-gallery-item').forEach((item, index) => {
|
||||
const title = item.querySelector('[data-type-gallery-title]');
|
||||
const meta = item.querySelector('[data-type-gallery-meta]');
|
||||
if (title) title.textContent = gallery[index]?.title ?? '';
|
||||
if (meta) meta.textContent = gallery[index]?.meta ?? '';
|
||||
});
|
||||
}
|
||||
|
||||
function syncFontPair(pair) {
|
||||
const specimen = { ...fontManifest.specimen, ...pair.specimen };
|
||||
const manifest = fontManifest;
|
||||
const preview = {
|
||||
...FALLBACK_FONTS.preview,
|
||||
...manifest.preview,
|
||||
...pair.preview,
|
||||
};
|
||||
const specimen = { ...manifest.specimen, ...pair.specimen };
|
||||
const desktop = typePreview.querySelector('.ps-desktop');
|
||||
const phoneBody = typePreview.querySelector('.ps-phone-body');
|
||||
const phoneFooter = typePreview.querySelector('.ps-phone-footer');
|
||||
typePreview.style.setProperty('--pt-heading', fontStack(pair.heading.family));
|
||||
typePreview.style.setProperty('--pt-body', fontStack(pair.body.family));
|
||||
typePreview.style.setProperty('--pt-heading-weight', pair.heading.weight);
|
||||
for (const node of document.querySelectorAll('[data-type-brand]')) node.textContent = preview.brand;
|
||||
fillIndexed(desktop, '[data-type-nav]', preview.nav);
|
||||
for (const node of document.querySelectorAll('[data-type-nav-action]')) node.textContent = preview.navAction;
|
||||
for (const node of document.querySelectorAll('[data-type-menu-action]')) node.textContent = preview.menuAction;
|
||||
for (const node of document.querySelectorAll('[data-type-headline]')) node.textContent = specimen.headline;
|
||||
for (const node of document.querySelectorAll('[data-type-body]')) node.textContent = specimen.body;
|
||||
document.querySelectorAll('[data-type-cta-primary]').forEach((node) => {
|
||||
node.textContent = preview.ctaPrimary;
|
||||
});
|
||||
document.querySelectorAll('[data-type-cta-secondary]').forEach((node) => {
|
||||
node.textContent = preview.ctaSecondary;
|
||||
});
|
||||
fillIndexed(desktop, '[data-type-proof]', preview.proof);
|
||||
fillIndexed(phoneBody, '[data-type-proof]', preview.proof);
|
||||
document.querySelectorAll('[data-type-section-title]').forEach((node) => {
|
||||
node.textContent = preview.sectionTitle;
|
||||
});
|
||||
fillIndexed(desktop, '[data-type-section-body]', preview.sectionBody);
|
||||
fillIndexed(phoneBody, '[data-type-section-body]', preview.sectionBody);
|
||||
document.querySelectorAll('[data-type-section-link]').forEach((node) => {
|
||||
node.textContent = preview.sectionLink;
|
||||
});
|
||||
fillGallery(desktop, preview.gallery);
|
||||
fillGallery(phoneBody, preview.gallery);
|
||||
fillIndexed(desktop.querySelector('.ps-footer'), '[data-type-footer-link]', preview.footerLinks);
|
||||
fillIndexed(phoneFooter, '[data-type-footer-link]', preview.footerLinks);
|
||||
document.querySelectorAll('[data-type-footer-mark]').forEach((node) => {
|
||||
node.textContent = preview.footerMark;
|
||||
});
|
||||
document.querySelector('[name="font-heading"]').value = pair.heading.family;
|
||||
document.querySelector('[name="font-body"]').value = pair.body.family;
|
||||
}
|
||||
|
||||
function renderFontPairs(manifest, fallback) {
|
||||
fontManifest = manifest;
|
||||
fontManifest = normalizeFontManifest(manifest);
|
||||
fontOptions.toggleAttribute('data-fallback', fallback);
|
||||
const fragment = document.createDocumentFragment();
|
||||
manifest.pairs.forEach((pair, index) => {
|
||||
@@ -504,7 +619,7 @@ try {
|
||||
const response = await fetch('/fonts.json');
|
||||
const candidate = response.ok ? await response.json() : null;
|
||||
if (isFontManifest(candidate)) {
|
||||
manifest = candidate;
|
||||
manifest = normalizeFontManifest(candidate);
|
||||
usingFallback = false;
|
||||
}
|
||||
} catch {
|
||||
|
||||
+230
-79
@@ -1064,11 +1064,12 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(340px, 420px) minmax(0, 1fr);
|
||||
gap: 50px;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* One raised panel, hairline rows — same vocabulary as the palette bands
|
||||
on screen 02, not four floating SaaS cards. */
|
||||
on screen 02, not four floating SaaS cards. Screen 04 reuses the base
|
||||
shell; screen 03 opts into the choices treatment below. */
|
||||
.picker-strategy-options {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
@@ -1164,6 +1165,44 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Screen 03: copy-only rows inside the shared options panel. */
|
||||
.picker-strategy-choices .picker-strategy-option {
|
||||
padding: 22px 26px 22px 28px;
|
||||
}
|
||||
|
||||
.picker-strategy-choices .picker-strategy-option::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.picker-strategy-choices .picker-strategy-option:hover {
|
||||
background: color-mix(in oklab, var(--ks-patina-deep) 5%, var(--ks-lacquer-raised));
|
||||
}
|
||||
|
||||
.picker-strategy-choices .picker-strategy-option:has(input:checked) {
|
||||
background: color-mix(in oklab, var(--ks-patina-deep) 9%, var(--ks-lacquer-raised));
|
||||
}
|
||||
|
||||
.picker-strategy-choices .picker-strategy-option:has(input:checked) .picker-strategy-copy strong {
|
||||
color: var(--ks-link-on-paper);
|
||||
}
|
||||
|
||||
.picker-strategy-choices .picker-strategy-copy {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.picker-strategy-choices .picker-strategy-copy strong {
|
||||
font-family: var(--ks-font);
|
||||
font-size: 24px;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.picker-strategy-choices .picker-strategy-copy span {
|
||||
font-size: 16px;
|
||||
line-height: 1.55;
|
||||
max-width: 34ch;
|
||||
}
|
||||
|
||||
/* Screen 03 shows one editorial landing page at desktop and phone sizes.
|
||||
Geometry stays fixed while the strategy radios remap its color slots. */
|
||||
.picker-strategy-preview {
|
||||
@@ -1175,12 +1214,13 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
--pv-p-ink: var(--ks-champagne);
|
||||
--pv-surface-2: color-mix(in oklab, var(--pv-neutral) 92%, var(--pv-n-ink));
|
||||
--pv-bone: color-mix(in oklab, var(--pv-neutral) 62%, var(--pv-n-ink));
|
||||
--pv-muted: color-mix(in oklab, var(--pv-neutral) 84%, var(--pv-n-ink));
|
||||
--pv-strong: color-mix(in oklab, var(--pv-neutral) 22%, var(--pv-n-ink));
|
||||
--pvs-ground: var(--pv-neutral);
|
||||
--pvs-surface: var(--pv-surface-2);
|
||||
--pvs-headline: color-mix(in oklab, var(--pv-primary) 80%, var(--pv-strong));
|
||||
--pvs-eyebrow: var(--pv-tertiary);
|
||||
--pvs-copy: var(--pv-bone);
|
||||
--pvs-headline: var(--pv-strong);
|
||||
--pvs-eyebrow: var(--pv-bone);
|
||||
--pvs-copy: var(--pv-muted);
|
||||
--pvs-title: var(--pv-strong);
|
||||
--pvs-cta: var(--pv-primary);
|
||||
--pvs-signal: var(--pv-primary);
|
||||
@@ -1190,12 +1230,12 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
--pvs-image-a: color-mix(in oklab, var(--pv-secondary) 26%, var(--pv-neutral));
|
||||
--pvs-image-b: color-mix(in oklab, var(--pv-secondary) 50%, var(--pv-neutral));
|
||||
--pvs-image-edge: color-mix(in oklab, var(--pv-secondary) 58%, var(--pv-neutral));
|
||||
--pvs-bars: var(--pv-bone);
|
||||
--pvs-bars: var(--pv-muted);
|
||||
--pvs-rule: var(--ks-rule);
|
||||
--pvs-accent-a: var(--pv-secondary);
|
||||
--pvs-accent-b: color-mix(in oklab, var(--pv-secondary) 38%, var(--pv-neutral));
|
||||
--pvs-accent-a: var(--pv-bone);
|
||||
--pvs-accent-b: var(--pv-bone);
|
||||
--pvs-accent-c: var(--pv-primary);
|
||||
--pvs-accent-d: var(--pv-tertiary);
|
||||
--pvs-accent-d: var(--pv-bone);
|
||||
width: 100%;
|
||||
aspect-ratio: 1.865 / 1;
|
||||
display: grid;
|
||||
@@ -1207,8 +1247,8 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
While any strategy option is hovered, the hovered branch drives the
|
||||
preview and the checked branch stands down; hovering Restrained needs
|
||||
no block of its own because standing down lands on the defaults. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="color-strategy"][value="committed"]) .picker-strategy-preview,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="committed"]:checked) .picker-strategy-preview {
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="color-strategy"][value="committed"]) .picker-strategy-preview:not(.picker-preview-type),
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="committed"]:checked) .picker-strategy-preview:not(.picker-preview-type) {
|
||||
--pvs-headline: color-mix(in oklab, var(--pv-primary) 80%, var(--pv-strong));
|
||||
--pvs-eyebrow: var(--pv-primary);
|
||||
--pvs-ghost: color-mix(in oklab, var(--pv-primary) 14%, var(--pv-neutral));
|
||||
@@ -1226,8 +1266,8 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
/* Full palette: every role on duty. Primary carries the headline, image,
|
||||
and CTA; secondary the ghost action and gallery; tertiary the eyebrow,
|
||||
signal, and accents; neutral stays the ground. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="color-strategy"][value="full-palette"]) .picker-strategy-preview,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="full-palette"]:checked) .picker-strategy-preview {
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="color-strategy"][value="full-palette"]) .picker-strategy-preview:not(.picker-preview-type),
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="full-palette"]:checked) .picker-strategy-preview:not(.picker-preview-type) {
|
||||
--pvs-headline: color-mix(in oklab, var(--pv-primary) 80%, var(--pv-strong));
|
||||
--pvs-eyebrow: var(--pv-tertiary);
|
||||
--pvs-ghost: color-mix(in oklab, var(--pv-secondary) 18%, var(--pv-neutral));
|
||||
@@ -1246,8 +1286,8 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
/* Drenched: the primary is the page. Everything on top is either ink,
|
||||
an ink wash of the primary, or the secondary and tertiary doing accent
|
||||
work; nothing is left bare white and the hairlines tint with the ground. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="color-strategy"][value="drenched"]) .picker-strategy-preview,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="drenched"]:checked) .picker-strategy-preview {
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="color-strategy"][value="drenched"]) .picker-strategy-preview:not(.picker-preview-type),
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="drenched"]:checked) .picker-strategy-preview:not(.picker-preview-type) {
|
||||
--pvs-ground: var(--pv-primary);
|
||||
--pvs-surface: color-mix(in oklab, var(--pv-p-ink) 10%, var(--pv-primary));
|
||||
--pvs-headline: var(--pv-p-ink);
|
||||
@@ -1276,8 +1316,7 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background-color: var(--pvs-ground);
|
||||
border: 1px solid color-mix(in oklab, var(--pvs-rule) 72%, var(--ks-rule));
|
||||
box-shadow: 0 16px 32px -20px oklch(0% 0 0 / 0.28);
|
||||
border: 1px solid var(--pvs-rule);
|
||||
}
|
||||
|
||||
.ps-desktop {
|
||||
@@ -1349,22 +1388,22 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
|
||||
.ps-eyebrow {
|
||||
width: 54%;
|
||||
height: 9px;
|
||||
margin-bottom: 16px;
|
||||
height: 10px;
|
||||
margin-bottom: 14px;
|
||||
background-color: var(--pvs-eyebrow);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.ps-headline {
|
||||
display: grid;
|
||||
gap: clamp(6px, 0.8vw, 9px);
|
||||
margin: 0 0 15px;
|
||||
gap: 8px;
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
|
||||
.ps-headline i {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 14px;
|
||||
height: 15px;
|
||||
background-color: var(--pvs-headline);
|
||||
border-radius: 2px;
|
||||
}
|
||||
@@ -1375,14 +1414,14 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
|
||||
.ps-copy {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
margin-bottom: 20px;
|
||||
gap: 8px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.ps-copy i {
|
||||
display: block;
|
||||
width: 64%;
|
||||
height: 7px;
|
||||
height: 8px;
|
||||
background-color: var(--pvs-copy);
|
||||
border-radius: 2px;
|
||||
}
|
||||
@@ -1435,30 +1474,36 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1.05fr 1fr 1fr 1.15fr;
|
||||
align-items: center;
|
||||
padding-inline: 0;
|
||||
border-block: 1px solid var(--pvs-rule);
|
||||
align-items: stretch;
|
||||
padding: 2% 3.2% 2% 5.5%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ps-proof::before,
|
||||
.ps-proof::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 5.5%;
|
||||
right: 3.2%;
|
||||
height: 1px;
|
||||
background-color: var(--pvs-rule);
|
||||
}
|
||||
|
||||
.ps-proof::before {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.ps-proof::after {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.ps-proof-item {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9%;
|
||||
height: 46%;
|
||||
padding: 0 8% 0 42px;
|
||||
}
|
||||
|
||||
.ps-proof-item:nth-child(2) {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.ps-proof-item:nth-child(3) {
|
||||
padding-left: 39px;
|
||||
}
|
||||
|
||||
.ps-proof-item:nth-child(4) {
|
||||
padding-left: 37px;
|
||||
gap: 10%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ps-proof-item + .ps-proof-item {
|
||||
@@ -1473,9 +1518,21 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.picker-strategy-preview:not(.picker-preview-type) .ps-proof-item:nth-child(2) i {
|
||||
background-color: var(--pvs-accent-b);
|
||||
}
|
||||
|
||||
.picker-strategy-preview:not(.picker-preview-type) .ps-proof-item:nth-child(3) i {
|
||||
background-color: var(--pvs-accent-c);
|
||||
}
|
||||
|
||||
.picker-strategy-preview:not(.picker-preview-type) .ps-proof-item:nth-child(4) i {
|
||||
background-color: var(--pvs-accent-d);
|
||||
}
|
||||
|
||||
.ps-proof-item span {
|
||||
width: 46%;
|
||||
height: 5px;
|
||||
height: 6px;
|
||||
background-color: var(--pvs-bars);
|
||||
border-radius: 2px;
|
||||
}
|
||||
@@ -1496,20 +1553,20 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
|
||||
.ps-editorial-copy strong {
|
||||
width: 78%;
|
||||
height: 10px;
|
||||
height: 11px;
|
||||
background-color: var(--pvs-title);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.ps-editorial-copy span {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ps-editorial-copy span i {
|
||||
display: block;
|
||||
width: 90%;
|
||||
height: 5px;
|
||||
height: 6px;
|
||||
background-color: var(--pvs-bars);
|
||||
border-radius: 2px;
|
||||
}
|
||||
@@ -1542,11 +1599,11 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
}
|
||||
|
||||
.ps-desktop .ps-editorial-copy {
|
||||
gap: 8px;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.ps-desktop .ps-editorial-copy strong {
|
||||
height: 9px;
|
||||
height: 11px;
|
||||
}
|
||||
|
||||
.ps-desktop .ps-editorial-copy > em {
|
||||
@@ -1575,13 +1632,13 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
|
||||
.ps-gallery-item span {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ps-gallery-item b {
|
||||
display: block;
|
||||
width: 76%;
|
||||
height: 5px;
|
||||
height: 6px;
|
||||
background-color: var(--pvs-bars);
|
||||
border-radius: 2px;
|
||||
}
|
||||
@@ -1656,26 +1713,28 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
}
|
||||
|
||||
.ps-phone-body .ps-headline {
|
||||
margin: 6px 0 0;
|
||||
margin: 8px 0 0;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.ps-phone-body .ps-headline i:first-child {
|
||||
width: 54%;
|
||||
height: 8px;
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
.ps-phone-body .ps-headline i:last-child {
|
||||
width: 82%;
|
||||
height: 7px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.ps-phone-body .ps-copy {
|
||||
margin: 2px 0 0;
|
||||
margin: 4px 0 0;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.ps-phone-body .ps-copy i {
|
||||
width: 68%;
|
||||
height: 5px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.ps-phone-body .ps-copy i:last-child {
|
||||
@@ -1695,14 +1754,15 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
|
||||
.ps-phone-body .ps-proof {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin-inline: -6.5%;
|
||||
align-items: stretch;
|
||||
margin-inline: 0;
|
||||
margin-top: 5px;
|
||||
border-block-color: var(--pvs-rule);
|
||||
padding: 2% 3.2% 2% 5.5%;
|
||||
}
|
||||
|
||||
.ps-phone-body .ps-proof-item {
|
||||
height: 100%;
|
||||
padding: 0 6% 0 14%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ps-phone-body .ps-proof-item i {
|
||||
@@ -1711,20 +1771,26 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
|
||||
.ps-phone-body .ps-proof-item span {
|
||||
width: 56%;
|
||||
height: 5px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.ps-phone-body .ps-editorial-copy strong {
|
||||
width: 38%;
|
||||
height: 6px;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
.ps-phone-body .ps-editorial-copy {
|
||||
margin-top: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ps-phone-body .ps-editorial-copy span i {
|
||||
height: 5px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.ps-phone-body .ps-proof::before,
|
||||
.ps-phone-body .ps-proof::after {
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.ps-phone-body .ps-gallery {
|
||||
@@ -1740,12 +1806,12 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
}
|
||||
|
||||
.ps-phone-body .ps-gallery-item b {
|
||||
height: 4px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
.ps-phone-body .ps-gallery-item span {
|
||||
align-content: start;
|
||||
gap: 5px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ps-phone-footer {
|
||||
@@ -1758,11 +1824,50 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
border-top: 1px solid var(--pvs-rule);
|
||||
}
|
||||
|
||||
.picker-strategy-preview :is(
|
||||
.picker-strategy-preview:not(.picker-preview-type) :is(
|
||||
.ps-desktop,
|
||||
.ps-phone,
|
||||
.ps-nav,
|
||||
.ps-proof,
|
||||
.ps-proof::before,
|
||||
.ps-proof::after,
|
||||
.ps-proof-item,
|
||||
.ps-footer,
|
||||
.ps-phone-top,
|
||||
.ps-phone-footer,
|
||||
.ps-brand-block,
|
||||
.ps-nav-bars i,
|
||||
.ps-nav-action,
|
||||
.ps-eyebrow,
|
||||
.ps-headline i,
|
||||
.ps-copy i,
|
||||
.ps-actions i,
|
||||
.ps-image,
|
||||
.ps-image::after,
|
||||
.ps-proof-item i,
|
||||
.ps-proof-item span,
|
||||
.ps-editorial-copy strong,
|
||||
.ps-editorial-copy span i,
|
||||
.ps-editorial-copy > em,
|
||||
.ps-gallery-item > i,
|
||||
.ps-gallery-item b,
|
||||
.ps-footer-links i,
|
||||
.ps-footer-mark,
|
||||
.ps-phone-footer i
|
||||
) {
|
||||
transition:
|
||||
background-color 280ms var(--ks-ease),
|
||||
background-image 280ms var(--ks-ease),
|
||||
border-color 280ms var(--ks-ease);
|
||||
}
|
||||
|
||||
.picker-preview-type :is(
|
||||
.ps-desktop,
|
||||
.ps-phone,
|
||||
.ps-nav,
|
||||
.ps-proof,
|
||||
.ps-proof::before,
|
||||
.ps-proof::after,
|
||||
.ps-proof-item,
|
||||
.ps-footer,
|
||||
.ps-phone-top,
|
||||
@@ -1793,7 +1898,6 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
background-color 280ms var(--ks-ease),
|
||||
background-image 280ms var(--ks-ease),
|
||||
border-color 280ms var(--ks-ease),
|
||||
box-shadow 280ms var(--ks-ease),
|
||||
color 280ms var(--ks-ease);
|
||||
}
|
||||
|
||||
@@ -1836,32 +1940,51 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
scrollbar-color: color-mix(in oklab, var(--ks-patina-deep) 35%, transparent) transparent;
|
||||
}
|
||||
|
||||
.picker-type-option {
|
||||
/* Screen 04 mirrors screen 03's options panel: copy-only rows, no spine. */
|
||||
.picker-type-options .picker-strategy-option {
|
||||
gap: 0;
|
||||
padding: 22px 24px 22px 28px;
|
||||
padding: 22px 26px 22px 28px;
|
||||
}
|
||||
|
||||
.picker-type-options .picker-strategy-option::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.picker-type-options .picker-strategy-option:hover {
|
||||
background: color-mix(in oklab, var(--ks-patina-deep) 5%, var(--ks-lacquer-raised));
|
||||
}
|
||||
|
||||
.picker-type-options .picker-strategy-option:has(input:checked) {
|
||||
background: color-mix(in oklab, var(--ks-patina-deep) 9%, var(--ks-lacquer-raised));
|
||||
}
|
||||
|
||||
.picker-type-options .picker-strategy-option:has(input:checked) .picker-type-sample-heading {
|
||||
color: var(--ks-link-on-paper);
|
||||
}
|
||||
|
||||
.picker-type-copy {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* Fonttrio-style specimen: the heading family name set big in its own
|
||||
face, then one sentence in the body face that names its partner. The
|
||||
sentence demonstrates the body font, so no Heading/Body labels. */
|
||||
/* The heading renders in its pair's display face; the description renders
|
||||
in the body face so both are judged as type, not as labels. */
|
||||
.picker-type-sample-heading {
|
||||
color: var(--ks-champagne);
|
||||
font-family: var(--pair-heading, var(--ks-font-display));
|
||||
font-size: 1.65rem;
|
||||
font-weight: var(--pair-heading-weight, 600);
|
||||
line-height: 1.15;
|
||||
font-size: 24px;
|
||||
font-weight: var(--pair-heading-weight);
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.picker-type-desc {
|
||||
margin-top: 10px;
|
||||
color: var(--ks-text);
|
||||
margin-top: 0;
|
||||
max-width: 34ch;
|
||||
color: var(--ks-text-muted);
|
||||
font-family: var(--pair-body, var(--ks-font));
|
||||
font-size: 0.92rem;
|
||||
font-size: 16px;
|
||||
font-weight: var(--pair-body-weight, 400);
|
||||
line-height: 1.55;
|
||||
}
|
||||
@@ -2080,7 +2203,35 @@ html.light .ks-button.ks-button-ghost:hover {
|
||||
.picker-tip::after,
|
||||
.picker-palette-hint,
|
||||
.picker-strategy-option,
|
||||
.picker-strategy-preview :is(
|
||||
.picker-strategy-preview:not(.picker-preview-type) :is(
|
||||
.ps-desktop,
|
||||
.ps-phone,
|
||||
.ps-nav,
|
||||
.ps-proof,
|
||||
.ps-proof-item,
|
||||
.ps-footer,
|
||||
.ps-phone-top,
|
||||
.ps-phone-footer,
|
||||
.ps-brand-block,
|
||||
.ps-nav-bars i,
|
||||
.ps-nav-action,
|
||||
.ps-eyebrow,
|
||||
.ps-headline i,
|
||||
.ps-copy i,
|
||||
.ps-actions i,
|
||||
.ps-image,
|
||||
.ps-proof-item i,
|
||||
.ps-proof-item span,
|
||||
.ps-editorial-copy strong,
|
||||
.ps-editorial-copy span i,
|
||||
.ps-editorial-copy > em,
|
||||
.ps-gallery-item > i,
|
||||
.ps-gallery-item b,
|
||||
.ps-footer-links i,
|
||||
.ps-footer-mark,
|
||||
.ps-phone-footer i
|
||||
),
|
||||
.picker-preview-type :is(
|
||||
.ps-desktop,
|
||||
.ps-phone,
|
||||
.ps-nav,
|
||||
|
||||
@@ -381,7 +381,10 @@ Compose six distinct territories, then resolve each into one heading and body pa
|
||||
- Write one sentence in `why` that names the Q4 reference, Users fact, positioning line, commitment, or letterform observation the pair serves. Replace a sentence that could describe any brand.
|
||||
- Order the pairs best-first. `pairs[0]` is the recommendation and reaches the picker pre-selected.
|
||||
|
||||
Choose two specimen strings from the product's own world: a headline of at most six words and one honest body sentence. Do not invent a claim or use placeholder prose.
|
||||
Choose specimen strings and wireframe copy from the product's own world. Do not invent claims or use placeholder prose that could describe any brand.
|
||||
|
||||
- **Hero**: a headline of at most six words and one honest body sentence (`specimen.headline`, `specimen.body`).
|
||||
- **Wireframe**: every other label the type-preview artboard shows (`preview`): a short brand mark, four nav labels, nav and menu actions, two CTA labels, four proof chips, a section title, two section body lines, one section link, four gallery cards (`title` + `meta`), four footer links, and a footer mark. Pull each string from PRODUCT.md, the interview, or supplied assets. Keep labels short enough to fit the artboard.
|
||||
|
||||
Write `.impeccable/visual-cues/fonts.json` with this shape:
|
||||
|
||||
@@ -392,6 +395,26 @@ Write `.impeccable/visual-cues/fonts.json` with this shape:
|
||||
"headline": "Six words from the product's world",
|
||||
"body": "One honest sentence in the product's voice for the body face."
|
||||
},
|
||||
"preview": {
|
||||
"brand": "Ab",
|
||||
"nav": ["Shop", "Stories", "Visit", "About"],
|
||||
"navAction": "Order",
|
||||
"menuAction": "Menu",
|
||||
"ctaPrimary": "Primary action",
|
||||
"ctaSecondary": "Secondary action",
|
||||
"proof": ["Proof one", "Proof two", "Proof three", "Proof four"],
|
||||
"sectionTitle": "Section title",
|
||||
"sectionBody": ["First body line.", "Second body line."],
|
||||
"sectionLink": "Section link",
|
||||
"gallery": [
|
||||
{ "title": "Card one", "meta": "Detail one" },
|
||||
{ "title": "Card two", "meta": "Detail two" },
|
||||
{ "title": "Card three", "meta": "Detail three" },
|
||||
{ "title": "Card four", "meta": "Detail four" }
|
||||
],
|
||||
"footerLinks": ["Link one", "Link two", "Link three", "Link four"],
|
||||
"footerMark": "© Brand"
|
||||
},
|
||||
"pairs": [
|
||||
{
|
||||
"id": "kebab-slug",
|
||||
@@ -404,13 +427,13 @@ Write `.impeccable/visual-cues/fonts.json` with this shape:
|
||||
}
|
||||
```
|
||||
|
||||
Write exactly six pair entries. Each role carries the single weight it needs; the picker also loads weight 700 for each body family. A per-pair `specimen` override may replace the shared strings when the brand evidence warrants it.
|
||||
Write exactly six pair entries. Each role carries the single weight it needs; the picker also loads weight 700 for each body family. A per-pair `specimen` or `preview` override may replace the shared strings when the brand evidence warrants it.
|
||||
|
||||
If Q2 is missing because the interview was skipped, say in one line that the typography set is direction-neutral, then compose all six from the four allowed PRODUCT.md sections alone. Still write the file.
|
||||
|
||||
Parse the finished file as JSON and verify its version, specimen, six unique ids, six unique heading families, role names, weights, and one-sentence `why` fields before continuing.
|
||||
Parse the finished file as JSON and verify its version, specimen, preview (every field above), six unique ids, six unique heading families, role names, weights, and one-sentence `why` fields before continuing.
|
||||
|
||||
Done when: `fonts.json` is parseable, contains exactly six ranked pairs, and every family name has been checked against Google Fonts.
|
||||
Done when: `fonts.json` is parseable, contains exactly six ranked pairs, every family name has been checked against Google Fonts, and the preview copy reads as this product, not generic SaaS filler.
|
||||
|
||||
## Step 7: Launch the picker
|
||||
|
||||
|
||||
@@ -37,6 +37,29 @@ const fontManifestFixture = {
|
||||
headline: 'Flowers shaped by hand',
|
||||
body: 'Seasonal stems become arrangements made for one room and one moment.',
|
||||
},
|
||||
preview: {
|
||||
brand: 'Ha',
|
||||
nav: ['Bouquets', 'Workshops', 'Seasonal', 'About'],
|
||||
navAction: 'Order',
|
||||
menuAction: 'Menu',
|
||||
ctaPrimary: 'Shop stems',
|
||||
ctaSecondary: 'See the studio',
|
||||
proof: ['Same-day pickup', 'Local growers', 'Hand tied', 'Studio open'],
|
||||
sectionTitle: 'Stems in season',
|
||||
sectionBody: [
|
||||
'Each arrangement starts with stems',
|
||||
'chosen the morning it ships.',
|
||||
],
|
||||
sectionLink: 'Our growers',
|
||||
gallery: [
|
||||
{ title: 'Market bunch', meta: 'From $38' },
|
||||
{ title: 'Table vase', meta: 'From $52' },
|
||||
{ title: 'Ceremony', meta: 'From $120' },
|
||||
{ title: 'Workshop', meta: 'Next Sat' },
|
||||
],
|
||||
footerLinks: ['Care guide', 'Delivery', 'Contact', 'Instagram'],
|
||||
footerMark: '© Hanazono',
|
||||
},
|
||||
pairs: [
|
||||
{
|
||||
id: 'marcellus-karla',
|
||||
@@ -173,6 +196,8 @@ test('serves picker and cues, writes submission, prints answers, and exits 0', a
|
||||
assert.match(pageHtml, /aria-label="Copy link"/);
|
||||
assert.match(pageHtml, />Start<span class="ks-button-arrow"/);
|
||||
assert.match(pageHtml, /rel="icon" type="image\/svg\+xml" href="\.\/favicon\.svg"/);
|
||||
assert.match(pageHtml, /data-type-headline/);
|
||||
assert.doesNotMatch(pageHtml, />Made to last</);
|
||||
assert.match(pageHtml, /assets\/hero-light\.jpg/);
|
||||
const stylesheet = pageHtml.match(/href="(\.\/assets\/[^"]+\.css)"/)?.[1];
|
||||
assert.ok(stylesheet);
|
||||
|
||||
Reference in New Issue
Block a user