Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/astro/src/lib/docs.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions examples/next/docs.config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions examples/nuxt/docs.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions examples/sveltekit/src/lib/docs.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions examples/tanstack-start/docs.config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,5 @@ export default defineDocs({
enabled: true,
default: "dark",
},
tweaks: { reader: true, author: process.env.NODE_ENV !== "production" },
});
1 change: 1 addition & 0 deletions packages/docs/src/define-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/docs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ export type {
DocsAgentFeedbackData,
FeedbackConfig,
AgentFeedbackConfig,
TweaksConfig,
DocsAskAIMcpConfig,
DocsSearchAdapter,
DocsSearchAdapterContext,
Expand Down
113 changes: 113 additions & 0 deletions packages/docs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
* `<TweaksSidebarTrigger>` 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<string, string>) => void | Promise<void>;
}

export interface DocsI18nConfig {
/** Supported locale identifiers (e.g. ["en", "fr"]). */
locales: string[];
Expand Down Expand Up @@ -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.
*
Expand Down
5 changes: 5 additions & 0 deletions packages/fumadocs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
87 changes: 87 additions & 0 deletions packages/fumadocs/src/docs-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
AIConfig,
OrderingItem,
PageFrontmatter,
TweaksConfig,
OpenDocsConfig,
} from "@farming-labs/docs";
import { DocsPageClient } from "./docs-page-client.js";
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1057,6 +1061,10 @@ export function createDocsLayout(config: DocsConfig, options?: { locale?: string
const finalSidebarProps = { ...sidebarProps } as Record<string, unknown>;
const sidebarFooter = sidebarProps.footer as ReactNode;

const tweaksAutoTrigger =
tweaksConfig.mode !== null &&
(tweaksConfig.position === "sidebar-footer" || tweaksConfig.position === "both");

if (i18n) {
finalSidebarProps.footer = (
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
Expand Down Expand Up @@ -1098,6 +1106,32 @@ export function createDocsLayout(config: DocsConfig, options?: { locale?: string
<TypographyStyle typography={typography} />
<LayoutStyle layout={layoutDimensions} />
{forcedTheme && <ForcedThemeScript theme={forcedTheme} />}
{tweaksConfig.mode !== null && (
<script
dangerouslySetInnerHTML={{
__html: buildTweaksFoucScript(tweaksConfig.storageKey),
}}
/>
)}
{tweaksConfig.mode !== null && (
<Suspense fallback={null}>
<TweaksControl
mode={tweaksConfig.mode!}
knobs={tweaksConfig.knobs}
position={tweaksConfig.position}
label={tweaksConfig.label}
storageKey={tweaksConfig.storageKey}
persist={tweaksConfig.persist}
fontOptions={tweaksConfig.fontOptions}
onApply={tweaksConfig.onApply}
/>
</Suspense>
)}
{tweaksAutoTrigger && (
<Suspense fallback={null}>
<TweaksAutoPortalTrigger label={tweaksConfig.label} />
</Suspense>
)}
{!staticExport && (
<Suspense fallback={null}>
<DocsCommandSearch
Expand Down Expand Up @@ -1216,6 +1250,59 @@ function resolveFeedbackConfig(feedback: DocsConfig["feedback"]) {
};
}

interface ResolvedTweaksConfig {
/**
* Which dialog to mount, if any:
* "author" — full knob set + code export (typically dev-only)
* "reader" — limited knob set, public-facing
* null — no dialog mounted at all
*/
mode: "author" | "reader" | null;
knobs: NonNullable<TweaksConfig["knobs"]>;
position: "sidebar-footer" | "floating" | "both" | "manual";
label: string;
storageKey: string;
persist: boolean;
fontOptions: ReadonlyArray<{ label: string; value: string }> | undefined;
onApply: TweaksConfig["onApply"];
}

function resolveTweaksConfig(tweaks: DocsConfig["tweaks"]): ResolvedTweaksConfig {
const defaults: ResolvedTweaksConfig = {
mode: null,
knobs: ["color", "density", "radius", "preset", "font-family"],
position: "both",
label: "Tweaks",
storageKey: "fd:tweaks:v1",
persist: true,
fontOptions: undefined,
onApply: undefined,
};

if (tweaks === undefined || tweaks === false) return defaults;
if (tweaks === true) return { ...defaults, mode: "author" };

// Author mode supersedes reader when both are set
const mode: "author" | "reader" | null = tweaks.author
? "author"
: tweaks.reader
? "reader"
: null;

return {
mode,
knobs: tweaks.knobs ?? defaults.knobs,
position: tweaks.position ?? defaults.position,
label: tweaks.label ?? defaults.label,
storageKey: tweaks.storageKey ?? defaults.storageKey,
persist: tweaks.persist !== false,
fontOptions: tweaks.fontOptions
? tweaks.fontOptions.map((value: string) => ({ label: value.split(",")[0] ?? value, value }))
: undefined,
onApply: tweaks.onApply,
};
}

/**
* Tiny inline script to force a theme when the toggle is hidden.
* Sets the class on <html> before React hydrates to avoid FOUC.
Expand Down
14 changes: 14 additions & 0 deletions packages/fumadocs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,19 @@ export { DocsFeedback } from "./docs-feedback.js";
// ─── Page action buttons (Copy Markdown, Open in LLM) ─────────────────
export { PageActions } from "./page-actions.js";

// ─── Tweaks dialog (runtime theme tweaker) ────────────────────────────
export {
TweaksAutoPortalTrigger,
TweaksControl,
TweaksFoucScript,
TweaksSidebarTrigger,
} from "./tweaks-dialog.js";
export type {
TweaksAutoPortalTriggerProps,
TweaksKnob,
TweaksSidebarTriggerProps,
} from "./tweaks-dialog.js";

// ─── Built-in Command Palette Search ────────────────────────────────
export { DocsCommandSearch } from "./docs-command-search.js";
export { withLangInUrl } from "./i18n.js";
Expand Down Expand Up @@ -119,6 +132,7 @@ export type {
FeedbackConfig,
DocsFeedbackData,
DocsFeedbackValue,
TweaksConfig,
} from "@farming-labs/docs";

// ─── MDX components (for use in custom layouts or overrides) ──────────
Expand Down
Loading