diff --git a/.impeccable/surfaces/crates-context-src-question-page-rs.md b/.impeccable/surfaces/crates-context-src-question-page-rs.md new file mode 100644 index 000000000..d9748b641 --- /dev/null +++ b/.impeccable/surfaces/crates-context-src-question-page-rs.md @@ -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. diff --git a/crates/cli/tests/serve_question_security.rs b/crates/cli/tests/serve_question_security.rs index 1a82731da..76eed23bc 100644 --- a/crates/cli/tests/serve_question_security.rs +++ b/crates/cli/tests/serve_question_security.rs @@ -106,6 +106,16 @@ fn detached_posts_require_key_and_loopback_host_origin() { assert!(page.contains("/heartbeat' + 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. let flip = r#"{"value":"comp"}"#; let (st, _) = raw_request(port, "POST", "/build-path", &[json], Some(flip)); diff --git a/crates/context/src/question_page.rs b/crates/context/src/question_page.rs index 7e1511a96..62199cbd1 100644 --- a/crates/context/src/question_page.rs +++ b/crates/context/src/question_page.rs @@ -1,7 +1,11 @@ //! Generated from skill/scripts/serve-question.mjs `page()` template by scratchpad/gen-page.mjs. //! `@@N@@` marks the N-th `${...}` interpolation (see PAGE_EXPRS), filled by serve_question.rs. +//! Picker theme follows impeccable-site's paper-and-instruments system. +//! Inline logo outlines reuse site/public/assets/brand/impeccable-logo-on-light.svg +//! from impeccable-site at eb5348e48fcd94d0d74895488b985988a401a2e2, with a tight +//! viewBox and theme-aware fills. All remaining typography uses system fonts. -pub const PAGE: &str = "\n\n@@1@@\n\n\n\n\n
\n
\n\n\n@@3@@\n
\n
\n \n Impeccable\n
\n
\n
\n
\n
\n \n

@@4@@

\n @@5@@\n
\n @@6@@\n
\n
@@7@@
\n
\n
\n \n \n
\n
\n
\n\n"; +pub const PAGE: &str = "\n\n@@1@@\n\n
\n
\n\n\n@@3@@\n
\n
\n \n \n \n \n \n \n\n
\n
\n
\n
\n
\n \n

@@4@@

\n @@5@@\n
\n @@6@@\n
\n
@@7@@
\n
\n
\n \n \n
\n
\n
\n\n"; /// The JS expressions, in order, for reference. pub const PAGE_EXPRS: &[&str] = &[ diff --git a/crates/context/src/serve_question.rs b/crates/context/src/serve_question.rs index b007d378c..d846d6849 100644 --- a/crates/context/src/serve_question.rs +++ b/crates/context/src/serve_question.rs @@ -1264,6 +1264,18 @@ impl ServerState { mod tests { 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, // eaaecbd1 + 2e075dc5 + 7982002d). diff --git a/tests/new-work-e2e.test.mjs b/tests/new-work-e2e.test.mjs index f9fc3ccdd..f33d67068 100644 --- a/tests/new-work-e2e.test.mjs +++ b/tests/new-work-e2e.test.mjs @@ -129,6 +129,76 @@ function spawnSyncGen(prompt, out, size = null) { // serve-question interactive cycles // -------------------------------------------------------------------------- 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 () => { const cwd = makeWorkspace(); const key = 'pick';