@stampui/blocks 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/components/ai-chat-shell.d.ts +1 -0
  2. package/dist/components/ai-chat-shell.js +23 -0
  3. package/dist/components/prompt-input.d.ts +5 -0
  4. package/dist/components/prompt-input.js +47 -0
  5. package/dist/components/registry-card.d.ts +6 -0
  6. package/dist/components/registry-card.js +15 -0
  7. package/dist/components/registry-explorer.d.ts +8 -0
  8. package/dist/components/registry-explorer.js +38 -0
  9. package/dist/components/token-stream.d.ts +7 -0
  10. package/dist/components/token-stream.js +21 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +23 -0
  13. package/dist/manifests.d.ts +3 -0
  14. package/dist/manifests.js +1666 -0
  15. package/dist/types.d.ts +44 -0
  16. package/dist/types.js +2 -0
  17. package/package.json +28 -0
  18. package/src/components/blocks/ai-chat-shell.tsx +97 -0
  19. package/src/components/blocks/auth-panel.tsx +203 -0
  20. package/src/components/blocks/feature-grid.tsx +122 -0
  21. package/src/components/blocks/hero-section.tsx +73 -0
  22. package/src/components/blocks/notification-center.tsx +185 -0
  23. package/src/components/blocks/onboarding-flow.tsx +230 -0
  24. package/src/components/blocks/pricing-section.tsx +135 -0
  25. package/src/components/blocks/project-command-center.tsx +188 -0
  26. package/src/components/blocks/prompt-input.tsx +81 -0
  27. package/src/components/blocks/registry-card.tsx +104 -0
  28. package/src/components/blocks/registry-explorer.tsx +78 -0
  29. package/src/components/blocks/settings-layout.tsx +178 -0
  30. package/src/components/blocks/stats-strip.tsx +100 -0
  31. package/src/components/blocks/token-stream.tsx +42 -0
  32. package/src/components/blocks/usage-card.tsx +116 -0
  33. package/src/components/core/accordion.tsx +58 -0
  34. package/src/components/core/alert-dialog.tsx +113 -0
  35. package/src/components/core/alert.tsx +48 -0
  36. package/src/components/core/animated-number.tsx +77 -0
  37. package/src/components/core/aspect-ratio.tsx +20 -0
  38. package/src/components/core/avatar-stack.tsx +61 -0
  39. package/src/components/core/avatar.tsx +90 -0
  40. package/src/components/core/badge.tsx +39 -0
  41. package/src/components/core/breadcrumb.tsx +63 -0
  42. package/src/components/core/button-group.tsx +37 -0
  43. package/src/components/core/button.tsx +110 -0
  44. package/src/components/core/calendar.tsx +143 -0
  45. package/src/components/core/card.tsx +60 -0
  46. package/src/components/core/carousel.tsx +170 -0
  47. package/src/components/core/chart.tsx +377 -0
  48. package/src/components/core/checkbox.tsx +64 -0
  49. package/src/components/core/collapsible.tsx +30 -0
  50. package/src/components/core/combobox.tsx +114 -0
  51. package/src/components/core/command-box.tsx +22 -0
  52. package/src/components/core/command.tsx +165 -0
  53. package/src/components/core/confirm-action.tsx +94 -0
  54. package/src/components/core/context-menu.tsx +139 -0
  55. package/src/components/core/copy-button.tsx +41 -0
  56. package/src/components/core/data-table.tsx +173 -0
  57. package/src/components/core/date-picker.tsx +73 -0
  58. package/src/components/core/dialog.tsx +83 -0
  59. package/src/components/core/drawer.tsx +87 -0
  60. package/src/components/core/dropdown-menu.tsx +147 -0
  61. package/src/components/core/empty.tsx +34 -0
  62. package/src/components/core/field.tsx +39 -0
  63. package/src/components/core/file-upload.tsx +143 -0
  64. package/src/components/core/hover-card.tsx +31 -0
  65. package/src/components/core/inline-edit.tsx +104 -0
  66. package/src/components/core/input-group.tsx +47 -0
  67. package/src/components/core/input-otp.tsx +108 -0
  68. package/src/components/core/input.tsx +37 -0
  69. package/src/components/core/kbd.tsx +47 -0
  70. package/src/components/core/label.tsx +28 -0
  71. package/src/components/core/marquee.tsx +61 -0
  72. package/src/components/core/menubar.tsx +120 -0
  73. package/src/components/core/multi-select.tsx +145 -0
  74. package/src/components/core/native-select.tsx +27 -0
  75. package/src/components/core/navigation-menu.tsx +130 -0
  76. package/src/components/core/number-stepper.tsx +80 -0
  77. package/src/components/core/pagination.tsx +80 -0
  78. package/src/components/core/password-input.tsx +90 -0
  79. package/src/components/core/popover.tsx +34 -0
  80. package/src/components/core/progress.tsx +63 -0
  81. package/src/components/core/radio-group.tsx +77 -0
  82. package/src/components/core/resizable.tsx +250 -0
  83. package/src/components/core/scroll-area.tsx +38 -0
  84. package/src/components/core/select.tsx +128 -0
  85. package/src/components/core/separator.tsx +47 -0
  86. package/src/components/core/sheet.tsx +118 -0
  87. package/src/components/core/sidebar.tsx +129 -0
  88. package/src/components/core/skeleton.tsx +32 -0
  89. package/src/components/core/slider.tsx +97 -0
  90. package/src/components/core/sonner.tsx +29 -0
  91. package/src/components/core/spinner.tsx +60 -0
  92. package/src/components/core/status-pulse.tsx +67 -0
  93. package/src/components/core/stepper.tsx +111 -0
  94. package/src/components/core/switch.tsx +72 -0
  95. package/src/components/core/table.tsx +104 -0
  96. package/src/components/core/tabs.tsx +55 -0
  97. package/src/components/core/tag-input.tsx +93 -0
  98. package/src/components/core/textarea.tsx +44 -0
  99. package/src/components/core/timeline.tsx +81 -0
  100. package/src/components/core/toggle-group.tsx +56 -0
  101. package/src/components/core/toggle.tsx +66 -0
  102. package/src/components/core/tooltip.tsx +31 -0
  103. package/src/components/core/typing-indicator.tsx +51 -0
  104. package/src/index.ts +8 -0
  105. package/src/manifests.ts +1682 -0
  106. package/src/types.ts +58 -0
  107. package/src/ui.ts +13 -0
