@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.
- package/dist/components/ai-chat-shell.d.ts +1 -0
- package/dist/components/ai-chat-shell.js +23 -0
- package/dist/components/prompt-input.d.ts +5 -0
- package/dist/components/prompt-input.js +47 -0
- package/dist/components/registry-card.d.ts +6 -0
- package/dist/components/registry-card.js +15 -0
- package/dist/components/registry-explorer.d.ts +8 -0
- package/dist/components/registry-explorer.js +38 -0
- package/dist/components/token-stream.d.ts +7 -0
- package/dist/components/token-stream.js +21 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +23 -0
- package/dist/manifests.d.ts +3 -0
- package/dist/manifests.js +1666 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.js +2 -0
- package/package.json +28 -0
- package/src/components/blocks/ai-chat-shell.tsx +97 -0
- package/src/components/blocks/auth-panel.tsx +203 -0
- package/src/components/blocks/feature-grid.tsx +122 -0
- package/src/components/blocks/hero-section.tsx +73 -0
- package/src/components/blocks/notification-center.tsx +185 -0
- package/src/components/blocks/onboarding-flow.tsx +230 -0
- package/src/components/blocks/pricing-section.tsx +135 -0
- package/src/components/blocks/project-command-center.tsx +188 -0
- package/src/components/blocks/prompt-input.tsx +81 -0
- package/src/components/blocks/registry-card.tsx +104 -0
- package/src/components/blocks/registry-explorer.tsx +78 -0
- package/src/components/blocks/settings-layout.tsx +178 -0
- package/src/components/blocks/stats-strip.tsx +100 -0
- package/src/components/blocks/token-stream.tsx +42 -0
- package/src/components/blocks/usage-card.tsx +116 -0
- package/src/components/core/accordion.tsx +58 -0
- package/src/components/core/alert-dialog.tsx +113 -0
- package/src/components/core/alert.tsx +48 -0
- package/src/components/core/animated-number.tsx +77 -0
- package/src/components/core/aspect-ratio.tsx +20 -0
- package/src/components/core/avatar-stack.tsx +61 -0
- package/src/components/core/avatar.tsx +90 -0
- package/src/components/core/badge.tsx +39 -0
- package/src/components/core/breadcrumb.tsx +63 -0
- package/src/components/core/button-group.tsx +37 -0
- package/src/components/core/button.tsx +110 -0
- package/src/components/core/calendar.tsx +143 -0
- package/src/components/core/card.tsx +60 -0
- package/src/components/core/carousel.tsx +170 -0
- package/src/components/core/chart.tsx +377 -0
- package/src/components/core/checkbox.tsx +64 -0
- package/src/components/core/collapsible.tsx +30 -0
- package/src/components/core/combobox.tsx +114 -0
- package/src/components/core/command-box.tsx +22 -0
- package/src/components/core/command.tsx +165 -0
- package/src/components/core/confirm-action.tsx +94 -0
- package/src/components/core/context-menu.tsx +139 -0
- package/src/components/core/copy-button.tsx +41 -0
- package/src/components/core/data-table.tsx +173 -0
- package/src/components/core/date-picker.tsx +73 -0
- package/src/components/core/dialog.tsx +83 -0
- package/src/components/core/drawer.tsx +87 -0
- package/src/components/core/dropdown-menu.tsx +147 -0
- package/src/components/core/empty.tsx +34 -0
- package/src/components/core/field.tsx +39 -0
- package/src/components/core/file-upload.tsx +143 -0
- package/src/components/core/hover-card.tsx +31 -0
- package/src/components/core/inline-edit.tsx +104 -0
- package/src/components/core/input-group.tsx +47 -0
- package/src/components/core/input-otp.tsx +108 -0
- package/src/components/core/input.tsx +37 -0
- package/src/components/core/kbd.tsx +47 -0
- package/src/components/core/label.tsx +28 -0
- package/src/components/core/marquee.tsx +61 -0
- package/src/components/core/menubar.tsx +120 -0
- package/src/components/core/multi-select.tsx +145 -0
- package/src/components/core/native-select.tsx +27 -0
- package/src/components/core/navigation-menu.tsx +130 -0
- package/src/components/core/number-stepper.tsx +80 -0
- package/src/components/core/pagination.tsx +80 -0
- package/src/components/core/password-input.tsx +90 -0
- package/src/components/core/popover.tsx +34 -0
- package/src/components/core/progress.tsx +63 -0
- package/src/components/core/radio-group.tsx +77 -0
- package/src/components/core/resizable.tsx +250 -0
- package/src/components/core/scroll-area.tsx +38 -0
- package/src/components/core/select.tsx +128 -0
- package/src/components/core/separator.tsx +47 -0
- package/src/components/core/sheet.tsx +118 -0
- package/src/components/core/sidebar.tsx +129 -0
- package/src/components/core/skeleton.tsx +32 -0
- package/src/components/core/slider.tsx +97 -0
- package/src/components/core/sonner.tsx +29 -0
- package/src/components/core/spinner.tsx +60 -0
- package/src/components/core/status-pulse.tsx +67 -0
- package/src/components/core/stepper.tsx +111 -0
- package/src/components/core/switch.tsx +72 -0
- package/src/components/core/table.tsx +104 -0
- package/src/components/core/tabs.tsx +55 -0
- package/src/components/core/tag-input.tsx +93 -0
- package/src/components/core/textarea.tsx +44 -0
- package/src/components/core/timeline.tsx +81 -0
- package/src/components/core/toggle-group.tsx +56 -0
- package/src/components/core/toggle.tsx +66 -0
- package/src/components/core/tooltip.tsx +31 -0
- package/src/components/core/typing-indicator.tsx +51 -0
- package/src/index.ts +8 -0
- package/src/manifests.ts +1682 -0
- package/src/types.ts +58 -0
- 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
|
+
}
|