@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
@@ -1,8 +1,20 @@
1
1
  import type { AppLocale } from "./types.js";
2
2
  import type { AppDateFormat } from "./date-formats.js";
3
3
  export declare const APP_DATE_FORMAT_OPTIONS: {
4
- value: "dmy" | "iso" | "mdy";
4
+ value: "dmy" | "iso" | "mdy" | "ymd";
5
5
  }[];
6
6
  export declare function getDateFormatLabel(dateFormat: AppDateFormat, locale: AppLocale, fallbackLocale?: AppLocale): string;
7
- /** Suggested default per locale — vi → dmy, ja → iso, en → mdy. */
7
+ /**
8
+ * Suggested default per locale — vi → dmy, ja → ymd, en → mdy.
9
+ *
10
+ * `ja` used to be `iso` (`yyyy-MM-dd`). Nothing recorded a reason for it, and it is not the form a
11
+ * Japanese business document is written in: those use `YYYY/MM/DD` (or `YYYY年MM月DD日`). The cost
12
+ * of the old default was measured in a consumer — gino-cloud shipped its own date formatter as an
13
+ * explicit stopgap purely to get the slashes, which is the package's own rule 1 broken by the
14
+ * package's own default.
15
+ *
16
+ * ONLY THE DEFAULT MOVES. A stored preference still wins (see `resolveDateFormat` in
17
+ * app-provider.tsx), a service can pin any value through `defaultDateFormat`, and `iso` is still
18
+ * one keystroke away in `DateFormatPicker`.
19
+ */
8
20
  export declare function resolveDefaultDateFormat(locale: AppLocale): AppDateFormat;
@@ -6,7 +6,7 @@ function getDateFormatLabel(dateFormat, locale, fallbackLocale = "en") {
6
6
  }
7
7
  function resolveDefaultDateFormat(locale) {
8
8
  if (locale === "en") return "mdy";
9
- if (locale === "ja") return "iso";
9
+ if (locale === "ja") return "ymd";
10
10
  return "dmy";
11
11
  }
