jig-ui 0.8.1 → 0.8.2

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 CHANGED
@@ -1,5 +1,84 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.2
4
+
5
+ Every fix here was found by a consumer using Jig rather than by Jig checking
6
+ itself: the documentation site was upgraded to 0.8.1 and then styled in
7
+ Tailwind, which is the first time Jig's Tailwind guidance had been followed
8
+ end to end by anything other than its own tests.
9
+
10
+ Three of the four are the same shape — two places answering one question, and
11
+ disagreeing without either knowing the other existed.
12
+
13
+ ### Fixed
14
+
15
+ - **`H-47` read a value differently depending on how it was spelled.**
16
+ `00-anti-patterns.md:11` sets the scope of the whole rule file: "**Framework:**
17
+ agnostic. […] Where a utility-class framework is in use, translate — the rule
18
+ is about the resulting style, not the syntax." The detector was the exact
19
+ inverse, in both directions at once. Its CSS branch matched `px` alone and
20
+ excluded `0/1px/2px`; its Tailwind branch matched ten units and excluded
21
+ nothing. So `font-size: 0.9em` was silent while `text-[0.9em]` was an error,
22
+ and `p-[1px]` was an error while `padding: 1px` was not.
23
+
24
+ A project on plain CSS got a clean `check` for code a Tailwind project got
25
+ eight errors for. Found by converting a real stylesheet to utilities without
26
+ changing one computed value and watching the report go from `No findings` to
27
+ eight; the two declarations responsible had been in that file since it was
28
+ written and had never been flagged. Both branches now read one definition.
29
+
30
+ **This widens what `check` reports.** A stylesheet carrying `1rem` or `0.9em`
31
+ past the token layer was always an H-47 violation and is now reported as one,
32
+ so a repo that was clean under 0.8.1 can have findings here without its CSS
33
+ having moved.
34
+
35
+ - **The naming contract named namespaces Tailwind does not have.**
36
+ `02-tokens.md` opened with "an alias block can expose **any of them** as
37
+ Tailwind utilities" over a table of thirteen. Three generate nothing in
38
+ Tailwind v4 — `--duration-*`, `--measure-*` and `--focus-ring-*` — and
39
+ `--duration-*` shared a row with `--ease-*`, so only half of that row worked.
40
+ Aliasing one is accepted, emits the custom property, and produces no rule, so
41
+ the class lands on the element and does nothing: the same silent failure the
42
+ file warns about 190 lines later, reached from the opposite direction. The
43
+ table now carries a **Utility** column, and the three say how to be read from
44
+ a class instead — `max-w-(--measure-prose)`, which keeps the semantic token
45
+ and satisfies `H-47` without going through `@theme`.
46
+
47
+ `init`'s own generator was wrong in both directions. It correctly filtered
48
+ `--measure-*` and `--focus-ring-*`, and it also filtered `--size-*` and
49
+ `--border-width-*`, which both generate working utilities — `size-control`
50
+ sets width and height, `border-hairline` sets a border width. Jig was
51
+ withholding correct aliases for its own tokens, and a test asserted that as
52
+ correct, which is how it survived.
53
+
54
+ - **`check` did not say which files it had looked at.** It defaults to the
55
+ files changed since HEAD and falls back to the whole repo when that diff is
56
+ empty. Nothing in the output said so, so the same repo reported `files=31` on
57
+ a clean tree and `files=9` with nine files touched, minutes apart, with
58
+ nothing committed — and a consumer bisected it by reverting files one at a
59
+ time to find out why.
60
+
61
+ Worse, `mechanical=pass:0` on a dirty tree means "nothing in your diff
62
+ fired", not "the project is clean", and the skill tells an agent to run
63
+ `check` before finishing — exactly when the tree is dirty and the scope is
64
+ narrowest. The exempt line compounded it: `src/content/rules.ts (matches
65
+ nothing — check the path)` was printed for a path that exists, is tracked,
66
+ and had matched a file on the previous run. The glob was fine; it matched
67
+ nothing *within the narrowed set*, and "check the path" sends you to debug a
68
+ correct config.
69
+
70
+ The summary now names the scope, a narrowed run says a clean result is not a
71
+ clean project, the exempt note distinguishes "no such path" from "not in this
72
+ scan", and `--all`'s help says it widens the files rather than the rules —
73
+ which is how it had been read.
74
+
75
+ - **A flag written across two lines vanished from the metadata guard.**
76
+ `registeredFlags` read `src/index.ts` line by line and required the flag
77
+ string to sit on the same line as `.option(`, so reformatting one option to
78
+ fit a longer description dropped `--all` from the parsed set and from every
79
+ guard built on it. Caught by its own canary the moment an option wrapped.
80
+
81
+
3
82
  ## 0.8.1
