@bacnh85/pi-ux 0.3.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/AGENTS.md +29 -0
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/extensions/index.js +252 -0
- package/extensions/package.json +3 -0
- package/hooks/ux-audit.js +162 -0
- package/hooks/ux-config.js +108 -0
- package/hooks/ux-instructions.js +42 -0
- package/package.json +43 -0
- package/skills/ux-design/SKILL.md +93 -0
- package/skills/ux-presets/SKILL.md +110 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# pi-ux
|
|
2
|
+
|
|
3
|
+
Anti-slop UI/UX design discipline extension. Forks the pi-ponytail shape
|
|
4
|
+
(hook + command + skill) for design discipline instead of code laziness.
|
|
5
|
+
|
|
6
|
+
Before writing any code here, stop at the first rung that holds:
|
|
7
|
+
|
|
8
|
+
1. Does this need to be built at all? (YAGNI)
|
|
9
|
+
2. Does it already exist in this codebase? Reuse it.
|
|
10
|
+
3. Does the standard library already do this? Use it.
|
|
11
|
+
4. Can it be one line? Make it one line.
|
|
12
|
+
5. Only then: write the minimum code that works.
|
|
13
|
+
|
|
14
|
+
This is a **discipline enforcer**, not a generator. Generation stays with
|
|
15
|
+
`agy_execute` (Gemini/Claude) and the main Pi models (DeepSeek-v4, GLM-5.2).
|
|
16
|
+
pi-ux only makes sure their output lands inside a defensible design system.
|
|
17
|
+
|
|
18
|
+
## What NOT to build here
|
|
19
|
+
|
|
20
|
+
- ❌ A design-token generator (use agy/Gemini or reuse shadcn/ui).
|
|
21
|
+
- ❌ A bundled CSS framework (reference only).
|
|
22
|
+
- ❌ A UI renderer (that's agy + the main model's job).
|
|
23
|
+
- ❌ TypeScript (plain JS like ponytail — zero deps).
|
|
24
|
+
|
|
25
|
+
## Test
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm test # node --test extensions/test/*.test.js
|
|
29
|
+
```
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- **`ux_audit` tool (deterministic slop-audit gate).** New LLM-callable tool
|
|
6
|
+
that runs three computable gates — no model needed:
|
|
7
|
+
- **Contrast**: WCAG 2.x relative-luminance ratio for fg/bg colour pairs
|
|
8
|
+
(4.5:1 body, 3:1 large/UI).
|
|
9
|
+
- **Tokens**: flags hardcoded hex outside `:root` token defs and `box-shadow`
|
|
10
|
+
declarations not built from `var(--…)` tokens.
|
|
11
|
+
- **States**: flags interactive selectors (`button`/`a`/`input`/…/`[role=button]`)
|
|
12
|
+
missing `:focus-visible` or `:disabled` rules.
|
|
13
|
+
- Returns a pass/fail per gate + a formatted report. In `strict` mode this is
|
|
14
|
+
the gate that blocks handoff.
|
|
15
|
+
- **`ux-presets` skill.** Reference design-system presets for Step 0 (Own the
|
|
16
|
+
system) of the ux-design method — no bundled CSS. Option A: reuse
|
|
17
|
+
shadcn/ui, Material 3, Radix, or Park/Ark UI (YAGNI-first). Option B: a
|
|
18
|
+
compact ~20-line token set (one accent + neutrals, modular type scale, 8px
|
|
19
|
+
spacing grid, 4 named elevation levels) plus audit-ready colour pairs.
|
|
20
|
+
- Zero dependencies retained (WCAG math is 10 lines; token scan is regex;
|
|
21
|
+
state scan is substring checks). Plain JS, `node --test`.
|
|
22
|
+
|
|
23
|
+
## 0.1.0
|
|
24
|
+
|
|
25
|
+
- Initial release.
|
|
26
|
+
- Anti-slop guardrail via `before_agent_start` hook (enforced, not ignorable).
|
|
27
|
+
- `/ux` command: `lite | strict | off | status | default <mode>`.
|
|
28
|
+
- `skills/ux-design/SKILL.md`: full Constraint-First method + model routing
|
|
29
|
+
(Gemini/Claude via `agy_execute`, DeepSeek-v4 / GLM-5.2 as main Pi models).
|
|
30
|
+
- Modes: `lite` (guardrail only), `strict` (guardrail + audit gate, default).
|
|
31
|
+
- Config via env (`PI_UX_DEFAULT_MODE`, `PI_UX_QUIET_STARTUP`,
|
|
32
|
+
`PI_UX_HIDE_STATUS`) and `~/.config/pi-ux/config.json`.
|
|
33
|
+
- Zero dependencies (plain JS, `node --test`).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 bacnh85
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# pi-ux
|
|
2
|
+
|
|
3
|
+
Anti-slop UI/UX design discipline for the [Pi coding agent](https://pi.dev). Enforces industrial-design principles (Dieter Rams: honest, thorough to the last detail, as little design as possible) so AI-generated interfaces are defensible systems, not statistical-default slop (purple glow, shadow-as-texture, missing states).
|
|
4
|
+
|
|
5
|
+
This is a **discipline enforcer**, not a generator. Generation stays with `agy_execute` (Gemini/Claude) and the main Pi models (DeepSeek-v4, GLM-5.2); pi-ux makes sure their output lands inside a defensible design system.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
AI-generated UI converges on the same defaults — purple/indigo glow, shadow-heavy cards, missing focus/disabled/error states — because under vague direction ("make it modern/clean"), models reach for high-frequency statistical patterns. Slop is an **ownership problem**: "the design has no owner at the system level." A skill alone is ignorable; a `before_agent_start` hook is not.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @bacnh85/pi-ux
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Pi auto-discovers the extension and skill.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
/ux # toggle / show status
|
|
23
|
+
/ux strict # guardrail + enforce ux_audit gate before handoff (default)
|
|
24
|
+
/ux lite # guardrail only (ideation, exploration)
|
|
25
|
+
/ux off # disable
|
|
26
|
+
/ux status # current + default mode
|
|
27
|
+
/ux default lite # persist default mode
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The guardrail is injected into the system prompt on every agent start while active. Turn off with `/ux off` or the phrases "stop ux" / "normal mode".
|
|
31
|
+
|
|
32
|
+
## Modes
|
|
33
|
+
|
|
34
|
+
| Mode | Behavior |
|
|
35
|
+
|------|----------|
|
|
36
|
+
| `off` | No guardrail |
|
|
37
|
+
| `lite` | Anti-slop guardrail enforced; audit gate recommended but not blocking |
|
|
38
|
+
| `strict` (default) | Guardrail + enforce `ux_audit` gate before declaring a screen done |
|
|
39
|
+
|
|
40
|
+
## The method
|
|
41
|
+
|
|
42
|
+
The injected skill enforces **Constraint-First Design Generation**:
|
|
43
|
+
|
|
44
|
+
0. **Own the system** — tokens (colour/type/spacing/radius), 3–5 named elevation levels, state contract. If absent, no generation yet — use the `ux-presets` skill to reuse shadcn/ui, Material, or Radix, or copy a compact token set.
|
|
45
|
+
1. **5-field brief per screen** — user job, inventory, token constraints, required states, one reference.
|
|
46
|
+
2. **Generate fast, converge early** — 2-loop convergence trigger, else tighten the brief.
|
|
47
|
+
3. **Normalise** the draft back into tokens/elevation/spacing.
|
|
48
|
+
4. **Slop-audit gate** — run `ux_audit` (measurable): token coverage ≥8/10, ≤3 shadow recipes, WCAG AA contrast, full state coverage.
|
|
49
|
+
|
|
50
|
+
## The `ux_audit` tool
|
|
51
|
+
|
|
52
|
+
Deterministic slop-audit — no model needed, all gates are computable:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
ux_audit css="..." pairs=[{fg:"#111",bg:"#fff",label:"body",min:4.5}]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| Gate | What it checks |
|
|
59
|
+
|------|----------------|
|
|
60
|
+
| **Contrast** | WCAG 2.x relative-luminance ratio for each fg/bg pair (4.5:1 body, 3:1 large/UI) |
|
|
61
|
+
| **Tokens** | Hardcoded hex outside `:root` token defs; `box-shadow` not built from `var(--…)` tokens |
|
|
62
|
+
| **States** | Interactive selectors (`button`/`a`/`input`/…/`[role=button]`) missing `:focus-visible` or `:disabled` |
|
|
63
|
+
|
|
64
|
+
Returns pass/fail per gate + a formatted report. In `strict` mode this is the gate that blocks handoff.
|
|
65
|
+
|
|
66
|
+
## Skills
|
|
67
|
+
|
|
68
|
+
- **`ux-design`** — the Constraint-First method + model routing (auto-injected by the hook when active).
|
|
69
|
+
- **`ux-presets`** — reference design-system presets for Step 0: reuse shadcn/ui / Material 3 / Radix, or copy a compact token set. Reference only — no bundled CSS.
|
|
70
|
+
|
|
71
|
+
The skill tells the agent which model to use for each step:
|
|
72
|
+
|
|
73
|
+
| Step | Best tool/model |
|
|
74
|
+
|------|-----------------|
|
|
75
|
+
| Define system (tokens, elevation) | `agy_execute mode=plan pro-high` (Gemini) or reuse shadcn/Material |
|
|
76
|
+
| Per-screen brief | **GLM-5.2** (200K ctx) |
|
|
77
|
+
| Generate variants | `agy_execute mode=accept-edits flash-high` (Gemini) |
|
|
78
|
+
| Normalise into system | **DeepSeek-v4** or **GLM-5.2** |
|
|
79
|
+
| Slop audit | `ux_audit` tool (deterministic) |
|
|
80
|
+
| Final review/polish | `agy_execute mode=plan sonnet` (Claude) |
|
|
81
|
+
|
|
82
|
+
**The inversion rule:** the cheaper/weaker the model, the MORE you must externalise constraints. DeepSeek/GLM are safe for design only inside a fully-specified system.
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
86
|
+
Environment variables (override config file):
|
|
87
|
+
|
|
88
|
+
| Variable | Default | Effect |
|
|
89
|
+
|----------|---------|--------|
|
|
90
|
+
| `PI_UX_DEFAULT_MODE` | `strict` | Default mode on startup |
|
|
91
|
+
| `PI_UX_QUIET_STARTUP` | unset | Suppress the startup toast |
|
|
92
|
+
| `PI_UX_HIDE_STATUS` | unset | Hide the status-bar indicator |
|
|
93
|
+
|
|
94
|
+
Config file: `~/.config/pi-ux/config.json` (or `$XDG_CONFIG_HOME/pi-ux/`):
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{ "defaultMode": "strict", "quietStartup": false, "hideStatus": false }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const {
|
|
5
|
+
DEFAULT_MODE,
|
|
6
|
+
RUNTIME_MODES,
|
|
7
|
+
getDefaultMode,
|
|
8
|
+
getQuietStartup,
|
|
9
|
+
getHideStatus,
|
|
10
|
+
normalizeMode,
|
|
11
|
+
normalizePersistedMode,
|
|
12
|
+
isDeactivationCommand,
|
|
13
|
+
writeDefaultMode,
|
|
14
|
+
} = require("../hooks/ux-config.js");
|
|
15
|
+
const { getUxInstructions } = require("../hooks/ux-instructions.js");
|
|
16
|
+
const { audit } = require("../hooks/ux-audit.js");
|
|
17
|
+
|
|
18
|
+
export const readDefaultMode = getDefaultMode;
|
|
19
|
+
export const readQuietStartup = getQuietStartup;
|
|
20
|
+
|
|
21
|
+
export function resolveSessionMode(entries, fallbackMode = DEFAULT_MODE) {
|
|
22
|
+
const fallback = normalizePersistedMode(fallbackMode) || DEFAULT_MODE;
|
|
23
|
+
if (!Array.isArray(entries)) return fallback;
|
|
24
|
+
|
|
25
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
26
|
+
const entry = entries[i];
|
|
27
|
+
if (entry?.type !== "custom" || entry?.customType !== "ux-mode") continue;
|
|
28
|
+
|
|
29
|
+
const mode = normalizePersistedMode(entry?.data?.mode);
|
|
30
|
+
if (mode) return mode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseUxCommand(text, defaultMode = DEFAULT_MODE) {
|
|
37
|
+
const fallback = normalizePersistedMode(defaultMode) || DEFAULT_MODE;
|
|
38
|
+
const normalizedText = String(text || "").trim().toLowerCase();
|
|
39
|
+
|
|
40
|
+
if (!normalizedText) {
|
|
41
|
+
return { type: "set-mode", mode: fallback === "off" ? "strict" : fallback };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const [primary, secondary] = normalizedText.split(/\s+/);
|
|
45
|
+
|
|
46
|
+
if (primary === "status") return { type: "status" };
|
|
47
|
+
|
|
48
|
+
if (primary === "default") {
|
|
49
|
+
// ponytail: a default must be a runtime level, not a session-only mode.
|
|
50
|
+
const mode = normalizeMode(secondary);
|
|
51
|
+
return mode ? { type: "set-default", mode } : { type: "invalid", reason: "invalid-default-mode" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const mode = normalizeMode(primary);
|
|
55
|
+
return mode ? { type: "set-mode", mode } : { type: "invalid", reason: "invalid-mode", mode: primary };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { writeDefaultMode };
|
|
59
|
+
|
|
60
|
+
// ponytail: plain JSON-schema object, not TypeBox — at runtime the symbols are
|
|
61
|
+
// stripped on JSON.stringify anyway, and we keep zero deps. Shape matches what
|
|
62
|
+
// Type.Object produces for the LLM tool spec.
|
|
63
|
+
function auditParametersSchema() {
|
|
64
|
+
return {
|
|
65
|
+
type: "object",
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
properties: {
|
|
68
|
+
css: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "CSS stylesheet content to audit (inline stylesheets, styled-components output, or a concatenated .css file).",
|
|
71
|
+
},
|
|
72
|
+
pairs: {
|
|
73
|
+
type: "array",
|
|
74
|
+
description: "Foreground/background colour pairs to check for WCAG contrast (fg + bg as #hex, a label, and a min ratio defaulting to 4.5).",
|
|
75
|
+
items: {
|
|
76
|
+
type: "object",
|
|
77
|
+
additionalProperties: false,
|
|
78
|
+
properties: {
|
|
79
|
+
fg: { type: "string", description: "Foreground colour, e.g. '#111111'." },
|
|
80
|
+
bg: { type: "string", description: "Background colour, e.g. '#ffffff'." },
|
|
81
|
+
label: { type: "string", description: "Human label for this text style (e.g. 'body')." },
|
|
82
|
+
min: { type: "number", description: "Minimum contrast ratio. 4.5 for body, 3.0 for large/UI. Defaults to 4.5." },
|
|
83
|
+
},
|
|
84
|
+
required: ["fg", "bg"],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function formatAuditResult(result) {
|
|
92
|
+
const lines = [];
|
|
93
|
+
lines.push(result.pass ? "✅ UX AUDIT PASSED" : "❌ UX AUDIT FAILED");
|
|
94
|
+
lines.push("");
|
|
95
|
+
|
|
96
|
+
const c = result.gates.contrast;
|
|
97
|
+
lines.push(c.pass ? "✓ Contrast" : "✗ Contrast");
|
|
98
|
+
for (const r of c.results) {
|
|
99
|
+
const ratio = r.ratio === null ? "n/a" : `${r.ratio.toFixed(2)}:1`;
|
|
100
|
+
lines.push(` ${r.pass ? "✓" : "✗"} ${r.label || `${r.fg}/${r.bg}`}: ${ratio} (min ${r.min ?? 4.5})`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const t = result.gates.tokens;
|
|
104
|
+
lines.push(t.pass ? "✓ Tokens" : "✗ Tokens");
|
|
105
|
+
for (const h of t.hardcodedHex) lines.push(` ✗ hardcoded hex: ${h}`);
|
|
106
|
+
for (const s of t.adhocShadow) lines.push(` ✗ ad-hoc box-shadow: ${s}`);
|
|
107
|
+
|
|
108
|
+
const s = result.gates.states;
|
|
109
|
+
lines.push(s.pass ? "✓ States" : "✗ States");
|
|
110
|
+
for (const m of s.missingFocusVisible) lines.push(` ✗ ${m}`);
|
|
111
|
+
for (const m of s.missingDisabled) lines.push(` ✗ ${m}`);
|
|
112
|
+
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default function uxExtension(pi) {
|
|
117
|
+
let currentMode = DEFAULT_MODE;
|
|
118
|
+
let configuredDefaultMode = getDefaultMode();
|
|
119
|
+
let hideStatus = getHideStatus();
|
|
120
|
+
|
|
121
|
+
function syncStatus(ctx) {
|
|
122
|
+
if (hideStatus) return;
|
|
123
|
+
if (!ctx?.ui?.setStatus) return;
|
|
124
|
+
// ponytail: try/catch guards against theme proxy throwing before init.
|
|
125
|
+
let theme;
|
|
126
|
+
try { theme = ctx.ui.theme; if (!theme?.fg) return; } catch { return; }
|
|
127
|
+
if (currentMode === "off") {
|
|
128
|
+
try { ctx.ui.setStatus("ux", ""); } catch { return; }
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const levelIcons = { lite: "🎨", strict: "📐" };
|
|
132
|
+
const icon = levelIcons[currentMode] || "";
|
|
133
|
+
const label = currentMode.toUpperCase();
|
|
134
|
+
try {
|
|
135
|
+
ctx.ui.setStatus("ux", " 🎨 " + theme.fg("muted", "ux: ") + theme.fg("text", icon + " " + label));
|
|
136
|
+
} catch { return; }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const setMode = (mode, ctx) => {
|
|
140
|
+
const normalized = normalizePersistedMode(mode);
|
|
141
|
+
if (!normalized) return;
|
|
142
|
+
|
|
143
|
+
pi.appendEntry("ux-mode", { mode: normalized });
|
|
144
|
+
currentMode = normalized;
|
|
145
|
+
syncStatus(ctx);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
pi.registerTool({
|
|
149
|
+
name: "ux_audit",
|
|
150
|
+
label: "UX Slop Audit",
|
|
151
|
+
description:
|
|
152
|
+
"Run deterministic slop-audit gates on CSS: WCAG contrast, off-system token values (hardcoded hex / ad-hoc shadows), and missing interaction states (:focus-visible / :disabled). No model needed — all gates are computable. In strict mode, block handoff until this passes.",
|
|
153
|
+
promptSnippet: "Run deterministic UX slop-audit (contrast + tokens + states)",
|
|
154
|
+
promptGuidelines: [
|
|
155
|
+
"Contrast and token-coverage are computable, not judgement — use this tool instead of eyeballing.",
|
|
156
|
+
"Pass fg/bg colour pairs to check WCAG AA contrast (4.5:1 body, 3:1 large/UI).",
|
|
157
|
+
"Pass the CSS string to scan for hardcoded hex and ad-hoc box-shadow not built from var() tokens.",
|
|
158
|
+
"State coverage flags interactive elements (button/a/input/...) missing :focus-visible or :disabled rules.",
|
|
159
|
+
],
|
|
160
|
+
parameters: auditParametersSchema(),
|
|
161
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
162
|
+
const css = typeof params.css === "string" ? params.css : "";
|
|
163
|
+
const pairs = Array.isArray(params.pairs) ? params.pairs : [];
|
|
164
|
+
const result = audit({ css, pairs });
|
|
165
|
+
return {
|
|
166
|
+
content: [{ type: "text", text: formatAuditResult(result) }],
|
|
167
|
+
details: result,
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
pi.registerCommand("ux", {
|
|
173
|
+
description: `Anti-slop UI/UX discipline. Modes: ${RUNTIME_MODES.join("|")}. Commands: status, default <mode>`,
|
|
174
|
+
handler: async (args, ctx) => {
|
|
175
|
+
const parsed = parseUxCommand(args, configuredDefaultMode);
|
|
176
|
+
|
|
177
|
+
if (parsed.type === "status") {
|
|
178
|
+
ctx?.ui?.notify?.(`UX: current ${currentMode} • default ${configuredDefaultMode}`, "info");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (parsed.type === "set-default") {
|
|
183
|
+
try {
|
|
184
|
+
const written = writeDefaultMode(parsed.mode);
|
|
185
|
+
if (written) {
|
|
186
|
+
configuredDefaultMode = getDefaultMode();
|
|
187
|
+
const message = configuredDefaultMode === written
|
|
188
|
+
? `Default UX mode set to ${written}.`
|
|
189
|
+
: `Saved default ${written}, but env override keeps default at ${configuredDefaultMode}.`;
|
|
190
|
+
ctx?.ui?.notify?.(message, "info");
|
|
191
|
+
} else {
|
|
192
|
+
ctx?.ui?.notify?.(`Invalid default mode. Use: lite, strict, or off.`, "warning");
|
|
193
|
+
}
|
|
194
|
+
} catch (e) {
|
|
195
|
+
ctx?.ui?.notify?.(`Failed to save default mode: ${e.message}`, "error");
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (parsed.type === "set-mode") {
|
|
201
|
+
setMode(parsed.mode, ctx);
|
|
202
|
+
ctx?.ui?.notify?.(`UX discipline: ${parsed.mode}`, "info");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (parsed.type === "invalid") {
|
|
207
|
+
const msg = parsed.reason === "invalid-default-mode"
|
|
208
|
+
? "Invalid default mode. Use: lite, strict, or off."
|
|
209
|
+
: `Unknown mode: ${parsed.mode}. Use: lite, strict, off, status, or default <mode>.`;
|
|
210
|
+
ctx?.ui?.notify?.(msg, "warning");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
ctx?.ui?.notify?.("Unknown or unsupported /ux mode.", "warning");
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
pi.on("input", async (event, ctx) => {
|
|
219
|
+
if (event?.source === "extension") return;
|
|
220
|
+
|
|
221
|
+
const text = String(event?.text || "");
|
|
222
|
+
if (currentMode !== "off" && isDeactivationCommand(text)) {
|
|
223
|
+
setMode("off", ctx);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
228
|
+
syncStatus(ctx);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
232
|
+
syncStatus(ctx);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
236
|
+
const entries = ctx?.sessionManager?.getBranch?.() || ctx?.sessionManager?.getEntries?.() || [];
|
|
237
|
+
configuredDefaultMode = getDefaultMode();
|
|
238
|
+
hideStatus = getHideStatus();
|
|
239
|
+
currentMode = resolveSessionMode(entries, configuredDefaultMode);
|
|
240
|
+
syncStatus(ctx);
|
|
241
|
+
if (!getQuietStartup()) {
|
|
242
|
+
ctx?.ui?.notify?.(`pi-ux loaded: ${currentMode}`, "info");
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
pi.on("before_agent_start", async (event) => {
|
|
247
|
+
if (!currentMode || currentMode === "off") return;
|
|
248
|
+
// Guard null/undefined event and missing systemPrompt.
|
|
249
|
+
const base = event?.systemPrompt ? `${event.systemPrompt}\n\n` : "";
|
|
250
|
+
return { systemPrompt: `${base}${getUxInstructions(currentMode)}` };
|
|
251
|
+
});
|
|
252
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// pi-ux — deterministic slop-audit engine (no model, no deps).
|
|
2
|
+
//
|
|
3
|
+
// Three gates, all computable:
|
|
4
|
+
// 1. Contrast — WCAG 2.x relative-luminance ratio for colour pairs.
|
|
5
|
+
// 2. Tokens — off-system values (raw hex, magic px, ad-hoc shadows).
|
|
6
|
+
// 3. States — interactive elements missing focus-visible / disabled.
|
|
7
|
+
//
|
|
8
|
+
// Ponytail: WCAG math is ~10 lines; token scan is regex; state scan is a
|
|
9
|
+
// selector-set check. No wcag-contrast lib, no css parser, no AST. This is
|
|
10
|
+
// mechanical linting, not judgement — that is the whole point of the gate.
|
|
11
|
+
|
|
12
|
+
// --- WCAG contrast --------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
function channelLuminance(c) {
|
|
15
|
+
// sRGB channel (0-255) -> linear luminance component
|
|
16
|
+
const s = c / 255;
|
|
17
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function relativeLuminance(hex) {
|
|
21
|
+
// Accept 3/4/6/8-digit hex; strip alpha channel from 4/8-digit forms.
|
|
22
|
+
const m = /^#?([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})$/i.exec(String(hex).trim());
|
|
23
|
+
if (!m) return null;
|
|
24
|
+
let h = m[1];
|
|
25
|
+
// 4/8-digit: drop the trailing alpha channel (last 1/2 digits).
|
|
26
|
+
if (h.length === 8) h = h.slice(0, 6);
|
|
27
|
+
if (h.length === 4) h = h.slice(0, 3);
|
|
28
|
+
if (h.length === 3) h = h.split('').map((c) => c + c).join('');
|
|
29
|
+
const r = parseInt(h.slice(0, 2), 16);
|
|
30
|
+
const g = parseInt(h.slice(2, 4), 16);
|
|
31
|
+
const b = parseInt(h.slice(4, 6), 16);
|
|
32
|
+
return 0.2126 * channelLuminance(r) + 0.7152 * channelLuminance(g) + 0.0722 * channelLuminance(b);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function contrastRatio(fg, bg) {
|
|
36
|
+
const l1 = relativeLuminance(fg);
|
|
37
|
+
const l2 = relativeLuminance(bg);
|
|
38
|
+
if (l1 === null || l2 === null) return null;
|
|
39
|
+
const light = Math.max(l1, l2);
|
|
40
|
+
const dark = Math.min(l1, l2);
|
|
41
|
+
return (light + 0.05) / (dark + 0.05);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- token coverage -------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
// ponytail: a real CSS parser is overkill. Extract token *names* and *values*
|
|
47
|
+
// from :root { --token: value; } so we can tell generated code apart from the
|
|
48
|
+
// system.
|
|
49
|
+
// ponytail: :root variants (:root.dark, :root[data-theme], grouped :root, .x)
|
|
50
|
+
// are standard dark-mode patterns (shadcn, Tailwind, MUI). \b after :root avoids
|
|
51
|
+
// false matches like :rootCause. Matches any combinator/selector up to '{'.
|
|
52
|
+
function extractTokens(css) {
|
|
53
|
+
const tokens = new Map(); // value -> name(s)
|
|
54
|
+
const rootBlock = /:root\b[^{]*\{([^}]*)\}/g;
|
|
55
|
+
let rootMatch;
|
|
56
|
+
while ((rootMatch = rootBlock.exec(css)) !== null) {
|
|
57
|
+
const decls = rootMatch[1];
|
|
58
|
+
const declRe = /--([a-zA-Z0-9-]+)\s*:\s*([^;]+);/g;
|
|
59
|
+
let decl;
|
|
60
|
+
while ((decl = declRe.exec(decls)) !== null) {
|
|
61
|
+
const name = decl[1].trim();
|
|
62
|
+
const value = decl[2].trim();
|
|
63
|
+
// index by normalised value so we can match usage in generated css
|
|
64
|
+
const key = value.toLowerCase().replace(/\s+/g, ' ');
|
|
65
|
+
const existing = tokens.get(key);
|
|
66
|
+
tokens.set(key, existing ? `${existing},--${name}` : `--${name}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return tokens;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Hardcoded hex colours not referencing a token.
|
|
73
|
+
const RAW_HEX_RE = /#(?:[0-9a-f]{3}|[0-9a-f]{6})\b/gi;
|
|
74
|
+
|
|
75
|
+
// box-shadow declarations (we only flag these if they DON'T reference a var)
|
|
76
|
+
const BOX_SHADOW_RE = /box-shadow\s*:\s*([^;}]+)/gi;
|
|
77
|
+
|
|
78
|
+
function scanOffSystem(css, tokens) {
|
|
79
|
+
const findings = { hardcodedHex: [], adhocShadow: [] };
|
|
80
|
+
|
|
81
|
+
// 1. hardcoded hex outside :root token defs (and :root.dark / grouped variants)
|
|
82
|
+
const rootFree = css.replace(/:root\b[^{]*\{[^}]*\}/g, '');
|
|
83
|
+
let hexMatch;
|
|
84
|
+
while ((hexMatch = RAW_HEX_RE.exec(rootFree)) !== null) {
|
|
85
|
+
const hex = hexMatch[0];
|
|
86
|
+
const key = hex.toLowerCase();
|
|
87
|
+
if (!tokens.has(key)) findings.hardcodedHex.push(hex);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 2. box-shadow not built from tokens
|
|
91
|
+
let shadowMatch;
|
|
92
|
+
while ((shadowMatch = BOX_SHADOW_RE.exec(rootFree)) !== null) {
|
|
93
|
+
const val = shadowMatch[1];
|
|
94
|
+
if (!/var\(--/.test(val)) findings.adhocShadow.push(val.trim());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return findings;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- state coverage -------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
// ponytail: presence check, not a CSS parser. Substring tests are enough to
|
|
103
|
+
// catch the common slop (missing :focus-visible / :disabled). Stateful /g
|
|
104
|
+
// regexes with .test() flake across calls, so use plain includes().
|
|
105
|
+
function scanStates(css) {
|
|
106
|
+
const findings = { missingFocusVisible: [], missingDisabled: [] };
|
|
107
|
+
|
|
108
|
+
const hasInteractive =
|
|
109
|
+
/\b(?:button|a|input|select|textarea)\b/i.test(css) || /\[role\s*=\s*"?button"?\]/i.test(css);
|
|
110
|
+
if (!hasInteractive) return findings; // no interactive selectors => nothing to flag
|
|
111
|
+
|
|
112
|
+
if (!css.includes(':focus-visible')) findings.missingFocusVisible.push('no :focus-visible rule for interactive elements');
|
|
113
|
+
if (!css.includes(':disabled')) findings.missingDisabled.push('no :disabled rule for interactive elements');
|
|
114
|
+
|
|
115
|
+
return findings;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- aggregate gate -------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
function audit({ css = '', pairs = [] }) {
|
|
121
|
+
// pairs: [{ fg: '#000', bg: '#fff', label: 'body', min: 4.5 }]
|
|
122
|
+
// ponytail: destructure defaults don't cover null — guard explicitly so a
|
|
123
|
+
// malformed { pairs: null } or { css: null } call behaves like the empty
|
|
124
|
+
// value, not a throw.
|
|
125
|
+
const safeCss = typeof css === 'string' ? css : '';
|
|
126
|
+
const safePairs = Array.isArray(pairs) ? pairs : [];
|
|
127
|
+
const tokens = extractTokens(safeCss);
|
|
128
|
+
const off = scanOffSystem(safeCss, tokens);
|
|
129
|
+
const states = scanStates(safeCss);
|
|
130
|
+
|
|
131
|
+
const contrastResults = safePairs.map((p) => {
|
|
132
|
+
const ratio = contrastRatio(p.fg, p.bg);
|
|
133
|
+
return {
|
|
134
|
+
...p,
|
|
135
|
+
ratio: ratio === null ? null : Math.round(ratio * 100) / 100,
|
|
136
|
+
pass: ratio === null ? false : ratio >= (p.min ?? 4.5),
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const contrastPass = contrastResults.every((r) => r.pass);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
gates: {
|
|
144
|
+
contrast: { pass: contrastPass, results: contrastResults },
|
|
145
|
+
tokens: { pass: off.hardcodedHex.length === 0 && off.adhocShadow.length === 0, ...off },
|
|
146
|
+
states: { pass: states.missingFocusVisible.length === 0 && states.missingDisabled.length === 0, ...states },
|
|
147
|
+
},
|
|
148
|
+
pass: contrastPass
|
|
149
|
+
&& off.hardcodedHex.length === 0 && off.adhocShadow.length === 0
|
|
150
|
+
&& states.missingFocusVisible.length === 0 && states.missingDisabled.length === 0,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = {
|
|
155
|
+
channelLuminance,
|
|
156
|
+
relativeLuminance,
|
|
157
|
+
contrastRatio,
|
|
158
|
+
extractTokens,
|
|
159
|
+
scanOffSystem,
|
|
160
|
+
scanStates,
|
|
161
|
+
audit,
|
|
162
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// pi-ux — shared configuration resolver
|
|
2
|
+
//
|
|
3
|
+
// Resolution order for default mode:
|
|
4
|
+
// 1. PI_UX_DEFAULT_MODE environment variable
|
|
5
|
+
// 2. Config file defaultMode field (XDG_CONFIG_HOME/pi-ux/config.json)
|
|
6
|
+
// 3. 'strict'
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_MODE = 'strict';
|
|
13
|
+
const RUNTIME_MODES = ['off', 'lite', 'strict'];
|
|
14
|
+
|
|
15
|
+
function normalizeMode(mode) {
|
|
16
|
+
if (typeof mode !== 'string') return null;
|
|
17
|
+
const normalized = mode.trim().toLowerCase();
|
|
18
|
+
return RUNTIME_MODES.includes(normalized) ? normalized : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizePersistedMode(mode) {
|
|
22
|
+
return normalizeMode(mode);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isDeactivationCommand(text) {
|
|
26
|
+
const t = String(text || '').trim().toLowerCase().replace(/[.!?\s]+$/, '');
|
|
27
|
+
return t === 'stop ux' || t === 'normal mode';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getConfigDir() {
|
|
31
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
32
|
+
return path.join(process.env.XDG_CONFIG_HOME, 'pi-ux');
|
|
33
|
+
}
|
|
34
|
+
if (process.platform === 'win32') {
|
|
35
|
+
return path.join(
|
|
36
|
+
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
|
|
37
|
+
'pi-ux'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return path.join(os.homedir(), '.config', 'pi-ux');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getConfigPath() {
|
|
44
|
+
return path.join(getConfigDir(), 'config.json');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readConfig() {
|
|
48
|
+
try {
|
|
49
|
+
const raw = fs.readFileSync(getConfigPath(), 'utf8').replace(/^\uFEFF/, '');
|
|
50
|
+
const config = JSON.parse(raw);
|
|
51
|
+
if (config && typeof config === 'object') return config;
|
|
52
|
+
} catch (_) {}
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readConfigBool(envVar, configKey) {
|
|
57
|
+
const env = process.env[envVar];
|
|
58
|
+
if (env !== undefined) {
|
|
59
|
+
const v = env.trim().toLowerCase();
|
|
60
|
+
return v !== '' && v !== '0' && v !== 'false' && v !== 'no';
|
|
61
|
+
}
|
|
62
|
+
return readConfig()[configKey] === true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getDefaultMode() {
|
|
66
|
+
const envMode = process.env.PI_UX_DEFAULT_MODE;
|
|
67
|
+
if (envMode && RUNTIME_MODES.includes(envMode.toLowerCase())) {
|
|
68
|
+
return envMode.toLowerCase();
|
|
69
|
+
}
|
|
70
|
+
const config = readConfig();
|
|
71
|
+
if (config.defaultMode && RUNTIME_MODES.includes(config.defaultMode.toLowerCase())) {
|
|
72
|
+
return config.defaultMode.toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
return DEFAULT_MODE;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getQuietStartup() {
|
|
78
|
+
return readConfigBool('PI_UX_QUIET_STARTUP', 'quietStartup');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getHideStatus() {
|
|
82
|
+
return readConfigBool('PI_UX_HIDE_STATUS', 'hideStatus');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function writeDefaultMode(mode) {
|
|
86
|
+
const normalized = normalizeMode(mode);
|
|
87
|
+
if (!normalized) return null;
|
|
88
|
+
|
|
89
|
+
const config = readConfig();
|
|
90
|
+
config.defaultMode = normalized;
|
|
91
|
+
|
|
92
|
+
const configPath = getConfigPath();
|
|
93
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
94
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
95
|
+
return normalized;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
DEFAULT_MODE,
|
|
100
|
+
RUNTIME_MODES,
|
|
101
|
+
getDefaultMode,
|
|
102
|
+
getQuietStartup,
|
|
103
|
+
getHideStatus,
|
|
104
|
+
normalizeMode,
|
|
105
|
+
normalizePersistedMode,
|
|
106
|
+
isDeactivationCommand,
|
|
107
|
+
writeDefaultMode,
|
|
108
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Shared pi-ux instruction builder for Claude hooks and Pi extension.
|
|
2
|
+
//
|
|
3
|
+
// Reads the skill body, strips frontmatter, prepends a mode banner.
|
|
4
|
+
// ponytail: no per-mode row filtering needed here — the UX method is mode-
|
|
5
|
+
// invariant; only the banner differs (strict enforces the audit gate).
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { DEFAULT_MODE, normalizeMode, normalizePersistedMode } = require('./ux-config');
|
|
10
|
+
const SKILL_PATH = path.join(__dirname, '..', 'skills', 'ux-design', 'SKILL.md');
|
|
11
|
+
|
|
12
|
+
function getUxInstructions(mode) {
|
|
13
|
+
const configuredMode = normalizePersistedMode(mode) || DEFAULT_MODE;
|
|
14
|
+
const effectiveMode = normalizeMode(configuredMode) || DEFAULT_MODE;
|
|
15
|
+
|
|
16
|
+
const banner = effectiveMode === 'strict'
|
|
17
|
+
? 'UX DISCIPLINE ACTIVE — level: strict. Run ux_audit before declaring a screen done; block handoff on fail.'
|
|
18
|
+
: 'UX DISCIPLINE ACTIVE — level: lite. Anti-slop guardrail enforced; audit gate recommended but not blocking.';
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const body = String(fs.readFileSync(SKILL_PATH, 'utf8')).replace(/^---[\s\S]*?---\s*/, '');
|
|
22
|
+
return banner + '\n\n' + body;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
// ponytail: SKILL.md missing or unreadable — compact inline fallback keeps the guardrail.
|
|
25
|
+
return [
|
|
26
|
+
banner,
|
|
27
|
+
'',
|
|
28
|
+
'You implement UI INSIDE an existing design system. You do NOT invent visual language.',
|
|
29
|
+
'- Tokens ONLY (colour/type/spacing/radius/elevation). No off-system values.',
|
|
30
|
+
'- Elevation: named levels only. Never invent shadow blur/opacity.',
|
|
31
|
+
'- Accent: ONLY the defined accent token. No purple/indigo glow unless requested.',
|
|
32
|
+
'- Type: modular scale only. No custom font sizes.',
|
|
33
|
+
'- Spacing: 8px grid via tokens. No magic pixel values.',
|
|
34
|
+
'- Every interactive element declares: default, hover, focus-visible, active, disabled',
|
|
35
|
+
' + error/empty/loading where relevant.',
|
|
36
|
+
'- Before markup: output a 1-line inventory of components + states.',
|
|
37
|
+
'- If ambiguous, ASK. Do not guess aesthetics.',
|
|
38
|
+
].join('\n');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { getUxInstructions };
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bacnh85/pi-ux",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Anti-slop UI/UX design discipline for your Pi agent — enforces industrial-design principles, runs deterministic slop-audit gates, and ships reference design-system presets.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"ux",
|
|
9
|
+
"ui",
|
|
10
|
+
"design",
|
|
11
|
+
"anti-slop",
|
|
12
|
+
"skills"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/bacnh85/pi-extensions.git"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"AGENTS.md",
|
|
21
|
+
"README.md",
|
|
22
|
+
"CHANGELOG.md",
|
|
23
|
+
"hooks/",
|
|
24
|
+
"skills/",
|
|
25
|
+
"extensions/index.js",
|
|
26
|
+
"extensions/package.json",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node --test extensions/test/*.test.js"
|
|
31
|
+
},
|
|
32
|
+
"pi": {
|
|
33
|
+
"extensions": [
|
|
34
|
+
"./extensions/index.js"
|
|
35
|
+
],
|
|
36
|
+
"skills": [
|
|
37
|
+
"./skills"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ux-design
|
|
3
|
+
description: >
|
|
4
|
+
Anti-slop UI/UX design discipline for AI-generated interfaces. Enforces
|
|
5
|
+
industrial-design principles (Dieter Rams: honest, thorough to the last
|
|
6
|
+
detail, as little design as possible) so output is a defensible system, not
|
|
7
|
+
statistical-default slop (purple glow, shadow-as-texture, missing states).
|
|
8
|
+
Covers the Constraint-First method: own the system, write a 5-field brief,
|
|
9
|
+
generate inside constraints, normalise, pass a measurable slop-audit gate.
|
|
10
|
+
Includes model routing: Gemini/Claude via agy_execute for generation and
|
|
11
|
+
review, DeepSeek-v4 / GLM-5.2 as the main Pi models for briefs and
|
|
12
|
+
normalisation. Use when designing or building any UI — web, mobile, or
|
|
13
|
+
desktop. Active via /ux lite|strict|off.
|
|
14
|
+
argument-hint: ""
|
|
15
|
+
license: MIT
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# UX Design Discipline — Anti-Slop, Industrial-Design Method
|
|
19
|
+
|
|
20
|
+
You implement UI INSIDE an existing design system. You do NOT invent visual
|
|
21
|
+
language. Slop fills the gaps you leave — so you stop leaving gaps.
|
|
22
|
+
|
|
23
|
+
## The anti-slop guardrail (hard rules)
|
|
24
|
+
|
|
25
|
+
- **Tokens ONLY** — colour, type, spacing, radius, elevation. No off-system values.
|
|
26
|
+
- **Elevation:** named levels only (sm/md/lg/xl). Never invent shadow blur/opacity. A modal is nearer than a card; a dropdown nearer than the page — and it stays that way whoever edited last.
|
|
27
|
+
- **Accent:** ONLY the defined accent token. No purple/indigo gradients. No glow unless explicitly requested. (Glow is the #1 AI-slop signature.)
|
|
28
|
+
- **Type:** modular scale only (e.g. 1.25 ratio). No custom font sizes.
|
|
29
|
+
- **Spacing:** 8px grid via tokens. No magic pixel values.
|
|
30
|
+
- **States:** every interactive element declares `default`, `hover`, `focus-visible`, `active`, `disabled` + `error`/`empty`/`loading` where relevant.
|
|
31
|
+
- **Inventory first:** before writing markup, output a 1-line inventory of components + states you will produce.
|
|
32
|
+
- **Ambiguity = ask.** If a requirement is ambiguous, ASK. Do not guess aesthetics.
|
|
33
|
+
|
|
34
|
+
## The Constraint-First method
|
|
35
|
+
|
|
36
|
+
### Step 0 — Own the system (the model must NEVER invent this)
|
|
37
|
+
If these don't exist, don't generate screens yet — keep the model in ideation mode.
|
|
38
|
+
- **Tokens:** colour (one accent + neutrals), type scale, spacing (8px grid), radius, elevation.
|
|
39
|
+
- **Elevation scale:** 3–5 named levels only. Map every shadow to one.
|
|
40
|
+
- **State contract:** every interactive component declares all states.
|
|
41
|
+
- Reuse an existing system (Material, shadcn/ui, Radix) — YAGNI applies to design systems too.
|
|
42
|
+
|
|
43
|
+
### Step 1 — The 5-field brief (one per screen)
|
|
44
|
+
All five are required. If you can't produce inventory + states with confidence, the work is under-specified — more generation will mostly manufacture cleanup debt.
|
|
45
|
+
1. **User job + success state** — one sentence.
|
|
46
|
+
2. **Screen inventory** — required components and actions.
|
|
47
|
+
3. **Token constraints** — palette, type, spacing, elevation (paste the tokens).
|
|
48
|
+
4. **Required interaction states** per interactive component.
|
|
49
|
+
5. **One reference** screen/component that already feels like the product (brand tone).
|
|
50
|
+
|
|
51
|
+
### Step 2 — Generate fast, converge early
|
|
52
|
+
Generate several variants, commit early to the direction with the clearest hierarchy under real content.
|
|
53
|
+
- **Convergence trigger:** after two generate-revise loops, variants must visibly converge toward your constraints. If they don't, tighten the brief first — prompting forever compounds drift.
|
|
54
|
+
|
|
55
|
+
### Step 3 — Normalise the draft into the system
|
|
56
|
+
Replace ad-hoc colours with tokens, remap shadows to the elevation scale, snap spacing to the rhythm, turn one-offs into component variants.
|
|
57
|
+
- Gate: sample 10 components, verify token mapping. If fewer than 8 map cleanly, stop and repair the baseline.
|
|
58
|
+
|
|
59
|
+
### Step 4 — Slop-audit gate (blocks handoff on fail)
|
|
60
|
+
|
|
61
|
+
| Gate | Pass | Fail action |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| Token coverage | ≥8/10 components map to tokens | Pause, repair baseline |
|
|
64
|
+
| Shadow recipes | ≤3 named recipes on core surfaces | Collapse to named elevations |
|
|
65
|
+
| Contrast | WCAG AA: 4.5:1 body, 3:1 large/UI | Block handoff until fixed |
|
|
66
|
+
| State coverage | all interactive elements have focus + disabled | Keep in draft |
|
|
67
|
+
| Component hygiene | no duplicates, no frame-pile | Refactor before handoff |
|
|
68
|
+
|
|
69
|
+
## Model routing (who does what)
|
|
70
|
+
|
|
71
|
+
Split the work along each model's strength. **The inversion rule:** the cheaper/weaker the model, the MORE you must externalise constraints. Taste lives in the brief, not the weights.
|
|
72
|
+
|
|
73
|
+
| Step | Best tool/model | Why |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| **Define system** (tokens, elevation, type) | `agy_execute mode=plan pro-high` (Gemini) OR reuse shadcn/Material | Gemini = strongest visual reasoning (~80% UI similarity). Or human-owned preset. |
|
|
76
|
+
| **Per-screen brief** | Main Pi model: **GLM-5.2** | 200K ctx holds the whole system doc while scoping one screen |
|
|
77
|
+
| **Generate variants** | `agy_execute mode=accept-edits flash-high` (Gemini) | Visual king, safe inside constraints |
|
|
78
|
+
| **Normalise into system** | Main Pi model: **DeepSeek-v4** or **GLM-5.2** | Long context, token remapping, mechanical precision |
|
|
79
|
+
| **Slop audit** | `ux_audit` tool (deterministic) | Contrast + token coverage are computable, not judgement |
|
|
80
|
+
| **Final review/polish** | `agy_execute mode=plan sonnet` (Claude) or `opus` | Cross-family review; Claude = structured, conservative |
|
|
81
|
+
|
|
82
|
+
**Cross-family rule:** Gemini produces → Claude-class reviews. Don't spend both quota groups on trivial tasks.
|
|
83
|
+
|
|
84
|
+
DeepSeek/GLM are safe for design **only inside a fully-specified system**. If no system exists yet, generate it with Gemini via `agy` first, or reuse a preset.
|
|
85
|
+
|
|
86
|
+
## Banned anti-patterns
|
|
87
|
+
|
|
88
|
+
- Purple/indigo glow gradients unless the brand explicitly calls for them.
|
|
89
|
+
- Shadows as texture (drifting blur/opacity per component). Shadows = named elevation only.
|
|
90
|
+
- Glassmorphism / faux-3D that implies capability the feature lacks (violates "honest").
|
|
91
|
+
- Magic pixel values; off-scale font sizes; ad-hoc accent colours.
|
|
92
|
+
- Shipping a component without `focus-visible` + `disabled` states.
|
|
93
|
+
- Prompting "make it modern/clean" with no tokens — the single biggest slop trigger.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ux-presets
|
|
3
|
+
description: >
|
|
4
|
+
Reference design-system presets for ux-design. Anti-slop shortcuts: instead of
|
|
5
|
+
inventing tokens, elevation, and state contracts from scratch, reuse one of
|
|
6
|
+
these battle-tested systems (shadcn/ui, Material 3, Radix) or copy a compact
|
|
7
|
+
token set into the project. Use when starting a new UI project with no
|
|
8
|
+
existing design system, when Step 0 (Own the system) of the ux-design method
|
|
9
|
+
has nothing to reuse, or when asked for a starter token set. These are
|
|
10
|
+
REFERENCES only — pi-ux never bundles CSS.
|
|
11
|
+
argument-hint: ""
|
|
12
|
+
license: MIT
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# UX System Presets — anti-slop shortcuts for Step 0
|
|
16
|
+
|
|
17
|
+
The Constraint-First method starts at "Own the system." If the project has no
|
|
18
|
+
tokens / elevation scale / state contract, do NOT generate screens yet — either
|
|
19
|
+
**reuse an existing system** or **copy a compact token set**. This skill is the
|
|
20
|
+
shortcut drawer for both.
|
|
21
|
+
|
|
22
|
+
## Option A — reuse an existing system (preferred, YAGNI)
|
|
23
|
+
|
|
24
|
+
| System | When | Install |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| **shadcn/ui** | React/Next, want copy-in components you own | `npx shadcn@latest init` — copies component source into your repo, not a dependency |
|
|
27
|
+
| **Material 3 (MUI)** | Want the full Material system, theming maturity | `@mui/material` + MUI theme — elevation, states, colour system all defined |
|
|
28
|
+
| **Radix UI Primitives** | Want unstyled, accessible primitives (dialogs, menus, popovers) you style yourself | `@radix-ui/react-*` — focus/disabled/aria handled correctly already |
|
|
29
|
+
| **Park UI / Ark UI** | Multi-framework, headless + styled variants | Ark UI primitives + Park UI recipes |
|
|
30
|
+
|
|
31
|
+
These already define tokens, 3–5 elevation levels, and full state contracts —
|
|
32
|
+
which is exactly what the anti-slop guardrail demands. **Reusing beats
|
|
33
|
+
redefining.** YAGNI applies to design systems too.
|
|
34
|
+
|
|
35
|
+
## Option B — minimal token set (when no framework, or CSS-only)
|
|
36
|
+
|
|
37
|
+
When there's nothing to reuse, drop this compact baseline into the project's
|
|
38
|
+
`:root` and point the guardrail at it. It satisfies all three audit gates
|
|
39
|
+
(tokens, elevation, states) with ~20 lines.
|
|
40
|
+
|
|
41
|
+
```css
|
|
42
|
+
:root {
|
|
43
|
+
/* Colour — one accent + neutrals. No purple glow. */
|
|
44
|
+
--accent: #0066ff; /* swap for the brand accent; ONE accent only */
|
|
45
|
+
--accent-hover: #0052cc;
|
|
46
|
+
--text: #111111;
|
|
47
|
+
--text-muted: #595959;
|
|
48
|
+
--bg: #ffffff;
|
|
49
|
+
--surface: #f7f7f8;
|
|
50
|
+
--border: #e4e4e7;
|
|
51
|
+
--danger: #c2261b;
|
|
52
|
+
|
|
53
|
+
/* Type — modular scale (ratio 1.25). No custom sizes. */
|
|
54
|
+
--text-sm: 0.8rem;
|
|
55
|
+
--text-base: 1rem;
|
|
56
|
+
--text-lg: 1.25rem;
|
|
57
|
+
--text-xl: 1.563rem;
|
|
58
|
+
--text-2xl: 1.953rem;
|
|
59
|
+
|
|
60
|
+
/* Spacing — 8px grid. No magic pixels. */
|
|
61
|
+
--space-1: 4px; /* half-step for hairlines only */
|
|
62
|
+
--space-2: 8px;
|
|
63
|
+
--space-3: 16px;
|
|
64
|
+
--space-4: 24px;
|
|
65
|
+
--space-5: 32px;
|
|
66
|
+
--space-6: 48px;
|
|
67
|
+
|
|
68
|
+
/* Radius */
|
|
69
|
+
--radius-sm: 4px;
|
|
70
|
+
--radius-md: 8px;
|
|
71
|
+
--radius-lg: 12px;
|
|
72
|
+
|
|
73
|
+
/* Elevation — 4 named levels. Map EVERY shadow to one of these. */
|
|
74
|
+
--elev-sm: 0 1px 2px rgba(0,0,0,0.05);
|
|
75
|
+
--elev-md: 0 2px 8px rgba(0,0,0,0.08);
|
|
76
|
+
--elev-lg: 0 8px 24px rgba(0,0,0,0.12);
|
|
77
|
+
--elev-xl: 0 16px 48px rgba(0,0,0,0.16);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* State contract — interactive elements MUST declare these. */
|
|
81
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
82
|
+
:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## How to use with ux-design
|
|
86
|
+
|
|
87
|
+
1. Run **Step 0** of ux-design. If the project already has tokens/elevation/states → use them.
|
|
88
|
+
2. If not, pick **Option A** (reuse shadcn/Material/Radix) first. This is the lazy, correct path.
|
|
89
|
+
3. Only if nothing fits, copy **Option B** into the project and adapt `--accent` to the brand.
|
|
90
|
+
4. Generate screens with `agy flash-high` INSIDE these constraints.
|
|
91
|
+
5. Run `ux_audit` with the CSS + the 5 most common text colour pairs before handoff.
|
|
92
|
+
|
|
93
|
+
## Audit-ready pairs for the minimal set
|
|
94
|
+
|
|
95
|
+
```jsonc
|
|
96
|
+
// Pass these to ux_audit.pairs to check the baseline itself.
|
|
97
|
+
[
|
|
98
|
+
{ "fg": "#111111", "bg": "#ffffff", "label": "body", "min": 4.5 },
|
|
99
|
+
{ "fg": "#595959", "bg": "#ffffff", "label": "muted", "min": 4.5 },
|
|
100
|
+
{ "fg": "#0066ff", "bg": "#ffffff", "label": "accent-link","min": 4.5 },
|
|
101
|
+
{ "fg": "#ffffff", "bg": "#0066ff", "label": "button", "min": 4.5 },
|
|
102
|
+
{ "fg": "#c2261b", "bg": "#ffffff", "label": "danger", "min": 4.5 }
|
|
103
|
+
]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## What this preset is NOT
|
|
107
|
+
|
|
108
|
+
- Not a CSS framework — reference only. Don't bundle it; copy what you need.
|
|
109
|
+
- Not themeable at runtime — it's a baseline to extend, not a product.
|
|
110
|
+
- Not opinionated about the accent — `--accent` is the ONE knob you turn for brand.
|