@godxjp/ui 23.0.0 → 23.1.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 (52) hide show
  1. package/dist/app/date-format-labels.d.ts +14 -2
  2. package/dist/app/date-format-labels.js +1 -1
  3. package/dist/app/date-formats.d.ts +14 -2
  4. package/dist/app/date-formats.js +9 -2
  5. package/dist/components/data-display/data-table.d.ts +8 -1
  6. package/dist/components/data-display/data-table.js +9 -2
  7. package/dist/components/data-entry/checkbox.js +1 -1
  8. package/dist/components/data-entry/form-field.js +2 -2
  9. package/dist/components/data-entry/input.js +1 -1
  10. package/dist/components/data-entry/number-input.js +27 -6
  11. package/dist/components/data-entry/radio.js +1 -1
  12. package/dist/components/data-entry/switch.js +1 -1
  13. package/dist/components/feedback/dialog.js +2 -2
  14. package/dist/components/feedback/sheet.js +2 -22
  15. package/dist/components/navigation/filter-bar.js +2 -1
  16. package/dist/components/navigation/tabs.js +16 -4
  17. package/dist/i18n/messages/en.json +5 -1
  18. package/dist/i18n/messages/ja.json +5 -1
  19. package/dist/i18n/messages/vi.json +5 -1
  20. package/dist/lib/breakpoint-token.d.ts +8 -0
  21. package/dist/lib/breakpoint-token.js +29 -0
  22. package/dist/props/components/data-display.prop.d.ts +8 -1
  23. package/dist/props/components/navigation.prop.d.ts +8 -1
  24. package/dist/props/registry.d.ts +6 -1
  25. package/dist/props/registry.js +6 -0
  26. package/dist/props/vocabulary/data.prop.d.ts +5 -0
  27. package/dist/props/vocabulary/index.d.ts +1 -1
  28. package/dist/styles/control.css +41 -5
  29. package/dist/styles/focus-ring.css +28 -0
  30. package/dist/styles/form-layout.css +4 -0
  31. package/dist/styles/layout.css +2 -1
  32. package/dist/styles/navigation-layout.css +13 -2
  33. package/dist/styles/shell-layout.css +4 -0
  34. package/dist/styles/table-layout.css +5 -2
  35. package/dist/tokens/components/control.css +8 -3
  36. package/dist/tokens/components/navigation.css +2 -0
  37. package/docs/CONSUMER-RULES.md +7 -2
  38. package/docs/CUSTOMER-THEMING.md +18 -8
  39. package/docs/DATETIME.md +11 -2
  40. package/docs/SPACING.md +35 -5
  41. package/docs/TOKENS.md +1 -1
  42. package/docs/data-display/data-table/index.tsx +17 -2
  43. package/docs/data-display/table.tsx +1 -1
  44. package/docs/data-entry/segmented-in-filter-row.tsx +77 -0
  45. package/docs/navigation/dropdown-menu.tsx +4 -4
  46. package/docs/navigation/tabs.tsx +72 -0
  47. package/docs/roadmap/ai-chat-components.md +1 -2
  48. package/docs/roadmap/tree-components.md +0 -1
  49. package/package.json +11 -17
  50. package/scripts/_agent-setup.mjs +18 -0
  51. package/scripts/guinea-pig-skill.md +12 -6
  52. package/scripts/ui-audit.mjs +184 -6
