diff --git a/picker/pages/index.astro b/picker/pages/index.astro index b3cd17a31..ec1afbc96 100644 --- a/picker/pages/index.astro +++ b/picker/pages/index.astro @@ -191,7 +191,7 @@ const roles = [

How much color should this design use?

-
+
Color strategy
diff --git a/picker/scripts/palette-picker.js b/picker/scripts/palette-picker.js index 7e36cc8dc..e0b659f1a 100644 --- a/picker/scripts/palette-picker.js +++ b/picker/scripts/palette-picker.js @@ -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 { diff --git a/picker/styles/picker.css b/picker/styles/picker.css index 1ec67418c..b20973295 100644 --- a/picker/styles/picker.css +++ b/picker/styles/picker.css @@ -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, diff --git a/skill/reference/visual-cues.md b/skill/reference/visual-cues.md index d4adbe045..48a9d0fae 100644 --- a/skill/reference/visual-cues.md +++ b/skill/reference/visual-cues.md @@ -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 diff --git a/tests/picker-server.test.mjs b/tests/picker-server.test.mjs index f80ac8c2f..45f6ceed1 100644 --- a/tests/picker-server.test.mjs +++ b/tests/picker-server.test.mjs @@ -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