@stampui/blocks 1.1.0 → 2.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 (56) hide show
  1. package/dist/manifests.js +917 -170
  2. package/package.json +15 -10
  3. package/src/components/blocks/animated-counter.tsx +70 -0
  4. package/src/components/blocks/changelog-feed.tsx +3 -3
  5. package/src/components/blocks/gradient-text.tsx +39 -0
  6. package/src/components/blocks/grid-wave.tsx +40 -0
  7. package/src/components/blocks/loading-card.tsx +48 -0
  8. package/src/components/blocks/loading-dots.tsx +68 -0
  9. package/src/components/blocks/orbit-trail.tsx +30 -0
  10. package/src/components/blocks/progress-ring.tsx +72 -0
  11. package/src/components/blocks/registry-card.tsx +9 -10
  12. package/src/components/blocks/signal-arc.tsx +32 -0
  13. package/src/components/blocks/typewriter-text.tsx +62 -0
  14. package/src/components/blocks/waitlist-section.tsx +1 -1
  15. package/src/components/core/alert-dialog.tsx +2 -2
  16. package/src/components/core/avatar.tsx +8 -4
  17. package/src/components/core/button.tsx +1 -1
  18. package/src/components/core/checkbox.tsx +1 -1
  19. package/src/components/core/combobox.tsx +1 -1
  20. package/src/components/core/command.tsx +7 -4
  21. package/src/components/core/date-picker.tsx +1 -1
  22. package/src/components/core/dialog.tsx +1 -1
  23. package/src/components/core/drawer.tsx +1 -1
  24. package/src/components/core/input.tsx +2 -0
  25. package/src/components/core/label.tsx +1 -1
  26. package/src/components/core/multi-select.tsx +1 -1
  27. package/src/components/core/native-select.tsx +1 -1
  28. package/src/components/core/password-input.tsx +3 -0
  29. package/src/components/core/radio-group.tsx +1 -1
  30. package/src/components/core/resizable.tsx +1 -1
  31. package/src/components/core/select.tsx +1 -1
  32. package/src/components/core/sheet.tsx +1 -1
  33. package/src/components/core/slider.tsx +1 -1
  34. package/src/components/core/status-pulse.tsx +6 -0
  35. package/src/components/core/switch.tsx +1 -1
  36. package/src/components/core/table.tsx +7 -2
  37. package/src/components/core/tabs.tsx +1 -1
  38. package/src/components/core/toggle.tsx +1 -1
  39. package/src/components/core/typing-indicator.tsx +41 -27
  40. package/src/manifests.ts +932 -183
  41. package/src/components/blocks/ai-chat-shell.tsx +0 -97
  42. package/src/components/blocks/auth-panel.tsx +0 -203
  43. package/src/components/blocks/dashboard-shell.tsx +0 -135
  44. package/src/components/blocks/notification-center.tsx +0 -185
  45. package/src/components/blocks/onboarding-flow.tsx +0 -230
  46. package/src/components/blocks/project-command-center.tsx +0 -188
  47. package/src/components/blocks/prompt-input.tsx +0 -81
  48. package/src/components/blocks/settings-layout.tsx +0 -178
  49. package/src/components/blocks/token-stream.tsx +0 -42
  50. package/src/components/core/carousel.tsx +0 -170
  51. package/src/components/core/chart.tsx +0 -377
  52. package/src/components/core/data-table.tsx +0 -173
  53. package/src/components/core/file-upload.tsx +0 -143
  54. package/src/components/core/input-otp.tsx +0 -108
  55. package/src/components/core/stepper.tsx +0 -111
  56. package/src/components/core/timeline.tsx +0 -81
