@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,479 @@
1
+ import * as React from "react"
2
+ import {
3
+ ArrowUpDown,
4
+ Search,
5
+ MoreHorizontal,
6
+ Download,
7
+ Eye,
8
+ Copy,
9
+ Trash2,
10
+ ChevronLeft,
11
+ ChevronRight,
12
+ SlidersHorizontal,
13
+ } from "lucide-react"
14
+ import { cn } from "../utils/cn"
15
+ import { Avatar } from "../primitives/avatar"
16
+ import { Badge } from "../primitives/badge"
17
+ import { Button } from "../primitives/button"
18
+ import { Checkbox } from "../primitives/switch"
19
+ import { Input } from "../primitives/input"
20
+ import {
21
+ DropdownMenu,
22
+ DropdownMenuTrigger,
23
+ DropdownMenuContent,
24
+ DropdownMenuItem,
25
+ DropdownMenuSeparator,
26
+ } from "../primitives/dropdown-menu"
27
+ import {
28
+ Drawer,
29
+ DrawerContent,
30
+ DrawerHeader,
31
+ DrawerTitle,
32
+ DrawerDescription,
33
+ DrawerFooter,
34
+ } from "../primitives/drawer"
35
+ import { Progress } from "../primitives/progress"
36
+
37
+ export interface EnterpriseRowData {
38
+ id: string
39
+ customer: string
40
+ email: string
41
+ plan: string
42
+ status: "active" | "trialing" | "past_due" | "canceled"
43
+ mrrAmount: number
44
+ mrrFormatted: string
45
+ seatUsage: number
46
+ updatedAt: string
47
+ }
48
+
49
+ const defaultRows: EnterpriseRowData[] = [
50
+ {
51
+ id: "cus-9921",
52
+ customer: "Linear Orbit Inc.",
53
+ email: "billing@linearorbit.io",
54
+ plan: "Enterprise Annual",
55
+ status: "active",
56
+ mrrAmount: 4200,
57
+ mrrFormatted: "$4,200.00",
58
+ seatUsage: 84,
59
+ updatedAt: "2 hours ago",
60
+ },
61
+ {
62
+ id: "cus-8840",
63
+ customer: "Raycast Labs GmbH",
64
+ email: "finance@raycastlabs.de",
65
+ plan: "Enterprise Annual",
66
+ status: "active",
67
+ mrrAmount: 3150,
68
+ mrrFormatted: "$3,150.00",
69
+ seatUsage: 62,
70
+ updatedAt: "5 hours ago",
71
+ },
72
+ {
73
+ id: "cus-7732",
74
+ customer: "chronicle.so",
75
+ email: "accounts@chronicle.so",
76
+ plan: "Pro Monthly",
77
+ status: "trialing",
78
+ mrrAmount: 890,
79
+ mrrFormatted: "$890.00",
80
+ seatUsage: 45,
81
+ updatedAt: "1 day ago",
82
+ },
83
+ {
84
+ id: "cus-6419",
85
+ customer: "Northstar Analytics",
86
+ email: "ops@northstar.co",
87
+ plan: "Scale Annual",
88
+ status: "past_due",
89
+ mrrAmount: 1950,
90
+ mrrFormatted: "$1,950.00",
91
+ seatUsage: 92,
92
+ updatedAt: "2 days ago",
93
+ },
94
+ {
95
+ id: "cus-5108",
96
+ customer: "Veloce Design Studio",
97
+ email: "team@veloce.design",
98
+ plan: "Pro Monthly",
99
+ status: "active",
100
+ mrrAmount: 640,
101
+ mrrFormatted: "$640.00",
102
+ seatUsage: 38,
103
+ updatedAt: "3 days ago",
104
+ },
105
+ ]
106
+
107
+ const statusBadgeVariant: Record<
108
+ EnterpriseRowData["status"],
109
+ { variant: "emerald" | "blue" | "amber" | "rose"; label: string }
110
+ > = {
111
+ active: { variant: "emerald", label: "Active" },
112
+ trialing: { variant: "blue", label: "Trialing" },
113
+ past_due: { variant: "amber", label: "Past Due" },
114
+ canceled: { variant: "rose", label: "Canceled" },
115
+ }
116
+
117
+ export interface EnterpriseDataTableProps {
118
+ data?: EnterpriseRowData[]
119
+ className?: string
120
+ }
121
+
122
+ export const EnterpriseDataTable: React.FC<EnterpriseDataTableProps> = ({
123
+ data = defaultRows,
124
+ className,
125
+ }) => {
126
+ const [rows, setRows] = React.useState<EnterpriseRowData[]>(data)
127
+ const [search, setSearch] = React.useState("")
128
+ const [statusFilter, setStatusFilter] = React.useState<string>("all")
129
+ const [sortAsc, setSortAsc] = React.useState(false)
130
+ const [selectedIds, setSelectedIds] = React.useState<Record<string, boolean>>({})
131
+ const [inspectedRow, setInspectedRow] = React.useState<EnterpriseRowData | null>(null)
132
+ const [showPlanCol, setShowPlanCol] = React.useState(true)
133
+
134
+ const filteredRows = React.useMemo(() => {
135
+ return rows
136
+ .filter((r) => {
137
+ if (statusFilter !== "all" && r.status !== statusFilter) return false
138
+ if (!search.trim()) return true
139
+ const q = search.toLowerCase()
140
+ return (
141
+ r.customer.toLowerCase().includes(q) ||
142
+ r.email.toLowerCase().includes(q) ||
143
+ r.id.toLowerCase().includes(q) ||
144
+ r.plan.toLowerCase().includes(q)
145
+ )
146
+ })
147
+ .sort((a, b) =>
148
+ sortAsc ? a.mrrAmount - b.mrrAmount : b.mrrAmount - a.mrrAmount
149
+ )
150
+ }, [rows, search, statusFilter, sortAsc])
151
+
152
+ const selectedCount = Object.values(selectedIds).filter(Boolean).length
153
+
154
+ return (
155
+ <div
156
+ className={cn(
157
+ "rounded-xl border border-zinc-200 bg-white overflow-hidden dark:border-zinc-800 dark:bg-zinc-900",
158
+ className
159
+ )}
160
+ >
161
+ {/* Table Toolbar */}
162
+ <div className="flex flex-col gap-3 border-b border-zinc-200 p-4 lg:flex-row lg:items-center lg:justify-between dark:border-zinc-800">
163
+ <div className="flex flex-wrap items-center gap-2.5">
164
+ <Input
165
+ value={search}
166
+ onChange={(e) => setSearch(e.target.value)}
167
+ placeholder="Filter customers..."
168
+ leftIcon={<Search className="h-3.5 w-3.5" />}
169
+ className="w-full sm:w-64"
170
+ />
171
+
172
+ <div className="flex items-center rounded-lg border border-zinc-200 bg-zinc-100/70 p-0.5 text-xs dark:border-zinc-800 dark:bg-zinc-800/60">
173
+ {(["all", "active", "trialing", "past_due"] as const).map((st) => (
174
+ <button
175
+ key={st}
176
+ type="button"
177
+ onClick={() => setStatusFilter(st)}
178
+ className={cn(
179
+ "rounded-md px-2.5 py-1 capitalize transition-colors cursor-pointer",
180
+ statusFilter === st
181
+ ? "bg-white text-zinc-900 font-medium shadow-2xs dark:bg-zinc-900 dark:text-white"
182
+ : "text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-200"
183
+ )}
184
+ >
185
+ {st.replace("_", " ")}
186
+ </button>
187
+ ))}
188
+ </div>
189
+ </div>
190
+
191
+ <div className="flex items-center gap-2">
192
+ {selectedCount > 0 && (
193
+ <Badge variant="blue">{selectedCount} selected</Badge>
194
+ )}
195
+ <Button
196
+ variant="outline"
197
+ size="sm"
198
+ onClick={() => setShowPlanCol((prev) => !prev)}
199
+ >
200
+ <SlidersHorizontal className="h-3.5 w-3.5" />
201
+ Columns
202
+ </Button>
203
+ <Button variant="outline" size="sm">
204
+ <Download className="h-3.5 w-3.5" />
205
+ Export
206
+ </Button>
207
+ </div>
208
+ </div>
209
+
210
+ {/* Table */}
211
+ <div className="overflow-x-auto">
212
+ <table className="w-full border-collapse text-left text-xs">
213
+ <thead>
214
+ <tr className="border-b border-zinc-200 bg-zinc-50/70 text-[11px] font-medium text-zinc-500 dark:border-zinc-800 dark:bg-zinc-950/40 dark:text-zinc-400">
215
+ <th className="w-10 px-4 py-3">
216
+ <Checkbox
217
+ checked={
218
+ filteredRows.length > 0 &&
219
+ filteredRows.every((r) => selectedIds[r.id])
220
+ }
221
+ onCheckedChange={(checked) => {
222
+ const next: Record<string, boolean> = {}
223
+ if (checked) {
224
+ filteredRows.forEach((r) => {
225
+ next[r.id] = true
226
+ })
227
+ }
228
+ setSelectedIds(next)
229
+ }}
230
+ />
231
+ </th>
232
+ <th className="px-4 py-3">Customer</th>
233
+ {showPlanCol && <th className="px-4 py-3">Plan</th>}
234
+ <th className="px-4 py-3">Status</th>
235
+ <th className="px-4 py-3">
236
+ <button
237
+ type="button"
238
+ onClick={() => setSortAsc(!sortAsc)}
239
+ className="inline-flex items-center gap-1 text-zinc-600 hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-white cursor-pointer"
240
+ >
241
+ MRR
242
+ <ArrowUpDown className="h-3 w-3 text-zinc-400" />
243
+ </button>
244
+ </th>
245
+ <th className="px-4 py-3">Seat Utilization</th>
246
+ <th className="px-4 py-3">Updated</th>
247
+ <th className="px-4 py-3 text-right">Actions</th>
248
+ </tr>
249
+ </thead>
250
+ <tbody className="divide-y divide-zinc-200 dark:divide-zinc-800">
251
+ {filteredRows.length === 0 ? (
252
+ <tr>
253
+ <td
254
+ colSpan={8}
255
+ className="py-12 text-center text-xs text-zinc-400"
256
+ >
257
+ No customers match your filter criteria.
258
+ </td>
259
+ </tr>
260
+ ) : (
261
+ filteredRows.map((row, idx) => {
262
+ const badgeInfo = statusBadgeVariant[row.status]
263
+ return (
264
+ <tr
265
+ key={row.id}
266
+ onClick={() => setInspectedRow(row)}
267
+ className="group transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-800/40 cursor-pointer"
268
+ >
269
+ <td
270
+ className="px-4 py-3"
271
+ onClick={(e) => e.stopPropagation()}
272
+ >
273
+ <Checkbox
274
+ checked={Boolean(selectedIds[row.id])}
275
+ onCheckedChange={(checked) =>
276
+ setSelectedIds((prev) => ({
277
+ ...prev,
278
+ [row.id]: Boolean(checked),
279
+ }))
280
+ }
281
+ />
282
+ </td>
283
+ <td className="px-4 py-3">
284
+ <div className="flex items-center gap-2.5">
285
+ <Avatar
286
+ fallback={row.customer.slice(0, 2)}
287
+ tint={idx % 2 === 0 ? "blue" : "neutral"}
288
+ size="sm"
289
+ />
290
+ <div>
291
+ <div className="font-medium text-zinc-900 dark:text-zinc-100">
292
+ {row.customer}
293
+ </div>
294
+ <div className="text-[11px] text-zinc-500">
295
+ {row.email}
296
+ </div>
297
+ </div>
298
+ </div>
299
+ </td>
300
+ {showPlanCol && (
301
+ <td className="px-4 py-3 text-zinc-600 dark:text-zinc-300">
302
+ {row.plan}
303
+ </td>
304
+ )}
305
+ <td className="px-4 py-3">
306
+ <Badge variant={badgeInfo.variant} dot>
307
+ {badgeInfo.label}
308
+ </Badge>
309
+ </td>
310
+ <td className="px-4 py-3 font-medium text-zinc-900 dark:text-zinc-100">
311
+ {row.mrrFormatted}
312
+ </td>
313
+ <td className="px-4 py-3 w-36">
314
+ <Progress
315
+ value={row.seatUsage}
316
+ size="sm"
317
+ variant={row.seatUsage > 85 ? "amber" : "blue"}
318
+ />
319
+ </td>
320
+ <td className="px-4 py-3 text-zinc-500">{row.updatedAt}</td>
321
+ <td
322
+ className="px-4 py-3 text-right"
323
+ onClick={(e) => e.stopPropagation()}
324
+ >
325
+ <DropdownMenu>
326
+ <DropdownMenuTrigger asChild>
327
+ <button
328
+ type="button"
329
+ className="rounded-lg p-1.5 text-zinc-400 hover:bg-zinc-100 hover:text-zinc-700 dark:hover:bg-zinc-800 dark:hover:text-zinc-200 cursor-pointer"
330
+ >
331
+ <MoreHorizontal className="h-4 w-4" />
332
+ </button>
333
+ </DropdownMenuTrigger>
334
+ <DropdownMenuContent align="end">
335
+ <DropdownMenuItem
336
+ onSelect={() => setInspectedRow(row)}
337
+ >
338
+ <Eye className="h-3.5 w-3.5 text-zinc-400" />
339
+ View details
340
+ </DropdownMenuItem>
341
+ <DropdownMenuItem
342
+ onSelect={() =>
343
+ navigator.clipboard?.writeText(row.id)
344
+ }
345
+ >
346
+ <Copy className="h-3.5 w-3.5 text-zinc-400" />
347
+ Copy customer ID
348
+ </DropdownMenuItem>
349
+ <DropdownMenuSeparator />
350
+ <DropdownMenuItem
351
+ destructive
352
+ onSelect={() =>
353
+ setRows((prev) =>
354
+ prev.filter((item) => item.id !== row.id)
355
+ )
356
+ }
357
+ >
358
+ <Trash2 className="h-3.5 w-3.5" />
359
+ Remove customer
360
+ </DropdownMenuItem>
361
+ </DropdownMenuContent>
362
+ </DropdownMenu>
363
+ </td>
364
+ </tr>
365
+ )
366
+ })
367
+ )}
368
+ </tbody>
369
+ </table>
370
+ </div>
371
+
372
+ {/* Pagination Footer */}
373
+ <div className="flex items-center justify-between border-t border-zinc-200 px-4 py-3 text-xs text-zinc-500 dark:border-zinc-800 dark:text-zinc-400">
374
+ <div>
375
+ Showing <strong className="text-zinc-900 dark:text-zinc-100">1</strong>{" "}
376
+ to{" "}
377
+ <strong className="text-zinc-900 dark:text-zinc-100">
378
+ {filteredRows.length}
379
+ </strong>{" "}
380
+ of{" "}
381
+ <strong className="text-zinc-900 dark:text-zinc-100">
382
+ {rows.length}
383
+ </strong>{" "}
384
+ results
385
+ </div>
386
+ <div className="flex items-center gap-1.5">
387
+ <Button variant="outline" size="sm" disabled>
388
+ <ChevronLeft className="h-3.5 w-3.5" />
389
+ Previous
390
+ </Button>
391
+ <Button variant="outline" size="sm">
392
+ Next
393
+ <ChevronRight className="h-3.5 w-3.5" />
394
+ </Button>
395
+ </div>
396
+ </div>
397
+
398
+ {/* Slide-over Inspector Drawer */}
399
+ <Drawer
400
+ open={Boolean(inspectedRow)}
401
+ onOpenChange={(open) => !open && setInspectedRow(null)}
402
+ >
403
+ <DrawerContent side="right">
404
+ {inspectedRow && (
405
+ <>
406
+ <DrawerHeader>
407
+ <div className="flex items-center gap-2">
408
+ <Badge
409
+ variant={statusBadgeVariant[inspectedRow.status].variant}
410
+ dot
411
+ >
412
+ {statusBadgeVariant[inspectedRow.status].label}
413
+ </Badge>
414
+ <span className="font-mono text-xs text-zinc-400">
415
+ {inspectedRow.id}
416
+ </span>
417
+ </div>
418
+ <DrawerTitle className="mt-2">
419
+ {inspectedRow.customer}
420
+ </DrawerTitle>
421
+ <DrawerDescription>
422
+ {inspectedRow.email} • {inspectedRow.plan}
423
+ </DrawerDescription>
424
+ </DrawerHeader>
425
+
426
+ <div className="my-5 space-y-4 overflow-y-auto text-xs">
427
+ <div className="grid grid-cols-2 gap-3">
428
+ <div className="rounded-xl border border-zinc-200 bg-zinc-50 p-3.5 dark:border-zinc-800 dark:bg-zinc-800/50">
429
+ <span className="text-zinc-500">Monthly Revenue</span>
430
+ <div className="mt-1 text-lg font-semibold text-zinc-900 dark:text-white">
431
+ {inspectedRow.mrrFormatted}
432
+ </div>
433
+ </div>
434
+ <div className="rounded-xl border border-zinc-200 bg-zinc-50 p-3.5 dark:border-zinc-800 dark:bg-zinc-800/50">
435
+ <span className="text-zinc-500">Seat Utilization</span>
436
+ <div className="mt-1 text-lg font-semibold text-zinc-900 dark:text-white">
437
+ {inspectedRow.seatUsage}%
438
+ </div>
439
+ </div>
440
+ </div>
441
+
442
+ <div className="rounded-xl border border-zinc-200 p-4 space-y-3 dark:border-zinc-800">
443
+ <div className="font-medium text-zinc-900 dark:text-zinc-100">
444
+ Quota Usage
445
+ </div>
446
+ <Progress
447
+ value={inspectedRow.seatUsage}
448
+ label="Team Seats"
449
+ showValue
450
+ variant="blue"
451
+ />
452
+ <Progress
453
+ value={64}
454
+ label="Storage Volume"
455
+ showValue
456
+ variant="emerald"
457
+ />
458
+ </div>
459
+ </div>
460
+
461
+ <DrawerFooter>
462
+ <Button
463
+ variant="outline"
464
+ size="sm"
465
+ onClick={() => setInspectedRow(null)}
466
+ >
467
+ Close
468
+ </Button>
469
+ <Button variant="primary" size="sm">
470
+ Edit subscription
471
+ </Button>
472
+ </DrawerFooter>
473
+ </>
474
+ )}
475
+ </DrawerContent>
476
+ </Drawer>
477
+ </div>
478
+ )
479
+ }
@@ -0,0 +1,134 @@
1
+ import * as React from "react"
2
+ import { Search, SlidersHorizontal, X, RotateCcw, LayoutGrid, List } from "lucide-react"
3
+ import { cn } from "../utils/cn"
4
+ import { Input } from "../primitives/input"
5
+ import { Button, ToggleGroup } from "../primitives/button"
6
+ import { DateRangePicker } from "../primitives/date-picker"
7
+
8
+ export interface ActiveFilterPill {
9
+ id: string
10
+ field: string
11
+ value: string
12
+ }
13
+
14
+ export interface AdvancedFilterBarProps {
15
+ searchQuery?: string
16
+ onSearchChange?: (q: string) => void
17
+ initialPills?: ActiveFilterPill[]
18
+ className?: string
19
+ }
20
+
21
+ export const AdvancedFilterBar: React.FC<AdvancedFilterBarProps> = ({
22
+ searchQuery = "",
23
+ onSearchChange,
24
+ initialPills = [
25
+ { id: "status", field: "Status", value: "Active, Trialing" },
26
+ { id: "plan", field: "Plan", value: "Enterprise, Pro" },
27
+ ],
28
+ className,
29
+ }) => {
30
+ const [query, setQuery] = React.useState(searchQuery)
31
+ const [pills, setPills] = React.useState<ActiveFilterPill[]>(initialPills)
32
+ const [viewMode, setViewMode] = React.useState("table")
33
+
34
+ const handleSearch = (val: string) => {
35
+ setQuery(val)
36
+ onSearchChange?.(val)
37
+ }
38
+
39
+ return (
40
+ <div
41
+ className={cn(
42
+ "rounded-xl border border-zinc-200 bg-white p-3.5 space-y-3 dark:border-zinc-800 dark:bg-zinc-900",
43
+ className
44
+ )}
45
+ >
46
+ <div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
47
+ <div className="flex flex-1 items-center gap-2.5">
48
+ <Input
49
+ value={query}
50
+ onChange={(e) => handleSearch(e.target.value)}
51
+ placeholder="Search customers, emails, or invoices..."
52
+ leftIcon={<Search className="h-3.5 w-3.5" />}
53
+ className="max-w-md"
54
+ />
55
+ <Button
56
+ variant="outline"
57
+ size="sm"
58
+ onClick={() => {
59
+ if (!pills.some((p) => p.id === "region")) {
60
+ setPills((prev) => [
61
+ ...prev,
62
+ { id: "region", field: "Region", value: "North America" },
63
+ ])
64
+ }
65
+ }}
66
+ >
67
+ <SlidersHorizontal className="h-3.5 w-3.5 text-zinc-400" />
68
+ Filter
69
+ </Button>
70
+ </div>
71
+
72
+ <div className="flex flex-wrap items-center gap-2">
73
+ <DateRangePicker />
74
+ <ToggleGroup
75
+ size="sm"
76
+ value={viewMode}
77
+ onValueChange={setViewMode}
78
+ items={[
79
+ {
80
+ value: "table",
81
+ label: "List",
82
+ icon: <List className="h-3.5 w-3.5" />,
83
+ },
84
+ {
85
+ value: "grid",
86
+ label: "Grid",
87
+ icon: <LayoutGrid className="h-3.5 w-3.5" />,
88
+ },
89
+ ]}
90
+ />
91
+ </div>
92
+ </div>
93
+
94
+ {pills.length > 0 && (
95
+ <div className="flex flex-wrap items-center gap-2 border-t border-zinc-200 pt-2.5 dark:border-zinc-800">
96
+ <span className="text-[11px] font-medium text-zinc-400">
97
+ Active filters:
98
+ </span>
99
+ {pills.map((pill) => (
100
+ <span
101
+ key={pill.id}
102
+ className="inline-flex items-center gap-1.5 rounded-lg border border-zinc-200 bg-zinc-50 px-2.5 py-1 text-xs text-zinc-700 dark:border-zinc-800 dark:bg-zinc-800/70 dark:text-zinc-200"
103
+ >
104
+ <span className="text-zinc-400">{pill.field}:</span>
105
+ <span className="font-medium text-zinc-900 dark:text-zinc-100">
106
+ {pill.value}
107
+ </span>
108
+ <button
109
+ type="button"
110
+ onClick={() =>
111
+ setPills((prev) => prev.filter((item) => item.id !== pill.id))
112
+ }
113
+ className="ml-0.5 rounded p-0.5 text-zinc-400 hover:bg-zinc-200/70 hover:text-zinc-700 dark:hover:bg-zinc-700 dark:hover:text-zinc-200 cursor-pointer"
114
+ >
115
+ <X className="h-3 w-3" />
116
+ </button>
117
+ </span>
118
+ ))}
119
+ <button
120
+ type="button"
121
+ onClick={() => {
122
+ setPills([])
123
+ handleSearch("")
124
+ }}
125
+ className="inline-flex items-center gap-1 text-xs text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white ml-auto cursor-pointer"
126
+ >
127
+ <RotateCcw className="h-3 w-3" />
128
+ Clear all
129
+ </button>
130
+ </div>
131
+ )}
132
+ </div>
133
+ )
134
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ HeaderNavbar,
3
+ PageHeaderBar,
4
+ type HeaderNavbarProps,
5
+ type PageHeaderBarProps,
6
+ } from "./app-shell"
7
+
@@ -0,0 +1,5 @@
1
+ export {
2
+ MultiMetricGrid,
3
+ type MultiMetricGridProps,
4
+ } from "./stat-cards"
5
+
@@ -0,0 +1,2 @@
1
+ export { UserProfileFormBlock } from "./settings-blocks"
2
+
@@ -0,0 +1,2 @@
1
+ export { SecurityPasswordFormBlock } from "./settings-blocks"
2
+