diff --git a/examples/astro/src/lib/docs.config.ts b/examples/astro/src/lib/docs.config.ts index 285d4295..4c74438a 100644 --- a/examples/astro/src/lib/docs.config.ts +++ b/examples/astro/src/lib/docs.config.ts @@ -82,6 +82,7 @@ export default defineDocs({ sitemap: { enabled: true, baseUrl: "https://docs.farming-labs.dev" }, lastUpdated: { enabled: true, position: "below-title" }, themeToggle: { enabled: true, default: "dark" }, + tweaks: { reader: true, author: process.env.NODE_ENV !== "production" }, breadcrumb: { enabled: true }, metadata: { titleTemplate: "%s – Docs", diff --git a/examples/next/docs.config.tsx b/examples/next/docs.config.tsx index 5de5e033..81ea03e7 100644 --- a/examples/next/docs.config.tsx +++ b/examples/next/docs.config.tsx @@ -189,6 +189,7 @@ export default defineDocs({ enabled: true, default: "light", }, + tweaks: { reader: true, author: process.env.NODE_ENV !== "production", position: "both" }, og: { enabled: true, type: "dynamic", diff --git a/examples/nuxt/docs.config.ts b/examples/nuxt/docs.config.ts index fc95da37..13fb2913 100644 --- a/examples/nuxt/docs.config.ts +++ b/examples/nuxt/docs.config.ts @@ -52,6 +52,7 @@ export default defineDocs({ ], }, themeToggle: { enabled: true, default: "dark" }, + tweaks: { reader: true, author: process.env.NODE_ENV !== "production" }, breadcrumb: { enabled: true }, metadata: { titleTemplate: "%s – Docs", diff --git a/examples/sveltekit/src/lib/docs.config.ts b/examples/sveltekit/src/lib/docs.config.ts index b167ae03..d8128531 100644 --- a/examples/sveltekit/src/lib/docs.config.ts +++ b/examples/sveltekit/src/lib/docs.config.ts @@ -60,6 +60,7 @@ export default defineDocs({ }, readingTime: { enabled: true, wordsPerMinute: 220 }, themeToggle: { enabled: true, default: "dark" }, + tweaks: { reader: true, author: process.env.NODE_ENV !== "production" }, breadcrumb: { enabled: true }, metadata: { titleTemplate: "%s – Docs", diff --git a/examples/tanstack-start/docs.config.tsx b/examples/tanstack-start/docs.config.tsx index b5800734..d06e60ba 100644 --- a/examples/tanstack-start/docs.config.tsx +++ b/examples/tanstack-start/docs.config.tsx @@ -67,4 +67,5 @@ export default defineDocs({ enabled: true, default: "dark", }, + tweaks: { reader: true, author: process.env.NODE_ENV !== "production" }, }); diff --git a/packages/docs/src/define-docs.ts b/packages/docs/src/define-docs.ts index 2596c3ee..67676030 100644 --- a/packages/docs/src/define-docs.ts +++ b/packages/docs/src/define-docs.ts @@ -22,6 +22,7 @@ export function defineDocs(config: DocsConfig): DocsConfig { onCopyClick: config.onCopyClick, codeBlocks: config.codeBlocks, feedback: config.feedback, + tweaks: config.tweaks, search: config.search, mcp: config.mcp, icons: config.icons, diff --git a/packages/docs/src/index.ts b/packages/docs/src/index.ts index ccd045ed..187405e6 100644 --- a/packages/docs/src/index.ts +++ b/packages/docs/src/index.ts @@ -272,6 +272,7 @@ export type { DocsAgentFeedbackData, FeedbackConfig, AgentFeedbackConfig, + TweaksConfig, DocsAskAIMcpConfig, DocsSearchAdapter, DocsSearchAdapterContext, diff --git a/packages/docs/src/types.ts b/packages/docs/src/types.ts index b865607b..0a74a173 100644 --- a/packages/docs/src/types.ts +++ b/packages/docs/src/types.ts @@ -2107,6 +2107,96 @@ export interface FeedbackConfig { agent?: boolean | AgentFeedbackConfig; } +/** + * Built-in "Tweaks" dialog configuration. + * + * The dialog has two modes, each independently opt-in. **Both are off by + * default** — site authors must explicitly enable whichever they want. + * + * - **Reader mode** (`reader: true`) exposes a limited knob set (accent, + * density, radius, preset, font) to anyone visiting the docs. Choices + * persist per-browser via `localStorage` and re-apply pre-paint on the + * next load. + * - **Author mode** (`author: true`) exposes the *full* knob set — every + * color token, sidebar/TOC style, layout dimensions — plus a code-export + * panel that emits a ready-to-paste `createTheme({...})` snippet. Use + * while iterating on a theme locally; typically gated to development: + * `author: process.env.NODE_ENV !== "production"`. + * + * When both are enabled, author mode supersedes reader mode for the docs + * author. Public visitors still see only the reader-mode dialog. + * + * @example Reader-only — let visitors tweak their view in production + * ```ts + * export default defineDocs({ + * entry: "docs", + * theme: fumadocs(), + * tweaks: { reader: true }, + * }); + * ``` + * + * @example Author-only — fast theme iteration during local dev + * ```ts + * export default defineDocs({ + * entry: "docs", + * theme: fumadocs(), + * tweaks: { author: process.env.NODE_ENV !== "production" }, + * }); + * ``` + */ +export interface TweaksConfig { + /** + * Show the limited reader-mode Tweaks dialog to all visitors. + * @default false + */ + reader?: boolean; + /** + * Show the full author-mode Tweaks dialog (extra knobs + code export). + * Typically gated to development. Supersedes reader mode when both are + * enabled. + * @default false + */ + author?: boolean; + /** + * Which knobs to show in **reader mode**. Author mode always shows the + * full set regardless of this value. + * @default ["color", "density", "radius", "preset", "font-family"] + */ + knobs?: Array<"color" | "density" | "radius" | "preset" | "font-family">; + /** + * Where the trigger button is mounted. + * + * - `"sidebar-footer"` — portaled next to the theme toggle (recommended). + * - `"floating"` — fixed bottom-right button only. + * - `"both"` — sidebar trigger on wide screens, floating fallback below `md`. + * - `"manual"` — neither trigger is auto-mounted; render + * `` yourself wherever you want. The dialog still + * mounts and opens when your trigger toggles it (shared state). + * + * @default "both" + */ + position?: "sidebar-footer" | "floating" | "both" | "manual"; + /** Label shown on the trigger button. @default "Tweaks" */ + label?: string; + /** localStorage key used to persist selections. @default "fd:tweaks:v1" */ + storageKey?: string; + /** Persist selections across page loads. @default true */ + persist?: boolean; + /** + * Curated list of sans-serif fonts the reader can pick from. Each value is + * written into `--fd-font-sans`; the font must already be loaded on the + * page (the framework does not fetch arbitrary fonts at runtime). + * + * @default ["Inter", "Geist", "system-ui", "serif"] + */ + fontOptions?: string[]; + /** + * Async callback fired whenever a tweak is applied (color pick, preset + * change, slider drag-end, etc.). Useful for analytics. + */ + onApply?: (state: Record) => void | Promise; +} + export interface DocsI18nConfig { /** Supported locale identifiers (e.g. ["en", "fr"]). */ locales: string[]; @@ -2722,6 +2812,29 @@ export interface DocsConfig { * ``` */ feedback?: boolean | FeedbackConfig; + /** + * Built-in "Tweaks" dialog for live-tweaking theme tokens at runtime. + * + * Both modes are **off by default** — pick which audiences you want: + * + * - `{ reader: true }` → reader-mode dialog visible to all visitors. + * - `{ author: true }` → author-mode dialog (extra knobs + code export), + * typically gated to development with + * `author: process.env.NODE_ENV !== "production"`. + * - `true` → shorthand for `{ author: true }` (fast local iteration). + * - `false` / omitted → no dialog at all. + * + * @example Reader-facing, persisted per-browser + * ```ts + * tweaks: { reader: true } + * ``` + * + * @example Author-only, dev-time theme iteration + * ```ts + * tweaks: { author: process.env.NODE_ENV !== "production" } + * ``` + */ + tweaks?: boolean | TweaksConfig; /** * Built-in MCP server for agent/assistant access to your docs content. * diff --git a/packages/fumadocs/package.json b/packages/fumadocs/package.json index 866ac80b..598725ff 100644 --- a/packages/fumadocs/package.json +++ b/packages/fumadocs/package.json @@ -113,6 +113,11 @@ "import": "./dist/ai-search-dialog.mjs", "default": "./dist/ai-search-dialog.mjs" }, + "./tweaks": { + "types": "./dist/tweaks-dialog.d.mts", + "import": "./dist/tweaks-dialog.mjs", + "default": "./dist/tweaks-dialog.mjs" + }, "./css": "./styles/default.css", "./base/css": "./styles/base.css", "./default/css": "./styles/default.css", diff --git a/packages/fumadocs/src/docs-layout.tsx b/packages/fumadocs/src/docs-layout.tsx index 6d601a8d..9637ea10 100644 --- a/packages/fumadocs/src/docs-layout.tsx +++ b/packages/fumadocs/src/docs-layout.tsx @@ -25,6 +25,7 @@ import type { AIConfig, OrderingItem, PageFrontmatter, + TweaksConfig, OpenDocsConfig, } from "@farming-labs/docs"; import { DocsPageClient } from "./docs-page-client.js"; @@ -34,6 +35,8 @@ import { resolveOpenDocsProviders } from "./open-docs-providers.js"; import { resolvePageReadingTime, resolveReadingTimeOptions } from "./reading-time.js"; import { SidebarSearchWithAI } from "./sidebar-search-ai.js"; import { LocaleThemeControl } from "./locale-theme-control.js"; +import { TweaksAutoPortalTrigger, TweaksControl } from "./tweaks-dialog.js"; +import { buildFoucScript as buildTweaksFoucScript } from "./tweaks-runtime.js"; import { withLangInUrl } from "./i18n.js"; // ─── Tree node types (mirrors fumadocs-core/page-tree) ─────────────── interface PageNode { @@ -968,6 +971,7 @@ export function createDocsLayout(config: DocsConfig, options?: { locale?: string // llms.txt config const llmsTxtEnabled = resolveEnabledByDefault(config.llmsTxt); const feedbackConfig = resolveFeedbackConfig(config.feedback); + const tweaksConfig = resolveTweaksConfig(config.tweaks); // Serialize provider icons to HTML strings so they survive the // server → client component boundary. @@ -1057,6 +1061,10 @@ export function createDocsLayout(config: DocsConfig, options?: { locale?: string const finalSidebarProps = { ...sidebarProps } as Record; const sidebarFooter = sidebarProps.footer as ReactNode; + const tweaksAutoTrigger = + tweaksConfig.mode !== null && + (tweaksConfig.position === "sidebar-footer" || tweaksConfig.position === "both"); + if (i18n) { finalSidebarProps.footer = (
@@ -1098,6 +1106,32 @@ export function createDocsLayout(config: DocsConfig, options?: { locale?: string {forcedTheme && } + {tweaksConfig.mode !== null && ( +