@@ -0,0 +1,188 @@
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
+ }
@@ -0,0 +1,81 @@
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"
@@ -0,0 +1,104 @@
1
+ import * as React from "react"
2
+ import Link from "next/link"
3
+ import { Terminal, ArrowRight } from "lucide-react"
4
+ import { Badge } from "@/components/core/badge"
5
+ import { cx } from "@/lib/cx"
6
+ import { CopyButton } from "@/components/core/copy-button"
7
+ import type { BlockManifest } from "../../types"
8
+
9
+ export interface RegistryCardProps extends React.HTMLAttributes<HTMLDivElement> {
10
+ block: BlockManifest
11
+ }
12
+
13
+ /** Intentional fallback for blocks without a generated preview asset. */
14
+ function BlockPreviewFallback({ slug }: { slug: string }) {
15
+ return (
16
+ <div className="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-surface-2">
17
+ <div className="w-32 h-20 rounded-lg border border-border-soft bg-surface flex flex-col gap-2 p-3">
18
+ <div className="h-2 w-3/4 rounded-sm bg-border-strong/60" />
19
+ <div className="h-2 w-1/2 rounded-sm bg-border-strong/40" />
20
+ <div className="mt-auto h-5 w-full rounded-md bg-border-strong/30" />
21
+ </div>
22
+ <span className="text-[10px] font-mono text-muted-foreground/60">{slug}.tsx</span>
23
+ </div>
24
+ )
25
+ }
26
+
27
+ export function RegistryCard({
28
+ block,
29
+ className,
30
+ ...props
31
+ }: RegistryCardProps) {
32
+ const { title, slug, description, category, status, previewPath, difficulty, frameworks, dependencies, promptReady, version, licenseRequired } = block
33
+ const isGated = licenseRequired || status === "pro" || status === "locked"
34
+
35
+ return (
36
+ <div
37
+ className={cx(
38
+ "group flex flex-col overflow-hidden rounded-xl border border-border bg-surface text-card-foreground transition-all hover:border-border-strong",
39
+ className
40
+ )}
41
+ {...props}
42
+ >
43
+ <Link href={`/blocks/${slug}`} className="block relative aspect-video w-full overflow-hidden border-b border-border bg-surface-2 outline-none focus-visible:ring-2 focus-visible:ring-border-strong">
44
+ {previewPath ? (
45
+ <img
46
+ src={previewPath}
47
+ alt={`${title} preview`}
48
+ className="object-contain w-full h-full p-3"
49
+ />
50
+ ) : (
51
+ <BlockPreviewFallback slug={slug} />
52
+ )}
53
+
54
+ <div className="absolute top-3 right-3 flex gap-1.5">
55
+ {isGated && (
56
+ <Badge variant="pro" className="bg-background/70 backdrop-blur-sm text-[10px] tracking-widest font-mono">
57
+ PRO
58
+ </Badge>
59
+ )}
60
+ {status === "new" && !isGated && (
61
+ <Badge variant="new" className="bg-background/70 backdrop-blur-sm text-[10px] tracking-widest font-mono">
62
+ NEW
63
+ </Badge>
64
+ )}
65
+ {promptReady && (
66
+ <Badge variant="neutral" className="bg-background/70 backdrop-blur-sm text-[10px] font-mono hidden group-hover:inline-flex">
67
+ Prompt Ready
68
+ </Badge>
69
+ )}
70
+ </div>
71
+ </Link>
72
+
73
+ <div className="flex flex-1 flex-col p-4">
74
+ <Link href={`/blocks/${slug}`} className="outline-none focus-visible:underline">
75
+ <h3 className="font-semibold text-sm leading-tight tracking-tight text-foreground group-hover:text-primary transition-colors">
76
+ {title}
77
+ </h3>
78
+ </Link>
79
+ <p className="text-xs text-muted-foreground line-clamp-1 mt-1">
80
+ {description}
81
+ </p>
82
+
83
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1.5 mt-4 text-[10px] font-mono text-muted-foreground h-4">
84
+ <span className="text-border-strong">v{version}</span>
85
+ <span className="text-border-strong">·</span>
86
+ <span>{category}</span>
87
+ </div>
88
+
89
+ <div className="mt-4 pt-4 border-t border-border flex items-center justify-between gap-2">
90
+ <div className="flex items-center gap-2 bg-surface-2 border border-border rounded-md px-2.5 py-1.5 w-[190px] flex-none group/cmd cursor-text">
91
+ <Terminal className="h-3 w-3 text-muted-foreground flex-shrink-0" />
92
+ <code className="text-[10px] font-mono text-muted-foreground truncate flex-1">
93
+ stampui add {slug}
94
+ </code>
95
+ <CopyButton value={`pnpm dlx stampui add ${slug}`} className="h-5 w-5 opacity-0 group-hover/cmd:opacity-100 transition-opacity flex-shrink-0" />
96
+ </div>
97
+ <Link href={`/blocks/${slug}`} className="text-xs font-medium text-foreground flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-all duration-200 ease-out translate-x-1 group-hover:translate-x-0 flex-shrink-0">
98
+ View <ArrowRight className="h-3 w-3" />
99
+ </Link>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ )
104
+ }
@@ -0,0 +1,78 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { useSearchParams } from "next/navigation"
5
+ import { Check, Command } from "lucide-react"
6
+ import { cx } from "@/lib/cx"
7
+ import { RegistryCard } from "./registry-card"
8
+ import type { BlockManifest } from "../../types"
9
+
10
+ export interface RegistryExplorerProps {
11
+ initialBlocks: (BlockManifest & { name?: string; previewThumbnail?: string })[]
12
+ }
13
+
14
+ export function RegistryExplorer({ initialBlocks }: RegistryExplorerProps) {
15
+ const searchParams = useSearchParams()
16
+ const search = searchParams?.get("q") || ""
17
+ const [activeCategory, setActiveCategory] = React.useState<string>("All")
18
+ const [activeDifficulty, setActiveDifficulty] = React.useState<string>("All")
19
+ const [activeStatus, setActiveStatus] = React.useState<string>("All")
20
+
21
+
22
+ const categories = ["All", ...Array.from(new Set(initialBlocks.map(b => b.category)))]
23
+ const difficulties = ["All", "beginner", "intermediate", "advanced"]
24
+ const statuses = ["All", "free", "pro", "new"]
25
+
26
+ const filteredBlocks = React.useMemo(() => {
27
+ return initialBlocks.filter((block) => {
28
+ const matchesSearch = block.title.toLowerCase().includes(search.toLowerCase()) ||
29
+ block.slug.toLowerCase().includes(search.toLowerCase()) ||
30
+ block.description.toLowerCase().includes(search.toLowerCase())
31
+
32
+ const matchesCategory = activeCategory === "All" || block.category === activeCategory
33
+ const matchesDifficulty = activeDifficulty === "All" || block.difficulty === activeDifficulty
34
+ const matchesStatus = activeStatus === "All" || block.status === activeStatus
35
+
36
+ return matchesSearch && matchesCategory && matchesDifficulty && matchesStatus
37
+ })
38
+ }, [initialBlocks, search, activeCategory, activeDifficulty, activeStatus])
39
+
40
+ return (
41
+ <div className="flex flex-col w-full gap-8">
42
+ <div className="flex flex-col gap-6">
43
+ <div className="flex items-center justify-between border-b border-border pb-4">
44
+ <div className="flex items-center gap-3">
45
+ <h1 className="text-2xl font-semibold tracking-tight">Blocks Registry</h1>
46
+ <span className="text-xs font-mono bg-surface-2 text-muted-foreground px-2 py-0.5 rounded-full border border-border">
47
+ {filteredBlocks.length} items
48
+ </span>
49
+ </div>
50
+ </div>
51
+
52
+ </div>
53
+ {filteredBlocks.length > 0 ? (
54
+ <div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3">
55
+ {filteredBlocks.map((block) => (
56
+ <RegistryCard key={block.slug} block={block} />
57
+ ))}
58
+ </div>
59
+ ) : (
60
+ <div className="flex flex-col items-center justify-center py-24 text-center border border-dashed border-border rounded-xl">
61
+ <Command className="h-8 w-8 text-muted-foreground mb-4 opacity-50" />
62
+ <h3 className="text-lg font-medium">No blocks found</h3>
63
+ <p className="text-sm text-muted-foreground mt-1 max-w-sm">
64
+ We couldn't find any blocks matching your current filters. Try adjusting your search or clearing filters.
65
+ </p>
66
+ <button
67
+ onClick={() => {
68
+ window.location.href = "/blocks"
69
+ }}
70
+ className="mt-6 text-sm text-foreground font-medium underline underline-offset-4 decoration-border-strong hover:decoration-foreground"
71
+ >
72
+ Clear all filters
73
+ </button>
74
+ </div>
75
+ )}
76
+ </div>
77
+ )
78
+ }
@@ -0,0 +1,178 @@
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
+ }