@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 +23 -0
- package/README.md +4 -1
- package/extensions/index.js +27 -3
- package/package.json +1 -1
- package/skills/ux-capture/SKILL.md +54 -7
- package/skills/ux-design/SKILL.md +3 -2
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
|
|
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. |
|
package/extensions/index.js
CHANGED
|
@@ -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,
|
|
184
|
-
const 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.
|
|
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**:
|
|
16
|
-
|
|
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)
|
|
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
|
-
##
|
|
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
|
-
|
|
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.
|
|
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;
|
|
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. **
|
|
140
|
-
5. **
|
|
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
|
|