@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,156 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
import { useShape } from "../lib/shape-context";
|
|
6
|
+
|
|
7
|
+
// ─── Lazy pdfjs loader ────────────────────────────────────────────────────
|
|
8
|
+
// Imports pdfjs-dist on first PDF, caches the module, and points the worker
|
|
9
|
+
// at the matching CDN build. Consumers don't need bundler-side worker config.
|
|
10
|
+
type PdfjsModule = typeof import("pdfjs-dist");
|
|
11
|
+
let pdfjsPromise: Promise<PdfjsModule> | null = null;
|
|
12
|
+
|
|
13
|
+
async function loadPdfjs(): Promise<PdfjsModule> {
|
|
14
|
+
if (!pdfjsPromise) {
|
|
15
|
+
pdfjsPromise = import("pdfjs-dist").then((mod) => {
|
|
16
|
+
if (!mod.GlobalWorkerOptions.workerSrc) {
|
|
17
|
+
mod.GlobalWorkerOptions.workerSrc = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${mod.version}/build/pdf.worker.min.mjs`;
|
|
18
|
+
}
|
|
19
|
+
return mod;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return pdfjsPromise;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function renderPdfFirstPage(file: File, targetWidth: number): Promise<string> {
|
|
26
|
+
const pdfjs = await loadPdfjs();
|
|
27
|
+
const buffer = await file.arrayBuffer();
|
|
28
|
+
const pdf = await pdfjs.getDocument({ data: buffer }).promise;
|
|
29
|
+
const page = await pdf.getPage(1);
|
|
30
|
+
const baseViewport = page.getViewport({ scale: 1 });
|
|
31
|
+
const scale = (targetWidth * 2) / baseViewport.width; // 2× for retina
|
|
32
|
+
const viewport = page.getViewport({ scale });
|
|
33
|
+
const canvas = document.createElement("canvas");
|
|
34
|
+
canvas.width = viewport.width;
|
|
35
|
+
canvas.height = viewport.height;
|
|
36
|
+
await page.render({ canvas, viewport }).promise;
|
|
37
|
+
return canvas.toDataURL("image/png");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── File thumbnail ───────────────────────────────────────────────────────
|
|
41
|
+
// Read-only square preview of a File. Images use object-cover via
|
|
42
|
+
// `URL.createObjectURL`; PDFs render the first page via pdfjs; while either is
|
|
43
|
+
// resolving a spinner is shown. Self-contained (border + surface + sizing) so
|
|
44
|
+
// it can be reused both inside the composer's preview row and to render
|
|
45
|
+
// already-sent attachments in a chat transcript.
|
|
46
|
+
interface FileThumbnailProps {
|
|
47
|
+
file?: File;
|
|
48
|
+
name?: string;
|
|
49
|
+
type?: string;
|
|
50
|
+
/** Side length of the square thumbnail in pixels. */
|
|
51
|
+
size?: number | string;
|
|
52
|
+
className?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function FileThumbnail({ file, name, type, size = 48, className }: FileThumbnailProps) {
|
|
56
|
+
const shape = useShape();
|
|
57
|
+
const numericSize = typeof size === "number" ? size : 48;
|
|
58
|
+
|
|
59
|
+
if (!file) {
|
|
60
|
+
return (
|
|
61
|
+
<div className={cn("flex items-center gap-3 rounded-lg border border-border p-2", className)}>
|
|
62
|
+
<div
|
|
63
|
+
className={cn(
|
|
64
|
+
"flex shrink-0 items-center justify-center bg-accent text-[11px] text-muted-foreground",
|
|
65
|
+
shape.item,
|
|
66
|
+
)}
|
|
67
|
+
style={{ width: numericSize, height: numericSize }}
|
|
68
|
+
>
|
|
69
|
+
{type ?? "File"}
|
|
70
|
+
</div>
|
|
71
|
+
<div className="min-w-0">
|
|
72
|
+
<div className="truncate text-[13px] text-foreground">{name ?? "Untitled file"}</div>
|
|
73
|
+
<div className="text-[12px] text-muted-foreground">
|
|
74
|
+
{typeof size === "string" ? size : (type ?? "File")}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const isImage = file.type.startsWith("image/");
|
|
82
|
+
const isPdf = file.type === "application/pdf";
|
|
83
|
+
|
|
84
|
+
// Create blob URL inside an effect (NOT useMemo) so the cleanup-revoke
|
|
85
|
+
// and the URL-creation stay in sync. In React 18 StrictMode dev, a
|
|
86
|
+
// useMemo-created URL gets revoked by the simulated effect-cleanup but
|
|
87
|
+
// useMemo doesn't re-run on the simulated re-mount (no re-render happens),
|
|
88
|
+
// leaving the DOM with a stale, revoked `blob:` URL — broken image.
|
|
89
|
+
// Putting both in the same effect means the simulated re-mount creates a
|
|
90
|
+
// fresh URL and updates state. The one-frame "before URL" state is
|
|
91
|
+
// covered by the bg-accent (no fallback icon shown for images), so the
|
|
92
|
+
// transition is visually clean.
|
|
93
|
+
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!isImage) return;
|
|
96
|
+
const url = URL.createObjectURL(file);
|
|
97
|
+
setImageUrl(url);
|
|
98
|
+
return () => URL.revokeObjectURL(url);
|
|
99
|
+
}, [isImage, file]);
|
|
100
|
+
|
|
101
|
+
// PDFs need async rendering — loading flash is unavoidable for the first
|
|
102
|
+
// ~100–300ms while pdfjs loads. Falls back to the generic icon on error.
|
|
103
|
+
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!isPdf) return;
|
|
106
|
+
let cancelled = false;
|
|
107
|
+
renderPdfFirstPage(file, numericSize)
|
|
108
|
+
.then((url) => {
|
|
109
|
+
if (!cancelled) setPdfUrl(url);
|
|
110
|
+
})
|
|
111
|
+
.catch(() => {
|
|
112
|
+
/* fall through to spinner */
|
|
113
|
+
});
|
|
114
|
+
return () => {
|
|
115
|
+
cancelled = true;
|
|
116
|
+
};
|
|
117
|
+
}, [file, isPdf, numericSize]);
|
|
118
|
+
|
|
119
|
+
const previewUrl = imageUrl ?? pdfUrl;
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div
|
|
123
|
+
className={cn(
|
|
124
|
+
"relative shrink-0 overflow-hidden bg-accent border border-border",
|
|
125
|
+
shape.bg,
|
|
126
|
+
className,
|
|
127
|
+
)}
|
|
128
|
+
style={{ width: numericSize, height: numericSize }}
|
|
129
|
+
>
|
|
130
|
+
{previewUrl ? (
|
|
131
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
132
|
+
<img
|
|
133
|
+
src={previewUrl}
|
|
134
|
+
alt={file.name}
|
|
135
|
+
className="absolute inset-0 w-full h-full object-cover"
|
|
136
|
+
/>
|
|
137
|
+
) : (
|
|
138
|
+
// Circular spinner while we wait for the preview to be ready.
|
|
139
|
+
// Used for both images (brief URL-creation gap) and PDFs (longer
|
|
140
|
+
// pdfjs render). The thin ring is mostly subtle (border-border)
|
|
141
|
+
// with one quadrant accented (border-t-muted-foreground) so the
|
|
142
|
+
// `animate-spin` rotation reads as a moving arc.
|
|
143
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
144
|
+
<div
|
|
145
|
+
className="w-6 h-6 rounded-full border-2 border-border border-t-muted-foreground animate-spin"
|
|
146
|
+
aria-label="Loading preview"
|
|
147
|
+
role="status"
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export { FileThumbnail, loadPdfjs, renderPdfFirstPage };
|
|
156
|
+
export type { FileThumbnailProps };
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { HoverCard } from "radix-ui";
|
|
5
5
|
import { cn } from "../lib/utils";
|
|
6
|
+
import { motionClasses } from "../lib/interaction";
|
|
6
7
|
|
|
7
8
|
const HoverCardRoot = HoverCard.Root;
|
|
8
9
|
const HoverCardTrigger = HoverCard.Trigger;
|
|
@@ -18,6 +19,7 @@ const HoverCardContent = React.forwardRef<
|
|
|
18
19
|
sideOffset={sideOffset}
|
|
19
20
|
className={cn(
|
|
20
21
|
"z-50 w-64 rounded-xl border border-border bg-card p-4 shadow-elevated outline-none",
|
|
22
|
+
motionClasses,
|
|
21
23
|
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
22
24
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
23
25
|
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useState, useCallback, useRef, useEffect, type HTMLAttributes } from "react";
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
import { useIcon } from "../lib/icon-context";
|
|
7
|
+
import { fontWeights } from "../lib/font-weight";
|
|
8
|
+
import { useShape } from "../lib/shape-context";
|
|
9
|
+
import { spring } from "../lib/springs";
|
|
10
|
+
import { Tooltip } from "./tooltip";
|
|
11
|
+
|
|
12
|
+
type InputCopyVariant = "icon" | "button";
|
|
13
|
+
type InputCopyAlign = "right" | "left";
|
|
14
|
+
|
|
15
|
+
interface InputCopyProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
16
|
+
/** The value to display and copy to clipboard. */
|
|
17
|
+
value: string;
|
|
18
|
+
/** Optional label displayed above the input. */
|
|
19
|
+
label?: string;
|
|
20
|
+
/** Callback fired after the value is copied. */
|
|
21
|
+
onCopy?: () => void;
|
|
22
|
+
/** Whether the component is disabled. */
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
/** Display variant: icon-only with tooltip, or button with label. */
|
|
25
|
+
variant?: InputCopyVariant;
|
|
26
|
+
/** Position of the copy action relative to the value. */
|
|
27
|
+
align?: InputCopyAlign;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const InputCopy = forwardRef<HTMLDivElement, InputCopyProps>(
|
|
31
|
+
(
|
|
32
|
+
{ value, label, onCopy, disabled, variant = "icon", align = "right", className, ...props },
|
|
33
|
+
ref,
|
|
34
|
+
) => {
|
|
35
|
+
const CopyIcon = useIcon("copy");
|
|
36
|
+
const [copied, setCopied] = useState(false);
|
|
37
|
+
const [copyCount, setCopyCount] = useState(0);
|
|
38
|
+
// "idle" = normal tooltip behavior, "copied" = force open, "suppressed" = force closed
|
|
39
|
+
const [tooltipState, setTooltipState] = useState<"idle" | "copied" | "suppressed">("idle");
|
|
40
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
|
|
41
|
+
const tooltipVisibleRef = useRef(false);
|
|
42
|
+
const tooltipWasVisibleRef = useRef(false);
|
|
43
|
+
const shape = useShape();
|
|
44
|
+
|
|
45
|
+
const handlePointerDown = useCallback(() => {
|
|
46
|
+
// Capture tooltip visibility before Radix closes it on pointer down
|
|
47
|
+
tooltipWasVisibleRef.current = tooltipVisibleRef.current;
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const handleCopy = useCallback(async () => {
|
|
51
|
+
if (disabled) return;
|
|
52
|
+
try {
|
|
53
|
+
await navigator.clipboard.writeText(value);
|
|
54
|
+
setCopied(true);
|
|
55
|
+
setCopyCount((c) => c + 1);
|
|
56
|
+
setTooltipState(tooltipWasVisibleRef.current ? "copied" : "suppressed");
|
|
57
|
+
onCopy?.();
|
|
58
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
59
|
+
timeoutRef.current = setTimeout(() => {
|
|
60
|
+
setCopied(false);
|
|
61
|
+
setTooltipState("suppressed");
|
|
62
|
+
}, 2000);
|
|
63
|
+
} catch {
|
|
64
|
+
// Clipboard API not available — silently fail
|
|
65
|
+
}
|
|
66
|
+
}, [value, disabled, onCopy]);
|
|
67
|
+
|
|
68
|
+
const handleTooltipOpenChange = useCallback((open: boolean) => {
|
|
69
|
+
tooltipVisibleRef.current = open;
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
return () => {
|
|
74
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
75
|
+
};
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const handleMouseEnter = useCallback(() => {
|
|
79
|
+
setTooltipState((prev) => (prev === "suppressed" ? "idle" : prev));
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const handleMouseLeave = useCallback(() => {
|
|
83
|
+
setTooltipState((prev) => (prev === "copied" ? "suppressed" : prev));
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
const iconSwitch = (
|
|
87
|
+
<AnimatePresence mode="wait" initial={false}>
|
|
88
|
+
{copied ? (
|
|
89
|
+
<motion.span
|
|
90
|
+
key={`check-${copyCount}`}
|
|
91
|
+
initial={{ opacity: 0, scale: 0.6 }}
|
|
92
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
93
|
+
exit={{ opacity: 0, scale: 0.8 }}
|
|
94
|
+
transition={spring.fast}
|
|
95
|
+
className="flex items-center justify-center [&_svg]:stroke-[1.5] [&_svg]:transition-[stroke-width] [&_svg]:duration-80 group-hover:[&_svg]:stroke-[2]"
|
|
96
|
+
>
|
|
97
|
+
<svg
|
|
98
|
+
width={14}
|
|
99
|
+
height={14}
|
|
100
|
+
viewBox="2 4 20 16"
|
|
101
|
+
fill="none"
|
|
102
|
+
stroke="currentColor"
|
|
103
|
+
strokeLinecap="round"
|
|
104
|
+
strokeLinejoin="round"
|
|
105
|
+
>
|
|
106
|
+
<motion.path
|
|
107
|
+
d="M6 12L10 16L18 8"
|
|
108
|
+
initial={{ pathLength: 0 }}
|
|
109
|
+
animate={{
|
|
110
|
+
pathLength: 1,
|
|
111
|
+
transition: { duration: 0.08, ease: "easeOut" },
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
</svg>
|
|
115
|
+
</motion.span>
|
|
116
|
+
) : (
|
|
117
|
+
<motion.span
|
|
118
|
+
key="copy"
|
|
119
|
+
initial={{ opacity: 0, scale: 0.8 }}
|
|
120
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
121
|
+
exit={{ opacity: 0, scale: 0.8 }}
|
|
122
|
+
transition={spring.fast}
|
|
123
|
+
className="flex items-center justify-center"
|
|
124
|
+
>
|
|
125
|
+
<CopyIcon
|
|
126
|
+
size={14}
|
|
127
|
+
strokeWidth={1.5}
|
|
128
|
+
className="transition-[stroke-width] duration-80 group-hover:stroke-[2]"
|
|
129
|
+
/>
|
|
130
|
+
</motion.span>
|
|
131
|
+
)}
|
|
132
|
+
</AnimatePresence>
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const actionElement =
|
|
136
|
+
variant === "button" ? (
|
|
137
|
+
<span
|
|
138
|
+
className={cn(
|
|
139
|
+
"shrink-0 flex items-center gap-1.5 px-1.5 py-2 text-[13px] transition-colors duration-80",
|
|
140
|
+
"text-muted-foreground group-hover:text-foreground",
|
|
141
|
+
)}
|
|
142
|
+
style={{ fontVariationSettings: fontWeights.normal }}
|
|
143
|
+
>
|
|
144
|
+
<AnimatePresence mode="wait" initial={false}>
|
|
145
|
+
{copied ? (
|
|
146
|
+
<motion.span
|
|
147
|
+
key={`check-label-${copyCount}`}
|
|
148
|
+
className="flex items-center gap-1.5"
|
|
149
|
+
initial={{ opacity: 0, scale: 0.6 }}
|
|
150
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
151
|
+
exit={{ opacity: 0, scale: 0.8 }}
|
|
152
|
+
transition={spring.fast}
|
|
153
|
+
>
|
|
154
|
+
<span className="flex items-center justify-center">
|
|
155
|
+
<svg
|
|
156
|
+
width={14}
|
|
157
|
+
height={14}
|
|
158
|
+
viewBox="2 4 20 16"
|
|
159
|
+
fill="none"
|
|
160
|
+
stroke="currentColor"
|
|
161
|
+
strokeWidth={2}
|
|
162
|
+
strokeLinecap="round"
|
|
163
|
+
strokeLinejoin="round"
|
|
164
|
+
>
|
|
165
|
+
<motion.path
|
|
166
|
+
d="M6 12L10 16L18 8"
|
|
167
|
+
initial={{ pathLength: 0 }}
|
|
168
|
+
animate={{
|
|
169
|
+
pathLength: 1,
|
|
170
|
+
transition: { duration: 0.08, ease: "easeOut" },
|
|
171
|
+
}}
|
|
172
|
+
/>
|
|
173
|
+
</svg>
|
|
174
|
+
</span>
|
|
175
|
+
<span className="select-none inline-grid text-left">
|
|
176
|
+
<span className="col-start-1 row-start-1 invisible" aria-hidden="true">
|
|
177
|
+
Copied
|
|
178
|
+
</span>
|
|
179
|
+
<span className="col-start-1 row-start-1">Copied</span>
|
|
180
|
+
</span>
|
|
181
|
+
</motion.span>
|
|
182
|
+
) : (
|
|
183
|
+
<motion.span
|
|
184
|
+
key="copy-label"
|
|
185
|
+
className="flex items-center gap-1.5"
|
|
186
|
+
initial={{ opacity: 0, scale: 0.8 }}
|
|
187
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
188
|
+
exit={{ opacity: 0, scale: 0.8 }}
|
|
189
|
+
transition={spring.fast}
|
|
190
|
+
>
|
|
191
|
+
<span className="flex items-center justify-center">
|
|
192
|
+
<CopyIcon
|
|
193
|
+
size={14}
|
|
194
|
+
strokeWidth={1.5}
|
|
195
|
+
className="transition-[stroke-width] duration-80 group-hover:stroke-[2]"
|
|
196
|
+
/>
|
|
197
|
+
</span>
|
|
198
|
+
<span className="select-none inline-grid text-left">
|
|
199
|
+
<span className="col-start-1 row-start-1 invisible" aria-hidden="true">
|
|
200
|
+
Copied
|
|
201
|
+
</span>
|
|
202
|
+
<span className="col-start-1 row-start-1">Copy</span>
|
|
203
|
+
</span>
|
|
204
|
+
</motion.span>
|
|
205
|
+
)}
|
|
206
|
+
</AnimatePresence>
|
|
207
|
+
</span>
|
|
208
|
+
) : (
|
|
209
|
+
<span
|
|
210
|
+
className={cn(
|
|
211
|
+
"shrink-0 px-1.5 py-2 transition-colors duration-80",
|
|
212
|
+
"text-muted-foreground group-hover:text-foreground",
|
|
213
|
+
)}
|
|
214
|
+
>
|
|
215
|
+
{iconSwitch}
|
|
216
|
+
</span>
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const valueElement = (
|
|
220
|
+
<span
|
|
221
|
+
className={cn(
|
|
222
|
+
"flex-1 min-w-0 text-left text-[13px] text-foreground font-mono py-2 select-none truncate",
|
|
223
|
+
align === "left" ? "pl-1" : "pl-0",
|
|
224
|
+
)}
|
|
225
|
+
style={{ fontVariationSettings: fontWeights.normal }}
|
|
226
|
+
>
|
|
227
|
+
<mark className="bg-transparent text-foreground transition-colors duration-80 group-hover:bg-[#6B97FF]/20 group-hover:text-foreground">
|
|
228
|
+
{value}
|
|
229
|
+
</mark>
|
|
230
|
+
</span>
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const buttonContent =
|
|
234
|
+
align === "left" ? (
|
|
235
|
+
<>
|
|
236
|
+
{actionElement}
|
|
237
|
+
{valueElement}
|
|
238
|
+
</>
|
|
239
|
+
) : (
|
|
240
|
+
<>
|
|
241
|
+
{valueElement}
|
|
242
|
+
{actionElement}
|
|
243
|
+
</>
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const button = (
|
|
247
|
+
<button
|
|
248
|
+
type="button"
|
|
249
|
+
onPointerDown={handlePointerDown}
|
|
250
|
+
onClick={handleCopy}
|
|
251
|
+
disabled={disabled}
|
|
252
|
+
aria-label={copied ? "Copied" : "Copy to clipboard"}
|
|
253
|
+
className={cn(
|
|
254
|
+
"group flex items-center w-full cursor-pointer outline-none transition-all duration-80",
|
|
255
|
+
"focus-visible:ring-1 focus-visible:ring-[#6B97FF]",
|
|
256
|
+
shape.input,
|
|
257
|
+
)}
|
|
258
|
+
>
|
|
259
|
+
{buttonContent}
|
|
260
|
+
</button>
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<div
|
|
265
|
+
ref={ref}
|
|
266
|
+
className={cn(
|
|
267
|
+
"flex flex-col gap-0.5",
|
|
268
|
+
disabled && "opacity-50 pointer-events-none",
|
|
269
|
+
className,
|
|
270
|
+
)}
|
|
271
|
+
onMouseEnter={handleMouseEnter}
|
|
272
|
+
onMouseLeave={handleMouseLeave}
|
|
273
|
+
{...props}
|
|
274
|
+
>
|
|
275
|
+
{label && (
|
|
276
|
+
<span
|
|
277
|
+
className={cn("text-[13px] text-muted-foreground", align === "left" ? "pl-1" : "pl-0")}
|
|
278
|
+
style={{ fontVariationSettings: fontWeights.normal }}
|
|
279
|
+
>
|
|
280
|
+
{label}
|
|
281
|
+
</span>
|
|
282
|
+
)}
|
|
283
|
+
{variant === "icon" ? (
|
|
284
|
+
<Tooltip
|
|
285
|
+
content={tooltipState === "idle" ? "Copy to clipboard" : "Copied"}
|
|
286
|
+
delayDuration={500}
|
|
287
|
+
sideOffset={2}
|
|
288
|
+
forceOpen={
|
|
289
|
+
tooltipState === "copied" ? true : tooltipState === "suppressed" ? false : undefined
|
|
290
|
+
}
|
|
291
|
+
onOpenChange={handleTooltipOpenChange}
|
|
292
|
+
>
|
|
293
|
+
{button}
|
|
294
|
+
</Tooltip>
|
|
295
|
+
) : (
|
|
296
|
+
button
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
InputCopy.displayName = "InputCopy";
|
|
304
|
+
|
|
305
|
+
export { InputCopy };
|
|
306
|
+
export type { InputCopyProps };
|
|
307
|
+
export default InputCopy;
|