jig-ui 0.3.0 → 0.5.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 +258 -74
- package/dist/index.js +1589 -333
- package/package.json +2 -1
- package/rules/00-anti-patterns.md +44 -8
- package/rules/01-modes.md +8 -7
- package/rules/02-tokens.md +184 -9
- package/rules/03-patterns.md +71 -4
- package/templates/COMMAND.md.tmpl +92 -0
- package/templates/SKILL.md.tmpl +27 -12
- package/templates/command-metadata.json +6 -6
- package/tokens/brand.default.css +67 -2
- package/tokens/mode.editorial.css +45 -2
- package/tokens/mode.operator.css +17 -1
- package/tokens/mode.product.css +25 -2
package/templates/SKILL.md.tmpl
CHANGED
|
@@ -3,15 +3,22 @@ cite the number when you follow or deliberately break one.
|
|
|
3
3
|
|
|
4
4
|
## Before generating or reviewing any UI
|
|
5
5
|
|
|
6
|
+
0. Check that `{{config_file}}` exists. If it does not, this project has no
|
|
7
|
+
token layer: run `init` and stop until it has been run. Do not proceed by
|
|
8
|
+
writing token definitions of your own — a `:root` block you author, however
|
|
9
|
+
clearly you label it, is an invented design system wearing this one's names,
|
|
10
|
+
and it will not be replaced when the real tokens arrive.
|
|
6
11
|
1. Load `{{rules_path}}/00-anti-patterns.md` and `{{rules_path}}/01-modes.md`.
|
|
7
12
|
2. Determine the mode from `{{config_file}}`, or infer it using the procedure in
|
|
8
13
|
`{{rules_path}}/01-modes.md` and state the inference in one line before building.
|
|
9
14
|
3. Load the relevant section of `{{rules_path}}/03-patterns.md` for the component you are building.
|
|
10
15
|
4. Load `{{rules_path}}/05-copy.md` whenever you write a label, button, heading, error or empty state.
|
|
11
16
|
5. Consume tokens by semantic name only. Never write a raw colour or pixel value
|
|
12
|
-
at a call site
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
at a call site, and never resolve a name yourself: if a token you need has no
|
|
18
|
+
value, that is a finding to report, not a number to supply.
|
|
19
|
+
6. Run `check` for the rules the CLI can detect, then run the self-check at the
|
|
20
|
+
end of `{{rules_path}}/00-anti-patterns.md` for the rules it cannot. Both
|
|
21
|
+
halves, every time — a clean `check` is not a clean review.
|
|
15
22
|
7. Cite any rule you deliberately break, with the reason, in one line.
|
|
16
23
|
|
|
17
24
|
Load `{{rules_path}}/04-principles.md` only when two rules conflict.
|
|
@@ -20,9 +27,15 @@ Load `{{rules_path}}/04-principles.md` only when two rules conflict.
|
|
|
20
27
|
|
|
21
28
|
{{available_commands}}
|
|
22
29
|
|
|
23
|
-
Commands marked `available` run via the CLI at `{{scripts_path}}
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
Commands marked `available` run via the CLI at `{{scripts_path}}` — the version
|
|
31
|
+
this skill was installed with, so the CLI and these rules always agree.
|
|
32
|
+
|
|
33
|
+
`update` is the exception: its job is to move that version forward, so run it
|
|
34
|
+
as `{{update_path}} update`. Run pinned, it refreshes to the version already
|
|
35
|
+
installed and reports success for a no-op.
|
|
36
|
+
|
|
37
|
+
Commands marked `planned` are listed for context only — running one errors out,
|
|
38
|
+
since it is not yet registered.
|
|
26
39
|
|
|
27
40
|
## Reading command output
|
|
28
41
|
|
|
@@ -42,12 +55,14 @@ changed since, do not re-run it.
|
|
|
42
55
|
Before you finish a task that generated or modified UI, emit this line:
|
|
43
56
|
|
|
44
57
|
```text
|
|
45
|
-
JIG_CHECK: version=<version> mode=<mode>
|
|
58
|
+
JIG_CHECK: version=<version> mode=<mode> mechanical=<pass|fail|skipped>:<n> judgment=<ran|skipped>
|
|
46
59
|
```
|
|
47
60
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
61
|
+
`jig check` emits this same line for the half it can do, with
|
|
62
|
+
`judgment=not-run`. Take `mechanical=` from its output rather than counting by
|
|
63
|
+
hand; fill `judgment=` yourself. If `check` could not run at all, that is
|
|
64
|
+
`mechanical=skipped:0` — never `pass`, which would report a clean result for a
|
|
65
|
+
check that never inspected anything.
|
|
51
66
|
|
|
52
|
-
A skipped
|
|
53
|
-
|
|
67
|
+
A skipped check must say `skipped` and give the reason. Do not report `ran` for
|
|
68
|
+
a check you did not perform.
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
"status": "available"
|
|
16
16
|
},
|
|
17
17
|
"check": {
|
|
18
|
-
"description": "Check
|
|
19
|
-
"argumentHint": "[
|
|
20
|
-
"status": "
|
|
18
|
+
"description": "Check the repo against the mechanical and hybrid rules the CLI can detect. Reports findings by rule id; the judgment rules remain yours to apply.",
|
|
19
|
+
"argumentHint": "[--all] [--ci] [--json]",
|
|
20
|
+
"status": "available"
|
|
21
21
|
},
|
|
22
22
|
"explain": {
|
|
23
|
-
"description": "
|
|
24
|
-
"argumentHint": "<rule-id>",
|
|
25
|
-
"status": "
|
|
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
|
+
"status": "available"
|
|
26
26
|
}
|
|
27
27
|
}
|
package/tokens/brand.default.css
CHANGED
|
@@ -99,6 +99,23 @@
|
|
|
99
99
|
|
|
100
100
|
--color-focus: var(--color-text-strong);
|
|
101
101
|
|
|
102
|
+
/* ---- Stroke widths. Options; modes select. ----
|
|
103
|
+
Every rule requiring a visible border or focus ring — E-28, E-29, P-02's
|
|
104
|
+
3:1 shape floor — used to end at a call site writing `1px` or `2px` by
|
|
105
|
+
hand, which H-47 forbids. Two agents building different components hit
|
|
106
|
+
that independently and reported the same gap: no semantic name existed to
|
|
107
|
+
consume. These are it. */
|
|
108
|
+
--border-width-hairline: 1px; /* control and surface borders */
|
|
109
|
+
--border-width-strong: 2px; /* emphasis, selected states */
|
|
110
|
+
|
|
111
|
+
/* Focus ring geometry. NOT mode-negotiable, for the same reason
|
|
112
|
+
--size-touch-target is not: WCAG 2.4.11 wants a perimeter at least 2px
|
|
113
|
+
thick, and density is never a reason to go under an accessibility floor.
|
|
114
|
+
The offset keeps the ring off the control's own border so both stay
|
|
115
|
+
legible. */
|
|
116
|
+
--focus-ring-width: 2px;
|
|
117
|
+
--focus-ring-offset: 2px;
|
|
118
|
+
|
|
102
119
|
/* ================= TYPE ================= */
|
|
103
120
|
--font-text: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
104
121
|
--font-display: var(--font-text);
|
|
@@ -111,16 +128,33 @@
|
|
|
111
128
|
--radius-lg: 32px; /* large surfaces, hero media */
|
|
112
129
|
--radius-full: 9999px;
|
|
113
130
|
|
|
114
|
-
/* ================= ELEVATION — two shadows =================
|
|
131
|
+
/* ================= ELEVATION — two shadows, plus none =================
|
|
115
132
|
Shadow colour is derived from the text colour, not pure black, so it sits in
|
|
116
|
-
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`. */
|
|
117
136
|
--shadow-none: none;
|
|
118
137
|
--shadow-raised: 0 1px 2px rgb(0 0 0 / 6%), 0 2px 4px -2px rgb(0 0 0 / 8%);
|
|
119
138
|
--shadow-overlay: 0 4px 16px -4px rgb(0 0 0 / 14%), 0 1px 3px rgb(0 0 0 / 8%);
|
|
120
139
|
}
|
|
121
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. */
|
|
122
155
|
@media (prefers-color-scheme: dark) {
|
|
123
156
|
:root:not([data-theme="light"]) {
|
|
157
|
+
|
|
124
158
|
/* Three solid levels. In dark, DEPTH COMES FROM BACKGROUND, NOT SHADOW —
|
|
125
159
|
shadows are nearly invisible on a dark surface. Never pure black. */
|
|
126
160
|
--color-bg-base: hsl(var(--brand-h) 6% 10%);
|
|
@@ -150,3 +184,34 @@
|
|
|
150
184
|
--shadow-overlay: 0 8px 24px -8px rgb(0 0 0 / 60%);
|
|
151
185
|
}
|
|
152
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:
|
|
33
|
-
--text-h1:
|
|
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;
|
|
@@ -59,14 +77,39 @@
|
|
|
59
77
|
--size-control: 48px; --size-control-sm: 40px;
|
|
60
78
|
--size-touch-target: 48px; --size-icon: 20px;
|
|
61
79
|
|
|
80
|
+
--border-width-control: var(--border-width-hairline);
|
|
62
81
|
--radius-control: var(--radius-sm); --radius-surface: var(--radius-md);
|
|
63
82
|
--shadow-surface: var(--shadow-none);
|
|
64
83
|
|
|
65
84
|
--duration-fast: 150ms; --duration-base: 250ms; --duration-slow: 300ms;
|
|
66
85
|
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
|
67
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;
|
|
68
105
|
}
|
|
69
106
|
|
|
70
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. */
|
|
71
114
|
:root { --duration-fast: 1ms; --duration-base: 1ms; --duration-slow: 1ms; }
|
|
72
115
|
}
|
package/tokens/mode.operator.css
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
@@ -59,6 +70,7 @@
|
|
|
59
70
|
--size-row: 36px; --size-row-compact: 32px;
|
|
60
71
|
--size-touch-target: 48px; --size-icon: 16px;
|
|
61
72
|
|
|
73
|
+
--border-width-control: var(--border-width-hairline);
|
|
62
74
|
--radius-control: var(--radius-sm); --radius-surface: var(--radius-sm);
|
|
63
75
|
--shadow-surface: var(--shadow-none);
|
|
64
76
|
|
|
@@ -66,6 +78,10 @@
|
|
|
66
78
|
--ease-out: cubic-bezier(0.2, 0, 0, 1);
|
|
67
79
|
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
|
68
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
|
+
|
|
69
85
|
--font-numeric: var(--font-numeric-features);
|
|
70
86
|
}
|
|
71
87
|
|
package/tokens/mode.product.css
CHANGED
|
@@ -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:
|
|
32
|
-
--text-h1:
|
|
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;
|
|
@@ -56,12 +74,17 @@
|
|
|
56
74
|
--size-control: 40px; --size-control-sm: 32px; --size-row: 48px;
|
|
57
75
|
--size-touch-target: 48px; --size-icon: 18px;
|
|
58
76
|
|
|
77
|
+
--border-width-control: var(--border-width-hairline);
|
|
59
78
|
--radius-control: var(--radius-sm); --radius-surface: var(--radius-md);
|
|
60
79
|
--shadow-surface: var(--shadow-none);
|
|
61
80
|
|
|
62
81
|
--duration-fast: 100ms; --duration-base: 150ms; --duration-slow: 200ms;
|
|
63
82
|
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
|
64
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. */
|
|
65
88
|
}
|
|
66
89
|
|
|
67
90
|
@media (prefers-reduced-motion: reduce) {
|