@allbluecn/web-app 0.10.2 → 0.11.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 (169) hide show
  1. package/package.json +26 -11
  2. package/public/favicon.ico +0 -0
  3. package/public/logo/allblue-logo-app-icon.svg +7 -0
  4. package/public/logo/allblue-logo-dark-square.svg +4 -0
  5. package/public/logo/allblue-logo-dark.svg +4 -0
  6. package/public/logo/allblue-logo-light-square.svg +4 -0
  7. package/public/logo/allblue-logo-light.svg +4 -0
  8. package/src/__tests__/bench/baselines/format-timestamp.json +44 -0
  9. package/src/__tests__/bench/core-hot-path.bench.ts +71 -0
  10. package/src/__tests__/integration/db-crud.integration.test.ts +79 -0
  11. package/src/__tests__/rejects-tothrow-regression.test.ts +31 -0
  12. package/src/__tests__/security/ai/prompt-injection.test.ts +105 -0
  13. package/src/__tests__/security/ssrf-url-whitelist.test.ts +54 -0
  14. package/src/__tests__/security/xss-sanitize.test.ts +99 -0
  15. package/src/__tests__/setup.ts +9 -0
  16. package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-dark-baseline-1-chromium-darwin.png +0 -0
  17. package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-light-baseline-1-chromium-darwin.png +0 -0
  18. package/src/__tests__/visual/__screenshots__/user-avatar.vrt.test.tsx/user-avatar-initial-fallback-baseline-1-chromium-darwin.png +0 -0
  19. package/src/__tests__/visual/theme-surface.vrt.test.tsx +54 -0
  20. package/src/__tests__/visual/user-avatar.vrt.test.tsx +32 -0
  21. package/src/components/error/global-error-boundary.tsx +2 -2
  22. package/src/components/knowledge/general-groups.ts +15 -0
  23. package/src/components/knowledge/kb-filter-popover.tsx +143 -0
  24. package/src/components/knowledge-public/legal-compliance-editor.tsx +4 -2
  25. package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +9 -6
  26. package/src/components/layout/sidebar-03/nav-knowledge.tsx +19 -17
  27. package/src/components/layout/sidebar-03/nav-secondary.tsx +57 -47
  28. package/src/components/layout/sidebar-03/nav-workspace.tsx +20 -22
  29. package/src/components/mdx.tsx +9 -0
  30. package/src/components/shared-kit/avatars/notion-avatar.tsx +2 -1
  31. package/src/components/shared-kit/icons/__tests__/lucide-curated-icons.test.ts +2 -2
  32. package/src/components/shared-kit/icons/__tests__/lucide-icon-picker.test.tsx +3 -2
  33. package/src/components/shared-kit/icons/lucide-icon-picker/curated-icons.ts +5 -10
  34. package/src/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name.tsx +3 -3
  35. package/src/components/shared-kit/theme/theme-surface.tsx +52 -0
  36. package/src/components/story/relations/general-groups.ts +1 -15
  37. package/src/components/story/relations/knowledge-select-dialog.tsx +13 -87
  38. package/src/components/story/story-list-view.tsx +7 -1
  39. package/src/components/tiptap/node/table-node/ui/table-move-row-column-button/use-table-move-row-column.ts +0 -2
  40. package/src/components/workspace/__tests__/activity/workspace-activity-card.test.tsx +36 -0
  41. package/src/components/workspace/__tests__/contribution/workspace-contribution-card.test.tsx +17 -0
  42. package/src/components/workspace/__tests__/dialogs/create-workspace-dialog.test.tsx +63 -0
  43. package/src/components/workspace/__tests__/dialogs/delete-workspace-options.test.tsx +89 -0
  44. package/src/components/workspace/__tests__/dialogs/edit-workspace-dialog.test.tsx +68 -0
  45. package/src/components/workspace/__tests__/header/workspace-header.test.tsx +40 -0
  46. package/src/components/workspace/__tests__/kb/workspace-kb-card.test.tsx +154 -0
  47. package/src/components/workspace/__tests__/members/workspace-member-item.test.tsx +24 -0
  48. package/src/components/workspace/__tests__/members/workspace-members-manager.test.tsx +51 -0
  49. package/src/components/workspace/__tests__/projects/workspace-project-list.test.tsx +46 -0
  50. package/src/components/workspace/{create-workspace-dialog.tsx → dialogs/create-workspace-dialog.tsx} +1 -1
  51. package/src/components/workspace/{edit-workspace-dialog.tsx → dialogs/edit-workspace-dialog.tsx} +1 -1
  52. package/src/components/workspace/kb/workspace-kb-card.tsx +594 -0
  53. package/src/components/workspace/kb/workspace-kb-manage-dialog.tsx +174 -0
  54. package/src/components/workspace/kb/workspace-quick-doc-dialog.tsx +431 -0
  55. package/src/components/workspace/{workspace-members-manager.tsx → members/workspace-members-manager.tsx} +78 -40
  56. package/src/lib/__tests__/msw-sample.test.ts +68 -0
  57. package/src/lib/__tests__/pgvector.test.ts +2 -2
  58. package/src/lib/changelog/sdk-versions.ts +21 -21
  59. package/src/lib/collections/knowledge/knowledge-bases-collection.ts +3 -1
  60. package/src/lib/collections/reference/workspaces-collection.ts +3 -1
  61. package/src/lib/fetch-json.ts +23 -0
  62. package/src/lib/fumadocs-i18n.ts +7 -0
  63. package/src/{components/workspace/workspace-kb-card.tsx → lib/i18n/resolver.ts} +6 -12
  64. package/src/lib/layout.shared.tsx +40 -0
  65. package/src/lib/sanitize.ts +14 -1
  66. package/src/lib/session.ts +9 -1
  67. package/src/lib/source.ts +15 -0
  68. package/src/lib/vector-store/__tests__/factory.test.ts +1 -1
  69. package/src/lib/vector-store/__tests__/sqlite-vec-store.test.ts +2 -2
  70. package/src/plugins/core/tags/__tests__/tags.test.ts +10 -10
  71. package/src/routeTree.gen.ts +65 -0
  72. package/src/routes/$lang/docs/$/route.lazy.tsx +62 -0
  73. package/src/routes/$lang/docs/$/route.tsx +52 -0
  74. package/src/routes/-tests/docs-redirect.test.ts +45 -0
  75. package/src/routes/__root.tsx +13 -7
  76. package/src/routes/__tests__/-edition-routing.test.tsx +4 -4
  77. package/src/routes/__tests__/route-tree-structure.test.ts +65 -0
  78. package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +5 -2
  79. package/src/routes/_nav/knowledge/index.lazy.tsx +1 -1
  80. package/src/routes/_nav/knowledge/laws/index.lazy.tsx +5 -2
  81. package/src/routes/_nav/route.tsx +21 -0
  82. package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +13 -11
  83. package/src/routes/_nav/workspaces/$workspaceId/route.tsx +4 -2
  84. package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +1 -1
  85. package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
  86. package/src/routes/api/search.ts +13 -0
  87. package/src/routes/docs.tsx +9 -0
  88. package/src/server/__tests__/agent-crud.test.ts +4 -4
  89. package/src/server/__tests__/chat-conversations.test.ts +1 -3
  90. package/src/server/__tests__/fetch-guard.test.ts +99 -0
  91. package/src/server/__tests__/permissions.test.ts +1 -3
  92. package/src/server/__tests__/resources.team-seats.test.ts +2 -0
  93. package/src/server/__tests__/skill-crud.test.ts +3 -3
  94. package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +95 -0
  95. package/src/server/ai-gateway/__tests__/kb-context-guard.test.ts +136 -0
  96. package/src/server/ai-gateway/__tests__/knowledge-search-fence.test.ts +240 -0
  97. package/src/server/ai-gateway/__tests__/llm-fence.test.ts +125 -0
  98. package/src/server/ai-gateway/__tests__/run-store.test.ts +52 -0
  99. package/src/server/ai-gateway/ai-output-guard.ts +77 -0
  100. package/src/server/ai-gateway/index.ts +5 -0
  101. package/src/server/ai-gateway/kb-context-guard.ts +46 -0
  102. package/src/server/ai-gateway/llm-fence.ts +82 -0
  103. package/src/server/ai-gateway/persistence/run-store.ts +19 -0
  104. package/src/server/ai-gateway/story-prompt.ts +4 -2
  105. package/src/server/ai-gateway/tools/definitions.ts +12 -6
  106. package/src/server/ai-gateway/tools/server.ts +97 -54
  107. package/src/server/builtin-project.ts +21 -0
  108. package/src/server/email/sender.ts +8 -2
  109. package/src/server/fetch-guard.ts +170 -0
  110. package/src/server/middlewares/security-headers.ts +113 -0
  111. package/src/server/serverFns/__tests__/billing.test.ts +263 -0
  112. package/src/server/serverFns/__tests__/link-preview.test.ts +239 -0
  113. package/src/server/serverFns/__tests__/project-kb.test.ts +3 -3
  114. package/src/server/serverFns/__tests__/project-team-assign.test.ts +1 -1
  115. package/src/server/serverFns/__tests__/story-comments.test.ts +265 -0
  116. package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +266 -0
  117. package/src/server/serverFns/__tests__/story-crud-labels-users.test.ts +227 -0
  118. package/src/server/serverFns/__tests__/story-crud.test.ts +526 -0
  119. package/src/server/serverFns/__tests__/story-links.test.ts +262 -0
  120. package/src/server/serverFns/__tests__/story-list-query.test.ts +259 -0
  121. package/src/server/serverFns/__tests__/story-reactions.test.ts +230 -0
  122. package/src/server/serverFns/__tests__/story-tree.test.ts +317 -0
  123. package/src/server/serverFns/__tests__/story-types.test.ts +294 -0
  124. package/src/server/serverFns/__tests__/team-accept.test.ts +6 -6
  125. package/src/server/serverFns/__tests__/team-invitation.test.ts +3 -3
  126. package/src/server/serverFns/__tests__/team-join-request.test.ts +8 -8
  127. package/src/server/serverFns/__tests__/update-seats.test.ts +4 -4
  128. package/src/server/serverFns/__tests__/workspace-kb-idor.test.ts +160 -0
  129. package/src/server/serverFns/__tests__/workspace-kb-utils.test.ts +54 -0
  130. package/src/server/serverFns/__tests__/workspace-kb.test.ts +348 -0
  131. package/src/server/serverFns/__tests__/workspace.test.ts +28 -8
  132. package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +3 -5
  133. package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +1 -1
  134. package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +2 -2
  135. package/src/server/serverFns/kb-doc-search.ts +111 -0
  136. package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
  137. package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
  138. package/src/server/serverFns/story/__tests__/bulk.test.ts +1 -1
  139. package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +2 -6
  140. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +2 -4
  141. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +2 -2
  142. package/src/server/serverFns/story/__tests__/story-groups.test.ts +2 -2
  143. package/src/server/serverFns/story/kb-general-consts.ts +39 -0
  144. package/src/server/serverFns/story/kb-general.ts +30 -17
  145. package/src/server/serverFns/story/story-assoc.ts +13 -103
  146. package/src/server/serverFns/story/story-crud.ts +2 -5
  147. package/src/server/serverFns/story/story-groups.ts +4 -13
  148. package/src/server/serverFns/team.ts +1 -1
  149. package/src/server/serverFns/workspace-kb-utils.ts +138 -0
  150. package/src/server/serverFns/workspace-kb.ts +403 -0
  151. package/src/server/serverFns/workspace.ts +4 -1
  152. package/src/shared/ai-cli/__tests__/adapter.test.ts +67 -0
  153. package/src/shared/ai-cli/__tests__/cli-detector.test.ts +99 -0
  154. package/src/shared/ai-cli/adapter.ts +2 -2
  155. package/src/shared/ai-cli/cli-detector.ts +18 -9
  156. package/src/start.ts +2 -1
  157. package/src/styles/globals.css +116 -0
  158. package/src/types/jest-dom-vitest.d.ts +8 -7
  159. package/vite.shared.ts +18 -1
  160. package/public/file.svg +0 -1
  161. package/public/globe.svg +0 -1
  162. package/public/window.svg +0 -1
  163. package/src/lib/__tests__/cli-detector.test.ts +0 -80
  164. /package/src/components/workspace/{workspace-activity-card.tsx → activity/workspace-activity-card.tsx} +0 -0
  165. /package/src/components/workspace/{workspace-contribution-card.tsx → contribution/workspace-contribution-card.tsx} +0 -0
  166. /package/src/components/workspace/{delete-workspace-options.tsx → dialogs/delete-workspace-options.tsx} +0 -0
  167. /package/src/components/workspace/{workspace-header.tsx → header/workspace-header.tsx} +0 -0
  168. /package/src/components/workspace/{workspace-member-item.tsx → members/workspace-member-item.tsx} +0 -0
  169. /package/src/components/workspace/{workspace-project-list.tsx → projects/workspace-project-list.tsx} +0 -0
