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,205 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Compass } from "lucide-react";
|
|
5
|
+
import { Accordion, AccordionItem } from "@/components/ui/accordion";
|
|
6
|
+
import { Badge } from "@/components/ui/badge";
|
|
7
|
+
import { CodeBlock } from "@/components/ui/code-block";
|
|
8
|
+
import { PackageManagerTabs } from "@/components/ui/package-manager-tabs";
|
|
9
|
+
import { DocsPage, DocsPageHeader, DocsSection } from "@/components/docs/docs-page";
|
|
10
|
+
import { NextSteps } from "@/components/docs/next-steps";
|
|
11
|
+
import { cn } from "@/lib/cn";
|
|
12
|
+
|
|
13
|
+
const PREREQUISITES = ["Node.js 18+", "React 18+", "Tailwind CSS 4", "TypeScript (recommended)"];
|
|
14
|
+
|
|
15
|
+
const FRAMEWORKS = [
|
|
16
|
+
{
|
|
17
|
+
id: "next",
|
|
18
|
+
label: "Next.js",
|
|
19
|
+
create: "npx create-next-app@latest my-app --typescript",
|
|
20
|
+
cssFile: "app/globals.css",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "vite",
|
|
24
|
+
label: "Vite",
|
|
25
|
+
create: "npm create vite@latest my-app -- --template react-ts",
|
|
26
|
+
cssFile: "src/index.css",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "cra",
|
|
30
|
+
label: "Create React App",
|
|
31
|
+
create: "npx create-react-app my-app --template typescript",
|
|
32
|
+
cssFile: "src/index.css",
|
|
33
|
+
},
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
const THEME_CSS = (cssFile: string) => `/* ${cssFile} */
|
|
37
|
+
@import "tailwindcss";
|
|
38
|
+
|
|
39
|
+
:root {
|
|
40
|
+
--background: #ffffff;
|
|
41
|
+
--foreground: #09090b;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.dark {
|
|
45
|
+
--background: #09090b;
|
|
46
|
+
--foreground: #fafafa;
|
|
47
|
+
}`;
|
|
48
|
+
|
|
49
|
+
const APP_CODE = `import { Button } from 'ruler.ui'
|
|
50
|
+
|
|
51
|
+
export function App() {
|
|
52
|
+
return <Button variant="primary">Get Started</Button>
|
|
53
|
+
}`;
|
|
54
|
+
|
|
55
|
+
function StepNumber({ index }: { index: number }) {
|
|
56
|
+
return (
|
|
57
|
+
<span className="absolute -left-12 flex h-8 w-8 items-center justify-center rounded-full bg-zinc-900 text-sm font-semibold text-white dark:bg-white dark:text-zinc-900">
|
|
58
|
+
{index}
|
|
59
|
+
</span>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function GettingStartedDocPage() {
|
|
64
|
+
const [framework, setFramework] = useState<(typeof FRAMEWORKS)[number]>(FRAMEWORKS[0]);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<DocsPage>
|
|
68
|
+
<DocsPageHeader
|
|
69
|
+
icon={Compass}
|
|
70
|
+
title="Getting Started"
|
|
71
|
+
description="A complete walkthrough — from a blank project to your first themed component."
|
|
72
|
+
/>
|
|
73
|
+
|
|
74
|
+
<DocsSection first title="Prerequisites" description="What you'll need before you start.">
|
|
75
|
+
<div className="flex flex-wrap gap-2">
|
|
76
|
+
{PREREQUISITES.map((item) => (
|
|
77
|
+
<Badge key={item} variant="outline">
|
|
78
|
+
{item}
|
|
79
|
+
</Badge>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
</DocsSection>
|
|
83
|
+
|
|
84
|
+
<DocsSection
|
|
85
|
+
title="Choose your framework"
|
|
86
|
+
description="Pick whichever you're starting from — the rest of this guide adjusts automatically."
|
|
87
|
+
>
|
|
88
|
+
<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 sm:w-fit">
|
|
89
|
+
{FRAMEWORKS.map((f) => (
|
|
90
|
+
<button
|
|
91
|
+
key={f.id}
|
|
92
|
+
type="button"
|
|
93
|
+
onClick={() => setFramework(f)}
|
|
94
|
+
className={cn(
|
|
95
|
+
"flex-1 rounded-md px-3.5 py-1.5 text-sm font-medium transition-colors sm:flex-none",
|
|
96
|
+
framework.id === f.id
|
|
97
|
+
? "bg-white text-zinc-900 shadow-sm dark:bg-zinc-800 dark:text-white"
|
|
98
|
+
: "text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200"
|
|
99
|
+
)}
|
|
100
|
+
>
|
|
101
|
+
{f.label}
|
|
102
|
+
</button>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
</DocsSection>
|
|
106
|
+
|
|
107
|
+
<DocsSection
|
|
108
|
+
title="Step by step"
|
|
109
|
+
description={`Setting up ruler.ui in a ${framework.label} project.`}
|
|
110
|
+
>
|
|
111
|
+
<div className="relative flex flex-col gap-10 pl-12">
|
|
112
|
+
<span className="absolute -left-[1.9375rem] top-8 bottom-8 w-px bg-zinc-200 dark:bg-zinc-800" aria-hidden />
|
|
113
|
+
|
|
114
|
+
<div className="relative">
|
|
115
|
+
<StepNumber index={1} />
|
|
116
|
+
<h3 className="text-base font-semibold text-zinc-900 dark:text-white">Create a project</h3>
|
|
117
|
+
<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
|
|
118
|
+
Skip this if you already have a {framework.label} app.
|
|
119
|
+
</p>
|
|
120
|
+
<div className="mt-3">
|
|
121
|
+
<CodeBlock code={framework.create} />
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div className="relative">
|
|
126
|
+
<StepNumber index={2} />
|
|
127
|
+
<h3 className="text-base font-semibold text-zinc-900 dark:text-white">Install ruler.ui</h3>
|
|
128
|
+
<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
|
|
129
|
+
Add the package with whichever manager you prefer.
|
|
130
|
+
</p>
|
|
131
|
+
<div className="mt-3">
|
|
132
|
+
<PackageManagerTabs />
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div className="relative">
|
|
137
|
+
<StepNumber index={3} />
|
|
138
|
+
<h3 className="text-base font-semibold text-zinc-900 dark:text-white">Add the theme variables</h3>
|
|
139
|
+
<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
|
|
140
|
+
Two CSS variables drive every component — light values on{" "}
|
|
141
|
+
<code className="font-mono text-xs">:root</code>, dark values under{" "}
|
|
142
|
+
<code className="font-mono text-xs">.dark</code>.
|
|
143
|
+
</p>
|
|
144
|
+
<div className="mt-3">
|
|
145
|
+
<CodeBlock code={THEME_CSS(framework.cssFile)} />
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div className="relative">
|
|
150
|
+
<StepNumber index={4} />
|
|
151
|
+
<h3 className="text-base font-semibold text-zinc-900 dark:text-white">Render your first component</h3>
|
|
152
|
+
<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
|
|
153
|
+
Import from <code className="font-mono text-xs">ruler.ui</code> like any other package.
|
|
154
|
+
</p>
|
|
155
|
+
<div className="mt-3 overflow-hidden rounded-2xl border border-zinc-200/50 bg-zinc-950 shadow-2xl dark:border-zinc-800/60">
|
|
156
|
+
<CodeBlock code={APP_CODE} filename="App.tsx" />
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div className="relative">
|
|
161
|
+
<StepNumber index={5} />
|
|
162
|
+
<h3 className="text-base font-semibold text-zinc-900 dark:text-white">Wire up dark mode</h3>
|
|
163
|
+
<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
|
|
164
|
+
ruler.ui doesn't ship a theme provider — toggle the{" "}
|
|
165
|
+
<code className="font-mono text-xs">.dark</code> class on{" "}
|
|
166
|
+
<code className="font-mono text-xs"><html></code> however fits your app (a button,{" "}
|
|
167
|
+
<code className="font-mono text-xs">next-themes</code>, a stored cookie). See{" "}
|
|
168
|
+
<a href="/docs/theming" className="font-medium text-zinc-900 underline underline-offset-2 dark:text-white">
|
|
169
|
+
Theming
|
|
170
|
+
</a>{" "}
|
|
171
|
+
for the full picture.
|
|
172
|
+
</p>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</DocsSection>
|
|
176
|
+
|
|
177
|
+
<DocsSection title="Troubleshooting" description="Common issues when wiring things up for the first time.">
|
|
178
|
+
<div className="rounded-2xl border border-zinc-200 p-2 shadow-[0_1px_2px_rgba(0,0,0,0.03)] dark:border-zinc-800">
|
|
179
|
+
<Accordion defaultValue="1">
|
|
180
|
+
<AccordionItem value="1" title="Styles aren't applying to components">
|
|
181
|
+
Make sure your Tailwind content globs pick up ruler.ui's source — components ship as plain
|
|
182
|
+
TypeScript/JSX, so Tailwind needs to scan{" "}
|
|
183
|
+
<code className="font-mono text-xs">node_modules/ruler.ui/**/*.{"{js,ts,jsx,tsx}"}</code> (or the
|
|
184
|
+
equivalent for your Tailwind version) alongside your own app files.
|
|
185
|
+
</AccordionItem>
|
|
186
|
+
<AccordionItem value="2" title="Dark mode doesn't switch">
|
|
187
|
+
Confirm something is actually toggling the <code className="font-mono text-xs">.dark</code> class on{" "}
|
|
188
|
+
<code className="font-mono text-xs"><html></code> — ruler.ui reads that class but never sets it
|
|
189
|
+
itself, by design.
|
|
190
|
+
</AccordionItem>
|
|
191
|
+
<AccordionItem value="3" title="TypeScript can't find component types">
|
|
192
|
+
ruler.ui ships its own <code className="font-mono text-xs">.d.ts</code> files. If imports resolve but
|
|
193
|
+
types don't, check that <code className="font-mono text-xs">moduleResolution</code> in your{" "}
|
|
194
|
+
<code className="font-mono text-xs">tsconfig.json</code> is set to{" "}
|
|
195
|
+
<code className="font-mono text-xs">"bundler"</code> or <code className="font-mono text-xs">"node16"</code>{" "}
|
|
196
|
+
or newer.
|
|
197
|
+
</AccordionItem>
|
|
198
|
+
</Accordion>
|
|
199
|
+
</div>
|
|
200
|
+
</DocsSection>
|
|
201
|
+
|
|
202
|
+
<NextSteps />
|
|
203
|
+
</DocsPage>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Tag } from "lucide-react";
|
|
3
|
+
import { Field } from "@/components/ui/label";
|
|
4
|
+
import { Input } from "@/components/ui/input";
|
|
5
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import {
|
|
8
|
+
Card,
|
|
9
|
+
CardContent,
|
|
10
|
+
CardDescription,
|
|
11
|
+
CardFooter,
|
|
12
|
+
CardHeader,
|
|
13
|
+
CardTitle,
|
|
14
|
+
} from "@/components/ui/card";
|
|
15
|
+
import {
|
|
16
|
+
DocsAccessibility,
|
|
17
|
+
DocsApiSection,
|
|
18
|
+
DocsApiTable,
|
|
19
|
+
DocsPage,
|
|
20
|
+
DocsPageHeader,
|
|
21
|
+
DocsSection,
|
|
22
|
+
DocsSectionGrid,
|
|
23
|
+
} from "@/components/docs/docs-page";
|
|
24
|
+
|
|
25
|
+
const API_ROWS = [
|
|
26
|
+
{ prop: "htmlFor", type: "string", default: "—", description: "Associates the label with a form control" },
|
|
27
|
+
{ prop: "required", type: "boolean", default: "false", description: "Shows a required asterisk indicator" },
|
|
28
|
+
{ prop: "optional", type: "boolean", default: "false", description: "Shows an optional tag on the right" },
|
|
29
|
+
{ prop: "hint", type: "string", default: "—", description: "Helper text displayed below the label" },
|
|
30
|
+
{ prop: "error", type: "string", default: "—", description: "Error message displayed below the label" },
|
|
31
|
+
{ prop: "variant", type: '"default" | "muted"', default: '"default"', description: "The visual style of the label" },
|
|
32
|
+
{ prop: "size", type: '"sm" | "md" | "lg"', default: '"md"', description: "The size of the label text" },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const FIELD_API_ROWS = [
|
|
36
|
+
{ prop: "label", type: "string", default: "—", description: "Label text for the field" },
|
|
37
|
+
{ prop: "htmlFor", type: "string", default: "—", description: "ID of the associated input" },
|
|
38
|
+
{ prop: "required", type: "boolean", default: "false", description: "Marks the field as required" },
|
|
39
|
+
{ prop: "optional", type: "boolean", default: "false", description: "Marks the field as optional" },
|
|
40
|
+
{ prop: "hint", type: "string", default: "—", description: "Helper text below the label" },
|
|
41
|
+
{ prop: "error", type: "string", default: "—", description: "Validation error message" },
|
|
42
|
+
{ prop: "children", type: "ReactNode", default: "—", description: "The form control element" },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const ACCESSIBILITY_ITEMS = [
|
|
46
|
+
"Always associate labels with form controls using htmlFor.",
|
|
47
|
+
"Don't use placeholder text as a substitute for labels.",
|
|
48
|
+
"Required fields should be indicated visually and programmatically.",
|
|
49
|
+
"Error messages should use role=\"alert\" for screen readers.",
|
|
50
|
+
"Clicking a label should focus or activate the associated control.",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export function LabelDocPage() {
|
|
54
|
+
return (
|
|
55
|
+
<DocsPage>
|
|
56
|
+
<DocsPageHeader
|
|
57
|
+
icon={Tag}
|
|
58
|
+
title="Label"
|
|
59
|
+
description="Gives every input a clear, accessible name — with built-in support for hints, errors, and required states."
|
|
60
|
+
/>
|
|
61
|
+
|
|
62
|
+
<DocsSection
|
|
63
|
+
first
|
|
64
|
+
title="Contact Form"
|
|
65
|
+
description="A real-world form using the Field helper for consistent spacing and hierarchy."
|
|
66
|
+
>
|
|
67
|
+
<div className="max-w-md">
|
|
68
|
+
<Card>
|
|
69
|
+
<CardHeader>
|
|
70
|
+
<CardTitle>Get in touch</CardTitle>
|
|
71
|
+
<CardDescription>We'll respond within one business day.</CardDescription>
|
|
72
|
+
</CardHeader>
|
|
73
|
+
<CardContent className="space-y-4 pt-1">
|
|
74
|
+
<Field label="Full name" htmlFor="contact-name" required>
|
|
75
|
+
<Input id="contact-name" placeholder="Jane Cooper" />
|
|
76
|
+
</Field>
|
|
77
|
+
<Field label="Email address" htmlFor="contact-email" required hint="We'll never share your email.">
|
|
78
|
+
<Input id="contact-email" type="email" placeholder="jane@company.com" />
|
|
79
|
+
</Field>
|
|
80
|
+
<Field label="Company" htmlFor="contact-company" optional>
|
|
81
|
+
<Input id="contact-company" placeholder="Acme Inc." />
|
|
82
|
+
</Field>
|
|
83
|
+
</CardContent>
|
|
84
|
+
<CardFooter>
|
|
85
|
+
<Button size="sm">Send message</Button>
|
|
86
|
+
</CardFooter>
|
|
87
|
+
</Card>
|
|
88
|
+
</div>
|
|
89
|
+
</DocsSection>
|
|
90
|
+
|
|
91
|
+
<DocsSection title="States" description="Required, hinted, error, and optional variants of a field.">
|
|
92
|
+
<DocsSectionGrid>
|
|
93
|
+
<Card>
|
|
94
|
+
<CardHeader>
|
|
95
|
+
<CardTitle className="text-base">Required</CardTitle>
|
|
96
|
+
<CardDescription>Marks the field with a required asterisk.</CardDescription>
|
|
97
|
+
</CardHeader>
|
|
98
|
+
<CardContent>
|
|
99
|
+
<Field label="Username" htmlFor="username-demo" required>
|
|
100
|
+
<Input id="username-demo" placeholder="johndoe" />
|
|
101
|
+
</Field>
|
|
102
|
+
</CardContent>
|
|
103
|
+
</Card>
|
|
104
|
+
<Card>
|
|
105
|
+
<CardHeader>
|
|
106
|
+
<CardTitle className="text-base">With Hint</CardTitle>
|
|
107
|
+
<CardDescription>Adds helper text below the label.</CardDescription>
|
|
108
|
+
</CardHeader>
|
|
109
|
+
<CardContent>
|
|
110
|
+
<Field label="Password" htmlFor="password-demo" hint="Use 8+ characters with a number and symbol.">
|
|
111
|
+
<Input id="password-demo" type="password" placeholder="••••••••" />
|
|
112
|
+
</Field>
|
|
113
|
+
</CardContent>
|
|
114
|
+
</Card>
|
|
115
|
+
<Card>
|
|
116
|
+
<CardHeader>
|
|
117
|
+
<CardTitle className="text-base">Error State</CardTitle>
|
|
118
|
+
<CardDescription>Surfaces a validation message below the field.</CardDescription>
|
|
119
|
+
</CardHeader>
|
|
120
|
+
<CardContent>
|
|
121
|
+
<Field label="Email address" htmlFor="email-error" error="Please enter a valid email address.">
|
|
122
|
+
<Input id="email-error" state="error" defaultValue="not-an-email" />
|
|
123
|
+
</Field>
|
|
124
|
+
</CardContent>
|
|
125
|
+
</Card>
|
|
126
|
+
<Card>
|
|
127
|
+
<CardHeader>
|
|
128
|
+
<CardTitle className="text-base">Optional Field</CardTitle>
|
|
129
|
+
<CardDescription>Flags the field as optional with a hint.</CardDescription>
|
|
130
|
+
</CardHeader>
|
|
131
|
+
<CardContent>
|
|
132
|
+
<Field label="Phone number" htmlFor="phone-optional" optional hint="Include country code if outside the US.">
|
|
133
|
+
<Input id="phone-optional" type="tel" placeholder="+1 (555) 000-0000" />
|
|
134
|
+
</Field>
|
|
135
|
+
</CardContent>
|
|
136
|
+
</Card>
|
|
137
|
+
</DocsSectionGrid>
|
|
138
|
+
</DocsSection>
|
|
139
|
+
|
|
140
|
+
<DocsSection title="Sizes" description="Three label sizes to match the paired input.">
|
|
141
|
+
<Card>
|
|
142
|
+
<CardContent className="grid max-w-md gap-4 pt-6">
|
|
143
|
+
<Field label="Small" htmlFor="size-sm" labelSize="sm">
|
|
144
|
+
<Input id="size-sm" size="sm" />
|
|
145
|
+
</Field>
|
|
146
|
+
<Field label="Medium" htmlFor="size-md" labelSize="md">
|
|
147
|
+
<Input id="size-md" size="md" />
|
|
148
|
+
</Field>
|
|
149
|
+
<Field label="Large" htmlFor="size-lg" labelSize="lg">
|
|
150
|
+
<Input id="size-lg" size="lg" />
|
|
151
|
+
</Field>
|
|
152
|
+
</CardContent>
|
|
153
|
+
</Card>
|
|
154
|
+
</DocsSection>
|
|
155
|
+
|
|
156
|
+
<DocsSection
|
|
157
|
+
title="Standalone Label"
|
|
158
|
+
description="Use Label directly when pairing with custom controls like checkboxes."
|
|
159
|
+
>
|
|
160
|
+
<Checkbox
|
|
161
|
+
label="I agree to the terms and conditions"
|
|
162
|
+
description="You must accept before continuing."
|
|
163
|
+
/>
|
|
164
|
+
</DocsSection>
|
|
165
|
+
|
|
166
|
+
<DocsApiSection>
|
|
167
|
+
<DocsApiTable title="API Reference — Label" rows={API_ROWS} />
|
|
168
|
+
<DocsApiTable title="API Reference — Field" rows={FIELD_API_ROWS} />
|
|
169
|
+
<DocsAccessibility items={ACCESSIBILITY_ITEMS} />
|
|
170
|
+
</DocsApiSection>
|
|
171
|
+
</DocsPage>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { Check, Copy, Loader2 } from "lucide-react";
|
|
5
|
+
import * as UI from "@/components/ui/loaders";
|
|
6
|
+
import { Tooltip } from "@/components/ui/tooltip";
|
|
7
|
+
import {
|
|
8
|
+
DocsAccessibility,
|
|
9
|
+
DocsApiSection,
|
|
10
|
+
DocsDemoPanel,
|
|
11
|
+
DocsPage,
|
|
12
|
+
DocsPageHeader,
|
|
13
|
+
DocsSection,
|
|
14
|
+
DocsSimpleTable,
|
|
15
|
+
} from "@/components/docs/docs-page";
|
|
16
|
+
import { cn } from "@/lib/cn";
|
|
17
|
+
|
|
18
|
+
const PLAYGROUND_LOADERS = {
|
|
19
|
+
SpinnerGradient: UI.SpinnerGradient,
|
|
20
|
+
ProgressRing: UI.ProgressRing,
|
|
21
|
+
SpinnerDots: UI.SpinnerDots,
|
|
22
|
+
DotWave: UI.DotWave,
|
|
23
|
+
PulseLoader: UI.PulseLoader,
|
|
24
|
+
SquaresLoader: UI.SquaresLoader,
|
|
25
|
+
MorphingLoader: UI.MorphingLoader,
|
|
26
|
+
AILoader: UI.AILoader,
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
type PlaygroundLoader = keyof typeof PLAYGROUND_LOADERS;
|
|
30
|
+
|
|
31
|
+
const PLAYGROUND_STYLES: PlaygroundLoader[] = [
|
|
32
|
+
"SpinnerGradient",
|
|
33
|
+
"ProgressRing",
|
|
34
|
+
"SpinnerDots",
|
|
35
|
+
"DotWave",
|
|
36
|
+
"PulseLoader",
|
|
37
|
+
"SquaresLoader",
|
|
38
|
+
"MorphingLoader",
|
|
39
|
+
"AILoader",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function codeFor(style: PlaygroundLoader) {
|
|
43
|
+
return `import { ${style} } from "@/components/ui/loaders";\n\n<${style} />`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function LoaderSwatch({ label, children }: { label: string; children: React.ReactNode }) {
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex flex-col items-center gap-3">
|
|
49
|
+
<div className="flex h-12 items-center justify-center">{children}</div>
|
|
50
|
+
<span className="text-xs text-zinc-500 dark:text-zinc-400">{label}</span>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const LOADER_ROWS = [
|
|
56
|
+
{ name: "ProgressRing", description: "A rotating ring with a partial arc — the default spinner." },
|
|
57
|
+
{ name: "SpinnerGradient", description: "A bordered circle spinning with a solid leading edge." },
|
|
58
|
+
{ name: "SpinnerDots", description: "Eight dots fading around a circle, iOS-style." },
|
|
59
|
+
{ name: "HalfCircles", description: "Two overlapping arcs spinning at different opacities." },
|
|
60
|
+
{ name: "SquaresLoader", description: "Two squares pulsing out of phase." },
|
|
61
|
+
{ name: "LoadBar", description: "A pill-shaped track with a bar sweeping across it." },
|
|
62
|
+
{ name: "PulseLoader", description: "A dot with an expanding, fading ring around it." },
|
|
63
|
+
{ name: "MorphingLoader", description: "A shape that morphs between a square and a circle while rotating." },
|
|
64
|
+
{ name: "ThreeDots", description: "Three dots pulsing in sequence — compact inline status." },
|
|
65
|
+
{ name: "DotWave", description: "Three dots bouncing in a wave pattern." },
|
|
66
|
+
{ name: "TypingIndicator", description: "A chat-bubble-style typing indicator." },
|
|
67
|
+
{ name: "ProgressBar", description: "An indeterminate linear progress bar." },
|
|
68
|
+
{ name: "SkeletonShimmer", description: "A shimmering placeholder block for loading content." },
|
|
69
|
+
{ name: "OrbitLoader", description: "A particle orbiting a fixed center dot." },
|
|
70
|
+
{ name: "AILoader", description: "A spinning ring around a pulsing sparkle, with a status label." },
|
|
71
|
+
{ name: "BlurReveal", description: "Fades and sharpens content into view once it's ready." },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const ACCESSIBILITY_ITEMS = [
|
|
75
|
+
"Pair loaders with an aria-live region or aria-busy so screen readers announce the loading state.",
|
|
76
|
+
"Give icon-only loaders an accessible label (e.g. aria-label=\"Loading\") — the animation alone conveys nothing to assistive tech.",
|
|
77
|
+
"Avoid loaders that flash faster than 3 times per second — they can trigger photosensitive reactions.",
|
|
78
|
+
"Show a loader only after a short delay (~300ms) so fast responses don't flicker unnecessarily.",
|
|
79
|
+
"Replace the loader with the real content or an error state as soon as it's ready — never leave it spinning indefinitely.",
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
export function LoadersDocPage() {
|
|
83
|
+
const [style, setStyle] = useState<PlaygroundLoader>("SpinnerGradient");
|
|
84
|
+
const [copied, setCopied] = useState(false);
|
|
85
|
+
|
|
86
|
+
const SelectedLoader = PLAYGROUND_LOADERS[style];
|
|
87
|
+
const snippet = codeFor(style);
|
|
88
|
+
|
|
89
|
+
function copySnippet() {
|
|
90
|
+
navigator.clipboard.writeText(snippet).catch(() => {});
|
|
91
|
+
setCopied(true);
|
|
92
|
+
setTimeout(() => setCopied(false), 1200);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<DocsPage>
|
|
97
|
+
<DocsPageHeader
|
|
98
|
+
icon={Loader2}
|
|
99
|
+
title="Loaders"
|
|
100
|
+
description="Loading indicators for every context — spinners, progress bars, and AI-style status animations."
|
|
101
|
+
/>
|
|
102
|
+
|
|
103
|
+
<DocsSection
|
|
104
|
+
first
|
|
105
|
+
title="Playground"
|
|
106
|
+
description="Switch between styles and copy the exact markup — live, with the animation restarting each time."
|
|
107
|
+
>
|
|
108
|
+
<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)]">
|
|
109
|
+
<div className="relative flex min-h-[11rem] items-center justify-center overflow-hidden bg-zinc-50 p-6 dark:bg-zinc-900/40">
|
|
110
|
+
<div
|
|
111
|
+
aria-hidden
|
|
112
|
+
className="pointer-events-none absolute inset-0 opacity-[0.4] dark:opacity-[0.15]"
|
|
113
|
+
style={{
|
|
114
|
+
backgroundImage:
|
|
115
|
+
"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)",
|
|
116
|
+
backgroundSize: "24px 24px",
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
<div key={style} className="relative animate-[pagination-pop_0.2s_ease-out]">
|
|
120
|
+
<SelectedLoader />
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div className="flex flex-col gap-5 border-t border-zinc-200 p-5 dark:border-zinc-800">
|
|
125
|
+
<div className="flex flex-wrap items-start justify-between gap-6">
|
|
126
|
+
<div>
|
|
127
|
+
<p className="mb-2 text-xs font-medium uppercase tracking-wide text-zinc-400 dark:text-zinc-500">
|
|
128
|
+
Style
|
|
129
|
+
</p>
|
|
130
|
+
<div className="flex flex-wrap gap-1.5">
|
|
131
|
+
{PLAYGROUND_STYLES.map((s) => (
|
|
132
|
+
<button
|
|
133
|
+
key={s}
|
|
134
|
+
type="button"
|
|
135
|
+
onClick={() => setStyle(s)}
|
|
136
|
+
className={cn(
|
|
137
|
+
"rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors",
|
|
138
|
+
style === s
|
|
139
|
+
? "border-zinc-900 bg-zinc-900 text-white dark:border-white dark:bg-white dark:text-zinc-900"
|
|
140
|
+
: "border-zinc-200 text-zinc-500 hover:border-zinc-300 dark:border-zinc-700 dark:text-zinc-400 dark:hover:border-zinc-600"
|
|
141
|
+
)}
|
|
142
|
+
>
|
|
143
|
+
{s}
|
|
144
|
+
</button>
|
|
145
|
+
))}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div className="flex items-center justify-between gap-3 rounded-xl bg-zinc-900 px-4 py-3 dark:bg-black">
|
|
151
|
+
<pre className="overflow-x-auto font-mono text-[13px] leading-relaxed text-zinc-100">
|
|
152
|
+
<code>{snippet}</code>
|
|
153
|
+
</pre>
|
|
154
|
+
<Tooltip content={copied ? "Copied!" : "Copy code"}>
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
onClick={copySnippet}
|
|
158
|
+
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"
|
|
159
|
+
>
|
|
160
|
+
{copied ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />}
|
|
161
|
+
</button>
|
|
162
|
+
</Tooltip>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</DocsSection>
|
|
167
|
+
|
|
168
|
+
<DocsSection title="Circular Spinners" description="Classic rotating indicators for buttons, cards, and page loads.">
|
|
169
|
+
<DocsDemoPanel>
|
|
170
|
+
<LoaderSwatch label="Progress Ring">
|
|
171
|
+
<UI.ProgressRing />
|
|
172
|
+
</LoaderSwatch>
|
|
173
|
+
<LoaderSwatch label="Gradient Spinner">
|
|
174
|
+
<UI.SpinnerGradient />
|
|
175
|
+
</LoaderSwatch>
|
|
176
|
+
<LoaderSwatch label="Spinner Dots">
|
|
177
|
+
<UI.SpinnerDots />
|
|
178
|
+
</LoaderSwatch>
|
|
179
|
+
<LoaderSwatch label="Half Circles">
|
|
180
|
+
<UI.HalfCircles />
|
|
181
|
+
</LoaderSwatch>
|
|
182
|
+
</DocsDemoPanel>
|
|
183
|
+
</DocsSection>
|
|
184
|
+
|
|
185
|
+
<DocsSection title="Geometric & Modern" description="Shape-driven motion for a distinct, branded feel.">
|
|
186
|
+
<DocsDemoPanel>
|
|
187
|
+
<LoaderSwatch label="Squares Motion">
|
|
188
|
+
<UI.SquaresLoader />
|
|
189
|
+
</LoaderSwatch>
|
|
190
|
+
<LoaderSwatch label="Pill Load Bar">
|
|
191
|
+
<UI.LoadBar />
|
|
192
|
+
</LoaderSwatch>
|
|
193
|
+
<LoaderSwatch label="Pulse Dot">
|
|
194
|
+
<UI.PulseLoader />
|
|
195
|
+
</LoaderSwatch>
|
|
196
|
+
<LoaderSwatch label="Morphing Shape">
|
|
197
|
+
<UI.MorphingLoader />
|
|
198
|
+
</LoaderSwatch>
|
|
199
|
+
</DocsDemoPanel>
|
|
200
|
+
</DocsSection>
|
|
201
|
+
|
|
202
|
+
<DocsSection title="Status Indicators" description="Compact indicators for inline, chat, and progress contexts.">
|
|
203
|
+
<DocsDemoPanel>
|
|
204
|
+
<LoaderSwatch label="Three Dots">
|
|
205
|
+
<UI.ThreeDots />
|
|
206
|
+
</LoaderSwatch>
|
|
207
|
+
<LoaderSwatch label="Dot Wave">
|
|
208
|
+
<UI.DotWave />
|
|
209
|
+
</LoaderSwatch>
|
|
210
|
+
<LoaderSwatch label="Typing Mode">
|
|
211
|
+
<UI.TypingIndicator />
|
|
212
|
+
</LoaderSwatch>
|
|
213
|
+
<LoaderSwatch label="Linear Progress">
|
|
214
|
+
<UI.ProgressBar />
|
|
215
|
+
</LoaderSwatch>
|
|
216
|
+
</DocsDemoPanel>
|
|
217
|
+
</DocsSection>
|
|
218
|
+
|
|
219
|
+
<DocsSection title="Skeleton Placeholder" description="Shimmering blocks that mimic the shape of content while it loads.">
|
|
220
|
+
<div className="flex max-w-sm items-center gap-4 rounded-2xl border border-zinc-200/70 bg-white p-5 shadow-[0_2px_8px_rgba(0,0,0,0.06)] dark:border-zinc-800/80 dark:bg-zinc-900 dark:shadow-[0_2px_8px_rgba(0,0,0,0.25)]">
|
|
221
|
+
<UI.SkeletonShimmer className="h-12 w-12 shrink-0 rounded-full" />
|
|
222
|
+
<div className="flex flex-1 flex-col gap-2.5">
|
|
223
|
+
<UI.SkeletonShimmer className="h-3 w-3/4" />
|
|
224
|
+
<UI.SkeletonShimmer className="h-3 w-1/2" />
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</DocsSection>
|
|
228
|
+
|
|
229
|
+
<DocsSection title="Premium & AI" description="Distinct, branded loaders for premium flows and AI responses.">
|
|
230
|
+
<DocsDemoPanel>
|
|
231
|
+
<LoaderSwatch label="Orbit Particle">
|
|
232
|
+
<UI.OrbitLoader />
|
|
233
|
+
</LoaderSwatch>
|
|
234
|
+
<UI.AILoader />
|
|
235
|
+
</DocsDemoPanel>
|
|
236
|
+
</DocsSection>
|
|
237
|
+
|
|
238
|
+
<DocsApiSection>
|
|
239
|
+
<DocsSimpleTable title="Available Loaders" rows={LOADER_ROWS} />
|
|
240
|
+
<DocsAccessibility items={ACCESSIBILITY_ITEMS} />
|
|
241
|
+
</DocsApiSection>
|
|
242
|
+
</DocsPage>
|
|
243
|
+
);
|
|
244
|
+
}
|