@godxjp/ui 17.0.1 → 18.0.1

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 (74) hide show
  1. package/dist/components/data-display/card.d.ts +37 -1
  2. package/dist/components/data-display/card.js +7 -1
  3. package/dist/components/data-display/data-table.js +19 -4
  4. package/dist/components/data-display/empty-state.d.ts +1 -1
  5. package/dist/components/data-display/empty-state.js +5 -1
  6. package/dist/components/data-display/scroll-area.js +1 -1
  7. package/dist/components/data-display/table.d.ts +20 -1
  8. package/dist/components/data-display/table.js +23 -9
  9. package/dist/components/data-entry/calendar.js +4 -0
  10. package/dist/components/data-entry/cascader.d.ts +1 -1
  11. package/dist/components/data-entry/cascader.js +10 -1
  12. package/dist/components/data-entry/checkbox-group.d.ts +1 -1
  13. package/dist/components/data-entry/checkbox-group.js +13 -4
  14. package/dist/components/data-entry/color-picker.d.ts +1 -1
  15. package/dist/components/data-entry/color-picker.js +5 -2
  16. package/dist/components/data-entry/command.d.ts +2 -2
  17. package/dist/components/data-entry/date-picker.d.ts +1 -1
  18. package/dist/components/data-entry/date-picker.js +11 -1
  19. package/dist/components/data-entry/date-range-picker.d.ts +1 -1
  20. package/dist/components/data-entry/date-range-picker.js +11 -3
  21. package/dist/components/data-entry/form-field.js +7 -3
  22. package/dist/components/data-entry/month-picker.d.ts +1 -1
  23. package/dist/components/data-entry/month-picker.js +13 -1
  24. package/dist/components/data-entry/month-range-picker.d.ts +1 -1
  25. package/dist/components/data-entry/month-range-picker.js +11 -3
  26. package/dist/components/data-entry/number-input.d.ts +19 -2
  27. package/dist/components/data-entry/number-input.js +5 -7
  28. package/dist/components/data-entry/radio.d.ts +1 -1
  29. package/dist/components/data-entry/radio.js +11 -3
  30. package/dist/components/data-entry/search-input.d.ts +3 -2
  31. package/dist/components/data-entry/search-input.js +5 -2
  32. package/dist/components/data-entry/search-select.d.ts +1 -1
  33. package/dist/components/data-entry/search-select.js +51 -10
  34. package/dist/components/data-entry/select.d.ts +7 -0
  35. package/dist/components/data-entry/select.js +21 -3
  36. package/dist/components/data-entry/time-picker.d.ts +1 -1
  37. package/dist/components/data-entry/time-picker.js +11 -1
  38. package/dist/components/data-entry/transfer.d.ts +1 -1
  39. package/dist/components/data-entry/transfer.js +73 -59
  40. package/dist/components/data-entry/tree-select.d.ts +1 -1
  41. package/dist/components/data-entry/tree-select.js +10 -1
  42. package/dist/components/data-entry/upload.d.ts +1 -1
  43. package/dist/components/data-entry/upload.js +7 -2
  44. package/dist/components/layout/app-shell.d.ts +1 -1
  45. package/dist/components/layout/app-shell.js +42 -2
  46. package/dist/components/layout/breadcrumb.d.ts +7 -1
  47. package/dist/components/layout/breadcrumb.js +6 -2
  48. package/dist/components/layout/page-container.d.ts +1 -1
  49. package/dist/components/layout/page-container.js +32 -22
  50. package/dist/components/layout/responsive-grid.js +1 -1
  51. package/dist/components/layout/sidebar.d.ts +1 -1
  52. package/dist/components/layout/sidebar.js +44 -11
  53. package/dist/components/layout/split-pane.js +2 -2
  54. package/dist/components/navigation/app-setting-picker.js +15 -7
  55. package/dist/components/navigation/pagination.d.ts +1 -1
  56. package/dist/components/navigation/pagination.js +84 -95
  57. package/dist/components/navigation/tabs.js +21 -4
  58. package/dist/components/ui/tag-input.js +1 -0
  59. package/dist/i18n/messages/en.json +3 -1
  60. package/dist/i18n/messages/ja.json +3 -1
  61. package/dist/i18n/messages/vi.json +3 -1
  62. package/dist/lib/field-a11y.d.ts +66 -0
  63. package/dist/lib/field-a11y.js +44 -0
  64. package/dist/props/components/app.prop.d.ts +9 -5
  65. package/dist/props/components/data-display.prop.d.ts +16 -1
  66. package/dist/props/components/data-entry.prop.d.ts +69 -19
  67. package/dist/props/components/layout.prop.d.ts +42 -0
  68. package/dist/props/components/navigation.prop.d.ts +16 -0
  69. package/dist/props/registry.d.ts +8 -3
  70. package/dist/props/registry.js +21 -3
  71. package/dist/props/vocabulary/data.prop.d.ts +9 -0
  72. package/dist/styles/layout.css +46 -9
  73. package/dist/styles/shell-layout.css +19 -0
  74. package/package.json +8 -4
