@windrun-huaiin/third-ui 31.0.1 → 31.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/fuma/base/custom-header.d.ts +1 -1
  2. package/dist/fuma/base/custom-header.js +16 -8
  3. package/dist/fuma/base/custom-header.mjs +16 -8
  4. package/dist/fuma/base/docs-root-provider.d.ts +2 -5
  5. package/dist/fuma/base/docs-root-provider.js +4 -7
  6. package/dist/fuma/base/docs-root-provider.mjs +4 -7
  7. package/dist/fuma/base/header-theme-switch.d.ts +2 -2
  8. package/dist/fuma/base/header-theme-switch.js +2 -10
  9. package/dist/fuma/base/header-theme-switch.mjs +2 -10
  10. package/dist/fuma/base/site-docs-layout.d.ts +1 -0
  11. package/dist/fuma/base/site-docs-layout.js +2 -1
  12. package/dist/fuma/base/site-docs-layout.mjs +2 -1
  13. package/dist/fuma/base/site-docs-theme-switch.d.ts +2 -0
  14. package/dist/fuma/base/site-docs-theme-switch.js +16 -0
  15. package/dist/fuma/base/site-docs-theme-switch.mjs +14 -0
  16. package/dist/fuma/base/site-layout-shared.d.ts +4 -1
  17. package/dist/fuma/base/site-layout-shared.js +1 -1
  18. package/dist/fuma/base/site-layout-shared.mjs +1 -1
  19. package/dist/fuma/base/site-theme-context.d.ts +8 -0
  20. package/dist/fuma/base/site-theme-context.js +19 -0
  21. package/dist/fuma/base/site-theme-context.mjs +16 -0
  22. package/dist/fuma/base/site-theme-provider.d.ts +8 -0
  23. package/dist/fuma/base/site-theme-provider.js +34 -0
  24. package/dist/fuma/base/site-theme-provider.mjs +32 -0
  25. package/package.json +3 -3
  26. package/src/fuma/base/custom-header.tsx +28 -6
  27. package/src/fuma/base/custom-home-layout.tsx +12 -12
  28. package/src/fuma/base/docs-root-provider.tsx +9 -28
  29. package/src/fuma/base/header-theme-switch.tsx +3 -32
  30. package/src/fuma/base/site-docs-layout.tsx +7 -1
  31. package/src/fuma/base/site-docs-theme-switch.tsx +15 -0
  32. package/src/fuma/base/site-layout-shared.tsx +9 -2
  33. package/src/fuma/base/site-theme-context.tsx +30 -0
  34. package/src/fuma/base/site-theme-provider.tsx +51 -0
