@godxjp/ui 20.2.1 → 22.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 (141) hide show
  1. package/dist/app/app-provider.js +2 -1
  2. package/dist/components/data-display/badge.d.ts +28 -1
  3. package/dist/components/data-display/badge.js +2 -0
  4. package/dist/components/data-display/chat-bubble.d.ts +30 -0
  5. package/dist/components/data-display/chat-bubble.js +229 -0
  6. package/dist/components/data-display/data-table.d.ts +18 -2
  7. package/dist/components/data-display/data-table.js +4 -0
  8. package/dist/components/data-display/descriptions.js +4 -1
  9. package/dist/components/data-display/index.d.ts +5 -3
  10. package/dist/components/data-display/index.js +5 -2
  11. package/dist/components/data-display/popover.d.ts +19 -1
  12. package/dist/components/data-display/popover.js +12 -3
  13. package/dist/components/data-display/progress.d.ts +37 -0
  14. package/dist/components/data-display/progress.js +57 -5
  15. package/dist/components/data-display/scroll-area.js +26 -1
  16. package/dist/components/data-display/tree.d.ts +8 -0
  17. package/dist/components/data-display/tree.js +426 -0
  18. package/dist/components/data-entry/chat-composer.d.ts +50 -0
  19. package/dist/components/data-entry/chat-composer.js +163 -0
  20. package/dist/components/data-entry/chat-suggestion.d.ts +28 -0
  21. package/dist/components/data-entry/chat-suggestion.js +285 -0
  22. package/dist/components/data-entry/date-picker.d.ts +10 -3
  23. package/dist/components/data-entry/date-picker.js +503 -238
  24. package/dist/components/data-entry/form-field.d.ts +1 -1
  25. package/dist/components/data-entry/form-field.js +4 -1
  26. package/dist/components/data-entry/form.js +10 -1
  27. package/dist/components/data-entry/index.d.ts +4 -6
  28. package/dist/components/data-entry/index.js +4 -6
  29. package/dist/components/data-entry/label.d.ts +14 -1
  30. package/dist/components/data-entry/label.js +6 -1
  31. package/dist/components/data-entry/radio.d.ts +27 -6
  32. package/dist/components/data-entry/radio.js +88 -43
  33. package/dist/components/data-entry/search-select.js +8 -1
  34. package/dist/components/data-entry/select.js +2 -0
  35. package/dist/components/data-entry/switch.d.ts +6 -3
  36. package/dist/components/data-entry/switch.js +34 -11
  37. package/dist/components/data-entry/textarea.js +3 -1
  38. package/dist/components/data-entry/tree-utils.d.ts +10 -48
  39. package/dist/components/data-entry/tree-utils.js +1 -154
  40. package/dist/components/layout/mobile-shell.d.ts +1 -1
  41. package/dist/components/layout/mobile-shell.js +2 -0
  42. package/dist/components/navigation/menubar.d.ts +10 -3
  43. package/dist/components/navigation/steps.d.ts +1 -1
  44. package/dist/components/navigation/steps.js +3 -1
  45. package/dist/components/navigation/tabs-scroll.d.ts +27 -0
  46. package/dist/components/navigation/tabs-scroll.js +52 -1
  47. package/dist/components/navigation/tabs.d.ts +2 -2
  48. package/dist/components/navigation/tabs.js +88 -23
  49. package/dist/components/ui/index.d.ts +0 -1
  50. package/dist/components/ui/index.js +0 -1
  51. package/dist/components/ui/segmented.d.ts +15 -1
  52. package/dist/components/ui/segmented.js +13 -6
  53. package/dist/i18n/messages/en.json +48 -2
  54. package/dist/i18n/messages/ja.json +46 -2
  55. package/dist/i18n/messages/vi.json +47 -2
  56. package/dist/lib/datetime/picker-format.d.ts +18 -0
  57. package/dist/lib/datetime/picker-format.js +35 -1
  58. package/dist/lib/field-a11y.d.ts +12 -0
  59. package/dist/lib/field-a11y.js +14 -1
  60. package/dist/lib/tree.d.ts +53 -0
  61. package/dist/lib/tree.js +155 -0
  62. package/dist/props/components/data-display.prop.d.ts +194 -1
  63. package/dist/props/components/data-entry.prop.d.ts +307 -99
  64. package/dist/props/components/index.d.ts +1 -1
  65. package/dist/props/components/layout.prop.d.ts +12 -1
  66. package/dist/props/components/navigation.prop.d.ts +76 -1
  67. package/dist/props/registry.d.ts +156 -19
  68. package/dist/props/registry.js +218 -18
  69. package/dist/props/vocabulary/data.prop.d.ts +12 -0
  70. package/dist/props/vocabulary/index.d.ts +2 -2
  71. package/dist/props/vocabulary/layout.prop.d.ts +11 -0
  72. package/dist/styles/badge-layout.css +4 -0
  73. package/dist/styles/card-layout.css +6 -6
  74. package/dist/styles/control.css +22 -6
  75. package/dist/styles/data-display-layout.css +385 -68
  76. package/dist/styles/data-entry-layout.css +64 -0
  77. package/dist/styles/focus-ring.css +4 -2
  78. package/dist/styles/navigation-layout.css +25 -0
  79. package/dist/styles/shell-layout.css +7 -1
  80. package/dist/styles/table-layout.css +34 -0
  81. package/dist/tokens/base.css +3 -0
  82. package/dist/tokens/components/chat-bubble.css +36 -0
  83. package/dist/tokens/components/chat-composer.css +19 -0
  84. package/dist/tokens/components/data-display.css +9 -6
  85. package/dist/tokens/components/descriptions.css +4 -0
  86. package/dist/tokens/components/navigation.css +6 -0
  87. package/dist/tokens/components/segmented.css +0 -2
  88. package/dist/tokens/components/shell.css +3 -0
  89. package/dist/tokens/components/table.css +3 -0
  90. package/dist/tokens/components/tree.css +27 -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/FRAME-COVERAGE-LEDGER.md +1 -1
  95. package/docs/FRAME-COVERAGE-REPORT.md +9 -7
  96. package/docs/TOKENS.md +65 -0
  97. package/docs/data-display/badge.tsx +37 -0
  98. package/docs/data-display/chat-bubble.tsx +397 -0
  99. package/docs/data-display/data-table/index.tsx +18 -0
  100. package/docs/data-display/popover.tsx +41 -1
  101. package/docs/data-display/progress.tsx +28 -0
  102. package/docs/data-display/timeline.tsx +46 -0
  103. package/docs/data-display/tree.tsx +394 -0
  104. package/docs/data-entry/calendar.tsx +2 -2
  105. package/docs/data-entry/chat-composer.tsx +464 -0
  106. package/docs/data-entry/chat-suggestion.tsx +301 -0
  107. package/docs/data-entry/date-picker.tsx +143 -3
  108. package/docs/data-entry/form/examples/invoice-form.tsx +2 -2
  109. package/docs/data-entry/form-field/examples/a11y-contract.tsx +3 -5
  110. package/docs/data-entry/form.tsx +4 -4
  111. package/docs/data-entry/number-input.tsx +31 -0
  112. package/docs/data-entry/segmented.tsx +103 -1
  113. package/docs/layout/mobile-shell.tsx +5 -0
  114. package/docs/navigation/tabs.tsx +149 -0
  115. package/docs/roadmap/ai-chat-components.md +207 -0
  116. package/docs/roadmap/antd-parity.md +155 -0
  117. package/docs/roadmap/badge-tag-chip-count.md +172 -0
  118. package/docs/roadmap/list-masonry.md +159 -0
  119. package/docs/roadmap/parity-audit-data-display-feedback.md +567 -0
  120. package/docs/roadmap/parity-audit-data-entry.md +357 -0
  121. package/docs/roadmap/parity-audit-layout-navigation-general.md +464 -0
  122. package/docs/roadmap/parity-backlog.md +78 -0
  123. package/docs/roadmap/tree-components.md +151 -0
  124. package/docs/showcase/acme-portal.tsx +7 -0
  125. package/docs/showcase/table-tree-rows.tsx +4 -4
  126. package/package.json +13 -23
  127. package/scripts/ui-audit.mjs +17 -4
  128. package/dist/components/data-display/tree-list.d.ts +0 -13
  129. package/dist/components/data-display/tree-list.js +0 -26
  130. package/dist/components/data-entry/date-range-picker.d.ts +0 -11
  131. package/dist/components/data-entry/date-range-picker.js +0 -349
  132. package/dist/components/data-entry/month-picker.d.ts +0 -10
  133. package/dist/components/data-entry/month-picker.js +0 -241
  134. package/dist/components/data-entry/month-range-picker.d.ts +0 -10
  135. package/dist/components/data-entry/month-range-picker.js +0 -301
  136. package/dist/components/ui/date-range-picker.d.ts +0 -1
  137. package/dist/components/ui/date-range-picker.js +0 -2
  138. package/docs/data-display/tree-list.tsx +0 -107
  139. package/docs/data-entry/date-range-picker.tsx +0 -175
  140. package/docs/data-entry/month-picker.tsx +0 -101
  141. package/docs/data-entry/month-range-picker.tsx +0 -115
