@hilum/ui 3.2.3 → 3.2.5
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/index.d.ts +348 -32
- package/dist/index.js +2511 -676
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/alert-dialog.tsx +19 -6
- package/src/components/callout.tsx +99 -0
- package/src/components/card.tsx +13 -0
- package/src/components/chart.tsx +398 -29
- package/src/components/color-picker.tsx +39 -45
- package/src/components/combobox.tsx +83 -64
- package/src/components/command.tsx +35 -0
- package/src/components/context-menu.tsx +49 -27
- package/src/components/data-transfer-controls.tsx +156 -0
- package/src/components/dialog.tsx +19 -6
- package/src/components/dropdown-menu.tsx +50 -28
- package/src/components/file-dropzone.tsx +171 -0
- package/src/components/help-tooltip.tsx +90 -0
- package/src/components/media-asset-card.tsx +118 -0
- package/src/components/menubar.tsx +52 -30
- package/src/components/popover.tsx +31 -17
- package/src/components/rich-text-editor.tsx +448 -0
- package/src/components/searchable-table.tsx +412 -0
- package/src/components/select.tsx +44 -28
- package/src/components/sidebar.tsx +105 -14
- package/src/components/stat-card.tsx +122 -8
- package/src/components/status-badge.tsx +144 -0
- package/src/components/titled-card.tsx +102 -0
- package/src/components/url-redirect-prompt.tsx +85 -0
- package/src/index.ts +10 -0
- package/src/lib/mobile-popper-sheet.ts +78 -0
|
@@ -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
|
+
mobilePopperSheetMotionClassName,
|
|
9
|
+
mobilePopperSheetPositionClassName,
|
|
10
|
+
mobilePopperSheetStyle,
|
|
11
|
+
mobilePopperSheetSurfaceClassName,
|
|
12
|
+
} from "../lib/mobile-popper-sheet";
|
|
7
13
|
|
|
8
14
|
const DropdownMenuRoot = DropdownMenu.Root;
|
|
9
15
|
const DropdownMenuTrigger = DropdownMenu.Trigger;
|
|
@@ -16,22 +22,30 @@ const DropdownMenuContent = React.forwardRef<
|
|
|
16
22
|
React.ComponentRef<typeof DropdownMenu.Content>,
|
|
17
23
|
React.ComponentPropsWithoutRef<typeof DropdownMenu.Content>
|
|
18
24
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
19
|
-
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
<>
|
|
26
|
+
<style>{mobilePopperSheetStyle}</style>
|
|
27
|
+
<DropdownMenu.Portal>
|
|
28
|
+
<DropdownMenu.Content
|
|
29
|
+
ref={ref}
|
|
30
|
+
data-hilum-mobile-sheet="true"
|
|
31
|
+
sideOffset={sideOffset}
|
|
32
|
+
className={cn(
|
|
33
|
+
"z-50 min-w-[8rem] bg-card rounded-xl shadow-natural p-1",
|
|
34
|
+
mobilePopperSheetPositionClassName,
|
|
35
|
+
mobilePopperSheetSurfaceClassName,
|
|
36
|
+
"max-sm:overflow-y-auto max-sm:px-2 max-sm:pb-2 max-sm:pt-5",
|
|
37
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
38
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
39
|
+
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
40
|
+
mobilePopperSheetMotionClassName,
|
|
41
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
|
|
42
|
+
"data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
|
|
43
|
+
className,
|
|
44
|
+
)}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
</DropdownMenu.Portal>
|
|
48
|
+
</>
|
|
35
49
|
));
|
|
36
50
|
DropdownMenuContent.displayName = "DropdownMenuContent";
|
|
37
51
|
|
|
@@ -162,18 +176,26 @@ const DropdownMenuSubContent = React.forwardRef<
|
|
|
162
176
|
React.ComponentRef<typeof DropdownMenu.SubContent>,
|
|
163
177
|
React.ComponentPropsWithoutRef<typeof DropdownMenu.SubContent>
|
|
164
178
|
>(({ className, ...props }, ref) => (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
179
|
+
<>
|
|
180
|
+
<style>{mobilePopperSheetStyle}</style>
|
|
181
|
+
<DropdownMenu.SubContent
|
|
182
|
+
ref={ref}
|
|
183
|
+
data-hilum-mobile-sheet="true"
|
|
184
|
+
className={cn(
|
|
185
|
+
"z-50 min-w-[8rem] bg-card rounded-xl border border-border shadow-natural p-1",
|
|
186
|
+
mobilePopperSheetPositionClassName,
|
|
187
|
+
mobilePopperSheetSurfaceClassName,
|
|
188
|
+
"max-sm:overflow-y-auto max-sm:px-2 max-sm:pb-2 max-sm:pt-5",
|
|
189
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
190
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
191
|
+
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
192
|
+
mobilePopperSheetMotionClassName,
|
|
193
|
+
"data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
|
|
194
|
+
className,
|
|
195
|
+
)}
|
|
196
|
+
{...props}
|
|
197
|
+
/>
|
|
198
|
+
</>
|
|
177
199
|
));
|
|
178
200
|
DropdownMenuSubContent.displayName = "DropdownMenuSubContent";
|
|
179
201
|
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { File as FileIcon, Upload } from "lucide-react";
|
|
5
|
+
import { Spinner } from "./spinner";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
type FileDropzoneSelectedFile = Pick<File, "name" | "size">;
|
|
9
|
+
|
|
10
|
+
interface FileDropzoneProps extends Omit<
|
|
11
|
+
React.HTMLAttributes<HTMLLabelElement>,
|
|
12
|
+
"children" | "onChange" | "onDrop" | "onDragOver" | "onDragLeave"
|
|
13
|
+
> {
|
|
14
|
+
accept?: string;
|
|
15
|
+
multiple?: boolean;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
loading?: boolean;
|
|
18
|
+
loadingText?: React.ReactNode;
|
|
19
|
+
label?: React.ReactNode;
|
|
20
|
+
activeLabel?: React.ReactNode;
|
|
21
|
+
description?: React.ReactNode;
|
|
22
|
+
icon?: React.ReactNode;
|
|
23
|
+
selectedFiles?: readonly FileDropzoneSelectedFile[] | null;
|
|
24
|
+
inputRef?: React.Ref<HTMLInputElement>;
|
|
25
|
+
inputClassName?: string;
|
|
26
|
+
inputName?: string;
|
|
27
|
+
onFilesSelected?: (files: File[]) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatFileSize(bytes: number) {
|
|
31
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
32
|
+
|
|
33
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
34
|
+
const unitIndex = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
35
|
+
const value = bytes / Math.pow(1024, unitIndex);
|
|
36
|
+
|
|
37
|
+
return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getFileSummary(files: readonly FileDropzoneSelectedFile[]) {
|
|
41
|
+
if (files.length === 0) return null;
|
|
42
|
+
if (files.length === 1) return `${files[0].name} (${formatFileSize(files[0].size)})`;
|
|
43
|
+
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
|
44
|
+
return `${files.length} files selected (${formatFileSize(totalSize)})`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function FileDropzone({
|
|
48
|
+
accept,
|
|
49
|
+
multiple,
|
|
50
|
+
disabled,
|
|
51
|
+
loading,
|
|
52
|
+
loadingText = "Uploading...",
|
|
53
|
+
label = "Drag files here or click to upload",
|
|
54
|
+
activeLabel = "Drop files to upload",
|
|
55
|
+
description,
|
|
56
|
+
icon,
|
|
57
|
+
selectedFiles,
|
|
58
|
+
inputRef,
|
|
59
|
+
inputClassName,
|
|
60
|
+
inputName,
|
|
61
|
+
onFilesSelected,
|
|
62
|
+
className,
|
|
63
|
+
id,
|
|
64
|
+
tabIndex,
|
|
65
|
+
onKeyDown,
|
|
66
|
+
onClick,
|
|
67
|
+
...props
|
|
68
|
+
}: FileDropzoneProps) {
|
|
69
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
70
|
+
const inputId = React.useId();
|
|
71
|
+
const resolvedId = id ?? inputId;
|
|
72
|
+
const fileSummary = getFileSummary(selectedFiles ?? []);
|
|
73
|
+
const isUnavailable = Boolean(disabled || loading);
|
|
74
|
+
|
|
75
|
+
const emitFiles = React.useCallback(
|
|
76
|
+
(fileList: FileList | null) => {
|
|
77
|
+
if (!fileList || isUnavailable) return;
|
|
78
|
+
onFilesSelected?.(Array.from(fileList));
|
|
79
|
+
},
|
|
80
|
+
[isUnavailable, onFilesSelected],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<label
|
|
85
|
+
htmlFor={resolvedId}
|
|
86
|
+
role="button"
|
|
87
|
+
tabIndex={isUnavailable ? -1 : (tabIndex ?? 0)}
|
|
88
|
+
aria-disabled={isUnavailable || undefined}
|
|
89
|
+
data-dragging={isDragging ? "true" : "false"}
|
|
90
|
+
className={cn(
|
|
91
|
+
"group flex min-h-36 cursor-pointer flex-col items-center justify-center rounded-xl border border-dashed border-border bg-card p-6 text-center shadow-natural",
|
|
92
|
+
"transition-[background-color,border-color,box-shadow,scale] duration-150",
|
|
93
|
+
"hover:border-brand-primary/50 hover:bg-muted/40 active:scale-[0.96]",
|
|
94
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary/30",
|
|
95
|
+
"data-[dragging=true]:border-brand-primary data-[dragging=true]:bg-brand-secondary/25",
|
|
96
|
+
isUnavailable && "pointer-events-none cursor-not-allowed opacity-60 active:scale-100",
|
|
97
|
+
className,
|
|
98
|
+
)}
|
|
99
|
+
onDragOver={(event) => {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
if (!isUnavailable) setIsDragging(true);
|
|
102
|
+
}}
|
|
103
|
+
onDragLeave={(event) => {
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
setIsDragging(false);
|
|
106
|
+
}}
|
|
107
|
+
onDrop={(event) => {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
setIsDragging(false);
|
|
110
|
+
emitFiles(event.dataTransfer.files);
|
|
111
|
+
}}
|
|
112
|
+
onKeyDown={(event) => {
|
|
113
|
+
onKeyDown?.(event);
|
|
114
|
+
if (event.defaultPrevented || isUnavailable) return;
|
|
115
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
document.getElementById(resolvedId)?.click();
|
|
118
|
+
}
|
|
119
|
+
}}
|
|
120
|
+
onClick={(event) => {
|
|
121
|
+
if (isUnavailable) {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
onClick?.(event);
|
|
126
|
+
}}
|
|
127
|
+
{...props}
|
|
128
|
+
>
|
|
129
|
+
<input
|
|
130
|
+
ref={inputRef}
|
|
131
|
+
id={resolvedId}
|
|
132
|
+
name={inputName}
|
|
133
|
+
type="file"
|
|
134
|
+
accept={accept}
|
|
135
|
+
multiple={multiple}
|
|
136
|
+
disabled={isUnavailable}
|
|
137
|
+
className={cn("sr-only", inputClassName)}
|
|
138
|
+
onChange={(event) => emitFiles(event.currentTarget.files)}
|
|
139
|
+
/>
|
|
140
|
+
<span
|
|
141
|
+
className={cn(
|
|
142
|
+
"mb-3 flex size-12 items-center justify-center rounded-xl bg-muted text-muted-foreground",
|
|
143
|
+
"transition-[background-color,color,scale] duration-150 group-hover:bg-brand-secondary/35 group-hover:text-foreground",
|
|
144
|
+
"group-data-[dragging=true]:bg-brand-secondary group-data-[dragging=true]:text-foreground",
|
|
145
|
+
)}
|
|
146
|
+
aria-hidden="true"
|
|
147
|
+
>
|
|
148
|
+
{loading ? <Spinner size="sm" /> : (icon ?? <Upload className="size-5" />)}
|
|
149
|
+
</span>
|
|
150
|
+
<span className="body-sm font-medium text-foreground text-balance">
|
|
151
|
+
{loading ? loadingText : isDragging ? activeLabel : fileSummary ? fileSummary : label}
|
|
152
|
+
</span>
|
|
153
|
+
{description && (
|
|
154
|
+
<span className="caption mt-1 max-w-md text-pretty text-muted-foreground">
|
|
155
|
+
{description}
|
|
156
|
+
</span>
|
|
157
|
+
)}
|
|
158
|
+
{fileSummary && !loading && (
|
|
159
|
+
<span className="caption mt-2 inline-flex min-h-7 max-w-full items-center gap-1 rounded-full bg-muted px-2.5 text-muted-foreground">
|
|
160
|
+
<FileIcon className="size-3.5 shrink-0" aria-hidden="true" />
|
|
161
|
+
<span className="min-w-0 truncate">{multiple ? "Ready to upload" : "Selected"}</span>
|
|
162
|
+
</span>
|
|
163
|
+
)}
|
|
164
|
+
</label>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
FileDropzone.displayName = "FileDropzone";
|
|
169
|
+
|
|
170
|
+
export { FileDropzone, formatFileSize };
|
|
171
|
+
export type { FileDropzoneProps, FileDropzoneSelectedFile };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { CircleHelp } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
|
|
7
|
+
|
|
8
|
+
interface HelpTooltipProps {
|
|
9
|
+
text: string;
|
|
10
|
+
learnMoreUrl?: string;
|
|
11
|
+
learnMoreURL?: string;
|
|
12
|
+
placement?: "top" | "right" | "bottom" | "left";
|
|
13
|
+
className?: string;
|
|
14
|
+
contentClassName?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function HelpTooltip({
|
|
18
|
+
text,
|
|
19
|
+
learnMoreUrl,
|
|
20
|
+
learnMoreURL,
|
|
21
|
+
placement = "top",
|
|
22
|
+
className,
|
|
23
|
+
contentClassName,
|
|
24
|
+
}: HelpTooltipProps) {
|
|
25
|
+
const [open, setOpen] = React.useState(false);
|
|
26
|
+
const rootRef = React.useRef<HTMLSpanElement>(null);
|
|
27
|
+
const url = learnMoreUrl ?? learnMoreURL;
|
|
28
|
+
const body = text.length > 120 ? `${text.slice(0, 117)}...` : text;
|
|
29
|
+
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
if (!open) return;
|
|
32
|
+
|
|
33
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
34
|
+
if (!rootRef.current?.contains(event.target as Node)) {
|
|
35
|
+
setOpen(false);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
40
|
+
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
41
|
+
}, [open]);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<TooltipProvider delayDuration={150}>
|
|
45
|
+
<Tooltip open={open} onOpenChange={setOpen}>
|
|
46
|
+
<span ref={rootRef} className={cn("inline-flex align-middle", className)}>
|
|
47
|
+
<TooltipTrigger asChild>
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
className={cn(
|
|
51
|
+
"relative ml-1 inline-flex size-5 items-center justify-center rounded-full text-muted-foreground",
|
|
52
|
+
"transition-[color,scale] duration-150 hover:text-foreground active:scale-[0.96]",
|
|
53
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary/30",
|
|
54
|
+
"before:absolute before:-inset-2.5 before:content-['']",
|
|
55
|
+
)}
|
|
56
|
+
aria-label="Help"
|
|
57
|
+
onClick={(event) => {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
setOpen((current) => !current);
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<CircleHelp className="size-4" aria-hidden="true" />
|
|
63
|
+
</button>
|
|
64
|
+
</TooltipTrigger>
|
|
65
|
+
<TooltipContent
|
|
66
|
+
side={placement}
|
|
67
|
+
collisionPadding={12}
|
|
68
|
+
className={cn("max-w-xs text-sm", contentClassName)}
|
|
69
|
+
>
|
|
70
|
+
<p className="text-pretty">{body}</p>
|
|
71
|
+
{url && (
|
|
72
|
+
<a
|
|
73
|
+
href={url}
|
|
74
|
+
target="_blank"
|
|
75
|
+
rel="noopener noreferrer"
|
|
76
|
+
className="mt-1 block text-xs font-medium text-brand-primary underline-offset-4 hover:underline"
|
|
77
|
+
>
|
|
78
|
+
Learn more
|
|
79
|
+
</a>
|
|
80
|
+
)}
|
|
81
|
+
</TooltipContent>
|
|
82
|
+
</span>
|
|
83
|
+
</Tooltip>
|
|
84
|
+
</TooltipProvider>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
HelpTooltip.displayName = "HelpTooltip";
|
|
89
|
+
|
|
90
|
+
export { HelpTooltip, type HelpTooltipProps };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { File, Image } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
interface MediaAssetCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
8
|
+
name: React.ReactNode;
|
|
9
|
+
src?: string | null;
|
|
10
|
+
alt?: string;
|
|
11
|
+
meta?: React.ReactNode;
|
|
12
|
+
details?: React.ReactNode;
|
|
13
|
+
actions?: React.ReactNode;
|
|
14
|
+
selected?: boolean;
|
|
15
|
+
mediaType?: "image" | "file" | "video" | "unknown";
|
|
16
|
+
orientation?: "responsive" | "grid" | "list";
|
|
17
|
+
onSelect?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function MediaAssetCard({
|
|
21
|
+
name,
|
|
22
|
+
src,
|
|
23
|
+
alt = "",
|
|
24
|
+
meta,
|
|
25
|
+
details,
|
|
26
|
+
actions,
|
|
27
|
+
selected,
|
|
28
|
+
mediaType = src ? "image" : "file",
|
|
29
|
+
orientation = "responsive",
|
|
30
|
+
onSelect,
|
|
31
|
+
className,
|
|
32
|
+
onClick,
|
|
33
|
+
...props
|
|
34
|
+
}: MediaAssetCardProps) {
|
|
35
|
+
const isInteractive = Boolean(onSelect || onClick);
|
|
36
|
+
const Wrapper = isInteractive ? "button" : "div";
|
|
37
|
+
const isList = orientation === "list";
|
|
38
|
+
const isGrid = orientation === "grid";
|
|
39
|
+
const showImage = Boolean(src && mediaType === "image");
|
|
40
|
+
|
|
41
|
+
const fallbackIcon =
|
|
42
|
+
mediaType === "image" ? (
|
|
43
|
+
<Image className="size-6 text-muted-foreground" aria-hidden="true" />
|
|
44
|
+
) : (
|
|
45
|
+
<File className="size-6 text-muted-foreground" aria-hidden="true" />
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
className={cn(
|
|
51
|
+
"group min-w-0",
|
|
52
|
+
isGrid && "overflow-hidden rounded-xl border border-border bg-card shadow-natural",
|
|
53
|
+
isList && "flex gap-3 border-b border-border py-3 last:border-b-0",
|
|
54
|
+
orientation === "responsive" &&
|
|
55
|
+
"flex gap-3 border-b border-border py-3 last:border-b-0 sm:block sm:overflow-hidden sm:rounded-xl sm:border sm:bg-card sm:py-0 sm:shadow-natural",
|
|
56
|
+
selected && "ring-2 ring-brand-primary/35",
|
|
57
|
+
className,
|
|
58
|
+
)}
|
|
59
|
+
data-slot="media-asset-card"
|
|
60
|
+
data-selected={selected ? "true" : "false"}
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
<Wrapper
|
|
64
|
+
type={Wrapper === "button" ? "button" : undefined}
|
|
65
|
+
className={cn(
|
|
66
|
+
"relative flex shrink-0 items-center justify-center overflow-hidden bg-muted text-left",
|
|
67
|
+
"transition-[background-color,box-shadow,scale] duration-150",
|
|
68
|
+
isInteractive &&
|
|
69
|
+
"cursor-pointer active:scale-[0.96] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary/30",
|
|
70
|
+
isList && "size-16 rounded-lg",
|
|
71
|
+
isGrid && "aspect-square w-full",
|
|
72
|
+
orientation === "responsive" &&
|
|
73
|
+
"size-16 rounded-lg sm:aspect-square sm:size-auto sm:w-full sm:rounded-none",
|
|
74
|
+
)}
|
|
75
|
+
onClick={(event) => {
|
|
76
|
+
onClick?.(event as React.MouseEvent<HTMLDivElement>);
|
|
77
|
+
if (!event.defaultPrevented) onSelect?.();
|
|
78
|
+
}}
|
|
79
|
+
aria-label={isInteractive && typeof name === "string" ? name : undefined}
|
|
80
|
+
>
|
|
81
|
+
{showImage ? (
|
|
82
|
+
<img
|
|
83
|
+
src={src ?? undefined}
|
|
84
|
+
alt={alt}
|
|
85
|
+
className="size-full object-cover shadow-[inset_0_0_0_1px_rgba(0,0,0,0.1)] dark:shadow-[inset_0_0_0_1px_rgba(255,255,255,0.1)]"
|
|
86
|
+
/>
|
|
87
|
+
) : (
|
|
88
|
+
fallbackIcon
|
|
89
|
+
)}
|
|
90
|
+
</Wrapper>
|
|
91
|
+
|
|
92
|
+
<div
|
|
93
|
+
className={cn(
|
|
94
|
+
"min-w-0 flex-1",
|
|
95
|
+
isGrid && "p-3",
|
|
96
|
+
isList && "py-0.5",
|
|
97
|
+
orientation === "responsive" && "py-0.5 sm:p-3",
|
|
98
|
+
)}
|
|
99
|
+
>
|
|
100
|
+
<div className="flex min-w-0 items-start justify-between gap-2">
|
|
101
|
+
<div className="min-w-0">
|
|
102
|
+
<p className="line-clamp-2 break-words text-sm font-medium leading-5 text-foreground sm:truncate">
|
|
103
|
+
{name}
|
|
104
|
+
</p>
|
|
105
|
+
{meta && <p className="caption mt-0.5 truncate text-muted-foreground">{meta}</p>}
|
|
106
|
+
</div>
|
|
107
|
+
{actions && <div className="shrink-0">{actions}</div>}
|
|
108
|
+
</div>
|
|
109
|
+
{details && <div className="caption mt-2 text-pretty text-muted-foreground">{details}</div>}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
MediaAssetCard.displayName = "MediaAssetCard";
|
|
116
|
+
|
|
117
|
+
export { MediaAssetCard };
|
|
118
|
+
export type { MediaAssetCardProps };
|
|
@@ -4,6 +4,12 @@ import * as React from "react";
|
|
|
4
4
|
import { Menubar } from "radix-ui";
|
|
5
5
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
|
6
6
|
import { cn } from "../lib/utils";
|
|
7
|
+
import {
|
|
8
|
+
mobilePopperSheetMotionClassName,
|
|
9
|
+
mobilePopperSheetPositionClassName,
|
|
10
|
+
mobilePopperSheetStyle,
|
|
11
|
+
mobilePopperSheetSurfaceClassName,
|
|
12
|
+
} from "../lib/mobile-popper-sheet";
|
|
7
13
|
|
|
8
14
|
const MenubarRoot = React.forwardRef<
|
|
9
15
|
React.ComponentRef<typeof Menubar.Root>,
|
|
@@ -50,24 +56,32 @@ const MenubarContent = React.forwardRef<
|
|
|
50
56
|
React.ComponentRef<typeof Menubar.Content>,
|
|
51
57
|
React.ComponentPropsWithoutRef<typeof Menubar.Content>
|
|
52
58
|
>(({ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, ref) => (
|
|
53
|
-
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
<>
|
|
60
|
+
<style>{mobilePopperSheetStyle}</style>
|
|
61
|
+
<Menubar.Portal>
|
|
62
|
+
<Menubar.Content
|
|
63
|
+
ref={ref}
|
|
64
|
+
data-hilum-mobile-sheet="true"
|
|
65
|
+
align={align}
|
|
66
|
+
alignOffset={alignOffset}
|
|
67
|
+
sideOffset={sideOffset}
|
|
68
|
+
className={cn(
|
|
69
|
+
"z-50 min-w-[12rem] bg-card rounded-xl shadow-natural p-1",
|
|
70
|
+
mobilePopperSheetPositionClassName,
|
|
71
|
+
mobilePopperSheetSurfaceClassName,
|
|
72
|
+
"max-sm:overflow-y-auto max-sm:px-2 max-sm:pb-2 max-sm:pt-5",
|
|
73
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
74
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
75
|
+
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
76
|
+
mobilePopperSheetMotionClassName,
|
|
77
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
|
|
78
|
+
"data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
|
|
79
|
+
className,
|
|
80
|
+
)}
|
|
81
|
+
{...props}
|
|
82
|
+
/>
|
|
83
|
+
</Menubar.Portal>
|
|
84
|
+
</>
|
|
71
85
|
));
|
|
72
86
|
MenubarContent.displayName = "MenubarContent";
|
|
73
87
|
|
|
@@ -191,18 +205,26 @@ const MenubarSubContent = React.forwardRef<
|
|
|
191
205
|
React.ComponentRef<typeof Menubar.SubContent>,
|
|
192
206
|
React.ComponentPropsWithoutRef<typeof Menubar.SubContent>
|
|
193
207
|
>(({ className, ...props }, ref) => (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
208
|
+
<>
|
|
209
|
+
<style>{mobilePopperSheetStyle}</style>
|
|
210
|
+
<Menubar.SubContent
|
|
211
|
+
ref={ref}
|
|
212
|
+
data-hilum-mobile-sheet="true"
|
|
213
|
+
className={cn(
|
|
214
|
+
"z-50 min-w-[8rem] bg-card rounded-xl border border-border shadow-natural p-1",
|
|
215
|
+
mobilePopperSheetPositionClassName,
|
|
216
|
+
mobilePopperSheetSurfaceClassName,
|
|
217
|
+
"max-sm:overflow-y-auto max-sm:px-2 max-sm:pb-2 max-sm:pt-5",
|
|
218
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
219
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
220
|
+
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
221
|
+
mobilePopperSheetMotionClassName,
|
|
222
|
+
"data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
|
|
223
|
+
className,
|
|
224
|
+
)}
|
|
225
|
+
{...props}
|
|
226
|
+
/>
|
|
227
|
+
</>
|
|
206
228
|
));
|
|
207
229
|
MenubarSubContent.displayName = "MenubarSubContent";
|
|
208
230
|
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Popover } from "radix-ui";
|
|
5
5
|
import { cn } from "../lib/utils";
|
|
6
|
+
import {
|
|
7
|
+
mobilePopperSheetMotionClassName,
|
|
8
|
+
mobilePopperSheetPositionClassName,
|
|
9
|
+
mobilePopperSheetStyle,
|
|
10
|
+
mobilePopperSheetSurfaceClassName,
|
|
11
|
+
} from "../lib/mobile-popper-sheet";
|
|
6
12
|
|
|
7
13
|
const PopoverRoot = Popover.Root;
|
|
8
14
|
const PopoverTrigger = Popover.Trigger;
|
|
@@ -12,23 +18,31 @@ const PopoverContent = React.forwardRef<
|
|
|
12
18
|
React.ComponentRef<typeof Popover.Content>,
|
|
13
19
|
React.ComponentPropsWithoutRef<typeof Popover.Content>
|
|
14
20
|
>(({ className, align = "center", sideOffset = 6, ...props }, ref) => (
|
|
15
|
-
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
<>
|
|
22
|
+
<style>{mobilePopperSheetStyle}</style>
|
|
23
|
+
<Popover.Portal>
|
|
24
|
+
<Popover.Content
|
|
25
|
+
ref={ref}
|
|
26
|
+
data-hilum-mobile-sheet="true"
|
|
27
|
+
align={align}
|
|
28
|
+
sideOffset={sideOffset}
|
|
29
|
+
className={cn(
|
|
30
|
+
"relative z-50 w-72 rounded-xl bg-card p-4 shadow-natural outline-none",
|
|
31
|
+
mobilePopperSheetPositionClassName,
|
|
32
|
+
mobilePopperSheetSurfaceClassName,
|
|
33
|
+
"max-sm:overflow-y-auto max-sm:px-4 max-sm:pb-4 max-sm:pt-6",
|
|
34
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
35
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
36
|
+
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
37
|
+
mobilePopperSheetMotionClassName,
|
|
38
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
|
|
39
|
+
"data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
|
|
40
|
+
className,
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
</Popover.Portal>
|
|
45
|
+
</>
|
|
32
46
|
));
|
|
33
47
|
PopoverContent.displayName = "PopoverContent";
|
|
34
48
|
|