@@ -50,4 +50,4 @@ export interface CustomHomeHeaderProps extends HomeLayoutProps {
50
50
  export type DesktopAction = 'search' | 'theme' | 'i18n' | 'secondary' | 'github';
51
51
  export type MobileBarAction = 'pinned' | 'search' | 'menu';
52
52
  export type MobileMenuAction = 'secondary' | 'github' | 'separator' | 'i18n' | 'theme';
53
- export declare function CustomHomeHeader({ nav, i18n, links, githubUrl, themeSwitch, searchToggle, bannerHeight, headerHeight, maxContentWidth, navbarClassName, floating, desktopActionsOrder, mobileBarActionsOrder, mobileMenuActionsOrder, }: CustomHomeHeaderProps): import("react/jsx-runtime").JSX.Element;
53
+ export declare function CustomHomeHeader({ nav, i18n, links, githubUrl, searchToggle, bannerHeight, headerHeight, maxContentWidth, navbarClassName, floating, desktopActionsOrder, mobileBarActionsOrder, mobileMenuActionsOrder, }: CustomHomeHeaderProps): import("react/jsx-runtime").JSX.Element;
@@ -15,6 +15,7 @@ var popover = require('fumadocs-ui/components/ui/popover');
15
15
  var button = require('fumadocs-ui/components/ui/button');
16
16
  var i18n = require('fumadocs-ui/contexts/i18n');
17
17
  var headerThemeSwitch = require('./header-theme-switch.js');
18
+ var siteThemeContext = require('./site-theme-context.js');
18
19
 
19
20
  const PrefetchLinkItem = shared.LinkItem;
20
21
  const DEFAULT_DESKTOP_ACTIONS = [
@@ -34,8 +35,9 @@ const DEFAULT_MOBILE_MENU_ACTIONS = [
34
35
  'i18n',
35
36
  'theme',
36
37
  ];
37
- function CustomHomeHeader({ nav = {}, i18n = false, links, githubUrl, themeSwitch = {}, searchToggle = {}, bannerHeight = 0, headerHeight = 2.5, maxContentWidth = 1400, navbarClassName, floating = false, desktopActionsOrder = DEFAULT_DESKTOP_ACTIONS, mobileBarActionsOrder = DEFAULT_MOBILE_BAR_ACTIONS, mobileMenuActionsOrder = DEFAULT_MOBILE_MENU_ACTIONS, }) {
38
- var _a, _b, _c, _d, _e, _f, _g;
38
+ function CustomHomeHeader({ nav = {}, i18n = false, links, githubUrl, searchToggle = {}, bannerHeight = 0, headerHeight = 2.5, maxContentWidth = 1400, navbarClassName, floating = false, desktopActionsOrder = DEFAULT_DESKTOP_ACTIONS, mobileBarActionsOrder = DEFAULT_MOBILE_BAR_ACTIONS, mobileMenuActionsOrder = DEFAULT_MOBILE_MENU_ACTIONS, }) {
39
+ var _a, _b, _c, _d, _e;
40
+ const themeMode = siteThemeContext.useSiteThemeMode();
39
41
  const finalLinks = React.useMemo(() => shared.resolveLinkItems({ links, githubUrl }), [links, githubUrl]);
40
42
  const navItems = finalLinks.filter((item) => { var _a; return ['nav', 'all'].includes((_a = item.on) !== null && _a !== void 0 ? _a : 'all'); });
41
43
  const menuItems = finalLinks.filter((item) => { var _a; return ['menu', 'all'].includes((_a = item.on) !== null && _a !== void 0 ? _a : 'all'); });
@@ -55,8 +57,8 @@ function CustomHomeHeader({ nav = {}, i18n = false, links, githubUrl, themeSwitc
55
57
  search: searchToggle.enabled !== false
56
58
  ? (_b = (_a = searchToggle.components) === null || _a === void 0 ? void 0 : _a.lg) !== null && _b !== void 0 ? _b : null
57
59
  : null,
58
- theme: themeSwitch.enabled !== false
59
- ? (_c = themeSwitch.component) !== null && _c !== void 0 ? _c : jsxRuntime.jsx(headerThemeSwitch.HeaderThemeSwitch, { mode: themeSwitch === null || themeSwitch === void 0 ? void 0 : themeSwitch.mode })
60
+ theme: shouldShowThemeSwitch(themeMode)
61
+ ? (jsxRuntime.jsx(headerThemeSwitch.HeaderThemeSwitch, { mode: normalizeThemeSwitchMode() }))
60
62
  : null,
61
63
  i18n: i18n ? (jsxRuntime.jsx(CompactLanguageToggle, { children: jsxRuntime.jsx(icons.LanguagesIcon, { className: "size-5" }) })) : null,
62
64
  secondary: desktopSecondaryDisplayItems.length ? (jsxRuntime.jsx("ul", { className: "flex flex-row gap-2 items-center empty:hidden", children: desktopSecondaryDisplayItems.map((item, i) => (jsxRuntime.jsx(NavbarLinkItem, { item: item, className: utils.cn(item.type === 'icon' && [
@@ -78,8 +80,8 @@ function CustomHomeHeader({ nav = {}, i18n = false, links, githubUrl, themeSwitc
78
80
  github: githubMobileMenuItem ? (jsxRuntime.jsx(MenuLinkItem, { item: githubMobileMenuItem, className: "-me-1.5" })) : null,
79
81
  separator: jsxRuntime.jsx("div", { role: "separator", className: "flex-1" }),
80
82
  i18n: i18n ? (jsxRuntime.jsxs(CompactLanguageToggle, { children: [jsxRuntime.jsx(icons.LanguagesIcon, { className: "size-5" }), jsxRuntime.jsx(languageSelect.LanguageSelectText, {}), jsxRuntime.jsx(icons.ChevronDownIcon, { className: "size-3 text-fd-muted-foreground" })] })) : null,
81
- theme: themeSwitch.enabled !== false
82
- ? (_d = themeSwitch.component) !== null && _d !== void 0 ? _d : jsxRuntime.jsx(headerThemeSwitch.HeaderThemeSwitch, { mode: themeSwitch === null || themeSwitch === void 0 ? void 0 : themeSwitch.mode })
83
+ theme: shouldShowThemeSwitch(themeMode)
84
+ ? (jsxRuntime.jsx(headerThemeSwitch.HeaderThemeSwitch, { mode: normalizeThemeSwitchMode() }))
83
85
  : null,
84
86
  };
85
87
  const shouldRenderMobileUtilities = mobileMenuActionsOrder.some((action) => action !== 'separator' && Boolean(mobileMenuActionNodes[action]));
@@ -100,7 +102,7 @@ function CustomHomeHeader({ nav = {}, i18n = false, links, githubUrl, themeSwitc
100
102
  }) })) : null] })] }));
101
103
  const mobilePinnedNode = mobilePinnedItems.length > 0 ? (jsxRuntime.jsx(jsxRuntime.Fragment, { children: mobilePinnedItems.map((item, i) => (jsxRuntime.jsx(NavbarLinkItem, { item: item, className: "max-sm:-mr-1" }, `mobile-pinned-${i}`))) })) : null;
102
104
  const mobileSearchNode = searchToggle.enabled !== false
103
- ? (_f = (_e = searchToggle.components) === null || _e === void 0 ? void 0 : _e.sm) !== null && _f !== void 0 ? _f : null
105
+ ? (_d = (_c = searchToggle.components) === null || _c === void 0 ? void 0 : _c.sm) !== null && _d !== void 0 ? _d : null
104
106
  : null;
105
107
  const mobileBarNodes = {
106
108
  pinned: mobilePinnedNode,
@@ -108,7 +110,7 @@ function CustomHomeHeader({ nav = {}, i18n = false, links, githubUrl, themeSwitc
108
110
  menu: menuNode,
109
111
  };
110
112
  const getMobileBarNode = (action) => { var _a; return (_a = mobileBarNodes[action]) !== null && _a !== void 0 ? _a : null; };
111
- return (jsxRuntime.jsxs(CustomNavbar, { bannerHeight: bannerHeight, headerHeight: headerHeight, maxContentWidth: maxContentWidth, className: navbarClassName, floating: floating, children: [jsxRuntime.jsx(Link, { href: (_g = nav.url) !== null && _g !== void 0 ? _g : '/', prefetch: false, className: "inline-flex items-center gap-2.5 font-semibold", children: renderNavTitle(nav.title) }), nav.children, jsxRuntime.jsx("ul", { className: "flex flex-row items-center gap-2 px-6 max-sm:hidden", children: navItems
113
+ return (jsxRuntime.jsxs(CustomNavbar, { bannerHeight: bannerHeight, headerHeight: headerHeight, maxContentWidth: maxContentWidth, className: navbarClassName, floating: floating, children: [jsxRuntime.jsx(Link, { href: (_e = nav.url) !== null && _e !== void 0 ? _e : '/', prefetch: false, className: "inline-flex items-center gap-2.5 font-semibold", children: renderNavTitle(nav.title) }), nav.children, jsxRuntime.jsx("ul", { className: "flex flex-row items-center gap-2 px-6 max-sm:hidden", children: navItems
112
114
  .filter((item) => !isSecondary(item))
113
115
  .map((item, i) => (jsxRuntime.jsx(NavbarLinkItem, { item: item, className: "text-sm" }, i))) }), jsxRuntime.jsx("div", { className: "flex flex-row items-center justify-end gap-1.5 flex-1 max-lg:hidden", children: desktopActionsOrder.map((action) => {
114
116
  const node = desktopActionNodes[action];
@@ -120,6 +122,12 @@ function CustomHomeHeader({ nav = {}, i18n = false, links, githubUrl, themeSwitc
120
122
  return node ? (jsxRuntime.jsx(React.Fragment, { children: node }, `mobile-bar-${action}`)) : null;
121
123
  }) })] }));
122
124
  }
125
+ function shouldShowThemeSwitch(mode) {
126
+ return mode === 'light-dark-system' || mode == null;
127
+ }
128
+ function normalizeThemeSwitchMode(mode) {
129
+ return 'light-dark-system';
130
+ }
123
131
  function CustomNavbar(_a) {
124
132
  var { bannerHeight = 0, headerHeight = 2.5, maxContentWidth = 1400, className, style, floating = false } = _a, props = tslib.__rest(_a, ["bannerHeight", "headerHeight", "maxContentWidth", "className", "style", "floating"]);
125
133
  const [value, setValue] = React.useState('');
@@ -13,6 +13,7 @@ import { Popover, PopoverTrigger, PopoverContent } from 'fumadocs-ui/components/
13
13
  import { buttonVariants } from 'fumadocs-ui/components/ui/button';
14
14
  import { useI18n } from 'fumadocs-ui/contexts/i18n';
15
15
  import { HeaderThemeSwitch } from './header-theme-switch.mjs';
16
+ import { useSiteThemeMode } from './site-theme-context.mjs';
16
17
 
17
18
  const PrefetchLinkItem = LinkItem;
18
19
  const DEFAULT_DESKTOP_ACTIONS = [
@@ -32,8 +33,9 @@ const DEFAULT_MOBILE_MENU_ACTIONS = [
32
33
  'i18n',
33
34
  'theme',
34
35
  ];
35
- function CustomHomeHeader({ nav = {}, i18n = false, links, githubUrl, themeSwitch = {}, searchToggle = {}, bannerHeight = 0, headerHeight = 2.5, maxContentWidth = 1400, navbarClassName, floating = false, desktopActionsOrder = DEFAULT_DESKTOP_ACTIONS, mobileBarActionsOrder = DEFAULT_MOBILE_BAR_ACTIONS, mobileMenuActionsOrder = DEFAULT_MOBILE_MENU_ACTIONS, }) {
36
- var _a, _b, _c, _d, _e, _f, _g;
36
+ function CustomHomeHeader({ nav = {}, i18n = false, links, githubUrl, searchToggle = {}, bannerHeight = 0, headerHeight = 2.5, maxContentWidth = 1400, navbarClassName, floating = false, desktopActionsOrder = DEFAULT_DESKTOP_ACTIONS, mobileBarActionsOrder = DEFAULT_MOBILE_BAR_ACTIONS, mobileMenuActionsOrder = DEFAULT_MOBILE_MENU_ACTIONS, }) {
37
+ var _a, _b, _c, _d, _e;
38
+ const themeMode = useSiteThemeMode();
37
39
  const finalLinks = useMemo(() => resolveLinkItems({ links, githubUrl }), [links, githubUrl]);
38
40
  const navItems = finalLinks.filter((item) => { var _a; return ['nav', 'all'].includes((_a = item.on) !== null && _a !== void 0 ? _a : 'all'); });
39
41
  const menuItems = finalLinks.filter((item) => { var _a; return ['menu', 'all'].includes((_a = item.on) !== null && _a !== void 0 ? _a : 'all'); });
@@ -53,8 +55,8 @@ function CustomHomeHeader({ nav = {}, i18n = false, links, githubUrl, themeSwitc
53
55
  search: searchToggle.enabled !== false
54
56
  ? (_b = (_a = searchToggle.components) === null || _a === void 0 ? void 0 : _a.lg) !== null && _b !== void 0 ? _b : null
55
57
  : null,
56
- theme: themeSwitch.enabled !== false
57
- ? (_c = themeSwitch.component) !== null && _c !== void 0 ? _c : jsx(HeaderThemeSwitch, { mode: themeSwitch === null || themeSwitch === void 0 ? void 0 : themeSwitch.mode })
58
+ theme: shouldShowThemeSwitch(themeMode)
59
+ ? (jsx(HeaderThemeSwitch, { mode: normalizeThemeSwitchMode() }))
58
60
  : null,
59
61
  i18n: i18n ? (jsx(CompactLanguageToggle, { children: jsx(LanguagesIcon, { className: "size-5" }) })) : null,
60
62
  secondary: desktopSecondaryDisplayItems.length ? (jsx("ul", { className: "flex flex-row gap-2 items-center empty:hidden", children: desktopSecondaryDisplayItems.map((item, i) => (jsx(NavbarLinkItem, { item: item, className: cn(item.type === 'icon' && [
@@ -76,8 +78,8 @@ function CustomHomeHeader({ nav = {}, i18n = false, links, githubUrl, themeSwitc
76
78
  github: githubMobileMenuItem ? (jsx(MenuLinkItem, { item: githubMobileMenuItem, className: "-me-1.5" })) : null,
77
79
  separator: jsx("div", { role: "separator", className: "flex-1" }),
78
80
  i18n: i18n ? (jsxs(CompactLanguageToggle, { children: [jsx(LanguagesIcon, { className: "size-5" }), jsx(LanguageSelectText, {}), jsx(ChevronDownIcon, { className: "size-3 text-fd-muted-foreground" })] })) : null,
79
- theme: themeSwitch.enabled !== false
80
- ? (_d = themeSwitch.component) !== null && _d !== void 0 ? _d : jsx(HeaderThemeSwitch, { mode: themeSwitch === null || themeSwitch === void 0 ? void 0 : themeSwitch.mode })
81
+ theme: shouldShowThemeSwitch(themeMode)
82
+ ? (jsx(HeaderThemeSwitch, { mode: normalizeThemeSwitchMode() }))
81
83
  : null,
82
84
  };
83
85
  const shouldRenderMobileUtilities = mobileMenuActionsOrder.some((action) => action !== 'separator' && Boolean(mobileMenuActionNodes[action]));
@@ -98,7 +100,7 @@ function CustomHomeHeader({ nav = {}, i18n = false, links, githubUrl, themeSwitc
98
100
  }) })) : null] })] }));
99
101
  const mobilePinnedNode = mobilePinnedItems.length > 0 ? (jsx(Fragment, { children: mobilePinnedItems.map((item, i) => (jsx(NavbarLinkItem, { item: item, className: "max-sm:-mr-1" }, `mobile-pinned-${i}`))) })) : null;
100
102
  const mobileSearchNode = searchToggle.enabled !== false
101
- ? (_f = (_e = searchToggle.components) === null || _e === void 0 ? void 0 : _e.sm) !== null && _f !== void 0 ? _f : null
103
+ ? (_d = (_c = searchToggle.components) === null || _c === void 0 ? void 0 : _c.sm) !== null && _d !== void 0 ? _d : null
102
104
  : null;
103
105
  const mobileBarNodes = {
104
106
  pinned: mobilePinnedNode,
@@ -106,7 +108,7 @@ function CustomHomeHeader({ nav = {}, i18n = false, links, githubUrl, themeSwitc
106
108
  menu: menuNode,
107
109
  };
108
110
  const getMobileBarNode = (action) => { var _a; return (_a = mobileBarNodes[action]) !== null && _a !== void 0 ? _a : null; };
109
- return (jsxs(CustomNavbar, { bannerHeight: bannerHeight, headerHeight: headerHeight, maxContentWidth: maxContentWidth, className: navbarClassName, floating: floating, children: [jsx(Link, { href: (_g = nav.url) !== null && _g !== void 0 ? _g : '/', prefetch: false, className: "inline-flex items-center gap-2.5 font-semibold", children: renderNavTitle(nav.title) }), nav.children, jsx("ul", { className: "flex flex-row items-center gap-2 px-6 max-sm:hidden", children: navItems
111
+ return (jsxs(CustomNavbar, { bannerHeight: bannerHeight, headerHeight: headerHeight, maxContentWidth: maxContentWidth, className: navbarClassName, floating: floating, children: [jsx(Link, { href: (_e = nav.url) !== null && _e !== void 0 ? _e : '/', prefetch: false, className: "inline-flex items-center gap-2.5 font-semibold", children: renderNavTitle(nav.title) }), nav.children, jsx("ul", { className: "flex flex-row items-center gap-2 px-6 max-sm:hidden", children: navItems
110
112
  .filter((item) => !isSecondary(item))
111
113
  .map((item, i) => (jsx(NavbarLinkItem, { item: item, className: "text-sm" }, i))) }), jsx("div", { className: "flex flex-row items-center justify-end gap-1.5 flex-1 max-lg:hidden", children: desktopActionsOrder.map((action) => {
112
114
  const node = desktopActionNodes[action];
@@ -118,6 +120,12 @@ function CustomHomeHeader({ nav = {}, i18n = false, links, githubUrl, themeSwitc
118
120
  return node ? (jsx(Fragment$1, { children: node }, `mobile-bar-${action}`)) : null;
119
121
  }) })] }));
120
122
  }
123
+ function shouldShowThemeSwitch(mode) {
124
+ return mode === 'light-dark-system' || mode == null;
125
+ }
126
+ function normalizeThemeSwitchMode(mode) {
127
+ return 'light-dark-system';
128
+ }
121
129
  function CustomNavbar(_a) {
122
130
  var { bannerHeight = 0, headerHeight = 2.5, maxContentWidth = 1400, className, style, floating = false } = _a, props = __rest(_a, ["bannerHeight", "headerHeight", "maxContentWidth", "className", "style", "floating"]);
123
131
  const [value, setValue] = useState('');
@@ -1,17 +1,14 @@
1
1
  import type { ComponentProps, ReactNode } from 'react';
2
2
  import { NextProvider } from 'fumadocs-core/framework/next';
3
3
  import { type I18nProviderProps } from 'fumadocs-ui/contexts/i18n';
4
- import { type ThemeProviderProps } from 'next-themes';
4
+ import type { SiteThemeProviderProps } from './site-theme-provider';
5
5
  type NextProviderComponents = {
6
6
  Link?: ComponentProps<typeof NextProvider>['Link'];
7
7
  Image?: ComponentProps<typeof NextProvider>['Image'];
8
8
  };
9
- type ThemeOptions = ThemeProviderProps & {
10
- enabled?: boolean;
11
- };
12
9
  export interface DocsRootProviderProps {
13
10
  i18n: Omit<I18nProviderProps, 'children'>;
14
- theme?: ThemeOptions;
11
+ theme?: Omit<SiteThemeProviderProps, 'children'>;
15
12
  components?: NextProviderComponents;
16
13
  children: ReactNode;
17
14
  }
@@ -3,15 +3,12 @@
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
4
  var next = require('fumadocs-core/framework/next');
5
5
  var i18n = require('fumadocs-ui/contexts/i18n');
6
- var nextThemes = require('next-themes');
6
+ var siteThemeContext = require('./site-theme-context.js');
7
7
 
8
8
  function DocsRootProvider({ i18n: i18n$1, theme = {}, components, children, }) {
9
- let body = children;
10
- if (theme.enabled !== false) {
11
- body = (jsxRuntime.jsx(nextThemes.ThemeProvider, Object.assign({ attribute: "class", defaultTheme: "system", enableSystem: true, disableTransitionOnChange: true }, theme, { children: body })));
12
- }
13
- body = (jsxRuntime.jsx(i18n.I18nProvider, Object.assign({}, i18n$1, { children: body })));
14
- return (jsxRuntime.jsx(next.NextProvider, { Link: components === null || components === void 0 ? void 0 : components.Link, Image: components === null || components === void 0 ? void 0 : components.Image, children: body }));
9
+ var _a;
10
+ const themeMode = (_a = theme.mode) !== null && _a !== void 0 ? _a : 'light-dark-system';
11
+ return (jsxRuntime.jsx(next.NextProvider, { Link: components === null || components === void 0 ? void 0 : components.Link, Image: components === null || components === void 0 ? void 0 : components.Image, children: jsxRuntime.jsx(i18n.I18nProvider, Object.assign({}, i18n$1, { children: jsxRuntime.jsx(siteThemeContext.SiteThemeRootProvider, Object.assign({}, theme, { mode: themeMode, children: children })) })) }));
15
12
  }
16
13
 
17
14
  exports.DocsRootProvider = DocsRootProvider;
@@ -1,15 +1,12 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import { NextProvider } from 'fumadocs-core/framework/next';
3
3
  import { I18nProvider } from 'fumadocs-ui/contexts/i18n';
4
- import { ThemeProvider } from 'next-themes';
4
+ import { SiteThemeRootProvider } from './site-theme-context.mjs';
5
5
 
6
6
  function DocsRootProvider({ i18n, theme = {}, components, children, }) {
7
- let body = children;
8
- if (theme.enabled !== false) {
9
- body = (jsx(ThemeProvider, Object.assign({ attribute: "class", defaultTheme: "system", enableSystem: true, disableTransitionOnChange: true }, theme, { children: body })));
10
- }
11
- body = (jsx(I18nProvider, Object.assign({}, i18n, { children: body })));
12
- return (jsx(NextProvider, { Link: components === null || components === void 0 ? void 0 : components.Link, Image: components === null || components === void 0 ? void 0 : components.Image, children: body }));
7
+ var _a;
8
+ const themeMode = (_a = theme.mode) !== null && _a !== void 0 ? _a : 'light-dark-system';
9
+ return (jsx(NextProvider, { Link: components === null || components === void 0 ? void 0 : components.Link, Image: components === null || components === void 0 ? void 0 : components.Image, children: jsx(I18nProvider, Object.assign({}, i18n, { children: jsx(SiteThemeRootProvider, Object.assign({}, theme, { mode: themeMode, children: children })) })) }));
13
10
  }
14
11
 
15
12
  export { DocsRootProvider };
@@ -1,5 +1,5 @@
1
1
  import { type ComponentProps } from 'react';
2
2
  export interface HeaderThemeSwitchProps extends ComponentProps<'div'> {
3
- mode?: 'light-dark' | 'light-dark-system';
3
+ mode?: 'light-dark-system';
4
4
  }
5
- export declare function HeaderThemeSwitch({ className, mode, ...props }: HeaderThemeSwitchProps): import("react/jsx-runtime").JSX.Element;
5
+ export declare function HeaderThemeSwitch({ className, ...props }: HeaderThemeSwitchProps): import("react/jsx-runtime").JSX.Element;
@@ -19,22 +19,14 @@ const itemVariants = classVarianceAuthority.cva('inline-flex size-6.5 items-cent
19
19
  });
20
20
  const full = [['light', icons.SunIcon], ['dark', icons.MoonIcon], ['system', icons.AirplayIcon]];
21
21
  function HeaderThemeSwitch(_a) {
22
- var { className, mode = 'light-dark' } = _a, props = tslib.__rest(_a, ["className", "mode"]);
23
- const { setTheme, theme, resolvedTheme } = nextThemes.useTheme();
22
+ var { className } = _a, props = tslib.__rest(_a, ["className"]);
23
+ const { setTheme, theme } = nextThemes.useTheme();
24
24
  const [mounted, setMounted] = React.useState(false);
25
25
  React.useEffect(() => {
26
26
  setMounted(true);
27
27
  }, []);
28
28
  const container = utils.cn('inline-flex items-center rounded-full border p-1 overflow-hidden *:rounded-full', className);
29
29
  const iconClassName = 'size-3.5 text-neutral-600 dark:text-neutral-300';
30
- if (mode === 'light-dark') {
31
- const value = mounted ? resolvedTheme : null;
32
- return (jsxRuntime.jsx("button", { type: "button", className: container, "aria-label": "Toggle Theme", onClick: () => setTheme(value === 'light' ? 'dark' : 'light'), "data-theme-toggle": "", children: full.map(([key, Icon]) => {
33
- if (key === 'system')
34
- return null;
35
- return (jsxRuntime.jsx(Icon, { fill: "currentColor", className: utils.cn(itemVariants({ active: value === key }), iconClassName) }, key));
36
- }) }));
37
- }
38
30
  const value = mounted ? theme : null;
39
31
  return (jsxRuntime.jsx("div", Object.assign({ className: container, "data-theme-toggle": "" }, props, { children: full.map(([key, Icon]) => (jsxRuntime.jsx("button", { type: "button", "aria-label": key, className: utils.cn(itemVariants({ active: value === key })), onClick: () => setTheme(key), children: jsxRuntime.jsx(Icon, { className: iconClassName, fill: "currentColor" }) }, key))) })));
40
32
  }
@@ -17,22 +17,14 @@ const itemVariants = cva('inline-flex size-6.5 items-center justify-center round
17
17
  });
18
18
  const full = [['light', SunIcon], ['dark', MoonIcon], ['system', AirplayIcon]];
19
19
  function HeaderThemeSwitch(_a) {
20
- var { className, mode = 'light-dark' } = _a, props = __rest(_a, ["className", "mode"]);
21
- const { setTheme, theme, resolvedTheme } = useTheme();
20
+ var { className } = _a, props = __rest(_a, ["className"]);
21
+ const { setTheme, theme } = useTheme();
22
22
  const [mounted, setMounted] = useState(false);
23
23
  useEffect(() => {
24
24
  setMounted(true);
25
25
  }, []);
26
26
  const container = cn('inline-flex items-center rounded-full border p-1 overflow-hidden *:rounded-full', className);
27
27
  const iconClassName = 'size-3.5 text-neutral-600 dark:text-neutral-300';
28
- if (mode === 'light-dark') {
29
- const value = mounted ? resolvedTheme : null;
30
- return (jsx("button", { type: "button", className: container, "aria-label": "Toggle Theme", onClick: () => setTheme(value === 'light' ? 'dark' : 'light'), "data-theme-toggle": "", children: full.map(([key, Icon]) => {
31
- if (key === 'system')
32
- return null;
33
- return (jsx(Icon, { fill: "currentColor", className: cn(itemVariants({ active: value === key }), iconClassName) }, key));
34
- }) }));
35
- }
36
28
  const value = mounted ? theme : null;
37
29
  return (jsx("div", Object.assign({ className: container, "data-theme-toggle": "" }, props, { children: full.map(([key, Icon]) => (jsx("button", { type: "button", "aria-label": key, className: cn(itemVariants({ active: value === key })), onClick: () => setTheme(key), children: jsx(Icon, { className: iconClassName, fill: "currentColor" }) }, key))) })));
38
30
  }
@@ -4,6 +4,7 @@ import { type SiteBaseLayoutConfig } from './site-layout-shared';
4
4
  export interface SiteDocsLayoutConfig extends SiteBaseLayoutConfig {
5
5
  tree: DocsLayoutProps['tree'];
6
6
  sidebar?: DocsLayoutProps['sidebar'];
7
+ slots?: DocsLayoutProps['slots'];
7
8
  }
8
9
  export declare function SiteDocsLayout({ config, children, }: {
9
10
  config: SiteDocsLayoutConfig;
@@ -3,9 +3,10 @@
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
4
  var docs = require('fumadocs-ui/layouts/docs');
5
5
  var siteLayoutShared = require('./site-layout-shared.js');
6
+ var siteDocsThemeSwitch = require('./site-docs-theme-switch.js');
6
7
 
7
8
  function toDocsLayoutOptions(config) {
8
- return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (config.nav ? { nav: config.nav } : {})), (config.i18n ? { i18n: config.i18n } : {})), (config.githubUrl ? { githubUrl: config.githubUrl } : {})), (config.links ? { links: siteLayoutShared.normalizeNavItems(config.links) } : {})), (config.searchToggle ? { searchToggle: config.searchToggle } : {})), (config.themeSwitch ? { themeSwitch: config.themeSwitch } : {})), (config.sidebar ? { sidebar: config.sidebar } : {})), { tree: config.tree });
9
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (config.nav ? { nav: config.nav } : {})), (config.i18n ? { i18n: config.i18n } : {})), (config.githubUrl ? { githubUrl: config.githubUrl } : {})), (config.links ? { links: siteLayoutShared.normalizeNavItems(config.links) } : {})), (config.searchToggle ? { searchToggle: config.searchToggle } : {})), { slots: Object.assign(Object.assign({}, config.slots), { themeSwitch: siteDocsThemeSwitch.SiteDocsThemeSwitch }) }), (config.sidebar ? { sidebar: config.sidebar } : {})), { tree: config.tree });
9
10
  }
10
11
  function SiteDocsLayout({ config, children, }) {
11
12
  const options = toDocsLayoutOptions(config);
@@ -1,9 +1,10 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import { DocsLayout } from 'fumadocs-ui/layouts/docs';
3
3
  import { normalizeNavItems } from './site-layout-shared.mjs';
4
+ import { SiteDocsThemeSwitch } from './site-docs-theme-switch.mjs';
4
5
 
5
6
  function toDocsLayoutOptions(config) {
6
- return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (config.nav ? { nav: config.nav } : {})), (config.i18n ? { i18n: config.i18n } : {})), (config.githubUrl ? { githubUrl: config.githubUrl } : {})), (config.links ? { links: normalizeNavItems(config.links) } : {})), (config.searchToggle ? { searchToggle: config.searchToggle } : {})), (config.themeSwitch ? { themeSwitch: config.themeSwitch } : {})), (config.sidebar ? { sidebar: config.sidebar } : {})), { tree: config.tree });
7
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (config.nav ? { nav: config.nav } : {})), (config.i18n ? { i18n: config.i18n } : {})), (config.githubUrl ? { githubUrl: config.githubUrl } : {})), (config.links ? { links: normalizeNavItems(config.links) } : {})), (config.searchToggle ? { searchToggle: config.searchToggle } : {})), { slots: Object.assign(Object.assign({}, config.slots), { themeSwitch: SiteDocsThemeSwitch }) }), (config.sidebar ? { sidebar: config.sidebar } : {})), { tree: config.tree });
7
8
  }
