mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-15 23:56:29 +03:00
update
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 300 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 176 KiB |
@@ -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" />
|
||||
|
||||
@@ -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
@@ -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
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user