@@ -4,4 +4,4 @@ export type { TitleProp, SubtitleProp, StatusProp, DescriptionProp, ExtraProp, F
4
4
  export type { PageDensityProp, PageContainerVariantProp, CenteredShellWidthProp, CenteredShellAlignProp, CenteredShellPresetProp, ErrorSurfaceModeProp, ErrorSurfaceStatusProp, AuthShellPresetProp, MobileShellHeightProp, MobileShellWidthProp, OrientationProp, TableDensityProp, DensityProp, GapNameProp, PadProp, PadRawProp, PadSides, GapProp, GapStepProp, } from "./layout.prop.js";
5
5
  export type { ButtonVariantProp, ButtonSizeProp, BadgeVariantProp, AppSettingPickerAppearanceProp, AppSettingToggleAppearanceProp, ShapeProp, AvatarShapeProp, TextSizeProp, TextToneProp, FontWeightProp, HeadingLevelProp, TextAlignProp, TextWhitespaceProp, SizeProp, ControlStatusProp, ControlVariantProp, FormLayoutProp, DescriptionsLayoutProp, BreakpointProp, ConfirmVariantProp, ToneProp, AlertVariantProp, SortDirectionProp, ColumnAlignProp, SortStateProp, RevealDelayProp, ActivityVariantProp, ActivityAnnounceProp, } from "./interaction.prop.js";
6
6
  export type { BreadcrumbItemProp, BreadcrumbProp, BreadcrumbItemMenuProp, BreadcrumbItemMenuEntryProp, BreadcrumbSeparatorProp, BreadcrumbItemRenderProp, } from "./navigation.prop.js";
7
- export type { GetRowIdProp, OnRowClickProp, ColumnDefProp, TablePresetProp, TableColumnPriorityProp, TableCellIndentProp, SelectedIdsProp, OnSelectChangeProp, OnTableDensityChangeProp, TableRowToneProp, RowToneProp, OnSortChangeProp, OnSearchChangeProp, OnClearFiltersProp, HasActiveFiltersProp, StickyProp, ColumnFixedProp, ColumnFilterValueProp, ColumnFilterItemProp, ColumnFilterStateProp, OnColumnFilterChangeProp, ColumnCompareProp, ColumnSorterProp, TableSelectionItemProp, TableRowSelectionProp, TableExpandableProp, TableSummaryProp, TableScrollProp, TableStickyProp, OnRowProp, TablePaginationProp, DescriptionsColumnProp, DescriptionsSpanProp, DescriptionsItemsProp, } from "./data.prop.js";
7
+ export type { GetRowIdProp, GetRowLabelProp, OnRowClickProp, ColumnDefProp, TablePresetProp, TableColumnPriorityProp, TableCellIndentProp, SelectedIdsProp, OnSelectChangeProp, OnTableDensityChangeProp, TableRowToneProp, RowToneProp, OnSortChangeProp, OnSearchChangeProp, OnClearFiltersProp, HasActiveFiltersProp, StickyProp, ColumnFixedProp, ColumnFilterValueProp, ColumnFilterItemProp, ColumnFilterStateProp, OnColumnFilterChangeProp, ColumnCompareProp, ColumnSorterProp, TableSelectionItemProp, TableRowSelectionProp, TableExpandableProp, TableSummaryProp, TableScrollProp, TableStickyProp, OnRowProp, TablePaginationProp, DescriptionsColumnProp, DescriptionsSpanProp, DescriptionsItemsProp, } from "./data.prop.js";
@@ -177,8 +177,8 @@
177
177
  content: "";
178
178
  position: absolute;
179
179
  inset: 0;
180
- min-inline-size: var(--button-bare-target-size);
181
- min-block-size: var(--button-bare-target-size);
180
+ min-inline-size: var(--button-bare-target-size, var(--control-height-xs));
181
+ min-block-size: var(--button-bare-target-size, var(--control-height-xs));
182
182
  margin: auto;
183
183
  }
184
184
 
@@ -1338,6 +1338,20 @@
1338
1338
  inline-size: 0.75rem;
1339
1339
  block-size: 0.75rem;
1340
1340
  }
1341
+
1342
+ @media (pointer: coarse) {
1343
+ .ui-number-input:not([data-size="xs"]):not([data-size="sm"]) {
1344
+ --control-height: var(--number-input-touch-height);
1345
+ }
1346
+
1347
+ .ui-number-input-steppers {
1348
+ gap: 0;
1349
+ }
1350
+
1351
+ .ui-number-input-step {
1352
+ block-size: calc(var(--control-height) / 2);
1353
+ }
1354
+ }
1341
1355
  }
1342
1356
 
1343
1357
  @layer components {
@@ -1887,6 +1901,22 @@
1887
1901
  transition: opacity 150ms ease;
1888
1902
  }
1889
1903
 
1904
+ .ui-control-inline-affix-action {
1905
+ position: relative;
1906
+ }
1907
+
1908
+ .ui-control-inline-affix-action::after {
1909
+ content: "";
1910
+ position: absolute;
1911
+ inset-block-start: 50%;
1912
+ inset-inline-start: 50%;
1913
+ translate: -50% -50%;
1914
+ min-inline-size: var(--touch-target-min);
1915
+ min-block-size: var(--touch-target-min);
1916
+ inline-size: 100%;
1917
+ block-size: 100%;
1918
+ }
1919
+
1890
1920
  .ui-control-inline-affix-action:hover,
1891
1921
  .ui-control-inline-affix-action:focus-visible {
1892
1922
  color: hsl(var(--foreground));
@@ -2108,6 +2138,8 @@
2108
2138
  background-color: hsl(var(--muted));
2109
2139
 
2110
2140
  border-radius: 0;
2141
+
2142
+ padding-block: var(--calendar-bordered-weekday-padding-block);
2111
2143
  }
2112
2144
 
2113
2145
  .ui-calendar[data-bordered="true"] .ui-calendar-grid {
@@ -2536,7 +2568,7 @@
2536
2568
  }
2537
2569
 
2538
2570
  .ui-button--xs {
2539
- height: var(--button-xs-height);
2571
+ height: var(--button-xs-height, var(--control-height-xs));
2540
2572
  gap: var(--button-xs-space-gap);
2541
2573
  padding-inline: var(--button-xs-space-inline);
2542
2574
  font-size: var(--button-xs-font-size);
@@ -2642,8 +2674,8 @@
2642
2674
 
2643
2675
  .ui-control[data-status="warning"],
2644
2676
  .ui-control-multiline[data-status="warning"] {
2645
- --focus-ring-color: var(--control-status-warning-border-color);
2646
- --focus-outline-color: var(--control-status-warning-border-color);
2677
+ --focus-ring-color: var(--control-status-warning-outline-color);
2678
+ --focus-outline-color: var(--control-status-warning-outline-color);
2647
2679
  --focus-ring-glow-color: var(--control-status-warning-glow-color);
2648
2680
  --focus-ring-glow-alpha: var(--control-status-warning-glow-alpha);
2649
2681
  }
@@ -2988,6 +3020,10 @@
2988
3020
  justify-content: center;
2989
3021
  }
