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.
Files changed (114) hide show
  1. package/AGENTS.md +9 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +21 -0
  4. package/README.md +45 -0
  5. package/app/docs/[slug]/page.tsx +147 -0
  6. package/app/docs/layout.tsx +26 -0
  7. package/app/docs/page.tsx +148 -0
  8. package/app/favicon.ico +0 -0
  9. package/app/globals.css +168 -0
  10. package/app/icon.png +0 -0
  11. package/app/layout.tsx +90 -0
  12. package/app/opengraph-image.tsx +10 -0
  13. package/app/page.tsx +34 -0
  14. package/app/twitter-image.tsx +10 -0
  15. package/components/docs/accordion-doc-page.tsx +352 -0
  16. package/components/docs/alert-doc-page.tsx +293 -0
  17. package/components/docs/allinput-doc-page.tsx +183 -0
  18. package/components/docs/avatar-doc-page.tsx +424 -0
  19. package/components/docs/badge-doc-page.tsx +363 -0
  20. package/components/docs/button-doc-page.tsx +406 -0
  21. package/components/docs/card-doc-page.tsx +364 -0
  22. package/components/docs/checkbox-doc-page.tsx +277 -0
  23. package/components/docs/colors-doc-page.tsx +430 -0
  24. package/components/docs/dialog-doc-page.tsx +364 -0
  25. package/components/docs/docs-footer.tsx +11 -0
  26. package/components/docs/docs-mobile-menu-bar.tsx +22 -0
  27. package/components/docs/docs-mobile-nav-context.tsx +27 -0
  28. package/components/docs/docs-page.tsx +263 -0
  29. package/components/docs/docs-search.tsx +303 -0
  30. package/components/docs/docs-sidebar.tsx +165 -0
  31. package/components/docs/docs-topbar.tsx +32 -0
  32. package/components/docs/dropdown-doc-page.tsx +522 -0
  33. package/components/docs/getting-started-doc-page.tsx +205 -0
  34. package/components/docs/label-doc-page.tsx +173 -0
  35. package/components/docs/loaders-doc-page.tsx +244 -0
  36. package/components/docs/menu-doc-page.tsx +373 -0
  37. package/components/docs/modal-doc-page.tsx +436 -0
  38. package/components/docs/next-steps.tsx +53 -0
  39. package/components/docs/pagination-doc-page.tsx +316 -0
  40. package/components/docs/progress-doc-page.tsx +387 -0
  41. package/components/docs/radio-doc-page.tsx +270 -0
  42. package/components/docs/skeleton-doc-page.tsx +230 -0
  43. package/components/docs/stepper-doc-page.tsx +309 -0
  44. package/components/docs/switch-doc-page.tsx +264 -0
  45. package/components/docs/table-doc-page.tsx +689 -0
  46. package/components/docs/tabs-doc-page.tsx +359 -0
  47. package/components/docs/theming-doc-page.tsx +243 -0
  48. package/components/docs/tooltip-doc-page.tsx +357 -0
  49. package/components/docs/usage-section.tsx +21 -0
  50. package/components/icons/discord.tsx +9 -0
  51. package/components/icons/github.tsx +9 -0
  52. package/components/icons/x.tsx +9 -0
  53. package/components/installation-section.tsx +34 -0
  54. package/components/landing/feature-highlights.tsx +105 -0
  55. package/components/landing/footer-strip.tsx +45 -0
  56. package/components/landing/hero-card.tsx +14 -0
  57. package/components/landing/hero-spotlight.tsx +126 -0
  58. package/components/landing/hero.tsx +64 -0
  59. package/components/landing/navbar.tsx +44 -0
  60. package/components/landing/quick-start-section.tsx +59 -0
  61. package/components/landing/site-footer.tsx +55 -0
  62. package/components/theme-provider.tsx +60 -0
  63. package/components/ui/accordion.tsx +174 -0
  64. package/components/ui/alert.tsx +103 -0
  65. package/components/ui/allinput.tsx +210 -0
  66. package/components/ui/avatar.tsx +179 -0
  67. package/components/ui/badge.tsx +52 -0
  68. package/components/ui/button.tsx +75 -0
  69. package/components/ui/card.tsx +105 -0
  70. package/components/ui/checkbox.tsx +130 -0
  71. package/components/ui/code-block.tsx +60 -0
  72. package/components/ui/data-table.tsx +235 -0
  73. package/components/ui/dialog.tsx +173 -0
  74. package/components/ui/dropdown.tsx +78 -0
  75. package/components/ui/feature-card.tsx +38 -0
  76. package/components/ui/input.tsx +44 -0
  77. package/components/ui/label.tsx +110 -0
  78. package/components/ui/loaders.tsx +139 -0
  79. package/components/ui/menu.tsx +118 -0
  80. package/components/ui/modal.tsx +399 -0
  81. package/components/ui/package-manager-tabs.tsx +40 -0
  82. package/components/ui/pagination.tsx +200 -0
  83. package/components/ui/progress.tsx +134 -0
  84. package/components/ui/radio.tsx +262 -0
  85. package/components/ui/skeleton.tsx +102 -0
  86. package/components/ui/steppers.tsx +309 -0
  87. package/components/ui/switch.tsx +153 -0
  88. package/components/ui/table.tsx +58 -0
  89. package/components/ui/tabs.tsx +166 -0
  90. package/components/ui/theme-toggle.tsx +60 -0
  91. package/components/ui/tooltip.tsx +238 -0
  92. package/data/color-palette.ts +35 -0
  93. package/data/docs-nav.ts +84 -0
  94. package/eslint.config.mjs +18 -0
  95. package/index.ts +8 -0
  96. package/lib/cn.ts +4 -0
  97. package/lib/color.ts +85 -0
  98. package/lib/og-image.tsx +138 -0
  99. package/lib/theme.ts +33 -0
  100. package/next.config.ts +7 -0
  101. package/package.json +30 -0
  102. package/postcss.config.mjs +7 -0
  103. package/public/dark.png +0 -0
  104. package/public/dark1.png +0 -0
  105. package/public/dark2.png +0 -0
  106. package/public/file.svg +1 -0
  107. package/public/globe.svg +1 -0
  108. package/public/light.png +0 -0
  109. package/public/light1.png +0 -0
  110. package/public/next.svg +1 -0
  111. package/public/vercel.svg +1 -0
  112. package/public/video/hero-demo.mp4 +0 -0
  113. package/public/window.svg +1 -0
  114. package/tsconfig.json +34 -0
