@godxjp/ui 22.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 (115) hide show
  1. package/README.md +4 -4
  2. package/dist/app/date-format-labels.d.ts +14 -2
  3. package/dist/app/date-format-labels.js +1 -1
  4. package/dist/app/date-formats.d.ts +14 -2
  5. package/dist/app/date-formats.js +9 -2
  6. package/dist/components/data-display/data-table.d.ts +8 -1
  7. package/dist/components/data-display/data-table.js +9 -2
  8. package/dist/components/data-display/scroll-area.d.ts +24 -4
  9. package/dist/components/data-display/scroll-area.js +34 -72
  10. package/dist/components/data-entry/cascader.js +8 -6
  11. package/dist/components/data-entry/checkbox.js +3 -1
  12. package/dist/components/data-entry/choice-hit-target.d.ts +24 -0
  13. package/dist/components/data-entry/choice-hit-target.js +12 -0
  14. package/dist/components/data-entry/control-surface.d.ts +8 -0
  15. package/dist/components/data-entry/control-surface.js +10 -1
  16. package/dist/components/data-entry/form-field.js +5 -4
  17. package/dist/components/data-entry/input.js +1 -1
  18. package/dist/components/data-entry/number-input.js +27 -6
  19. package/dist/components/data-entry/radio.js +4 -3
  20. package/dist/components/data-entry/search-select.js +225 -95
  21. package/dist/components/data-entry/select.d.ts +76 -14
  22. package/dist/components/data-entry/select.js +495 -163
  23. package/dist/components/data-entry/slider.d.ts +3 -11
  24. package/dist/components/data-entry/slider.js +582 -93
  25. package/dist/components/data-entry/switch.js +3 -1
  26. package/dist/components/feedback/dialog.js +2 -2
  27. package/dist/components/feedback/sheet.js +2 -22
  28. package/dist/components/navigation/dropdown-menu.d.ts +11 -3
  29. package/dist/components/navigation/dropdown-menu.js +190 -15
  30. package/dist/components/navigation/filter-bar.js +2 -1
  31. package/dist/components/navigation/index.d.ts +0 -3
  32. package/dist/components/navigation/index.js +0 -78
  33. package/dist/components/navigation/tabs.js +16 -4
  34. package/dist/components/ui/index.d.ts +0 -3
  35. package/dist/components/ui/index.js +0 -3
  36. package/dist/components/ui/input-otp.d.ts +1 -1
  37. package/dist/components/ui/segmented.js +3 -1
  38. package/dist/components/ui/tag-input.js +3 -5
  39. package/dist/i18n/messages/en.json +5 -1
  40. package/dist/i18n/messages/ja.json +5 -1
  41. package/dist/i18n/messages/vi.json +5 -1
  42. package/dist/lib/breakpoint-token.d.ts +8 -0
  43. package/dist/lib/breakpoint-token.js +29 -0
  44. package/dist/lib/select-options.d.ts +21 -0
  45. package/dist/lib/select-options.js +46 -0
  46. package/dist/props/components/data-display.prop.d.ts +17 -7
  47. package/dist/props/components/data-entry.prop.d.ts +323 -30
  48. package/dist/props/components/index.d.ts +1 -1
  49. package/dist/props/components/navigation.prop.d.ts +25 -1
  50. package/dist/props/registry.d.ts +69 -1
  51. package/dist/props/registry.js +75 -0
  52. package/dist/props/vocabulary/data.prop.d.ts +5 -0
  53. package/dist/props/vocabulary/index.d.ts +1 -1
  54. package/dist/styles/control.css +246 -28
  55. package/dist/styles/data-display-layout.css +18 -10
  56. package/dist/styles/focus-ring.css +28 -0
  57. package/dist/styles/fonts.css +88 -4
  58. package/dist/styles/form-layout.css +4 -0
  59. package/dist/styles/layout.css +2 -1
  60. package/dist/styles/navigation-layout.css +24 -238
  61. package/dist/styles/shell-layout.css +148 -139
  62. package/dist/styles/table-layout.css +12 -5
  63. package/dist/tokens/components/control.css +12 -3
  64. package/dist/tokens/components/data-display.css +0 -5
  65. package/dist/tokens/components/navigation.css +5 -5
  66. package/dist/tokens/components/scroll-area.css +7 -0
  67. package/docs/CONSUMER-RULES.md +7 -2
  68. package/docs/CUSTOMER-THEMING.md +25 -9
  69. package/docs/DATETIME.md +11 -2
  70. package/docs/FRAME-COVERAGE-REPORT.md +4 -7
  71. package/docs/SPACING.md +35 -5
  72. package/docs/TOKENS.md +1 -1
  73. package/docs/data-display/data-table/index.tsx +44 -3
  74. package/docs/data-display/popover.tsx +4 -6
  75. package/docs/data-display/scroll-area.tsx +51 -45
  76. package/docs/data-display/table.tsx +1 -1
  77. package/docs/data-entry/date-picker.tsx +2 -3
  78. package/docs/data-entry/input-otp.tsx +5 -2
  79. package/docs/data-entry/segmented-in-filter-row.tsx +77 -0
  80. package/docs/data-entry/segmented.tsx +40 -1
  81. package/docs/data-entry/select-matrix.tsx +1 -2
  82. package/docs/data-entry/select.tsx +108 -3
  83. package/docs/data-entry/slider.tsx +125 -47
  84. package/docs/feedback/dialog.tsx +1 -2
  85. package/docs/feedback/sheet.tsx +1 -2
  86. package/docs/layout/app-shell-states.tsx +143 -0
  87. package/docs/navigation/dropdown-menu.tsx +77 -6
  88. package/docs/navigation/tabs.tsx +72 -0
  89. package/docs/roadmap/ai-chat-components.md +1 -2
  90. package/docs/roadmap/parity-audit-data-entry.md +44 -33
  91. package/docs/roadmap/parity-audit-layout-navigation-general.md +304 -310
  92. package/docs/roadmap/parity-backlog.md +2 -3
  93. package/docs/roadmap/tree-components.md +0 -1
  94. package/package.json +25 -55
  95. package/scripts/_agent-setup.mjs +18 -0
  96. package/scripts/guinea-pig-skill.md +12 -6
  97. package/scripts/ui-audit.mjs +185 -7
  98. package/scripts/visual-audit-rules.mjs +0 -7
  99. package/scripts/visual-audit.mjs +6 -27
  100. package/dist/components/navigation/context-menu.d.ts +0 -21
  101. package/dist/components/navigation/context-menu.js +0 -149
  102. package/dist/components/navigation/menubar.d.ts +0 -28
  103. package/dist/components/navigation/menubar.js +0 -136
  104. package/dist/components/navigation/navigation-menu.d.ts +0 -10
  105. package/dist/components/navigation/navigation-menu.js +0 -91
  106. package/dist/components/ui/context-menu.d.ts +0 -1
  107. package/dist/components/ui/context-menu.js +0 -2
  108. package/dist/components/ui/menubar.d.ts +0 -1
  109. package/dist/components/ui/menubar.js +0 -2
  110. package/dist/components/ui/navigation-menu.d.ts +0 -1
  111. package/dist/components/ui/navigation-menu.js +0 -2
  112. package/docs/FRAME-A11Y-CI.md +0 -349
  113. package/docs/navigation/context-menu.tsx +0 -128
  114. package/docs/navigation/menubar.tsx +0 -141
  115. package/docs/navigation/navigation-menu.tsx +0 -158
