Skip to content

Commit 2cb7904

Browse files
Add docs command menu and harden accessibility across docs and buttons.
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5fd0216 commit 2cb7904

20 files changed

Lines changed: 1076 additions & 97 deletions

app/error.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,26 @@ import { useEffect } from "react";
55

66
export default function Error({
77
error,
8+
reset,
89
unstable_retry,
910
}: {
1011
error: Error & { digest?: string };
12+
reset: () => void;
1113
unstable_retry: () => void;
1214
}) {
1315
useEffect(() => {
1416
console.error(error);
1517
}, [error]);
1618

19+
function retry() {
20+
// Prefer Next 16.2+ server re-fetch retry; fall back to client reset.
21+
if (typeof unstable_retry === "function") {
22+
unstable_retry();
23+
return;
24+
}
25+
reset();
26+
}
27+
1728
return (
1829
<main className="flex min-h-0 flex-1 flex-col items-center justify-center gap-6 bg-background px-6 py-16 text-center text-foreground">
1930
<div className="space-y-3">
@@ -37,7 +48,7 @@ export default function Error({
3748
<div className="flex flex-wrap items-center justify-center gap-2">
3849
<button
3950
type="button"
40-
onClick={() => unstable_retry()}
51+
onClick={retry}
4152
className="inline-flex h-9 items-center justify-center bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
4253
>
4354
Try again

app/global-error.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"use client"; // Global error boundaries must be Client Components
2+
3+
import Link from "next/link";
4+
import { useEffect } from "react";
5+
6+
export default function GlobalError({
7+
error,
8+
reset,
9+
unstable_retry,
10+
}: {
11+
error: Error & { digest?: string };
12+
reset: () => void;
13+
unstable_retry: () => void;
14+
}) {
15+
useEffect(() => {
16+
console.error(error);
17+
}, [error]);
18+
19+
function retry() {
20+
if (typeof unstable_retry === "function") {
21+
unstable_retry();
22+
return;
23+
}
24+
reset();
25+
}
26+
27+
return (
28+
<html lang="en">
29+
<body className="flex min-h-dvh flex-col items-center justify-center gap-6 bg-background px-6 py-16 text-center text-foreground">
30+
<div className="space-y-3">
31+
<p className="font-mono text-xs uppercase tracking-[0.25em] text-muted-foreground">
32+
Something broke
33+
</p>
34+
<h1 className="text-4xl font-black tracking-tighter">
35+
An evil error appeared.
36+
</h1>
37+
<p className="mx-auto max-w-md text-sm text-muted-foreground">
38+
Something went wrong at the root of the app. You can try again, or
39+
head back to safety.
40+
</p>
41+
{error.digest ? (
42+
<p className="font-mono text-[11px] text-muted-foreground/70">
43+
Error ID: {error.digest}
44+
</p>
45+
) : null}
46+
</div>
47+
48+
<div className="flex flex-wrap items-center justify-center gap-2">
49+
<button
50+
type="button"
51+
onClick={retry}
52+
className="inline-flex h-9 items-center justify-center bg-foreground px-4 text-sm font-medium text-background"
53+
>
54+
Try again
55+
</button>
56+
<Link
57+
href="/"
58+
className="inline-flex h-9 items-center justify-center border border-border px-4 text-sm font-medium"
59+
>
60+
Go home
61+
</Link>
62+
</div>
63+
</body>
64+
</html>
65+
);
66+
}

app/layout.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Geist, Geist_Mono, Inter, Doto } from "next/font/google";
2+
import type { Metadata } from "next";
23
import "./globals.css";
34
import { JsonLd } from "@/components/seo/json-ld";
45
import { ThemeProvider } from "@/components/theme-provider";
@@ -29,7 +30,18 @@ const dotoVar = Doto({
2930
variable: "--font-doto",
3031
});
3132

32-
export const metadata = rootMetadata;
33+
// Keep rootMetadata as the SEO source of truth; restate title/description here
34+
// so the root layout exports complete document metadata without relying only on
35+
// the imported object.
36+
export const metadata: Metadata = {
37+
...rootMetadata,
38+
title: {
39+
default: "Evil Buttons — Animated shadcn/ui components with Motion",
40+
template: "%s | Evil Buttons",
41+
},
42+
description:
43+
"A shadcn/ui registry of animated buttons built With an Evil Touch. Live previews, copy-paste docs, and one-command CLI installs.",
44+
};
3345

3446
// Runs before paint so the persisted/system theme is applied without a flash of
3547
// the wrong color scheme. Kept dependency-free and rendered as the first node in
@@ -66,6 +78,12 @@ export default function RootLayout({
6678
/>
6779
<Analytics />
6880
<SpeedInsights />
81+
{/*
82+
Toast provider intentionally omitted. Copy/async feedback is already
83+
inline on controls (e.g. CopyButton aria-label + icon swap). Mount
84+
Sonner only when a real cross-app async feedback need appears — do not
85+
add an unused toaster solely for audit score.
86+
*/}
6987
<ThemeProvider>{children}</ThemeProvider>
7088
</body>
7189
</html>

components/docs-command-menu.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"use client";
2+
3+
import { FileIcon } from "@phosphor-icons/react";
4+
import { useRouter } from "next/navigation";
5+
import { useEffect, useState } from "react";
6+
import {
7+
Command,
8+
CommandDialog,
9+
CommandEmpty,
10+
CommandGroup,
11+
CommandInput,
12+
CommandItem,
13+
CommandList,
14+
} from "@/components/ui/command";
15+
16+
type DocsNavPage = {
17+
title: string;
18+
url: string;
19+
};
20+
21+
const EDITABLE_TAGS = new Set(["INPUT", "TEXTAREA", "SELECT"]);
22+
23+
function isEditableTarget(target: EventTarget | null): boolean {
24+
const element = target as HTMLElement | null;
25+
if (!element) return false;
26+
if (EDITABLE_TAGS.has(element.tagName)) return true;
27+
return element.isContentEditable;
28+
}
29+
30+
export function DocsCommandMenu({ pages }: { pages: DocsNavPage[] }) {
31+
const router = useRouter();
32+
const [open, setOpen] = useState(false);
33+
34+
useEffect(() => {
35+
const onKeyDown = (event: KeyboardEvent) => {
36+
if (event.defaultPrevented) return;
37+
if (event.isComposing) return;
38+
if (!(event.metaKey || event.ctrlKey)) return;
39+
if (event.key.toLowerCase() !== "k") return;
40+
if (isEditableTarget(event.target)) return;
41+
42+
event.preventDefault();
43+
setOpen((value) => !value);
44+
};
45+
46+
window.addEventListener("keydown", onKeyDown);
47+
return () => window.removeEventListener("keydown", onKeyDown);
48+
}, []);
49+
50+
return (
51+
<CommandDialog
52+
open={open}
53+
onOpenChange={setOpen}
54+
title="Search docs"
55+
description="Jump to a button documentation page"
56+
>
57+
<Command>
58+
<CommandInput placeholder="Search buttons…" />
59+
<CommandList>
60+
<CommandEmpty>No pages found.</CommandEmpty>
61+
<CommandGroup heading="Buttons">
62+
{pages.map((page) => (
63+
<CommandItem
64+
key={page.url}
65+
value={`${page.title} ${page.url}`}
66+
onSelect={() => {
67+
setOpen(false);
68+
router.push(page.url);
69+
}}
70+
>
71+
<FileIcon />
72+
<span>{page.title}</span>
73+
</CommandItem>
74+
))}
75+
</CommandGroup>
76+
</CommandList>
77+
</Command>
78+
</CommandDialog>
79+
);
80+
}

components/docs-shell.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Link from "next/link";
22
import Image from "next/image";
33
import type { ReactNode } from "react";
4+
import { DocsCommandMenu } from "@/components/docs-command-menu";
45
import { DocsSidebar } from "@/components/docs-sidebar";
56

67
type DocsNavPage = {
@@ -40,6 +41,7 @@ export function DocsShell({
4041
}
4142
/>
4243
<main className="docs-scroll min-w-0 flex-1 overflow-y-auto">{children}</main>
44+
<DocsCommandMenu pages={componentPages} />
4345
</div>
4446
);
4547
}

0 commit comments

Comments
 (0)