jig-ui 0.4.0 → 0.6.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.
@@ -11,14 +11,42 @@ output — findings are ordered by severity, not position, so `head`, `tail`,
11
11
 
12
12
  ## init
13
13
 
14
- Report what it detected, the brand colour it derived and where that came from,
15
- and the mode it wrote. The mode is the load-bearing decision: `jig.config.json`
16
- outranks your own inference from then on, so if the project's signals point
17
- elsewhere — the mode table in `{{rules_path}}/01-modes.md` — say so and ask
18
- before leaving it.
19
-
20
- If the token layer does not exist yet, this is the command that creates it. Do
21
- not author token values yourself under any circumstances.
14
+ **Settle the surfaces before you run it.** Mode is the most consequential thing
15
+ `init` writes and the thing it is worst at choosing: with `--yes` it takes
16
+ `'/' → product` without reading the project at all, and `{{rules_path}}/01-modes.md`
17
+ rule 1 then makes that config outrank your own reading of the project from then
18
+ on. A default chosen in a second binds the project indefinitely, and density is
19
+ expensive to reverse.
20
+
21
+ You are the half that can fix this, because you are talking to someone who knows
22
+ the answer and the CLI is not. So, first:
23
+
24
+ 1. **Ask what the product is**, in their words rather than Jig's — a marketing
25
+ or content site, a signed-in application, an internal tool people use all
26
+ day? A project usually has more than one of these, and each is a surface.
27
+ 2. **Map each to a mode** using the table in `{{rules_path}}/01-modes.md`:
28
+ `editorial` for first-visit content, `product` for the signed-in app,
29
+ `operator` for dense daily tools. State the mapping in one line and let them
30
+ correct it.
31
+ 3. **Write `jig.config.json`** with those surfaces before running anything.
32
+ `init` reads an existing config and honours it — writing one mode file per
33
+ declared mode — so this is how a decision reaches the tokens.
34
+ 4. **Then run the command.**
35
+
36
+ Where there is genuinely one surface, say so and move on; the point is that the
37
+ mode was chosen rather than defaulted into.
38
+
39
+ Afterwards, report what it detected, the brand colour it derived and where that
40
+ came from, and the surfaces it used — the output says whether they came from
41
+ `jig.config.json` or from the default.
42
+
43
+ If the token layer does not exist yet, this is the command that creates it. **Do
44
+ not author token values yourself.** `init` derives the brand colour, checks it
45
+ against the contrast floor in both light and dark, and records a checksum for
46
+ each file it writes; a file you write by hand is untracked, so `jig update` can
47
+ never refresh it again. `jig check` now reads the token layer back and holds it
48
+ to those floors either way, so a hand-written value will be caught — but caught
49
+ late, and unmaintainable, is worse than derived.
22
50
 
23
51
  ## check
24
52
 
@@ -57,14 +85,32 @@ Print the CLI's output as it stands. It is already the rule's full text — do n
57
85
  summarise it, and do not paraphrase the correction into your own words: the
58
86
  wording is the rule.
59
87
 
88
+ **It takes a word as well as an id.** `explain contrast` searches every title
89
+ and body and lists what matches; one match prints in full. Use it whenever you
90
+ are about to reason about an area of the system rather than a specific finding
91
+ — before reviewing colour, before writing motion — instead of guessing which
92
+ rules apply or reading a whole rule file to find out. `explain <letter> --list`
93
+ prints one section, `explain --list` prints every id.
94
+
95
+ Reach for this before paraphrasing the rules from memory. A rule you half-recall
96
+ is the failure mode this command exists to prevent.
97
+
60
98
  If the id is a `P-` or `M-` spec, the output says it is a specification rather
61
99
  than a rule, with no ❌/✅ pair and no detector. That is correct, not a gap.
62
100
 
63
- If the id does not exist, the error names every id in that section. Offer the
64
- nearest one rather than guessing what the user meant.
101
+ A well-formed id that does not exist errors, naming every id in that section —
102
+ offer the nearest one rather than guessing what the user meant. A word that
103
+ matches nothing errors differently, suggesting `--list`; that is a search with
104
+ no hits, not a typo, so widen the word rather than inventing an id.
65
105
 
66
106
  ## update
67
107
 
108
+ **Run this one as `{{update_path}} update`, not at the pinned version.** Every
109
+ other subcommand uses the pin so the CLI and these rules agree; `update` exists
110
+ to move that pin forward, so pinned it refreshes to the version already
111
+ installed and reports success for a no-op — "Updated Jig → <same version>" —
112
+ and nobody ever upgrades.
113
+
68
114
  Report what moved and what was left alone. Files reported as skipped were edited
