Skip to content

Commit 33d64d6

Browse files
authored
fix: replace default FumaDocs search component with custom search (CopilotKit#5050)
## Summary - Disabled Fumadocs search in `showcase/shell-docs` so Cmd/Ctrl+K no longer opens the built-in dialog. - Centralized the custom search modal behind a single app-level provider/event bridge so desktop and mobile triggers share one instance. - Kept the custom search button and hotkey behavior intact, including Escape to close. ## Testing - `npm run typecheck` in `showcase/shell-docs` passed. - `npm run lint` in `showcase/shell-docs` passed with pre-existing warnings only. - Verified locally in the in-app browser that Cmd+K opens one custom search modal, Escape closes it, and no Fumadocs search dialog appears.
2 parents 9df2e26 + 926a91b commit 33d64d6

2 files changed

Lines changed: 74 additions & 48 deletions

File tree

showcase/shell-docs/src/app/layout.tsx

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AnalyticsClient } from "@/components/analytics-client";
66
import { Banners } from "@/components/banners";
77
import { BrandNav } from "@/components/brand-nav";
88
import { FrameworkProvider } from "@/components/framework-provider";
9+
import { ShellSearchProvider } from "@/components/search-trigger";
910
import { PostHogProvider } from "@/lib/providers/posthog-provider";
1011
import { ScarfPixel } from "@/lib/providers/scarf-pixel";
1112
import { getIntegrations } from "@/lib/registry";
@@ -162,24 +163,27 @@ export default function RootLayout({
162163
*/}
163164
<PostHogProvider>
164165
<FrameworkProvider knownFrameworks={knownFrameworks}>
165-
{/* RootProvider supplies Fumadocs's theme provider (next-themes)
166-
* and the search-dialog context, which DocsLayout and other
167-
* fumadocs-ui components read from. We keep BrandNav + Banners
168-
* outside DocsLayout so chrome remains shell-docs's own. */}
169-
<RootProvider theme={{ enabled: true, defaultTheme: "system" }}>
170-
{/* Body is a fixed-height (100vh) flex column with hidden
171-
* overflow (see globals.css). Banner + nav sit naturally
172-
* at the top; <main> takes the remaining height and is
173-
* the horizontal flex row that hosts sidebar + the
174-
* scrolling `.docs-content-wrapper`. No sticky positioning
175-
* is needed — chrome stays put because it's outside the
176-
* scroll container. Mirrors canonical `#nd-home-layout`
177-
* (margin: 0 4px; xl: 0 8px 8px 8px). */}
178-
<Banners />
179-
<BrandNav />
180-
<main className="flex flex-1 min-h-0 overflow-hidden mx-1 md:mx-[22px] mt-2 md:mt-6 mb-2 md:mb-3">
181-
{children}
182-
</main>
166+
{/* RootProvider supplies Fumadocs's theme provider (next-themes).
167+
* Search is handled exclusively by shell-docs's SearchTrigger. */}
168+
<RootProvider
169+
theme={{ enabled: true, defaultTheme: "system" }}
170+
search={{ enabled: false }}
171+
>
172+
<ShellSearchProvider>
173+
{/* Body is a fixed-height (100vh) flex column with hidden
174+
* overflow (see globals.css). Banner + nav sit naturally
175+
* at the top; <main> takes the remaining height and is
176+
* the horizontal flex row that hosts sidebar + the
177+
* scrolling `.docs-content-wrapper`. No sticky positioning
178+
* is needed — chrome stays put because it's outside the
179+
* scroll container. Mirrors canonical `#nd-home-layout`
180+
* (margin: 0 4px; xl: 0 8px 8px 8px). */}
181+
<Banners />
182+
<BrandNav />
183+
<main className="flex flex-1 min-h-0 overflow-hidden mx-1 md:mx-[22px] mt-2 md:mt-6 mb-2 md:mb-3">
184+
{children}
185+
</main>
186+
</ShellSearchProvider>
183187
</RootProvider>
184188
</FrameworkProvider>
185189
</PostHogProvider>

showcase/shell-docs/src/components/search-trigger.tsx

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"use client";
22

3-
import { useState, useEffect } from "react";
3+
import type { ReactNode } from "react";
4+
import { useCallback, useEffect, useState } from "react";
45
import { createPortal } from "react-dom";
56
import { Command, Search } from "lucide-react";
67
import { SearchModal } from "./search-modal";
78

9+
const TOGGLE_SEARCH_EVENT = "shell-docs:toggle-search";
10+
811
function isEditableTarget(target: EventTarget | null): boolean {
912
if (!target || !(target instanceof HTMLElement)) return false;
1013
const tag = target.tagName;
@@ -13,23 +16,14 @@ function isEditableTarget(target: EventTarget | null): boolean {
1316
return false;
1417
}
1518

16-
export function SearchTrigger({
17-
iconOnly = false,
18-
}: { iconOnly?: boolean } = {}) {
19-
// Start as null so SSR output matches the initial client render; resolve
20-
// after mount to avoid hydration mismatch flashing ⌘K → Ctrl+K on non-Mac.
21-
const [isMac, setIsMac] = useState<boolean | null>(null);
19+
export function ShellSearchProvider({ children }: { children: ReactNode }) {
2220
const [open, setOpen] = useState(false);
23-
24-
useEffect(() => {
25-
const mac =
26-
typeof navigator !== "undefined" && /mac/i.test(navigator.userAgent);
27-
setIsMac(mac);
28-
}, []);
21+
const closeSearch = useCallback(() => setOpen(false), []);
22+
const toggleSearch = useCallback(() => setOpen((prev) => !prev), []);
2923

3024
useEffect(() => {
3125
function onKeyDown(e: KeyboardEvent) {
32-
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
26+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
3327
// Don't hijack Cmd/Ctrl+K when the user is typing in an unrelated
3428
// input / textarea / contenteditable — only steal the shortcut when
3529
// focus is outside an editable element or already inside our own
@@ -40,26 +34,55 @@ export function SearchTrigger({
4034
if (isEditableTarget(target) && !insideSearchModal) return;
4135

4236
e.preventDefault();
43-
setOpen((prev) => !prev);
37+
e.stopPropagation();
38+
toggleSearch();
4439
}
45-
if (e.key === "Escape") setOpen(false);
40+
if (e.key === "Escape") closeSearch();
4641
}
47-
document.addEventListener("keydown", onKeyDown);
48-
return () => document.removeEventListener("keydown", onKeyDown);
42+
43+
document.addEventListener("keydown", onKeyDown, { capture: true });
44+
window.addEventListener(TOGGLE_SEARCH_EVENT, toggleSearch);
45+
46+
return () => {
47+
document.removeEventListener("keydown", onKeyDown, { capture: true });
48+
window.removeEventListener(TOGGLE_SEARCH_EVENT, toggleSearch);
49+
};
50+
}, [closeSearch, toggleSearch]);
51+
52+
return (
53+
<>
54+
{children}
55+
{open && <SearchModalWrapper onClose={closeSearch} />}
56+
</>
57+
);
58+
}
59+
60+
function toggleShellSearch() {
61+
window.dispatchEvent(new Event(TOGGLE_SEARCH_EVENT));
62+
}
63+
64+
export function SearchTrigger({
65+
iconOnly = false,
66+
}: { iconOnly?: boolean } = {}) {
67+
// Start as null so SSR output matches the initial client render; resolve
68+
// after mount to avoid hydration mismatch flashing ⌘K → Ctrl+K on non-Mac.
69+
const [isMac, setIsMac] = useState<boolean | null>(null);
70+
71+
useEffect(() => {
72+
const mac =
73+
typeof navigator !== "undefined" && /mac/i.test(navigator.userAgent);
74+
setIsMac(mac);
4975
}, []);
5076

5177
if (iconOnly) {
5278
return (
53-
<>
54-
<button
55-
onClick={() => setOpen((prev) => !prev)}
56-
className="flex items-center justify-center w-8 h-8 rounded-md text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-[var(--bg-elevated)] transition-colors cursor-pointer"
57-
aria-label="Search"
58-
>
59-
<Search className="h-4 w-4" aria-hidden="true" />
60-
</button>
61-
{open && <SearchModalWrapper onClose={() => setOpen(false)} />}
62-
</>
79+
<button
80+
onClick={toggleShellSearch}
81+
className="flex items-center justify-center w-8 h-8 rounded-md text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-[var(--bg-elevated)] transition-colors cursor-pointer"
82+
aria-label="Search"
83+
>
84+
<Search className="h-4 w-4" aria-hidden="true" />
85+
</button>
6386
);
6487
}
6588

@@ -68,7 +91,7 @@ export function SearchTrigger({
6891
return (
6992
<>
7093
<button
71-
onClick={() => setOpen((prev) => !prev)}
94+
onClick={toggleShellSearch}
7295
aria-label="Search"
7396
className="lg:min-w-[250px] xl:min-w-[300px] flex gap-2 items-center px-3 h-10 rounded-xl cursor-pointer border border-[var(--border)] bg-[var(--bg-elevated)]/70 text-[var(--text-muted)] hover:text-[var(--text)] hover:bg-[var(--bg-surface)] transition-colors shadow-[0_1px_0_rgba(1,5,7,0.03)]"
7497
>
@@ -96,7 +119,6 @@ export function SearchTrigger({
96119
)}
97120
</span>
98121
</button>
99-
{open && <SearchModalWrapper onClose={() => setOpen(false)} />}
100122
</>
101123
);
102124
}

0 commit comments

Comments
 (0)