8
9
  function SiteDocsLayout({ config, children, }) {
9
10
  const options = toDocsLayoutOptions(config);
@@ -0,0 +1,2 @@
1
+ import type { ComponentProps } from 'react';
2
+ export declare function SiteDocsThemeSwitch(props: ComponentProps<'div'>): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var headerThemeSwitch = require('./header-theme-switch.js');
6
+ var siteThemeContext = require('./site-theme-context.js');
7
+
8
+ function SiteDocsThemeSwitch(props) {
9
+ const themeMode = siteThemeContext.useSiteThemeMode();
10
+ if (themeMode !== 'light-dark-system') {
11
+ return null;
12
+ }
13
+ return jsxRuntime.jsx(headerThemeSwitch.HeaderThemeSwitch, Object.assign({}, props, { mode: "light-dark-system" }));
14
+ }
15
+
16
+ exports.SiteDocsThemeSwitch = SiteDocsThemeSwitch;
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { jsx } from 'react/jsx-runtime';
3
+ import { HeaderThemeSwitch } from './header-theme-switch.mjs';
4
+ import { useSiteThemeMode } from './site-theme-context.mjs';
5
+
6
+ function SiteDocsThemeSwitch(props) {
7
+ const themeMode = useSiteThemeMode();
8
+ if (themeMode !== 'light-dark-system') {
9
+ return null;
10
+ }
11
+ return jsx(HeaderThemeSwitch, Object.assign({}, props, { mode: "light-dark-system" }));
12
+ }
13
+
14
+ export { SiteDocsThemeSwitch };
@@ -47,7 +47,10 @@ export interface SiteBaseLayoutConfig {
47
47
  githubUrl?: string;
48
48
  links?: SiteNavItemConfig[];
49
49
  searchToggle?: HomeLayoutProps['searchToggle'];
50
- themeSwitch?: HomeLayoutProps['themeSwitch'];
50
+ }
51
+ export type SiteThemeSwitchMode = 'light-dark-system' | 'light-only' | 'dark-only';
52
+ export interface SiteThemeSwitchConfig {
53
+ mode?: SiteThemeSwitchMode;
51
54
  }
52
55
  export interface SiteMenuLeafConfig {
53
56
  text: ReactNode;
@@ -38,7 +38,7 @@ function createSiteBaseLayoutConfig(options) {
38
38
  } }, (options.i18n ? { i18n: options.i18n } : {})), (options.githubUrl ? { githubUrl: options.githubUrl } : {}));