@@ -1,154 +1 @@
1
- function reactNodeText(value) {
2
- if (value == null || typeof value === "boolean") return "";
3
- if (typeof value === "string" || typeof value === "number" || typeof value === "bigint") {
4
- return String(value);
5
- }
6
- if (Array.isArray(value)) {
7
- return value.map((item) => reactNodeText(item)).join("");
8
- }
9
- return "";
10
- }
11
- function unknownText(value) {
12
- if (typeof value === "string" || typeof value === "number" || typeof value === "bigint") {
13
- return String(value);
14
- }
15
- return "";
16
- }
17
- function normalizeTreeOptions(nodes, fieldNames) {
18
- if (!nodes?.length) return [];
19
- const labelKey = fieldNames?.label ?? "label";
20
- const valueKey = fieldNames?.value ?? "value";
21
- const childrenKey = fieldNames?.children ?? "children";
22
- return nodes.map((node) => {
23
- const children = node[childrenKey];
24
- const value = unknownText(node[valueKey]);
25
- const label = node[labelKey];
26
- return {
27
- value,
28
- label: label ?? value,
29
- disabled: Boolean(node.disabled),
30
- disableCheckbox: Boolean(node.disableCheckbox),
31
- isLeaf: node.isLeaf,
32
- children: Array.isArray(children) ? normalizeTreeOptions(children, fieldNames) : void 0
33
- };
34
- });
35
- }
36
- function getNodeByPath(options, path) {
37
- const chain = [];
38
- let level = options;
39
- for (const segment of path) {
40
- const found = level.find((n) => n.value === segment);
41
- if (!found) break;
42
- chain.push(found);
43
- level = found.children ?? [];
44
- }
45
- return chain;
46
- }
47
- function getOptionsAtPath(options, path) {
48
- if (!path.length) return options;
49
- const chain = getNodeByPath(options, path);
50
- return chain.at(-1)?.children ?? [];
51
- }
52
- function formatPathLabels(chain, separator = " / ") {
53
- return chain.map((n) => reactNodeText(n.label)).join(separator);
54
- }
55
- function collectLeafPaths(options, prefix = [], root = options) {
56
- const out = [];
57
- for (const node of options) {
58
- const path = [...prefix, node.value];
59
- const hasChildren = (node.children?.length ?? 0) > 0;
60
- if (!hasChildren || node.isLeaf === true) {
61
- out.push({ path, labels: getNodeByPath(root, path).map((n) => reactNodeText(n.label)) });
62
- }
63
- if (hasChildren) out.push(...collectLeafPaths(node.children, path, root));
64
- }
65
- return out;
66
- }
67
- function collectAllPaths(options, prefix = [], root = options) {
68
- const out = [];
69
- for (const node of options) {
70
- const path = [...prefix, node.value];
71
- out.push({ path, labels: getNodeByPath(root, path).map((n) => reactNodeText(n.label)) });
72
- if (node.children?.length) out.push(...collectAllPaths(node.children, path, root));
73
- }
74
- return out;
75
- }
76
- function pathKey(path) {
77
- return path.join("\0");
78
- }
79
- function pathsEqual(a, b) {
80
- return a.length === b.length && a.every((v, i) => v === b[i]);
81
- }
82
- function filterTreeOptions(options, query, filter) {
83
- const q = query.trim().toLowerCase();
84
- if (!q) return [];
85
- const paths = collectLeafPaths(options);
86
- return paths.filter(({ path }) => {
87
- const chain = getNodeByPath(options, path);
88
- if (filter) return filter(query, chain);
89
- return chain.some((n) => reactNodeText(n.label).toLowerCase().includes(q));
90
- });
91
- }
92
- function getDescendantValues(node) {
93
- const values = [node.value];
94
- for (const child of node.children ?? []) values.push(...getDescendantValues(child));
95
- return values;
96
- }
97
- function flattenVisibleTree(options, expandedKeys, depth = 0) {
98
- const out = [];
99
- for (const node of options) {
100
- const hasChildren = (node.children?.length ?? 0) > 0 && node.isLeaf !== true;
101
- out.push({ node, depth, hasChildren });
102
- if (hasChildren && expandedKeys.has(node.value)) {
103
- out.push(...flattenVisibleTree(node.children, expandedKeys, depth + 1));
104
- }
105
- }
106
- return out;
107
- }
108
- function filterVisibleTree(options, query) {
109
- const q = query.trim().toLowerCase();
110
- if (!q) return flattenVisibleTree(options, new Set(collectAllExpandableKeys(options)));
111
- function matches(nodes, depth) {
112
- return nodes.flatMap((node) => {
113
- const children = node.isLeaf ? [] : matches(node.children ?? [], depth + 1);
114
- if (!reactNodeText(node.label).toLowerCase().includes(q) && children.length === 0) return [];
115
- return [{ node, depth, hasChildren: children.length > 0 }, ...children];
116
- });
117
- }
118
- return matches(options, 0);
119
- }
120
- function collectAllExpandableKeys(options) {
121
- const keys = [];
122
- for (const node of options) {
123
- if ((node.children?.length ?? 0) > 0 && node.isLeaf !== true) {
124
- keys.push(node.value);
125
- keys.push(...collectAllExpandableKeys(node.children));
126
- }
127
- }
128
- return keys;
129
- }
130
- function findNodeByValue(options, value) {
131
- for (const node of options) {
132
- if (node.value === value) return node;
133
- const nested = node.children ? findNodeByValue(node.children, value) : void 0;
134
- if (nested) return nested;
135
- }
136
- return void 0;
137
- }
138
- export {
139
- collectAllExpandableKeys,
140
- collectAllPaths,
141
- collectLeafPaths,
142
- filterTreeOptions,
143
- filterVisibleTree,
144
- findNodeByValue,
145
- flattenVisibleTree,
146
- formatPathLabels,
147
- getDescendantValues,
148
- getNodeByPath,
149
- getOptionsAtPath,
150
- normalizeTreeOptions,
151
- pathKey,
152
- pathsEqual,
153
- reactNodeText
154
- };
1
+ export * from "../../lib/tree.js";
@@ -18,4 +18,4 @@ export type { MobileShellProp, MobileShellProp as MobileShellProps, } from "../.
18
18
  * IS the app's primary navigation). They are the same strings in all three locales, and a fourth