69
115
  locally and are the user's; never re-apply Jig's version over them.
70
116
 
@@ -20,8 +20,8 @@
20
20
  "status": "available"
21
21
  },
22
22
  "explain": {
23
- "description": "Print a rule's full text, its correction, the version it arrived in, and who checks it. Also resolves the P-/M- pattern and mode specs, which no rule index contains.",
24
- "argumentHint": "<rule-id>",
23
+ "description": "Explain a rule, or find the rules you cannot name. Given an id, prints the rule's full text, its correction, the version it arrived in and who checks it — including the P-/M- pattern and mode specs, which no rule index contains. Given a word instead, searches every title and body and lists what matches. `--list` prints every id, or one section's.",
24
+ "argumentHint": "<rule-id | search term> [--list]",
25
25
  "status": "available"
26
26
  }
27
27
  }
@@ -128,16 +128,33 @@
128
128
  --radius-lg: 32px; /* large surfaces, hero media */
129
129
  --radius-full: 9999px;
130
130
 
131
- /* ================= ELEVATION — two shadows =================
131
+ /* ================= ELEVATION — two shadows, plus none =================
132
132
  Shadow colour is derived from the text colour, not pure black, so it sits in
133
- the palette. Light from above: y-offset positive, blur grows with elevation. */
133
+ the palette. Light from above: y-offset positive, blur grows with elevation.
134
+ `--shadow-none` is the explicit absence a mode selects when its elevation is
135
+ stroke-led; every mode currently does, via `--shadow-surface`. */
134
136
  --shadow-none: none;
135
137
  --shadow-raised: 0 1px 2px rgb(0 0 0 / 6%), 0 2px 4px -2px rgb(0 0 0 / 8%);
136
138
  --shadow-overlay: 0 4px 16px -4px rgb(0 0 0 / 14%), 0 1px 3px rgb(0 0 0 / 8%);
137
139
  }
138
140
 
141
+ /* Dark mode has THREE states, and this file needs two blocks to serve them.
142
+ The media query below covers "the OS says dark", guarded by
143
+ :not([data-theme="light"]) so an explicit light choice still wins. It does
144
+ NOT cover "the OS says light and the user chose dark" — a selector inside a
145
+ media query cannot match when the query is false, so before this second
146
+ block existed, setting data-theme="dark" on a light-mode system produced no
147
+ dark tokens at all. The preview's own dark toggle did nothing there, which is
148
+ why this went unnoticed: dark mode was only ever seen by people whose OS was
149
+ already in it.
150
+
151
+ The two bodies are identical and MUST stay so; check-tokens rule 10 fails the
152
+ build if they diverge. CSS gives no way to share them — a selector list cannot
153
+ straddle a media-query boundary — so the duplication is required and the guard
154
+ is the answer to it. */
139
155
  @media (prefers-color-scheme: dark) {
140
156
  :root:not([data-theme="light"]) {
157
+
141
158
  /* Three solid levels. In dark, DEPTH COMES FROM BACKGROUND, NOT SHADOW —
142
159
  shadows are nearly invisible on a dark surface. Never pure black. */
143
160
  --color-bg-base: hsl(var(--brand-h) 6% 10%);
@@ -167,3 +184,34 @@
167
184
  --shadow-overlay: 0 8px 24px -8px rgb(0 0 0 / 60%);
168
185
  }
169
186
  }
187
+
188
+ :root[data-theme="dark"] {
189
+
190
+ /* Three solid levels. In dark, DEPTH COMES FROM BACKGROUND, NOT SHADOW —
191
+ shadows are nearly invisible on a dark surface. Never pure black. */
192
+ --color-bg-base: hsl(var(--brand-h) 6% 10%);
193
+ --color-bg-raised: hsl(var(--brand-h) 6% 15%);
194
+ --color-bg-overlay: hsl(var(--brand-h) 6% 20%);
195
+
196
+ /* Foregrounds become opacities of white. Higher than the light-mode values,
197
+ because dark interfaces lose detail faster. */
198
+ --color-text-strong: rgb(255 255 255 / 100%);
199
+ --color-text-weak: rgb(255 255 255 / 78%);
200
+ --color-stroke-strong: rgb(255 255 255 / 60%);
201
+ --color-stroke-weak: rgb(255 255 255 / 12%);
202
+ --color-fill: rgb(255 255 255 / 6%);
203
+
204
+ --brand-l: 88%; /* lighten and desaturate the brand for dark surfaces */
205
+ --color-on-brand: hsl(var(--brand-h) 6% 10%);
206
+
207
+ /* System colours lighten and desaturate in dark mode. Only the h/s/l and
208
+ fill-alpha vars need overriding here — the four hsl() declarations
209
+ above pick the new values up automatically. */
210
+ --error-h: 0; --error-s: 60%; --error-l: 72%; --error-fill-a: 8%;
211
+ --warning-h: 42; --warning-s: 70%; --warning-l: 70%; --warning-fill-a: 8%;
212
+ --success-h: 162; --success-s: 45%; --success-l: 66%; --success-fill-a: 8%;
213
+ --info-h: 220; --info-s: 55%; --info-l: 74%; --info-fill-a: 8%;
214
+
215
+ --shadow-raised: none;
216
+ --shadow-overlay: 0 8px 24px -8px rgb(0 0 0 / 60%);
217
+ }
@@ -28,9 +28,27 @@
28
28
  --text-body: 16px; --leading-body: 1.5; /* UI text: labels, controls */
