@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,97 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { Menu, Plus, MessageSquare, Settings } from "lucide-react"
|
|
5
|
-
import { cx } from "@/lib/cx"
|
|
6
|
-
import { Button } from "@/components/core/button"
|
|
7
|
-
import { PromptInput } from "@/components/blocks/prompt-input"
|
|
8
|
-
import { TokenStream } from "@/components/blocks/token-stream"
|
|
9
|
-
|
|
10
|
-
export function AIChatShell() {
|
|
11
|
-
const [sidebarOpen, setSidebarOpen] = React.useState(true)
|
|
12
|
-
const [messages, setMessages] = React.useState([
|
|
13
|
-
{ role: "assistant", content: "Hello! I am ready to help you build your next app. What would you like to create today?" }
|
|
14
|
-
])
|
|
15
|
-
|
|
16
|
-
const handleSubmit = (val: string) => {
|
|
17
|
-
setMessages([...messages, { role: "user", content: val }])
|
|
18
|
-
setTimeout(() => {
|
|
19
|
-
setMessages(prev => [...prev, { role: "assistant", content: "I can certainly help with that! Let's break down the requirements..." }])
|
|
20
|
-
}, 600)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<div className="flex h-[600px] w-full overflow-hidden rounded-xl border border-border bg-background shadow-xl">
|
|
25
|
-
{/* Sidebar */}
|
|
26
|
-
<div
|
|
27
|
-
className={cx(
|
|
28
|
-
"flex flex-col border-r border-border bg-muted/20 transition-all duration-300",
|
|
29
|
-
sidebarOpen ? "w-64" : "w-0 overflow-hidden border-r-0"
|
|
30
|
-
)}
|
|
31
|
-
>
|
|
32
|
-
<div className="flex h-14 items-center justify-between px-4 border-b border-border">
|
|
33
|
-
<span className="font-semibold text-sm">Chats</span>
|
|
34
|
-
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
35
|
-
<Plus className="h-4 w-4" />
|
|
36
|
-
</Button>
|
|
37
|
-
</div>
|
|
38
|
-
<div className="flex-1 overflow-y-auto p-2 space-y-1">
|
|
39
|
-
<Button variant="ghost" className="w-full justify-start font-normal text-sm text-muted-foreground hover:text-foreground">
|
|
40
|
-
<MessageSquare className="mr-2 h-4 w-4" /> E-commerce Data Model
|
|
41
|
-
</Button>
|
|
42
|
-
<Button variant="ghost" className="w-full justify-start font-normal text-sm text-muted-foreground hover:text-foreground">
|
|
43
|
-
<MessageSquare className="mr-2 h-4 w-4" /> Fix Next.js Hydration Error
|
|
44
|
-
</Button>
|
|
45
|
-
</div>
|
|
46
|
-
<div className="p-2 border-t border-border">
|
|
47
|
-
<Button variant="ghost" className="w-full justify-start text-sm">
|
|
48
|
-
<Settings className="mr-2 h-4 w-4" /> Settings
|
|
49
|
-
</Button>
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
|
-
{/* Main Chat Area */}
|
|
54
|
-
<div className="flex flex-1 flex-col bg-background">
|
|
55
|
-
<div className="flex h-14 items-center border-b border-border px-4">
|
|
56
|
-
<Button
|
|
57
|
-
variant="ghost"
|
|
58
|
-
size="icon"
|
|
59
|
-
className="h-8 w-8 mr-2 md:hidden"
|
|
60
|
-
onClick={() => setSidebarOpen(!sidebarOpen)}
|
|
61
|
-
>
|
|
62
|
-
<Menu className="h-4 w-4" />
|
|
63
|
-
</Button>
|
|
64
|
-
<span className="font-medium text-sm">New Chat</span>
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
<div className="flex-1 overflow-y-auto p-4 md:p-6 space-y-6">
|
|
68
|
-
{messages.map((m, i) => (
|
|
69
|
-
<div key={i} className={cx("flex w-full", m.role === "user" ? "justify-end" : "justify-start")}>
|
|
70
|
-
<div className={cx(
|
|
71
|
-
"max-w-[80%] rounded-2xl px-5 py-3 text-sm",
|
|
72
|
-
m.role === "user"
|
|
73
|
-
? "bg-primary text-primary-foreground"
|
|
74
|
-
: "bg-muted/50 text-foreground border border-border/50"
|
|
75
|
-
)}>
|
|
76
|
-
{m.role === "assistant" && i === messages.length - 1 ? (
|
|
77
|
-
<TokenStream content={m.content} speed={20} />
|
|
78
|
-
) : (
|
|
79
|
-
<span className="whitespace-pre-wrap">{m.content}</span>
|
|
80
|
-
)}
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
83
|
-
))}
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
<div className="p-4 md:p-6 pt-0">
|
|
87
|
-
<div className="mx-auto max-w-3xl">
|
|
88
|
-
<PromptInput onSubmit={handleSubmit} />
|
|
89
|
-
<div className="mt-2 text-center text-xs text-muted-foreground">
|
|
90
|
-
AI can make mistakes. Verify important information.
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
)
|
|
97
|
-
}
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { Eye, EyeOff } from "lucide-react"
|
|
5
|
-
import { cx } from "@/lib/cx"
|
|
6
|
-
|
|
7
|
-
type AuthMode = "login" | "register"
|
|
8
|
-
|
|
9
|
-
interface AuthPanelProps {
|
|
10
|
-
defaultMode?: AuthMode
|
|
11
|
-
onLogin?: (email: string, password: string) => Promise<void> | void
|
|
12
|
-
onRegister?: (email: string, password: string, name: string) => Promise<void> | void
|
|
13
|
-
onGithub?: () => void
|
|
14
|
-
onGoogle?: () => void
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const GitHubIcon = () => (
|
|
18
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
19
|
-
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z" />
|
|
20
|
-
</svg>
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
const GoogleIcon = () => (
|
|
24
|
-
<svg width="16" height="16" viewBox="0 0 24 24">
|
|
25
|
-
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
|
|
26
|
-
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
|
27
|
-
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
|
28
|
-
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
|
29
|
-
</svg>
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
const INPUT_CLS = "h-9 w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm outline-none transition-colors placeholder:text-muted-foreground focus-visible:ring-1 focus-visible:ring-border-strong focus-visible:ring-offset-1 focus-visible:ring-offset-background"
|
|
33
|
-
|
|
34
|
-
export function AuthPanel({
|
|
35
|
-
defaultMode = "login",
|
|
36
|
-
onLogin,
|
|
37
|
-
onRegister,
|
|
38
|
-
onGithub,
|
|
39
|
-
onGoogle,
|
|
40
|
-
}: AuthPanelProps) {
|
|
41
|
-
const [mode, setMode] = React.useState<AuthMode>(defaultMode)
|
|
42
|
-
const [email, setEmail] = React.useState("")
|
|
43
|
-
const [password, setPassword] = React.useState("")
|
|
44
|
-
const [name, setName] = React.useState("")
|
|
45
|
-
const [showPassword, setShowPassword] = React.useState(false)
|
|
46
|
-
const [loading, setLoading] = React.useState(false)
|
|
47
|
-
|
|
48
|
-
async function handleSubmit(e: React.FormEvent) {
|
|
49
|
-
e.preventDefault()
|
|
50
|
-
setLoading(true)
|
|
51
|
-
try {
|
|
52
|
-
if (mode === "login") {
|
|
53
|
-
await onLogin?.(email, password)
|
|
54
|
-
} else {
|
|
55
|
-
await onRegister?.(email, password, name)
|
|
56
|
-
}
|
|
57
|
-
} finally {
|
|
58
|
-
setLoading(false)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<div className="w-full max-w-sm rounded-2xl border border-border bg-card p-8">
|
|
64
|
-
<div className="mb-6">
|
|
65
|
-
<h1 className="text-xl font-semibold tracking-tight">
|
|
66
|
-
{mode === "login" ? "Sign in" : "Create account"}
|
|
67
|
-
</h1>
|
|
68
|
-
<p className="text-sm text-muted-foreground mt-1">
|
|
69
|
-
{mode === "login"
|
|
70
|
-
? "Welcome back. Enter your credentials to continue."
|
|
71
|
-
: "Get started in seconds. No credit card required."}
|
|
72
|
-
</p>
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
<div className="flex gap-2 mb-6">
|
|
76
|
-
{onGithub && (
|
|
77
|
-
<button
|
|
78
|
-
type="button"
|
|
79
|
-
onClick={onGithub}
|
|
80
|
-
className="flex flex-1 items-center justify-center gap-2 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm font-medium transition-colors hover:bg-surface-3 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-strong"
|
|
81
|
-
>
|
|
82
|
-
<GitHubIcon />
|
|
83
|
-
GitHub
|
|
84
|
-
</button>
|
|
85
|
-
)}
|
|
86
|
-
{onGoogle && (
|
|
87
|
-
<button
|
|
88
|
-
type="button"
|
|
89
|
-
onClick={onGoogle}
|
|
90
|
-
className="flex flex-1 items-center justify-center gap-2 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm font-medium transition-colors hover:bg-surface-3 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-strong"
|
|
91
|
-
>
|
|
92
|
-
<GoogleIcon />
|
|
93
|
-
Google
|
|
94
|
-
</button>
|
|
95
|
-
)}
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
|
-
{(onGithub || onGoogle) && (
|
|
99
|
-
<div className="relative flex items-center justify-center mb-6">
|
|
100
|
-
<div className="absolute inset-0 flex items-center">
|
|
101
|
-
<div className="w-full border-t border-border" />
|
|
102
|
-
</div>
|
|
103
|
-
<span className="relative bg-card px-3 text-xs text-muted-foreground">or continue with email</span>
|
|
104
|
-
</div>
|
|
105
|
-
)}
|
|
106
|
-
|
|
107
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
108
|
-
{mode === "register" && (
|
|
109
|
-
<div className="space-y-1.5">
|
|
110
|
-
<label className="text-xs font-medium text-foreground">Name</label>
|
|
111
|
-
<input
|
|
112
|
-
type="text"
|
|
113
|
-
placeholder="Your name"
|
|
114
|
-
value={name}
|
|
115
|
-
onChange={(e) => setName(e.target.value)}
|
|
116
|
-
required
|
|
117
|
-
autoComplete="name"
|
|
118
|
-
className={INPUT_CLS}
|
|
119
|
-
/>
|
|
120
|
-
</div>
|
|
121
|
-
)}
|
|
122
|
-
|
|
123
|
-
<div className="space-y-1.5">
|
|
124
|
-
<label className="text-xs font-medium text-foreground">Email</label>
|
|
125
|
-
<input
|
|
126
|
-
type="email"
|
|
127
|
-
placeholder="you@example.com"
|
|
128
|
-
value={email}
|
|
129
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
130
|
-
required
|
|
131
|
-
autoComplete="email"
|
|
132
|
-
className={INPUT_CLS}
|
|
133
|
-
/>
|
|
134
|
-
</div>
|
|
135
|
-
|
|
136
|
-
<div className="space-y-1.5">
|
|
137
|
-
<div className="flex items-center justify-between">
|
|
138
|
-
<label className="text-xs font-medium text-foreground">Password</label>
|
|
139
|
-
{mode === "login" && (
|
|
140
|
-
<button type="button" className="text-xs text-muted-foreground hover:text-foreground transition-colors">
|
|
141
|
-
Forgot password?
|
|
142
|
-
</button>
|
|
143
|
-
)}
|
|
144
|
-
</div>
|
|
145
|
-
<div className="relative">
|
|
146
|
-
<input
|
|
147
|
-
type={showPassword ? "text" : "password"}
|
|
148
|
-
placeholder="••••••••"
|
|
149
|
-
value={password}
|
|
150
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
151
|
-
required
|
|
152
|
-
minLength={8}
|
|
153
|
-
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
|
154
|
-
className={cx(INPUT_CLS, "pr-9")}
|
|
155
|
-
/>
|
|
156
|
-
<button
|
|
157
|
-
type="button"
|
|
158
|
-
onClick={() => setShowPassword(!showPassword)}
|
|
159
|
-
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
|
160
|
-
aria-label={showPassword ? "Hide password" : "Show password"}
|
|
161
|
-
>
|
|
162
|
-
{showPassword ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
|
163
|
-
</button>
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
|
|
167
|
-
<button
|
|
168
|
-
type="submit"
|
|
169
|
-
disabled={loading}
|
|
170
|
-
className="w-full rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-strong focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50"
|
|
171
|
-
>
|
|
172
|
-
{loading ? "Loading…" : mode === "login" ? "Sign in" : "Create account"}
|
|
173
|
-
</button>
|
|
174
|
-
</form>
|
|
175
|
-
|
|
176
|
-
<p className="mt-5 text-center text-xs text-muted-foreground">
|
|
177
|
-
{mode === "login" ? (
|
|
178
|
-
<>
|
|
179
|
-
Don't have an account?{" "}
|
|
180
|
-
<button
|
|
181
|
-
type="button"
|
|
182
|
-
onClick={() => setMode("register")}
|
|
183
|
-
className="font-medium text-foreground hover:underline underline-offset-2"
|
|
184
|
-
>
|
|
185
|
-
Sign up
|
|
186
|
-
</button>
|
|
187
|
-
</>
|
|
188
|
-
) : (
|
|
189
|
-
<>
|
|
190
|
-
Already have an account?{" "}
|
|
191
|
-
<button
|
|
192
|
-
type="button"
|
|
193
|
-
onClick={() => setMode("login")}
|
|
194
|
-
className="font-medium text-foreground hover:underline underline-offset-2"
|
|
195
|
-
>
|
|
196
|
-
Sign in
|
|
197
|
-
</button>
|
|
198
|
-
</>
|
|
199
|
-
)}
|
|
200
|
-
</p>
|
|
201
|
-
</div>
|
|
202
|
-
)
|
|
203
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import type { LucideIcon } from "lucide-react"
|
|
5
|
-
import { Menu, X } from "lucide-react"
|
|
6
|
-
import { cx } from "@/lib/cx"
|
|
7
|
-
|
|
8
|
-
export interface NavItem {
|
|
9
|
-
label: string
|
|
10
|
-
href: string
|
|
11
|
-
icon?: LucideIcon
|
|
12
|
-
active?: boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface SidebarConfig {
|
|
16
|
-
logo?: React.ReactNode
|
|
17
|
-
nav: NavItem[]
|
|
18
|
-
footer?: React.ReactNode
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface DashboardShellProps {
|
|
22
|
-
sidebar: SidebarConfig
|
|
23
|
-
header?: {
|
|
24
|
-
title?: string
|
|
25
|
-
actions?: React.ReactNode
|
|
26
|
-
}
|
|
27
|
-
children: React.ReactNode
|
|
28
|
-
className?: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function DashboardShell({
|
|
32
|
-
sidebar,
|
|
33
|
-
header,
|
|
34
|
-
children,
|
|
35
|
-
className,
|
|
36
|
-
}: DashboardShellProps) {
|
|
37
|
-
const [mobileOpen, setMobileOpen] = React.useState(false)
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<div className={cx("flex min-h-screen bg-[#070708]", className)}>
|
|
41
|
-
{mobileOpen && (
|
|
42
|
-
<div
|
|
43
|
-
className="fixed inset-0 z-20 bg-[#070708]/80 lg:hidden"
|
|
44
|
-
onClick={() => setMobileOpen(false)}
|
|
45
|
-
aria-hidden="true"
|
|
46
|
-
/>
|
|
47
|
-
)}
|
|
48
|
-
|
|
49
|
-
<aside
|
|
50
|
-
className={cx(
|
|
51
|
-
"fixed inset-y-0 left-0 z-30 flex w-[240px] flex-col border-r border-[#23252A] bg-[#09090B]",
|
|
52
|
-
"transition-transform duration-[200ms] ease-out",
|
|
53
|
-
"lg:static lg:translate-x-0",
|
|
54
|
-
mobileOpen ? "translate-x-0" : "-translate-x-full"
|
|
55
|
-
)}
|
|
56
|
-
>
|
|
57
|
-
{sidebar.logo && (
|
|
58
|
-
<div className="flex h-14 shrink-0 items-center border-b border-[#23252A] px-4">
|
|
59
|
-
{sidebar.logo}
|
|
60
|
-
</div>
|
|
61
|
-
)}
|
|
62
|
-
|
|
63
|
-
<nav className="flex-1 overflow-y-auto px-2 py-3">
|
|
64
|
-
<ul className="flex flex-col gap-0.5">
|
|
65
|
-
{sidebar.nav.map((item, i) => {
|
|
66
|
-
const Icon = item.icon
|
|
67
|
-
return (
|
|
68
|
-
<li key={i}>
|
|
69
|
-
<a
|
|
70
|
-
href={item.href}
|
|
71
|
-
className={cx(
|
|
72
|
-
"flex items-center gap-2.5 rounded-xl px-3 py-2 text-sm transition-colors duration-[150ms] ease-out",
|
|
73
|
-
item.active
|
|
74
|
-
? "bg-[#101114] text-[#FAFAFA] font-medium"
|
|
75
|
-
: "text-muted-foreground hover:bg-[#101114] hover:text-[#FAFAFA]"
|
|
76
|
-
)}
|
|
77
|
-
>
|
|
78
|
-
{Icon && <Icon size={15} strokeWidth={1.75} className="shrink-0" />}
|
|
79
|
-
<span className="truncate">{item.label}</span>
|
|
80
|
-
</a>
|
|
81
|
-
</li>
|
|
82
|
-
)
|
|
83
|
-
})}
|
|
84
|
-
</ul>
|
|
85
|
-
</nav>
|
|
86
|
-
|
|
87
|
-
{sidebar.footer && (
|
|
88
|
-
<div className="shrink-0 border-t border-[#23252A] px-4 py-3">
|
|
89
|
-
{sidebar.footer}
|
|
90
|
-
</div>
|
|
91
|
-
)}
|
|
92
|
-
</aside>
|
|
93
|
-
|
|
94
|
-
<div className="flex min-w-0 flex-1 flex-col">
|
|
95
|
-
{(header || true) && (
|
|
96
|
-
<header className="flex h-14 shrink-0 items-center gap-3 border-b border-[#23252A] px-4 lg:px-6">
|
|
97
|
-
<button
|
|
98
|
-
type="button"
|
|
99
|
-
onClick={() => setMobileOpen(true)}
|
|
100
|
-
className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground transition-colors duration-[150ms] ease-out hover:bg-[#101114] hover:text-[#FAFAFA] lg:hidden"
|
|
101
|
-
aria-label="Open navigation"
|
|
102
|
-
>
|
|
103
|
-
<Menu size={16} />
|
|
104
|
-
</button>
|
|
105
|
-
|
|
106
|
-
{header?.title && (
|
|
107
|
-
<h1 className="text-sm font-semibold text-[#FAFAFA] truncate">
|
|
108
|
-
{header.title}
|
|
109
|
-
</h1>
|
|
110
|
-
)}
|
|
111
|
-
|
|
112
|
-
{header?.actions && (
|
|
113
|
-
<div className="ml-auto flex items-center gap-2">
|
|
114
|
-
{header.actions}
|
|
115
|
-
</div>
|
|
116
|
-
)}
|
|
117
|
-
</header>
|
|
118
|
-
)}
|
|
119
|
-
|
|
120
|
-
<main className="flex-1 overflow-y-auto p-4 lg:p-6">{children}</main>
|
|
121
|
-
</div>
|
|
122
|
-
|
|
123
|
-
{mobileOpen && (
|
|
124
|
-
<button
|
|
125
|
-
type="button"
|
|
126
|
-
onClick={() => setMobileOpen(false)}
|
|
127
|
-
className="fixed right-4 top-4 z-40 flex h-8 w-8 items-center justify-center rounded-lg border border-[#23252A] bg-[#09090B] text-muted-foreground transition-colors duration-[150ms] ease-out hover:text-[#FAFAFA] lg:hidden"
|
|
128
|
-
aria-label="Close navigation"
|
|
129
|
-
>
|
|
130
|
-
<X size={14} />
|
|
131
|
-
</button>
|
|
132
|
-
)}
|
|
133
|
-
</div>
|
|
134
|
-
)
|
|
135
|
-
}
|
|
@@ -1,185 +0,0 @@
|
|
|
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
|
-
}
|