@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,140 @@
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 { useState } from "react";
19
+ import { useTranslation } from "react-i18next";
20
+ import { Button } from "@/components/ui/button";
21
+ import { Input } from "@/components/ui/input";
22
+ import { Textarea } from "@/components/ui/textarea";
23
+ import {
24
+ Dialog,
25
+ DialogContent,
26
+ DialogFooter,
27
+ DialogHeader,
28
+ DialogTitle,
29
+ } from "@/components/ui/dialog";
30
+ import { createIterationsFn, updateIterationsFn } from "@/server/serverFns/iteration";
31
+ import { toast } from "@/components/ui/sonner";
32
+ import type { IterationSummary } from "@/server/serverFns/iteration/schemas";
33
+
34
+ interface CreateIterationDialogProps {
35
+ open: boolean;
36
+ onOpenChange: (open: boolean) => void;
37
+ projectId: string;
38
+ editing?: IterationSummary | null;
39
+ onSaved?: () => void;
40
+ }
41
+
42
+ export function CreateIterationDialog({ open, onOpenChange, projectId, editing, onSaved }: CreateIterationDialogProps) {
43
+ const { t } = useTranslation("iterations");
44
+ const [name, setName] = useState(editing?.name ?? "");
45
+ const [startDate, setStartDate] = useState(editing?.startDate ?? "");
46
+ const [endDate, setEndDate] = useState(editing?.endDate ?? "");
47
+ const [description, setDescription] = useState(editing?.description ?? "");
48
+ const [loading, setLoading] = useState(false);
49
+ const [error, setError] = useState<string | null>(null);
50
+
51
+ async function handleSubmit() {
52
+ setError(null);
53
+ if (!name.trim()) {
54
+ toast.error(t("errors.nameRequired"));
55
+ return;
56
+ }
57
+ if (startDate && endDate && startDate > endDate) {
58
+ toast.error(t("errors.invalidRange"));
59
+ return;
60
+ }
61
+ setLoading(true);
62
+ try {
63
+ if (editing) {
64
+ await updateIterationsFn({
65
+ data: {
66
+ id: editing.id,
67
+ name: name.trim(),
68
+ description: description.trim() || undefined,
69
+ ...(startDate ? { startDate } : {}),
70
+ ...(endDate ? { endDate } : {}),
71
+ },
72
+ });
73
+ toast.success(t("actions.edit"));
74
+ } else {
75
+ await createIterationsFn({
76
+ data: {
77
+ projectId,
78
+ name: name.trim(),
79
+ description: description.trim() || undefined,
80
+ startDate,
81
+ endDate,
82
+ },
83
+ });
84
+ toast.success(t("actions.create"));
85
+ }
86
+ onSaved?.();
87
+ onOpenChange(false);
88
+ } catch (e) {
89
+ setError(e instanceof Error ? e.message : String(e));
90
+ toast.error(e instanceof Error ? e.message : "操作失败");
91
+ } finally {
92
+ setLoading(false);
93
+ }
94
+ }
95
+
96
+ return (
97
+ <Dialog open={open} onOpenChange={onOpenChange}>
98
+ <DialogContent className="sm:max-w-md">
99
+ <DialogHeader>
100
+ <DialogTitle>{editing ? t("actions.edit") : t("actions.create")}</DialogTitle>
101
+ </DialogHeader>
102
+ <div className="space-y-4 py-4">
103
+ <div className="space-y-2">
104
+ <label className="text-sm font-medium leading-none">{t("fields.name")}</label>
105
+ <Input
106
+ placeholder={t("fields.namePlaceholder")}
107
+ value={name}
108
+ onChange={(e) => setName(e.target.value)}
109
+ onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); void handleSubmit(); } }}
110
+ />
111
+ </div>
112
+ <div className="grid grid-cols-2 gap-4">
113
+ <div className="space-y-2">
114
+ <label className="text-sm font-medium leading-none">{t("fields.startDate")}</label>
115
+ <Input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} />
116
+ </div>
117
+ <div className="space-y-2">
118
+ <label className="text-sm font-medium leading-none">{t("fields.endDate")}</label>
119
+ <Input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} />
120
+ </div>
121
+ </div>
122
+ <div className="space-y-2">
123
+ <label className="text-sm font-medium leading-none">{t("fields.description")}</label>
124
+ <Textarea
125
+ placeholder={t("fields.description")}
126
+ value={description}
127
+ onChange={(e) => setDescription(e.target.value)}
128
+ className="min-h-[80px]"
129
+ />
130
+ </div>
131
+ {error && <p className="text-xs text-destructive">{error}</p>}
132
+ </div>
133
+ <DialogFooter>
134
+ <Button variant="outline" onClick={() => onOpenChange(false)}>{t("actions.cancel")}</Button>
135
+ <Button onClick={() => void handleSubmit()} disabled={loading}>{loading ? t("actions.saving") : t("actions.save")}</Button>
136
+ </DialogFooter>
137
+ </DialogContent>
138
+ </Dialog>
139
+ );
140
+ }
@@ -0,0 +1,60 @@
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 { Link } from "@tanstack/react-router";
19
+ import { useTranslation } from "react-i18next";
20
+ import { IterationStatusBadge } from "./iteration-status-badge";
21
+ import { IterationProgressBar } from "./iteration-progress-bar";
22
+ import type { IterationSummary } from "@/server/serverFns/iteration/schemas";
23
+
24
+ export function IterationCard({ item, projectId }: { item: IterationSummary; projectId: string }) {
25
+ const { t } = useTranslation("iterations");
26
+ const today = new Date().toISOString().slice(0, 10);
27
+ const remainingDays = Math.round((Date.parse(item.endDate) - Date.parse(today)) / 86400000);
28
+ const isOverdue = remainingDays < 0 && item.status === "active";
29
+
30
+ return (
31
+ <Link
32
+ to="/projects/$projectId/iterations/$iterationId"
33
+ params={{ projectId, iterationId: item.id }}
34
+ className="block rounded-lg border bg-card p-4 transition-colors hover:border-primary/50"
35
+ >
36
+ <div className="flex items-start justify-between gap-2">
37
+ <div className="min-w-0">
38
+ <div className="flex items-center gap-2">
39
+ <h3 className="truncate text-sm font-semibold">{item.name}</h3>
40
+ <IterationStatusBadge status={item.status} />
41
+ </div>
42
+ <p className="mt-1 text-xs text-muted-foreground">
43
+ {item.startDate} ~ {item.endDate}
44
+ {item.status === "active" && !isOverdue && (
45
+ <span className="ml-2 text-muted-foreground/80">{t("progress.remainingDays", { days: remainingDays })}</span>
46
+ )}
47
+ {isOverdue && (
48
+ <span className="ml-2 text-red-500">{t("progress.overdueDays", { days: Math.abs(remainingDays) })}</span>
49
+ )}
50
+ </p>
51
+ </div>
52
+ </div>
53
+ {item.storyCount > 0 && (
54
+ <div className="mt-3">
55
+ <IterationProgressBar stateCounts={item.stateCounts} />
56
+ </div>
57
+ )}
58
+ </Link>
59
+ );
60
+ }
@@ -0,0 +1,116 @@
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 { useState } from "react";
19
+ import { ChevronDown, X } from "lucide-react";
20
+ import { useTranslation } from "react-i18next";
21
+ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
22
+ import { Button } from "@/components/ui/button";
23
+ import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from "@/components/ui/command";
24
+ import {
25
+ AlertDialog,
26
+ AlertDialogAction,
27
+ AlertDialogCancel,
28
+ AlertDialogContent,
29
+ AlertDialogDescription,
30
+ AlertDialogFooter,
31
+ AlertDialogHeader,
32
+ AlertDialogTitle,
33
+ } from "@/components/ui/alert-dialog";
34
+ import { setStoryIterationFn } from "@/server/serverFns/iteration";
35
+ import { toast } from "@/components/ui/sonner";
36
+ import type { IterationSummary } from "@/server/serverFns/iteration/schemas";
37
+
38
+ interface IterationEditorProps {
39
+ storyId: string;
40
+ current: { id: string; name: string } | null;
41
+ options: IterationSummary[];
42
+ onChanged: (iteration: { id: string; name: string } | null) => void;
43
+ }
44
+
45
+ export function IterationEditor({ storyId, current, options, onChanged }: IterationEditorProps) {
46
+ const { t } = useTranslation("iterations");
47
+ const [open, setOpen] = useState(false);
48
+ const [pendingId, setPendingId] = useState<string | null>(null);
49
+ const joinable = options.filter((i) => i.status !== "completed");
50
+
51
+ async function executePick(id: string | null) {
52
+ setPendingId(null);
53
+ await setStoryIterationFn({ data: { storyId, iterationId: id } });
54
+ const next = id ? joinable.find((i) => i.id === id) ?? null : null;
55
+ toast.success(id && next ? t("editor.setSuccess", { name: next.name }) : t("editor.clearSuccess"));
56
+ onChanged(next ? { id: next.id, name: next.name } : null);
57
+ }
58
+
59
+ function handlePick(id: string | null) {
60
+ setOpen(false);
61
+ if (current && id && current.id !== id) {
62
+ setPendingId(id);
63
+ } else {
64
+ void executePick(id);
65
+ }
66
+ }
67
+
68
+ return (
69
+ <>
70
+ <Popover open={open} onOpenChange={setOpen}>
71
+ <PopoverTrigger asChild>
72
+ <Button variant="ghost" size="sm" className="h-6 justify-start gap-1 px-1 text-xs font-normal">
73
+ {current ? current.name : <span className="text-muted-foreground">{t("editor.placeholder")}</span>}
74
+ {current ? (
75
+ <X
76
+ className="ml-1 size-3 hover:text-destructive"
77
+ onClick={(e) => { e.stopPropagation(); void executePick(null); }}
78
+ />
79
+ ) : null}
80
+ <ChevronDown className="ml-auto size-3 text-muted-foreground" />
81
+ </Button>
82
+ </PopoverTrigger>
83
+ <PopoverContent className="w-56 p-0" align="start">
84
+ <Command>
85
+ <CommandList>
86
+ <CommandEmpty>{t("editor.none")}</CommandEmpty>
87
+ <CommandGroup>
88
+ {joinable.map((i) => (
89
+ <CommandItem key={i.id} value={i.id} onSelect={() => handlePick(i.id)}>
90
+ {i.name}
91
+ <span className="ml-auto text-xs text-muted-foreground">{i.startDate} ~ {i.endDate}</span>
92
+ </CommandItem>
93
+ ))}
94
+ </CommandGroup>
95
+ </CommandList>
96
+ </Command>
97
+ </PopoverContent>
98
+ </Popover>
99
+
100
+ <AlertDialog open={pendingId !== null} onOpenChange={(v) => { if (!v) setPendingId(null); }}>
101
+ <AlertDialogContent>
102
+ <AlertDialogHeader>
103
+ <AlertDialogTitle>{t("editor.confirmSwitch", { name: current?.name ?? "" })}</AlertDialogTitle>
104
+ <AlertDialogDescription />
105
+ </AlertDialogHeader>
106
+ <AlertDialogFooter>
107
+ <AlertDialogCancel onClick={() => setPendingId(null)}>{t("actions.cancel")}</AlertDialogCancel>
108
+ <AlertDialogAction onClick={() => { if (pendingId) void executePick(pendingId); }}>
109
+ {t("actions.confirm")}
110
+ </AlertDialogAction>
111
+ </AlertDialogFooter>
112
+ </AlertDialogContent>
113
+ </AlertDialog>
114
+ </>
115
+ );
116
+ }
@@ -0,0 +1,44 @@
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 { useTranslation } from "react-i18next";
19
+ import { STATE_COLOR } from "@/components/story/constants";
20
+ import type { StoryState } from "@/components/story/constants";
21
+
22
+ const ORDER: StoryState[] = ["backlog", "todo", "in_progress", "done", "cancelled"];
23
+
24
+ export function IterationProgressBar({ stateCounts }: { stateCounts: Record<string, number> }) {
25
+ const { t } = useTranslation("iterations");
26
+ const total = ORDER.reduce((a, s) => a + (stateCounts[s] ?? 0), 0);
27
+ const denom = total - (stateCounts.cancelled ?? 0);
28
+ const percent = denom > 0 ? Math.round(((stateCounts.done ?? 0) / denom) * 100) : 0;
29
+ return (
30
+ <div className="space-y-1">
31
+ <div className="flex h-1.5 w-full overflow-hidden rounded-full bg-muted">
32
+ {ORDER.map((s) => {
33
+ const n = stateCounts[s] ?? 0;
34
+ if (!n) return null;
35
+ return <div key={s} className={STATE_COLOR[s]} style={{ width: `${(n / Math.max(1, total)) * 100}%` }} />;
36
+ })}
37
+ </div>
38
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
39
+ <span>{t("progress.total")}: {total}</span>
40
+ <span>{t("progress.percent")}: {percent}%</span>
41
+ </div>
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,33 @@
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 { useTranslation } from "react-i18next";
19
+ import { ITERATION_STATUS_META } from "./constants";
20
+ import type { IterationStatus } from "@/lib/iteration/status";
21
+ import { cn } from "@/lib/utils";
22
+
23
+ export function IterationStatusBadge({ status }: { status: IterationStatus }) {
24
+ const { t } = useTranslation("iterations");
25
+ const meta = ITERATION_STATUS_META[status];
26
+ const Icon = meta.icon;
27
+ return (
28
+ <span className={cn("inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium", meta.className)}>
29
+ <Icon className="size-3" />
30
+ {t(meta.labelKey)}
31
+ </span>
32
+ );
33
+ }
@@ -21,17 +21,14 @@ import { useForm } from "@tanstack/react-form";
21
21
  import { useRouter } from "@tanstack/react-router";
22
22
  import { Button } from "@/components/ui/button";
23
23
  import { Input } from "@/components/ui/input";
24
- import { Textarea } from "@/components/ui/textarea";
25
24
  import {
26
25
  Dialog,
27
26
  DialogContent,
28
- DialogDescription,
29
27
  DialogFooter,
30
28
  DialogHeader,
31
29
  DialogTitle,
32
30
  } from "@/components/ui/dialog";
33
- import { IconPicker } from "@/components/shared/icon-picker";
34
- import { KNOWLEDGE_ICONS } from "@/components/shared/icon-packs/knowledge";
31
+ import { LucideIconPicker } from "@/components/shared/lucide-icon-picker";
35
32
  import { createKnowledgeFn, updateKnowledgeFn } from "@/server/serverFns/knowledge";
36
33
  import { knowledgeFormSchema, type KnowledgeFormValues } from "../schemas/knowledge-form";
37
34
 
@@ -53,7 +50,7 @@ export function KnowledgeFormDialog({ open, onOpenChange, kb, onSuccess }: Knowl
53
50
  defaultValues: {
54
51
  name: kb?.name ?? "",
55
52
  description: kb?.description ?? "",
56
- icon: kb?.icon ?? "notebook",
53
+ icon: kb?.icon ?? "",
57
54
  },
58
55
  validators: [{ run: knowledgeFormSchema as any, triggers: ['change'] }],
59
56
  });
@@ -102,7 +99,7 @@ export function KnowledgeFormDialog({ open, onOpenChange, kb, onSuccess }: Knowl
102
99
 
103
100
  const handleOpenChange = (newOpen: boolean) => {
104
101
  if (newOpen) {
105
- form.setFieldValue("icon", kb?.icon ?? "notebook");
102
+ form.setFieldValue("icon", kb?.icon ?? "");
106
103
  form.setFieldValue("name", kb?.name ?? "");
107
104
  form.setFieldValue("description", kb?.description ?? "");
108
105
  setError("");
@@ -112,27 +109,22 @@ export function KnowledgeFormDialog({ open, onOpenChange, kb, onSuccess }: Knowl
112
109
 
113
110
  return (
114
111
  <Dialog open={open} onOpenChange={handleOpenChange}>
115
- <DialogContent className="sm:max-w-200">
116
- <DialogHeader className="p-4">
117
- <DialogTitle>{isEdit ? t('kbForm.title.edit') : t('kbForm.title.create')}</DialogTitle>
118
- <DialogDescription>
119
- {isEdit ? t('kbForm.description.edit') : t('kbForm.description.create')}
120
- </DialogDescription>
112
+ <DialogContent className="sm:max-w-260 -mt-50">
113
+ <DialogHeader>
114
+ <DialogTitle className="text-xl">
115
+ {isEdit ? t('kbForm.title.edit') : t('kbForm.title.create')}
116
+ </DialogTitle>
121
117
  </DialogHeader>
122
118
 
123
- <div className="space-y-4 p-4">
124
- <div className="flex flex-col gap-3 mb-20">
119
+ <div className="space-y-3 overflow-y-auto px-4 py-2 max-h-[60vh]">
120
+ <div className="flex gap-2">
125
121
  <form.Field name="icon" children={(field) => (
126
- <IconPicker
127
- icons={KNOWLEDGE_ICONS}
128
- value={field.value ?? "notebook"}
122
+ <LucideIconPicker
123
+ value={field.value}
129
124
  onChange={(v) => field.handleChange(v)}
125
+ modal
130
126
  />
131
127
  )} />
132
- </div>
133
-
134
- <div className="flex flex-col gap-3">
135
- <label className="text-sm font-medium ml-2">{t('kbForm.nameLabel')}</label>
136
128
  <form.Field name="name" children={(field) => (
137
129
  <Input
138
130
  value={field.value ?? ""}
@@ -140,7 +132,7 @@ export function KnowledgeFormDialog({ open, onOpenChange, kb, onSuccess }: Knowl
140
132
  onBlur={field.handleBlur}
141
133
  placeholder={t('kbForm.namePlaceholder')}
142
134
  maxLength={50}
143
- autoFocus
135
+ className="h-10 min-w-0 flex-1"
144
136
  onKeyDown={(e) => {
145
137
  if (e.key === "Enter" && !e.shiftKey) {
146
138
  e.preventDefault();
@@ -151,26 +143,25 @@ export function KnowledgeFormDialog({ open, onOpenChange, kb, onSuccess }: Knowl
151
143
  )} />
152
144
  </div>
153
145
 
154
- <div className="flex flex-col gap-3">
155
- <label className="text-sm font-medium ml-2">{t('kbForm.descriptionLabel')}</label>
156
- <form.Field name="description" children={(field) => (
157
- <Textarea
146
+ <form.Field name="description" children={(field) => (
147
+ <div className="notion-full-width min-h-50 max-h-150 flex flex-col overflow-y-auto border rounded-md border-border">
148
+ <textarea
158
149
  value={field.value ?? ""}
159
150
  onChange={(e) => field.handleChange(e.target.value)}
160
151
  onBlur={field.handleBlur}
161
152
  placeholder={t('kbForm.descriptionPlaceholder')}
162
153
  maxLength={200}
163
- rows={2}
154
+ className="flex-1 min-h-0 resize-none bg-transparent px-3 py-2 text-sm outline-none placeholder:text-muted-foreground"
164
155
  />
165
- )} />
166
- </div>
156
+ </div>
157
+ )} />
167
158
 
168
159
  {error && (
169
160
  <p className="text-sm text-destructive break-words">{error}</p>
170
161
  )}
171
162
  </div>
172
163
 
173
- <DialogFooter>
164
+ <DialogFooter className="items-center gap-3">
174
165
  <Button variant="outline" onClick={() => handleOpenChange(false)}>
175
166
  {t('kbForm.cancel')}
176
167
  </Button>