@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,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
|
+
}
|