@godxjp/ui 28.4.0 → 28.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.
Files changed (49) hide show
  1. package/README.md +58 -0
  2. package/dist/app/theme-axes.js +6 -0
  3. package/dist/components/data-entry/field.d.ts +1 -1
  4. package/dist/components/data-entry/field.js +35 -4
  5. package/dist/components/data-entry/index.d.ts +1 -0
  6. package/dist/components/data-entry/index.js +2 -0
  7. package/dist/components/data-entry/input.d.ts +1 -1
  8. package/dist/components/layout/index.d.ts +1 -1
  9. package/dist/components/layout/nav-list.d.ts +9 -1
  10. package/dist/components/layout/nav-list.js +22 -11
  11. package/dist/components/layout/sidebar.d.ts +16 -2
  12. package/dist/components/layout/sidebar.js +1 -0
  13. package/dist/components/ui/password-input.d.ts +1 -1
  14. package/dist/contracts/measurement.json +1 -1
  15. package/dist/lib/control-styles.d.ts +18 -3
  16. package/dist/lib/control-styles.js +1 -1
  17. package/dist/props/components/data-entry.prop.d.ts +36 -2
  18. package/dist/props/components/layout.prop.d.ts +16 -5
  19. package/dist/props/registry.d.ts +1 -1
  20. package/dist/props/registry.js +8 -1
  21. package/dist/styles/base.css +5 -3
  22. package/dist/styles/card-layout.css +11 -6
  23. package/dist/styles/control.css +20 -8
  24. package/dist/styles/data-display-layout.css +51 -20
  25. package/dist/styles/data-entry-layout.css +3 -3
  26. package/dist/styles/dialog-layout.css +12 -0
  27. package/dist/styles/layout.css +1 -1
  28. package/dist/styles/table-layout.css +9 -6
  29. package/dist/styles/text-layout.css +1 -1
  30. package/dist/theme/famgia.service.css +2 -4
  31. package/dist/tokens/components/control.css +4 -3
  32. package/dist/tokens/components/data-display.css +2 -1
  33. package/dist/tokens/components/feedback.css +2 -0
  34. package/dist/tokens/derived.css +20 -0
  35. package/dist/tokens/foundation.css +6 -13
  36. package/docs/CUSTOMER-THEMING.md +82 -3
  37. package/docs/FRAME-COVERAGE-REPORT.md +2 -2
  38. package/docs/data-display/collapsible.tsx +5 -5
  39. package/docs/data-display/descriptions.tsx +188 -109
  40. package/docs/data-display/legend.tsx +17 -4
  41. package/docs/data-display/scroll-area.tsx +75 -5
  42. package/docs/data-display/stat-card.tsx +319 -64
  43. package/docs/data-entry/form.tsx +700 -6
  44. package/docs/data-entry/input.tsx +663 -53
  45. package/docs/data-entry/radio-group.tsx +216 -2
  46. package/docs/data-entry/switch.tsx +210 -4
  47. package/docs/data-entry/upload-crop-dialog.tsx +109 -0
  48. package/docs/layout/nav-list.tsx +556 -35
  49. package/package.json +99 -91
@@ -182,19 +182,19 @@
182
182
  .ui-progress-bar {
183
183
  height: 100%;
184
184
  border-radius: inherit;
185
- background: var(--progress-fill-background, hsl(var(--mark-success)));
185
+ background: var(--progress-fill-background, hsl(var(--mark-success, var(--text-success))));
186
186
  }
187
187
 
188
188
  .ui-progress[data-tone="warning"] .ui-progress-bar {
189
- background: hsl(var(--mark-warning));
189
+ background: hsl(var(--mark-warning, var(--text-warning)));
190
190
  }
191
191
 
192
192
  .ui-progress[data-tone="destructive"] .ui-progress-bar {
193
- background: hsl(var(--mark-destructive));
193
+ background: hsl(var(--mark-destructive, var(--text-error)));
194
194
  }
195
195
 