@@ -0,0 +1,263 @@
1
+ import * as React from "react";
2
+ import { CheckCircle2, type LucideIcon } from "lucide-react";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ export function DocsPage({
6
+ children,
7
+ className,
8
+ wide,
9
+ }: React.HTMLAttributes<HTMLElement> & { wide?: boolean }) {
10
+ return (
11
+ <article
12
+ className={cn(
13
+ "mx-auto w-full min-w-0 pb-20",
14
+ wide ? "max-w-5xl xl:max-w-[1600px] 2xl:max-w-[2200px]" : "max-w-5xl",
15
+ className
16
+ )}
17
+ >
18
+ {children}
19
+ </article>
20
+ );
21
+ }
22
+
23
+ export function DocsPageHeader({
24
+ icon: Icon,
25
+ title,
26
+ description,
27
+ }: {
28
+ icon: LucideIcon;
29
+ title: string;
30
+ description: string;
31
+ }) {
32
+ return (
33
+ <header>
34
+ <div className="flex items-center gap-4">
35
+ <span className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl border border-zinc-200 bg-zinc-50 text-zinc-900 dark:border-white/10 dark:bg-white/[0.06] dark:text-zinc-100">
36
+ <Icon className="h-6 w-6" />
37
+ </span>
38
+ <h1 className="min-w-0 text-2xl font-bold tracking-tight text-zinc-900 sm:text-3xl dark:text-white">{title}</h1>
39
+ </div>
40
+ <p className="mt-4 max-w-2xl text-base leading-relaxed text-zinc-500 dark:text-zinc-400">
41
+ {description}
42
+ </p>
43
+ </header>
44
+ );
45
+ }
46
+
47
+ export function DocsSection({
48
+ title,
49
+ description,
50
+ first,
51
+ children,
52
+ className,
53
+ contentClassName,
54
+ }: {
55
+ title?: string;
56
+ description?: string;
57
+ first?: boolean;
58
+ children: React.ReactNode;
59
+ className?: string;
60
+ contentClassName?: string;
61
+ }) {
62
+ const hasHeader = title || description;
63
+
64
+ return (
65
+ <section className={cn(first ? "mt-10" : "mt-16", className)}>
66
+ {title && (
67
+ <h2 className="text-xl font-semibold tracking-tight text-zinc-900 dark:text-white">
68
+ {title}
69
+ </h2>
70
+ )}
71
+ {description && (
72
+ <p className="mt-1 text-sm leading-relaxed text-zinc-500 dark:text-zinc-400">
73
+ {description}
74
+ </p>
75
+ )}
76
+ <div className={cn(hasHeader && "mt-6", contentClassName)}>{children}</div>
77
+ </section>
78
+ );
79
+ }
80
+
81
+ export function DocsSubsection({
82
+ title,
83
+ description,
84
+ children,
85
+ className,
86
+ }: {
87
+ title?: string;
88
+ description?: string;
89
+ children: React.ReactNode;
90
+ className?: string;
91
+ }) {
92
+ const hasHeader = title || description;
93
+
94
+ return (
95
+ <div className={cn("flex flex-col", className)}>
96
+ {title && (
97
+ <h3 className="text-lg font-semibold tracking-tight text-zinc-900 dark:text-white">
98
+ {title}
99
+ </h3>
100
+ )}
101
+ {description && (
102
+ <p className="mt-1 text-sm leading-relaxed text-zinc-500 dark:text-zinc-400">
103
+ {description}
104
+ </p>
105
+ )}
106
+ <div className={cn("flex-1", hasHeader && "mt-4")}>{children}</div>
107
+ </div>
108
+ );
109
+ }
110
+
111
+ export function DocsSectionGrid({
112
+ children,
113
+ className,
114
+ }: {
115
+ children: React.ReactNode;
116
+ className?: string;
117
+ }) {
118
+ return <div className={cn("grid gap-6 sm:grid-cols-2", className)}>{children}</div>;
119
+ }
120
+
121
+ export function DocsApiSection({ children }: { children: React.ReactNode }) {
122
+ return <section className="mt-16 flex flex-col gap-12">{children}</section>;
123
+ }
124
+
125
+ export interface DocsApiRow {
126
+ prop: string;
127
+ type: string;
128
+ default: string;
129
+ description: string;
130
+ }
131
+
132
+ export function DocsApiTable({ title, rows }: { title: string; rows: DocsApiRow[] }) {
133
+ return (
134
+ <div className="w-full">
135
+ <h2 className="text-xl font-semibold tracking-tight text-zinc-900 dark:text-white">{title}</h2>
136
+ <div className="mt-3 overflow-x-auto rounded-xl 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)]">
137
+ <table className="w-full text-left text-sm">
138
+ <thead>
139
+ <tr className="border-b border-zinc-200 bg-zinc-50/70 dark:border-zinc-800 dark:bg-zinc-900/40">
140
+ <th className="px-3 py-2.5 font-medium text-zinc-500 dark:text-zinc-400">Prop</th>
141
+ <th className="px-3 py-2.5 font-medium text-zinc-500 dark:text-zinc-400">Type</th>
142
+ <th className="px-3 py-2.5 font-medium text-zinc-500 dark:text-zinc-400">Default</th>
143
+ <th className="px-3 py-2.5 font-medium text-zinc-500 dark:text-zinc-400">Description</th>
144
+ </tr>
145
+ </thead>
146
+ <tbody>
147
+ {rows.map((row) => (
148
+ <tr
149
+ key={row.prop}
150
+ className="border-b border-zinc-200 transition-colors last:border-0 hover:bg-zinc-50/70 dark:border-zinc-800 dark:hover:bg-zinc-900/40"
151
+ >
152
+ <td className="px-3 py-2.5 font-mono text-xs text-zinc-900 dark:text-white">
153
+ {row.prop}
154
+ </td>
155
+ <td className="px-3 py-2.5 font-mono text-xs text-zinc-500 dark:text-zinc-400">
156
+ {row.type}
157
+ </td>
158
+ <td className="px-3 py-2.5 font-mono text-xs text-zinc-500 dark:text-zinc-400">
159
+ {row.default}
160
+ </td>
161
+ <td className="px-3 py-2.5 text-zinc-600 dark:text-zinc-300">{row.description}</td>
162
+ </tr>
163
+ ))}
164
+ </tbody>
165
+ </table>
166
+ </div>
167
+ </div>
168
+ );
169
+ }
170
+
171
+ export function DocsSimpleTable({
172
+ title,
173
+ rows,
174
+ nameHeader = "Component",
175
+ }: {
176
+ title: string;
177
+ rows: { name: string; description: string }[];
178
+ nameHeader?: string;
179
+ }) {
180
+ return (
181
+ <div className="w-full">
182
+ <h2 className="text-xl font-semibold tracking-tight text-zinc-900 dark:text-white">{title}</h2>
183
+ <div className="mt-3 overflow-x-auto rounded-xl 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)]">
184
+ <table className="w-full text-left text-sm">
185
+ <thead>
186
+ <tr className="border-b border-zinc-200 bg-zinc-50/70 dark:border-zinc-800 dark:bg-zinc-900/40">
187
+ <th className="px-3 py-2.5 font-medium text-zinc-500 dark:text-zinc-400">{nameHeader}</th>
188
+ <th className="px-3 py-2.5 font-medium text-zinc-500 dark:text-zinc-400">Description</th>
189
+ </tr>
190
+ </thead>
191
+ <tbody>
192
+ {rows.map((row) => (
193
+ <tr
194
+ key={row.name}
195
+ className="border-b border-zinc-200 transition-colors last:border-0 hover:bg-zinc-50/70 dark:border-zinc-800 dark:hover:bg-zinc-900/40"
196
+ >
197
+ <td className="px-3 py-2.5 font-mono text-xs text-zinc-900 dark:text-white">
198
+ {row.name}
199
+ </td>
200
+ <td className="px-3 py-2.5 text-zinc-600 dark:text-zinc-300">{row.description}</td>
201
+ </tr>
202
+ ))}
203
+ </tbody>
204
+ </table>
205
+ </div>
206
+ </div>
207
+ );
208
+ }
209
+
210
+ export function DocsAccessibility({ items }: { items: string[] }) {
211
+ return (
212
+ <div className="w-full">
213
+ <h2 className="text-xl font-semibold tracking-tight text-zinc-900 dark:text-white">
214
+ Accessibility
215
+ </h2>
216
+ <ul className="mt-3 flex flex-col gap-2">
217
+ {items.map((item) => (
218
+ <li
219
+ key={item}
220
+ className="flex items-start gap-2 text-sm leading-relaxed text-zinc-600 dark:text-zinc-300"
221
+ >
222
+ <CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-green-500" />
223
+ {item}
224
+ </li>
225
+ ))}
226
+ </ul>
227
+ </div>
228
+ );
229
+ }
230
+
231
+ export function DocsDemoPanel({
232
+ children,
233
+ className,
234
+ dark = false,
235
+ }: {
236
+ children: React.ReactNode;
237
+ className?: string;
238
+ dark?: boolean;
239
+ }) {
240
+ return (
241
+ <div
242
+ className={cn(
243
+ "relative overflow-hidden rounded-2xl border shadow-[0_2px_8px_rgba(0,0,0,0.04)]",
244
+ dark
245
+ ? "border-zinc-800 bg-zinc-950"
246
+ : "border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900/40 dark:shadow-[0_2px_8px_rgba(0,0,0,0.2)]",
247
+ className,
248
+ )}
249
+ >
250
+ <div
251
+ className="pointer-events-none absolute inset-0 opacity-[0.35] dark:opacity-[0.12]"
252
+ style={{
253
+ backgroundImage:
254
+ "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)",
255
+ backgroundSize: "24px 24px",
256
+ }}
257
+ />
258
+ <div className="relative flex flex-wrap items-center justify-center gap-3 px-6 py-14">
259
+ {children}
260
+ </div>
261
+ </div>
262
+ );
263
+ }
@@ -0,0 +1,303 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { useRouter } from "next/navigation";
6
+ import { Search, X, CornerDownLeft, SearchX } from "lucide-react";
7
+ import { DOCS_NAV, type DocsNavItem } from "@/data/docs-nav";
8
+ import { cn } from "@/lib/cn";
9
+
10
+ interface FlatItem extends DocsNavItem {
11
+ group: string;
12
+ href: string;
13
+ }
14
+
15
+ const FLAT_ITEMS: FlatItem[] = DOCS_NAV.flatMap((group) =>
16
+ group.items.map((item) => ({
17
+ ...item,
18
+ group: group.title,
19
+ href: `/docs${item.slug ? `/${item.slug}` : ""}`,
20
+ }))
21
+ );
22
+
23
+ function normalize(value: string) {
24
+ return value.trim().toLowerCase().replace(/s$/, "");
25
+ }
26
+
27
+ function matchScore(query: string, item: FlatItem) {
28
+ const title = normalize(item.title);
29
+ const slug = normalize(item.slug);
30
+
31
+ if (title === query) return 100;
32
+ if (title.startsWith(query)) return 80;
33
+ if (slug.startsWith(query)) return 70;
34
+ if (title.includes(query) || query.includes(title)) return 50;
35
+ if (slug.includes(query) || query.includes(slug)) return 40;
36
+ return 0;
37
+ }
38
+
39
+ function HighlightedTitle({ title, query }: { title: string; query: string }) {
40
+ const q = query.trim();
41
+ if (!q) return <>{title}</>;
42
+
43
+ const idx = title.toLowerCase().indexOf(q.toLowerCase());
44
+ if (idx === -1) return <>{title}</>;
45
+
46
+ return (
47
+ <>
48
+ {title.slice(0, idx)}
49
+ <span className="rounded-[3px] bg-zinc-900/10 dark:bg-white/15">
50
+ {title.slice(idx, idx + q.length)}
51
+ </span>
52
+ {title.slice(idx + q.length)}
53
+ </>
54
+ );
55
+ }
56
+
57
+ export function DocsSearch() {
58
+ const [open, setOpen] = useState(false);
59
+ const [mounted, setMounted] = useState(false);
60
+ const [visible, setVisible] = useState(false);
61
+ const [query, setQuery] = useState("");
62
+ const [activeIndex, setActiveIndex] = useState(0);
63
+ const inputRef = useRef<HTMLInputElement>(null);
64
+ const router = useRouter();
65
+
66
+ useEffect(() => setMounted(true), []);
67
+
68
+ const results = useMemo(() => {
69
+ const q = normalize(query);
70
+ if (!q) return FLAT_ITEMS;
71
+
72
+ return FLAT_ITEMS.map((item) => ({ item, score: matchScore(q, item) }))
73
+ .filter(({ score }) => score > 0)
74
+ .sort((a, b) => b.score - a.score)
75
+ .map(({ item }) => item);
76
+ }, [query]);
77
+
78
+ const groupedResults = useMemo(() => {
79
+ const groups = new Map<string, { item: FlatItem; index: number }[]>();
80
+ results.forEach((item, index) => {
81
+ const bucket = groups.get(item.group) ?? [];
82
+ bucket.push({ item, index });
83
+ groups.set(item.group, bucket);
84
+ });
85
+ return Array.from(groups.entries());
86
+ }, [results]);
87
+
88
+ useEffect(() => {
89
+ setActiveIndex(0);
90
+ }, [query]);
91
+
92
+ useEffect(() => {
93
+ function handleShortcut(e: KeyboardEvent) {
94
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
95
+ e.preventDefault();
96
+ setOpen((prev) => !prev);
97
+ }
98
+ }
99
+ document.addEventListener("keydown", handleShortcut);
100
+ return () => document.removeEventListener("keydown", handleShortcut);
101
+ }, []);
102
+
103
+ useEffect(() => {
104
+ if (!open) {
105
+ setVisible(false);
106
+ document.body.style.overflow = "";
107
+ return;
108
+ }
109
+
110
+ setQuery("");
111
+ setActiveIndex(0);
112
+ document.body.style.overflow = "hidden";
113
+ const frame = requestAnimationFrame(() => setVisible(true));
114
+ const timer = setTimeout(() => inputRef.current?.focus(), 10);
115
+
116
+ return () => {
117
+ cancelAnimationFrame(frame);
118
+ clearTimeout(timer);
119
+ document.body.style.overflow = "";
120
+ };
121
+ }, [open]);
122
+
123
+ useEffect(() => {
124
+ if (!open) return;
125
+ function handleKeyDown(e: KeyboardEvent) {
126
+ if (e.key === "Escape") setOpen(false);
127
+ }
128
+ window.addEventListener("keydown", handleKeyDown);
129
+ return () => window.removeEventListener("keydown", handleKeyDown);
130
+ }, [open]);
131
+
132
+ function go(item: FlatItem) {
133
+ router.push(item.href);
134
+ setOpen(false);
135
+ }
136
+
137
+ function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
138
+ if (e.key === "ArrowDown") {
139
+ e.preventDefault();
140
+ setActiveIndex((i) => Math.min(i + 1, results.length - 1));
141
+ } else if (e.key === "ArrowUp") {
142
+ e.preventDefault();
143
+ setActiveIndex((i) => Math.max(i - 1, 0));
144
+ } else if (e.key === "Enter") {
145
+ e.preventDefault();
146
+ const item = results[activeIndex];
147
+ if (item) go(item);
148
+ }
149
+ }
150
+
151
+ return (
152
+ <>
153
+ <button
154
+ type="button"
155
+ onClick={() => setOpen(true)}
156
+ className="flex w-full min-w-0 max-w-md items-center gap-2 rounded-lg border border-zinc-200 bg-zinc-50/80 px-2.5 py-2 text-sm text-zinc-400 shadow-[0_1px_2px_rgba(0,0,0,0.03)] transition-all duration-200 hover:border-zinc-300 hover:bg-white hover:shadow-[0_2px_8px_rgba(0,0,0,0.06)] dark:border-zinc-800 dark:bg-zinc-900/60 dark:text-zinc-500 dark:hover:border-zinc-700 dark:hover:bg-zinc-900 sm:gap-2.5 sm:px-3"
157
+ >
158
+ <Search className="h-4 w-4 shrink-0" />
159
+ <span className="min-w-0 flex-1 truncate text-left">Search documentation...</span>
160
+ <kbd className="hidden shrink-0 rounded-md border border-zinc-300 bg-white px-1.5 py-0.5 font-mono text-[10px] font-medium text-zinc-500 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-500 sm:inline-flex">
161
+ ⌘K
162
+ </kbd>
163
+ </button>
164
+
165
+ {mounted && open && createPortal(
166
+ <div className="fixed inset-0 z-[100] isolate flex items-start justify-center p-4 pt-[12vh]">
167
+ <div
168
+ aria-hidden
169
+ onClick={() => setOpen(false)}
170
+ className={cn(
171
+ "absolute inset-0 bg-black/20 transition-opacity duration-200 ease-out dark:bg-black/50",
172
+ visible ? "opacity-100" : "opacity-0"
173
+ )}
174
+ />
175
+ <div
176
+ aria-hidden
177
+ className={cn(
178
+ "pointer-events-none absolute inset-x-0 top-0 h-16 bg-white/10 backdrop-blur-[2px] backdrop-saturate-150 transition-opacity duration-200 ease-out dark:bg-black/10",
179
+ visible ? "opacity-100" : "opacity-0"
180
+ )}
181
+ />
182
+
183
+ <div
184
+ role="dialog"
185
+ aria-modal="true"
186
+ onClick={(e) => e.stopPropagation()}
187
+ className={cn(
188
+ "relative z-10 flex w-full max-w-xl flex-col overflow-hidden rounded-2xl border border-zinc-200/90 bg-white shadow-[0_20px_60px_rgba(0,0,0,0.12),0_0_0_1px_rgba(0,0,0,0.04)] transition-[opacity,transform] duration-200 ease-out dark:border-zinc-800 dark:bg-zinc-950 dark:shadow-[0_20px_60px_rgba(0,0,0,0.45),0_0_0_1px_rgba(255,255,255,0.06)]",
189
+ visible ? "opacity-100 scale-100" : "opacity-0 scale-[0.97]"
190
+ )}
191
+ >
192
+ <div className="flex items-center gap-3 border-b border-zinc-200 px-4 dark:border-zinc-800">
193
+ <Search className="h-4 w-4 shrink-0 text-zinc-400 dark:text-zinc-500" />
194
+ <input
195
+ ref={inputRef}
196
+ value={query}
197
+ onChange={(e) => setQuery(e.target.value)}
198
+ onKeyDown={handleKeyDown}
199
+ placeholder="Search documentation..."
200
+ className="h-14 w-full bg-transparent text-base text-zinc-900 outline-none placeholder:text-zinc-400 dark:text-zinc-100 dark:placeholder:text-zinc-500"
201
+ />
202
+ <button
203
+ type="button"
204
+ onClick={() => setOpen(false)}
205
+ className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-700 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
206
+ >
207
+ <X className="h-3.5 w-3.5" />
208
+ </button>
209
+ </div>
210
+
211
+ <div className="docs-scrollbar max-h-[22rem] overflow-y-auto p-2">
212
+ {results.length === 0 ? (
213
+ <div className="flex flex-col items-center gap-2 py-12 text-center">
214
+ <span className="flex h-11 w-11 items-center justify-center rounded-full bg-zinc-100 dark:bg-zinc-900">
215
+ <SearchX className="h-5 w-5 text-zinc-400 dark:text-zinc-600" />
216
+ </span>
217
+ <p className="mt-1 text-sm font-medium text-zinc-700 dark:text-zinc-300">
218
+ No results for &quot;{query}&quot;
219
+ </p>
220
+ <p className="text-xs text-zinc-400 dark:text-zinc-600">
221
+ Try a different name, e.g. Button, Dialog, Stepper.
222
+ </p>
223
+ </div>
224
+ ) : (
225
+ groupedResults.map(([groupName, entries]) => (
226
+ <div key={groupName} className="mb-1 last:mb-0">
227
+ <p className="px-3 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wide text-zinc-400 dark:text-zinc-600">
228
+ {groupName}
229
+ </p>
230
+ {entries.map(({ item, index }) => {
231
+ const Icon = item.icon;
232
+ const active = index === activeIndex;
233
+ return (
234
+ <button
235
+ key={`${item.group}-${item.slug}`}
236
+ type="button"
237
+ onClick={() => go(item)}
238
+ onMouseEnter={() => setActiveIndex(index)}
239
+ className={cn(
240
+ "flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm transition-colors",
241
+ active
242
+ ? "bg-zinc-100 text-zinc-900 dark:bg-zinc-800/80 dark:text-white"
243
+ : "text-zinc-600 dark:text-zinc-400"
244
+ )}
245
+ >
246
+ <span
247
+ className={cn(
248
+ "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border transition-colors",
249
+ active
250
+ ? "border-zinc-300 bg-white text-zinc-900 dark:border-zinc-700 dark:bg-zinc-900 dark:text-white"
251
+ : "border-zinc-200 bg-zinc-50 text-zinc-500 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-500"
252
+ )}
253
+ >
254
+ <Icon className="h-3.5 w-3.5" />
255
+ </span>
256
+ <span className="flex-1 truncate font-medium">
257
+ <HighlightedTitle title={item.title} query={query} />
258
+ </span>
259
+ {active && (
260
+ <span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-zinc-200/70 text-zinc-500 dark:bg-zinc-700/60 dark:text-zinc-400">
261
+ <CornerDownLeft className="h-3 w-3" />
262
+ </span>
263
+ )}
264
+ </button>
265
+ );
266
+ })}
267
+ </div>
268
+ ))
269
+ )}
270
+ </div>
271
+
272
+ <div className="flex items-center justify-between border-t border-zinc-200 px-4 py-2.5 text-[11px] text-zinc-400 dark:border-zinc-800 dark:text-zinc-600">
273
+ <span>
274
+ {results.length} result{results.length === 1 ? "" : "s"}
275
+ </span>
276
+ <span className="flex items-center gap-3">
277
+ <span className="flex items-center gap-1">
278
+ <kbd className="rounded border border-zinc-300 px-1 py-0.5 font-mono dark:border-zinc-700">
279
+ ↑↓
280
+ </kbd>
281
+ navigate
282
+ </span>
283
+ <span className="flex items-center gap-1">
284
+ <kbd className="rounded border border-zinc-300 px-1 py-0.5 font-mono dark:border-zinc-700">
285
+
286
+ </kbd>
287
+ select
288
+ </span>
289
+ <span className="flex items-center gap-1">
290
+ <kbd className="rounded border border-zinc-300 px-1 py-0.5 font-mono dark:border-zinc-700">
291
+ esc
292
+ </kbd>
293
+ close
294
+ </span>
295
+ </span>
296
+ </div>
297
+ </div>
298
+ </div>,
299
+ document.body
300
+ )}
301
+ </>
302
+ );
303
+ }