@hilum/ui 3.3.4 → 3.5.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/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 +800 -122
- package/dist/index.js +10458 -1119
- 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-number.tsx +62 -21
- 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
|
@@ -7,17 +7,21 @@ interface InputNumberProps extends Omit<
|
|
|
7
7
|
React.ComponentProps<"input">,
|
|
8
8
|
"type" | "value" | "onChange"
|
|
9
9
|
> {
|
|
10
|
-
value: number;
|
|
10
|
+
value: number | null;
|
|
11
11
|
onChange: (next: number) => void;
|
|
12
|
-
min?: number;
|
|
13
|
-
max?: number;
|
|
14
|
-
step?: number;
|
|
12
|
+
min?: number | undefined;
|
|
13
|
+
max?: number | undefined;
|
|
14
|
+
step?: number | undefined;
|
|
15
15
|
/** Suffix shown after the number (e.g. "px", "mm", "°", "%"). */
|
|
16
|
-
unit?: string;
|
|
16
|
+
unit?: string | undefined;
|
|
17
17
|
/** Number of decimals to display. Default: 0. */
|
|
18
|
-
precision?: number;
|
|
18
|
+
precision?: number | undefined;
|
|
19
|
+
/** Text shown when value is null. Useful for mixed multi-selection values. */
|
|
20
|
+
mixedLabel?: string | undefined;
|
|
21
|
+
/** Calls onChange while typing instead of waiting for blur/Enter. */
|
|
22
|
+
commitOnChange?: boolean | undefined;
|
|
19
23
|
/** Hides the up/down stepper buttons. */
|
|
20
|
-
hideSteppers?: boolean;
|
|
24
|
+
hideSteppers?: boolean | undefined;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
/**
|
|
@@ -34,39 +38,59 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>(
|
|
|
34
38
|
step = 1,
|
|
35
39
|
unit,
|
|
36
40
|
precision = 0,
|
|
41
|
+
mixedLabel = "Mixed",
|
|
42
|
+
commitOnChange = false,
|
|
37
43
|
hideSteppers = false,
|
|
38
44
|
className,
|
|
39
45
|
disabled,
|
|
46
|
+
onBlur,
|
|
47
|
+
onFocus,
|
|
48
|
+
onKeyDown,
|
|
40
49
|
...rest
|
|
41
50
|
},
|
|
42
51
|
ref,
|
|
43
52
|
) => {
|
|
44
|
-
const
|
|
53
|
+
const formatValue = React.useCallback(
|
|
54
|
+
(next: number | null) => (next === null ? mixedLabel : next.toFixed(precision)),
|
|
55
|
+
[mixedLabel, precision],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const [text, setText] = React.useState<string>(formatValue(value));
|
|
45
59
|
|
|
46
60
|
React.useEffect(() => {
|
|
47
|
-
setText(value
|
|
48
|
-
}, [
|
|
61
|
+
setText(formatValue(value));
|
|
62
|
+
}, [formatValue, value]);
|
|
49
63
|
|
|
50
64
|
const clamp = React.useCallback((n: number) => Math.min(max, Math.max(min, n)), [min, max]);
|
|
51
65
|
|
|
52
|
-
const
|
|
66
|
+
const parseAndClamp = React.useCallback((raw: string) => {
|
|
53
67
|
const n = parseFloat(raw);
|
|
54
68
|
if (Number.isFinite(n)) {
|
|
55
|
-
|
|
56
|
-
onChange(next);
|
|
57
|
-
setText(next.toFixed(precision));
|
|
58
|
-
} else {
|
|
59
|
-
setText(value.toFixed(precision));
|
|
69
|
+
return clamp(n);
|
|
60
70
|
}
|
|
71
|
+
return null;
|
|
72
|
+
}, [clamp]);
|
|
73
|
+
|
|
74
|
+
const commit = (raw: string) => {
|
|
75
|
+
const next = parseAndClamp(raw);
|
|
76
|
+
if (next === null) {
|
|
77
|
+
setText(formatValue(value));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
onChange(next);
|
|
81
|
+
setText(next.toFixed(precision));
|
|
61
82
|
};
|
|
62
83
|
|
|
63
84
|
const bump = (delta: number) => {
|
|
64
|
-
const next = clamp(value + delta);
|
|
85
|
+
const next = clamp((value ?? 0) + delta);
|
|
65
86
|
onChange(next);
|
|
66
87
|
setText(next.toFixed(precision));
|
|
67
88
|
};
|
|
68
89
|
|
|
69
|
-
const
|
|
90
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
91
|
+
onKeyDown?.(e);
|
|
92
|
+
if (e.defaultPrevented) return;
|
|
93
|
+
|
|
70
94
|
if (e.key === "ArrowUp") {
|
|
71
95
|
e.preventDefault();
|
|
72
96
|
bump(step * (e.shiftKey ? 10 : 1));
|
|
@@ -93,9 +117,26 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>(
|
|
|
93
117
|
inputMode="decimal"
|
|
94
118
|
spellCheck={false}
|
|
95
119
|
value={text}
|
|
96
|
-
onChange={(e) =>
|
|
97
|
-
|
|
98
|
-
|
|
120
|
+
onChange={(e) => {
|
|
121
|
+
setText(e.target.value);
|
|
122
|
+
if (commitOnChange) {
|
|
123
|
+
const next = parseAndClamp(e.target.value);
|
|
124
|
+
if (next !== null) {
|
|
125
|
+
onChange(next);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}}
|
|
129
|
+
onFocus={(e) => {
|
|
130
|
+
if (value === null) {
|
|
131
|
+
setText("");
|
|
132
|
+
}
|
|
133
|
+
onFocus?.(e);
|
|
134
|
+
}}
|
|
135
|
+
onBlur={(e) => {
|
|
136
|
+
commit(e.target.value);
|
|
137
|
+
onBlur?.(e);
|
|
138
|
+
}}
|
|
139
|
+
onKeyDown={handleKeyDown}
|
|
99
140
|
disabled={disabled}
|
|
100
141
|
className="w-full caption text-foreground px-2 bg-transparent focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none text-right"
|
|
101
142
|
{...rest}
|
package/src/components/input.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { cn } from "../lib/utils";
|
|
3
|
+
import { controlSurfaceClasses, focusRingClasses, motionClasses } from "../lib/interaction";
|
|
3
4
|
|
|
4
5
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
5
6
|
return (
|
|
@@ -7,10 +8,12 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
|
7
8
|
type={type}
|
|
8
9
|
data-slot="input"
|
|
9
10
|
className={cn(
|
|
10
|
-
"flex h-10 w-full rounded-md
|
|
11
|
+
"flex h-10 w-full rounded-md px-3 py-1",
|
|
11
12
|
"body text-foreground placeholder:text-muted-foreground",
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
controlSurfaceClasses,
|
|
14
|
+
motionClasses,
|
|
15
|
+
focusRingClasses,
|
|
16
|
+
"focus-visible:border-brand-primary",
|
|
14
17
|
"disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-muted",
|
|
15
18
|
"file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground",
|
|
16
19
|
className,
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useRef,
|
|
5
|
+
useEffect,
|
|
6
|
+
forwardRef,
|
|
7
|
+
isValidElement,
|
|
8
|
+
type HTMLAttributes,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
|
+
import type { IconComponent } from "../lib/icon-context";
|
|
12
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
13
|
+
import { useDropdown } from "./dropdown";
|
|
14
|
+
import { cn } from "../lib/utils";
|
|
15
|
+
import { fontWeights } from "../lib/font-weight";
|
|
16
|
+
import { shapeMap } from "../lib/shape-context";
|
|
17
|
+
|
|
18
|
+
// MenuItem is only used inside Dropdown, which opts out of the global pill
|
|
19
|
+
// shape — see dropdown.tsx for the rationale.
|
|
20
|
+
const shape = shapeMap.rounded;
|
|
21
|
+
|
|
22
|
+
interface MenuItemProps extends HTMLAttributes<HTMLDivElement> {
|
|
23
|
+
/** Optional leading icon. When omitted, the row renders text-only with no
|
|
24
|
+
* reserved icon column. */
|
|
25
|
+
icon?: IconComponent | ReactNode;
|
|
26
|
+
label?: string;
|
|
27
|
+
index?: number;
|
|
28
|
+
checked?: boolean;
|
|
29
|
+
onSelect?: () => void;
|
|
30
|
+
trailing?: ReactNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const MenuItem = forwardRef<HTMLDivElement, MenuItemProps>(
|
|
34
|
+
(
|
|
35
|
+
{ icon: Icon, label, index = 0, checked, onSelect, trailing, className, children, ...props },
|
|
36
|
+
ref,
|
|
37
|
+
) => {
|
|
38
|
+
const internalRef = useRef<HTMLDivElement>(null);
|
|
39
|
+
const hasMounted = useRef(false);
|
|
40
|
+
const { registerItem, activeIndex, checkedIndex } = useDropdown();
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
registerItem(index, internalRef.current);
|
|
44
|
+
return () => registerItem(index, null);
|
|
45
|
+
}, [index, registerItem]);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
hasMounted.current = true;
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const isActive = activeIndex === index;
|
|
52
|
+
const skipAnimation = !hasMounted.current;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
ref={(node) => {
|
|
57
|
+
(internalRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
58
|
+
if (typeof ref === "function") ref(node);
|
|
59
|
+
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
60
|
+
}}
|
|
61
|
+
data-proximity-index={index}
|
|
62
|
+
tabIndex={index === (checkedIndex ?? 0) ? 0 : -1}
|
|
63
|
+
role="menuitemradio"
|
|
64
|
+
aria-checked={!!checked}
|
|
65
|
+
aria-label={label ?? (typeof children === "string" ? children : undefined)}
|
|
66
|
+
onClick={onSelect}
|
|
67
|
+
onKeyDown={(e) => {
|
|
68
|
+
if (e.key === " " || e.key === "Enter") {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
onSelect?.();
|
|
71
|
+
}
|
|
72
|
+
}}
|
|
73
|
+
className={cn(
|
|
74
|
+
`relative z-10 flex items-center gap-2 ${shape.item} px-2 py-2 cursor-pointer outline-none`,
|
|
75
|
+
className,
|
|
76
|
+
)}
|
|
77
|
+
{...props}
|
|
78
|
+
>
|
|
79
|
+
{Icon && typeof Icon === "function" && (
|
|
80
|
+
<span className="inline-grid">
|
|
81
|
+
<span className="col-start-1 row-start-1 invisible">
|
|
82
|
+
<Icon size={16} strokeWidth={2} />
|
|
83
|
+
</span>
|
|
84
|
+
<Icon
|
|
85
|
+
size={16}
|
|
86
|
+
strokeWidth={isActive || checked ? 2 : 1.5}
|
|
87
|
+
className={cn(
|
|
88
|
+
"col-start-1 row-start-1 transition-[color,stroke-width] duration-80",
|
|
89
|
+
isActive || checked ? "text-foreground" : "text-muted-foreground",
|
|
90
|
+
)}
|
|
91
|
+
/>
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
94
|
+
{Icon && isValidElement(Icon) && <span className="text-muted-foreground">{Icon}</span>}
|
|
95
|
+
<span className="inline-grid flex-1 text-[13px]">
|
|
96
|
+
<span
|
|
97
|
+
className="col-start-1 row-start-1 invisible"
|
|
98
|
+
style={{ fontVariationSettings: fontWeights.semibold }}
|
|
99
|
+
aria-hidden="true"
|
|
100
|
+
>
|
|
101
|
+
{label ?? children}
|
|
102
|
+
</span>
|
|
103
|
+
<span
|
|
104
|
+
className={cn(
|
|
105
|
+
"col-start-1 row-start-1 transition-[color,font-variation-settings] duration-80",
|
|
106
|
+
isActive || checked ? "text-foreground" : "text-muted-foreground",
|
|
107
|
+
)}
|
|
108
|
+
style={{
|
|
109
|
+
fontVariationSettings: checked ? fontWeights.semibold : fontWeights.normal,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
{label ?? children}
|
|
113
|
+
</span>
|
|
114
|
+
</span>
|
|
115
|
+
{trailing && <span className="text-[12px] text-muted-foreground">{trailing}</span>}
|
|
116
|
+
<AnimatePresence>
|
|
117
|
+
{checked && (
|
|
118
|
+
<motion.svg
|
|
119
|
+
key="check"
|
|
120
|
+
width={16}
|
|
121
|
+
height={16}
|
|
122
|
+
viewBox="0 0 24 24"
|
|
123
|
+
fill="none"
|
|
124
|
+
stroke="currentColor"
|
|
125
|
+
strokeWidth={2}
|
|
126
|
+
strokeLinecap="round"
|
|
127
|
+
strokeLinejoin="round"
|
|
128
|
+
className="text-foreground shrink-0"
|
|
129
|
+
initial={{ opacity: 1 }}
|
|
130
|
+
animate={{ opacity: 1 }}
|
|
131
|
+
exit={{ opacity: 1 }}
|
|
132
|
+
>
|
|
133
|
+
<motion.path
|
|
134
|
+
d="M4 12L9 17L20 6"
|
|
135
|
+
initial={{ pathLength: skipAnimation ? 1 : 0 }}
|
|
136
|
+
animate={{
|
|
137
|
+
pathLength: 1,
|
|
138
|
+
transition: { duration: 0.08, ease: "easeOut" },
|
|
139
|
+
}}
|
|
140
|
+
exit={{
|
|
141
|
+
pathLength: 0,
|
|
142
|
+
transition: { duration: 0.04, ease: "easeIn" },
|
|
143
|
+
}}
|
|
144
|
+
/>
|
|
145
|
+
</motion.svg>
|
|
146
|
+
)}
|
|
147
|
+
</AnimatePresence>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
MenuItem.displayName = "MenuItem";
|
|
154
|
+
|
|
155
|
+
export { MenuItem };
|
|
156
|
+
export default MenuItem;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Menu } from "lucide-react";
|
|
5
|
+
import { Button } from "./button";
|
|
6
|
+
import {
|
|
7
|
+
Sheet,
|
|
8
|
+
SheetContent,
|
|
9
|
+
SheetDescription,
|
|
10
|
+
SheetHeader,
|
|
11
|
+
SheetTitle,
|
|
12
|
+
SheetTrigger,
|
|
13
|
+
} from "./sheet";
|
|
14
|
+
|
|
15
|
+
interface MobileDrawerProps extends React.ComponentProps<typeof Sheet> {
|
|
16
|
+
title: React.ReactNode;
|
|
17
|
+
description?: React.ReactNode;
|
|
18
|
+
triggerLabel?: string;
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function MobileDrawer({
|
|
23
|
+
title,
|
|
24
|
+
description,
|
|
25
|
+
triggerLabel = "Open menu",
|
|
26
|
+
children,
|
|
27
|
+
...props
|
|
28
|
+
}: MobileDrawerProps) {
|
|
29
|
+
return (
|
|
30
|
+
<Sheet {...props}>
|
|
31
|
+
<SheetTrigger asChild>
|
|
32
|
+
<Button type="button" variant="ghost" size="icon" aria-label={triggerLabel}>
|
|
33
|
+
<Menu />
|
|
34
|
+
</Button>
|
|
35
|
+
</SheetTrigger>
|
|
36
|
+
<SheetContent side="left" className="w-[min(22rem,calc(100vw-2rem))] p-4">
|
|
37
|
+
<SheetHeader>
|
|
38
|
+
<SheetTitle>{title}</SheetTitle>
|
|
39
|
+
{description && <SheetDescription>{description}</SheetDescription>}
|
|
40
|
+
</SheetHeader>
|
|
41
|
+
{children}
|
|
42
|
+
</SheetContent>
|
|
43
|
+
</Sheet>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
MobileDrawer.displayName = "MobileDrawer";
|
|
48
|
+
|
|
49
|
+
export { MobileDrawer };
|
|
50
|
+
export type { MobileDrawerProps };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import {
|
|
4
|
+
focusRingClasses,
|
|
5
|
+
iconStrokeClasses,
|
|
6
|
+
pressClasses,
|
|
7
|
+
springMotionClasses,
|
|
8
|
+
} from "../lib/interaction";
|
|
9
|
+
|
|
10
|
+
interface NavItemProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
11
|
+
icon?: React.ReactNode;
|
|
12
|
+
active?: boolean;
|
|
13
|
+
trailing?: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const NavItem = React.forwardRef<HTMLAnchorElement, NavItemProps>(
|
|
17
|
+
({ icon, active, trailing, className, children, ...props }, ref) => (
|
|
18
|
+
<a
|
|
19
|
+
ref={ref}
|
|
20
|
+
aria-current={active ? "page" : undefined}
|
|
21
|
+
className={cn(
|
|
22
|
+
"group flex min-h-10 items-center gap-2 rounded-xl px-3 body font-medium",
|
|
23
|
+
active
|
|
24
|
+
? "bg-brand-secondary/35 text-foreground"
|
|
25
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
26
|
+
springMotionClasses,
|
|
27
|
+
pressClasses,
|
|
28
|
+
focusRingClasses,
|
|
29
|
+
iconStrokeClasses,
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
{icon && <span className="flex size-4 shrink-0 items-center justify-center">{icon}</span>}
|
|
35
|
+
<span className="min-w-0 flex-1 truncate">{children}</span>
|
|
36
|
+
{trailing && <span className="shrink-0 text-muted-foreground">{trailing}</span>}
|
|
37
|
+
</a>
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
NavItem.displayName = "NavItem";
|
|
41
|
+
|
|
42
|
+
export { NavItem };
|
|
43
|
+
export type { NavItemProps };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { useProximityIndex } from "../lib/interaction";
|
|
4
|
+
import { NavItem } from "./nav-item";
|
|
5
|
+
|
|
6
|
+
export interface NavMenuItem {
|
|
7
|
+
href: string;
|
|
8
|
+
label: React.ReactNode;
|
|
9
|
+
icon?: React.ReactNode;
|
|
10
|
+
active?: boolean;
|
|
11
|
+
trailing?: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface NavMenuProps extends React.HTMLAttributes<HTMLElement> {
|
|
15
|
+
items: NavMenuItem[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function NavMenu({ items, className, ...props }: NavMenuProps) {
|
|
19
|
+
const proximity = useProximityIndex<HTMLElement>("y");
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<nav
|
|
23
|
+
ref={proximity.containerRef}
|
|
24
|
+
className={cn("grid gap-1", className)}
|
|
25
|
+
{...proximity.handlers}
|
|
26
|
+
{...props}
|
|
27
|
+
>
|
|
28
|
+
{items.map((item, index) => (
|
|
29
|
+
<NavItem
|
|
30
|
+
key={item.href}
|
|
31
|
+
ref={(node) => proximity.registerItem(index, node)}
|
|
32
|
+
href={item.href}
|
|
33
|
+
icon={item.icon}
|
|
34
|
+
active={item.active || proximity.activeIndex === index}
|
|
35
|
+
trailing={item.trailing}
|
|
36
|
+
>
|
|
37
|
+
{item.label}
|
|
38
|
+
</NavItem>
|
|
39
|
+
))}
|
|
40
|
+
</nav>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
NavMenu.displayName = "NavMenu";
|
|
45
|
+
|
|
46
|
+
export { NavMenu };
|
|
47
|
+
export type { NavMenuProps };
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Popover } from "radix-ui";
|
|
5
5
|
import { cn } from "../lib/utils";
|
|
6
|
+
import { motionClasses } from "../lib/interaction";
|
|
6
7
|
import {
|
|
7
8
|
mobilePopperSheetMotionClassName,
|
|
8
9
|
mobilePopperSheetPositionClassName,
|
|
@@ -27,7 +28,8 @@ const PopoverContent = React.forwardRef<
|
|
|
27
28
|
align={align}
|
|
28
29
|
sideOffset={sideOffset}
|
|
29
30
|
className={cn(
|
|
30
|
-
"relative z-50 w-72 rounded-xl bg-card p-4 shadow-
|
|
31
|
+
"relative z-50 w-72 rounded-xl border border-border bg-card p-4 shadow-elevated outline-none",
|
|
32
|
+
motionClasses,
|
|
31
33
|
mobilePopperSheetPositionClassName,
|
|
32
34
|
mobilePopperSheetSurfaceClassName,
|
|
33
35
|
"max-sm:overflow-y-auto max-sm:px-4 max-sm:pb-4 max-sm:pt-6",
|