@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.
- package/dist/manifests.js +917 -170
- package/package.json +15 -10
- package/src/components/blocks/animated-counter.tsx +70 -0
- package/src/components/blocks/changelog-feed.tsx +3 -3
- package/src/components/blocks/gradient-text.tsx +39 -0
- package/src/components/blocks/grid-wave.tsx +40 -0
- package/src/components/blocks/loading-card.tsx +48 -0
- package/src/components/blocks/loading-dots.tsx +68 -0
- package/src/components/blocks/orbit-trail.tsx +30 -0
- package/src/components/blocks/progress-ring.tsx +72 -0
- package/src/components/blocks/registry-card.tsx +9 -10
- package/src/components/blocks/signal-arc.tsx +32 -0
- package/src/components/blocks/typewriter-text.tsx +62 -0
- package/src/components/blocks/waitlist-section.tsx +1 -1
- package/src/components/core/alert-dialog.tsx +2 -2
- package/src/components/core/avatar.tsx +8 -4
- package/src/components/core/button.tsx +1 -1
- package/src/components/core/checkbox.tsx +1 -1
- package/src/components/core/combobox.tsx +1 -1
- package/src/components/core/command.tsx +7 -4
- package/src/components/core/date-picker.tsx +1 -1
- package/src/components/core/dialog.tsx +1 -1
- package/src/components/core/drawer.tsx +1 -1
- package/src/components/core/input.tsx +2 -0
- package/src/components/core/label.tsx +1 -1
- package/src/components/core/multi-select.tsx +1 -1
- package/src/components/core/native-select.tsx +1 -1
- package/src/components/core/password-input.tsx +3 -0
- package/src/components/core/radio-group.tsx +1 -1
- package/src/components/core/resizable.tsx +1 -1
- package/src/components/core/select.tsx +1 -1
- package/src/components/core/sheet.tsx +1 -1
- package/src/components/core/slider.tsx +1 -1
- package/src/components/core/status-pulse.tsx +6 -0
- package/src/components/core/switch.tsx +1 -1
- package/src/components/core/table.tsx +7 -2
- package/src/components/core/tabs.tsx +1 -1
- package/src/components/core/toggle.tsx +1 -1
- package/src/components/core/typing-indicator.tsx +41 -27
- package/src/manifests.ts +932 -183
- package/src/components/blocks/ai-chat-shell.tsx +0 -97
- package/src/components/blocks/auth-panel.tsx +0 -203
- package/src/components/blocks/dashboard-shell.tsx +0 -135
- package/src/components/blocks/notification-center.tsx +0 -185
- package/src/components/blocks/onboarding-flow.tsx +0 -230
- package/src/components/blocks/project-command-center.tsx +0 -188
- package/src/components/blocks/prompt-input.tsx +0 -81
- package/src/components/blocks/settings-layout.tsx +0 -178
- package/src/components/blocks/token-stream.tsx +0 -42
- package/src/components/core/carousel.tsx +0 -170
- package/src/components/core/chart.tsx +0 -377
- package/src/components/core/data-table.tsx +0 -173
- package/src/components/core/file-upload.tsx +0 -143
- package/src/components/core/input-otp.tsx +0 -108
- package/src/components/core/stepper.tsx +0 -111
- 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
|
-
}
|