12
12
  export {
@@ -1,7 +1,19 @@
1
1
  /** Date-only display preset — sent as `x-date-format` to backend. */
2
2
  import { type AppTimeFormat } from "./time-formats.js";
3
- export type AppDateFormat = "iso" | "dmy" | "mdy";
4
- export declare const APP_DATE_FORMATS: readonly ["iso", "dmy", "mdy"];
3
+ export type AppDateFormat = "iso" | "ymd" | "dmy" | "mdy";
4
+ /**
5
+ * `ymd` (`yyyy/MM/dd`) IS THE JAPANESE BUSINESS FORM, AND ITS ABSENCE WAS A HOLE WITH NO WAY OUT.
6
+ *
7
+ * The axis shipped three values and none of them said `2026/05/01`: `iso` writes hyphens, and the
8
+ * two slash forms put the day or the month first. A Japanese-first consumer (gino-cloud) therefore
9
+ * wrote its own formatter and marked it an explicit stopgap — the tell that a named axis was
10
+ * missing a value rather than the consumer wanting something exotic, since the only alternative
11
+ * the package left was the thing docs/DATETIME.md rule 1 forbids ("never use date-fns/format,
12
+ * toLocaleString, or raw ISO slices for UI").
13
+ *
14
+ * It sits BEFORE the day/month-first pair because it is the other year-first form, next to `iso`.
15
+ */
16
+ export declare const APP_DATE_FORMATS: readonly ["iso", "ymd", "dmy", "mdy"];
5
17
  export declare const APP_REQUEST_HEADER_DATE_FORMAT: "x-date-format";
6
18
  /** date-fns pattern for date-only display. */
7
19
  export declare function getDatePattern(dateFormat: AppDateFormat): string;
@@ -1,8 +1,15 @@
1
1
  import { getTimePattern } from "./time-formats.js";
2
- const APP_DATE_FORMATS = ["iso", "dmy", "mdy"];
2
+ const APP_DATE_FORMATS = [
3
+ "iso",
4
+ "ymd",
5
+ "dmy",
6
+ "mdy"
7
+ ];
3
8
  const APP_REQUEST_HEADER_DATE_FORMAT = "x-date-format";
4
9
  function getDatePattern(dateFormat) {
5
10
  switch (dateFormat) {
11
+ case "ymd":
12
+ return "yyyy/MM/dd";
6
13
  case "dmy":
7
14
  return "dd/MM/yyyy";
8
15
  case "mdy":
@@ -16,7 +23,7 @@ function getDateTimePattern(timeFormat, dateFormat) {
16
23
  return `${getDatePattern(dateFormat)} ${getTimePattern(timeFormat)}`;
17
24
  }
18
25
  function isAppDateFormat(value) {
19
- return value === "iso" || value === "dmy" || value === "mdy";
26
+ return APP_DATE_FORMATS.includes(value);
20
27
  }
21
28
  export {
22
29
  APP_DATE_FORMATS,
@@ -13,6 +13,13 @@ interface DataTableProps<T> {
13
13
  columns: ColumnDef<T>[];
14
14
  /** Required when `selectable` is true. Default: assume row.id (typed as any). */
15
15
  getRowId?: (row: T) => string;
16
+ /**
17
+ * Human name of a row, announced by its selection checkbox or radio as "Select row {label}".
18
+ * Default: the text of the `priority: "primary"` column, else of the first column, when that
19
+ * value is a string or number; the row id only as a last resort — an id is a KEY, and announced
20
+ * it reads a UUID aloud. `rowSelection.getCheckboxProps` `aria-label` still overrides a row.
21
+ */
22
+ getRowLabel?: (row: T) => string;
16
23
  selectable?: boolean;
17
24
  selected?: Set<string>;
18
25
  onSelectChange?: (next: Set<string>) => void;
@@ -130,7 +137,7 @@ interface DataTableProps<T> {
130
137
  className?: string;
131
138
  children?: React.ReactNode;
132
139
  }
133
- export declare function DataTable<T>({ data, columns, getRowId, selectable, selected: controlledSelected, onSelectChange, onRowClick, density: controlledDensity, onDensityChange, sort, onSortChange, globalFilter: controlledGlobalFilter, onGlobalFilterChange, pagination: paginationProp, onPaginationChange, rowCount, columnVisibility: controlledVisibility, onColumnVisibilityChange, manualSorting, manualFiltering, manualPagination, loading, empty, error, denied, onRetry, striped, hoverable, stickyHeader, preset, collapseBelow, rowClassName, rowTone, rowSelection, expandable, summary, scroll, sticky, onRow, bordered, showSorterTooltip, sortDirections, onFilterChange, className, children, }: DataTableProps<T>): React.JSX.Element;
140
+ export declare function DataTable<T>({ data, columns, getRowId, getRowLabel, selectable, selected: controlledSelected, onSelectChange, onRowClick, density: controlledDensity, onDensityChange, sort, onSortChange, globalFilter: controlledGlobalFilter, onGlobalFilterChange, pagination: paginationProp, onPaginationChange, rowCount, columnVisibility: controlledVisibility, onColumnVisibilityChange, manualSorting, manualFiltering, manualPagination, loading, empty, error, denied, onRetry, striped, hoverable, stickyHeader, preset, collapseBelow, rowClassName, rowTone, rowSelection, expandable, summary, scroll, sticky, onRow, bordered, showSorterTooltip, sortDirections, onFilterChange, className, children, }: DataTableProps<T>): React.JSX.Element;
134
141
  export declare namespace DataTable {
135
142
  var Toolbar: ({ children, className, }: {
136
143
  children?: React.ReactNode;
@@ -190,6 +190,7 @@ function DataTable({
190
190
  data,
191
191
  columns,
192
192
  getRowId = noopGetRowId,
193
+ getRowLabel,
193
194
  selectable = false,
194
195
  selected: controlledSelected,
195
196
  onSelectChange,
@@ -443,6 +444,7 @@ function DataTable({
443
444
  toggleExpanded,
444
445
  rowSelection,
445
446
  getRowId,
447
+ getRowLabel,
446
448
  showSorterTooltip,
447
449
  sortDirections,
448
450
  sortPriorities,
@@ -764,6 +766,7 @@ DataTable.Content = function DataTableContent() {
764
766
  toggleExpanded,
765
767
  rowSelection,
766
768
  getRowId,
769
+ getRowLabel,
767
770
  showSorterTooltip,
768
771
  sortDirections,
769
772
  sortPriorities
@@ -791,6 +794,7 @@ DataTable.Content = function DataTableContent() {
791
794
  );
792
795
  }
793
796
  }, [missingHeaderNames]);
797
+ const selectLabelColumn = visibleColumns.find((col) => col.priority === "primary") ?? visibleColumns[0];
794
798
  const scrollRef = React.useRef(null);
795
799
  const [hasOverflowEnd, setHasOverflowEnd] = React.useState(false);
796
800
  React.useEffect(() => {
@@ -1096,6 +1100,9 @@ DataTable.Content = function DataTableContent() {
1096
1100
  const rowKey = getRowId(original);
1097
1101
  const isSelected = row.getIsSelected();
1098
1102
  const checkboxProps = rowSelection?.getCheckboxProps?.(original) ?? {};
1103
+ const labelValue = selectLabelColumn ? original[selectLabelColumn.key] : void 0;
1104
+ const rowLabel = getRowLabel?.(original) ?? (typeof labelValue === "string" && labelValue.trim() !== "" || typeof labelValue === "number" ? String(labelValue) : row.id);
1105
+ const selectRowLabel = checkboxProps["aria-label"] ?? t("dataTable.selectRow", { id: rowLabel });
1099
1106
  const canExpand = expandColumnShown && (expandable?.rowExpandable?.(original) ?? true);
1100
1107
  const isExpanded = canExpand && expandedKeys.includes(rowKey);
1101
1108
  const rowProps = onRow?.(original, rowIndex) ?? {};
@@ -1206,7 +1213,7 @@ DataTable.Content = function DataTableContent() {
1206
1213
  RadioItem,
1207
1214
  {
1208
1215
  value: rowKey,
1209
- "aria-label": checkboxProps["aria-label"] ?? t("dataTable.selectRow", { id: row.id }),
1216
+ "aria-label": selectRowLabel,
1210
1217
  onClick: (e) => {
1211
1218
  e.stopPropagation();
1212
1219
  }
@@ -1222,7 +1229,7 @@ DataTable.Content = function DataTableContent() {
1222
1229
  onCheckedChange: (v) => {
1223
1230
  row.toggleSelected(!!v);
1224
1231
  },
1225
- "aria-label": checkboxProps["aria-label"] ?? t("dataTable.selectRow", { id: row.id }),
1232
+ "aria-label": selectRowLabel,
1226
1233
  onClick: (e) => {
1227
1234
  e.stopPropagation();
1228
1235
  }
@@ -94,7 +94,7 @@ const CheckboxRoot = React.forwardRef((props, ref) => {
94
94
  // itself). react-aria's root is a `<label>`, which is `display:inline` — without this the
95
95
  // 16px box collapses to nothing. CheckboxVisual already carries the same three for the
96
96
  // same reason.
97
- "peer ui-checkbox data-[invalid]:border-destructive data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground inline-flex shrink-0 items-center justify-center shadow-xs transition-shadow outline-none",
97
+ "peer ui-checkbox data-[invalid]:border-destructive data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground inline-flex shrink-0 items-center justify-center shadow-xs transition-shadow",
98
98
  className
99
99
  ),
100
100
  render: (domProps) => /* @__PURE__ */ jsx("label", { ...domProps, children: withOwnHitTarget(domProps.children) }),
@@ -47,7 +47,7 @@ function FormField({
47
47
  const resolvedId = id ?? autoId;
48
48
  const labelId = `${resolvedId}-label`;
49
49
  const helperId = helper ? `${resolvedId}-helper` : void 0;
50
- const helperNode = helper ? /* @__PURE__ */ jsx("p", { id: helperId, className: "text-muted-foreground text-xs", children: helper }) : null;
50
+ const helperNode = helper ? /* @__PURE__ */ jsx("p", { id: helperId, className: "ui-form-field-note text-muted-foreground text-xs", children: helper }) : null;
51
51
  const validationStatus = error ? "error" : validateStatus;
52
52
  const feedbackId = hasFeedback && validationStatus ? `${resolvedId}-feedback` : void 0;
53
53
  const FeedbackIcon = validationStatus === "validating" ? LoaderCircle : validationStatus === "error" ? CircleAlert : validationStatus === "warning" ? TriangleAlert : CheckCircle;
@@ -175,7 +175,7 @@ function FormField({
175
175
  t(`dataEntry.form.${validationStatus}`)
176
176
  ] }) : null,
177
177
  helperPlacement === "after" ? helperNode : null,
178
- error ? /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "text-destructive text-xs", children: error }) : null
178
+ error ? /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "ui-form-field-note text-destructive text-xs", children: error }) : null
179
179
  ] })
180
180
  ]
181
181
  }
@@ -13,7 +13,7 @@ import {
13
13
  } from "./control-appearance.js";
14
14
  import { resolveAllowClear } from "./control-surface.js";
15
15
  const inputBaseClass = [
16
- "ui-control ui-input w-full rounded-[var(--control-radius)] transition-[color,box-shadow] outline-none",
16
+ "ui-control ui-input w-full rounded-[var(--control-radius)] transition-[color,box-shadow]",
17
17
  "selection:bg-primary selection:text-primary-foreground",
18
18
  "placeholder:text-muted-foreground",
19
19
  "aria-invalid:border-destructive",
@@ -57,6 +57,21 @@ const NumberInput = React.forwardRef(
57
57
  }, ref) => {
58
58
  const { t, locale } = useTranslation();
59
59
  const fieldA11y = pickFieldA11y(ariaProps);
60
+ const stepperVerbId = React.useId();
61
+ const fieldLabel = typeof fieldA11y["aria-label"] === "string" ? fieldA11y["aria-label"] : void 0;
62
+ const fieldLabelledBy = typeof fieldA11y["aria-labelledby"] === "string" ? fieldA11y["aria-labelledby"] : void 0;
63
+ const stepperName = (direction) => {
64
+ const verb = t(`ui.numberInput.${direction}`);
65
+ if (fieldLabelledBy) {
66
+ return { "aria-labelledby": `${fieldLabelledBy} ${stepperVerbId}-${direction}` };
67
+ }
68
+ if (fieldLabel) {
69
+ return {
70
+ "aria-label": t(`ui.numberInput.${direction}Field`, { label: fieldLabel }) || verb
71
+ };
72
+ }
73
+ return { "aria-label": verb };
74
+ };
60
75
  const isControlled = controlledValue !== void 0;
61
76
  const [internal, setInternal] = React.useState(defaultValue);
62
77
  const numericValue = isControlled ? controlledValue ?? null : internal;
@@ -228,7 +243,7 @@ const NumberInput = React.forwardRef(
228
243
  }
229
244
  ) : null,
230
245
  controls ? /* @__PURE__ */ jsxs("span", { "data-slot": "number-input-steppers", className: "ui-number-input-steppers", children: [
231
- /* @__PURE__ */ jsx(
246
+ /* @__PURE__ */ jsxs(
232
247
  Button,
233
248
  {
234
249
  type: "button",
@@ -237,12 +252,15 @@ const NumberInput = React.forwardRef(
237
252
  className: "ui-number-input-step ui-number-input-step-up",
238
253
  tabIndex: -1,
239
254
  disabled: !interactive || atMax,
240
- "aria-label": t("ui.numberInput.increment"),
255
+ ...stepperName("increment"),
241
256
  onClick: () => stepBy(1),
242
- children: /* @__PURE__ */ jsx(ChevronUp, { "aria-hidden": "true" })
257
+ children: [
258
+ /* @__PURE__ */ jsx(ChevronUp, { "aria-hidden": "true" }),
259
+ /* @__PURE__ */ jsx("span", { id: `${stepperVerbId}-increment`, className: "sr-only", children: t("ui.numberInput.increment") })
260
+ ]
243
261
  }
244
262
  ),
245
- /* @__PURE__ */ jsx(
263
+ /* @__PURE__ */ jsxs(
246
264
  Button,
247
265
  {
248
266
  type: "button",
@@ -251,9 +269,12 @@ const NumberInput = React.forwardRef(
251
269
  className: "ui-number-input-step ui-number-input-step-down",
252
270
  tabIndex: -1,
253
271
  disabled: !interactive || atMin,
254
- "aria-label": t("ui.numberInput.decrement"),
272
+ ...stepperName("decrement"),
255
273
  onClick: () => stepBy(-1),
256
- children: /* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": "true" })
274
+ children: [
275
+ /* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": "true" }),
276
+ /* @__PURE__ */ jsx("span", { id: `${stepperVerbId}-decrement`, className: "sr-only", children: t("ui.numberInput.decrement") })
277
+ ]
257
278
  }
258
279
  )
259
280
  ] }) : null
@@ -62,7 +62,7 @@ const RadioItem = React.forwardRef(
62
62
  // records: react-aria's root is a `<label>`, which is `display:inline`, so without it the
63
63
  // 16px dot collapses to nothing. `data-[invalid]` replaces `aria-invalid:` because
64
64
  // react-aria puts `aria-invalid` on the `<input>` and `data-invalid` on this box.
65
- "ui-radio data-[invalid]:border-destructive inline-flex shrink-0 items-center justify-center shadow-xs transition-shadow outline-none",
65
+ "ui-radio data-[invalid]:border-destructive inline-flex shrink-0 items-center justify-center shadow-xs transition-shadow",
66
66
  className
67
67
  ),
68
68
  ...props,
@@ -80,7 +80,7 @@ const Switch = React.forwardRef(
80
80
  // components, so it silently outranked that token. Byte-identical: the token
81
81
  // defaults to 0.5. (`shadow-xs` STAYS — .ui-switch declares --shadow-sm, so this
82
82
  // utility is the switch's real resting elevation, not a duplicate.)
83
- "peer ui-switch shadow-xs transition-all outline-none",
83
+ "peer ui-switch shadow-xs transition-all",
84
84
  className
85
85
  ),
86
86
  children: [
@@ -149,7 +149,7 @@ function DialogContent({
149
149
  showCloseButton ? /* @__PURE__ */ jsxs(
150
150
  DialogClose,
151
151
  {
152
- className: "ui-focus-ring transition-opacity focus:outline-hidden disabled:pointer-events-none",
152
+ className: "ui-focus-ring transition-opacity disabled:pointer-events-none",
153
153
  children: [
154
154
  /* @__PURE__ */ jsx(X, { className: "ui-dialog-close-icon", "aria-hidden": "true" }),
155
155
  /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("feedback.alert.dismiss") })
@@ -265,7 +265,7 @@ function AlertDialogContent({
265
265
  {
266
266
  type: "button",
267
267
  "data-slot": "dialog-close",
268
- className: "ui-focus-ring transition-opacity focus:outline-hidden disabled:pointer-events-none",
268
+ className: "ui-focus-ring transition-opacity disabled:pointer-events-none",
269
269
  "aria-label": t("feedback.alert.dismiss"),
270
270
  onClick: () => {
271
271
  state.setOpen(false);
@@ -6,7 +6,7 @@ import { chain, mergeRefs } from "@react-aria/utils";
6
6
  import { Dialog as RacDialog, Modal, ModalOverlay } from "react-aria-components";
7
7
  import { cva } from "class-variance-authority";
8
8
  import { X } from "lucide-react";
9
- import { useMediaQuery } from "../../lib/hooks.js";
9
+ import { useMaxWidthBreakpoint } from "../../lib/breakpoint-token.js";
10
10
  import { cn } from "../../lib/utils.js";
11
11
  import { Slot } from "../../lib/slot.js";
12
12
  import { overlayHeaderToneClass } from "./overlay-header-tone.js";
@@ -15,28 +15,8 @@ import { useTranslation } from "../../i18n/use-translation.js";
15
15
  const toCssLength = (v) => typeof v === "number" ? `${v}px` : v;
16
16
  const SHEET_BREAKPOINT_TOKEN = "--sheet-responsive-breakpoint-width";
17
17
  const SHEET_BREAKPOINT_FALLBACK_QUERY = "(max-width: 768px)";
18
- function cssLengthToPx(value, rootFontSize) {
19
- const match = /^(-?\d*\.?\d+)(px|rem|em)?$/.exec(value.trim());
20
- if (match == null) return void 0;
21
- const amount = Number(match[1]);
22
- if (!Number.isFinite(amount)) return void 0;
23
- return match[2] === "rem" || match[2] === "em" ? amount * rootFontSize : amount;
24
- }
25
- function readSheetBreakpointQuery() {
26
- if (typeof document === "undefined" || typeof window.getComputedStyle !== "function") {
27
- return SHEET_BREAKPOINT_FALLBACK_QUERY;
28
- }
29
- const rootStyle = window.getComputedStyle(document.documentElement);
30
- const rootFontSize = cssLengthToPx(rootStyle.fontSize || "16px", 16) ?? 16;
31
- const px = cssLengthToPx(rootStyle.getPropertyValue(SHEET_BREAKPOINT_TOKEN), rootFontSize);
32
- return px == null ? SHEET_BREAKPOINT_FALLBACK_QUERY : `(max-width: ${String(px)}px)`;
33
- }
34
18
  function useSheetResponsiveMode(responsive = "side") {
35
- const [query, setQuery] = React.useState(SHEET_BREAKPOINT_FALLBACK_QUERY);
36
- React.useEffect(() => {
37
- setQuery(readSheetBreakpointQuery());
38
- }, []);
39
- const compact = useMediaQuery(query);
19
+ const compact = useMaxWidthBreakpoint(SHEET_BREAKPOINT_TOKEN, SHEET_BREAKPOINT_FALLBACK_QUERY);
40
20
  if (responsive === "side" || responsive === "bottom") return responsive;
41
21
  return compact ? "bottom" : "side";
42
22
  }
@@ -96,7 +96,8 @@ function Toolbar({
96
96
  defaultValue: filter.defaultSelected,
97
97
  onValueChange: (selected) => filter.onSelectedChange?.(selected),
98
98
  placeholder: filter.placeholder,
99
- disabled: disabled || filter.disabled
99
+ disabled: disabled || filter.disabled,
100
+ clearLabel: typeof filter.label === "string" ? t("navigation.filterBar.clearFilter", { label: filter.label }) : void 0
100
101
  }
101
102
  )
102
103
  },
@@ -9,6 +9,7 @@ import {
9
9
  } from "react-aria-components";
10
10
  import { MoreHorizontal, Plus, X } from "lucide-react";
11
11
  import { useTranslation } from "../../i18n/use-translation.js";
12
+ import { useMaxWidthBreakpoint } from "../../lib/breakpoint-token.js";
12
13
  import { cn } from "../../lib/utils.js";
13
14
  import {
14
15
  DropdownMenu,
@@ -57,6 +58,13 @@ function resolveTabsAxis(tabPlacement, orientation) {
57
58
  orientation: orientation ?? "horizontal"
58
59
  };
59
60
  }
61
+ const TABS_PLACEMENT_BREAKPOINT_TOKEN = "--tabs-placement-responsive-breakpoint-width";
62
+ const TABS_PLACEMENT_BREAKPOINT_FALLBACK_QUERY = "(max-width: 768px)";
63
+ function foldVerticalPlacement(axis, narrow) {
64
+ if (!narrow || axis.orientation !== "vertical") return axis;
65
+ const trailing = axis.placement === "end" || axis.placement === "bottom";
66
+ return { placement: trailing ? "bottom" : "top", orientation: "horizontal" };
67
+ }
60
68
  function Tabs({
61
69
  className,
62
70
  orientation,
@@ -85,9 +93,13 @@ function Tabs({
85
93
  }) {
86
94
  const { t } = useTranslation();
87
95
  const resolvedDefault = resolveFallbackTabValue(items, defaultValue);
88
- const { placement, orientation: resolvedOrientation } = resolveTabsAxis(
89
- tabPlacement,
90
- orientation
96
+ const narrow = useMaxWidthBreakpoint(
97
+ TABS_PLACEMENT_BREAKPOINT_TOKEN,
98
+ TABS_PLACEMENT_BREAKPOINT_FALLBACK_QUERY
99
+ );
100
+ const { placement, orientation: resolvedOrientation } = foldVerticalPlacement(
101
+ resolveTabsAxis(tabPlacement, orientation),
102
+ narrow
91
103
  );
92
104
  const selectionSuppressed = value === void 0 && items != null && items.length > 0 && resolvedDefault === void 0;
93
105
  const frame = React.useMemo(
@@ -470,7 +482,7 @@ const TabsContent = React.forwardRef(
470
482
  ref,
471
483
  id: value,
472
484
  shouldForceMount: forceMount,
473
- className: cn("ui-focus-ring flex-1 outline-none", className),
485
+ className: cn("ui-focus-ring flex-1", className),
474
486
  render: (domProps, renderProps) => withDomProps("div", "tabs-content", props, domProps, {
475
487
  // A force-mounted panel whose tab is not selected: Radix marked it `data-state="inactive"`
476
488
  // and `hidden`; RAC marks it inert. The state hook is re-emitted so CSS keys the same way.
@@ -341,7 +341,9 @@
341
341
  },
342
342
  "numberInput": {
343
343
  "increment": "Increase",
344
- "decrement": "Decrease"
344
+ "decrement": "Decrease",
345
+ "incrementField": "Increase {label}",
346
+ "decrementField": "Decrease {label}"
345
347
  }
346
348
  },
347
349
  "navigation": {
@@ -368,6 +370,7 @@
368
370
  },
369
371
  "filterBar": {
370
372
  "appliedFilters": "Applied filters",
373
+ "clearFilter": "Clear {label} selection",
371
374
  "removeFilter": "Remove filter: {label}",
372
375
  "resultCount": {
373
376
  "one": "{count} result",
@@ -442,6 +445,7 @@
442
445
  },
443
446
  "dateFormat": {
444
447
  "iso": "ISO (yyyy-MM-dd)",
448
+ "ymd": "Year / Month / Day (yyyy/MM/dd)",
445
449
  "dmy": "Day / Month / Year",
446
450
  "mdy": "Month / Day / Year"
447
451
  },
@@ -334,7 +334,9 @@
334
334
  },
335
335
  "numberInput": {
336
336
  "increment": "増やす",
337
- "decrement": "減らす"
337
+ "decrement": "減らす",
338
+ "incrementField": "{label}を増やす",
339
+ "decrementField": "{label}を減らす"
338
340
  }
339
341
  },
340
342
  "navigation": {
@@ -361,6 +363,7 @@
361
363
  },
362
364
  "filterBar": {
363
365
  "appliedFilters": "適用中のフィルター",
366
+ "clearFilter": "{label}の選択をクリア",
364
367
  "removeFilter": "フィルターを解除: {label}",
365
368
  "resultCount": "{count} 件の結果"
366
369
  },
@@ -429,6 +432,7 @@
429
432
  },
430
433
  "dateFormat": {
431
434
  "iso": "YYYY-MM-DD(年-月-日)",
435
+ "ymd": "YYYY/MM/DD(年/月/日)",
432
436
  "dmy": "日/月/年",
433
437
  "mdy": "月/日/年"
434
438
  },
@@ -335,7 +335,9 @@
335
335
  },
336
336
  "numberInput": {
337
337
  "increment": "Tăng",
338
- "decrement": "Giảm"
338
+ "decrement": "Giảm",
339
+ "incrementField": "Tăng {label}",
340
+ "decrementField": "Giảm {label}"
339
341
  }
340
342
  },
341
343
  "navigation": {
@@ -362,6 +364,7 @@
362
364
  },
363
365
  "filterBar": {
364
366
  "appliedFilters": "Bộ lọc đang áp dụng",
367
+ "clearFilter": "Xóa lựa chọn {label}",
365
368
  "removeFilter": "Bỏ bộ lọc: {label}",
366
369
  "resultCount": "{count} kết quả"
367
370
  },
@@ -430,6 +433,7 @@
430
433
  },
431
434
  "dateFormat": {
432
435
  "iso": "ISO (yyyy-MM-dd)",
436
+ "ymd": "Năm / Tháng / Ngày (yyyy/MM/dd)",
433
437
  "dmy": "Ngày / Tháng / Năm",
434
438
  "mdy": "Tháng / Ngày / Năm"
435
439
  },
@@ -0,0 +1,8 @@
1
+ /**
2
+ * True while the viewport is at or below the width the theme token `token` names.
3
+ *
4
+ * The token is read once per mount rather than at module scope: the fallback is what SSR and the
5
+ * first client render agree on, and the themed value arrives in the effect. Same-value updates
6
+ * bail out inside React, so a theme that left the default alone re-renders nothing.
7
+ */
8
+ export declare function useMaxWidthBreakpoint(token: string, fallbackQuery: string): boolean;
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { useMediaQuery } from "./hooks.js";
4
+ function cssLengthToPx(value, rootFontSize) {
5
+ const match = /^(-?\d*\.?\d+)(px|rem|em)?$/.exec(value.trim());
6
+ if (match == null) return void 0;
7
+ const amount = Number(match[1]);
8
+ if (!Number.isFinite(amount)) return void 0;
9
+ return match[2] === "rem" || match[2] === "em" ? amount * rootFontSize : amount;
10
+ }
11
+ function readMaxWidthQuery(token, fallbackQuery) {
12
+ if (typeof document === "undefined" || typeof window.getComputedStyle !== "function") {
13
+ return fallbackQuery;
14
+ }
15
+ const rootStyle = window.getComputedStyle(document.documentElement);
16
+ const rootFontSize = cssLengthToPx(rootStyle.fontSize || "16px", 16) ?? 16;
17
+ const px = cssLengthToPx(rootStyle.getPropertyValue(token), rootFontSize);
18
+ return px == null ? fallbackQuery : `(max-width: ${String(px)}px)`;
19
+ }
20
+ function useMaxWidthBreakpoint(token, fallbackQuery) {
21
+ const [query, setQuery] = React.useState(fallbackQuery);
22
+ React.useEffect(() => {
23
+ setQuery(readMaxWidthQuery(token, fallbackQuery));
24
+ }, [token, fallbackQuery]);
25
+ return useMediaQuery(query);
26
+ }
27
+ export {
28
+ useMaxWidthBreakpoint
29
+ };
@@ -28,7 +28,7 @@ export type ProseProp = {
28
28
  className?: ClassNameProp;
29
29
  children?: ChildrenProp;
30
30
  };
31
- import type { ActionProp, ClassNameProp, DescriptionProp, IconProp, TitleProp, ColumnDefProp, GetRowIdProp, OnRowClickProp, OnSelectChangeProp, OnSortChangeProp, OnTableDensityChangeProp, SelectedIdsProp, SortStateProp, TableDensityProp, TablePresetProp, BreakpointProp, DensityProp, ChildrenProp, PendingProp, ToneProp, AvatarShapeProp, HeadingLevelProp, HandlerProp, SizeProp, LabelProp, IdProp, DescriptionsLayoutProp, DescriptionsColumnProp, DescriptionsSpanProp, DescriptionsItemsProp, SortDirectionProp, OnColumnFilterChangeProp, OnRowProp, TableExpandableProp, TableRowSelectionProp, TableScrollProp, TableStickyProp, TableSummaryProp, DisabledProp, ValueProp, DefaultValueProp, OnValueChangeProp } from "../vocabulary/index.js";
31
+ import type { ActionProp, ClassNameProp, DescriptionProp, IconProp, TitleProp, ColumnDefProp, GetRowIdProp, GetRowLabelProp, OnRowClickProp, OnSelectChangeProp, OnSortChangeProp, OnTableDensityChangeProp, SelectedIdsProp, SortStateProp, TableDensityProp, TablePresetProp, BreakpointProp, DensityProp, ChildrenProp, PendingProp, ToneProp, AvatarShapeProp, HeadingLevelProp, HandlerProp, SizeProp, LabelProp, IdProp, DescriptionsLayoutProp, DescriptionsColumnProp, DescriptionsSpanProp, DescriptionsItemsProp, SortDirectionProp, OnColumnFilterChangeProp, OnRowProp, TableExpandableProp, TableRowSelectionProp, TableScrollProp, TableStickyProp, TableSummaryProp, DisabledProp, ValueProp, DefaultValueProp, OnValueChangeProp } from "../vocabulary/index.js";
32
32
  import type { TreeFieldNamesProp, TreeOptionProp } from "./data-entry.prop.js";
33
33
  /**
34
34
  * One key in a `Legend`: a tone, and the words that tone stands for.
@@ -255,6 +255,13 @@ export type DataTableProp<T> = {
255
255
  data: T[];
256
256
  columns: ColumnDefProp<T>[];
257
257
  getRowId?: GetRowIdProp<T>;
258
+ /**
259
+ * Human name of a row, announced by its selection checkbox or radio as "Select row {label}".
260
+ * Default: the text of the `priority: "primary"` column, else of the first column, when that
261
+ * value is a string or number; the row id only as a last resort. `rowSelection.getCheckboxProps`
262
+ * `aria-label` still overrides a single row.
263
+ */
264
+ getRowLabel?: GetRowLabelProp<T>;
258
265
  selectable?: boolean;
259
266
  selected?: SelectedIdsProp;
260
267
  onSelectChange?: OnSelectChangeProp;
@@ -337,7 +337,14 @@ export type TabsProp = {
337
337
  defaultValue?: string;
338
338
  onValueChange?: (value: string) => void;
339
339
  variant?: TabsVariantProp;
340
- /** Ant Design `tabPlacement`. Default `top`. */
340
+ /**
341
+ * Ant Design `tabPlacement`. Default `top`.
342
+ *
343
+ * `start`/`end` FOLD to `top`/`bottom` at or below
344
+ * `--tabs-placement-responsive-breakpoint-width` (48rem), arrow keys included — a vertical strip
345
+ * shares one inline axis with its panel and a phone has no room for both. Ant Design folds the
346
+ * same pair the same way. Set the token to `0px` to keep the strip vertical at every width.
347
+ */
341
348
  tabPlacement?: TabsPlacementProp;
342
349
  /** Control tier of the triggers. Default `md`. Ant Design `size` (`small`/`middle`/`large`). */
343
350
  size?: "sm" | "md" | "lg";
@@ -584,6 +584,11 @@ export declare const VOCABULARY_REGISTRY: {
584
584
  readonly category: "data";
585
585
  readonly description: "Row ID extractor generic";
586
586
  };
587
+ readonly GetRowLabelProp: {
588
+ readonly file: "vocabulary/data.prop.ts";
589
+ readonly category: "data";
590
+ readonly description: "Row label extractor generic — the accessible name of a row's selection control";
591
+ };
587
592
  readonly OnRowClickProp: {
588
593
  readonly file: "vocabulary/data.prop.ts";
589
594
  readonly category: "data";
@@ -1533,7 +1538,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
1533
1538
  readonly DataTableProp: {
1534
1539
  readonly group: "data-display";
1535
1540
  readonly file: "components/data-display.prop.ts";
1536
- readonly vocabulary: readonly ["ColumnDefProp", "DensityProp", "SortStateProp", "SelectedIdsProp", "HandlerProp", "TablePresetProp", "TableColumnPriorityProp", "BreakpointProp", "ColumnFixedProp", "ColumnFilterItemProp", "ColumnFilterStateProp", "ColumnSorterProp", "TableRowSelectionProp", "TableExpandableProp", "TableSummaryProp", "TableScrollProp", "TableStickyProp", "OnRowProp", "TablePaginationProp", "SortDirectionProp", "OnColumnFilterChangeProp"];
1541
+ readonly vocabulary: readonly ["ColumnDefProp", "DensityProp", "SortStateProp", "SelectedIdsProp", "GetRowLabelProp", "HandlerProp", "TablePresetProp", "TableColumnPriorityProp", "BreakpointProp", "ColumnFixedProp", "ColumnFilterItemProp", "ColumnFilterStateProp", "ColumnSorterProp", "TableRowSelectionProp", "TableExpandableProp", "TableSummaryProp", "TableScrollProp", "TableStickyProp", "OnRowProp", "TablePaginationProp", "SortDirectionProp", "OnColumnFilterChangeProp"];
1537
1542
  };
1538
1543
  readonly ListRowDensityProp: {
1539
1544
  readonly group: "data-display";
@@ -585,6 +585,11 @@ const VOCABULARY_REGISTRY = {
585
585
  category: "data",
586
586
  description: "Row ID extractor generic"
587
587
  },
588
+ GetRowLabelProp: {
589
+ file: "vocabulary/data.prop.ts",
590
+ category: "data",
591
+ description: "Row label extractor generic \u2014 the accessible name of a row's selection control"
592
+ },
588
593
  OnRowClickProp: {
589
594
  file: "vocabulary/data.prop.ts",
590
595
  category: "data",
@@ -1833,6 +1838,7 @@ const COMPONENT_PROP_REGISTRY = {
1833
1838
  "DensityProp",
1834
1839
  "SortStateProp",
1835
1840
  "SelectedIdsProp",
1841
+ "GetRowLabelProp",
1836
1842
  "HandlerProp",
1837
1843
  "TablePresetProp",
1838
1844
  "TableColumnPriorityProp",
@@ -7,6 +7,11 @@ import type { BreakpointProp, ColumnAlignProp, SortDirectionProp } from "./inter
7
7
  import type { TableDensityProp } from "./layout.prop.js";
8
8
  /** Generic row identifier extractor for tables with selection. */
9
9
  export type GetRowIdProp<T> = (row: T) => string;
10
+ /**
11
+ * Human-readable row name — what a row's selection control is announced as. The row id is a KEY,
12
+ * not a name: announced, it reads a UUID aloud.
13
+ */
14
+ export type GetRowLabelProp<T> = (row: T) => string;
10
15
  /** Row click navigation handler. */
11
16
  export type OnRowClickProp<T> = (row: T) => void;
12
17
  /** Column definition for DataTable. */