2990
3022
 
3023
+ .ui-segmented:not([data-orientation="vertical"]) {
3024
+ row-gap: var(--segmented-track-padding);
3025
+ }
3026
+
2991
3027
  .ui-segmented[data-orientation="vertical"] {
2992
3028
  flex-direction: column;
2993
3029
  }
@@ -13,6 +13,19 @@
13
13
  .ui-toggle,
14
14
  .ui-checkbox,
15
15
  .ui-radio,
16
+ .ui-slider-thumb,
17
+ .ui-control-affix-action,
18
+ .ui-control-inline-affix-action,
19
+ .ui-search-input-clear,
20
+ .ui-tag-input-remove,
21
+ .ui-color-picker-input,
22
+ .ui-upload-tile-add,
23
+ .ui-upload-picture-empty,
24
+ .ui-upload-dropzone,
25
+ .ui-tabs-add,
26
+ .ui-steps-control,
27
+ .ui-carousel-previous,
28
+ .ui-carousel-next,
16
29
  .ui-steps-inline-control,
17
30
  .ui-pagination-link,
18
31
  .ui-legal-document-toc-link,
@@ -28,6 +41,7 @@
28
41
  ):focus-visible,
29
42
 
30
43
  .ui-checkbox[data-focus-visible],
44
+ .ui-radio[data-focus-visible],
31
45
  .ui-switch[data-focus-visible],
32
46
  .ui-slider-thumb[data-focus-visible],
33
47
  .ui-focus-ring[data-focus-visible],
@@ -59,6 +73,19 @@
59
73
  .ui-toggle,
60
74
  .ui-checkbox,
61
75
  .ui-radio,
76
+ .ui-slider-thumb,
77
+ .ui-control-affix-action,
78
+ .ui-control-inline-affix-action,
79
+ .ui-search-input-clear,
80
+ .ui-tag-input-remove,
81
+ .ui-color-picker-input,
82
+ .ui-upload-tile-add,
83
+ .ui-upload-picture-empty,
84
+ .ui-upload-dropzone,
85
+ .ui-tabs-add,
86
+ .ui-steps-control,
87
+ .ui-carousel-previous,
88
+ .ui-carousel-next,
62
89
  .ui-steps-inline-control,
63
90
  .ui-pagination-link,
64
91
  .ui-legal-document-toc-link,
@@ -74,6 +101,7 @@
74
101
  ):focus-visible,
75
102
 
76
103
  :root[data-focus-outline="on"] .ui-checkbox[data-focus-visible],
104
+ :root[data-focus-outline="on"] .ui-radio[data-focus-visible],
77
105
  :root[data-focus-outline="on"] .ui-switch[data-focus-visible],
78
106
  :root[data-focus-outline="on"] .ui-slider-thumb[data-focus-visible],
79
107
  :root[data-focus-outline="on"] .ui-focus-ring[data-focus-visible],
@@ -2,6 +2,10 @@
2
2
 
3
3
  @layer components {
4
4
 
5
+ .ui-form-field-note {
6
+ word-break: auto-phrase;
7
+ }
8
+
5
9
  .ui-form-field-required {
6
10
  margin-inline-start: var(--space-inline-xs);
7
11
  color: hsl(var(--destructive));
@@ -997,7 +997,8 @@
997
997
  flex-wrap: nowrap;
998
998
  min-width: 0;
999
999
  align-items: center;
1000
- justify-content: flex-end;
1000
+
1001
+ justify-content: safe flex-end;
1001
1002
  gap: var(--pagination-gap);
1002
1003
  }
1003
1004
 
@@ -103,6 +103,11 @@
103
103
  flex: 1 1 auto;
104
104
  }
105
105
 
106
+ [data-slot="tabs"][data-orientation="vertical"] > [data-slot="tabs-list"],
107
+ [data-slot="tabs"][data-orientation="vertical"] > .ui-tabs-bar {
108
+ flex-shrink: 0;
109
+ }
110
+
106
111
  .ui-tabs-extra {
107
112
  display: flex;
108
113
  flex: 0 0 auto;
@@ -194,6 +199,12 @@
194
199
  block-size: var(--tabs-add-icon-size);
195
200
  }
196
201
 
202
+ .ui-pagination {
203
+ max-inline-size: 100%;
204
+ overflow-x: auto;
205
+ scrollbar-width: thin;
206
+ }
207
+
197
208
  .ui-pagination .ui-button svg,
198
209
  .ui-pagination .ui-pagination-link svg {
199
210
  inline-size: var(--pagination-icon-size);
@@ -205,11 +216,11 @@
205
216
  }
206
217
 
207
218
  .ui-pagination[data-align="center"] {
208
- justify-content: center;
219
+ justify-content: safe center;
209
220
  }
210
221
 
