@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.
- package/package.json +26 -11
- package/public/favicon.ico +0 -0
- package/public/logo/allblue-logo-app-icon.svg +7 -0
- package/public/logo/allblue-logo-dark-square.svg +4 -0
- package/public/logo/allblue-logo-dark.svg +4 -0
- package/public/logo/allblue-logo-light-square.svg +4 -0
- package/public/logo/allblue-logo-light.svg +4 -0
- package/src/__tests__/bench/baselines/format-timestamp.json +44 -0
- package/src/__tests__/bench/core-hot-path.bench.ts +71 -0
- package/src/__tests__/integration/db-crud.integration.test.ts +79 -0
- package/src/__tests__/rejects-tothrow-regression.test.ts +31 -0
- package/src/__tests__/security/ai/prompt-injection.test.ts +105 -0
- package/src/__tests__/security/ssrf-url-whitelist.test.ts +54 -0
- package/src/__tests__/security/xss-sanitize.test.ts +99 -0
- package/src/__tests__/setup.ts +9 -0
- package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-dark-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-light-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/__screenshots__/user-avatar.vrt.test.tsx/user-avatar-initial-fallback-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/theme-surface.vrt.test.tsx +54 -0
- package/src/__tests__/visual/user-avatar.vrt.test.tsx +32 -0
- package/src/components/error/global-error-boundary.tsx +2 -2
- package/src/components/knowledge/general-groups.ts +15 -0
- package/src/components/knowledge/kb-filter-popover.tsx +143 -0
- package/src/components/knowledge-public/legal-compliance-editor.tsx +4 -2
- package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +9 -6
- package/src/components/layout/sidebar-03/nav-knowledge.tsx +19 -17
- package/src/components/layout/sidebar-03/nav-secondary.tsx +57 -47
- package/src/components/layout/sidebar-03/nav-workspace.tsx +20 -22
- package/src/components/mdx.tsx +9 -0
- package/src/components/shared-kit/avatars/notion-avatar.tsx +2 -1
- package/src/components/shared-kit/icons/__tests__/lucide-curated-icons.test.ts +2 -2
- package/src/components/shared-kit/icons/__tests__/lucide-icon-picker.test.tsx +3 -2
- package/src/components/shared-kit/icons/lucide-icon-picker/curated-icons.ts +5 -10
- package/src/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name.tsx +3 -3
- package/src/components/shared-kit/theme/theme-surface.tsx +52 -0
- package/src/components/story/relations/general-groups.ts +1 -15
- package/src/components/story/relations/knowledge-select-dialog.tsx +13 -87
- package/src/components/story/story-list-view.tsx +7 -1
- package/src/components/tiptap/node/table-node/ui/table-move-row-column-button/use-table-move-row-column.ts +0 -2
- package/src/components/workspace/__tests__/activity/workspace-activity-card.test.tsx +36 -0
- package/src/components/workspace/__tests__/contribution/workspace-contribution-card.test.tsx +17 -0
- package/src/components/workspace/__tests__/dialogs/create-workspace-dialog.test.tsx +63 -0
- package/src/components/workspace/__tests__/dialogs/delete-workspace-options.test.tsx +89 -0
- package/src/components/workspace/__tests__/dialogs/edit-workspace-dialog.test.tsx +68 -0
- package/src/components/workspace/__tests__/header/workspace-header.test.tsx +40 -0
- package/src/components/workspace/__tests__/kb/workspace-kb-card.test.tsx +154 -0
- package/src/components/workspace/__tests__/members/workspace-member-item.test.tsx +24 -0
- package/src/components/workspace/__tests__/members/workspace-members-manager.test.tsx +51 -0
- package/src/components/workspace/__tests__/projects/workspace-project-list.test.tsx +46 -0
- package/src/components/workspace/{create-workspace-dialog.tsx → dialogs/create-workspace-dialog.tsx} +1 -1
- package/src/components/workspace/{edit-workspace-dialog.tsx → dialogs/edit-workspace-dialog.tsx} +1 -1
- package/src/components/workspace/kb/workspace-kb-card.tsx +594 -0
- package/src/components/workspace/kb/workspace-kb-manage-dialog.tsx +174 -0
- package/src/components/workspace/kb/workspace-quick-doc-dialog.tsx +431 -0
- package/src/components/workspace/{workspace-members-manager.tsx → members/workspace-members-manager.tsx} +78 -40
- package/src/lib/__tests__/msw-sample.test.ts +68 -0
- package/src/lib/__tests__/pgvector.test.ts +2 -2
- package/src/lib/changelog/sdk-versions.ts +21 -21
- package/src/lib/collections/knowledge/knowledge-bases-collection.ts +3 -1
- package/src/lib/collections/reference/workspaces-collection.ts +3 -1
- package/src/lib/fetch-json.ts +23 -0
- package/src/lib/fumadocs-i18n.ts +7 -0
- package/src/{components/workspace/workspace-kb-card.tsx → lib/i18n/resolver.ts} +6 -12
- package/src/lib/layout.shared.tsx +40 -0
- package/src/lib/sanitize.ts +14 -1
- package/src/lib/session.ts +9 -1
- package/src/lib/source.ts +15 -0
- package/src/lib/vector-store/__tests__/factory.test.ts +1 -1
- package/src/lib/vector-store/__tests__/sqlite-vec-store.test.ts +2 -2
- package/src/plugins/core/tags/__tests__/tags.test.ts +10 -10
- package/src/routeTree.gen.ts +65 -0
- package/src/routes/$lang/docs/$/route.lazy.tsx +62 -0
- package/src/routes/$lang/docs/$/route.tsx +52 -0
- package/src/routes/-tests/docs-redirect.test.ts +45 -0
- package/src/routes/__root.tsx +13 -7
- package/src/routes/__tests__/-edition-routing.test.tsx +4 -4
- package/src/routes/__tests__/route-tree-structure.test.ts +65 -0
- package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +5 -2
- package/src/routes/_nav/knowledge/index.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge/laws/index.lazy.tsx +5 -2
- package/src/routes/_nav/route.tsx +21 -0
- package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +13 -11
- package/src/routes/_nav/workspaces/$workspaceId/route.tsx +4 -2
- package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +1 -1
- package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
- package/src/routes/api/search.ts +13 -0
- package/src/routes/docs.tsx +9 -0
- package/src/server/__tests__/agent-crud.test.ts +4 -4
- package/src/server/__tests__/chat-conversations.test.ts +1 -3
- package/src/server/__tests__/fetch-guard.test.ts +99 -0
- package/src/server/__tests__/permissions.test.ts +1 -3
- package/src/server/__tests__/resources.team-seats.test.ts +2 -0
- package/src/server/__tests__/skill-crud.test.ts +3 -3
- package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +95 -0
- package/src/server/ai-gateway/__tests__/kb-context-guard.test.ts +136 -0
- package/src/server/ai-gateway/__tests__/knowledge-search-fence.test.ts +240 -0
- package/src/server/ai-gateway/__tests__/llm-fence.test.ts +125 -0
- package/src/server/ai-gateway/__tests__/run-store.test.ts +52 -0
- package/src/server/ai-gateway/ai-output-guard.ts +77 -0
- package/src/server/ai-gateway/index.ts +5 -0
- package/src/server/ai-gateway/kb-context-guard.ts +46 -0
- package/src/server/ai-gateway/llm-fence.ts +82 -0
- package/src/server/ai-gateway/persistence/run-store.ts +19 -0
- package/src/server/ai-gateway/story-prompt.ts +4 -2
- package/src/server/ai-gateway/tools/definitions.ts +12 -6
- package/src/server/ai-gateway/tools/server.ts +97 -54
- package/src/server/builtin-project.ts +21 -0
- package/src/server/email/sender.ts +8 -2
- package/src/server/fetch-guard.ts +170 -0
- package/src/server/middlewares/security-headers.ts +113 -0
- package/src/server/serverFns/__tests__/billing.test.ts +263 -0
- package/src/server/serverFns/__tests__/link-preview.test.ts +239 -0
- package/src/server/serverFns/__tests__/project-kb.test.ts +3 -3
- package/src/server/serverFns/__tests__/project-team-assign.test.ts +1 -1
- package/src/server/serverFns/__tests__/story-comments.test.ts +265 -0
- package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +266 -0
- package/src/server/serverFns/__tests__/story-crud-labels-users.test.ts +227 -0
- package/src/server/serverFns/__tests__/story-crud.test.ts +526 -0
- package/src/server/serverFns/__tests__/story-links.test.ts +262 -0
- package/src/server/serverFns/__tests__/story-list-query.test.ts +259 -0
- package/src/server/serverFns/__tests__/story-reactions.test.ts +230 -0
- package/src/server/serverFns/__tests__/story-tree.test.ts +317 -0
- package/src/server/serverFns/__tests__/story-types.test.ts +294 -0
- package/src/server/serverFns/__tests__/team-accept.test.ts +6 -6
- package/src/server/serverFns/__tests__/team-invitation.test.ts +3 -3
- package/src/server/serverFns/__tests__/team-join-request.test.ts +8 -8
- package/src/server/serverFns/__tests__/update-seats.test.ts +4 -4
- package/src/server/serverFns/__tests__/workspace-kb-idor.test.ts +160 -0
- package/src/server/serverFns/__tests__/workspace-kb-utils.test.ts +54 -0
- package/src/server/serverFns/__tests__/workspace-kb.test.ts +348 -0
- package/src/server/serverFns/__tests__/workspace.test.ts +28 -8
- package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +3 -5
- package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +1 -1
- package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +2 -2
- package/src/server/serverFns/kb-doc-search.ts +111 -0
- package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
- package/src/server/serverFns/story/__tests__/bulk.test.ts +1 -1
- package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +2 -6
- package/src/server/serverFns/story/__tests__/story-assoc.test.ts +2 -4
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +2 -2
- package/src/server/serverFns/story/__tests__/story-groups.test.ts +2 -2
- package/src/server/serverFns/story/kb-general-consts.ts +39 -0
- package/src/server/serverFns/story/kb-general.ts +30 -17
- package/src/server/serverFns/story/story-assoc.ts +13 -103
- package/src/server/serverFns/story/story-crud.ts +2 -5
- package/src/server/serverFns/story/story-groups.ts +4 -13
- package/src/server/serverFns/team.ts +1 -1
- package/src/server/serverFns/workspace-kb-utils.ts +138 -0
- package/src/server/serverFns/workspace-kb.ts +403 -0
- package/src/server/serverFns/workspace.ts +4 -1
- package/src/shared/ai-cli/__tests__/adapter.test.ts +67 -0
- package/src/shared/ai-cli/__tests__/cli-detector.test.ts +99 -0
- package/src/shared/ai-cli/adapter.ts +2 -2
- package/src/shared/ai-cli/cli-detector.ts +18 -9
- package/src/start.ts +2 -1
- package/src/styles/globals.css +116 -0
- package/src/types/jest-dom-vitest.d.ts +8 -7
- package/vite.shared.ts +18 -1
- package/public/file.svg +0 -1
- package/public/globe.svg +0 -1
- package/public/window.svg +0 -1
- package/src/lib/__tests__/cli-detector.test.ts +0 -80
- /package/src/components/workspace/{workspace-activity-card.tsx → activity/workspace-activity-card.tsx} +0 -0
- /package/src/components/workspace/{workspace-contribution-card.tsx → contribution/workspace-contribution-card.tsx} +0 -0
- /package/src/components/workspace/{delete-workspace-options.tsx → dialogs/delete-workspace-options.tsx} +0 -0
- /package/src/components/workspace/{workspace-header.tsx → header/workspace-header.tsx} +0 -0
- /package/src/components/workspace/{workspace-member-item.tsx → members/workspace-member-item.tsx} +0 -0
- /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 {
|
|
74
|
-
import {
|
|
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
|
-
(
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
(
|
|
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
|
-
(
|
|
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 &&
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
</
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 {
|
|
34
|
-
import {
|
|
35
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
|
|
174
|
+
<WorkspaceLinks pathname={pathname} />
|
|
177
175
|
<CreateWorkspaceDialog open={createOpen} onOpenChange={setCreateOpen} />
|
|
178
176
|
</SidebarGroup>
|
|
179
177
|
);
|
|
@@ -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("数量在目标区间
|
|
39
|
-
expect(CURATED_ICONS.length).toBeGreaterThanOrEqual(
|
|
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
|
|
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?.
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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 {
|
|
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
|
-
/** 默认:
|
|
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
|
-
<
|
|
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
|
-
|
|
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";
|