@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
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRef, forwardRef, type ReactNode, type HTMLAttributes } from "react";
|
|
4
|
+
import { motion } from "framer-motion";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
import { useIcon } from "../lib/icon-context";
|
|
7
|
+
import type { IconName } from "../lib/icon-context";
|
|
8
|
+
import { spring } from "../lib/springs";
|
|
9
|
+
import { fontWeights } from "../lib/font-weight";
|
|
10
|
+
import { useShape } from "../lib/shape-context";
|
|
11
|
+
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "./accordion";
|
|
12
|
+
import { Badge } from "./badge";
|
|
13
|
+
import type { BadgeColor } from "./badge";
|
|
14
|
+
|
|
15
|
+
// ─── ThinkingSteps (root) ───────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
interface ThinkingStepsProps extends HTMLAttributes<HTMLDivElement> {
|
|
18
|
+
defaultOpen?: boolean;
|
|
19
|
+
open?: boolean;
|
|
20
|
+
onOpenChange?: (open: boolean) => void;
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
steps?: Array<{
|
|
23
|
+
label: string;
|
|
24
|
+
status?: ThinkingStepStatus | "current";
|
|
25
|
+
description?: string;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ThinkingSteps = forwardRef<HTMLDivElement, ThinkingStepsProps>(
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
31
|
+
(
|
|
32
|
+
{
|
|
33
|
+
defaultOpen = true,
|
|
34
|
+
open,
|
|
35
|
+
onOpenChange,
|
|
36
|
+
children,
|
|
37
|
+
steps,
|
|
38
|
+
className,
|
|
39
|
+
defaultValue: _,
|
|
40
|
+
...props
|
|
41
|
+
},
|
|
42
|
+
ref,
|
|
43
|
+
) => {
|
|
44
|
+
const controlled = open !== undefined;
|
|
45
|
+
const content =
|
|
46
|
+
children ??
|
|
47
|
+
(steps ? (
|
|
48
|
+
<>
|
|
49
|
+
<ThinkingStepsHeader>Thinking</ThinkingStepsHeader>
|
|
50
|
+
<ThinkingStepsContent>
|
|
51
|
+
{steps.map((step, index) => (
|
|
52
|
+
<ThinkingStep
|
|
53
|
+
key={`${step.label}-${index}`}
|
|
54
|
+
label={step.label}
|
|
55
|
+
{...(step.description !== undefined ? { description: step.description } : {})}
|
|
56
|
+
{...(step.status !== undefined
|
|
57
|
+
? { status: step.status === "current" ? "active" : step.status }
|
|
58
|
+
: {})}
|
|
59
|
+
index={index}
|
|
60
|
+
isLast={index === steps.length - 1}
|
|
61
|
+
/>
|
|
62
|
+
))}
|
|
63
|
+
</ThinkingStepsContent>
|
|
64
|
+
</>
|
|
65
|
+
) : null);
|
|
66
|
+
return (
|
|
67
|
+
<Accordion
|
|
68
|
+
ref={ref}
|
|
69
|
+
type="single"
|
|
70
|
+
collapsible
|
|
71
|
+
{...(controlled
|
|
72
|
+
? { value: open ? "thinking" : "" }
|
|
73
|
+
: { defaultValue: defaultOpen ? "thinking" : "" })}
|
|
74
|
+
{...(onOpenChange
|
|
75
|
+
? { onValueChange: (v: string | string[]) => onOpenChange(v === "thinking") }
|
|
76
|
+
: {})}
|
|
77
|
+
className={cn("w-80 max-w-full", className)}
|
|
78
|
+
{...props}
|
|
79
|
+
>
|
|
80
|
+
{/* Hide standalone accordion expanded bg */}
|
|
81
|
+
<AccordionItem value="thinking" className="[&>.absolute]:hidden">
|
|
82
|
+
{content}
|
|
83
|
+
</AccordionItem>
|
|
84
|
+
</Accordion>
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
ThinkingSteps.displayName = "ThinkingSteps";
|
|
89
|
+
|
|
90
|
+
// ─── ThinkingStepsHeader ────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
interface ThinkingStepsHeaderProps extends HTMLAttributes<HTMLButtonElement> {
|
|
93
|
+
children?: ReactNode;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const ThinkingStepsHeader = forwardRef<HTMLButtonElement, ThinkingStepsHeaderProps>(
|
|
97
|
+
({ children = "Thinking", className, ...props }, ref) => {
|
|
98
|
+
return (
|
|
99
|
+
<div className="w-fit">
|
|
100
|
+
<AccordionTrigger
|
|
101
|
+
ref={ref}
|
|
102
|
+
className={cn("[&>span:first-child]:flex-none w-auto", className)}
|
|
103
|
+
{...props}
|
|
104
|
+
>
|
|
105
|
+
{children}
|
|
106
|
+
</AccordionTrigger>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
ThinkingStepsHeader.displayName = "ThinkingStepsHeader";
|
|
112
|
+
|
|
113
|
+
// ─── ThinkingStepsContent ───────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
interface ThinkingStepsContentProps extends HTMLAttributes<HTMLDivElement> {
|
|
116
|
+
children: ReactNode;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const ThinkingStepsContent = forwardRef<HTMLDivElement, ThinkingStepsContentProps>(
|
|
120
|
+
({ children, className, ...props }, ref) => {
|
|
121
|
+
return (
|
|
122
|
+
<AccordionContent>
|
|
123
|
+
<div ref={ref} className={cn("flex flex-col", className)} {...props}>
|
|
124
|
+
{children}
|
|
125
|
+
</div>
|
|
126
|
+
</AccordionContent>
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
ThinkingStepsContent.displayName = "ThinkingStepsContent";
|
|
131
|
+
|
|
132
|
+
// ─── ThinkingStep ───────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
type ThinkingStepStatus = "complete" | "active" | "pending";
|
|
135
|
+
|
|
136
|
+
interface ThinkingStepProps {
|
|
137
|
+
icon?: IconName;
|
|
138
|
+
showIcon?: boolean;
|
|
139
|
+
label: string;
|
|
140
|
+
description?: string;
|
|
141
|
+
status?: ThinkingStepStatus;
|
|
142
|
+
index: number;
|
|
143
|
+
delay?: number;
|
|
144
|
+
isLast?: boolean;
|
|
145
|
+
children?: ReactNode;
|
|
146
|
+
className?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function ThinkingStep({
|
|
150
|
+
icon = "dot",
|
|
151
|
+
showIcon = true,
|
|
152
|
+
label,
|
|
153
|
+
description,
|
|
154
|
+
status = "complete",
|
|
155
|
+
index,
|
|
156
|
+
delay = 0,
|
|
157
|
+
isLast = false,
|
|
158
|
+
children,
|
|
159
|
+
className,
|
|
160
|
+
}: ThinkingStepProps) {
|
|
161
|
+
const Icon = useIcon(icon);
|
|
162
|
+
const shape = useShape();
|
|
163
|
+
|
|
164
|
+
if (status === "pending") return null;
|
|
165
|
+
|
|
166
|
+
const isActive = status === "active";
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
/* Outer: animates height to create space smoothly */
|
|
170
|
+
<motion.div
|
|
171
|
+
className={cn("relative z-10 overflow-hidden", className)}
|
|
172
|
+
initial={{ height: 0 }}
|
|
173
|
+
animate={{ height: "auto" }}
|
|
174
|
+
transition={spring.slow}
|
|
175
|
+
>
|
|
176
|
+
{/* Inner: fades content in after space starts opening */}
|
|
177
|
+
<motion.div
|
|
178
|
+
initial={{ opacity: 0 }}
|
|
179
|
+
animate={{ opacity: 1 }}
|
|
180
|
+
transition={{ duration: 0.24, delay: 0.08, ease: "easeOut" }}
|
|
181
|
+
>
|
|
182
|
+
{/* Content row — this is the proximity hover target */}
|
|
183
|
+
<div className={cn("flex gap-2.5 px-2 py-1.5", shape.item)}>
|
|
184
|
+
{/* Icon column with continuous connector line */}
|
|
185
|
+
<div className="flex flex-col items-center shrink-0 w-[14px]">
|
|
186
|
+
<div className="pt-0.5">
|
|
187
|
+
{showIcon ? (
|
|
188
|
+
<Icon size={14} strokeWidth={1.5} className="text-muted-foreground" />
|
|
189
|
+
) : (
|
|
190
|
+
<div className="w-[14px] h-[14px] flex items-center justify-center">
|
|
191
|
+
<div className="w-1.5 h-1.5 rounded-full bg-muted-foreground/60" />
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
{/* Line stretches from icon to bottom of this step */}
|
|
196
|
+
{!isLast && <div className="flex-1 w-px bg-border/60 mt-1" />}
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{/* Text content */}
|
|
200
|
+
<div className="flex-1 flex flex-col gap-1 min-w-0">
|
|
201
|
+
<span
|
|
202
|
+
className={cn(
|
|
203
|
+
"text-[13px] leading-tight text-foreground",
|
|
204
|
+
isActive && "shimmer-text",
|
|
205
|
+
)}
|
|
206
|
+
style={{ fontVariationSettings: fontWeights.medium }}
|
|
207
|
+
>
|
|
208
|
+
{label}
|
|
209
|
+
</span>
|
|
210
|
+
{description && (
|
|
211
|
+
<span className="text-[13px] text-muted-foreground leading-snug">{description}</span>
|
|
212
|
+
)}
|
|
213
|
+
{children}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</motion.div>
|
|
217
|
+
</motion.div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ─── ThinkingStepDetails (nested accordion) ────────────────────────────────
|
|
222
|
+
|
|
223
|
+
interface ThinkingStepDetailsProps {
|
|
224
|
+
summary: string;
|
|
225
|
+
details?: string[];
|
|
226
|
+
defaultOpen?: boolean;
|
|
227
|
+
children?: ReactNode;
|
|
228
|
+
className?: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function ThinkingStepDetails({
|
|
232
|
+
summary,
|
|
233
|
+
details,
|
|
234
|
+
defaultOpen = false,
|
|
235
|
+
children,
|
|
236
|
+
className,
|
|
237
|
+
}: ThinkingStepDetailsProps) {
|
|
238
|
+
const shape = useShape();
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<Accordion
|
|
242
|
+
type="single"
|
|
243
|
+
collapsible
|
|
244
|
+
defaultValue={defaultOpen ? "details" : ""}
|
|
245
|
+
className={cn("mt-1 -ml-3", className)}
|
|
246
|
+
>
|
|
247
|
+
<AccordionItem value="details" className="[&>.absolute]:hidden">
|
|
248
|
+
<div className="w-fit">
|
|
249
|
+
<AccordionTrigger
|
|
250
|
+
className={cn("[&>span:first-child]:flex-none w-auto py-1 px-3 gap-1.5", shape.item)}
|
|
251
|
+
>
|
|
252
|
+
{summary}
|
|
253
|
+
</AccordionTrigger>
|
|
254
|
+
</div>
|
|
255
|
+
<AccordionContent>
|
|
256
|
+
<div className="flex flex-col gap-0.5 pt-0.5">
|
|
257
|
+
{details?.map((item, i) => (
|
|
258
|
+
<span key={i} className="text-[12px] text-muted-foreground leading-snug">
|
|
259
|
+
{item}
|
|
260
|
+
</span>
|
|
261
|
+
))}
|
|
262
|
+
{children}
|
|
263
|
+
</div>
|
|
264
|
+
</AccordionContent>
|
|
265
|
+
</AccordionItem>
|
|
266
|
+
</Accordion>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ─── ThinkingStepSources ────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
interface ThinkingStepSourcesProps extends HTMLAttributes<HTMLDivElement> {
|
|
273
|
+
children: ReactNode;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const ThinkingStepSources = forwardRef<HTMLDivElement, ThinkingStepSourcesProps>(
|
|
277
|
+
({ children, className, ...props }, ref) => {
|
|
278
|
+
return (
|
|
279
|
+
<div ref={ref} className={cn("flex flex-wrap gap-1.5 mt-1", className)} {...props}>
|
|
280
|
+
{children}
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
ThinkingStepSources.displayName = "ThinkingStepSources";
|
|
286
|
+
|
|
287
|
+
// ─── ThinkingStepSource ─────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
interface ThinkingStepSourceProps {
|
|
290
|
+
color?: BadgeColor;
|
|
291
|
+
delay?: number;
|
|
292
|
+
children: ReactNode;
|
|
293
|
+
className?: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function ThinkingStepSource({
|
|
297
|
+
color = "gray",
|
|
298
|
+
delay = 0,
|
|
299
|
+
children,
|
|
300
|
+
className,
|
|
301
|
+
}: ThinkingStepSourceProps) {
|
|
302
|
+
return (
|
|
303
|
+
<motion.span
|
|
304
|
+
initial={{ opacity: 0, scale: 0.85, filter: "blur(4px)" }}
|
|
305
|
+
animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
|
|
306
|
+
transition={{
|
|
307
|
+
...spring.moderate,
|
|
308
|
+
delay,
|
|
309
|
+
filter: { duration: 0.12, delay },
|
|
310
|
+
}}
|
|
311
|
+
>
|
|
312
|
+
<Badge variant="solid" size="sm" color={color} className={className}>
|
|
313
|
+
{children}
|
|
314
|
+
</Badge>
|
|
315
|
+
</motion.span>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
ThinkingStepSource.displayName = "ThinkingStepSource";
|
|
319
|
+
|
|
320
|
+
// ─── ThinkingStepImage ──────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
interface ThinkingStepImageProps {
|
|
323
|
+
src: string;
|
|
324
|
+
alt?: string;
|
|
325
|
+
caption?: string;
|
|
326
|
+
delay?: number;
|
|
327
|
+
className?: string;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function ThinkingStepImage({
|
|
331
|
+
src,
|
|
332
|
+
alt = "",
|
|
333
|
+
caption,
|
|
334
|
+
delay = 0,
|
|
335
|
+
className,
|
|
336
|
+
}: ThinkingStepImageProps) {
|
|
337
|
+
const shape = useShape();
|
|
338
|
+
return (
|
|
339
|
+
<motion.div
|
|
340
|
+
className={cn("mt-1.5", className)}
|
|
341
|
+
initial={{ opacity: 0, filter: "blur(4px)" }}
|
|
342
|
+
animate={{ opacity: 1, filter: "blur(0px)" }}
|
|
343
|
+
transition={{
|
|
344
|
+
opacity: { duration: 0.2, delay, ease: "easeOut" },
|
|
345
|
+
filter: { duration: 0.15, delay },
|
|
346
|
+
}}
|
|
347
|
+
>
|
|
348
|
+
<img
|
|
349
|
+
src={src}
|
|
350
|
+
alt={alt}
|
|
351
|
+
className={cn("w-full max-w-[200px] object-cover", shape.container)}
|
|
352
|
+
/>
|
|
353
|
+
{caption && <span className="text-[11px] text-muted-foreground mt-1 block">{caption}</span>}
|
|
354
|
+
</motion.div>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
ThinkingStepImage.displayName = "ThinkingStepImage";
|
|
358
|
+
|
|
359
|
+
// ─── Exports ────────────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
export {
|
|
362
|
+
ThinkingSteps,
|
|
363
|
+
ThinkingStepsHeader,
|
|
364
|
+
ThinkingStepsContent,
|
|
365
|
+
ThinkingStep,
|
|
366
|
+
ThinkingStepDetails,
|
|
367
|
+
ThinkingStepSources,
|
|
368
|
+
ThinkingStepSource,
|
|
369
|
+
ThinkingStepImage,
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
export type {
|
|
373
|
+
ThinkingStepsProps,
|
|
374
|
+
ThinkingStepsHeaderProps,
|
|
375
|
+
ThinkingStepsContentProps,
|
|
376
|
+
ThinkingStepProps,
|
|
377
|
+
ThinkingStepDetailsProps,
|
|
378
|
+
ThinkingStepSourcesProps,
|
|
379
|
+
ThinkingStepSourceProps,
|
|
380
|
+
ThinkingStepImageProps,
|
|
381
|
+
ThinkingStepStatus,
|
|
382
|
+
};
|
|
@@ -1,43 +1,168 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
|
|
4
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
5
|
+
import { motion } from "framer-motion";
|
|
5
6
|
import { cn } from "../lib/utils";
|
|
7
|
+
import { spring } from "../lib/springs";
|
|
8
|
+
import { fontWeights } from "../lib/font-weight";
|
|
9
|
+
import { useShape } from "../lib/shape-context";
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Portal container context
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
const TooltipPortalContainerContext = createContext<HTMLElement | null>(null);
|
|
16
|
+
|
|
17
|
+
function TooltipPortalContainer({
|
|
18
|
+
value,
|
|
8
19
|
children,
|
|
9
|
-
|
|
10
|
-
|
|
20
|
+
}: {
|
|
21
|
+
value: HTMLElement | null;
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
}) {
|
|
11
24
|
return (
|
|
12
|
-
<
|
|
25
|
+
<TooltipPortalContainerContext.Provider value={value}>
|
|
13
26
|
{children}
|
|
14
|
-
</
|
|
27
|
+
</TooltipPortalContainerContext.Provider>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Types
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
type TooltipSide = "top" | "right" | "bottom" | "left";
|
|
36
|
+
|
|
37
|
+
interface TooltipProps {
|
|
38
|
+
content?: ReactNode;
|
|
39
|
+
children: ReactNode;
|
|
40
|
+
side?: TooltipSide;
|
|
41
|
+
sideOffset?: number;
|
|
42
|
+
delayDuration?: number;
|
|
43
|
+
className?: string;
|
|
44
|
+
open?: boolean | undefined;
|
|
45
|
+
/** When true, forces the tooltip open. When false, forces it closed. When undefined, uses default hover/focus behavior. */
|
|
46
|
+
forceOpen?: boolean | undefined;
|
|
47
|
+
/** Called when the tooltip's internal open state changes (before forceOpen is applied). */
|
|
48
|
+
onOpenChange?: (open: boolean) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Animation helpers
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
function getSlideOffset(side: TooltipSide) {
|
|
56
|
+
switch (side) {
|
|
57
|
+
case "top":
|
|
58
|
+
return { y: 4 };
|
|
59
|
+
case "bottom":
|
|
60
|
+
return { y: -4 };
|
|
61
|
+
case "left":
|
|
62
|
+
return { x: 4 };
|
|
63
|
+
case "right":
|
|
64
|
+
return { x: -4 };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Tooltip
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
function Tooltip({
|
|
73
|
+
content,
|
|
74
|
+
children,
|
|
75
|
+
side = "top",
|
|
76
|
+
sideOffset = 8,
|
|
77
|
+
delayDuration = 200,
|
|
78
|
+
className,
|
|
79
|
+
open: openProp,
|
|
80
|
+
forceOpen,
|
|
81
|
+
onOpenChange: onOpenChangeProp,
|
|
82
|
+
}: TooltipProps) {
|
|
83
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
84
|
+
if (content === undefined) {
|
|
85
|
+
return (
|
|
86
|
+
<TooltipPrimitive.Root
|
|
87
|
+
{...(openProp !== undefined ? { open: openProp } : {})}
|
|
88
|
+
{...(onOpenChangeProp ? { onOpenChange: onOpenChangeProp } : {})}
|
|
89
|
+
>
|
|
90
|
+
{children}
|
|
91
|
+
</TooltipPrimitive.Root>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const open = forceOpen ?? openProp ?? internalOpen;
|
|
95
|
+
const [mounted, setMounted] = useState(false);
|
|
96
|
+
const shape = useShape();
|
|
97
|
+
const portalContainer = useContext(TooltipPortalContainerContext);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (open) setMounted(true);
|
|
101
|
+
}, [open]);
|
|
102
|
+
|
|
103
|
+
const handleExitComplete = () => {
|
|
104
|
+
if (!open) setMounted(false);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const slideOffset = getSlideOffset(side);
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<TooltipPrimitive.Provider delayDuration={delayDuration}>
|
|
111
|
+
<TooltipPrimitive.Root
|
|
112
|
+
open={open}
|
|
113
|
+
onOpenChange={(v) => {
|
|
114
|
+
setInternalOpen(v);
|
|
115
|
+
onOpenChangeProp?.(v);
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
<TooltipPrimitive.Trigger asChild>
|
|
119
|
+
{children as React.ReactElement}
|
|
120
|
+
</TooltipPrimitive.Trigger>
|
|
121
|
+
{mounted && (
|
|
122
|
+
<TooltipPrimitive.Portal forceMount container={portalContainer ?? undefined}>
|
|
123
|
+
<TooltipPrimitive.Content
|
|
124
|
+
side={side}
|
|
125
|
+
sideOffset={sideOffset}
|
|
126
|
+
forceMount
|
|
127
|
+
className="z-50"
|
|
128
|
+
>
|
|
129
|
+
<motion.div
|
|
130
|
+
className={cn(
|
|
131
|
+
"bg-foreground text-background text-[12px] px-2 py-1",
|
|
132
|
+
shape.bg,
|
|
133
|
+
className,
|
|
134
|
+
)}
|
|
135
|
+
style={{ fontVariationSettings: fontWeights.medium }}
|
|
136
|
+
initial={{ opacity: 0, ...slideOffset }}
|
|
137
|
+
animate={{
|
|
138
|
+
opacity: open ? 1 : 0,
|
|
139
|
+
x: 0,
|
|
140
|
+
y: 0,
|
|
141
|
+
}}
|
|
142
|
+
transition={open ? spring.fast : spring.fast.exit}
|
|
143
|
+
onAnimationComplete={handleExitComplete}
|
|
144
|
+
>
|
|
145
|
+
{content}
|
|
146
|
+
</motion.div>
|
|
147
|
+
</TooltipPrimitive.Content>
|
|
148
|
+
</TooltipPrimitive.Portal>
|
|
149
|
+
)}
|
|
150
|
+
</TooltipPrimitive.Root>
|
|
151
|
+
</TooltipPrimitive.Provider>
|
|
15
152
|
);
|
|
16
153
|
}
|
|
17
154
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const TooltipContent =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
33
|
-
"data-[side=bottom]:slide-in-from-top-1 data-[side=top]:slide-in-from-bottom-1",
|
|
34
|
-
"data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1",
|
|
35
|
-
className,
|
|
36
|
-
)}
|
|
37
|
-
{...props}
|
|
38
|
-
/>
|
|
39
|
-
</Tooltip.Portal>
|
|
40
|
-
));
|
|
41
|
-
TooltipContent.displayName = "TooltipContent";
|
|
42
|
-
|
|
43
|
-
export { TooltipProvider, TooltipRoot as Tooltip, TooltipTrigger, TooltipContent };
|
|
155
|
+
const TooltipProvider = TooltipPrimitive.Provider;
|
|
156
|
+
const TooltipRoot = TooltipPrimitive.Root;
|
|
157
|
+
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
158
|
+
const TooltipContent = TooltipPrimitive.Content;
|
|
159
|
+
|
|
160
|
+
export {
|
|
161
|
+
Tooltip,
|
|
162
|
+
TooltipPortalContainer,
|
|
163
|
+
TooltipProvider,
|
|
164
|
+
TooltipRoot,
|
|
165
|
+
TooltipTrigger,
|
|
166
|
+
TooltipContent,
|
|
167
|
+
};
|
|
168
|
+
export type { TooltipProps, TooltipSide };
|