211
222
  .ui-pagination[data-align="end"] {
212
- justify-content: flex-end;
223
+ justify-content: safe flex-end;
213
224
  }
214
225
 
215
226
  .ui-pagination[data-size="sm"] {
@@ -2322,6 +2322,10 @@
2322
2322
  .ui-mobile-shell {
2323
2323
  --control-height: var(--mobile-shell-control-height);
2324
2324
 
2325
+ --control-height-sm: calc(var(--control-height) - calc(0.25rem * var(--scaling)));
2326
+ --control-height-lg: calc(var(--control-height) + calc(0.25rem * var(--scaling)));
2327
+ --control-height-xs: calc(var(--control-height) - calc(0.5rem * var(--scaling)));
2328
+
2325
2329
  --mobile-shell-padding-inline: calc(
2326
2330
  var(--mobile-shell-inset-inline) + var(--mobile-shell-safe-inset-inline)
2327
2331
  );
@@ -44,8 +44,7 @@
44
44
  }
45
45
 
46
46
  .ui-table-row[data-tone] > [data-slot="table-cell"]:first-child {
47
- border-inline-start: var(--table-row-tone-rail-width) solid
48
- hsl(var(--table-row-tone-color));
47
+ border-inline-start: var(--table-row-tone-rail-width) solid hsl(var(--table-row-tone-color));
49
48
  padding-inline-start: calc(var(--table-cell-space-x) - var(--table-row-tone-rail-width));
50
49
  }
51
50
 
@@ -206,6 +205,10 @@
206
205
  }
207
206
 
208
207
  .ui-data-table-empty {
208
+ padding: var(--empty-state-space-y) var(--empty-state-space-x);
209
+ }
210
+
211
+ .ui-data-table-empty .ui-empty-state {
209
212
  padding: 0;
210
213
  }
211
214
 
@@ -32,6 +32,8 @@
32
32
  --control-status-warning-glow-color: var(--warning);
33
33
  --control-status-warning-glow-alpha: var(--control-outline-error-alpha);
34
34
 
35
+ --number-input-touch-height: var(--band-height-2xl);
36
+
35
37
  --control-variant-filled-background: var(--muted);
36
38
  --control-variant-filled-hover-background: var(--accent);
37
39
 
@@ -286,6 +288,8 @@
286
288
  --calendar-footer-space-block-start: var(--space-2);
287
289
  --calendar-footer-border-width: var(--stroke-hairline);
288
290
 
291
+ --calendar-bordered-weekday-padding-block: var(--space-1);
292
+
289
293
  --transfer-pane-min-height: 14rem;
290
294
  --transfer-pane-radius: var(--radius-md);
291
295
  --transfer-panes-space-gap: var(--space-3);
@@ -317,9 +321,9 @@
317
321
  --month-picker-icon-size: var(--icon-size-md);
318
322
  --month-picker-separator-icon-size: var(--icon-size-sm);
319
323
 
320
- --button-xs-height: var(--control-height-xs);
324
+ --button-xs-height: initial;
321
325
 
322
- --button-bare-target-size: var(--control-height-xs);
326
+ --button-bare-target-size: initial;
323
327
  --button-xs-space-inline: var(--space-2);
324
328
  --button-xs-space-gap: var(--space-1);
325
329
  --button-xs-font-size: var(--font-size-xs);
@@ -369,7 +373,8 @@
369
373
 
370
374
  --control-status-error-border-color: hsl(var(--destructive));
371
375
  --control-status-warning-border-color: hsl(var(--text-warning));
372
- --control-status-warning-outline-color: var(--warning);
376
+
377
+ --control-status-warning-outline-color: var(--text-warning);
373
378
  }
374
379
 
