@windrun-huaiin/third-ui 3.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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/dist/clerk/index.d.mts +33 -0
  4. package/dist/clerk/index.d.ts +33 -0
  5. package/dist/clerk/index.js +2395 -0
  6. package/dist/clerk/index.js.map +1 -0
  7. package/dist/clerk/index.mjs +2361 -0
  8. package/dist/clerk/index.mjs.map +1 -0
  9. package/dist/cta.css +16 -0
  10. package/dist/fuma/index.d.mts +51 -0
  11. package/dist/fuma/index.d.ts +51 -0
  12. package/dist/fuma/index.js +2976 -0
  13. package/dist/fuma/index.js.map +1 -0
  14. package/dist/fuma/index.mjs +2944 -0
  15. package/dist/fuma/index.mjs.map +1 -0
  16. package/dist/fuma/mdx/index.d.mts +94 -0
  17. package/dist/fuma/mdx/index.d.ts +94 -0
  18. package/dist/fuma/mdx/index.js +2866 -0
  19. package/dist/fuma/mdx/index.js.map +1 -0
  20. package/dist/fuma/mdx/index.mjs +2827 -0
  21. package/dist/fuma/mdx/index.mjs.map +1 -0
  22. package/dist/fuma.css +132 -0
  23. package/dist/index.d.mts +5 -0
  24. package/dist/index.d.ts +5 -0
  25. package/dist/index.js +3597 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/index.mjs +3549 -0
  28. package/dist/index.mjs.map +1 -0
  29. package/dist/lib/index.d.mts +4414 -0
  30. package/dist/lib/index.d.ts +4414 -0
  31. package/dist/lib/index.js +127 -0
  32. package/dist/lib/index.js.map +1 -0
  33. package/dist/lib/index.mjs +95 -0
  34. package/dist/lib/index.mjs.map +1 -0
  35. package/dist/main/index.d.mts +25 -0
  36. package/dist/main/index.d.ts +25 -0
  37. package/dist/main/index.js +3003 -0
  38. package/dist/main/index.js.map +1 -0
  39. package/dist/main/index.mjs +2963 -0
  40. package/dist/main/index.mjs.map +1 -0
  41. package/dist/third-ui.css +44 -0
  42. package/package.json +106 -0
  43. package/src/clerk/clerk-organization.tsx +47 -0
  44. package/src/clerk/clerk-page-generator.tsx +42 -0
  45. package/src/clerk/clerk-provider-client.tsx +57 -0
  46. package/src/clerk/clerk-user.tsx +59 -0
  47. package/src/clerk/index.ts +5 -0
  48. package/src/fuma/fuma-banner-suit.tsx +16 -0
  49. package/src/fuma/fuma-github-info.tsx +194 -0
  50. package/src/fuma/fuma-page-genarator.tsx +94 -0
  51. package/src/fuma/index.ts +4 -0
  52. package/src/fuma/mdx/airtical-card.tsx +56 -0
  53. package/src/fuma/mdx/gradient-button.tsx +62 -0
  54. package/src/fuma/mdx/image-grid.tsx +35 -0
  55. package/src/fuma/mdx/image-zoom.tsx +84 -0
  56. package/src/fuma/mdx/index.ts +8 -0
  57. package/src/fuma/mdx/mermaid.tsx +87 -0
  58. package/src/fuma/mdx/toc-base.tsx +88 -0
  59. package/src/fuma/mdx/toc.tsx +35 -0
  60. package/src/fuma/mdx/trophy-card.tsx +36 -0
  61. package/src/fuma/mdx/zia-card.tsx +46 -0
  62. package/src/index.ts +4 -0
  63. package/src/lib/clerk-intl.ts +13 -0
  64. package/src/lib/fuma-schema-check-util.ts +73 -0
  65. package/src/lib/fuma-search-util.ts +6 -0
  66. package/src/lib/index.ts +3 -0
  67. package/src/main/ads-alert-dialog.tsx +133 -0
  68. package/src/main/cta.tsx +28 -0
  69. package/src/main/faq.tsx +58 -0
  70. package/src/main/features.tsx +35 -0
  71. package/src/main/footer.tsx +37 -0
  72. package/src/main/gallery.tsx +68 -0
  73. package/src/main/go-to-top.tsx +44 -0
  74. package/src/main/index.ts +12 -0
  75. package/src/main/loading.tsx +93 -0
  76. package/src/main/nprogress-bar.tsx +24 -0
  77. package/src/main/seo-content.tsx +34 -0
  78. package/src/main/tips.tsx +38 -0
  79. package/src/main/usage.tsx +45 -0
  80. package/src/styles/cta.css +16 -0
  81. package/src/styles/fuma.css +132 -0
  82. package/src/styles/third-ui.css +43 -0
