@avenue-ticketing/ui 0.2.0 → 0.4.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.
- package/dist/react/badge.d.ts +55 -0
- package/dist/react/badge.js +64 -0
- package/dist/react/badge.js.map +1 -0
- package/dist/react/dialog.d.ts +16 -1
- package/dist/react/dialog.js +242 -101
- package/dist/react/dialog.js.map +1 -1
- package/dist/react/dropdown.d.ts +109 -0
- package/dist/react/dropdown.js +1171 -0
- package/dist/react/dropdown.js.map +1 -0
- package/dist/react/scroll-header.d.ts +19 -1
- package/dist/react/scroll-header.js +129 -40
- package/dist/react/scroll-header.js.map +1 -1
- package/dist/react/sheet.d.ts +15 -1
- package/dist/react/sheet.js +44 -16
- package/dist/react/sheet.js.map +1 -1
- package/dist/react/tabs.d.ts +28 -0
- package/dist/react/tabs.js +267 -0
- package/dist/react/tabs.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1171 @@
|
|
|
1
|
+
import * as React2 from 'react';
|
|
2
|
+
import React2__default, { useCallback, useState, useRef, useMemo, useEffect, useLayoutEffect } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { X, ChevronDown, Check, ChevronRight } from 'lucide-react';
|
|
5
|
+
import { clsx } from 'clsx';
|
|
6
|
+
import { twMerge } from 'tailwind-merge';
|
|
7
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
8
|
+
|
|
9
|
+
function cn(...inputs) {
|
|
10
|
+
return twMerge(clsx(inputs));
|
|
11
|
+
}
|
|
12
|
+
var sizeClass = {
|
|
13
|
+
xs: "h-8 min-h-8 gap-2 px-4 text-xs has-[>svg]:px-3 [&_svg:not([class*='size-'])]:size-3",
|
|
14
|
+
default: "h-10 min-h-10 gap-2 px-5 text-sm has-[>svg]:px-4 [&_svg:not([class*='size-'])]:size-4",
|
|
15
|
+
lg: "h-11 min-h-11 gap-2 px-6 text-base has-[>svg]:px-5 [&_svg:not([class*='size-'])]:size-5"
|
|
16
|
+
};
|
|
17
|
+
var iconOnlySizeClass = {
|
|
18
|
+
xs: "size-8 min-h-8 min-w-8 gap-0 p-0 [&_svg:not([class*='size-'])]:size-3",
|
|
19
|
+
default: "size-10 min-h-10 min-w-10 gap-0 p-0 [&_svg:not([class*='size-'])]:size-4",
|
|
20
|
+
lg: "size-11 min-h-11 min-w-11 gap-0 p-0 [&_svg:not([class*='size-'])]:size-5"
|
|
21
|
+
};
|
|
22
|
+
var roundedClass = {
|
|
23
|
+
full: "rounded-full",
|
|
24
|
+
lg: "rounded-lg",
|
|
25
|
+
md: "rounded-md"
|
|
26
|
+
};
|
|
27
|
+
var variantClass = {
|
|
28
|
+
primary: "bg-primary text-background border border-transparent hover:bg-primary/90 active:bg-primary/85",
|
|
29
|
+
secondary: "bg-background text-primary border border-primary/10 hover:bg-primary/5",
|
|
30
|
+
destructive: "bg-background text-red-500 border border-red-500/25 hover:bg-red-500/5",
|
|
31
|
+
success: "bg-background text-green-500 border border-green-500/25 hover:bg-green-500/5"
|
|
32
|
+
};
|
|
33
|
+
var Button = React2.forwardRef(
|
|
34
|
+
({
|
|
35
|
+
className,
|
|
36
|
+
type = "button",
|
|
37
|
+
variant = "secondary",
|
|
38
|
+
rounded: roundedProp,
|
|
39
|
+
size = "default",
|
|
40
|
+
iconOnly = false,
|
|
41
|
+
disabled,
|
|
42
|
+
...props
|
|
43
|
+
}, ref) => {
|
|
44
|
+
const rounded = roundedProp ?? (iconOnly ? "md" : "full");
|
|
45
|
+
return /* @__PURE__ */ jsx(
|
|
46
|
+
"button",
|
|
47
|
+
{
|
|
48
|
+
type,
|
|
49
|
+
disabled,
|
|
50
|
+
"data-slot": "button",
|
|
51
|
+
"data-icon-only": iconOnly ? "" : void 0,
|
|
52
|
+
className: cn(
|
|
53
|
+
"inline-flex shrink-0 cursor-pointer items-center justify-center whitespace-nowrap outline-none transition-[color,background-color,box-shadow,transform] duration-150 ease-out active:scale-[0.98] [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
54
|
+
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
55
|
+
"focus-visible:border-ring font-bold tracking-wide focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
56
|
+
iconOnly ? iconOnlySizeClass[size] : sizeClass[size],
|
|
57
|
+
roundedClass[rounded],
|
|
58
|
+
variantClass[variant],
|
|
59
|
+
className
|
|
60
|
+
),
|
|
61
|
+
ref,
|
|
62
|
+
...props
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
Button.displayName = "Button";
|
|
68
|
+
var DROPDOWN_PANEL_OPEN_EASING = "cubic-bezier(0,0.55,0.45,1)";
|
|
69
|
+
var DROPDOWN_PANEL_CLOSE_EASING = "cubic-bezier(0.55,0,1,0.45)";
|
|
70
|
+
var DROPDOWN_MENU_MIN_WIDTH_PX = 192;
|
|
71
|
+
var DROPDOWN_MOBILE_SHEET_MAX_PX = 1024;
|
|
72
|
+
var DROPDOWN_MOBILE_SHEET_MOTION_MS = 175;
|
|
73
|
+
var DROPDOWN_MOBILE_SHEET_ENTRY_EASING = "cubic-bezier(0.85, 0, 0.15, 1)";
|
|
74
|
+
var DROPDOWN_MOBILE_SHEET_EXIT_EASING = "cubic-bezier(0.85, 0, 1, 0.15)";
|
|
75
|
+
var DROPDOWN_MOBILE_SHEET_SLIDE_ENTRANCE_OFFSET_DEFAULT_PX = 120;
|
|
76
|
+
var DROPDOWN_SUB_CONTENT_ATTR = "data-dropdown-sub-content";
|
|
77
|
+
var DROPDOWN_SHEET_MENU_TEXT = "max-[1024px]:text-base";
|
|
78
|
+
var DROPDOWN_SHEET_MENU_SHORTCUT = "max-[1024px]:text-sm";
|
|
79
|
+
var DROPDOWN_SHEET_MENU_LABEL_TEXT = "max-[1024px]:text-sm";
|
|
80
|
+
var DROPDOWN_CONTENT_ORIGIN = {
|
|
81
|
+
bottom: "top center",
|
|
82
|
+
top: "bottom center",
|
|
83
|
+
left: "right center",
|
|
84
|
+
right: "left center"
|
|
85
|
+
};
|
|
86
|
+
var DROPDOWN_CONTENT_HIDDEN = {
|
|
87
|
+
bottom: "translateY(-4px) scale(0.97)",
|
|
88
|
+
top: "translateY(4px) scale(0.97)",
|
|
89
|
+
left: "translateX(4px) scale(0.97)",
|
|
90
|
+
right: "translateX(-4px) scale(0.97)"
|
|
91
|
+
};
|
|
92
|
+
var SUB_CONTENT_ORIGIN = {
|
|
93
|
+
bottom: "top center",
|
|
94
|
+
top: "bottom center",
|
|
95
|
+
left: "right top",
|
|
96
|
+
right: "left top"
|
|
97
|
+
};
|
|
98
|
+
function computePos(trigger, menu, side, align, offset, pad) {
|
|
99
|
+
const tr = trigger.getBoundingClientRect();
|
|
100
|
+
const mr = menu.getBoundingClientRect();
|
|
101
|
+
const vw = window.innerWidth;
|
|
102
|
+
const vh = window.innerHeight;
|
|
103
|
+
const sx = window.scrollX;
|
|
104
|
+
const sy = window.scrollY;
|
|
105
|
+
let top = 0;
|
|
106
|
+
let left = 0;
|
|
107
|
+
let effectiveSide = side;
|
|
108
|
+
const calc = (s) => {
|
|
109
|
+
switch (s) {
|
|
110
|
+
case "bottom":
|
|
111
|
+
top = tr.bottom + sy + offset;
|
|
112
|
+
if (align === "start") left = tr.left + sx;
|
|
113
|
+
else if (align === "end") left = tr.right + sx - mr.width;
|
|
114
|
+
else left = tr.left + sx + tr.width / 2 - mr.width / 2;
|
|
115
|
+
break;
|
|
116
|
+
case "top":
|
|
117
|
+
top = tr.top + sy - mr.height - offset;
|
|
118
|
+
if (align === "start") left = tr.left + sx;
|
|
119
|
+
else if (align === "end") left = tr.right + sx - mr.width;
|
|
120
|
+
else left = tr.left + sx + tr.width / 2 - mr.width / 2;
|
|
121
|
+
break;
|
|
122
|
+
case "right":
|
|
123
|
+
left = tr.right + sx + offset;
|
|
124
|
+
if (align === "start") top = tr.top + sy;
|
|
125
|
+
else if (align === "end") top = tr.bottom + sy - mr.height;
|
|
126
|
+
else top = tr.top + sy + tr.height / 2 - mr.height / 2;
|
|
127
|
+
break;
|
|
128
|
+
case "left":
|
|
129
|
+
left = tr.left + sx - mr.width - offset;
|
|
130
|
+
if (align === "start") top = tr.top + sy;
|
|
131
|
+
else if (align === "end") top = tr.bottom + sy - mr.height;
|
|
132
|
+
else top = tr.top + sy + tr.height / 2 - mr.height / 2;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
calc(side);
|
|
137
|
+
if (side === "bottom" && top + mr.height > vh + sy - pad) {
|
|
138
|
+
const ft = tr.top + sy - mr.height - offset;
|
|
139
|
+
if (ft >= sy + pad) {
|
|
140
|
+
effectiveSide = "top";
|
|
141
|
+
top = ft;
|
|
142
|
+
}
|
|
143
|
+
} else if (side === "top" && top < sy + pad) {
|
|
144
|
+
const ft = tr.bottom + sy + offset;
|
|
145
|
+
if (ft + mr.height <= vh + sy - pad) {
|
|
146
|
+
effectiveSide = "bottom";
|
|
147
|
+
top = ft;
|
|
148
|
+
}
|
|
149
|
+
} else if (side === "right" && left + mr.width > vw + sx - pad) {
|
|
150
|
+
const fl = tr.left + sx - mr.width - offset;
|
|
151
|
+
if (fl >= sx + pad) {
|
|
152
|
+
effectiveSide = "left";
|
|
153
|
+
left = fl;
|
|
154
|
+
}
|
|
155
|
+
} else if (side === "left" && left < sx + pad) {
|
|
156
|
+
const fl = tr.right + sx + offset;
|
|
157
|
+
if (fl + mr.width <= vw + sx - pad) {
|
|
158
|
+
effectiveSide = "right";
|
|
159
|
+
left = fl;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
left = Math.max(sx + pad, Math.min(left, vw + sx - mr.width - pad));
|
|
163
|
+
top = Math.max(sy + pad, Math.min(top, vh + sy - mr.height - pad));
|
|
164
|
+
return { top, left, side: effectiveSide };
|
|
165
|
+
}
|
|
166
|
+
function useIsMobile(breakpoint = 1025) {
|
|
167
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
const mq = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
|
|
170
|
+
setIsMobile(mq.matches);
|
|
171
|
+
const handler = (e) => setIsMobile(e.matches);
|
|
172
|
+
mq.addEventListener("change", handler);
|
|
173
|
+
return () => mq.removeEventListener("change", handler);
|
|
174
|
+
}, [breakpoint]);
|
|
175
|
+
return isMobile;
|
|
176
|
+
}
|
|
177
|
+
var DropdownContext = React2__default.createContext(void 0);
|
|
178
|
+
function useDropdown() {
|
|
179
|
+
const ctx = React2__default.useContext(DropdownContext);
|
|
180
|
+
if (!ctx)
|
|
181
|
+
throw new Error("Dropdown components must be used within <Dropdown />");
|
|
182
|
+
return ctx;
|
|
183
|
+
}
|
|
184
|
+
var SubContext = React2__default.createContext(void 0);
|
|
185
|
+
var Dropdown = ({
|
|
186
|
+
children,
|
|
187
|
+
open: controlledOpen,
|
|
188
|
+
onOpenChange
|
|
189
|
+
}) => {
|
|
190
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
191
|
+
const isControlled = controlledOpen !== void 0;
|
|
192
|
+
const open = isControlled ? controlledOpen : internalOpen;
|
|
193
|
+
const triggerRef = useRef(null);
|
|
194
|
+
const [radioValues, setRadioValues] = useState({});
|
|
195
|
+
const setOpen = useCallback(
|
|
196
|
+
(v) => {
|
|
197
|
+
if (!isControlled) setInternalOpen(v);
|
|
198
|
+
onOpenChange?.(v);
|
|
199
|
+
},
|
|
200
|
+
[isControlled, onOpenChange]
|
|
201
|
+
);
|
|
202
|
+
const setRadioValue = useCallback((group, value) => {
|
|
203
|
+
setRadioValues((prev) => ({ ...prev, [group]: value }));
|
|
204
|
+
}, []);
|
|
205
|
+
const ctx = useMemo(
|
|
206
|
+
() => ({ open, setOpen, triggerRef, radioValues, setRadioValue }),
|
|
207
|
+
[open, setOpen, radioValues, setRadioValue]
|
|
208
|
+
);
|
|
209
|
+
return /* @__PURE__ */ jsx(DropdownContext.Provider, { value: ctx, children });
|
|
210
|
+
};
|
|
211
|
+
function mergeTriggerRef(triggerRef, node, childRef) {
|
|
212
|
+
triggerRef.current = node;
|
|
213
|
+
if (typeof childRef === "function") childRef(node);
|
|
214
|
+
else if (childRef && typeof childRef === "object")
|
|
215
|
+
childRef.current = node;
|
|
216
|
+
}
|
|
217
|
+
var DropdownTrigger = React2__default.forwardRef(
|
|
218
|
+
({
|
|
219
|
+
children,
|
|
220
|
+
asChild,
|
|
221
|
+
className,
|
|
222
|
+
onClick: onClickProp,
|
|
223
|
+
disabled,
|
|
224
|
+
...buttonProps
|
|
225
|
+
}, ref) => {
|
|
226
|
+
const { open, setOpen, triggerRef } = useDropdown();
|
|
227
|
+
const handleClick = useCallback(
|
|
228
|
+
(e) => {
|
|
229
|
+
onClickProp?.(e);
|
|
230
|
+
e.stopPropagation();
|
|
231
|
+
if (disabled) return;
|
|
232
|
+
setOpen(!open);
|
|
233
|
+
},
|
|
234
|
+
[disabled, onClickProp, open, setOpen]
|
|
235
|
+
);
|
|
236
|
+
const setButtonRef = useCallback(
|
|
237
|
+
(el) => {
|
|
238
|
+
triggerRef.current = el;
|
|
239
|
+
if (typeof ref === "function") ref(el);
|
|
240
|
+
else if (ref) ref.current = el;
|
|
241
|
+
},
|
|
242
|
+
[ref, triggerRef]
|
|
243
|
+
);
|
|
244
|
+
if (asChild && React2__default.isValidElement(children)) {
|
|
245
|
+
const child = children;
|
|
246
|
+
return React2__default.cloneElement(child, {
|
|
247
|
+
ref: (node) => {
|
|
248
|
+
mergeTriggerRef(
|
|
249
|
+
triggerRef,
|
|
250
|
+
node,
|
|
251
|
+
child.ref
|
|
252
|
+
);
|
|
253
|
+
},
|
|
254
|
+
className: cn("group", child.props.className),
|
|
255
|
+
onClick: (e) => {
|
|
256
|
+
child.props.onClick?.(e);
|
|
257
|
+
handleClick(e);
|
|
258
|
+
},
|
|
259
|
+
"aria-expanded": open,
|
|
260
|
+
"aria-haspopup": "menu",
|
|
261
|
+
"data-state": open ? "open" : "closed",
|
|
262
|
+
...disabled !== void 0 ? {
|
|
263
|
+
disabled
|
|
264
|
+
} : {}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return /* @__PURE__ */ jsx(
|
|
268
|
+
Button,
|
|
269
|
+
{
|
|
270
|
+
ref: setButtonRef,
|
|
271
|
+
type: "button",
|
|
272
|
+
...buttonProps,
|
|
273
|
+
disabled,
|
|
274
|
+
className: cn("group", className),
|
|
275
|
+
"aria-expanded": open,
|
|
276
|
+
"aria-haspopup": "menu",
|
|
277
|
+
"data-state": open ? "open" : "closed",
|
|
278
|
+
onClick: handleClick,
|
|
279
|
+
children
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
DropdownTrigger.displayName = "DropdownTrigger";
|
|
285
|
+
function DropdownChevron({
|
|
286
|
+
className,
|
|
287
|
+
...props
|
|
288
|
+
}) {
|
|
289
|
+
const { open } = useDropdown();
|
|
290
|
+
return /* @__PURE__ */ jsx(
|
|
291
|
+
ChevronDown,
|
|
292
|
+
{
|
|
293
|
+
"aria-hidden": true,
|
|
294
|
+
className: cn("size-4 shrink-0", open && "rotate-180", className),
|
|
295
|
+
...props
|
|
296
|
+
}
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
var DropdownMobileClose = React2__default.forwardRef(({ className, type = "button", ...props }, ref) => {
|
|
300
|
+
return /* @__PURE__ */ jsxs(
|
|
301
|
+
"button",
|
|
302
|
+
{
|
|
303
|
+
ref,
|
|
304
|
+
type,
|
|
305
|
+
className: cn(
|
|
306
|
+
"z-100 flex size-12 shrink-0 cursor-pointer items-center justify-center rounded-full transition-all hover:bg-secondary-background active:scale-[0.96]",
|
|
307
|
+
className
|
|
308
|
+
),
|
|
309
|
+
...props,
|
|
310
|
+
children: [
|
|
311
|
+
/* @__PURE__ */ jsx(X, { className: "size-5.5" }),
|
|
312
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: "Close" })
|
|
313
|
+
]
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
DropdownMobileClose.displayName = "DropdownMobileClose";
|
|
318
|
+
function DropdownMobileBottomSheetPortal({
|
|
319
|
+
open,
|
|
320
|
+
isAnimating,
|
|
321
|
+
slideEntrance,
|
|
322
|
+
slideOffsetPx,
|
|
323
|
+
mobileTitle,
|
|
324
|
+
onRequestClose,
|
|
325
|
+
menuRef,
|
|
326
|
+
portalZClassName = "z-50",
|
|
327
|
+
children,
|
|
328
|
+
className,
|
|
329
|
+
style,
|
|
330
|
+
...panelProps
|
|
331
|
+
}) {
|
|
332
|
+
const sheetMotion = open ? DROPDOWN_MOBILE_SHEET_ENTRY_EASING : DROPDOWN_MOBILE_SHEET_EXIT_EASING;
|
|
333
|
+
const sheetHiddenTransform = slideEntrance ? `translateY(${slideOffsetPx}px)` : "translateY(100%)";
|
|
334
|
+
return createPortal(
|
|
335
|
+
/* @__PURE__ */ jsxs(
|
|
336
|
+
"div",
|
|
337
|
+
{
|
|
338
|
+
className: cn(
|
|
339
|
+
"fixed inset-0 flex items-end justify-center p-0",
|
|
340
|
+
portalZClassName
|
|
341
|
+
),
|
|
342
|
+
children: [
|
|
343
|
+
/* @__PURE__ */ jsx(
|
|
344
|
+
"div",
|
|
345
|
+
{
|
|
346
|
+
className: cn(
|
|
347
|
+
"fixed inset-0 bg-black/40 dark:bg-black/60",
|
|
348
|
+
isAnimating ? "opacity-100" : "opacity-0"
|
|
349
|
+
),
|
|
350
|
+
style: {
|
|
351
|
+
transitionProperty: "opacity",
|
|
352
|
+
transitionDuration: `${DROPDOWN_MOBILE_SHEET_MOTION_MS}ms`,
|
|
353
|
+
transitionTimingFunction: sheetMotion
|
|
354
|
+
},
|
|
355
|
+
onClick: onRequestClose
|
|
356
|
+
}
|
|
357
|
+
),
|
|
358
|
+
/* @__PURE__ */ jsxs(
|
|
359
|
+
"div",
|
|
360
|
+
{
|
|
361
|
+
...panelProps,
|
|
362
|
+
ref: menuRef,
|
|
363
|
+
className: cn(
|
|
364
|
+
"bg-background border-primary/10 relative z-10 flex w-full max-h-[min(90dvh,calc(100dvh-env(safe-area-inset-bottom,0px)))] flex-col overflow-hidden shadow-2xl outline-none",
|
|
365
|
+
"rounded-t-2xl rounded-b-none border-x-0 border-b-0 border-t",
|
|
366
|
+
className
|
|
367
|
+
),
|
|
368
|
+
style: {
|
|
369
|
+
transform: isAnimating ? "translateY(0)" : sheetHiddenTransform,
|
|
370
|
+
opacity: isAnimating ? 1 : 0,
|
|
371
|
+
transitionProperty: "transform, opacity",
|
|
372
|
+
transitionDuration: `${DROPDOWN_MOBILE_SHEET_MOTION_MS}ms`,
|
|
373
|
+
transitionTimingFunction: sheetMotion,
|
|
374
|
+
...style
|
|
375
|
+
},
|
|
376
|
+
children: [
|
|
377
|
+
/* @__PURE__ */ jsxs(
|
|
378
|
+
"div",
|
|
379
|
+
{
|
|
380
|
+
className: cn(
|
|
381
|
+
"flex w-full shrink-0 items-center py-2 pl-4 pr-2",
|
|
382
|
+
mobileTitle ? "justify-between gap-3" : "justify-end"
|
|
383
|
+
),
|
|
384
|
+
children: [
|
|
385
|
+
mobileTitle ? /* @__PURE__ */ jsx("p", { className: "text-foreground min-w-0 flex-1 truncate text-base font-semibold", children: mobileTitle }) : null,
|
|
386
|
+
/* @__PURE__ */ jsx(
|
|
387
|
+
DropdownMobileClose,
|
|
388
|
+
{
|
|
389
|
+
onClick: (e) => {
|
|
390
|
+
e.stopPropagation();
|
|
391
|
+
onRequestClose();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
)
|
|
395
|
+
]
|
|
396
|
+
}
|
|
397
|
+
),
|
|
398
|
+
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto pb-[max(0.375rem,env(safe-area-inset-bottom,0px))]", children: /* @__PURE__ */ jsx("div", { className: "pb-1", children }) })
|
|
399
|
+
]
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
]
|
|
403
|
+
}
|
|
404
|
+
),
|
|
405
|
+
document.body
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
var DropdownContent = ({
|
|
409
|
+
children,
|
|
410
|
+
side = "bottom",
|
|
411
|
+
align = "start",
|
|
412
|
+
offset = 10,
|
|
413
|
+
duration = 80,
|
|
414
|
+
viewportPadding = 8,
|
|
415
|
+
closeOnEscape = true,
|
|
416
|
+
minWidth = "trigger",
|
|
417
|
+
loop = true,
|
|
418
|
+
mobileSheet = true,
|
|
419
|
+
mobileTitle,
|
|
420
|
+
slideEntrance = true,
|
|
421
|
+
slideEntranceOffsetPx: slideEntranceOffsetPxProp,
|
|
422
|
+
className,
|
|
423
|
+
style,
|
|
424
|
+
...props
|
|
425
|
+
}) => {
|
|
426
|
+
const isMobile = useIsMobile(DROPDOWN_MOBILE_SHEET_MAX_PX + 1);
|
|
427
|
+
const { open, setOpen, triggerRef } = useDropdown();
|
|
428
|
+
const [shouldRender, setShouldRender] = useState(false);
|
|
429
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
430
|
+
const [pos, setPos] = useState({ top: -9999, left: -9999, side });
|
|
431
|
+
const [triggerW, setTriggerW] = useState(0);
|
|
432
|
+
const menuRef = useRef(null);
|
|
433
|
+
const slideOffsetPx = useMemo(
|
|
434
|
+
() => slideEntranceOffsetPxProp ?? DROPDOWN_MOBILE_SHEET_SLIDE_ENTRANCE_OFFSET_DEFAULT_PX,
|
|
435
|
+
[slideEntranceOffsetPxProp]
|
|
436
|
+
);
|
|
437
|
+
const closeDuration = isMobile && mobileSheet ? DROPDOWN_MOBILE_SHEET_MOTION_MS : duration;
|
|
438
|
+
const closeMenu = useCallback(() => setOpen(false), [setOpen]);
|
|
439
|
+
useEffect(() => {
|
|
440
|
+
if (open) {
|
|
441
|
+
setShouldRender(true);
|
|
442
|
+
} else {
|
|
443
|
+
setIsAnimating(false);
|
|
444
|
+
const timer = setTimeout(() => setShouldRender(false), closeDuration);
|
|
445
|
+
return () => clearTimeout(timer);
|
|
446
|
+
}
|
|
447
|
+
}, [open, closeDuration]);
|
|
448
|
+
useEffect(() => {
|
|
449
|
+
if (!shouldRender || !open) return;
|
|
450
|
+
let raf2 = 0;
|
|
451
|
+
const raf1 = requestAnimationFrame(() => {
|
|
452
|
+
raf2 = requestAnimationFrame(() => setIsAnimating(true));
|
|
453
|
+
});
|
|
454
|
+
return () => {
|
|
455
|
+
cancelAnimationFrame(raf1);
|
|
456
|
+
if (raf2) cancelAnimationFrame(raf2);
|
|
457
|
+
};
|
|
458
|
+
}, [shouldRender, open]);
|
|
459
|
+
useLayoutEffect(() => {
|
|
460
|
+
if (!shouldRender || !triggerRef.current || !menuRef.current) return;
|
|
461
|
+
const update = () => {
|
|
462
|
+
if (!triggerRef.current || !menuRef.current) return;
|
|
463
|
+
setTriggerW(triggerRef.current.getBoundingClientRect().width);
|
|
464
|
+
setPos(
|
|
465
|
+
computePos(
|
|
466
|
+
triggerRef.current,
|
|
467
|
+
menuRef.current,
|
|
468
|
+
side,
|
|
469
|
+
align,
|
|
470
|
+
offset,
|
|
471
|
+
viewportPadding
|
|
472
|
+
)
|
|
473
|
+
);
|
|
474
|
+
};
|
|
475
|
+
update();
|
|
476
|
+
window.addEventListener("resize", update);
|
|
477
|
+
window.addEventListener("scroll", update, true);
|
|
478
|
+
return () => {
|
|
479
|
+
window.removeEventListener("resize", update);
|
|
480
|
+
window.removeEventListener("scroll", update, true);
|
|
481
|
+
};
|
|
482
|
+
}, [shouldRender, side, align, offset, viewportPadding]);
|
|
483
|
+
useEffect(() => {
|
|
484
|
+
if (isAnimating && menuRef.current) {
|
|
485
|
+
menuRef.current.focus();
|
|
486
|
+
}
|
|
487
|
+
}, [isAnimating]);
|
|
488
|
+
useEffect(() => {
|
|
489
|
+
if (!open) return;
|
|
490
|
+
const handler = (e) => {
|
|
491
|
+
const t = e.target;
|
|
492
|
+
if (menuRef.current?.contains(t) || triggerRef.current?.contains(t))
|
|
493
|
+
return;
|
|
494
|
+
const el = e.target instanceof Element ? e.target : null;
|
|
495
|
+
if (el?.closest?.(`[${DROPDOWN_SUB_CONTENT_ATTR}]`)) return;
|
|
496
|
+
setOpen(false);
|
|
497
|
+
};
|
|
498
|
+
document.addEventListener("mousedown", handler);
|
|
499
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
500
|
+
}, [open, setOpen, triggerRef]);
|
|
501
|
+
useEffect(() => {
|
|
502
|
+
if (!open) return;
|
|
503
|
+
const handler = (e) => {
|
|
504
|
+
const menu = menuRef.current;
|
|
505
|
+
if (!menu) return;
|
|
506
|
+
const focusedEl = document.activeElement;
|
|
507
|
+
if (focusedEl && !menu.contains(focusedEl)) return;
|
|
508
|
+
const items = getItems(menu);
|
|
509
|
+
const idx = items.indexOf(focusedEl);
|
|
510
|
+
switch (e.key) {
|
|
511
|
+
case "Escape":
|
|
512
|
+
if (closeOnEscape) {
|
|
513
|
+
e.preventDefault();
|
|
514
|
+
setOpen(false);
|
|
515
|
+
triggerRef.current?.focus();
|
|
516
|
+
}
|
|
517
|
+
break;
|
|
518
|
+
case "ArrowDown":
|
|
519
|
+
e.preventDefault();
|
|
520
|
+
if (items.length === 0) break;
|
|
521
|
+
if (idx === -1 || idx === items.length - 1 && loop)
|
|
522
|
+
items[0]?.focus();
|
|
523
|
+
else if (idx < items.length - 1) items[idx + 1]?.focus();
|
|
524
|
+
break;
|
|
525
|
+
case "ArrowUp":
|
|
526
|
+
e.preventDefault();
|
|
527
|
+
if (items.length === 0) break;
|
|
528
|
+
if (idx <= 0 && loop) items[items.length - 1]?.focus();
|
|
529
|
+
else if (idx > 0) items[idx - 1]?.focus();
|
|
530
|
+
break;
|
|
531
|
+
case "Home":
|
|
532
|
+
e.preventDefault();
|
|
533
|
+
items[0]?.focus();
|
|
534
|
+
break;
|
|
535
|
+
case "End":
|
|
536
|
+
e.preventDefault();
|
|
537
|
+
items[items.length - 1]?.focus();
|
|
538
|
+
break;
|
|
539
|
+
case "Tab":
|
|
540
|
+
setOpen(false);
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
window.addEventListener("keydown", handler);
|
|
545
|
+
return () => window.removeEventListener("keydown", handler);
|
|
546
|
+
}, [open, closeOnEscape, loop, setOpen, triggerRef]);
|
|
547
|
+
useEffect(() => {
|
|
548
|
+
if (open && isMobile && mobileSheet) {
|
|
549
|
+
document.body.style.overflow = "hidden";
|
|
550
|
+
}
|
|
551
|
+
return () => {
|
|
552
|
+
document.body.style.overflow = "";
|
|
553
|
+
};
|
|
554
|
+
}, [open, isMobile, mobileSheet]);
|
|
555
|
+
if (!shouldRender || typeof document === "undefined") return null;
|
|
556
|
+
if (isMobile && mobileSheet) {
|
|
557
|
+
return /* @__PURE__ */ jsx(
|
|
558
|
+
DropdownMobileBottomSheetPortal,
|
|
559
|
+
{
|
|
560
|
+
...props,
|
|
561
|
+
open,
|
|
562
|
+
isAnimating,
|
|
563
|
+
slideEntrance,
|
|
564
|
+
slideOffsetPx,
|
|
565
|
+
mobileTitle,
|
|
566
|
+
onRequestClose: closeMenu,
|
|
567
|
+
menuRef,
|
|
568
|
+
portalZClassName: "z-50",
|
|
569
|
+
className,
|
|
570
|
+
style,
|
|
571
|
+
role: "menu",
|
|
572
|
+
"aria-orientation": "vertical",
|
|
573
|
+
tabIndex: -1,
|
|
574
|
+
children
|
|
575
|
+
}
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
const resolvedMinW = minWidth === "trigger" ? Math.max(triggerW, DROPDOWN_MENU_MIN_WIDTH_PX) : minWidth;
|
|
579
|
+
return createPortal(
|
|
580
|
+
/* @__PURE__ */ jsx(
|
|
581
|
+
"div",
|
|
582
|
+
{
|
|
583
|
+
...props,
|
|
584
|
+
ref: menuRef,
|
|
585
|
+
role: "menu",
|
|
586
|
+
"aria-orientation": "vertical",
|
|
587
|
+
tabIndex: -1,
|
|
588
|
+
className: cn(
|
|
589
|
+
"bg-background border-primary/10 absolute z-50 overflow-hidden rounded-xl border py-1.5 shadow-xl outline-none",
|
|
590
|
+
className
|
|
591
|
+
),
|
|
592
|
+
style: {
|
|
593
|
+
position: "absolute",
|
|
594
|
+
top: pos.top,
|
|
595
|
+
left: pos.left,
|
|
596
|
+
minWidth: resolvedMinW,
|
|
597
|
+
transformOrigin: DROPDOWN_CONTENT_ORIGIN[pos.side],
|
|
598
|
+
transform: isAnimating ? "none" : DROPDOWN_CONTENT_HIDDEN[pos.side],
|
|
599
|
+
opacity: isAnimating ? 1 : 0,
|
|
600
|
+
transitionProperty: "opacity, transform",
|
|
601
|
+
transitionDuration: `${duration}ms`,
|
|
602
|
+
transitionTimingFunction: isAnimating ? DROPDOWN_PANEL_OPEN_EASING : DROPDOWN_PANEL_CLOSE_EASING,
|
|
603
|
+
...style
|
|
604
|
+
},
|
|
605
|
+
children
|
|
606
|
+
}
|
|
607
|
+
),
|
|
608
|
+
document.body
|
|
609
|
+
);
|
|
610
|
+
};
|
|
611
|
+
function getItems(menu) {
|
|
612
|
+
return Array.from(
|
|
613
|
+
menu.querySelectorAll(
|
|
614
|
+
'[role="menuitem"]:not([aria-disabled="true"]),[role="menuitemcheckbox"]:not([aria-disabled="true"]),[role="menuitemradio"]:not([aria-disabled="true"])'
|
|
615
|
+
)
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
var DropdownItem = ({
|
|
619
|
+
children,
|
|
620
|
+
disabled = false,
|
|
621
|
+
destructive = false,
|
|
622
|
+
icon,
|
|
623
|
+
shortcut,
|
|
624
|
+
closeOnSelect = true,
|
|
625
|
+
inset = false,
|
|
626
|
+
className,
|
|
627
|
+
onClick,
|
|
628
|
+
...props
|
|
629
|
+
}) => {
|
|
630
|
+
const { setOpen } = useDropdown();
|
|
631
|
+
const handleClick = (e) => {
|
|
632
|
+
if (disabled) return;
|
|
633
|
+
onClick?.(e);
|
|
634
|
+
if (closeOnSelect) setOpen(false);
|
|
635
|
+
};
|
|
636
|
+
const handleKeyDown = (e) => {
|
|
637
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
638
|
+
e.preventDefault();
|
|
639
|
+
handleClick(e);
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
return /* @__PURE__ */ jsxs(
|
|
643
|
+
"div",
|
|
644
|
+
{
|
|
645
|
+
...props,
|
|
646
|
+
role: "menuitem",
|
|
647
|
+
tabIndex: disabled ? void 0 : -1,
|
|
648
|
+
"aria-disabled": disabled,
|
|
649
|
+
onClick: handleClick,
|
|
650
|
+
onKeyDown: handleKeyDown,
|
|
651
|
+
className: cn(
|
|
652
|
+
"relative mx-1.5 flex items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors outline-none select-none",
|
|
653
|
+
DROPDOWN_SHEET_MENU_TEXT,
|
|
654
|
+
inset && "pl-9",
|
|
655
|
+
!disabled && "cursor-pointer",
|
|
656
|
+
disabled && "lg:cursor-not-allowed",
|
|
657
|
+
!disabled && !destructive && "text-foreground hover:bg-primary/8 focus-visible:bg-primary/8 dark:hover:bg-primary/4 dark:focus-visible:bg-primary/4",
|
|
658
|
+
!disabled && destructive && "text-destructive hover:bg-destructive/10 focus-visible:bg-destructive/10 dark:text-destructive-foreground dark:hover:bg-destructive-foreground/18 dark:focus-visible:bg-destructive-foreground/18",
|
|
659
|
+
disabled && !destructive && "text-foreground/45 dark:text-foreground/50",
|
|
660
|
+
disabled && destructive && "bg-destructive/5 text-destructive/75 dark:bg-destructive-foreground/12 dark:text-destructive-foreground/78",
|
|
661
|
+
className
|
|
662
|
+
),
|
|
663
|
+
children: [
|
|
664
|
+
icon && /* @__PURE__ */ jsx("span", { className: "flex size-4 shrink-0 items-center justify-center", children: icon }),
|
|
665
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1", children }),
|
|
666
|
+
shortcut ? /* @__PURE__ */ jsx(
|
|
667
|
+
"span",
|
|
668
|
+
{
|
|
669
|
+
className: cn(
|
|
670
|
+
"ml-auto text-xs tracking-widest",
|
|
671
|
+
DROPDOWN_SHEET_MENU_SHORTCUT,
|
|
672
|
+
destructive ? "text-destructive/70 dark:text-destructive-foreground/80" : "opacity-40"
|
|
673
|
+
),
|
|
674
|
+
children: shortcut
|
|
675
|
+
}
|
|
676
|
+
) : null
|
|
677
|
+
]
|
|
678
|
+
}
|
|
679
|
+
);
|
|
680
|
+
};
|
|
681
|
+
var DropdownSeparator = ({ className, ...props }) => /* @__PURE__ */ jsx(
|
|
682
|
+
"div",
|
|
683
|
+
{
|
|
684
|
+
role: "separator",
|
|
685
|
+
className: cn("border-primary/10 my-1.5 border-t", className),
|
|
686
|
+
...props
|
|
687
|
+
}
|
|
688
|
+
);
|
|
689
|
+
var DropdownLabel = ({ className, inset, ...props }) => /* @__PURE__ */ jsx(
|
|
690
|
+
"div",
|
|
691
|
+
{
|
|
692
|
+
className: cn(
|
|
693
|
+
"text-primary/50 px-4 py-2 text-xs font-medium",
|
|
694
|
+
DROPDOWN_SHEET_MENU_LABEL_TEXT,
|
|
695
|
+
inset && "pl-9",
|
|
696
|
+
className
|
|
697
|
+
),
|
|
698
|
+
...props
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
var DROPDOWN_CHECKBOX_TICK_DELAY_MS = 60;
|
|
702
|
+
var DROPDOWN_CHECKBOX_TICK_DRAW_MS = 200;
|
|
703
|
+
function DropdownCheckboxItemCheckMark() {
|
|
704
|
+
const lineRef = useRef(null);
|
|
705
|
+
useLayoutEffect(() => {
|
|
706
|
+
const poly = lineRef.current;
|
|
707
|
+
if (!poly || typeof poly.getTotalLength !== "function") return;
|
|
708
|
+
const len = poly.getTotalLength();
|
|
709
|
+
if (len <= 0) return;
|
|
710
|
+
poly.style.strokeDasharray = `${len}`;
|
|
711
|
+
poly.style.strokeDashoffset = `${len}`;
|
|
712
|
+
if (typeof poly.animate !== "function") {
|
|
713
|
+
poly.style.strokeDashoffset = "0";
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const anim = poly.animate(
|
|
717
|
+
[{ strokeDashoffset: len }, { strokeDashoffset: 0 }],
|
|
718
|
+
{
|
|
719
|
+
duration: DROPDOWN_CHECKBOX_TICK_DRAW_MS,
|
|
720
|
+
delay: DROPDOWN_CHECKBOX_TICK_DELAY_MS,
|
|
721
|
+
easing: "cubic-bezier(0.45, 0, 0.2, 1)",
|
|
722
|
+
fill: "forwards"
|
|
723
|
+
}
|
|
724
|
+
);
|
|
725
|
+
return () => anim.cancel();
|
|
726
|
+
}, []);
|
|
727
|
+
return /* @__PURE__ */ jsx(
|
|
728
|
+
"svg",
|
|
729
|
+
{
|
|
730
|
+
className: "size-4 shrink-0 overflow-visible",
|
|
731
|
+
viewBox: "0 0 24 24",
|
|
732
|
+
"aria-hidden": true,
|
|
733
|
+
children: /* @__PURE__ */ jsx(
|
|
734
|
+
"polyline",
|
|
735
|
+
{
|
|
736
|
+
ref: lineRef,
|
|
737
|
+
points: "4 12 9 17 20 6",
|
|
738
|
+
fill: "none",
|
|
739
|
+
stroke: "currentColor",
|
|
740
|
+
strokeWidth: "3",
|
|
741
|
+
strokeLinecap: "round",
|
|
742
|
+
strokeLinejoin: "round"
|
|
743
|
+
}
|
|
744
|
+
)
|
|
745
|
+
}
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
var DropdownCheckboxItem = ({
|
|
749
|
+
children,
|
|
750
|
+
checked = false,
|
|
751
|
+
onCheckedChange,
|
|
752
|
+
disabled = false,
|
|
753
|
+
shortcut,
|
|
754
|
+
closeOnSelect = false,
|
|
755
|
+
className,
|
|
756
|
+
onClick,
|
|
757
|
+
...props
|
|
758
|
+
}) => {
|
|
759
|
+
const { setOpen } = useDropdown();
|
|
760
|
+
const handleClick = (e) => {
|
|
761
|
+
if (disabled) return;
|
|
762
|
+
onCheckedChange?.(!checked);
|
|
763
|
+
onClick?.(e);
|
|
764
|
+
if (closeOnSelect) setOpen(false);
|
|
765
|
+
};
|
|
766
|
+
const handleKeyDown = (e) => {
|
|
767
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
768
|
+
e.preventDefault();
|
|
769
|
+
handleClick(e);
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
return /* @__PURE__ */ jsxs(
|
|
773
|
+
"div",
|
|
774
|
+
{
|
|
775
|
+
...props,
|
|
776
|
+
role: "menuitemcheckbox",
|
|
777
|
+
"aria-checked": checked,
|
|
778
|
+
tabIndex: disabled ? void 0 : -1,
|
|
779
|
+
"aria-disabled": disabled,
|
|
780
|
+
onClick: handleClick,
|
|
781
|
+
onKeyDown: handleKeyDown,
|
|
782
|
+
className: cn(
|
|
783
|
+
"mx-1.5 flex items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors outline-none select-none",
|
|
784
|
+
DROPDOWN_SHEET_MENU_TEXT,
|
|
785
|
+
!disabled && "cursor-pointer text-foreground hover:bg-primary/8 focus-visible:bg-primary/8 dark:hover:bg-primary/4 dark:focus-visible:bg-primary/4",
|
|
786
|
+
disabled && "lg:cursor-not-allowed text-foreground/45 dark:text-foreground/50",
|
|
787
|
+
className
|
|
788
|
+
),
|
|
789
|
+
children: [
|
|
790
|
+
/* @__PURE__ */ jsx(
|
|
791
|
+
"span",
|
|
792
|
+
{
|
|
793
|
+
"aria-hidden": true,
|
|
794
|
+
className: cn(
|
|
795
|
+
"pointer-events-none flex size-5 shrink-0 items-center justify-center rounded-[4px] border mr-1",
|
|
796
|
+
!disabled && (checked ? "border-primary bg-primary text-background" : "border-primary/20 bg-background"),
|
|
797
|
+
disabled && (checked ? "border-primary/40 bg-primary/45 text-primary-foreground" : "border-primary/10 bg-muted/25")
|
|
798
|
+
),
|
|
799
|
+
children: checked ? /* @__PURE__ */ jsx(DropdownCheckboxItemCheckMark, {}) : null
|
|
800
|
+
}
|
|
801
|
+
),
|
|
802
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1", children }),
|
|
803
|
+
shortcut && /* @__PURE__ */ jsx(
|
|
804
|
+
"span",
|
|
805
|
+
{
|
|
806
|
+
className: cn(
|
|
807
|
+
"ml-auto text-xs tracking-widest opacity-40",
|
|
808
|
+
DROPDOWN_SHEET_MENU_SHORTCUT
|
|
809
|
+
),
|
|
810
|
+
children: shortcut
|
|
811
|
+
}
|
|
812
|
+
)
|
|
813
|
+
]
|
|
814
|
+
}
|
|
815
|
+
);
|
|
816
|
+
};
|
|
817
|
+
var DropdownRadioGroup = ({ children, value, onValueChange, group }) => {
|
|
818
|
+
const { setRadioValue, radioValues } = useDropdown();
|
|
819
|
+
const resolvedValue = value ?? radioValues[group] ?? "";
|
|
820
|
+
const handleValueChange = useCallback(
|
|
821
|
+
(v) => {
|
|
822
|
+
setRadioValue(group, v);
|
|
823
|
+
onValueChange?.(v);
|
|
824
|
+
},
|
|
825
|
+
[group, onValueChange, setRadioValue]
|
|
826
|
+
);
|
|
827
|
+
const radioCtx = useMemo(
|
|
828
|
+
() => ({
|
|
829
|
+
group,
|
|
830
|
+
value: resolvedValue,
|
|
831
|
+
onValueChange: handleValueChange
|
|
832
|
+
}),
|
|
833
|
+
[group, resolvedValue, handleValueChange]
|
|
834
|
+
);
|
|
835
|
+
return /* @__PURE__ */ jsx(RadioGroupContext.Provider, { value: radioCtx, children });
|
|
836
|
+
};
|
|
837
|
+
var RadioGroupContext = React2__default.createContext(
|
|
838
|
+
void 0
|
|
839
|
+
);
|
|
840
|
+
function useRadioGroup() {
|
|
841
|
+
const ctx = React2__default.useContext(RadioGroupContext);
|
|
842
|
+
if (!ctx)
|
|
843
|
+
throw new Error("DropdownRadioItem must be inside DropdownRadioGroup");
|
|
844
|
+
return ctx;
|
|
845
|
+
}
|
|
846
|
+
var DropdownRadioItem = ({
|
|
847
|
+
children,
|
|
848
|
+
value,
|
|
849
|
+
disabled = false,
|
|
850
|
+
shortcut,
|
|
851
|
+
closeOnSelect = false,
|
|
852
|
+
className,
|
|
853
|
+
onClick,
|
|
854
|
+
...props
|
|
855
|
+
}) => {
|
|
856
|
+
const { setOpen } = useDropdown();
|
|
857
|
+
const { value: groupValue, onValueChange } = useRadioGroup();
|
|
858
|
+
const checked = groupValue === value;
|
|
859
|
+
const handleClick = (e) => {
|
|
860
|
+
if (disabled) return;
|
|
861
|
+
onValueChange(value);
|
|
862
|
+
onClick?.(e);
|
|
863
|
+
if (closeOnSelect) setOpen(false);
|
|
864
|
+
};
|
|
865
|
+
const handleKeyDown = (e) => {
|
|
866
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
867
|
+
e.preventDefault();
|
|
868
|
+
handleClick(e);
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
return /* @__PURE__ */ jsxs(
|
|
872
|
+
"div",
|
|
873
|
+
{
|
|
874
|
+
...props,
|
|
875
|
+
role: "menuitemradio",
|
|
876
|
+
"aria-checked": checked,
|
|
877
|
+
tabIndex: disabled ? void 0 : -1,
|
|
878
|
+
"aria-disabled": disabled,
|
|
879
|
+
onClick: handleClick,
|
|
880
|
+
onKeyDown: handleKeyDown,
|
|
881
|
+
className: cn(
|
|
882
|
+
"relative mx-1.5 flex items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors outline-none select-none",
|
|
883
|
+
DROPDOWN_SHEET_MENU_TEXT,
|
|
884
|
+
!disabled && "cursor-pointer text-foreground hover:bg-primary/8 focus-visible:bg-primary/8 dark:hover:bg-primary/4 dark:focus-visible:bg-primary/4",
|
|
885
|
+
disabled && "lg:cursor-not-allowed text-foreground/45 dark:text-foreground/50",
|
|
886
|
+
className
|
|
887
|
+
),
|
|
888
|
+
children: [
|
|
889
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1", children }),
|
|
890
|
+
shortcut ? /* @__PURE__ */ jsx(
|
|
891
|
+
"span",
|
|
892
|
+
{
|
|
893
|
+
className: cn(
|
|
894
|
+
"shrink-0 text-xs tracking-widest opacity-40",
|
|
895
|
+
DROPDOWN_SHEET_MENU_SHORTCUT
|
|
896
|
+
),
|
|
897
|
+
children: shortcut
|
|
898
|
+
}
|
|
899
|
+
) : null,
|
|
900
|
+
/* @__PURE__ */ jsx(
|
|
901
|
+
"span",
|
|
902
|
+
{
|
|
903
|
+
className: "flex size-4 shrink-0 items-center justify-center",
|
|
904
|
+
"aria-hidden": true,
|
|
905
|
+
children: checked ? /* @__PURE__ */ jsx(Check, { className: "size-3.5", strokeWidth: 2.5 }) : null
|
|
906
|
+
}
|
|
907
|
+
)
|
|
908
|
+
]
|
|
909
|
+
}
|
|
910
|
+
);
|
|
911
|
+
};
|
|
912
|
+
var DropdownSub = ({
|
|
913
|
+
children
|
|
914
|
+
}) => {
|
|
915
|
+
const [open, setOpen] = useState(false);
|
|
916
|
+
const triggerRef = useRef(null);
|
|
917
|
+
const subCtx = useMemo(
|
|
918
|
+
() => ({ open, setOpen, triggerRef }),
|
|
919
|
+
[open, setOpen]
|
|
920
|
+
);
|
|
921
|
+
return /* @__PURE__ */ jsx(SubContext.Provider, { value: subCtx, children });
|
|
922
|
+
};
|
|
923
|
+
var DropdownSubTrigger = ({ children, icon, inset, disabled = false, className, ...props }) => {
|
|
924
|
+
const sub = React2__default.useContext(SubContext);
|
|
925
|
+
if (!sub) throw new Error("DropdownSubTrigger must be inside DropdownSub");
|
|
926
|
+
const { open, setOpen, triggerRef } = sub;
|
|
927
|
+
const isMobile = useIsMobile(DROPDOWN_MOBILE_SHEET_MAX_PX + 1);
|
|
928
|
+
return /* @__PURE__ */ jsxs(
|
|
929
|
+
"div",
|
|
930
|
+
{
|
|
931
|
+
...props,
|
|
932
|
+
ref: (el) => {
|
|
933
|
+
triggerRef.current = el;
|
|
934
|
+
},
|
|
935
|
+
role: "menuitem",
|
|
936
|
+
"aria-haspopup": "menu",
|
|
937
|
+
"aria-expanded": open,
|
|
938
|
+
tabIndex: disabled ? void 0 : -1,
|
|
939
|
+
"aria-disabled": disabled,
|
|
940
|
+
onMouseEnter: () => {
|
|
941
|
+
if (disabled || isMobile) return;
|
|
942
|
+
setOpen(true);
|
|
943
|
+
},
|
|
944
|
+
onMouseLeave: () => {
|
|
945
|
+
if (isMobile) return;
|
|
946
|
+
setOpen(false);
|
|
947
|
+
},
|
|
948
|
+
onKeyDown: (e) => {
|
|
949
|
+
if (disabled) return;
|
|
950
|
+
if (e.key === "ArrowRight" || e.key === "Enter") {
|
|
951
|
+
e.preventDefault();
|
|
952
|
+
setOpen(true);
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
onClick: () => {
|
|
956
|
+
if (disabled) return;
|
|
957
|
+
setOpen(!open);
|
|
958
|
+
},
|
|
959
|
+
className: cn(
|
|
960
|
+
"relative mx-1.5 flex items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors outline-none select-none",
|
|
961
|
+
DROPDOWN_SHEET_MENU_TEXT,
|
|
962
|
+
inset && "pl-9",
|
|
963
|
+
!disabled && "cursor-pointer text-foreground hover:bg-primary/8 focus-visible:bg-primary/8 dark:hover:bg-primary/4 dark:focus-visible:bg-primary/4",
|
|
964
|
+
disabled && "lg:cursor-not-allowed text-foreground/45 dark:text-foreground/50",
|
|
965
|
+
className
|
|
966
|
+
),
|
|
967
|
+
children: [
|
|
968
|
+
icon && /* @__PURE__ */ jsx("span", { className: "flex size-4 shrink-0 items-center justify-center", children: icon }),
|
|
969
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1", children }),
|
|
970
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: "ml-auto size-4 opacity-50" })
|
|
971
|
+
]
|
|
972
|
+
}
|
|
973
|
+
);
|
|
974
|
+
};
|
|
975
|
+
var DropdownSubContent = ({
|
|
976
|
+
children,
|
|
977
|
+
duration = 80,
|
|
978
|
+
viewportPadding = 8,
|
|
979
|
+
mobileSheet = true,
|
|
980
|
+
mobileTitle,
|
|
981
|
+
slideEntrance = true,
|
|
982
|
+
slideEntranceOffsetPx: slideEntranceOffsetPxProp,
|
|
983
|
+
className,
|
|
984
|
+
style,
|
|
985
|
+
onKeyDown: onKeyDownProp,
|
|
986
|
+
...props
|
|
987
|
+
}) => {
|
|
988
|
+
const sub = React2__default.useContext(SubContext);
|
|
989
|
+
if (!sub) throw new Error("DropdownSubContent must be inside DropdownSub");
|
|
990
|
+
const { open, setOpen, triggerRef } = sub;
|
|
991
|
+
const isMobile = useIsMobile(DROPDOWN_MOBILE_SHEET_MAX_PX + 1);
|
|
992
|
+
const slideOffsetPx = useMemo(
|
|
993
|
+
() => slideEntranceOffsetPxProp ?? DROPDOWN_MOBILE_SHEET_SLIDE_ENTRANCE_OFFSET_DEFAULT_PX,
|
|
994
|
+
[slideEntranceOffsetPxProp]
|
|
995
|
+
);
|
|
996
|
+
const [shouldRender, setShouldRender] = useState(false);
|
|
997
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
998
|
+
const [pos, setPos] = useState({
|
|
999
|
+
top: -9999,
|
|
1000
|
+
left: -9999,
|
|
1001
|
+
side: "right"
|
|
1002
|
+
});
|
|
1003
|
+
const menuRef = useRef(null);
|
|
1004
|
+
const closeDuration = isMobile && mobileSheet ? DROPDOWN_MOBILE_SHEET_MOTION_MS : duration;
|
|
1005
|
+
const closeSub = useCallback(() => setOpen(false), [setOpen]);
|
|
1006
|
+
useEffect(() => {
|
|
1007
|
+
if (open) {
|
|
1008
|
+
setShouldRender(true);
|
|
1009
|
+
} else {
|
|
1010
|
+
setIsAnimating(false);
|
|
1011
|
+
const timer = setTimeout(() => setShouldRender(false), closeDuration);
|
|
1012
|
+
return () => clearTimeout(timer);
|
|
1013
|
+
}
|
|
1014
|
+
}, [open, closeDuration]);
|
|
1015
|
+
useEffect(() => {
|
|
1016
|
+
if (!shouldRender || !open) return;
|
|
1017
|
+
let raf2 = 0;
|
|
1018
|
+
const raf1 = requestAnimationFrame(() => {
|
|
1019
|
+
raf2 = requestAnimationFrame(() => setIsAnimating(true));
|
|
1020
|
+
});
|
|
1021
|
+
return () => {
|
|
1022
|
+
cancelAnimationFrame(raf1);
|
|
1023
|
+
if (raf2) cancelAnimationFrame(raf2);
|
|
1024
|
+
};
|
|
1025
|
+
}, [shouldRender, open]);
|
|
1026
|
+
useLayoutEffect(() => {
|
|
1027
|
+
if (!shouldRender || !triggerRef.current || !menuRef.current) return;
|
|
1028
|
+
if (isMobile && mobileSheet) return;
|
|
1029
|
+
const update = () => {
|
|
1030
|
+
if (!triggerRef.current || !menuRef.current) return;
|
|
1031
|
+
setPos(
|
|
1032
|
+
computePos(
|
|
1033
|
+
triggerRef.current,
|
|
1034
|
+
menuRef.current,
|
|
1035
|
+
"right",
|
|
1036
|
+
"start",
|
|
1037
|
+
-8,
|
|
1038
|
+
viewportPadding
|
|
1039
|
+
)
|
|
1040
|
+
);
|
|
1041
|
+
};
|
|
1042
|
+
update();
|
|
1043
|
+
window.addEventListener("resize", update);
|
|
1044
|
+
window.addEventListener("scroll", update, true);
|
|
1045
|
+
return () => {
|
|
1046
|
+
window.removeEventListener("resize", update);
|
|
1047
|
+
window.removeEventListener("scroll", update, true);
|
|
1048
|
+
};
|
|
1049
|
+
}, [shouldRender, viewportPadding, isMobile, mobileSheet]);
|
|
1050
|
+
useEffect(() => {
|
|
1051
|
+
if (isAnimating && menuRef.current) {
|
|
1052
|
+
menuRef.current.focus();
|
|
1053
|
+
}
|
|
1054
|
+
}, [isAnimating]);
|
|
1055
|
+
useEffect(() => {
|
|
1056
|
+
if (!open) return;
|
|
1057
|
+
const handler = (e) => {
|
|
1058
|
+
const t = e.target;
|
|
1059
|
+
if (menuRef.current?.contains(t) || triggerRef.current?.contains(t))
|
|
1060
|
+
return;
|
|
1061
|
+
const el = e.target instanceof Element ? e.target : null;
|
|
1062
|
+
const subPanel = el?.closest?.(`[${DROPDOWN_SUB_CONTENT_ATTR}]`);
|
|
1063
|
+
if (subPanel && subPanel !== menuRef.current) return;
|
|
1064
|
+
setOpen(false);
|
|
1065
|
+
};
|
|
1066
|
+
document.addEventListener("mousedown", handler);
|
|
1067
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
1068
|
+
}, [open, setOpen, triggerRef]);
|
|
1069
|
+
const handleSubMenuKeyDown = useCallback(
|
|
1070
|
+
(e) => {
|
|
1071
|
+
onKeyDownProp?.(e);
|
|
1072
|
+
if (e.defaultPrevented) return;
|
|
1073
|
+
const menu = menuRef.current;
|
|
1074
|
+
if (!menu) return;
|
|
1075
|
+
const items = getItems(menu);
|
|
1076
|
+
const focused = document.activeElement;
|
|
1077
|
+
const idx = items.indexOf(focused);
|
|
1078
|
+
switch (e.key) {
|
|
1079
|
+
case "ArrowLeft":
|
|
1080
|
+
case "Escape":
|
|
1081
|
+
e.preventDefault();
|
|
1082
|
+
setOpen(false);
|
|
1083
|
+
triggerRef.current?.focus();
|
|
1084
|
+
break;
|
|
1085
|
+
case "ArrowDown":
|
|
1086
|
+
e.preventDefault();
|
|
1087
|
+
if (items.length === 0) break;
|
|
1088
|
+
if (idx === -1 || idx === items.length - 1) items[0]?.focus();
|
|
1089
|
+
else items[idx + 1]?.focus();
|
|
1090
|
+
break;
|
|
1091
|
+
case "ArrowUp":
|
|
1092
|
+
e.preventDefault();
|
|
1093
|
+
if (items.length === 0) break;
|
|
1094
|
+
if (idx <= 0) items[items.length - 1]?.focus();
|
|
1095
|
+
else items[idx - 1]?.focus();
|
|
1096
|
+
break;
|
|
1097
|
+
case "Home":
|
|
1098
|
+
e.preventDefault();
|
|
1099
|
+
items[0]?.focus();
|
|
1100
|
+
break;
|
|
1101
|
+
case "End":
|
|
1102
|
+
e.preventDefault();
|
|
1103
|
+
items[items.length - 1]?.focus();
|
|
1104
|
+
break;
|
|
1105
|
+
}
|
|
1106
|
+
},
|
|
1107
|
+
[onKeyDownProp, setOpen, triggerRef]
|
|
1108
|
+
);
|
|
1109
|
+
if (!shouldRender || typeof document === "undefined") return null;
|
|
1110
|
+
if (isMobile && mobileSheet) {
|
|
1111
|
+
return /* @__PURE__ */ jsx(
|
|
1112
|
+
DropdownMobileBottomSheetPortal,
|
|
1113
|
+
{
|
|
1114
|
+
...props,
|
|
1115
|
+
open,
|
|
1116
|
+
isAnimating,
|
|
1117
|
+
slideEntrance,
|
|
1118
|
+
slideOffsetPx,
|
|
1119
|
+
mobileTitle,
|
|
1120
|
+
onRequestClose: closeSub,
|
|
1121
|
+
menuRef,
|
|
1122
|
+
portalZClassName: "z-[70]",
|
|
1123
|
+
className,
|
|
1124
|
+
style,
|
|
1125
|
+
role: "menu",
|
|
1126
|
+
tabIndex: -1,
|
|
1127
|
+
onKeyDown: handleSubMenuKeyDown,
|
|
1128
|
+
"data-dropdown-sub-content": "",
|
|
1129
|
+
children
|
|
1130
|
+
}
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
return createPortal(
|
|
1134
|
+
/* @__PURE__ */ jsx(
|
|
1135
|
+
"div",
|
|
1136
|
+
{
|
|
1137
|
+
...props,
|
|
1138
|
+
ref: menuRef,
|
|
1139
|
+
role: "menu",
|
|
1140
|
+
tabIndex: -1,
|
|
1141
|
+
onMouseEnter: () => setOpen(true),
|
|
1142
|
+
onMouseLeave: () => setOpen(false),
|
|
1143
|
+
onKeyDown: handleSubMenuKeyDown,
|
|
1144
|
+
"data-dropdown-sub-content": "",
|
|
1145
|
+
className: cn(
|
|
1146
|
+
"bg-background border-primary/10 absolute z-60 overflow-hidden rounded-xl border py-1.5 shadow-xl outline-none",
|
|
1147
|
+
className
|
|
1148
|
+
),
|
|
1149
|
+
style: {
|
|
1150
|
+
position: "absolute",
|
|
1151
|
+
top: pos.top,
|
|
1152
|
+
left: pos.left,
|
|
1153
|
+
minWidth: DROPDOWN_MENU_MIN_WIDTH_PX,
|
|
1154
|
+
transformOrigin: SUB_CONTENT_ORIGIN[pos.side],
|
|
1155
|
+
transform: isAnimating ? "none" : DROPDOWN_CONTENT_HIDDEN[pos.side],
|
|
1156
|
+
opacity: isAnimating ? 1 : 0,
|
|
1157
|
+
transitionProperty: "opacity, transform",
|
|
1158
|
+
transitionDuration: `${duration}ms`,
|
|
1159
|
+
transitionTimingFunction: isAnimating ? DROPDOWN_PANEL_OPEN_EASING : DROPDOWN_PANEL_CLOSE_EASING,
|
|
1160
|
+
...style
|
|
1161
|
+
},
|
|
1162
|
+
children
|
|
1163
|
+
}
|
|
1164
|
+
),
|
|
1165
|
+
document.body
|
|
1166
|
+
);
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
export { Dropdown, DropdownCheckboxItem, DropdownChevron, DropdownContent, DropdownItem, DropdownLabel, DropdownMobileClose, DropdownRadioGroup, DropdownRadioItem, DropdownSeparator, DropdownSub, DropdownSubContent, DropdownSubTrigger, DropdownTrigger };
|
|
1170
|
+
//# sourceMappingURL=dropdown.js.map
|
|
1171
|
+
//# sourceMappingURL=dropdown.js.map
|