@etherealui/react 0.0.0-stage → 0.0.3
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/LICENSE +21 -0
- package/README.md +62 -2
- package/dist/index.d.mts +583 -0
- package/dist/index.d.ts +583 -0
- package/dist/index.js +5189 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5189 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +83 -3
- package/src/blocks/app-shell.tsx +441 -0
- package/src/blocks/billing-card.tsx +2 -0
- package/src/blocks/data-table.tsx +479 -0
- package/src/blocks/filter-bar.tsx +134 -0
- package/src/blocks/header-navbar.tsx +7 -0
- package/src/blocks/metric-grid.tsx +5 -0
- package/src/blocks/profile-form.tsx +2 -0
- package/src/blocks/security-settings.tsx +2 -0
- package/src/blocks/settings-blocks.tsx +406 -0
- package/src/blocks/sidebar.tsx +6 -0
- package/src/blocks/stat-card.tsx +7 -0
- package/src/blocks/stat-cards.tsx +212 -0
- package/src/blocks/team-members.tsx +5 -0
- package/src/index.ts +114 -0
- package/src/primitives/accordion.tsx +61 -0
- package/src/primitives/announcement.tsx +33 -0
- package/src/primitives/avatar.tsx +156 -0
- package/src/primitives/badge.tsx +108 -0
- package/src/primitives/breadcrumb.tsx +45 -0
- package/src/primitives/button-group.tsx +79 -0
- package/src/primitives/button.tsx +165 -0
- package/src/primitives/carousel.tsx +84 -0
- package/src/primitives/chart.tsx +831 -0
- package/src/primitives/checkbox.tsx +26 -0
- package/src/primitives/chip.tsx +37 -0
- package/src/primitives/close-button.tsx +29 -0
- package/src/primitives/color-picker.tsx +62 -0
- package/src/primitives/combobox.tsx +123 -0
- package/src/primitives/command-palette.tsx +216 -0
- package/src/primitives/context-menu.tsx +84 -0
- package/src/primitives/date-calendar.tsx +76 -0
- package/src/primitives/date-picker.tsx +400 -0
- package/src/primitives/dialog.tsx +279 -0
- package/src/primitives/divider.tsx +31 -0
- package/src/primitives/drawer.tsx +95 -0
- package/src/primitives/dropdown-menu.tsx +164 -0
- package/src/primitives/file-upload.tsx +123 -0
- package/src/primitives/icon-button.tsx +23 -0
- package/src/primitives/input.tsx +124 -0
- package/src/primitives/modal.tsx +26 -0
- package/src/primitives/pagination.tsx +58 -0
- package/src/primitives/popover.tsx +28 -0
- package/src/primitives/progress.tsx +145 -0
- package/src/primitives/radio-group.tsx +63 -0
- package/src/primitives/select.tsx +207 -0
- package/src/primitives/skeleton.tsx +61 -0
- package/src/primitives/slider.tsx +44 -0
- package/src/primitives/switch.tsx +146 -0
- package/src/primitives/table.tsx +47 -0
- package/src/primitives/tabs.tsx +86 -0
- package/src/primitives/textarea.tsx +40 -0
- package/src/primitives/toast.tsx +71 -0
- package/src/primitives/tooltip.tsx +58 -0
- package/src/styles/globals.css +49 -0
- package/src/templates/control-tower.tsx +41 -0
- package/src/templates/executive-analytics.tsx +272 -0
- package/src/templates/workspace-settings.tsx +179 -0
- package/src/utils/cn.ts +7 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
|
3
|
+
import {
|
|
4
|
+
Calendar as CalendarIcon,
|
|
5
|
+
UploadCloud,
|
|
6
|
+
FileText,
|
|
7
|
+
CheckCircle2,
|
|
8
|
+
Trash2,
|
|
9
|
+
ChevronLeft,
|
|
10
|
+
ChevronRight,
|
|
11
|
+
} from "lucide-react"
|
|
12
|
+
import { cn } from "../utils/cn"
|
|
13
|
+
import { Progress } from "./progress"
|
|
14
|
+
|
|
15
|
+
export interface DateRangePreset {
|
|
16
|
+
label: string
|
|
17
|
+
value: string
|
|
18
|
+
rangeText: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const defaultPresets: DateRangePreset[] = [
|
|
22
|
+
{ label: "Last 7 days", value: "7d", rangeText: "Oct 31 – Nov 06, 2026" },
|
|
23
|
+
{ label: "Last 30 days", value: "30d", rangeText: "Oct 08 – Nov 06, 2026" },
|
|
24
|
+
{ label: "Last 90 days", value: "90d", rangeText: "Aug 09 – Nov 06, 2026" },
|
|
25
|
+
{ label: "Year to date", value: "ytd", rangeText: "Jan 01 – Nov 06, 2026" },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
export interface DateRangePickerProps {
|
|
29
|
+
value?: string
|
|
30
|
+
onValueChange?: (preset: DateRangePreset) => void
|
|
31
|
+
className?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const DateRangePicker: React.FC<DateRangePickerProps> = ({
|
|
35
|
+
value = "30d",
|
|
36
|
+
onValueChange,
|
|
37
|
+
className,
|
|
38
|
+
}) => {
|
|
39
|
+
const [selected, setSelected] = React.useState(value)
|
|
40
|
+
const activePreset =
|
|
41
|
+
defaultPresets.find((p) => p.value === selected) || defaultPresets[1]
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<PopoverPrimitive.Root>
|
|
45
|
+
<PopoverPrimitive.Trigger asChild>
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
className={cn(
|
|
49
|
+
"inline-flex h-9 items-center gap-2.5 rounded-lg border border-zinc-200 bg-white px-3 text-xs font-medium text-zinc-700 transition-colors hover:bg-zinc-50 focus:outline-none dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800 cursor-pointer",
|
|
50
|
+
className
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
<CalendarIcon className="h-3.5 w-3.5 text-zinc-400" />
|
|
54
|
+
<span>{activePreset.label}</span>
|
|
55
|
+
<span className="hidden sm:inline border-l border-zinc-200 pl-2 text-[11px] text-zinc-400 dark:border-zinc-800">
|
|
56
|
+
{activePreset.rangeText}
|
|
57
|
+
</span>
|
|
58
|
+
</button>
|
|
59
|
+
</PopoverPrimitive.Trigger>
|
|
60
|
+
<PopoverPrimitive.Portal>
|
|
61
|
+
<PopoverPrimitive.Content
|
|
62
|
+
align="end"
|
|
63
|
+
sideOffset={6}
|
|
64
|
+
className="z-50 w-72 rounded-xl border border-zinc-200 bg-white p-2 text-zinc-900 shadow-lg focus:outline-none dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100"
|
|
65
|
+
>
|
|
66
|
+
<div className="px-2 py-1.5 text-[11px] font-medium text-zinc-400">
|
|
67
|
+
Date range
|
|
68
|
+
</div>
|
|
69
|
+
<div className="space-y-0.5">
|
|
70
|
+
{defaultPresets.map((preset) => {
|
|
71
|
+
const isSelected = preset.value === selected
|
|
72
|
+
return (
|
|
73
|
+
<button
|
|
74
|
+
key={preset.value}
|
|
75
|
+
type="button"
|
|
76
|
+
onClick={() => {
|
|
77
|
+
setSelected(preset.value)
|
|
78
|
+
onValueChange?.(preset)
|
|
79
|
+
}}
|
|
80
|
+
className={cn(
|
|
81
|
+
"flex w-full items-center justify-between rounded-lg px-2.5 py-2 text-xs transition-colors cursor-pointer",
|
|
82
|
+
isSelected
|
|
83
|
+
? "bg-blue-50 text-blue-700 dark:bg-blue-500/15 dark:text-blue-300"
|
|
84
|
+
: "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
<span className="font-medium">{preset.label}</span>
|
|
88
|
+
<span className="text-[11px] text-zinc-400">
|
|
89
|
+
{preset.rangeText}
|
|
90
|
+
</span>
|
|
91
|
+
</button>
|
|
92
|
+
)
|
|
93
|
+
})}
|
|
94
|
+
</div>
|
|
95
|
+
</PopoverPrimitive.Content>
|
|
96
|
+
</PopoverPrimitive.Portal>
|
|
97
|
+
</PopoverPrimitive.Root>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Standalone DateCalendar Primitive
|
|
102
|
+
export const DateCalendar: React.FC<{ className?: string }> = ({ className }) => {
|
|
103
|
+
const [selectedDay, setSelectedDay] = React.useState(14)
|
|
104
|
+
const days = Array.from({ length: 30 }, (_, i) => i + 1)
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div
|
|
108
|
+
className={cn(
|
|
109
|
+
"w-64 rounded-2xl border border-zinc-200 bg-white p-4 shadow-2xs dark:border-zinc-800 dark:bg-zinc-900",
|
|
110
|
+
className
|
|
111
|
+
)}
|
|
112
|
+
>
|
|
113
|
+
<div className="mb-3 flex items-center justify-between">
|
|
114
|
+
<span className="text-xs font-semibold text-zinc-900 dark:text-zinc-100">
|
|
115
|
+
November 2026
|
|
116
|
+
</span>
|
|
117
|
+
<div className="flex items-center gap-1">
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
className="rounded-md p-1 text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer"
|
|
121
|
+
>
|
|
122
|
+
<ChevronLeft className="h-3.5 w-3.5" />
|
|
123
|
+
</button>
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
className="rounded-md p-1 text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer"
|
|
127
|
+
>
|
|
128
|
+
<ChevronRight className="h-3.5 w-3.5" />
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div className="grid grid-cols-7 gap-1 text-center text-[10px] font-medium text-zinc-400 mb-1.5">
|
|
133
|
+
<span>Su</span>
|
|
134
|
+
<span>Mo</span>
|
|
135
|
+
<span>Tu</span>
|
|
136
|
+
<span>We</span>
|
|
137
|
+
<span>Th</span>
|
|
138
|
+
<span>Fr</span>
|
|
139
|
+
<span>Sa</span>
|
|
140
|
+
</div>
|
|
141
|
+
<div className="grid grid-cols-7 gap-1">
|
|
142
|
+
{days.map((d) => {
|
|
143
|
+
const active = d === selectedDay
|
|
144
|
+
return (
|
|
145
|
+
<button
|
|
146
|
+
key={d}
|
|
147
|
+
type="button"
|
|
148
|
+
onClick={() => setSelectedDay(d)}
|
|
149
|
+
className={cn(
|
|
150
|
+
"flex h-7 w-7 items-center justify-center rounded-lg text-xs transition-colors cursor-pointer",
|
|
151
|
+
active
|
|
152
|
+
? "bg-blue-600 font-semibold text-white"
|
|
153
|
+
: "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
|
154
|
+
)}
|
|
155
|
+
>
|
|
156
|
+
{d}
|
|
157
|
+
</button>
|
|
158
|
+
)
|
|
159
|
+
})}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ColorPicker Primitive
|
|
166
|
+
export const ColorPicker: React.FC<{ className?: string }> = ({ className }) => {
|
|
167
|
+
const swatches = [
|
|
168
|
+
"#2563eb",
|
|
169
|
+
"#0ea5e9",
|
|
170
|
+
"#10b981",
|
|
171
|
+
"#f59e0b",
|
|
172
|
+
"#f43f5e",
|
|
173
|
+
"#8b5cf6",
|
|
174
|
+
"#18181b",
|
|
175
|
+
]
|
|
176
|
+
const [color, setColor] = React.useState(swatches[0])
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div
|
|
180
|
+
className={cn(
|
|
181
|
+
"inline-flex flex-col gap-3 rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900",
|
|
182
|
+
className
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
185
|
+
<div className="flex items-center gap-2.5">
|
|
186
|
+
<span
|
|
187
|
+
className="h-6 w-6 rounded-lg border border-black/10 shrink-0"
|
|
188
|
+
style={{ backgroundColor: color }}
|
|
189
|
+
/>
|
|
190
|
+
<input
|
|
191
|
+
type="text"
|
|
192
|
+
value={color}
|
|
193
|
+
onChange={(e) => setColor(e.target.value)}
|
|
194
|
+
className="w-28 rounded-lg border border-zinc-200 bg-zinc-50 px-2.5 py-1 font-mono text-xs text-zinc-800 focus:outline-none dark:border-zinc-800 dark:bg-zinc-800 dark:text-zinc-200"
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
<div className="flex items-center gap-1.5">
|
|
198
|
+
{swatches.map((sw) => (
|
|
199
|
+
<button
|
|
200
|
+
key={sw}
|
|
201
|
+
type="button"
|
|
202
|
+
onClick={() => setColor(sw)}
|
|
203
|
+
style={{ backgroundColor: sw }}
|
|
204
|
+
className={cn(
|
|
205
|
+
"h-6 w-6 rounded-full transition-transform cursor-pointer",
|
|
206
|
+
color === sw && "scale-110 ring-2 ring-offset-2 ring-blue-500 dark:ring-offset-zinc-900"
|
|
207
|
+
)}
|
|
208
|
+
/>
|
|
209
|
+
))}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Carousel Primitive
|
|
216
|
+
export const Carousel: React.FC<{
|
|
217
|
+
slides?: Array<{ title: string; subtitle: string; metric: string }>
|
|
218
|
+
className?: string
|
|
219
|
+
}> = ({
|
|
220
|
+
slides = [
|
|
221
|
+
{
|
|
222
|
+
title: "Q4 Revenue Velocity",
|
|
223
|
+
subtitle: "Net dollar retention across enterprise accounts",
|
|
224
|
+
metric: "128.4%",
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
title: "Global Checkout Conversion",
|
|
228
|
+
subtitle: "30-day rolling average across 14 currencies",
|
|
229
|
+
metric: "4.82%",
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
title: "Customer Support SLA",
|
|
233
|
+
subtitle: "Median first response time for priority tickets",
|
|
234
|
+
metric: "3m 12s",
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
className,
|
|
238
|
+
}) => {
|
|
239
|
+
const [idx, setIdx] = React.useState(0)
|
|
240
|
+
const current = slides[idx]
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div
|
|
244
|
+
className={cn(
|
|
245
|
+
"w-full max-w-md rounded-2xl border border-zinc-200 bg-white p-5 dark:border-zinc-800 dark:bg-zinc-900",
|
|
246
|
+
className
|
|
247
|
+
)}
|
|
248
|
+
>
|
|
249
|
+
<div className="flex items-center justify-between">
|
|
250
|
+
<span className="text-[11px] font-medium text-zinc-400">
|
|
251
|
+
Slide {idx + 1} of {slides.length}
|
|
252
|
+
</span>
|
|
253
|
+
<div className="flex items-center gap-1">
|
|
254
|
+
<button
|
|
255
|
+
type="button"
|
|
256
|
+
onClick={() => setIdx((p) => (p - 1 + slides.length) % slides.length)}
|
|
257
|
+
className="rounded-lg border border-zinc-200 p-1 text-zinc-500 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800 cursor-pointer"
|
|
258
|
+
>
|
|
259
|
+
<ChevronLeft className="h-3.5 w-3.5" />
|
|
260
|
+
</button>
|
|
261
|
+
<button
|
|
262
|
+
type="button"
|
|
263
|
+
onClick={() => setIdx((p) => (p + 1) % slides.length)}
|
|
264
|
+
className="rounded-lg border border-zinc-200 p-1 text-zinc-500 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800 cursor-pointer"
|
|
265
|
+
>
|
|
266
|
+
<ChevronRight className="h-3.5 w-3.5" />
|
|
267
|
+
</button>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
<div className="mt-4 space-y-1">
|
|
271
|
+
<div className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100">
|
|
272
|
+
{current.metric}
|
|
273
|
+
</div>
|
|
274
|
+
<div className="text-xs font-medium text-zinc-800 dark:text-zinc-200">
|
|
275
|
+
{current.title}
|
|
276
|
+
</div>
|
|
277
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
|
278
|
+
{current.subtitle}
|
|
279
|
+
</p>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface UploadedFileItem {
|
|
286
|
+
name: string
|
|
287
|
+
size: string
|
|
288
|
+
progress: number
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface FileDropzoneProps {
|
|
292
|
+
className?: string
|
|
293
|
+
initialFiles?: UploadedFileItem[]
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export const FileDropzone: React.FC<FileDropzoneProps> = ({
|
|
297
|
+
className,
|
|
298
|
+
initialFiles = [
|
|
299
|
+
{ name: "quarterly-report-2026.csv", size: "1.4 MB", progress: 100 },
|
|
300
|
+
{ name: "customer-invoices.pdf", size: "420 KB", progress: 76 },
|
|
301
|
+
],
|
|
302
|
+
}) => {
|
|
303
|
+
const [files, setFiles] = React.useState<UploadedFileItem[]>(initialFiles)
|
|
304
|
+
const [isDragging, setIsDragging] = React.useState(false)
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<div className={cn("space-y-3", className)}>
|
|
308
|
+
<div
|
|
309
|
+
onDragOver={(e) => {
|
|
310
|
+
e.preventDefault()
|
|
311
|
+
setIsDragging(true)
|
|
312
|
+
}}
|
|
313
|
+
onDragLeave={() => setIsDragging(false)}
|
|
314
|
+
onDrop={(e) => {
|
|
315
|
+
e.preventDefault()
|
|
316
|
+
setIsDragging(false)
|
|
317
|
+
const dropped = Array.from(e.dataTransfer.files)
|
|
318
|
+
if (dropped.length > 0) {
|
|
319
|
+
setFiles((prev) => [
|
|
320
|
+
...prev,
|
|
321
|
+
...dropped.map((f) => ({
|
|
322
|
+
name: f.name,
|
|
323
|
+
size: `${Math.max(1, Math.round(f.size / 1024))} KB`,
|
|
324
|
+
progress: 100,
|
|
325
|
+
})),
|
|
326
|
+
])
|
|
327
|
+
}
|
|
328
|
+
}}
|
|
329
|
+
onClick={() => {
|
|
330
|
+
setFiles((prev) => [
|
|
331
|
+
...prev,
|
|
332
|
+
{
|
|
333
|
+
name: `attachment-${prev.length + 1}.pdf`,
|
|
334
|
+
size: "310 KB",
|
|
335
|
+
progress: 100,
|
|
336
|
+
},
|
|
337
|
+
])
|
|
338
|
+
}}
|
|
339
|
+
className={cn(
|
|
340
|
+
"flex flex-col items-center justify-center rounded-xl border border-dashed p-6 text-center transition-colors cursor-pointer",
|
|
341
|
+
isDragging
|
|
342
|
+
? "border-blue-500 bg-blue-50/50 dark:bg-blue-500/10"
|
|
343
|
+
: "border-zinc-300 bg-zinc-50/50 hover:border-zinc-400 dark:border-zinc-800 dark:bg-zinc-900/40 dark:hover:border-zinc-700"
|
|
344
|
+
)}
|
|
345
|
+
>
|
|
346
|
+
<div className="mb-2.5 flex h-9 w-9 items-center justify-center rounded-full bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">
|
|
347
|
+
<UploadCloud className="h-4 w-4" />
|
|
348
|
+
</div>
|
|
349
|
+
<p className="text-xs font-medium text-zinc-800 dark:text-zinc-200">
|
|
350
|
+
Click to upload or drag and drop
|
|
351
|
+
</p>
|
|
352
|
+
<p className="mt-1 text-[11px] text-zinc-400">
|
|
353
|
+
CSV, PDF, JSON, or PNG up to 25MB
|
|
354
|
+
</p>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
{files.length > 0 && (
|
|
358
|
+
<div className="space-y-2">
|
|
359
|
+
{files.map((file, idx) => (
|
|
360
|
+
<div
|
|
361
|
+
key={`${file.name}-${idx}`}
|
|
362
|
+
className="rounded-xl border border-zinc-200 bg-white p-3 space-y-2 dark:border-zinc-800 dark:bg-zinc-900"
|
|
363
|
+
>
|
|
364
|
+
<div className="flex items-center justify-between gap-2">
|
|
365
|
+
<div className="flex items-center gap-2.5 min-w-0">
|
|
366
|
+
<FileText className="h-4 w-4 text-blue-600 dark:text-blue-400 shrink-0" />
|
|
367
|
+
<div className="min-w-0">
|
|
368
|
+
<p className="truncate text-xs font-medium text-zinc-800 dark:text-zinc-200">
|
|
369
|
+
{file.name}
|
|
370
|
+
</p>
|
|
371
|
+
<p className="text-[11px] text-zinc-400">{file.size}</p>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
<div className="flex items-center gap-2">
|
|
375
|
+
{file.progress >= 100 && (
|
|
376
|
+
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
|
|
377
|
+
)}
|
|
378
|
+
<button
|
|
379
|
+
type="button"
|
|
380
|
+
onClick={() =>
|
|
381
|
+
setFiles((prev) => prev.filter((_, i) => i !== idx))
|
|
382
|
+
}
|
|
383
|
+
className="rounded-md p-1 text-zinc-400 hover:bg-zinc-100 hover:text-rose-500 dark:hover:bg-zinc-800 cursor-pointer"
|
|
384
|
+
>
|
|
385
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
386
|
+
</button>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
<Progress
|
|
390
|
+
value={file.progress}
|
|
391
|
+
size="sm"
|
|
392
|
+
variant={file.progress >= 100 ? "emerald" : "blue"}
|
|
393
|
+
/>
|
|
394
|
+
</div>
|
|
395
|
+
))}
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
</div>
|
|
399
|
+
)
|
|
400
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
3
|
+
import { X, ChevronDown, ChevronRight, ArrowRight, ChevronLeft } from "lucide-react"
|
|
4
|
+
import { cn } from "../utils/cn"
|
|
5
|
+
|
|
6
|
+
const Dialog = DialogPrimitive.Root
|
|
7
|
+
const DialogTrigger = DialogPrimitive.Trigger
|
|
8
|
+
const DialogPortal = DialogPrimitive.Portal
|
|
9
|
+
const DialogClose = DialogPrimitive.Close
|
|
10
|
+
|
|
11
|
+
const DialogOverlay = React.forwardRef<
|
|
12
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
13
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
14
|
+
>(({ className, ...props }, ref) => (
|
|
15
|
+
<DialogPrimitive.Overlay
|
|
16
|
+
ref={ref}
|
|
17
|
+
className={cn(
|
|
18
|
+
"fixed inset-0 z-50 bg-black/50 backdrop-blur-xs transition-opacity",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
))
|
|
24
|
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|
25
|
+
|
|
26
|
+
const DialogContent = React.forwardRef<
|
|
27
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
28
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
29
|
+
>(({ className, children, ...props }, ref) => (
|
|
30
|
+
<DialogPortal>
|
|
31
|
+
<DialogOverlay />
|
|
32
|
+
<DialogPrimitive.Content
|
|
33
|
+
ref={ref}
|
|
34
|
+
className={cn(
|
|
35
|
+
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 rounded-2xl border border-zinc-200 bg-white p-6 text-zinc-900 shadow-2xl focus:outline-none dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100",
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-lg p-1.5 text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-700 focus:outline-none dark:hover:bg-zinc-800 dark:hover:text-zinc-200 cursor-pointer">
|
|
42
|
+
<X className="h-4 w-4" />
|
|
43
|
+
<span className="sr-only">Close</span>
|
|
44
|
+
</DialogPrimitive.Close>
|
|
45
|
+
</DialogPrimitive.Content>
|
|
46
|
+
</DialogPortal>
|
|
47
|
+
))
|
|
48
|
+
DialogContent.displayName = DialogPrimitive.Content.displayName
|
|
49
|
+
|
|
50
|
+
const DialogHeader = ({
|
|
51
|
+
className,
|
|
52
|
+
...props
|
|
53
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
54
|
+
<div
|
|
55
|
+
className={cn("flex flex-col space-y-1.5 text-left", className)}
|
|
56
|
+
{...props}
|
|
57
|
+
/>
|
|
58
|
+
)
|
|
59
|
+
DialogHeader.displayName = "DialogHeader"
|
|
60
|
+
|
|
61
|
+
const DialogFooter = ({
|
|
62
|
+
className,
|
|
63
|
+
...props
|
|
64
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
65
|
+
<div
|
|
66
|
+
className={cn(
|
|
67
|
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 pt-3 border-t border-zinc-200 dark:border-zinc-800",
|
|
68
|
+
className
|
|
69
|
+
)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
)
|
|
73
|
+
DialogFooter.displayName = "DialogFooter"
|
|
74
|
+
|
|
75
|
+
const DialogTitle = React.forwardRef<
|
|
76
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
77
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
78
|
+
>(({ className, ...props }, ref) => (
|
|
79
|
+
<DialogPrimitive.Title
|
|
80
|
+
ref={ref}
|
|
81
|
+
className={cn("text-base font-semibold leading-none tracking-tight text-zinc-900 dark:text-zinc-100", className)}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
))
|
|
85
|
+
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
|
86
|
+
|
|
87
|
+
const DialogDescription = React.forwardRef<
|
|
88
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
89
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
90
|
+
>(({ className, ...props }, ref) => (
|
|
91
|
+
<DialogPrimitive.Description
|
|
92
|
+
ref={ref}
|
|
93
|
+
className={cn("text-xs text-zinc-500 dark:text-zinc-400 leading-relaxed", className)}
|
|
94
|
+
{...props}
|
|
95
|
+
/>
|
|
96
|
+
))
|
|
97
|
+
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
|
98
|
+
|
|
99
|
+
// Accordion Primitive
|
|
100
|
+
export interface AccordionItemData {
|
|
101
|
+
id: string
|
|
102
|
+
title: string
|
|
103
|
+
content: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const Accordion: React.FC<{
|
|
107
|
+
items: AccordionItemData[]
|
|
108
|
+
defaultOpenId?: string
|
|
109
|
+
className?: string
|
|
110
|
+
}> = ({ items, defaultOpenId, className }) => {
|
|
111
|
+
const [openId, setOpenId] = React.useState<string | null>(
|
|
112
|
+
defaultOpenId ?? items[0]?.id ?? null
|
|
113
|
+
)
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
className={cn(
|
|
117
|
+
"w-full divide-y divide-zinc-200 rounded-xl border border-zinc-200 bg-white dark:divide-zinc-800 dark:border-zinc-800 dark:bg-zinc-900",
|
|
118
|
+
className
|
|
119
|
+
)}
|
|
120
|
+
>
|
|
121
|
+
{items.map((item) => {
|
|
122
|
+
const isOpen = openId === item.id
|
|
123
|
+
return (
|
|
124
|
+
<div key={item.id}>
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
onClick={() => setOpenId(isOpen ? null : item.id)}
|
|
128
|
+
className="flex w-full items-center justify-between px-4 py-3.5 text-left text-xs font-medium text-zinc-900 hover:bg-zinc-50 dark:text-zinc-100 dark:hover:bg-zinc-800/50 cursor-pointer"
|
|
129
|
+
>
|
|
130
|
+
<span>{item.title}</span>
|
|
131
|
+
<ChevronDown
|
|
132
|
+
className={cn(
|
|
133
|
+
"h-4 w-4 text-zinc-400 transition-transform",
|
|
134
|
+
isOpen && "rotate-180"
|
|
135
|
+
)}
|
|
136
|
+
/>
|
|
137
|
+
</button>
|
|
138
|
+
{isOpen && (
|
|
139
|
+
<div className="px-4 pb-3.5 text-xs text-zinc-500 dark:text-zinc-400 leading-relaxed">
|
|
140
|
+
{item.content}
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
})}
|
|
146
|
+
</div>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Announcement Pill Primitive
|
|
151
|
+
export const Announcement: React.FC<{
|
|
152
|
+
tag?: string
|
|
153
|
+
title: string
|
|
154
|
+
href?: string
|
|
155
|
+
className?: string
|
|
156
|
+
}> = ({ tag = "New", title, href = "#", className }) => (
|
|
157
|
+
<a
|
|
158
|
+
href={href}
|
|
159
|
+
className={cn(
|
|
160
|
+
"inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white px-3 py-1 text-xs text-zinc-700 shadow-2xs transition-colors hover:bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800",
|
|
161
|
+
className
|
|
162
|
+
)}
|
|
163
|
+
>
|
|
164
|
+
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-[10px] font-semibold text-blue-700 dark:bg-blue-500/20 dark:text-blue-300">
|
|
165
|
+
{tag}
|
|
166
|
+
</span>
|
|
167
|
+
<span className="font-medium">{title}</span>
|
|
168
|
+
<ArrowRight className="h-3 w-3 text-zinc-400" />
|
|
169
|
+
</a>
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
// Breadcrumb Primitive
|
|
173
|
+
export const Breadcrumb: React.FC<{
|
|
174
|
+
items: Array<{ label: string; href?: string }>
|
|
175
|
+
className?: string
|
|
176
|
+
}> = ({ items, className }) => (
|
|
177
|
+
<nav aria-label="Breadcrumb" className={cn("flex items-center gap-1.5 text-xs", className)}>
|
|
178
|
+
{items.map((item, idx) => {
|
|
179
|
+
const isLast = idx === items.length - 1
|
|
180
|
+
return (
|
|
181
|
+
<React.Fragment key={item.label}>
|
|
182
|
+
{idx > 0 && <ChevronRight className="h-3.5 w-3.5 text-zinc-400 shrink-0" />}
|
|
183
|
+
{item.href && !isLast ? (
|
|
184
|
+
<a
|
|
185
|
+
href={item.href}
|
|
186
|
+
className="text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors"
|
|
187
|
+
>
|
|
188
|
+
{item.label}
|
|
189
|
+
</a>
|
|
190
|
+
) : (
|
|
191
|
+
<span className="font-medium text-zinc-900 dark:text-zinc-100">
|
|
192
|
+
{item.label}
|
|
193
|
+
</span>
|
|
194
|
+
)}
|
|
195
|
+
</React.Fragment>
|
|
196
|
+
)
|
|
197
|
+
})}
|
|
198
|
+
</nav>
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
// Divider Primitive
|
|
202
|
+
export const Divider: React.FC<{ label?: string; className?: string }> = ({
|
|
203
|
+
label,
|
|
204
|
+
className,
|
|
205
|
+
}) => {
|
|
206
|
+
if (!label) {
|
|
207
|
+
return <hr className={cn("border-t border-zinc-200 dark:border-zinc-800", className)} />
|
|
208
|
+
}
|
|
209
|
+
return (
|
|
210
|
+
<div className={cn("relative flex items-center py-2", className)}>
|
|
211
|
+
<div className="grow border-t border-zinc-200 dark:border-zinc-800" />
|
|
212
|
+
<span className="mx-3 shrink text-[11px] font-medium text-zinc-400 uppercase tracking-wider">
|
|
213
|
+
{label}
|
|
214
|
+
</span>
|
|
215
|
+
<div className="grow border-t border-zinc-200 dark:border-zinc-800" />
|
|
216
|
+
</div>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Pagination Primitive
|
|
221
|
+
export const Pagination: React.FC<{
|
|
222
|
+
totalPages?: number
|
|
223
|
+
initialPage?: number
|
|
224
|
+
className?: string
|
|
225
|
+
}> = ({ totalPages = 5, initialPage = 1, className }) => {
|
|
226
|
+
const [page, setPage] = React.useState(initialPage)
|
|
227
|
+
return (
|
|
228
|
+
<div className={cn("inline-flex items-center gap-1", className)}>
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
disabled={page <= 1}
|
|
232
|
+
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
|
233
|
+
className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 disabled:opacity-40 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800 cursor-pointer"
|
|
234
|
+
>
|
|
235
|
+
<ChevronLeft className="h-3.5 w-3.5" />
|
|
236
|
+
</button>
|
|
237
|
+
{Array.from({ length: totalPages }).map((_, idx) => {
|
|
238
|
+
const num = idx + 1
|
|
239
|
+
const active = num === page
|
|
240
|
+
return (
|
|
241
|
+
<button
|
|
242
|
+
key={num}
|
|
243
|
+
type="button"
|
|
244
|
+
onClick={() => setPage(num)}
|
|
245
|
+
className={cn(
|
|
246
|
+
"inline-flex h-8 w-8 items-center justify-center rounded-lg text-xs font-medium transition-colors cursor-pointer",
|
|
247
|
+
active
|
|
248
|
+
? "bg-blue-600 text-white"
|
|
249
|
+
: "border border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
|
250
|
+
)}
|
|
251
|
+
>
|
|
252
|
+
{num}
|
|
253
|
+
</button>
|
|
254
|
+
)
|
|
255
|
+
})}
|
|
256
|
+
<button
|
|
257
|
+
type="button"
|
|
258
|
+
disabled={page >= totalPages}
|
|
259
|
+
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
|
260
|
+
className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 disabled:opacity-40 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800 cursor-pointer"
|
|
261
|
+
>
|
|
262
|
+
<ChevronRight className="h-3.5 w-3.5" />
|
|
263
|
+
</button>
|
|
264
|
+
</div>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export {
|
|
269
|
+
Dialog,
|
|
270
|
+
DialogPortal,
|
|
271
|
+
DialogOverlay,
|
|
272
|
+
DialogClose,
|
|
273
|
+
DialogTrigger,
|
|
274
|
+
DialogContent,
|
|
275
|
+
DialogHeader,
|
|
276
|
+
DialogFooter,
|
|
277
|
+
DialogTitle,
|
|
278
|
+
DialogDescription,
|
|
279
|
+
}
|