39
39
  }
40
40
  function toHomeLayoutOptions(config) {
41
- return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (config.nav ? { nav: config.nav } : {})), (config.i18n ? { i18n: config.i18n } : {})), (config.githubUrl ? { githubUrl: config.githubUrl } : {})), (config.links ? { links: normalizeNavItems(config.links) } : {})), (config.searchToggle ? { searchToggle: config.searchToggle } : {})), (config.themeSwitch ? { themeSwitch: config.themeSwitch } : {}));
41
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (config.nav ? { nav: config.nav } : {})), (config.i18n ? { i18n: config.i18n } : {})), (config.githubUrl ? { githubUrl: config.githubUrl } : {})), (config.links ? { links: normalizeNavItems(config.links) } : {})), (config.searchToggle ? { searchToggle: config.searchToggle } : {}));
42
42
  }
43
43
 
44
44
  exports.createSiteBaseLayoutConfig = createSiteBaseLayoutConfig;
@@ -36,7 +36,7 @@ function createSiteBaseLayoutConfig(options) {
36
36
  } }, (options.i18n ? { i18n: options.i18n } : {})), (options.githubUrl ? { githubUrl: options.githubUrl } : {}));
37
37
  }
38
38
  function toHomeLayoutOptions(config) {
39
- return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (config.nav ? { nav: config.nav } : {})), (config.i18n ? { i18n: config.i18n } : {})), (config.githubUrl ? { githubUrl: config.githubUrl } : {})), (config.links ? { links: normalizeNavItems(config.links) } : {})), (config.searchToggle ? { searchToggle: config.searchToggle } : {})), (config.themeSwitch ? { themeSwitch: config.themeSwitch } : {}));
39
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (config.nav ? { nav: config.nav } : {})), (config.i18n ? { i18n: config.i18n } : {})), (config.githubUrl ? { githubUrl: config.githubUrl } : {})), (config.links ? { links: normalizeNavItems(config.links) } : {})), (config.searchToggle ? { searchToggle: config.searchToggle } : {}));
40
40
  }
