From cadb70a09e24d15da2227ece3b3bba7e2463fa7e Mon Sep 17 00:00:00 2001 From: Abdul Wahab Date: Fri, 31 Jul 2026 01:48:27 +0500 Subject: [PATCH] update --- .impeccable/config.json | 12 +- CLAUDE.md | 12 +- picker/components/Artboard.astro | 195 ++- picker/components/QuestionScreen.astro | 45 +- picker/data/surfaces.js | 47 +- picker/layouts/Picker.astro | 2 + picker/pages/index.astro | 358 +++++- picker/scripts/design-context.js | 1114 +++++++++++++++++ picker/scripts/palette-picker.js | 235 +++- picker/styles/design-context.css | 1591 ++++++++++++++++++++++++ picker/styles/picker.css | 1300 +++++++++++++++++-- picker/styles/screens/boundaries.css | 87 ++ picker/styles/screens/depth.css | 39 + picker/styles/screens/layout.css | 178 ++- picker/styles/screens/motion.css | 575 +++++++++ skill/reference/document.md | 37 +- skill/reference/visual-cues.md | 55 +- skill/scripts/picker-doc-poll.mjs | 99 ++ skill/scripts/picker-doc-session.mjs | 355 ++++++ skill/scripts/picker-server.mjs | 34 +- tests/picker-server.test.mjs | 24 +- 21 files changed, 6065 insertions(+), 329 deletions(-) create mode 100644 picker/scripts/design-context.js create mode 100644 picker/styles/design-context.css create mode 100644 skill/scripts/picker-doc-poll.mjs create mode 100644 skill/scripts/picker-doc-session.mjs diff --git a/.impeccable/config.json b/.impeccable/config.json index 62fe2ea47..53f341689 100644 --- a/.impeccable/config.json +++ b/.impeccable/config.json @@ -4,7 +4,8 @@ "ignoreFiles": [ "tests/fixtures/**", "tests/framework-fixtures/**", - "tests/detect-antipatterns.test.js" + "tests/detect-antipatterns.test.js", + "picker/styles/design-context.css" ], "ignoreValues": [ { @@ -32,6 +33,15 @@ ], "createdAt": "2026-07-23T21:00:00.000Z", "reason": "The picker is a self-contained questionnaire widget with its own kit-token type scale, and screen 04's cards render arbitrary Google Fonts at specimen sizes; DESIGN.md's ramp describes the impeccable website, not this tool (same footing as the live-browser.js entry)" + }, + { + "rule": "design-system-font", + "value": "*", + "files": [ + "picker/scripts/design-context.js" + ], + "createdAt": "2026-07-30T13:11:54.730Z", + "reason": "Runtime font loader: builds a Google Fonts URL from the family the agent/user chose in the questionnaire; no literal face is being introduced" } ] }, diff --git a/CLAUDE.md b/CLAUDE.md index 030dd9717..5dc28eedc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -238,16 +238,20 @@ After `bun run build:picker`, scan the **built directory**: node cli/bin/cli.js detect skill/scripts/picker/ ``` -**Name the directory, not `index.html`.** The built page only links its stylesheet, and the rules that read CSS text run per file, so scanning the page alone never reads a line of the picker's CSS. That is how three `design-system-color` findings vanished with nothing fixed: the mode-tile demo palette moved out of an inline `style` attribute and into `picker.css`, and the gate went quiet while the values stayed put. Naming the bundle by path is not a fix either, because Vite content-hashes the filename and the stale path silently narrows the scan back to the page. +**Name the directory, not `index.html`.** The built page only links its stylesheet and its script, and the rules that read CSS or JS text run per file, so scanning the page alone never reads a line of either. That is how three `design-system-color` findings vanished with nothing fixed: the mode-tile demo palette moved out of an inline `style` attribute and into `picker.css`, and the gate went quiet while the values stayed put. Naming a bundle by path is not a fix either, because Vite content-hashes the filenames and a stale path silently narrows the scan back to the page. -Baseline: **39 findings**, 27 on `index.html` and 12 on the bundled stylesheet. The groups below are accepted with a reason on record, so hold 39 rather than treating it as a backlog: +Baseline: **54 findings**, 29 on `index.html`, 24 on the bundled stylesheet, and 1 on the bundled script. The groups below are accepted with a reason on record, so hold 54 rather than treating it as a backlog: - 18 `cramped-padding` on the palette strips, nine for each of the two: the editable one on screen 02 and the reading copy under screen 03's choices. Each strip reports its group, its four items, and its four swatches. A swatch is a color that has to reach its own edges, so `.picker-band` carries no padding and places its hex readout and drag grip by inset instead, which the rule has no way to see. Adding padding here would put a frame around every color the visitor picked. - 3 `design-system-color` for the mode-tile demo palette (`#195856`, `#5f7c7a`, `#3a958b`). They are examples standing in for the palette the visitor is about to pick, which is the carve-out DESIGN.md's OKLCH-Only Rule already names. The argument is at the declaration in `picker/styles/picker.css`; read it before touching them. -- 7 `design-system-font-size` already waived for `picker/styles/picker.css` in `.impeccable/config.json`. They reappear under the built path only because that waiver is scoped to the source path. +- 8 `design-system-font-size` from `picker/styles/picker.css`, seven of them already waived there in `.impeccable/config.json`. They reappear under the built path only because that waiver is scoped to the source path. The eighth is `1.05rem` on `.picker-finish-lede`, the review copy on the last screen, which sits on the questionnaire's own kit-token ramp for the same reason the other seven do. - 2 `border-accent-on-rounded` on `.ks-bento` in the vendored `kinpaku-kit.css`, which carries no `border-radius` at all. The rule wants a radius on the same line, and minification puts the whole stylesheet on one line, so every line-scoped regex rule reads this bundle with no line context. Check a snippet against the source before acting on it. +- 13 from the design context document, the review UI the picker hands off to after the last question. Eleven are `picker/styles/design-context.css`: 5 `design-system-font-size` and 1 `design-system-radius` (`22px`) for the ported prototype's own type ramp and shape vocabulary, 2 `design-system-color` (`#7ba98f`, `#c26d5a`) for the tray status dots, 2 `design-system-font` naming the two faces the document sets itself in, and 1 `layout-transition` on the tile expander, which animates `width` and `height` because the tile morphing into a panel is the geometry it is animating. One is `picker/scripts/design-context.js`, whose Google Fonts URL is built from whichever family the run chose, so the rule sees a template where it wants a declared face. The last is `all-caps-body` on the doc masthead's mono note. The user locked that UI; both files carry a waiver in `.impeccable/config.json` saying so, and like the picker.css group those waivers are source-scoped, so the values resurface under the built path. +- 1 `undersized-ui-text` on the `Change` legend in the document's request-scope control, at 10.88px. Same slot and same argument as the micro-labels below. -The last 9 are the page findings the questionnaire has carried since those screens were built: `cramped-padding` on the option panel and the icon sheet, both edge-to-edge grids whose rows hold their own padding, and the mono readouts and micro-labels under the text-size floors. They are the group to revisit first if the number is ever worth lowering. +The last 9 are the page findings the questionnaire has carried since those screens were built: 3 `cramped-padding` on the option panel and the icon sheet, both edge-to-edge grids whose rows hold their own padding, and 6 mono readouts and micro-labels under the text-size and tracking floors. They are the group to revisit first if the number is ever worth lowering. + +**When the number moves, find out which file moved it before editing this section.** The baseline read 39 until the design context document landed, and all 15 findings that arrived with it are named above: 13 in its own two files, plus the finish-screen lede and the `Change` legend that ship in the picker's files but belong to that feature. The five other workstreams in the same batch added none. Building the tree at the previous commit in a `git worktree` and scanning both builds is how that was established, and it beats reasoning about which change is responsible. For line numbers during triage, `node cli/bin/cli.js detect picker/styles/` scans the unminified sources. It is not the gate: the source scan's style-context heuristic skips custom-property declarations, so it misses the palette entirely. A served URL is worse than either, because the browser pass only measures the screen in front of it and the picker hides all but the active one. diff --git a/picker/components/Artboard.astro b/picker/components/Artboard.astro index 88b8bf14c..d53ebac14 100644 --- a/picker/components/Artboard.astro +++ b/picker/components/Artboard.astro @@ -8,6 +8,13 @@ adds the data-type-* hooks the font screen writes specimen copy into; that screen is judging glyphs, so bars would tell it nothing. + Every body is drawn both ways for the same reason. The slots stay one tree and + one set of class names; what changes is whether a slot holds a data-type-* + hook or the bars that stand in for the words. Only the font screen's boards are + filled, so a hook on any other board would promise copy that never arrives, + and a paragraph is a stack of bars rather than one because the block a + structural answer acts on is the shape of the passage, not a single line. + phone: false drops the handset and leaves the desktop artboard alone in the frame, for a screen whose answer only reads at desktop width. */ @@ -89,41 +96,59 @@ const hook = type ? '' : undefined;
{[0, 1, 2].map((n) => ( - + ))}
- +
{[0, 1, 2].map(() => ( - - + + ))}
+ {/* A chart is the one thing a dashboard has that no other surface + does, and it sets two kinds of text a pair has to be judged on + together: a heading over a plot, and a row of category labels + sitting under it at the smallest step on the board. The bars + themselves stay schematic, drawn as a share of the plot rather + than from numbers, because what is being chosen here is a pair + and not a reading of the data. */} +
+ +
+ {[46, 66, 100, 58, 44].map((h, n) => ( + + ))} +
+
+ {[0, 1, 2, 3, 4].map(() => )} +
+
- + {[0, 1, 2].map((n) => ( - - - + + + ))}
- - + + {[0, 1].map((n) => ( - + ))} @@ -137,7 +162,7 @@ const hook = type ? '' : undefined;
{[0, 1, 2, 3].map((n) => ( - + ))}
@@ -146,35 +171,71 @@ const hook = type ? '' : undefined; and the heading under it is visible at a glance, so the board shows both rather than the single heading the drawing carries. */}
-