196
196
  .ui-progress[data-over] .ui-progress-bar {
197
- background-color: hsl(var(--mark-destructive));
197
+ background-color: hsl(var(--mark-destructive, var(--text-error)));
198
198
  background-image: repeating-linear-gradient(
199
199
  45deg,
200
200
  hsl(var(--destructive-foreground) / 0.4) 0,
@@ -223,15 +223,15 @@
223
223
  }
224
224
 
225
225
  .ui-progress-segment[data-tone="success"] {
226
- background: hsl(var(--mark-success));
226
+ background: hsl(var(--mark-success, var(--text-success)));
227
227
  }
228
228
 
229
229
  .ui-progress-segment[data-tone="warning"] {
230
- background: hsl(var(--mark-warning));
230
+ background: hsl(var(--mark-warning, var(--text-warning)));
231
231
  }
232
232
 
233
233
  .ui-progress-segment[data-tone="destructive"] {
234
- background: hsl(var(--mark-destructive));
234
+ background: hsl(var(--mark-destructive, var(--text-error)));
235
235
  }
236
236
 
237
237
  .ui-progress-label {
@@ -273,25 +273,48 @@
273
273
  }
274
274
 
275
275
  .ui-progress-ring-indicator {
276
- stroke: var(--progress-fill-background, hsl(var(--mark-success)));
276
+ stroke: var(--progress-fill-background, hsl(var(--mark-success, var(--text-success))));
277
277
 
278
278
  stroke-linecap: butt;
279
279
  }
280
280
 
281
281
  .ui-progress[data-tone="warning"] .ui-progress-ring-indicator {
282
- stroke: hsl(var(--mark-warning));
282
+ stroke: hsl(var(--mark-warning, var(--text-warning)));
283
283
  }
284
284
 
285
285
  .ui-progress[data-tone="destructive"] .ui-progress-ring-indicator {
286
- stroke: hsl(var(--mark-destructive));
286
+ stroke: hsl(var(--mark-destructive, var(--text-error)));
287
287
  }
288
288
 
289
289
  .ui-progress-ring-label {
290
290
  position: relative;
291
291
  color: hsl(var(--foreground));
292
- font-size: var(--progress-ring-label-font-size);
292
+ font-size: var(--progress-ring-label-font-size, calc(var(--progress-ring-size) * 0.28));
293
293
  font-variant-numeric: tabular-nums;
294
- line-height: 1;
294
+
295
+ line-height: var(--line-height-normal);
296
+
297
+ max-inline-size: calc(
298
+ var(--progress-ring-size) - 2 * var(--progress-ring-border-width) - var(--space-1)
299
+ );
300
+ overflow: hidden;
301
+ text-overflow: ellipsis;
302
+ white-space: nowrap;
303
+ }
304
+
305
+ .ui-disclosure-chevron {
306
+ transition: rotate var(--duration-fast) var(--ease-standard);
307
+ }
308
+
309
+ :is([data-state="open"], [aria-expanded="true"]) > .ui-disclosure-chevron,
310
+ :is([data-state="open"], [aria-expanded="true"]) .ui-disclosure-chevron {
311
+ rotate: 180deg;
312
+ }
313
+
314
+ @media (prefers-reduced-motion: reduce) {
315
+ .ui-disclosure-chevron {
316
+ transition: none;
317
+ }
295
318
  }
296
319
 
297
320
  .ui-legend {
@@ -320,23 +343,28 @@
320
343
  }
321
344
 
322
345
  .ui-legend-swatch[data-tone="default"] {
323
- background: hsl(var(--mark-primary));
346
+ background: hsl(
347
+ var(
348
+ --mark-primary,
349
+ var(--text-primary, from hsl(var(--primary)) var(--text-primary-channels))
350
+ )
351
+ );
324
352
  }
325
353
 
326
354
  .ui-legend-swatch[data-tone="success"] {
327
- background: hsl(var(--mark-success));
355
+ background: hsl(var(--mark-success, var(--text-success)));
328
356
  }
329
357
 
330
358
  .ui-legend-swatch[data-tone="warning"] {
331
- background: hsl(var(--mark-warning));
359
+ background: hsl(var(--mark-warning, var(--text-warning)));
332
360
  }
333
361
 
334
362
  .ui-legend-swatch[data-tone="destructive"] {
335
- background: hsl(var(--mark-destructive));
363
+ background: hsl(var(--mark-destructive, var(--text-error)));
336
364
  }
337
365
 
338
366
  .ui-legend-swatch[data-tone="info"] {
339
- background: hsl(var(--mark-info));
367
+ background: hsl(var(--mark-info, var(--text-info)));
340
368
  }
341
369
 
342
370
  .ui-legend-swatch[data-tone="muted"],
@@ -386,11 +414,11 @@
386
414
  }
