@windrun-huaiin/third-ui 31.0.0 → 31.1.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 (42) hide show
  1. package/dist/fuma/base/custom-header.d.ts +3 -1
  2. package/dist/fuma/base/custom-header.js +13 -7
  3. package/dist/fuma/base/custom-header.mjs +13 -7
  4. package/dist/fuma/base/custom-home-layout.d.ts +6 -1
  5. package/dist/fuma/base/custom-home-layout.js +6 -3
  6. package/dist/fuma/base/custom-home-layout.mjs +6 -3
  7. package/dist/fuma/base/docs-root-provider.d.ts +1 -6
  8. package/dist/fuma/base/docs-root-provider.js +2 -8
  9. package/dist/fuma/base/docs-root-provider.mjs +2 -8
  10. package/dist/fuma/base/header-theme-switch.d.ts +2 -1
  11. package/dist/fuma/base/site-docs-layout.d.ts +1 -0
  12. package/dist/fuma/base/site-docs-layout.js +22 -2
  13. package/dist/fuma/base/site-docs-layout.mjs +22 -2
  14. package/dist/fuma/base/site-home-layout.js +2 -2
  15. package/dist/fuma/base/site-home-layout.mjs +2 -2
  16. package/dist/fuma/base/site-layout-shared.d.ts +5 -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-provider.d.ts +8 -0
  20. package/dist/fuma/base/site-theme-provider.js +41 -0
  21. package/dist/fuma/base/site-theme-provider.mjs +39 -0
  22. package/dist/fuma/mdx/fuma-github-info.d.ts +1 -2
  23. package/dist/fuma/mdx/fuma-github-info.js +3 -6
  24. package/dist/fuma/mdx/fuma-github-info.mjs +3 -6
  25. package/dist/main/credit/credit-overview-nav-client.d.ts +12 -0
  26. package/dist/main/credit/credit-overview-nav-client.js +65 -0
  27. package/dist/main/credit/credit-overview-nav-client.mjs +63 -0
  28. package/dist/main/credit/index.d.ts +2 -0
  29. package/dist/main/credit/index.js +2 -0
  30. package/dist/main/credit/index.mjs +1 -0
  31. package/package.json +3 -3
  32. package/src/fuma/base/custom-header.tsx +30 -6
  33. package/src/fuma/base/custom-home-layout.tsx +24 -12
  34. package/src/fuma/base/docs-root-provider.tsx +3 -30
  35. package/src/fuma/base/header-theme-switch.tsx +2 -1
  36. package/src/fuma/base/site-docs-layout.tsx +23 -2
  37. package/src/fuma/base/site-home-layout.tsx +1 -0
  38. package/src/fuma/base/site-layout-shared.tsx +11 -2
  39. package/src/fuma/base/site-theme-provider.tsx +59 -0
  40. package/src/fuma/mdx/fuma-github-info.tsx +3 -8
  41. package/src/main/credit/credit-overview-nav-client.tsx +95 -0
  42. package/src/main/credit/index.ts +5 -0
@@ -5,7 +5,7 @@ import { useState, useEffect } from 'react';
5
5
  import { ExternalLinkIcon, StarIcon } from '@windrun-huaiin/base-ui/icons';
6
6
 
7
7
  // Loading state component
