ruler-components 0.1.1 → 0.1.2
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/dist/index.d.mts +51 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +534 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +498 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +21 -2
- package/AGENTS.md +0 -9
- package/CLAUDE.md +0 -1
- package/app/docs/[slug]/page.tsx +0 -147
- package/app/docs/layout.tsx +0 -26
- package/app/docs/page.tsx +0 -148
- package/app/favicon.ico +0 -0
- package/app/globals.css +0 -168
- package/app/icon.png +0 -0
- package/app/layout.tsx +0 -90
- package/app/opengraph-image.tsx +0 -10
- package/app/page.tsx +0 -34
- package/app/twitter-image.tsx +0 -10
- package/components/docs/accordion-doc-page.tsx +0 -352
- package/components/docs/alert-doc-page.tsx +0 -293
- package/components/docs/allinput-doc-page.tsx +0 -183
- package/components/docs/avatar-doc-page.tsx +0 -424
- package/components/docs/badge-doc-page.tsx +0 -363
- package/components/docs/button-doc-page.tsx +0 -406
- package/components/docs/card-doc-page.tsx +0 -364
- package/components/docs/checkbox-doc-page.tsx +0 -277
- package/components/docs/colors-doc-page.tsx +0 -430
- package/components/docs/dialog-doc-page.tsx +0 -364
- package/components/docs/docs-footer.tsx +0 -11
- package/components/docs/docs-mobile-menu-bar.tsx +0 -22
- package/components/docs/docs-mobile-nav-context.tsx +0 -27
- package/components/docs/docs-page.tsx +0 -263
- package/components/docs/docs-search.tsx +0 -303
- package/components/docs/docs-sidebar.tsx +0 -165
- package/components/docs/docs-topbar.tsx +0 -32
- package/components/docs/dropdown-doc-page.tsx +0 -522
- package/components/docs/getting-started-doc-page.tsx +0 -205
- package/components/docs/label-doc-page.tsx +0 -173
- package/components/docs/loaders-doc-page.tsx +0 -244
- package/components/docs/menu-doc-page.tsx +0 -373
- package/components/docs/modal-doc-page.tsx +0 -436
- package/components/docs/next-steps.tsx +0 -53
- package/components/docs/pagination-doc-page.tsx +0 -316
- package/components/docs/progress-doc-page.tsx +0 -387
- package/components/docs/radio-doc-page.tsx +0 -270
- package/components/docs/skeleton-doc-page.tsx +0 -230
- package/components/docs/stepper-doc-page.tsx +0 -309
- package/components/docs/switch-doc-page.tsx +0 -264
- package/components/docs/table-doc-page.tsx +0 -689
- package/components/docs/tabs-doc-page.tsx +0 -359
- package/components/docs/theming-doc-page.tsx +0 -243
- package/components/docs/tooltip-doc-page.tsx +0 -357
- package/components/docs/usage-section.tsx +0 -21
- package/components/icons/discord.tsx +0 -9
- package/components/icons/github.tsx +0 -9
- package/components/icons/x.tsx +0 -9
- package/components/installation-section.tsx +0 -34
- package/components/landing/feature-highlights.tsx +0 -105
- package/components/landing/footer-strip.tsx +0 -45
- package/components/landing/hero-card.tsx +0 -14
- package/components/landing/hero-spotlight.tsx +0 -126
- package/components/landing/hero.tsx +0 -64
- package/components/landing/navbar.tsx +0 -44
- package/components/landing/quick-start-section.tsx +0 -59
- package/components/landing/site-footer.tsx +0 -55
- package/components/theme-provider.tsx +0 -60
- package/components/ui/accordion.tsx +0 -174
- package/components/ui/alert.tsx +0 -103
- package/components/ui/allinput.tsx +0 -210
- package/components/ui/avatar.tsx +0 -179
- package/components/ui/badge.tsx +0 -52
- package/components/ui/button.tsx +0 -75
- package/components/ui/card.tsx +0 -105
- package/components/ui/checkbox.tsx +0 -130
- package/components/ui/code-block.tsx +0 -60
- package/components/ui/data-table.tsx +0 -235
- package/components/ui/dialog.tsx +0 -173
- package/components/ui/dropdown.tsx +0 -78
- package/components/ui/feature-card.tsx +0 -38
- package/components/ui/input.tsx +0 -44
- package/components/ui/label.tsx +0 -110
- package/components/ui/loaders.tsx +0 -139
- package/components/ui/menu.tsx +0 -118
- package/components/ui/modal.tsx +0 -399
- package/components/ui/package-manager-tabs.tsx +0 -40
- package/components/ui/pagination.tsx +0 -200
- package/components/ui/progress.tsx +0 -134
- package/components/ui/radio.tsx +0 -262
- package/components/ui/skeleton.tsx +0 -102
- package/components/ui/steppers.tsx +0 -309
- package/components/ui/switch.tsx +0 -153
- package/components/ui/table.tsx +0 -58
- package/components/ui/tabs.tsx +0 -166
- package/components/ui/theme-toggle.tsx +0 -60
- package/components/ui/tooltip.tsx +0 -238
- package/data/color-palette.ts +0 -35
- package/data/docs-nav.ts +0 -84
- package/eslint.config.mjs +0 -18
- package/index.ts +0 -8
- package/lib/cn.ts +0 -4
- package/lib/color.ts +0 -85
- package/lib/og-image.tsx +0 -138
- package/lib/theme.ts +0 -33
- package/next.config.ts +0 -7
- package/postcss.config.mjs +0 -7
- package/public/dark.png +0 -0
- package/public/dark1.png +0 -0
- package/public/dark2.png +0 -0
- package/public/file.svg +0 -1
- package/public/globe.svg +0 -1
- package/public/light.png +0 -0
- package/public/light1.png +0 -0
- package/public/next.svg +0 -1
- package/public/vercel.svg +0 -1
- package/public/video/hero-demo.mp4 +0 -0
- package/public/window.svg +0 -1
- package/tsconfig.json +0 -34
|
@@ -1,263 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,303 +0,0 @@
|
|
|
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 "{query}"
|
|
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
|
-
}
|