387
415
 
388
416
  .ui-feature-list-item[data-state="included"] .ui-feature-list-mark {
389
- color: hsl(var(--mark-success));
417
+ color: hsl(var(--mark-success, var(--text-success)));
390
418
  }
391
419
 
392
420
  .ui-feature-list-item[data-state="limited"] .ui-feature-list-mark {
393
- color: hsl(var(--mark-warning));
421
+ color: hsl(var(--mark-warning, var(--text-warning)));
394
422
  }
395
423
 
396
424
  .ui-feature-list-item[data-state="excluded"] .ui-feature-list-label {
@@ -1069,7 +1097,10 @@
1069
1097
 
1070
1098
  .ui-code-block [data-code-token="link"],
1071
1099
  .ui-prose pre [data-code-token="link"] {
1072
- color: var(--code-block-token-link-color, hsl(var(--text-link)));
1100
+ color: var(
1101
+ --code-block-token-link-color,
1102
+ hsl(var(--text-link, from hsl(var(--primary)) var(--text-link-channels)))
1103
+ );
1073
1104
  text-decoration-line: underline;
1074
1105
 
1075
1106
  text-underline-offset: var(--text-link-underline-offset);
@@ -92,15 +92,15 @@
92
92
  }
93
93
 
94
94
  .ui-password-strength-segment[data-tone="destructive"] {
95
- background: hsl(var(--mark-destructive));
95
+ background: hsl(var(--mark-destructive, var(--text-error)));
96
96
  }
97
97
 
98
98
  .ui-password-strength-segment[data-tone="warning"] {
99
- background: hsl(var(--mark-warning));
99
+ background: hsl(var(--mark-warning, var(--text-warning)));
100
100
  }
101
101
 
102
102
  .ui-password-strength-segment[data-tone="success"] {
103
- background: hsl(var(--mark-success));
103
+ background: hsl(var(--mark-success, var(--text-success)));
104
104
  }
105
105
 
106
106
  .ui-password-strength-meta {
@@ -146,6 +146,18 @@
146
146
  opacity: var(--dialog-close-rest-alpha);
147
147
  }
148
148
 
149
+ :is([data-slot="dialog-close"], [data-slot="sheet-close"])::after {
150
+ content: "";
151
+ position: absolute;
152
+ inset-block-start: 50%;
153
+ inset-inline-start: 50%;
154
+ translate: -50% -50%;
155
+ min-inline-size: var(--dialog-close-size);
156
+ min-block-size: var(--dialog-close-size);
157
+ inline-size: 100%;
158
+ block-size: 100%;
159
+ }
160
+
149
161
  [data-slot="dialog-close"]:hover {
150
162
  opacity: 1;
151
163
  }
@@ -834,7 +834,7 @@
834
834
  .ui-page-header-extra {
835
835
  width: auto;
836
836
  flex-shrink: 1;
837
- min-inline-size: 0;
837
+ min-inline-size: auto;
838
838
  justify-content: flex-end;
839
839
  }
840
840
 
@@ -16,27 +16,30 @@
16
16
  }
17
17
 
18
18
  .ui-table-row[data-tone="primary"] {
19
- --table-row-tone-color: var(--mark-primary);
19
+ --table-row-tone-color: var(
20
+ --mark-primary,
21
+ var(--text-primary, from hsl(var(--primary)) var(--text-primary-channels))
22
+ );
20
23
  }