@@ -1,230 +0,0 @@
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
- }
@@ -1,188 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import { ArrowUpRight, CheckCircle2, Clock3, GitBranch, Rocket, ShieldCheck } from "lucide-react"
5
- import { AvatarStack } from "@/components/core/avatar-stack"
6
- import { Badge } from "@/components/core/badge"
7
- import { Button } from "@/components/core/button"
8
- import { Card, CardBody, CardHeader, CardTitle } from "@/components/core/card"
9
- import { Progress } from "@/components/core/progress"
10
- import { Separator } from "@/components/core/separator"
11
- import { StatusPulse } from "@/components/core/status-pulse"
12
- import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/core/tabs"
13
- import { cx } from "@/lib/cx"
14
-
15
- export interface ProjectCommandCenterProps {
16
- className?: string
17
- }
18
-
19
- const team = [
20
- { name: "Ava" },
21
- { name: "Mert" },
22
- { name: "Lina" },
23
- { name: "Noah" },
24
- { name: "Iris" },
25
- ]
26
-
27
- const milestones = [
28
- { label: "Checkout flow", status: "Ready", progress: 92 },
29
- { label: "Usage limits", status: "Review", progress: 68 },
30
- { label: "Billing webhooks", status: "Blocked", progress: 34 },
31
- ]
32
-
33
- const events = [
34
- { icon: CheckCircle2, label: "Auth panel merged", meta: "12 min ago", tone: "success" },
35
- { icon: GitBranch, label: "Preview branch deployed", meta: "31 min ago", tone: "info" },
36
- { icon: ShieldCheck, label: "Security scan passed", meta: "1h ago", tone: "neutral" },
37
- ]
38
-
39
- const health = [
40
- { label: "API", value: "99.98%", status: "online" as const },
41
- { label: "Queue", value: "42ms", status: "processing" as const },
42
- { label: "Sync", value: "Live", status: "online" as const },
43
- ]
44
-
45
- function MetricCard({
46
- label,
47
- value,
48
- description,
49
- }: {
50
- label: string
51
- value: string
52
- description: string
53
- }) {
54
- return (
55
- <Card variant="surface" className="min-h-[116px]">
56
- <CardHeader className="pb-2">
57
- <p className="text-xs font-medium uppercase tracking-widest text-muted-foreground">{label}</p>
58
- <CardTitle className="text-2xl tabular-nums">{value}</CardTitle>
59
- </CardHeader>
60
- <CardBody className="pt-0">
61
- <p className="text-xs leading-relaxed text-muted-foreground">{description}</p>
62
- </CardBody>
63
- </Card>
64
- )
65
- }
66
-
67
- export function ProjectCommandCenter({ className }: ProjectCommandCenterProps) {
68
- return (
69
- <section className={cx("w-full rounded-2xl border border-border bg-background p-4 sm:p-6", className)}>
70
- <div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
71
- <div className="space-y-3">
72
- <div className="flex flex-wrap items-center gap-2">
73
- <Badge variant="new">Sprint 24</Badge>
74
- <StatusPulse status="online" label="Production healthy" />
75
- </div>
76
- <div>
77
- <h2 className="text-2xl font-semibold tracking-tight text-foreground">Project Command Center</h2>
78
- <p className="mt-1 max-w-xl text-sm leading-relaxed text-muted-foreground">
79
- A compact delivery dashboard for launch readiness, team ownership, and live system status.
80
- </p>
81
- </div>
82
- </div>
83
-
84
- <div className="flex items-center gap-3">
85
- <AvatarStack users={team} max={4} />
86
- <Button size="sm" variant="outline">
87
- Open board
88
- <ArrowUpRight className="h-3.5 w-3.5" />
89
- </Button>
90
- </div>
91
- </div>
92
-
93
- <div className="mt-6 grid gap-3 sm:grid-cols-3">
94
- <MetricCard label="Ship score" value="84%" description="Readiness improved 12 points this week." />
95
- <MetricCard label="Open tasks" value="18" description="6 tasks need owner review before release." />
96
- <MetricCard label="Deploys" value="7" description="All preview deploys completed successfully." />
97
- </div>
98
-
99
- <Tabs defaultValue="milestones" className="mt-6">
100
- <TabsList>
101
- <TabsTrigger value="milestones">Milestones</TabsTrigger>
102
- <TabsTrigger value="activity">Activity</TabsTrigger>
103
- <TabsTrigger value="health">Health</TabsTrigger>
104
- </TabsList>
105
-
106
- <TabsContent value="milestones">
107
- <Card variant="surface">
108
- <CardBody className="space-y-5 p-5">
109
- {milestones.map((item) => (
110
- <div key={item.label} className="space-y-2">
111
- <div className="flex items-center justify-between gap-3">
112
- <div>
113
- <p className="text-sm font-medium text-foreground">{item.label}</p>
114
- <p className="text-xs text-muted-foreground">{item.progress}% complete</p>
115
- </div>
116
- <Badge variant={item.status === "Blocked" ? "warning" : item.status === "Ready" ? "success" : "neutral"}>
117
- {item.status}
118
- </Badge>
119
- </div>
120
- <Progress value={item.progress} size="sm" variant={item.status === "Blocked" ? "warning" : "default"} />
121
- </div>
122
- ))}
123
- </CardBody>
124
- </Card>
125
- </TabsContent>
126
-
127
- <TabsContent value="activity">
128
- <Card variant="surface">
129
- <CardBody className="p-0">
130
- {events.map((event, index) => {
131
- const Icon = event.icon
132
- return (
133
- <React.Fragment key={event.label}>
134
- <div className="flex items-center gap-3 p-4">
135
- <div className="flex h-9 w-9 items-center justify-center rounded-lg border border-border bg-surface-2">
136
- <Icon className="h-4 w-4 text-foreground" />
137
- </div>
138
- <div className="min-w-0 flex-1">
139
- <p className="truncate text-sm font-medium text-foreground">{event.label}</p>
140
- <p className="text-xs text-muted-foreground">{event.meta}</p>
141
- </div>
142
- <Badge variant={event.tone === "success" ? "success" : event.tone === "info" ? "info" : "neutral"}>
143
- Done
144
- </Badge>
145
- </div>
146
- {index < events.length - 1 && <Separator />}
147
- </React.Fragment>
148
- )
149
- })}
150
- </CardBody>
151
- </Card>
152
- </TabsContent>
153
-
154
- <TabsContent value="health">
155
- <Card variant="surface">
156
- <CardBody className="grid gap-3 p-5 sm:grid-cols-3">
157
- {health.map((item) => (
158
- <div key={item.label} className="rounded-lg border border-border bg-surface-2 p-4">
159
- <div className="flex items-center justify-between">
160
- <p className="text-xs font-medium uppercase tracking-widest text-muted-foreground">{item.label}</p>
161
- <StatusPulse status={item.status} pulse={item.status !== "processing"} />
162
- </div>
163
- <p className="mt-3 text-xl font-semibold tabular-nums text-foreground">{item.value}</p>
164
- </div>
165
- ))}
166
- </CardBody>
167
- </Card>
168
- </TabsContent>
169
- </Tabs>
170
-
171
- <div className="mt-6 flex flex-col gap-3 rounded-xl border border-border bg-surface-2 p-4 sm:flex-row sm:items-center sm:justify-between">
172
- <div className="flex items-center gap-3">
173
- <div className="flex h-10 w-10 items-center justify-center rounded-lg border border-border bg-background">
174
- <Rocket className="h-4 w-4 text-foreground" />
175
- </div>
176
- <div>
177
- <p className="text-sm font-medium text-foreground">Release candidate is almost ready</p>
178
- <p className="text-xs text-muted-foreground">Resolve blocked billing work to unlock final QA.</p>
179
- </div>
180
- </div>
181
- <div className="flex items-center gap-2">
182
- <Clock3 className="h-4 w-4 text-muted-foreground" />
183
- <span className="text-xs text-muted-foreground">ETA 2 days</span>
184
- </div>
185
- </div>
186
- </section>
187
- )
188
- }
@@ -1,81 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import { Paperclip, ArrowUp } from "lucide-react"
5
- import { cx } from "@/lib/cx"
6
- import { Button } from "@/components/core/button"
7
-
8
- export interface PromptInputProps
9
- extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "onSubmit"> {
10
- onValueSubmit?: (value: string) => void
11
- }
12
-
13
- export const PromptInput = React.forwardRef<HTMLTextAreaElement, PromptInputProps>(
14
- ({ className, onValueSubmit, ...props }, ref) => {
15
- const internalRef = React.useRef<HTMLTextAreaElement>(null)
16
- const [value, setValue] = React.useState("")
17
-
18
- const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
19
- setValue(e.target.value)
20
- if (internalRef.current) {
21
- internalRef.current.style.height = "auto"
22
- internalRef.current.style.height = `${internalRef.current.scrollHeight}px`
23
- }
24
- if (props.onChange) props.onChange(e)
25
- }
26
-
27
- const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
28
- if (e.key === "Enter" && !e.shiftKey) {
29
- e.preventDefault()
30
- if (value.trim() && onValueSubmit) {
31
- onValueSubmit(value)
32
- setValue("")
33
- if (internalRef.current) internalRef.current.style.height = "auto"
34
- }
35
- }
36
- if (props.onKeyDown) props.onKeyDown(e)
37
- }
38
-
39
- return (
40
- <div className={cx("relative flex w-full flex-col rounded-xl border border-border bg-card shadow-sm transition-colors focus-within:border-primary/50 focus-within:ring-1 focus-within:ring-primary/50", className)}>
41
- <textarea
42
- ref={(node) => {
43
- internalRef.current = node
44
- if (typeof ref === "function") ref(node)
45
- else if (ref) ref.current = node
46
- }}
47
- value={value}
48
- onChange={handleInput}
49
- onKeyDown={handleKeyDown}
50
- rows={1}
51
- placeholder="Send a message..."
52
- className="max-h-64 min-h-[56px] w-full resize-none bg-transparent px-4 py-4 pr-24 text-sm text-foreground outline-none placeholder:text-muted-foreground"
53
- {...props}
54
- />
55
- <div className="absolute bottom-2 right-2 flex items-center gap-1">
56
- <Button variant="ghost" size="icon" className="h-8 w-8 rounded-lg text-muted-foreground hover:text-foreground">
57
- <Paperclip className="h-4 w-4" />
58
- <span className="sr-only">Attach file</span>
59
- </Button>
60
- <Button
61
- variant={value.trim() ? "primary" : "ghost"}
62
- size="icon"
63
- className="h-8 w-8 rounded-lg transition-colors"
64
- disabled={!value.trim()}
65
- onClick={() => {
66
- if (value.trim() && onValueSubmit) {
67
- onValueSubmit(value)
68
- setValue("")
69
- if (internalRef.current) internalRef.current.style.height = "auto"
70
- }
71
- }}
72
- >
73
- <ArrowUp className="h-4 w-4" />
74
- <span className="sr-only">Send message</span>
75
- </Button>
76
- </div>
77
- </div>
78
- )
79
- }
80
- )
81
- PromptInput.displayName = "PromptInput"
@@ -1,178 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/core/tabs"
5
- import { Input } from "@/components/core/input"
6
- import { Label } from "@/components/core/label"
7
- import { Button } from "@/components/core/button"
8
- import { Switch } from "@/components/core/switch"
9
- import { Separator } from "@/components/core/separator"
10
- import { Select } from "@/components/core/select"
11
- import { Badge } from "@/components/core/badge"
12
- import { cx } from "@/lib/cx"
13
-
14
- // ── Types ──────────────────────────────────────────────────────────────────
15
-
16
- export interface SettingsLayoutProps {
17
- className?: string
18
- onSave?: (section: string, data: Record<string, unknown>) => void
19
- }
20
-
21
- // ── Account Tab ────────────────────────────────────────────────────────────
22
-
23
- function AccountSection({ onSave }: { onSave?: SettingsLayoutProps["onSave"] }) {
24
- const [name, setName] = React.useState("Alex Johnson")
25
- const [email, setEmail] = React.useState("alex@example.com")
26
- const [username, setUsername] = React.useState("alexj")
27
-
28
- return (
29
- <div className="space-y-8">
30
- <div>
31
- <h3 className="text-base font-semibold">Profile</h3>
32
- <p className="text-sm text-muted-foreground mt-0.5">Update your display name and username.</p>
33
- </div>
34
- <Separator />
35
- <div className="grid gap-5 max-w-md">
36
- <div className="space-y-2">
37
- <Label htmlFor="name">Full name</Label>
38
- <Input id="name" value={name} onChange={(e) => setName(e.target.value)} />
39
- </div>
40
- <div className="space-y-2">
41
- <Label htmlFor="username">Username</Label>
42
- <div className="flex">
43
- <span className="inline-flex items-center rounded-l-lg border border-r-0 border-border bg-surface-2 px-3 text-sm text-muted-foreground">
44
- @
45
- </span>
46
- <Input id="username" value={username} onChange={(e) => setUsername(e.target.value)} className="rounded-l-none" />
47
- </div>
48
- </div>
49
- <div className="space-y-2">
50
- <Label htmlFor="email">Email</Label>
51
- <Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
52
- </div>
53
- </div>
54
- <div className="flex gap-3">
55
- <Button onClick={() => onSave?.("account", { name, email, username })}>Save changes</Button>
56
- <Button variant="outline">Cancel</Button>
57
- </div>
58
- </div>
59
- )
60
- }
61
-
62
- // ── Notifications Tab ──────────────────────────────────────────────────────
63
-
64
- interface NotifPref { id: string; label: string; description: string; enabled: boolean }
65
-
66
- function NotificationsSection({ onSave }: { onSave?: SettingsLayoutProps["onSave"] }) {
67
- const [prefs, setPrefs] = React.useState<NotifPref[]>([
68
- { id: "email_updates", label: "Email updates", description: "Product news and announcements.", enabled: true },
69
- { id: "security_alerts", label: "Security alerts", description: "Sign-in from new device or location.", enabled: true },
70
- { id: "billing", label: "Billing notifications", description: "Invoices, renewals, and payment issues.", enabled: true },
71
- { id: "marketing", label: "Marketing emails", description: "Tips, tutorials, and new features.", enabled: false },
72
- { id: "weekly_digest", label: "Weekly digest", description: "Summary of activity from your projects.", enabled: false },
73
- ])
74
-
75
- const toggle = (id: string) =>
76
- setPrefs((p) => p.map((pref) => pref.id === id ? { ...pref, enabled: !pref.enabled } : pref))
77
-
78
- return (
79
- <div className="space-y-8">
80
- <div>
81
- <h3 className="text-base font-semibold">Notifications</h3>
82
- <p className="text-sm text-muted-foreground mt-0.5">Choose what you want to be notified about.</p>
83
- </div>
84
- <Separator />
85
- <div className="space-y-6">
86
- {prefs.map((pref) => (
87
- <div key={pref.id} className="flex items-center justify-between gap-4">
88
- <div className="flex-1 min-w-0">
89
- <p className="text-sm font-medium">{pref.label}</p>
90
- <p className="text-xs text-muted-foreground mt-0.5">{pref.description}</p>
91
- </div>
92
- <Switch
93
- checked={pref.enabled}
94
- onCheckedChange={() => toggle(pref.id)}
95
- />
96
- </div>
97
- ))}
98
- </div>
99
- <Button onClick={() => onSave?.("notifications", Object.fromEntries(prefs.map(p => [p.id, p.enabled])))}>
100
- Save preferences
101
- </Button>
102
- </div>
103
- )
104
- }
105
-
106
- // ── Security Tab ───────────────────────────────────────────────────────────
107
-
108
- function SecuritySection({ onSave }: { onSave?: SettingsLayoutProps["onSave"] }) {
109
- const [current, setCurrent] = React.useState("")
110
- const [newPw, setNewPw] = React.useState("")
111
- const [confirm, setConfirm] = React.useState("")
112
- const [twoFa, setTwoFa] = React.useState(false)
113
-
114
- return (
115
- <div className="space-y-8">
116
- <div>
117
- <h3 className="text-base font-semibold">Security</h3>
118
- <p className="text-sm text-muted-foreground mt-0.5">Manage your password and two-factor authentication.</p>
119
- </div>
120
- <Separator />
121
- <div className="grid gap-5 max-w-md">
122
- <div className="space-y-2">
123
- <Label htmlFor="current-pw">Current password</Label>
124
- <Input id="current-pw" type="password" value={current} onChange={(e) => setCurrent(e.target.value)} />
125
- </div>
126
- <div className="space-y-2">
127
- <Label htmlFor="new-pw">New password</Label>
128
- <Input id="new-pw" type="password" value={newPw} onChange={(e) => setNewPw(e.target.value)} />
129
- </div>
130
- <div className="space-y-2">
131
- <Label htmlFor="confirm-pw">Confirm new password</Label>
132
- <Input id="confirm-pw" type="password" value={confirm} onChange={(e) => setConfirm(e.target.value)} />
133
- </div>
134
- </div>
135
- <Button onClick={() => onSave?.("password", { current, newPw, confirm })}>Update password</Button>
136
-
137
- <Separator />
138
-
139
- <div className="flex items-center justify-between max-w-md">
140
- <div>
141
- <p className="text-sm font-medium">Two-factor authentication</p>
142
- <p className="text-xs text-muted-foreground mt-0.5">Add an extra layer of security to your account.</p>
143
- </div>
144
- <Switch checked={twoFa} onCheckedChange={setTwoFa} />
145
- </div>
146
- </div>
147
- )
148
- }
149
-
150
- // ── Root ───────────────────────────────────────────────────────────────────
151
-
152
- const TABS = [
153
- { value: "account", label: "Account" },
154
- { value: "notifications", label: "Notifications" },
155
- { value: "security", label: "Security" },
156
- ]
157
-
158
- export function SettingsLayout({ className, onSave }: SettingsLayoutProps) {
159
- return (
160
- <div className={cx("w-full max-w-3xl mx-auto py-10 px-6", className)}>
161
- <div className="mb-8">
162
- <h1 className="text-2xl font-semibold tracking-tight">Settings</h1>
163
- <p className="text-sm text-muted-foreground mt-1">Manage your account preferences and security.</p>
164
- </div>
165
-
166
- <Tabs defaultValue="account">
167
- <TabsList className="mb-8">
168
- {TABS.map((tab) => (
169
- <TabsTrigger key={tab.value} value={tab.value}>{tab.label}</TabsTrigger>
170
- ))}
171
- </TabsList>
172
- <TabsContent value="account"><AccountSection onSave={onSave} /></TabsContent>
173
- <TabsContent value="notifications"><NotificationsSection onSave={onSave} /></TabsContent>
174
- <TabsContent value="security"><SecuritySection onSave={onSave} /></TabsContent>
175
- </Tabs>
176
- </div>
177
- )
178
- }