375
380
  @media (pointer: coarse) {
@@ -41,6 +41,8 @@
41
41
  --tabs-list-max-inline-size: 100%;
42
42
  --tabs-list-overflow: auto;
43
43
 
44
+ --tabs-placement-responsive-breakpoint-width: 48rem;
45
+
44
46
  --tabs-indicator-background: initial;
45
47
  --tabs-indicator-size: 2px;
46
48
  --tabs-indicator-offset: 0px;
@@ -11,7 +11,12 @@ Read this once; the audit enforces it. Everything else in `docs/` is for contrib
11
11
  7. Colours are semantic tokens (`tone="destructive"`, `bg-primary`), never palette names, hex, or `bg-black` / `text-white`.
12
12
  8. Sizes come from props (`size`, `width`, `columns`), never `w-[240px]` / `max-h-[420px]`.
13
13
  9. Logical directions (`ms-`, `me-`, `start-`, `end-`) when a utility is unavoidable; never `ml-` / `left-`.
14
- 10. Run `node node_modules/@godxjp/ui/scripts/ui-audit.mjs <dir>` before every review; then `visual-audit.mjs <url>` on the running app. Zero errors is the bar.
14
+ 10. A `Card` whose whole body is a table gets `<CardContent flush>` the table must touch the
15
+ card's inner edge. A default `CardContent` pads 16px while the table draws its own border, so
16
+ the table reads as a second box inside the card, and a wide one runs past the card entirely.
17
+ (`flush` alone is enough: it drops the border, the radius and the inline padding. `tight` is a
18
+ different knob — it governs the header band, not the body's edges.)
19
+ 11. Run `node node_modules/@godxjp/ui/scripts/ui-audit.mjs --changed` before every review; a browser sweep with `visual-audit.mjs` is a separate, heavier run — do it when the work is a visual review, not on every change; `visual-audit.mjs <url>` on the running app. Zero errors is the bar.
15
20
 
16
21
  **Opting a deliberate exception out.** Name the rule; the block form also has to say why.
17
22
 
@@ -45,6 +50,6 @@ information belongs on the screen:
45
50
  - `ServiceLauncherCard` already has this shape as a prop — `disabledReason`, rendered above the
46
51
  action — and it is the pattern to copy anywhere else.
47
52
 
48
- If the control is disabled *pending something the user can fix*, prefer leaving it enabled and
53
+ If the control is disabled _pending something the user can fix_, prefer leaving it enabled and
49
54
  explaining on submit: an inert control with a paragraph beside it is often a validation message
50
55
  wearing a disguise.
@@ -89,7 +89,7 @@ Import the styles, then set anchor tokens in your app's `theme.css` (loaded afte
89
89
 
90
90
  /* ── Shadow tint — ONE knob ─────────────────────────────────────── */ --shadow-color: 12 26 49; /* RGB channels (default 0 0 0). Tints the WHOLE shadow ramp. */
91
91
 
92
- /* ── Brand depth — all opt-in, all quiet by default ─────────────── */ --shadow-glow: 0 8px 20px hsl(var(--primary) / 0.32); /* glow halo on the primary CTA */ --card-shadow: 0 1px 2px rgb(12 26 49 / 0.06), 0 10px 28px -14px rgb(12 26 49 / 0.2); /* lift every Card */ --focus-ring-color: var(--ring); /* hue of every focus ring */ --focus-ring-width: 2px; /* thickness of every focus ring — 0 turns it OFF */ --focus-ring-opacity: 1; /* alpha of every focus ring */ --focus-ring-offset: 0px; /* gap, outline-form rings only (star, dot, anchor) */ --gradient-hero: linear-gradient( 180deg, hsl(var(--accent)), transparent ); /* PageContainer header banner */ --gradient-glow: radial-gradient( 60% 70% at 90% -8%, hsl(var(--primary) / 0.1), transparent 70% ); /* AppShell ambient wash */ --overlay-background: rgb(12 26 49 / 0.55); /* modal scrim (Dialog / Sheet / Drawer) */ } ```
92
+ /* ── Brand depth — all opt-in, all quiet by default ─────────────── */ --shadow-glow: 0 8px 20px hsl(var(--primary) / 0.32); /* glow halo on the primary CTA */ --card-shadow: 0 1px 2px rgb(12 26 49 / 0.06), 0 10px 28px -14px rgb(12 26 49 / 0.2); /* lift every Card */ --focus-ring-color: var(--ring); /* hue of every focus ring */ --focus-ring-weight: var(--stroke-md); /* thickness of every focus ring — 2px; nothing paints until <html data-focus-outline="on"> */ --focus-ring-opacity: 1; /* alpha of every focus ring */ --focus-ring-offset: 0px; /* gap, outline-form rings only (star, dot, anchor) */ --gradient-hero: linear-gradient( 180deg, hsl(var(--accent)), transparent ); /* PageContainer header banner */ --gradient-glow: radial-gradient( 60% 70% at 90% -8%, hsl(var(--primary) / 0.1), transparent 70% ); /* AppShell ambient wash */ --overlay-background: rgb(12 26 49 / 0.55); /* modal scrim (Dialog / Sheet / Drawer) */ } ```
93
93
 
94
94
  ### The anchor tokens (single knobs that propagate)
95
95
 
@@ -101,23 +101,33 @@ Import the styles, then set anchor tokens in your app's `theme.css` (loaded afte
101
101
  | `--shadow-color` | `0 0 0` | every shadow step `--shadow-{xs…2xl}` (`rgb(color / α)`) |
102
102
  | `--shadow-glow` | invisible | brand glow halo on the primary CTA (set the full shadow value) |
103
103
  | `--card-shadow` | `none` | resting elevation of every Card |
104
- | `--focus-ring-color` / `--focus-ring-width` | `var(--ring)` / `2px` | hue & thickness of every keyboard-focus ring (`width: 0` turns every ring OFF) |
104
+ | `--focus-ring-color` / `--focus-ring-weight` | `var(--ring)` / `1px` | hue & thickness of every keyboard-focus ring but nothing paints until the switch below is on |
105
105
  | `--focus-ring-opacity` / `--focus-ring-offset` | `1` / `0px` | alpha of every ring · gap for outline-form rings (star, carousel dot, heading anchor) |
106
106
  | `--gradient-{hero,glow,brand}` | `none` | hero header banner / AppShell ambient wash / spare (set the full gradient) |
107
107
  | `--overlay-background` | `rgb(0 0 0 / .5)` | the scrim of every overlay (Dialog / AlertDialog / Sheet / Drawer) |
108
108
 
109
- ### Focus ring — one definition, three levels of override
109
+ ### Focus ring — THE SWITCH FIRST, then three levels of override
110
110
 
111
- Every keyboard-focus ring in the system is drawn by a single rule (`src/styles/focus-ring.css`) reading the four tokens above. Nothing else paints one; a test fails the build if a stylesheet tries.
111
+ **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:
112
112
 
113
- That means retuning is a one-liner and it reaches everything:
113
+ ```html
114
+ <html data-focus-outline="on"></html>
115
+ ```
116
+
117
+ (`AppProvider` has the equivalent.) Every `:focus-visible` selector is present either way; only the paint is switched.
118
+
119
+ 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.
120
+
121
+ **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.
114
122
 
115
123
  ```css
116
124
  :root {
117
125
  --focus-ring-color: 24 99% 46%; /* every ring, brand orange */
118
- --focus-ring-width: 3px; /* every ring, thicker */
126
+ --focus-ring-weight: var(--stroke-md); /* every ring, 2px */
119
127
  }
120
- ````
128
+ ```
129
+
130
+ **A slim ring that still satisfies the criteria.** Switched on, the shipped weight is one hairline (1px) in the focus hue, measured in Chromium at **5.05:1** light and **7.07:1** dark on every surface a control sits on — so SC 1.4.11 (3:1, non-text contrast) is met on colour, independent of thickness. SC 2.4.13 Focus Appearance (AAA) additionally wants a perimeter of at least 2 CSS px: set `--focus-ring-weight: var(--stroke-md)` (measured: a 2px ring, same hue, same ratio). `var(--stroke-lg)` is the heavy 3px mark.
121
131
 
122
132
  **Per component.** A component that genuinely needs a different ring publishes its own knob and the rule picks it up locally — Toggle and TimeInput ship a heavier, softer ring because they are filled surfaces where a hard 2px reads as a second border:
123
133
 
@@ -136,7 +146,7 @@ That means retuning is a one-liner and it reaches everything:
136
146
  <div style={{ "--focus-ring-color": "0 84% 60%" } as React.CSSProperties}>…</div>
137
147
  ```
138
148
 
139
- **Turning it off** — `--focus-ring-width: 0`, at any of the three levels. It ships **on** (WCAG 2.4.7 / 2.4.11); switching it off is a deliberate act on your side, never a default of this package. Removing the visible focus indicator fails WCAG 2.4.7do it only where another indicator takes over.
149
+ **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.
140
150
 
141
151
  **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.
142
152
 
package/docs/DATETIME.md CHANGED
@@ -23,10 +23,18 @@ Omit `locale`, `timezone`, `timeFormat`, `dateFormat` → synced from AppProvide
23
23
 
24
24
  | value | Pattern | Default locale |
25
25
  | ----- | ------------ | -------------- |
26
- | `iso` | `yyyy-MM-dd` | ja |
26
+ | `iso` | `yyyy-MM-dd` | |
27
+ | `ymd` | `yyyy/MM/dd` | ja |
27
28
  | `dmy` | `dd/MM/yyyy` | vi |
28
29
  | `mdy` | `MM/dd/yyyy` | en |
29
30
 
31
+ `ymd` and `iso` are both year-first and are NOT interchangeable: a Japanese business document
32
+ (請求書, 申請書) is written `2026/05/01`, never `2026-05-01`. `ja` defaulted to `iso` until a
33
+ Japanese-first consumer shipped its own date formatter as an explicit stopgap purely to get the
34
+ slashes — rule 1 above broken by this package's own default. Only the DEFAULT moved: a stored
35
+ preference still wins, `defaultDateFormat` still pins any value, and `iso` is one option away in
36
+ `<DateFormatPicker />`.
37
+
30
38
  Sent to backend as `x-date-format`. Pick with `<DateFormatPicker />`.
31
39
 
32
40
  ## kind presets
@@ -43,9 +51,10 @@ Sent to backend as `x-date-format`. Pick with `<DateFormatPicker />`.
43
51
 
44
52
  ## ISO 8601 patterns
45
53
 
46
- Date order follows `dateFormat` from AppProvider (`iso` | `dmy` | `mdy`):
54
+ Date order follows `dateFormat` from AppProvider (`iso` | `ymd` | `dmy` | `mdy`):
47
55
 
48
56
  - `iso`: `yyyy-MM-dd` + `yyyy-MM-dd HH:mm`
57
+ - `ymd`: `yyyy/MM/dd` + `yyyy/MM/dd HH:mm`
49
58
  - `dmy`: `dd/MM/yyyy` + `dd/MM/yyyy HH:mm`
50
59
  - `mdy`: `MM/dd/yyyy` + `MM/dd/yyyy HH:mm`
51
60
 
package/docs/SPACING.md CHANGED
@@ -42,11 +42,11 @@ Implementation: `src/tokens/base.css` (values) · layout owners: `src/styles/*-l
42
42
  | `--card-space-body-y` | `--space-section-active` | Header↔body gap |
43
43
  | `--card-space-header-y` / `--card-space-footer-y` | `--space-stack-sm` | Banded header + separated footer band |
44
44
 
45
- | Component | Use |
46
- | ------------------------- | ------------------------------------------- |
47
- | `StatCard` | KPI / stat tile (`solo` path) |
48
- | `CardContent solo` | Body-only card (same padding as `StatCard`) |
49
- | `CardContent flush tight` | Edge-to-edge table/tabs in card |
45
+ | Component | Use |
46
+ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
47
+ | `StatCard` | KPI / stat tile (`solo` path) |
48
+ | `CardContent solo` | Body-only card (same padding as `StatCard`) |
49
+ | `CardContent flush` | Edge-to-edge table/tabs in card — REQUIRED when the table IS the body (rule 10). `flush` alone drops the border, radius and inline padding; add `tight` only to change the HEADER band. |
50
50
 
51
51
  See preview **Data Display → Card** for live examples.
52
52
 
@@ -64,6 +64,36 @@ Pick by AXIS, and note that the three do **not** share one value: each reads a d
64
64
 
65
65
  Because `ui-card-inset-x` reads `--card-space-inset`, it follows the card it sits in: a `Card density="tight"` or `"cozy"` moves those cells with the shell. The other two read fixed steps (band and body) and do not.
66
66
 
67
+ ## Control interiors are NOT on this scale
68
+
69
+ Everything above is the space **between** things — page sections, siblings in a stack, a card's
70
+ shell against its content. A control's **inside** is a different measurement and it is derived from
71
+ the control band (`--control-height`, `--control-padding-x`), never from `--space-*`. The two do not
72
+ meet, and reading a step off the table above as a minimum for a control's interior gives the wrong
73
+ answer.
74
+
75
+ The case that keeps coming up is `Segmented`'s track (gh#503). Its padding is **2px**, and the
76
+ label inside it therefore sits ~5px from the track's outer edge — under `--space-2` (8px), which is
77
+ NOT a floor this document sets for anything.
78
+
79
+ 2px is the whole geometry of the control:
80
+
81
+ ```
82
+ label height = --control-height − track padding × 2 → 32 − 4 = 28
83
+ ```
84
+
85
+ so the track measures **exactly** `--control-height` and a `Segmented` sits level with the `Input`
86
+ and the `Button` beside it on the same row. Raise the padding to 8px and one of two things has to
87
+ give: either the label band drops to 16px — a 14px type size in a 16px box, under every hit target
88
+ this library holds — or the track grows to 44px (28 + 8 × 2) and stops lining up with every other
89
+ control. The same derivation is why a `Button`'s own label sits ~5px inside its border. A control
90
+ is sized by its band; the band is what a dense enterprise UI is for.
91
+
92
+ **The knob, if a service wants a roomier control.** `--segmented-track-padding` is a published
93
+ component token (`src/tokens/components/segmented.css`); raising it re-derives the item height from
94
+ the same formula, so the track stays exactly one control tall. Do not reach for a Tailwind `p-*` on
95
+ the control — the audit rejects it, and it would break the identity above.
96
+
67
97
  ## MCP
68
98
 
69
99
  `get_pattern page-sections` (a page of Cards, spaced by the page) · `get_rule 40` (mobile-first spacing) · `list_audit_rules` (the spacing rules the CLI enforces).
package/docs/TOKENS.md CHANGED
@@ -220,7 +220,7 @@ The census confirmed the number and rejected the diagnosis: **`width` is not one
220
220
 
221
221
  **Px, never rem.** A stroke is a _device_ line: its job is to read as one crisp rule at any type size, so it must not grow when the root font-size does. Every token that moved onto this scale was already `px` for that reason; a `rem` thickness (`--slider-track-height: 0.375rem`) is a different decision and stayed where it was. **Not `--scaling`-multiplied** either — `--scaling` is a density knob, and a 1px divider at 0.92px would blur rather than tighten.
222
222
 
223
- `--focus-ring-width` is now a member (`var(--stroke-md)`) rather than a parallel authority, so a high-contrast theme that wants every emphasis stroke at 3px sets one token.
223
+ The focus mark reads this scale too, through `--focus-ring-weight` `var(--focus-outline-weight)`, which is `var(--stroke-hairline)` (the light 1px mark the ON state ships). `--focus-ring-width` is DERIVED from it (`weight × --focus-outline`, the on/off switch) and must never be set directly; a theme that wants a 2px focus perimeter (WCAG 2.2 SC 2.4.13) sets `--focus-ring-weight: var(--stroke-md)`. See docs/CUSTOMER-THEMING.md.
224
224
 
225
225
  #### `--band-height-*` — the vertical extent of a horizontal band
226
226
 
@@ -290,13 +290,28 @@ export default function Demo() {
290
290
  </AppProvider>
291
291
  </Flex>
292
292
 
293
- <Flex direction="col" gap="sm">
293
+ <Flex direction="col" gap="sm" id="empty-builtin">
294
294
  <Text as="div" weight="medium">
295
295
  空の状態(data が空のとき自動表示)
296
296
  </Text>
297
297
  <DataTable data={[]} columns={columns} getRowId={(row) => row.id} />
298
298
  </Flex>
299
299
 
300
+ {/* The SAME cell with consumer content in it. `empty` takes a ReactNode, so a plain string
301
+ is a legal call — and the lifecycle cell used to be `padding: 0`, which put that string
302
+ flush against the table's edge. Measured here; see check:data-table-empty-inset. */}
303
+ <Flex direction="col" gap="sm" id="empty-plain-string">
304
+ <Text as="div" weight="medium">
305
+ 空の状態(empty に文字列を渡す)
306
+ </Text>
307
+ <DataTable
308
+ data={[]}
309
+ columns={columns}
310
+ getRowId={(row) => row.id}
311
+ empty="まだ登録がありません"
312
+ />
313
+ </Flex>
314
+
300
315
  {/* — the two failure states, in the same table grid as empty/loading. */}
