jig-ui 0.26.2 → 0.27.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/CHANGELOG.md +59 -0
- package/README.md +3 -3
- package/dist/index.js +834 -592
- package/examples/E-165.html +15 -0
- package/package.json +4 -2
- package/rules/00-anti-patterns.md +11 -4
- package/rules/01-modes.md +1 -1
- package/rules/02-tokens.md +5 -4
- package/rules/03-patterns.md +5 -1
- package/rules.index.json +8 -0
- package/tokens/mode.operator.css +8 -2
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!-- E-165 · A link that opens a new tab for no reason -->
|
|
2
|
+
<figure data-example="dont">
|
|
3
|
+
<div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><pre style="font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;line-height:1.5;background:#ffffff;border:1px solid #d6d6d6;border-radius:6px;padding:10px;margin:0;white-space:pre-wrap;"><a href="https://acme.example"
|
|
4
|
+
target="_blank"
|
|
5
|
+
rel="noopener">GitHub</a></pre><div style="font-size:11px;color:#5c5c5c;margin-top:8px">A tab the reader did not ask for, and nothing says it opened. Back no longer returns.</div></div>
|
|
6
|
+
</figure>
|
|
7
|
+
<figure data-example="do">
|
|
8
|
+
<div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><pre style="font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;line-height:1.5;background:#ffffff;border:1px solid #d6d6d6;border-radius:6px;padding:10px;margin:0;white-space:pre-wrap;"><a href="https://acme.example">
|
|
9
|
+
GitHub</a>
|
|
10
|
+
|
|
11
|
+
<!-- in a half-filled form -->
|
|
12
|
+
<a href="/terms.pdf"
|
|
13
|
+
target="_blank" rel="noopener">
|
|
14
|
+
Terms (opens in a new tab)</a></pre></div>
|
|
15
|
+
</figure>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jig-ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A design system for coding agents.
|
|
3
|
+
"version": "0.27.0",
|
|
4
|
+
"description": "A design system for coding agents. 161 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",
|
|
7
7
|
"bin": {
|
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
"commander": "^12.1.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
+
"@tailwindcss/node": "4.3.3",
|
|
38
39
|
"@types/node": "^22.0.0",
|
|
40
|
+
"tailwindcss": "4.3.3",
|
|
39
41
|
"tsup": "^8.3.0",
|
|
40
42
|
"tsx": "^4.23.15",
|
|
41
43
|
"typescript": "^5.6.0",
|
|
@@ -366,7 +366,7 @@ Without `viewport-fit=cover`, the browser keeps the page inside the safe area on
|
|
|
366
366
|
### D-115 The page scrolls sideways on a phone
|
|
367
367
|
❌ At phone width the whole page is wider than the screen — a data table, a long URL, an image, a `width: 100vw` element or a fixed-width block pushes it out, and the reader can drag the page left and right
|
|
368
368
|
✅ At every width, nothing makes the page wider than the screen. Content that is genuinely wider — a data table, a code block — scrolls inside its own container with a visible edge (`E-62`), and the page itself never does. `editorial` goes further and allows no scrolling regions on mobile at all (`M-01`): there a code block wraps its long lines and a wide table stacks its rows (`L-01`, step 6).
|
|
369
|
-
Judge it on a render, not in the source: at 360px, `document.documentElement.scrollWidth` must not be greater than `document.documentElement.clientWidth`. The usual causes are each one line to fix — `overflow-wrap: anywhere` on text the author does not control, `max-width: 100%` on media, `width: 100%` instead of `100vw` (which includes the scrollbar), and a wrapper with `overflow-x: auto` around anything tabular.
|
|
369
|
+
Judge it on a render, not in the source: at 360px, `document.documentElement.scrollWidth` must not be greater than `document.documentElement.clientWidth`. The usual causes are each one line to fix — `overflow-wrap: anywhere` on text the author does not control, `max-width: 100%` on media, `width: 100%` instead of `100vw` (which includes the scrollbar), and a wrapper with `overflow-x: auto` around anything tabular. That wrapper also takes `position: relative`, or a visually hidden label inside it, positioned absolutely, lays itself out against the page and widens it.
|
|
370
370
|
A page that scrolls sideways is not merely untidy. The reader's vertical swipes drift, the page slides half off the screen, and every line of text needs re-centring before it can be read.
|
|
371
371
|
|
|
372
372
|
## E. States and interaction
|
|
@@ -489,6 +489,12 @@ On multi-step forms put **"Back" as a tertiary button at the top left** — away
|
|
|
489
489
|
A Menu button that does nothing looks finished in every screenshot. On a phone it is the only way to the rest of the site, so the reader who taps it and sees nothing change has nowhere to go. A button that opens the menu without `aria-expanded` is the same failure for a screen reader: it announces "Menu, button" before and after, and the reader never learns anything happened.
|
|
490
490
|
`jig check` catches the hidden navigation with no recorded open state anywhere in the project. It cannot tell whether the button actually opens the menu — `critique` operates it on a render at 360px: tap it, and the links appear, `aria-expanded` changes, and the label or icon reads as close.
|
|
491
491
|
|
|
492
|
+
### E-165 A link that opens a new tab for no reason
|
|
493
|
+
❌ `target="_blank"` on an ordinary link (another site, an article, a source) because it leads away from the site
|
|
494
|
+
✅ The same tab. Open a new tab only when leaving would lose something: the reader is in the middle of a task (a half-filled form, an unsaved edit, a checkout, a signed-in session), something is playing or running, or the link is a file read alongside the page, such as a PDF. When a link does open a new tab, say so: "(opens in a new tab)" in its accessible name, and an icon for sighted readers. Open it safely too (`K-128`).
|
|
495
|
+
The reader can already open any link in a new tab, with Cmd or Ctrl-click or a long-press. Forcing it takes that choice away and breaks Back: the new tab has no history, and on a phone the page they came from is hidden among their other tabs. A tab that opens unannounced is also a change of context nobody asked for, and a screen reader user may not know they have left.
|
|
496
|
+
`jig check` warns on a link that opens a new tab and never says so. Whether the reason is good, a task the reader would lose, is a judgment a `critique` makes on the page.
|
|
497
|
+
|
|
492
498
|
---
|
|
493
499
|
|
|
494
500
|
## F. Forms
|
|
@@ -556,8 +562,8 @@ Where people must *browse* to decide, split the list into two dependent fields
|
|
|
556
562
|
---
|
|
557
563
|
|
|
558
564
|
### F-113 Form text small enough to make the phone zoom
|
|
559
|
-
❌ An input, select or textarea whose text is below 16px — including `operator`'s 14px `--text-body
|
|
560
|
-
✅ At least 16px on touch screens. To keep
|
|
565
|
+
❌ An input, select or textarea whose text is below 16px — including `--text-caption` in every mode, a size written as `font: inherit` under a 14px body, and `operator`'s 14px `--text-body` where the tokens do not raise it on touch screens
|
|
566
|
+
✅ At least 16px on touch screens. `operator`'s tokens raise `--text-body` to 16px under a coarse pointer, so a control set in the body size, or inheriting it, is safe there. To keep any other denser size on desktop, raise it only where it matters: `@media (pointer: coarse) { input, select, textarea { font-size: max(16px, var(--text-body)); } }`
|
|
561
567
|
iOS Safari zooms the whole page when a field whose text is below 16px takes focus, and it does not zoom back out when the field loses it. The reader is left with a form wider than the screen, scrolling sideways to find the next field (`D-115`).
|
|
562
568
|
Setting `maximum-scale=1` on the viewport to stop it is not the fix. That disables pinch zoom for everyone, which is an accessibility failure in its own right.
|
|
563
569
|
|
|
@@ -646,10 +652,11 @@ Deletion is a real answer and the easy one to miss, because the correction point
|
|
|
646
652
|
Related: `E-33` (an interactive `div`), `B-17` (heading levels), `H-48` (native elements over scripted ones), `P-14` (navigation).
|
|
647
653
|
|
|
648
654
|
### H-117 A token name nothing declares
|
|
649
|
-
❌ `font-family: var(--font-body)`, `padding: var(--space-lg)` — in a project whose token layer declares neither
|
|
655
|
+
❌ `font-family: var(--font-body)`, `padding: var(--space-lg)` — in a project whose token layer declares neither. The same in a Tailwind class: `min-h-touch` with no `--spacing-touch` in the theme
|
|
650
656
|
✅ Use the names the token layer declares — `02-tokens.md` lists them, and the token files in the project are the source. If the value you need has no token, that is a finding to report or a value to delete (`H-47`), never a name to make up.
|
|
651
657
|
The browser does not warn. A `var()` that cannot resolve makes its whole declaration invalid, so the property falls back to its initial value: the font becomes the browser default serif, padding becomes 0, the border disappears. The page still renders, the source still looks tokenised, and every file-based review passes it. In a live run three of four pages invented their token names this way and shipped mostly unstyled.
|
|
652
658
|
`jig check` reads every custom property the project declares — the token layer, its own stylesheets, a Tailwind `@theme`, `style` attributes — and reports each reference to one that is not there. A reference with a fallback, `var(--x, 1rem)`, resolves, and is not reported.
|
|
659
|
+
A Tailwind v4 class whose value nothing declares fails the same way with less trace: Tailwind generates no CSS for it, and says nothing, so no `var()` is ever written. `jig check` asks the project's own Tailwind which of the classes in its markup are utilities that generate nothing, and reports those. A class the project names and styles itself is left alone. Tailwind reads some values from a name that is not the token's own: a duration from `--transition-duration-*`, so `duration-fast` needs that name in the theme, not `--duration-fast`.
|
|
653
660
|
|
|
654
661
|
---
|
|
655
662
|
|
package/rules/01-modes.md
CHANGED
|
@@ -190,7 +190,7 @@ Attempting to vary these by mode is a category error:
|
|
|
190
190
|
- **Accessibility floors.** Contrast, focus indication, target size, semantic markup. Identical in all three. `operator` being dense does not license a 24px tap target or a 3:1 body contrast.
|
|
191
191
|
- **Brand identity.** Palette, typeface, logo, voice.
|
|
192
192
|
- **State completeness.** Every mode renders loading, empty, error and disabled.
|
|
193
|
-
- **The anti-pattern file.** All
|
|
193
|
+
- **The anti-pattern file.** All 130 rules in it apply everywhere.
|
|
194
194
|
|
|
195
195
|
---
|
|
196
196
|
|
package/rules/02-tokens.md
CHANGED
|
@@ -355,7 +355,7 @@ Resist per-component tokens (`--button-bg`). They multiply fast and rarely earn
|
|
|
355
355
|
|
|
356
356
|
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.
|
|
357
357
|
|
|
358
|
-
**Most, not all.**
|
|
358
|
+
**Most, not all.** Two of the namespaces below are Jig's own and have no utility behind them in Tailwind v4 — the **Utility** column says which. One more has a utility under another name. 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.
|
|
359
359
|
|
|
360
360
|
| Namespace | Holds | Layer | Utility |
|
|
361
361
|
| --- | --- | --- | --- |
|
|
@@ -370,13 +370,14 @@ Names align to Tailwind v4's theme namespaces. This is free for other frameworks
|
|
|
370
370
|
| `--focus-ring-*` | Focus indicator geometry | brand — an accessibility floor, so not mode-negotiable | **no** |
|
|
371
371
|
| `--shadow-*` | Elevation | brand | yes |
|
|
372
372
|
| `--ease-*` | Motion easing | mode | yes |
|
|
373
|
-
| `--duration-*` | Motion duration | mode |
|
|
373
|
+
| `--duration-*` | Motion duration | mode | yes, as `--transition-duration-*`: `duration-fast` |
|
|
374
|
+
| `--opacity-*` | Opacity of a disabled control | brand | yes |
|
|
374
375
|
| `--size-*` | Control and row heights | mode | yes |
|
|
375
376
|
| `--measure-*` | Line length caps | mode | **no** |
|
|
376
377
|
|
|
377
|
-
`--duration-*` and `--ease-*` had one row between them and
|
|
378
|
+
`--duration-*` and `--ease-*` had one row between them and work differently, which is why they are separated here. Tailwind reads a duration from `--transition-duration-*`, so the alias block names it that way, `--transition-duration-fast: var(--duration-fast)`; an alias under Jig's own name generates nothing. `jig init` asks the project's own Tailwind which aliases generate a utility, and `jig check` asks it which classes in the markup generate nothing (`H-117`).
|
|
378
379
|
|
|
379
|
-
**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)`, `
|
|
380
|
+
**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)`, `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.
|
|
380
381
|
|
|
381
382
|
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.
|
|
382
383
|
|
package/rules/03-patterns.md
CHANGED
|
@@ -578,7 +578,11 @@ everything else follows from.
|
|
|
578
578
|
5. **Content that is genuinely wider scrolls inside itself, except in
|
|
579
579
|
`editorial` on a phone.** A code block, a wide table, a long identifier: its
|
|
580
580
|
own container scrolls with a visible edge (`E-62`), keeps its type size, and
|
|
581
|
-
the page never scrolls sideways (`D-115`).
|
|
581
|
+
the page never scrolls sideways (`D-115`). The container is
|
|
582
|
+
`position: relative` as well as `overflow-x: auto`: text hidden for screen
|
|
583
|
+
readers in a cell is positioned absolutely, against the nearest positioned
|
|
584
|
+
ancestor, and without one it escapes the container and widens the page,
|
|
585
|
+
though nothing shows. `editorial` allows no scrolling
|
|
582
586
|
region on mobile (`M-01`), so there each one reflows instead:
|
|
583
587
|
- **A code block** keeps its line breaks and indents and wraps a long line
|
|
584
588
|
(`white-space: pre-wrap`, `overflow-wrap: anywhere`). What is copied is
|
package/rules.index.json
CHANGED
|
@@ -787,6 +787,14 @@
|
|
|
787
787
|
"since": "0.10.0",
|
|
788
788
|
"detector": "menu-state"
|
|
789
789
|
},
|
|
790
|
+
{
|
|
791
|
+
"id": "E-165",
|
|
792
|
+
"bucket": "hybrid",
|
|
793
|
+
"severity": "warning",
|
|
794
|
+
"since": "0.27.0",
|
|
795
|
+
"detector": "new-tab-link",
|
|
796
|
+
"pass": "code"
|
|
797
|
+
},
|
|
790
798
|
{
|
|
791
799
|
"id": "H-117",
|
|
792
800
|
"bucket": "mechanical",
|
package/tokens/mode.operator.css
CHANGED
|
@@ -107,7 +107,13 @@
|
|
|
107
107
|
compact) is above WCAG 2.2's 24px minimum for a mouse, and below the 48px a
|
|
108
108
|
finger needs, so under a coarse pointer every row rises to the floor. A
|
|
109
109
|
control inside a row never sets its own height to reach it: every row in a
|
|
110
|
-
table is one height, and here the tokens make that height the right one.
|
|
110
|
+
table is one height, and here the tokens make that height the right one.
|
|
111
|
+
Body text rises to 16px with it: an input, select or textarea set in 14px
|
|
112
|
+
makes iOS zoom the page on focus (`F-113`), and a form control's text is
|
|
113
|
+
body text, set by this token or inherited from it. */
|
|
111
114
|
@media (pointer: coarse) {
|
|
112
|
-
:root {
|
|
115
|
+
:root {
|
|
116
|
+
--size-row: var(--size-touch-target); --size-row-compact: var(--size-touch-target);
|
|
117
|
+
--text-body: 16px;
|
|
118
|
+
}
|
|
113
119
|
}
|