free-pi-cli 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,252 @@
1
+ ---
2
+ name: ux-visual
3
+ description: Exhaustive visual and hands-on design review of every page, layout and component, judged against strong UI norms. Screenshots and measures each surface (look_at_image plus DOM audits), tries it like a user, writes up problems, drafts three competing fixes per problem, scores them, implements the winner, and verifies it with tests and fresh screenshots. Use for "review the visual design", "audit the layout", "design critique", "check density and whitespace", "why does this page scroll".
4
+ ---
5
+
6
+ # UX Visual
7
+
8
+ Review whether every surface looks and behaves like the work of a strong product team,
9
+ from the smallest component up to the whole layout. Then fix what is wrong: draft three
10
+ different fixes, score them, implement the best, and prove it with measurements and new
11
+ screenshots.
12
+
13
+ Take the view of a demanding senior designer and a first-time user at once. Do not
14
+ excuse a problem as an edge case, a framework default, or something only small screens
15
+ show. If a person would hesitate, squint, scroll for what should be one screen, hunt for
16
+ a control, or misread a label, it is a finding. Also say what is excellent. No invented
17
+ findings.
18
+
19
+ This skill runs on your default model. Do not switch to a more expensive model. You
20
+ cannot see images directly: judge screenshots with the `look_at_image` tool (a cheap
21
+ vision model), and use DOM measurements for everything measurable.
22
+
23
+ Everything runs in one agent, in order. There are no helpers to hand work to. Work on one
24
+ page, one problem cluster, one proposal at a time. Write each result to its file at once
25
+ and keep only a one-line summary in context. After a context reset, resume from
26
+ `docs/ux-visual/` (see "Resuming").
27
+
28
+ `$SKILL_DIR` means the folder that contains this SKILL.md. The geometry audit lives in
29
+ the sibling skill: `$SKILL_DIR/../ux-walker/references/geometry-audit.js`. The two skills
30
+ ship together.
31
+
32
+ ## Options the user may give
33
+
34
+ url; `--scope <page|route|component>` (default: whole app); `--viewports <list>`
35
+ (default `1440x900,768x1024,390x844`); `--no-implement` (stop after the decisions);
36
+ `--max-fixes N` (default 8 clusters carried through to implementation); `--session <name>`.
37
+ With no arguments, detect the URL and start.
38
+
39
+ ## Output
40
+
41
+ ```
42
+ docs/ux-visual/
43
+ inventory.md pages, layouts, groups, components, states
44
+ pages/<slug>/shots/<viewport>-<state>.png (inside the project folder)
45
+ pages/<slug>/audit.json layout-audit and geometry-audit results
46
+ pages/<slug>/findings.md critique for this page
47
+ problems.md clustered, ranked problems
48
+ proposals/<cluster>/{A,B,C}.md (+ mock screenshots if made)
49
+ decisions/<cluster>.md scores, winner, defense
50
+ verification/<cluster>.md after-measurements and screenshots
51
+ run-history.json clusters fixed, deferred, rejected, commits
52
+ report.md final report
53
+ ```
54
+
55
+ ## Phase 0: Preflight
56
+
57
+ 1. `command -v agent-browser`; if missing use `npx -y agent-browser` throughout. Run
58
+ `agent-browser install` once if the browser is not installed.
59
+ 2. Check the URL with curl. If nothing answers, start the dev server in the background
60
+ with a log and saved PID, pick a free port, and stop only that PID at the end. Delete the saved browser state file (step 3) at the end too.
61
+ 3. Auth: find how the app signs in (test users, seed data, e2e helpers). Sign in once in
62
+ the session (`S=ux-visual-<domain>`), then `state save "${TMPDIR:-/tmp}/free-pi-ux-browser-state.json"`
63
+ (outside the project, because it holds cookies; never commit it; delete it at the end of the run).
64
+ Use test data only. Never act destructively on real data.
65
+ 4. Design system: find tokens, the component library, and any enforcement script. Every
66
+ fix lands in the design system first. Note the rules in `inventory.md`.
67
+ 5. Reuse earlier work if present: `docs/ux-paths/catalog.md` (which states to visit),
68
+ `docs/ux-flow/report.md` (decisions already made; do not reopen them),
69
+ `docs/ux-visual/run-history.json` (skip clusters fixed earlier unless their surfaces changed).
70
+ 6. `mkdir -p docs/ux-visual/pages docs/ux-visual/proposals docs/ux-visual/decisions docs/ux-visual/verification`
71
+
72
+ ## Phase 1: Inventory
73
+
74
+ Write `docs/ux-visual/inventory.md`: every page or route with how to reach each state
75
+ (empty, loading, populated, long content, error, each role, signed out, transient UI such
76
+ as dialogs, menus, toasts); the layouts and which pages use them; the component groups
77
+ (toolbars, cards, forms, lists); the design-system components and where each renders.
78
+ This is the work list. Nothing is audited that is not listed.
79
+
80
+ ## Phase 2: Inspect, one page at a time
81
+
82
+ Read `references/visual-heuristics.md` once first. Then for each page in the inventory,
83
+ fully finish it (including its findings.md) before starting the next.
84
+
85
+ For each viewport x state you reach:
86
+
87
+ 1. **Capture.** `agent-browser --session $S set viewport <w> <h>`, reach the state, wait
88
+ for a known element (live apps never reach network idle: use `wait --text` or a
89
+ selector, with a short timeout), then
90
+ `agent-browser --session $S screenshot docs/ux-visual/pages/<slug>/shots/<viewport>-<state>.png`
91
+ and a `--full` copy for pages that scroll.
92
+ 2. **Measure.** Run both audits and append the JSON to `pages/<slug>/audit.json`, keyed by
93
+ viewport and state:
94
+ ```bash
95
+ agent-browser --session $S eval --stdin < "$SKILL_DIR/references/layout-audit.js"
96
+ agent-browser --session $S eval --stdin < "$SKILL_DIR/../ux-walker/references/geometry-audit.js"
97
+ ```
98
+ `layout-audit.js` reports viewport fit and page scroll, off-screen primary actions,
99
+ scroll containers, dead space, spacing-scale drift, type sizes, alignment, small
100
+ targets, low contrast, overlaps and fixed overlays. `geometry-audit.js` reports uneven
101
+ siblings, spills and wrapped controls. These numbers decide anything measurable.
102
+ 3. **Look.** Make `look_at_image` calls for judgment only: hierarchy, clarity, balance,
103
+ whether the screen looks intentional. **Budget: one call per viewport for the page's
104
+ main state, plus at most two more per page for other key states (a dialog, an error).
105
+ No more than 5 per page.** One specific question per call, built from the norms:
106
+ ```
107
+ look_at_image(path="docs/ux-visual/pages/settings/shots/1440x900-main.png",
108
+ question="This is the settings page of a developer tool. (1) What is the single most prominent element, and is it the primary action? (2) Is any information shown twice? (3) Are there large empty areas with no purpose, or crowded clusters? (4) Which labels are unclear? (5) Does anything look misaligned or inconsistent with its neighbors? Name the elements.")
109
+ ```
110
+ Never ask twice about the same image. If a look_at_image error says the daily allowance
111
+ is used up, stop calling it, rely on measurements and the snapshot, and say so in the report.
112
+ 4. **Interact like a person.** Hover and click every control, tab through the page (focus
113
+ order and visible focus), open every menu and dialog, type into every field (long
114
+ text, emoji), resize between breakpoints, scroll every scroll container, trigger
115
+ errors (invalid input, `set offline on`). Use `snapshot -i` for refs and `../ux-walker/references/action-patterns.md` for commands. Note each hesitation,
116
+ layout jump, dead end and slow response. Do not use destructive actions on real data.
117
+ 5. **Critique bottom-up, then top-down**, naming the norm violated (IDs in visual-heuristics.md):
118
+ - Component: is the purpose obvious? the right pattern? all states (hover, focus,
119
+ active, disabled, loading, empty, error)? clear label? big enough target? aligned?
120
+ - Group: do the items belong together? one primary action? order matches the task?
121
+ spacing on the scale? duplicated by another group?
122
+ - Page: one purpose and focal point? Does it fit the viewport when it should be one
123
+ screen (app-like surfaces must not page-scroll)? density right for the surface?
124
+ whitespace purposeful? hierarchy matches importance? anything said twice?
125
+ - Layout: consistent with other pages that share it? regions on one grid with aligned
126
+ edges? predictable navigation? reflows rather than overflows at every breakpoint?
127
+ 6. **Severity.** P0 blocks or misleads (cannot complete, unreadable, key control off-screen).
128
+ P1 a person stumbles (scrolling for what should be one page, buried primary action,
129
+ confusing label, layout jump, inconsistent pattern). P2 visibly below excellent (uneven
130
+ spacing, wrong density, weak hierarchy, misalignment, repeated information). P3 polish.
131
+
132
+ Write `pages/<slug>/findings.md`: a one-paragraph verdict per viewport and state with
133
+ screenshot paths; a table (id | level | element | problem | norm | evidence | severity |
134
+ measured, observed or suspected); and an "Excellent as is" list. Every finding cites a
135
+ screenshot path and/or an audit.json metric. A look_at_image answer alone is "observed" or
136
+ "suspected" until a measurement confirms it. Do not edit code in this phase.
137
+
138
+ Keep one line per page in context: "settings: 1 P0, 3 P1, 5 P2; worst: page scrolls 412px at 1440x900".
139
+
140
+ ## Phase 3: Consolidate
141
+
142
+ Read the findings files one at a time and build `docs/ux-visual/problems.md`:
143
+ 1. Merge the same problem seen on several pages or viewports.
144
+ 2. Group by root cause (for example "the room grid does not constrain height" explains five
145
+ findings). One cluster is one design decision. Prefer fixing a shared component, layout
146
+ or token over patching pages.
147
+ 3. Rank clusters by severity x reach (surfaces and journeys affected) x centrality, divided
148
+ by effort. P0 first.
149
+ 4. Check coverage against inventory.md and list any page or state not audited.
150
+ 5. For each cluster: id, title, root cause, finding ids, affected surfaces, evidence,
151
+ severity, and acceptance criteria that can be measured ("no page scroll at 1440x900 on
152
+ /room", "all card padding uses the 16px token", "primary action inside the viewport").
153
+
154
+ Carry the top `--max-fixes` clusters forward. List the rest as deferred.
155
+
156
+ ## Phase 4: Propose (A, B, C, one after another)
157
+
158
+ For one cluster at a time, write three proposals to `docs/ux-visual/proposals/<cluster>/`,
159
+ one file after the other, each with a different strategy:
160
+ - **A Minimal**: the smallest change to existing components and tokens that meets the criteria.
161
+ - **B Restructure**: rethink layout, grouping or pattern (tabs instead of stacked panels,
162
+ a docked strip instead of inline cards).
163
+ - **C Convention**: what the best products in this category do. Name the pattern, not brand assets.
164
+
165
+ Each file: the change, a before/after description, an ASCII wireframe or (optionally) a
166
+ static HTML mock in a scratch folder inside the project that uses the design system's
167
+ tokens and is screenshotted into the proposal folder, the components, tokens and files
168
+ touched, effort, risks, and how it meets each acceptance criterion. Proposals stay inside
169
+ the design system. Write A, B and C before scoring any of them.
170
+
171
+ ## Phase 5: Score and decide
172
+
173
+ Use `references/solution-rubric.md`.
174
+
175
+ 1. Score A, B and C yourself, one lens at a time (usability, then visual craft, then
176
+ engineering), writing the scores down for each lens before the next.
177
+ 2. If mocks exist, make one `look_at_image` critique call per mock (at most 3 per cluster)
178
+ as described in the rubric. Verify its claims before using them.
179
+ 3. Write `docs/ux-visual/decisions/<cluster>.md` from the template: the chosen solution
180
+ (possibly with a graft), the score table, the defense (why it beats each other option,
181
+ norms met, cost, what it rules out), final acceptance criteria and the test plan.
182
+
183
+ With `--no-implement`, stop after all decisions and go to the report.
184
+
185
+ ## Phase 6: Implement, one cluster at a time
186
+
187
+ Create a branch `ux-visual/<cluster>` (or work on the current branch if the user prefers).
188
+
189
+ 1. Change the **design system first** (tokens, component CSS, component library, catalog),
190
+ then pages. No ad-hoc styles. Run the project's design-system check if it has one.
191
+ 2. Write a **failing test first** for behavior or layout changes: for example an assertion
192
+ that `document.scrollingElement.scrollHeight <= innerHeight` on the page, that the
193
+ primary action's bounding box is inside the viewport, that sibling cards share a
194
+ height. Run it and see it fail.
195
+ 3. Implement the decision as written. Justify any deviation in the verification note.
196
+ 4. Run typecheck, lint, unit tests and the affected end-to-end tests. Update tests whose
197
+ expectations legitimately changed. Never weaken a test to make it pass.
198
+ 5. Commit only the changed files with a message naming the cluster. Do not push.
199
+
200
+ ## Phase 7: Verify, per cluster
201
+
202
+ Repeat the Phase 2 capture and measure steps on every affected surface at every viewport.
203
+ Compare with the "before" numbers in audit.json. Check each acceptance criterion with a
204
+ measurement. Make one `look_at_image` call per key after-screenshot to confirm the
205
+ hierarchy and look are better ("Compare this to a layout with <the earlier problem>: is
206
+ <the problem> gone?"). Check neighboring surfaces for regressions. Write
207
+ `docs/ux-visual/verification/<cluster>.md` with pass or fail per criterion and the
208
+ after-screenshots. On a fail, fix and re-check, at most twice, then record it as
209
+ unresolved with the evidence.
210
+
211
+ ## Phase 8: Report
212
+
213
+ Update `run-history.json`. Write `docs/ux-visual/report.md` from `templates/report-template.md`:
214
+ before and after per cluster, criteria met, what was deferred and why, remaining P0 and P1, and the "excellent as is" list. For deferred clusters: if `command -v gh` finds
215
+ GitHub CLI and `gh auth status` passes, check for duplicates and run `gh issue create`
216
+ (label `ux-visual` only if it exists). Otherwise write `docs/ux-visual/issues/<cluster>.md`.
217
+
218
+ Tell the user: surfaces audited and coverage gaps; findings by severity; each implemented
219
+ cluster in one line with the decision, why it won and the verified result; deferred
220
+ clusters; anything still failing; the path to report.md. Say what you did not verify.
221
+
222
+ ## Resuming
223
+
224
+ If context was cleared, read `run-history.json`, `inventory.md`, `problems.md`, then list
225
+ `pages/*/findings.md`, `proposals/*`, `decisions/*`, `verification/*` to see the last
226
+ completed step. Continue from the next missing file.
227
+
228
+ ## Rules
229
+
230
+ - **Measure, look, then touch.** Numbers settle evenness, fit and density. The vision
231
+ model settles hierarchy and clarity, and may be wrong. Interaction finds what neither shows.
232
+ - **Check every level**: component, group, page, layout, and back down. A fine component
233
+ can be wrong inside its group.
234
+ - **One screen means one screen.** App surfaces (editors, consoles, dashboards) must fit
235
+ the viewport at 1280x800 or larger, with inner panels scrolling. Content pages may
236
+ scroll but keep the primary action visible.
237
+ - **Density is a decision**, measured against the norms for the surface type.
238
+ - **Fix at the root, in the design system.** One token or component change beats ten page patches.
239
+ - **Wait for content, not the network.** Live pages never go idle.
240
+ - **A fix is done when measurements and new screenshots show it, tests pass, and nothing regressed.**
241
+ - Keep look_at_image calls within the budgets above. Prefer `layout-audit.js` and `geometry-audit.js` for anything they can measure.
242
+
243
+ ## References
244
+
245
+ | File | Use |
246
+ |------|-----|
247
+ | references/visual-heuristics.md | The norms to cite (read first) |
248
+ | references/layout-audit.js | Viewport fit, scroll, density, spacing, type, targets, contrast |
249
+ | ../ux-walker/references/geometry-audit.js | Uneven siblings, spills, wrapped controls |
250
+ | ../ux-walker/references/action-patterns.md | agent-browser command patterns |
251
+ | references/solution-rubric.md | Scoring and decision template |
252
+ | templates/report-template.md | Final report layout |
@@ -0,0 +1,145 @@
1
+ // Layout & density audit — run via: agent-browser --session {S} eval "$(cat layout-audit.js)"
2
+ // (or Playwright: page.evaluate(fs.readFileSync(path,'utf8'))). Returns JSON.
3
+ // Measures what eyes misjudge: viewport fit, scroll containers, dead space,
4
+ // spacing-scale drift, alignment, type scale, small targets, low contrast.
5
+ (() => {
6
+ const vw = innerWidth, vh = innerHeight;
7
+ const vis = (e) => {
8
+ const r = e.getBoundingClientRect();
9
+ if (r.width < 1 || r.height < 1) return false;
10
+ const cs = getComputedStyle(e);
11
+ return cs.visibility !== 'hidden' && cs.display !== 'none' && +cs.opacity !== 0;
12
+ };
13
+ const sig = (e) => {
14
+ let s = e.tagName.toLowerCase();
15
+ if (e.id) s += '#' + e.id;
16
+ else if (typeof e.className === 'string' && e.className.trim())
17
+ s += '.' + e.className.trim().split(/\s+/).slice(0, 2).join('.');
18
+ const t = (e.getAttribute('aria-label') || e.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 30);
19
+ return t ? `${s} "${t}"` : s;
20
+ };
21
+ const all = [...document.querySelectorAll('body *')].filter(vis);
22
+ const se = document.scrollingElement || document.documentElement;
23
+
24
+ // 1. Viewport fit (L1/L2/L6)
25
+ const viewport = {
26
+ width: vw, height: vh,
27
+ pageScrollY: Math.max(0, se.scrollHeight - vh),
28
+ pageScrollX: Math.max(0, se.scrollWidth - vw),
29
+ };
30
+
31
+ // Primary actions outside the viewport (L2): high-emphasis buttons, submit buttons
32
+ const primaries = all.filter((e) =>
33
+ (e.matches('button[type=submit], [data-primary], [class*="--primary"]') && e.matches('button, a, [role=button]')));
34
+ const offscreenPrimary = primaries
35
+ .map((e) => ({ el: sig(e), r: e.getBoundingClientRect() }))
36
+ .filter(({ r }) => r.bottom > vh + 1 || r.right > vw + 1 || r.top < 0)
37
+ .map(({ el, r }) => ({ el, top: Math.round(r.top), bottom: Math.round(r.bottom) }));
38
+
39
+ // 2. Scroll containers (L1/L3): which regions scroll and how much
40
+ const scrollers = all
41
+ .filter((e) => {
42
+ const cs = getComputedStyle(e);
43
+ return /(auto|scroll)/.test(cs.overflowY) && e.scrollHeight > e.clientHeight + 1;
44
+ })
45
+ .slice(0, 20)
46
+ .map((e) => ({ el: sig(e), clientH: e.clientHeight, scrollH: e.scrollHeight }));
47
+
48
+ // 3. Dead space (D2): large visible blocks whose area is mostly empty
49
+ const deadSpace = [];
50
+ for (const e of all) {
51
+ const r = e.getBoundingClientRect();
52
+ const area = r.width * r.height;
53
+ if (area < vw * vh * 0.08 || r.width < 200 || r.height < 120) continue;
54
+ const kids = [...e.querySelectorAll('*')].filter(vis);
55
+ if (kids.length > 400) continue;
56
+ // union area of leaf content boxes (text/media/controls), clipped to e
57
+ let covered = 0;
58
+ for (const k of kids) {
59
+ if (k.children.length && !k.matches('button, a, input, textarea, select, img, svg, video, canvas, iframe')) continue;
60
+ const kr = k.getBoundingClientRect();
61
+ const w = Math.max(0, Math.min(kr.right, r.right) - Math.max(kr.left, r.left));
62
+ const h = Math.max(0, Math.min(kr.bottom, r.bottom) - Math.max(kr.top, r.top));
63
+ covered += w * h;
64
+ }
65
+ const fill = Math.min(1, covered / area);
66
+ if (fill < 0.12) deadSpace.push({ el: sig(e), w: Math.round(r.width), h: Math.round(r.height), fillPct: Math.round(fill * 100), pctOfViewport: Math.round((area / (vw * vh)) * 100) });
67
+ }
68
+ deadSpace.sort((a, b) => b.pctOfViewport - a.pctOfViewport);
69
+
70
+ // 4. Spacing scale drift (D3): gaps/paddings/margins observed
71
+ const spacing = {};
72
+ for (const e of all.slice(0, 3000)) {
73
+ const cs = getComputedStyle(e);
74
+ for (const p of ['paddingTop', 'paddingLeft', 'marginTop', 'rowGap', 'columnGap']) {
75
+ const v = parseFloat(cs[p]);
76
+ if (v > 0 && v < 200) spacing[Math.round(v)] = (spacing[Math.round(v)] || 0) + 1;
77
+ }
78
+ }
79
+ const values = Object.keys(spacing).map(Number).sort((a, b) => a - b);
80
+ const offScale = values.filter((v) => v % 4 !== 0 && v > 2);
81
+ const nearDupes = values.filter((v, i) => i && v - values[i - 1] <= 2 && v > 4).map((v, i) => [values[values.indexOf(v) - 1], v]);
82
+
83
+ // 5. Type scale (H3)
84
+ const sizes = {};
85
+ for (const e of all) {
86
+ if (![...e.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim())) continue;
87
+ const cs = getComputedStyle(e);
88
+ const key = `${Math.round(parseFloat(cs.fontSize))}px/${cs.fontWeight}`;
89
+ sizes[key] = (sizes[key] || 0) + 1;
90
+ }
91
+ const distinctFontSizes = [...new Set(Object.keys(sizes).map((k) => k.split('/')[0]))];
92
+
93
+ // 6. Left-edge alignment within regions (L4/H6): near-miss left edges
94
+ const edges = all.filter((e) => e.matches('h1,h2,h3,h4,p,label,button,input,section,article,header')).map((e) => Math.round(e.getBoundingClientRect().left));
95
+ const edgeCounts = {}; edges.forEach((x) => (edgeCounts[x] = (edgeCounts[x] || 0) + 1));
96
+ const edgeVals = Object.keys(edgeCounts).map(Number).sort((a, b) => a - b);
97
+ const nearMissEdges = edgeVals.filter((x, i) => i && x - edgeVals[i - 1] > 0 && x - edgeVals[i - 1] <= 3).map((x) => [edgeVals[edgeVals.indexOf(x) - 1], x]);
98
+
99
+ // 7. Small targets (I4)
100
+ const smallTargets = all
101
+ .filter((e) => e.matches('button, a[href], [role=button], input:not([type=hidden]), select, [tabindex]:not([tabindex="-1"])'))
102
+ .map((e) => ({ el: sig(e), r: e.getBoundingClientRect() }))
103
+ .filter(({ r }) => r.width < 24 || r.height < 24)
104
+ .slice(0, 30)
105
+ .map(({ el, r }) => ({ el, w: Math.round(r.width), h: Math.round(r.height) }));
106
+
107
+ // 8. Low contrast text (A1), approximate: walk up to the first opaque background
108
+ const rgb = (s) => (s.match(/[\d.]+/g) || []).map(Number);
109
+ const lum = ([r, g, b]) => { const f = (c) => { c /= 255; return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; }; return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b); };
110
+ const bgOf = (e) => { for (let n = e; n; n = n.parentElement) { const c = rgb(getComputedStyle(n).backgroundColor); if (c.length >= 3 && (c[3] === undefined || c[3] > 0.9)) return c; } return [255, 255, 255]; };
111
+ const lowContrast = [];
112
+ for (const e of all) {
113
+ if (![...e.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim())) continue;
114
+ const cs = getComputedStyle(e);
115
+ const fg = rgb(cs.color); if (fg.length < 3) continue;
116
+ const L1 = lum(fg), L2 = lum(bgOf(e));
117
+ const ratio = (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
118
+ const large = parseFloat(cs.fontSize) >= 24 || (parseFloat(cs.fontSize) >= 18.66 && +cs.fontWeight >= 700);
119
+ if (ratio < (large ? 3 : 4.5)) lowContrast.push({ el: sig(e), ratio: +ratio.toFixed(2) });
120
+ if (lowContrast.length >= 30) break;
121
+ }
122
+
123
+ // 9. Overlaps between interactive elements (I1/L5)
124
+ const inter = all.filter((e) => e.matches('button, a[href], input, textarea, select, [role=button]')).slice(0, 300).map((e) => ({ e, r: e.getBoundingClientRect() }));
125
+ const overlaps = [];
126
+ for (let i = 0; i < inter.length && overlaps.length < 20; i++)
127
+ for (let j = i + 1; j < inter.length; j++) {
128
+ const a = inter[i], b = inter[j];
129
+ if (a.e.contains(b.e) || b.e.contains(a.e)) continue;
130
+ const w = Math.min(a.r.right, b.r.right) - Math.max(a.r.left, b.r.left);
131
+ const h = Math.min(a.r.bottom, b.r.bottom) - Math.max(a.r.top, b.r.top);
132
+ if (w > 4 && h > 4) { overlaps.push({ a: sig(a.e), b: sig(b.e), w: Math.round(w), h: Math.round(h) }); break; }
133
+ }
134
+
135
+ // 10. Fixed/sticky overlays that may cover content (e.g. third-party badges)
136
+ const fixedOverlays = all.filter((e) => /fixed|sticky/.test(getComputedStyle(e).position)).slice(0, 15).map((e) => { const r = e.getBoundingClientRect(); return { el: sig(e), x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) }; });
137
+
138
+ return JSON.stringify({
139
+ viewport, offscreenPrimary, scrollers, deadSpace: deadSpace.slice(0, 10),
140
+ spacing: { observed: values, offScale, nearDupes: nearDupes.slice(0, 10) },
141
+ type: { distinctFontSizes, sizes },
142
+ alignment: { nearMissEdges: nearMissEdges.slice(0, 15) },
143
+ smallTargets, lowContrast, overlaps, fixedOverlays,
144
+ }, null, 1);
145
+ })();
@@ -0,0 +1,70 @@
1
+ # Solution scoring rubric and decision template
2
+
3
+ You score the proposals yourself, one lens at a time. Do each lens as a separate pass
4
+ over all three proposals and write the scores down before moving to the next lens, so
5
+ the lenses stay independent.
6
+
7
+ ## Lenses
8
+ - **Usability**: a first-time user doing the core task. Speed, discoverability,
9
+ error-proneness, clarity of state and labels.
10
+ - **Visual craft**: hierarchy, rhythm, density, alignment, consistency with the design
11
+ system and the rest of the app.
12
+ - **Engineering**: scope, risk, reuse of existing components and tokens, ease of
13
+ testing, effect on other surfaces.
14
+
15
+ ## Criteria (score 1 to 5; weight in brackets)
16
+ 1. Meets every acceptance criterion of the cluster, measurably [x3]
17
+ 2. Simplicity: fewer elements, steps and states for the user [x2]
18
+ 3. Consistency: reuses system patterns; one concept, one look [x2]
19
+ 4. Usability: task speed, error prevention, accessibility [x2]
20
+ 5. Visual quality: hierarchy, density, whitespace, alignment [x2]
21
+ 6. Effort and risk (5 = small and safe) [x1]
22
+
23
+ A proposal that fails criterion 1 is out, unless all fail it. Then name the graft that
24
+ makes the best one pass.
25
+
26
+ You may graft one concrete element from a runner-up into the winner if it improves the
27
+ winner without breaking its idea.
28
+
29
+ ## Vision critique of mocks (optional)
30
+
31
+ If a proposal has a mock screenshot, make one `look_at_image` call per mock with a
32
+ critique question, for example: "This is a proposed layout for <purpose>. Name any
33
+ element that is unclear, crowded, misaligned or low contrast. Which element is the
34
+ primary action? What would a first-time user do first?" At most 3 calls per cluster, one
35
+ per mock. Treat the answer as one more opinion, not a vote. A vision model cannot click or
36
+ measure, so verify its claims in the mock with DOM measurements before using them. If it
37
+ rejects your chosen option, the decision document must answer its reasons.
38
+
39
+ ## Decision document (decisions/{cluster}.md)
40
+
41
+ ```markdown
42
+ # {cluster-id}: {title}
43
+
44
+ ## Problem
45
+ {root cause, findings covered, evidence links, acceptance criteria}
46
+
47
+ ## Options considered
48
+ | Option | Idea | Weighted score (Usability / Visual / Engineering) |
49
+ |--------|------|---------------------------------------------------|
50
+ | A Minimal | ... | 34 / 31 / 38 |
51
+ | B Restructure | ... | 41 / 44 / 33 |
52
+ | C Convention | ... | 39 / 40 / 35 |
53
+
54
+ (The numbers above only show the layout. Use your real scores.)
55
+
56
+ ## Decision
57
+ {the chosen option, plus grafts, stated precisely enough to implement}
58
+
59
+ ## Why this is the best
60
+ - Against A: ...
61
+ - Against C: ...
62
+ - Norms satisfied: L1, L3, H1 ...
63
+ - Costs and what it rules out: ...
64
+
65
+ ## Implementation plan
66
+ {design-system changes first (tokens, CSS, components, catalog), then pages; files}
67
+
68
+ ## Test plan
69
+ {failing-first tests: layout assertions, e2e, unit; viewports; states}
70
+ ```
@@ -0,0 +1,102 @@
1
+ # Visual & Interaction Heuristics — the norms battery
2
+
3
+ Read this before inspecting, proposing or judging. Every finding names the norm
4
+ it violates (use the IDs). "It's the default" or "it's an edge case" is never a
5
+ defense.
6
+
7
+ ## L — Layout & viewport fit
8
+ - **L1 One screen for app surfaces.** Tools, editors, rooms, dashboards and
9
+ consoles fit the viewport at ≥ 1280×800 with no page scroll; long content
10
+ scrolls inside its panel. Measured: `layout-audit.js → viewport.pageScrollY`.
11
+ - **L2 Primary action always visible.** The main action of a screen (Send, Save,
12
+ Create) is inside the viewport without scrolling at every target viewport.
13
+ - **L3 Nothing grows the page by accumulation.** Repeated events (notifications,
14
+ approvals, toasts, logs) must not push layout; they dock, replace, collapse,
15
+ or scroll inside a container.
16
+ - **L4 One grid.** Regions align to shared edges; outer gutters equal; no
17
+ full-bleed panel beside inset panels.
18
+ - **L5 Stable layout.** No layout shift when data loads, states change, or
19
+ hover toolbars appear (reserve space or overlay).
20
+ - **L6 Reflow, not overflow.** No horizontal scroll at any viewport ≥ 320px;
21
+ components reflow (stack, wrap, collapse to menus) at breakpoints.
22
+ - **L7 Predictable regions.** Navigation where users expect it (top/left), the
23
+ primary work area largest, secondary info at the side, actions near what they
24
+ act on.
25
+
26
+ ## D — Density & whitespace
27
+ - **D1 Density fits the surface.** Tools/dashboards: dense (body 13–14px,
28
+ row height 28–36px, tight groups). Content/reading: comfortable (16px+,
29
+ line-height 1.5–1.7, measure 45–80ch). Marketing: airy, generous sections.
30
+ - **D2 Whitespace is intentional.** No dead bands (large empty areas that carry
31
+ no meaning, e.g. a mostly-empty panel reserving 40% of the screen), no cramped
32
+ clusters. Empty states are compact on tool surfaces.
33
+ - **D3 Spacing scale.** All gaps/paddings come from the design system's scale
34
+ (4/8-based); measured values off-scale or near-duplicates (e.g. 14 and 16
35
+ side by side) are findings.
36
+ - **D4 Proximity encodes grouping.** Space within a group < space between
37
+ groups (Gestalt proximity), consistently.
38
+ - **D5 Information per pixel.** Repeated chrome (avatars, names, timestamps,
39
+ labels) is collapsed where repetition carries no new information (e.g.
40
+ grouped chat messages).
41
+
42
+ ## H — Hierarchy & visual design
43
+ - **H1 One focal point and one primary action per view.** Exactly one
44
+ high-emphasis button per region; others secondary/tertiary.
45
+ - **H2 Weight matches importance.** Size, color, contrast and position follow
46
+ the task's priority; destructive actions are distinct but not dominant.
47
+ - **H3 Type scale.** A small set of sizes/weights from the system; headings
48
+ clearly stepped; no more than ~5 distinct sizes on a screen.
49
+ - **H4 Color with purpose.** Accent reserved for primary/active/live states;
50
+ semantic colors only for their meaning; not one accent smeared everywhere.
51
+ - **H5 Consistency.** Same thing, same look, same place, same name across pages.
52
+ - **H6 Alignment.** Text baselines and left edges align within a group; icons
53
+ centered with their labels; numeric columns right-aligned.
54
+
55
+ ## C — Clarity & content
56
+ - **C1 Plain labels.** Verbs for actions, nouns for places; no jargon the
57
+ persona lacks; one concept, one name (no driver/control/lock drift).
58
+ - **C2 No duplication.** The same fact appears once per screen unless a new
59
+ context needs it.
60
+ - **C3 Status is legible.** System state (working, waiting, error, offline) in
61
+ one predictable place, in words plus color (never color alone).
62
+ - **C4 Microcopy earns its place.** Hint text that repeats the placeholder or
63
+ the obvious is removed; truncation shows full text on hover/focus.
64
+
65
+ ## I — Interaction & usability
66
+ - **I1 Affordance.** Clickable things look clickable; non-clickable things
67
+ don't; links vs buttons used correctly.
68
+ - **I2 Feedback within 100ms**, progress for >1s, completion/failure stated.
69
+ - **I3 Error prevention and recovery.** Destructive actions confirm or undo;
70
+ errors say what happened and what to do; inputs keep their values on error.
71
+ - **I4 Fitts.** Targets ≥ 24×24px (≥ 44×44 on touch); frequent targets large
72
+ and near the work; related actions adjacent.
73
+ - **I5 Hick.** Few choices at once; progressive disclosure for advanced
74
+ options; defaults carry most decisions.
75
+ - **I6 Recognition over recall.** Visible options, remembered preferences,
76
+ clear current location (active tab/nav).
77
+ - **I7 Keyboard and focus.** Logical tab order, visible focus rings, Escape
78
+ closes overlays, Enter submits, focus returns after dialogs; hover-only UI
79
+ reachable by keyboard and touch.
80
+ - **I8 States complete.** Every component has hover, focus, active, disabled
81
+ (with reason), loading, empty, error, and long-content states designed.
82
+
83
+ ## A — Accessibility baseline
84
+ - **A1 Contrast** ≥ 4.5:1 text, ≥ 3:1 large text and UI boundaries/icons.
85
+ - **A2 Names.** Every control has an accessible name; icon-only buttons have
86
+ labels/tooltips.
87
+ - **A3 Motion.** Respects `prefers-reduced-motion`; no essential info only in
88
+ animation.
89
+ - **A4 Zoom.** Usable at 200% zoom without loss of content.
90
+
91
+ ## P — Platform & category conventions
92
+ - **P1 Follow the category's best-in-class patterns** (e.g. chat: grouped
93
+ messages, composer pinned bottom, inline media; code tools: tabs for views,
94
+ status bar, command palette; streaming: player-first layout, chat column,
95
+ channel bar) unless a deliberate improvement is documented.
96
+ - **P2 Familiar controls** for familiar jobs (segmented controls for 2–4
97
+ exclusive views, tabs for peer views, menus for overflow, toggles for
98
+ instant settings, checkboxes for deferred settings).
99
+
100
+ ## Severity guide
101
+ P0 blocks/misleads · P1 a human stumbles (L1, L2, L3, I3, C1 failures usually
102
+ land here) · P2 visibly below excellent (D3, H6, D5) · P3 polish.
@@ -0,0 +1,29 @@
1
+ # UX Visual Report — {DATE}
2
+
3
+ ## Summary
4
+ | Metric | Value |
5
+ |--------|-------|
6
+ | Surfaces audited | {pages} pages × {states} states × {viewports} viewports |
7
+ | Coverage gaps | {list or "none"} |
8
+ | Findings | P0 {n} · P1 {n} · P2 {n} · P3 {n} |
9
+ | By level | component {n} · group {n} · page {n} · layout {n} |
10
+ | Clusters | {total} found · {implemented} implemented · {deferred} deferred |
11
+
12
+ ## Implemented
13
+ ### {cluster-id}: {title}
14
+ - **Problem**: {one line + key metric before, e.g. "page scrolls 412px at 1440x900"}
15
+ - **Decision**: {one line} — chosen over {alternatives} because {reason}
16
+ - **Result**: {metric after, criteria pass/fail}
17
+ - **Before / after**: `pages/{slug}/shots/...png` → `verification/{cluster}/...png`
18
+ - **Tests**: {specs added/updated, results}
19
+ - **Commit**: {sha} on {branch}
20
+
21
+ ## Deferred
22
+ | Cluster | Severity | Why deferred | Issue |
23
+ |---------|----------|--------------|-------|
24
+
25
+ ## Still failing / unverified
26
+ - ...
27
+
28
+ ## Excellent as is
29
+ - {surfaces that passed clean, so re-runs can skip them}