@bacnh85/pi-ux 0.5.0 → 0.6.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 (2026-09-17)
4
+
5
+ ### Added
6
+
7
+ - **`ux_audit` accepts `path`** — audit a CSS file verbatim (absolute or
8
+ cwd-relative) instead of retyping it into the `css` string. Incident-driven:
9
+ retyped/condensed CSS drifted (inlined DESIGN.md shadow values, mislabeled
10
+ colour pairs) and produced false gate failures and false confidence.
11
+ Exactly one of `path`/`css`; the tool guidance now leads with
12
+ "NEVER retype CSS when the file is on disk".
13
+
14
+ ### Changed
15
+
16
+ - **ux-capture skill**: new Interaction section — `web_interact` (pi-web
17
+ ≥0.16.0) as the default path for behavior verification, with the manual CDP
18
+ recipe (trusted input, double-unwrap, clipboard activation caveats) as
19
+ fallback; honest sub-500px capture + `reduced_motion` documented as native
20
+ pi-web behavior; the iframe-wrapper layout probe demoted to a manual-capture
21
+ fallback.
22
+ - **ux-design skill** Step 4: render-and-inspect loop gains "Interact &
23
+ verify" — click the primary CTA, submit the form, read back state with
24
+ `web_interact` before the audit gate.
25
+
3
26
  ## 0.5.0 (2026-09-16)
4
27
 
5
28
  ### Added
package/README.md CHANGED
@@ -60,9 +60,12 @@ The **Direction playbook** ships in the skill as the positive layer: a typograph
60
60
  Deterministic slop-audit — no model needed, all gates are computable:
61
61
 
62
62
  ```
63
- ux_audit css="..." pairs=[{fg:"#111",bg:"#fff",label:"body",weight:400,size:16,min:4.5}]
63
+ ux_audit path="web/src/app.css" pairs=[{fg:"#111",bg:"#fff",label:"body",weight:400,size:16,min:4.5}]
64
+ ux_audit css="..." pairs=[...]
64
65
  ```
65
66
 
67
+ Pass `path` to a stylesheet file — it is audited **verbatim**. Never retype or condense CSS into the `css` string when the file is on disk: retyped copies drift (inlined tokens, mislabeled pairs) and cause false gate failures or false confidence. Exactly one of `path`/`css`.
68
+
66
69
  | Gate | What it checks |
67
70
  |------|----------------|
68
71
  | **Contrast (APCA)** | Perceptual APCA Lc per fg/bg pair (Lc ≥75 body, ≥45 large-bold, ≥30 non-text). hex or `oklch()`. Optional `weight`/`size` set the threshold. WCAG 2.x ratio shown as a compliance sidecar. |
@@ -1,4 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
2
4
 
3
5
  const require = createRequire(import.meta.url);