21
24
 
22
25
  .ui-table-row[data-tone="success"] {
23
- --table-row-tone-color: var(--mark-success);
26
+ --table-row-tone-color: var(--mark-success, var(--text-success));
24
27
  }
25
28
 
26
29
  .ui-table-row[data-tone="warning"] {
27
- --table-row-tone-color: var(--mark-warning);
30
+ --table-row-tone-color: var(--mark-warning, var(--text-warning));
28
31
  }
29
32
 
30
33
  .ui-table-row[data-tone="info"] {
31
- --table-row-tone-color: var(--mark-info);
34
+ --table-row-tone-color: var(--mark-info, var(--text-info));
32
35
  }
33
36
 
34
37
  .ui-table-row[data-tone="attention"] {
35
- --table-row-tone-color: var(--mark-attention);
38
+ --table-row-tone-color: var(--mark-attention, var(--attention));
36
39
  }
37
40
 
38
41
  .ui-table-row[data-tone="destructive"] {
39
- --table-row-tone-color: var(--mark-destructive);
42
+ --table-row-tone-color: var(--mark-destructive, var(--text-error));
40
43
  }
41
44
 
42
45
  .ui-table-row[data-tone] {
@@ -134,7 +134,7 @@
134
134
  }
135
135
 
136
136
  [data-slot="text"][data-link][data-tone="primary"]:not([data-disabled]) {
137
- color: hsl(var(--text-link));
137
+ color: hsl(var(--text-link, from hsl(var(--primary)) var(--text-link-channels)));
138
138
  }
139
139
  [data-slot="text"][data-link]:hover,
140
140
  [data-slot="text"][data-link]:focus-visible {
@@ -7,8 +7,7 @@
7
7
 
8
8
  --primary: 221 83% 53%;
9
9
  --primary-foreground: 0 0% 100%;
10
- --primary-hover: 224 71% 48%;
11
- --primary-active: 226 71% 40%;
10
+
12
11
  --ring: 221 83% 53%;
13
12
 
14
13
  --text-link: 224 76% 45%;
@@ -25,8 +24,7 @@
25
24
  .dark {
26
25
  --primary: 221 90% 66%;
27
26
  --primary-foreground: 222 47% 11%;
28
- --primary-hover: 221 90% 72%;
29
- --primary-active: 221 90% 60%;
27
+
30
28
  --ring: 221 90% 66%;
31
29
  --text-link: 221 90% 72%;
32
30
  --text-brand: 221 90% 70%;
@@ -39,7 +39,8 @@
39
39
 
40
40
  --control-count-font-size: var(--font-size-sm);
41
41
  --control-count-color: var(--muted-foreground);
42
- --control-count-exceeded-color: var(--text-error);
42
+
43
+ --control-count-exceeded-color: initial;
43
44
  --control-count-space-inline: var(--space-1);
44
45
  --textarea-count-inset-block-end: var(--space-1);
45
46
  --textarea-count-inset-inline-end: var(--space-2);
@@ -388,9 +389,9 @@
388
389
  --control-borderless-shadow: none;
389
390
 
390
391
  --control-status-error-border-color: hsl(var(--destructive));
391
- --control-status-warning-border-color: hsl(var(--text-warning));
392
+ --control-status-warning-border-color: initial;
392
393
 
393
- --control-status-warning-outline-color: var(--text-warning);
394
+ --control-status-warning-outline-color: initial;
394
395
  }
395
396
 
396
397
  @media (pointer: coarse) {
@@ -113,7 +113,8 @@
113
113
  --progress-ring-size: 2.75rem;
114
114
  --progress-ring-size-sm: 2rem;
115
115
  --progress-ring-border-width: var(--stroke-lg);
116
- --progress-ring-label-font-size: var(--font-size-xs);
116
+
117
+ --progress-ring-label-font-size: initial;
117
118
 
118
119
  --legend-gap: 0.875rem;
119
120
  --legend-item-gap: 0.3125rem;
@@ -43,6 +43,8 @@
43
43
 
44
44
  --dialog-close-rest-alpha: 0.7;
45
45
 
46
+ --dialog-close-size: var(--touch-target-min);
47
+
46
48
  --dialog-step-up-error-font-size: var(--font-size-sm);
47
49
 
48
50
  --alert-radius: var(--card-radius);
@@ -20,6 +20,15 @@
20
20
  --control-outline-channels: h s calc(l - 5.3);
21
21
  --control-outline-alpha: 0.11;
22
22
 
23
+ --text-link: initial;
24
+ --text-brand: initial;
25
+ --text-primary: initial;
26
+
27
+ --text-link-channels: var(--primary-hover-channels);
28
+ --text-brand-channels: var(--primary-hover-channels);
29
+
30
+ --text-primary-channels: var(--primary-active-channels);
31
+
23
32
  --destructive-hover: 359 50.6% 53.1%;
24
33
  --destructive-active: 355 70.6% 33.3%;
25
34
 
@@ -39,6 +48,11 @@
39
48
  --control-outline-channels: h calc(s * 0.99) calc(l - 25.1);
40
49
  --control-outline-alpha: 0.29;
41
50
 
51
+ --text-link-channels: h s l;
52
+ --text-brand-channels: h s l;
53
+
54
+ --text-primary-channels: var(--primary-hover-channels);
55
+
42
56
  --destructive-hover: 358.8 42.3% 53.1%;
43
57
  --destructive-active: 357.3 50.6% 34.1%;
44
58
 
@@ -54,6 +68,9 @@
54
68
  --primary-active: 268.7 100% 34.5%;
55
69
  --primary-border: 268.7 68% 66%;
56
70
  --control-outline: 268.7 100% 44.7%;
71
+ --text-link: 268.7 100% 41.6%;
72
+ --text-brand: 268.7 100% 41.6%;
73
+ --text-primary: 268.7 100% 34.5%;
57
74
  }
58
75
 
59
76
  .dark,
@@ -62,5 +79,8 @@
62
79
  --primary-active: 268.7 100% 81.1%;
63
80
  --primary-border: 268.7 46.7% 23.5%;
64
81
  --control-outline: 268.7 99% 61.8%;
82
+ --text-link: 268.7 100% 86.9%;
83
+ --text-brand: 268.7 100% 86.9%;
84
+ --text-primary: 268.7 100% 92.7%;
65
85
  }