41
41
 
42
42
  export { createSiteBaseLayoutConfig, createSiteNavGroup, createSiteNavLink, normalizeNavItems, toHomeLayoutOptions };
@@ -0,0 +1,8 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { SiteThemeSwitchMode } from './site-layout-shared';
3
+ import { type SiteThemeProviderProps } from './site-theme-provider';
4
+ export declare function useSiteThemeMode(): SiteThemeSwitchMode;
5
+ export interface SiteThemeRootProviderProps extends Omit<SiteThemeProviderProps, 'children'> {
6
+ children: ReactNode;
7
+ }
8
+ export declare function SiteThemeRootProvider({ mode, children, ...props }: SiteThemeRootProviderProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var tslib = require('tslib');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var React = require('react');
7
+ var siteThemeProvider = require('./site-theme-provider.js');
8
+
9
+ const SiteThemeModeContext = React.createContext('light-dark-system');
10
+ function useSiteThemeMode() {
11
+ return React.useContext(SiteThemeModeContext);
12
+ }
13
+ function SiteThemeRootProvider(_a) {
14
+ var { mode = 'light-dark-system', children } = _a, props = tslib.__rest(_a, ["mode", "children"]);
15
+ return (jsxRuntime.jsx(SiteThemeModeContext.Provider, { value: mode, children: jsxRuntime.jsx(siteThemeProvider.SiteThemeProvider, Object.assign({}, props, { mode: mode, children: children })) }));
16
+ }
17
+
18
+ exports.SiteThemeRootProvider = SiteThemeRootProvider;
19
+ exports.useSiteThemeMode = useSiteThemeMode;
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ import { __rest } from 'tslib';
3
+ import { jsx } from 'react/jsx-runtime';
4
+ import { createContext, useContext } from 'react';
5
+ import { SiteThemeProvider } from './site-theme-provider.mjs';
6
+
7
+ const SiteThemeModeContext = createContext('light-dark-system');
8
+ function useSiteThemeMode() {
9
+ return useContext(SiteThemeModeContext);
10
+ }
11
+ function SiteThemeRootProvider(_a) {
12
+ var { mode = 'light-dark-system', children } = _a, props = __rest(_a, ["mode", "children"]);
13
+ return (jsx(SiteThemeModeContext.Provider, { value: mode, children: jsx(SiteThemeProvider, Object.assign({}, props, { mode: mode, children: children })) }));
14
+ }
15
+
16
+ export { SiteThemeRootProvider, useSiteThemeMode };
@@ -0,0 +1,8 @@
1
+ import type { ReactNode } from 'react';
2
+ import { type ThemeProviderProps } from 'next-themes';
3
+ import type { SiteThemeSwitchMode } from './site-layout-shared';
4
+ export interface SiteThemeProviderProps extends Omit<ThemeProviderProps, 'children'> {
5
+ mode?: SiteThemeSwitchMode;
6
+ children: ReactNode;
7
+ }
8
+ export declare function SiteThemeProvider({ mode, children, ...props }: SiteThemeProviderProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var tslib = require('tslib');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var nextThemes = require('next-themes');
7
+
8
+ function SiteThemeProvider(_a) {
9
+ var { mode = 'light-dark-system', children } = _a, props = tslib.__rest(_a, ["mode", "children"]);
10
+ return (jsxRuntime.jsx(nextThemes.ThemeProvider, Object.assign({ attribute: "class", disableTransitionOnChange: true }, resolveThemeProviderProps(mode), props, { children: children })));
11
+ }
12
+ function resolveThemeProviderProps(mode) {
13
+ if (mode === 'light-only') {
14
+ return {
15
+ forcedTheme: 'light',
16
+ enableSystem: false,
17
+ defaultTheme: 'light',
18
+ };
19
+ }
20
+ if (mode === 'dark-only') {
21
+ return {
22
+ forcedTheme: 'dark',
23
+ enableSystem: false,
24
+ defaultTheme: 'dark',
25
+ };
26
+ }
27
+ return {
28
+ enableSystem: true,
29
+ defaultTheme: 'system',
30
+ forcedTheme: undefined,
31
+ };
32
+ }
33
+
34
+ exports.SiteThemeProvider = SiteThemeProvider;
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { __rest } from 'tslib';
3
+ import { jsx } from 'react/jsx-runtime';
4
+ import { ThemeProvider } from 'next-themes';
5
+
6
+ function SiteThemeProvider(_a) {
7
+ var { mode = 'light-dark-system', children } = _a, props = __rest(_a, ["mode", "children"]);
8
+ return (jsx(ThemeProvider, Object.assign({ attribute: "class", disableTransitionOnChange: true }, resolveThemeProviderProps(mode), props, { children: children })));
9
+ }
10
+ function resolveThemeProviderProps(mode) {
11
+ if (mode === 'light-only') {
12
+ return {
13
+ forcedTheme: 'light',
14
+ enableSystem: false,
15
+ defaultTheme: 'light',
16
+ };
17
+ }
18
+ if (mode === 'dark-only') {
19
+ return {
20
+ forcedTheme: 'dark',
21
+ enableSystem: false,
22
+ defaultTheme: 'dark',
23
+ };
24
+ }
25
+ return {
26
+ enableSystem: true,
27
+ defaultTheme: 'system',
28
+ forcedTheme: undefined,
29
+ };
30
+ }
31
+
32
+ export { SiteThemeProvider };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windrun-huaiin/third-ui",
3
- "version": "31.0.1",
3
+ "version": "31.2.0",
4
4
  "description": "Third-party integrated UI components for windrun-huaiin projects",
5
5
  "exports": {
6
6
  "./clerk": {
@@ -243,9 +243,9 @@
243
243
  "tslib": "^2.8.1",
244
244
  "unified": "^11.0.5",
245
245
  "zod": "^4.3.6",
246
+ "@windrun-huaiin/lib": "^31.0.1",
246
247
  "@windrun-huaiin/base-ui": "^31.0.0",
247
- "@windrun-huaiin/contracts": "^31.0.0",
248
- "@windrun-huaiin/lib": "^31.0.0"
248
+ "@windrun-huaiin/contracts": "^31.0.0"
249
249
  },
250
250
  "peerDependencies": {
251
251
  "clsx": "^2.1.1",
@@ -35,7 +35,11 @@ import { Popover, PopoverContent, PopoverTrigger } from 'fumadocs-ui/components/
35
35
  import { buttonVariants } from 'fumadocs-ui/components/ui/button';
36
36
  import { useI18n } from 'fumadocs-ui/contexts/i18n';
37
37
  import { HeaderThemeSwitch } from './header-theme-switch';
38
- import type { ExtendedLinkItem } from './site-layout-shared';
38
+ import { useSiteThemeMode } from './site-theme-context';
39
+ import type {
40
+ ExtendedLinkItem,
41
+ SiteThemeSwitchMode,
42
+ } from './site-layout-shared';
39
43
 
40
44
  export type NavbarCSSVars = CSSProperties & {
41
45
  '--fd-banner-height'?: string;
@@ -131,7 +135,6 @@ export function CustomHomeHeader({
131
135
  i18n = false,
132
136
  links,
133
137
  githubUrl,
134
- themeSwitch = {},
135
138
  searchToggle = {},
136
139
  bannerHeight = 0,
137
140
  headerHeight = 2.5,
@@ -142,6 +145,7 @@ export function CustomHomeHeader({
142
145
  mobileBarActionsOrder = DEFAULT_MOBILE_BAR_ACTIONS,
143
146
  mobileMenuActionsOrder = DEFAULT_MOBILE_MENU_ACTIONS,
144
147
  }: CustomHomeHeaderProps) {
148
+ const themeMode = useSiteThemeMode();
145
149
  const finalLinks = useMemo(
146
150
  () => resolveLinkItems({ links, githubUrl }),
147
151
  [links, githubUrl],
@@ -175,8 +179,12 @@ export function CustomHomeHeader({
175
179
  ? searchToggle.components?.lg ?? null
176
180
  : null,
177
181
  theme:
178
- themeSwitch.enabled !== false
179
- ? themeSwitch.component ?? <HeaderThemeSwitch mode={themeSwitch?.mode} />
182
+ shouldShowThemeSwitch(themeMode)
183
+ ? (
184
+ <HeaderThemeSwitch
185
+ mode={normalizeThemeSwitchMode(themeMode)}
186
+ />
187
+ )
180
188
  : null,
181
189
  i18n: i18n ? (
182
190
  <CompactLanguageToggle>
@@ -238,8 +246,12 @@ export function CustomHomeHeader({
238
246
  </CompactLanguageToggle>
239
247
  ) : null,
240
248
  theme:
241
- themeSwitch.enabled !== false
242
- ? themeSwitch.component ?? <HeaderThemeSwitch mode={themeSwitch?.mode} />
249
+ shouldShowThemeSwitch(themeMode)
250
+ ? (
251
+ <HeaderThemeSwitch
252
+ mode={normalizeThemeSwitchMode(themeMode)}
253
+ />
254
+ )
243
255
  : null,
244
256
  };
245
257
  const shouldRenderMobileUtilities = mobileMenuActionsOrder.some(
@@ -356,6 +368,16 @@ export function CustomHomeHeader({
356
368
  );
357
369
  }
358
370
 
371
+ function shouldShowThemeSwitch(mode?: SiteThemeSwitchMode): boolean {
372
+ return mode === 'light-dark-system' || mode == null;
373
+ }
374
+
375
+ function normalizeThemeSwitchMode(
376
+ mode?: SiteThemeSwitchMode,
377
+ ): 'light-dark-system' {
378
+ return 'light-dark-system';
379
+ }
380
+
359
381
  interface CustomNavbarProps extends ComponentProps<'div'> {
360
382
  bannerHeight?: number;
361
383
  headerHeight?: number;
@@ -159,18 +159,18 @@ export function CustomHomeLayout({
159
159
  />
160
160
  )}
161
161
  <HomeLayout
162
- {...homeLayoutProps}
163
- nav={{
164
- ...navOptions,
165
- component: header,
166
- }}
167
- className='bg-neutral-100 dark:bg-neutral-900'
168
- style={layoutStyle}
169
- >
170
- {children}
171
- {showFooter ? footer ?? <Footer locale={locale} localePrefixAsNeeded={localePrefixAsNeeded} defaultLocale={defaultLocale} /> : null}
172
- {showGoToTop ? goToTop ?? <GoToTop /> : null}
173
- </HomeLayout>
162
+ {...homeLayoutProps}
163
+ nav={{
164
+ ...navOptions,
165
+ component: header,
166
+ }}
167
+ className='bg-neutral-100 dark:bg-neutral-900'
168
+ style={layoutStyle}
169
+ >
170
+ {children}
171
+ {showFooter ? footer ?? <Footer locale={locale} localePrefixAsNeeded={localePrefixAsNeeded} defaultLocale={defaultLocale} /> : null}
172
+ {showGoToTop ? goToTop ?? <GoToTop /> : null}
173
+ </HomeLayout>
174
174
  </>
175
175
  );
176
176
  }
@@ -1,20 +1,17 @@
1
1
  import type { ComponentProps, ReactNode } from 'react';
2
2
  import { NextProvider } from 'fumadocs-core/framework/next';
3
3
  import { I18nProvider, type I18nProviderProps } from 'fumadocs-ui/contexts/i18n';
4
- import { ThemeProvider, type ThemeProviderProps } from 'next-themes';
4
+ import { SiteThemeRootProvider } from './site-theme-context';
5
+ import type { SiteThemeProviderProps } from './site-theme-provider';
5
6
 
6
7
  type NextProviderComponents = {
7
8
  Link?: ComponentProps<typeof NextProvider>['Link'];
8
9
  Image?: ComponentProps<typeof NextProvider>['Image'];
9
10
  };
10
11
 
11
- type ThemeOptions = ThemeProviderProps & {
12
- enabled?: boolean;
13
- };
14
-
15
12
  export interface DocsRootProviderProps {
16
13
  i18n: Omit<I18nProviderProps, 'children'>;
17
- theme?: ThemeOptions;
14
+ theme?: Omit<SiteThemeProviderProps, 'children'>;
18
15
  components?: NextProviderComponents;
19
16
  children: ReactNode;
20
17
  }
@@ -25,34 +22,18 @@ export function DocsRootProvider({
25
22
  components,
26
23
  children,
27
24
  }: DocsRootProviderProps) {
28
- let body = children;
29
-
30
- if (theme.enabled !== false) {
31
- body = (
32
- <ThemeProvider
33
- attribute="class"
34
- defaultTheme="system"
35
- enableSystem
36
- disableTransitionOnChange
37
- {...theme}
38
- >
39
- {body}
40
- </ThemeProvider>
41
- );
42
- }
43
-
44
- body = (
45
- <I18nProvider {...i18n}>
46
- {body}
47
- </I18nProvider>
48
- );
25
+ const themeMode = theme.mode ?? 'light-dark-system';
49
26
 
50
27
  return (
51
28
  <NextProvider
52
29
  Link={components?.Link}
53
30
  Image={components?.Image}
54
31
  >
55
- {body}
32
+ <I18nProvider {...i18n}>
33
+ <SiteThemeRootProvider {...theme} mode={themeMode}>
34
+ {children}
35
+ </SiteThemeRootProvider>
36
+ </I18nProvider>
56
37
  </NextProvider>
57
38
  );
58
39
  }
@@ -5,6 +5,7 @@ import { AirplayIcon, MoonIcon, SunIcon } from '@windrun-huaiin/base-ui/icons';
5
5
  import { useTheme } from 'next-themes';
6
6
  import { type ComponentProps, useEffect, useState } from 'react';
7
7
  import { cn } from '@windrun-huaiin/lib/utils';
8
+ import type { SiteThemeSwitchMode } from './site-layout-shared';
8
9
 
9
10
  const itemVariants = cva('inline-flex size-6.5 items-center justify-center rounded-full p-1.5', {
10
11
  variants: {
@@ -18,15 +19,14 @@ const itemVariants = cva('inline-flex size-6.5 items-center justify-center round
18
19
  const full = [['light', SunIcon] as const, ['dark', MoonIcon] as const, ['system', AirplayIcon] as const];
19
20
 
20
21
  export interface HeaderThemeSwitchProps extends ComponentProps<'div'> {
21
- mode?: 'light-dark' | 'light-dark-system';
22
+ mode?: 'light-dark-system';
22
23
  }
23
24
 
24
25
  export function HeaderThemeSwitch({
25
26
  className,
26
- mode = 'light-dark',
27
27
  ...props
28
28
  }: HeaderThemeSwitchProps) {
29
- const { setTheme, theme, resolvedTheme } = useTheme();
29
+ const { setTheme, theme } = useTheme();
30
30
  const [mounted, setMounted] = useState(false);
31
31
 
32
32
  useEffect(() => {
@@ -39,35 +39,6 @@ export function HeaderThemeSwitch({
39
39
  );
40
40
  const iconClassName = 'size-3.5 text-neutral-600 dark:text-neutral-300';
41
41
 
42
- if (mode === 'light-dark') {
43
- const value = mounted ? resolvedTheme : null;
44
-
45
- return (
46
- <button
47
- type="button"
48
- className={container}
49
- aria-label="Toggle Theme"
50
- onClick={() => setTheme(value === 'light' ? 'dark' : 'light')}
51
- data-theme-toggle=""
52
- >
53
- {full.map(([key, Icon]) => {
54
- if (key === 'system') return null;
55
-
56
- return (
57
- <Icon
58
- key={key}
59
- fill="currentColor"
60
- className={cn(
61
- itemVariants({ active: value === key }),
62
- iconClassName,
63
- )}
64
- />
65
- );
66
- })}
67
- </button>
68
- );
69
- }
70
-
71
42
  const value = mounted ? theme : null;
72
43
 
73
44
  return (
@@ -4,10 +4,12 @@ import {
4
4
  normalizeNavItems,
5
5
  type SiteBaseLayoutConfig,
6
6
  } from './site-layout-shared';
7
+ import { SiteDocsThemeSwitch } from './site-docs-theme-switch';
7
8
 
8
9
  export interface SiteDocsLayoutConfig extends SiteBaseLayoutConfig {
9
10
  tree: DocsLayoutProps['tree'];
10
11
  sidebar?: DocsLayoutProps['sidebar'];
12
+ slots?: DocsLayoutProps['slots'];
11
13
  }
12
14
 
13
15
  function toDocsLayoutOptions(config: SiteDocsLayoutConfig): DocsLayoutProps {
@@ -17,7 +19,10 @@ function toDocsLayoutOptions(config: SiteDocsLayoutConfig): DocsLayoutProps {
17
19
  ...(config.githubUrl ? { githubUrl: config.githubUrl } : {}),
18
20
  ...(config.links ? { links: normalizeNavItems(config.links) } : {}),
19
21
  ...(config.searchToggle ? { searchToggle: config.searchToggle } : {}),
20
- ...(config.themeSwitch ? { themeSwitch: config.themeSwitch } : {}),
22
+ slots: {
23
+ ...config.slots,
24
+ themeSwitch: SiteDocsThemeSwitch,
25
+ },
21
26
  ...(config.sidebar ? { sidebar: config.sidebar } : {}),
22
27
  tree: config.tree,
23
28
  };
@@ -31,5 +36,6 @@ export function SiteDocsLayout({
31
36
  children: ReactNode;
32
37
  }) {
33
38
  const options = toDocsLayoutOptions(config);
39
+
34
40
  return <DocsLayout {...options}>{children}</DocsLayout>;
35
41
  }
@@ -0,0 +1,15 @@
1
+ 'use client';
2
+
3
+ import type { ComponentProps } from 'react';
4
+ import { HeaderThemeSwitch } from './header-theme-switch';
5
+ import { useSiteThemeMode } from './site-theme-context';
6
+
7
+ export function SiteDocsThemeSwitch(props: ComponentProps<'div'>) {
8
+ const themeMode = useSiteThemeMode();
9
+
10
+ if (themeMode !== 'light-dark-system') {
11
+ return null;
12
+ }
13
+
14
+ return <HeaderThemeSwitch {...props} mode="light-dark-system" />;
15
+ }
@@ -58,7 +58,15 @@ export interface SiteBaseLayoutConfig {
58
58
  githubUrl?: string;
59
59
  links?: SiteNavItemConfig[];
60
60
  searchToggle?: HomeLayoutProps['searchToggle'];
61
- themeSwitch?: HomeLayoutProps['themeSwitch'];
61
+ }
62
+
63
+ export type SiteThemeSwitchMode =
64
+ | 'light-dark-system'
65
+ | 'light-only'
66
+ | 'dark-only';
67
+
68
+ export interface SiteThemeSwitchConfig {
69
+ mode?: SiteThemeSwitchMode;
62
70
  }
63
71
 
64
72
  export interface SiteMenuLeafConfig {
@@ -185,6 +193,5 @@ export function toHomeLayoutOptions(config: SiteBaseLayoutConfig): HomeLayoutPro
185
193
  ...(config.githubUrl ? { githubUrl: config.githubUrl } : {}),
186
194
  ...(config.links ? { links: normalizeNavItems(config.links) } : {}),
187
195
  ...(config.searchToggle ? { searchToggle: config.searchToggle } : {}),
188
- ...(config.themeSwitch ? { themeSwitch: config.themeSwitch } : {}),
189
196
  };
190
197
  }
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+
3
+ import { createContext, type ReactNode, useContext } from 'react';
4
+ import type { SiteThemeSwitchMode } from './site-layout-shared';
5
+ import { SiteThemeProvider, type SiteThemeProviderProps } from './site-theme-provider';
6
+
7
+ const SiteThemeModeContext = createContext<SiteThemeSwitchMode>('light-dark-system');
8
+
9
+ export function useSiteThemeMode(): SiteThemeSwitchMode {
10
+ return useContext(SiteThemeModeContext);
11
+ }
12
+
13
+ export interface SiteThemeRootProviderProps
14
+ extends Omit<SiteThemeProviderProps, 'children'> {
15
+ children: ReactNode;
16
+ }
17
+
18
+ export function SiteThemeRootProvider({
19
+ mode = 'light-dark-system',
20
+ children,
21
+ ...props
22
+ }: SiteThemeRootProviderProps) {
23
+ return (
24
+ <SiteThemeModeContext.Provider value={mode}>
25
+ <SiteThemeProvider {...props} mode={mode}>
26
+ {children}
27
+ </SiteThemeProvider>
28
+ </SiteThemeModeContext.Provider>
29
+ );
30
+ }
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import { ThemeProvider, type ThemeProviderProps } from 'next-themes';
5
+ import type { SiteThemeSwitchMode } from './site-layout-shared';
6
+
7
+ export interface SiteThemeProviderProps extends Omit<ThemeProviderProps, 'children'> {
8
+ mode?: SiteThemeSwitchMode;
9
+ children: ReactNode;
10
+ }
11
+
12
+ export function SiteThemeProvider({
13
+ mode = 'light-dark-system',
14
+ children,
15
+ ...props
16
+ }: SiteThemeProviderProps) {
17
+ return (
18
+ <ThemeProvider
19
+ attribute="class"
20
+ disableTransitionOnChange
21
+ {...resolveThemeProviderProps(mode)}
22
+ {...props}
23
+ >
24
+ {children}
25
+ </ThemeProvider>
26
+ );
27
+ }
28
+
29
+ function resolveThemeProviderProps(mode: SiteThemeSwitchMode): ThemeProviderProps {
30
+ if (mode === 'light-only') {
31
+ return {
32
+ forcedTheme: 'light',
33
+ enableSystem: false,
34
+ defaultTheme: 'light',
35
+ };
36
+ }
37
+
38
+ if (mode === 'dark-only') {
39
+ return {
40
+ forcedTheme: 'dark',
41
+ enableSystem: false,
42
+ defaultTheme: 'dark',
43
+ };
44
+ }
45
+
46
+ return {
47
+ enableSystem: true,
48
+ defaultTheme: 'system',
49
+ forcedTheme: undefined,
50
+ };
51
+ }