@allbluecn/web-app 0.10.2 → 0.10.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allbluecn/web-app",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "license": "AGPL-3.0-or-later",
5
5
  "type": "module",
6
6
  "files": [
@@ -66,6 +66,7 @@
66
66
  "@floating-ui/react": "^0.27.19",
67
67
  "@fontsource-variable/geist": "^5.3.0",
68
68
  "@fontsource-variable/geist-mono": "^5.3.0",
69
+ "@fumadocs/language": "^0.2.4",
69
70
  "@iconify/react": "^6.0.2",
70
71
  "@lobehub/icons": "^5.16.0",
71
72
  "@open-file-viewer/core": "^0.1.43",
@@ -146,6 +147,9 @@
146
147
  "date-fns": "^4.4.0",
147
148
  "date-fns-tz": "^3.2.0",
148
149
  "emoji-picker-react": "^4.19.1",
150
+ "fumadocs-core": "16.15.8",
151
+ "fumadocs-mdx": "15.4.0",
152
+ "fumadocs-ui": "16.15.8",
149
153
  "ioredis": "^5.11.0",
150
154
  "is-hotkey": "^0.2.0",
151
155
  "isomorphic-dompurify": "^3.22.0",
@@ -179,8 +183,8 @@
179
183
  "three": "^0.180.0",
180
184
  "tw-animate-css": "^1.4.0",
181
185
  "zod": "^4.4.3",
182
- "@allbluecn/database": "0.4.18",
183
- "@allbluecn/kernel": "0.5.2"
186
+ "@allbluecn/database": "0.5.0",
187
+ "@allbluecn/kernel": "0.6.0"
184
188
  },