package/README.md CHANGED
@@ -55,7 +55,7 @@ A value is defined **once** as a CSS var (`--primary`), mapped to a utility in t
55
55
  | **Data Display** | `@godxjp/ui/data-display` | `Table`, `DataTable`, `Card`, `StatCard`, `Badge`, `Avatar`, `Descriptions`, `Timeline`, `EmptyState`, `Progress`, `QrCode`, `CredentialReveal`, `Accordion`, `HoverCard`, `Carousel`, `Popover`, `Collapsible` |
56
56
  | **Feedback** | `@godxjp/ui/feedback` | `Dialog`, `AlertDialog`, `Sheet` (side), `Drawer` (bottom-sheet), `Toast`, `Skeleton`, `Alert`, `Tooltip` |
57
57
  | **Query** | `@godxjp/ui/query` | `DataState`, `InfiniteQueryState`, `PrefetchLink` (adapter subpath — pulls TanStack Query) |
58
- | **Navigation** | `@godxjp/ui/navigation` | `Tabs`, `Toolbar`, `DropdownMenu`, `ContextMenu`, `Menubar`, `NavigationMenu`, `Steps`, `Pagination`, `Breadcrumb`, `AppSettingPicker` |
58
+ | **Navigation** | `@godxjp/ui/navigation` | `Tabs`, `Toolbar`, `DropdownMenu`, `Steps`, `Pagination`, `Breadcrumb`, `AppSettingPicker` |
59
59
  | **App** | `@godxjp/ui/app` | `AppProvider`, `useDateTime` (adapter — i18n/datetime singleton) |