8
- function GitHubInfoSkeleton({ owner, repo, className }) {
8
+ function GitHubInfoSkeleton({ className }) {
9
9
  return (jsxs("div", { className: `flex flex-col gap-1.5 p-2 rounded-lg text-sm text-fd-foreground/80 lg:flex-row lg:items-center animate-pulse ${className}`, children: [jsxs("div", { className: "flex items-center gap-2", children: [jsx("div", { className: "size-3.5 bg-fd-muted rounded" }), jsx("div", { className: "h-4 bg-fd-muted rounded w-20" })] }), jsx("div", { className: "h-3 bg-fd-muted rounded w-8" })] }));
10
10
  }
11
11
  // Error state component - graceful fallback
@@ -44,7 +44,7 @@ function humanizeNumber(num) {
44
44
  * - 🎨 Three states: loading, success, error
45
45
  * - 💯 Not affected by network issues causing page crashes
46
46
  */
47
- function FumaGithubInfo({ owner, repo, token, className }) {
47
+ function FumaGithubInfo({ owner, repo, className }) {
48
48
  const [data, setData] = useState(null);
49
49
  const [loading, setLoading] = useState(true);
50
50
  const [error, setError] = useState(null);
@@ -59,9 +59,6 @@ function FumaGithubInfo({ owner, repo, token, className }) {
59
59
  const headers = new Headers({
60
60
  'Accept': 'application/vnd.github.v3+json',
61
61
  });
62
- if (token) {
63
- headers.set('Authorization', `Bearer ${token}`);
64
- }
65
62
  const response = yield fetch(`https://api.github.com/repos/${owner}/${repo}`, {
66
63
  signal: controller.signal,
67
64
  headers,
@@ -95,7 +92,7 @@ function FumaGithubInfo({ owner, repo, token, className }) {
95
92
  }
96
93
  });
97
94
  fetchRepoData();
98
- }, [owner, repo, token]);
95
+ }, [owner, repo]);
99
96
  // Loading state
100
97
  if (loading) {
101
98
  return jsx(GitHubInfoSkeleton, { owner: owner, repo: repo, className: className });
@@ -0,0 +1,12 @@
1
+ import { type CreditOverviewTranslations } from './credit-overview-client';
2
+ import type { CreditOverviewData } from './types';
3
+ export interface CreditOverviewPayload {
4
+ data: CreditOverviewData;
5
+ totalLabel: string;
6
+ translations: CreditOverviewTranslations;
7
+ }
8
+ export interface CreditOverviewNavClientProps {
9
+ locale: string;
10
+ endpoint: string;
11
+ }
12
+ export declare function CreditOverviewNavClient({ locale, endpoint, }: CreditOverviewNavClientProps): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,65 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var tslib = require('tslib');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var nextjs = require('@clerk/nextjs');
7
+ var React = require('react');
8
+ var creditNavButton = require('./credit-nav-button.js');
9
+ var creditOverviewClient = require('./credit-overview-client.js');
10
+
11
+ function buildCreditOverviewUrl(endpoint, locale) {
12
+ const url = new URL(endpoint, window.location.origin);
13
+ url.searchParams.set('locale', locale);
14
+ return url.toString();
15
+ }
16
+ function CreditOverviewNavClient({ locale, endpoint, }) {
17
+ const { isLoaded, isSignedIn, userId } = nextjs.useAuth();
18
+ const [payload, setPayload] = React.useState(null);
19
+ React.useEffect(() => {
20
+ if (!isLoaded) {
21
+ return;
22
+ }
23
+ if (!isSignedIn) {
24
+ setPayload(null);
25
+ return;
26
+ }
27
+ const controller = new AbortController();
28
+ function loadCreditOverview() {
29
+ return tslib.__awaiter(this, void 0, void 0, function* () {
30
+ try {
31
+ const response = yield fetch(buildCreditOverviewUrl(endpoint, locale), {
32
+ credentials: 'same-origin',
33
+ signal: controller.signal,
34
+ });
35
+ if (!response.ok) {
36
+ if (!controller.signal.aborted) {
37
+ setPayload(null);
38
+ }
39
+ return;
40
+ }
41
+ const nextPayload = (yield response.json());
42
+ if (!controller.signal.aborted) {
43
+ setPayload(nextPayload);
44
+ }
45
+ }
46
+ catch (error) {
47
+ if (!controller.signal.aborted) {
48
+ setPayload(null);
49
+ console.warn('[CreditOverviewNavClient] Failed to load credit overview', error);
50
+ }
51
+ }
52
+ });
53
+ }
54
+ loadCreditOverview();
55
+ return () => {
56
+ controller.abort();
57
+ };
58
+ }, [endpoint, isLoaded, isSignedIn, locale, userId]);
59
+ if (!payload) {
60
+ return null;
61
+ }
62
+ return (jsxRuntime.jsx(creditNavButton.CreditNavButton, { locale: locale, totalBalance: payload.data.totalBalance, totalLabel: payload.totalLabel, children: jsxRuntime.jsx(creditOverviewClient.CreditOverviewClient, { locale: locale, data: payload.data, translations: payload.translations }) }));
63
+ }
64
+
65
+ exports.CreditOverviewNavClient = CreditOverviewNavClient;
@@ -0,0 +1,63 @@
1
+ "use client";
2
+ import { __awaiter } from 'tslib';
3
+ import { jsx } from 'react/jsx-runtime';
4
+ import { useAuth } from '@clerk/nextjs';
5
+ import { useState, useEffect } from 'react';
6
+ import { CreditNavButton } from './credit-nav-button.mjs';
7
+ import { CreditOverviewClient } from './credit-overview-client.mjs';
8
+
9
+ function buildCreditOverviewUrl(endpoint, locale) {
10
+ const url = new URL(endpoint, window.location.origin);
11
+ url.searchParams.set('locale', locale);
12
+ return url.toString();
13
+ }
14
+ function CreditOverviewNavClient({ locale, endpoint, }) {
15
+ const { isLoaded, isSignedIn, userId } = useAuth();
16
+ const [payload, setPayload] = useState(null);
17
+ useEffect(() => {
18
+ if (!isLoaded) {
19
+ return;
20
+ }
21
+ if (!isSignedIn) {
22
+ setPayload(null);
23
+ return;
24
+ }
25
+ const controller = new AbortController();
26
+ function loadCreditOverview() {
27
+ return __awaiter(this, void 0, void 0, function* () {
28
+ try {
29
+ const response = yield fetch(buildCreditOverviewUrl(endpoint, locale), {
30
+ credentials: 'same-origin',
31
+ signal: controller.signal,
32
+ });
33
+ if (!response.ok) {
34
+ if (!controller.signal.aborted) {
35
+ setPayload(null);
36
+ }
37
+ return;
38
+ }
39
+ const nextPayload = (yield response.json());
40
+ if (!controller.signal.aborted) {
41
+ setPayload(nextPayload);
42
+ }
43
+ }
44
+ catch (error) {
45
+ if (!controller.signal.aborted) {
46
+ setPayload(null);
47
+ console.warn('[CreditOverviewNavClient] Failed to load credit overview', error);
48
+ }
49
+ }
50
+ });
51
+ }
52
+ loadCreditOverview();
53
+ return () => {
54
+ controller.abort();
55
+ };
56
+ }, [endpoint, isLoaded, isSignedIn, locale, userId]);
57
+ if (!payload) {
58
+ return null;
59
+ }
60
+ return (jsx(CreditNavButton, { locale: locale, totalBalance: payload.data.totalBalance, totalLabel: payload.totalLabel, children: jsx(CreditOverviewClient, { locale: locale, data: payload.data, translations: payload.translations }) }));
61
+ }
62
+
63
+ export { CreditOverviewNavClient };
@@ -1,4 +1,6 @@
1
1
  export { CreditOverviewClient } from './credit-overview-client';
2
+ export { CreditOverviewNavClient } from './credit-overview-nav-client';
2
3
  export { CreditNavButton } from './credit-nav-button';
3
4
  export type { CreditOverviewTranslations } from './credit-overview-client';
5
+ export type { CreditOverviewNavClientProps, CreditOverviewPayload, } from './credit-overview-nav-client';
4
6
  export type { CreditOverviewData, CreditBucket, CreditBucketStatus, SubscriptionInfo, } from './types';
@@ -2,9 +2,11 @@
2
2
  'use strict';
3
3
 
4
4
  var creditOverviewClient = require('./credit-overview-client.js');
5
+ var creditOverviewNavClient = require('./credit-overview-nav-client.js');
5
6
  var creditNavButton = require('./credit-nav-button.js');
6
7
 
7
8
 
8
9
 
9
10
  exports.CreditOverviewClient = creditOverviewClient.CreditOverviewClient;
11
+ exports.CreditOverviewNavClient = creditOverviewNavClient.CreditOverviewNavClient;
10
12
  exports.CreditNavButton = creditNavButton.CreditNavButton;
@@ -1,3 +1,4 @@
1
1
  "use client";
2
2
  export { CreditOverviewClient } from './credit-overview-client.mjs';
3
+ export { CreditOverviewNavClient } from './credit-overview-nav-client.mjs';
3
4
  export { CreditNavButton } from './credit-nav-button.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windrun-huaiin/third-ui",
3
- "version": "31.0.0",
3
+ "version": "31.1.0",
4
4
  "description": "Third-party integrated UI components for windrun-huaiin projects",
5
5
  "exports": {
6
6
  "./clerk": {
@@ -244,8 +244,8 @@
244
244
  "unified": "^11.0.5",
245
245
  "zod": "^4.3.6",
246
246
  "@windrun-huaiin/base-ui": "^31.0.0",
247
- "@windrun-huaiin/lib": "^31.0.0",
248
- "@windrun-huaiin/contracts": "^31.0.0"
247
+ "@windrun-huaiin/contracts": "^31.0.0",
248
+ "@windrun-huaiin/lib": "^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 type {
39
+ ExtendedLinkItem,
40
+ SiteThemeSwitchConfig,
41
+ SiteThemeSwitchMode,
42
+ } from './site-layout-shared';
39
43
 
40
44
  export type NavbarCSSVars = CSSProperties & {
41
45
  '--fd-banner-height'?: string;
@@ -47,7 +51,8 @@ const PrefetchLinkItem = LinkItem as (
47
51
  props: ComponentProps<typeof LinkItem> & { prefetch?: boolean },
48
52
  ) => ReactNode;
49
53
 
50
- export interface CustomHomeHeaderProps extends HomeLayoutProps {
54
+ export interface CustomHomeHeaderProps
55
+ extends Omit<HomeLayoutProps, 'themeSwitch'> {
51
56
  /**
52
57
  * Banner height in rem units
53
58
  *
@@ -92,6 +97,7 @@ export interface CustomHomeHeaderProps extends HomeLayoutProps {
92
97
  * Control order of utilities inside the mobile dropdown.
93
98
  */
94
99
  mobileMenuActionsOrder?: MobileMenuAction[];
100
+ themeSwitch?: SiteThemeSwitchConfig;
95
101
  }
96
102
 
97
103
  export type DesktopAction =
@@ -175,8 +181,12 @@ export function CustomHomeHeader({
175
181
  ? searchToggle.components?.lg ?? null
176
182
  : null,
177
183
  theme:
178
- themeSwitch.enabled !== false
179
- ? themeSwitch.component ?? <HeaderThemeSwitch mode={themeSwitch?.mode} />
184
+ shouldShowThemeSwitch(themeSwitch?.mode)
185
+ ? (
186
+ <HeaderThemeSwitch
187
+ mode={normalizeThemeSwitchMode(themeSwitch?.mode)}
188
+ />
189
+ )
180
190
  : null,
181
191
  i18n: i18n ? (
182
192
  <CompactLanguageToggle>
@@ -238,8 +248,12 @@ export function CustomHomeHeader({
238
248
  </CompactLanguageToggle>
239
249
  ) : null,
240
250
  theme:
241
- themeSwitch.enabled !== false
242
- ? themeSwitch.component ?? <HeaderThemeSwitch mode={themeSwitch?.mode} />
251
+ shouldShowThemeSwitch(themeSwitch?.mode)
252
+ ? (
253
+ <HeaderThemeSwitch
254
+ mode={normalizeThemeSwitchMode(themeSwitch?.mode)}
255
+ />
256
+ )
243
257
  : null,
244
258
  };
245
259
  const shouldRenderMobileUtilities = mobileMenuActionsOrder.some(
@@ -356,6 +370,16 @@ export function CustomHomeHeader({
356
370
  );
357
371
  }
358
372
 
373
+ function shouldShowThemeSwitch(mode?: SiteThemeSwitchMode): boolean {
374
+ return mode === 'light-dark' || mode === 'light-dark-system' || mode == null;
375
+ }
376
+
377
+ function normalizeThemeSwitchMode(
378
+ mode?: SiteThemeSwitchMode,
379
+ ): 'light-dark' | 'light-dark-system' {
380
+ return mode === 'light-dark' ? 'light-dark' : 'light-dark-system';
381
+ }
382
+
359
383
  interface CustomNavbarProps extends ComponentProps<'div'> {
360
384
  bannerHeight?: number;
361
385
  headerHeight?: number;
@@ -3,13 +3,16 @@ import { HomeLayout, type HomeLayoutProps } from 'fumadocs-ui/layouts/home';
3
3
  import { FumaBannerSuit } from '../fuma-banner-suit';
4
4
  import { Footer } from '../../main/footer';
5
5
  import { GoToTop } from '../../main/go-to-top';
6
+ import { SiteThemeProvider } from './site-theme-provider';
6
7
  import {
7
8
  NavbarCSSVars,
8
9
  CustomHomeHeader,
9
10
  type DesktopAction,
10
11
  type MobileBarAction,
11
12
  type MobileMenuAction,
13
+ type CustomHomeHeaderProps,
12
14
  } from './custom-header';
15
+ import type { SiteThemeSwitchConfig } from './site-layout-shared';
13
16
 
14
17
  // - Set bannerHeight/headerHeight to the rem values expected by the project. Use bannerHeight = 0 when there is no banner.
15
18
  // - layoutStyle passes the variables to HomeLayout's main element, offsetting content without has-banner/no-banner classes.
@@ -91,6 +94,10 @@ export interface CustomHomeLayoutProps {
91
94
  * The default locale for the application (default: 'en')
92
95
  */
93
96
  defaultLocale?: string;
97
+ /**
98
+ * Theme mode for this layout group.
99
+ */
100
+ themeSwitch?: SiteThemeSwitchConfig;
94
101
  children?: ReactNode;
95
102
  }
96
103
 
@@ -119,6 +126,7 @@ export function CustomHomeLayout({
119
126
  actionOrders,
120
127
  localePrefixAsNeeded = true,
121
128
  defaultLocale = 'en',
129
+ themeSwitch,
122
130
  }: CustomHomeLayoutProps) {
123
131
  const resolvedBannerHeight = bannerHeight ?? (showBanner ? 3 : 0.5);
124
132
  const resolvedPaddingTop =
@@ -139,6 +147,7 @@ export function CustomHomeLayout({
139
147
  <CustomHomeHeader
140
148
  {...homeLayoutProps}
141
149
  nav={navOptions}
150
+ themeSwitch={themeSwitch}
142
151
  bannerHeight={resolvedBannerHeight}
143
152
  headerHeight={headerHeight}
144
153
  navbarClassName={navbarClassName}
@@ -148,6 +157,7 @@ export function CustomHomeLayout({
148
157
  mobileMenuActionsOrder={actionOrders?.mobileMenu}
149
158
  />
150
159
  );
160
+ const themeMode = themeSwitch?.mode ?? 'light-dark-system';
151
161
 
152
162
  return (
153
163
  <>
@@ -158,19 +168,21 @@ export function CustomHomeLayout({
158
168
  floating={floatingNav}
159
169
  />
160
170
  )}
171
+ <SiteThemeProvider mode={themeMode}>
161
172
  <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>
173
+ {...homeLayoutProps}
174
+ nav={{
175
+ ...navOptions,
176
+ component: header,
177
+ }}
178
+ className='bg-neutral-100 dark:bg-neutral-900'
179
+ style={layoutStyle}
180
+ >
181
+ {children}
182
+ {showFooter ? footer ?? <Footer locale={locale} localePrefixAsNeeded={localePrefixAsNeeded} defaultLocale={defaultLocale} /> : null}
183
+ {showGoToTop ? goToTop ?? <GoToTop /> : null}
184
+ </HomeLayout>
185
+ </SiteThemeProvider>
174
186
  </>
175
187
  );
176
188
  }
@@ -1,58 +1,31 @@
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';
5
4
 
6
5
  type NextProviderComponents = {
7
6
  Link?: ComponentProps<typeof NextProvider>['Link'];
8
7
  Image?: ComponentProps<typeof NextProvider>['Image'];
9
8
  };
10
9
 
11
- type ThemeOptions = ThemeProviderProps & {
12
- enabled?: boolean;
13
- };
14
-
15
10
  export interface DocsRootProviderProps {
16
11
  i18n: Omit<I18nProviderProps, 'children'>;
17
- theme?: ThemeOptions;
18
12
  components?: NextProviderComponents;
19
13
  children: ReactNode;
20
14
  }
21
15
 
22
16
  export function DocsRootProvider({
23
17
  i18n,
24
- theme = {},
25
18
  components,
26
19
  children,
27
20
  }: 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
- );
49
-
50
21
  return (
51
22
  <NextProvider
52
23
  Link={components?.Link}
53
24
  Image={components?.Image}
54
25
  >
55
- {body}
26
+ <I18nProvider {...i18n}>
27
+ {children}
28
+ </I18nProvider>
56
29
  </NextProvider>
57
30
  );
58
31
  }
@@ -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,7 +19,7 @@ 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?: Exclude<SiteThemeSwitchMode, 'light-only' | 'dark-only'>;
22
23
  }
23
24
 
24
25
  export function HeaderThemeSwitch({
@@ -4,20 +4,36 @@ import {
4
4
  normalizeNavItems,
5
5
  type SiteBaseLayoutConfig,
6
6
  } from './site-layout-shared';
7
+ import { SiteThemeProvider } from './site-theme-provider';
7
8
 
8
9
  export interface SiteDocsLayoutConfig extends SiteBaseLayoutConfig {
9
10
  tree: DocsLayoutProps['tree'];
10
11
  sidebar?: DocsLayoutProps['sidebar'];
12
+ themeProvider?: boolean;
11
13
  }
12
14
 
13
15
  function toDocsLayoutOptions(config: SiteDocsLayoutConfig): DocsLayoutProps {
16
+ const themeMode = config.themeSwitch?.mode ?? 'light-dark-system';
17
+ const shouldShowThemeSwitch =
18
+ config.themeProvider !== false &&
19
+ (themeMode === 'light-dark' || themeMode === 'light-dark-system');
14
20
  return {
15
21
  ...(config.nav ? { nav: config.nav } : {}),
16
22
  ...(config.i18n ? { i18n: config.i18n } : {}),
17
23
  ...(config.githubUrl ? { githubUrl: config.githubUrl } : {}),
18
24
  ...(config.links ? { links: normalizeNavItems(config.links) } : {}),
19
25
  ...(config.searchToggle ? { searchToggle: config.searchToggle } : {}),
20
- ...(config.themeSwitch ? { themeSwitch: config.themeSwitch } : {}),
26
+ ...(shouldShowThemeSwitch
27
+ ? {
28
+ themeSwitch: {
29
+ mode: themeMode,
30
+ },
31
+ }
32
+ : {
33
+ themeSwitch: {
34
+ enabled: false,
35
+ },
36
+ }),
21
37
  ...(config.sidebar ? { sidebar: config.sidebar } : {}),
22
38
  tree: config.tree,
23
39
  };
@@ -31,5 +47,10 @@ export function SiteDocsLayout({
31
47
  children: ReactNode;
32
48
  }) {
33
49
  const options = toDocsLayoutOptions(config);
34
- return <DocsLayout {...options}>{children}</DocsLayout>;
50
+ const themeMode = config.themeSwitch?.mode ?? 'light-dark-system';
51
+ const body = <DocsLayout {...options}>{children}</DocsLayout>;
52
+
53
+ if (config.themeProvider === false) return body;
54
+
55
+ return <SiteThemeProvider mode={themeMode}>{body}</SiteThemeProvider>;
35
56
  }
@@ -72,6 +72,7 @@ export function SiteHomeLayout({
72
72
  ...(showBanner != null ? { showBanner } : {}),
73
73
  ...(showFooter != null ? { showFooter } : {}),
74
74
  ...(showGoToTop != null ? { showGoToTop } : {}),
75
+ ...(config.themeSwitch ? { themeSwitch: config.themeSwitch } : {}),
75
76
  };
76
77
 
77
78
  return <CustomHomeLayout {...layoutProps}>{children}</CustomHomeLayout>;
@@ -58,7 +58,17 @@ export interface SiteBaseLayoutConfig {
58
58
  githubUrl?: string;
59
59
  links?: SiteNavItemConfig[];
60
60
  searchToggle?: HomeLayoutProps['searchToggle'];
61
- themeSwitch?: HomeLayoutProps['themeSwitch'];
61
+ themeSwitch?: SiteThemeSwitchConfig;
62
+ }
63
+
64
+ export type SiteThemeSwitchMode =
65
+ | 'light-dark-system'
66
+ | 'light-dark'
67
+ | 'light-only'
68
+ | 'dark-only';
69
+
70
+ export interface SiteThemeSwitchConfig {
71
+ mode?: SiteThemeSwitchMode;
62
72
  }
63
73
 
64
74
  export interface SiteMenuLeafConfig {
@@ -185,6 +195,5 @@ export function toHomeLayoutOptions(config: SiteBaseLayoutConfig): HomeLayoutPro
185
195
  ...(config.githubUrl ? { githubUrl: config.githubUrl } : {}),
186
196
  ...(config.links ? { links: normalizeNavItems(config.links) } : {}),
187
197
  ...(config.searchToggle ? { searchToggle: config.searchToggle } : {}),
188
- ...(config.themeSwitch ? { themeSwitch: config.themeSwitch } : {}),
189
198
  };
190
199
  }
@@ -0,0 +1,59 @@
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
+ if (mode === 'light-dark') {
47
+ return {
48
+ enableSystem: false,
49
+ defaultTheme: 'light',
50
+ forcedTheme: undefined,
51
+ };
52
+ }
53
+
54
+ return {
55
+ enableSystem: true,
56
+ defaultTheme: 'system',
57
+ forcedTheme: undefined,
58
+ };
59
+ }
@@ -6,7 +6,6 @@ import { ExternalLinkIcon, StarIcon } from '@windrun-huaiin/base-ui/icons';
6
6
  interface FumaGithubInfoProps {
7
7
  owner: string;
8
8
  repo: string;
9
- token?: string;
10
9
  className?: string;
11
10
  }
12
11
 
@@ -16,7 +15,7 @@ interface GitHubRepoData {
16
15
  }
17
16
 
18
17
  // Loading state component
19
- function GitHubInfoSkeleton({ owner, repo, className }: Pick<FumaGithubInfoProps, 'owner' | 'repo' | 'className'>) {
18
+ function GitHubInfoSkeleton({ className }: Pick<FumaGithubInfoProps, 'owner' | 'repo' | 'className'>) {
20
19
  return (
21
20
  <div className={`flex flex-col gap-1.5 p-2 rounded-lg text-sm text-fd-foreground/80 lg:flex-row lg:items-center animate-pulse ${className}`}>
22
21
  <div className="flex items-center gap-2">
@@ -114,7 +113,7 @@ function humanizeNumber(num: number): string {
114
113
  * - 🎨 Three states: loading, success, error
115
114
  * - 💯 Not affected by network issues causing page crashes
116
115
  */
117
- export function FumaGithubInfo({ owner, repo, token, className }: FumaGithubInfoProps) {
116
+ export function FumaGithubInfo({ owner, repo, className }: FumaGithubInfoProps) {
118
117
  const [data, setData] = useState<GitHubRepoData | null>(null);
119
118
  const [loading, setLoading] = useState(true);
120
119
  const [error, setError] = useState<string | null>(null);
@@ -133,10 +132,6 @@ export function FumaGithubInfo({ owner, repo, token, className }: FumaGithubInfo
133
132
  'Accept': 'application/vnd.github.v3+json',
134
133
  });
135
134
 
136
- if (token) {
137
- headers.set('Authorization', `Bearer ${token}`);
138
- }
139
-
140
135
  const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
141
136
  signal: controller.signal,
142
137
  headers,
@@ -170,7 +165,7 @@ export function FumaGithubInfo({ owner, repo, token, className }: FumaGithubInfo
170
165
  };
171
166
 
172
167
  fetchRepoData();
173
- }, [owner, repo, token]);
168
+ }, [owner, repo]);
174
169
 
175
170
  // Loading state
176
171
  if (loading) {