free-pi-cli 0.6.2 → 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.
- package/dist/index.js +1 -1
- package/dist/{main-cwwgr8bh.js → main-2mvzsazb.js} +377 -65
- package/package.json +1 -1
- package/skills/ux-flow/SKILL.md +200 -0
- package/skills/ux-flow/references/simplification-heuristics.md +103 -0
- package/skills/ux-paths/SKILL.md +196 -0
- package/skills/ux-paths/references/path-categories.md +169 -0
- package/skills/ux-visual/SKILL.md +252 -0
- package/skills/ux-visual/references/layout-audit.js +145 -0
- package/skills/ux-visual/references/solution-rubric.md +70 -0
- package/skills/ux-visual/references/visual-heuristics.md +102 -0
- package/skills/ux-visual/templates/report-template.md +29 -0
- package/skills/ux-walker/SKILL.md +251 -0
- package/skills/ux-walker/references/action-patterns.md +75 -0
- package/skills/ux-walker/references/geometry-audit.js +82 -0
- package/skills/ux-walker/references/issue-template.md +110 -0
- package/skills/ux-walker/references/triage-rubric.md +55 -0
- package/skills/ux-walker/references/ux-audit-rubric.md +174 -0
- package/skills/ux-walker/references/visual-inspection.md +92 -0
- package/skills/ux-walker/templates/latest-report-template.md +139 -0
|
@@ -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}
|