4
83
 
5
84
  Both fixes here are the same shape: 0.8.0 corrected the instance it was looking
package/dist/index.js CHANGED
@@ -1448,7 +1448,7 @@ function formatReport(findings, meta) {
1448
1448
  if (notes > 0) summaryParts.push(plural(notes, "note"));
1449
1449
  const rulesFired = new Set(findings.map((f) => f.ruleId)).size;
1450
1450
  const scope = meta.totalSpecs ? `${meta.totalRules} rules (+ ${meta.totalSpecs} pattern and mode specs)` : `${meta.totalRules} rules`;
1451
- const examined = meta.scanned === void 0 ? "" : ` \xB7 ${plural(meta.scanned, "file")}, ${meta.withStyles ?? 0} with styles`;
1451
+ const examined = meta.scanned === void 0 ? "" : ` \xB7 ${plural(meta.scanned, "file")}${meta.scope === "changed" ? " changed since HEAD" : ""}, ${meta.withStyles ?? 0} with styles`;
1452
1452
  lines.push(` ${summaryParts.join(", ")} \xB7 ${scope}${examined}, ${rulesFired} fired`);
1453
1453
  if (meta.scanned !== void 0 && meta.withStyles === 0) {
1454
1454
  lines.push("");
@@ -1456,11 +1456,17 @@ function formatReport(findings, meta) {
1456
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
1457
  );
1458
1458
  }
1459
+ if (meta.scope === "changed") {
1460
+ lines.push("");
1461
+ lines.push(
1462
+ ` Scope: files changed since HEAD, not the whole project \u2014 this is the inner-loop default. A clean result here means nothing in your diff fired, not that the project is clean. Run 'jig check --all' for that.`
1463
+ );
1464
+ }
1459
1465
  if (meta.exemptPatterns && meta.exemptPatterns.length > 0) {
1460
1466
  const n = meta.exempt?.length ?? 0;
1461
1467
  lines.push(` ${n} file(s) exempt via jig.config.json and not scanned:`);
1462
1468
  for (const { pattern, count, tooBroad } of meta.exemptPatterns) {
1463
- const note = count === 0 ? "matches nothing \u2014 check the path" : tooBroad ? `${count} files \u2014 likely too broad, review it` : `${count} file${count > 1 ? "s" : ""}`;
1469
+ const note = count === 0 ? meta.scope === "changed" ? "matches nothing among the changed files \u2014 not necessarily wrong" : "matches nothing \u2014 check the path" : tooBroad ? `${count} files \u2014 likely too broad, review it` : `${count} file${count > 1 ? "s" : ""}`;
1464
1470
  lines.push(` ${pattern} (${note})`);
1465
1471
  }
1466
1472
  if (n > 0) {
@@ -2361,6 +2367,35 @@ var pureBlackWhite = {
2361
2367
  }
2362
2368
  };
2363
2369
 
2370
+ // src/check/length.ts
2371
+ var LENGTH_UNITS = [
2372
+ "px",
2373
+ "rem",
2374
+ "em",
2375
+ "pt",
2376
+ "vh",
2377
+ "vw",
2378
+ "vmin",
2379
+ "vmax",
2380
+ "ch",
2381
+ "ex"
2382
+ ];
2383
+ var UNITS = LENGTH_UNITS.join("|");
2384
+ var LENGTH_RE = new RegExp(`^-?\\d*\\.?\\d+(?:${UNITS})$`, "i");
2385
+ var LENGTH_SCAN_RE = new RegExp(`(-?\\d*\\.?\\d+)(${UNITS})`, "gi");
2386
+ function isExcludedLength(value, unit) {
2387
+ if (value === 0) return true;
2388
+ return unit.toLowerCase() === "px" && (Math.abs(value) === 1 || Math.abs(value) === 2);
2389
+ }
2390
+ function hasHardCodedLength(value) {
2391
+ LENGTH_SCAN_RE.lastIndex = 0;
2392
+ let m;
2393
+ while (m = LENGTH_SCAN_RE.exec(value)) {
2394
+ if (!isExcludedLength(parseFloat(m[1]), m[2])) return true;
2395
+ }
2396
+ return false;
2397
+ }
2398
+
2364
2399
  // src/check/tailwind.ts
2365
2400
  var CLASS_ATTR = /\b(?:class|className)\s*=\s*/g;
2366
2401
  function readBraced(source, open) {
@@ -2404,15 +2439,21 @@ function classAttributeValues(source) {
2404
2439
  }
2405
2440
  var ARBITRARY = /(?:^|\s)(?:[\w-]+:)*([a-z][\w-]*)-\[([^\]\s]+)\]/gi;
2406
2441
  var COLOUR = /^(?:#[0-9a-f]{3,8}|(?:rgba?|hsla?|oklch|oklab|lab|lch|color)\()/i;
2407
- var LENGTH = /^-?\d*\.?\d+(?:px|rem|em|pt|vh|vw|vmin|vmax|ch|ex)$/i;
2442
+ var LENGTH_PARTS = /^(-?\d*\.?\d+)([a-z]+)$/i;
2408
2443
  function arbitraryValues(classes) {
2409
2444
  const out = [];
2410
2445
  for (const m of classes.matchAll(ARBITRARY)) {
2411
2446
  const [, utility, rawValue] = m;
2412
2447
  const value = rawValue.replace(/_/g, " ");
2413
2448
  if (/var\(|--/.test(value)) continue;
2414
- if (COLOUR.test(value)) out.push({ utility, value, kind: "colour" });
2415
- else if (LENGTH.test(value)) out.push({ utility, value, kind: "length" });
2449
+ if (COLOUR.test(value)) {
2450
+ out.push({ utility, value, kind: "colour" });
2451
+ continue;
2452
+ }
2453
+ if (!LENGTH_RE.test(value)) continue;
2454
+ const parts = LENGTH_PARTS.exec(value);
2455
+ if (isExcludedLength(parseFloat(parts[1]), parts[2])) continue;
2456
+ out.push({ utility, value, kind: "length" });
2416
2457
  }
2417
2458
  return out;
2418
2459
  }
@@ -2604,8 +2645,6 @@ var SPACING_PROPS = /* @__PURE__ */ new Set([
2604
2645
  ]);
2605
2646
  var DECL_RE2 = /(?<![-\w])([a-zA-Z-]+)\s*:\s*([^;]+)(?:;|$)/g;
2606
2647
  var COLOR_LITERAL_RE = /#[0-9a-fA-F]{3,8}\b|(?:rgb|hsl)a?\([^)]*\)/;
2607
- var PX_RE = /(-?\d*\.?\d+)px/g;
2608
- var EXCLUDED_PX = /* @__PURE__ */ new Set([0, 1, 2]);
2609
2648
  var KEYFRAME_STEP_RE = /^(from|to|\d+(\.\d+)?%)$/i;
2610
2649
  var PRIMITIVE_RE = /var\(\s*(--(?:brand|error|warning|success|info)-(?:h|s|l|fill-a))\s*[,)]/g;
2611
2650
  var TOKEN_LAYER_RE = /(^|\/)(\.jig\/tokens|jig)\/[^/]+\.css$/;
@@ -2662,16 +2701,7 @@ var hardcodedValue = {
2662
2701
  continue;
2663
2702
  }
2664
2703
  if (SPACING_PROPS.has(prop)) {
2665
- PX_RE.lastIndex = 0;
2666
- let px;
2667
- let flagged = false;
2668
- while (px = PX_RE.exec(value)) {
2669
- if (!EXCLUDED_PX.has(Math.abs(parseFloat(px[1])))) {
2670
- flagged = true;
2671
- break;
2672
- }
2673
- }
2674
- if (flagged) {
2704
+ if (hasHardCodedLength(value)) {
2675
2705
  const line = lineOfOffset(block, m.index);
2676
2706
  findings.push(
2677
2707
  mkFinding(
@@ -3132,7 +3162,8 @@ function check(opts) {
3132
3162
  scanned: files.length,
3133
3163
  withStyles,
3134
3164
  exempt,
3135
- exemptPatterns: byPattern
3165
+ exemptPatterns: byPattern,
3166
+ scope: selection.mode
3136
3167
  });
3137
3168
  return { findings, report, hasError };
3138
3169
  }
@@ -3547,7 +3578,9 @@ var TAILWIND_NAMESPACES = [
3547
3578
  "--perspective-",
3548
3579
  "--aspect-",
3549
3580
  "--ease-",
3550
- "--animate-"
3581
+ "--animate-",
3582
+ "--size-",
3583
+ "--border-width-"
3551
3584
  ];
3552
3585
  function tailwindNamespaced(names) {
3553
3586
  const kept = names.filter((n) => TAILWIND_NAMESPACES.some((ns) => n.startsWith(ns)));
@@ -3587,8 +3620,10 @@ function utilitiesBody(names, version2) {
3587
3620
  order, so Jig's value always wins. Do not "fix" it \u2014 deleting the alias
3588
3621
  removes the utility, deleting Jig's removes the value.
3589
3622
 
3590
- Only tokens in a Tailwind namespace are listed. \`--size-*\`, \`--measure-*\`,
3591
- \`--focus-ring-*\` and \`--border-width-*\` have none, so they stay \`var()\`-only.
3623
+ Only tokens in a Tailwind namespace are listed. \`--measure-*\`, \`--focus-ring-*\`
3624
+ and \`--duration-*\` have none, so they stay \`var()\`-only \u2014 read them from a class
3625
+ with Tailwind's custom-property form instead: \`max-w-(--measure-prose)\`,
3626
+ \`duration-(--duration-fast)\`.
3592
3627
 
3593
3628
  Regenerate with \`jig init\` after the token layer changes; a token missing here
3594
3629
  is a class that renders and matches nothing. */
@@ -4240,7 +4275,13 @@ program.command("update").description("Update vendored Jig rules, skipping files
4240
4275
  process.exit(1);
4241
4276
  }
4242
4277
  });
4243
- program.command("check").description("Check the repo against Jig's mechanical + hybrid rules.").option("--all", "check the whole repo instead of just changed files", false).option("--ci", "mechanical bucket only; exits non-zero on any error, deterministic", false).option("--json", "emit findings as JSON", false).action((opts) => {
4278
+ program.command("check").description("Check the repo against Jig's mechanical + hybrid rules.").option(
4279
+ "--all",
4280
+ // Names the files, because the default scope surprised a user who read
4281
+ // this as widening the RULES and attested a clean diff as a clean project.
4282
+ "scan every file in the repo, not just those changed since HEAD (same rules either way)",
4283
+ false
4284
+ ).option("--ci", "mechanical bucket only; exits non-zero on any error, deterministic", false).option("--json", "emit findings as JSON", false).action((opts) => {
4244
4285
  const projectRoot = findProjectRoot(process.cwd());
4245
4286
  try {
4246
4287
  const result = check({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jig-ui",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
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",
@@ -281,23 +281,32 @@ Resist per-component tokens (`--button-bg`). They multiply fast and rarely earn
281
281
 
282
282
  ## Naming contract
283
283
 
284
- Names align to Tailwind v4's theme namespaces. This is free for other frameworks — they are ordinary custom properties — and means an alias block can expose any of them as Tailwind utilities without any framework taking a dependency on Tailwind.
284
+ Names align to Tailwind v4's theme namespaces. This is free for other frameworks — they are ordinary custom properties — and means most of them can be exposed as Tailwind utilities through an alias block, without any framework taking a dependency on Tailwind.
285
285
 
286
- | Namespace | Holds | Layer |
287
- | --- | --- | --- |
288
- | `--color-*` | All colour | brand |
289
- | `--font-*` | Font families | brand |
290
- | `--text-*` | Font sizes | mode |
291
- | `--leading-*` | Line heights | mode |
292
- | `--tracking-*` | Letter spacing | mode |
293
- | `--spacing-*` | Spacing values | mode |
294
- | `--radius-*` | Corner radii | brand scale, mode selection |
295
- | `--border-width-*` | Stroke widths | brand options, mode selection |
296
- | `--focus-ring-*` | Focus indicator geometry | brand — an accessibility floor, so not mode-negotiable |
297
- | `--shadow-*` | Elevation | brand |
298
- | `--duration-*`, `--ease-*` | Motion | mode |
299
- | `--size-*` | Control and row heights | mode |
300
- | `--measure-*` | Line length caps | mode |
286
+ **Most, not all.** Three of the namespaces below are Jig's own and have no utility behind them in Tailwind v4 — the **Utility** column says which. Aliasing one is accepted silently, emits the custom property, and generates no rule, so the class lands on the element and does nothing. That is the same silent failure this file warns about under [Optional: Tailwind utility classes](#optional-tailwind-utility-classes), reached from the other direction: there the alias is missing, here the alias is present and the utility does not exist.
287
+
288
+ | Namespace | Holds | Layer | Utility |
289
+ | --- | --- | --- | --- |
290
+ | `--color-*` | All colour | brand | yes |
291
+ | `--font-*` | Font families | brand | yes |
292
+ | `--text-*` | Font sizes | mode | yes |
293
+ | `--leading-*` | Line heights | mode | yes |
294
+ | `--tracking-*` | Letter spacing | mode | yes |
295
+ | `--spacing-*` | Spacing values | mode | yes |
296
+ | `--radius-*` | Corner radii | brand scale, mode selection | yes |
297
+ | `--border-width-*` | Stroke widths | brand options, mode selection | yes |
298
+ | `--focus-ring-*` | Focus indicator geometry | brand — an accessibility floor, so not mode-negotiable | **no** |
299
+ | `--shadow-*` | Elevation | brand | yes |
300
+ | `--ease-*` | Motion easing | mode | yes |
301
+ | `--duration-*` | Motion duration | mode | **no** |
302
+ | `--size-*` | Control and row heights | mode | yes |
303
+ | `--measure-*` | Line length caps | mode | **no** |
304
+
305
+ `--duration-*` and `--ease-*` had one row between them and only one half of it works, which is why they are separated here.
306
+
307
+ **Reading a `no` token from a class.** Use Tailwind's custom-property value form, which needs no alias and keeps the semantic name: `max-w-(--measure-prose)`, `duration-(--duration-fast)`, `outline-(--focus-ring-width)`. That satisfies `H-47` — the value is still the token — it simply does not route through `@theme`. The same form covers any token a mode file adds that has no namespace at all, such as the `--grid-*` values.
308
+
309
+ Verified by compiling one alias per namespace against `tailwindcss@4.3.3` and reading the output. Probe each namespace with a *distinct* token name if you re-check this: `text-*`, `border-*`, `outline-*` and `max-w-*` each read more than one namespace, so a shared suffix makes a colour alias look like proof that four other namespaces work.
301
310
 
302
311
  **Rules**
303
312
  1. Semantic names only at the point of use. `--color-text-strong`, not `--color-neutral-900`, in component code. Primitives exist to build semantics, not to be consumed directly.
@@ -89,3 +89,13 @@ check that never inspected anything.
89
89
 
90
90
  A skipped check must say `skipped` and give the reason. Do not report `ran` for
91
91
  a check you did not perform.
92
+
93
+ **`files=` and `styled=` describe a scope, and the scope is not fixed.**
94
+ `jig check` defaults to the files changed since HEAD and falls back to the
95
+ whole repo only when that diff is empty. So the same project reports a
96
+ different `files=` before and after a commit with nothing else having moved,
97
+ and two `JIG_CHECK:` lines are comparable only when both runs had the same
98
+ scope. Run `jig check --all` for a number that describes the project rather
99
+ than your diff — `--all` widens the files, not the rules. A narrowed run says
100
+ so in its own output; attest from what it printed, not from what you assumed
101
+ it looked at.