@stampui/blocks 1.0.0

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 (107) hide show
  1. package/dist/components/ai-chat-shell.d.ts +1 -0
  2. package/dist/components/ai-chat-shell.js +23 -0
  3. package/dist/components/prompt-input.d.ts +5 -0
  4. package/dist/components/prompt-input.js +47 -0
  5. package/dist/components/registry-card.d.ts +6 -0
  6. package/dist/components/registry-card.js +15 -0
  7. package/dist/components/registry-explorer.d.ts +8 -0
  8. package/dist/components/registry-explorer.js +38 -0
  9. package/dist/components/token-stream.d.ts +7 -0
  10. package/dist/components/token-stream.js +21 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +23 -0
  13. package/dist/manifests.d.ts +3 -0
  14. package/dist/manifests.js +1666 -0
  15. package/dist/types.d.ts +44 -0
  16. package/dist/types.js +2 -0
  17. package/package.json +28 -0
  18. package/src/components/blocks/ai-chat-shell.tsx +97 -0
  19. package/src/components/blocks/auth-panel.tsx +203 -0
  20. package/src/components/blocks/feature-grid.tsx +122 -0
  21. package/src/components/blocks/hero-section.tsx +73 -0
  22. package/src/components/blocks/notification-center.tsx +185 -0
  23. package/src/components/blocks/onboarding-flow.tsx +230 -0
  24. package/src/components/blocks/pricing-section.tsx +135 -0
  25. package/src/components/blocks/project-command-center.tsx +188 -0
  26. package/src/components/blocks/prompt-input.tsx +81 -0
  27. package/src/components/blocks/registry-card.tsx +104 -0
  28. package/src/components/blocks/registry-explorer.tsx +78 -0
  29. package/src/components/blocks/settings-layout.tsx +178 -0
  30. package/src/components/blocks/stats-strip.tsx +100 -0
  31. package/src/components/blocks/token-stream.tsx +42 -0
  32. package/src/components/blocks/usage-card.tsx +116 -0
  33. package/src/components/core/accordion.tsx +58 -0
  34. package/src/components/core/alert-dialog.tsx +113 -0
  35. package/src/components/core/alert.tsx +48 -0
  36. package/src/components/core/animated-number.tsx +77 -0
  37. package/src/components/core/aspect-ratio.tsx +20 -0
  38. package/src/components/core/avatar-stack.tsx +61 -0
  39. package/src/components/core/avatar.tsx +90 -0
  40. package/src/components/core/badge.tsx +39 -0
  41. package/src/components/core/breadcrumb.tsx +63 -0
  42. package/src/components/core/button-group.tsx +37 -0
  43. package/src/components/core/button.tsx +110 -0
  44. package/src/components/core/calendar.tsx +143 -0
  45. package/src/components/core/card.tsx +60 -0
  46. package/src/components/core/carousel.tsx +170 -0
  47. package/src/components/core/chart.tsx +377 -0
  48. package/src/components/core/checkbox.tsx +64 -0
  49. package/src/components/core/collapsible.tsx +30 -0
  50. package/src/components/core/combobox.tsx +114 -0
  51. package/src/components/core/command-box.tsx +22 -0
  52. package/src/components/core/command.tsx +165 -0
  53. package/src/components/core/confirm-action.tsx +94 -0
  54. package/src/components/core/context-menu.tsx +139 -0
  55. package/src/components/core/copy-button.tsx +41 -0
  56. package/src/components/core/data-table.tsx +173 -0
  57. package/src/components/core/date-picker.tsx +73 -0
  58. package/src/components/core/dialog.tsx +83 -0
  59. package/src/components/core/drawer.tsx +87 -0
  60. package/src/components/core/dropdown-menu.tsx +147 -0
  61. package/src/components/core/empty.tsx +34 -0
  62. package/src/components/core/field.tsx +39 -0
  63. package/src/components/core/file-upload.tsx +143 -0
  64. package/src/components/core/hover-card.tsx +31 -0
  65. package/src/components/core/inline-edit.tsx +104 -0
  66. package/src/components/core/input-group.tsx +47 -0
  67. package/src/components/core/input-otp.tsx +108 -0
  68. package/src/components/core/input.tsx +37 -0
  69. package/src/components/core/kbd.tsx +47 -0
  70. package/src/components/core/label.tsx +28 -0
  71. package/src/components/core/marquee.tsx +61 -0
  72. package/src/components/core/menubar.tsx +120 -0
  73. package/src/components/core/multi-select.tsx +145 -0
  74. package/src/components/core/native-select.tsx +27 -0
  75. package/src/components/core/navigation-menu.tsx +130 -0
  76. package/src/components/core/number-stepper.tsx +80 -0
  77. package/src/components/core/pagination.tsx +80 -0
  78. package/src/components/core/password-input.tsx +90 -0
  79. package/src/components/core/popover.tsx +34 -0
  80. package/src/components/core/progress.tsx +63 -0
  81. package/src/components/core/radio-group.tsx +77 -0
  82. package/src/components/core/resizable.tsx +250 -0
  83. package/src/components/core/scroll-area.tsx +38 -0
  84. package/src/components/core/select.tsx +128 -0
  85. package/src/components/core/separator.tsx +47 -0
  86. package/src/components/core/sheet.tsx +118 -0
  87. package/src/components/core/sidebar.tsx +129 -0
  88. package/src/components/core/skeleton.tsx +32 -0
  89. package/src/components/core/slider.tsx +97 -0
  90. package/src/components/core/sonner.tsx +29 -0
  91. package/src/components/core/spinner.tsx +60 -0
  92. package/src/components/core/status-pulse.tsx +67 -0
  93. package/src/components/core/stepper.tsx +111 -0
  94. package/src/components/core/switch.tsx +72 -0
  95. package/src/components/core/table.tsx +104 -0
  96. package/src/components/core/tabs.tsx +55 -0
  97. package/src/components/core/tag-input.tsx +93 -0
  98. package/src/components/core/textarea.tsx +44 -0
  99. package/src/components/core/timeline.tsx +81 -0
  100. package/src/components/core/toggle-group.tsx +56 -0
  101. package/src/components/core/toggle.tsx +66 -0
  102. package/src/components/core/tooltip.tsx +31 -0
  103. package/src/components/core/typing-indicator.tsx +51 -0
  104. package/src/index.ts +8 -0
  105. package/src/manifests.ts +1682 -0
  106. package/src/types.ts +58 -0
  107. package/src/ui.ts +13 -0
