diff --git a/docs/bun.lock b/docs/bun.lock
index ab97619..aa7857e 100644
--- a/docs/bun.lock
+++ b/docs/bun.lock
@@ -6,6 +6,7 @@
"name": "docs",
"dependencies": {
"@base-ui/react": "^1.6.0",
+ "cnfast": "^0.1.0",
"fumadocs-core": "^16.14.0",
"fumadocs-mdx": "^15.2.2",
"fumadocs-ui": "npm:@fumadocs/base-ui@^16.14.0",
diff --git a/docs/package.json b/docs/package.json
index b8469c4..ed0cdcd 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -12,6 +12,7 @@
},
"dependencies": {
"@base-ui/react": "^1.6.0",
+ "cnfast": "^0.1.0",
"fumadocs-core": "^16.14.0",
"fumadocs-mdx": "^15.2.2",
"fumadocs-ui": "npm:@fumadocs/base-ui@^16.14.0",
diff --git a/docs/press.config.tsx b/docs/press.config.tsx
index 2488c64..d35876a 100644
--- a/docs/press.config.tsx
+++ b/docs/press.config.tsx
@@ -2,10 +2,13 @@ import { defineDocs } from "fumadocs-mdx/macro";
import { defineConfig } from "fumapress";
import { fumadocsMdx } from "fumapress/adapters/mdx";
import { metaSchema, pageSchema } from "fumapress/adapters/mdx/schema";
+import { createDocsLayoutPage } from "fumapress/layouts/docs";
import { linkValidationPlugin } from "fumapress/plugins/link-validation";
import { sitemapPlugin } from "fumapress/plugins/sitemap";
import { takumiPlugin } from "fumapress/plugins/takumi";
+import { version } from "../package.json";
+import { ThemeSwitch } from "./src/components/theme-switch";
import { MARKETPLACE, REPO } from "./src/lib/links";
import { MARK_VIEW_BOX, markPath } from "./src/lib/mark";
import { url } from "./src/lib/url";
@@ -37,11 +40,17 @@ const OG = {
export default defineConfig({
content: docs.toFumadocsSource(),
defaultLayoutProps: {
+ // The sidebar's bottom strip is a bordered box holding whatever icon links
+ // and the theme switch it is given. With one icon link duplicating a nav
+ // link it was a full-width border around a lone button, so both go and the
+ // switch is rendered as the sidebar's own footer below.
+ githubUrl: "",
links: [
{ text: "GitHub", url: REPO },
{ text: "Marketplace", url: MARKETPLACE },
],
nav: { title: SITE_NAME },
+ themeSwitch: { enabled: false },
},
meta: {
root() {
@@ -84,6 +93,30 @@ export default defineConfig({
},
// Deployed to a CDN as a static site, so no route may depend on a server.
mode: "static",
+ renderPage: createDocsLayoutPage({
+ renderLayout({ next, props }) {
+ return next({
+ ...props,
+ sidebar: {
+ ...props.sidebar,
+ footer: (
+
+ ),
+ },
+ });
+ },
+ }),
site: {
baseUrl: url,
git: {
diff --git a/docs/src/components/footer.tsx b/docs/src/components/footer.tsx
index fd817dc..b6b5a13 100644
--- a/docs/src/components/footer.tsx
+++ b/docs/src/components/footer.tsx
@@ -1,6 +1,5 @@
-import { ThemeSwitch } from "fumadocs-ui/layouts/shared/slots/theme-switch";
-
import { MARKETPLACE, REPO } from "../lib/links";
+import { ThemeSwitch } from "./theme-switch";
interface FooterColumn {
heading: string;
@@ -182,7 +181,7 @@ export const SiteFooter = () => (
{/* Closing the gap opened by not using Fumapress's own nav: without
this the home page had no way to change appearance. */}
-
+
diff --git a/docs/src/components/theme-switch.tsx b/docs/src/components/theme-switch.tsx
new file mode 100644
index 0000000..ec324f2
--- /dev/null
+++ b/docs/src/components/theme-switch.tsx
@@ -0,0 +1,147 @@
+"use client";
+
+import { cn } from "cnfast";
+import { useTheme } from "fumadocs-ui/provider/base";
+import type { ComponentProps } from "react";
+import { useCallback, useId, useState, useSyncExternalStore } from "react";
+import { flushSync } from "react-dom";
+
+type ThemeSwitchProps = ComponentProps<"fieldset"> & {
+ mode?: "light-dark" | "light-dark-system";
+};
+
+interface Thumb {
+ left: number;
+ width: number;
+}
+
+const THEMES = [
+ {
+ icon: {
+ d: "M2.5 5.25C2.5 3.45 3.96 2 5.75 2h4.5c1.8 0 3.25 1.46 3.25 3.25V14h-11V5.25M5.75 3.5C4.78 3.5 4 4.28 4 5.25v7.25h8V5.25c0-.97-.78-1.75-1.75-1.75zM5 5.5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1V9H5zm3.5 6H11V10H8.5z",
+ rule: "evenodd",
+ },
+ label: "System theme",
+ value: "system",
+ },
+ {
+ icon: {
+ d: "M8.75 2v-.75h-1.5V3h1.5V2M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4m0 1.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7m.75 1.5v1.75h-1.5V13zM13 7.25h1.75v1.5H13zm-11 0h-.75v1.5H3v-1.5H2m9-3.32.54-.53.17-.17.53-.53 1.06 1.06-.53.53-.17.17-.53.53zm-7.77 7.78-.53.53 1.06 1.06.53-.53.17-.17.53-.53L3.93 11l-.53.53zM3.93 5l-.53-.53-.17-.17-.53-.53L3.76 2.7l.53.53.17.17.53.53zm7.78 7.78.53.53 1.06-1.06-.53-.53-.17-.17-.53-.53L11 12.07l.53.53z",
+ rule: "evenodd",
+ },
+ label: "Light theme",
+ value: "light",
+ },
+ {
+ icon: {
+ d: "m6.3 3.3.7.25A4.25 4.25 0 0 0 12.45 9l.96.96-.08.2A5.75 5.75 0 1 1 6.04 2.6zM5.25 4.76a4.24 4.24 0 1 0 6 5.99H11a5.75 5.75 0 0 1-5.75-6M12.5 3.5h1.25V5H12.5v1.25H11V5H9.75V3.5H11V2.25h1.5zM7 3.55l-.7-.25-.26-.7z",
+ rule: "nonzero",
+ },
+ label: "Dark theme",
+ value: "dark",
+ },
+] as const;
+
+const listeners = new Set<() => void>();
+const subscribe = (listener: () => void) => {
+ listeners.add(listener);
+ return () => listeners.delete(listener);
+};
+const onClient = () => true;
+const onServer = () => false;
+
+export const ThemeSwitch = ({
+ className,
+ mode = "light-dark-system",
+ ...props
+}: ThemeSwitchProps) => {
+ const { resolvedTheme, setTheme, theme } = useTheme();
+ const hydrated = useSyncExternalStore(subscribe, onClient, onServer);
+ const [thumb, setThumb] = useState(null);
+ const name = useId();
+
+ const measure = useCallback((cell: HTMLLabelElement | null) => {
+ if (!cell) {
+ return;
+ }
+
+ const place = () =>
+ setThumb({ left: cell.offsetLeft, width: cell.offsetWidth });
+
+ place();
+ const observer = new ResizeObserver(place);
+ observer.observe(cell);
+ if (cell.parentElement) {
+ observer.observe(cell.parentElement);
+ }
+
+ return () => observer.disconnect();
+ }, []);
+
+ const themes =
+ mode === "light-dark"
+ ? THEMES.filter((option) => option.value !== "system")
+ : THEMES;
+ const selected = mode === "light-dark" ? resolvedTheme : theme;
+ const active = hydrated ? selected : null;
+
+ const change = (value: string) => {
+ if (document.startViewTransition) {
+ document.startViewTransition(() => flushSync(() => setTheme(value)));
+ } else {
+ setTheme(value);
+ }
+ };
+
+ return (
+
+ );
+};