60
60
  | **Datetime** | `@godxjp/ui/datetime` | `formatDate` (mandatory for display) |
61
61
  | **Form** | `@godxjp/ui/form` | `useZodForm`, `FormRoot` (adapter subpath — pulls react-hook-form) |
@@ -155,9 +155,9 @@ pnpm release --ui <patch|minor|major> --mcp <…|skip> # publish lib + MCP in
155
155
  | `check:mcp-orphans` | every public component HAS a catalog entry (catalog can't rot) |
156
156
  | `check:core-isolation` | the root export pulls no foreign runtime (adapters stay on subpaths) |
157
157
 
158
- ### Runtime visual audit (Playwright + axe-core)
158
+ ### Runtime visual audit (Playwright)
159
159
 
160
- `scripts/visual-audit.mjs` drives a **real browser** over a running app and runs axe-core plus computed-style heuristics (target size, OKLCH accent chroma, rendered emoji, mis-laid-out alerts) — catching what the static `pnpm audit` (source regexes) can't see. Playwright + `@axe-core/playwright` are **optional peers**, installed only by apps that run the audit.
160
+ `scripts/visual-audit.mjs` drives a **real browser** over a running app and runs computed-style heuristics (target size, OKLCH accent chroma, rendered emoji, mis-laid-out alerts) — catching what the static `pnpm audit` (source regexes) can't see. Playwright + `@axe-core/playwright` are **optional peers**, installed only by apps that run the audit.
161
161
 
162
162
  ```bash
163
163
  # from a consumer, against its running dev/preview server:
@@ -166,7 +166,7 @@ node node_modules/@godxjp/ui/scripts/visual-audit.mjs http://localhost:5173 --fo
166
166
  node node_modules/@godxjp/ui/scripts/visual-audit.mjs --strict http://localhost:5173 # CI gate
167
167
  ```
168
168
 
169
- **Tested peer range** (pin one of these): `playwright >=1.55 <2` (tested 1.61.1) · `@axe-core/playwright >=4.10 <5` (tested 4.12.1) · `axe-core >=4.10 <5` (tested 4.12.1). Playwright 1.55+ is required for the `browser.newContext()` → `context.newPage()` flow axe expects; older `browser.newPage()` throws _"Please use browser.newContext()"_.
169
+ **Tested peer range** (pin one of these): `playwright >=1.55 <2` (tested 1.61.1)
170
170
 
171
171
  `--format json` **always** emits valid JSON — even on bootstrap failure — with a `status` (`ok` · `partial` · `error`) that separates **infrastructure errors** (missing peers, page won't load, axe won't inject → `errors[]`, `summary: null`/flagged) from **product findings** (`findings[]`). A tool failure can therefore never be misread as "zero violations". `pnpm check:visual-audit` is the CI smoke test: it serves a fixture page tripping all five rule families and asserts each one fires.
172
172
 
@@ -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
  }
@@ -1,6 +1,26 @@
1
1
  import * as React from "react";
2
- import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
3
2
  import type { ScrollAreaProp } from "../../props/components/data-display.prop.js";