-

- -

-

+

{!type && }

+

{!type && [0, 1].map(() => )}

+ +

{!type && [0, 1, 2].map(() => )}

+

{!type && [0, 1, 2].map(() => )}

+ {/* Prose, a list, and a callout are what a documentation page is + made of, and the board carried two of the three. The list is + also the only setting here that asks the body face to hold a + short ragged line against a marker. */} +
    + {[0, 1].map(() =>
  • )} +
- - - + + {!type && [0, 1].map(() => )}
+ ) : index && !type ? ( + /* The index as a wireframe, for the screen that judges movement rather + than glyphs. Same four bands as the set version below, drawn in bars: + a page title, two staggered entries, and the carousel rail. */ +
+ + {[0, 1].map((n) => ( +
+
+
+ + + +
+
+ ))} +
+ +
+ {[0, 1, 2, 3].map((n) => ( + + ))} +
+ +
+
) : index ? (
- {/* The drawing staggers two plates and neither says anything the other - does not. One row buys the page title above it, which is the only - place in the set where the heading face is allowed to be as large - as this surface would really set it. */} + {/* The page title the drawing has no room for, over the two staggered + entries it draws. The second entry is what the handset shows too, + and it is the one that proves the caption ladder repeats: a pair set + on an index is read down a run of entries rather than at one of + them. Alternating the plate keeps the run from collapsing into a + single column at a width that has room for two. */}

-
-
-
- - - + {[0, 1].map((n) => ( +
+
+
+ + + +
-
+ ))} {/* The chevrons stay shapes because an arrow is not a word. The track's segments are, so they are set: a carousel's stops are named. */}
@@ -229,8 +290,14 @@ const hook = type ? '' : undefined; : }
+ {/* Three cards once the labels are words, four while they are bars. The + type board sets three because a fourth column leaves the copy beside + it under the width the widest pairing needs, and the preview copy is + written to that count: a fourth card here would draw a picture with + no caption under it. The bar composition is measured at four by the + layout screens, so it keeps them. */} diff --git a/picker/components/QuestionScreen.astro b/picker/components/QuestionScreen.astro index c89a8f568..eaf2b2c99 100644 --- a/picker/components/QuestionScreen.astro +++ b/picker/components/QuestionScreen.astro @@ -5,7 +5,7 @@ as data and its design lives in its own stylesheet. */ import Artboard from './Artboard.astro'; -import { SURFACE_MODES, SURFACE_ANSWERS, surfaceTabsAttrs } from '../data/surfaces.js'; +import { SURFACE_ANSWERS, surfacesAsked, surfaceTabsAttrs } from '../data/surfaces.js'; interface Option { value: string; @@ -16,6 +16,14 @@ interface Option { blocked?: string; } +/* A screen whose answer reads differently on each surface draws a board per + surface instead of repainting one, the arrangement the font screen already + uses. `class` is the modifier that board's own scene keys on. */ +interface Board { + surface: 'persuade' | 'operate' | 'read' | 'experience'; + class?: string; +} + interface Props { id: string; step: string; @@ -27,15 +35,21 @@ interface Props { options: Option[]; previewClass: string; phone?: boolean; + boards?: Board[]; } -const { id, step, slug, name, title, legend, cta, options, previewClass, phone = true } = Astro.props; +const { id, step, slug, name, title, legend, cta, options, previewClass, phone = true, boards } = Astro.props; const titleId = `picker-${slug}-title`; const artboardClass = phone ? previewClass : `${previewClass} picker-artboard--solo`; // A question listed in the matrix is answered once per chosen surface, which // buys it a tab strip on the frame and a field per surface to answer into. // Everything else about the screen is unchanged by opting in. const perSurface = SURFACE_ANSWERS[name]; +// Only the surfaces the question is put to. A surface with no field here is a +// surface that was never asked, and that absence is the whole of how the script +// and the final document know it. +const asked = perSurface ? surfacesAsked(name) : []; +const tabsAttrs = perSurface ? surfaceTabsAttrs(name) : null; ---
diff --git a/picker/data/surfaces.js b/picker/data/surfaces.js index 740685f8d..1ad563ec2 100644 --- a/picker/data/surfaces.js +++ b/picker/data/surfaces.js @@ -34,6 +34,14 @@ export const SURFACE_MODES = ['persuade', 'operate', 'read', 'experience']; surface's choice is written down. A key per surface would promise whoever reads the answers a distinction the run has nowhere to spend. + A question that leaves a surface out of `surfaces` is not asked of that + surface at all, which is a stronger statement than withholding an option from + it. Withholding says the surface would answer this badly; leaving it out says + the surface has no stake in the question. So the tab strip only offers the + surfaces the question names, and a run whose surfaces are all left out never + sees the screen and records no answer for it. Only motion is scoped that way + today; the rest name all four. + Persuade is allowed everything on every question: earning attention is the whole job of the surface, and no answer here is too much for it. The other three are ruled by what the surface is for rather than by how loud an option @@ -83,6 +91,32 @@ export const SURFACE_ANSWERS = { }, }, + /* + The one question that is not put to every surface. Motion energy is a claim + on attention, and only two of the four surfaces are in a position to make + one: a landing page earning a decision and a portfolio presenting work. A + tool and a document are moved through rather than watched, and their motion + follows from what the interface is doing rather than from a house style, so + asking them would collect an answer nothing should act on. An app-UI-only or + docs-only run therefore never reaches this screen. + + Both surfaces that are asked can take all three energies, so nothing is + withheld and no option carries a reason. Their defaults differ, because what + a surface does with movement when nobody says otherwise is the whole of what + it is for: a page earning a decision has to answer the pointer it is trying + to keep, and a portfolio stages the work's arrival, which is what a reveal + is for. + */ + 'motion-energy': { + tablist: 'Surface being moved', + answered: '{} movement', + unanswered: 'no motion energy chosen yet', + surfaces: { + persuade: { fallback: 'responsive' }, + experience: { fallback: 'choreographed' }, + }, + }, + /* Flat, and the tabs here are worth having anyway. The twelve-column ruler on this screen describes a page, and it describes one whether the run ships a @@ -190,10 +224,12 @@ export const SURFACE_ANSWERS = { /* The matrix reaches the browser on the surface tiles, which is already where the script looks for everything a surface knows about itself. One pair of attributes per question, read by name rather than by dataset key so the - question's own value is the lookup. */ + question's own value is the lookup. A question this surface is not asked + contributes nothing, and the absent per-surface field is what the script + reads that from. */ export const surfaceAttrs = (mode) => Object.fromEntries( Object.entries(SURFACE_ANSWERS).flatMap(([name, question]) => { - const { allow, fallback } = question.surfaces[mode]; + const { allow, fallback } = question.surfaces[mode] ?? {}; return [ ...(allow ? [[`data-allow-${name}`, allow]] : []), ...(fallback ? [[`data-default-${name}`, fallback]] : []), @@ -201,6 +237,13 @@ export const surfaceAttrs = (mode) => Object.fromEntries( }), ); +/* Which surfaces a question is put to at all, in tile order. The per-surface + fields a screen renders are the browser's copy of this, and the leading + applicable surface is the one whose answer the bare key carries. */ +export const surfacesAsked = (name) => SURFACE_MODES.filter( + (mode) => mode in SURFACE_ANSWERS[name].surfaces, +); + /* Everything the script needs to run a per-surface question is on its tab strip, so a screen opts in by rendering one of these and nothing else. */ export const surfaceTabsAttrs = (name) => ({ diff --git a/picker/layouts/Picker.astro b/picker/layouts/Picker.astro index a94d3038e..da0efd474 100644 --- a/picker/layouts/Picker.astro +++ b/picker/layouts/Picker.astro @@ -8,6 +8,8 @@ import '../styles/screens/layout.css'; import '../styles/screens/boundaries.css'; import '../styles/screens/corners.css'; import '../styles/screens/depth.css'; +// The design context document that replaces the picker after submission. +import '../styles/design-context.css'; interface Props { title?: string; diff --git a/picker/pages/index.astro b/picker/pages/index.astro index 76a88bc88..0da6071e0 100644 --- a/picker/pages/index.astro +++ b/picker/pages/index.astro @@ -44,10 +44,29 @@ const iconRows = iconPacks.packs.map((pack) => ({ // data/surfaces.js, so the tiles carry what each surface allows without the // matrix being restated on every screen that reads it. -// The five questions that are answered by looking at the same wireframe page. -// Each one only rewrites the artboard slots it owns, so by the last screen the -// page is showing every decision made before it. Motion is the one question -// whose answer cannot be read on a handset, so it takes the frame alone. +// The five questions that are answered by looking at a wireframe page. Each one +// only rewrites the artboard slots it owns, so by the last screen the page is +// showing every decision made before it. Motion is the one question whose answer +// cannot be read on a handset, so it takes the frame alone. +// +// All five draw a board per surface. A structural answer is a claim about a kind +// of page, and the four kinds are not the same shape: an app shell divides into +// a rail, a working column, and a panel, a document is a rail and a measure, an +// index is a run of entries. Repainting one landing page under four labels +// showed the same page four times and asked the visitor to imagine the rest. +// +// Which board is showing is read off `data-surface`, the attribute the component +// already publishes and the one the tab strip toggles, rather than a class per +// screen per surface. Screen 06 takes a class because its scenes are written per +// board; these four ask the same question of every board and only need to know +// which anatomy is under them. +const boards = [ + { surface: 'persuade' }, + { surface: 'operate' }, + { surface: 'read' }, + { surface: 'experience' }, +]; + const questions = [ { id: '06', @@ -59,6 +78,15 @@ const questions = [ cta: 'Select motion energy', previewClass: 'picker-preview-motion', phone: false, + // The two surfaces this question is put to, and the only screen where a + // surface answers on a page of its own kind: an energy is demonstrated by + // watching a page use it, and a landing page and a portfolio use movement + // for different ends. Repainting one wireframe would show the same scene + // twice under two labels. + boards: [ + { surface: 'persuade' }, + { surface: 'experience', class: 'picker-preview-motion--index' }, + ], options: [ { value: 'restrained', @@ -86,6 +114,7 @@ const questions = [ legend: 'Layout structure', cta: 'Select layout structure', previewClass: 'picker-strategy-preview picker-preview-layout', + boards, options: [ { value: 'simple-grid', @@ -114,6 +143,7 @@ const questions = [ legend: 'Boundary style', cta: 'Select boundary style', previewClass: 'picker-preview-boundaries', + boards, options: [ { value: 'open-space', @@ -148,6 +178,7 @@ const questions = [ legend: 'Corner style', cta: 'Select corner style', previewClass: 'picker-preview-corners', + boards, options: [ { value: 'sharp', @@ -181,6 +212,7 @@ const questions = [ legend: 'Depth style', cta: 'Select depth style', previewClass: 'picker-preview-depth', + boards, options: [ { value: 'flat', @@ -947,46 +979,46 @@ const questions = [ no editorial page would spend a heading level on. */}