@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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -2
  3. package/dist/index.d.mts +583 -0
  4. package/dist/index.d.ts +583 -0
  5. package/dist/index.js +5189 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +5189 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +83 -3
  10. package/src/blocks/app-shell.tsx +441 -0
  11. package/src/blocks/billing-card.tsx +2 -0
  12. package/src/blocks/data-table.tsx +479 -0
  13. package/src/blocks/filter-bar.tsx +134 -0
  14. package/src/blocks/header-navbar.tsx +7 -0
  15. package/src/blocks/metric-grid.tsx +5 -0
  16. package/src/blocks/profile-form.tsx +2 -0
  17. package/src/blocks/security-settings.tsx +2 -0
  18. package/src/blocks/settings-blocks.tsx +406 -0
  19. package/src/blocks/sidebar.tsx +6 -0
  20. package/src/blocks/stat-card.tsx +7 -0
  21. package/src/blocks/stat-cards.tsx +212 -0
  22. package/src/blocks/team-members.tsx +5 -0
  23. package/src/index.ts +114 -0
  24. package/src/primitives/accordion.tsx +61 -0
  25. package/src/primitives/announcement.tsx +33 -0
  26. package/src/primitives/avatar.tsx +156 -0
  27. package/src/primitives/badge.tsx +108 -0
  28. package/src/primitives/breadcrumb.tsx +45 -0
  29. package/src/primitives/button-group.tsx +79 -0
  30. package/src/primitives/button.tsx +165 -0
  31. package/src/primitives/carousel.tsx +84 -0
  32. package/src/primitives/chart.tsx +831 -0
  33. package/src/primitives/checkbox.tsx +26 -0
  34. package/src/primitives/chip.tsx +37 -0
  35. package/src/primitives/close-button.tsx +29 -0
  36. package/src/primitives/color-picker.tsx +62 -0
  37. package/src/primitives/combobox.tsx +123 -0
  38. package/src/primitives/command-palette.tsx +216 -0
  39. package/src/primitives/context-menu.tsx +84 -0
  40. package/src/primitives/date-calendar.tsx +76 -0
  41. package/src/primitives/date-picker.tsx +400 -0
  42. package/src/primitives/dialog.tsx +279 -0
  43. package/src/primitives/divider.tsx +31 -0
  44. package/src/primitives/drawer.tsx +95 -0
  45. package/src/primitives/dropdown-menu.tsx +164 -0
  46. package/src/primitives/file-upload.tsx +123 -0
  47. package/src/primitives/icon-button.tsx +23 -0
  48. package/src/primitives/input.tsx +124 -0
  49. package/src/primitives/modal.tsx +26 -0
  50. package/src/primitives/pagination.tsx +58 -0
  51. package/src/primitives/popover.tsx +28 -0
  52. package/src/primitives/progress.tsx +145 -0
  53. package/src/primitives/radio-group.tsx +63 -0
  54. package/src/primitives/select.tsx +207 -0
  55. package/src/primitives/skeleton.tsx +61 -0
  56. package/src/primitives/slider.tsx +44 -0
  57. package/src/primitives/switch.tsx +146 -0
  58. package/src/primitives/table.tsx +47 -0
  59. package/src/primitives/tabs.tsx +86 -0
  60. package/src/primitives/textarea.tsx +40 -0
  61. package/src/primitives/toast.tsx +71 -0
  62. package/src/primitives/tooltip.tsx +58 -0
  63. package/src/styles/globals.css +49 -0
  64. package/src/templates/control-tower.tsx +41 -0
  65. package/src/templates/executive-analytics.tsx +272 -0
  66. package/src/templates/workspace-settings.tsx +179 -0
  67. 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
+ }