@allbluecn/web-app 0.5.0 → 0.6.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 (170) hide show
  1. package/package.json +239 -30
  2. package/src/components/iteration/auto-schedule-dialog.tsx +150 -0
  3. package/src/components/iteration/burndown-chart.tsx +63 -0
  4. package/src/components/iteration/constants.ts +28 -0
  5. package/src/components/iteration/create-iteration-dialog.tsx +140 -0
  6. package/src/components/iteration/iteration-card.tsx +60 -0
  7. package/src/components/iteration/iteration-editor.tsx +116 -0
  8. package/src/components/iteration/iteration-progress-bar.tsx +44 -0
  9. package/src/components/iteration/iteration-status-badge.tsx +33 -0
  10. package/src/components/knowledge/dialog/knowledge-form-dialog.tsx +21 -30
  11. package/src/components/knowledge/knowledge-toolbar.tsx +124 -1
  12. package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +2 -2
  13. package/src/components/layout/sidebar-03/nav-workspace.tsx +2 -2
  14. package/src/components/project/create-project-dialog.tsx +44 -57
  15. package/src/components/shared/__tests__/lucide-curated-icons.test.ts +83 -0
  16. package/src/components/shared/__tests__/lucide-icon-data.test.ts +131 -0
  17. package/src/components/shared/__tests__/lucide-icon-picker.test.tsx +197 -0
  18. package/src/components/shared/lucide-icon-picker/curated-icons.ts +478 -0
  19. package/src/components/shared/lucide-icon-picker/icon-data.ts +109 -0
  20. package/src/components/shared/lucide-icon-picker/index.tsx +82 -0
  21. package/src/components/shared/lucide-icon-picker/lucide-icon-by-name.tsx +78 -0
  22. package/src/components/shared/lucide-icon-picker/panel.tsx +163 -0
  23. package/src/components/story/__tests__/constants.test.ts +255 -0
  24. package/src/components/story/ai/story-ai-panel.tsx +23 -15
  25. package/src/components/story/ai/story-ai-preview-list.tsx +19 -0
  26. package/src/components/story/constants.ts +43 -1
  27. package/src/components/story/create-story-dialog.tsx +42 -16
  28. package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +114 -0
  29. package/src/components/story/detail/__tests__/categories.test.tsx +62 -0
  30. package/src/components/story/detail/__tests__/dependencies-button.test.tsx +49 -0
  31. package/src/components/story/detail/activity-timeline.tsx +26 -4
  32. package/src/components/story/detail/story-detail-body.tsx +90 -38
  33. package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +857 -0
  34. package/src/components/story/detail/story-detail-toolbar/categories.tsx +23 -18
  35. package/src/components/story/detail/story-detail-toolbar/children.tsx +9 -4
  36. package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +95 -27
  37. package/src/components/story/detail/story-detail-toolbar.tsx +159 -8
  38. package/src/components/story/detail/story-properties.tsx +21 -4
  39. package/src/components/story/display/__tests__/group-fields.test.ts +107 -0
  40. package/src/components/story/display/group-resolver.tsx +2 -2
  41. package/src/components/story/inline-editors/__tests__/dependency-type-select.test.tsx +155 -0
  42. package/src/components/story/inline-editors/__tests__/labels-editor.test.tsx +155 -0
  43. package/src/components/story/inline-editors/__tests__/priority-editor.test.tsx +173 -0
  44. package/src/components/story/inline-editors/__tests__/state-editor.test.tsx +174 -0
  45. package/src/components/story/inline-editors/dependency-type-select.tsx +180 -0
  46. package/src/components/story/inline-editors/index.ts +2 -1
  47. package/src/components/story/inline-editors/labels-editor.tsx +159 -3
  48. package/src/components/story/inline-editors/story-select-dialog.tsx +41 -63
  49. package/src/components/story/peek-panel.tsx +121 -21
  50. package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +171 -0
  51. package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +174 -0
  52. package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +93 -0
  53. package/src/components/story/relations/attachment-manage-dialog.tsx +490 -0
  54. package/src/components/story/relations/file-preview.tsx +173 -0
  55. package/src/components/story/relations/format.ts +120 -0
  56. package/src/components/story/relations/general-groups.ts +32 -0
  57. package/src/components/story/relations/knowledge-select-dialog.tsx +403 -0
  58. package/src/components/story/relations/link-manage-dialog.tsx +192 -0
  59. package/src/components/story/relations/story-ref-row.tsx +17 -27
  60. package/src/components/story/story-panel-header.tsx +25 -19
  61. package/src/components/story/story-toolbar.tsx +3 -3
  62. package/src/components/story/transfer-story-popover.tsx +2 -2
  63. package/src/components/story/types.ts +14 -0
  64. package/src/components/story/views/create-view-dialog.tsx +3 -3
  65. package/src/components/story/views/story-list/__tests__/group-header.test.tsx +219 -0
  66. package/src/components/story/views/story-list/group.tsx +1 -1
  67. package/src/components/story/views/story-list/inline-story-creator.tsx +2 -2
  68. package/src/components/story/views/story-list/row.tsx +1 -1
  69. package/src/components/workspace/create-workspace-dialog.tsx +41 -49
  70. package/src/lib/__tests__/project-display-id.test.ts +8 -5
  71. package/src/lib/changelog/sdk-versions.ts +20 -20
  72. package/src/lib/iteration/__tests__/status.test.ts +41 -0
  73. package/src/lib/iteration/status.ts +28 -0
  74. package/src/lib/project-display-id.ts +14 -5
  75. package/src/lib/project-module-meta.ts +77 -0
  76. package/src/lib/story/__tests__/video-embed.test.ts +77 -0
  77. package/src/lib/story/video-embed.ts +59 -0
  78. package/src/lib/utils.d.ts +1 -1
  79. package/src/plugins/.gen-manifest.json +13 -0
  80. package/src/plugins/modules.gen.ts +20 -0
  81. package/src/plugins/plugins.gen.ts +20 -0
  82. package/src/plugins/ui.gen.ts +8 -0
  83. package/src/providers/index.tsx +6 -0
  84. package/src/routeTree.gen.ts +96 -0
  85. package/src/routes/__root.tsx +0 -8
  86. package/src/routes/_nav/inspiration/route.tsx +8 -0
  87. package/src/routes/_nav/knowledge/index.lazy.tsx +2 -2
  88. package/src/routes/_nav/prd/$id/route.tsx +9 -0
  89. package/src/routes/_nav/prd/index.tsx +9 -0
  90. package/src/routes/_nav/prd/route.tsx +8 -0
  91. package/src/routes/_nav/projects/$projectId/$storyId/route.lazy.tsx +52 -25
  92. package/src/routes/_nav/projects/$projectId/$storyId/route.tsx +6 -3
  93. package/src/routes/_nav/projects/$projectId/index.lazy.tsx +32 -15
  94. package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy.tsx +289 -0
  95. package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.tsx +27 -0
  96. package/src/routes/_nav/projects/$projectId/iterations/index.lazy.tsx +111 -0
  97. package/src/routes/_nav/projects/$projectId/iterations/index.tsx +24 -0
  98. package/src/routes/_nav/projects/$projectId/route.tsx +4 -2
  99. package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +78 -7
  100. package/src/routes/_nav/projects/$projectId/settings/route.tsx +6 -2
  101. package/src/routes/_nav/stories/$storyId/route.lazy.tsx +5 -1
  102. package/src/routes/_nav/stories/$storyId/route.tsx +4 -0
  103. package/src/routes/_nav/tags/$id/route.tsx +8 -0
  104. package/src/routes/_nav/tags/index.tsx +8 -0
  105. package/src/routes/_nav/tags/route.tsx +8 -0
  106. package/src/routes/_nav/tags/settings/route.tsx +9 -0
  107. package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +3 -3
  108. package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +4 -4
  109. package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
  110. package/src/routes/api/attachments/$id/content.ts +60 -0
  111. package/src/routes/api/attachments/upload.ts +148 -0
  112. package/src/routes/share/prd/$prdId/route.tsx +10 -0
  113. package/src/server/serverFns/__tests__/project-kb.test.ts +147 -0
  114. package/src/server/serverFns/iteration/__tests__/auto-schedule-core.test.ts +62 -0
  115. package/src/server/serverFns/iteration/__tests__/burndown-core.test.ts +90 -0
  116. package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +211 -0
  117. package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +155 -0
  118. package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +136 -0
  119. package/src/server/serverFns/iteration/auto-schedule-core.ts +54 -0
  120. package/src/server/serverFns/iteration/auto-schedule-internal.ts +88 -0
  121. package/src/server/serverFns/iteration/burndown-core.ts +60 -0
  122. package/src/server/serverFns/iteration/index.ts +22 -0
  123. package/src/server/serverFns/iteration/iteration-auto-schedule.ts +66 -0
  124. package/src/server/serverFns/iteration/iteration-burndown.ts +70 -0
  125. package/src/server/serverFns/iteration/iteration-crud.ts +208 -0
  126. package/src/server/serverFns/iteration/iteration-stories.ts +111 -0
  127. package/src/server/serverFns/iteration/iteration-transfer.ts +42 -0
  128. package/src/server/serverFns/iteration/schemas.ts +58 -0
  129. package/src/server/serverFns/iteration/transfer-internal.ts +62 -0
  130. package/src/server/serverFns/knowledge.ts +50 -4
  131. package/src/server/serverFns/project/module-overview.ts +109 -0
  132. package/src/server/serverFns/project.ts +45 -2
  133. package/src/server/serverFns/story/__tests__/kb-scope.test.ts +56 -0
  134. package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +100 -0
  135. package/src/server/serverFns/story/__tests__/labels.test.ts +67 -0
  136. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +222 -0
  137. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +132 -2
  138. package/src/server/serverFns/story/index.ts +9 -4
  139. package/src/server/serverFns/story/kb-general.ts +39 -0
  140. package/src/server/serverFns/story/kb-scope.ts +52 -0
  141. package/src/server/serverFns/story/link-preview.ts +115 -0
  142. package/src/server/serverFns/story/story-assoc.ts +372 -51
  143. package/src/server/serverFns/story/story-attachments.ts +32 -45
  144. package/src/{components/project/project-icon.tsx → server/serverFns/story/story-crud/label-palette.ts} +16 -19
  145. package/src/server/serverFns/story/story-crud/label-suggest-core.ts +88 -0
  146. package/src/server/serverFns/story/story-crud/label-suggest.ts +38 -0
  147. package/src/{components/knowledge/knowledge-icon.tsx → server/serverFns/story/story-crud/label-upsert.ts} +13 -19
  148. package/src/server/serverFns/story/story-crud/labels.ts +3 -5
  149. package/src/server/serverFns/story/story-crud.ts +53 -6
  150. package/src/server/serverFns/story/story-dependencies.ts +158 -5
  151. package/src/server/serverFns/story/story-links.ts +43 -7
  152. package/src/server/serverFns/story/story-transfer.ts +5 -4
  153. package/src/server/serverFns/story/story-tree.ts +28 -1
  154. package/src/server/serverFns/story/types.ts +38 -0
  155. package/src/styles/globals.css +25 -0
  156. package/src/types/project.ts +5 -5
  157. package/vite.shared.ts +2 -0
  158. package/src/components/project/project-animated-icon.tsx +0 -73
  159. package/src/components/shared/icon-packs/index.ts +0 -1
  160. package/src/components/shared/icon-packs/knowledge.ts +0 -1
  161. package/src/components/shared/icon-packs/prd.ts +0 -1
  162. package/src/components/shared/icon-packs/project.ts +0 -1
  163. package/src/components/shared/icon-picker.tsx +0 -1
  164. package/src/components/story/detail/story-detail-toolbar/attachments.tsx +0 -73
  165. package/src/components/story/detail/story-detail-toolbar/knowledge.tsx +0 -73
  166. package/src/components/story/detail/story-detail-toolbar/links.tsx +0 -73
  167. package/src/components/story/relations/attachments-panel.tsx +0 -179
  168. package/src/components/story/relations/knowledge-panel.tsx +0 -284
  169. package/src/components/story/relations/links-panel.tsx +0 -240
  170. package/src/lib/project-mock.ts +0 -156