29
29
  --text-prose: 18px; --leading-prose: 1.6; /* LONG-FORM text. Never below 18. */
30
30
  --text-lead: 20px; --leading-lead: 1.5;
31
+
32
+ /* H1 and H2 are FLUID: they interpolate with viewport width instead of
33
+ switching at a breakpoint, which is why this system defines no breakpoint
34
+ token. Each clamp is the line through (360px viewport, min) and (1024px,
35
+ max); outside that range it saturates at the bounds.
36
+
37
+ The `rem` term is not decoration. `clamp(32px, 8vw, 48px)` would ignore a
38
+ reader who has set a larger default font size — page zoom scales `vw`, a
39
+ font-size preference does not — which fails WCAG 1.4.4. Keeping every term
40
+ in `rem` plus `vw` means the whole curve moves with the user's setting.
41
+
42
+ Only headings. Body and prose stay fixed: readability is absolute, set by
43
+ the eye and viewing distance, while heading size is relative — it exists to
44
+ contrast with body, and that contrast can compress on a narrow screen. A
45
+ fluid `--text-prose` would also breach the 18px floor (B-75, T20).
46
+
47
+ The minimums land on rungs the ladder already has rather than on new
48
+ values. Line heights are unitless, so they follow the size for free. */
31
49
  --text-h3: 24px; --leading-h3: 1.333;
32
- --text-h2: 32px; --leading-h2: 1.25;
33
- --text-h1: 48px; --leading-h1: 1.1;
50
+ --text-h2: clamp(1.5rem, 1.229rem + 1.205vw, 2rem); --leading-h2: 1.25; /* 24 → 32 */
51
+ --text-h1: clamp(2rem, 1.458rem + 2.41vw, 3rem); --leading-h1: 1.1; /* 32 → 48 */
34
52
 
35
53
  /* Two weights only. More adds noise and is hard to apply consistently. */
36
54
  --font-weight-regular: 400;
@@ -66,8 +84,32 @@
66
84
  --duration-fast: 150ms; --duration-base: 250ms; --duration-slow: 300ms;
67
85
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
68
86
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
87
+
88
+ /* Ambient motion (P-13). Decorative looping movement, editorial only —
89
+ `product` and `operator` define none of these, deliberately.
90
+
91
+ THE THREE VALUES ARE NOT A LADDER, THEY ARE A CHORD. Ambient motion is
92
+ layered: several small movements running at once. If their periods share a
93
+ factor they re-align, and the whole composition pulses in unison — the one
94
+ thing ambient motion must never do. So the periods are mutually prime in
95
+ tenths of a second (30, 47, 71), which puts the first full re-alignment of
96
+ all three past two hours. Round numbers like 3/4/6s would re-align every
97
+ twelve seconds. That is why these look arbitrary; they are not.
98
+
99
+ Pick a different one per layer. Which is which carries no meaning beyond
100
+ speed — unlike the interaction durations above, there is no "this is the
101
+ one for a state change". */
102
+ --duration-ambient-fast: 3s;
103
+ --duration-ambient-base: 4.7s;
104
+ --duration-ambient-slow: 7.1s;
69
105
  }
70
106
 
71
107
  @media (prefers-reduced-motion: reduce) {
108
+ /* The ambient periods are deliberately NOT collapsed to 1ms here. These drive
109
+ infinite loops, so 1ms would spin them at a thousand cycles a second rather
110
+ than stop them — the opposite of what was asked for. P-13 requires the whole
111
+ ambient declaration to sit inside `prefers-reduced-motion: no-preference`,
112
+ so under a reduce preference the animation never starts and there is nothing
113
+ to shorten. Absence, not speed. */
72
114
  :root { --duration-fast: 1ms; --duration-base: 1ms; --duration-slow: 1ms; }
73
115
  }
