@godxjp/ui 28.6.0 → 28.7.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.
@@ -605,6 +605,20 @@ export type ScrollAreaProp = {
605
605
  * from it — anchoring must never be the only route back to new content.
606
606
  */
607
607
  onAnchoredChange?: (anchored: boolean) => void;
608
+ /**
609
+ * Accessible name for the scroll REGION — the `tabindex="0"` element a keyboard user lands on to
610
+ * scroll content taller or wider than the box.
611
+ *
612
+ * Optional on purpose. A consumer is never forced to invent a name for every scrolling box: left
613
+ * out, the region takes the localized `dataDisplay.scrollArea.region` default ("Scrollable
614
+ * region"), which is what a screen-reader user needs to hear anyway — that the arrow keys now
615
+ * scroll something. Pass a plain string when the screen can say WHICH region ("Activity log"); a
616
+ * non-string node cannot be an `aria-label`, so it falls back to the default.
617
+ *
618
+ * The region is only announced while it HAS overflow to reach, on the axes `orientation` opens:
619
+ * no overflow, no tab stop, no role and no name. (gh#821)
620
+ */
621
+ label?: LabelProp;
608
622
  };
609
623
  /**
610
624
  * @see TimelineGrid — one COLUMN of the grid (a day, a room, a machine). The label names the
@@ -1851,7 +1851,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
1851
1851
  readonly field: "onAnchoredChange";
1852
1852
  readonly local: true;
1853
1853
  readonly reason: 'Pinned-state change handler (boolean), so the consumer can render its own focusable "jump to newest" affordance.';
1854
- }];
1854
+ }, "LabelProp"];
1855
1855
  };
1856
1856
  readonly ChartSeriesProp: {
1857
1857
  readonly group: "data-display";
@@ -2261,7 +2261,8 @@ const COMPONENT_PROP_REGISTRY = {
2261
2261
  field: "onAnchoredChange",
2262
2262
  local: true,
2263
2263
  reason: 'Pinned-state change handler (boolean), so the consumer can render its own focusable "jump to newest" affordance.'
2264
- }
2264
+ },
2265
+ "LabelProp"
2265
2266
  ]
2266
2267
  },
2267
2268
  ChartSeriesProp: {
@@ -259,4 +259,26 @@
259
259
  text-align: end;
260
260
  }
261
261
  }
262
+
263
+ .ui-form-feedback > svg {
264
+ inline-size: var(--form-feedback-icon-size);
265
+ block-size: var(--form-feedback-icon-size);
266
+ flex-shrink: 0;
267
+ }
268
+
269
+ .ui-form-feedback[data-status="success"] {
270
+ color: hsl(var(--text-success));
271
+ }
272
+
273
+ .ui-form-feedback[data-status="warning"] {
274
+ color: hsl(var(--text-warning));
275
+ }
276
+
277
+ .ui-form-feedback[data-status="error"] {
278
+ color: hsl(var(--text-error));
279
+ }
280
+
281
+ .ui-form-feedback[data-status="validating"] {
282
+ color: hsl(var(--muted-foreground));
283
+ }
262
284
  }
@@ -1053,7 +1053,6 @@
1053
1053
  }
1054
1054
 
1055
1055
  .ui-pagination-size-trigger {
1056
- width: var(--pagination-size-width);
1057
1056
 
1058
1057
  height: var(--control-height);
1059
1058
  }
@@ -220,27 +220,27 @@
220
220
  }
221
221
 
222
222
  [data-slot="tabs"][data-variant="card"][data-placement="start"] [data-slot="tabs-list"],
223
- [data-slot="tabs"][data-variant="editable-card"][data-placement="start"]
224
- [data-slot="tabs-list"] {
223
+ [data-slot="tabs"][data-variant="editable-card"][data-placement="start"] [data-slot="tabs-list"] {
225
224
  box-shadow: inset calc(-1 * var(--tabs-card-rail-border-width)) 0 0 hsl(var(--border));
226
225
  }
227
226
 
228
227
  [data-slot="tabs"][data-variant="card"][data-placement="end"] [data-slot="tabs-list"],
229
- [data-slot="tabs"][data-variant="editable-card"][data-placement="end"]
230
- [data-slot="tabs-list"] {
228
+ [data-slot="tabs"][data-variant="editable-card"][data-placement="end"] [data-slot="tabs-list"] {
231
229
  box-shadow: inset var(--tabs-card-rail-border-width) 0 0 hsl(var(--border));
232
230
  }
233
231
 
234
- [dir="rtl"] [data-slot="tabs"][data-variant="card"][data-placement="start"]
232
+ [dir="rtl"]
233
+ [data-slot="tabs"][data-variant="card"][data-placement="start"]
235
234
  [data-slot="tabs-list"],
236
- [dir="rtl"] [data-slot="tabs"][data-variant="editable-card"][data-placement="start"]
235
+ [dir="rtl"]
236
+ [data-slot="tabs"][data-variant="editable-card"][data-placement="start"]
237
237
  [data-slot="tabs-list"] {
238
238
  box-shadow: inset var(--tabs-card-rail-border-width) 0 0 hsl(var(--border));
239
239
  }
240
240
 
241
- [dir="rtl"] [data-slot="tabs"][data-variant="card"][data-placement="end"]
242
- [data-slot="tabs-list"],
243
- [dir="rtl"] [data-slot="tabs"][data-variant="editable-card"][data-placement="end"]
241
+ [dir="rtl"] [data-slot="tabs"][data-variant="card"][data-placement="end"] [data-slot="tabs-list"],
242
+ [dir="rtl"]
243
+ [data-slot="tabs"][data-variant="editable-card"][data-placement="end"]
244
244
  [data-slot="tabs-list"] {
245
245
  box-shadow: inset calc(-1 * var(--tabs-card-rail-border-width)) 0 0 hsl(var(--border));
246
246
  }
@@ -277,7 +277,7 @@
277
277
  margin-inline-start: calc(var(--tabs-panel-space-inset) + var(--tabs-panel-border-width));
278
278
  }
279
279
 
280
- [data-slot="tabs"][data-bodied="true"] > [data-slot="tabs-panel"],
280
+ [data-slot="tabs"][data-bodied="true"] > [data-slot="tabs-panel"],
281
281
  [data-slot="tabs"][data-bodied="true"] > [data-slot="tabs-content"] {
282
282
  border: var(--tabs-panel-border-width) solid hsl(var(--border));
283
283
  border-radius: var(--tabs-panel-radius);
@@ -1002,7 +1002,6 @@
1002
1002
  }
1003
1003
 
1004
1004
  .ui-app-setting-picker-icon {
1005
- inline-size: var(--control-height);
1006
1005
  justify-content: center;
1007
1006
  border-color: transparent;
1008
1007
  background: transparent;
@@ -1026,35 +1025,35 @@
1026
1025
 
1027
1026
  @media (min-width: 40rem) {
1028
1027
  .ui-app-setting-picker-trigger[data-kind="locale"] {
1029
- inline-size: var(--app-setting-picker-locale-width);
1028
+ --app-setting-picker-trigger-width: var(--app-setting-picker-locale-width);
1030
1029
  }
1031
1030
 
1032
1031
  .ui-app-setting-picker-trigger[data-kind="timezone"] {
1033
- inline-size: var(--app-setting-picker-timezone-width);
1032
+ --app-setting-picker-trigger-width: var(--app-setting-picker-timezone-width);
1034
1033
  }
1035
1034
 
1036
1035
  .ui-app-setting-picker-trigger[data-kind="dateFormat"] {
1037
- inline-size: var(--app-setting-picker-date-format-width);
1036
+ --app-setting-picker-trigger-width: var(--app-setting-picker-date-format-width);
1038
1037
  }
1039
1038
 
1040
1039
  .ui-app-setting-picker-trigger[data-kind="timeFormat"] {
1041
- inline-size: var(--app-setting-picker-time-format-width);
1040
+ --app-setting-picker-trigger-width: var(--app-setting-picker-time-format-width);
1042
1041
  }
1043
1042
 
1044
1043
  .ui-app-setting-picker-trigger[data-kind="theme"] {
1045
- inline-size: var(--app-setting-picker-theme-width);
1044
+ --app-setting-picker-trigger-width: var(--app-setting-picker-theme-width);
1046
1045
  }
1047
1046
 
1048
1047
  .ui-app-setting-picker-trigger[data-kind="brand"] {
1049
- inline-size: var(--app-setting-picker-brand-width);
1048
+ --app-setting-picker-trigger-width: var(--app-setting-picker-brand-width);
1050
1049
  }
1051
1050
 
1052
1051
  .ui-app-setting-picker-trigger[data-kind="density"] {
1053
- inline-size: var(--app-setting-picker-density-width);
1052
+ --app-setting-picker-trigger-width: var(--app-setting-picker-density-width);
1054
1053
  }
1055
1054
 
1056
1055
  .ui-app-setting-picker-trigger[data-kind="fontSize"] {
1057
- inline-size: var(--app-setting-picker-font-size-width);
1056
+ --app-setting-picker-trigger-width: var(--app-setting-picker-font-size-width);
1058
1057
  }
1059
1058
  }
1060
1059
  }
@@ -10,6 +10,8 @@
10
10
 
11
11
  --form-block-gap: var(--space-4);
12
12
 
13
+ --form-feedback-icon-size: var(--icon-size-sm);
14
+
13
15
  --form-field-row-gap: var(--space-3);
14
16
 
15
17
  --form-grid-row-gap: var(--form-field-row-gap);
@@ -187,6 +187,8 @@
187
187
  --app-setting-picker-density-width: 10rem;
188
188
  --app-setting-picker-font-size-width: 9rem;
189
189
 
190
+ --app-setting-picker-trigger-width: auto;
191
+
190
192
  --app-setting-picker-width-breakpoint: 40rem;
191
193
  --app-setting-picker-bar-border-width: 0;
192
194
  --app-setting-picker-bar-shadow: none;
@@ -77,6 +77,7 @@ new UI need
77
77
  | `Select` (incl. search/async) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | **Framework component** |
78
78
  | `DataTable`, `Dialog`, `Calendar`, `Switch`, `Combobox` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | **Framework component** |
79
79
  | `StatCard` (+ `icon`) | ✅ | ➖ | ✅ | ✅ | ✅ | ✅ | ✅ | **Framework component** (a reusable KPI tile with a stable API + tokens) |
80
+ | `ServiceLauncherCard` | ➖ | ❌ | ❌ | ✅ | ✅ | ➖ | ➖ | **Composition by the test — RETAINED as a recorded exception** (gh#814) |
80
81
  | Marketing **Hero** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **Composition** — section, static, composable, brand-specific |
81
82
  | **Navbar** / **Footer** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **Composition** — layout of `Text`/`Button`/`Flex` |
82
83
  | **PricingTable** / feature grid | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **Composition** — `ResponsiveGrid` + `Card` |
@@ -85,6 +86,8 @@ new UI need
85
86
 
86
87
  `✅ pass · ❌ fail · ➖ borderline`. **StatCard** is the instructive borderline: C2 is weak (it owns little behavior), but it is a universal KPI tile with a controlled API, fully tokenized, broadly reused — so it earns its place. A **Hero** fails six of seven; it is unambiguously a composition.
87
88
 
89
+ **ServiceLauncherCard is the ONE recorded exception, and it is recorded so that it stays one.** It shipped before the test was run against it, and when the test was run it came back with two hard FAILs: **C2** — no state, no keyboard handling, no focus management, its only ARIA three static attributes — and **C3** — its own imports are `Card` + `CardContent` + `Badge` + a Lucide glyph, so "could I build this right now from primitives?" is yes. By §2 that makes it a composition pattern. It is kept anyway because `src/components/layout/app-launcher.tsx` consumes it: it is an internal building block of a component that **does** pass, so the question was never "should it exist" but "should it be PUBLIC", and removing a public export is breaking. Keeping it public was the cheaper call and the ledger lives on gh#814 and at the top of `src/components/data-display/service-launcher-card.tsx`. **Consumers should compose `Card` + `Badge` for a service tile** — reach for `ServiceLauncherCard` only to match `AppLauncher`'s own tiles — and this row is not precedent for adding another static tile to `src/components/`.
90
+
88
91
  ---
89
92
 
90
93
  ## 4. How to build a composition pattern correctly
@@ -648,11 +648,31 @@ because removing it would break the contents anchor.
648
648
 
649
649
  **Every other focusable non-interactive element in this library is a genuinely scrollable region**
650
650
  — `.app-main`, `.ui-mobile-shell-main`, `.ui-timeline-grid`, `.ui-code-block` when it overflows,
651
- MasterDetail's bounded master, the DataTable and Table scroll containers, ScrollArea, and the
652
- BranchScopePicker list. Each carries `tabIndex={0}` for axe `scrollable-region-focusable` and must
653
- stay reachable, so each keeps BOTH halves rather than becoming focusable-but-unpainted. The region
654
- ring (`--region-focus-ring-width`, off by default) now also multiplies by `--focus-outline`, so a
655
- service that opts it in still cannot paint while the library-wide switch is off.
651
+ MasterDetail's bounded master, the DataTable and Table scroll containers when they overflow,
652
+ ScrollArea, and the BranchScopePicker list. Each carries `tabIndex={0}` for axe
653
+ `scrollable-region-focusable` and must stay reachable, so each keeps BOTH halves rather than
654
+ becoming focusable-but-unpainted. The region ring (`--region-focus-ring-width`, off by default) now
655
+ also multiplies by `--focus-outline`, so a service that opts it in still cannot paint while the
656
+ library-wide switch is off.
657
+
658
+ ### A focusable scroll region is ROLED and NAMED, and only exists while it scrolls (gh#817)
659
+
660
+ Reaching the overflow is half the contract; knowing what you reached is the other half. A stop with
661
+ no role and no accessible name announces nothing at all, which is why `godx-jp/id` found `Table`'s
662
+ wrapper sitting at tab stop 27 as a bare `<div tabindex="0">`. So the Table and DataTable scroll
663
+ containers carry `role="group"` plus a name that defaults to the localized `dataTable.scrollRegion`
664
+ — a consumer is never forced to invent one per table, and `label` overrides it per instance.
665
+
666
+ `group`, not `region`: a NAMED `region` is a landmark, so a page with three tables would ship three
667
+ identically-named landmarks and fail axe `landmark-unique`. That collision is the reason the role
668
+ was left off in the first place; `group` is announced, takes a name, and is not a landmark.
669
+
670
+ And all three attributes are withheld until the box provably overflows (`useScrollsHorizontally`,
671
+ `src/lib/hooks.ts`) — a focus stop that scrolls nothing is noise. The measurement may only ever
672
+ REMOVE the stop: an unlaid-out box reports `clientWidth === 0` (SSR, jsdom, a `display:none`
673
+ ancestor), and reading that as "nothing overflows" would strand the overflow from every keyboard
674
+ user, a worse failure than the extra stop. Gated by
675
+ `src/components/data-display/__tests__/table-scroll-region-a11y-817.test.tsx`.
656
676
 
657
677
  ## How a decision gets made from here
658
678
 
@@ -355,6 +355,37 @@ export default function Demo() {
355
355
  </CardContent>
356
356
  </Card>
357
357
 
358
+ <Card>
359
+ <CardHeader>
360
+ <CardTitle level={2}>スクロール領域に名前を付ける(label)</CardTitle>
361
+ <CardDescription>
362
+ 溢れている間、ビューポートは Tab
363
+ で止まる(キーボードだけで中身を送るために必要)。その停止点は名前を持つ。既定は
364
+ 「スクロール可能な領域」の翻訳で、画面がその領域を呼べるときは label
365
+ で「監査ログ」と名乗る。ロールはランドマークの region ではなく group
366
+ にしてある。同じ名前のランドマークが一画面に並ぶと axe landmark-unique
367
+ に触れるため。溢れていなければ停止点も role も名前も出ない(gh#821)。
368
+ </CardDescription>
369
+ </CardHeader>
370
+ <CardContent>
371
+ <Card variant="outline" className="w-full">
372
+ <CardContent flush>
373
+ <ScrollArea className="h-40" label="監査ログ">
374
+ <CardContent>
375
+ <Flex direction="col" gap="xs">
376
+ {entries.map((e) => (
377
+ <div key={e} className="text-sm tabular-nums">
378
+ {e}
379
+ </div>
380
+ ))}
381
+ </Flex>
382
+ </CardContent>
383
+ </ScrollArea>
384
+ </CardContent>
385
+ </Card>
386
+ </CardContent>
387
+ </Card>
388
+
358
389
  <Card>
359
390
  <CardHeader>
360
391
  <CardTitle level={2}>内容が収まる場合(バー非表示)</CardTitle>