ruler-components 0.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.
- package/AGENTS.md +9 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/app/docs/[slug]/page.tsx +147 -0
- package/app/docs/layout.tsx +26 -0
- package/app/docs/page.tsx +148 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +168 -0
- package/app/icon.png +0 -0
- package/app/layout.tsx +90 -0
- package/app/opengraph-image.tsx +10 -0
- package/app/page.tsx +34 -0
- package/app/twitter-image.tsx +10 -0
- package/components/docs/accordion-doc-page.tsx +352 -0
- package/components/docs/alert-doc-page.tsx +293 -0
- package/components/docs/allinput-doc-page.tsx +183 -0
- package/components/docs/avatar-doc-page.tsx +424 -0
- package/components/docs/badge-doc-page.tsx +363 -0
- package/components/docs/button-doc-page.tsx +406 -0
- package/components/docs/card-doc-page.tsx +364 -0
- package/components/docs/checkbox-doc-page.tsx +277 -0
- package/components/docs/colors-doc-page.tsx +430 -0
- package/components/docs/dialog-doc-page.tsx +364 -0
- package/components/docs/docs-footer.tsx +11 -0
- package/components/docs/docs-mobile-menu-bar.tsx +22 -0
- package/components/docs/docs-mobile-nav-context.tsx +27 -0
- package/components/docs/docs-page.tsx +263 -0
- package/components/docs/docs-search.tsx +303 -0
- package/components/docs/docs-sidebar.tsx +165 -0
- package/components/docs/docs-topbar.tsx +32 -0
- package/components/docs/dropdown-doc-page.tsx +522 -0
- package/components/docs/getting-started-doc-page.tsx +205 -0
- package/components/docs/label-doc-page.tsx +173 -0
- package/components/docs/loaders-doc-page.tsx +244 -0
- package/components/docs/menu-doc-page.tsx +373 -0
- package/components/docs/modal-doc-page.tsx +436 -0
- package/components/docs/next-steps.tsx +53 -0
- package/components/docs/pagination-doc-page.tsx +316 -0
- package/components/docs/progress-doc-page.tsx +387 -0
- package/components/docs/radio-doc-page.tsx +270 -0
- package/components/docs/skeleton-doc-page.tsx +230 -0
- package/components/docs/stepper-doc-page.tsx +309 -0
- package/components/docs/switch-doc-page.tsx +264 -0
- package/components/docs/table-doc-page.tsx +689 -0
- package/components/docs/tabs-doc-page.tsx +359 -0
- package/components/docs/theming-doc-page.tsx +243 -0
- package/components/docs/tooltip-doc-page.tsx +357 -0
- package/components/docs/usage-section.tsx +21 -0
- package/components/icons/discord.tsx +9 -0
- package/components/icons/github.tsx +9 -0
- package/components/icons/x.tsx +9 -0
- package/components/installation-section.tsx +34 -0
- package/components/landing/feature-highlights.tsx +105 -0
- package/components/landing/footer-strip.tsx +45 -0
- package/components/landing/hero-card.tsx +14 -0
- package/components/landing/hero-spotlight.tsx +126 -0
- package/components/landing/hero.tsx +64 -0
- package/components/landing/navbar.tsx +44 -0
- package/components/landing/quick-start-section.tsx +59 -0
- package/components/landing/site-footer.tsx +55 -0
- package/components/theme-provider.tsx +60 -0
- package/components/ui/accordion.tsx +174 -0
- package/components/ui/alert.tsx +103 -0
- package/components/ui/allinput.tsx +210 -0
- package/components/ui/avatar.tsx +179 -0
- package/components/ui/badge.tsx +52 -0
- package/components/ui/button.tsx +75 -0
- package/components/ui/card.tsx +105 -0
- package/components/ui/checkbox.tsx +130 -0
- package/components/ui/code-block.tsx +60 -0
- package/components/ui/data-table.tsx +235 -0
- package/components/ui/dialog.tsx +173 -0
- package/components/ui/dropdown.tsx +78 -0
- package/components/ui/feature-card.tsx +38 -0
- package/components/ui/input.tsx +44 -0
- package/components/ui/label.tsx +110 -0
- package/components/ui/loaders.tsx +139 -0
- package/components/ui/menu.tsx +118 -0
- package/components/ui/modal.tsx +399 -0
- package/components/ui/package-manager-tabs.tsx +40 -0
- package/components/ui/pagination.tsx +200 -0
- package/components/ui/progress.tsx +134 -0
- package/components/ui/radio.tsx +262 -0
- package/components/ui/skeleton.tsx +102 -0
- package/components/ui/steppers.tsx +309 -0
- package/components/ui/switch.tsx +153 -0
- package/components/ui/table.tsx +58 -0
- package/components/ui/tabs.tsx +166 -0
- package/components/ui/theme-toggle.tsx +60 -0
- package/components/ui/tooltip.tsx +238 -0
- package/data/color-palette.ts +35 -0
- package/data/docs-nav.ts +84 -0
- package/eslint.config.mjs +18 -0
- package/index.ts +8 -0
- package/lib/cn.ts +4 -0
- package/lib/color.ts +85 -0
- package/lib/og-image.tsx +138 -0
- package/lib/theme.ts +33 -0
- package/next.config.ts +7 -0
- package/package.json +30 -0
- package/postcss.config.mjs +7 -0
- package/public/dark.png +0 -0
- package/public/dark1.png +0 -0
- package/public/dark2.png +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/light.png +0 -0
- package/public/light1.png +0 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/video/hero-demo.mp4 +0 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { Check, Copy, Rows3 } from "lucide-react";
|
|
5
|
+
import {
|
|
6
|
+
AccordionSkeletonPlus,
|
|
7
|
+
AccordionSkeletonChevron,
|
|
8
|
+
CardSkeletonGrid,
|
|
9
|
+
ListSkeleton,
|
|
10
|
+
TableSkeleton
|
|
11
|
+
} from "@/components/ui/skeleton";
|
|
12
|
+
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
|
|
13
|
+
import { Tooltip } from "@/components/ui/tooltip";
|
|
14
|
+
import {
|
|
15
|
+
DocsAccessibility,
|
|
16
|
+
DocsApiSection,
|
|
17
|
+
DocsPage,
|
|
18
|
+
DocsPageHeader,
|
|
19
|
+
DocsSection,
|
|
20
|
+
DocsSectionGrid,
|
|
21
|
+
DocsSimpleTable,
|
|
22
|
+
} from "@/components/docs/docs-page";
|
|
23
|
+
import { cn } from "@/lib/cn";
|
|
24
|
+
|
|
25
|
+
const COMPONENT_ROWS = [
|
|
26
|
+
{ name: "AccordionSkeletonPlus", description: "Rows matching the plus-style accordion — icon circle and title bar. Accepts count (default 3)." },
|
|
27
|
+
{ name: "AccordionSkeletonChevron", description: "Rows matching the chevron-style accordion — title bar and trailing icon. Accepts count (default 3)." },
|
|
28
|
+
{ name: "CardSkeletonGrid", description: "A responsive grid of image, title, and description placeholders. Accepts count (default 3)." },
|
|
29
|
+
{ name: "ListSkeleton", description: "A feed-style card with an avatar and multi-line text placeholders. Accepts count (default 3)." },
|
|
30
|
+
{ name: "TableSkeleton", description: "A structured data table placeholder with headers and multiple rows. Accepts count (default 5)." },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const ACCESSIBILITY_ITEMS = [
|
|
34
|
+
"Wrap a skeleton group in a container with aria-busy=\"true\" so assistive tech knows content is still loading.",
|
|
35
|
+
"Skeletons are decorative — hide them from screen readers with aria-hidden if a loading announcement is provided elsewhere.",
|
|
36
|
+
"Match each skeleton's shape and dimensions to the real content to prevent layout shift once it loads.",
|
|
37
|
+
"Swap the skeleton for real content, or an error state, as soon as data resolves — never leave it rendered indefinitely.",
|
|
38
|
+
"Keep the pulse animation slow and subtle — avoid flashing effects that could trigger photosensitivity.",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
type Style = "accordion-plus" | "accordion-chevron" | "card-grid" | "list" | "table";
|
|
42
|
+
|
|
43
|
+
const STYLES: { value: Style; label: string; component: string }[] = [
|
|
44
|
+
{ value: "accordion-plus", label: "Accordion (plus)", component: "AccordionSkeletonPlus" },
|
|
45
|
+
{ value: "accordion-chevron", label: "Accordion (chevron)", component: "AccordionSkeletonChevron" },
|
|
46
|
+
{ value: "card-grid", label: "Card grid", component: "CardSkeletonGrid" },
|
|
47
|
+
{ value: "list", label: "List", component: "ListSkeleton" },
|
|
48
|
+
{ value: "table", label: "Table", component: "TableSkeleton" },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const COUNTS = [2, 3, 5] as const;
|
|
52
|
+
|
|
53
|
+
const STYLE_RENDERERS: Record<Style, (count: number) => React.ReactNode> = {
|
|
54
|
+
"accordion-plus": (count) => <AccordionSkeletonPlus count={count} />,
|
|
55
|
+
"accordion-chevron": (count) => <AccordionSkeletonChevron count={count} />,
|
|
56
|
+
"card-grid": (count) => <CardSkeletonGrid count={count} />,
|
|
57
|
+
list: (count) => <ListSkeleton count={count} />,
|
|
58
|
+
table: (count) => <TableSkeleton count={count} />,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function codeFor(style: Style, count: number) {
|
|
62
|
+
const { component } = STYLES.find((s) => s.value === style)!;
|
|
63
|
+
return `import { ${component} } from "@/components/ui/skeleton"\n\n<${component} count={${count}} />`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function SkeletonDocPage() {
|
|
67
|
+
const [style, setStyle] = useState<Style>("list");
|
|
68
|
+
const [count, setCount] = useState(3);
|
|
69
|
+
const [copied, setCopied] = useState(false);
|
|
70
|
+
|
|
71
|
+
const snippet = codeFor(style, count);
|
|
72
|
+
|
|
73
|
+
function copySnippet() {
|
|
74
|
+
navigator.clipboard.writeText(snippet).catch(() => {});
|
|
75
|
+
setCopied(true);
|
|
76
|
+
setTimeout(() => setCopied(false), 1200);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<DocsPage>
|
|
81
|
+
<DocsPageHeader
|
|
82
|
+
icon={Rows3}
|
|
83
|
+
title="Skeleton"
|
|
84
|
+
description="Placeholder loading states that mirror the shape of real content — accordions, card grids, and feeds — to reduce layout shift and improve perceived performance."
|
|
85
|
+
/>
|
|
86
|
+
|
|
87
|
+
<DocsSection
|
|
88
|
+
first
|
|
89
|
+
title="Playground"
|
|
90
|
+
description="Pick a layout and item count, then copy the exact markup — live."
|
|
91
|
+
>
|
|
92
|
+
<div className="overflow-hidden rounded-2xl border border-zinc-200 shadow-[0_2px_8px_rgba(0,0,0,0.04)] dark:border-zinc-800 dark:shadow-[0_2px_8px_rgba(0,0,0,0.2)]">
|
|
93
|
+
<div className="relative flex min-h-[12rem] items-center justify-center overflow-hidden bg-zinc-50 p-6 dark:bg-zinc-900/40">
|
|
94
|
+
<div
|
|
95
|
+
aria-hidden
|
|
96
|
+
className="pointer-events-none absolute inset-0 opacity-[0.4] dark:opacity-[0.15]"
|
|
97
|
+
style={{
|
|
98
|
+
backgroundImage:
|
|
99
|
+
"linear-gradient(to right, rgb(161 161 170 / 0.35) 1px, transparent 1px), linear-gradient(to bottom, rgb(161 161 170 / 0.35) 1px, transparent 1px)",
|
|
100
|
+
backgroundSize: "24px 24px",
|
|
101
|
+
}}
|
|
102
|
+
/>
|
|
103
|
+
<div
|
|
104
|
+
key={`${style}-${count}`}
|
|
105
|
+
className="relative w-full max-w-md animate-[pagination-pop_0.2s_ease-out]"
|
|
106
|
+
>
|
|
107
|
+
{STYLE_RENDERERS[style](count)}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div className="flex flex-col gap-5 border-t border-zinc-200 p-5 dark:border-zinc-800">
|
|
112
|
+
<div className="flex flex-wrap items-start justify-between gap-6">
|
|
113
|
+
<div>
|
|
114
|
+
<p className="mb-2 text-xs font-medium uppercase tracking-wide text-zinc-400 dark:text-zinc-500">
|
|
115
|
+
Style
|
|
116
|
+
</p>
|
|
117
|
+
<div className="flex flex-wrap gap-1.5">
|
|
118
|
+
{STYLES.map((s) => (
|
|
119
|
+
<button
|
|
120
|
+
key={s.value}
|
|
121
|
+
type="button"
|
|
122
|
+
onClick={() => setStyle(s.value)}
|
|
123
|
+
className={cn(
|
|
124
|
+
"rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors",
|
|
125
|
+
style === s.value
|
|
126
|
+
? "border-zinc-900 bg-zinc-900 text-white dark:border-white dark:bg-white dark:text-zinc-900"
|
|
127
|
+
: "border-zinc-200 text-zinc-500 hover:border-zinc-300 dark:border-zinc-700 dark:text-zinc-400 dark:hover:border-zinc-600"
|
|
128
|
+
)}
|
|
129
|
+
>
|
|
130
|
+
{s.label}
|
|
131
|
+
</button>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div>
|
|
137
|
+
<p className="mb-2 text-xs font-medium uppercase tracking-wide text-zinc-400 dark:text-zinc-500">
|
|
138
|
+
Count
|
|
139
|
+
</p>
|
|
140
|
+
<div className="flex flex-wrap items-center gap-1 rounded-lg border border-zinc-200 bg-zinc-50 p-1 dark:border-zinc-800 dark:bg-zinc-900/60">
|
|
141
|
+
{COUNTS.map((c) => (
|
|
142
|
+
<button
|
|
143
|
+
key={c}
|
|
144
|
+
type="button"
|
|
145
|
+
onClick={() => setCount(c)}
|
|
146
|
+
className={cn(
|
|
147
|
+
"rounded-md px-3 py-1.5 text-xs font-medium uppercase tracking-wide transition-colors",
|
|
148
|
+
count === c
|
|
149
|
+
? "bg-white text-zinc-900 shadow-sm dark:bg-zinc-800 dark:text-white"
|
|
150
|
+
: "text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200"
|
|
151
|
+
)}
|
|
152
|
+
>
|
|
153
|
+
{c}
|
|
154
|
+
</button>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div className="flex items-center justify-between gap-3 rounded-xl bg-zinc-900 px-4 py-3 dark:bg-black">
|
|
161
|
+
<pre className="overflow-x-auto font-mono text-[13px] leading-relaxed text-zinc-100">
|
|
162
|
+
<code>{snippet}</code>
|
|
163
|
+
</pre>
|
|
164
|
+
<Tooltip content={copied ? "Copied!" : "Copy code"}>
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
onClick={copySnippet}
|
|
168
|
+
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-zinc-400 transition-colors hover:bg-white/10 hover:text-white"
|
|
169
|
+
>
|
|
170
|
+
{copied ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />}
|
|
171
|
+
</button>
|
|
172
|
+
</Tooltip>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</DocsSection>
|
|
177
|
+
|
|
178
|
+
<DocsSection title="Default" description="Match each skeleton to the component it's standing in for.">
|
|
179
|
+
<Card className="max-w-md p-4">
|
|
180
|
+
<AccordionSkeletonPlus />
|
|
181
|
+
</Card>
|
|
182
|
+
</DocsSection>
|
|
183
|
+
|
|
184
|
+
<DocsSection title="Accordion Variants" description="Skeletons for both accordion trigger styles.">
|
|
185
|
+
<DocsSectionGrid>
|
|
186
|
+
<Card>
|
|
187
|
+
<CardHeader>
|
|
188
|
+
<CardTitle className="text-base">Plus style</CardTitle>
|
|
189
|
+
<CardDescription>Matches the default accordion trigger.</CardDescription>
|
|
190
|
+
</CardHeader>
|
|
191
|
+
<CardContent>
|
|
192
|
+
<AccordionSkeletonPlus />
|
|
193
|
+
</CardContent>
|
|
194
|
+
</Card>
|
|
195
|
+
<Card>
|
|
196
|
+
<CardHeader>
|
|
197
|
+
<CardTitle className="text-base">Chevron style</CardTitle>
|
|
198
|
+
<CardDescription>Matches the chevron accordion trigger.</CardDescription>
|
|
199
|
+
</CardHeader>
|
|
200
|
+
<CardContent>
|
|
201
|
+
<AccordionSkeletonChevron />
|
|
202
|
+
</CardContent>
|
|
203
|
+
</Card>
|
|
204
|
+
</DocsSectionGrid>
|
|
205
|
+
</DocsSection>
|
|
206
|
+
|
|
207
|
+
<DocsSection title="Card Grid" description="A media-heavy placeholder for image and text cards.">
|
|
208
|
+
<Card className="p-6">
|
|
209
|
+
<CardSkeletonGrid count={3} />
|
|
210
|
+
</Card>
|
|
211
|
+
</DocsSection>
|
|
212
|
+
|
|
213
|
+
<DocsSection title="Feed / List" description="A detailed placeholder with an avatar and multiple text lines.">
|
|
214
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
215
|
+
<ListSkeleton count={1} />
|
|
216
|
+
<ListSkeleton count={1} />
|
|
217
|
+
</div>
|
|
218
|
+
</DocsSection>
|
|
219
|
+
|
|
220
|
+
<DocsSection title="Data Table" description="A comprehensive layout for list-based data views.">
|
|
221
|
+
<TableSkeleton count={8} />
|
|
222
|
+
</DocsSection>
|
|
223
|
+
|
|
224
|
+
<DocsApiSection>
|
|
225
|
+
<DocsSimpleTable title="Available Skeletons" rows={COMPONENT_ROWS} />
|
|
226
|
+
<DocsAccessibility items={ACCESSIBILITY_ITEMS} />
|
|
227
|
+
</DocsApiSection>
|
|
228
|
+
</DocsPage>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { Check, ChevronLeft, ChevronRight, Copy, Layers, ShieldCheck, Box, Truck, CheckCircle2 } from "lucide-react";
|
|
5
|
+
import {
|
|
6
|
+
HorizontalStepper, VerticalStepper, ProgressStepper,
|
|
7
|
+
DotStepper, SegmentedStepper, BreadcrumbStepper,
|
|
8
|
+
TimelineStepper, InteractiveStepper, type Step
|
|
9
|
+
} from "@/components/ui/steppers";
|
|
10
|
+
import { Card } from "@/components/ui/card";
|
|
11
|
+
import { Tooltip } from "@/components/ui/tooltip";
|
|
12
|
+
import {
|
|
13
|
+
DocsAccessibility,
|
|
14
|
+
DocsApiSection,
|
|
15
|
+
DocsApiTable,
|
|
16
|
+
DocsPage,
|
|
17
|
+
DocsPageHeader,
|
|
18
|
+
DocsSection,
|
|
19
|
+
DocsSectionGrid,
|
|
20
|
+
DocsSimpleTable,
|
|
21
|
+
DocsSubsection,
|
|
22
|
+
} from "@/components/docs/docs-page";
|
|
23
|
+
import { cn } from "@/lib/cn";
|
|
24
|
+
|
|
25
|
+
const DEMO_STEPS: Step[] = [
|
|
26
|
+
{ title: "Verification", description: "Identity and document check", icon: ShieldCheck },
|
|
27
|
+
{ title: "Inventory", description: "Product stock allocation", icon: Box },
|
|
28
|
+
{ title: "Shipping", description: "Logistics and dispatching", icon: Truck },
|
|
29
|
+
{ title: "Delivery", description: "Final destination arrival", icon: CheckCircle2 },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const PLAYGROUND_STEPS: Step[] = [
|
|
33
|
+
{ title: "Account" },
|
|
34
|
+
{ title: "Profile" },
|
|
35
|
+
{ title: "Payment" },
|
|
36
|
+
{ title: "Confirm" },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
type StyleKey = "horizontal" | "vertical" | "progress" | "dot" | "segmented";
|
|
40
|
+
|
|
41
|
+
const STYLES: { value: StyleKey; label: string; component: React.ComponentType<{ steps: Step[]; currentStep: number; className?: string }>; importName: string }[] = [
|
|
42
|
+
{ value: "horizontal", label: "Horizontal", component: HorizontalStepper, importName: "HorizontalStepper" },
|
|
43
|
+
{ value: "vertical", label: "Vertical", component: VerticalStepper, importName: "VerticalStepper" },
|
|
44
|
+
{ value: "progress", label: "Progress", component: ProgressStepper, importName: "ProgressStepper" },
|
|
45
|
+
{ value: "dot", label: "Dot", component: DotStepper, importName: "DotStepper" },
|
|
46
|
+
{ value: "segmented", label: "Segmented", component: SegmentedStepper, importName: "SegmentedStepper" },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
function codeFor(importName: string, currentStep: number) {
|
|
50
|
+
const stepsSrc = PLAYGROUND_STEPS.map((s) => ` { title: "${s.title}" },`).join("\n");
|
|
51
|
+
return `import { ${importName} } from "@/components/ui/steppers";\n\nconst steps = [\n${stepsSrc}\n];\n\n<${importName} steps={steps} currentStep={${currentStep}} />`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const API_ROWS = [
|
|
55
|
+
{ prop: "steps", type: "Step[]", default: "—", description: "Ordered list of steps, each with a title, optional description, and icon." },
|
|
56
|
+
{ prop: "currentStep", type: "number", default: "—", description: "Zero-based index of the active step." },
|
|
57
|
+
{ prop: "onStepClick", type: "(step: number) => void", default: "—", description: "Called with the clicked index. Only wired up by InteractiveStepper." },
|
|
58
|
+
{ prop: "className", type: "string", default: "—", description: "Extra classes applied to the stepper's root element." },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const ACCESSIBILITY_ITEMS = [
|
|
62
|
+
"Wrap the stepper in a <nav> landmark for assistive technology.",
|
|
63
|
+
"aria-current=\"step\" is applied to the active indicator automatically.",
|
|
64
|
+
"Interactive steps have clearly defined focus-visible rings for keyboard users.",
|
|
65
|
+
"Provide text alternatives for any icons used within the steps.",
|
|
66
|
+
"Contrast ratios between background and text meet 4.5:1 standards in both themes.",
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
function useStepper(length: number, initial = 0) {
|
|
70
|
+
const [step, setStep] = useState(Math.min(initial, length - 1));
|
|
71
|
+
return {
|
|
72
|
+
step,
|
|
73
|
+
setStep,
|
|
74
|
+
next: () => setStep((s) => Math.min(length - 1, s + 1)),
|
|
75
|
+
prev: () => setStep((s) => Math.max(0, s - 1)),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function StepperControls({
|
|
80
|
+
step,
|
|
81
|
+
total,
|
|
82
|
+
onPrev,
|
|
83
|
+
onNext,
|
|
84
|
+
}: {
|
|
85
|
+
step: number;
|
|
86
|
+
total: number;
|
|
87
|
+
onPrev: () => void;
|
|
88
|
+
onNext: () => void;
|
|
89
|
+
}) {
|
|
90
|
+
return (
|
|
91
|
+
<div className="mt-6 flex items-center justify-between border-t border-zinc-100 pt-4 dark:border-zinc-800">
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
onClick={onPrev}
|
|
95
|
+
disabled={step === 0}
|
|
96
|
+
className="flex h-8 w-8 items-center justify-center rounded-lg border border-zinc-200 text-zinc-600 transition-colors hover:bg-zinc-50 disabled:opacity-30 disabled:hover:bg-transparent dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
|
97
|
+
aria-label="Previous step"
|
|
98
|
+
>
|
|
99
|
+
<ChevronLeft size={16} />
|
|
100
|
+
</button>
|
|
101
|
+
<span className="text-[11px] font-semibold uppercase tracking-wide text-zinc-400 dark:text-zinc-500">
|
|
102
|
+
Step {step + 1} of {total}
|
|
103
|
+
</span>
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
onClick={onNext}
|
|
107
|
+
disabled={step === total - 1}
|
|
108
|
+
className="flex h-8 w-8 items-center justify-center rounded-lg border border-zinc-200 text-zinc-600 transition-colors hover:bg-zinc-50 disabled:opacity-30 disabled:hover:bg-transparent dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
|
109
|
+
aria-label="Next step"
|
|
110
|
+
>
|
|
111
|
+
<ChevronRight size={16} />
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function StepperDocPage() {
|
|
118
|
+
const horizontal = useStepper(DEMO_STEPS.length, 1);
|
|
119
|
+
const vertical = useStepper(DEMO_STEPS.length, 2);
|
|
120
|
+
const progress = useStepper(DEMO_STEPS.length, 1);
|
|
121
|
+
const breadcrumbSegmented = useStepper(DEMO_STEPS.length, 2);
|
|
122
|
+
const icon = useStepper(DEMO_STEPS.length, 1);
|
|
123
|
+
const numberedDot = useStepper(DEMO_STEPS.length, 2);
|
|
124
|
+
const timeline = useStepper(3, 1);
|
|
125
|
+
const interactive = useStepper(DEMO_STEPS.length, 1);
|
|
126
|
+
|
|
127
|
+
const [style, setStyle] = useState<StyleKey>("horizontal");
|
|
128
|
+
const [currentStep, setCurrentStep] = useState(1);
|
|
129
|
+
const [copied, setCopied] = useState(false);
|
|
130
|
+
|
|
131
|
+
const active = STYLES.find((s) => s.value === style) ?? STYLES[0];
|
|
132
|
+
const ActiveStepper = active.component;
|
|
133
|
+
const snippet = codeFor(active.importName, currentStep);
|
|
134
|
+
|
|
135
|
+
function copySnippet() {
|
|
136
|
+
navigator.clipboard.writeText(snippet).catch(() => {});
|
|
137
|
+
setCopied(true);
|
|
138
|
+
setTimeout(() => setCopied(false), 1200);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<DocsPage>
|
|
143
|
+
<DocsPageHeader
|
|
144
|
+
icon={Layers}
|
|
145
|
+
title="Stepper"
|
|
146
|
+
description="A collection of 10 stepper variants with a minimalist black & white aesthetic, a success-green accent for completed steps, and smooth Framer Motion animations. Each demo below keeps its own state — driving one never affects another."
|
|
147
|
+
/>
|
|
148
|
+
|
|
149
|
+
<DocsSection
|
|
150
|
+
first
|
|
151
|
+
title="Playground"
|
|
152
|
+
description="Switch between stepper styles and step through the flow — live, with the exact markup to match."
|
|
153
|
+
>
|
|
154
|
+
<div className="overflow-hidden rounded-2xl border border-zinc-200 shadow-[0_2px_8px_rgba(0,0,0,0.04)] dark:border-zinc-800 dark:shadow-[0_2px_8px_rgba(0,0,0,0.2)]">
|
|
155
|
+
<div className="relative flex min-h-[12rem] items-center justify-center overflow-hidden bg-zinc-50 p-6 dark:bg-zinc-900/40">
|
|
156
|
+
<div
|
|
157
|
+
aria-hidden
|
|
158
|
+
className="pointer-events-none absolute inset-0 opacity-[0.4] dark:opacity-[0.15]"
|
|
159
|
+
style={{
|
|
160
|
+
backgroundImage:
|
|
161
|
+
"linear-gradient(to right, rgb(161 161 170 / 0.35) 1px, transparent 1px), linear-gradient(to bottom, rgb(161 161 170 / 0.35) 1px, transparent 1px)",
|
|
162
|
+
backgroundSize: "24px 24px",
|
|
163
|
+
}}
|
|
164
|
+
/>
|
|
165
|
+
<div
|
|
166
|
+
key={`${style}-${currentStep}`}
|
|
167
|
+
className="relative w-full max-w-sm animate-[pagination-pop_0.2s_ease-out]"
|
|
168
|
+
>
|
|
169
|
+
<ActiveStepper steps={PLAYGROUND_STEPS} currentStep={currentStep} />
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div className="flex flex-col gap-5 border-t border-zinc-200 p-5 dark:border-zinc-800">
|
|
174
|
+
<div className="flex flex-wrap items-start justify-between gap-6">
|
|
175
|
+
<div>
|
|
176
|
+
<p className="mb-2 text-xs font-medium uppercase tracking-wide text-zinc-400 dark:text-zinc-500">
|
|
177
|
+
Style
|
|
178
|
+
</p>
|
|
179
|
+
<div className="flex flex-wrap items-center gap-1 rounded-lg border border-zinc-200 bg-zinc-50 p-1 dark:border-zinc-800 dark:bg-zinc-900/60">
|
|
180
|
+
{STYLES.map((s) => (
|
|
181
|
+
<button
|
|
182
|
+
key={s.value}
|
|
183
|
+
type="button"
|
|
184
|
+
onClick={() => setStyle(s.value)}
|
|
185
|
+
className={cn(
|
|
186
|
+
"rounded-md px-3 py-1.5 text-xs font-medium uppercase tracking-wide transition-colors",
|
|
187
|
+
style === s.value
|
|
188
|
+
? "bg-white text-zinc-900 shadow-sm dark:bg-zinc-800 dark:text-white"
|
|
189
|
+
: "text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200"
|
|
190
|
+
)}
|
|
191
|
+
>
|
|
192
|
+
{s.label}
|
|
193
|
+
</button>
|
|
194
|
+
))}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div>
|
|
199
|
+
<p className="mb-2 text-xs font-medium uppercase tracking-wide text-zinc-400 dark:text-zinc-500">
|
|
200
|
+
Current step
|
|
201
|
+
</p>
|
|
202
|
+
<div className="flex items-center gap-2">
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
onClick={() => setCurrentStep((s) => Math.max(0, s - 1))}
|
|
206
|
+
disabled={currentStep === 0}
|
|
207
|
+
className="flex h-8 w-8 items-center justify-center rounded-lg border border-zinc-200 text-zinc-600 transition-colors hover:bg-zinc-50 disabled:opacity-30 disabled:hover:bg-transparent dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
|
208
|
+
aria-label="Previous step"
|
|
209
|
+
>
|
|
210
|
+
<ChevronLeft size={16} />
|
|
211
|
+
</button>
|
|
212
|
+
<span className="w-20 text-center text-[11px] font-semibold uppercase tracking-wide text-zinc-400 dark:text-zinc-500">
|
|
213
|
+
{currentStep + 1} of {PLAYGROUND_STEPS.length}
|
|
214
|
+
</span>
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
217
|
+
onClick={() => setCurrentStep((s) => Math.min(PLAYGROUND_STEPS.length - 1, s + 1))}
|
|
218
|
+
disabled={currentStep === PLAYGROUND_STEPS.length - 1}
|
|
219
|
+
className="flex h-8 w-8 items-center justify-center rounded-lg border border-zinc-200 text-zinc-600 transition-colors hover:bg-zinc-50 disabled:opacity-30 disabled:hover:bg-transparent dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
|
220
|
+
aria-label="Next step"
|
|
221
|
+
>
|
|
222
|
+
<ChevronRight size={16} />
|
|
223
|
+
</button>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<div className="flex items-center justify-between gap-3 rounded-xl bg-zinc-900 px-4 py-3 dark:bg-black">
|
|
229
|
+
<pre className="overflow-x-auto font-mono text-[13px] leading-relaxed text-zinc-100">
|
|
230
|
+
<code>{snippet}</code>
|
|
231
|
+
</pre>
|
|
232
|
+
<Tooltip content={copied ? "Copied!" : "Copy code"}>
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
onClick={copySnippet}
|
|
236
|
+
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-zinc-400 transition-colors hover:bg-white/10 hover:text-white"
|
|
237
|
+
>
|
|
238
|
+
{copied ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />}
|
|
239
|
+
</button>
|
|
240
|
+
</Tooltip>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
</DocsSection>
|
|
245
|
+
|
|
246
|
+
<DocsSection title="Standard Flows" description="Common patterns for linear progression.">
|
|
247
|
+
<DocsSectionGrid>
|
|
248
|
+
<DocsSubsection title="Horizontal" description="A classic top-of-form progress rail.">
|
|
249
|
+
<Card className="flex h-full flex-col justify-between p-8">
|
|
250
|
+
<div className="flex flex-1 items-center justify-center">
|
|
251
|
+
<HorizontalStepper steps={DEMO_STEPS} currentStep={horizontal.step} />
|
|
252
|
+
</div>
|
|
253
|
+
<StepperControls step={horizontal.step} total={DEMO_STEPS.length} onPrev={horizontal.prev} onNext={horizontal.next} />
|
|
254
|
+
</Card>
|
|
255
|
+
</DocsSubsection>
|
|
256
|
+
<DocsSubsection title="Vertical" description="Best for detailed, multi-line steps.">
|
|
257
|
+
<Card className="flex h-full flex-col justify-between p-8">
|
|
258
|
+
<VerticalStepper steps={DEMO_STEPS} currentStep={vertical.step} />
|
|
259
|
+
<StepperControls step={vertical.step} total={DEMO_STEPS.length} onPrev={vertical.prev} onNext={vertical.next} />
|
|
260
|
+
</Card>
|
|
261
|
+
</DocsSubsection>
|
|
262
|
+
</DocsSectionGrid>
|
|
263
|
+
</DocsSection>
|
|
264
|
+
|
|
265
|
+
<DocsSection title="Metrics & Segments" description="Visualizing data-driven completion states.">
|
|
266
|
+
<DocsSectionGrid>
|
|
267
|
+
<DocsSubsection title="Progress" description="Turns green once the flow reaches 100%.">
|
|
268
|
+
<Card className="flex h-full flex-col justify-between p-8">
|
|
269
|
+
<div className="flex flex-1 items-center">
|
|
270
|
+
<ProgressStepper steps={DEMO_STEPS} currentStep={progress.step} />
|
|
271
|
+
</div>
|
|
272
|
+
<StepperControls step={progress.step} total={DEMO_STEPS.length} onPrev={progress.prev} onNext={progress.next} />
|
|
273
|
+
</Card>
|
|
274
|
+
</DocsSubsection>
|
|
275
|
+
<DocsSubsection title="Breadcrumb & Segmented" description="Compact bars for dashboards and toolbars.">
|
|
276
|
+
<Card className="flex h-full flex-col justify-between p-8">
|
|
277
|
+
<div className="flex flex-1 flex-col justify-center gap-8">
|
|
278
|
+
<BreadcrumbStepper steps={DEMO_STEPS} currentStep={breadcrumbSegmented.step} />
|
|
279
|
+
<SegmentedStepper steps={DEMO_STEPS} currentStep={breadcrumbSegmented.step} />
|
|
280
|
+
</div>
|
|
281
|
+
<StepperControls step={breadcrumbSegmented.step} total={DEMO_STEPS.length} onPrev={breadcrumbSegmented.prev} onNext={breadcrumbSegmented.next} />
|
|
282
|
+
</Card>
|
|
283
|
+
</DocsSubsection>
|
|
284
|
+
</DocsSectionGrid>
|
|
285
|
+
</DocsSection>
|
|
286
|
+
|
|
287
|
+
<DocsSection title="Deep Navigation" description="Extended history and interactive control styles.">
|
|
288
|
+
<DocsSectionGrid>
|
|
289
|
+
<DocsSubsection title="Timeline" description="An activity-log style history view.">
|
|
290
|
+
<Card className="flex h-full flex-col justify-between p-8">
|
|
291
|
+
<TimelineStepper steps={DEMO_STEPS.slice(0, 3)} currentStep={timeline.step} />
|
|
292
|
+
<StepperControls step={timeline.step} total={3} onPrev={timeline.prev} onNext={timeline.next} />
|
|
293
|
+
</Card>
|
|
294
|
+
</DocsSubsection>
|
|
295
|
+
<DocsSubsection title="Interactive" description="Click any card to jump directly to that step.">
|
|
296
|
+
<Card className="flex h-full items-center justify-center border-dashed border-2 bg-zinc-50/50 p-8 dark:bg-zinc-900/30">
|
|
297
|
+
<InteractiveStepper steps={DEMO_STEPS} currentStep={interactive.step} onStepClick={interactive.setStep} />
|
|
298
|
+
</Card>
|
|
299
|
+
</DocsSubsection>
|
|
300
|
+
</DocsSectionGrid>
|
|
301
|
+
</DocsSection>
|
|
302
|
+
|
|
303
|
+
<DocsApiSection>
|
|
304
|
+
<DocsApiTable title="API Reference" rows={API_ROWS} />
|
|
305
|
+
<DocsAccessibility items={ACCESSIBILITY_ITEMS} />
|
|
306
|
+
</DocsApiSection>
|
|
307
|
+
</DocsPage>
|
|
308
|
+
);
|
|
309
|
+
}
|