@etherealui/react 0.0.0-stage → 0.0.2

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,406 @@
1
+ import * as React from "react"
2
+ import {
3
+ Shield,
4
+ Plus,
5
+ CreditCard,
6
+ Check,
7
+ Smartphone,
8
+ Laptop,
9
+ } from "lucide-react"
10
+ import { cn } from "../utils/cn"
11
+ import { Avatar } from "../primitives/avatar"
12
+ import { Badge } from "../primitives/badge"
13
+ import { Button } from "../primitives/button"
14
+ import { Input, Label, Textarea } from "../primitives/input"
15
+ import { Switch } from "../primitives/switch"
16
+ import {
17
+ Select,
18
+ SelectTrigger,
19
+ SelectValue,
20
+ SelectContent,
21
+ SelectItem,
22
+ } from "../primitives/select"
23
+ import { Progress } from "../primitives/progress"
24
+
25
+ export const UserProfileFormBlock: React.FC<{ className?: string }> = ({
26
+ className,
27
+ }) => {
28
+ const [saved, setSaved] = React.useState(false)
29
+ const [timezone, setTimezone] = React.useState("utc+7")
30
+
31
+ return (
32
+ <div
33
+ className={cn(
34
+ "rounded-xl border border-zinc-200 bg-white p-6 space-y-6 dark:border-zinc-800 dark:bg-zinc-900",
35
+ className
36
+ )}
37
+ >
38
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between border-b border-zinc-200 pb-5 dark:border-zinc-800">
39
+ <div className="flex items-center gap-4">
40
+ <Avatar fallback="SR" tint="blue" size="xl" status="online" />
41
+ <div>
42
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
43
+ Profile details
44
+ </h3>
45
+ <p className="text-xs text-zinc-500 dark:text-zinc-400">
46
+ Update your photo and personal details here.
47
+ </p>
48
+ </div>
49
+ </div>
50
+ <Button variant="outline" size="sm">
51
+ Change photo
52
+ </Button>
53
+ </div>
54
+
55
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
56
+ <div className="space-y-2">
57
+ <Label>Full name</Label>
58
+ <Input defaultValue="Saeful Rohman" />
59
+ </div>
60
+ <div className="space-y-2">
61
+ <Label>Username</Label>
62
+ <Input prefixText="ethereal-ui.dev/" defaultValue="saefulrdevs" />
63
+ </div>
64
+ <div className="space-y-2">
65
+ <Label>Email address</Label>
66
+ <Input type="email" defaultValue="saeful@acme.com" />
67
+ </div>
68
+ <div className="space-y-2">
69
+ <Label>Timezone</Label>
70
+ <Select value={timezone} onValueChange={setTimezone}>
71
+ <SelectTrigger>
72
+ <SelectValue placeholder="Select timezone" />
73
+ </SelectTrigger>
74
+ <SelectContent>
75
+ <SelectItem value="utc+7">Asia/Jakarta (GMT+07:00)</SelectItem>
76
+ <SelectItem value="utc+0">UTC / London (GMT+00:00)</SelectItem>
77
+ <SelectItem value="utc-5">America/New_York (EST)</SelectItem>
78
+ <SelectItem value="utc+9">Asia/Tokyo (JST)</SelectItem>
79
+ </SelectContent>
80
+ </Select>
81
+ </div>
82
+ <div className="sm:col-span-2 space-y-2">
83
+ <Label optional>Bio</Label>
84
+ <Textarea
85
+ autoResize
86
+ defaultValue="Product engineer focused on design systems and web architecture."
87
+ />
88
+ </div>
89
+ </div>
90
+
91
+ <div className="flex items-center justify-end gap-2 border-t border-zinc-200 pt-4 dark:border-zinc-800">
92
+ <Button variant="outline" size="sm">
93
+ Cancel
94
+ </Button>
95
+ <Button
96
+ variant="primary"
97
+ size="sm"
98
+ onClick={() => {
99
+ setSaved(true)
100
+ setTimeout(() => setSaved(false), 2000)
101
+ }}
102
+ >
103
+ {saved ? "Saved ✓" : "Save changes"}
104
+ </Button>
105
+ </div>
106
+ </div>
107
+ )
108
+ }
109
+
110
+ export const SecurityPasswordFormBlock: React.FC<{ className?: string }> = ({
111
+ className,
112
+ }) => {
113
+ const [mfaEnabled, setMfaEnabled] = React.useState(true)
114
+ const [hardwareKey, setHardwareKey] = React.useState(true)
115
+
116
+ return (
117
+ <div
118
+ className={cn(
119
+ "rounded-xl border border-zinc-200 bg-white p-6 space-y-6 dark:border-zinc-800 dark:bg-zinc-900",
120
+ className
121
+ )}
122
+ >
123
+ <div className="flex items-start justify-between">
124
+ <div>
125
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 flex items-center gap-2">
126
+ <Shield className="h-4 w-4 text-blue-600 dark:text-blue-400" />
127
+ Two-factor authentication &amp; sessions
128
+ </h3>
129
+ <p className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
130
+ Manage passkeys, authenticator apps, and active devices.
131
+ </p>
132
+ </div>
133
+ <Badge variant="emerald" dot>
134
+ Protected
135
+ </Badge>
136
+ </div>
137
+
138
+ <div className="space-y-3">
139
+ <div className="flex items-center justify-between rounded-xl border border-zinc-200 bg-zinc-50/60 p-4 dark:border-zinc-800 dark:bg-zinc-800/40">
140
+ <div className="space-y-0.5">
141
+ <p className="text-xs font-medium text-zinc-900 dark:text-zinc-100">
142
+ Authenticator App (TOTP)
143
+ </p>
144
+ <p className="text-xs text-zinc-500 dark:text-zinc-400">
145
+ Use a 6-digit verification code when signing in.
146
+ </p>
147
+ </div>
148
+ <Switch checked={mfaEnabled} onCheckedChange={setMfaEnabled} />
149
+ </div>
150
+
151
+ <div className="flex items-center justify-between rounded-xl border border-zinc-200 bg-zinc-50/60 p-4 dark:border-zinc-800 dark:bg-zinc-800/40">
152
+ <div className="space-y-0.5">
153
+ <p className="text-xs font-medium text-zinc-900 dark:text-zinc-100">
154
+ Passkey / Security Key
155
+ </p>
156
+ <p className="text-xs text-zinc-500 dark:text-zinc-400">
157
+ Biometric or hardware key sign-in via WebAuthn.
158
+ </p>
159
+ </div>
160
+ <Switch checked={hardwareKey} onCheckedChange={setHardwareKey} />
161
+ </div>
162
+ </div>
163
+
164
+ <div className="space-y-3 border-t border-zinc-200 pt-4 dark:border-zinc-800">
165
+ <div className="text-xs font-medium text-zinc-700 dark:text-zinc-300">
166
+ Active sessions
167
+ </div>
168
+ <div className="divide-y divide-zinc-200 rounded-xl border border-zinc-200 dark:divide-zinc-800 dark:border-zinc-800">
169
+ <div className="flex items-center justify-between p-3.5">
170
+ <div className="flex items-center gap-3">
171
+ <Laptop className="h-4 w-4 text-zinc-500" />
172
+ <div>
173
+ <p className="text-xs font-medium text-zinc-900 dark:text-zinc-100">
174
+ MacBook Pro • Chrome 134 (Current)
175
+ </p>
176
+ <p className="text-[11px] text-zinc-500">
177
+ Jakarta, Indonesia • Active now
178
+ </p>
179
+ </div>
180
+ </div>
181
+ <Badge variant="emerald">Current</Badge>
182
+ </div>
183
+ <div className="flex items-center justify-between p-3.5">
184
+ <div className="flex items-center gap-3">
185
+ <Smartphone className="h-4 w-4 text-zinc-400" />
186
+ <div>
187
+ <p className="text-xs font-medium text-zinc-900 dark:text-zinc-100">
188
+ iPhone 16 Pro • Safari
189
+ </p>
190
+ <p className="text-[11px] text-zinc-500">
191
+ Last active 3 hours ago
192
+ </p>
193
+ </div>
194
+ </div>
195
+ <Button variant="outline" size="sm">
196
+ Revoke
197
+ </Button>
198
+ </div>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ )
203
+ }
204
+
205
+ export interface TeamMember {
206
+ id: string
207
+ name: string
208
+ email: string
209
+ fallback: string
210
+ role: string
211
+ status: "online" | "away" | "offline"
212
+ }
213
+
214
+ export const TeamRbacTableBlock: React.FC<{ className?: string }> = ({
215
+ className,
216
+ }) => {
217
+ const [members, setMembers] = React.useState<TeamMember[]>([
218
+ {
219
+ id: "m1",
220
+ name: "Saeful Rohman",
221
+ email: "saeful@acme.com",
222
+ fallback: "SR",
223
+ role: "owner",
224
+ status: "online",
225
+ },
226
+ {
227
+ id: "m2",
228
+ name: "Elena Rostova",
229
+ email: "elena@acme.com",
230
+ fallback: "ER",
231
+ role: "admin",
232
+ status: "online",
233
+ },
234
+ {
235
+ id: "m3",
236
+ name: "Marcus Vance",
237
+ email: "mvance@acme.com",
238
+ fallback: "MV",
239
+ role: "member",
240
+ status: "away",
241
+ },
242
+ ])
243
+
244
+ return (
245
+ <div
246
+ className={cn(
247
+ "rounded-xl border border-zinc-200 bg-white overflow-hidden dark:border-zinc-800 dark:bg-zinc-900",
248
+ className
249
+ )}
250
+ >
251
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between border-b border-zinc-200 p-5 dark:border-zinc-800">
252
+ <div>
253
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
254
+ Team members
255
+ </h3>
256
+ <p className="text-xs text-zinc-500 dark:text-zinc-400">
257
+ Manage who has access to this workspace and their permissions.
258
+ </p>
259
+ </div>
260
+ <Button
261
+ variant="primary"
262
+ size="sm"
263
+ onClick={() =>
264
+ setMembers((prev) => [
265
+ ...prev,
266
+ {
267
+ id: `m-${prev.length + 1}`,
268
+ name: "Aria Chen",
269
+ email: "aria@acme.com",
270
+ fallback: "AC",
271
+ role: "member",
272
+ status: "online",
273
+ },
274
+ ])
275
+ }
276
+ >
277
+ <Plus className="h-3.5 w-3.5" />
278
+ Invite member
279
+ </Button>
280
+ </div>
281
+
282
+ <div className="divide-y divide-zinc-200 dark:divide-zinc-800">
283
+ {members.map((member, idx) => (
284
+ <div
285
+ key={member.id}
286
+ className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between px-5 py-3.5"
287
+ >
288
+ <div className="flex items-center gap-3">
289
+ <Avatar
290
+ fallback={member.fallback}
291
+ tint={idx % 2 === 0 ? "blue" : "neutral"}
292
+ status={member.status}
293
+ size="default"
294
+ />
295
+ <div>
296
+ <p className="text-xs font-medium text-zinc-900 dark:text-zinc-100">
297
+ {member.name}
298
+ </p>
299
+ <p className="text-[11px] text-zinc-500">{member.email}</p>
300
+ </div>
301
+ </div>
302
+
303
+ <div className="flex items-center gap-3">
304
+ <Select
305
+ value={member.role}
306
+ onValueChange={(newRole) =>
307
+ setMembers((prev) =>
308
+ prev.map((m) =>
309
+ m.id === member.id ? { ...m, role: newRole } : m
310
+ )
311
+ )
312
+ }
313
+ >
314
+ <SelectTrigger className="w-36 h-8 text-xs">
315
+ <SelectValue />
316
+ </SelectTrigger>
317
+ <SelectContent>
318
+ <SelectItem value="owner">Owner</SelectItem>
319
+ <SelectItem value="admin">Admin</SelectItem>
320
+ <SelectItem value="member">Member</SelectItem>
321
+ <SelectItem value="viewer">Billing Viewer</SelectItem>
322
+ </SelectContent>
323
+ </Select>
324
+ </div>
325
+ </div>
326
+ ))}
327
+ </div>
328
+ </div>
329
+ )
330
+ }
331
+
332
+ export const BillingPlanCardBlock: React.FC<{ className?: string }> = ({
333
+ className,
334
+ }) => {
335
+ return (
336
+ <div
337
+ className={cn(
338
+ "rounded-xl border border-zinc-200 bg-white p-6 space-y-6 dark:border-zinc-800 dark:bg-zinc-900",
339
+ className
340
+ )}
341
+ >
342
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between border-b border-zinc-200 pb-5 dark:border-zinc-800">
343
+ <div>
344
+ <div className="flex items-center gap-2">
345
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
346
+ Pro Plan
347
+ </h3>
348
+ <Badge variant="blue">Active</Badge>
349
+ </div>
350
+ <p className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
351
+ Includes unlimited projects, custom domains, and team collaboration.
352
+ </p>
353
+ </div>
354
+ <div className="text-right">
355
+ <div className="text-2xl font-bold text-zinc-900 dark:text-zinc-100">
356
+ $49<span className="text-xs font-normal text-zinc-400">/month</span>
357
+ </div>
358
+ </div>
359
+ </div>
360
+
361
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
362
+ <div className="rounded-xl border border-zinc-200 bg-zinc-50/50 p-4 space-y-2 dark:border-zinc-800 dark:bg-zinc-800/40">
363
+ <Progress value={78} label="Team seats" showValue variant="blue" />
364
+ <p className="text-[11px] text-zinc-500">39 of 50 seats used</p>
365
+ </div>
366
+ <div className="rounded-xl border border-zinc-200 bg-zinc-50/50 p-4 space-y-2 dark:border-zinc-800 dark:bg-zinc-800/40">
367
+ <Progress
368
+ value={44}
369
+ label="Storage volume"
370
+ showValue
371
+ variant="emerald"
372
+ />
373
+ <p className="text-[11px] text-zinc-500">44 GB of 100 GB</p>
374
+ </div>
375
+ <div className="rounded-xl border border-zinc-200 bg-zinc-50/50 p-4 space-y-2 dark:border-zinc-800 dark:bg-zinc-800/40">
376
+ <Progress value={62} label="Monthly bandwidth" showValue variant="blue" />
377
+ <p className="text-[11px] text-zinc-500">620 GB of 1 TB</p>
378
+ </div>
379
+ </div>
380
+
381
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between rounded-xl border border-zinc-200 p-4 dark:border-zinc-800">
382
+ <div className="flex items-center gap-3">
383
+ <div className="flex h-9 w-9 items-center justify-center rounded-lg border border-zinc-200 bg-zinc-50 text-zinc-700 dark:border-zinc-800 dark:bg-zinc-800 dark:text-zinc-200">
384
+ <CreditCard className="h-4 w-4" />
385
+ </div>
386
+ <div>
387
+ <p className="text-xs font-medium text-zinc-900 dark:text-zinc-100">
388
+ Visa ending in 4242
389
+ </p>
390
+ <p className="text-[11px] text-zinc-500">
391
+ Expires 09/2028 • Default payment method
392
+ </p>
393
+ </div>
394
+ </div>
395
+ <div className="flex items-center gap-2">
396
+ <Badge variant="emerald">
397
+ <Check className="h-3 w-3" /> Verified
398
+ </Badge>
399
+ <Button variant="outline" size="sm">
400
+ Edit card
401
+ </Button>
402
+ </div>
403
+ </div>
404
+ </div>
405
+ )
406
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ SidebarShell,
3
+ type SidebarNavItem,
4
+ type SidebarShellProps,
5
+ } from "./app-shell"
6
+
@@ -0,0 +1,7 @@
1
+ export {
2
+ StatMetricCard,
3
+ ProgressMetricCard,
4
+ type StatMetricCardProps,
5
+ type ProgressMetricCardProps,
6
+ } from "./stat-cards"
7
+
@@ -0,0 +1,212 @@
1
+ import * as React from "react"
2
+ import { ArrowUpRight, ArrowDownRight } from "lucide-react"
3
+ import { cn } from "../utils/cn"
4
+ import { Badge } from "../primitives/badge"
5
+ import { Progress } from "../primitives/progress"
6
+
7
+ export interface StatMetricCardProps {
8
+ title: string
9
+ value: string
10
+ change: string
11
+ trend?: "up" | "down" | "neutral"
12
+ subtitle?: string
13
+ sparkline?: number[]
14
+ accent?: "blue" | "indigo" | "emerald" | "amber" | "rose"
15
+ className?: string
16
+ }
17
+
18
+ const strokeColors = {
19
+ blue: "#2563eb",
20
+ indigo: "#2563eb",
21
+ emerald: "#10b981",
22
+ amber: "#f59e0b",
23
+ rose: "#f43f5e",
24
+ }
25
+
26
+ export const StatMetricCard: React.FC<StatMetricCardProps> = ({
27
+ title,
28
+ value,
29
+ change,
30
+ trend = "up",
31
+ subtitle = "vs. previous 30 days",
32
+ sparkline = [28, 34, 31, 45, 42, 56, 64, 61, 78, 86],
33
+ accent = "blue",
34
+ className,
35
+ }) => {
36
+ const min = Math.min(...sparkline)
37
+ const max = Math.max(...sparkline)
38
+ const range = Math.max(1, max - min)
39
+ const width = 104
40
+ const height = 34
41
+
42
+ const points = sparkline
43
+ .map((val, i) => {
44
+ const x = (i / Math.max(1, sparkline.length - 1)) * width
45
+ const y = height - ((val - min) / range) * (height - 8) - 4
46
+ return `${x.toFixed(1)},${y.toFixed(1)}`
47
+ })
48
+ .join(" ")
49
+
50
+ return (
51
+ <div
52
+ className={cn(
53
+ "rounded-xl border border-zinc-200 bg-white p-5 transition-colors dark:border-zinc-800 dark:bg-zinc-900",
54
+ className
55
+ )}
56
+ >
57
+ <div className="flex items-center justify-between">
58
+ <span className="text-xs font-medium text-zinc-500 dark:text-zinc-400">
59
+ {title}
60
+ </span>
61
+ <Badge
62
+ variant={
63
+ trend === "up" ? "emerald" : trend === "down" ? "rose" : "default"
64
+ }
65
+ className="text-[11px]"
66
+ >
67
+ {trend === "up" ? (
68
+ <ArrowUpRight className="h-3 w-3" />
69
+ ) : (
70
+ <ArrowDownRight className="h-3 w-3" />
71
+ )}
72
+ {change}
73
+ </Badge>
74
+ </div>
75
+
76
+ <div className="mt-3 flex items-end justify-between gap-4">
77
+ <div>
78
+ <div className="text-2xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">
79
+ {value}
80
+ </div>
81
+ <p className="mt-1 text-[11px] text-zinc-400 dark:text-zinc-500">
82
+ {subtitle}
83
+ </p>
84
+ </div>
85
+
86
+ <svg
87
+ width={width}
88
+ height={height}
89
+ viewBox={`0 0 ${width} ${height}`}
90
+ className="overflow-visible shrink-0"
91
+ >
92
+ <polyline
93
+ fill="none"
94
+ stroke={strokeColors[accent]}
95
+ strokeWidth="2"
96
+ strokeLinecap="round"
97
+ strokeLinejoin="round"
98
+ points={points}
99
+ />
100
+ </svg>
101
+ </div>
102
+ </div>
103
+ )
104
+ }
105
+
106
+ export interface MultiMetricGridProps {
107
+ metrics?: StatMetricCardProps[]
108
+ className?: string
109
+ }
110
+
111
+ const defaultMetrics: StatMetricCardProps[] = [
112
+ {
113
+ title: "Monthly Recurring Revenue",
114
+ value: "$128,430",
115
+ change: "+14.2%",
116
+ trend: "up",
117
+ subtitle: "Compared to $112.4k last month",
118
+ sparkline: [32, 38, 40, 48, 52, 60, 68, 74, 82, 95],
119
+ accent: "blue",
120
+ },
121
+ {
122
+ title: "Active Subscribers",
123
+ value: "4,892",
124
+ change: "+8.4%",
125
+ trend: "up",
126
+ subtitle: "312 new trials converted",
127
+ sparkline: [40, 42, 49, 55, 53, 64, 72, 79, 88, 92],
128
+ accent: "emerald",
129
+ },
130
+ {
131
+ title: "Net Revenue Retention",
132
+ value: "118.6%",
133
+ change: "+2.1%",
134
+ trend: "up",
135
+ subtitle: "Enterprise cohort expansion",
136
+ sparkline: [70, 72, 75, 78, 80, 84, 86, 89, 92, 95],
137
+ accent: "blue",
138
+ },
139
+ {
140
+ title: "Logo Churn Rate",
141
+ value: "1.24%",
142
+ change: "-0.4%",
143
+ trend: "up",
144
+ subtitle: "Lowest in 4 quarters",
145
+ sparkline: [65, 58, 50, 44, 39, 32, 28, 22, 18, 12],
146
+ accent: "emerald",
147
+ },
148
+ ]
149
+
150
+ export const MultiMetricGrid: React.FC<MultiMetricGridProps> = ({
151
+ metrics = defaultMetrics,
152
+ className,
153
+ }) => {
154
+ return (
155
+ <div
156
+ className={cn(
157
+ "grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4",
158
+ className
159
+ )}
160
+ >
161
+ {metrics.map((m) => (
162
+ <StatMetricCard key={m.title} {...m} />
163
+ ))}
164
+ </div>
165
+ )
166
+ }
167
+
168
+ export interface ProgressMetricCardProps {
169
+ title?: string
170
+ current?: string
171
+ target?: string
172
+ percentage?: number
173
+ statusText?: string
174
+ variant?: "blue" | "indigo" | "emerald" | "amber" | "rose"
175
+ className?: string
176
+ }
177
+
178
+ export const ProgressMetricCard: React.FC<ProgressMetricCardProps> = ({
179
+ title = "Monthly API Usage",
180
+ current = "768,000",
181
+ target = "1,000,000 reqs",
182
+ percentage = 76.8,
183
+ statusText = "Resets in 9 days • Pay-as-you-go enabled",
184
+ variant = "blue",
185
+ className,
186
+ }) => {
187
+ return (
188
+ <div
189
+ className={cn(
190
+ "rounded-xl border border-zinc-200 bg-white p-5 space-y-4 dark:border-zinc-800 dark:bg-zinc-900",
191
+ className
192
+ )}
193
+ >
194
+ <div className="flex items-center justify-between">
195
+ <span className="text-xs font-medium text-zinc-700 dark:text-zinc-200">
196
+ {title}
197
+ </span>
198
+ <span className="text-xs text-zinc-500 dark:text-zinc-400">
199
+ <strong className="text-zinc-900 dark:text-white">{current}</strong> /{" "}
200
+ {target}
201
+ </span>
202
+ </div>
203
+ <Progress value={percentage} variant={variant} showValue />
204
+ <p className="text-[11px] text-zinc-400 dark:text-zinc-500">
205
+ {statusText}
206
+ </p>
207
+ </div>
208
+ )
209
+ }
210
+
211
+ export type StatCardProps = StatMetricCardProps
212
+ export const StatCard = StatMetricCard
@@ -0,0 +1,5 @@
1
+ export {
2
+ TeamRbacTableBlock,
3
+ type TeamMember,
4
+ } from "./settings-blocks"
5
+