@@ -0,0 +1,192 @@
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
+ import * as React from "react";
19
+ import { useTranslation } from "react-i18next";
20
+ import { useServerFn } from "@tanstack/react-start";
21
+ import { toast } from "sonner";
22
+ import { Button } from "@/components/ui/button";
23
+ import { Input } from "@/components/ui/input";
24
+ import { Textarea } from "@/components/ui/textarea";
25
+ import {
26
+ Dialog,
27
+ DialogContent,
28
+ DialogHeader,
29
+ DialogTitle,
30
+ } from "@/components/ui/dialog";
31
+ import { addLinkFn, updateLinkFn } from "@/server/serverFns/story";
32
+ import type { StoryLinkItem } from "../types";
33
+
34
+ export interface LinkManageDialogProps {
35
+ open: boolean;
36
+ onOpenChange: (open: boolean) => void;
37
+ storyId: string;
38
+ data: StoryLinkItem[];
39
+ onUpdate: (links: StoryLinkItem[]) => void;
40
+ /** 添加链接产生的活动,供时间线即时追加 */
41
+ onActivity?: (activity: import("../types").ActivityItem) => void;
42
+ /** 非空时进入编辑态(由合并面板「编辑」触发) */
43
+ initialEdit?: StoryLinkItem | null;
44
+ }
45
+
46
+ export function LinkManageDialog({
47
+ open,
48
+ onOpenChange,
49
+ storyId,
50
+ data,
51
+ onUpdate,
52
+ onActivity,
53
+ initialEdit = null,
54
+ }: LinkManageDialogProps) {
55
+ const { t } = useTranslation("story");
56
+ const addLink = useServerFn(addLinkFn);
57
+ const updateLink = useServerFn(updateLinkFn);
58
+ const [editing, setEditing] = React.useState<StoryLinkItem | null>(null);
59
+ const [form, setForm] = React.useState({ url: "", title: "", description: "" });
60
+ const [submitting, setSubmitting] = React.useState(false);
61
+
62
+ React.useEffect(() => {
63
+ if (open) {
64
+ setEditing(initialEdit);
65
+ setForm(
66
+ initialEdit
67
+ ? { url: initialEdit.url, title: initialEdit.title ?? "", description: initialEdit.description ?? "" }
68
+ : { url: "", title: "", description: "" },
69
+ );
70
+ }
71
+ }, [open, initialEdit]);
72
+
73
+ const handleSubmit = async (e: React.FormEvent) => {
74
+ e.preventDefault();
75
+ if (submitting) return;
76
+ setSubmitting(true);
77
+ try {
78
+ if (editing) {
79
+ const updated = await updateLink({
80
+ data: {
81
+ linkId: editing.id,
82
+ url: form.url,
83
+ title: form.title || undefined,
84
+ description: form.description || undefined,
85
+ },
86
+ });
87
+ toast.success(t("linkDialog.updated"));
88
+ if (updated.activity) onActivity?.(updated.activity);
89
+ onUpdate(
90
+ data.map((l) =>
91
+ l.id === editing.id
92
+ ? { ...l, url: form.url, title: form.title || null, description: form.description || null }
93
+ : l,
94
+ ),
95
+ );
96
+ } else {
97
+ const created = await addLink({
98
+ data: {
99
+ storyId,
100
+ url: form.url,
101
+ title: form.title || undefined,
102
+ description: form.description || undefined,
103
+ },
104
+ });
105
+ toast.success(t("linkDialog.added"));
106
+ if (created.activity) onActivity?.(created.activity);
107
+ onUpdate([
108
+ ...data,
109
+ {
110
+ id: created.id,
111
+ url: created.url,
112
+ title: created.title,
113
+ description: created.description,
114
+ },
115
+ ]);
116
+ setForm({ url: "", title: "", description: "" });
117
+ }
118
+ setEditing(null);
119
+ onOpenChange(false);
120
+ } catch (err) {
121
+ toast.error((err as Error)?.message || t("linkDialog.saveFailed"));
122
+ } finally {
123
+ setSubmitting(false);
124
+ }
125
+ };
126
+
127
+ return (
128
+ <Dialog open={open} onOpenChange={onOpenChange}>
129
+ <DialogContent className="sm:max-w-130">
130
+ <DialogHeader>
131
+ <DialogTitle className="text-xl">
132
+ {t(editing ? "linkDialog.editTitle" : "linkDialog.title")}
133
+ </DialogTitle>
134
+ </DialogHeader>
135
+ <form onSubmit={handleSubmit} className="flex h-96 flex-col gap-4 overflow-y-auto px-1 pb-1">
136
+ <div className="space-y-1.5">
137
+ <label htmlFor="link-url" className="text-xs font-medium text-muted-foreground">
138
+ URL
139
+ </label>
140
+ <Input
141
+ id="link-url"
142
+ type="url"
143
+ required
144
+ placeholder={t("linkDialog.urlPlaceholder")}
145
+ value={form.url}
146
+ onChange={(e) => setForm({ ...form, url: e.target.value })}
147
+ className="h-10 text-base"
148
+ />
149
+ </div>
150
+ <div className="space-y-1.5">
151
+ <label htmlFor="link-title" className="text-xs font-medium text-muted-foreground">
152
+ {t("linkDialog.titleOptional")}
153
+ </label>
154
+ <Input
155
+ id="link-title"
156
+ placeholder={t("linkDialog.titlePlaceholder")}
157
+ value={form.title}
158
+ onChange={(e) => setForm({ ...form, title: e.target.value })}
159
+ className="h-10"
160
+ />
161
+ </div>
162
+ <div className="flex min-h-0 flex-1 flex-col gap-1.5">
163
+ <label htmlFor="link-desc" className="text-xs font-medium text-muted-foreground">
164
+ {t("linkDialog.descOptional")}
165
+ </label>
166
+ <Textarea
167
+ id="link-desc"
168
+ placeholder={t("linkDialog.descPlaceholder")}
169
+ value={form.description}
170
+ onChange={(e) => setForm({ ...form, description: e.target.value })}
171
+ className="min-h-20 flex-1 resize-none"
172
+ />
173
+ </div>
174
+ <div className="flex shrink-0 justify-end gap-2 pt-1">
175
+ {editing && (
176
+ <Button
177
+ type="button"
178
+ variant="outline"
179
+ onClick={() => onOpenChange(false)}
180
+ >
181
+ {t("linkDialog.cancel")}
182
+ </Button>
183
+ )}
184
+ <Button type="submit" disabled={submitting}>
185
+ {t(editing ? "linkDialog.save" : "linkDialog.add")}
186
+ </Button>
187
+ </div>
188
+ </form>
189
+ </DialogContent>
190
+ </Dialog>
191
+ );
192
+ }
@@ -36,11 +36,10 @@ import {
36
36
  unarchiveStoryFn,
37
37
  deleteStoryFn,
38
38
  } from "@/server/serverFns/story";