@@ -70,11 +70,14 @@ import { Link, useLocation } from "@tanstack/react-router";
70
70
  import { cn } from "@/lib/utils";
71
71
  import { isParentActive } from "@/lib/sidebar-utils";
72
72
  import { useEffect, useState } from "react";
73
- import { useLiveQuery } from "@tanstack/react-db";
74
- import { knowledgeBasesCollection } from "@/lib/collections/knowledge/knowledge-bases-collection";
73
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
74
+ import { KNOWLEDGE_BASES_QUERY_KEY } from "@/lib/collections/knowledge/knowledge-bases-collection";
75
+ import {
76
+ listKnowledgeFn,
77
+ toggleKnowledgePinFn,
78
+ } from "@/server/serverFns/knowledge";
75
79
  import { KnowledgeFormDialog } from "@/components/knowledge/dialog/knowledge-form-dialog";
76
80
  import { useServerFn } from "@tanstack/react-start";
77
- import { toggleKnowledgePinFn } from "@/server/serverFns/knowledge";
78
81
 
79
82
  import { AnimatedGradientText } from "@/components/magicui/animated-gradient-text";
80
83
  import { m } from "@/paraglide/messages.js";
@@ -151,6 +154,7 @@ interface KnowledgeItem {
151
154
 
152
155
  function UnpinButton({ kb }: { kb: KnowledgeItem }) {
153
156
  const togglePin = useServerFn(toggleKnowledgePinFn);
157
+ const queryClient = useQueryClient();
154
158
  const [pinned, setPinned] = useState(kb.pinnedToSidebar);
155
159
 
156
160
  const handleClick = async () => {
@@ -158,7 +162,7 @@ function UnpinButton({ kb }: { kb: KnowledgeItem }) {
158
162
  setPinned(newPinned);
159
163
  try {
160
164
  await togglePin({ data: { kbId: kb.id, pinned: newPinned } });
161
- (knowledgeBasesCollection as any).utils?.refetch?.();
165
+ void queryClient.invalidateQueries({ queryKey: KNOWLEDGE_BASES_QUERY_KEY });
162
166
  window.dispatchEvent(new CustomEvent("knowledge-pinned", { detail: { kbId: kb.id, pinned: newPinned } }));
163
167
  } catch {
164
168
  setPinned(!newPinned);
@@ -183,11 +187,13 @@ function UnpinButton({ kb }: { kb: KnowledgeItem }) {
183
187
  }
184
188
 
185
189
  function KnowledgeBaseLinks({ pathname }: { pathname: string }) {
186
- const { data: items } = useLiveQuery(
187
- (q) => q.from({ kb: knowledgeBasesCollection })
188
- );
189
- const itemsList =
190
- (items as unknown as Array<KnowledgeItem> | undefined) ?? [];
190
+ // useQuery 直读 per-request queryClient(_nav loader 已 SSR 预取),
191
+ // 列表随 SSR HTML 直出,刷新无需等待;避免 module-level collection 的跨请求泄漏
192
+ const { data: items } = useQuery({
193
+ queryKey: KNOWLEDGE_BASES_QUERY_KEY,
194
+ queryFn: () => listKnowledgeFn(),
195
+ });
196
+ const itemsList = (items as unknown as Array<KnowledgeItem> | undefined) ?? [];
191
197
 
192
198
  if (itemsList.length === 0) return null;
193
199
 
@@ -228,18 +234,14 @@ export function NavKnowledge() {
228
234
  const { state } = useSidebar();
229
235
  const { pathname } = useLocation();
230
236
  const isCollapsed = state === "collapsed";
231
- const [mounted, setMounted] = useState(false);
232
237
  const [open, setOpen] = useState(false);
233
238
  const [createOpen, setCreateOpen] = useState(false);
239
+ const queryClient = useQueryClient();
234
240
  const active = isParentActive(pathname, "/knowledge");
235
241
 
236
- useEffect(() => {
237
- setMounted(true);
238
- }, []);
239
-
240
242
  useEffect(() => {
241
243
  const refresh = () => {
242
- (knowledgeBasesCollection as any).utils?.refetch?.();
244
+ void queryClient.invalidateQueries({ queryKey: KNOWLEDGE_BASES_QUERY_KEY });
243
245
  };
244
246
  window.addEventListener("knowledge-created", refresh);
245
247
  window.addEventListener("knowledge-updated", refresh);
@@ -251,7 +253,7 @@ export function NavKnowledge() {
251
253
  window.removeEventListener("knowledge-deleted", refresh);
252
254
  window.removeEventListener("knowledge-pinned", refresh);
253
255
  };
254
- }, []);
256
+ }, [queryClient]);
255
257
 
256
258
  return (
257
259
  <SidebarGroup className="p-0 space-y-1">
@@ -350,7 +352,7 @@ export function NavKnowledge() {
350
352
  )}
351
353
  </Collapsible>
352
354
 
353
- {!isCollapsed && mounted && (
355
+ {!isCollapsed && (
354
356
  <KnowledgeBaseLinks pathname={pathname} />
355
357
  )}
356
358
 
@@ -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
  ))}
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { Link, useLocation } from "@tanstack/react-router";
19
19
  import { useEffect, useState } from "react";
20
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
20
21
  import { Layers, Plus, PinOff } from "lucide-react";
21
22
  import {
22
23
  SidebarGroup,
@@ -29,22 +30,23 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
29
30
  import { cn } from "@/lib/utils";
30
31
  import { isParentActive } from "@/lib/sidebar-utils";
31
32
  import { LucideIconByName } from "@/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name";
32
- import { CreateWorkspaceDialog } from "@/components/workspace/create-workspace-dialog";
33
- import { useLiveQuery } from "@tanstack/react-db";
34
- import { workspacesCollection } from "@/lib/collections/reference/workspaces-collection";
35
- import { toggleWorkspacePinFn } from "@/server/serverFns/workspace";
33
+ import { CreateWorkspaceDialog } from "@/components/workspace/dialogs/create-workspace-dialog";
34
+ import { WORKSPACES_QUERY_KEY } from "@/lib/collections/reference/workspaces-collection";
35
+ import {
36
+ listPinnedWorkspacesBriefFn,
37
+ toggleWorkspacePinFn,
38
+ } from "@/server/serverFns/workspace";
36
39
  import { m } from "@/paraglide/messages.js";
37
40
 
38
41
  function WorkspaceLinks({ pathname }: { pathname: string }) {
39
- const { data: workspaces } = useLiveQuery((q) =>
40
- q.from({ w: workspacesCollection })
41
- );
42
- const workspacesList =
43
- (workspaces as unknown as Array<{
44
- id: string;
45
- name: string;
46
- icon: string;
47
- }> | undefined) ?? [];
42
+ // useQuery 直读 per-request queryClient(_nav loader SSR 预取),
43
+ // 列表随 SSR HTML 直出,刷新无需等待;避免 module-level collection 的跨请求泄漏
44
+ const { data } = useQuery({
45
+ queryKey: WORKSPACES_QUERY_KEY,
46
+ queryFn: () => listPinnedWorkspacesBriefFn(),
47
+ });
48
+ const queryClient = useQueryClient();
49
+ const workspacesList = data ?? [];
48
50
 
49
51
  return (
50
52
  <SidebarMenu className="space-y-1">
@@ -90,7 +92,7 @@ function WorkspaceLinks({ pathname }: { pathname: string }) {
90
92
  className="absolute right-1 top-1/2 -translate-y-1/2 flex size-5 items-center justify-center rounded text-sidebar-foreground/70 opacity-0 transition-opacity group-hover/ws:opacity-100 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
91
93
  onClick={async () => {
92
94
  await toggleWorkspacePinFn({ data: { workspaceId: ws.id, pinned: false } });
93
- (workspacesCollection as any).utils?.refetch?.();
95
+ void queryClient.invalidateQueries({ queryKey: WORKSPACES_QUERY_KEY });
94
96
  window.dispatchEvent(new CustomEvent("workspace-pinned", { detail: { workspaceId: ws.id, pinned: false } }));
95
97
  }}
96
98
  >
@@ -112,16 +114,12 @@ export function NavWorkspace() {
112
114
  const { state } = useSidebar();
113
115
  const { pathname } = useLocation();
114
116
  const isCollapsed = state === "collapsed";
115
- const [mounted, setMounted] = useState(false);
116
117
  const [createOpen, setCreateOpen] = useState(false);
117
-
118
- useEffect(() => {
119
- setMounted(true);
120
- }, []);
118
+ const queryClient = useQueryClient();
121
119
 
122
120
  useEffect(() => {
123
121
  const refresh = () => {
124
- (workspacesCollection as any).utils?.refetch?.();
122
+ void queryClient.invalidateQueries({ queryKey: WORKSPACES_QUERY_KEY });
125
123
  };
126
124
  window.addEventListener("workspace-created", refresh);
127
125
  window.addEventListener("workspace-deleted", refresh);
@@ -133,7 +131,7 @@ export function NavWorkspace() {
133
131
  window.removeEventListener("workspace-updated", refresh);
134
132
  window.removeEventListener("workspace-pinned", refresh);
135
133
  };
136
- }, []);
134
+ }, [queryClient]);
137
135
 
138
136
  if (isCollapsed) {
139
137
  return (
@@ -173,7 +171,7 @@ export function NavWorkspace() {
173
171
  <Plus className="size-3.5" />
174
172
  </button>
175
173
  </div>
176
- {mounted && <WorkspaceLinks pathname={pathname} />}
174
+ <WorkspaceLinks pathname={pathname} />
177
175
  <CreateWorkspaceDialog open={createOpen} onOpenChange={setCreateOpen} />
178
176
  </SidebarGroup>
179
177
  );
@@ -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
+ }
@@ -19,6 +19,7 @@
19
19
 
20
20
  import { useEffect, useState } from "react"
21
21
  import type { AvatarConfig, AvatarPart, ShapeType } from "@allbluecn/kernel"
22
+ import { sanitizeSvg } from "../../../lib/sanitize"
22
23
 
23
24
  const noneParts: AvatarPart[] = ["glasses", "hair", "accessories", "details", "beard"]
24
25
 
@@ -120,7 +121,7 @@ export function NotionAvatar({
120
121
  aspectRatio: "1 / 1",
121
122
  ...style,
122
123
  }}
123
- dangerouslySetInnerHTML={{ __html: svgContent }}
124
+ dangerouslySetInnerHTML={{ __html: sanitizeSvg(svgContent) }}
124
125
  />
125
126
  )
126
127
  }
@@ -35,8 +35,8 @@ const NUMBER_TAIL_ALLOWLIST = new Set([
35
35
  ]);
36
36
 
37
37
  describe("CURATED_ICONS 精选名单完整性", () => {
38
- it("数量在目标区间 400-520", () => {
39
- expect(CURATED_ICONS.length).toBeGreaterThanOrEqual(400);
38
+ it("数量在目标区间 380-520", () => {
39
+ expect(CURATED_ICONS.length).toBeGreaterThanOrEqual(380);
40
40
  expect(CURATED_ICONS.length).toBeLessThanOrEqual(520);
41
41
  });
42
42
 
@@ -34,11 +34,12 @@ describe("LucideIconByName", () => {
34
34
  expect(svg?.innerHTML).toContain("<path");
35
35
  });
36
36
 
37
- it("精选外/未知 name 渲染 fallback(默认 Circle 占位)", () => {
37
+ it("精选外/未知 name 渲染默认占位图标(FaceSlightlySmilingPlus)", () => {
38
38
  const { container } = render(<LucideIconByName name="not-exist-icon" />);
39
39
  const svg = container.querySelector("svg");
40
40
  expect(svg).toBeTruthy();
41
- expect(svg?.innerHTML).toContain("<circle");
41
+ expect(svg?.getAttribute("class")).toContain("lucide-face-slightly-smiling-plus");
42
+ expect(svg?.innerHTML).toContain("<path");
42
43
  });
43
44
 
44
45
  it("undefined name 渲染 fallback", () => {
@@ -20,7 +20,7 @@
20
20
  // 机械禁则由 lucide-curated-icons.test.ts 强制校验。
21
21
 
22
22
  import {
23
- Accessibility, Activity, Ambulance, Ampersand, Anchor, Angry, Aperture, Archive, ArrowDown, ArrowDownLeft,
23
+ Accessibility, Activity, Ambulance, Ampersand, Anchor, Aperture, Archive, ArrowDown, ArrowDownLeft,
24
24
  ArrowDownRight, ArrowLeft, ArrowRight, ArrowUp, ArrowUpLeft, ArrowUpRight, Asterisk, AtSign, Atom, AudioLines,
25
25
  Award, BadgeCheck, Bandage, Banknote, Barcode, Bath, Battery, BatteryCharging, Bed, BedDouble,
26
26
  Beer, Bell, BellPlus, BellRing, Bike, Binary, Binoculars, Bird, Bold, Book,
@@ -35,15 +35,15 @@ import {
35
35
  EthernetPort, ExternalLink, Eye, Factory, FastForward, File, FileArchive, FileBadge, FileBox, FileCheck,
36
36
  FileClock, FileCode, FilePlus, FileText, Files, Film, Fish, Flag, FlaskConical, Flower,
37
37
  Folder, FolderCheck, FolderClock, FolderClosed, FolderCode, FolderGit, FolderHeart, FolderKanban, FolderOpen, FolderPlus,
38
- FolderRoot, FolderSearch, FolderSync, FolderTree, Frown, Funnel, Gamepad, Gauge, Gift, Ghost,
38
+ FolderRoot, FolderSearch, FolderSync, FolderTree, Funnel, Gamepad, Gauge, Gift, Ghost,
39
39
  GitBranch, GitCommitHorizontal, GitCommitVertical, GitCompare, GitFork, GitGraph, GitMerge, Globe, GraduationCap, Grid2x2Check,
40
40
  Grid2x2Plus, Guitar, Hammer, HardDrive, Hash, Headphones, Headset, Heart, HeartPulse, Helicopter,
41
41
  Hexagon, Hospital, Hourglass, IdCard, Image, ImageDown, ImagePlus, ImageUp, Images, Inbox,
42
42
  Infinity, Info, Italic, Joystick, Key, KeyRound, Keyboard, Lamp, Landmark, Laptop,
43
- Laugh, Layers, LayoutDashboard, LayoutFreeform, LayoutGrid, LayoutList, LayoutPanelLeft, LayoutTemplate, Leaf, Library,
43
+ Layers, LayoutDashboard, LayoutFreeform, LayoutGrid, LayoutList, LayoutPanelLeft, LayoutTemplate, Leaf, Library,
44
44
  Lightbulb, Link, List, ListChecks, ListOrdered, Loader, LoaderCircle, LoaderPinwheel, LocateFixed, Lock,
45
45
  LogIn, LogOut, Luggage, Mail, MailCheck, MailOpen, MailPlus, MailSearch, MailWarning, Mailbox,
46
- Map, MapPin, Medal, Megaphone, Meh, MessageCircle, MessageCircleCheck, MessageCircleHeart, MessageCircleMore, MessageCirclePlus,
46
+ Map, MapPin, Medal, Megaphone, MessageCircle, MessageCircleCheck, MessageCircleHeart, MessageCircleMore, MessageCirclePlus,
47
47
  MessageCircleQuestionMark, MessageSquare, MessageSquareCheck, MessageSquareCode, MessageSquareLock, MessagesSquare, Mic, Microscope, Minus, Monitor,
48
48
  Moon, Mountain, MountainSnow, Mouse, MousePointer, MousePointerClick, Music, Navigation, Network, Newspaper,
49
49
  Nfc, Octagon, PaintBucket, Paintbrush, Palette, PanelTop, PartyPopper, Pause, PenLine, PenTool,
@@ -52,7 +52,7 @@ import {
52
52
  Reply, Rewind, Rocket, Route, Sailboat, Satellite, Save, Scan, ScanBarcode, ScanFace,
53
53
  ScanLine, ScanSearch, ScanText, School, Search, SearchAlert, SearchCode, Send, Server, Shapes,
54
54
  Shield, ShieldAlert, ShieldCheck, Ship, ShoppingBag, ShoppingBasket, ShoppingCart, Sigma, Signal, Signpost,
55
- Siren, SkipBack, SkipForward, Slash, SlidersHorizontal, Smartphone, Smile, Skull, Snowflake, Sofa,
55
+ Siren, SkipBack, SkipForward, Slash, SlidersHorizontal, Smartphone, Skull, Snowflake, Sofa,
56
56
  Spade, Sparkles, Spline, Sprout, Square, SquarePen, SquareTerminal, Stamp, Star, StarCheck,
57
57
  StarHalf, StarMinus, StarPlus, Stethoscope, Store, Strikethrough, Sun, Sunrise, Sunset, SwatchBook,
58
58
  Syringe, Table, TableOfContents, Tablet, Tag, Tags, Target, Tent, Terminal, TestTube,
@@ -391,11 +391,6 @@ export const CURATED_ICONS: CuratedIcon[] = [
391
391
  { name: "star-minus", Icon: StarMinus, zh: ["减星"] },
392
392
  { name: "star-plus", Icon: StarPlus, zh: ["加星"] },
393
393
  { name: "thumbs-down", Icon: ThumbsDown, zh: ["踩"] },
394
- { name: "smile", Icon: Smile, zh: ["微笑"] },
395
- { name: "frown", Icon: Frown, zh: ["愁眉"] },
396
- { name: "meh", Icon: Meh, zh: ["一般"] },
397
- { name: "laugh", Icon: Laugh, zh: ["大笑"] },
398
- { name: "angry", Icon: Angry, zh: ["愤怒"] },
399
394
  { name: "map", Icon: Map, zh: ["地图"] },
400
395
  { name: "map-pin", Icon: MapPin, zh: ["地图钉"] },
401
396
  { name: "navigation", Icon: Navigation, zh: ["导航"] },
@@ -16,7 +16,7 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  import type { ReactNode } from "react";
19
- import { Smile } from "lucide-react";
19
+ import { FaceSlightlySmilingPlus } from "lucide-react";
20
20
  import { cn } from "@/lib/utils";
21
21
  import { CURATED_ICON_MAP } from "./curated-icons";
22
22
  import { pascalToKebab } from "./icon-data";
@@ -48,7 +48,7 @@ interface LucideIconByNameProps {
48
48
  name?: string;
49
49
  size?: number;
50
50
  className?: string;
51
- /** 默认:Smile 微笑脸(未选择图标时的占位) */
51
+ /** 默认:FaceSlightlySmilingPlus 微笑脸(未选择图标时的占位) */
52
52
  fallback?: ReactNode;
53
53
  }
54
54
 
@@ -64,7 +64,7 @@ export function LucideIconByName({
64
64
  return (
65
65
  <>
66
66
  {fallback ?? (
67
- <Smile
67
+ <FaceSlightlySmilingPlus
68
68
  size={size}
69
69
  aria-hidden
70
70
  className={cn("text-muted-foreground", className)}
@@ -0,0 +1,52 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ // 测试专用样板组件:VRT 视觉回归基线的主题表面聚合。
19
+ // 不接入任何路由,不加入 shared-kit/index.ts 导出(避免被产品代码误引用)。
20
+ // 文案为测试样板硬编码中文(不出现在产品 UI),豁免 i18n。
21
+
22
+ "use client"
23
+
24
+ import { Button } from "@/components/ui/button"
25
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
26
+ import { Input } from "@/components/ui/input"
27
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
28
+
29
+ export function ThemeSurface() {
30
+ return (
31
+ <div data-testid="theme-surface" className="bg-background text-foreground p-6 space-y-4 w-96">
32
+ <Card>
33
+ <CardHeader><CardTitle className="text-base">语义色表面</CardTitle></CardHeader>
34
+ <CardContent className="space-y-3">
35
+ <Input placeholder="输入框 placeholder" />
36
+ <div className="flex gap-2">
37
+ <Button size="sm">主按钮</Button>
38
+ <Button size="sm" variant="secondary">次按钮</Button>
39
+ <Button size="sm" variant="destructive">危险</Button>
40
+ <Tooltip>
41
+ <TooltipTrigger asChild>
42
+ <Button size="sm" variant="outline">带提示</Button>
43
+ </TooltipTrigger>
44
+ <TooltipContent>工具提示内容</TooltipContent>
45
+ </Tooltip>
46
+ </div>
47
+ <div className="rounded-lg border bg-muted p-3 text-sm text-muted-foreground">muted 区域</div>
48
+ </CardContent>
49
+ </Card>
50
+ </div>
51
+ )
52
+ }
@@ -1,15 +1 @@
1
- import { m } from '@/paraglide/messages.js'
2
-
3
- const GROUP_LABELS: Record<string, () => string> = {
4
- legal: m.story_knowledgeDialog_groupLegal,
5
- compliance: m.story_knowledgeDialog_groupCompliance,
6
- dictionary: m.story_knowledgeDialog_groupDictionary,
7
- bookmark: m.story_knowledgeDialog_groupBookmark,
8
- summary: m.story_knowledgeDialog_groupSummary,
9
- }
10
-
11
- /** 通用知识库分组 key → 标签(未知 key 原样返回) */
12
- export function generalGroupLabel(group: string): string {
13
- const labelFn = GROUP_LABELS[group]
14
- return labelFn ? labelFn() : group
15
- }
1
+ export { generalGroupLabel } from "@/components/knowledge/general-groups";