@thangnqdev/ui 0.1.2 → 0.2.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 (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +54 -206
  3. package/dist/bento/index.cjs.map +1 -1
  4. package/dist/bento/index.js +2 -2
  5. package/dist/bento/styles.css +2 -46
  6. package/dist/buttons/index.cjs +111 -18
  7. package/dist/buttons/index.cjs.map +1 -1
  8. package/dist/buttons/index.d.cts +1 -1
  9. package/dist/buttons/index.d.ts +1 -1
  10. package/dist/buttons/index.js +3 -3
  11. package/dist/buttons/styles.css +2 -6
  12. package/dist/{chunk-4S6L5V7X.js → chunk-2YJBW4E2.js} +2 -2
  13. package/dist/chunk-2YJBW4E2.js.map +1 -0
  14. package/dist/{chunk-F5Q7BTMH.js → chunk-4CRFNALF.js} +1011 -556
  15. package/dist/chunk-4CRFNALF.js.map +1 -0
  16. package/dist/{chunk-LWFRUJRU.js → chunk-4MS6ZHFQ.js} +225 -41
  17. package/dist/chunk-4MS6ZHFQ.js.map +1 -0
  18. package/dist/{chunk-2OXZK6IP.js → chunk-77KTYOMQ.js} +3 -3
  19. package/dist/chunk-77KTYOMQ.js.map +1 -0
  20. package/dist/{chunk-IABPUCAQ.js → chunk-ADO3SVUA.js} +2 -2
  21. package/dist/chunk-ADO3SVUA.js.map +1 -0
  22. package/dist/{chunk-GXI2H5XA.js → chunk-D7FB3TCA.js} +3 -3
  23. package/dist/chunk-D7FB3TCA.js.map +1 -0
  24. package/dist/chunk-DSEL7ELZ.js +314 -0
  25. package/dist/chunk-DSEL7ELZ.js.map +1 -0
  26. package/dist/{chunk-KJRBRTQE.js → chunk-EXFR2BFJ.js} +37 -28
  27. package/dist/chunk-EXFR2BFJ.js.map +1 -0
  28. package/dist/{chunk-PF4ZHGVW.js → chunk-GXWB6BSY.js} +116 -23
  29. package/dist/chunk-GXWB6BSY.js.map +1 -0
  30. package/dist/{chunk-66VWUJEI.js → chunk-I2FQBMVW.js} +3 -3
  31. package/dist/chunk-I2FQBMVW.js.map +1 -0
  32. package/dist/{chunk-EU2LPUYP.js → chunk-I5O2WCGF.js} +3 -3
  33. package/dist/chunk-I5O2WCGF.js.map +1 -0
  34. package/dist/{chunk-ATYGDTMK.js → chunk-ME4XILG5.js} +5 -5
  35. package/dist/chunk-ME4XILG5.js.map +1 -0
  36. package/dist/{chunk-5FZ6ZFII.js → chunk-PC5SZFZC.js} +373 -150
  37. package/dist/chunk-PC5SZFZC.js.map +1 -0
  38. package/dist/{chunk-C4SUXMI3.js → chunk-S6XJMYME.js} +3 -3
  39. package/dist/chunk-S6XJMYME.js.map +1 -0
  40. package/dist/{chunk-A7P3O5Z5.js → chunk-V52JOSDF.js} +3 -3
  41. package/dist/chunk-V52JOSDF.js.map +1 -0
  42. package/dist/{chunk-NFVONJUK.js → chunk-W5WNWTQC.js} +455 -211
  43. package/dist/chunk-W5WNWTQC.js.map +1 -0
  44. package/dist/{chunk-WAFHC4DQ.js → chunk-YJ64RI7J.js} +3 -3
  45. package/dist/chunk-YJ64RI7J.js.map +1 -0
  46. package/dist/chunk-YOCGBORV.js +155 -0
  47. package/dist/chunk-YOCGBORV.js.map +1 -0
  48. package/dist/chunk-ZEDXARXW.js +96 -0
  49. package/dist/chunk-ZEDXARXW.js.map +1 -0
  50. package/dist/components.css +2 -0
  51. package/dist/core/index.cjs +33 -24
  52. package/dist/core/index.cjs.map +1 -1
  53. package/dist/core/index.js +2 -2
  54. package/dist/core/styles.css +2 -134
  55. package/dist/data-display/index.cjs +770 -247
  56. package/dist/data-display/index.cjs.map +1 -1
  57. package/dist/data-display/index.d.cts +1 -1
  58. package/dist/data-display/index.d.ts +1 -1
  59. package/dist/data-display/index.js +5 -4
  60. package/dist/data-display/styles.css +2 -4
  61. package/dist/feedback/index.cjs.map +1 -1
  62. package/dist/feedback/index.js +2 -2
  63. package/dist/feedback/styles.css +2 -4
  64. package/dist/forms/index.cjs +1211 -459
  65. package/dist/forms/index.cjs.map +1 -1
  66. package/dist/forms/index.d.cts +1 -1
  67. package/dist/forms/index.d.ts +1 -1
  68. package/dist/forms/index.js +8 -8
  69. package/dist/forms/styles.css +2 -6
  70. package/dist/glass/index.cjs.map +1 -1
  71. package/dist/glass/index.js +2 -2
  72. package/dist/glass/styles.css +2 -81
  73. package/dist/{index-D6fE7w-b.d.cts → index-BGt_hecO.d.cts} +1 -0
  74. package/dist/{index-D6fE7w-b.d.ts → index-BGt_hecO.d.ts} +1 -0
  75. package/dist/{index-DzcWh1Hs.d.cts → index-BtDvpZCf.d.cts} +7 -0
  76. package/dist/{index-DzcWh1Hs.d.ts → index-BtDvpZCf.d.ts} +7 -0
  77. package/dist/{index-Cle6QzQl.d.ts → index-COX1sBUN.d.ts} +27 -3
  78. package/dist/{index-BkRqGNG1.d.ts → index-Cg8MuP7u.d.cts} +40 -13
  79. package/dist/{index-BkRqGNG1.d.cts → index-Cg8MuP7u.d.ts} +40 -13
  80. package/dist/{index-CenuF_P2.d.ts → index-DHkPRTvE.d.cts} +39 -8
  81. package/dist/{index-CenuF_P2.d.cts → index-DHkPRTvE.d.ts} +39 -8
  82. package/dist/{index-TyK-Ldpy.d.cts → index-Du_oht9k.d.cts} +27 -3
  83. package/dist/index.cjs +2481 -917
  84. package/dist/index.cjs.map +1 -1
  85. package/dist/index.d.cts +5 -5
  86. package/dist/index.d.ts +5 -5
  87. package/dist/index.js +19 -19
  88. package/dist/layout/index.cjs.map +1 -1
  89. package/dist/layout/index.js +2 -2
  90. package/dist/layout/styles.css +2 -8
  91. package/dist/navigation/index.cjs +288 -48
  92. package/dist/navigation/index.cjs.map +1 -1
  93. package/dist/navigation/index.d.cts +1 -1
  94. package/dist/navigation/index.d.ts +1 -1
  95. package/dist/navigation/index.js +6 -6
  96. package/dist/navigation/styles.css +2 -4
  97. package/dist/overlays/index.cjs +566 -229
  98. package/dist/overlays/index.cjs.map +1 -1
  99. package/dist/overlays/index.d.cts +1 -1
  100. package/dist/overlays/index.d.ts +1 -1
  101. package/dist/overlays/index.js +7 -7
  102. package/dist/overlays/styles.css +2 -8
  103. package/dist/reset.css +45 -0
  104. package/dist/styles.css +1 -1
  105. package/dist/tokens.css +11 -0
  106. package/dist/typography/index.cjs.map +1 -1
  107. package/dist/typography/index.js +4 -4
  108. package/dist/typography/styles.css +2 -5
  109. package/llms.txt +211 -113
  110. package/package.json +160 -116
  111. package/src/bento/BentoCard.tsx +140 -140
  112. package/src/bento/BentoGrid.tsx +50 -50
  113. package/src/bento/index.ts +2 -2
  114. package/src/bento/tokens.css +46 -46
  115. package/src/buttons/Button.tsx +104 -104
  116. package/src/buttons/ButtonGroup.tsx +43 -43
  117. package/src/buttons/IconButton.tsx +79 -79
  118. package/src/buttons/SplitButton.tsx +186 -86
  119. package/src/buttons/ToggleButton.tsx +157 -157
  120. package/src/buttons/index.ts +5 -5
  121. package/src/buttons/tokens.css +6 -6
  122. package/src/core/ThemeProvider.tsx +135 -123
  123. package/src/core/index.ts +2 -2
  124. package/src/core/tokens.css +134 -134
  125. package/src/data-display/Accordion.tsx +147 -147
  126. package/src/data-display/Avatar.tsx +134 -134
  127. package/src/data-display/Card.tsx +112 -112
  128. package/src/data-display/Chip.tsx +74 -74
  129. package/src/data-display/DataTablePro.tsx +299 -121
  130. package/src/data-display/StatCard.tsx +60 -60
  131. package/src/data-display/Table.tsx +74 -74
  132. package/src/data-display/Timeline.tsx +62 -62
  133. package/src/data-display/TreeView.tsx +243 -104
  134. package/src/data-display/tokens.css +4 -4
  135. package/src/feedback/Alert.tsx +80 -80
  136. package/src/feedback/EmptyState.tsx +45 -45
  137. package/src/feedback/Skeleton.tsx +66 -66
  138. package/src/feedback/Spinner.tsx +65 -65
  139. package/src/feedback/index.ts +5 -5
  140. package/src/feedback/tokens.css +4 -4
  141. package/src/forms/Checkbox.tsx +149 -69
  142. package/src/forms/CheckboxGroup.tsx +76 -29
  143. package/src/forms/Combobox.tsx +172 -28
  144. package/src/forms/DatePicker.tsx +385 -81
  145. package/src/forms/FileUpload.tsx +51 -7
  146. package/src/forms/FormField.tsx +115 -115
  147. package/src/forms/Input.tsx +151 -151
  148. package/src/forms/NumberInput.tsx +34 -20
  149. package/src/forms/PasswordInput.tsx +35 -35
  150. package/src/forms/PinInput.tsx +139 -125
  151. package/src/forms/Radio.tsx +142 -142
  152. package/src/forms/RangeSlider.tsx +237 -111
  153. package/src/forms/SearchInput.tsx +34 -34
  154. package/src/forms/Select.tsx +103 -103
  155. package/src/forms/Slider.tsx +85 -85
  156. package/src/forms/Textarea.tsx +114 -114
  157. package/src/forms/tokens.css +6 -6
  158. package/src/glass/GlassButton.tsx +59 -59
  159. package/src/glass/GlassCard.tsx +71 -71
  160. package/src/glass/index.ts +3 -3
  161. package/src/glass/tokens.css +81 -81
  162. package/src/index.ts +26 -26
  163. package/src/layout/AspectRatio.tsx +39 -39
  164. package/src/layout/Container.tsx +53 -53
  165. package/src/layout/Divider.tsx +61 -61
  166. package/src/layout/Flex.tsx +79 -79
  167. package/src/layout/Grid.tsx +124 -124
  168. package/src/layout/PageLayout.tsx +59 -59
  169. package/src/layout/Section.tsx +47 -47
  170. package/src/layout/Spacer.tsx +30 -30
  171. package/src/layout/Stack.tsx +88 -88
  172. package/src/layout/tokens.css +8 -8
  173. package/src/navigation/Breadcrumb.tsx +73 -73
  174. package/src/navigation/CommandPalette.tsx +142 -142
  175. package/src/navigation/DropdownMenu.tsx +244 -160
  176. package/src/navigation/Pagination.tsx +114 -114
  177. package/src/navigation/Stepper.tsx +93 -93
  178. package/src/navigation/Tabs.tsx +265 -133
  179. package/src/navigation/index.ts +6 -6
  180. package/src/navigation/tokens.css +4 -4
  181. package/src/overlays/AlertDialog.tsx +63 -54
  182. package/src/overlays/BottomSheet.tsx +30 -10
  183. package/src/overlays/ContextMenu.tsx +51 -9
  184. package/src/overlays/Drawer.tsx +160 -145
  185. package/src/overlays/FocusTrap.tsx +122 -49
  186. package/src/overlays/Modal.tsx +296 -219
  187. package/src/overlays/Popover.tsx +55 -25
  188. package/src/overlays/Portal.tsx +23 -23
  189. package/src/overlays/Toast.tsx +175 -137
  190. package/src/overlays/Tooltip.tsx +125 -100
  191. package/src/overlays/tokens.css +8 -8
  192. package/src/registry/components.ts +1368 -0
  193. package/src/styles/components.css +39 -0
  194. package/src/styles/index.css +2 -93
  195. package/src/styles/reset.css +45 -0
  196. package/src/styles/tokens.css +11 -2
  197. package/src/typography/Blockquote.tsx +32 -32
  198. package/src/typography/Code.tsx +89 -89
  199. package/src/typography/Heading.tsx +56 -56
  200. package/src/typography/Kbd.tsx +25 -25
  201. package/src/typography/Label.tsx +47 -47
  202. package/src/typography/Link.tsx +56 -56
  203. package/src/typography/Text.tsx +62 -62
  204. package/src/typography/index.ts +7 -7
  205. package/src/typography/tokens.css +5 -5
  206. package/src/utils/cn.ts +6 -6
  207. package/src/utils/scroll-lock.ts +19 -0
  208. package/src/utils/useControllableState.ts +54 -0
  209. package/ui-contract.json +192 -35
  210. package/dist/chunk-2OXZK6IP.js.map +0 -1
  211. package/dist/chunk-4S6L5V7X.js.map +0 -1
  212. package/dist/chunk-5FZ6ZFII.js.map +0 -1
  213. package/dist/chunk-66VWUJEI.js.map +0 -1
  214. package/dist/chunk-A7P3O5Z5.js.map +0 -1
  215. package/dist/chunk-ATYGDTMK.js.map +0 -1
  216. package/dist/chunk-C4SUXMI3.js.map +0 -1
  217. package/dist/chunk-EU2LPUYP.js.map +0 -1
  218. package/dist/chunk-F5Q7BTMH.js.map +0 -1
  219. package/dist/chunk-GXI2H5XA.js.map +0 -1
  220. package/dist/chunk-H3LCNPMC.js +0 -101
  221. package/dist/chunk-H3LCNPMC.js.map +0 -1
  222. package/dist/chunk-IABPUCAQ.js.map +0 -1
  223. package/dist/chunk-KJRBRTQE.js.map +0 -1
  224. package/dist/chunk-LBUBXUYQ.js +0 -40
  225. package/dist/chunk-LBUBXUYQ.js.map +0 -1
  226. package/dist/chunk-LWFRUJRU.js.map +0 -1
  227. package/dist/chunk-NFVONJUK.js.map +0 -1
  228. package/dist/chunk-PF4ZHGVW.js.map +0 -1
  229. package/dist/chunk-WAFHC4DQ.js.map +0 -1
  230. package/dist/chunk-X76KBH4Y.js +0 -71
  231. package/dist/chunk-X76KBH4Y.js.map +0 -1
@@ -19,29 +19,114 @@ function useTabs() {
19
19
  return context;
20
20
  }
21
21
  var Tabs = react.forwardRef(
22
- ({ defaultValue = "", value: controlledValue, onValueChange, variant = "pills", className, children, ...props }, ref) => {
22
+ ({
23
+ defaultValue = "",
24
+ value: controlledValue,
25
+ onValueChange,
26
+ variant = "pills",
27
+ orientation = "horizontal",
28
+ className,
29
+ children,
30
+ ...props
31
+ }, ref) => {
32
+ const baseId = react.useId();
23
33
  const [selectedTab, setSelectedTab] = react.useState(defaultValue);
24
34
  const activeTab = controlledValue !== void 0 ? controlledValue : selectedTab;
35
+ const tabRegistryRef = react.useRef(
36
+ /* @__PURE__ */ new Map()
37
+ );
38
+ const registerTab = (value, disabled, element) => {
39
+ tabRegistryRef.current.set(value, { disabled, element });
40
+ };
41
+ const unregisterTab = (value) => {
42
+ tabRegistryRef.current.delete(value);
43
+ };
25
44
  const handleTabChange = (val) => {
26
- setSelectedTab(val);
45
+ if (!controlledValue) {
46
+ setSelectedTab(val);
47
+ }
27
48
  onValueChange?.(val);
28
49
  };
29
- return /* @__PURE__ */ jsxRuntime.jsx(TabsContext.Provider, { value: { activeTab, setActiveTab: handleTabChange, variant }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: cn("w-full flex flex-col space-y-4", className), ...props, children }) });
50
+ return /* @__PURE__ */ jsxRuntime.jsx(
51
+ TabsContext.Provider,
52
+ {
53
+ value: {
54
+ activeTab,
55
+ setActiveTab: handleTabChange,
56
+ variant,
57
+ orientation,
58
+ baseId,
59
+ registerTab,
60
+ unregisterTab
61
+ },
62
+ children: /* @__PURE__ */ jsxRuntime.jsx(
63
+ "div",
64
+ {
65
+ ref,
66
+ className: cn(
67
+ "w-full flex space-y-4",
68
+ orientation === "vertical" ? "flex-row space-y-0 space-x-6" : "flex-col",
69
+ className
70
+ ),
71
+ ...props,
72
+ children
73
+ }
74
+ )
75
+ }
76
+ );
30
77
  }
31
78
  );