185
189
  "devDependencies": {
186
190
  "@babel/parser": "^8.0.4",
@@ -200,6 +204,7 @@
200
204
  "@types/bcryptjs": "^2.4.6",
201
205
  "@types/better-sqlite3": "^7.6.13",
202
206
  "@types/is-hotkey": "^0.1.10",
207
+ "@types/mdx": "^2.0.14",
203
208
  "@types/node": "^24.13.3",
204
209
  "@types/react": "^19",
205
210
  "@types/react-dom": "^19",
@@ -4,24 +4,19 @@ import {
4
4
  SidebarMenuItem,
5
5
  useSidebar,
6
6
  } from "@/components/ui/sidebar"
7
- import { BookOpen, ScrollText } from "lucide-react"
7
+ import { BookOpen, ExternalLink, ScrollText } from "lucide-react"
8
8
  import { cn } from "@/lib/utils"
9
9
  import { Link } from "@tanstack/react-router"
10
10
  import { APP_VERSION_DISPLAY } from "@/lib/app-version"
11
+ import { getLocale } from "@/paraglide/runtime.js"
12
+ import { m } from "@/paraglide/messages.js"
11
13
 
12
- const items = [
14
+ const changelogItems = [
13
15
  {
14
- title: "使用文档",
15
- Icon: BookOpen,
16
- href: "/docs",
17
- tooltip: "使用文档",
18
- external: true,
19
- },
20
- {
21
- title: "开发者日志",
16
+ title: m.common_changelog_title(),
22
17
  Icon: ScrollText,
23
18
  href: "/changelog",
24
- tooltip: "开发者日志",
19
+ tooltip: m.common_changelog_title(),
25
20
  badge: APP_VERSION_DISPLAY,
26
21
  },
27
22
  ]
@@ -29,6 +24,7 @@ const items = [
29
24
  export function NavSecondary() {
30
25
  const { state } = useSidebar()
31
26
  const isCollapsed = state === "collapsed"
27
+ const lang = getLocale()
32
28
 
33
29
  return (
34
30
  <SidebarMenu className={cn("space-y-1", isCollapsed && "px-1")}>
@@ -37,7 +33,42 @@ export function NavSecondary() {
37
33
  {/* 资源 */}
38
34
  </div>
39
35
  )}
40
- {items.map((item) => (
36
+ <SidebarMenuItem className="relative group/docs">
37
+ <SidebarMenuButton
38
+ asChild
39
+ tooltip={m.common_docs_title()}
40
+ className={cn(
41
+ "flex items-center rounded-lg px-2 pr-8",
42
+ isCollapsed && "justify-center"
43
+ )}
44
+ >
45
+ <Link
46
+ to="/$lang/docs/$"
47
+ params={{ lang, _splat: "" }}
48
+ className="flex w-full items-center gap-3 text-muted-foreground hover:text-foreground"
49
+ style={{ width: "100%", height: "100%" }}
50
+ >
51
+ <BookOpen className="text-muted-foreground size-3.5" />
52
+ {!isCollapsed && (
53
+ <span className="text-xs text-muted-foreground">
54
+ {m.common_docs_title()}
55
+ </span>
56
+ )}
57
+ </Link>
58
+ </SidebarMenuButton>
59
+ {!isCollapsed && (
60
+ <a
61
+ href={`/${lang}/docs`}
62
+ target="_blank"
63
+ rel="noopener noreferrer"
64
+ aria-label={m.common_docs_title()}
65
+ className="absolute right-1.5 top-1/2 -translate-y-1/2 hidden items-center rounded-md p-1 text-muted-foreground/60 hover:text-foreground group-hover/docs:flex"
66
+ >
67
+ <ExternalLink className="size-3" />
68
+ </a>
69
+ )}
70
+ </SidebarMenuItem>
71
+ {changelogItems.map((item) => (
41
72
  <SidebarMenuItem key={item.title}>
42
73
  <SidebarMenuButton
43
74
  asChild
@@ -47,44 +78,23 @@ export function NavSecondary() {
47
78
  isCollapsed && "justify-center"
48
79
  )}
49
80
  >
50
- {item.external ? (
51
- <a
52
- href={item.href}
53
- className={cn(
54
- "flex w-full items-center gap-3 text-muted-foreground hover:text-foreground",
55
- isCollapsed && "justify-center -ml-1"
56
- )}
57
- target="_blank"
58
- rel="noopener noreferrer"
59
- style={{ width: "100%", height: "100%" }}
60
- >
61
- <item.Icon className={cn("text-muted-foreground", !isCollapsed && "size-3.5!")} />
81
+ <Link
82
+ to={item.href}
83
+ className="flex w-full items-center justify-between gap-3 text-muted-foreground hover:text-foreground"
84
+ style={{ width: "100%", height: "100%" }}
85
+ >
86
+ <div className="flex items-center gap-3">
87
+ <item.Icon className="text-muted-foreground size-3.5" />
62
88
  {!isCollapsed && (
63
89
  <span className="text-xs text-muted-foreground">{item.title}</span>
64
90
  )}
65
- </a>
66
- ) : (
67
- <Link
68
- to={item.href}
69
- className={cn(
70
- "flex w-full items-center justify-between gap-3 text-muted-foreground hover:text-foreground",
71
- isCollapsed && "justify-center -ml-1"
72
- )}
73
- style={{ width: "100%", height: "100%" }}
74
- >
75
- <div className="flex items-center gap-3">
76
- <item.Icon className={cn("text-muted-foreground", !isCollapsed && "size-3.5!")} />
77
- {!isCollapsed && (
78
- <span className="text-xs text-muted-foreground">{item.title}</span>
79
- )}
80
- </div>
81
- {!isCollapsed && item.badge && (
82
- <code className="text-[10px] font-mono text-muted-foreground/60 bg-muted/50 px-1.5 py-0.5 rounded shrink-0">
83
- {item.badge}
84
- </code>
85
- )}
86
- </Link>
87
- )}
91
+ </div>
92
+ {!isCollapsed && item.badge && (
93
+ <code className="text-[10px] font-mono text-muted-foreground/60 bg-muted/50 px-1.5 py-0.5 rounded shrink-0">
94
+ {item.badge}
95
+ </code>
96
+ )}
97
+ </Link>
88
98
  </SidebarMenuButton>
89
99
  </SidebarMenuItem>
90
100
  ))}
@@ -0,0 +1,9 @@
1
+ import defaultMdxComponents from 'fumadocs-ui/mdx'
2
+ import type { MDXComponents } from 'mdx/types'
3
+
4
+ export function getMDXComponents(components?: MDXComponents) {
5
+ return {
6
+ ...defaultMdxComponents,
7
+ ...components,
8
+ } satisfies MDXComponents
9
+ }
@@ -10,7 +10,7 @@ export const SDK_GROUPS: SdkGroup[] = [
10
10
  "name": "@tanstack/react-start",
11
11
  "displayName": "TanStack Start",
12
12
  "version": "v1.168.49",
13
- "lastUpdate": "Sep 8, 2026",
13
+ "lastUpdate": "Sep 9, 2026",
14
14
  "source": "apps/web/package.json#dependencies",
15
15
  "docsUrl": "https://tanstack.com/start/latest"
16
16
  },
@@ -18,7 +18,7 @@ export const SDK_GROUPS: SdkGroup[] = [
18
18
  "name": "@tanstack/react-router",
19
19
  "displayName": "TanStack Router",
20
20
  "version": "v1.170.32",
21
- "lastUpdate": "Sep 8, 2026",
21
+ "lastUpdate": "Sep 9, 2026",
22
22
  "source": "apps/web/package.json#dependencies",
23
23
  "docsUrl": "https://tanstack.com/router/latest"
24
24
  },
@@ -26,7 +26,7 @@ export const SDK_GROUPS: SdkGroup[] = [
26
26
  "name": "@tanstack/react-query",
27
27
  "displayName": "TanStack Query",
28
28
  "version": "v5.102.8",
29
- "lastUpdate": "Sep 8, 2026",
29
+ "lastUpdate": "Sep 9, 2026",
30
30
  "source": "apps/web/package.json#dependencies",
31
31
  "docsUrl": "https://tanstack.com/query/latest"
32
32
  },
@@ -34,7 +34,7 @@ export const SDK_GROUPS: SdkGroup[] = [
34
34
  "name": "@tanstack/ai",
35
35
  "displayName": "TanStack AI",
36
36
  "version": "v0.52.0",
37
- "lastUpdate": "Sep 8, 2026",
37
+ "lastUpdate": "Sep 9, 2026",
38
38
  "source": "apps/web/package.json#dependencies",
39
39
  "docsUrl": "https://tanstack.com/ai/latest"
40
40
  },
@@ -42,7 +42,7 @@ export const SDK_GROUPS: SdkGroup[] = [
42
42
  "name": "@tanstack/react-table",
43
43
  "displayName": "TanStack Table",
44
44
  "version": "v9.2.3",
45
- "lastUpdate": "Sep 8, 2026",
45
+ "lastUpdate": "Sep 9, 2026",
46
46
  "source": "apps/web/package.json#dependencies",
47
47
  "docsUrl": "https://tanstack.com/table/latest"
48
48
  },
@@ -50,7 +50,7 @@ export const SDK_GROUPS: SdkGroup[] = [
50
50
  "name": "@tanstack/react-form",
51
51
  "displayName": "TanStack Form",
52
52
  "version": "v2.0.0-alpha.2",
53
- "lastUpdate": "Sep 8, 2026",
53
+ "lastUpdate": "Sep 9, 2026",
54
54
  "source": "apps/web/package.json#dependencies",
55
55
  "docsUrl": "https://tanstack.com/form/latest"
56
56
  },
@@ -58,7 +58,7 @@ export const SDK_GROUPS: SdkGroup[] = [
58
58
  "name": "@tanstack/charts",
59
59
  "displayName": "TanStack Charts",
60
60
  "version": "v0.16.0",
61
- "lastUpdate": "Sep 8, 2026",
61
+ "lastUpdate": "Sep 9, 2026",
62
62
  "source": "apps/admin/package.json#dependencies",
63
63
  "docsUrl": "https://tanstack.com/charts/latest"
64
64
  },
@@ -66,7 +66,7 @@ export const SDK_GROUPS: SdkGroup[] = [
66
66
  "name": "@tanstack/react-pacer",
67
67
  "displayName": "TanStack Pacer",
68
68
  "version": "v0.23.0",
69
- "lastUpdate": "Sep 8, 2026",
69
+ "lastUpdate": "Sep 9, 2026",
70
70
  "source": "apps/web/package.json#dependencies",
71
71
  "docsUrl": "https://tanstack.com/pacer/latest"
72
72
  },
@@ -74,7 +74,7 @@ export const SDK_GROUPS: SdkGroup[] = [
74
74
  "name": "@tanstack/react-hotkeys",
75
75
  "displayName": "TanStack Hotkeys",
76
76
  "version": "v0.10.0",
77
- "lastUpdate": "Sep 8, 2026",
77
+ "lastUpdate": "Sep 9, 2026",
78
78
  "source": "apps/web/package.json#dependencies",
79
79
  "docsUrl": "https://tanstack.com/hotkeys/latest"
80
80
  },
@@ -82,7 +82,7 @@ export const SDK_GROUPS: SdkGroup[] = [
82
82
  "name": "@tanstack/react-virtual",
83
83
  "displayName": "TanStack Virtual",
84
84
  "version": "v3.14.10",
85
- "lastUpdate": "Sep 8, 2026",
85
+ "lastUpdate": "Sep 9, 2026",
86
86
  "source": "apps/admin/package.json#dependencies",
87
87
  "docsUrl": "https://tanstack.com/virtual/latest"
88
88
  }
@@ -95,15 +95,15 @@ export const SDK_GROUPS: SdkGroup[] = [
95
95
  "items": [
96
96
  {
97
97
  "name": "@allbluecn/web-app",
98
- "version": "v0.10.2",
99
- "lastUpdate": "Sep 8, 2026",
98
+ "version": "v0.10.3",
99
+ "lastUpdate": "Sep 9, 2026",
100
100
  "source": "apps/web/package.json",
101
101
  "docsUrl": "https://github.com/allbluecn/allblue"
102
102
  },
103
103
  {
104
104
  "name": "@allbluecn/kernel",
105
- "version": "v0.5.2",
106
- "lastUpdate": "Sep 8, 2026",
105
+ "version": "v0.6.0",
106
+ "lastUpdate": "Sep 9, 2026",
107
107
  "source": "packages/kernel/package.json",
108
108
  "docsUrl": "https://github.com/allbluecn/allblue"
109
109
  }
@@ -117,42 +117,42 @@ export const SDK_GROUPS: SdkGroup[] = [
117
117
  {
118
118
  "name": "Vite",
119
119
  "version": "v8.1.5",
120
- "lastUpdate": "Sep 8, 2026",
120
+ "lastUpdate": "Sep 9, 2026",
121
121
  "source": "apps/web/package.json#devDependencies",
122
122
  "docsUrl": "https://vite.dev/"
123
123
  },
124
124
  {
125
125
  "name": "Node.js",
126
126
  "version": "v24.19.0",
127
- "lastUpdate": "Sep 8, 2026",
127
+ "lastUpdate": "Sep 9, 2026",
128
128
  "source": ".nvmrc",
129
129
  "docsUrl": "https://nodejs.org/"
130
130
  },
131
131
  {
132
132
  "name": "Prisma",
133
133
  "version": "v7.8.0",
134
- "lastUpdate": "Sep 8, 2026",
134
+ "lastUpdate": "Sep 9, 2026",
135
135
  "source": "packages/database/package.json#devDependencies",
136
136
  "docsUrl": "https://www.prisma.io/docs"
137
137
  },
138
138
  {
139
139
  "name": "shadcn UI",
140
140
  "version": "v1.2.4",
141
- "lastUpdate": "Sep 8, 2026",
141
+ "lastUpdate": "Sep 9, 2026",
142
142
  "source": "apps/web/package.json#dependencies",
143
143
  "docsUrl": "https://ui.shadcn.com/"
144
144
  },
145
145
  {
146
146
  "name": "TipTap",
147
147
  "version": "v3.27.4",
148
- "lastUpdate": "Sep 8, 2026",
148
+ "lastUpdate": "Sep 9, 2026",
149
149
  "source": "apps/web/package.json#dependencies",
150
150
  "docsUrl": "https://tiptap.dev/"
151
151
  },
152
152
  {
153
153
  "name": "Lucide React",
154
154
  "version": "v1.27.0",
155
- "lastUpdate": "Sep 8, 2026",
155
+ "lastUpdate": "Sep 9, 2026",
156
156
  "source": "apps/web/package.json#dependencies",
157
157
  "docsUrl": "https://lucide.dev/"
158
158
  }
@@ -0,0 +1,7 @@
1
+ import { defineI18n } from 'fumadocs-core/i18n'
2
+
3
+ export const fumadocsI18n = defineI18n({
4
+ defaultLanguage: 'zh',
5
+ languages: ['zh', 'en'],
6
+ parser: 'dir',
7
+ })
@@ -0,0 +1,40 @@
1
+ import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'
2
+ import { uiTranslations } from 'fumadocs-ui/i18n'
3
+ import { zhCN } from '@fumadocs/language/zh-cn'
4
+ import { fumadocsI18n } from './fumadocs-i18n'
5
+
6
+ export type DocsLang = 'zh' | 'en'
7
+
8
+ const DOCS_LANGS: readonly DocsLang[] = ['zh', 'en'] as const
9
+
10
+ export function isDocsLang(v: string): v is DocsLang {
11
+ return DOCS_LANGS.includes(v as DocsLang)
12
+ }
13
+
14
+ export const translations = fumadocsI18n
15
+ .translations()
16
+ .extend(uiTranslations())
17
+ .preset('zh', zhCN())
18
+ .add({
19
+ zh: {
20
+ displayName: '简体中文',
21
+ },
22
+ en: {
23
+ displayName: 'English',
24
+ },
25
+ })
26
+
27
+ export function baseOptions(lang: DocsLang): BaseLayoutProps {
28
+ return {
29
+ nav: {
30
+ title: lang === 'en' ? 'AllBlue Docs' : 'AllBlue 使用文档',
31
+ url: `/${lang}/docs`,
32
+ },
33
+ links: [
34
+ {
35
+ text: lang === 'en' ? 'Back to App' : '返回应用',
36
+ url: '/dashboard',
37
+ },
38
+ ],
39
+ }
40
+ }
@@ -0,0 +1,15 @@
1
+ import { defineDocs } from 'fumadocs-mdx/macro'
2
+ import { loader } from 'fumadocs-core/source'
3
+ import { fumadocsI18n } from './fumadocs-i18n'
4
+
5
+ const docs = defineDocs({
6
+ dir: 'content/docs',
7
+ })
8
+
9
+ export const source = loader({
10
+ i18n: fumadocsI18n,
11
+ baseUrl: '/docs',
12
+ source: docs.toFumadocsSource(),
13
+ })
14
+
15
+ export { docs }
@@ -14,6 +14,7 @@ import { Route as rootRouteImport } from './routes/__root'
14
14
  import { Route as LoginRouteRouteImport } from './routes/_login/route'
15
15
  import { Route as NavRouteRouteImport } from './routes/_nav/route'
16
16
  import { Route as PublicRouteRouteImport } from './routes/_public/route'
17
+ import { Route as DocsRouteImport } from './routes/docs'
17
18
  import { Route as ShareRouteRouteImport } from './routes/share/route'
18
19
  import { Route as WorkspaceRouteImport } from './routes/workspace'
19
20
  import { Route as _desktopOnboardingRouteRouteImport } from './routes/__desktop/onboarding/route'
@@ -46,11 +47,13 @@ import { Route as PublicPricingRouteRouteImport } from './routes/_public/pricing
46
47
  import { Route as ApiBillingWebhookRouteImport } from './routes/api/billing-webhook'
47
48
  import { Route as ApiChatRouteImport } from './routes/api/chat'
48
49
  import { Route as ApiHealthRouteImport } from './routes/api/health'
50
+ import { Route as ApiSearchRouteImport } from './routes/api/search'
49
51
  import { Route as ApiStoryAiRouteImport } from './routes/api/story-ai'
50
52
  import { Route as InviteTokenRouteRouteImport } from './routes/invite/$token/route'
51
53
  import { Route as ServerAiStreamRouteImport } from './routes/server.ai-stream'
52
54
  import { Route as ServerWsRouteImport } from './routes/server.ws'
53
55
  import { Route as ShareVersionIdRouteRouteImport } from './routes/share/$versionId/route'
56
+ import { Route as LangDocsSplatRouteRouteImport } from './routes/$lang/docs/$/route'
54
57
  import { Route as NavAppsAnyhubRouteRouteImport } from './routes/_nav/apps/anyhub/route'
55
58
  import { Route as NavChatThreadIdRouteImport } from './routes/_nav/chat/$threadId'
56
59
  import { Route as NavDesignIndexRouteImport } from './routes/_nav/design/index'
@@ -160,6 +163,11 @@ const PublicRouteRoute = PublicRouteRouteImport.update({
160
163
  id: '/_public',
161
164
  getParentRoute: () => rootRouteImport,
162
165
  } as any)
166
+ const DocsRoute = DocsRouteImport.update({
167
+ id: '/docs',
168
+ path: '/docs',
169
+ getParentRoute: () => rootRouteImport,
170
+ } as any)
163
171
  const ShareRouteRoute = ShareRouteRouteImport.update({
164
172
  id: '/share',
165
173
  path: '/share',
@@ -346,6 +354,11 @@ const ApiHealthRoute = ApiHealthRouteImport.update({
346
354
  path: '/api/health',
347
355
  getParentRoute: () => rootRouteImport,
348
356
  } as any)
357
+ const ApiSearchRoute = ApiSearchRouteImport.update({
358
+ id: '/api/search',
359
+ path: '/api/search',
360
+ getParentRoute: () => rootRouteImport,
361
+ } as any)
349
362
  const ApiStoryAiRoute = ApiStoryAiRouteImport.update({
350
363
  id: '/api/story-ai',
351
364
  path: '/api/story-ai',
@@ -375,6 +388,13 @@ const ShareVersionIdRouteRoute = ShareVersionIdRouteRouteImport.update({
375
388
  } as any).lazy(() =>
376
389
  import('./routes/share/$versionId/route.lazy').then((d) => d.Route),
377
390
  )
391
+ const LangDocsSplatRouteRoute = LangDocsSplatRouteRouteImport.update({
392
+ id: '/$lang/docs/$',
393
+ path: '/$lang/docs/$',
394
+ getParentRoute: () => rootRouteImport,
395
+ } as any).lazy(() =>
396
+ import('./routes/$lang/docs/$/route.lazy').then((d) => d.Route),
397
+ )
378
398
  const NavAppsAnyhubRouteRoute = NavAppsAnyhubRouteRouteImport.update({
379
399
  id: '/anyhub',
380
400
  path: '/anyhub',
@@ -878,6 +898,7 @@ const ApiSharePrdPrdIdEventsRoute = ApiSharePrdPrdIdEventsRouteImport.update({
878
898
  export interface FileRoutesByFullPath {
879
899
  '/': typeof PublicIndexRouteRoute
880
900
  '/share': typeof ShareRouteRouteWithChildren
901
+ '/docs': typeof DocsRoute
881
902
  '/workspace': typeof WorkspaceRoute
882
903
  '/onboarding': typeof _desktopOnboardingRouteRoute
883
904
  '/forgot-password': typeof LoginForgotPasswordRouteRoute
@@ -910,9 +931,11 @@ export interface FileRoutesByFullPath {
910
931
  '/api/billing-webhook': typeof ApiBillingWebhookRoute
911
932
  '/api/chat': typeof ApiChatRoute
912
933
  '/api/health': typeof ApiHealthRoute
934
+ '/api/search': typeof ApiSearchRoute
913
935
  '/api/story-ai': typeof ApiStoryAiRoute
914
936
  '/server/ai-stream': typeof ServerAiStreamRoute
915
937
  '/server/ws': typeof ServerWsRoute
938
+ '/$lang/docs/$': typeof LangDocsSplatRouteRoute
916
939
  '/apps/anyhub': typeof NavAppsAnyhubRouteRouteWithChildren
917
940
  '/design/$projectId': typeof NavDesignProjectIdRouteRouteWithChildren
918
941
  '/knowledge/$kbId': typeof NavKnowledgeKbIdRouteRouteWithChildren
@@ -987,6 +1010,7 @@ export interface FileRoutesByFullPath {
987
1010
  export interface FileRoutesByTo {
988
1011
  '/': typeof PublicIndexRouteRoute
989
1012
  '/share': typeof ShareRouteRouteWithChildren
1013
+ '/docs': typeof DocsRoute
990
1014
  '/workspace': typeof WorkspaceRoute
991
1015
  '/onboarding': typeof _desktopOnboardingRouteRoute
992
1016
  '/forgot-password': typeof LoginForgotPasswordRouteRoute
@@ -1013,9 +1037,11 @@ export interface FileRoutesByTo {
1013
1037
  '/api/billing-webhook': typeof ApiBillingWebhookRoute
1014
1038
  '/api/chat': typeof ApiChatRoute
1015
1039
  '/api/health': typeof ApiHealthRoute
1040
+ '/api/search': typeof ApiSearchRoute
1016
1041
  '/api/story-ai': typeof ApiStoryAiRoute
1017
1042
  '/server/ai-stream': typeof ServerAiStreamRoute
1018
1043
  '/server/ws': typeof ServerWsRoute
1044
+ '/$lang/docs/$': typeof LangDocsSplatRouteRoute
1019
1045
  '/apps/anyhub': typeof NavAppsAnyhubRouteRouteWithChildren
1020
1046
  '/design/$projectId': typeof NavDesignProjectIdRouteRouteWithChildren
1021
1047
  '/prd/$id': typeof NavPrdIdRouteRoute
@@ -1085,6 +1111,7 @@ export interface FileRoutesById {
1085
1111
  '/_nav': typeof NavRouteRouteWithChildren
1086
1112
  '/_public': typeof PublicRouteRouteWithChildren
1087
1113
  '/share': typeof ShareRouteRouteWithChildren
1114
+ '/docs': typeof DocsRoute
1088
1115
  '/workspace': typeof WorkspaceRoute
1089
1116
  '/_public/': typeof PublicIndexRouteRoute
1090
1117
  '/__desktop/onboarding': typeof _desktopOnboardingRouteRoute
@@ -1118,9 +1145,11 @@ export interface FileRoutesById {
1118
1145
  '/api/billing-webhook': typeof ApiBillingWebhookRoute
1119
1146
  '/api/chat': typeof ApiChatRoute
1120
1147
  '/api/health': typeof ApiHealthRoute
1148
+ '/api/search': typeof ApiSearchRoute
1121
1149
  '/api/story-ai': typeof ApiStoryAiRoute
1122
1150
  '/server/ai-stream': typeof ServerAiStreamRoute
1123
1151
  '/server/ws': typeof ServerWsRoute
1152
+ '/$lang/docs/$': typeof LangDocsSplatRouteRoute
1124
1153
  '/_nav/apps/anyhub': typeof NavAppsAnyhubRouteRouteWithChildren
1125
1154
  '/_nav/design/$projectId': typeof NavDesignProjectIdRouteRouteWithChildren
1126
1155
  '/_nav/knowledge/$kbId': typeof NavKnowledgeKbIdRouteRouteWithChildren
@@ -1197,6 +1226,7 @@ export interface FileRouteTypes {
1197
1226
  fullPaths:
1198
1227
  | '/'
1199
1228
  | '/share'
1229
+ | '/docs'
1200
1230
  | '/workspace'
1201
1231
  | '/onboarding'
1202
1232
  | '/forgot-password'
@@ -1229,9 +1259,11 @@ export interface FileRouteTypes {
1229
1259
  | '/api/billing-webhook'
1230
1260
  | '/api/chat'
1231
1261
  | '/api/health'
1262
+ | '/api/search'
1232
1263
  | '/api/story-ai'
1233
1264
  | '/server/ai-stream'
1234
1265
  | '/server/ws'
1266
+ | '/$lang/docs/$'
1235
1267
  | '/apps/anyhub'
1236
1268
  | '/design/$projectId'
1237
1269
  | '/knowledge/$kbId'
@@ -1306,6 +1338,7 @@ export interface FileRouteTypes {
1306
1338
  to:
1307
1339
  | '/'
1308
1340
  | '/share'
1341
+ | '/docs'
1309
1342
  | '/workspace'
1310
1343
  | '/onboarding'
1311
1344
  | '/forgot-password'
@@ -1332,9 +1365,11 @@ export interface FileRouteTypes {
1332
1365
  | '/api/billing-webhook'
1333
1366
  | '/api/chat'
1334
1367
  | '/api/health'
1368
+ | '/api/search'
1335
1369
  | '/api/story-ai'
1336
1370
  | '/server/ai-stream'
1337
1371
  | '/server/ws'
1372
+ | '/$lang/docs/$'
1338
1373
  | '/apps/anyhub'
1339
1374
  | '/design/$projectId'
1340
1375
  | '/prd/$id'
@@ -1403,6 +1438,7 @@ export interface FileRouteTypes {
1403
1438
  | '/_nav'
1404
1439
  | '/_public'
1405
1440
  | '/share'
1441
+ | '/docs'
1406
1442
  | '/workspace'
1407
1443
  | '/_public/'
1408
1444
  | '/__desktop/onboarding'
@@ -1436,9 +1472,11 @@ export interface FileRouteTypes {
1436
1472
  | '/api/billing-webhook'
1437
1473
  | '/api/chat'
1438
1474
  | '/api/health'
1475
+ | '/api/search'
1439
1476
  | '/api/story-ai'
1440
1477
  | '/server/ai-stream'
1441
1478
  | '/server/ws'
1479
+ | '/$lang/docs/$'
1442
1480
  | '/_nav/apps/anyhub'
1443
1481
  | '/_nav/design/$projectId'
1444
1482
  | '/_nav/knowledge/$kbId'
@@ -1516,15 +1554,18 @@ export interface RootRouteChildren {
1516
1554
  NavRouteRoute: typeof NavRouteRouteWithChildren
1517
1555
  PublicRouteRoute: typeof PublicRouteRouteWithChildren
1518
1556
  ShareRouteRoute: typeof ShareRouteRouteWithChildren
1557
+ DocsRoute: typeof DocsRoute
1519
1558
  WorkspaceRoute: typeof WorkspaceRoute
1520
1559
  _desktopOnboardingRouteRoute: typeof _desktopOnboardingRouteRoute
1521
1560
  InviteTokenRouteRoute: typeof InviteTokenRouteRoute
1522
1561
  ApiBillingWebhookRoute: typeof ApiBillingWebhookRoute
1523
1562
  ApiChatRoute: typeof ApiChatRoute
1524
1563
  ApiHealthRoute: typeof ApiHealthRoute
1564
+ ApiSearchRoute: typeof ApiSearchRoute
1525
1565
  ApiStoryAiRoute: typeof ApiStoryAiRoute
1526
1566
  ServerAiStreamRoute: typeof ServerAiStreamRoute
1527
1567
  ServerWsRoute: typeof ServerWsRoute
1568
+ LangDocsSplatRouteRoute: typeof LangDocsSplatRouteRoute
1528
1569
  ApiLicenseActivateRouteRoute: typeof ApiLicenseActivateRouteRoute
1529
1570
  ApiLicenseHeartbeatRouteRoute: typeof ApiLicenseHeartbeatRouteRoute
1530
1571
  ApiAttachmentsUploadRoute: typeof ApiAttachmentsUploadRoute
@@ -1562,6 +1603,13 @@ declare module '@tanstack/react-router' {
1562
1603
  preLoaderRoute: typeof PublicRouteRouteImport
1563
1604
  parentRoute: typeof rootRouteImport
1564
1605
  }
1606
+ '/docs': {
1607
+ id: '/docs'
1608
+ path: '/docs'
1609
+ fullPath: '/docs'
1610
+ preLoaderRoute: typeof DocsRouteImport
1611
+ parentRoute: typeof rootRouteImport
1612
+ }
1565
1613
  '/share': {
1566
1614
  id: '/share'
1567
1615
  path: '/share'
@@ -1786,6 +1834,13 @@ declare module '@tanstack/react-router' {
1786
1834
  preLoaderRoute: typeof ApiHealthRouteImport
1787
1835
  parentRoute: typeof rootRouteImport
1788
1836
  }
1837
+ '/api/search': {
1838
+ id: '/api/search'
1839
+ path: '/api/search'
1840
+ fullPath: '/api/search'
1841
+ preLoaderRoute: typeof ApiSearchRouteImport
1842
+ parentRoute: typeof rootRouteImport
1843
+ }
1789
1844
  '/api/story-ai': {
1790
1845
  id: '/api/story-ai'
1791
1846
  path: '/api/story-ai'
@@ -1821,6 +1876,13 @@ declare module '@tanstack/react-router' {
1821
1876
  preLoaderRoute: typeof ShareVersionIdRouteRouteImport
1822
1877
  parentRoute: typeof ShareRouteRoute
1823
1878
  }
1879
+ '/$lang/docs/$': {
1880
+ id: '/$lang/docs/$'
1881
+ path: '/$lang/docs/$'
1882
+ fullPath: '/$lang/docs/$'
1883
+ preLoaderRoute: typeof LangDocsSplatRouteRouteImport
1884
+ parentRoute: typeof rootRouteImport
1885
+ }
1824
1886
  '/_nav/apps/anyhub': {
1825
1887
  id: '/_nav/apps/anyhub'
1826
1888
  path: '/anyhub'
@@ -2761,15 +2823,18 @@ const rootRouteChildren: RootRouteChildren = {
2761
2823
  NavRouteRoute: NavRouteRouteWithChildren,
2762
2824
  PublicRouteRoute: PublicRouteRouteWithChildren,
2763
2825
  ShareRouteRoute: ShareRouteRouteWithChildren,
2826
+ DocsRoute: DocsRoute,
2764
2827
  WorkspaceRoute: WorkspaceRoute,
2765
2828
  _desktopOnboardingRouteRoute: _desktopOnboardingRouteRoute,
2766
2829
  InviteTokenRouteRoute: InviteTokenRouteRoute,
2767
2830
  ApiBillingWebhookRoute: ApiBillingWebhookRoute,
2768
2831
  ApiChatRoute: ApiChatRoute,
2769
2832
  ApiHealthRoute: ApiHealthRoute,
2833
+ ApiSearchRoute: ApiSearchRoute,
2770
2834
  ApiStoryAiRoute: ApiStoryAiRoute,
2771
2835
  ServerAiStreamRoute: ServerAiStreamRoute,
2772
2836
  ServerWsRoute: ServerWsRoute,
2837
+ LangDocsSplatRouteRoute: LangDocsSplatRouteRoute,
2773
2838
  ApiLicenseActivateRouteRoute: ApiLicenseActivateRouteRoute,
2774
2839
  ApiLicenseHeartbeatRouteRoute: ApiLicenseHeartbeatRouteRoute,
2775
2840
  ApiAttachmentsUploadRoute: ApiAttachmentsUploadRoute,
@@ -0,0 +1,62 @@
1
+ import { createLazyFileRoute, Link } from '@tanstack/react-router'
2
+ import { Suspense, useEffect } from 'react'
3
+ import { DocsLayout } from 'fumadocs-ui/layouts/docs'
4
+ import {
5
+ DocsBody,
6
+ DocsDescription,
7
+ DocsPage,
8
+ DocsTitle,
9
+ } from 'fumadocs-ui/layouts/docs/page'
10
+ import { useFumadocsLoader } from 'fumadocs-core/source/client'
11
+ import { baseOptions, isDocsLang, type DocsLang } from '@/lib/layout.shared'
12
+ import { getMDXComponents } from '@/components/mdx'
13
+ import { docs } from '@/lib/source'
14
+ import { setLocale } from '@/paraglide/runtime.js'
15
+
16
+ export const Route = createLazyFileRoute('/$lang/docs/$')({
17
+ component: DocsRouteComponent,
18
+ })
19
+
20
+ function Content({ path, lang }: { path: string; lang: DocsLang }) {
21
+ const page = docs.getPage(path)
22
+ if (!page) throw new Error(`unknown page: ${path}`)
23
+
24
+ const { toc, body: MdxContent } = page
25
+
26
+ return (
27
+ <DocsPage toc={toc} className="pt-8">
28
+ <Link
29
+ to="/dashboard"
30
+ className="text-fd-muted-foreground hover:text-fd-foreground mb-2 inline-flex items-center gap-1 text-sm"
31
+ >
32
+ ← {lang === 'en' ? 'Back to App' : '返回应用'}
33
+ </Link>
34
+ <DocsTitle>{page.title}</DocsTitle>
35
+ <DocsDescription>{page.description}</DocsDescription>
36
+ <DocsBody>
37
+ <MdxContent components={getMDXComponents()} />
38
+ </DocsBody>
39
+ </DocsPage>
40
+ )
41
+ }
42
+
43
+ function DocsRouteComponent() {
44
+ const { lang: rawLang } = Route.useParams()
45
+ const data = useFumadocsLoader(Route.useLoaderData())
46
+
47
+ useEffect(() => {
48
+ if (isDocsLang(rawLang)) setLocale(rawLang)
49
+ }, [rawLang])
50
+
51
+ if (!isDocsLang(rawLang)) return null
52
+
53
+ const lang: DocsLang = rawLang
54
+
55
+ return (
56
+ <DocsLayout {...baseOptions(lang)} tree={data.pageTree}>
57
+ <Suspense>
58
+ <Content path={data.path} lang={lang} />
59
+ </Suspense>
60
+ </DocsLayout>
61
+ )
62
+ }
@@ -0,0 +1,52 @@
1
+ import { createFileRoute, notFound } from '@tanstack/react-router'
2
+ import { createServerFn } from '@tanstack/react-start'
3
+ import { docs, source } from '@/lib/source'
4
+
5
+ // fumadocs-ui 16.15.8 的 getLayoutTabs 遍历 root/folder 的 children 时
6
+ // 会 `for of child` 递归调用 next,但 page 节点(叶子)无 children 字段导致
7
+ // `node.children is not iterable`。给每个无 children 的节点补 `children: []`。
8
+ function fixPageTreeChildren<T extends { data: unknown }>(tree: T): T {
9
+ const patch = (node: { type?: string; children?: unknown[] }): void => {
10
+ if (!node || typeof node !== 'object') return
11
+ if (Array.isArray(node.children)) {
12
+ for (const child of node.children) patch(child as Parameters<typeof patch>[0])
13
+ } else {
14
+ ;(node as { children: unknown[] }).children = []
15
+ }
16
+ }
17
+ patch(tree.data as { children?: unknown[] })
18
+ return tree
19
+ }
20
+
21
+ const loadDocsPage = createServerFn({ method: 'GET' })
22
+ .validator((data: { slugs: string[]; lang: string }) => data)
23
+ .handler(async ({ data: { slugs, lang } }) => {
24
+ const page = source.getPage(slugs, lang)
25
+ if (!page) return null
26
+
27
+ const pageTree = fixPageTreeChildren(await source.serializePageTree(source.getPageTree(lang)))
28
+ return {
29
+ path: page.path,
30
+ title: page.data.title,
31
+ description: page.data.description,
32
+ pageTree,
33
+ }
34
+ })
35
+
36
+ export const Route = createFileRoute('/$lang/docs/$')({
37
+ loader: async ({ params }) => {
38
+ const slugs = params._splat?.split('/') ?? []
39
+ const data = await loadDocsPage({ data: { slugs, lang: params.lang } })
40
+ if (!data) throw notFound()
41
+ await docs.getPage(data.path)?.preload()
42
+ return data
43
+ },
44
+ head: ({ loaderData }) => ({
45
+ meta: [
46
+ { title: loaderData ? `${loaderData.title} - AllBlue` : 'AllBlue' },
47
+ ...(loaderData?.description
48
+ ? [{ name: 'description', content: loaderData.description }]
49
+ : []),
50
+ ],
51
+ }),
52
+ })
@@ -0,0 +1,45 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ vi.mock('@/paraglide/runtime.js', () => ({
4
+ getLocale: vi.fn(() => 'zh'),
5
+ }))
6
+
7
+ import { getLocale } from '@/paraglide/runtime.js'
8
+ import { Route } from '../docs'
9
+
10
+ type BeforeLoadFn = (ctx: never) => unknown
11
+ const beforeLoad = Route.options.beforeLoad as unknown as BeforeLoadFn
12
+
13
+ async function catchRedirectTarget(fn: () => unknown): Promise<string> {
14
+ try {
15
+ await fn()
16
+ return ''
17
+ } catch (e) {
18
+ const err = e as {
19
+ href?: string
20
+ options?: { to?: string; params?: Record<string, string> }
21
+ }
22
+ if (err.href) return err.href
23
+ if (err.options?.to) {
24
+ const p = err.options.params ?? {}
25
+ return err.options.to.replace(/\$(\w+)/g, (_, name: string) => p[name] ?? `$${name}`)
26
+ }
27
+ return JSON.stringify(e)
28
+ }
29
+ }
30
+
31
+ describe('/docs redirect', () => {
32
+ beforeEach(() => {
33
+ vi.clearAllMocks()
34
+ })
35
+
36
+ it('zh locale 重定向到 /zh/docs', async () => {
37
+ vi.mocked(getLocale).mockReturnValue('zh')
38
+ await expect(catchRedirectTarget(() => beforeLoad({} as never))).resolves.toContain('/zh/docs')
39
+ })
40
+
41
+ it('en locale 重定向到 /en/docs', async () => {
42
+ vi.mocked(getLocale).mockReturnValue('en')
43
+ await expect(catchRedirectTarget(() => beforeLoad({} as never))).resolves.toContain('/en/docs')
44
+ })
45
+ })
@@ -15,7 +15,7 @@
15
15
  // You should have received a copy of the GNU Affero General Public License
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
- import { Link, Outlet, createRootRouteWithContext, HeadContent, Scripts } from '@tanstack/react-router'
18
+ import { Link, Outlet, createRootRouteWithContext, HeadContent, Scripts, useParams } from '@tanstack/react-router'
19
19
  import { HotkeysProvider } from '@tanstack/react-hotkeys'
20
20
  import { ShortcutsPanel } from '@/components/hotkeys/shortcuts-panel'
21
21
  import { Analytics } from '@vercel/analytics/react'
@@ -23,6 +23,9 @@ import { SpeedInsights } from '@vercel/speed-insights/react'
23
23
  import globalsCss from '@/styles/globals.css?url'
24
24
  import type { RouterContext } from '@/lib/router-context'
25
25
  import { Providers } from '@/providers'
26
+ import { RootProvider } from 'fumadocs-ui/provider/tanstack'
27
+ import { i18nProvider } from 'fumadocs-ui/i18n'
28
+ import { translations } from '@/lib/layout.shared'
26
29
  import { Toaster } from '@/components/ui/sonner'
27
30
  import { DevtoolsPanel } from '@/components/devtools'
28
31
  import { getSessionFn } from '@/server/serverFns/auth/session'
@@ -62,6 +65,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
62
65
 
63
66
  function RootComponent() {
64
67
  const session = Route.useRouteContext({ select: (c) => c.session }) as Session | null
68
+ const { lang } = useParams({ strict: false }) as { lang?: string | undefined }
65
69
  return (
66
70
  <html lang={getLocale()} suppressHydrationWarning>
67
71
  <head>
@@ -69,12 +73,14 @@ function RootComponent() {
69
73
  </head>
70
74
  <body className="w-full h-full min-h-screen antialiased">
71
75
  <Providers session={session}>
72
- <HotkeysProvider>
73
- <Outlet />
74
- <ShortcutsPanel />
75
- <Toaster position="top-center" />
76
- {import.meta.env.DEV && <DevtoolsPanel />}
77
- </HotkeysProvider>
76
+ <RootProvider i18n={i18nProvider(translations, lang)}>
77
+ <HotkeysProvider>
78
+ <Outlet />
79
+ <ShortcutsPanel />
80
+ <Toaster position="top-center" />
81
+ {import.meta.env.DEV && <DevtoolsPanel />}
82
+ </HotkeysProvider>
83
+ </RootProvider>
78
84
  </Providers>
79
85
  <Scripts />
80
86
  <Analytics />
@@ -0,0 +1,13 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { createFromSource } from 'fumadocs-core/search/server'
3
+ import { source } from '@/lib/source'
4
+
5
+ const server = createFromSource(source)
6
+
7
+ export const Route = createFileRoute('/api/search')({
8
+ server: {
9
+ handlers: {
10
+ GET: async ({ request }) => server.GET(request),
11
+ },
12
+ },
13
+ })
@@ -0,0 +1,9 @@
1
+ import { createFileRoute, redirect } from '@tanstack/react-router'
2
+ import { getLocale } from '@/paraglide/runtime.js'
3
+
4
+ export const Route = createFileRoute('/docs')({
5
+ beforeLoad: () => {
6
+ const lang = getLocale()
7
+ throw redirect({ to: '/$lang/docs/$', params: { lang, _splat: '' } })
8
+ },
9
+ })
@@ -1,6 +1,8 @@
1
1
  @import './_variables.scss';
2
2
  @import './_keyframe-animations.scss';
3
3
  @import "tailwindcss";
4
+ @import 'fumadocs-ui/css/neutral.css';
5
+ @import 'fumadocs-ui/css/preset.css';
4
6
  @import "@fontsource-variable/geist";
5
7
  @import "@fontsource-variable/geist-mono";
6
8
  @import "./typeset.css";