@stampui/blocks 1.0.0 → 1.1.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.
@@ -0,0 +1,135 @@
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
+ }
@@ -0,0 +1,64 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import type { LucideIcon } from "lucide-react"
5
+ import { Inbox } from "lucide-react"
6
+ import { Button } from "@/components/core/button"
7
+ import { cx } from "@/lib/cx"
8
+
9
+ export interface EmptyStateAction {
10
+ label: string
11
+ onClick?: () => void
12
+ href?: string
13
+ }
14
+
15
+ export interface EmptyStateProps {
16
+ icon?: LucideIcon
17
+ title: string
18
+ description?: string
19
+ action?: EmptyStateAction
20
+ className?: string
21
+ }
22
+
23
+ export function EmptyState({
24
+ icon: Icon = Inbox,
25
+ title,
26
+ description,
27
+ action,
28
+ className,
29
+ }: EmptyStateProps) {
30
+ return (
31
+ <div
32
+ className={cx(
33
+ "flex flex-col items-center justify-center text-center px-6 py-16",
34
+ className
35
+ )}
36
+ >
37
+ <div className="mb-5 flex h-14 w-14 items-center justify-center rounded-xl border border-[#23252A] bg-[#09090B]">
38
+ <Icon size={24} className="text-muted-foreground" strokeWidth={1.5} />
39
+ </div>
40
+
41
+ <h3 className="text-sm font-semibold text-[#FAFAFA] mb-1.5">{title}</h3>
42
+
43
+ {description && (
44
+ <p className="text-sm text-muted-foreground max-w-xs leading-relaxed mb-6">
45
+ {description}
46
+ </p>
47
+ )}
48
+
49
+ {action && (
50
+ <div className={cx(!description && "mt-6")}>
51
+ {action.href ? (
52
+ <Button variant="outline" size="sm" asChild>
53
+ <a href={action.href}>{action.label}</a>
54
+ </Button>
55
+ ) : (
56
+ <Button variant="outline" size="sm" onClick={action.onClick}>
57
+ {action.label}
58
+ </Button>
59
+ )}
60
+ </div>
61
+ )}
62
+ </div>
63
+ )
64
+ }
@@ -0,0 +1,69 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Button } from "@/components/core/button"
5
+ import { cx } from "@/lib/cx"
6
+
7
+ export interface ErrorPageAction {
8
+ label: string
9
+ href?: string
10
+ onClick?: () => void
11
+ }
12
+
13
+ export interface ErrorPageProps {
14
+ code?: string | number
15
+ title?: string
16
+ description?: string
17
+ action?: ErrorPageAction
18
+ className?: string
19
+ }
20
+
21
+ export function ErrorPage({
22
+ code = "404",
23
+ title = "Page not found",
24
+ description = "The page you're looking for doesn't exist or has been moved.",
25
+ action = { label: "Back to home", href: "/" },
26
+ className,
27
+ }: ErrorPageProps) {
28
+ return (
29
+ <div
30
+ className={cx(
31
+ "min-h-screen w-full bg-[#070708] flex flex-col items-center justify-center px-6",
32
+ className
33
+ )}
34
+ >
35
+ <div className="flex flex-col items-center text-center max-w-sm">
36
+ <span
37
+ className="text-[96px] font-bold leading-none tracking-tighter text-muted-foreground/30 select-none tabular-nums"
38
+ aria-hidden="true"
39
+ >
40
+ {code}
41
+ </span>
42
+
43
+ <h1 className="mt-4 text-xl font-semibold text-[#FAFAFA] tracking-tight">
44
+ {title}
45
+ </h1>
46
+
47
+ {description && (
48
+ <p className="mt-2 text-sm text-muted-foreground leading-relaxed">
49
+ {description}
50
+ </p>
51
+ )}
52
+
53
+ {action && (
54
+ <div className="mt-8">
55
+ {action.href ? (
56
+ <Button variant="outline" size="md" asChild>
57
+ <a href={action.href}>{action.label}</a>
58
+ </Button>
59
+ ) : (
60
+ <Button variant="outline" size="md" onClick={action.onClick}>
61
+ {action.label}
62
+ </Button>
63
+ )}
64
+ </div>
65
+ )}
66
+ </div>
67
+ </div>
68
+ )
69
+ }
@@ -0,0 +1,94 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Plus, Minus } from "lucide-react"
5
+ import { cx } from "@/lib/cx"
6
+
7
+ export interface FaqItem {
8
+ question: string
9
+ answer: string
10
+ }
11
+
12
+ export interface FaqAccordionProps {
13
+ items?: FaqItem[]
14
+ title?: string
15
+ className?: string
16
+ }
17
+
18
+ const DEFAULT_ITEMS: FaqItem[] = [
19
+ {
20
+ question: "Do I need to install a runtime?",
21
+ answer: "No. StampUI copies source files directly into your project. There is no runtime dependency — you own every line of code after stamping.",
22
+ },
23
+ {
24
+ question: "What frameworks are supported?",
25
+ answer: "Next.js 13+ (App Router), Remix, and any React project using Tailwind CSS. Vite-based setups work with minor config adjustments.",
26
+ },
27
+ {
28
+ question: "Can I customize the components after stamping?",
29
+ answer: "Yes, and that's the point. Once stamped, components are plain TypeScript/React files in your codebase. Modify them freely without worrying about upstream conflicts.",
30
+ },
31
+ {
32
+ question: "Is there a free tier?",
33
+ answer: "All blocks are free to stamp and use in personal or commercial projects. A pro plan unlocks private block collections and team sharing.",
34
+ },
35
+ ]
36
+
37
+ function AccordionItem({ question, answer }: FaqItem) {
38
+ const [open, setOpen] = React.useState(false)
39
+
40
+ return (
41
+ <div className="border-b border-[#23252A] last:border-b-0">
42
+ <button
43
+ onClick={() => setOpen((v) => !v)}
44
+ className={cx(
45
+ "w-full flex items-center justify-between gap-4 py-5 text-left",
46
+ "text-sm font-medium text-[#FAFAFA] leading-snug",
47
+ "transition-colors duration-[170ms] ease-out",
48
+ "hover:text-[#FAFAFA]/80"
49
+ )}
50
+ aria-expanded={open}
51
+ >
52
+ <span>{question}</span>
53
+ <span className="shrink-0 text-muted-foreground">
54
+ {open ? <Minus size={14} /> : <Plus size={14} />}
55
+ </span>
56
+ </button>
57
+
58
+ <div
59
+ className={cx(
60
+ "overflow-hidden transition-all duration-[200ms] ease-out",
61
+ open ? "max-h-96 opacity-100" : "max-h-0 opacity-0"
62
+ )}
63
+ >
64
+ <p className="pb-5 text-sm text-muted-foreground leading-relaxed">
65
+ {answer}
66
+ </p>
67
+ </div>
68
+ </div>
69
+ )
70
+ }
71
+
72
+ export function FaqAccordion({
73
+ items = DEFAULT_ITEMS,
74
+ title = "Frequently asked questions",
75
+ className,
76
+ }: FaqAccordionProps) {
77
+ return (
78
+ <section className={cx("w-full py-16 px-6", className)}>
79
+ <div className="max-w-2xl mx-auto">
80
+ {title && (
81
+ <h2 className="text-2xl font-bold tracking-tight text-[#FAFAFA] mb-8">
82
+ {title}
83
+ </h2>
84
+ )}
85
+
86
+ <div className="rounded-xl border border-[#23252A] bg-[#09090B] px-6">
87
+ {items.map((item, i) => (
88
+ <AccordionItem key={i} question={item.question} answer={item.answer} />
89
+ ))}
90
+ </div>
91
+ </div>
92
+ </section>
93
+ )
94
+ }
@@ -0,0 +1,110 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cx } from "@/lib/cx"
5
+
6
+ export interface ComparisonPlan {
7
+ name: string
8
+ highlighted?: boolean
9
+ }
10
+
11
+ export interface ComparisonFeature {
12
+ label: string
13
+ values: (boolean | string)[]
14
+ }
15
+
16
+ export interface FeatureComparisonProps {
17
+ plans?: ComparisonPlan[]
18
+ features?: ComparisonFeature[]
19
+ className?: string
20
+ }
21
+
22
+ const defaultPlans: ComparisonPlan[] = [
23
+ { name: "Free" },
24
+ { name: "Pro", highlighted: true },
25
+ { name: "Team" },
26
+ ]
27
+
28
+ const defaultFeatures: ComparisonFeature[] = [
29
+ { label: "Components", values: ["50", "200+", "200+"] },
30
+ { label: "CLI access", values: [true, true, true] },
31
+ { label: "Custom themes", values: [false, true, true] },
32
+ { label: "Team seats", values: ["1", "1", "Unlimited"] },
33
+ { label: "Priority support", values: [false, false, true] },
34
+ { label: "Private blocks", values: [false, true, true] },
35
+ { label: "Analytics", values: [false, false, true] },
36
+ ]
37
+
38
+ function CellValue({ value }: { value: boolean | string }) {
39
+ if (typeof value === "boolean") {
40
+ return value ? (
41
+ <span className="text-[#FAFAFA] text-sm font-medium">✓</span>
42
+ ) : (
43
+ <span className="text-muted-foreground text-sm opacity-40">—</span>
44
+ )
45
+ }
46
+ return <span className="text-sm text-[#FAFAFA]">{value}</span>
47
+ }
48
+
49
+ export function FeatureComparison({
50
+ plans = defaultPlans,
51
+ features = defaultFeatures,
52
+ className,
53
+ }: FeatureComparisonProps) {
54
+ return (
55
+ <div className={cx("w-full overflow-x-auto", className)}>
56
+ <table className="w-full min-w-[480px] border-collapse">
57
+ <thead>
58
+ <tr>
59
+ <th className="w-[40%] pb-4 pr-4 text-left">
60
+ <span className="sr-only">Feature</span>
61
+ </th>
62
+ {plans.map((plan, i) => (
63
+ <th
64
+ key={i}
65
+ className={cx(
66
+ "pb-4 px-4 text-center text-sm font-semibold text-[#FAFAFA]",
67
+ plan.highlighted && "rounded-t-xl bg-[#101114]"
68
+ )}
69
+ >
70
+ {plan.name}
71
+ </th>
72
+ ))}
73
+ </tr>
74
+ </thead>
75
+ <tbody>
76
+ {features.map((feature, fi) => {
77
+ const isLast = fi === features.length - 1
78
+ return (
79
+ <tr
80
+ key={fi}
81
+ className="border-t border-[#23252A] transition-colors duration-150 ease-out hover:bg-[#09090B]"
82
+ >
83
+ <td
84
+ className={cx(
85
+ "py-3 pr-4 text-sm text-muted-foreground",
86
+ isLast && "pb-4"
87
+ )}
88
+ >
89
+ {feature.label}
90
+ </td>
91
+ {plans.map((plan, pi) => (
92
+ <td
93
+ key={pi}
94
+ className={cx(
95
+ "px-4 py-3 text-center",
96
+ plan.highlighted && "bg-[#101114]",
97
+ isLast && plan.highlighted && "rounded-b-xl pb-4"
98
+ )}
99
+ >
100
+ <CellValue value={feature.values[pi] ?? false} />
101
+ </td>
102
+ ))}
103
+ </tr>
104
+ )
105
+ })}
106
+ </tbody>
107
+ </table>
108
+ </div>
109
+ )
110
+ }
@@ -0,0 +1,138 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { TrendingUp, TrendingDown, Minus, type LucideIcon } from "lucide-react"
5
+ import { cx } from "@/lib/cx"
6
+
7
+ export interface MetricChange {
8
+ value: string
9
+ direction: "up" | "down" | "neutral"
10
+ }
11
+
12
+ export interface MetricItem {
13
+ label: string
14
+ value: string
15
+ change?: MetricChange
16
+ icon?: LucideIcon
17
+ description?: string
18
+ }
19
+
20
+ export interface MetricsGridProps {
21
+ metrics?: MetricItem[]
22
+ columns?: 2 | 3 | 4
23
+ className?: string
24
+ }
25
+
26
+ const defaultMetrics: MetricItem[] = [
27
+ {
28
+ label: "Monthly Revenue",
29
+ value: "$48,290",
30
+ change: { value: "+12.4%", direction: "up" },
31
+ description: "vs. last month",
32
+ },
33
+ {
34
+ label: "Active Users",
35
+ value: "9,841",
36
+ change: { value: "+3.1%", direction: "up" },
37
+ description: "vs. last month",
38
+ },
39
+ {
40
+ label: "Churn Rate",
41
+ value: "2.3%",
42
+ change: { value: "+0.4%", direction: "down" },
43
+ description: "vs. last month",
44
+ },
45
+ {
46
+ label: "Avg. Session",
47
+ value: "4m 12s",
48
+ change: { value: "0%", direction: "neutral" },
49
+ description: "vs. last month",
50
+ },
51
+ ]
52
+
53
+ const colClass: Record<2 | 3 | 4, string> = {
54
+ 2: "sm:grid-cols-2",
55
+ 3: "sm:grid-cols-2 lg:grid-cols-3",
56
+ 4: "sm:grid-cols-2 lg:grid-cols-4",
57
+ }
58
+
59
+ const changeStyles: Record<MetricChange["direction"], string> = {
60
+ up: "text-emerald-400",
61
+ down: "text-red-400",
62
+ neutral: "text-muted-foreground",
63
+ }
64
+
65
+ const ChangeIcon: Record<MetricChange["direction"], LucideIcon> = {
66
+ up: TrendingUp,
67
+ down: TrendingDown,
68
+ neutral: Minus,
69
+ }
70
+
71
+ export function MetricsGrid({
72
+ metrics = defaultMetrics,
73
+ columns = 4,
74
+ className,
75
+ }: MetricsGridProps) {
76
+ return (
77
+ <div className={cx("w-full", className)}>
78
+ <div className={cx("grid grid-cols-1 gap-4", colClass[columns])}>
79
+ {metrics.map((metric, i) => {
80
+ const Icon = metric.icon
81
+ const change = metric.change
82
+ const ChangeIconComp = change ? ChangeIcon[change.direction] : null
83
+
84
+ return (
85
+ <div
86
+ key={i}
87
+ className="flex flex-col gap-3 rounded-xl border border-[#23252A] bg-[#09090B] p-5 transition-colors duration-150 ease-out hover:border-[#2e3035] hover:bg-[#0c0c0f]"
88
+ >
89
+ <div className="flex items-center justify-between">
90
+ <span className="text-xs font-medium text-muted-foreground">
91
+ {metric.label}
92
+ </span>
93
+ {Icon && (
94
+ <Icon
95
+ size={15}
96
+ className="text-muted-foreground opacity-60"
97
+ strokeWidth={1.5}
98
+ />
99
+ )}
100
+ </div>
101
+
102
+ <p className="text-2xl font-semibold tracking-tight text-[#FAFAFA]">
103
+ {metric.value}
104
+ </p>
105
+
106
+ {(change || metric.description) && (
107
+ <div className="flex items-center gap-1.5">
108
+ {change && ChangeIconComp && (
109
+ <ChangeIconComp
110
+ size={13}
111
+ className={changeStyles[change.direction]}
112
+ strokeWidth={2}
113
+ />
114
+ )}
115
+ {change && (
116
+ <span
117
+ className={cx(
118
+ "text-xs font-medium",
119
+ changeStyles[change.direction]
120
+ )}
121
+ >
122
+ {change.value}
123
+ </span>
124
+ )}
125
+ {metric.description && (
126
+ <span className="text-xs text-muted-foreground">
127
+ {metric.description}
128
+ </span>
129
+ )}
130
+ </div>
131
+ )}
132
+ </div>
133
+ )
134
+ })}
135
+ </div>
136
+ </div>
137
+ )
138
+ }