4
- export type ScrollAreaProps = ScrollAreaProp & Omit<React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>, keyof ScrollAreaProp>;
5
- export declare const ScrollArea: React.ForwardRefExoticComponent<ScrollAreaProp & Omit<Omit<ScrollAreaPrimitive.ScrollAreaProps & React.RefAttributes<HTMLDivElement>, "ref">, keyof ScrollAreaProp> & React.RefAttributes<HTMLDivElement>>;
6
- export declare const ScrollBar: React.ForwardRefExoticComponent<Omit<ScrollAreaPrimitive.ScrollAreaScrollbarProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
3
+ export type ScrollAreaProps = ScrollAreaProp & Omit<React.ComponentPropsWithoutRef<"div">, keyof ScrollAreaProp>;
4
+ export declare const ScrollArea: React.ForwardRefExoticComponent<ScrollAreaProp & Omit<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, keyof ScrollAreaProp> & React.RefAttributes<HTMLDivElement>>;
5
+ /**
6
+ * Deliberately NOT exported: a deprecated no-op must not grow the package's public type surface.
7
+ * Nothing was exported for it before either — it used to be typed straight off Radix.
8
+ */
9
+ type ScrollBarProps = {
10
+ orientation?: "vertical" | "horizontal";
11
+ className?: string;
12
+ };
13
+ /**
14
+ * @deprecated Renders nothing. Native scrolling has no separate bar element to mount: the browser
15
+ * draws the scrollbar for whichever axis overflows, and WHICH axes may overflow is
16
+ * `<ScrollArea orientation="vertical | horizontal | both">`.
17
+ *
18
+ * Under Radix, mounting a `ScrollBar` was what ENABLED its axis (Radix read `scrollbarXEnabled` /
19
+ * `scrollbarYEnabled` from these children and wrote the viewport's inline `overflowX`/`overflowY`
20
+ * from them), so `<ScrollBar orientation="horizontal" />` was load-bearing rather than decorative.
21
+ * It is kept as an inert export so existing call sites keep compiling; move the intent to
22
+ * `orientation` — `<ScrollArea orientation="both">` is the replacement for a vertical area that
23
+ * also mounted a horizontal bar.
24
+ */
25
+ export declare function ScrollBar(_props: ScrollBarProps): null;
26
+ export {};
@@ -1,8 +1,6 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useLayoutEffect } from "@react-aria/utils";
2
+ import { jsx } from "react/jsx-runtime";
4
3
  import * as React from "react";
5
- import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
6
4
  import { cn } from "../../lib/utils.js";
7
5
  const ANCHOR_OFFSET_TOKEN = "--scroll-area-anchor-offset";
8
6
  const ANCHOR_OFFSET_FALLBACK_PX = 48;
@@ -113,18 +111,6 @@ function useBottomAnchor(viewport, enabled, offset, onAnchoredChange) {
113
111
  };
114
112
  }, [viewport, enabled, offset]);
115
113
  }