@@ -0,0 +1,185 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Bell, Check, X, MessageSquare, AlertCircle, CreditCard, UserPlus } from "lucide-react"
5
+ import { Badge } from "@/components/core/badge"
6
+ import { Button } from "@/components/core/button"
7
+ import { Separator } from "@/components/core/separator"
8
+ import { StatusPulse } from "@/components/core/status-pulse"
9
+ import { cx } from "@/lib/cx"
10
+
11
+ // ── Types ──────────────────────────────────────────────────────────────────
12
+
13
+ export type NotificationType = "message" | "alert" | "billing" | "invite" | "system"
14
+
15
+ export interface Notification {
16
+ id: string
17
+ type: NotificationType
18
+ title: string
19
+ body: string
20
+ time: string
21
+ read: boolean
22
+ href?: string
23
+ }
24
+
25
+ export interface NotificationCenterProps {
26
+ notifications?: Notification[]
27
+ className?: string
28
+ }
29
+
30
+ // ── Icons ──────────────────────────────────────────────────────────────────
31
+
32
+ const typeConfig: Record<NotificationType, { icon: React.ReactNode; color: string }> = {
33
+ message: { icon: <MessageSquare className="h-3.5 w-3.5" />, color: "bg-blue-500/15 text-blue-500 border-blue-500/20" },
34
+ alert: { icon: <AlertCircle className="h-3.5 w-3.5" />, color: "bg-orange-500/15 text-orange-500 border-orange-500/20" },
35
+ billing: { icon: <CreditCard className="h-3.5 w-3.5" />, color: "bg-green-500/15 text-green-500 border-green-500/20" },
36
+ invite: { icon: <UserPlus className="h-3.5 w-3.5" />, color: "bg-purple-500/15 text-purple-500 border-purple-500/20" },
37
+ system: { icon: <AlertCircle className="h-3.5 w-3.5" />, color: "bg-surface-3 text-muted-foreground border-border" },
38
+ }
39
+
40
+ const defaultNotifications: Notification[] = [
41
+ { id: "1", type: "invite", title: "New team invite", body: "Sarah invited you to join Acme Design team.", time: "2m ago", read: false },
42
+ { id: "2", type: "message", title: "Comment on your block", body: "Alex left a comment on HeroSection: 'Love the variant...'", time: "14m ago", read: false },
43
+ { id: "3", type: "billing", title: "Payment successful", body: "Your Pro plan has been renewed for $29.", time: "1h ago", read: false },
44
+ { id: "4", type: "alert", title: "Usage limit approaching",body: "You're at 80% of your monthly API limit.", time: "3h ago", read: true },
45
+ { id: "5", type: "system", title: "Maintenance window", body: "Scheduled downtime on May 12 from 02:00–04:00 UTC.", time: "1d ago", read: true },
46
+ ]
47
+
48
+ // ── Notification Item ──────────────────────────────────────────────────────
49
+
50
+ function NotifItem({
51
+ notif,
52
+ onRead,
53
+ onDismiss,
54
+ }: {
55
+ notif: Notification
56
+ onRead: (id: string) => void
57
+ onDismiss: (id: string) => void
58
+ }) {
59
+ const { icon, color } = typeConfig[notif.type]
60
+ return (
61
+ <div
62
+ className={cx(
63
+ "flex gap-3 px-4 py-3 transition-colors hover:bg-surface-2/50",
64
+ !notif.read && "bg-surface-2/30"
65
+ )}
66
+ onClick={() => onRead(notif.id)}
67
+ role="button"
68
+ tabIndex={0}
69
+ onKeyDown={(e) => e.key === "Enter" && onRead(notif.id)}
70
+ >
71
+ <div className={cx("mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full border", color)}>
72
+ {icon}
73
+ </div>
74
+ <div className="flex-1 min-w-0">
75
+ <div className="flex items-start justify-between gap-2">
76
+ <p className={cx("text-sm leading-snug", notif.read ? "text-muted-foreground" : "font-medium text-foreground")}>
77
+ {notif.title}
78
+ </p>
79
+ <div className="flex items-center gap-1 shrink-0">
80
+ <span className="text-[11px] text-muted-foreground whitespace-nowrap">{notif.time}</span>
81
+ {!notif.read && <StatusPulse status="online" size="sm" pulse={false} />}
82
+ </div>
83
+ </div>
84
+ <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2 leading-relaxed">{notif.body}</p>
85
+ </div>
86
+ <button
87
+ type="button"
88
+ onClick={(e) => { e.stopPropagation(); onDismiss(notif.id) }}
89
+ className="mt-0.5 shrink-0 opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-foreground transition-opacity"
90
+ aria-label="Dismiss"
91
+ >
92
+ <X className="h-3.5 w-3.5" />
93
+ </button>
94
+ </div>
95
+ )
96
+ }
97
+
98
+ // ── Root ───────────────────────────────────────────────────────────────────
99
+
100
+ export function NotificationCenter({ notifications: initial = defaultNotifications, className }: NotificationCenterProps) {
101
+ const [open, setOpen] = React.useState(false)
102
+ const [notifs, setNotifs] = React.useState(initial)
103
+ const unread = notifs.filter((n) => !n.read).length
104
+ const ref = React.useRef<HTMLDivElement>(null)
105
+
106
+ React.useEffect(() => {
107
+ if (!open) return
108
+ const close = (e: MouseEvent) => {
109
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
110
+ }
111
+ document.addEventListener("mousedown", close)
112
+ return () => document.removeEventListener("mousedown", close)
113
+ }, [open])
114
+
115
+ const markRead = (id: string) => setNotifs((n) => n.map((x) => x.id === id ? { ...x, read: true } : x))
116
+ const dismiss = (id: string) => setNotifs((n) => n.filter((x) => x.id !== id))
117
+ const markAllRead = () => setNotifs((n) => n.map((x) => ({ ...x, read: true })))
118
+
119
+ return (
120
+ <div ref={ref} className={cx("relative inline-block", className)}>
121
+ <button
122
+ type="button"
123
+ onClick={() => setOpen((v) => !v)}
124
+ aria-label={`Notifications${unread > 0 ? `, ${unread} unread` : ""}`}
125
+ className="relative inline-flex h-9 w-9 items-center justify-center rounded-lg border border-border text-muted-foreground hover:bg-surface-2 hover:text-foreground transition-colors"
126
+ >
127
+ <Bell className="h-4 w-4" />
128
+ {unread > 0 && (
129
+ <span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-foreground text-[10px] font-bold text-background">
130
+ {unread > 9 ? "9+" : unread}
131
+ </span>
132
+ )}
133
+ </button>
134
+
135
+ {open && (
136
+ <div className="absolute right-0 top-full mt-2 z-50 w-[380px] rounded-xl border border-border bg-background shadow-lg overflow-hidden">
137
+ {/* Header */}
138
+ <div className="flex items-center justify-between px-4 py-3 border-b border-border">
139
+ <div className="flex items-center gap-2">
140
+ <span className="text-sm font-semibold">Notifications</span>
141
+ {unread > 0 && (
142
+ <Badge variant="neutral" className="h-5 px-1.5 text-[10px] tabular-nums">{unread}</Badge>
143
+ )}
144
+ </div>
145
+ {unread > 0 && (
146
+ <button
147
+ type="button"
148
+ onClick={markAllRead}
149
+ className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
150
+ >
151
+ <Check className="h-3 w-3" />
152
+ Mark all read
153
+ </button>
154
+ )}
155
+ </div>
156
+
157
+ {/* List */}
158
+ <div className="max-h-[420px] overflow-y-auto divide-y divide-border/50 group">
159
+ {notifs.length === 0 ? (
160
+ <div className="flex flex-col items-center justify-center py-12 text-center px-6">
161
+ <Bell className="h-8 w-8 text-muted-foreground/30 mb-3" />
162
+ <p className="text-sm font-medium">All caught up</p>
163
+ <p className="text-xs text-muted-foreground mt-1">No new notifications</p>
164
+ </div>
165
+ ) : (
166
+ notifs.map((notif) => (
167
+ <NotifItem key={notif.id} notif={notif} onRead={markRead} onDismiss={dismiss} />
168
+ ))
169
+ )}
170
+ </div>
171
+
172
+ {/* Footer */}
173
+ {notifs.length > 0 && (
174
+ <>
175
+ <Separator />
176
+ <div className="px-4 py-2">
177
+ <Button variant="ghost" className="w-full text-xs h-8">View all notifications</Button>
178
+ </div>
179
+ </>
180
+ )}
181
+ </div>
182
+ )}
183
+ </div>
184
+ )
185
+ }
@@ -0,0 +1,230 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Button } from "@/components/core/button"
5
+ import { Input } from "@/components/core/input"
6
+ import { Label } from "@/components/core/label"
7
+ import { Progress } from "@/components/core/progress"
8
+ import { Badge } from "@/components/core/badge"
9
+ import { Check } from "lucide-react"
10
+ import { cx } from "@/lib/cx"
11
+
12
+ // ── Types ──────────────────────────────────────────────────────────────────
13
+
14
+ export interface OnboardingStep {
15
+ id: string
16
+ title: string
17
+ description: string
18
+ content: React.ReactNode
19
+ }
20
+
21
+ export interface OnboardingFlowProps {
22
+ steps?: OnboardingStep[]
23
+ onComplete?: (data: Record<string, unknown>) => void
24
+ className?: string
25
+ }
26
+
27
+ // ── Default Steps ──────────────────────────────────────────────────────────
28
+
29
+ function WorkspaceStep({ data, onChange }: { data: Record<string, string>; onChange: (k: string, v: string) => void }) {
30
+ return (
31
+ <div className="space-y-4">
32
+ <div className="space-y-2">
33
+ <Label htmlFor="workspace-name">Workspace name</Label>
34
+ <Input
35
+ id="workspace-name"
36
+ placeholder="Acme Corp"
37
+ value={data.workspaceName ?? ""}
38
+ onChange={(e) => onChange("workspaceName", e.target.value)}
39
+ />
40
+ </div>
41
+ <div className="space-y-2">
42
+ <Label htmlFor="workspace-slug">URL slug</Label>
43
+ <div className="flex">
44
+ <span className="inline-flex items-center rounded-l-lg border border-r-0 border-border bg-surface-2 px-3 text-xs text-muted-foreground whitespace-nowrap">
45
+ app.example.com/
46
+ </span>
47
+ <Input
48
+ id="workspace-slug"
49
+ placeholder="acme"
50
+ value={data.workspaceSlug ?? ""}
51
+ onChange={(e) => onChange("workspaceSlug", e.target.value)}
52
+ className="rounded-l-none"
53
+ />
54
+ </div>
55
+ </div>
56
+ </div>
57
+ )
58
+ }
59
+
60
+ function InviteStep({ data, onChange }: { data: Record<string, string>; onChange: (k: string, v: string) => void }) {
61
+ return (
62
+ <div className="space-y-4">
63
+ <p className="text-sm text-muted-foreground">Invite teammates to collaborate. You can skip this and do it later.</p>
64
+ {[0, 1, 2].map((i) => (
65
+ <div key={i} className="space-y-1.5">
66
+ <Label>Email {i + 1}</Label>
67
+ <Input
68
+ type="email"
69
+ placeholder={`teammate${i + 1}@example.com`}
70
+ value={data[`invite_${i}`] ?? ""}
71
+ onChange={(e) => onChange(`invite_${i}`, e.target.value)}
72
+ />
73
+ </div>
74
+ ))}
75
+ </div>
76
+ )
77
+ }
78
+
79
+ function PlanStep() {
80
+ const [selected, setSelected] = React.useState<"free" | "pro">("free")
81
+ const plans = [
82
+ { id: "free", label: "Free", price: "$0/mo", features: ["5 projects", "3 seats", "Community support"] },
83
+ { id: "pro", label: "Pro", price: "$29/mo", features: ["Unlimited projects", "Unlimited seats", "Priority support", "Pro blocks"] },
84
+ ] as const
85
+ return (
86
+ <div className="grid gap-3 sm:grid-cols-2">
87
+ {plans.map((plan) => (
88
+ <button
89
+ key={plan.id}
90
+ type="button"
91
+ onClick={() => setSelected(plan.id)}
92
+ className={cx(
93
+ "flex flex-col gap-3 rounded-xl border p-5 text-left transition-colors",
94
+ selected === plan.id
95
+ ? "border-border-strong bg-surface-2"
96
+ : "border-border hover:bg-surface-2/50"
97
+ )}
98
+ >
99
+ <div className="flex items-center justify-between">
100
+ <span className="font-semibold text-sm">{plan.label}</span>
101
+ {plan.id === "pro" && <Badge variant="neutral" className="text-[10px]">Popular</Badge>}
102
+ </div>
103
+ <span className="text-2xl font-bold tracking-tight">{plan.price}</span>
104
+ <ul className="space-y-1.5">
105
+ {plan.features.map((f) => (
106
+ <li key={f} className="flex items-center gap-2 text-xs text-muted-foreground">
107
+ <Check className="h-3 w-3 shrink-0 text-foreground" />
108
+ {f}
109
+ </li>
110
+ ))}
111
+ </ul>
112
+ </button>
113
+ ))}
114
+ </div>
115
+ )
116
+ }
117
+
118
+ function CompleteStep() {
119
+ return (
120
+ <div className="flex flex-col items-center text-center py-6 gap-4">
121
+ <div className="h-14 w-14 rounded-full bg-foreground text-background flex items-center justify-center">
122
+ <Check className="h-7 w-7" />
123
+ </div>
124
+ <div>
125
+ <h3 className="font-semibold text-lg">You're all set!</h3>
126
+ <p className="text-sm text-muted-foreground mt-1 max-w-xs">Your workspace is ready. Start building your first project.</p>
127
+ </div>
128
+ </div>
129
+ )
130
+ }
131
+
132
+ // ── Root ───────────────────────────────────────────────────────────────────
133
+
134
+ export function OnboardingFlow({ onComplete, className }: OnboardingFlowProps) {
135
+ const [step, setStep] = React.useState(0)
136
+ const [data, setData] = React.useState<Record<string, string>>({})
137
+
138
+ const updateData = (k: string, v: string) => setData((d) => ({ ...d, [k]: v }))
139
+
140
+ const steps = [
141
+ {
142
+ id: "workspace",
143
+ title: "Create your workspace",
144
+ description: "Give your team a home.",
145
+ content: <WorkspaceStep data={data} onChange={updateData} />,
146
+ },
147
+ {
148
+ id: "invite",
149
+ title: "Invite your team",
150
+ description: "Collaboration starts here.",
151
+ content: <InviteStep data={data} onChange={updateData} />,
152
+ },
153
+ {
154
+ id: "plan",
155
+ title: "Choose a plan",
156
+ description: "Start free, upgrade anytime.",
157
+ content: <PlanStep />,
158
+ },
159
+ {
160
+ id: "complete",
161
+ title: "You're ready",
162
+ description: "Let's build something great.",
163
+ content: <CompleteStep />,
164
+ },
165
+ ]
166
+
167
+ const total = steps.length
168
+ const current = steps[step]
169
+ const progress = Math.round(((step) / (total - 1)) * 100)
170
+ const isLast = step === total - 1
171
+
172
+ return (
173
+ <div className={cx("w-full max-w-lg mx-auto", className)}>
174
+ <div className="rounded-2xl border border-border bg-background shadow-sm overflow-hidden">
175
+ {/* Header */}
176
+ <div className="px-8 pt-8 pb-6 border-b border-border">
177
+ <div className="flex items-center justify-between mb-4">
178
+ <span className="text-xs font-mono text-muted-foreground">Step {step + 1} of {total}</span>
179
+ <span className="text-xs text-muted-foreground">{progress}%</span>
180
+ </div>
181
+ <Progress value={progress} className="h-1.5 mb-6" />
182
+ <h2 className="text-lg font-semibold">{current.title}</h2>
183
+ <p className="text-sm text-muted-foreground mt-0.5">{current.description}</p>
184
+ </div>
185
+
186
+ {/* Content */}
187
+ <div className="px-8 py-6">
188
+ {current.content}
189
+ </div>
190
+
191
+ {/* Footer */}
192
+ <div className="px-8 pb-8 flex items-center justify-between">
193
+ <Button
194
+ variant="ghost"
195
+ onClick={() => setStep((s) => Math.max(0, s - 1))}
196
+ disabled={step === 0}
197
+ >
198
+ Back
199
+ </Button>
200
+ <Button
201
+ onClick={() => {
202
+ if (isLast) {
203
+ onComplete?.(data)
204
+ } else {
205
+ setStep((s) => s + 1)
206
+ }
207
+ }}
208
+ >
209
+ {isLast ? "Go to dashboard" : step === 1 ? "Skip for now" : "Continue"}
210
+ </Button>
211
+ </div>
212
+ </div>
213
+
214
+ {/* Step dots */}
215
+ <div className="flex justify-center gap-2 mt-4">
216
+ {steps.map((_, i) => (
217
+ <button
218
+ key={i}
219
+ type="button"
220
+ onClick={() => i < step && setStep(i)}
221
+ className={cx(
222
+ "h-1.5 rounded-full transition-all",
223
+ i === step ? "w-6 bg-foreground" : i < step ? "w-1.5 bg-muted-foreground/50" : "w-1.5 bg-border"
224
+ )}
225
+ />
226
+ ))}
227
+ </div>
228
+ </div>
229
+ )
230
+ }
@@ -0,0 +1,135 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Check } from "lucide-react"
5
+ import { cx } from "@/lib/cx"
6
+
7
+ interface PricingTier {
8
+ name: string
9
+ price: string
10
+ period?: string
11
+ description: string
12
+ features: string[]
13
+ cta: string
14
+ highlighted?: boolean
15
+ }
16
+
17
+ const DEFAULT_TIERS: PricingTier[] = [
18
+ {
19
+ name: "Starter",
20
+ price: "$0",
21
+ period: "/month",
22
+ description: "For side projects and personal use.",
23
+ features: [
24
+ "Up to 3 projects",
25
+ "Core UI components",
26
+ "Community support",
27
+ "MIT license",
28
+ ],
29
+ cta: "Get Started",
30
+ },
31
+ {
32
+ name: "Pro",
33
+ price: "$29",
34
+ period: "/month",
35
+ description: "For teams shipping production products.",
36
+ features: [
37
+ "Unlimited projects",
38
+ "All UI components",
39
+ "Advanced blocks",
40
+ "Priority support",
41
+ "Private registry",
42
+ "Team access",
43
+ ],
44
+ cta: "Start Free Trial",
45
+ highlighted: true,
46
+ },
47
+ {
48
+ name: "Enterprise",
49
+ price: "Custom",
50
+ description: "For organizations with custom requirements.",
51
+ features: [
52
+ "Everything in Pro",
53
+ "SSO / SAML",
54
+ "SLA guarantee",
55
+ "Dedicated support",
56
+ "Custom licensing",
57
+ "Source audit",
58
+ ],
59
+ cta: "Contact Sales",
60
+ },
61
+ ]
62
+
63
+ interface PricingSectionProps {
64
+ tiers?: PricingTier[]
65
+ onSelect?: (tier: PricingTier) => void
66
+ }
67
+
68
+ export function PricingSection({ tiers = DEFAULT_TIERS, onSelect }: PricingSectionProps) {
69
+ return (
70
+ <section className="w-full py-16 px-4">
71
+ <div className="mx-auto max-w-5xl">
72
+ <div className="mb-12 text-center">
73
+ <p className="text-xs font-mono text-muted-foreground uppercase tracking-widest mb-3">Pricing</p>
74
+ <h2 className="text-3xl font-semibold tracking-tight mb-4">Simple, transparent pricing</h2>
75
+ <p className="text-muted-foreground max-w-md mx-auto text-sm leading-relaxed">
76
+ Start free. Scale when you need to. No hidden fees, no per-seat traps.
77
+ </p>
78
+ </div>
79
+
80
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4 items-start">
81
+ {tiers.map((tier) => (
82
+ <div
83
+ key={tier.name}
84
+ className={cx(
85
+ "relative flex flex-col rounded-2xl border p-6 transition-shadow",
86
+ tier.highlighted
87
+ ? "border-border-strong bg-surface-2 shadow-lg -mt-2 mb-[-0.5rem]"
88
+ : "border-border bg-card"
89
+ )}
90
+ >
91
+ {tier.highlighted && (
92
+ <span className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full border border-border bg-primary px-3 py-0.5 text-xs font-medium text-primary-foreground">
93
+ Most popular
94
+ </span>
95
+ )}
96
+
97
+ <div className="mb-6">
98
+ <p className="text-sm font-medium text-muted-foreground mb-1">{tier.name}</p>
99
+ <div className="flex items-end gap-1 mb-2">
100
+ <span className="text-3xl font-bold tracking-tight">{tier.price}</span>
101
+ {tier.period && (
102
+ <span className="text-sm text-muted-foreground pb-1">{tier.period}</span>
103
+ )}
104
+ </div>
105
+ <p className="text-sm text-muted-foreground">{tier.description}</p>
106
+ </div>
107
+
108
+ <ul className="flex-1 space-y-2.5 mb-6">
109
+ {tier.features.map((feature) => (
110
+ <li key={feature} className="flex items-start gap-2.5 text-sm">
111
+ <Check className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
112
+ <span>{feature}</span>
113
+ </li>
114
+ ))}
115
+ </ul>
116
+
117
+ <button
118
+ type="button"
119
+ onClick={() => onSelect?.(tier)}
120
+ className={cx(
121
+ "w-full rounded-lg px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-strong focus-visible:ring-offset-1 focus-visible:ring-offset-background",
122
+ tier.highlighted
123
+ ? "bg-primary text-primary-foreground hover:bg-primary/90"
124
+ : "border border-border bg-surface-2 hover:bg-surface-3 text-foreground"
125
+ )}
126
+ >
127
+ {tier.cta}
128
+ </button>
129
+ </div>
130
+ ))}
131
+ </div>
132
+ </div>
133
+ </section>
134
+ )
135
+ }