301
316
  <Flex direction="col" gap="sm">
302
317
  <Text as="div" weight="medium">
@@ -318,7 +333,7 @@ export default function Demo() {
318
333
  <DataTable data={[]} columns={columns} getRowId={(row) => row.id} error />
319
334
  </Flex>
320
335
 
321
- <Flex direction="col" gap="sm">
336
+ <Flex direction="col" gap="sm" id="error-custom-node">
322
337
  <Text as="div" weight="medium">
323
338
  エラー(error にノードを渡して文言を差し替え)
324
339
  </Text>
@@ -181,7 +181,7 @@ export default function Demo() {
181
181
  full frame. The suppression above is scoped to a flush body, never to `bordered`.
182
182
  </CardDescription>
183
183
  </CardHeader>
184
- <CardContent>
184
+ <CardContent flush>
185
185
  <Table bordered>
186
186
  <TableHeader>
187
187
  <TableRow>
@@ -0,0 +1,77 @@
1
+ import {
2
+ Badge,
3
+ Card,
4
+ CardContent,
5
+ CardDescription,
6
+ CardHeader,
7
+ CardTitle,
8
+ } from "@godxjp/ui/data-display";
9
+ import { Segmented } from "@godxjp/ui/data-entry";
10
+ import { Button, Text } from "@godxjp/ui/general";
11
+ import { Flex, PageContainer } from "@godxjp/ui/layout";
12
+
13
+ /**
14
+ * The composition a consumer actually filters a list with: a Segmented that carries a count badge
15
+ * per option, sharing a WRAPPING flex row with the other filter controls, inside a Card.
16
+ *
17
+ * It is a separate frame from `segmented.tsx` because the row is the variable. `#four-with-counts`
18
+ * over there puts the track directly in a CardContent, where it is a block box and shrinks to the
19
+ * column; here it is a FLEX ITEM, and a flex item's `min-inline-size` defaults to `auto` — its
20
+ * max-content width — so nothing makes it give ground to the row.
21
+ */
22
+ const OPTIONS = [
23
+ { value: "all", label: "すべて", count: 133 },
24
+ { value: "ginou", label: "技能実習", count: 82 },
25
+ { value: "tokutei", label: "特定技能", count: 51 },
26
+ { value: "ikusei", label: "育成就労", count: 0 },
27
+ ].map(({ value, label, count }) => ({
28
+ value,
29
+ label: (
30
+ <>
31
+ {label}
32
+ <Badge as="span" variant="secondary">
33
+ {count}
34
+ </Badge>
35
+ </>
36
+ ),
37
+ }));
38
+
39
+ export default function SegmentedInFilterRow() {
40
+ return (
41
+ <PageContainer title="Segmented in a filter row" subtitle="4 択 + 件数バッジ · Flex wrap の中">
42
+ <Card id="four-in-wrap-row">
43
+ <CardHeader>
44
+ <CardTitle level={2}>制度の絞り込み</CardTitle>
45
+ <CardDescription>
46
+ `check:segmented-wrap` がこのカードも測ります。トラックは行の中でも縮み、393px
47
+ でラベルも件数も切り詰めません。
48
+ </CardDescription>
49
+ </CardHeader>
50
+ <CardContent>
51
+ <Flex gap="sm" align="center" wrap>
52
+ <Segmented aria-label="制度" defaultValue="all" options={OPTIONS} />
53
+ <Button variant="outline" size="sm">
54
+ 絞り込みを解除
55
+ </Button>
56
+ </Flex>
57
+ </CardContent>
58
+ </Card>
59
+
60
+ <Card id="four-in-wrap-row-alone">
61
+ <CardHeader>
62
+ <CardTitle level={2}>同じトラック、行の中に一つだけ</CardTitle>
63
+ <CardDescription>
64
+ <Text size="sm" tone="muted">
65
+ 兄弟が無くても行は行。フレックス項目としての縮み方を切り分けるための対照。
66
+ </Text>
67
+ </CardDescription>
68
+ </CardHeader>
69
+ <CardContent>
70
+ <Flex gap="sm" align="center" wrap>
71
+ <Segmented aria-label="制度(単独)" defaultValue="all" options={OPTIONS} />
72
+ </Flex>
73
+ </CardContent>
74
+ </Card>
75
+ </PageContainer>
76
+ );
77
+ }