116
- function useAmbientDirection(root, provided) {
117
- const [ambient, setAmbient] = React.useState(void 0);
118
- useLayoutEffect(() => {
119
- if (provided !== void 0) return;
120
- const parent = root?.parentElement;
121
- if (!parent || typeof window === "undefined" || typeof window.getComputedStyle !== "function") {
122
- return;
123
- }
124
- setAmbient(window.getComputedStyle(parent).direction === "rtl" ? "rtl" : "ltr");
125
- }, [root, provided]);
126
- return provided ?? ambient;
127
- }
128
114
  const ScrollArea = React.forwardRef(
129
115
  ({
130
116
  className,
@@ -134,75 +120,51 @@ const ScrollArea = React.forwardRef(
134
120
  anchorOffset,
135
121
  onAnchoredChange,
136
122
  orientation = "vertical",
137
- dir,
138
123
  ...props
139
124
  }, ref) => {
140
125
  const [viewport, setViewport] = React.useState(null);
141
- const [root, setRoot] = React.useState(null);
142
- const attachRoot = React.useCallback(
143
- (node) => {
144
- setRoot(node);
145
- if (typeof ref === "function") ref(node);
146
- else if (ref) ref.current = node;
147
- },
148
- [ref]
149
- );
150
- const direction = useAmbientDirection(root, dir);
151
126
  const attachViewport = React.useCallback(
152
127
  (node) => {
153
128
  setViewport(node);
154
- if (typeof viewportRef === "function") viewportRef(node);
155
- else if (viewportRef)
156
- viewportRef.current = node;
129
+ for (const target of [ref, viewportRef]) {
130
+ if (typeof target === "function") target(node);
131
+ else if (target) target.current = node;
132
+ }
157
133
  },
158
- [viewportRef]
134
+ [ref, viewportRef]
159
135
  );
160
136
  useBottomAnchor(viewport, anchor === "bottom", anchorOffset, onAnchoredChange);
161
- return /* @__PURE__ */ jsxs(
162
- ScrollAreaPrimitive.Root,
163
- {
164
- ref: attachRoot,
165
- dir: direction,
166
- className: cn("relative overflow-hidden", className),
167
- ...props,
168
- children: [
169
- /* @__PURE__ */ jsx(
170
- ScrollAreaPrimitive.Viewport,
171
- {
172
- ref: attachViewport,
173
- tabIndex: 0,
174
- "data-slot": "scroll-area-viewport",
175
- "data-anchor": anchor,
176
- "data-orientation": orientation,
177
- className: "size-full rounded-[inherit]",
178
- children
179
- }
180
- ),
181
- orientation !== "horizontal" && /* @__PURE__ */ jsx(ScrollBar, { orientation: "vertical" }),
182
- orientation !== "vertical" && /* @__PURE__ */ jsx(ScrollBar, { orientation: "horizontal" }),
183
- /* @__PURE__ */ jsx(ScrollAreaPrimitive.Corner, {})
184
- ]
185
- }
137
+ return (
138
+ // `tabIndex={0}` keeps the scroll viewport keyboard-reachable so overflowing content can be
139
+ // scrolled without a pointer (WCAG 2.1.1 / axe scrollable-region-focusable).
140
+ //
141
+ // NO `dir` IS STAMPED HERE, deliberately. Radix's Root called `useDirection(dir)`, which
142
+ // falls back to the literal "ltr" and writes it onto the element — and a `dir` attribute is
143
+ // not advisory: it resets the inline axis for the whole subtree, so every `margin-inline-*`,
144
+ // `text-align: start` and `align-items: flex-end` inside resolved LTR on an RTL page
145
+ // (measured in Chromium: a chat feed inside `dir="rtl"` kept its own bubbles on the wrong
146
+ // side). A plain element inherits the page's direction, which is the correct answer and
147
+ // needs no code. A caller may still pass `dir` explicitly; it rides through with the rest.
148
+ /* @__PURE__ */ jsx(
149
+ "div",
150
+ {
151
+ ref: attachViewport,
152
+ tabIndex: 0,
153
+ "data-slot": "scroll-area-viewport",
154
+ "data-anchor": anchor,
155
+ "data-orientation": orientation,
156
+ className: cn("ui-scroll-area", className),
157
+ ...props,
158
+ children: /* @__PURE__ */ jsx("div", { "data-slot": "scroll-area-content", className: "ui-scroll-area-content", children })
159
+ }
160
+ )
186
161
  );
187
162
  }
188
163
  );
189
- ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
190
- const ScrollBar = React.forwardRef(({ className, orientation = "vertical", ...props }, ref) => /* @__PURE__ */ jsx(
191
- ScrollAreaPrimitive.ScrollAreaScrollbar,
192
- {
193
- ref,
194
- orientation,
195
- className: cn(
196
- "ui-scroll-area-bar flex touch-none transition-colors select-none",
197
- orientation === "vertical" && "ui-scroll-area-bar--vertical h-full border-s border-s-transparent",
198
- orientation === "horizontal" && "ui-scroll-area-bar--horizontal flex-col border-t border-t-transparent",
199
- className
200
- ),
201
- ...props,
202
- children: /* @__PURE__ */ jsx(ScrollAreaPrimitive.ScrollAreaThumb, { className: "ui-scroll-area-thumb bg-border relative flex-1" })
203
- }
204
- ));
205
- ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
164
+ ScrollArea.displayName = "ScrollArea";
165
+ function ScrollBar(_props) {
166
+ return null;
167
+ }
206
168
  export {
207
169
  ScrollArea,
208
170
  ScrollBar
@@ -13,7 +13,7 @@ import {
13
13
  resolveAriaInvalid
14
14
  } from "./control-surface.js";
15
15
  import { Popover, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
16
- import { ScrollArea, ScrollBar } from "../data-display/scroll-area.js";
16
+ import { ScrollArea } from "../data-display/scroll-area.js";
17
17
  import { Command, CommandInput } from "./command.js";
18
18
  import {
19
19
  filterTreeOptions,
@@ -311,8 +311,11 @@ function Cascader({
311
311
  if (multiple) commitMulti([]);
312
312
  else commitSingle([]);
313
313
  };
314
- const renderCascadeColumns = () => /* @__PURE__ */ jsxs(ScrollArea, { className: "w-full", children: [
315
- /* @__PURE__ */ jsx("div", { className: "ui-cascader-columns", children: columns.map((col, colIndex) => /* @__PURE__ */ jsx(
314
+ const renderCascadeColumns = () => (
315
+ // `both`: the columns strip overflows sideways, and a tall column scrolls down. Under Radix
316
+ // this was a vertical area plus a mounted `<ScrollBar orientation="horizontal" />`, which was
317
+ // how Radix was told to open the second axis at all.
318
+ /* @__PURE__ */ jsx(ScrollArea, { className: "w-full", orientation: "both", children: /* @__PURE__ */ jsx("div", { className: "ui-cascader-columns", children: columns.map((col, colIndex) => /* @__PURE__ */ jsx(
316
319
  "ul",
317
320
  {
318
321
  role: "listbox",
@@ -371,9 +374,8 @@ function Cascader({
371
374
  })
372
375
  },
373
376
  colIndex
374
- )) }),
375
- /* @__PURE__ */ jsx(ScrollBar, { orientation: "horizontal" })
376
- ] });
377
+ )) }) })
378
+ );
377
379
  const clearControl = resolveAllowClear(allowClear, true, t("dataEntry.cascader.clear"));
378
380
  const showClear = clearControl.enabled && hasValue && !disabled && !readOnly && !loading;
379
381
  const surface = controlSurfaceAttrs({ variant, status, size });
@@ -6,6 +6,7 @@ import { Check, Minus } from "lucide-react";
6
6
  import { cn } from "../../lib/utils.js";
7
7
  import { useFieldIdentity } from "../../lib/field-a11y.js";
8
8
  import { CheckboxGroup } from "./checkbox-group.js";
9
+ import { withOwnHitTarget } from "./choice-hit-target.js";
9
10
  function CheckboxGlyph({ state }) {
10
11
  return state === "indeterminate" ? /* @__PURE__ */ jsx(Minus, { className: "ui-checkbox-icon", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Check, { className: "ui-checkbox-icon", "aria-hidden": "true" });
11
12
  }
@@ -93,9 +94,10 @@ const CheckboxRoot = React.forwardRef((props, ref) => {
93
94
  // itself). react-aria's root is a `<label>`, which is `display:inline` — without this the
94
95
  // 16px box collapses to nothing. CheckboxVisual already carries the same three for the
95
96
  // same reason.
96
- "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",
97
98
  className
98
99
  ),
100
+ render: (domProps) => /* @__PURE__ */ jsx("label", { ...domProps, children: withOwnHitTarget(domProps.children) }),
99
101
  onChange: (next) => {
100
102
  setUncontrolled(next);
101
103
  onCheckedChange?.(next);
@@ -0,0 +1,24 @@
1
+ import * as React from "react";
2
+ /**
3
+ * THE PAINTED BOX OF A CHOICE CONTROL IS ITS OWN HIT TARGET (gh#476).
4
+ *
5
+ * react-aria-components renders the real `<input>` inside a `VisuallyHidden` span, and that span
6
+ * carries its geometry as an INLINE style: `position:absolute; width:1px; height:1px;
7
+ * clip:rect(0 0 0 0); clip-path:inset(50%)`, pinned at the label's top-left. The thing a user
8
+ * sees and aims at is the `<label>` wrapped around it, so every pointer landing on the control
9
+ * hits the LABEL, and the input's own hit area is a 13x13 box at a different origin than the
10
+ * 16x16 one that is painted.
11
+ *
12
+ * Measured on `data-entry-checkbox` before this helper existed: of 49 points scanned across the
13
+ * box, **0** reached the input, 46 reached the label; Playwright `check()` / `uncheck()` without
14
+ * `force` timed out on `<label data-slot="checkbox"> intercepts pointer events`, the same check a
15
+ * browser applies to a real pointer. `force: true` passed, which is the tell: the control is
16
+ * functional, it is simply covered.
17
+ *
18
+ * An inline style can only be beaten with `!important`, so the WRAPPER is replaced instead of
19
+ * fought. The `render` prop hands over the DOM children react-aria built; the VisuallyHidden
20
+ * element among them becomes a `.ui-choice-input` span that control.css sizes to the painted box.
21
+ * The `<input>` itself — its props, its ref, its keyboard handling, its place in the a11y tree —
22
+ * is passed through untouched, so this is a geometry change and nothing else.
23
+ */
24
+ export declare function withOwnHitTarget(children: React.ReactNode): React.ReactNode;
@@ -0,0 +1,12 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { VisuallyHidden } from "react-aria-components";
4
+ function withOwnHitTarget(children) {
5
+ return React.Children.map(
6
+ children,
7
+ (child) => React.isValidElement(child) && child.type === VisuallyHidden ? /* @__PURE__ */ jsx("span", { "data-slot": "choice-input", className: "ui-choice-input", children: child.props.children }) : child
8
+ );
9
+ }
10
+ export {
11
+ withOwnHitTarget
12
+ };
@@ -59,3 +59,11 @@ export declare function applyMaxTagCount<T extends {
59
59
  omitted: T[];
60
60
  overflow: React.ReactNode | undefined;
61
61
  };
62
+ /**
63
+ * Split a run of text on antd's `tokenSeparators` — the contract behind "paste a column out of a
64
+ * spreadsheet and get one value per line".
65
+ *
66
+ * Shared by `TagInput` and the tags/multiple `Select`: they are the two controls that turn typing
67
+ * into several values, and two copies of this would be two answers to "what is a separator".
68
+ */
69
+ export declare function splitByTokenSeparators(text: string, separators: string[]): string[];
@@ -31,9 +31,18 @@ function applyMaxTagCount(items, maxTagCount, maxTagPlaceholder) {
31
31
  const overflow = typeof maxTagPlaceholder === "function" ? maxTagPlaceholder(omitted.map(({ value, label }) => ({ value, label }))) : maxTagPlaceholder;
32
32
  return { visible, omitted, overflow };
33
33
  }
34
+ function escapeForCharClass(character) {
35
+ return character.replace(/[\\\]^-]/g, "\\$&");
36
+ }
37
+ function splitByTokenSeparators(text, separators) {
38
+ if (!separators.length) return [text];
39
+ const pattern = new RegExp(`[${separators.map(escapeForCharClass).join("")}]`);
40
+ return text.split(pattern);
41
+ }
34
42
  export {
35
43
  applyMaxTagCount,
36
44
  controlSurfaceAttrs,
37
45
  resolveAllowClear,
38
- resolveAriaInvalid
46
+ resolveAriaInvalid,
47
+ splitByTokenSeparators
39
48
  };
@@ -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;
@@ -69,6 +69,7 @@ function FormField({
69
69
  [fieldKey, emitFieldNames]
70
70
  );
71
71
  const childProps = React.isValidElement(children) ? children.props : void 0;
72
+ const controlId = childProps?.id ?? resolvedId;
72
73
  const mergeIds = mergeAriaIds;
73
74
  const childWithA11y = isStatic ? (
74
75
  // read-only FormField row and a Descriptions value are indistinguishable when mixed. That
@@ -78,7 +79,7 @@ function FormField({
78
79
  // The label is associated via aria-labelledby (not <label for>): composite
79
80
  // controls (Radio.Group, checkbox lists, range pairs) have no labelable root,
80
81
  // and a dangling `for` triggers Chrome's "Incorrect use of <label>" issue.
81
- id: childProps?.id ?? resolvedId,
82
+ id: controlId,
82
83
  ...form?.disabled && childProps?.disabled !== false ? { disabled: true } : {},
83
84
  ...validationStatus === "validating" ? { "aria-busy": true } : {},
84
85
  // Read the child's own value first in BOTH cases: cloneElement
@@ -133,7 +134,7 @@ function FormField({
133
134
  "span",
134
135
  {
135
136
  onClick: () => {
136
- const el = document.getElementById(resolvedId);
137
+ const el = document.getElementById(controlId);
137
138
  if (!(el instanceof HTMLElement)) return;
138
139
  const focusable = el.matches(FOCUSABLE_SELECTOR) ? el : el.querySelector(FOCUSABLE_SELECTOR);
139
140
  (focusable ?? el).focus();
@@ -174,7 +175,7 @@ function FormField({
174
175
  t(`dataEntry.form.${validationStatus}`)
175
176
  ] }) : null,
176
177
  helperPlacement === "after" ? helperNode : null,
177
- 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
178
179
  ] })
179
180
  ]
180
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