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.
- package/README.md +91 -0
- package/dist/design.js +128 -0
- package/dist/design.js.map +1 -0
- package/dist/flow.js +52 -0
- package/dist/flow.js.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/linter/contrast.js +46 -0
- package/dist/linter/contrast.js.map +1 -0
- package/dist/linter/index.js +190 -0
- package/dist/linter/index.js.map +1 -0
- package/dist/linter/linter.test.js +99 -0
- package/dist/linter/linter.test.js.map +1 -0
- package/dist/mcp/tools.js +129 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/presets/catalog.js +162 -0
- package/dist/presets/catalog.js.map +1 -0
- package/dist/presets/contract.js +140 -0
- package/dist/presets/contract.js.map +1 -0
- package/dist/presets/types.js +2 -0
- package/dist/presets/types.js.map +1 -0
- package/dist/preview-server/index.js +143 -0
- package/dist/preview-server/index.js.map +1 -0
- package/dist/screens.js +52 -0
- package/dist/screens.js.map +1 -0
- package/dist/session.js +50 -0
- package/dist/session.js.map +1 -0
- package/dist/workspace/index.js +149 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/schemas.js +48 -0
- package/dist/workspace/schemas.js.map +1 -0
- package/package.json +47 -0
- package/preview-app/index.html +12 -0
- package/preview-app/src/App.tsx +53 -0
- package/preview-app/src/FlowCanvas.tsx +208 -0
- package/preview-app/src/Gallery.tsx +35 -0
- package/preview-app/src/api.ts +51 -0
- package/preview-app/src/main.tsx +10 -0
- package/preview-app/src/styles.css +173 -0
- package/preview-app/src/useLiveReload.ts +17 -0
- package/preview-app/src/vite-env.d.ts +1 -0
- 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
|
package/dist/flow.js.map
ADDED
|
@@ -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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"}
|