@godxjp/ui 21.0.0 → 23.0.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 (154) hide show
  1. package/README.md +4 -4
  2. package/dist/app/app-provider.js +2 -1
  3. package/dist/components/data-display/badge.d.ts +28 -1
  4. package/dist/components/data-display/badge.js +2 -0
  5. package/dist/components/data-display/data-table.d.ts +18 -2
  6. package/dist/components/data-display/data-table.js +4 -0
  7. package/dist/components/data-display/index.d.ts +1 -1
  8. package/dist/components/data-display/popover.d.ts +19 -1
  9. package/dist/components/data-display/popover.js +9 -1
  10. package/dist/components/data-display/progress.d.ts +37 -0
  11. package/dist/components/data-display/progress.js +57 -5
  12. package/dist/components/data-display/scroll-area.d.ts +24 -4
  13. package/dist/components/data-display/scroll-area.js +34 -72
  14. package/dist/components/data-entry/cascader.js +8 -6
  15. package/dist/components/data-entry/checkbox.js +2 -0
  16. package/dist/components/data-entry/choice-hit-target.d.ts +24 -0
  17. package/dist/components/data-entry/choice-hit-target.js +12 -0
  18. package/dist/components/data-entry/control-surface.d.ts +8 -0
  19. package/dist/components/data-entry/control-surface.js +10 -1
  20. package/dist/components/data-entry/date-picker.d.ts +10 -3
  21. package/dist/components/data-entry/date-picker.js +503 -238
  22. package/dist/components/data-entry/form-field.d.ts +1 -1
  23. package/dist/components/data-entry/form-field.js +7 -3
  24. package/dist/components/data-entry/form.js +10 -1
  25. package/dist/components/data-entry/index.d.ts +0 -6
  26. package/dist/components/data-entry/index.js +0 -6
  27. package/dist/components/data-entry/label.d.ts +14 -1
  28. package/dist/components/data-entry/label.js +6 -1
  29. package/dist/components/data-entry/radio.d.ts +27 -6
  30. package/dist/components/data-entry/radio.js +89 -43
  31. package/dist/components/data-entry/search-select.js +226 -89
  32. package/dist/components/data-entry/select.d.ts +76 -14
  33. package/dist/components/data-entry/select.js +497 -163
  34. package/dist/components/data-entry/slider.d.ts +3 -11
  35. package/dist/components/data-entry/slider.js +582 -93
  36. package/dist/components/data-entry/switch.d.ts +6 -3
  37. package/dist/components/data-entry/switch.js +36 -11
  38. package/dist/components/layout/mobile-shell.d.ts +1 -1
  39. package/dist/components/layout/mobile-shell.js +2 -0
  40. package/dist/components/navigation/dropdown-menu.d.ts +11 -3
  41. package/dist/components/navigation/dropdown-menu.js +190 -15
  42. package/dist/components/navigation/index.d.ts +0 -3
  43. package/dist/components/navigation/index.js +0 -78
  44. package/dist/components/navigation/steps.d.ts +1 -1
  45. package/dist/components/navigation/steps.js +3 -1
  46. package/dist/components/navigation/tabs-scroll.d.ts +27 -0
  47. package/dist/components/navigation/tabs-scroll.js +52 -1
  48. package/dist/components/navigation/tabs.d.ts +2 -2
  49. package/dist/components/navigation/tabs.js +88 -23
  50. package/dist/components/ui/index.d.ts +0 -4
  51. package/dist/components/ui/index.js +0 -4
  52. package/dist/components/ui/input-otp.d.ts +1 -1
  53. package/dist/components/ui/segmented.d.ts +15 -1
  54. package/dist/components/ui/segmented.js +15 -6
  55. package/dist/components/ui/tag-input.js +3 -5
  56. package/dist/i18n/messages/en.json +3 -2
  57. package/dist/i18n/messages/ja.json +3 -2
  58. package/dist/i18n/messages/vi.json +3 -2
  59. package/dist/lib/datetime/picker-format.d.ts +18 -0
  60. package/dist/lib/datetime/picker-format.js +35 -1
  61. package/dist/lib/field-a11y.d.ts +12 -0
  62. package/dist/lib/field-a11y.js +14 -1
  63. package/dist/lib/select-options.d.ts +21 -0
  64. package/dist/lib/select-options.js +46 -0
  65. package/dist/props/components/data-display.prop.d.ts +16 -6
  66. package/dist/props/components/data-entry.prop.d.ts +503 -129
  67. package/dist/props/components/index.d.ts +2 -2
  68. package/dist/props/components/layout.prop.d.ts +12 -1
  69. package/dist/props/components/navigation.prop.d.ts +93 -1
  70. package/dist/props/registry.d.ts +79 -16
  71. package/dist/props/registry.js +85 -15
  72. package/dist/props/vocabulary/data.prop.d.ts +12 -0
  73. package/dist/props/vocabulary/index.d.ts +2 -2
  74. package/dist/props/vocabulary/layout.prop.d.ts +11 -0
  75. package/dist/styles/badge-layout.css +4 -0
  76. package/dist/styles/card-layout.css +6 -6
  77. package/dist/styles/control.css +220 -29
  78. package/dist/styles/data-display-layout.css +94 -23
  79. package/dist/styles/focus-ring.css +6 -4
  80. package/dist/styles/fonts.css +88 -4
  81. package/dist/styles/navigation-layout.css +36 -236
  82. package/dist/styles/shell-layout.css +149 -139
  83. package/dist/styles/table-layout.css +41 -3
  84. package/dist/tokens/components/control.css +4 -0
  85. package/dist/tokens/components/data-display.css +9 -5
  86. package/dist/tokens/components/navigation.css +9 -5
  87. package/dist/tokens/components/scroll-area.css +7 -0
  88. package/dist/tokens/components/segmented.css +0 -2
  89. package/dist/tokens/components/shell.css +2 -0
  90. package/dist/tokens/components/table.css +3 -0
  91. package/dist/tokens/foundation.css +7 -0
  92. package/docs/COMPONENTS.md +14 -14
  93. package/docs/CONSUMER-RULES.md +24 -1
  94. package/docs/CUSTOMER-THEMING.md +7 -1
  95. package/docs/FRAME-COVERAGE-REPORT.md +5 -11
  96. package/docs/TOKENS.md +65 -0
  97. package/docs/data-display/badge.tsx +37 -0
  98. package/docs/data-display/data-table/index.tsx +45 -1
  99. package/docs/data-display/popover.tsx +43 -5
  100. package/docs/data-display/progress.tsx +28 -0
  101. package/docs/data-display/scroll-area.tsx +51 -45
  102. package/docs/data-entry/calendar.tsx +2 -2
  103. package/docs/data-entry/date-picker.tsx +144 -5
  104. package/docs/data-entry/form/examples/invoice-form.tsx +2 -2
  105. package/docs/data-entry/form-field/examples/a11y-contract.tsx +3 -5
  106. package/docs/data-entry/form.tsx +4 -4
  107. package/docs/data-entry/input-otp.tsx +5 -2
  108. package/docs/data-entry/number-input.tsx +31 -0
  109. package/docs/data-entry/segmented.tsx +143 -2
  110. package/docs/data-entry/select-matrix.tsx +1 -2
  111. package/docs/data-entry/select.tsx +108 -3
  112. package/docs/data-entry/slider.tsx +125 -47
  113. package/docs/feedback/dialog.tsx +1 -2
  114. package/docs/feedback/sheet.tsx +1 -2
  115. package/docs/layout/app-shell-states.tsx +143 -0
  116. package/docs/layout/mobile-shell.tsx +5 -0
  117. package/docs/navigation/dropdown-menu.tsx +75 -4
  118. package/docs/navigation/tabs.tsx +149 -0
  119. package/docs/roadmap/antd-parity.md +35 -34
  120. package/docs/roadmap/parity-audit-data-entry.md +196 -172
  121. package/docs/roadmap/parity-audit-layout-navigation-general.md +304 -310
  122. package/docs/roadmap/parity-backlog.md +8 -10
  123. package/docs/showcase/acme-portal.tsx +7 -0
  124. package/package.json +28 -64
  125. package/scripts/ui-audit.mjs +18 -5
  126. package/scripts/visual-audit-rules.mjs +0 -7
  127. package/scripts/visual-audit.mjs +6 -27
  128. package/dist/components/data-entry/date-range-picker.d.ts +0 -11
  129. package/dist/components/data-entry/date-range-picker.js +0 -349
  130. package/dist/components/data-entry/month-picker.d.ts +0 -10
  131. package/dist/components/data-entry/month-picker.js +0 -241
  132. package/dist/components/data-entry/month-range-picker.d.ts +0 -10
  133. package/dist/components/data-entry/month-range-picker.js +0 -301
  134. package/dist/components/navigation/context-menu.d.ts +0 -21
  135. package/dist/components/navigation/context-menu.js +0 -149
  136. package/dist/components/navigation/menubar.d.ts +0 -21
  137. package/dist/components/navigation/menubar.js +0 -136
  138. package/dist/components/navigation/navigation-menu.d.ts +0 -10
  139. package/dist/components/navigation/navigation-menu.js +0 -91
  140. package/dist/components/ui/context-menu.d.ts +0 -1
  141. package/dist/components/ui/context-menu.js +0 -2
  142. package/dist/components/ui/date-range-picker.d.ts +0 -1
  143. package/dist/components/ui/date-range-picker.js +0 -2
  144. package/dist/components/ui/menubar.d.ts +0 -1
  145. package/dist/components/ui/menubar.js +0 -2
  146. package/dist/components/ui/navigation-menu.d.ts +0 -1
  147. package/dist/components/ui/navigation-menu.js +0 -2
  148. package/docs/FRAME-A11Y-CI.md +0 -349
  149. package/docs/data-entry/date-range-picker.tsx +0 -175
  150. package/docs/data-entry/month-picker.tsx +0 -101
  151. package/docs/data-entry/month-range-picker.tsx +0 -115
  152. package/docs/navigation/context-menu.tsx +0 -128
  153. package/docs/navigation/menubar.tsx +0 -141
  154. package/docs/navigation/navigation-menu.tsx +0 -158
