jig-ui 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +822 -0
- package/README.md +107 -23
- package/dist/index.js +300 -15
- package/package.json +3 -2
- package/rules/00-anti-patterns.md +24 -14
- package/rules/01-modes.md +0 -14
- package/rules/02-tokens.md +80 -5
- package/rules/03-patterns.md +0 -14
- package/rules/04-principles.md +0 -14
- package/rules/05-copy.md +0 -10
- package/rules.index.json +12 -9
- package/templates/SKILL.md.tmpl +24 -1
package/README.md
CHANGED
|
@@ -75,25 +75,63 @@ skill.
|
|
|
75
75
|
npx jig-ui@latest init
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
`init` is the only command that writes into your repo. It
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
the most frequent literal colour — rather than interviewing you cold, validates
|
|
82
|
-
that colour against the contrast and collision requirements in Jig's own brand
|
|
83
|
-
file, writes the token files, wires the `@import`s into your stylesheet when
|
|
84
|
-
there is one unambiguous place for them, and runs a baseline `check` so you have
|
|
85
|
-
a number to move.
|
|
78
|
+
`init` is the only command that writes into your repo. It runs on a project
|
|
79
|
+
with nothing in it and on one with years of CSS, and behaves differently in
|
|
80
|
+
each — because the useful thing to do differs.
|
|
86
81
|
|
|
87
|
-
|
|
82
|
+
### An existing site
|
|
83
|
+
|
|
84
|
+
`init` reads what you already have. It detects the CSS system (Tailwind v4,
|
|
85
|
+
Tailwind v3, plain CSS), derives a brand colour from the project rather than
|
|
86
|
+
interviewing you cold — custom properties first, then a Tailwind config, then
|
|
87
|
+
the most frequent literal colour — and validates that colour against the
|
|
88
|
+
contrast and collision requirements in Jig's own brand file. It puts the token
|
|
89
|
+
layer **beside the stylesheet it wires**, and adds one import to it:
|
|
88
90
|
|
|
89
91
|
```
|
|
92
|
+
src/styles/app.css ← @import "./jig/theme.css"; added at the top
|
|
93
|
+
src/styles/jig/
|
|
94
|
+
brand.<project>.css your identity, edit freely
|
|
95
|
+
mode.<mode>.css a copy of Jig's mode file, refreshed by `update`
|
|
96
|
+
theme.css the barrel: brand + mode, in order
|
|
90
97
|
jig.config.json route → mode map
|
|
91
|
-
.jig/
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
.jig/state.json bookkeeping — version, modes, checksums
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Nothing you wrote is touched beyond that one import line. Re-running `init`
|
|
102
|
+
never overwrites a config or brand file you have edited.
|
|
103
|
+
|
|
104
|
+
### A brand-new site
|
|
105
|
+
|
|
106
|
+
There is no CSS to read, so there is nothing to derive from and nowhere obvious
|
|
107
|
+
to wire. `init` says so rather than guessing:
|
|
108
|
+
|
|
96
109
|
```
|
|
110
|
+
Detected: unknown
|
|
111
|
+
Token layer: jig/ — no stylesheet found to follow, so the project root.
|
|
112
|
+
Could not find a single unambiguous stylesheet to wire the import into.
|
|
113
|
+
Add this near the top of your global stylesheet:
|
|
114
|
+
@import "./jig/theme.css";
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The brand colour resolves to the unbranded near-black default, which ships a
|
|
118
|
+
coherent monochrome UI and makes the missing decision visible instead of
|
|
119
|
+
inventing a purple (`A-01`). Set `--brand-h/-s/-l` in the brand file when you
|
|
120
|
+
have decided, or tell your agent to ask you.
|
|
121
|
+
|
|
122
|
+
Add `--yes` to accept every derived default non-interactively — the mode CI and
|
|
123
|
+
agents run in.
|
|
124
|
+
|
|
125
|
+
### Where the token layer goes
|
|
126
|
+
|
|
127
|
+
Beside the stylesheet it wires, so it sits with the rest of your CSS rather than
|
|
128
|
+
in a dotfolder next to your lockfile. `src/styles/jig/` in a project whose CSS
|
|
129
|
+
lives in `src/styles/`, `app/assets/stylesheets/jig/` in a Rails app, `jig/` at
|
|
130
|
+
the root when there is no stylesheet to follow. `init` prints the path it chose.
|
|
131
|
+
|
|
132
|
+
Set `brand` in `jig.config.json` to put it somewhere else. Projects set up
|
|
133
|
+
before 0.7.0 keep their `.jig/tokens/` layout; `update` does not move them, and
|
|
134
|
+
`init` offers to.
|
|
97
135
|
|
|
98
136
|
The mode file is the one thing genuinely copied: a stylesheet `@import` is an
|
|
99
137
|
edge in a build graph and has to resolve locally, on every machine that builds.
|
|
@@ -143,7 +181,7 @@ on the result — the CLI reports, the agent applies the judgment half.
|
|
|
143
181
|
| Slash command | Equivalent |
|
|
144
182
|
| --- | --- |
|
|
145
183
|
| `/jig init` | `jig init` — then states the mode it chose and what it wired |
|
|
146
|
-
| `/jig check` | `jig check` — then applies the
|
|
184
|
+
| `/jig check` | `jig check` — then applies the 94 judgment rules and reports both halves |
|
|
147
185
|
| `/jig explain C-19` | `jig explain C-19` — prints the rule as-is, without paraphrasing it |
|
|
148
186
|
| `/jig explain contrast` | `jig explain contrast` — every rule matching a word, when you do not have an id |
|
|
149
187
|
| `/jig install --agent cursor` | `jig install --agent cursor` |
|
|
@@ -224,7 +262,7 @@ In CI:
|
|
|
224
262
|
code — nothing model-dependent, no network. As a pre-commit hook, plain `check`
|
|
225
263
|
looks at changed files only.
|
|
226
264
|
|
|
227
|
-
What you will not get from the CLI alone is the other
|
|
265
|
+
What you will not get from the CLI alone is the other 94 rules. `check` says so
|
|
228
266
|
rather than letting a narrow pass read as a broad one.
|
|
229
267
|
|
|
230
268
|
## What `check` covers
|
|
@@ -251,7 +289,7 @@ contrast below the floor (`C-19`), removed focus rings (`E-29`), gradient text
|
|
|
251
289
|
(`A-02`), backdrop blur (`A-04`), pure black and white (`C-18`), and the
|
|
252
290
|
violet-band hue check (`A-01`, which asks rather than fails).
|
|
253
291
|
|
|
254
|
-
**It also reads the token layer itself.**
|
|
292
|
+
**It also reads the token layer itself.** The token layer is not application
|
|
255
293
|
code, so no detector scans it — but it is where a mistake costs most, since every
|
|
256
294
|
call site inherits it. `check` reads back what is declared there and holds it to
|
|
257
295
|
the floors the token layer claims: 4.5:1 for text roles, 3:1 for interface
|
|
@@ -298,8 +336,9 @@ treatment.
|
|
|
298
336
|
| `rules/03-patterns.md` | Component anatomy and behaviour |
|
|
299
337
|
| `rules/04-principles.md` | Five frames + seven tiebreakers |
|
|
300
338
|
| `rules/05-copy.md` | Interface text rules |
|
|
301
|
-
|
|
|
302
|
-
|
|
|
339
|
+
| `<css dir>/jig/brand.*.css` | Identity. One per project. |
|
|
340
|
+
| `<css dir>/jig/mode.*.css` | Density, scale, rhythm, motion |
|
|
341
|
+
| `<css dir>/jig/theme.css` | The barrel — brand + mode. This is what you import. |
|
|
303
342
|
|
|
304
343
|
`rules/*` and `rules.index.json` live beside your installed skill file, not
|
|
305
344
|
in the project — see above.
|
|
@@ -314,7 +353,8 @@ Drop this in the project root so mode selection does not require asking on every
|
|
|
314
353
|
// jig.config.json
|
|
315
354
|
{
|
|
316
355
|
// Where the token layer lives. `init` writes the brand file here and puts
|
|
317
|
-
// the mode files beside it. Omit it and
|
|
356
|
+
// the mode files beside it. Omit it and the layer follows your own
|
|
357
|
+
// layout — beside the stylesheet init wires, or the project root.
|
|
318
358
|
"brand": "src/styles/jig/brand.acme.css",
|
|
319
359
|
|
|
320
360
|
// One entry per surface. This outranks an agent's own reading of the
|
|
@@ -346,12 +386,56 @@ Without this file, follow the selection procedure in `rules/01-modes.md`: infer,
|
|
|
346
386
|
|
|
347
387
|
## Consuming tokens
|
|
348
388
|
|
|
389
|
+
### Plain CSS, any framework
|
|
390
|
+
|
|
391
|
+
One import, the barrel:
|
|
392
|
+
|
|
349
393
|
```css
|
|
350
|
-
@import "
|
|
351
|
-
@import ".jig/tokens/mode.product.css"; /* one per surface */
|
|
394
|
+
@import "./jig/theme.css";
|
|
352
395
|
```
|
|
353
396
|
|
|
354
|
-
Then `var(--color-text-strong)`, `var(--spacing-card)`, `var(--text-body)`
|
|
397
|
+
Then `var(--color-text-strong)`, `var(--spacing-card)`, `var(--text-body)`
|
|
398
|
+
anywhere — plain CSS, CSS modules, styled-components, Vue, Svelte, Rails. They
|
|
399
|
+
are ordinary custom properties and nothing takes a dependency on anything.
|
|
400
|
+
|
|
401
|
+
### Tailwind v4
|
|
402
|
+
|
|
403
|
+
The same import. Jig needs nothing from Tailwind and Tailwind needs nothing from
|
|
404
|
+
Jig:
|
|
405
|
+
|
|
406
|
+
```css
|
|
407
|
+
@import "tailwindcss";
|
|
408
|
+
@import "./jig/theme.css";
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
That is what `init` wires, and it is enough. Every token is readable as
|
|
412
|
+
`var(--color-text-strong)` from any component.
|
|
413
|
+
|
|
414
|
+
**Optionally**, Tailwind can also generate utility classes from the tokens —
|
|
415
|
+
`p-card`, `rounded-surface`, `text-text-strong`. It only does that for names
|
|
416
|
+
declared in a `@theme` block, so `init` offers to generate one:
|
|
417
|
+
|
|
418
|
+
```css
|
|
419
|
+
@import "tailwindcss";
|
|
420
|
+
@import "./jig/utilities.css"; /* the generated @theme block */
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
`init` asks before writing it, because it changes how every component in the
|
|
424
|
+
project is written and both styles are correct. Under `--yes` it declines and
|
|
425
|
+
tells you how to get it.
|
|
426
|
+
|
|
427
|
+
One set of utilities serves every mode: the utility references the variable
|
|
428
|
+
rather than a resolved value, so whichever mode barrel a route loaded supplies
|
|
429
|
+
it. No `dark:` variants, nothing per-mode.
|
|
430
|
+
|
|
431
|
+
**Do not nest the import inside `@theme`.** Tailwind rejects it — *"@theme
|
|
432
|
+
blocks must only contain custom properties or @keyframes"* — and Jig's tokens
|
|
433
|
+
cannot move into one regardless, since they live in `:root` and are redeclared
|
|
434
|
+
under `[data-theme="dark"]` and a `prefers-color-scheme` query. That structure
|
|
435
|
+
is what makes dark mode work.
|
|
436
|
+
|
|
437
|
+
Full detail, including why a duplicate declaration in the compiled CSS is
|
|
438
|
+
correct and must not be "fixed": `rules/02-tokens.md`.
|
|
355
439
|
|
|
356
440
|
---
|
|
357
441
|
|
package/dist/index.js
CHANGED
|
@@ -1013,15 +1013,26 @@ function parseRules(markdown, sourceFile) {
|
|
|
1013
1013
|
title,
|
|
1014
1014
|
wrong: "",
|
|
1015
1015
|
correction: "",
|
|
1016
|
+
notes: [],
|
|
1017
|
+
preamble: [],
|
|
1016
1018
|
source: `${sourceFile}#${id.toLowerCase()}`
|
|
1017
1019
|
};
|
|
1018
1020
|
continue;
|
|
1019
1021
|
}
|
|
1020
1022
|
if (!current) continue;
|
|
1023
|
+
if (line.startsWith("#")) {
|
|
1024
|
+
push();
|
|
1025
|
+
current = null;
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1021
1028
|
if (line.startsWith("\u274C") && !current.wrong) {
|
|
1022
1029
|
current.wrong = line.slice(1).trim();
|
|
1023
1030
|
} else if (line.startsWith("\u2705") && !current.correction) {
|
|
1024
1031
|
current.correction = line.slice(1).trim();
|
|
1032
|
+
} else {
|
|
1033
|
+
const text = line.trim();
|
|
1034
|
+
if (!text || /^-{3,}$/.test(text)) continue;
|
|
1035
|
+
(current.correction ? current.notes : current.preamble).push(text);
|
|
1025
1036
|
}
|
|
1026
1037
|
}
|
|
1027
1038
|
push();
|
|
@@ -1114,6 +1125,7 @@ function parseSpecs(markdown, sourceFile) {
|
|
|
1114
1125
|
current = null;
|
|
1115
1126
|
continue;
|
|
1116
1127
|
}
|
|
1128
|
+
if (current && /^-{3,}$/.test(line.trim())) continue;
|
|
1117
1129
|
if (current) body.push(line);
|
|
1118
1130
|
}
|
|
1119
1131
|
push();
|
|
@@ -1140,8 +1152,16 @@ function renderRule(rule) {
|
|
|
1140
1152
|
return [
|
|
1141
1153
|
`${rule.id} ${rule.title}`,
|
|
1142
1154
|
"",
|
|
1155
|
+
// Source order: what the rule leads with comes first. C-49's table sets up
|
|
1156
|
+
// the distinction its pair then relies on, and reading them the other way
|
|
1157
|
+
// round is reading the answer before the question.
|
|
1158
|
+
...rule.preamble.length > 0 ? [...rule.preamble, ""] : [],
|
|
1143
1159
|
`\u274C ${rule.wrong}`,
|
|
1144
1160
|
`\u2705 ${rule.correction}`,
|
|
1161
|
+
// The reasoning, where the rule carries any. This was dropped at the parser
|
|
1162
|
+
// for three releases, so `explain` showed the pair and none of the argument
|
|
1163
|
+
// for it — which is the half that tells you when the rule does not apply.
|
|
1164
|
+
...rule.notes.length > 0 ? ["", ...rule.notes] : [],
|
|
1145
1165
|
"",
|
|
1146
1166
|
` ${rule.bucket} \xB7 ${rule.severity}${rule.detector ? ` \xB7 detector: ${rule.detector}` : ""}`,
|
|
1147
1167
|
` since ${rule.since} \xB7 ${rule.source}`
|
|
@@ -1410,7 +1430,9 @@ function formatReport(findings, meta) {
|
|
|
1410
1430
|
}
|
|
1411
1431
|
lines.push("");
|
|
1412
1432
|
} else {
|
|
1413
|
-
lines.push(
|
|
1433
|
+
lines.push(
|
|
1434
|
+
meta.withStyles === 0 && meta.scanned !== void 0 ? " Nothing inspected." : " No findings."
|
|
1435
|
+
);
|
|
1414
1436
|
lines.push("");
|
|
1415
1437
|
if (meta.noTokenLayer) {
|
|
1416
1438
|
lines.push(" No file references a Jig token, so H-47 (hard-coded values) was not run.");
|
|
@@ -1426,7 +1448,14 @@ function formatReport(findings, meta) {
|
|
|
1426
1448
|
if (notes > 0) summaryParts.push(plural(notes, "note"));
|
|
1427
1449
|
const rulesFired = new Set(findings.map((f) => f.ruleId)).size;
|
|
1428
1450
|
const scope = meta.totalSpecs ? `${meta.totalRules} rules (+ ${meta.totalSpecs} pattern and mode specs)` : `${meta.totalRules} rules`;
|
|
1429
|
-
|
|
1451
|
+
const examined = meta.scanned === void 0 ? "" : ` \xB7 ${plural(meta.scanned, "file")}, ${meta.withStyles ?? 0} with styles`;
|
|
1452
|
+
lines.push(` ${summaryParts.join(", ")} \xB7 ${scope}${examined}, ${rulesFired} fired`);
|
|
1453
|
+
if (meta.scanned !== void 0 && meta.withStyles === 0) {
|
|
1454
|
+
lines.push("");
|
|
1455
|
+
lines.push(
|
|
1456
|
+
` No file carried a style region, so the detectors examined nothing. This is not a pass \u2014 a project with no styling and a project with clean styling report the same findings, and only one of them has been checked.`
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1430
1459
|
if (meta.exemptPatterns && meta.exemptPatterns.length > 0) {
|
|
1431
1460
|
const n = meta.exempt?.length ?? 0;
|
|
1432
1461
|
lines.push(` ${n} file(s) exempt via jig.config.json and not scanned:`);
|
|
@@ -1448,7 +1477,7 @@ function formatReport(findings, meta) {
|
|
|
1448
1477
|
const mechanicalErrors = findings.filter((f) => f.bucket === "mechanical" && f.severity === "error").length;
|
|
1449
1478
|
const mechStatus = `${mechanicalErrors > 0 ? "fail" : "pass"}:${mechanicalErrors}`;
|
|
1450
1479
|
lines.push(
|
|
1451
|
-
` JIG_CHECK: version=${meta.version} mode=${meta.mode ?? "unknown"} mechanical=${mechStatus} judgment=not-run`
|
|
1480
|
+
` JIG_CHECK: version=${meta.version} mode=${meta.mode ?? "unknown"} mechanical=${mechStatus} judgment=not-run files=${meta.scanned ?? "unknown"} styled=${meta.withStyles ?? "unknown"}`
|
|
1452
1481
|
);
|
|
1453
1482
|
return lines.join("\n");
|
|
1454
1483
|
}
|
|
@@ -1622,7 +1651,7 @@ function splitRuleBlocks(source) {
|
|
|
1622
1651
|
}
|
|
1623
1652
|
function blankNested(body) {
|
|
1624
1653
|
const out = body.split("");
|
|
1625
|
-
const
|
|
1654
|
+
const blank3 = (from, to) => {
|
|
1626
1655
|
for (let j = from; j < to; j++) if (out[j] !== "\n") out[j] = " ";
|
|
1627
1656
|
};
|
|
1628
1657
|
let depth = 0;
|
|
@@ -1630,7 +1659,7 @@ function blankNested(body) {
|
|
|
1630
1659
|
for (let i = 0; i < body.length; i++) {
|
|
1631
1660
|
const ch = body[i];
|
|
1632
1661
|
if (ch === "{") {
|
|
1633
|
-
if (depth === 0)
|
|
1662
|
+
if (depth === 0) blank3(segmentStart, i);
|
|
1634
1663
|
depth++;
|
|
1635
1664
|
out[i] = " ";
|
|
1636
1665
|
} else if (ch === "}") {
|
|
@@ -1940,6 +1969,126 @@ var gradientText = {
|
|
|
1940
1969
|
}
|
|
1941
1970
|
};
|
|
1942
1971
|
|
|
1972
|
+
// src/check/detectors/text-scan.ts
|
|
1973
|
+
var blank2 = (m) => m.replace(/[^\n]/g, " ");
|
|
1974
|
+
function maskProseComments(raw) {
|
|
1975
|
+
return raw.replace(/\/\*[\s\S]*?\*\//g, blank2).replace(/<!--[\s\S]*?-->/g, blank2).replace(/(^|[^:@\w])\/\/[^\n]*/g, (m, lead) => lead + blank2(m.slice(lead.length)));
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// src/check/detectors/emoji-icon.ts
|
|
1979
|
+
var EMOJI_RE = /[\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F2FF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{FE0F}]/gu;
|
|
1980
|
+
var TEXTUAL = /* @__PURE__ */ new Set(["\u2192", "\u2190", "\u2191", "\u2193", "\u2194", "\u21D2", "\u21D0", "\u2122", "\u2713", "\u2717", "\u2212", "\u2217", "\u2318", "\u2325", "\u23CE"]);
|
|
1981
|
+
var emojiIcon = {
|
|
1982
|
+
name: "emoji-icon",
|
|
1983
|
+
appliesTo: (file) => isStyleBearing(file),
|
|
1984
|
+
run(_source, file, ctx) {
|
|
1985
|
+
const findings = [];
|
|
1986
|
+
const masked = maskProseComments(ctx.raw);
|
|
1987
|
+
const lines = masked.split("\n");
|
|
1988
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1989
|
+
for (const m of masked.matchAll(EMOJI_RE)) {
|
|
1990
|
+
if (TEXTUAL.has(m[0])) continue;
|
|
1991
|
+
const line = masked.slice(0, m.index).split("\n").length;
|
|
1992
|
+
if (seen.has(line)) continue;
|
|
1993
|
+
seen.add(line);
|
|
1994
|
+
findings.push(
|
|
1995
|
+
mkFinding(
|
|
1996
|
+
ctx,
|
|
1997
|
+
"emoji-icon",
|
|
1998
|
+
file,
|
|
1999
|
+
line,
|
|
2000
|
+
`Emoji used as interface iconography (${m[0]}). Emoji render differently on every platform, carry no consistent weight or colour, and cannot be styled \u2014 use an icon set, or words.`,
|
|
2001
|
+
lines[line - 1] ?? ""
|
|
2002
|
+
)
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
return findings;
|
|
2006
|
+
}
|
|
2007
|
+
};
|
|
2008
|
+
|
|
2009
|
+
// src/check/detectors/marketing-voice.ts
|
|
2010
|
+
var PHRASES = [
|
|
2011
|
+
"supercharge",
|
|
2012
|
+
"unleash",
|
|
2013
|
+
"revolutioni",
|
|
2014
|
+
"game-chang",
|
|
2015
|
+
"effortlessly",
|
|
2016
|
+
"seamlessly",
|
|
2017
|
+
"blazing fast",
|
|
2018
|
+
"take it to the next level",
|
|
2019
|
+
"unlock the power",
|
|
2020
|
+
"elevate your",
|
|
2021
|
+
"transform your workflow",
|
|
2022
|
+
"delight your users"
|
|
2023
|
+
];
|
|
2024
|
+
var RE = new RegExp(`\\b(${PHRASES.join("|")})`, "gi");
|
|
2025
|
+
var APPLIES_IN = /* @__PURE__ */ new Set(["product", "operator"]);
|
|
2026
|
+
var marketingVoice = {
|
|
2027
|
+
name: "marketing-voice",
|
|
2028
|
+
appliesTo: (file) => isStyleBearing(file),
|
|
2029
|
+
run(_source, file, ctx) {
|
|
2030
|
+
if (!ctx.mode || !APPLIES_IN.has(ctx.mode)) return [];
|
|
2031
|
+
const findings = [];
|
|
2032
|
+
const masked = maskProseComments(ctx.raw);
|
|
2033
|
+
const lines = masked.split("\n");
|
|
2034
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2035
|
+
for (const m of masked.matchAll(RE)) {
|
|
2036
|
+
const line = masked.slice(0, m.index).split("\n").length;
|
|
2037
|
+
if (seen.has(line)) continue;
|
|
2038
|
+
seen.add(line);
|
|
2039
|
+
findings.push(
|
|
2040
|
+
mkFinding(
|
|
2041
|
+
ctx,
|
|
2042
|
+
"marketing-voice",
|
|
2043
|
+
file,
|
|
2044
|
+
line,
|
|
2045
|
+
`Marketing voice in a \`${ctx.mode}\` surface ("${m[0]}"). Someone using this every day wants to know what it does, not how it feels.`,
|
|
2046
|
+
lines[line - 1] ?? ""
|
|
2047
|
+
)
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
return findings;
|
|
2051
|
+
}
|
|
2052
|
+
};
|
|
2053
|
+
|
|
2054
|
+
// src/check/detectors/placeholder-content.ts
|
|
2055
|
+
var PATTERNS = [
|
|
2056
|
+
{ re: /\blorem\s+ipsum\b/gi, what: "lorem ipsum" },
|
|
2057
|
+
{ re: /https?:\/\/(?:www\.)?example\.(?:com|org|net)\b/gi, what: "an example.com URL" },
|
|
2058
|
+
{ re: /\b(?:someone|user|test|foo|bar)@example\.(?:com|org|net)\b/gi, what: "a placeholder email" },
|
|
2059
|
+
{ re: /\bAcme\s+(?:Inc\.?|Corp\.?|Co\.?|Ltd\.?|LLC|Company)\b/gi, what: "a placeholder company" },
|
|
2060
|
+
{ re: /\byour\s+(?:company|brand|product)\s+name\b/gi, what: "a placeholder name" }
|
|
2061
|
+
];
|
|
2062
|
+
var placeholderContent = {
|
|
2063
|
+
name: "placeholder-content",
|
|
2064
|
+
appliesTo: (file) => isStyleBearing(file),
|
|
2065
|
+
run(_source, file, ctx) {
|
|
2066
|
+
const findings = [];
|
|
2067
|
+
const masked = maskProseComments(ctx.raw);
|
|
2068
|
+
const lines = masked.split("\n");
|
|
2069
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2070
|
+
for (const { re, what } of PATTERNS) {
|
|
2071
|
+
re.lastIndex = 0;
|
|
2072
|
+
for (const m of masked.matchAll(re)) {
|
|
2073
|
+
const line = masked.slice(0, m.index).split("\n").length;
|
|
2074
|
+
if (seen.has(line)) continue;
|
|
2075
|
+
seen.add(line);
|
|
2076
|
+
findings.push(
|
|
2077
|
+
mkFinding(
|
|
2078
|
+
ctx,
|
|
2079
|
+
"placeholder-content",
|
|
2080
|
+
file,
|
|
2081
|
+
line,
|
|
2082
|
+
`Placeholder content shipped \u2014 ${what} ("${m[0]}"). Real content changes layout: names are longer, copy wraps, images are the wrong ratio.`,
|
|
2083
|
+
lines[line - 1] ?? ""
|
|
2084
|
+
)
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
return findings;
|
|
2089
|
+
}
|
|
2090
|
+
};
|
|
2091
|
+
|
|
1943
2092
|
// src/check/color.ts
|
|
1944
2093
|
var NAMED = {
|
|
1945
2094
|
white: { r: 255, g: 255, b: 255 },
|
|
@@ -2596,6 +2745,9 @@ var violetBandHue = {
|
|
|
2596
2745
|
// src/check/registry.ts
|
|
2597
2746
|
var DETECTORS = [
|
|
2598
2747
|
gradientText,
|
|
2748
|
+
emojiIcon,
|
|
2749
|
+
marketingVoice,
|
|
2750
|
+
placeholderContent,
|
|
2599
2751
|
backdropBlur,
|
|
2600
2752
|
pureBlackWhite,
|
|
2601
2753
|
contrastFloor,
|
|
@@ -2608,7 +2760,7 @@ function getDetector(name) {
|
|
|
2608
2760
|
}
|
|
2609
2761
|
|
|
2610
2762
|
// src/check/run.ts
|
|
2611
|
-
function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParticipates = false) {
|
|
2763
|
+
function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParticipates = false, mode) {
|
|
2612
2764
|
const findings = [];
|
|
2613
2765
|
const active = index.filter(
|
|
2614
2766
|
(e) => !!e.detector && !!getDetector(e.detector)
|
|
@@ -2626,7 +2778,7 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
|
|
|
2626
2778
|
}
|
|
2627
2779
|
const source = maskComments(maskNonStyleRegions(raw, file));
|
|
2628
2780
|
for (const { entry, detector } of applicable) {
|
|
2629
|
-
const ctx = { ruleId: entry.id, bucket: entry.bucket, severity: entry.severity, tokens, projectParticipates, raw };
|
|
2781
|
+
const ctx = { ruleId: entry.id, bucket: entry.bucket, severity: entry.severity, tokens, projectParticipates, raw, mode };
|
|
2630
2782
|
findings.push(...detector.run(source, file, ctx));
|
|
2631
2783
|
}
|
|
2632
2784
|
}
|
|
@@ -2927,7 +3079,16 @@ function check(opts) {
|
|
|
2927
3079
|
return false;
|
|
2928
3080
|
}
|
|
2929
3081
|
});
|
|
2930
|
-
const
|
|
3082
|
+
const withStyles = files.filter((f) => {
|
|
3083
|
+
try {
|
|
3084
|
+
const src = readFileSync14(join19(opts.projectRoot, f), "utf8");
|
|
3085
|
+
return hasExtension(f, CSS_EXTENSIONS) ? src.trim().length > 0 : maskNonStyleRegions(src, f).trim().length > 0;
|
|
3086
|
+
} catch {
|
|
3087
|
+
return false;
|
|
3088
|
+
}
|
|
3089
|
+
}).length;
|
|
3090
|
+
const resolvedMode = resolveMode(opts.projectRoot);
|
|
3091
|
+
const findings = runChecks(opts.projectRoot, files, index, tokens, bucketFilter, projectParticipates, resolvedMode);
|
|
2931
3092
|
const TOKEN_RULE = {
|
|
2932
3093
|
"--text-prose": "B-75",
|
|
2933
3094
|
"--size-touch-target": "E-51"
|
|
@@ -2966,8 +3127,10 @@ function check(opts) {
|
|
|
2966
3127
|
totalSpecs: countSpecs(),
|
|
2967
3128
|
version: opts.version,
|
|
2968
3129
|
noTokenLayer,
|
|
2969
|
-
mode:
|
|
3130
|
+
mode: resolvedMode,
|
|
2970
3131
|
unscanned: summariseUnscanned(files),
|
|
3132
|
+
scanned: files.length,
|
|
3133
|
+
withStyles,
|
|
2971
3134
|
exempt,
|
|
2972
3135
|
exemptPatterns: byPattern
|
|
2973
3136
|
});
|
|
@@ -3365,6 +3528,77 @@ function deriveProjectSlug(projectRoot) {
|
|
|
3365
3528
|
return slugify(basename(projectRoot));
|
|
3366
3529
|
}
|
|
3367
3530
|
|
|
3531
|
+
// src/init/utilities.ts
|
|
3532
|
+
var TAILWIND_NAMESPACES = [
|
|
3533
|
+
"--color-",
|
|
3534
|
+
"--font-",
|
|
3535
|
+
"--text-",
|
|
3536
|
+
"--font-weight-",
|
|
3537
|
+
"--tracking-",
|
|
3538
|
+
"--leading-",
|
|
3539
|
+
"--breakpoint-",
|
|
3540
|
+
"--container-",
|
|
3541
|
+
"--spacing-",
|
|
3542
|
+
"--radius-",
|
|
3543
|
+
"--shadow-",
|
|
3544
|
+
"--inset-shadow-",
|
|
3545
|
+
"--drop-shadow-",
|
|
3546
|
+
"--blur-",
|
|
3547
|
+
"--perspective-",
|
|
3548
|
+
"--aspect-",
|
|
3549
|
+
"--ease-",
|
|
3550
|
+
"--animate-"
|
|
3551
|
+
];
|
|
3552
|
+
function tailwindNamespaced(names) {
|
|
3553
|
+
const kept = names.filter((n) => TAILWIND_NAMESPACES.some((ns) => n.startsWith(ns)));
|
|
3554
|
+
return [...new Set(kept)].sort();
|
|
3555
|
+
}
|
|
3556
|
+
function declaredTokenNames(css) {
|
|
3557
|
+
return [...css.matchAll(/(^|[;{\s])(--[\w-]+)\s*:/g)].map((m) => m[2]);
|
|
3558
|
+
}
|
|
3559
|
+
function utilitiesBody(names, version2) {
|
|
3560
|
+
if (names.length === 0) {
|
|
3561
|
+
throw new Error(
|
|
3562
|
+
"Refusing to write an empty @theme block: it generates no utilities while looking like it should. No token in this layer matches a Tailwind namespace."
|
|
3563
|
+
);
|
|
3564
|
+
}
|
|
3565
|
+
const width = Math.max(...names.map((n) => n.length));
|
|
3566
|
+
const lines = names.map((n) => ` ${n}:${" ".repeat(width - n.length)} var(${n});`);
|
|
3567
|
+
return `/* utilities.css \u2014 vendored from Jig v${version2}.
|
|
3568
|
+
Licensed Apache-2.0. LICENSE and NOTICE ship beside the jig skill.
|
|
3569
|
+
|
|
3570
|
+
Exposes the token layer to Tailwind v4 as utility classes: \`p-card\`,
|
|
3571
|
+
\`rounded-surface\`, \`text-text-strong\`. ONE per project, never one per mode \u2014
|
|
3572
|
+
the utility references the variable rather than a resolved value, so whichever
|
|
3573
|
+
mode barrel a route loads supplies it. One set of utilities serves every mode.
|
|
3574
|
+
|
|
3575
|
+
Import this once, in the stylesheet that imports Tailwind:
|
|
3576
|
+
|
|
3577
|
+
@import "tailwindcss";
|
|
3578
|
+
@import "./utilities.css";
|
|
3579
|
+
|
|
3580
|
+
YOU WILL SEE A DUPLICATE IN THE COMPILED CSS. That is correct:
|
|
3581
|
+
|
|
3582
|
+
@layer theme { :root,:host { --radius-surface: var(--radius-surface) } }
|
|
3583
|
+
:root { --radius-surface: var(--radius-md) }
|
|
3584
|
+
|
|
3585
|
+
The first is Tailwind's, inside \`@layer theme\`. The second is Jig's, unlayered.
|
|
3586
|
+
Unlayered declarations beat layered ones in the cascade regardless of source
|
|
3587
|
+
order, so Jig's value always wins. Do not "fix" it \u2014 deleting the alias
|
|
3588
|
+
removes the utility, deleting Jig's removes the value.
|
|
3589
|
+
|
|
3590
|
+
Only tokens in a Tailwind namespace are listed. \`--size-*\`, \`--measure-*\`,
|
|
3591
|
+
\`--focus-ring-*\` and \`--border-width-*\` have none, so they stay \`var()\`-only.
|
|
3592
|
+
|
|
3593
|
+
Regenerate with \`jig init\` after the token layer changes; a token missing here
|
|
3594
|
+
is a class that renders and matches nothing. */
|
|
3595
|
+
|
|
3596
|
+
@theme inline {
|
|
3597
|
+
${lines.join("\n")}
|
|
3598
|
+
}
|
|
3599
|
+
`;
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3368
3602
|
// src/commands/init.ts
|
|
3369
3603
|
var MODES = ["editorial", "product", "operator"];
|
|
3370
3604
|
var DEFAULT_SURFACES = [{ match: "/", mode: "product" }];
|
|
@@ -3549,7 +3783,7 @@ async function init(opts) {
|
|
|
3549
3783
|
const prompt = opts.prompt ?? defaultPrompt;
|
|
3550
3784
|
if (!opts.yes && !opts.prompt && !process.stdin.isTTY) {
|
|
3551
3785
|
throw new Error(
|
|
3552
|
-
"'jig init' asks questions and stdin is not a terminal, so it cannot. Re-run with --yes to accept the derived defaults, or run it in a terminal.
|
|
3786
|
+
"'jig init' asks questions and stdin is not a terminal, so it cannot. Re-run with --yes to accept the derived defaults, or run it in a terminal. A jig.config.json does not replace --yes, because this check runs before it is read. With both, init takes the mode from the config instead of deriving it."
|
|
3553
3787
|
);
|
|
3554
3788
|
}
|
|
3555
3789
|
const migrateLegacy = async (report, describe) => {
|
|
@@ -3780,6 +4014,56 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
3780
4014
|
writeFileSync4(abs, content, "utf8");
|
|
3781
4015
|
files[rel] = checksum(content);
|
|
3782
4016
|
}
|
|
4017
|
+
if (detection.cssSystem === "tailwind-v4") {
|
|
4018
|
+
const declared = declaredTokenNames(
|
|
4019
|
+
[brandAbsPath, modeAbsPath].map((f) => {
|
|
4020
|
+
try {
|
|
4021
|
+
return readFileSync18(f, "utf8");
|
|
4022
|
+
} catch {
|
|
4023
|
+
return "";
|
|
4024
|
+
}
|
|
4025
|
+
}).join("\n")
|
|
4026
|
+
);
|
|
4027
|
+
const aliasable = tailwindNamespaced(declared);
|
|
4028
|
+
const rel = relKey(...tokensRelDir, "utilities.css");
|
|
4029
|
+
if (aliasable.length === 0) {
|
|
4030
|
+
log(`
|
|
4031
|
+
Tailwind v4 detected, but no token here matches a Tailwind namespace \u2014 skipping ${rel}.`);
|
|
4032
|
+
} else if (opts.yes) {
|
|
4033
|
+
log(`
|
|
4034
|
+
Tailwind v4 detected. Utility classes (\`p-card\`, \`rounded-surface\`) are`);
|
|
4035
|
+
log(` available but not set up: re-run without --yes to be offered ${rel},`);
|
|
4036
|
+
log(` or see "Optional: Tailwind utility classes" in 02-tokens.md.`);
|
|
4037
|
+
} else {
|
|
4038
|
+
log(`
|
|
4039
|
+
Tailwind v4 detected. Jig works as-is \u2014 every token reads as`);
|
|
4040
|
+
log(` var(--color-text-strong) from any component, and nothing further is needed.`);
|
|
4041
|
+
log(` Optionally, ${aliasable.length} tokens can also become utility classes`);
|
|
4042
|
+
log(` (\`p-card\`, \`rounded-surface\`, \`text-text-strong\`) via ${rel}.`);
|
|
4043
|
+
log(` This changes how components are written. Both styles are correct.`);
|
|
4044
|
+
const answer = (await prompt(" Generate it? [y/N]: ")).toLowerCase();
|
|
4045
|
+
if (answer === "y" || answer === "yes") {
|
|
4046
|
+
const abs = join23(opts.projectRoot, ...tokensRelDir, "utilities.css");
|
|
4047
|
+
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
4048
|
+
if (state.existsOnDisk && (!state.tracked || state.modified)) {
|
|
4049
|
+
log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
|
|
4050
|
+
} else {
|
|
4051
|
+
const content = utilitiesBody(aliasable, opts.version);
|
|
4052
|
+
mkdirSync5(dirname6(abs), { recursive: true });
|
|
4053
|
+
writeFileSync4(abs, content, "utf8");
|
|
4054
|
+
files[rel] = checksum(content);
|
|
4055
|
+
const target = findWireTarget(detection);
|
|
4056
|
+
const importPath = target ? relativeImportPath(
|
|
4057
|
+
dirname6(join23(opts.projectRoot, target)),
|
|
4058
|
+
join23(opts.projectRoot, ...tokensRelDir, "utilities.css")
|
|
4059
|
+
) : `./${rel}`;
|
|
4060
|
+
log(` Wrote ${rel}. Import it in the stylesheet that imports Tailwind:`);
|
|
4061
|
+
log(` @import "tailwindcss";`);
|
|
4062
|
+
log(` @import "${importPath}";`);
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
3783
4067
|
if (declaredModes.length > 1) {
|
|
3784
4068
|
log("\nOne barrel per surface. Import each at that route's entry point:");
|
|
3785
4069
|
for (const surface of effectiveConfig.surfaces) {
|
|
@@ -3836,10 +4120,11 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
3836
4120
|
log(` ${snippet.split("\n").join("\n ")}`);
|
|
3837
4121
|
}
|
|
3838
4122
|
} else {
|
|
3839
|
-
const
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
4123
|
+
const barrelImport = relativeImportPath(
|
|
4124
|
+
opts.projectRoot,
|
|
4125
|
+
join23(opts.projectRoot, ...tokensRelDir, "theme.css")
|
|
4126
|
+
);
|
|
4127
|
+
const snippet = `@import "${barrelImport}";`;
|
|
3843
4128
|
wiring = { target: null, status: "print-only", snippet };
|
|
3844
4129
|
log("\nCould not find a single unambiguous stylesheet to wire the import into.");
|
|
3845
4130
|
const pasteTargets = detection.cssFiles.filter((f) => !isCssModule(f));
|
|
@@ -3855,7 +4140,7 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
3855
4140
|
}
|
|
3856
4141
|
} else {
|
|
3857
4142
|
log(
|
|
3858
|
-
"Add this near the top of your global stylesheet. A CSS @import resolves relative to the file it sits in, so
|
|
4143
|
+
"Add this near the top of your global stylesheet. A CSS @import resolves relative to the file it sits in, so this project-root path needs a ../ per directory of depth:"
|
|
3859
4144
|
);
|
|
3860
4145
|
log(` ${snippet.split("\n").join("\n ")}`);
|
|
3861
4146
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jig-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "A design system for coding agents. 104 numbered UI rules, brand x mode design tokens, and an installer for Claude Code, Codex, Cursor and opencode.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"rules.index.json",
|
|
17
17
|
"LICENSE",
|
|
18
18
|
"NOTICE",
|
|
19
|
-
"README.md"
|
|
19
|
+
"README.md",
|
|
20
|
+
"CHANGELOG.md"
|
|
20
21
|
],
|
|
21
22
|
"engines": {
|
|
22
23
|
"node": ">=20"
|