4
6
  const {
@@ -67,9 +69,13 @@ function auditParametersSchema() {
67
69
  type: "object",
68
70
  additionalProperties: false,
69
71
  properties: {
72
+ path: {
73
+ type: "string",
74
+ description: "Path to a CSS stylesheet file to audit verbatim (absolute or cwd-relative). PREFERRED over retyping `css` — retyped copies drift (inlined tokens, mislabeled pairs) and cause false audit failures. Exactly one of path/css.",
75
+ },
70
76
  css: {
71
77
  type: "string",
72
- description: "CSS stylesheet content to audit (inline stylesheets, styled-components output, or a concatenated .css file).",
78
+ description: "CSS stylesheet content to audit (inline stylesheets, styled-components output, or a concatenated .css file). Prefer `path` for on-disk files. Exactly one of path/css.",
73
79
  },
74
80
  pairs: {
75
81
  type: "array",
@@ -92,6 +98,23 @@ function auditParametersSchema() {
92
98
  };
93
99
  }
94
100
 
101
+ /**
102
+ * Resolve the stylesheet to audit: a file `path` read verbatim (preferred —
103
+ * retyped `css` drifts and causes false gate failures) or inline `css`.
104
+ * Exactly one of the two.
105
+ */
106
+ export function resolveAuditCss(params, cwd) {
107
+ const css = typeof params.css === "string" ? params.css : "";
108
+ const hasCss = css.trim().length > 0;
109
+ const p = typeof params.path === "string" ? params.path.trim() : "";
110
+ if (hasCss && p) throw new Error("Pass exactly one of `path` or `css` — not both.");
111
+ if (!hasCss && !p) throw new Error("Pass a stylesheet to audit: `path` (preferred, read verbatim) or `css`.");
112
+ // Relative paths resolve against the tool-call cwd (falls back to the
113
+ // process cwd) — the session cwd can differ from this process's cwd.
114
+ if (p) return fs.readFileSync(path.resolve(cwd || process.cwd(), p), "utf8");
115
+ return css;
116
+ }
117
+
95
118
  export function formatAuditResult(result) {
96
119
  const lines = [];
97
120
  lines.push(result.pass ? "✅ UX AUDIT PASSED" : "❌ UX AUDIT FAILED");
@@ -173,6 +196,7 @@ export default function uxExtension(pi) {
173
196
  "Run deterministic slop-audit gates on CSS: APCA contrast (perceptual; WCAG sidecar), off-system token values (hardcoded hex / ad-hoc shadows), missing interaction states (:focus-visible / :disabled + prefers-reduced-motion), and named AI slop tells (glassmorphism, gradient orbs, neon glow, default-card, tracked-out eyebrows, tinted near-black). No model needed — all gates are computable. In strict mode, handoff is blocked until this passes. AUDIT THE COMPLETE STYLESHEET, not fragments. If no contrast pairs are supplied, they are auto-extracted from rules that declare both colour and background.",
174
197
  promptSnippet: "Run deterministic UX slop-audit (APCA contrast + tokens + states + slop tells)",
175
198
  promptGuidelines: [
199
+ "Pass `path` to the stylesheet file — it is audited verbatim. NEVER retype or condense CSS into the `css` string when the file is on disk: retyped copies drift (inlined DESIGN.md shadow values, mislabeled pairs) and produce false failures or false confidence.",
176
200
  "Contrast, token-coverage, and slop-tells are computable, not judgement — use this tool instead of eyeballing or calling a vision model.",
177
201
  "Pass fg/bg colour pairs (hex or oklch()) + optional weight/size to set the APCA threshold; the WCAG ratio is shown as a compliance sidecar. Omit pairs and they are auto-extracted from colour+background rules — but hand-picking catches text-on-inherited-backgrounds that auto-extraction misses.",
178
202
  "Audit the COMPLETE stylesheet — fragment input falsely fails the States gate (no interactive selectors present) and misses off-system values elsewhere.",
@@ -180,8 +204,8 @@ export default function uxExtension(pi) {
180
204
  "State coverage flags interactive elements (button/a/input/...) missing :focus-visible or :disabled rules.",
181
205
  ],
182
206
  parameters: auditParametersSchema(),
183
- async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
184
- const css = typeof params.css === "string" ? params.css : "";
207
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
208
+ const css = resolveAuditCss(params, ctx?.cwd);
185
209
  const pairs = Array.isArray(params.pairs) ? params.pairs : [];
186
210
  const result = audit({ css, pairs });
187
211
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-ux",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Anti-slop UI/UX design discipline for your Pi agent — anchors a lintable DESIGN.md, derives a design direction (mood, type voice, color mood, signature), runs deterministic slop-audit gates (APCA contrast + tokens + states + slop tells), works with text-only models, ships reference design-system presets.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -12,8 +12,9 @@ description: >
12
12
 
13
13
  Judge captures at viewer resolution (1×–3×); never chase sub-visible precision.
14
14
 
15
- **Disable entrance animations when capturing**: add
16
- `--force-prefers-reduced-motion` to headless Chrome (or emulate the media
15
+ **Disable entrance animations when capturing**: pass `reduced_motion=true` to
16
+ `web_screenshot` / `web_interact` (pi-web ≥0.16.0), or add
17
+ `--force-prefers-reduced-motion` to manual headless Chrome (or emulate the media
17
18
  query). Pages rightly use staggered page-load reveals with `opacity:0`
18
19
  backwards-fill — captured mid-animation they screenshot as blank sections,
19
20
  and you will "fix" content that isn't broken. The same forced query doubles
@@ -22,15 +23,56 @@ fully visible and readable.
22
23
 
23
24
  **Capture at the brief's target viewport.** Web pages: 1280–1440 wide. App
24
25
  screens and mobile-first briefs: the width the brief names (usually 390) at
25
- its target height (~844) but know your tooling: many headless Chrome builds
26
+ its target height (~844). pi-web handles this honestly now: `web_screenshot`
27
+ and `web_interact` with `width`/`viewport` below 500px automatically use CDP
28
+ device-metrics emulation (true 390px CSS viewport) and return a
29
+ `scrollWidth`/`innerWidth` probe — `scrollWidth > width` means overflowing CSS.
30
+ If you fall back to manual headless Chrome, know its trap: many builds
26
31
  **clamp window width to 500px**, so a "390 capture" secretly renders at 500
27
32
  and crops (see the layout probe below for the wrapper that does it honestly).
28
33
  If content overflows or dead-ends at the target size, the page is broken —
29
34
  **fix the page. Never widen the viewport to make a problem invisible.**
30
35
 
31
- ## Layout probe (required for fixed-viewport briefs)
36
+ ## Interaction web_interact (pi-web ≥0.16.0)
37
+
38
+ A screenshot proves the page LOOKS right; only interaction proves it WORKS.
39
+ After visual inspection, verify behavior with `web_interact` — one call = one
40
+ browser lifecycle: open `url`, run `steps` in order, get per-step results, a
41
+ final inline PNG, and a scrollWidth probe.
42
+
43
+ ```text
44
+ web_interact url="http://localhost:5173" viewport={width:390,height:844} \
45
+ reduced_motion=true grant=["clipboard-read","clipboard-write"] steps=[
46
+ {click: "#copy-btn"},
47
+ {evaluate: "document.getElementById('status').textContent", label: "status"},
48
+ {type: {selector: "#email", text: "a@b.co"}},
49
+ {press: "Enter"},
50
+ {wait_for: "[data-success]"}
51
+ ```
32
52
 
33
- Two Chrome facts make naive mobile checks lie:
53
+ - **Trusted clicks**: steps click via CDP `Input.dispatchMouseEvent` at the
54
+ element's center — synthetic `el.click()` grants no user activation, so
55
+ `document.execCommand('copy')` and login/clipboard flows would silently fail
56
+ under it. Under a trusted click, copy returns true.
57
+ - **evaluate is double-unwrapped**: `Runtime.evaluate` nests the value at
58
+ `{result:{result:{value}}}` — the tool returns the real value; if you ever
59
+ hand-roll CDP, single-unwrapping yields `undefined` and makes the app LOOK
60
+ broken when it isn't.
61
+ - Steps stop at the first failure with the reason — a broken selector surfaces
62
+ loudly instead of no-op'ing later steps.
63
+ - Clipboard readback on insecure origins: there is no clipboard API to read
64
+ back with; `execCommand` returning true under a trusted click is the
65
+ strongest available signal (grant permissions for secure origins).
66
+
67
+ Manual CDP (fallback only, when web_interact is unavailable): launch Chrome
68
+ with `--remote-debugging-port=0`, read `<profile>/DevToolsActivePort` for the
69
+ ws URL, create targets over the websocket (`Target.createTarget` — not the
70
+ `/json/new` HTTP endpoint, whose method flipped to PUT), attach with
71
+ `flatten: true`.
72
+
73
+ ## Layout probe (fallback for manual captures)
74
+
75
+ Needed only when CDP tooling above is unavailable. Two Chrome facts make naive mobile checks lie:
34
76
 
35
77
  1. **Headless Chrome clamps window width to 500px.** A `--window-size=390`
36
78
  capture renders the page at 500px and crops the PNG to 390 — cuts at the
@@ -84,17 +126,22 @@ probe): `npm i puppeteer-core` once, then launch with
84
126
  Probe with `document.documentElement.scrollWidth` via `page.evaluate` before
85
127
  screenshotting; `page.screenshot({ fullPage: true })` for the tall capture.
86
128
 
87
- ## Default — web_screenshot (pi-web ≥0.7.0, auto local detection)
129
+ ## Default — web_screenshot (pi-web ≥0.16.0, auto local detection)
88
130
 
89
131
  `web_screenshot` auto-routes localhost/LAN/file URLs to the locally installed
90
132
  headless Chrome and returns the PNG inline — no daemon, no manual commands:
91
133
 
92
134
  - `web_screenshot url="http://localhost:PORT" width=390 height=844` — the
93
- model sees the render; pass `width`/`height` explicitly for mobile targets.
135
+ model sees the render; below 500px the capture is CDP device-emulated
136
+ (honest viewport, no clamp) and includes the scrollWidth probe.
137
+ - `reduced_motion=true` disables entrance animations for the shot.
94
138
  - `full_page=true` captures a tall 8000px window; `wait_for` settles JS via
95
139
  `--virtual-time-budget`; `engine="local"` forces local on a public URL.
96
140
  - `web_pdf` works the same way (`--print-to-pdf`) for full-content archival.
97
141
  - If Chrome is missing: `web_status` shows `localChrome.path`; set `CHROME_PATH`.
142
+ - Empty replies / connection resets from a dev-server URL usually mean a
143
+ STALE HUNG server on the port (accepts TCP, returns nothing) — `lsof -ti
144
+ :PORT` and kill it before diagnosing the tools.
98
145
 
99
146
  ## Fallback — manual headless Chrome (pi-web <0.7.0 or if the tool errors)
100
147
 
@@ -136,8 +136,9 @@ even when `ux_audit` passes. (Text-only models, or genuinely no capture path
136
136
  - **Timidity:** cover the logo — could this page belong to anyone? Then amplify the display scale or the signature; the direction is not coming through.
137
137
  - **Type & overflow at target width:** display sizes actually large? measure comfortable? **any horizontal scroll, cut-off text, or squeezed badges at the target width?** orphans, cramped labels?
138
138
  - **Mood:** is the palette's temperature visible at a glance, or is it generic white+blue?
139
- 4. **Visibility baseline.** Judge at what a viewer sees at 1×–3×. Nothing sub-visible can fail, and nothing sub-visible may be produced no ±1px claims, no per-pixel diffs, no instrument-read values on either side.
140
- 5. **Gates stay final.** `ux_audit` (Step 5) remains the blocking authority; vision settles only what looking can settle.
139
+ 4. **Interact & verify** a screenshot proves looks, not behavior. Drive the page with `web_interact` (pi-web ≥0.16.0): click the primary CTA, submit the form, toggle a control, and read back state steps stop at the first failure with the reason. Trusted CDP clicks grant user activation, so clipboard/login flows behave for real.
140
+ 5. **Visibility baseline.** Judge at what a viewer sees at 1×–3×. Nothing sub-visible can fail, and nothing sub-visible may be produced — no ±1px claims, no per-pixel diffs, no instrument-read values on either side.
141
+ 6. **Gates stay final.** `ux_audit` (Step 5) remains the blocking authority; vision settles only what looking can settle.
141
142
 
142
143
  ### Step 5 — Slop-audit gate (blocks handoff on fail)
143
144