@@ -67,10 +67,61 @@ function useKeepActiveTabVisible(listRef) {
67
67
  };
68
68
  }, [listRef]);
69
69
  }
70
+ function resolveHiddenTabValues(list, values) {
71
+ const scrollport = list.getBoundingClientRect();
72
+ const triggers = [...list.querySelectorAll('[role="tab"]')];
73
+ const hidden = [];
74
+ triggers.forEach((trigger, index) => {
75
+ const value = values[index];
76
+ if (value === void 0) return;
77
+ if (!isTabFullyVisible(scrollport, trigger.getBoundingClientRect())) hidden.push(value);
78
+ });
79
+ return hidden;
80
+ }
81
+ function useTabsOverflowValues(listRef, values, onChange) {
82
+ const onChangeRef = React.useRef(onChange);
83
+ React.useEffect(() => {
84
+ onChangeRef.current = onChange;
85
+ });
86
+ const key = values === void 0 ? null : values.join(" ");
87
+ React.useEffect(() => {
88
+ if (key === null) return void 0;
89
+ const list = listRef.current;
90
+ if (!list) return void 0;
91
+ const all = key === "" ? [] : key.split(" ");
92
+ let last = null;
93
+ const measure = () => {
94
+ const hidden = resolveHiddenTabValues(list, all);
95
+ const next = hidden.join(" ");
96
+ if (next === last) return;
97
+ last = next;
98
+ onChangeRef.current(hidden);
99
+ };
100
+ measure();
101
+ list.addEventListener("scroll", measure, { passive: true });
102
+ const cleanups = [
103
+ () => {
104
+ list.removeEventListener("scroll", measure);
105
+ }
106
+ ];
107
+ if (typeof ResizeObserver !== "undefined") {
108
+ const resizeObserver = new ResizeObserver(measure);
109
+ resizeObserver.observe(list);
110
+ cleanups.push(() => {
111
+ resizeObserver.disconnect();
112
+ });
113
+ }
114
+ return () => {
115
+ for (const cleanup of cleanups) cleanup();
116
+ };
117
+ }, [key, listRef]);
118
+ }
70
119
  export {
71
120
  isTabFullyVisible,
72
121
  prefersReducedMotion,
122
+ resolveHiddenTabValues,
73
123
  resolveTabScrollTarget,
74
124
  syncActiveTabIntoView,
75
- useKeepActiveTabVisible
125
+ useKeepActiveTabVisible,
126
+ useTabsOverflowValues
76
127
  };
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import type { TabItemProp, TabsProp } from "../../props/components/navigation.prop.js";
3
- export type { TabItemProp, TabsProp, TabsVariantProp, TabsPlacementProp, TabsExtraProp, } from "../../props/components/navigation.prop.js";
3
+ export type { TabItemProp, TabsProp, TabsVariantProp, TabsPlacementProp, TabsExtraProp, TabsOverflowProp, } from "../../props/components/navigation.prop.js";
4
4
  export type TabsOrientation = "vertical" | "horizontal";