39
- import { PriorityEditor, StateEditor, AssigneeEditor, DateEditor, LabelsEditor } from "../inline-editors";
39
+ import { PriorityEditor, StateEditor, AssigneeEditor, DateEditor, LabelsEditor, DependencyTypeSelect } from "../inline-editors";
40
40
  import { NameConfirmDialog } from "@/components/shared/name-confirm-dialog";
41
41
  import { cn } from "@/lib/utils";
42
- import { ProjectIconDisplay } from "@/components/project/project-icon";
43
- import { Badge } from "@/components/ui/badge";
42
+ import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
44
43
  import { dependencyTypeOption } from "../constants";
45
44
  import type { DisplayProperty } from "../display/types";
46
45
  import type { StoryPatch } from "../display/group-fields";
@@ -50,6 +49,7 @@ import type {
50
49
  DependencyEdgeType,
51
50
  DependencyGroup,
52
51
  } from "../types";
52
+ import type { DependencyTypeOption } from "../constants";
53
53
 
54
54
  /** 行内编辑/归档/删除后回传给面板的补丁载荷(StoryPatch 字段 + 面板控制信号) */
55
55
  export type StoryRelationPatch = StoryPatch & {
@@ -72,8 +72,10 @@ export interface StoryRefRowProps {
72
72
  /** 移除关系:子需求=移除子需求,依赖=移除依赖 */
73
73
  onRemove: (storyId: string) => void;
74
74
  removeMenuLabel: string;
75
- /** 依赖行类型标签:传 depType + group 时行内渲染类型 Badge */
75
+ /** 依赖行类型标签:传 depType + group 时行内渲染可编辑的类型选择器 */
76
76
  dependencyMeta?: { depType: DependencyEdgeType; group: keyof DependencyGroup };
77
+ /** 依赖类型就地编辑回调(依赖列表场景;children 列表不传) */
78
+ onDependencyTypeChange?: (otherStoryId: string, key: DependencyTypeOption["key"]) => void;
77
79
  /** 当前需求编号(tooltip 中以编号替代「我」指代当前需求) */
78
80
  currentIdentifier?: string;
79
81
  }
@@ -92,6 +94,7 @@ export function StoryRefRow({
92
94
  onRemove,
93
95
  removeMenuLabel,
94
96
  dependencyMeta,
97
+ onDependencyTypeChange,
95
98
  currentIdentifier,
96
99
  }: StoryRefRowProps) {
97
100
  const { t } = useTranslation("story");
@@ -190,7 +193,7 @@ export function StoryRefRow({
190
193
  className="shrink-0"
191
194
  style={{ color: "oklch(80.517% 0.20237 152.675 / 0.909)" }}
192
195
  >
193
- <ProjectIconDisplay name={storyProjectInfo.icon} size={14} />
196
+ <LucideIconByName name={storyProjectInfo.icon} size={14} />
194
197
  </span>
195
198
  </TooltipTrigger>
196
199
  <TooltipContent side="top" sideOffset={6} className="flex flex-col items-start gap-0.5 text-xs">
@@ -199,7 +202,7 @@ export function StoryRefRow({
199
202
  </TooltipContent>
200
203
  </Tooltip>
201
204
  ) : storyProjectInfo ? (
202
- <ProjectIconDisplay
205
+ <LucideIconByName
203
206
  name={storyProjectInfo.icon}
204
207
  size={14}
205
208
  className="shrink-0 text-muted-foreground"
@@ -224,26 +227,13 @@ export function StoryRefRow({
224
227
  <span className="font-mono text-xs text-muted-foreground shrink-0">{storyState.identifier}</span>
225
228
  )}
226
229
  {dependencyMeta && (
227
- <Tooltip>
228
- <TooltipTrigger asChild>
229
- <Badge variant="outline" className="max-w-56 shrink-0 truncate text-[10px] font-normal text-muted-foreground">
230
- {t(dependencyTypeOption(dependencyMeta.group, dependencyMeta.depType).badgeLabelKey, {
231
- identifier: storyState.identifier,
232
- })}
233
- </Badge>
234
- </TooltipTrigger>
235
- <TooltipContent side="top" className="text-xs">
236
- {t(
237
- currentIdentifier
238
- ? dependencyTypeOption(dependencyMeta.group, dependencyMeta.depType).tooltipLabelKey
239
- : dependencyTypeOption(dependencyMeta.group, dependencyMeta.depType).badgeLabelKey,
240
- {
241
- identifier: storyState.identifier,
242
- me: currentIdentifier,
243
- },
244
- )}
245
- </TooltipContent>
246
- </Tooltip>
230
+ <DependencyTypeSelect
231
+ variant="badge"
232
+ value={dependencyTypeOption(dependencyMeta.group, dependencyMeta.depType).key}
233
+ onChange={(key) => onDependencyTypeChange?.(storyState.id, key)}
234
+ identifier={storyState.identifier}
235
+ currentIdentifier={currentIdentifier}
236
+ />
247
237
  )}
248
238
  <span className="min-w-0 flex-1 truncate font-medium">{storyState.name}</span>
249
239
  </button>
@@ -263,7 +253,7 @@ export function StoryRefRow({
263
253
  <DateEditor value={storyState.targetDate} onChange={(v) => updateField({ targetDate: v })} label={t("children.dueDate")} icon={CalendarCheck} overdue={overdue} />
264
254
  )}
265
255
  {properties.includes("labels") && (
266
- <LabelsEditor labelIds={currentLabelIds} labels={labels} onChange={(v) => updateField({ labelIds: v })} variant="button" />
256
+ <LabelsEditor labelIds={currentLabelIds} labels={labels} onChange={(v) => updateField({ labelIds: v })} variant="button" aiContext={{ name: () => storyState.name }} />
267
257
  )}
268
258
  <DropdownMenu>
269
259
  <DropdownMenuTrigger asChild>
@@ -20,9 +20,9 @@ import type { ReactNode } from "react";
20
20
  import { useTranslation } from "react-i18next";
21
21
  import { Link } from "@tanstack/react-router";
22
22
  import {
23
+ Archive,
23
24
  ArrowLeft,
24
25
  ArrowRight,
25
- Archive,
26
26
  ChevronDown,
27
27
  ChevronUp,
28
28
  Link2,
@@ -59,6 +59,14 @@ export const PEEK_MODE_OPTIONS: { value: PeekMode; label: string; icon: typeof P
59
59
  ];
60
60
 
61
61
  export type PanelCloseType = "collapse" | "close" | "back";
62
+ export type PeekRelationKind = "child" | "dependency";
63
+
64
+ export interface PeekBreadcrumbItem {
65
+ identifier?: string;
66
+ projectId?: string;
67
+ label: string;
68
+ onClick?: () => void;
69
+ }
62
70
 
63
71
  export interface StoryPanelHeaderProps {
64
72
  closeType?: PanelCloseType;
@@ -86,6 +94,7 @@ export interface StoryPanelHeaderProps {
86
94
  hideCopyLink?: boolean;
87
95
  hideMoreMenu?: boolean;
88
96
  hideTransfer?: boolean;
97
+ breadcrumbItems?: PeekBreadcrumbItem[];
89
98
  }
90
99
 
91
100
  export function StoryPanelHeader({
@@ -114,6 +123,7 @@ export function StoryPanelHeader({
114
123
  hideCopyLink = false,
115
124
  hideMoreMenu = false,
116
125
  hideTransfer = false,
126
+ breadcrumbItems,
117
127
  }: StoryPanelHeaderProps) {
118
128
  const { t } = useTranslation("story");
119
129
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@@ -121,28 +131,24 @@ export function StoryPanelHeader({
121
131
 
122
132
  const currentMode = PEEK_MODE_OPTIONS.find((o) => o.value === mode) ?? PEEK_MODE_OPTIONS[0];
123
133
  const CurrentModeIcon = currentMode.icon;
134
+ const closeTooltip = closeType === "back" ? t("panel.back") : closeType === "collapse" ? t("panel.collapse") : t("panel.close");
135
+ // 形变动画(MorphIcon)因主线程阻塞掉帧卡顿已移除,静态切换图标
136
+ const CloseIcon = closeType === "back" ? ArrowLeft : closeType === "collapse" ? ArrowRight : X;
124
137
 
125
- const closeButton =
126
- closeType === "back" ? (
127
- <Button variant="ghost" size="icon" className="size-8" aria-label={t("panel.back")} onClick={onClose}>
128
- <ArrowLeft className="size-4" />
129
- </Button>
130
- ) : (
131
- <Button
132
- variant="ghost"
133
- size="icon"
134
- className="size-8"
135
- onClick={onClose}
136
- aria-label={closeType === "collapse" ? t("panel.collapse") : t("panel.close")}
137
- >
138
- {closeType === "collapse" ? <ArrowRight className="size-4" /> : <X className="size-4" />}
139
- </Button>
140
- );
138
+ const closeButton = (
139
+ <Button
140
+ variant="ghost"
141
+ size="icon"
142
+ className="size-8 panel-close-btn"
143
+ onClick={onClose}
144
+ aria-label={closeTooltip}
145
+ >
146
+ <CloseIcon className="size-4" />
147
+ </Button>
148
+ );
141
149
 
142
150
  const closeButtonWithLink = backTo ? <Link to={backTo}>{closeButton}</Link> : closeButton;
143
151
 
144
- const closeTooltip = closeType === "back" ? t("panel.back") : closeType === "collapse" ? t("panel.collapse") : t("panel.close");
145
-
146
152
  const handleCopyLink = async () => {
147
153
  const url = storyId ? `${window.location.origin}/stories/${storyId}` : window.location.href;
148
154
  try {
@@ -35,7 +35,7 @@ import type { SavedView } from "./views/view-types";
35
35
  import { DisplayDropdown } from "./display/display-dropdown";
36
36
  import type { DisplayOptions } from "./display/types";
37
37
  import { StoryFilterToggle, StoryFilterRow, AppliedFilters, type FilterCondition, type FilterKey } from "./filters";
38
- import { ProjectIconDisplay } from "@/components/project/project-icon";
38
+ import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
39
39
  import { CreateProjectDialog } from "@/components/project/create-project-dialog";
40
40
  import type { UserBrief } from "./types";
41
41
 
@@ -121,7 +121,7 @@ export function StoryToolbar({ filters, filterConditions, onFilterChange, onFilt
121
121
  {filters.projectId === "__archived__" ? (
122
122
  <Archive className="size-4 text-muted-foreground" />
123
123
  ) : filters.projectId ? (
124
- <ProjectIconDisplay name={projects.find((p) => p.id === filters.projectId)?.icon ?? ""} size={16} className="text-muted-foreground" />
124
+ <LucideIconByName name={projects.find((p) => p.id === filters.projectId)?.icon ?? ""} size={16} className="text-muted-foreground" />
125
125
  ) : (
126
126
  <Trees className="size-4 text-muted-foreground" />
127
127
  )}
@@ -149,7 +149,7 @@ export function StoryToolbar({ filters, filterConditions, onFilterChange, onFilt
149
149
  className={`flex w-full items-center justify-between gap-2 rounded px-2 py-1.5 text-sm ${filters.projectId === p.id ? "bg-accent font-medium" : "hover:bg-muted"}`}
150
150
  >
151
151
  <span className="flex min-w-0 items-center gap-2">
152
- <ProjectIconDisplay name={p.icon} size={16} className="text-muted-foreground" />
152
+ <LucideIconByName name={p.icon} size={16} className="text-muted-foreground" />
153
153
  <span className="truncate">{p.name}</span>
154
154
  </span>
155
155
  {p.displayId && (
@@ -18,7 +18,7 @@
18
18
  import { useState } from "react";
19
19
  import { useTranslation } from "react-i18next";
20
20
  import { GitCompareArrows, GitPullRequest, Check } from "lucide-react";
21
- import { ProjectIconDisplay } from "@/components/project/project-icon";
21
+ import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
22
22
  import { Button } from "@/components/ui/button";
23
23
  import { Separator } from "@/components/ui/separator";
24
24
  import {
@@ -194,7 +194,7 @@ export function TransferStoryPopover({
194
194
  isSelected ? "bg-accent" : "hover:bg-muted"
195
195
  }`}
196
196
  >
197
- <ProjectIconDisplay name={p.icon} size={16} />
197
+ <LucideIconByName name={p.icon} size={16} />
198
198
  <span className="min-w-0 flex-1 truncate">{p.name}</span>
199
199
  {p.displayId && (
200
200
  <span className="shrink-0 rounded-sm bg-muted px-1.5 py-0.5 text-[10px] font-normal text-muted-foreground">
@@ -51,6 +51,7 @@ export interface StoryListItem {
51
51
  labels: { id: string; name: string; color: string }[];
52
52
  createdAt: string;
53
53
  updatedAt: string;
54
+ iteration?: { id: string; name: string } | null;
54
55
  }
55
56
 
56
57
  export interface StoryDetail extends StoryListItem {
@@ -145,7 +146,9 @@ export interface StoryAttachmentItem {
145
146
  fileName: string;
146
147
  fileSize: number;
147
148
  mimeType: string;
149
+ description?: string | null;
148
150
  storageUrl: string;
151
+ fileHash?: string | null;
149
152
  }
150
153
 
151
154
  // PRD 简要信息
@@ -158,6 +161,11 @@ export interface PrdBrief {
158
161
  export interface DocBrief {
159
162
  id: string;
160
163
  title: string;
164
+ kbId: string;
165
+ kbName: string;
166
+ kbIcon: string;
167
+ docIcon: string | null;
168
+ parentId: string | null;
161
169
  }
162
170
 
163
171
  // 关联 Tab 数据汇总
@@ -176,6 +184,12 @@ export interface RelationsData {
176
184
  };
177
185
  }
178
186
 
187
+ /** 关联附件面板统一数据类型 */
188
+ export type AssociatedItem =
189
+ | { kind: "link"; data: StoryLinkItem }
190
+ | { kind: "attachment"; data: StoryAttachmentItem }
191
+ | { kind: "doc"; data: DocBrief };
192
+
179
193
  // ====== 依赖关系(对齐 Plane,新模型) ======
180
194
 
181
195
  // 规范化存储类型:语义统一为「source 领先于 target」
@@ -31,7 +31,7 @@ import {
31
31
  PopoverTrigger,
32
32
  } from "@/components/ui/popover";
33
33
  import { Button } from "@/components/ui/button";
34
- import { ProjectIconDisplay } from "@/components/project/project-icon";
34
+ import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
35
35
  import { cn } from "@/lib/utils";
36
36
  import { StoryFilterRow } from "../filters/filter-row";
37
37
  import { createDefaultCondition, type FilterCondition } from "../filters/filter-types";
@@ -160,7 +160,7 @@ export function CreateViewDialog({ open, onOpenChange, mode, initialValues, view
160
160
  className="h-8 gap-1.5 border-0 bg-muted/50 px-2.5 text-sm font-medium"
161
161
  >
162
162
  {selectedProject ? (
163
- <ProjectIconDisplay name={selectedProject.icon} size={16} />
163
+ <LucideIconByName name={selectedProject.icon} size={16} />
164
164
  ) : (
165
165
  <Trees className="size-4 text-muted-foreground" />
166
166
  )}
@@ -190,7 +190,7 @@ export function CreateViewDialog({ open, onOpenChange, mode, initialValues, view
190
190
  className={`flex w-full items-center justify-between gap-2 rounded px-2 py-1.5 text-sm ${projectId === p.id ? "bg-accent font-medium" : "hover:bg-muted"}`}
191
191
  >
192
192
  <span className="flex min-w-0 items-center gap-2">
193
- <ProjectIconDisplay name={p.icon} size={16} />
193
+ <LucideIconByName name={p.icon} size={16} />
194
194
  <span className="truncate">{p.name}</span>
195
195
  </span>
196
196
  {p.displayId && (