@@ -88,12 +88,15 @@ function Pagination({
88
88
  pageSizeOptions = [10, 20, 50, 100],
89
89
  showSizeChanger,
90
90
  showTotal,
91
+ hideOnSinglePage = true,
91
92
  simple,
92
93
  disabled,
93
94
  className,
94
- onValueChange
95
+ onValueChange,
96
+ "aria-label": ariaLabel
95
97
  }) {
96
98
  const { t } = useTranslation();
99
+ const navLabel = ariaLabel ?? t("navigation.pagination.ariaLabel");
97
100
  const totalPages = Math.max(1, Math.ceil(total / pageSize));
98
101
  const safeCurrent = Math.min(Math.max(1, value), totalPages);
99
102
  const pages = buildPageRange(safeCurrent, totalPages);
@@ -106,112 +109,98 @@ function Pagination({
106
109
  total === 0 ? 0 : (safeCurrent - 1) * pageSize + 1,
107
110
  Math.min(safeCurrent * pageSize, total)
108
111
  ]) : showTotal ? t("navigation.pagination.total", { total }) : null;
109
- if (total <= pageSize) return null;
112
+ if (total <= 0) return null;
113
+ if (hideOnSinglePage && totalPages <= 1) return null;
110
114
  if (simple) {
111
- return /* @__PURE__ */ jsxs(
112
- "nav",
115
+ return /* @__PURE__ */ jsxs("nav", { "aria-label": navLabel, "data-simple": "true", className: cn("ui-pagination", className), children: [
116
+ totalLabel && /* @__PURE__ */ jsx("span", { className: "ui-pagination-total", children: totalLabel }),
117
+ /* @__PURE__ */ jsx(
118
+ Button,
119
+ {
120
+ type: "button",
121
+ variant: "outline",
122
+ size: "sm",
123
+ disabled: Boolean(disabled) || safeCurrent <= 1,
124
+ "aria-label": t("navigation.pagination.prev"),
125
+ onClick: () => go(safeCurrent - 1),
126
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4", "aria-hidden": "true" })
127
+ }
128
+ ),
129
+ /* @__PURE__ */ jsxs("span", { className: "ui-pagination-count", children: [
130
+ safeCurrent,
131
+ " / ",
132
+ totalPages
133
+ ] }),
134
+ /* @__PURE__ */ jsx(
135
+ Button,
136
+ {
137
+ type: "button",
138
+ variant: "outline",
139
+ size: "sm",
140
+ disabled: Boolean(disabled) || safeCurrent >= totalPages,
141
+ "aria-label": t("navigation.pagination.next"),
142
+ onClick: () => go(safeCurrent + 1),
143
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4", "aria-hidden": "true" })
144
+ }
145
+ )
146
+ ] });
147
+ }
148
+ return /* @__PURE__ */ jsxs("nav", { "aria-label": navLabel, className: cn("ui-pagination", className), children: [
149
+ totalLabel && /* @__PURE__ */ jsx("span", { className: "ui-pagination-total", children: totalLabel }),
150
+ showSizeChanger && /* @__PURE__ */ jsxs(
151
+ Select,
113
152
  {
114
- "aria-label": t("navigation.pagination.ariaLabel"),
115
- "data-simple": "true",
116
- className: cn("ui-pagination", className),
153
+ value: String(pageSize),
154
+ onValueChange: (v) => go(1, Number(v)),
155
+ disabled,
117
156
  children: [
118
- totalLabel && /* @__PURE__ */ jsx("span", { className: "ui-pagination-total", children: totalLabel }),
119
157
  /* @__PURE__ */ jsx(
120
- Button,
158
+ SelectTrigger,
121
159
  {
122
- type: "button",
123
- variant: "outline",
124
- size: "sm",
125
- disabled: Boolean(disabled) || safeCurrent <= 1,
126
- "aria-label": t("navigation.pagination.prev"),
127
- onClick: () => go(safeCurrent - 1),
128
- children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4", "aria-hidden": "true" })
160
+ className: "ui-pagination-size-trigger w-[var(--pagination-size-width)]",
161
+ "aria-label": t("navigation.pagination.pageSize"),
162
+ children: /* @__PURE__ */ jsx(SelectValue, {})
129
163
  }
130
164
  ),
131
- /* @__PURE__ */ jsxs("span", { className: "ui-pagination-count", children: [
132
- safeCurrent,
133
- " / ",
134
- totalPages
135
- ] }),
136
- /* @__PURE__ */ jsx(
137
- Button,
138
- {
139
- type: "button",
140
- variant: "outline",
141
- size: "sm",
142
- disabled: Boolean(disabled) || safeCurrent >= totalPages,
143
- "aria-label": t("navigation.pagination.next"),
144
- onClick: () => go(safeCurrent + 1),
145
- children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4", "aria-hidden": "true" })
146
- }
147
- )
165
+ /* @__PURE__ */ jsx(SelectContent, { children: pageSizeOptions.map((size) => /* @__PURE__ */ jsx(SelectItem, { value: String(size), children: t("navigation.pagination.pageSizeOption", { size }) }, size)) })
148
166
  ]
149
167
  }
150
- );
151
- }
152
- return /* @__PURE__ */ jsxs(
153
- "nav",
154
- {
155
- "aria-label": t("navigation.pagination.ariaLabel"),
156
- className: cn("ui-pagination", className),
157
- children: [
158
- totalLabel && /* @__PURE__ */ jsx("span", { className: "ui-pagination-total", children: totalLabel }),
159
- showSizeChanger && /* @__PURE__ */ jsxs(
160
- Select,
168
+ ),
169
+ /* @__PURE__ */ jsxs(PaginationContent, { children: [
170
+ /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(
171
+ PaginationPrevious,
172
+ {
173
+ "aria-label": t("navigation.pagination.prev"),
174
+ onClick: () => go(safeCurrent - 1),
175
+ disabled: Boolean(disabled) || safeCurrent <= 1,
176
+ children: /* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": "true" })
177
+ }
178
+ ) }),
179
+ pages.map(
180
+ (page, index) => page === "ellipsis" ? /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(PaginationEllipsis, {}) }, `e-${index}`) : /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(
181
+ PaginationLink,
161
182
  {
162
- value: String(pageSize),
163
- onValueChange: (v) => go(1, Number(v)),
183
+ isActive: page === safeCurrent,
184
+ "aria-label": t("navigation.pagination.page", { page }),
185
+ onClick: () => {
186
+ if (!disabled) go(page);
187
+ },
164
188
  disabled,
165
- children: [
166
- /* @__PURE__ */ jsx(
167
- SelectTrigger,
168
- {
169
- className: "ui-pagination-size-trigger w-[var(--pagination-size-width)]",
170
- "aria-label": t("navigation.pagination.pageSize"),
171
- children: /* @__PURE__ */ jsx(SelectValue, {})
172
- }
173
- ),
174
- /* @__PURE__ */ jsx(SelectContent, { children: pageSizeOptions.map((size) => /* @__PURE__ */ jsx(SelectItem, { value: String(size), children: t("navigation.pagination.pageSizeOption", { size }) }, size)) })
175
- ]
189
+ children: page
176
190
  }
177
- ),
178
- /* @__PURE__ */ jsxs(PaginationContent, { children: [
179
- /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(
180
- PaginationPrevious,
181
- {
182
- "aria-label": t("navigation.pagination.prev"),
183
- onClick: () => go(safeCurrent - 1),
184
- disabled: Boolean(disabled) || safeCurrent <= 1,
185
- children: /* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": "true" })
186
- }
187
- ) }),
188
- pages.map(
189
- (page, index) => page === "ellipsis" ? /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(PaginationEllipsis, {}) }, `e-${index}`) : /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(
190
- PaginationLink,
191
- {
192
- isActive: page === safeCurrent,
193
- "aria-label": t("navigation.pagination.page", { page }),
194
- onClick: () => {
195
- if (!disabled) go(page);
196
- },
197
- disabled,
198
- children: page
199
- }
200
- ) }, page)
201
- ),
202
- /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(
203
- PaginationNext,
204
- {
205
- "aria-label": t("navigation.pagination.next"),
206
- onClick: () => go(safeCurrent + 1),
207
- disabled: Boolean(disabled) || safeCurrent >= totalPages,
208
- children: /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" })
209
- }
210
- ) })
211
- ] })
212
- ]
213
- }
214
- );
191
+ ) }, page)
192
+ ),
193
+ /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(
194
+ PaginationNext,
195
+ {
196
+ "aria-label": t("navigation.pagination.next"),
197
+ onClick: () => go(safeCurrent + 1),
198
+ disabled: Boolean(disabled) || safeCurrent >= totalPages,
199
+ children: /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" })
200
+ }
201
+ ) })
202
+ ] })
203
+ ] });
215
204
  }
216
205
  export {
217
206
  Pagination,
@@ -3,6 +3,14 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import * as TabsPrimitive from "@radix-ui/react-tabs";
5
5
  import { cn } from "../../lib/utils.js";
6
+ function resolveFallbackTabValue(items, requested) {
7
+ if (!items || items.length === 0) return requested;
8
+ if (requested !== void 0) {
9
+ const requestedItem = items.find((item) => item.value === requested);
10
+ if (requestedItem && !requestedItem.disabled) return requested;
11
+ }
12
+ return items.find((item) => !item.disabled)?.value;
13
+ }
6
14
  function Tabs({
7
15
  className,
8
16
  orientation = "horizontal",
@@ -14,8 +22,7 @@ function Tabs({
14
22
  contentClassName,
15
23
  ...props
16
24
  }) {
17
- const firstValue = items?.[0]?.value;
18
- const resolvedDefault = defaultValue ?? firstValue;
25
+ const resolvedDefault = resolveFallbackTabValue(items, defaultValue);
19
26
  return /* @__PURE__ */ jsx(
20
27
  TabsPrimitive.Root,
21
28
  {
@@ -24,7 +31,10 @@ function Tabs({
24
31
  orientation,
25
32
  value,
26
33
  defaultValue: value === void 0 ? resolvedDefault : void 0,
27
- className: cn("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className),
34
+ className: cn(
35
+ "group/tabs flex min-w-0 gap-2 data-[orientation=horizontal]:flex-col",
36
+ className
37
+ ),
28
38
  ...props,
29
39
  children: items ? /* @__PURE__ */ jsxs(Fragment, { children: [
30
40
  /* @__PURE__ */ jsx(
@@ -72,7 +82,14 @@ const TabsList = React.forwardRef(({ className, variant = "default", ...props },
72
82
  "data-slot": "tabs-list",
73
83
  "data-variant": variant,
74
84
  className: cn(
75
- "group/tabs-list text-muted-foreground data-[variant=default]:bg-muted inline-flex w-fit items-center justify-center rounded-lg p-1 group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:gap-1 data-[variant=line]:rounded-none data-[variant=line]:bg-transparent",
85
+ // `min-w-0 max-w-full` let the list shrink to (and never exceed) whatever width its
86
+ // ancestors actually give it instead of forcing them wider; horizontal orientation then
87
+ // scrolls its own overflow rather than clipping/hiding long localized labels in a narrow
88
+ // container (gh#175). Hidden scrollbar keeps the strip visually clean while staying
89
+ // swipeable on touch and reachable via keyboard (arrow-key roving focus still scrolls the
90
+ // newly-focused trigger into view natively). Vertical orientation is untouched — it already
91
+ // stacks in a column and is sized by its own `h-*`/`w-*` overrides.
92
+ "group/tabs-list text-muted-foreground data-[variant=default]:bg-muted inline-flex w-fit max-w-full min-w-0 items-center justify-center rounded-lg p-1 group-data-[orientation=vertical]/tabs:flex-col data-[orientation=horizontal]:[scrollbar-width:none] data-[orientation=horizontal]:overflow-x-auto data-[orientation=horizontal]:overflow-y-hidden data-[variant=line]:gap-1 data-[variant=line]:rounded-none data-[variant=line]:bg-transparent [&[data-orientation=horizontal]::-webkit-scrollbar]:hidden",
76
93
  className
77
94
  ),
78
95
  ...props
@@ -43,6 +43,7 @@ const TagInput = React.forwardRef(
43
43
  "div",
44
44
  {
45
45
  "data-slot": "tag-input",
46
+ "aria-disabled": disabled || void 0,
46
47
  className: cn("ui-tag-input", disabled && "ui-tag-input-disabled", className),
47
48
  children: [
48
49
  tags.length > 0 ? /* @__PURE__ */ jsx("ul", { role: "list", className: "ui-tag-input-list", "data-slot": "tag-input-list", children: tags.map((tag, i) => /* @__PURE__ */ jsxs(
@@ -129,7 +129,9 @@
129
129
  "sidebarLabel": "Sidebar",
130
130
  "headerLabel": "Header",
131
131
  "mainLabel": "Main content",
132
- "footerLabel": "Footer"
132
+ "footerLabel": "Footer",
133
+ "openNav": "Open navigation menu",
134
+ "navLabel": "Menu"
133
135
  },
134
136
  "authShell": {
135
137
  "brandLabel": "Brand",
@@ -129,7 +129,9 @@
129
129
  "sidebarLabel": "サイドバー",
130
130
  "headerLabel": "ヘッダー",
131
131
  "mainLabel": "メインコンテンツ",
132
- "footerLabel": "フッター"
132
+ "footerLabel": "フッター",
133
+ "openNav": "ナビゲーションメニューを開く",
134
+ "navLabel": "メニュー"
133
135
  },
134
136
  "authShell": {
135
137
  "brandLabel": "ブランド",
@@ -129,7 +129,9 @@
129
129
  "sidebarLabel": "Thanh bên",
130
130
  "headerLabel": "Đầu trang",
131
131
  "mainLabel": "Nội dung chính",
132
- "footerLabel": "Chân trang"
132
+ "footerLabel": "Chân trang",
133
+ "openNav": "Mở menu điều hướng",
134
+ "navLabel": "Menu"
133
135
  },
134
136
  "authShell": {
135
137
  "brandLabel": "Thương hiệu",
@@ -0,0 +1,66 @@
1
+ import type * as React from "react";
2
+ /**
3
+ * The accessible-name / description / validation contract that {@link FormField} injects onto its
4
+ * single control child (via `cloneElement`). EVERY form-capable `@godxjp/ui` component accepts
5
+ * these props and forwards them to its **semantic focus target** — the real `<input>` / combobox /
6
+ * trigger the user tabs to, never a presentational wrapper `<div>`. Without this forwarding the
7
+ * visible label / helper / error rendered by FormField is silently disconnected from the control
8
+ * for assistive technology (issue #164).
9
+ *
10
+ * These are the standard WAI-ARIA field relationships:
11
+ * - `aria-labelledby` — points at the FormField label → the control's accessible **name**.
12
+ * - `aria-describedby` — points at helper text → the control's **description**.
13
+ * - `aria-errormessage` + `aria-invalid` — the validation message, announced when invalid.
14
+ * - `aria-required` — required-field semantics.
15
+ * - `aria-label` — a name supplied directly (used when there is no visible label element).
16
+ */
17
+ export interface FieldA11yProps {
18
+ "aria-label"?: string;
19
+ "aria-labelledby"?: string;
20
+ "aria-describedby"?: string;
21
+ "aria-errormessage"?: string;
22
+ "aria-invalid"?: React.AriaAttributes["aria-invalid"];
23
+ "aria-required"?: React.AriaAttributes["aria-required"];
24
+ }
25
+ /**
26
+ * Merge one or more space-separated id-reference lists (`aria-describedby`, `aria-labelledby`,
27
+ * `aria-errormessage`) into a single deduplicated token list. Returns `undefined` when empty so the
28
+ * attribute is omitted rather than rendered as `""`.
29
+ */
30
+ export declare function mergeAriaIds(...values: Array<string | undefined>): string | undefined;
31
+ /**
32
+ * Extract ONLY the defined field-a11y attributes from a props bag, ready to spread onto a
33
+ * component's semantic focus target. Undefined entries are dropped so a forwarded contract never
34
+ * clobbers a control's own intrinsic `aria-label` / `aria-invalid`.
35
+ *
36
+ * ```tsx
37
+ * const fieldA11y = pickFieldA11y(props);
38
+ * return <input {...fieldA11y} />; // the real focus target owns the relationship
39
+ * ```
40
+ */
41
+ export declare function pickFieldA11y(props: FieldA11yProps): FieldA11yProps;
42
+ /**
43
+ * As {@link pickFieldA11y}, but resolves the accessible **name** for a control that also has an
44
+ * intrinsic `aria-label` (e.g. SearchInput's built-in "Search"): when the FormField supplies an
45
+ * `aria-labelledby`, it wins and the intrinsic `aria-label` is dropped (a control must not carry
46
+ * both — `aria-labelledby` takes precedence and the redundant `aria-label` only adds noise).
47
+ */
48
+ export declare function resolveFieldA11y(props: FieldA11yProps, intrinsicAriaLabel?: string): FieldA11yProps;
49
+ /**
50
+ * The field-a11y attributes valid on a **group container** (`role="group"`), used by composite
51
+ * controls that have no single labelable focus target — CheckboxGroup, and the two-input range
52
+ * pickers / Transfer shuttle.
53
+ *
54
+ * Per WAI-ARIA 1.2 only `aria-labelledby` and `aria-describedby` are globally allowed on
55
+ * `role="group"`; `aria-invalid` / `aria-errormessage` / `aria-required` are widget-only and would
56
+ * be an invalid-ARIA violation on a group. So the validation message id (`aria-errormessage`) is
57
+ * **folded into `aria-describedby`** — the error text is still announced as part of the group's
58
+ * description, and FormField's `role="alert"` error node announces it live regardless.
59
+ *
60
+ * `role="radiogroup"` IS a widget and supports the full validation set — those groups use
61
+ * {@link pickFieldA11y} directly, not this helper.
62
+ */
63
+ export declare function pickGroupFieldA11y(props: FieldA11yProps): {
64
+ "aria-labelledby"?: string;
65
+ "aria-describedby"?: string;
66
+ };
@@ -0,0 +1,44 @@
1
+ const FIELD_A11Y_KEYS = [
2
+ "aria-label",
3
+ "aria-labelledby",
4
+ "aria-describedby",
5
+ "aria-errormessage",
6
+ "aria-invalid",
7
+ "aria-required"
8
+ ];
9
+ function mergeAriaIds(...values) {
10
+ return Array.from(new Set(values.flatMap((value) => value?.split(/\s+/).filter(Boolean) ?? []))).join(
11
+ " "
12
+ ) || void 0;
13
+ }
14
+ function pickFieldA11y(props) {
15
+ const out = {};
16
+ for (const key of FIELD_A11Y_KEYS) {
17
+ const value = props[key];
18
+ if (value !== void 0) out[key] = value;
19
+ }
20
+ return out;
21
+ }
22
+ function resolveFieldA11y(props, intrinsicAriaLabel) {
23
+ const picked = pickFieldA11y(props);
24
+ if (picked["aria-labelledby"]) {
25
+ return picked;
26
+ }
27
+ if (picked["aria-label"] === void 0 && intrinsicAriaLabel !== void 0) {
28
+ picked["aria-label"] = intrinsicAriaLabel;
29
+ }
30
+ return picked;
31
+ }
32
+ function pickGroupFieldA11y(props) {
33
+ const describedBy = mergeAriaIds(props["aria-describedby"], props["aria-errormessage"]);
34
+ return {
35
+ ...props["aria-labelledby"] !== void 0 ? { "aria-labelledby": props["aria-labelledby"] } : {},
36
+ ...describedBy !== void 0 ? { "aria-describedby": describedBy } : {}
37
+ };
38
+ }
39
+ export {
40
+ mergeAriaIds,
41
+ pickFieldA11y,
42
+ pickGroupFieldA11y,
43
+ resolveFieldA11y
44
+ };
@@ -69,11 +69,15 @@ export type AppSettingKind = "locale" | "timezone" | "dateFormat" | "timeFormat"
69
69
  export type AppSettingPickerProp = {
70
70
  kind: AppSettingKind;
71
71
  /**
72
- * Trigger presentation. `"labeled"` (default) shows the leading icon + the selected value in a
73
- * full-width control. `"icon"` renders a supported square, icon-only topbar trigger (e.g. a globe
74
- * language switcher): it structurally drops the value text and the picker's owned width — no
75
- * descendant-selector CSS overrides needed — while always keeping the localized `aria-label`, so
76
- * an icon-only trigger can never ship without an accessible name.
72
+ * Trigger presentation. `"labeled"` shows the leading icon + the selected value in a control
73
+ * sized to the setting. `"icon"` renders a supported square, icon-only topbar trigger (e.g. a
74
+ * globe language switcher): it structurally drops the value text and the picker's owned width —
75
+ * no descendant-selector CSS overrides needed — while always keeping the localized `aria-label`,
76
+ * so an icon-only trigger can never ship without an accessible name.
77
+ *
78
+ * Default is kind-dependent: `kind="locale"` defaults to `"icon"` (its product contract is the
79
+ * compact language switcher); every other kind defaults to `"labeled"`. Override explicitly for
80
+ * e.g. a labeled locale row inside a settings form (`appearance="labeled"`).
77
81
  */
78
82
  appearance?: AppSettingPickerAppearanceProp;
79
83
  className?: ClassNameProp;
@@ -1,6 +1,6 @@
1
1
  /** Data Display component prop types — @see docs/COMPONENTS.md#data-display */
2
2
  import type * as React from "react";
3
- import type { ActionProp, ClassNameProp, DescriptionProp, IconProp, TitleProp, ColumnDefProp, GetRowIdProp, OnRowClickProp, OnSelectChangeProp, OnSortChangeProp, OnTableDensityChangeProp, SelectedIdsProp, SortStateProp, TableDensityProp, ChildrenProp, ToneProp } from "../vocabulary/index.js";
3
+ import type { ActionProp, ClassNameProp, DescriptionProp, IconProp, TitleProp, ColumnDefProp, GetRowIdProp, OnRowClickProp, OnSelectChangeProp, OnSortChangeProp, OnTableDensityChangeProp, SelectedIdsProp, SortStateProp, TableDensityProp, ChildrenProp, ToneProp, HeadingLevelProp } from "../vocabulary/index.js";
4
4
  /** @see EmptyState */
5
5
  /**
6
6
  * Semantic intent of the EmptyState icon medallion — a subset of the shared `ToneProp` vocabulary
@@ -22,6 +22,21 @@ export type EmptyStateProp = {
22
22
  * consumer never hand-rolls a `.ui-success-state` class to recolour it.
23
23
  */
24
24
  tone?: EmptyStateToneProp;
25
+ /**
26
+ * Semantic heading level (`h1`–`h4`) for the title. Default `3`. Choose it to
27
+ * keep the page outline valid (`h1 → h2 → h3`, no skipped levels), NOT for
28
+ * visual size — the title size is fixed by `--empty-state` styles regardless
29
+ * of level. A page/onboarding empty state directly under a page `h1` uses
30
+ * `titleLevel={2}`; an empty state nested in an already-`h2` section uses `3`.
31
+ */
32
+ titleLevel?: HeadingLevelProp;
33
+ /**
34
+ * Render the title as a non-heading element (`p` / `div`) instead of a
35
+ * heading. Use for a `compact`/`section` empty state placed inside a section
36
+ * that already owns its heading, so the zero-state message is not announced as
37
+ * a heading and cannot skip an outline level. Overrides `titleLevel`.
38
+ */
39
+ titleAs?: "h1" | "h2" | "h3" | "h4" | "p" | "div";
25
40
  className?: ClassNameProp;
26
41
  };
27
42
  /** @see Descriptions */