@@ -0,0 +1,44 @@
1
+ @import '@windrun-huaiin/base-ui/styles/base-ui.css';
2
+ @import './fuma.css';
3
+ @import './cta.css';
4
+
5
+ /* Loading Animation Keyframes */
6
+ @keyframes loading-dot-pulse {
7
+ 0% {
8
+ transform: scale(0.2);
9
+ opacity: 0;
10
+ }
11
+ 25% { /* Peak of the pulse, more prominent */
12
+ transform: scale(1.2);
13
+ opacity: 1;
14
+ }
15
+ 50% { /* Start to fade and shrink */
16
+ transform: scale(0.8);
17
+ opacity: 0.7;
18
+ }
19
+ 100% { /* Fully faded and shrunk back */
20
+ transform: scale(0.2);
21
+ opacity: 0;
22
+ }
23
+ }
24
+
25
+ /* NProgress progress bar style */
26
+ #nprogress .bar {
27
+ background: #AC62FD !important; /* purple */
28
+ height: 1px !important;
29
+ }
30
+
31
+ /* Icon stroke colors for light/dark themes */
32
+ :root {
33
+ --clerk-icon-stroke-color: #616161; /* Light theme */
34
+ }
35
+
36
+ .dark {
37
+ --clerk-icon-stroke-color: #616161; /* Dark theme */
38
+ }
39
+
40
+ /* English and numeric characters use Montserrat font, Chinese use STKaiti */
41
+ body {
42
+ font-family: 'Montserrat', "STKaiti", "STKaiti", "Kaiti SC", "KaiTi", "楷体", "PingFang SC", "Microsoft YaHei", Arial, sans-serif !important;
43
+ }
44
+ /*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9zdHlsZXMvdGhpcmQtdWkuY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBIiwiZmlsZSI6InRoaXJkLXVpLmNzcyIsInNvdXJjZXNDb250ZW50IjpbIkBpbXBvcnQgJ0B3aW5kcnVuLWh1YWlpbi9iYXNlLXVpL3N0eWxlcy9iYXNlLXVpLmNzcyc7XG5AaW1wb3J0ICcuL2Z1bWEuY3NzJztcbkBpbXBvcnQgJy4vY3RhLmNzcyc7XG5cbi8qIExvYWRpbmcgQW5pbWF0aW9uIEtleWZyYW1lcyAqL1xuQGtleWZyYW1lcyBsb2FkaW5nLWRvdC1wdWxzZSB7XG4gIDAlIHtcbiAgICB0cmFuc2Zvcm06IHNjYWxlKDAuMik7XG4gICAgb3BhY2l0eTogMDtcbiAgfVxuICAyNSUgeyAvKiBQZWFrIG9mIHRoZSBwdWxzZSwgbW9yZSBwcm9taW5lbnQgKi9cbiAgICB0cmFuc2Zvcm06IHNjYWxlKDEuMik7XG4gICAgb3BhY2l0eTogMTtcbiAgfVxuICA1MCUgeyAvKiBTdGFydCB0byBmYWRlIGFuZCBzaHJpbmsgKi9cbiAgICAgIHRyYW5zZm9ybTogc2NhbGUoMC44KTtcbiAgICAgIG9wYWNpdHk6IDAuNztcbiAgfVxuICAxMDAlIHsgLyogRnVsbHkgZmFkZWQgYW5kIHNocnVuayBiYWNrICovXG4gICAgdHJhbnNmb3JtOiBzY2FsZSgwLjIpO1xuICAgIG9wYWNpdHk6IDA7XG4gIH1cbn1cblxuLyogTlByb2dyZXNzIHByb2dyZXNzIGJhciBzdHlsZSAqL1xuI25wcm9ncmVzcyAuYmFyIHtcbiAgYmFja2dyb3VuZDogI0FDNjJGRCAhaW1wb3J0YW50OyAvKiBwdXJwbGUgKi9cbiAgaGVpZ2h0OiAxcHggIWltcG9ydGFudDtcbn1cblxuLyogSWNvbiBzdHJva2UgY29sb3JzIGZvciBsaWdodC9kYXJrIHRoZW1lcyAqL1xuOnJvb3Qge1xuICAtLWNsZXJrLWljb24tc3Ryb2tlLWNvbG9yOiAjNjE2MTYxOyAvKiBMaWdodCB0aGVtZSAqL1xufVxuXG4uZGFyayB7XG4gIC0tY2xlcmstaWNvbi1zdHJva2UtY29sb3I6ICM2MTYxNjE7IC8qIERhcmsgdGhlbWUgKi9cbn1cblxuLyogRW5nbGlzaCBhbmQgbnVtZXJpYyBjaGFyYWN0ZXJzIHVzZSBNb250c2VycmF0IGZvbnQsIENoaW5lc2UgdXNlIFNUS2FpdGkgKi9cbmJvZHkge1xuICBmb250LWZhbWlseTogJ01vbnRzZXJyYXQnLCBcIlNUS2FpdGlcIiwgXCJTVEthaXRpXCIsIFwiS2FpdGkgU0NcIiwgXCJLYWlUaVwiLCBcIualt+S9k1wiLCBcIlBpbmdGYW5nIFNDXCIsIFwiTWljcm9zb2Z0IFlhSGVpXCIsIEFyaWFsLCBzYW5zLXNlcmlmICFpbXBvcnRhbnQ7XG59Il19 */
package/package.json ADDED
@@ -0,0 +1,106 @@
1
+ {
2
+ "name": "@windrun-huaiin/third-ui",
3
+ "version": "3.2.0",
4
+ "description": "Third-party integrated UI components for windrun-huaiin projects",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./clerk": {
15
+ "types": "./dist/clerk/index.d.ts",
16
+ "import": "./dist/clerk/index.mjs",
17
+ "require": "./dist/clerk/index.js"
18
+ },
19
+ "./main": {
20
+ "types": "./dist/main/index.d.ts",
21
+ "import": "./dist/main/index.mjs",
22
+ "require": "./dist/main/index.js"
23
+ },
24
+ "./fuma": {
25
+ "types": "./dist/fuma/index.d.ts",
26
+ "import": "./dist/fuma/index.mjs",
27
+ "require": "./dist/fuma/index.js"
28
+ },
29
+ "./fuma/mdx": {
30
+ "types": "./dist/fuma/mdx/index.d.ts",
31
+ "import": "./dist/fuma/mdx/index.mjs",
32
+ "require": "./dist/fuma/mdx/index.js"
33
+ },
34
+ "./lib": {
35
+ "types": "./dist/lib/index.d.ts",
36
+ "import": "./dist/lib/index.mjs",
37
+ "require": "./dist/lib/index.js"
38
+ },
39
+ "./styles/third-ui.css": "./dist/third-ui.css",
40
+ "./src/*": "./src/*"
41
+ },
42
+ "files": [
43
+ "src",
44
+ "dist",
45
+ "package.json",
46
+ "README.md",
47
+ "LICENSE"
48
+ ],
49
+ "scripts": {
50
+ "build:css": "postcss src/styles/third-ui.css -o dist/third-ui.css",
51
+ "build": "tsup && pnpm build:css",
52
+ "build:prod": "tsup && pnpm build:css",
53
+ "dev": "tsup --watch",
54
+ "clean": "rm -rf dist",
55
+ "type-check": "tsc --noEmit"
56
+ },
57
+ "dependencies": {
58
+ "@clerk/localizations": "^3.16.0",
59
+ "@clerk/types": "^4.59.0",
60
+ "@windrun-huaiin/base-ui": "workspace:*",
61
+ "@windrun-huaiin/lib": "workspace:*",
62
+ "fumadocs-core": "15.3.3",
63
+ "fumadocs-mdx": "11.6.3",
64
+ "fumadocs-typescript": "4.0.4",
65
+ "fumadocs-ui": "15.3.3",
66
+ "mermaid": "^11.6.0",
67
+ "react-medium-image-zoom": "^5.2.14",
68
+ "@clerk/nextjs": "^6.19.4",
69
+ "zod": "^3.22.4"
70
+ },
71
+ "peerDependencies": {
72
+ "react": "^19.1.0",
73
+ "react-dom": "^19.1.0",
74
+ "next": "^15.3.2",
75
+ "next-intl": "^3.26.5",
76
+ "next-themes": "^0.4.6",
77
+ "tailwindcss": "^4.0.0",
78
+ "clsx": "^2.0.0",
79
+ "tailwind-merge": "^3.0.0",
80
+ "nprogress": "^0.2.0"
81
+ },
82
+ "devDependencies": {
83
+ "@types/mdx": "^2.0.13",
84
+ "@types/react": "19.1.2",
85
+ "@types/react-dom": "19.1.3",
86
+ "tailwindcss": "^4.1.7",
87
+ "@types/node": "^22.0.0",
88
+ "tsup": "^8.3.5",
89
+ "typescript": "^5.8.3",
90
+ "@types/nprogress": "^0.2.3"
91
+ },
92
+ "keywords": [
93
+ "ui",
94
+ "components",
95
+ "react",
96
+ "nextjs",
97
+ "clerk",
98
+ "fumadocs",
99
+ "tailwindcss"
100
+ ],
101
+ "author": "windrun-huaiin",
102
+ "license": "MIT",
103
+ "publishConfig": {
104
+ "access": "public"
105
+ }
106
+ }
@@ -0,0 +1,47 @@
1
+ import { OrganizationSwitcher } from '@clerk/nextjs';
2
+ import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
3
+ interface ClerkOrganizationProps {
4
+ className?: string;
5
+ locale: string;
6
+ }
7
+
8
+ export default function ClerkOrganization({
9
+ locale,
10
+ className = '',
11
+ }: ClerkOrganizationProps) {
12
+ return (
13
+ <div className={` ms-3 me-2 flex items-center h-10 rounded-full border shadow-lg ${className}`}>
14
+ <div className="flex items-center gap-x-4 w-full min-w-40">
15
+ <OrganizationSwitcher
16
+ appearance={{
17
+ elements: {
18
+ organizationSwitcherTrigger:
19
+ "w-40 h-10 border !rounded-full bg-transparent flex items-center justify-between box-border",
20
+ organizationSwitcherTriggerIcon: "",
21
+ userButtonAvatarBox: "w-8 h-8 border rounded-full",
22
+ },
23
+ }}
24
+ >
25
+ <OrganizationSwitcher.OrganizationProfilePage
26
+ label="Homepage"
27
+ url="/"
28
+ labelIcon={<icons.D8 />}
29
+ />
30
+ <OrganizationSwitcher.OrganizationProfilePage
31
+ labelIcon={<icons.ReceiptText />}
32
+ label="服务"
33
+ url={`/${locale}/legal/terms`}
34
+ >
35
+ </OrganizationSwitcher.OrganizationProfilePage>
36
+
37
+ <OrganizationSwitcher.OrganizationProfilePage
38
+ labelIcon={<icons.ShieldUser />}
39
+ label="隐私"
40
+ url={`/${locale}/legal/privacy`}
41
+ >
42
+ </OrganizationSwitcher.OrganizationProfilePage>
43
+ </OrganizationSwitcher>
44
+ </div>
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @license
3
+ * MIT License
4
+ * Copyright (c) 2025 D8ger
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+
10
+ 'use client';
11
+
12
+ import { SignIn, SignUp, Waitlist } from '@clerk/nextjs';
13
+
14
+ export function createSignInPage() {
15
+ return function SignInPage() {
16
+ return (
17
+ <div className="flex-1 flex justify-center mb-64">
18
+ <SignIn />
19
+ </div>
20
+ );
21
+ };
22
+ }
23
+
24
+ export function createSignUpPage() {
25
+ return function SignUpPage() {
26
+ return (
27
+ <div className="flex-1 flex justify-center mt-0 mb-32">
28
+ <SignUp />
29
+ </div>
30
+ );
31
+ };
32
+ }
33
+
34
+ export function createWaitlistPage() {
35
+ return function WaitlistPage() {
36
+ return (
37
+ <div className="flex-1 flex justify-center mt-10">
38
+ <Waitlist />
39
+ </div>
40
+ );
41
+ };
42
+ }
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+
3
+ import { clerkIntl } from '@third-ui/lib/clerk-intl';
4
+ import { ClerkProvider } from '@clerk/nextjs';
5
+ import React from 'react';
6
+
7
+ interface ClerkProviderClientProps {
8
+ children: React.ReactNode;
9
+ locale: string;
10
+ signInUrl?: string;
11
+ signUpUrl?: string;
12
+ fallbackSignInUrl?: string;
13
+ fallbackSignUpUrl?: string;
14
+ waitlistUrl?: string;
15
+ }
16
+
17
+ export function ClerkProviderClient({
18
+ children,
19
+ locale,
20
+ signInUrl,
21
+ signUpUrl,
22
+ fallbackSignInUrl,
23
+ fallbackSignUpUrl,
24
+ waitlistUrl,
25
+ }: ClerkProviderClientProps) {
26
+ const currentLocalization = clerkIntl[locale as keyof typeof clerkIntl];
27
+
28
+ // build the ClerkProvider props, only add when the parameter is not empty
29
+ const clerkProviderProps: Record<string, any> = {
30
+ localization: currentLocalization,
31
+ };
32
+
33
+ // 只有传入参数非空时才拼接 URL 并添加对应的属性
34
+ if (signInUrl) {
35
+ clerkProviderProps.signInUrl = `/${locale}${signInUrl}`;
36
+ }
37
+ if (signUpUrl) {
38
+ clerkProviderProps.signUpUrl = `/${locale}${signUpUrl}`;
39
+ }
40
+ if (fallbackSignInUrl) {
41
+ clerkProviderProps.signInFallbackRedirectUrl = `/${locale}${fallbackSignInUrl}`;
42
+ }
43
+ if (fallbackSignUpUrl) {
44
+ clerkProviderProps.signUpFallbackRedirectUrl = `/${locale}${fallbackSignUpUrl}`;
45
+ }
46
+ if (waitlistUrl) {
47
+ clerkProviderProps.waitlistUrl = `/${locale}${waitlistUrl}`;
48
+ }
49
+
50
+ // console.log('ClerkProviderClient props:', clerkProviderProps);
51
+
52
+ return (
53
+ <ClerkProvider {...clerkProviderProps}>
54
+ {children}
55
+ </ClerkProvider>
56
+ );
57
+ }
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
4
+ import { ClerkLoaded, ClerkLoading, SignedIn, SignedOut, SignInButton, UserButton } from "@clerk/nextjs";
5
+ import { useTranslations } from 'next-intl';
6
+ import { type JSX } from 'react';
7
+
8
+ interface ClerkUserProps {
9
+ locale: string;
10
+ clerkAuthInModal?: boolean;
11
+ }
12
+
13
+ export function ClerkUser({
14
+ locale,
15
+ clerkAuthInModal = false
16
+ }: ClerkUserProps): JSX.Element {
17
+ const t = useTranslations('clerk');
18
+ const t2 = useTranslations('footer');
19
+ return (
20
+ <div className="ms-1.5 flex items-center gap-2 h-10 me-3">
21
+ <ClerkLoading>
22
+ <div className="w-20 h-9 px-2 border border-gray-300 rounded-full hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800 text-center text-sm"></div>
23
+ </ClerkLoading>
24
+ <ClerkLoaded>
25
+ <SignedOut>
26
+ <SignInButton mode={clerkAuthInModal ? 'modal' : 'redirect'}>
27
+ <button className="w-20 h-9 px-2 border border-gray-300 rounded-full hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800 text-center text-sm">
28
+ {t('signIn')}
29
+ </button>
30
+ </SignInButton>
31
+ </SignedOut>
32
+ <SignedIn>
33
+ <UserButton
34
+ appearance={{
35
+ elements: {
36
+ userButtonAvatarBox: "w-8 h-8 border",
37
+ }
38
+ }}
39
+ >
40
+ <UserButton.MenuItems>
41
+ <UserButton.Action label="manageAccount" />
42
+ {<UserButton.Link
43
+ labelIcon={<icons.ReceiptText className="size-4 fill-none stroke-[var(--clerk-icon-stroke-color)]" />}
44
+ label={t2('terms')}
45
+ href={`/${locale}/legal/terms`}>
46
+ </UserButton.Link>}
47
+ {<UserButton.Link
48
+ labelIcon={<icons.ShieldUser className="size-4 fill-none stroke-[var(--clerk-icon-stroke-color)]" />}
49
+ label={t2('privacy')}
50
+ href={`/${locale}/legal/privacy`}>
51
+ </UserButton.Link>}
52
+ <UserButton.Action label="signOut" />
53
+ </UserButton.MenuItems>
54
+ </UserButton>
55
+ </SignedIn>
56
+ </ClerkLoaded>
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,5 @@
1
+ // Clerk related components
2
+ export * from './clerk-organization';
3
+ export * from './clerk-provider-client';
4
+ export * from './clerk-user';
5
+ export * from './clerk-page-generator';
@@ -0,0 +1,16 @@
1
+ 'use client'
2
+
3
+ import { Banner } from 'fumadocs-ui/components/banner';
4
+ import { useTranslations } from 'next-intl';
5
+
6
+ export function FumaBannerSuit({ showText }: { showText: boolean }) {
7
+ const t = useTranslations('home');
8
+ return (
9
+ showText ?
10
+ (<Banner variant="rainbow" changeLayout={true}>
11
+ <p className="text-xl">{t('banner')}</p>
12
+ </Banner>)
13
+ : (<Banner variant="normal" changeLayout={true} className="bg-white dark:bg-[rgb(10,10,10)]"/>)
14
+ );
15
+ }
16
+
@@ -0,0 +1,194 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
5
+
6
+ interface FumaGithubInfoProps {
7
+ owner: string;
8
+ repo: string;
9
+ token?: string;
10
+ className?: string;
11
+ }
12
+
13
+ interface GitHubRepoData {
14
+ stargazers_count: number;
15
+ forks_count: number;
16
+ }
17
+
18
+ // Loading state component
19
+ function GitHubInfoSkeleton({ owner, repo, className }: Pick<FumaGithubInfoProps, 'owner' | 'repo' | 'className'>) {
20
+ return (
21
+ <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
+ <div className="flex items-center gap-2">
23
+ <div className="size-3.5 bg-fd-muted rounded"></div>
24
+ <div className="h-4 bg-fd-muted rounded w-20"></div>
25
+ </div>
26
+ <div className="h-3 bg-fd-muted rounded w-8"></div>
27
+ </div>
28
+ );
29
+ }
30
+
31
+ // Error state component - graceful fallback
32
+ function GitHubInfoFallback({ owner, repo, className }: Pick<FumaGithubInfoProps, 'owner' | 'repo' | 'className'>) {
33
+ return (
34
+ <a
35
+ href={`https://github.com/${owner}/${repo}`}
36
+ rel="noreferrer noopener"
37
+ target="_blank"
38
+ className={`flex flex-col gap-1.5 p-2 rounded-lg text-sm text-fd-foreground/80 transition-colors lg:flex-row lg:items-center hover:text-fd-accent-foreground hover:bg-fd-accent ${className}`}
39
+ >
40
+ <p className="flex items-center gap-2 truncate">
41
+ <svg fill="currentColor" viewBox="0 0 24 24" className="size-3.5">
42
+ <title>GitHub</title>
43
+ <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
44
+ </svg>
45
+ {owner}/{repo}
46
+ </p>
47
+ <p className="flex text-xs items-center gap-1 text-fd-muted-foreground">
48
+ <icons.ExternalLink className="size-3" />
49
+ GitHub
50
+ </p>
51
+ </a>
52
+ );
53
+ }
54
+
55
+ // Success state component
56
+ function GitHubInfoSuccess({
57
+ owner,
58
+ repo,
59
+ stars,
60
+ className
61
+ }: Pick<FumaGithubInfoProps, 'owner' | 'repo' | 'className'> & { stars: number }) {
62
+ const humanizedStars = humanizeNumber(stars);
63
+
64
+ return (
65
+ <a
66
+ href={`https://github.com/${owner}/${repo}`}
67
+ rel="noreferrer noopener"
68
+ target="_blank"
69
+ className={`flex flex-col gap-1.5 p-2 rounded-lg text-sm text-fd-foreground/80 transition-colors lg:flex-row lg:items-center hover:text-fd-accent-foreground hover:bg-fd-accent ${className}`}
70
+ >
71
+ <p className="flex items-center gap-2 truncate">
72
+ <svg fill="currentColor" viewBox="0 0 24 24" className="size-3.5">
73
+ <title>GitHub</title>
74
+ <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
75
+ </svg>
76
+ {owner}/{repo}
77
+ </p>
78
+ <p className="flex text-xs items-center gap-1 text-fd-muted-foreground">
79
+ <icons.Star className="size-3" />
80
+ {humanizedStars}
81
+ </p>
82
+ </a>
83
+ );
84
+ }
85
+
86
+ /**
87
+ * Humanize number display
88
+ */
89
+ function humanizeNumber(num: number): string {
90
+ if (num < 1000) {
91
+ return num.toString();
92
+ }
93
+
94
+ if (num < 100000) {
95
+ const value = (num / 1000).toFixed(1);
96
+ const formattedValue = value.endsWith('.0') ? value.slice(0, -2) : value;
97
+ return `${formattedValue}K`;
98
+ }
99
+
100
+ if (num < 1000000) {
101
+ return `${Math.floor(num / 1000)}K`;
102
+ }
103
+
104
+ return num.toString();
105
+ }
106
+
107
+ /**
108
+ * GitHub repository information component with graceful fallback
109
+ *
110
+ * Features:
111
+ * - 🛡️ Client-side rendering, avoiding server-side network issues
112
+ * - ⏱️ 5 second timeout control
113
+ * - 🎯 Graceful fallback: display basic link when network fails
114
+ * - 🎨 Three states: loading, success, error
115
+ * - 💯 Not affected by network issues causing page crashes
116
+ */
117
+ export function FumaGithubInfo({ owner, repo, token, className }: FumaGithubInfoProps) {
118
+ const [data, setData] = useState<GitHubRepoData | null>(null);
119
+ const [loading, setLoading] = useState(true);
120
+ const [error, setError] = useState<string | null>(null);
121
+
122
+ useEffect(() => {
123
+ const fetchRepoData = async () => {
124
+ try {
125
+ setLoading(true);
126
+ setError(null);
127
+
128
+ // Add timeout control
129
+ const controller = new AbortController();
130
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
131
+
132
+ const headers = new Headers({
133
+ 'Accept': 'application/vnd.github.v3+json',
134
+ });
135
+
136
+ if (token) {
137
+ headers.set('Authorization', `Bearer ${token}`);
138
+ }
139
+
140
+ const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
141
+ signal: controller.signal,
142
+ headers,
143
+ });
144
+
145
+ clearTimeout(timeoutId);
146
+
147
+ if (!response.ok) {
148
+ throw new Error(`GitHub API response error: ${response.status}`);
149
+ }
150
+
151
+ const repoData = await response.json();
152
+ setData({
153
+ stargazers_count: repoData.stargazers_count,
154
+ forks_count: repoData.forks_count,
155
+ });
156
+ } catch (err) {
157
+ console.warn('GitHub API call failed:', err);
158
+ if (err instanceof Error) {
159
+ if (err.name === 'AbortError') {
160
+ setError('Request timeout');
161
+ } else {
162
+ setError('Failed to get repository information');
163
+ }
164
+ } else {
165
+ setError('Unknown error');
166
+ }
167
+ } finally {
168
+ setLoading(false);
169
+ }
170
+ };
171
+
172
+ fetchRepoData();
173
+ }, [owner, repo, token]);
174
+
175
+ // Loading state
176
+ if (loading) {
177
+ return <GitHubInfoSkeleton owner={owner} repo={repo} className={className} />;
178
+ }
179
+
180
+ // Error state - graceful fallback
181
+ if (error || !data) {
182
+ return <GitHubInfoFallback owner={owner} repo={repo} className={className} />;
183
+ }
184
+
185
+ // Success state
186
+ return (
187
+ <GitHubInfoSuccess
188
+ owner={owner}
189
+ repo={repo}
190
+ stars={data.stargazers_count}
191
+ className={className}
192
+ />
193
+ );
194
+ }
@@ -0,0 +1,94 @@
1
+ import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page';
2
+ import { NotFoundPage } from '@base-ui/components/404-page';
3
+ import { TocFooter } from '@third-ui/fuma/mdx/toc';
4
+
5
+ interface FumaPageParams {
6
+ /*
7
+ * The source of the mdx content
8
+ */
9
+ mdxContentSource: any;
10
+ /*
11
+ * The mdx components handler, refer to fumadocs
12
+ */
13
+ getMDXComponents: () => any;
14
+ /*
15
+ * The source directory of the mdx content, used to generate the edit path
16
+ */
17
+ mdxSourceDir: string;
18
+ /*
19
+ * The github base url, used to generate the edit path, if not provided, the edit path will not be shown
20
+ */
21
+ githubBaseUrl?: string;
22
+ /*
23
+ * Whether to show the copy button, default is true
24
+ */
25
+ showCopy?: boolean;
26
+ }
27
+
28
+ export function createFumaPage({
29
+ mdxContentSource,
30
+ getMDXComponents,
31
+ mdxSourceDir,
32
+ githubBaseUrl,
33
+ showCopy = true,
34
+ }: FumaPageParams) {
35
+ const Page = async function Page({ params }: { params: Promise<{ locale: string; slug?: string[] }> }) {
36
+ const { slug, locale } = await params;
37
+ const page = mdxContentSource.getPage(slug, locale);
38
+ if (!page) {
39
+ return <NotFoundPage />;
40
+ }
41
+
42
+ const path = githubBaseUrl ? `${mdxSourceDir}/${page.file.path}` : undefined;
43
+ const tocFooterElement = (
44
+ <TocFooter
45
+ lastModified={page.data.date}
46
+ showCopy={showCopy}
47
+ editPath={path}
48
+ githubBaseUrl={githubBaseUrl}
49
+ />
50
+ );
51
+
52
+ const MDX = page.data.body;
53
+ return (
54
+ <DocsPage
55
+ tableOfContent={{ style: 'clerk', single: false, footer: tocFooterElement }}
56
+ tableOfContentPopover={{ footer: tocFooterElement }}
57
+ toc={page.data.toc}
58
+ full={page.data.full}
59
+ article={{ className: 'max-sm:pb-16' }}
60
+ >
61
+ <DocsTitle>{page.data.title}</DocsTitle>
62
+ <DocsDescription className="mb-2">{page.data.description}</DocsDescription>
63
+ <DocsBody className="text-fd-foreground/80">
64
+ <MDX components={getMDXComponents()} />
65
+ </DocsBody>
66
+ </DocsPage>
67
+ );
68
+ };
69
+
70
+ function generateStaticParams() {
71
+ return mdxContentSource.generateParams('slug', 'locale');
72
+ }
73
+
74
+ async function generateMetadata(props: { params: Promise<{ slug?: string[]; locale?: string }> }) {
75
+ const params = await props.params;
76
+ const page = mdxContentSource.getPage(params.slug, params.locale);
77
+ if (!page) {
78
+ return {
79
+ title: '404 - Page Not Found',
80
+ description: 'This page could not be found.',
81
+ };
82
+ }
83
+ return {
84
+ title: page.data.title,
85
+ description: page.data.description,
86
+ };
87
+ }
88
+
89
+ return {
90
+ Page,
91
+ generateStaticParams,
92
+ generateMetadata,
93
+ };
94
+ }
@@ -0,0 +1,4 @@
1
+ // Fumadocs related components
2
+ export * from './fuma-banner-suit';
3
+ export * from './fuma-page-genarator';
4
+ export * from './fuma-github-info';