5
5
  export type TabsItem = TabItemProp;
6
6
  export type TabsProps = Omit<React.ComponentPropsWithoutRef<"div">, "defaultValue" | "dir"> & TabsProp & {
@@ -8,7 +8,7 @@ export type TabsProps = Omit<React.ComponentPropsWithoutRef<"div">, "defaultValu
8
8
  dir?: "ltr" | "rtl";
9
9
  activationMode?: "manual" | "automatic";
10
10
  };
11
- export declare function Tabs({ className, orientation, items, value, defaultValue, onValueChange, activationMode, dir, variant, tabPlacement, size, centered, extra, destroyOnHidden, onEdit, addIcon, hideAdd, listClassName, contentClassName, children, ...props }: TabsProps): React.JSX.Element;
11
+ export declare function Tabs({ className, orientation, items, value, defaultValue, onValueChange, activationMode, dir, variant, tabPlacement, size, centered, extra, destroyOnHidden, onEdit, addIcon, hideAdd, closeIcon, onTabClick, overflow, listClassName, contentClassName, children, ...props }: TabsProps): React.JSX.Element;
12
12
  export declare const TabsList: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
13
13
  variant?: "default" | "line";
14
14
  /**
@@ -7,10 +7,16 @@ import {
7
7
  TabPanel as AriaTabPanel,
8
8
  Tabs as AriaTabs
9
9
  } from "react-aria-components";
10
- import { Plus, X } from "lucide-react";
10
+ import { MoreHorizontal, Plus, X } from "lucide-react";
11
11
  import { useTranslation } from "../../i18n/use-translation.js";
12
12
  import { cn } from "../../lib/utils.js";
13
- import { useKeepActiveTabVisible } from "./tabs-scroll.js";
13
+ import {
14
+ DropdownMenu,
15
+ DropdownMenuContent,
16
+ DropdownMenuItem,
17
+ DropdownMenuTrigger
18
+ } from "./dropdown-menu.js";
19
+ import { useKeepActiveTabVisible, useTabsOverflowValues } from "./tabs-scroll.js";
14
20
  const TabsFrameContext = React.createContext({
15
21
  orientation: "horizontal",
16
22
  selectionSuppressed: false
@@ -69,6 +75,9 @@ function Tabs({
69
75
  onEdit,
70
76
  addIcon,
71
77
  hideAdd,
78
+ closeIcon,
79
+ onTabClick,
80
+ overflow = "scroll",
72
81
  listClassName,
73
82
  contentClassName,
74
83
  children,
@@ -86,6 +95,19 @@ function Tabs({
86
95
  [resolvedOrientation, selectionSuppressed]
87
96
  );
88
97
  const editable = variant === "editable-card";
98
+ const collapsible = items != null && overflow === "menu";
99
+ const listRef = React.useRef(null);
100
+ const [hiddenValues, setHiddenValues] = React.useState([]);
101
+ const itemValues = React.useMemo(
102
+ () => collapsible ? items.map((item) => item.value) : void 0,
103
+ [collapsible, items]
104
+ );
105
+ const handleHiddenChange = React.useCallback((next) => {
106
+ setHiddenValues(
107
+ (prev) => prev.length === next.length && prev.every((value2, index) => value2 === next[index]) ? prev : next
108
+ );
109
+ }, []);
110
+ useTabsOverflowValues(listRef, itemValues, handleHiddenChange);
89
111
  const [mirroredValue, setMirroredValue] = React.useState(
90
112
  value ?? resolvedDefault
91
113
  );
@@ -101,8 +123,9 @@ function Tabs({
101
123
  [onValueChange]
102
124
  );
103
125
  const { start: extraStart, end: extraEnd } = resolveTabsExtra(extra);
126
+ const overflowItems = collapsible ? items.filter((item) => hiddenValues.includes(item.value)) : [];
104
127
  const showAdd = editable && !hideAdd && Boolean(onEdit);
105
- const needsBar = Boolean(extraStart || extraEnd || showAdd);
128
+ const needsBar = Boolean(extraStart || extraEnd || showAdd || collapsible);
106
129
  const card = variant === "card" || variant === "editable-card";
107
130
  const sizeTriggerClassName = cn(
108
131
  size === "sm" && "min-h-[var(--tabs-trigger-height-sm)] px-[var(--tabs-trigger-padding-x-sm)] text-[length:var(--tabs-trigger-font-size-sm)]",
@@ -112,6 +135,7 @@ function Tabs({
112
135
  const list = items ? /* @__PURE__ */ jsx(
113
136
  TabsList,
114
137
  {
138
+ ref: listRef,
115
139
  "data-slot": "tabs-list",
116
140
  variant: variant === "line" ? "line" : "default",
117
141
  className: cn(
@@ -136,12 +160,18 @@ function Tabs({
136
160
  // the same block inset — so the two construction paths now agree. A service that
137
161
  // wants the active bar parked back on the hairline raises `--tabs-indicator-offset`,
138
162
  // which exists for exactly that.
139
- variant === "line" && "my-[calc(-1_*_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))] h-auto w-full justify-start border-b px-[var(--tabs-list-line-space-inset)] py-[calc(var(--tabs-list-line-space-inset)_+_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))]",
163
+ // `w-full` is scoped to the HORIZONTAL axis. Unscoped it also applied when the root is
164
+ // vertical, where the strip is a COLUMN beside the panel: `width: 100%` made the strip
165
+ // claim the whole row and the panel collapsed to its own min-content floor. Measured at a
166
+ // 1232px root: strip 1133.72px (92%) / panel 90.28px (7%). And it is not a layer problem —
167
+ // tailwind-merge drops the base `w-fit` as a same-group conflict, so `w-fit` never reached
168
+ // the DOM at all. Scoping puts it back on the axis it was written for.
169
+ variant === "line" && "my-[calc(-1_*_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))] h-auto justify-start border-b px-[var(--tabs-list-line-space-inset)] py-[calc(var(--tabs-list-line-space-inset)_+_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))] data-[orientation=horizontal]:w-full",
140
170
  // CARD strip. The list keeps `data-variant="default"` on purpose (a hand-composed
141
171
  // <TabsList> must be unaffected), so the card face is selected from the ROOT — but the
142
172
  // three properties the base list already claims as utilities (`bg-muted`, `p-1`,
143
173
  // `rounded-lg`) have to be replaced with utilities too, or the components layer loses.
144
- card && "w-full items-end justify-start gap-[var(--tabs-card-list-space-gap)] rounded-[var(--tabs-card-list-radius)] p-[var(--tabs-card-list-space-inset)] data-[variant=default]:bg-transparent",
174
+ card && "items-end justify-start gap-[var(--tabs-card-list-space-gap)] rounded-[var(--tabs-card-list-radius)] p-[var(--tabs-card-list-space-inset)] data-[orientation=horizontal]:w-full data-[variant=default]:bg-transparent",
145
175
  // CENTERED. Two independent moves, because the strip has two shapes: the pill/card strip
146
176
  // is `w-fit` (auto inline margins centre the BOX) and the line strip is `w-full`
147
177
  // (`justify-content` centres its CONTENT). `safe` keeps the overflow rule the strip
@@ -157,6 +187,7 @@ function Tabs({
157
187
  value: item.value,
158
188
  disabled: item.disabled,
159
189
  "aria-keyshortcuts": removable ? "Delete" : void 0,
190
+ onClick: onTabClick ? (event) => onTabClick(item.value, event) : void 0,
160
191
  onKeyDown: removable ? (event) => {
161
192
  if (event.key !== "Delete" && event.key !== "Backspace") return;
162
193
  event.preventDefault();
@@ -220,7 +251,7 @@ function Tabs({
220
251
  event.stopPropagation();
221
252
  onEdit?.(item.value, "remove");
222
253
  },
223
- children: item.closeIcon ?? /* @__PURE__ */ jsx(X, { className: "ui-tabs-tab-remove-icon", "aria-hidden": "true" })
254
+ children: item.closeIcon ?? closeIcon ?? /* @__PURE__ */ jsx(X, { className: "ui-tabs-tab-remove-icon", "aria-hidden": "true" })
224
255
  }
225
256
  )
226
257
  ) : null
@@ -242,8 +273,8 @@ function Tabs({
242
273
  "data-centered": centered ? "true" : void 0,
243
274
  orientation: resolvedOrientation,
244
275
  keyboardActivation: activationMode,
245
- selectedKey: value,
246
- defaultSelectedKey: value === void 0 ? resolvedDefault : void 0,
276
+ selectedKey: value ?? (collapsible ? mirroredValue : void 0),
277
+ defaultSelectedKey: value === void 0 && !collapsible ? resolvedDefault : void 0,
247
278
  onSelectionChange: (key) => handleValueChange(String(key)),
248
279
  className: cn(
249
280
  // Structure only. The paint (and the placement flip, which is `order`/`flex-direction`)
@@ -281,20 +312,42 @@ function Tabs({
281
312
  children: addIcon ?? /* @__PURE__ */ jsx(Plus, { className: "ui-tabs-add-icon", "aria-hidden": "true" })
282
313
  }
283
314
  ) : null,
315
+ overflowItems.length > 0 ? /* @__PURE__ */ jsxs(DropdownMenu, { children: [
316
+ /* @__PURE__ */ jsx(
317
+ DropdownMenuTrigger,
318
+ {
319
+ className: "ui-tabs-overflow",
320
+ "aria-label": t("navigation.tabs.moreTabs"),
321
+ children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "ui-tabs-overflow-icon", "aria-hidden": "true" })
322
+ }
323
+ ),
324
+ /* @__PURE__ */ jsx(DropdownMenuContent, { placement: "bottomEnd", children: overflowItems.map((item) => /* @__PURE__ */ jsx(
325
+ DropdownMenuItem,
326
+ {
327
+ disabled: item.disabled,
328
+ onSelect: () => handleValueChange(item.value),
329
+ children: item.label
330
+ },
331
+ item.value
332
+ )) })
333
+ ] }) : null,
284
334
  extraEnd ? /* @__PURE__ */ jsx("div", { "data-slot": "tabs-extra", "data-side": "end", className: "ui-tabs-extra", children: extraEnd }) : null
285
335
  ] }) : list,
286
- items.map((item) => /* @__PURE__ */ jsx(
287
- TabsContent,
288
- {
289
- value: item.value,
290
- "data-slot": "tabs-panel",
291
- forceMount: destroyOnHidden ? void 0 : true,
292
- hidden: destroyOnHidden ? void 0 : item.value !== activeValue,
293
- className: contentClassName,
294
- children: item.content
295
- },
296
- item.value
297
- ))
336
+ items.map((item) => {
337
+ const keepMounted = !destroyOnHidden || item.forceRender === true;
338
+ return /* @__PURE__ */ jsx(
339
+ TabsContent,
340
+ {
341
+ value: item.value,
342
+ "data-slot": "tabs-panel",
343
+ forceMount: keepMounted ? true : void 0,
344
+ hidden: keepMounted ? item.value !== activeValue : void 0,
345
+ className: contentClassName,
346
+ children: item.content
347
+ },
348
+ item.value
349
+ );
350
+ })
298
351
  ] }) : children
299
352
  }