66
86
  }
@@ -40,21 +40,17 @@
40
40
  --text-tertiary: 44 5% 55%;
41
41
  --text-disabled: 40 6% 68%;
42
42
 
43
- --text-link: 268.3 100% 41.6%;
44
- --text-brand: 268.3 100% 41.6%;
45
-
46
- --text-primary: 268 100% 34.5%;
47
43
  --text-success: 152 84% 22%;
48
44
  --text-warning: 36 100% 28%;
49
45
  --text-info: 221 70% 38%;
50
46
  --text-error: 357 75% 38%;
51
47
 
52
- --mark-primary: var(--text-primary);
53
- --mark-success: var(--text-success);
54
- --mark-warning: var(--text-warning);
55
- --mark-info: var(--text-info);
56
- --mark-destructive: var(--text-error);
57
- --mark-attention: var(--attention);
48
+ --mark-primary: initial;
49
+ --mark-success: initial;
50
+ --mark-warning: initial;
51
+ --mark-info: initial;
52
+ --mark-destructive: initial;
53
+ --mark-attention: initial;
58
54
 
59
55
  --secondary-hover: 45 15% 91%;
60
56
  --secondary-active: 45 15% 88%;
@@ -349,10 +345,7 @@
349
345
 
350
346
  --text-tertiary: 44 6% 56%;
351
347
  --text-disabled: 45 6% 40%;
352
- --text-link: 268.7 100% 86.9%;
353
- --text-brand: 268.7 100% 86.9%;
354
348
 
355
- --text-primary: 262.7 100% 92.7%;
356
349
  --text-success: 146 55% 64%;
357
350
  --text-warning: 44 95% 62%;
