mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 23:26:39 +03:00
Improve palette preview: dual artboard, drop register wiring.
Show landing and app wireframes side by side with palette-driven accents instead of PRODUCT.md register and /context.json, matching v4's per-surface modes. Assisted-by: Cursor AI agent. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,6 +15,14 @@
|
||||
],
|
||||
"createdAt": "2026-07-17T00:00:00.000Z",
|
||||
"reason": "Live overlay chrome is injected over arbitrary host pages and builds a self-contained UI with its own small type scale; DESIGN.md's ramp describes the impeccable website, not this widget"
|
||||
},
|
||||
{
|
||||
"rule": "broken-image",
|
||||
"value": "*",
|
||||
"files": [
|
||||
"picker/pages/index.astro"
|
||||
],
|
||||
"createdAt": "2026-07-23T14:51:18.241Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+35
-57
@@ -104,6 +104,17 @@ const roles = [
|
||||
<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}>
|
||||
@@ -133,70 +144,37 @@ const roles = [
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="picker-preview" data-register="brand" aria-hidden="true">
|
||||
<div class="pv-brand">
|
||||
<div class="pv-chrome"></div>
|
||||
<div class="pv-brand-page">
|
||||
<div class="pv-brand-nav">
|
||||
<div class="pv-logo"></div>
|
||||
<div class="pv-nav-bars"><i></i><i></i><i></i><i></i></div>
|
||||
<div class="pv-primary-pill"></div>
|
||||
</div>
|
||||
<div class="pv-brand-hero">
|
||||
<div class="pv-hero-copy">
|
||||
<div class="pv-eyebrow"></div>
|
||||
<div class="pv-headline"></div>
|
||||
<div class="pv-copy-bars"></div>
|
||||
<div class="pv-actions"><i></i><i></i></div>
|
||||
</div>
|
||||
<div class="pv-image"></div>
|
||||
</div>
|
||||
<div class="pv-features">
|
||||
<div><i></i><span></span></div>
|
||||
<div><i></i><span></span></div>
|
||||
<div><i></i><span></span></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-product">
|
||||
<div class="pv-chrome"></div>
|
||||
<div class="pv-app">
|
||||
<div class="pv-sidebar">
|
||||
<div class="pv-logo"></div>
|
||||
<div class="pv-active-nav"></div>
|
||||
<div class="pv-side-bars"><i></i><i></i><i></i><i></i></div>
|
||||
</div>
|
||||
<div class="pv-app-main">
|
||||
<div class="pv-topbar">
|
||||
<div class="pv-search"></div>
|
||||
<div class="pv-alert"></div>
|
||||
<div class="pv-avatar"></div>
|
||||
</div>
|
||||
<div class="pv-stats">
|
||||
<div><i></i><span></span></div>
|
||||
<div><i></i><span></span></div>
|
||||
<div><i></i><span></span></div>
|
||||
</div>
|
||||
<div class="pv-app-lower">
|
||||
<div class="pv-chart"><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div>
|
||||
<div class="pv-app-aside"><span></span><i></i></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>
|
||||
|
||||
<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>
|
||||
<button class="ks-button ks-button-primary picker-select" type="button" data-select-palette data-advance="next" disabled>Select this palette</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
@@ -324,9 +324,7 @@ document.addEventListener('picker:screenchange', (event) => activate(event.detai
|
||||
|
||||
try {
|
||||
const get = (url) => fetch(url).then((response) => response.ok ? response.json() : Promise.reject());
|
||||
const context = get('/context.json').catch(() => ({ register: 'brand' }));
|
||||
const [cueData, seedData, contextData] = await Promise.all([get('/cues.json'), get('/palettes.json'), context]);
|
||||
preview.dataset.register = contextData.register === 'product' ? 'product' : 'brand';
|
||||
const [cueData, seedData] = await Promise.all([get('/cues.json'), get('/palettes.json')]);
|
||||
cards = [
|
||||
...cueData.cues.map((id) => ({ id, type: 'cue', palette: cueData.palette[id] })),
|
||||
...seedData.seeds.map((seed) => ({ ...seed, type: 'seed' })),
|
||||
|
||||
+151
-217
@@ -403,6 +403,32 @@ html.light .ks-button.ks-button-primary:active {
|
||||
inset 0 0 0 1px color-mix(in srgb, var(--ks-champagne) 40%, transparent);
|
||||
}
|
||||
|
||||
.picker-ring::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 1px;
|
||||
border: 2px solid white;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: scale(0.78);
|
||||
transition:
|
||||
opacity 180ms var(--ks-ease),
|
||||
transform 180ms var(--ks-ease);
|
||||
}
|
||||
|
||||
.picker-palette:has(.picker-band[data-band="primary"]:is(:hover, :focus-within))
|
||||
.picker-card[data-pos="0"] .picker-ring[data-role="primary"]::after,
|
||||
.picker-palette:has(.picker-band[data-band="secondary"]:is(:hover, :focus-within))
|
||||
.picker-card[data-pos="0"] .picker-ring[data-role="secondary"]::after,
|
||||
.picker-palette:has(.picker-band[data-band="tertiary"]:is(:hover, :focus-within))
|
||||
.picker-card[data-pos="0"] .picker-ring[data-role="tertiary"]::after,
|
||||
.picker-palette:has(.picker-band[data-band="neutral"]:is(:hover, :focus-within))
|
||||
.picker-card[data-pos="0"] .picker-ring[data-role="neutral"]::after {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.picker-card[data-pos="0"] .picker-ring {
|
||||
pointer-events: auto;
|
||||
}
|
||||
@@ -756,60 +782,49 @@ html.light .ks-button.ks-button-primary:active {
|
||||
--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-strong: color-mix(in oklab, var(--pv-neutral) 22%, var(--pv-n-ink));
|
||||
--pv-chrome: color-mix(in oklab, var(--pv-neutral) 35%, var(--pv-n-ink));
|
||||
--pv-secondary-wash: color-mix(in oklab, var(--pv-secondary) 28%, var(--pv-neutral));
|
||||
--pv-tertiary-wash: color-mix(in oklab, var(--pv-tertiary) 22%, var(--pv-neutral));
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 7;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Two artboards floating on the page itself: a landing page on the desktop
|
||||
card (the palette persuading) and an app screen on the phone card (the
|
||||
palette operating). Every palette is judged doing both jobs, so no
|
||||
per-project switch is needed. The phone's fixed portrait ratio keeps the
|
||||
wide frame from stretching a single page mock. */
|
||||
.pv-desktop,
|
||||
.pv-phone {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--pv-neutral);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.picker-preview[data-register="brand"] .pv-product,
|
||||
.picker-preview[data-register="product"] .pv-brand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pv-brand,
|
||||
.pv-product {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--pv-neutral);
|
||||
}
|
||||
|
||||
.pv-chrome {
|
||||
position: relative;
|
||||
height: 13%;
|
||||
background: var(--pv-chrome);
|
||||
}
|
||||
|
||||
.pv-chrome::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 3.2%;
|
||||
width: 5px;
|
||||
aspect-ratio: 1;
|
||||
background: var(--pv-bone);
|
||||
border-radius: 50%;
|
||||
box-shadow: 10px 0 var(--pv-bone), 20px 0 var(--pv-bone);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.pv-brand-page {
|
||||
height: 87%;
|
||||
.pv-desktop {
|
||||
display: grid;
|
||||
grid-template-rows: 20% 56% 24%;
|
||||
grid-template-rows: auto 1fr;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.pv-brand-nav {
|
||||
.pv-phone {
|
||||
height: 100%;
|
||||
aspect-ratio: 9 / 19;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.pv-nav {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: 5%;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding-inline: 5%;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.pv-logo {
|
||||
@@ -822,74 +837,76 @@ html.light .ks-button.ks-button-primary:active {
|
||||
.pv-nav-bars {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 5%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pv-nav-bars i,
|
||||
.pv-side-bars i {
|
||||
.pv-nav-bars i {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 3px;
|
||||
background: var(--pv-bone);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pv-nav-bars i {
|
||||
width: 9%;
|
||||
}
|
||||
|
||||
.pv-primary-pill {
|
||||
.pv-pill {
|
||||
width: 32px;
|
||||
height: 12px;
|
||||
background: var(--pv-primary);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pv-brand-hero {
|
||||
.pv-hero {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
|
||||
grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
|
||||
gap: 8%;
|
||||
align-items: center;
|
||||
padding: 3% 9%;
|
||||
padding: 4px 8% 14px;
|
||||
}
|
||||
|
||||
.pv-hero-copy {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pv-eyebrow {
|
||||
width: 24%;
|
||||
height: 7px;
|
||||
background: var(--pv-tertiary-wash);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pv-headline,
|
||||
.pv-copy-bars {
|
||||
position: relative;
|
||||
width: 22%;
|
||||
height: 4px;
|
||||
background: var(--pv-strong);
|
||||
background: var(--pv-tertiary);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* The headline is display type set in the brand color, not permanent black
|
||||
ink: it must visibly react when the primary changes. */
|
||||
.pv-headline {
|
||||
width: 86%;
|
||||
height: 7px;
|
||||
margin-block: 2px 7px;
|
||||
box-shadow: 0 11px var(--pv-strong);
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
margin-block: 2px 3px;
|
||||
}
|
||||
|
||||
.pv-copy-bars {
|
||||
width: 66%;
|
||||
margin-top: 3px;
|
||||
.pv-headline i {
|
||||
display: block;
|
||||
width: 88%;
|
||||
height: 8px;
|
||||
background: color-mix(in oklab, var(--pv-primary) 80%, var(--pv-strong));
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pv-headline i:last-child {
|
||||
width: 58%;
|
||||
}
|
||||
|
||||
.pv-copy-bar {
|
||||
width: 64%;
|
||||
height: 5px;
|
||||
background: var(--pv-bone);
|
||||
box-shadow: 0 8px var(--pv-bone);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pv-actions {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
margin-top: 9px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.pv-actions i {
|
||||
@@ -905,104 +922,20 @@ html.light .ks-button.ks-button-primary:active {
|
||||
}
|
||||
|
||||
.pv-image {
|
||||
height: 82%;
|
||||
background: var(--pv-secondary-wash);
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 4 / 3;
|
||||
background: linear-gradient(135deg, var(--pv-secondary-wash), color-mix(in oklab, var(--pv-secondary) 52%, var(--pv-neutral)));
|
||||
border: 1px solid var(--pv-secondary);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pv-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10%;
|
||||
padding: 4% 11%;
|
||||
border-top: 1px solid var(--pv-bone);
|
||||
}
|
||||
|
||||
.pv-features > div {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.pv-features i {
|
||||
width: 12px;
|
||||
aspect-ratio: 1;
|
||||
background: var(--pv-secondary-wash);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.pv-features > div:last-child i {
|
||||
background: var(--pv-tertiary-wash);
|
||||
}
|
||||
|
||||
.pv-features span,
|
||||
.pv-app-aside span {
|
||||
width: 70%;
|
||||
height: 3px;
|
||||
background: var(--pv-bone);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 6px var(--pv-bone);
|
||||
}
|
||||
|
||||
.pv-app {
|
||||
height: 87%;
|
||||
display: grid;
|
||||
grid-template-columns: 18% 82%;
|
||||
}
|
||||
|
||||
.pv-sidebar {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
gap: 10%;
|
||||
padding: 16% 14%;
|
||||
background: var(--pv-surface-2);
|
||||
border-right: 1px solid var(--pv-bone);
|
||||
}
|
||||
|
||||
.pv-active-nav {
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
background: var(--pv-primary);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pv-side-bars {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.pv-side-bars i:nth-child(even) {
|
||||
width: 72%;
|
||||
}
|
||||
|
||||
.pv-app-main {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-rows: 20% 35% 45%;
|
||||
background: var(--pv-neutral);
|
||||
}
|
||||
|
||||
.pv-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4%;
|
||||
padding-inline: 5%;
|
||||
background: var(--pv-surface-2);
|
||||
border-bottom: 1px solid var(--pv-bone);
|
||||
}
|
||||
|
||||
.pv-search {
|
||||
width: 42%;
|
||||
height: 9px;
|
||||
margin-right: auto;
|
||||
background: var(--pv-neutral);
|
||||
border: 1px solid var(--pv-bone);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pv-alert {
|
||||
width: 6px;
|
||||
.pv-image::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
right: 9px;
|
||||
width: 7px;
|
||||
aspect-ratio: 1;
|
||||
background: var(--pv-tertiary);
|
||||
border-radius: 50%;
|
||||
@@ -1015,99 +948,99 @@ html.light .ks-button.ks-button-primary:active {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.pv-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 3%;
|
||||
padding: 4%;
|
||||
/* Phone artboard: an app screen. Each mini card highlights a different
|
||||
palette role so the secondary and tertiary are judged as UI accents,
|
||||
not just as washes. */
|
||||
.pv-phone-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 9px 10px;
|
||||
background: var(--pv-surface-2);
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.pv-stats > div {
|
||||
position: relative;
|
||||
.pv-phone-body {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
align-content: start;
|
||||
gap: 7px;
|
||||
padding: 9%;
|
||||
padding: 9px 10px 10px;
|
||||
}
|
||||
|
||||
.pv-mini-card {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
padding: 8px 9px;
|
||||
background: var(--pv-surface-2);
|
||||
border: 1px solid var(--pv-bone);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pv-stats i,
|
||||
.pv-stats span {
|
||||
.pv-mini-card i,
|
||||
.pv-mini-card span {
|
||||
display: block;
|
||||
height: 3px;
|
||||
background: var(--pv-bone);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pv-stats span {
|
||||
width: 68%;
|
||||
.pv-mini-card span {
|
||||
width: 62%;
|
||||
height: 6px;
|
||||
background: var(--pv-strong);
|
||||
background: var(--pv-secondary);
|
||||
}
|
||||
|
||||
.pv-stats > div:last-child::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 15%;
|
||||
right: 10%;
|
||||
width: 5px;
|
||||
aspect-ratio: 1;
|
||||
.pv-mini-card:nth-child(2) span {
|
||||
background: var(--pv-tertiary);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.pv-app-lower {
|
||||
.pv-mini-chart {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(0, 0.8fr);
|
||||
gap: 3%;
|
||||
padding: 0 4% 4%;
|
||||
}
|
||||
|
||||
.pv-chart,
|
||||
.pv-app-aside {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 4%;
|
||||
padding: 8%;
|
||||
gap: 4px;
|
||||
padding: 8px 9px;
|
||||
background: var(--pv-surface-2);
|
||||
border: 1px solid var(--pv-bone);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pv-chart i {
|
||||
.pv-mini-chart i {
|
||||
flex: 1;
|
||||
height: 28%;
|
||||
height: 34%;
|
||||
background: var(--pv-secondary);
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
|
||||
.pv-chart i:nth-child(2) { height: 50%; }
|
||||
.pv-chart i:nth-child(3) { height: 72%; }
|
||||
.pv-chart i:nth-child(4) { height: 42%; }
|
||||
.pv-chart i:nth-child(5) { height: 86%; background: var(--pv-primary); }
|
||||
.pv-chart i:nth-child(6) { height: 62%; }
|
||||
.pv-chart i:nth-child(7) { height: 38%; }
|
||||
.pv-mini-chart i:nth-child(2) { height: 58%; }
|
||||
.pv-mini-chart i:nth-child(3) { height: 44%; }
|
||||
.pv-mini-chart i:nth-child(4) { height: 82%; background: var(--pv-primary); }
|
||||
.pv-mini-chart i:nth-child(5) { height: 66%; }
|
||||
|
||||
.pv-app-aside {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
.pv-tabbar {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-top: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.pv-app-aside i {
|
||||
width: 32px;
|
||||
height: 11px;
|
||||
margin-top: 9px;
|
||||
.pv-tabbar i {
|
||||
width: 5px;
|
||||
aspect-ratio: 1;
|
||||
background: var(--pv-bone);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.pv-tabbar i:first-child {
|
||||
background: var(--pv-primary);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.picker-palette-hint {
|
||||
min-height: 3.2em;
|
||||
padding-inline-end: 48px;
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 0.84rem;
|
||||
line-height: 1.6;
|
||||
@@ -1136,6 +1069,7 @@ html.light .ks-button.ks-button-primary:active {
|
||||
.picker-progress span,
|
||||
.picker-card,
|
||||
.picker-ring,
|
||||
.picker-ring::after,
|
||||
.picker-ring-guide,
|
||||
.picker-icon-button,
|
||||
.picker-band-tools,
|
||||
|
||||
@@ -10,7 +10,6 @@ import { readFile, mkdir, stat, writeFile } from 'node:fs/promises';
|
||||
import net from 'node:net';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { extractSectionValue } from './context.mjs';
|
||||
import { SEEDS } from './palette.mjs';
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const pickerDir = path.join(scriptDir, 'picker');
|
||||
@@ -170,15 +169,6 @@ if (options.help) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let register = 'brand';
|
||||
try {
|
||||
const product = await readFile(path.resolve(process.cwd(), 'PRODUCT.md'), 'utf8');
|
||||
const value = (extractSectionValue(product, 'Register') || '').toLowerCase();
|
||||
if (value === 'brand' || value === 'product') register = value;
|
||||
} catch {
|
||||
// A missing or unreadable PRODUCT.md keeps the brand preview default.
|
||||
}
|
||||
|
||||
const port = await findOpenPort(options.port);
|
||||
let completed = false;
|
||||
let timeout;
|
||||
@@ -229,10 +219,6 @@ async function handleRequest(request, response) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (requestPath === '/context.json') {
|
||||
sendJson(response, 200, { register });
|
||||
return;
|
||||
}
|
||||
if (requestPath.startsWith('/cues/')) {
|
||||
const cueName = requestPath.slice('/cues/'.length);
|
||||
if (!cueName || cueName.includes('/')) {
|
||||
|
||||
@@ -40,7 +40,7 @@ before(() => {
|
||||
});
|
||||
});
|
||||
|
||||
async function createFixture(register = null) {
|
||||
async function createFixture() {
|
||||
const cwd = await realpath(await mkdtemp(path.join(tmpdir(), 'impeccable-picker-')));
|
||||
const cuesDir = path.join(cwd, '.impeccable/visual-cues');
|
||||
await mkdir(cuesDir, { recursive: true });
|
||||
@@ -49,9 +49,6 @@ async function createFixture(register = null) {
|
||||
path.join(cuesDir, 'cues.json'),
|
||||
`${JSON.stringify(cueManifestFixture)}\n`,
|
||||
);
|
||||
if (register) {
|
||||
await writeFile(path.join(cwd, 'PRODUCT.md'), `# Product\n\n## Register\n\n${register}\n`);
|
||||
}
|
||||
return { cwd, cuesDir };
|
||||
}
|
||||
|
||||
@@ -182,7 +179,7 @@ test('serves picker and cues, writes submission, prints answers, and exits 0', a
|
||||
}
|
||||
|
||||
const exitPromise = waitForExit(server.processHandle);
|
||||
const answers = { register: 'brand', direction: 'kinpaku' };
|
||||
const answers = { cue: 'hero-01', direction: 'kinpaku' };
|
||||
const submitResponse = await fetch(`${server.url}/submit`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -209,27 +206,6 @@ test('palette CLI still prints a seed', () => {
|
||||
assert.match(output, /Seed color \(anchor for your primary brand color\):/);
|
||||
});
|
||||
|
||||
test('serves the product register from PRODUCT.md', async (t) => {
|
||||
const fixture = await createFixture('product');
|
||||
const server = await startPicker(fixture.cwd, ['--port', String(portBase + 10)]);
|
||||
await cleanup(t, fixture, server);
|
||||
|
||||
const response = await fetch(`${server.url}/context.json`);
|
||||
assert.equal(response.status, 200);
|
||||
assert.match(response.headers.get('content-type'), /^application\/json/);
|
||||
assert.deepEqual(await response.json(), { register: 'product' });
|
||||
});
|
||||
|
||||
test('defaults picker context to brand without PRODUCT.md', async (t) => {
|
||||
const fixture = await createFixture();
|
||||
const server = await startPicker(fixture.cwd, ['--port', String(portBase + 11)]);
|
||||
await cleanup(t, fixture, server);
|
||||
|
||||
const response = await fetch(`${server.url}/context.json`);
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(await response.json(), { register: 'brand' });
|
||||
});
|
||||
|
||||
test('picker color math round-trips sRGB and clips out-of-gamut OKLCH', async () => {
|
||||
const {
|
||||
contrastInk,
|
||||
|
||||
Reference in New Issue
Block a user