19
19
  * identical copy under `layout.mobileShell.*` would be three files of duplication to keep in sync.
20
20
  */
21
- export declare function MobileShell({ statusBar, header, children, actions, tabBar, height, className, }: MobileShellProp): import("react").JSX.Element;
21
+ export declare function MobileShell({ statusBar, header, children, actions, tabBar, height, width, className, }: MobileShellProp): import("react").JSX.Element;
@@ -9,6 +9,7 @@ function MobileShell({
9
9
  actions,
10
10
  tabBar,
11
11
  height = "viewport",
12
+ width = "fill",
12
13
  className
13
14
  }) {
14
15
  const { t } = useTranslation();
@@ -17,6 +18,7 @@ function MobileShell({
17
18
  {
18
19
  "data-slot": "mobile-shell",
19
20
  "data-height": height === "viewport" ? void 0 : height,
21
+ "data-width": width === "fill" ? void 0 : width,
20
22
  className: cn("ui-mobile-shell", className),
21
23
  children: [
22
24
  (statusBar !== void 0 || header !== void 0) && /* @__PURE__ */ jsxs("header", { className: "ui-mobile-shell-chrome", "aria-label": t("layout.appShell.headerLabel"), children: [
@@ -1,9 +1,16 @@
1
1
  import * as React from "react";
2
2
  import * as MenubarPrimitive from "@radix-ui/react-menubar";
3
3
  export declare const Menubar: React.ForwardRefExoticComponent<MenubarPrimitive.MenubarProps & React.RefAttributes<HTMLDivElement>>;
4
- export declare const MenubarMenu: (props: MenubarPrimitive.MenubarMenuProps & {
5
- __scopeMenubar?: import("@radix-ui/react-context").Scope;
6
- }) => React.JSX.Element;
4
+ /**
5
+ * Annotated rather than inferred, and that is load-bearing.
6
+ *
7
+ * Radix types this one through `createContextScope`, so its INFERRED type reaches into
8
+ * `@radix-ui/react-context` for a `Scope` — and TypeScript then refuses to emit a declaration it
9
+ * cannot name portably (TS2883) unless that package is a DIRECT dependency. It was one, purely to
10
+ * satisfy this line: nothing in the tree ever imported it. Naming the props here severs that,
11
+ * which is what let the dependency go.
12
+ */
13
+ export declare const MenubarMenu: (props: React.ComponentProps<typeof MenubarPrimitive.Menu>) => React.ReactNode;
7
14
  export declare function MenubarTrigger(props: React.ComponentProps<typeof MenubarPrimitive.Trigger>): React.JSX.Element;
8
15
  export declare const MenubarContent: React.ForwardRefExoticComponent<Omit<MenubarPrimitive.MenubarContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
9
16
  export declare const MenubarItem: React.ForwardRefExoticComponent<Omit<MenubarPrimitive.MenubarItemProps & React.RefAttributes<HTMLDivElement>, "ref"> & {
@@ -1,4 +1,4 @@
1
1
  import * as React from "react";
2
2
  import type { StepsProp } from "../../props/components/navigation.prop.js";
3
3
  export type { StepsProp, StepsProp as StepsProps, StepItemProp, StepStatusProp, StepsSeparatorProp, StepsTypeProp, } from "../../props/components/navigation.prop.js";
4
- export declare function Steps({ items, value: current, defaultValue, status: currentStatus, orientation, type, size, titlePlacement, separator, percent, onValueChange, className, }: StepsProp): React.JSX.Element;
4
+ export declare function Steps({ items, value: current, defaultValue, status: currentStatus, orientation, type, size, titlePlacement, separator, percent, onValueChange, className, ...props }: StepsProp): React.JSX.Element;
@@ -50,7 +50,8 @@ function Steps({
50
50
  separator = "chevron",
51
51
  percent,
52
52
  onValueChange,
53
- className
53
+ className,
54
+ ...props
54
55
  }) {
55
56
  const { t } = useTranslation();
56
57
  const base = defaultValue;
@@ -69,6 +70,7 @@ function Steps({
69
70
  "data-type": type,
70
71
  className: cn("flex w-full", inline ? "ui-steps-inline" : "ui-steps-list", className),
71
72
  "aria-label": t("navigation.steps.ariaLabel"),
73
+ ...props,
72
74
  children: items.map((item, index) => {
73
75
  const absoluteIndex = base + index;
74
76
  const stepStatus = resolveStepStatus(absoluteIndex, current, item.status, currentStatus);
@@ -34,3 +34,30 @@ export declare function syncActiveTabIntoView(list: HTMLElement | null, behavior
34
34
  * pre-fix behaviour instead of throwing.
35
35
  */
36
36
  export declare function useKeepActiveTabVisible(listRef: React.RefObject<HTMLElement | null>): void;
37
+ /**
38
+ * The values of the triggers that are NOT fully inside the strip's scrollport, in document order.
39
+ *
40
+ * Pure apart from the two rect reads — `values` is the `items` array's own order, and the triggers
41
+ * are matched to it by INDEX rather than by DOM id: `TabsTrigger` hands its `value` to React Aria
42
+ * as the collection KEY, and the rendered `id` attribute is RAC-generated, so the element carries
43
+ * no readable handle back to the item it came from.
44
+ *
45
+ * This is the measurement behind `overflow="menu"`. It is separated from the hook below so it can
46
+ * be exercised on plain numbers — jsdom lays nothing out, so the only honest unit assertions about
47
+ * it are the ones that hand it real geometry.
48
+ */
49
+ export declare function resolveHiddenTabValues(list: HTMLElement, values: readonly string[]): string[];
50
+ /**
51
+ * Keeps `onChange` fed with the values currently out of the scrollport, recomputing on the two
52
+ * things that can move them: the strip resizing (a viewport change, a font swap, a tab added or
53
+ * removed) and the strip scrolling (the user swiping, or `useKeepActiveTabVisible` re-pinning a
54
+ * trigger).
55
+ *
56
+ * `values` is joined into the effect key rather than passed as a dependency, so a consumer that
57
+ * rebuilds its `items` array on every render does not re-arm the observers on every render.
58
+ *
59
+ * The first measurement ALWAYS reports, even when nothing is hidden. Suppressing it looks harmless
60
+ * and is not: on a remount the local `last` resets while the caller's state does not, so a strip
61
+ * that had overflowed and then stopped would keep a stale menu forever.
62
+ */
63
+ export declare function useTabsOverflowValues(listRef: React.RefObject<HTMLElement | null>, values: readonly string[] | undefined, onChange: (hidden: string[]) => void): void;
@@ -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) {
@@ -15,7 +15,6 @@ export * from "./menubar.js";
15
15
  export * from "./navigation-menu.js";
16
16
  export * from "./resizable.js";
17
17
  export * from "./date-picker.js";
18
- export * from "./date-range-picker.js";
19
18
  export * from "./dialog.js";
20
19
  export { Separator } from "./separator.js";
21
20
  export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../feedback/tooltip.js";
@@ -16,7 +16,6 @@ export * from "./menubar.js";
16
16
  export * from "./navigation-menu.js";
17
17
  export * from "./resizable.js";
18
18
  export * from "./date-picker.js";
19
- export * from "./date-range-picker.js";
20
19
  export * from "./dialog.js";
21
20
  import { Separator } from "./separator.js";
22
21
  import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../feedback/tooltip.js";
@@ -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,7 @@
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
5
  import { cn } from "../../lib/utils.js";
6
6
  const Segmented = React.forwardRef(function Segmented2({
7
7
  options,
@@ -18,7 +18,7 @@ const Segmented = React.forwardRef(function Segmented2({
18
18
  ...props
19
19
  }, ref) {
20
20
  return /* @__PURE__ */ jsx(
21
- RadioGroupPrimitive.Root,
21
+ AriaRadioGroup,
22
22
  {
23
23
  ref,
24
24
  id,
@@ -29,17 +29,24 @@ const Segmented = React.forwardRef(function Segmented2({
29
29
  orientation: vertical ? "vertical" : "horizontal",
30
30
  value,
31
31
  defaultValue,
32
- onValueChange,
33
- disabled,
32
+ onChange: onValueChange,
33
+ isDisabled: disabled,
34
34
  name,
35
35
  ...props,
36
36
  children: options.map((option) => /* @__PURE__ */ jsxs(
37
- RadioGroupPrimitive.Item,
37
+ AriaRadio,
38
38
  {
39
39
  value: option.value,
40
- disabled: option.disabled,
40
+ isDisabled: option.disabled,
41
41
  "data-slot": "segmented-item",
42
42
  className: "ui-segmented-item ui-focus-ring",
43
+ render: (domProps, state) => /* @__PURE__ */ jsx(
44
+ "label",
45
+ {
46
+ ...domProps,
47
+ "data-state": state.isSelected ? "checked" : "unchecked"
48
+ }
49
+ ),
43
50
  children: [
44
51
  option.icon == null ? null : /* @__PURE__ */ jsx(
45
52
  "span",