358
351
  --text-info: 221 70% 72%;
@@ -6,6 +6,82 @@ The design system ships a complete, **zero-config default theme**. Two audiences
6
6
 
7
7
  ---
8
8
 
9
+ ## The order to reach for things — read this before anything below
10
+
11
+ This document lists a lot of ways to change how the system looks. They are not alternatives; they
12
+ are a **priority order**, and the whole point of the order is that you stop at the first level that
13
+ does the job. Ant Design states the same rule for the same reason — _"In most cases, using Seed
14
+ Tokens is sufficient for custom themes"_ — and the cost of skipping down a level is real, not
15
+ stylistic.
16
+
17
+ | level | what it is | when | what you give up by going lower |
18
+ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------- |
19
+ | **1 · seed** | `--primary`, `--radius`, `--font-size-base`, `--shadow-color` — the handful everything derives from. `pnpm gen:brand '#RRGGBB'` writes them for you | **almost always** | nothing — this is the main road |
20
+ | **2 · role** | a named semantic token: `--text-link`, `--accent`, `--card-radius` | when the seed is right but ONE role must differ | that role stops following the seed; a later brand change will not move it |
21
+ | **3 · scope** | the same token under `[data-tenant]` / `.dark` / any subtree | multi-tenant, or one region that differs | nothing extra, provided you set the token and not a literal |
22
+ | **4 · instance** | a documented prop, or `style={{ "--x": … }}` on one element | this one element, this one time | it is invisible to every audit and every future theme |
23
+
24
+ **Why the order matters more than the count of knobs.** Every level below the first is a value that
25
+ has stopped being derived. A literal at level 4 is not "more control" — it is a pixel that has left
26
+ the system, and nothing will tell you when the brand moves past it. That is why the generator
27
+ writes three tokens and not thirty: the ones it leaves out are not missing, they are _downstream_.
28
+
29
+ **If you find yourself at level 4 twice for the same reason, the token is missing.** File it —
30
+ `docs/COMPOSITION-VS-COMPONENT.md` has the test for whether it is a token or a composition. Adding
31
+ the knob is how the system absorbs the change; repeating the literal is how it drifts.
32
+
33
+ ---
34
+
35
+ ## Start from one hex — `pnpm gen:brand`
36
+
37
+ Everything below this section is the manual route, and it is worth reading because it says what each
38
+ role means. But the colour half of a brand file is mechanical, and two of its decisions are ones CSS
39
+ cannot make at all, so there is a generator:
40
+
41
+ ```sh
42
+ pnpm gen:brand '#2563EB' --name acme --out src/theme
43
+ ```
44
+
45
+ It writes `acme.service.css` and `acme.email.ts`, and prints what it measured:
46
+
47
+ ```
48
+ light seed #2563eb · dark seed #5586ef (lifted 10.4%)
49
+
50
+ ✓ primary label on fill (light) 5.17:1 (needs 4.5:1)
51
+ ✓ primary label on fill (dark) 6.03:1 (needs 4.5:1)
52
+ ✓ primary fill on canvas (light) 5.09:1 (needs 3:1)
53
+ ✓ primary fill on canvas (dark) 5.10:1 (needs 3:1)
54
+ ✓ link ink on canvas (light) 6.73:1 (needs 4.5:1)
55
+ ✓ link ink on canvas (dark) 6.36:1 (needs 4.5:1)
56
+ ```
57
+
58
+ It exits non-zero when a ratio misses, so a brand that cannot meet AA says so at the point you
59
+ choose the colour rather than at the point a user cannot read a link.
60
+
61
+ **The two things it does that hand-authoring keeps getting wrong:**
62
+
63
+ - **A dark seed.** Nothing in CSS lifts a light seed onto the dark spine, so a brand file that sets
64
+ `--primary` only in `:root` keeps the GoDX violet in dark mode and nothing says so. The generator
65
+ holds your hue and saturation and moves lightness until the dark seed reads against the dark
66
+ canvas the way the light seed reads against the light one.
67
+ - **Email.** `src/email/tokens.generated.ts` bakes literal hex at build time, because Gmail strips
68
+ `<style>` and Outlook ignores custom properties. Those literals are _this package's_ seed, so a
69
+ re-themed product still sends GoDX-violet mail — the one surface no amount of CSS reaches. The
70
+ emitted `acme.email.ts` is the override that corrects it.
71
+
72
+ **What it deliberately leaves out**, because writing them would be a regression:
73
+
74
+ - `--primary-hover`, `--primary-active`, `--primary-border`, `--control-outline` — these DERIVE from
75
+ the `--primary` in scope at the element that paints them (gh#678). A literal pins them to one seed
76
+ and stops them following the next change.
77
+ - `--brand` / `--brand-foreground` — the identity role is independent of the action colour on
78
+ purpose (gh#250), so the logo does not retint when a tenant changes its button colour.
79
+
80
+ The output is a starting point, not a ceiling: every role in the tables below can still be set by
81
+ hand in the generated file, and an explicit value always beats a derived default.
82
+
83
+ ---
84
+
9
85
  ## Internal apps — zero config
10
86
 
11
87
  ```ts
@@ -165,17 +241,20 @@ Import the styles, then set anchor tokens in your app's `theme.css` (loaded afte
165
241
 
166
242
  ### Focus ring — THE SWITCH FIRST, then three levels of override
167
243
 
168
- **The indicator ships OFF.** `--focus-outline` is `0`, and every painted focus length multiplies by it, so by default nothing paints a focus mark. That is a product decision with a stated cost — it forfeits WCAG 2.2 SC 2.4.7 (AA) and a JIS X 8341-3 AA claim — recorded in `docs/DESIGN-AUTHORITY.md`. Turning it on is ONE attribute on the root element, no CSS:
244
+ **The indicator ships ON.** `--focus-outline` is `1` (`src/tokens/foundation.css`), and every painted focus length multiplies by it, so a focus mark paints by default and the package meets WCAG 2.2 SC 2.4.7. It did ship `0` once, and that earlier default — with its stated cost, a forfeited SC 2.4.7 and JIS X 8341-3 AA claim — is recorded in `docs/DESIGN-AUTHORITY.md`; gh#544 flipped it. Nothing has to be opted into.
245
+
246
+ Switching the paint is ONE attribute on the root element, no CSS:
169
247
 
170
248
  ```html
171
249
  <html data-focus-outline="on"></html>
250
+ <html data-focus-outline="off"></html>
172
251
  ```
173
252
 
174
253
  (`AppProvider` has the equivalent.) Every `:focus-visible` selector is present either way; only the paint is switched.
175
254
 
176
255
  Every ring is then drawn by a single rule (`src/styles/focus-ring.css`) reading the tokens above. Nothing else paints one; a test fails the build if a stylesheet tries.
177
256
 
178
- **Thickness is `--focus-ring-weight`, never `--focus-ring-width`.** The width is DERIVED — `calc(var(--focus-ring-weight) * var(--focus-outline))` — so assigning it directly paints a ring even while the indicator is switched off, and that is exactly what the build-failing test forbids a stylesheet to do.
257
+ **Thickness is `--focus-ring-weight`, never `--focus-ring-width`.** The width is DERIVED — `calc(var(--focus-ring-weight) * var(--focus-outline))` — so assigning it directly paints a ring even where the indicator is switched off, and that is exactly what the build-failing test forbids a stylesheet to do.
179
258
 
180
259
  ```css
181
260
  :root {
@@ -203,7 +282,7 @@ Every ring is then drawn by a single rule (`src/styles/focus-ring.css`) reading
203
282
  <div style={{ "--focus-ring-color": "0 84% 60%" } as React.CSSProperties}>…</div>
204
283
  ```
205
284
 
206
- **Turning it back off** — drop the `data-focus-outline` attribute (or set `--focus-outline: 0`), which is the shipped state. Do NOT reach for `--focus-ring-width`: it is derived from the weight and the switch, and pinning it to any value is the same mistake in the other direction. Note the cost either way — with no visible focus indicator the package does not meet WCAG 2.2 SC 2.4.7.
285
+ **Turning it off** — set `--focus-outline: 0` (or `data-focus-outline="off"`). Do NOT reach for `--focus-ring-width`: it is derived from the weight and the switch, and pinning it to any value is the same mistake in the other direction. Note the cost: with no visible focus indicator the package no longer meets WCAG 2.2 SC 2.4.7, so this is a claim you are giving up, not a style preference.
207
286
 
208
287
  **Adding your own component to the system**: put `ui-focus-ring` (or `ui-focus-ring-outline` when the mark needs a gap) on the focusable element.
209
288
 
@@ -135,7 +135,7 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
135
135
  | TreeSelect | ✓ | ✓ | · | · | · | · | · | · | · | · |
136
136
  | Transfer | ✓ | N/A | · | · | · | · | · | · | · | · |
137
137
  | Upload | ✓ | ✓ | · | · | · | · | · | · | · | · |
138
- | UploadCropDialog | ✓ | · | · | · | · | · | · | · | · | · |
138
+ | UploadCropDialog | ✓ | N/A | N/A | · | · | · | · | · | · | · |
139
139
  | ColorPicker | ✓ | N/A | · | · | · | · | · | · | · | · |
140
140
  | Slider | ✓ | N/A | · | · | · | · | · | · | · | · |
141
141
  | Calendar | ✓ | N/A | · | N/A | · | · | · | · | · | · |
@@ -173,7 +173,7 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
173
173
  | Carousel | ✓ | N/A | N/A | · | · | · | · | · | · | · |
174
174
  | AppSettingPicker | ✓ | N/A | · | · | · | · | · | · | · | · |
175
175
  | AppSettingToggle | ✓ | N/A | · | · | · | · | · | · | · | · |
176
- | Field | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
176
+ | Field | ✓ | N/A | · | N/A | · | · | · | · | · | · |
177
177
  | LineChart | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
178
178
  | BarChart | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
179
179
  | CompactBarTrend | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
@@ -10,7 +10,7 @@ import {
10
10
  } from "@godxjp/ui/data-display";
11
11
  import { Button, Text } from "@godxjp/ui/general";
12
12
  import { Flex, PageContainer } from "@godxjp/ui/layout";
13
- import { ChevronsUpDown } from "lucide-react";
13
+ import { ChevronDown } from "lucide-react";
14
14
  import { useState } from "react";
15
15
 
16
16
  /**
@@ -43,7 +43,7 @@ export default function Demo() {
43
43
  aria-label="開閉"
44
44
  className="transition-transform data-[state=open]:rotate-180"
45
45
  >
46
- <ChevronsUpDown />
46
+ <ChevronDown className="ui-disclosure-chevron" />
47
47
  </Button>
48
48
  </CollapsibleTrigger>
49
49
  </Flex>
@@ -74,7 +74,7 @@ export default function Demo() {
74
74
  aria-label="開閉"
75
75
  className="transition-transform data-[state=open]:rotate-180"
76
76
  >
77
- <ChevronsUpDown />
77
+ <ChevronDown className="ui-disclosure-chevron" />
78
78
  </Button>
79
79
  </CollapsibleTrigger>
80
80
  </Flex>
@@ -112,7 +112,7 @@ export default function Demo() {
112
112
  aria-label="開閉"
113
113
  className="transition-transform data-[state=open]:rotate-180"
114
114
  >
115
- <ChevronsUpDown />
115
+ <ChevronDown className="ui-disclosure-chevron" />
116
116
  </Button>
117
117
  </CollapsibleTrigger>
118
118
  </Flex>
@@ -144,7 +144,7 @@ export default function Demo() {
144
144
  aria-label="開閉"
145
145
  className="transition-transform data-[state=open]:rotate-180"
146
146
  >
147
- <ChevronsUpDown />
147
+ <ChevronDown className="ui-disclosure-chevron" />
148
148
  </Button>
149
149
  </CollapsibleTrigger>
150
150
  </Flex>