mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 13:46:32 +03:00
Fix: use system fonts and the light theme in the picker (#751)
Replace external fonts with system stacks and an outlined SVG logo. Adopt the light paper theme, refine Kinpaku accents, and preserve picker interactions with regression coverage. AI assistance: Codex, under maintainer direction.
This commit is contained in:
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
version: 1
|
||||||
|
slug: "crates-context-src-question-page-rs"
|
||||||
|
primary_target: "crates/context/src/question_page.rs"
|
||||||
|
related_targets: ["crates/context/src/serve_question.rs"]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Operate mode: the existing local question picker. Help builders compare the
|
||||||
|
current hand, choose, steer, or re-roll. Preserve payloads, copy, choices,
|
||||||
|
confirmation, loading, error handling, and responsive deck behavior.
|
||||||
|
|
||||||
|
## Direction contract
|
||||||
|
|
||||||
|
THESIS: Quiet picker chrome lets the offered directions carry the character.
|
||||||
|
OWN-WORLD: Inherit impeccable-site's paper-and-instruments proposal: neutral paper,
|
||||||
|
ink type, patina state text, small gold marks, tactile controls. Use system fonts
|
||||||
|
and the existing outlined SVG logo; no font files or external font requests.
|
||||||
|
STORY: Compare the options, inspect details, choose or request another hand.
|
||||||
|
FIRST VIEWPORT: Small brand at top; readable heading and build-path switch above
|
||||||
|
the existing card deck; steering and re-roll actions remain reachable below.
|
||||||
|
FORM: Existing picker structure, user-pinned site theme; no concept roll required.
|
||||||
|
FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance
|
||||||
|
|
||||||
|
## Authority
|
||||||
|
|
||||||
|
The user confirmed impeccable-site #34 (head eb5348e48fcd94d0d74895488b985988a401a2e2)
|
||||||
|
as the light-theme authority, with system fonts for the picker. This is a scoped adaptation,
|
||||||
|
not a rewrite of the repository's global DESIGN.md. No new raster assets.
|
||||||
|
|
||||||
|
## Built surface
|
||||||
|
|
||||||
|
Recorded from `crates/context/src/question_page.rs`, rendered
|
||||||
|
`.impeccable/review/after.html`, and the desktop/mobile captures alongside it.
|
||||||
|
Finish reviewer disposition: **ship**, with no requested fixes.
|
||||||
|
|
||||||
|
- **Paper and ink:** light-only chrome uses neutral paper (`oklch(97.8% 0 0)`),
|
||||||
|
raised paper (`oklch(99.5% 0 0)`), ink headings (`oklch(13% 0 0)`), and body
|
||||||
|
text (`oklch(22% 0 0)`). Dark instrument fills carry primary actions and image
|
||||||
|
controls; gold appears in the logo, lead-card border, and active switch dot.
|
||||||
|
- **Local palette adaptations:** patina is the site's text-safe deep value
|
||||||
|
(`oklch(49% 0.11 190)`); faint metadata shares muted ink (`oklch(46% 0 0)`).
|
||||||
|
Dividers and the build-path switch boundary use ink at 12% opacity; other
|
||||||
|
control boundaries use 45%. The lead-card outline (including hover) and
|
||||||
|
active switch dot use default Kinpaku (`oklch(84% 0.19 80.46)`), not deep gold.
|
||||||
|
- **Explicit font override:** headings, body, and controls use `system-ui,
|
||||||
|
-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`; metadata uses
|
||||||
|
`ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace`. Body is 15px/1.55;
|
||||||
|
headings are 2rem at weight 600, reduced to 1.5rem in portrait; card titles
|
||||||
|
are 1.125rem at weight 600. The existing wordmark is inline SVG path outlines
|
||||||
|
with a tight viewBox, displayed at 164 × 30px. No font files are required.
|
||||||
|
- **Comparison layout:** a 90rem maximum content width shares its inset with
|
||||||
|
the deck and footer. Landscape uses a horizontal snapping deck; portrait
|
||||||
|
uses a vertical deck capped at `min(68dvh, 44rem)` with Back/More controls.
|
||||||
|
The build-path switch moves below the heading in portrait. Footer controls
|
||||||
|
are sticky in landscape and remain in document flow in portrait.
|
||||||
|
- **Tactile controls:** cards and dialogs use 8px corners and layered soft
|
||||||
|
shadows. Actions and the steering field use 6px corners. The build-path
|
||||||
|
switch has a recessed gray track, a raised active cap, and a gold dot;
|
||||||
|
re-roll controls share the cap shadow and inset pressed state. Keyboard
|
||||||
|
focus uses a 2px patina outline. Image zoom, card flips, loading placeholders,
|
||||||
|
confirmation, and completion/error surfaces retain their existing roles.
|
||||||
|
|
||||||
|
These are picker-local facts, not a replacement global design system. Existing
|
||||||
|
card badges and dense uppercase metadata are preserved content treatments,
|
||||||
|
not new typography rules for other surfaces. The user-authorized system fonts
|
||||||
|
are an intentional local override. No raster assets were created or added.
|
||||||
@@ -106,6 +106,16 @@ fn detached_posts_require_key_and_loopback_host_origin() {
|
|||||||
assert!(page.contains("/heartbeat' + keyQ"));
|
assert!(page.contains("/heartbeat' + keyQ"));
|
||||||
assert!(page.contains("/build-path' + keyQ"));
|
assert!(page.contains("/build-path' + keyQ"));
|
||||||
|
|
||||||
|
// The picker is self-contained: system fonts and an outlined SVG
|
||||||
|
// wordmark, with no bundled font routes or external stylesheet.
|
||||||
|
assert!(!page.contains("@font-face"));
|
||||||
|
assert!(page.contains("--ks-font: system-ui,"));
|
||||||
|
assert!(page.contains("aria-label=\"Impeccable\""));
|
||||||
|
for path in ["/fonts/albert-sans.woff2", "/fonts/alumni-sans.ttf", "/fonts/%2e%2e/q.json"] {
|
||||||
|
let (st, _) = raw_request(port, "GET", path, &[], None);
|
||||||
|
assert_eq!(st, 404, "no font asset routes");
|
||||||
|
}
|
||||||
|
|
||||||
// The build-path flip takes the same gate as /answer.
|
// The build-path flip takes the same gate as /answer.
|
||||||
let flip = r#"{"value":"comp"}"#;
|
let flip = r#"{"value":"comp"}"#;
|
||||||
let (st, _) = raw_request(port, "POST", "/build-path", &[json], Some(flip));
|
let (st, _) = raw_request(port, "POST", "/build-path", &[json], Some(flip));
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1264,6 +1264,18 @@ impl ServerState {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn question_page_uses_system_fonts_and_inline_branding() {
|
||||||
|
assert!(!PAGE.contains("fonts.googleapis.com"));
|
||||||
|
assert!(!PAGE.contains("fonts.gstatic.com"));
|
||||||
|
assert!(!PAGE.contains("@font-face"));
|
||||||
|
assert!(!PAGE.contains("/fonts/"));
|
||||||
|
assert!(PAGE.contains("--ks-font: system-ui,"));
|
||||||
|
assert!(PAGE.contains("aria-label=\"Impeccable\""));
|
||||||
|
assert!(PAGE.contains("color-scheme: light"));
|
||||||
|
assert!(!PAGE.contains("color-scheme: dark"));
|
||||||
|
}
|
||||||
|
|
||||||
// JS scenarios: tests/serve-question.test.mjs (public repo main,
|
// JS scenarios: tests/serve-question.test.mjs (public repo main,
|
||||||
// eaaecbd1 + 2e075dc5 + 7982002d).
|
// eaaecbd1 + 2e075dc5 + 7982002d).
|
||||||
|
|
||||||
|
|||||||
@@ -129,6 +129,76 @@ function spawnSyncGen(prompt, out, size = null) {
|
|||||||
// serve-question interactive cycles
|
// serve-question interactive cycles
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
describe('new-work-e2e: serve-question decision page', () => {
|
describe('new-work-e2e: serve-question decision page', () => {
|
||||||
|
it('renders a light system-font picker without font or external requests', async () => {
|
||||||
|
const cwd = makeWorkspace();
|
||||||
|
const key = 'system-fonts';
|
||||||
|
const context = await browser.newContext({ colorScheme: 'dark' });
|
||||||
|
const externalRequests = [];
|
||||||
|
const fontResponses = [];
|
||||||
|
try {
|
||||||
|
const { url } = await startDaemon(cwd, {
|
||||||
|
title: 'Choose the visual world',
|
||||||
|
options: [{ id: 'assigned', label: 'Local typography' }],
|
||||||
|
buildPath: { value: 'code', toggle: true },
|
||||||
|
}, key);
|
||||||
|
const origin = new URL(url).origin;
|
||||||
|
await context.route('**/*', (route) => {
|
||||||
|
if (new URL(route.request().url()).origin !== origin) {
|
||||||
|
externalRequests.push(route.request().url());
|
||||||
|
return route.abort();
|
||||||
|
}
|
||||||
|
return route.continue();
|
||||||
|
});
|
||||||
|
const page = await context.newPage();
|
||||||
|
page.on('response', (response) => {
|
||||||
|
if (response.request().resourceType() === 'font') {
|
||||||
|
fontResponses.push({ url: response.url(), status: response.status(), type: response.headers()['content-type'] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await page.goto(url);
|
||||||
|
const appearance = await page.evaluate(async () => {
|
||||||
|
await document.fonts.ready;
|
||||||
|
const tokenColor = (token) => {
|
||||||
|
const probe = document.createElement('span');
|
||||||
|
probe.style.color = `var(${token})`;
|
||||||
|
document.body.append(probe);
|
||||||
|
const color = getComputedStyle(probe).color;
|
||||||
|
probe.remove();
|
||||||
|
return color;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
fonts: document.fonts.size,
|
||||||
|
family: getComputedStyle(document.body).fontFamily,
|
||||||
|
scheme: getComputedStyle(document.documentElement).colorScheme,
|
||||||
|
logoPaths: document.querySelectorAll('.brand svg path').length,
|
||||||
|
logoText: document.querySelectorAll('.brand svg text').length,
|
||||||
|
kinpaku: tokenColor('--ks-kinpaku'),
|
||||||
|
rule: tokenColor('--ks-rule'),
|
||||||
|
leadBorder: getComputedStyle(document.querySelector('.face.lead')).borderTopColor,
|
||||||
|
switchBorder: getComputedStyle(document.querySelector('.bp-switch')).borderTopColor,
|
||||||
|
activeDot: getComputedStyle(document.querySelector('.bp-opt.active'), '::before').backgroundColor,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
assert.equal(appearance.fonts, 0, 'no custom font faces');
|
||||||
|
assert.match(appearance.family, /^system-ui,/);
|
||||||
|
assert.equal(appearance.scheme, 'light', 'picker stays light even with a dark OS preference');
|
||||||
|
assert.ok(appearance.logoPaths > 2, 'the brand mark and wordmark are vector outlines');
|
||||||
|
assert.equal(appearance.logoText, 0, 'the logo does not depend on a font');
|
||||||
|
assert.equal(appearance.leadBorder, appearance.kinpaku, 'lead outline uses default Kinpaku');
|
||||||
|
assert.equal(appearance.activeDot, appearance.kinpaku, 'active switch dot uses default Kinpaku');
|
||||||
|
assert.equal(appearance.switchBorder, appearance.rule, 'switch track has a quiet border');
|
||||||
|
await page.locator('.card').first().hover();
|
||||||
|
assert.equal(await page.locator('.face.lead').evaluate((el) => getComputedStyle(el).borderTopColor), appearance.kinpaku, 'hover preserves the default Kinpaku outline');
|
||||||
|
await page.getByRole('img', { name: 'Impeccable', exact: true }).waitFor();
|
||||||
|
assert.deepEqual(externalRequests, [], 'dialog must not request third-party resources');
|
||||||
|
assert.deepEqual(fontResponses, [], 'no bundled or remote font downloads');
|
||||||
|
} finally {
|
||||||
|
await context.close();
|
||||||
|
await stopDaemon(cwd, key);
|
||||||
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('(a) pick assigned returns the option, hero/board fields, and the CHOSEN CARD directive', async () => {
|
it('(a) pick assigned returns the option, hero/board fields, and the CHOSEN CARD directive', async () => {
|
||||||
const cwd = makeWorkspace();
|
const cwd = makeWorkspace();
|
||||||
const key = 'pick';
|
const key = 'pick';
|
||||||
|
|||||||
Reference in New Issue
Block a user