@hilum/ui 3.4.0 → 3.5.1
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/README.md +4 -0
- package/dist/{chunk-MAHPAP65.js → chunk-RI6QJR2L.js} +6 -3
- package/dist/chunk-RI6QJR2L.js.map +1 -0
- package/dist/index.d.ts +789 -115
- package/dist/index.js +10404 -1099
- package/dist/index.js.map +1 -1
- package/dist/tokens.css +17 -0
- package/dist/tokens.d.ts +3 -0
- package/dist/tokens.js +1 -1
- package/llms.txt +18 -4
- package/package.json +19 -1
- package/src/components/accordion.tsx +746 -58
- package/src/components/alert-dialog.tsx +11 -5
- package/src/components/ask-user-questions.tsx +1366 -0
- package/src/components/badge.tsx +105 -24
- package/src/components/button.tsx +207 -38
- package/src/components/card.tsx +3 -2
- package/src/components/chat-message.tsx +116 -0
- package/src/components/checkbox-group.tsx +403 -0
- package/src/components/checkbox.tsx +6 -3
- package/src/components/color-input.tsx +3 -1
- package/src/components/color-picker.tsx +1992 -80
- package/src/components/dialog.tsx +141 -81
- package/src/components/dropdown-menu.tsx +21 -13
- package/src/components/dropdown.tsx +245 -0
- package/src/components/file-thumbnail.tsx +156 -0
- package/src/components/hover-card.tsx +2 -0
- package/src/components/input-copy.tsx +307 -0
- package/src/components/input-group.tsx +303 -108
- package/src/components/input-message.tsx +883 -0
- package/src/components/input.tsx +6 -3
- package/src/components/menu-item.tsx +156 -0
- package/src/components/mobile-drawer.tsx +50 -0
- package/src/components/nav-item.tsx +43 -0
- package/src/components/nav-menu.tsx +47 -0
- package/src/components/popover.tsx +3 -1
- package/src/components/radio-group.tsx +381 -35
- package/src/components/scroll-area.tsx +9 -2
- package/src/components/select.tsx +708 -121
- package/src/components/sheet.tsx +15 -6
- package/src/components/slider.tsx +1583 -20
- package/src/components/status-badge.tsx +2 -0
- package/src/components/switch.tsx +245 -28
- package/src/components/table.tsx +191 -77
- package/src/components/tabs-subtle.tsx +411 -0
- package/src/components/tabs.tsx +459 -49
- package/src/components/textarea.tsx +7 -3
- package/src/components/thinking-indicator.tsx +106 -0
- package/src/components/thinking-steps.tsx +382 -0
- package/src/components/tooltip.tsx +158 -33
- package/src/hooks/use-merge-split.tsx +359 -0
- package/src/hooks/use-proximity-hover.ts +208 -0
- package/src/index.ts +14 -0
- package/src/lib/elevated.tsx +52 -0
- package/src/lib/font-weight.ts +18 -0
- package/src/lib/icon-context.tsx +98 -0
- package/src/lib/icon-map.tsx +669 -0
- package/src/lib/interaction.ts +102 -0
- package/src/lib/scroll-fade.tsx +241 -0
- package/src/lib/shape-context.tsx +121 -0
- package/src/lib/springs.ts +30 -0
- package/src/lib/surface-classes.ts +43 -0
- package/src/lib/surface-context.tsx +17 -0
- package/src/tokens/tokens.ts +3 -0
- package/dist/chunk-MAHPAP65.js.map +0 -1
|
@@ -1,110 +1,170 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import { Dialog } from "radix-ui";
|
|
5
|
-
import { X } from "lucide-react";
|
|
6
|
-
import { cn } from "../lib/utils";
|
|
7
3
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
4
|
+
createContext,
|
|
5
|
+
forwardRef,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useState,
|
|
9
|
+
type ComponentPropsWithoutRef,
|
|
10
|
+
type HTMLAttributes,
|
|
11
|
+
} from "react";
|
|
12
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
13
|
+
import { motion } from "framer-motion";
|
|
14
|
+
import { cn } from "../lib/utils";
|
|
15
|
+
import { useIcon } from "../lib/icon-context";
|
|
16
|
+
import { spring } from "../lib/springs";
|
|
17
|
+
import { useShape } from "../lib/shape-context";
|
|
18
|
+
import { SurfaceProvider, useSurface } from "../lib/surface-context";
|
|
19
|
+
import { surfaceClasses } from "../lib/surface-classes";
|
|
20
|
+
import { Button } from "./button";
|
|
21
|
+
|
|
22
|
+
const DIALOG_OFFSET = 4;
|
|
23
|
+
|
|
24
|
+
const DialogOpenContext = createContext(false);
|
|
25
|
+
|
|
26
|
+
function Dialog({
|
|
27
|
+
children,
|
|
28
|
+
open: controlledOpen,
|
|
29
|
+
defaultOpen = false,
|
|
30
|
+
onOpenChange,
|
|
31
|
+
...props
|
|
32
|
+
}: DialogPrimitive.DialogProps) {
|
|
33
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
34
|
+
const open = controlledOpen ?? uncontrolledOpen;
|
|
35
|
+
const handleOpenChange = onOpenChange ?? setUncontrolledOpen;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<DialogOpenContext.Provider value={open}>
|
|
39
|
+
<DialogPrimitive.Root open={open} onOpenChange={handleOpenChange} {...props}>
|
|
40
|
+
{children}
|
|
41
|
+
</DialogPrimitive.Root>
|
|
42
|
+
</DialogOpenContext.Provider>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DialogTrigger = DialogPrimitive.Trigger;
|
|
47
|
+
const DialogClose = DialogPrimitive.Close;
|
|
48
|
+
|
|
49
|
+
interface DialogContentProps extends ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
|
|
50
|
+
size?: "sm" | "lg";
|
|
51
|
+
/** Portal target. When set, the overlay and panel render inside this element
|
|
52
|
+
* (positioned `absolute`) instead of covering the viewport (`fixed`). Pair
|
|
53
|
+
* with a `position: relative; overflow: hidden` container — and usually
|
|
54
|
+
* `<Dialog modal={false}>` — to scope a dialog to a bounded region, e.g. a
|
|
55
|
+
* docs preview. Defaults to the document body / full-viewport behaviour. */
|
|
56
|
+
container?: HTMLElement | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const DialogContent = forwardRef<HTMLDivElement, DialogContentProps>(
|
|
60
|
+
({ className, children, size = "sm", container, ...props }, ref) => {
|
|
61
|
+
const XIcon = useIcon("x");
|
|
62
|
+
const open = useContext(DialogOpenContext);
|
|
63
|
+
const shape = useShape();
|
|
64
|
+
const substrate = useSurface();
|
|
65
|
+
const dialogLevel = Math.min(substrate + DIALOG_OFFSET, 8);
|
|
66
|
+
const [mounted, setMounted] = useState(false);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (open) setMounted(true);
|
|
70
|
+
}, [open]);
|
|
71
|
+
|
|
72
|
+
const handleExitComplete = () => {
|
|
73
|
+
if (!open) setMounted(false);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (!mounted) return null;
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<DialogPrimitive.Portal forceMount container={container ?? undefined}>
|
|
80
|
+
<DialogPrimitive.Overlay asChild forceMount>
|
|
81
|
+
<motion.div
|
|
82
|
+
className={cn(
|
|
83
|
+
container ? "absolute" : "fixed",
|
|
84
|
+
"inset-0 z-50 bg-black/40 dark:bg-black/80",
|
|
85
|
+
)}
|
|
86
|
+
initial={{ opacity: 0 }}
|
|
87
|
+
animate={{ opacity: open ? 1 : 0 }}
|
|
88
|
+
transition={open ? spring.slow : spring.slow.exit}
|
|
89
|
+
/>
|
|
90
|
+
</DialogPrimitive.Overlay>
|
|
91
|
+
<DialogPrimitive.Content ref={ref} asChild forceMount {...props}>
|
|
92
|
+
<motion.div
|
|
93
|
+
className={cn(
|
|
94
|
+
container ? "absolute" : "fixed",
|
|
95
|
+
"left-1/2 top-1/2 z-50 w-[calc(100%-2rem)]",
|
|
96
|
+
surfaceClasses(dialogLevel),
|
|
97
|
+
"p-6 focus:outline-none",
|
|
98
|
+
"rounded-t-2xl max-sm:pb-[calc(1.5rem+env(safe-area-inset-bottom))] data-[state=open]:slide-in-from-bottom sm:data-[state=open]:zoom-in-95",
|
|
99
|
+
size === "sm" && "max-w-[400px]",
|
|
100
|
+
size === "lg" && "max-w-[540px]",
|
|
101
|
+
shape.container,
|
|
102
|
+
"rounded-t-2xl",
|
|
103
|
+
className,
|
|
104
|
+
)}
|
|
105
|
+
initial={{ opacity: 0, scale: 0.97, x: "-50%", y: "-50%" }}
|
|
106
|
+
animate={{
|
|
107
|
+
opacity: open ? 1 : 0,
|
|
108
|
+
scale: open ? 1 : 0.97,
|
|
109
|
+
x: "-50%",
|
|
110
|
+
y: "-50%",
|
|
111
|
+
}}
|
|
112
|
+
transition={open ? spring.slow : spring.slow.exit}
|
|
113
|
+
onAnimationComplete={handleExitComplete}
|
|
114
|
+
>
|
|
115
|
+
<SurfaceProvider value={dialogLevel}>
|
|
116
|
+
{children}
|
|
117
|
+
<DialogPrimitive.Close asChild>
|
|
118
|
+
<Button variant="ghost" size="icon-sm" className="absolute right-3 top-3">
|
|
119
|
+
<XIcon />
|
|
120
|
+
<span className="sr-only">Close</span>
|
|
121
|
+
</Button>
|
|
122
|
+
</DialogPrimitive.Close>
|
|
123
|
+
</SurfaceProvider>
|
|
124
|
+
</motion.div>
|
|
125
|
+
</DialogPrimitive.Content>
|
|
126
|
+
</DialogPrimitive.Portal>
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
);
|
|
59
130
|
DialogContent.displayName = "DialogContent";
|
|
60
131
|
|
|
61
|
-
function DialogHeader({ className, ...props }:
|
|
132
|
+
function DialogHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
62
133
|
return <div className={cn("flex flex-col gap-1.5 mb-4", className)} {...props} />;
|
|
63
134
|
}
|
|
64
|
-
DialogHeader.displayName = "DialogHeader";
|
|
65
135
|
|
|
66
|
-
function DialogFooter({ className, ...props }:
|
|
67
|
-
return (
|
|
68
|
-
<div
|
|
69
|
-
className={cn(
|
|
70
|
-
"mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-end",
|
|
71
|
-
"[&>*]:w-full sm:[&>*]:w-auto",
|
|
72
|
-
className,
|
|
73
|
-
)}
|
|
74
|
-
{...props}
|
|
75
|
-
/>
|
|
76
|
-
);
|
|
136
|
+
function DialogFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
137
|
+
return <div className={cn("flex justify-end gap-2 mt-6", className)} {...props} />;
|
|
77
138
|
}
|
|
78
|
-
DialogFooter.displayName = "DialogFooter";
|
|
79
139
|
|
|
80
|
-
const DialogTitle =
|
|
81
|
-
|
|
82
|
-
|
|
140
|
+
const DialogTitle = forwardRef<
|
|
141
|
+
HTMLHeadingElement,
|
|
142
|
+
ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
83
143
|
>(({ className, ...props }, ref) => (
|
|
84
|
-
<
|
|
144
|
+
<DialogPrimitive.Title
|
|
85
145
|
ref={ref}
|
|
86
|
-
className={cn("
|
|
146
|
+
className={cn("text-[16px] text-foreground leading-tight", className)}
|
|
147
|
+
style={{ fontVariationSettings: "'wght' 700" }}
|
|
87
148
|
{...props}
|
|
88
149
|
/>
|
|
89
150
|
));
|
|
90
151
|
DialogTitle.displayName = "DialogTitle";
|
|
91
152
|
|
|
92
|
-
const DialogDescription =
|
|
93
|
-
|
|
94
|
-
|
|
153
|
+
const DialogDescription = forwardRef<
|
|
154
|
+
HTMLParagraphElement,
|
|
155
|
+
ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
95
156
|
>(({ className, ...props }, ref) => (
|
|
96
|
-
<
|
|
157
|
+
<DialogPrimitive.Description
|
|
97
158
|
ref={ref}
|
|
98
|
-
className={cn("
|
|
159
|
+
className={cn("text-[13px] text-muted-foreground", className)}
|
|
99
160
|
{...props}
|
|
100
161
|
/>
|
|
101
162
|
));
|
|
102
163
|
DialogDescription.displayName = "DialogDescription";
|
|
103
164
|
|
|
104
165
|
export {
|
|
105
|
-
|
|
166
|
+
Dialog,
|
|
106
167
|
DialogTrigger,
|
|
107
|
-
DialogOverlay,
|
|
108
168
|
DialogContent,
|
|
109
169
|
DialogHeader,
|
|
110
170
|
DialogFooter,
|
|
@@ -4,6 +4,12 @@ import * as React from "react";
|
|
|
4
4
|
import { DropdownMenu } from "radix-ui";
|
|
5
5
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
|
6
6
|
import { cn } from "../lib/utils";
|
|
7
|
+
import {
|
|
8
|
+
menuItemActiveClasses,
|
|
9
|
+
menuItemClasses,
|
|
10
|
+
motionClasses,
|
|
11
|
+
pressClasses,
|
|
12
|
+
} from "../lib/interaction";
|
|
7
13
|
import {
|
|
8
14
|
mobilePopperSheetMotionClassName,
|
|
9
15
|
mobilePopperSheetPositionClassName,
|
|
@@ -30,7 +36,7 @@ const DropdownMenuContent = React.forwardRef<
|
|
|
30
36
|
data-hilum-mobile-sheet="true"
|
|
31
37
|
sideOffset={sideOffset}
|
|
32
38
|
className={cn(
|
|
33
|
-
"z-50 min-w-[8rem]
|
|
39
|
+
"z-50 min-w-[8rem] rounded-xl border border-border bg-card p-1 shadow-elevated",
|
|
34
40
|
mobilePopperSheetPositionClassName,
|
|
35
41
|
mobilePopperSheetSurfaceClassName,
|
|
36
42
|
"max-sm:overflow-y-auto max-sm:px-2 max-sm:pb-2 max-sm:pt-5",
|
|
@@ -59,11 +65,13 @@ const DropdownMenuItem = React.forwardRef<
|
|
|
59
65
|
<DropdownMenu.Item
|
|
60
66
|
ref={ref}
|
|
61
67
|
className={cn(
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
menuItemClasses,
|
|
69
|
+
motionClasses,
|
|
70
|
+
pressClasses,
|
|
64
71
|
destructive
|
|
65
72
|
? "text-destructive focus:bg-destructive/10 focus:text-destructive"
|
|
66
73
|
: "text-muted-foreground focus:bg-muted focus:text-foreground",
|
|
74
|
+
!destructive && menuItemActiveClasses,
|
|
67
75
|
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
68
76
|
inset && "pl-8",
|
|
69
77
|
className,
|
|
@@ -80,9 +88,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|
|
80
88
|
<DropdownMenu.CheckboxItem
|
|
81
89
|
ref={ref}
|
|
82
90
|
className={cn(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"
|
|
91
|
+
menuItemClasses,
|
|
92
|
+
menuItemActiveClasses,
|
|
93
|
+
"py-2 pl-8 pr-2.5 text-muted-foreground",
|
|
86
94
|
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
87
95
|
className,
|
|
88
96
|
)}
|
|
@@ -105,9 +113,9 @@ const DropdownMenuRadioItem = React.forwardRef<
|
|
|
105
113
|
<DropdownMenu.RadioItem
|
|
106
114
|
ref={ref}
|
|
107
115
|
className={cn(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"
|
|
116
|
+
menuItemClasses,
|
|
117
|
+
menuItemActiveClasses,
|
|
118
|
+
"py-2 pl-8 pr-2.5 text-muted-foreground",
|
|
111
119
|
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
112
120
|
className,
|
|
113
121
|
)}
|
|
@@ -158,9 +166,9 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
|
|
158
166
|
<DropdownMenu.SubTrigger
|
|
159
167
|
ref={ref}
|
|
160
168
|
className={cn(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
"
|
|
169
|
+
menuItemClasses,
|
|
170
|
+
menuItemActiveClasses,
|
|
171
|
+
"text-muted-foreground data-[state=open]:bg-muted",
|
|
164
172
|
inset && "pl-8",
|
|
165
173
|
className,
|
|
166
174
|
)}
|
|
@@ -182,7 +190,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
|
|
182
190
|
ref={ref}
|
|
183
191
|
data-hilum-mobile-sheet="true"
|
|
184
192
|
className={cn(
|
|
185
|
-
"z-50 min-w-[8rem]
|
|
193
|
+
"z-50 min-w-[8rem] rounded-xl border border-border bg-card p-1 shadow-elevated",
|
|
186
194
|
mobilePopperSheetPositionClassName,
|
|
187
195
|
mobilePopperSheetSurfaceClassName,
|
|
188
196
|
"max-sm:overflow-y-auto max-sm:px-2 max-sm:pb-2 max-sm:pt-5",
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
useEffect,
|
|
7
|
+
createContext,
|
|
8
|
+
useContext,
|
|
9
|
+
forwardRef,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
type HTMLAttributes,
|
|
12
|
+
} from "react";
|
|
13
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
14
|
+
import { cn } from "../lib/utils";
|
|
15
|
+
import { spring } from "../lib/springs";
|
|
16
|
+
import { useProximityHover } from "../hooks/use-proximity-hover";
|
|
17
|
+
import { shapeMap } from "../lib/shape-context";
|
|
18
|
+
import { Elevated } from "../lib/elevated";
|
|
19
|
+
|
|
20
|
+
// Dropdown opts out of the global pill/rounded shape context — popover surfaces
|
|
21
|
+
// look cleaner with the smaller "rounded" radii regardless of how the rest of
|
|
22
|
+
// the UI is shaped (the heavy pill bubbling distorts perceived padding at this
|
|
23
|
+
// scale and produces the corner-shadow asymmetry).
|
|
24
|
+
const shape = shapeMap.rounded;
|
|
25
|
+
|
|
26
|
+
interface DropdownContextValue {
|
|
27
|
+
registerItem: (index: number, element: HTMLElement | null) => void;
|
|
28
|
+
activeIndex: number | null;
|
|
29
|
+
checkedIndex?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DropdownContext = createContext<DropdownContextValue | null>(null);
|
|
33
|
+
|
|
34
|
+
export function useDropdown() {
|
|
35
|
+
const ctx = useContext(DropdownContext);
|
|
36
|
+
return ctx ?? { registerItem: () => undefined, activeIndex: null };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface DropdownProps extends HTMLAttributes<HTMLDivElement> {
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
checkedIndex?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const Dropdown = forwardRef<HTMLDivElement, DropdownProps>(
|
|
45
|
+
({ children, checkedIndex, className, ...props }, ref) => {
|
|
46
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
47
|
+
const {
|
|
48
|
+
activeIndex,
|
|
49
|
+
setActiveIndex,
|
|
50
|
+
itemRects,
|
|
51
|
+
sessionRef,
|
|
52
|
+
handlers,
|
|
53
|
+
registerItem,
|
|
54
|
+
measureItems,
|
|
55
|
+
} = useProximityHover(containerRef);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
measureItems();
|
|
59
|
+
}, [measureItems, children]);
|
|
60
|
+
|
|
61
|
+
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
|
62
|
+
|
|
63
|
+
const activeRect = activeIndex !== null ? itemRects[activeIndex] : null;
|
|
64
|
+
const checkedRect = checkedIndex != null ? itemRects[checkedIndex] : null;
|
|
65
|
+
const focusRect = focusedIndex !== null ? itemRects[focusedIndex] : null;
|
|
66
|
+
const isHoveringOther = activeIndex !== null && activeIndex !== checkedIndex;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<DropdownContext.Provider
|
|
70
|
+
value={
|
|
71
|
+
checkedIndex === undefined
|
|
72
|
+
? { registerItem, activeIndex }
|
|
73
|
+
: { registerItem, activeIndex, checkedIndex }
|
|
74
|
+
}
|
|
75
|
+
>
|
|
76
|
+
<Elevated
|
|
77
|
+
offset={2}
|
|
78
|
+
shadowLevel={3}
|
|
79
|
+
ref={(node) => {
|
|
80
|
+
(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
81
|
+
if (typeof ref === "function") ref(node);
|
|
82
|
+
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
83
|
+
}}
|
|
84
|
+
onMouseEnter={handlers.onMouseEnter}
|
|
85
|
+
onMouseMove={handlers.onMouseMove}
|
|
86
|
+
onMouseLeave={handlers.onMouseLeave}
|
|
87
|
+
onFocus={(e) => {
|
|
88
|
+
const indexAttr = (e.target as HTMLElement)
|
|
89
|
+
.closest("[data-proximity-index]")
|
|
90
|
+
?.getAttribute("data-proximity-index");
|
|
91
|
+
if (indexAttr != null) {
|
|
92
|
+
const idx = Number(indexAttr);
|
|
93
|
+
setActiveIndex(idx);
|
|
94
|
+
setFocusedIndex((e.target as HTMLElement).matches(":focus-visible") ? idx : null);
|
|
95
|
+
}
|
|
96
|
+
}}
|
|
97
|
+
onBlur={(e) => {
|
|
98
|
+
if (containerRef.current?.contains(e.relatedTarget as Node)) return;
|
|
99
|
+
setFocusedIndex(null);
|
|
100
|
+
setActiveIndex(null);
|
|
101
|
+
}}
|
|
102
|
+
onKeyDown={(e) => {
|
|
103
|
+
const items = Array.from(
|
|
104
|
+
containerRef.current?.querySelectorAll('[role="menuitemradio"]') ?? [],
|
|
105
|
+
) as HTMLElement[];
|
|
106
|
+
const currentIdx = items.indexOf(e.target as HTMLElement);
|
|
107
|
+
if (currentIdx === -1) return;
|
|
108
|
+
|
|
109
|
+
if (["ArrowDown", "ArrowUp", "ArrowRight", "ArrowLeft"].includes(e.key)) {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
const next = ["ArrowDown", "ArrowRight"].includes(e.key)
|
|
112
|
+
? (currentIdx + 1) % items.length
|
|
113
|
+
: (currentIdx - 1 + items.length) % items.length;
|
|
114
|
+
items[next].focus();
|
|
115
|
+
} else if (e.key === "Home") {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
items[0]?.focus();
|
|
118
|
+
} else if (e.key === "End") {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
items[items.length - 1]?.focus();
|
|
121
|
+
}
|
|
122
|
+
}}
|
|
123
|
+
role="menu"
|
|
124
|
+
className={cn(
|
|
125
|
+
`relative flex flex-col gap-0.5 w-72 max-w-full ${shape.container} p-1 select-none`,
|
|
126
|
+
className,
|
|
127
|
+
)}
|
|
128
|
+
{...props}
|
|
129
|
+
>
|
|
130
|
+
{/* Selected background */}
|
|
131
|
+
<AnimatePresence>
|
|
132
|
+
{checkedRect && (
|
|
133
|
+
<motion.div
|
|
134
|
+
className={`absolute ${shape.bg} bg-active pointer-events-none`}
|
|
135
|
+
initial={false}
|
|
136
|
+
animate={{
|
|
137
|
+
top: checkedRect.top,
|
|
138
|
+
left: checkedRect.left,
|
|
139
|
+
width: checkedRect.width,
|
|
140
|
+
height: checkedRect.height,
|
|
141
|
+
opacity: isHoveringOther ? 0.8 : 1,
|
|
142
|
+
}}
|
|
143
|
+
exit={{ opacity: 0, transition: spring.moderate.exit }}
|
|
144
|
+
transition={{
|
|
145
|
+
...spring.moderate,
|
|
146
|
+
opacity: { duration: 0.08 },
|
|
147
|
+
}}
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
150
|
+
</AnimatePresence>
|
|
151
|
+
|
|
152
|
+
{/* Hover background */}
|
|
153
|
+
<AnimatePresence>
|
|
154
|
+
{activeRect && (
|
|
155
|
+
<motion.div
|
|
156
|
+
key={sessionRef.current}
|
|
157
|
+
className={`absolute ${shape.bg} bg-hover pointer-events-none`}
|
|
158
|
+
initial={{
|
|
159
|
+
opacity: 0,
|
|
160
|
+
top: checkedRect?.top ?? activeRect.top,
|
|
161
|
+
left: checkedRect?.left ?? activeRect.left,
|
|
162
|
+
width: checkedRect?.width ?? activeRect.width,
|
|
163
|
+
height: checkedRect?.height ?? activeRect.height,
|
|
164
|
+
}}
|
|
165
|
+
animate={{
|
|
166
|
+
opacity: 1,
|
|
167
|
+
top: activeRect.top,
|
|
168
|
+
left: activeRect.left,
|
|
169
|
+
width: activeRect.width,
|
|
170
|
+
height: activeRect.height,
|
|
171
|
+
}}
|
|
172
|
+
exit={{ opacity: 0, transition: spring.fast.exit }}
|
|
173
|
+
transition={{
|
|
174
|
+
...spring.fast,
|
|
175
|
+
opacity: { duration: 0.08 },
|
|
176
|
+
}}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
179
|
+
</AnimatePresence>
|
|
180
|
+
|
|
181
|
+
{/* Focus ring */}
|
|
182
|
+
<AnimatePresence>
|
|
183
|
+
{focusRect && (
|
|
184
|
+
<motion.div
|
|
185
|
+
className={`absolute ${shape.focusRing} pointer-events-none z-20 border border-[#6B97FF]`}
|
|
186
|
+
initial={false}
|
|
187
|
+
animate={{
|
|
188
|
+
left: focusRect.left - 2,
|
|
189
|
+
top: focusRect.top - 2,
|
|
190
|
+
width: focusRect.width + 4,
|
|
191
|
+
height: focusRect.height + 4,
|
|
192
|
+
}}
|
|
193
|
+
exit={{ opacity: 0, transition: spring.fast.exit }}
|
|
194
|
+
transition={{
|
|
195
|
+
...spring.fast,
|
|
196
|
+
opacity: { duration: 0.08 },
|
|
197
|
+
}}
|
|
198
|
+
/>
|
|
199
|
+
)}
|
|
200
|
+
</AnimatePresence>
|
|
201
|
+
|
|
202
|
+
{children}
|
|
203
|
+
</Elevated>
|
|
204
|
+
</DropdownContext.Provider>
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
Dropdown.displayName = "Dropdown";
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// DropdownLabel
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
const DropdownLabel = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
216
|
+
({ className, ...props }, ref) => (
|
|
217
|
+
<div
|
|
218
|
+
ref={ref}
|
|
219
|
+
className={cn("px-2 py-1.5 text-[11px] text-muted-foreground", className)}
|
|
220
|
+
{...props}
|
|
221
|
+
/>
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
DropdownLabel.displayName = "DropdownLabel";
|
|
226
|
+
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// DropdownSeparator
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
const DropdownSeparator = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
232
|
+
({ className, ...props }, ref) => (
|
|
233
|
+
<div
|
|
234
|
+
ref={ref}
|
|
235
|
+
role="separator"
|
|
236
|
+
className={cn("my-1 -mx-1 h-px bg-border/60", className)}
|
|
237
|
+
{...props}
|
|
238
|
+
/>
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
DropdownSeparator.displayName = "DropdownSeparator";
|
|
243
|
+
|
|
244
|
+
export { Dropdown, DropdownLabel, DropdownSeparator };
|
|
245
|
+
export default Dropdown;
|