diff --git a/docs-site/app/api/search/route.ts b/docs-site/app/api/search/route.ts new file mode 100644 index 00000000..d86bfc5b --- /dev/null +++ b/docs-site/app/api/search/route.ts @@ -0,0 +1,4 @@ +import { source } from "@/lib/source"; +import { createFromSource } from "fumadocs-core/search/server"; + +export const { GET } = createFromSource(source); diff --git a/docs-site/app/global.css b/docs-site/app/global.css index bc4ed8a4..08bd9b83 100644 --- a/docs-site/app/global.css +++ b/docs-site/app/global.css @@ -69,7 +69,11 @@ --color-fd-muted-foreground: #7a8d96; } -html, body { +/* Keep html overflow at the default `visible` so body's overflow + propagates to the viewport (per CSS Overflow spec). That lets + `react-remove-scroll-bar` lock viewport scroll via body alone while + leaving the sticky sidebar placeholder anchored to the viewport. */ +body { overflow-x: clip; } @@ -778,8 +782,8 @@ body::after { mix-blend-mode: overlay; } -/* Make sure content stays above background */ -body > * { +/* Make sure page content stays above the decorative background. */ +.ktx-site-shell { position: relative; z-index: 2; } diff --git a/docs-site/app/layout.tsx b/docs-site/app/layout.tsx index 48e12a3f..7c808130 100644 --- a/docs-site/app/layout.tsx +++ b/docs-site/app/layout.tsx @@ -41,7 +41,9 @@ export default function RootLayout({ children }: { children: ReactNode }) { suppressHydrationWarning > - {children} + +
{children}
+
); diff --git a/docs-site/tests/docs-index-route.test.mjs b/docs-site/tests/docs-index-route.test.mjs index 7d1c62c0..721813ec 100644 --- a/docs-site/tests/docs-index-route.test.mjs +++ b/docs-site/tests/docs-index-route.test.mjs @@ -111,3 +111,21 @@ test("/ktx/docs redirects to the docs introduction", async () => { `${docsBasePath}/docs/getting-started/introduction`, ); }); + +test("/ktx/api/search returns docs search results", async () => { + const response = await fetch( + `${docsSiteUrl}${docsBasePath}/api/search?query=setup`, + ); + + assert.equal(response.status, 200); + + const results = await response.json(); + assert.ok(Array.isArray(results), "search response should be an array"); + assert.ok( + results.some( + (result) => + typeof result.url === "string" && result.url.startsWith("/docs/"), + ), + "search should return at least one docs result", + ); +}); diff --git a/docs-site/tests/docs-search-behavior.test.mjs b/docs-site/tests/docs-search-behavior.test.mjs new file mode 100644 index 00000000..0a96482b --- /dev/null +++ b/docs-site/tests/docs-search-behavior.test.mjs @@ -0,0 +1,53 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; + +const docsSiteDir = join(dirname(fileURLToPath(import.meta.url)), ".."); + +async function readDocsFile(path) { + return readFile(join(docsSiteDir, path), "utf8"); +} + +test("root provider uses the base-path-aware search API", async () => { + const layout = await readDocsFile("app/layout.tsx"); + + assert.match(layout, /search=\{\{/); + assert.match(layout, /api:\s*"\/ktx\/api\/search"/); +}); + +test("site background stacking does not target every body child", async () => { + const css = await readDocsFile("app/global.css"); + + assert.doesNotMatch(css, /body\s*>\s*\*\s*\{[^}]*z-index/s); + assert.match(css, /\.ktx-site-shell\s*\{[^}]*z-index:\s*2/s); +}); + +test("search lock relies on body overflow propagation, not html or sidebar overrides", async () => { + const css = await readDocsFile("app/global.css"); + + // Body still clips horizontal overflow defensively. + assert.match(css, /(^|\s)body\s*\{[^}]*overflow-x:\s*clip/s); + + // html must keep its default `visible` overflow so body's lock + // (`overflow: hidden` from react-remove-scroll-bar) propagates to the + // viewport. Locking html directly breaks `position: sticky` on the + // sidebar placeholder. + assert.doesNotMatch(css, /(^|\s)html\s*,?\s*\{[^}]*overflow(-y|\s*:)\s*(hidden|clip)/s); + assert.doesNotMatch( + css, + /html:has\(body\[data-scroll-locked\]\)[^{]*\{[^}]*overflow:\s*(hidden|clip)/s, + ); + + // No site-specific overrides to body's data-scroll-locked overflow or + // to the sidebar placeholder when locked. + assert.doesNotMatch( + css, + /html\s+body\[data-scroll-locked\][^{]*\{[^}]*overflow:/s, + ); + assert.doesNotMatch( + css, + /body\[data-scroll-locked\]\s+\[data-sidebar-placeholder\][^{]*\{[^}]*position:\s*fixed/s, + ); +});