@@ -27,8 +27,19 @@
27
27
  Line heights UNITLESS. FLOOR: 1.5 holds even here. */
28
28
  --text-caption: 12px; --leading-caption: 1.5;
29
29
  --text-body: 14px; --leading-body: 1.5;
30
- --text-prose: 16px; --leading-prose: 1.6; /* LONG-FORM text only */
30
+ /* 18px, not 16px, and not negotiable by density. `B-75` names 18px as the
31
+ floor for sustained reading, and the reference agrees — "make long body
32
+ text at least 18px". This token sat at 16px, which made our own rule false
33
+ in the one mode most likely to be read for long stretches by someone who
34
+ has been at it all day. Sustained readability is an accessibility floor
35
+ like `--size-touch-target`, not a density dial. Long prose is rare in an
36
+ operator surface; when it appears, it is legible. */
37
+ --text-prose: 18px; --leading-prose: 1.6; /* LONG-FORM text only */
31
38
  --text-lead: 16px; --leading-lead: 1.5;
39
+ /* No fluid headings here, unlike editorial and product. This mode's largest
40
+ heading is 22px, which already fits ~28 characters per line on a 360px
41
+ screen — fluid sizing would have nothing to do. The mechanism is absent
42
+ because the problem is, not by oversight. */
32
43
  --text-h3: 16px; --leading-h3: 1.5;
33
44
  --text-h2: 18px; --leading-h2: 1.4;
34
45
  --text-h1: 22px; --leading-h1: 1.333;
@@ -67,6 +78,10 @@
67
78
  --ease-out: cubic-bezier(0.2, 0, 0, 1);
68
79
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
69
80
 
81
+ /* No ambient duration tokens here, deliberately. Ambient motion is
82
+ editorial only (P-13) — a surface someone works in all day must not
83
+ have anything moving on it that they did not cause. */
84
+
70
85
  --font-numeric: var(--font-numeric-features);
71
86
  }
72
87
 
@@ -27,9 +27,27 @@
27
27
  --text-body: 16px; --leading-body: 1.5;
28
28
  --text-prose: 18px; --leading-prose: 1.6; /* LONG-FORM text only */
29
29
  --text-lead: 20px; --leading-lead: 1.5;
30
+
31
+ /* H1 and H2 are FLUID: they interpolate with viewport width instead of
32
+ switching at a breakpoint, which is why this system defines no breakpoint
33
+ token. Each clamp is the line through (360px viewport, min) and (1024px,
34
+ max); outside that range it saturates at the bounds.
35
+
36
+ The `rem` term is not decoration. `clamp(32px, 8vw, 48px)` would ignore a
37
+ reader who has set a larger default font size — page zoom scales `vw`, a
38
+ font-size preference does not — which fails WCAG 1.4.4. Keeping every term
39
+ in `rem` plus `vw` means the whole curve moves with the user's setting.
40
+
41
+ Only headings. Body and prose stay fixed: readability is absolute, set by
42
+ the eye and viewing distance, while heading size is relative — it exists to
43
+ contrast with body, and that contrast can compress on a narrow screen. A
44
+ fluid `--text-prose` would also breach the 18px floor (B-75, T20).
45
+
46
+ The minimums land on rungs the ladder already has rather than on new
47
+ values. Line heights are unitless, so they follow the size for free. */
30
48
  --text-h3: 20px; --leading-h3: 1.4;
31
- --text-h2: 24px; --leading-h2: 1.333;
32
- --text-h1: 32px; --leading-h1: 1.25;
49
+ --text-h2: clamp(1.25rem, 1.114rem + 0.602vw, 1.5rem); --leading-h2: 1.333; /* 20 → 24 */
50
+ --text-h1: clamp(1.5rem, 1.229rem + 1.205vw, 2rem); --leading-h1: 1.25; /* 24 → 32 */
33
51
 
34
52
  --font-weight-regular: 400;
35
53
  --font-weight-bold: 600;
@@ -63,6 +81,10 @@
63
81
  --duration-fast: 100ms; --duration-base: 150ms; --duration-slow: 200ms;
64
82
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
65
83
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
84
+
85
+ /* No ambient duration tokens here, deliberately. Ambient motion is
86
+ editorial only (P-13) — a surface someone works in all day must not
87
+ have anything moving on it that they did not cause. */
66
88
  }
67
89
 
68
90
  @media (prefers-reduced-motion: reduce) {