This commit is contained in:
Abdul Wahab
2026-09-01 10:02:39 +05:00
parent 83b4fbcb5d
commit 7af6e831fb
8 changed files with 51 additions and 33 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

+1 -1
View File
@@ -13,7 +13,7 @@ const {
---
<!doctype html>
<html lang="en" class="light">
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
+1 -1
View File
@@ -11,7 +11,7 @@ const roles = [
<Picker>
<main class="picker-shell">
<div class="picker-hero-art" style="background-image: url('assets/hero-light.jpg')" aria-hidden="true"></div>
<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">
+18 -3
View File
@@ -56,9 +56,24 @@ export function seedToRoles(seed) {
};
}
// Relative luminance of the colors --pk-ink-dark and --pk-ink-light resolve
// to in styles/picker.css: oklch(14% 0.018 95) and oklch(99% 0.008 95).
const INK_DARK_LUMINANCE = 0.0027;
const INK_LIGHT_LUMINANCE = 0.9716;
// The ink sits on a color the user picked, so it must not follow the picker's
// own theme: --ks-champagne and friends invert between light and dark and
// would blank the label on exactly the swatches that need it most. Comparing
// both ratios beats a fixed lightness threshold, which picks the losing ink
// for mid-tones sitting near the cutoff.
export function contrastInk(hex) {
const [red, green, blue] = parseHex(hex).map(linearize);
return 0.2126 * red + 0.7152 * green + 0.0722 * blue > 0.22
? 'var(--ks-champagne)'
: 'var(--ks-lacquer-raised)';
const swatch = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
const against = (ink) => {
const [hi, lo] = ink > swatch ? [ink, swatch] : [swatch, ink];
return (hi + 0.05) / (lo + 0.05);
};
return against(INK_DARK_LUMINANCE) >= against(INK_LIGHT_LUMINANCE)
? 'var(--pk-ink-dark)'
: 'var(--pk-ink-light)';
}
+20 -22
View File
@@ -3,6 +3,17 @@
@import "./vendor/kinpaku-tokens.css";
@import "./vendor/kinpaku-kit.css";
/* Ink for surfaces painted in a color the user chose, not in one of ours:
swatch labels and the wireframe artboards. Every --ks-* text token swaps
between the light and dark themes, which would invert these against
palettes that never changed. Values are the system's own ink and raised
paper, so neither is pure black or pure white. contrastInk() in
scripts/color.js mirrors their luminance; change both together. */
:root {
--pk-ink-dark: oklch(14% 0.018 95);
--pk-ink-light: oklch(99% 0.008 95);
}
/* Scoped subset of the reset imported by site/styles/main.css through
site/styles/tokens.css (lines 12-27). */
.picker-page,
@@ -34,19 +45,6 @@ body.picker-page {
text-rendering: optimizeLegibility;
}
/* Copied from site/styles/light-mode.css lines 189-194. Light mode remaps
--ks-lacquer-deep to paper, so the primary button needs its dark ink value. */
html.light .ks-button.ks-button-primary,
html.light .ks-button.ks-button-primary:hover,
html.light .ks-button.ks-button-primary:active {
color: oklch(14% 0.018 95);
}
/* On paper, ghost buttons lift toward deep patina, not fill gold. */
html.light .ks-button.ks-button-ghost:hover {
color: var(--ks-patina-deep);
}
.picker-shell {
position: relative;
min-height: 100vh;
@@ -513,8 +511,8 @@ html.light .ks-button.ks-button-ghost:hover {
.picker-icon-button:hover {
background: var(--ks-graphite);
border-color: var(--ks-link-on-paper);
color: var(--ks-link-on-paper);
border-color: var(--ks-patina);
color: var(--ks-patina);
}
.picker-icon-button:focus-visible,
@@ -789,7 +787,7 @@ html.light .ks-button.ks-button-ghost:hover {
.picker-preview {
--pv-primary: var(--ks-champagne);
--pv-secondary: var(--ks-patina);
--pv-tertiary: var(--ks-link-on-paper);
--pv-tertiary: var(--ks-kinpaku);
--pv-neutral: var(--ks-lacquer-raised);
--pv-n-ink: var(--ks-champagne);
--pv-p-ink: var(--ks-champagne);
@@ -1156,7 +1154,7 @@ html.light .ks-button.ks-button-ghost:hover {
}
.picker-strategy-option:has(input:checked) .picker-strategy-title {
color: var(--ks-link-on-paper);
color: var(--ks-patina);
}
.picker-strategy-option:has(input:focus-visible) {
@@ -1183,7 +1181,7 @@ html.light .ks-button.ks-button-ghost:hover {
}
.picker-strategy-choices .picker-strategy-option:has(input:checked) .picker-strategy-title {
color: var(--ks-link-on-paper);
color: var(--ks-patina);
}
.picker-strategy-choices .picker-strategy-copy {
@@ -1209,7 +1207,7 @@ html.light .ks-button.ks-button-ghost:hover {
.picker-strategy-preview {
--pv-primary: var(--ks-champagne);
--pv-secondary: var(--ks-patina);
--pv-tertiary: var(--ks-link-on-paper);
--pv-tertiary: var(--ks-kinpaku);
--pv-neutral: var(--ks-lacquer-raised);
--pv-n-ink: var(--ks-champagne);
--pv-p-ink: var(--ks-champagne);
@@ -1960,7 +1958,7 @@ html.light .ks-button.ks-button-ghost:hover {
}
.picker-type-options .picker-strategy-option:has(input:checked) .picker-type-sample-heading {
color: var(--ks-link-on-paper);
color: var(--ks-patina);
}
.picker-type-copy {
@@ -2324,7 +2322,7 @@ html.light .ks-button.ks-button-ghost:hover {
flex: 1 1 auto;
min-width: 18ch;
overflow-x: auto;
color: var(--ks-link-on-paper, var(--ks-champagne));
color: var(--ks-patina);
font-family: var(--ks-mono);
font-size: var(--ks-type-body-size);
letter-spacing: 0;
@@ -2346,7 +2344,7 @@ html.light .ks-button.ks-button-ghost:hover {
border: 1px solid var(--ks-rule);
border-radius: 2px;
background: transparent;
color: var(--ks-link-on-paper, var(--ks-champagne));
color: var(--ks-patina);
cursor: pointer;
transition:
background 180ms var(--ks-ease),
+2 -2
View File
@@ -12,8 +12,8 @@ const outputDir = path.join(root, 'skill/scripts/picker');
// private impeccable-site repo, so the picker carries its own copies).
const faviconSource = path.join(root, 'picker/assets/favicon.svg');
const faviconOutput = path.join(outputDir, 'favicon.svg');
const heroSource = path.join(root, 'picker/assets/hero-light.jpg');
const heroOutput = path.join(outputDir, 'assets/hero-light.jpg');
const heroSource = path.join(root, 'picker/assets/hero-dark.jpg');
const heroOutput = path.join(outputDir, 'assets/hero-dark.jpg');
await rm(buildDir, { recursive: true, force: true });
execFileSync(
+9 -4
View File
@@ -198,7 +198,7 @@ test('serves picker and cues, writes submission, prints answers, and exits 0', a
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/);
assert.match(pageHtml, /assets\/hero-dark\.jpg/);
const stylesheet = pageHtml.match(/href="(\.\/assets\/[^"]+\.css)"/)?.[1];
assert.ok(stylesheet);
assert.equal((await fetch(new URL(stylesheet, `${server.url}/`))).status, 200);
@@ -208,7 +208,7 @@ test('serves picker and cues, writes submission, prints answers, and exits 0', a
assert.match(faviconResponse.headers.get('content-type'), /^image\/svg\+xml/);
assert.match(await faviconResponse.text(), /<svg/);
const heroResponse = await fetch(`${server.url}/assets/hero-light.jpg`);
const heroResponse = await fetch(`${server.url}/assets/hero-dark.jpg`);
assert.equal(heroResponse.status, 200);
assert.equal(heroResponse.headers.get('content-type'), 'image/jpeg');
assert.ok((await heroResponse.arrayBuffer()).byteLength > 0);
@@ -299,8 +299,13 @@ test('picker color math round-trips sRGB and clips out-of-gamut OKLCH', async ()
assert.deepEqual(Object.keys(seedToRoles({ oklch: [0.62, 0.15, 210] })), [
'primary', 'secondary', 'tertiary', 'neutral',
]);
assert.equal(contrastInk('#FFFFFF'), 'var(--ks-champagne)');
assert.equal(contrastInk('#000000'), 'var(--ks-lacquer-raised)');
// Swatch ink is theme-independent: it sits on a color the user picked, so
// it must not follow the picker's own light/dark tokens.
assert.equal(contrastInk('#FFFFFF'), 'var(--pk-ink-dark)');
assert.equal(contrastInk('#000000'), 'var(--pk-ink-light)');
// A mid-tone reads better against the dark ink, which a fixed lightness
// threshold gets backwards.
assert.equal(contrastInk('#8D7352'), 'var(--pk-ink-dark)');
});
test('rejects raw, encoded, and double-encoded path traversal', async (t) => {