32
79
  Tabs.displayName = "Tabs";
33
80
  var TabsList = react.forwardRef(
34
- ({ className, children, ...props }, ref) => {
35
- const { variant } = useTabs();
81
+ ({ className, children, onKeyDown, ...props }, ref) => {
82
+ const { variant, orientation, activeTab, setActiveTab } = useTabs();
83
+ const listRef = react.useRef(null);
84
+ const handleKeyDown = (e) => {
85
+ onKeyDown?.(e);
86
+ if (e.defaultPrevented) return;
87
+ const tabs = Array.from(
88
+ listRef.current?.querySelectorAll('[role="tab"]:not([disabled])') || []
89
+ );
90
+ if (tabs.length === 0) return;
91
+ const currentIndex = tabs.findIndex((tab) => tab.getAttribute("aria-selected") === "true");
92
+ let nextIndex = -1;
93
+ const isPrev = orientation === "horizontal" && e.key === "ArrowLeft" || orientation === "vertical" && e.key === "ArrowUp";
94
+ const isNext = orientation === "horizontal" && e.key === "ArrowRight" || orientation === "vertical" && e.key === "ArrowDown";
95
+ if (isNext) {
96
+ e.preventDefault();
97
+ nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % tabs.length;
98
+ } else if (isPrev) {
99
+ e.preventDefault();
100
+ nextIndex = currentIndex === -1 ? tabs.length - 1 : (currentIndex - 1 + tabs.length) % tabs.length;
101
+ } else if (e.key === "Home") {
102
+ e.preventDefault();
103
+ nextIndex = 0;
104
+ } else if (e.key === "End") {
105
+ e.preventDefault();
106
+ nextIndex = tabs.length - 1;
107
+ }
108
+ if (nextIndex !== -1 && tabs[nextIndex]) {
109
+ const nextTab = tabs[nextIndex];
110
+ nextTab.focus();
111
+ nextTab.click();
112
+ }
113
+ };
36
114
  return /* @__PURE__ */ jsxRuntime.jsx(
37
115
  "div",
38
116
  {
39
- ref,
117
+ ref: (node) => {
118
+ listRef.current = node;
119
+ if (typeof ref === "function") ref(node);
120
+ else if (ref) ref.current = node;
121
+ },
40
122
  role: "tablist",
123
+ "aria-orientation": orientation,
124
+ onKeyDown: handleKeyDown,
41
125
  className: cn(
42
- "inline-flex items-center",
126
+ "inline-flex",
127
+ orientation === "vertical" ? "flex-col items-stretch gap-1" : "items-center",
43
128
  variant === "pills" && "p-1 rounded-[var(--ui-radius-lg)] bg-[var(--ui-bg-secondary)] border border-[var(--ui-border-subtle)] gap-1",
44
- variant === "line" && "border-b border-[var(--ui-border-default)] gap-6",
129
+ variant === "line" && (orientation === "vertical" ? "border-r border-[var(--ui-border-default)] pr-2" : "border-b border-[var(--ui-border-default)] gap-6"),
45
130
  variant === "enclosed" && "border-b border-[var(--ui-border-default)] gap-1",
46
131
  className
47
132
  ),
@@ -53,17 +138,35 @@ var TabsList = react.forwardRef(
53
138
  );
54
139
  TabsList.displayName = "TabsList";
55
140
  var TabsTrigger = react.forwardRef(
56
- ({ className, value, children, ...props }, ref) => {
57
- const { activeTab, setActiveTab, variant } = useTabs();
141
+ ({ className, value, disabled = false, children, id, onClick, ...props }, ref) => {
142
+ const { activeTab, setActiveTab, variant, baseId, registerTab, unregisterTab } = useTabs();
58
143
  const isActive = activeTab === value;
144
+ const triggerId = id || `tab-${baseId}-${value}`;
145
+ const panelId = `panel-${baseId}-${value}`;
146
+ const innerRef = react.useRef(null);
147
+ react.useEffect(() => {
148
+ registerTab(value, Boolean(disabled), innerRef.current);
149
+ return () => unregisterTab(value);
150
+ }, [value, disabled, registerTab, unregisterTab]);
59
151
  return /* @__PURE__ */ jsxRuntime.jsx(
60
152
  "button",
61
153
  {
62
- ref,
154
+ ref: (node) => {
155
+ innerRef.current = node;
156
+ if (typeof ref === "function") ref(node);
157
+ else if (ref) ref.current = node;
158
+ },
159
+ id: triggerId,
63
160
  role: "tab",
64
161
  type: "button",
162
+ disabled,
65
163
  "aria-selected": isActive,
66
- onClick: () => setActiveTab(value),
164
+ "aria-controls": panelId,
165
+ tabIndex: isActive ? 0 : -1,
166
+ onClick: (e) => {
167
+ onClick?.(e);
168
+ if (!disabled) setActiveTab(value);
169
+ },
67
170
  className: cn(
68
171
  "inline-flex items-center justify-center font-medium text-sm transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-primary)] disabled:pointer-events-none disabled:opacity-50 select-none cursor-pointer",
69
172
  variant === "pills" && "px-3.5 py-1.5 rounded-[var(--ui-radius-md)] text-[var(--ui-text-muted)] hover:text-[var(--ui-text-primary)]",
@@ -82,16 +185,24 @@ var TabsTrigger = react.forwardRef(
82
185
  );
83
186
  TabsTrigger.displayName = "TabsTrigger";
84
187
  var TabsContent = react.forwardRef(
85
- ({ className, value, children, ...props }, ref) => {
86
- const { activeTab } = useTabs();
87
- if (activeTab !== value) return null;
188
+ ({ className, value, children, id, ...props }, ref) => {
189
+ const { activeTab, baseId } = useTabs();
190
+ const isActive = activeTab === value;
191
+ if (!isActive) return null;
192
+ const panelId = id || `panel-${baseId}-${value}`;
193
+ const triggerId = `tab-${baseId}-${value}`;
88
194
  return /* @__PURE__ */ jsxRuntime.jsx(
89
195
  "div",
90
196
  {
91
197
  ref,
198
+ id: panelId,
92
199
  role: "tabpanel",
200
+ "aria-labelledby": triggerId,
93
201
  tabIndex: 0,
94
- className: cn("focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-primary)] animate-in fade-in duration-150", className),
202
+ className: cn(
203
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-primary)] animate-in fade-in duration-150",
204
+ className
205
+ ),
95
206
  ...props,
96
207
  children
97
208
  }
@@ -406,12 +517,13 @@ function useDropdown() {
406
517
  function DropdownMenu({ children }) {
407
518
  const [isOpen, setIsOpen] = react.useState(false);
408
519
  const [triggerRect, setTriggerRect] = react.useState(null);
409
- return /* @__PURE__ */ jsxRuntime.jsx(DropdownContext.Provider, { value: { isOpen, setIsOpen, triggerRect, setTriggerRect }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative inline-block text-left", children }) });
520
+ const triggerRef = react.useRef(null);
521
+ const menuId = react.useId();
522
+ return /* @__PURE__ */ jsxRuntime.jsx(DropdownContext.Provider, { value: { isOpen, setIsOpen, triggerRect, setTriggerRect, triggerRef, menuId }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative inline-block text-left", children }) });
410
523
  }
411
524
  var DropdownMenuTrigger = react.forwardRef(
412
- ({ children, onClick, ...props }, ref) => {
413
- const { isOpen, setIsOpen, setTriggerRect } = useDropdown();
414
- const triggerRef = react.useRef(null);
525
+ ({ children, onClick, onKeyDown, ...props }, ref) => {
526
+ const { isOpen, setIsOpen, setTriggerRect, triggerRef, menuId } = useDropdown();
415
527
  const handleClick = (e) => {
416
528
  if (triggerRef.current) {
417
529
  setTriggerRect(triggerRef.current.getBoundingClientRect());
@@ -419,39 +531,110 @@ var DropdownMenuTrigger = react.forwardRef(
419
531
  setIsOpen(!isOpen);
420
532
  onClick?.(e);
421
533
  };
422
- return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: triggerRef, onClick: handleClick, className: "inline-flex cursor-pointer", ...props, children });
534
+ const handleKeyDown = (e) => {
535
+ onKeyDown?.(e);
536
+ if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
537
+ e.preventDefault();
538
+ if (triggerRef.current) {
539
+ setTriggerRect(triggerRef.current.getBoundingClientRect());
540
+ }
541
+ setIsOpen(true);
542
+ }
543
+ };
544
+ return /* @__PURE__ */ jsxRuntime.jsx(
545
+ "div",
546
+ {
547
+ ref: (node) => {
548
+ triggerRef.current = node;
549
+ if (typeof ref === "function") ref(node);
550
+ else if (ref) ref.current = node;
551
+ },
552
+ onClick: handleClick,
553
+ onKeyDown: handleKeyDown,
554
+ role: "button",
555
+ tabIndex: 0,
556
+ "aria-haspopup": "menu",
557
+ "aria-expanded": isOpen,
558
+ "aria-controls": isOpen ? `menu-${menuId}` : void 0,
559
+ className: "inline-flex cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-primary)] rounded-[var(--ui-radius-sm)]",
560
+ ...props,
561
+ children
562
+ }
563
+ );
423
564
  }
424
565
  );
425
566
  DropdownMenuTrigger.displayName = "DropdownMenuTrigger";
426
567
  var DropdownMenuContent = react.forwardRef(
427
568
  ({ className, align = "start", children, ...props }, ref) => {
428
- const { isOpen, setIsOpen, triggerRect } = useDropdown();
569
+ const { isOpen, setIsOpen, triggerRect, triggerRef, menuId } = useDropdown();
429
570
  const menuRef = react.useRef(null);
430
571
  react.useEffect(() => {
572
+ if (!isOpen) return;
573
+ const timer = setTimeout(() => {
574
+ const firstItem = menuRef.current?.querySelector('[role="menuitem"]:not([disabled])');
575
+ firstItem?.focus();
576
+ }, 10);
431
577
  const handleOutside = (e) => {
432
- if (menuRef.current && !menuRef.current.contains(e.target)) {
578
+ if (menuRef.current && !menuRef.current.contains(e.target) && !triggerRef.current?.contains(e.target)) {
433
579
  setIsOpen(false);
434
580
  }
435
581
  };
436
- if (isOpen) {
437
- document.addEventListener("mousedown", handleOutside);
438
- }
439
- return () => document.removeEventListener("mousedown", handleOutside);
440
- }, [isOpen, setIsOpen]);
582
+ const handleKeyDown = (e) => {
583
+ if (!menuRef.current) return;
584
+ const items = Array.from(
585
+ menuRef.current.querySelectorAll('[role="menuitem"]:not([disabled])')
586
+ );
587
+ if (items.length === 0) return;
588
+ const currentIndex = items.indexOf(document.activeElement);
589
+ if (e.key === "ArrowDown") {
590
+ e.preventDefault();
591
+ const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % items.length;
592
+ items[nextIndex]?.focus();
593
+ } else if (e.key === "ArrowUp") {
594
+ e.preventDefault();
595
+ const prevIndex = currentIndex === -1 ? items.length - 1 : (currentIndex - 1 + items.length) % items.length;
596
+ items[prevIndex]?.focus();
597
+ } else if (e.key === "Home") {
598
+ e.preventDefault();
599
+ items[0]?.focus();
600
+ } else if (e.key === "End") {
601
+ e.preventDefault();
602
+ items[items.length - 1]?.focus();
603
+ } else if (e.key === "Escape") {
604
+ e.preventDefault();
605
+ setIsOpen(false);
606
+ triggerRef.current?.focus();
607
+ }
608
+ };
609
+ document.addEventListener("mousedown", handleOutside);
610
+ window.addEventListener("keydown", handleKeyDown);
611
+ return () => {
612
+ clearTimeout(timer);
613
+ document.removeEventListener("mousedown", handleOutside);
614
+ window.removeEventListener("keydown", handleKeyDown);
615
+ };
616
+ }, [isOpen, setIsOpen, triggerRef]);
441
617
  if (!isOpen || !triggerRect) return null;
442
618
  const top = triggerRect.bottom + window.scrollY + 6;
443
619
  const left = align === "end" ? triggerRect.right + window.scrollX : triggerRect.left + window.scrollX;
444
620
  return /* @__PURE__ */ jsxRuntime.jsx(Portal, { children: /* @__PURE__ */ jsxRuntime.jsx(
445
621
  "div",
446
622
  {
447
- ref: menuRef,
623
+ ref: (node) => {
624
+ menuRef.current = node;
625
+ if (typeof ref === "function") ref(node);
626
+ else if (ref) ref.current = node;
627
+ },
628
+ id: `menu-${menuId}`,
629
+ role: "menu",
630
+ tabIndex: -1,
448
631
  style: {
449
632
  position: "absolute",
450
633
  top: `${top}px`,
451
634
  left: `${left}px`
452
635
  },
453
636
  className: cn(
454
- "z-50 min-w-[12rem] overflow-hidden rounded-[var(--ui-radius-lg)] border border-[var(--ui-border-default)] bg-[var(--ui-bg-surface)] p-1 text-[var(--ui-text-primary)] shadow-xl animate-in fade-in zoom-in-95",
637
+ "z-50 min-w-[12rem] overflow-hidden rounded-[var(--ui-radius-lg)] border border-[var(--ui-border-default)] bg-[var(--ui-bg-surface)] p-1 text-[var(--ui-text-primary)] shadow-xl animate-in fade-in zoom-in-95 focus:outline-none",
455
638
  align === "end" && "-translate-x-full",
456
639
  className
457
640
  ),
@@ -469,14 +652,15 @@ var DropdownMenuItem = react.forwardRef(
469
652
  "button",
470
653
  {
471
654
  ref,
655
+ role: "menuitem",
472
656
  type: "button",
473
657
  onClick: (e) => {
474
658
  onClick?.(e);
475
659
  setIsOpen(false);
476
660
  },
477
661
  className: cn(
478
- "relative flex w-full cursor-pointer select-none items-center gap-2 rounded-[var(--ui-radius-sm)] px-2.5 py-1.5 text-xs font-medium outline-none transition-colors hover:bg-[var(--ui-bg-tertiary)] disabled:pointer-events-none disabled:opacity-50 text-left",
479
- destructive && "text-[var(--ui-error)] hover:bg-[var(--ui-error-subtle)]",
662
+ "relative flex w-full cursor-pointer select-none items-center gap-2 rounded-[var(--ui-radius-sm)] px-2.5 py-1.5 text-xs font-medium outline-none transition-colors hover:bg-[var(--ui-bg-tertiary)] focus:bg-[var(--ui-bg-tertiary)] disabled:pointer-events-none disabled:opacity-50 text-left",
663
+ destructive && "text-[var(--ui-error)] hover:bg-[var(--ui-error-subtle)] focus:bg-[var(--ui-error-subtle)]",
480
664
  className
481
665
  ),
482
666
  ...props,
@@ -492,7 +676,7 @@ var DropdownMenuItem = react.forwardRef(
492
676
  DropdownMenuItem.displayName = "DropdownMenuItem";
493
677
  var DropdownMenuSeparator = react.forwardRef(
494
678
  ({ className, ...props }, ref) => {
495
- return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: cn("-mx-1 my-1 h-px bg-[var(--ui-border-subtle)]", className), ...props });
679
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, role: "separator", className: cn("-mx-1 my-1 h-px bg-[var(--ui-border-subtle)]", className), ...props });
496
680
  }
497
681
  );
498
682
  DropdownMenuSeparator.displayName = "DropdownMenuSeparator";
@@ -502,37 +686,93 @@ var DropdownMenuLabel = react.forwardRef(
502
686
  }
503
687
  );
504
688
  DropdownMenuLabel.displayName = "DropdownMenuLabel";
505
- var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
506
- function FocusTrap({ children, active = true }) {
689
+ var FOCUSABLE_SELECTOR = [
690
+ "a[href]",
691
+ "button:not([disabled])",
692
+ "textarea:not([disabled])",
693
+ "input:not([disabled])",
694
+ "select:not([disabled])",
695
+ '[tabindex]:not([tabindex="-1"])',
696
+ '[contenteditable="true"]'
697
+ ].join(", ");
698
+ var trapStack = [];
699
+ function FocusTrap({
700
+ children,
701
+ active = true,
702
+ autoFocus = true,
703
+ restoreFocus = true
704
+ }) {
507
705
  const rootRef = react.useRef(null);
706
+ const previousActiveElementRef = react.useRef(null);
508
707
  react.useEffect(() => {
509
708
  if (!active) return;
709
+ if (typeof document !== "undefined") {
710
+ previousActiveElementRef.current = document.activeElement;
711
+ }
510
712
  const root = rootRef.current;
511
713
  if (!root) return;
512
- const focusable = root.querySelectorAll(FOCUSABLE_SELECTOR);
513
- const first = focusable[0];
514
- const last = focusable[focusable.length - 1];
515
- if (first) {
516
- first.focus();
714
+ trapStack.push(root);
715
+ if (autoFocus) {
716
+ const timer = setTimeout(() => {
717
+ const focusable = root.querySelectorAll(FOCUSABLE_SELECTOR);
718
+ if (focusable.length > 0) {
719
+ focusable[0].focus();
720
+ } else if (typeof root.focus === "function") {
721
+ root.focus();
722
+ }
723
+ }, 0);
724
+ return () => clearTimeout(timer);
517
725
  }
726
+ return () => {
727
+ const idx = trapStack.indexOf(root);
728
+ if (idx !== -1) trapStack.splice(idx, 1);
729
+ };
730
+ }, [active, autoFocus]);
731
+ react.useEffect(() => {
732
+ if (!active) return;
518
733
  const handleKeyDown = (e) => {
519
734
  if (e.key !== "Tab") return;
735
+ const root = rootRef.current;
736
+ if (!root) return;
737
+ if (trapStack.length > 0 && trapStack[trapStack.length - 1] !== root) {
738
+ return;
739
+ }
740
+ const focusable = Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).filter(
741
+ (el) => el.offsetParent !== null || el.getClientRects().length > 0
742
+ );
743
+ if (focusable.length === 0) {
744
+ e.preventDefault();
745
+ root.focus();
746
+ return;
747
+ }
748
+ const first = focusable[0];
749
+ const last = focusable[focusable.length - 1];
520
750
  if (e.shiftKey) {
521
- if (document.activeElement === first) {
751
+ if (document.activeElement === first || !root.contains(document.activeElement)) {
522
752
  e.preventDefault();
523
- last?.focus();
753
+ last.focus();
524
754
  }
525
755
  } else {
526
- if (document.activeElement === last) {
756
+ if (document.activeElement === last || !root.contains(document.activeElement)) {
527
757
  e.preventDefault();
528
- first?.focus();
758
+ first.focus();
529
759
  }
530
760
  }
531
761
  };
532
- window.addEventListener("keydown", handleKeyDown);
533
- return () => window.removeEventListener("keydown", handleKeyDown);
534
- }, [active]);
535
- return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: rootRef, className: "contents", children });
762
+ window.addEventListener("keydown", handleKeyDown, true);
763
+ return () => {
764
+ window.removeEventListener("keydown", handleKeyDown, true);
765
+ const root = rootRef.current;
766
+ if (root) {
767
+ const idx = trapStack.indexOf(root);
768
+ if (idx !== -1) trapStack.splice(idx, 1);
769
+ }
770
+ if (restoreFocus && previousActiveElementRef.current && typeof previousActiveElementRef.current.focus === "function") {
771
+ previousActiveElementRef.current.focus();
772
+ }
773
+ };
774
+ }, [active, restoreFocus]);
775
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: rootRef, tabIndex: -1, className: "contents", style: { outline: "none" }, children });
536
776
  }
537
777
  var Kbd = react.forwardRef(
538
778
  ({ className, children, ...props }, ref) => {