pixl-mcp 0.1.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.
Files changed (42) hide show
  1. package/README.md +91 -0
  2. package/dist/design.js +128 -0
  3. package/dist/design.js.map +1 -0
  4. package/dist/flow.js +52 -0
  5. package/dist/flow.js.map +1 -0
  6. package/dist/index.js +34 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/linter/contrast.js +46 -0
  9. package/dist/linter/contrast.js.map +1 -0
  10. package/dist/linter/index.js +190 -0
  11. package/dist/linter/index.js.map +1 -0
  12. package/dist/linter/linter.test.js +99 -0
  13. package/dist/linter/linter.test.js.map +1 -0
  14. package/dist/mcp/tools.js +129 -0
  15. package/dist/mcp/tools.js.map +1 -0
  16. package/dist/presets/catalog.js +162 -0
  17. package/dist/presets/catalog.js.map +1 -0
  18. package/dist/presets/contract.js +140 -0
  19. package/dist/presets/contract.js.map +1 -0
  20. package/dist/presets/types.js +2 -0
  21. package/dist/presets/types.js.map +1 -0
  22. package/dist/preview-server/index.js +143 -0
  23. package/dist/preview-server/index.js.map +1 -0
  24. package/dist/screens.js +52 -0
  25. package/dist/screens.js.map +1 -0
  26. package/dist/session.js +50 -0
  27. package/dist/session.js.map +1 -0
  28. package/dist/workspace/index.js +149 -0
  29. package/dist/workspace/index.js.map +1 -0
  30. package/dist/workspace/schemas.js +48 -0
  31. package/dist/workspace/schemas.js.map +1 -0
  32. package/package.json +47 -0
  33. package/preview-app/index.html +12 -0
  34. package/preview-app/src/App.tsx +53 -0
  35. package/preview-app/src/FlowCanvas.tsx +208 -0
  36. package/preview-app/src/Gallery.tsx +35 -0
  37. package/preview-app/src/api.ts +51 -0
  38. package/preview-app/src/main.tsx +10 -0
  39. package/preview-app/src/styles.css +173 -0
  40. package/preview-app/src/useLiveReload.ts +17 -0
  41. package/preview-app/src/vite-env.d.ts +1 -0
  42. package/preview-app/tsconfig.json +15 -0
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # Pixl
2
+
3
+ **Open-source MCP server for designing mobile UIs.** Pixl plugs into whatever AI coding host you already use (Claude Code, Cursor, …) and turns it into a guided mobile-UI design tool. Your host's model does the generating — Pixl supplies the taste it lacks: a locked design-system **token contract**, a **linter** that rejects off-standard output, and a **local browser preview** with a phone-frame gallery and a draggable user-journey flow map. Everything runs locally.
4
+
5
+ > Status: **v0.1, feature-complete** — MCP server, workspace, live preview, design-system presets + token contract, screen generation with the taste linter, and the draggable user-journey flow canvas.
6
+
7
+ ## How it fits together
8
+
9
+ ```
10
+ host (Claude Code / Cursor) ──MCP stdio──▶ pixl-mcp (Node/TS)
11
+ │ owns .pixl/ workspace, runs linter
12
+
13
+ localhost:4321 Vite + React preview
14
+ ├── Screen gallery (phone frames, live-reload)
15
+ └── Flow canvas (dotted bg, draggable)
16
+ ```
17
+
18
+ State lives in a `.pixl/` folder in your project: `tokens.json`, `design/*.md`, `screens/*.html`, `flow.json`.
19
+
20
+ ## Install
21
+
22
+ Pixl **auto-starts** when your host connects: it creates the `.pixl/` workspace, boots the preview, and opens `localhost:4321` in your browser. No `pixl_init` call needed — just point your host at it and start asking for screens.
23
+
24
+ **npx (recommended):**
25
+
26
+ Cursor — `~/.cursor/mcp.json`:
27
+
28
+ ```json
29
+ { "mcpServers": { "pixl": { "command": "npx", "args": ["-y", "pixl-mcp"] } } }
30
+ ```
31
+
32
+ Claude Code:
33
+
34
+ ```bash
35
+ claude mcp add pixl -- npx -y pixl-mcp
36
+ ```
37
+
38
+ **Docker** (no Node needed; fully isolated). `-p 4321:4321` forwards the preview; the `sh -c` wrapper reaps any prior container so relaunches don't clash on the port:
39
+
40
+ ```json
41
+ {
42
+ "mcpServers": {
43
+ "pixl": {
44
+ "command": "sh",
45
+ "args": [
46
+ "-c",
47
+ "docker rm -f pixl-mcp >/dev/null 2>&1; exec docker run --rm -i --name pixl-mcp -p 4321:4321 -v \"$PWD\":/work -w /work pixl-mcp"
48
+ ]
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ Build the image first with `docker build -t pixl-mcp .` (or pull it once published).
55
+
56
+ ## Develop
57
+
58
+ ```bash
59
+ npm install --include=dev # this machine has npm omit=dev set globally
60
+ npm run build # compile the server to dist/
61
+ npm run typecheck
62
+ npx vitest run # linter unit tests (pass/fail fixtures)
63
+ node scripts/smoke.mjs # workspace + preview API end-to-end
64
+ node scripts/mcp-smoke.mjs # drives the server as a real MCP client
65
+ node scripts/phase2-smoke.mjs # design system + screen plan
66
+ node scripts/phase3-smoke.mjs # write/lint screen: reject bad, accept fixed
67
+ node scripts/phase4-smoke.mjs # flow graph + drag-persist round-trip
68
+ ```
69
+
70
+ ## Register a local build (from source)
71
+
72
+ Claude Code:
73
+
74
+ ```bash
75
+ npm install --include=dev && npm run build
76
+ claude mcp add pixl -- node /absolute/path/to/pixl-mcp/dist/index.js
77
+ ```
78
+
79
+ The preview auto-starts and opens `localhost:4321` — then ask your assistant to set a design system and generate screens.
80
+
81
+ ## Tools
82
+
83
+ | Tool | Purpose |
84
+ |---|---|
85
+ | `pixl_init` | Re-open the preview / fetch its URL (auto-runs on startup — you don't call it) |
86
+ | `pixl_status` | Report workspace state |
87
+ | `pixl_list_presets` / `pixl_set_design_system` / `pixl_plan_screens` | Design system + screen plan |
88
+ | `pixl_write_screen` / `pixl_lint_screen` | Generate + lint screens |
89
+ | `pixl_set_flow` | User-journey flow graph |
90
+
91
+ MIT.
package/dist/design.js ADDED
@@ -0,0 +1,128 @@
1
+ import { TokensSchema } from "./workspace/schemas.js";
2
+ import { getPreset, presetSummaries } from "./presets/catalog.js";
3
+ import { buildDesignDocs, buildTokenContract } from "./presets/contract.js";
4
+ const DEFAULT_CUSTOM_RULES = [
5
+ "Consistent spacing on the scale; clear visual hierarchy.",
6
+ "Color roles only — no raw hex in screens.",
7
+ "One primary action per screen; adequate tap targets.",
8
+ ];
9
+ /**
10
+ * Resolve tokens (preset ± overrides, or fully custom), persist tokens.json +
11
+ * the five design/*.md docs, and return the token contract the model generates
12
+ * against. This is the heart of phase 2.
13
+ */
14
+ export async function applyDesignSystem(workspace, input) {
15
+ let tokens;
16
+ let rules;
17
+ if (input.customTokens) {
18
+ tokens = TokensSchema.parse(input.customTokens);
19
+ rules = DEFAULT_CUSTOM_RULES;
20
+ }
21
+ else {
22
+ if (!input.presetId)
23
+ throw new Error("provide either presetId or customTokens");
24
+ const preset = getPreset(input.presetId);
25
+ if (!preset) {
26
+ const ids = presetSummaries().map((p) => p.id).join(", ");
27
+ throw new Error(`unknown preset "${input.presetId}". Available: ${ids}`);
28
+ }
29
+ tokens = structuredClone(preset.tokens);
30
+ rules = preset.rules;
31
+ }
32
+ // Merge overrides.
33
+ if (input.overrides) {
34
+ if (input.overrides.name)
35
+ tokens.name = input.overrides.name;
36
+ if (input.overrides.fontFamilies)
37
+ tokens.fontFamilies = input.overrides.fontFamilies;
38
+ if (input.overrides.colors)
39
+ tokens.colors = { ...tokens.colors, ...input.overrides.colors };
40
+ tokens = TokensSchema.parse(tokens);
41
+ }
42
+ await workspace.writeTokens(tokens);
43
+ const docs = buildDesignDocs(tokens, rules);
44
+ await Promise.all([
45
+ workspace.writeDesignDoc("typography", docs.typography),
46
+ workspace.writeDesignDoc("color", docs.color),
47
+ workspace.writeDesignDoc("spacing", docs.spacing),
48
+ workspace.writeDesignDoc("components", docs.components),
49
+ workspace.writeDesignDoc("principles", docs.principles),
50
+ ]);
51
+ const contract = buildTokenContract(tokens, rules);
52
+ const referenceScreenHtml = input.customTokens
53
+ ? undefined
54
+ : getPreset(input.presetId)?.referenceScreenHtml;
55
+ return {
56
+ ok: true,
57
+ designSystem: tokens.name,
58
+ contract,
59
+ referenceScreenHtml,
60
+ wrote: ["tokens.json", "design/typography.md", "design/color.md", "design/spacing.md", "design/components.md", "design/principles.md"],
61
+ next: "Call pixl_plan_screens to lay out the screens and journey, then pixl_write_screen per screen.",
62
+ };
63
+ }
64
+ /**
65
+ * Record the screen list + user journey. Writes screens.md and seeds flow.json
66
+ * with a grid layout (real positions get refined on the canvas in phase 4).
67
+ */
68
+ export async function planScreens(workspace, input) {
69
+ const ids = new Set(input.screens.map((s) => s.id));
70
+ for (const j of input.journey ?? []) {
71
+ if (!ids.has(j.from) || !ids.has(j.to)) {
72
+ throw new Error(`journey references unknown screen id: ${!ids.has(j.from) ? j.from : j.to}`);
73
+ }
74
+ }
75
+ // Persist the screen index (screens keep any existing html; new ones are html-less until written).
76
+ const existing = await workspace.readScreenIndex();
77
+ const htmlById = new Map(existing.screens.map((s) => [s.id, s]));
78
+ const merged = input.screens.map((s) => ({ ...htmlById.get(s.id), ...s }));
79
+ await workspace.writeScreenIndex({ screens: merged });
80
+ // screens.md
81
+ const rows = input.screens
82
+ .map((s, i) => `| ${i + 1} | \`${s.id}\` | ${s.name} | ${s.description ?? ""} |`)
83
+ .join("\n");
84
+ const journeyLines = (input.journey ?? [])
85
+ .map((j) => `- \`${j.from}\` → \`${j.to}\`${j.label ? ` — ${j.label}` : ""}`)
86
+ .join("\n");
87
+ const md = `# Screens & user journey
88
+
89
+ | # | id | name | description |
90
+ |---|---|---|---|
91
+ ${rows}
92
+
93
+ ## Journey
94
+
95
+ ${journeyLines || "_No transitions defined yet._"}
96
+ `;
97
+ await writeScreensMd(workspace, md);
98
+ // Seed flow.json with a grid layout + journey edges.
99
+ const flow = {
100
+ nodes: input.screens.map((s, i) => ({
101
+ id: s.id,
102
+ screenId: s.id,
103
+ label: s.name,
104
+ x: (i % 3) * 300,
105
+ y: Math.floor(i / 3) * 240,
106
+ })),
107
+ edges: (input.journey ?? []).map((j, i) => ({
108
+ id: `e${i}`,
109
+ source: j.from,
110
+ target: j.to,
111
+ label: j.label ?? "",
112
+ })),
113
+ };
114
+ await workspace.writeFlow(flow);
115
+ return {
116
+ ok: true,
117
+ screenCount: input.screens.length,
118
+ edgeCount: flow.edges.length,
119
+ wrote: ["screens.md", "screens.json", "flow.json"],
120
+ next: "Generate each screen with pixl_write_screen (it lints against the token contract).",
121
+ };
122
+ }
123
+ async function writeScreensMd(workspace, md) {
124
+ const { promises: fs } = await import("node:fs");
125
+ const path = await import("node:path");
126
+ await fs.writeFile(path.join(workspace.dir, "screens.md"), md, "utf8");
127
+ }
128
+ //# sourceMappingURL=design.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"design.js","sourceRoot":"","sources":["../src/design.ts"],"names":[],"mappings":"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"}
package/dist/flow.js ADDED
@@ -0,0 +1,52 @@
1
+ import { FlowSchema } from "./workspace/schemas.js";
2
+ /**
3
+ * Set/update the user-journey graph, then persist flow.json (the canvas renders
4
+ * from it). Nodes default to one per planned screen; existing positions are
5
+ * preserved so an MCP update never resets a layout the user dragged. Edges are
6
+ * validated against the node set.
7
+ */
8
+ export async function setFlow(workspace, input) {
9
+ const index = await workspace.readScreenIndex();
10
+ const existing = await workspace.readFlow();
11
+ const posById = new Map(existing.nodes.map((n) => [n.id, { x: n.x, y: n.y }]));
12
+ const nameById = new Map(index.screens.map((s) => [s.id, s.name]));
13
+ // Node set: explicit nodes if given, else one per screen in the index.
14
+ const specs = input.nodes ?? index.screens.map((s) => ({ id: s.id, label: s.name }));
15
+ if (specs.length === 0) {
16
+ throw new Error("No screens to place. Call pixl_plan_screens (or pass nodes) first.");
17
+ }
18
+ const nodes = specs.map((spec, i) => {
19
+ const prev = posById.get(spec.id);
20
+ return {
21
+ id: spec.id,
22
+ screenId: spec.id,
23
+ label: spec.label ?? nameById.get(spec.id) ?? spec.id,
24
+ x: spec.x ?? prev?.x ?? (i % 3) * 300,
25
+ y: spec.y ?? prev?.y ?? Math.floor(i / 3) * 240,
26
+ };
27
+ });
28
+ const nodeIds = new Set(nodes.map((n) => n.id));
29
+ for (const e of input.edges) {
30
+ if (!nodeIds.has(e.from) || !nodeIds.has(e.to)) {
31
+ throw new Error(`edge references unknown node id: ${!nodeIds.has(e.from) ? e.from : e.to}`);
32
+ }
33
+ }
34
+ const flow = FlowSchema.parse({
35
+ nodes,
36
+ edges: input.edges.map((e, i) => ({
37
+ id: `e${i}`,
38
+ source: e.from,
39
+ target: e.to,
40
+ label: e.label ?? "",
41
+ })),
42
+ });
43
+ await workspace.writeFlow(flow);
44
+ return {
45
+ ok: true,
46
+ nodeCount: flow.nodes.length,
47
+ edgeCount: flow.edges.length,
48
+ wrote: ["flow.json"],
49
+ next: "Open the Flow tab in the preview to see the journey; drag nodes to rearrange (positions persist).",
50
+ };
51
+ }
52
+ //# sourceMappingURL=flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flow.js","sourceRoot":"","sources":["../src/flow.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { PixlSession } from "./session.js";
5
+ import { registerTools } from "./mcp/tools.js";
6
+ async function main() {
7
+ const session = new PixlSession(process.cwd());
8
+ const server = new McpServer({ name: "pixl", version: "0.1.0" });
9
+ registerTools(server, session);
10
+ const transport = new StdioServerTransport();
11
+ await server.connect(transport);
12
+ // stdio MCP servers must never write to stdout (it's the protocol channel).
13
+ process.stderr.write("pixl MCP server ready\n");
14
+ // Auto-start: create the workspace and boot the preview the moment the host
15
+ // connects, so the user never has to call pixl_init themselves. Open the
16
+ // browser too — except inside Docker (no browser; PIXL_HOST marks the
17
+ // container, where the port is forwarded) or when PIXL_OPEN=0 (tests).
18
+ const openBrowser = !process.env.PIXL_HOST && process.env.PIXL_OPEN !== "0";
19
+ session
20
+ .ensureStarted(openBrowser)
21
+ .then((url) => process.stderr.write(`pixl preview at ${url}\n`))
22
+ .catch((e) => process.stderr.write(`pixl preview boot failed: ${e.message}\n`));
23
+ const shutdown = async () => {
24
+ await session.shutdown();
25
+ process.exit(0);
26
+ };
27
+ process.on("SIGINT", shutdown);
28
+ process.on("SIGTERM", shutdown);
29
+ }
30
+ main().catch((err) => {
31
+ process.stderr.write(`pixl fatal: ${err.stack ?? err}\n`);
32
+ process.exit(1);
33
+ });
34
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C,KAAK,UAAU,IAAI;IACjB,MAAM,OAAO,GAAG,IAAI,WAAW,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IACjE,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAE/B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAEhC,4EAA4E;IAC5E,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAEhD,4EAA4E;IAC5E,yEAAyE;IACzE,sEAAsE;IACtE,uEAAuE;IACvE,MAAM,WAAW,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,KAAK,GAAG,CAAC;IAC5E,OAAO;SACJ,aAAa,CAAC,WAAW,CAAC;SAC1B,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;SAC/D,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6BAA8B,CAAW,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC;IAE7F,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE;QAC1B,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC;QACzB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,eAAgB,GAAa,CAAC,KAAK,IAAI,GAAG,IAAI,CAAC,CAAC;IACrE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * WCAG contrast math — pure, no I/O. Used by the contrast rule to reject
3
+ * text/background role pairs that fall below AA.
4
+ */
5
+ /** Parse #rgb / #rrggbb (with optional alpha, ignored) → rgb, or null. */
6
+ export function hexToRgb(hex) {
7
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(hex.trim());
8
+ if (!m)
9
+ return null;
10
+ let h = m[1];
11
+ if (h.length === 3 || h.length === 4) {
12
+ h = h
13
+ .slice(0, 3)
14
+ .split("")
15
+ .map((c) => c + c)
16
+ .join("");
17
+ }
18
+ else {
19
+ h = h.slice(0, 6);
20
+ }
21
+ return {
22
+ r: parseInt(h.slice(0, 2), 16),
23
+ g: parseInt(h.slice(2, 4), 16),
24
+ b: parseInt(h.slice(4, 6), 16),
25
+ };
26
+ }
27
+ /** Relative luminance per WCAG 2.1. */
28
+ export function relativeLuminance({ r, g, b }) {
29
+ const chan = (v) => {
30
+ const s = v / 255;
31
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
32
+ };
33
+ return 0.2126 * chan(r) + 0.7152 * chan(g) + 0.0722 * chan(b);
34
+ }
35
+ /** Contrast ratio between two hex colors (1–21); 0 if either is unparseable. */
36
+ export function contrastRatio(fg, bg) {
37
+ const a = hexToRgb(fg);
38
+ const b = hexToRgb(bg);
39
+ if (!a || !b)
40
+ return 0;
41
+ const la = relativeLuminance(a);
42
+ const lb = relativeLuminance(b);
43
+ const [hi, lo] = la >= lb ? [la, lb] : [lb, la];
44
+ return (hi + 0.05) / (lo + 0.05);
45
+ }
46
+ //# sourceMappingURL=contrast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contrast.js","sourceRoot":"","sources":["../../src/linter/contrast.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,0EAA0E;AAC1E,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,MAAM,CAAC,GAAG,uDAAuD,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IACnF,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpB,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACb,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrC,CAAC,GAAG,CAAC;aACF,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;aACX,KAAK,CAAC,EAAE,CAAC;aACT,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;aACjB,IAAI,CAAC,EAAE,CAAC,CAAC;IACd,CAAC;SAAM,CAAC;QACN,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACpB,CAAC;IACD,OAAO;QACL,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;QAC9B,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;QAC9B,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;KAC/B,CAAC;AACJ,CAAC;AAED,uCAAuC;AACvC,MAAM,UAAU,iBAAiB,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAO;IAChD,MAAM,IAAI,GAAG,CAAC,CAAS,EAAE,EAAE;QACzB,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;QAClB,OAAO,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC,IAAI,GAAG,CAAC;IACjE,CAAC,CAAC;IACF,OAAO,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAChE,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,aAAa,CAAC,EAAU,EAAE,EAAU;IAClD,MAAM,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IACvB,MAAM,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IACvB,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IACvB,MAAM,EAAE,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAChC,MAAM,EAAE,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAChC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IAChD,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,190 @@
1
+ import { contrastRatio } from "./contrast.js";
2
+ const TAP_MIN = 44; // px — minimum interactive target
3
+ const AA_NORMAL = 4.5; // WCAG AA for normal text
4
+ const TAILWIND_UNIT = 4; // px per Tailwind spacing/size step (n*4)
5
+ /**
6
+ * Lint a self-contained screen against the token contract. Pure: takes HTML +
7
+ * tokens, returns violations. Trusts named Tailwind utilities (they sit on the
8
+ * 4px grid and bind to tokens via the config) and instead hunts for the ways a
9
+ * model goes off-standard: raw hex, arbitrary bracket values, tiny tap targets,
10
+ * low contrast, a missing viewport lock, or a missing Tailwind head.
11
+ */
12
+ export function lintScreen(html, tokens) {
13
+ const violations = [];
14
+ const body = stripHeadAndScripts(html);
15
+ headBlockRule(html, violations);
16
+ viewportRule(html, tokens, violations);
17
+ rawHexRule(body, violations);
18
+ arbitraryValueRules(body, tokens, violations);
19
+ tapTargetRule(body, violations);
20
+ contrastRule(body, tokens, violations);
21
+ const errorCount = violations.filter((v) => v.severity === "error").length;
22
+ const warnCount = violations.length - errorCount;
23
+ return { ok: errorCount === 0, errorCount, warnCount, violations };
24
+ }
25
+ /**
26
+ * Remove <head>…</head> plus any <script>/<style> bodies so token hexes in the
27
+ * Tailwind config don't read as raw-color violations. Leaves body markup.
28
+ */
29
+ function stripHeadAndScripts(html) {
30
+ return html
31
+ .replace(/<head[\s\S]*?<\/head>/gi, " ")
32
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
33
+ .replace(/<style[\s\S]*?<\/style>/gi, " ");
34
+ }
35
+ function headBlockRule(html, out) {
36
+ if (!/cdn\.tailwindcss\.com/.test(html)) {
37
+ out.push({
38
+ rule: "missing-head",
39
+ severity: "error",
40
+ message: "Screen is missing the Tailwind head block (cdn.tailwindcss.com). Paste the contract's headBlock into <head>, or the design tokens won't apply.",
41
+ });
42
+ }
43
+ }
44
+ function viewportRule(html, tokens, out) {
45
+ const { width, height } = tokens.breakpoints;
46
+ const hasW = new RegExp(`(?:width|min-width)\\s*:\\s*${width}px`).test(html) || html.includes(`w-[${width}px]`);
47
+ const hasH = new RegExp(`(?:height|min-height)\\s*:\\s*${height}px`).test(html) || html.includes(`h-[${height}px]`);
48
+ if (!hasW || !hasH) {
49
+ out.push({
50
+ rule: "viewport-unlocked",
51
+ severity: "error",
52
+ message: `Screen must be locked to the phone viewport ${width}×${height}. Set the root container to width:${width}px;height:${height}px.`,
53
+ });
54
+ }
55
+ }
56
+ const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
57
+ function rawHexRule(body, out) {
58
+ // Hex in inline style attributes, or as arbitrary color classes bg-[#..]/text-[#..].
59
+ const found = new Set();
60
+ for (const m of body.matchAll(HEX_RE))
61
+ found.add(m[0].toLowerCase());
62
+ for (const hex of found) {
63
+ out.push({
64
+ rule: "raw-color",
65
+ severity: "error",
66
+ message: `Raw hex ${hex} in markup. Use a palette role class instead (bg-<role>, text-<role>, border-<role>).`,
67
+ snippet: hex,
68
+ });
69
+ }
70
+ }
71
+ // Arbitrary Tailwind values: prefix-[value]. e.g. p-[13px], text-[15px], rounded-[3px], bg-[#f00].
72
+ const ARBITRARY_RE = /\b([a-z]+(?:-[a-z]+)?)-\[([^\]]+)\]/g;
73
+ const SPACING_PREFIXES = new Set([
74
+ "p", "px", "py", "pt", "pb", "pl", "pr",
75
+ "m", "mx", "my", "mt", "mb", "ml", "mr",
76
+ "gap", "gap-x", "gap-y", "space-x", "space-y",
77
+ "w", "h", "min-w", "min-h", "max-w", "max-h",
78
+ "top", "bottom", "left", "right", "inset",
79
+ ]);
80
+ function arbitraryValueRules(body, tokens, out) {
81
+ for (const m of body.matchAll(ARBITRARY_RE)) {
82
+ const prefix = m[1];
83
+ const raw = m[2];
84
+ const token = m[0];
85
+ if (raw.startsWith("#"))
86
+ continue; // covered by raw-color rule
87
+ const px = parsePx(raw);
88
+ if (px === null)
89
+ continue; // non-px arbitrary (%, calc, vh) — leave it
90
+ if (prefix === "text") {
91
+ if (!tokens.typeScale.includes(px)) {
92
+ out.push({
93
+ rule: "off-scale-type",
94
+ severity: "error",
95
+ message: `Font size ${token} is off the type scale (${tokens.typeScale.join(", ")}px). Use a type-scale size.`,
96
+ snippet: token,
97
+ });
98
+ }
99
+ }
100
+ else if (prefix === "rounded" || prefix.startsWith("rounded-")) {
101
+ if (!tokens.radius.includes(px)) {
102
+ out.push({
103
+ rule: "off-scale-radius",
104
+ severity: "warn",
105
+ message: `Radius ${token} is off the radius scale (${tokens.radius.join(", ")}px). Use rounded-sm|md|lg|xl.`,
106
+ snippet: token,
107
+ });
108
+ }
109
+ }
110
+ else if (SPACING_PREFIXES.has(prefix)) {
111
+ // Viewport-locking width/height in brackets is legitimate; skip those.
112
+ const isViewport = px === tokens.breakpoints.width || px === tokens.breakpoints.height;
113
+ if (!isViewport && px % TAILWIND_UNIT !== 0) {
114
+ out.push({
115
+ rule: "off-scale-spacing",
116
+ severity: "error",
117
+ message: `${token} is off the 4px grid. Use a Tailwind spacing/size step (multiple of 4) instead of an arbitrary value.`,
118
+ snippet: token,
119
+ });
120
+ }
121
+ }
122
+ }
123
+ }
124
+ // Interactive elements whose height we can read; flag if < 44px.
125
+ const INTERACTIVE_RE = /<(button|a|input|select|textarea)\b([^>]*)>/gi;
126
+ function tapTargetRule(body, out) {
127
+ for (const m of body.matchAll(INTERACTIVE_RE)) {
128
+ const tag = m[1].toLowerCase();
129
+ const attrs = m[2];
130
+ // Skip hidden inputs and non-interactive anchors are still worth checking; keep simple.
131
+ if (tag === "input" && /type\s*=\s*["']?hidden/i.test(attrs))
132
+ continue;
133
+ const h = readHeightPx(attrs);
134
+ if (h !== null && h < TAP_MIN) {
135
+ out.push({
136
+ rule: "tap-target",
137
+ severity: "error",
138
+ message: `<${tag}> has height ${h}px — below the ${TAP_MIN}px minimum tap target. Use at least h-11 (44px).`,
139
+ snippet: m[0].slice(0, 80),
140
+ });
141
+ }
142
+ }
143
+ }
144
+ /** Read an explicit height from class (h-N, h-[Npx]) or inline style height. */
145
+ function readHeightPx(attrs) {
146
+ const styleH = /style\s*=\s*["'][^"']*\bheight\s*:\s*(\d+(?:\.\d+)?)px/i.exec(attrs);
147
+ if (styleH)
148
+ return Number(styleH[1]);
149
+ const arb = /\bh-\[(\d+(?:\.\d+)?)px\]/.exec(attrs);
150
+ if (arb)
151
+ return Number(arb[1]);
152
+ const unit = /\bh-(\d+)\b/.exec(attrs);
153
+ if (unit)
154
+ return Number(unit[1]) * TAILWIND_UNIT;
155
+ return null;
156
+ }
157
+ // Element carrying both a text role and a bg role — check that pair's contrast.
158
+ const EL_RE = /<[a-z][^>]*class\s*=\s*["']([^"']*)["'][^>]*>/gi;
159
+ function contrastRule(body, tokens, out) {
160
+ const seen = new Set();
161
+ for (const m of body.matchAll(EL_RE)) {
162
+ const classes = m[1];
163
+ const textRole = /\btext-([a-zA-Z]+)\b/.exec(classes)?.[1];
164
+ const bgRole = /\bbg-([a-zA-Z]+)\b/.exec(classes)?.[1];
165
+ if (!textRole || !bgRole)
166
+ continue;
167
+ const fg = tokens.colors[textRole];
168
+ const bg = tokens.colors[bgRole];
169
+ if (!fg || !bg)
170
+ continue; // unknown role handled elsewhere / trusted
171
+ const key = `${textRole}:${bgRole}`;
172
+ if (seen.has(key))
173
+ continue;
174
+ seen.add(key);
175
+ const ratio = contrastRatio(fg, bg);
176
+ if (ratio > 0 && ratio < AA_NORMAL) {
177
+ out.push({
178
+ rule: "contrast",
179
+ severity: "error",
180
+ message: `text-${textRole} on bg-${bgRole} has contrast ${ratio.toFixed(2)}:1 — below WCAG AA (${AA_NORMAL}:1). Pick a higher-contrast role pair.`,
181
+ snippet: key,
182
+ });
183
+ }
184
+ }
185
+ }
186
+ function parsePx(raw) {
187
+ const m = /^(\d+(?:\.\d+)?)px$/.exec(raw.trim());
188
+ return m ? Number(m[1]) : null;
189
+ }
190
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/linter/index.ts"],"names":[],"mappings":"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"}