300
353
  ) });
@@ -330,7 +383,7 @@ const TabsList = React.forwardRef(
330
383
  // — so the leading tab sat permanently outside the scrollport (an unreachable control at
331
384
  // 320px, WCAG 2.2 SC 2.1.1). `safe` falls back to start alignment exactly when it
332
385
  // overflows, and still centres whenever the tabs fit.
333
- "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-safe 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",
386
+ "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-safe 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-[var(--tabs-list-line-space-gap)] data-[variant=line]:rounded-none data-[variant=line]:bg-transparent [&[data-orientation=horizontal]::-webkit-scrollbar]:hidden",
334
387
  className
335
388
  ),
336
389
  render: (domProps) => withDomProps("div", "tabs-list", props, domProps, { "data-variant": variant }),
@@ -341,7 +394,7 @@ const TabsList = React.forwardRef(
341
394
  );
342
395
  TabsList.displayName = "TabsList";
343
396
  const TabsTrigger = React.forwardRef(
344
- ({ className, value, disabled, children, onKeyDown, ...props }, ref) => {
397
+ ({ className, value, disabled, children, onKeyDown, onClick, ...props }, ref) => {
345
398
  const { orientation, selectionSuppressed } = React.useContext(TabsFrameContext);
346
399
  return /* @__PURE__ */ jsx(
347
400
  AriaTab,
@@ -368,7 +421,7 @@ const TabsTrigger = React.forwardRef(
368
421
  // The line indicator lives in src/styles/navigation-layout.css so it reads --tabs-indicator-*.
369
422
  // Selected and focused stay visually distinct (WCAG 2.4.7): selected is a 1px hairline in the
370
423
  // border, focused is the 2px ring plus its halo outside it.
371
- "text-muted-foreground ring-offset-background hover:text-foreground ui-focus-ring data-[state=active]:bg-background data-[state=active]:text-foreground group-data-[variant=default]/tabs-list:data-[state=active]:border-primary/25 relative inline-flex flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=line]/tabs-list:border-e-0 group-data-[variant=line]/tabs-list:border-b-0 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none",
424
+ "text-muted-foreground ring-offset-background hover:text-foreground ui-focus-ring data-[state=active]:bg-background data-[state=active]:text-foreground group-data-[variant=default]/tabs-list:data-[state=active]:border-primary/25 relative inline-flex flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:flex-none group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=line]/tabs-list:border-e-0 group-data-[variant=line]/tabs-list:border-b-0 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none",
372
425
  className
373
426
  ),
374
427
  render: (domProps, renderProps) => withDomProps("button", "tabs-trigger", { type: "button", ...props }, domProps, {
@@ -379,6 +432,18 @@ const TabsTrigger = React.forwardRef(
379
432
  "aria-selected": renderProps.isSelected && !selectionSuppressed,
380
433
  "data-orientation": orientation,
381
434
  disabled,
435
+ // CHAINED, not spread — the same reason `onKeyDown` below is. `withDomProps` lays the
436
+ // caller's raw props down FIRST and RAC's merged ones second, so any handler RAC owns
437
+ // for this event would silently swallow the caller's. Caller first, then RAC's, and
438
+ // only while the caller has not called `preventDefault()`.
439
+ onClick: (event) => {
440
+ onClick?.(event);
441
+ if (!event.defaultPrevented) {
442
+ domProps.onClick?.(
443
+ event
444
+ );
445
+ }
446
+ },
382
447
  onKeyDown: (event) => {
383
448
  onKeyDown?.(event);
384
449
  if (!event.defaultPrevented) {
@@ -10,12 +10,8 @@ export * from "./card.js";
10
10
  export * from "./checkbox.js";
11
11
  export * from "./color-picker.js";
12
12
  export * from "./command.js";
13
- export * from "./context-menu.js";
14
- export * from "./menubar.js";
15
- export * from "./navigation-menu.js";
16
13
  export * from "./resizable.js";
17
14
  export * from "./date-picker.js";
18
- export * from "./date-range-picker.js";
19
15
  export * from "./dialog.js";
20
16
  export { Separator } from "./separator.js";
21
17
  export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../feedback/tooltip.js";
@@ -11,12 +11,8 @@ export * from "./card.js";
11
11
  export * from "./checkbox.js";
12
12
  export * from "./color-picker.js";
13
13
  export * from "./command.js";
14
- export * from "./context-menu.js";
15
- export * from "./menubar.js";
16
- export * from "./navigation-menu.js";
17
14
  export * from "./resizable.js";
18
15
  export * from "./date-picker.js";
19
- export * from "./date-range-picker.js";
20
16
  export * from "./dialog.js";
21
17
  import { Separator } from "./separator.js";
22
18
  import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../feedback/tooltip.js";
@@ -16,7 +16,7 @@ import * as React from "react";
16
16
  * too, and `input-otp` writes its own internal state inside its paste handler, which a wrapper
17
17
  * that only intercepted `onChange` could not undo.
18
18
  */
19
- export declare const InputOTP: React.ForwardRefExoticComponent<Omit<React.InputHTMLAttributes<HTMLInputElement>, "children" | "defaultValue" | "onChange" | "size" | "value"> & {
19
+ export declare const InputOTP: React.ForwardRefExoticComponent<Omit<React.InputHTMLAttributes<HTMLInputElement>, "children" | "defaultValue" | "onChange" | "size" | "style" | "value"> & {
20
20
  maxLength: number;
21
21
  value?: string;
22
22
  defaultValue?: string;
@@ -40,7 +40,7 @@ export type SegmentedProp = {
40
40
  export type SegmentedProps = SegmentedProp;
41
41
  /**
42
42
  * Segmented — one-of-N from a small, closed, always-visible set. The established enterprise
43
- * `Segmented` control (docs/DESIGN-AUTHORITY.md) drawn on Radix's RadioGroup,
43
+ * `Segmented` control (docs/DESIGN-AUTHORITY.md) drawn on react-aria-components' RadioGroup,
44
44
  * which is this repo's authority for behaviour primitives.
45
45
  *
46
46
  * WHY NOT `ToggleGroup`. A ToggleGroup is a row of PRESSED buttons: `aria-pressed`, independently
@@ -53,5 +53,19 @@ export type SegmentedProps = SegmentedProp;
53
53
  * The primitive gives roving tabindex, arrow-key traversal (RTL-aware), the radiogroup/radio roles
54
54
  * and the hidden input a native form submit needs. This file adds that geometry and nothing
55
55
  * else — the focus mark comes from `ui-focus-ring`, the ONE source in styles/focus-ring.css.
56
+ *
57
+ * "RTL-aware" IS true now, and it used not to be. On Radix it was: Radix read `dir` from its own
58
+ * `DirectionProvider`, this repo rendered none, and it never looked at `<html dir>` — measured in
59
+ * jsdom, with `document.documentElement.dir = "rtl"` ArrowLeft on the first option went to the
60
+ * LAST one, i.e. straight LTR traversal under a reversed layout, so the key that moves your eye
61
+ * left moved the selection right. The repair then was to hand Radix the ambient React Aria
62
+ * direction explicitly.
63
+ *
64
+ * On `react-aria-components` that hand-off is gone, because the primitive reads the SAME
65
+ * `useLocale()` itself. One source of truth, fed by `AppProvider`, nothing threaded through the
66
+ * call site — and `src/__tests__/rtl-arrow-direction.test.tsx` holds it: under an `ar-AE` locale
67
+ * ArrowLeft advances and ArrowRight retreats, and `<html dir="rtl">` alone still does not.
68
+ * `orientation` decides WHICH pair of arrows moves the focus; the locale decides which END of the
69
+ * row each of them means.
56
70
  */
57
71
  export declare const Segmented: React.ForwardRefExoticComponent<SegmentedProp & React.RefAttributes<HTMLDivElement>>;
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
4
+ import { Radio as AriaRadio, RadioGroup as AriaRadioGroup } from "react-aria-components";
5
+ import { withOwnHitTarget } from "../data-entry/choice-hit-target.js";
5
6
  import { cn } from "../../lib/utils.js";
6
7
  const Segmented = React.forwardRef(function Segmented2({
7
8
  options,
@@ -18,7 +19,7 @@ const Segmented = React.forwardRef(function Segmented2({
18
19
  ...props
19
20
  }, ref) {
20
21
  return /* @__PURE__ */ jsx(
21
- RadioGroupPrimitive.Root,
22
+ AriaRadioGroup,
22
23
  {
23
24
  ref,
24
25
  id,
@@ -29,17 +30,25 @@ const Segmented = React.forwardRef(function Segmented2({
29
30
  orientation: vertical ? "vertical" : "horizontal",
30
31
  value,
31
32
  defaultValue,
32
- onValueChange,
33
- disabled,
33
+ onChange: onValueChange,
34
+ isDisabled: disabled,
34
35
  name,
35
36
  ...props,
36
37
  children: options.map((option) => /* @__PURE__ */ jsxs(
37
- RadioGroupPrimitive.Item,
38
+ AriaRadio,
38
39
  {
39
40
  value: option.value,
40
- disabled: option.disabled,
41
+ isDisabled: option.disabled,
41
42
  "data-slot": "segmented-item",
42
43
  className: "ui-segmented-item ui-focus-ring",
44
+ render: (domProps, state) => /* @__PURE__ */ jsx(
45
+ "label",
46
+ {
47
+ ...domProps,
48
+ "data-state": state.isSelected ? "checked" : "unchecked",
49
+ children: withOwnHitTarget(domProps.children)
50
+ }
51
+ ),
43
52
  children: [
44
53
  option.icon == null ? null : /* @__PURE__ */ jsx(
45
54
  "span",
@@ -7,11 +7,9 @@ import { cn } from "../../lib/utils.js";
7
7
  import {
8
8
  applyMaxTagCount,
9
9
  controlSurfaceAttrs,
10
- resolveAllowClear
10
+ resolveAllowClear,
11
+ splitByTokenSeparators
11
12
  } from "../data-entry/control-surface.js";
12
- function escapeForCharClass(character) {
13
- return character.replace(/[\\\]^-]/g, "\\$&");
14
- }
15
13
  const TagInput = React.forwardRef(
16
14
  ({
17
15
  value,
@@ -51,7 +49,7 @@ const TagInput = React.forwardRef(
51
49
  };
52
50
  const addAll = (raw) => {
53
51
  let next = [...tags];
54
- const pieces = tokenSeparators.length ? raw.split(new RegExp(`[${tokenSeparators.map(escapeForCharClass).join("")}]`)) : [raw];
52
+ const pieces = splitByTokenSeparators(raw, tokenSeparators);
55
53
  for (const piece of pieces) {
56
54
  const tag = piece.trim();
57
55
  if (!tag || next.includes(tag)) continue;
@@ -132,7 +132,7 @@
132
132
  "openGrid": "Open month grid",
133
133
  "previousYear": "Previous year",
134
134
  "nextYear": "Next year",
135
- "placeholder": "yyyy/mm"
135
+ "placeholder": "Select month"
136
136
  },
137
137
  "branchScope": {
138
138
  "label": "Branch scope",
@@ -347,7 +347,8 @@
347
347
  "navigation": {
348
348
  "tabs": {
349
349
  "addTab": "Add tab",
350
- "removeTab": "Close tab"
350
+ "removeTab": "Close tab",
351
+ "moreTabs": "More tabs"
351
352
  },
352
353
  "steps": {
353
354
  "ariaLabel": "Progress",
@@ -132,7 +132,7 @@
132
132
  "openGrid": "月選択を開く",
133
133
  "previousYear": "前年へ",
134
134
  "nextYear": "翌年へ",
135
- "placeholder": "yyyy/mm"
135
+ "placeholder": "年月を選択"
136
136
  },
137
137
  "branchScope": {
138
138
  "label": "ブランチ範囲",
@@ -340,7 +340,8 @@
340
340
  "navigation": {
341
341
  "tabs": {
342
342
  "addTab": "タブを追加",
343
- "removeTab": "タブを閉じる"
343
+ "removeTab": "タブを閉じる",
344
+ "moreTabs": "他のタブ"
344
345
  },
345
346
  "steps": {
346
347
  "ariaLabel": "進行状況",
@@ -132,7 +132,7 @@
132
132
  "openGrid": "Mở chọn tháng",
133
133
  "previousYear": "Năm trước",
134
134
  "nextYear": "Năm sau",
135
- "placeholder": "yyyy/mm"
135
+ "placeholder": "Chọn tháng"
136
136
  },
137
137
  "branchScope": {
138
138
  "label": "Phạm vi chi nhánh",
@@ -341,7 +341,8 @@
341
341
  "navigation": {
342
342
  "tabs": {
343
343
  "addTab": "Thêm tab",
344
- "removeTab": "Đóng tab"
344
+ "removeTab": "Đóng tab",
345
+ "moreTabs": "Tab khác"
345
346
  },
346
347
  "steps": {
347
348
  "ariaLabel": "Tiến trình",
@@ -1,8 +1,26 @@
1
1
  import { type Locale } from "date-fns";
2
2
  import type { DatePickerBaseProp, PickerDateFormatProp } from "../../props/components/data-entry.prop.js";
3
+ /** The granularity axis a picker selects at — `DatePickerProp["picker"]`, resolved. */
4
+ export type DatePickerPicker = NonNullable<DatePickerBaseProp["picker"]>;
3
5
  /** Display is independent from the canonical value submitted by the field. */
4
6
  export declare function formatPickerDate(date: Date | undefined, format: PickerDateFormatProp | undefined, locale: string, withTime?: boolean): string;
5
7
  export declare function parsePickerDate(text: string, format: PickerDateFormatProp | undefined, parser?: (text: string) => Date | undefined, withTime?: boolean): Date | undefined;
6
8
  export declare function pickerDateAllowed(date: Date, min: Date | undefined, max: Date | undefined, disabled?: (date: Date) => boolean): boolean;
7
9
  /** Normalize a reporting period with the active calendar's week convention. */
8
10
  export declare function pickerPeriodStart(date: Date, picker: DatePickerBaseProp["picker"], locale: Pick<Locale, "options">): Date;
11
+ /**
12
+ * ISO-8601 at the precision the picker actually selects.
13
+ *
14
+ * The standard defines reduced forms, and a month picker that submits `2026/03` is simply using
15
+ * none of them: `/` is not an ISO separator and a server parsing an ISO date will reject it. Each
16
+ * granularity gets the shortest ISO form that loses nothing:
17
+ * year → `2026` · month → `2026-03` · quarter → `2026-01` (the quarter's first month, which is
18
+ * what the value IS) · week → `2026-W07` (ISO week-numbering year + week) · date → `2026-03-01`.
19
+ */
20
+ export declare function toIsoPeriod(date: Date | undefined, picker: DatePickerPicker): string;
21
+ /**
22
+ * The inverse of `toIsoPeriod`: read back the reduced ISO form the field displays for this
23
+ * granularity. `date` is left to `parsePickerDate`, which already owns the full `yyyy-MM-dd` path
24
+ * (custom parser, `format` pattern, era display).
25
+ */
26
+ export declare function parseIsoPeriod(raw: string, picker: DatePickerPicker): Date | undefined;
@@ -46,9 +46,43 @@ function pickerPeriodStart(date, picker, locale) {
46
46
  return date;
47
47
  }
48
48
  }
49
+ function toIsoPeriod(date, picker) {
50
+ if (!date || Number.isNaN(date.getTime())) return "";
51
+ switch (picker) {
52
+ case "year":
53
+ return String(date.getFullYear()).padStart(4, "0");
54
+ case "quarter":
55
+ case "month":
56
+ return toIsoDate(date).slice(0, 7);
57
+ case "week":
58
+ return formatDate(date, "RRRR-'W'II");
59
+ default:
60
+ return toIsoDate(date);
61
+ }
62
+ }
63
+ function parseIsoPeriod(raw, picker) {
64
+ const text = raw.trim();
65
+ if (picker === "year") {
66
+ const m = /^(\d{4})$/.exec(text);
67
+ return m ? new Date(Number(m[1]), 0, 1) : void 0;
68
+ }
69
+ if (picker === "month" || picker === "quarter") {
70
+ const m = /^(\d{4})-(\d{1,2})$/.exec(text);
71
+ if (!m) return void 0;
72
+ const month = Number(m[2]);
73
+ return month >= 1 && month <= 12 ? new Date(Number(m[1]), month - 1, 1) : void 0;
74
+ }
75
+ if (picker === "week") {
76
+ const parsed = parse(text, "RRRR-'W'II", new Date(2e3, 0, 1));
77
+ return isValid(parsed) ? parsed : void 0;
78
+ }
79
+ return void 0;
80
+ }
49
81
  export {
50
82
  formatPickerDate,
83
+ parseIsoPeriod,
51
84
  parsePickerDate,
52
85
  pickerDateAllowed,
53
- pickerPeriodStart
86
+ pickerPeriodStart,
87
+ toIsoPeriod
54
88
  };
@@ -51,6 +51,18 @@ export interface FieldNameContextValue {
51
51
  /** The label's plain-text content, when it is a string (belt-and-suspenders `aria-label`). */
52
52
  label?: string;
53
53
  }
54
+ /**
55
+ * Write attributes onto the real `<input>` react-aria hides inside a Checkbox / Radio / Switch.
56
+ *
57
+ * react-aria renders the painted control as a `<label>` and the FOCUS TARGET as a visually hidden
58
+ * `<input>` inside it — then strips every `data-*` off that input (`removeDataAttributes`) and
59
+ * forwards only the aria attributes its own props model knows about. So the two contracts this
60
+ * library puts on the semantic focus target — `data-field` (gh#337, the machine key automation
61
+ * addresses) and the aria state a control has no react-aria prop for — have to be written by hand.
62
+ *
63
+ * Deliberately NOT left on the root as well: one field must resolve to exactly one node.
64
+ */
65
+ export declare function useMirroredInputAttributes(inputRef: React.RefObject<HTMLInputElement | null>, attributes: Record<string, string | undefined>): void;
54
66
  export declare const FieldNameContext: React.Context<FieldNameContextValue | null>;
55
67
  /**
56
68
  * Resolve the last-resort accessible name for a control's semantic focus target (see
@@ -37,6 +37,18 @@ function resolveFieldA11y(props, intrinsicAriaLabel) {
37
37
  }
38
38
  return picked;
39
39
  }
40
+ function useMirroredInputAttributes(inputRef, attributes) {
41
+ const key = JSON.stringify(attributes);
42
+ React.useLayoutEffect(() => {
43
+ const input = inputRef.current;
44
+ if (!input) return;
45
+ const entries = JSON.parse(key);
46
+ for (const [name, value] of Object.entries(entries)) {
47
+ if (value === void 0) input.removeAttribute(name);
48
+ else input.setAttribute(name, value);
49
+ }
50
+ }, [inputRef, key]);
51
+ }
40
52
  const FieldNameContext = React.createContext(null);
41
53
  function useFieldNameFallback(name) {
42
54
  const field = React.useContext(FieldNameContext);
@@ -75,5 +87,6 @@ export {
75
87
  pickGroupFieldA11y,
76
88
  resolveFieldA11y,
77
89
  useFieldIdentity,
78
- useFieldNameFallback
90
+ useFieldNameFallback,
91
+ useMirroredInputAttributes
79
92
  };
@@ -0,0 +1,21 @@
1
+ import type { SearchSelectOptionProp, SelectFieldNamesProp, SelectOptionGroupProp } from "../props/components/data-entry.prop.js";
2
+ /**
3
+ * One list of rows, whatever shape the call site had.
4
+ *
5
+ * `Select` accepts three spellings of the same thing and they all have to arrive at the engine as
6
+ * ONE flat array, or every feature downstream (filtering, `maxCount`, the hidden `<option>` list)
7
+ * would need to know about all three:
8
+ *
9
+ * • this library's flat rows, `{ value, label, group? }`
10
+ * • antd's nested groups, `{ label, options: [...] }`
11
+ * • a foreign row read through `fieldNames`, `{ id, name, children }`
12
+ *
13
+ * Written once, here, rather than in both `select.tsx` and `search-select.tsx` — the two branches
14
+ * of one component must not disagree about what an option is. Group headings survive as the flat
15
+ * `group` string the rows already carried, so a grouped list renders identically whichever
16
+ * spelling produced it.
17
+ */
18
+ /** A row whose keys are not known ahead of time — what `fieldNames` exists to read. */
19
+ type RawOption = Record<string, unknown>;
20
+ export declare function normalizeSelectOptions(options: readonly (SearchSelectOptionProp | SelectOptionGroupProp | RawOption)[] | undefined, fieldNames?: SelectFieldNamesProp): SearchSelectOptionProp[];
21
+ export {};