@leanspec/ui 0.2.14 → 0.2.15-dev.21025278490

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 (150) hide show
  1. package/bin/leanspec-ui.js +191 -0
  2. package/dist/assets/_baseUniq-CRqreL7N.js +1 -0
  3. package/dist/assets/arc-DMhx9AJT.js +1 -0
  4. package/dist/assets/architectureDiagram-VXUJARFQ-DM0L0YzO.js +36 -0
  5. package/dist/assets/blockDiagram-VD42YOAC-DHQXDHsD.js +122 -0
  6. package/dist/assets/c4Diagram-YG6GDRKO-0L7o2gpH.js +10 -0
  7. package/dist/assets/channel-2tOl0nAZ.js +1 -0
  8. package/dist/assets/chunk-4BX2VUAB-CwFT-Uaj.js +1 -0
  9. package/dist/assets/chunk-55IACEB6-CjvuUHHG.js +1 -0
  10. package/dist/assets/chunk-B4BG7PRW-BRJBysMK.js +165 -0
  11. package/dist/assets/chunk-DI55MBZ5-BnNEeoaA.js +220 -0
  12. package/dist/assets/chunk-FMBD7UC4-BK2l30pm.js +15 -0
  13. package/dist/assets/chunk-QN33PNHL-BN_cZkCU.js +1 -0
  14. package/dist/assets/chunk-QZHKN3VN-Brc3Yrub.js +1 -0
  15. package/dist/assets/chunk-TZMSLE5B-D2zzpLfO.js +1 -0
  16. package/dist/assets/classDiagram-2ON5EDUG-BB9CSNmS.js +1 -0
  17. package/dist/assets/classDiagram-v2-WZHVMYZB-BB9CSNmS.js +1 -0
  18. package/dist/assets/clone-BjxVFtyI.js +1 -0
  19. package/dist/assets/core-DV6XEvTN.js +1 -0
  20. package/dist/assets/cose-bilkent-S5V4N54A-CLJgM3XR.js +1 -0
  21. package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
  22. package/dist/assets/dagre-6UL2VRFP-_IFvBJKJ.js +4 -0
  23. package/dist/assets/diagram-PSM6KHXK--83HIYSQ.js +24 -0
  24. package/dist/assets/diagram-QEK2KX5R-6jAWnCnZ.js +43 -0
  25. package/dist/assets/diagram-S2PKOQOG-D5pwHvjZ.js +24 -0
  26. package/dist/assets/erDiagram-Q2GNP2WA-B4FV3mTd.js +60 -0
  27. package/dist/assets/flowDiagram-NV44I4VS-mtD2kF4M.js +162 -0
  28. package/dist/assets/ganttDiagram-JELNMOA3-BKALgqTK.js +267 -0
  29. package/dist/assets/gitGraphDiagram-NY62KEGX-Bd7r0pAf.js +65 -0
  30. package/dist/assets/graph-B2rEI7cK.js +1 -0
  31. package/dist/assets/index-Bekv_o1t.css +1 -0
  32. package/dist/assets/index-DSRxU-E5.js +389 -0
  33. package/dist/assets/infoDiagram-WHAUD3N6--nJOBKqh.js +2 -0
  34. package/dist/assets/journeyDiagram-XKPGCS4Q-BzGutKN3.js +139 -0
  35. package/dist/assets/kanban-definition-3W4ZIXB7-DyQO17vq.js +89 -0
  36. package/dist/assets/katex-XbL3y5x-.js +261 -0
  37. package/dist/assets/layout-iCSHU015.js +1 -0
  38. package/dist/assets/min-BK_AIJdo.js +1 -0
  39. package/dist/assets/mindmap-definition-VGOIOE7T-BZMj_6zo.js +68 -0
  40. package/dist/assets/pieDiagram-ADFJNKIX-CkAGsq9p.js +30 -0
  41. package/dist/assets/quadrantDiagram-AYHSOK5B-CWa93px1.js +7 -0
  42. package/dist/assets/requirementDiagram-UZGBJVZJ-CufFVR8c.js +64 -0
  43. package/dist/assets/sankeyDiagram-TZEHDZUN-BEPgVgU4.js +10 -0
  44. package/dist/assets/sequenceDiagram-WL72ISMW-BkdBWhel.js +145 -0
  45. package/dist/assets/stateDiagram-FKZM4ZOC-D5T73yx0.js +1 -0
  46. package/dist/assets/stateDiagram-v2-4FDKWEC3-9hJWG2n6.js +1 -0
  47. package/dist/assets/timeline-definition-IT6M3QCI-CX7kTdU2.js +61 -0
  48. package/dist/assets/treemap-KMMF4GRG-ftWCQ9lJ.js +128 -0
  49. package/dist/assets/xychartDiagram-PRI3JC2R-Ngrels4n.js +7 -0
  50. package/{index.html → dist/index.html} +2 -1
  51. package/package.json +12 -2
  52. package/eslint.config.js +0 -23
  53. package/package.json.backup +0 -83
  54. package/postcss.config.js +0 -6
  55. package/src/App.css +0 -42
  56. package/src/App.tsx +0 -17
  57. package/src/assets/react.svg +0 -1
  58. package/src/components/LanguageSwitcher.tsx +0 -67
  59. package/src/components/Layout.tsx +0 -88
  60. package/src/components/MainSidebar.tsx +0 -163
  61. package/src/components/MermaidDiagram.tsx +0 -85
  62. package/src/components/MinimalLayout.tsx +0 -51
  63. package/src/components/Navigation.tsx +0 -254
  64. package/src/components/PriorityBadge.tsx +0 -59
  65. package/src/components/ProjectSwitcher.tsx +0 -222
  66. package/src/components/QuickSearch.tsx +0 -225
  67. package/src/components/RootRedirect.tsx +0 -40
  68. package/src/components/SpecDetailLayout.context.ts +0 -10
  69. package/src/components/SpecDetailLayout.tsx +0 -14
  70. package/src/components/SpecsNavSidebar.tsx +0 -615
  71. package/src/components/StatusBadge.tsx +0 -59
  72. package/src/components/ThemeToggle.tsx +0 -25
  73. package/src/components/Tooltip.tsx +0 -29
  74. package/src/components/context/ContextClient.tsx +0 -471
  75. package/src/components/context/ContextFileDetail.tsx +0 -163
  76. package/src/components/dashboard/ActivityItem.tsx +0 -36
  77. package/src/components/dashboard/DashboardClient.tsx +0 -218
  78. package/src/components/dashboard/SpecListItem.tsx +0 -58
  79. package/src/components/dashboard/StatCard.tsx +0 -52
  80. package/src/components/dependencies/SpecNode.tsx +0 -128
  81. package/src/components/dependencies/SpecSidebar.tsx +0 -256
  82. package/src/components/dependencies/constants.ts +0 -25
  83. package/src/components/dependencies/types.ts +0 -38
  84. package/src/components/dependencies/utils.ts +0 -261
  85. package/src/components/metadata-editors/PriorityEditor.tsx +0 -89
  86. package/src/components/metadata-editors/StatusEditor.tsx +0 -85
  87. package/src/components/metadata-editors/TagsEditor.tsx +0 -207
  88. package/src/components/projects/CreateProjectDialog.tsx +0 -162
  89. package/src/components/projects/DirectoryPicker.tsx +0 -182
  90. package/src/components/shared/BackToTop.tsx +0 -39
  91. package/src/components/shared/ColorPicker.tsx +0 -68
  92. package/src/components/shared/EmptyState.tsx +0 -35
  93. package/src/components/shared/ErrorBoundary.tsx +0 -79
  94. package/src/components/shared/PageHeader.tsx +0 -23
  95. package/src/components/shared/PageTransition.tsx +0 -40
  96. package/src/components/shared/ProjectAvatar.tsx +0 -107
  97. package/src/components/shared/Skeletons.tsx +0 -184
  98. package/src/components/spec-detail/EditableMetadata.tsx +0 -129
  99. package/src/components/spec-detail/MarkdownRenderer.tsx +0 -47
  100. package/src/components/spec-detail/TableOfContents.tsx +0 -150
  101. package/src/components/specs/BoardView.tsx +0 -204
  102. package/src/components/specs/ListView.tsx +0 -62
  103. package/src/components/specs/SpecsFilters.tsx +0 -190
  104. package/src/contexts/KeyboardShortcutsContext.tsx +0 -95
  105. package/src/contexts/LayoutContext.tsx +0 -45
  106. package/src/contexts/ProjectContext.tsx +0 -163
  107. package/src/contexts/ThemeContext.tsx +0 -90
  108. package/src/contexts/index.ts +0 -7
  109. package/src/hooks/useKeyboardShortcuts.ts +0 -87
  110. package/src/index.css +0 -624
  111. package/src/lib/api.ts +0 -72
  112. package/src/lib/backend-adapter.ts +0 -382
  113. package/src/lib/date-utils.ts +0 -122
  114. package/src/lib/i18n.test.ts +0 -57
  115. package/src/lib/i18n.ts +0 -51
  116. package/src/lib/markdown-utils.ts +0 -38
  117. package/src/lib/sub-spec-utils.ts +0 -166
  118. package/src/lib/utils.ts +0 -6
  119. package/src/locales/en/common.json +0 -660
  120. package/src/locales/en/errors.json +0 -20
  121. package/src/locales/en/help.json +0 -8
  122. package/src/locales/zh-CN/common.json +0 -660
  123. package/src/locales/zh-CN/errors.json +0 -20
  124. package/src/locales/zh-CN/help.json +0 -8
  125. package/src/main.tsx +0 -12
  126. package/src/pages/ContextPage.tsx +0 -111
  127. package/src/pages/DashboardPage.tsx +0 -97
  128. package/src/pages/DependenciesPage.tsx +0 -881
  129. package/src/pages/ProjectsPage.tsx +0 -432
  130. package/src/pages/SpecDetailPage.tsx +0 -592
  131. package/src/pages/SpecsPage.tsx +0 -319
  132. package/src/pages/StatsPage.tsx +0 -307
  133. package/src/router/projectRoutes.tsx +0 -36
  134. package/src/router.tsx +0 -33
  135. package/src/test/setup.ts +0 -39
  136. package/src/types/api.ts +0 -185
  137. package/tailwind.config.ts +0 -57
  138. package/tsconfig.app.json +0 -29
  139. package/tsconfig.json +0 -7
  140. package/tsconfig.node.json +0 -26
  141. package/tsconfig.tsbuildinfo +0 -1
  142. package/vite.config.ts +0 -27
  143. package/vitest.config.ts +0 -18
  144. /package/{public → dist}/favicon.ico +0 -0
  145. /package/{public → dist}/github-mark-white.svg +0 -0
  146. /package/{public → dist}/github-mark.svg +0 -0
  147. /package/{public → dist}/logo-dark-bg.svg +0 -0
  148. /package/{public → dist}/logo-with-bg.svg +0 -0
  149. /package/{public → dist}/logo.svg +0 -0
  150. /package/{public → dist}/vite.svg +0 -0
@@ -1,256 +0,0 @@
1
- import { Clock, PlayCircle, CheckCircle2, Archive, AlertCircle, ArrowUp, Minus, ArrowDown } from 'lucide-react';
2
- import { useTranslation } from 'react-i18next';
3
- import { cn } from '../../lib/utils';
4
- import type { SpecNode, FocusedNodeDetails } from './types';
5
-
6
- const statusIcons = {
7
- 'planned': Clock,
8
- 'in-progress': PlayCircle,
9
- 'complete': CheckCircle2,
10
- 'archived': Archive,
11
- };
12
-
13
- const priorityIcons = {
14
- 'critical': AlertCircle,
15
- 'high': ArrowUp,
16
- 'medium': Minus,
17
- 'low': ArrowDown,
18
- };
19
-
20
- interface SpecListItemProps {
21
- spec: SpecNode;
22
- type: 'upstream' | 'downstream';
23
- depth: number;
24
- onClick: () => void;
25
- }
26
-
27
- function SpecListItem({ spec, type, depth, onClick }: SpecListItemProps) {
28
- const { t } = useTranslation();
29
- const typeColors = {
30
- upstream: 'border-l-amber-500',
31
- downstream: 'border-l-emerald-500',
32
- };
33
-
34
- const depthLabel = depth === 1
35
- ? t('dependenciesPage.sidebar.depth.direct')
36
- : t('dependenciesPage.sidebar.depth.level', { depth });
37
-
38
- const StatusIcon = statusIcons[spec.status as keyof typeof statusIcons] || Clock;
39
- const PriorityIcon = priorityIcons[spec.priority as keyof typeof priorityIcons] || Minus;
40
-
41
- return (
42
- <button
43
- onClick={onClick}
44
- className={cn(
45
- 'w-full text-left px-2 py-1.5 rounded border-l-2 bg-muted/30 hover:bg-muted/50 transition-colors',
46
- typeColors[type]
47
- )}
48
- >
49
- <div className="flex items-center gap-1.5">
50
- <span className="text-[10px] font-bold text-muted-foreground">
51
- #{spec.number.toString().padStart(3, '0')}
52
- </span>
53
- {/* Status icon */}
54
- <div
55
- className={cn(
56
- 'rounded p-0.5 flex items-center justify-center',
57
- spec.status === 'planned' && 'bg-blue-500/20',
58
- spec.status === 'in-progress' && 'bg-orange-500/20',
59
- spec.status === 'complete' && 'bg-green-500/20',
60
- spec.status === 'archived' && 'bg-gray-500/20'
61
- )}
62
- title={t(`status.${spec.status}`)}
63
- >
64
- <StatusIcon
65
- className={cn(
66
- 'h-2.5 w-2.5',
67
- spec.status === 'planned' && 'text-blue-600 dark:text-blue-400',
68
- spec.status === 'in-progress' && 'text-orange-600 dark:text-orange-400',
69
- spec.status === 'complete' && 'text-green-600 dark:text-green-400',
70
- spec.status === 'archived' && 'text-gray-500 dark:text-gray-400'
71
- )}
72
- />
73
- </div>
74
- {/* Priority icon */}
75
- <div
76
- className={cn(
77
- 'rounded p-0.5 flex items-center justify-center',
78
- spec.priority === 'critical' && 'bg-red-500/20',
79
- spec.priority === 'high' && 'bg-orange-500/20',
80
- spec.priority === 'medium' && 'bg-blue-500/20',
81
- spec.priority === 'low' && 'bg-gray-500/20'
82
- )}
83
- title={spec.priority ? t(`priority.${spec.priority}`) : undefined}
84
- >
85
- <PriorityIcon
86
- className={cn(
87
- 'h-2.5 w-2.5',
88
- spec.priority === 'critical' && 'text-red-600 dark:text-red-400',
89
- spec.priority === 'high' && 'text-orange-600 dark:text-orange-400',
90
- spec.priority === 'medium' && 'text-blue-600 dark:text-blue-400',
91
- spec.priority === 'low' && 'text-gray-500 dark:text-gray-400'
92
- )}
93
- />
94
- </div>
95
- <span className="text-[8px] px-1 py-0.5 rounded bg-muted text-muted-foreground font-medium ml-auto">
96
- {depthLabel}
97
- </span>
98
- </div>
99
- <p className="text-[11px] text-foreground truncate leading-tight mt-0.5">{spec.name}</p>
100
- </button>
101
- );
102
- }
103
-
104
- interface SpecSidebarProps {
105
- focusedDetails: FocusedNodeDetails | null;
106
- onSelectSpec: (specId: string) => void;
107
- onOpenSpec: (specNumber: number) => void;
108
- }
109
-
110
- export function SpecSidebar({ focusedDetails, onSelectSpec, onOpenSpec }: SpecSidebarProps) {
111
- const { t } = useTranslation();
112
- if (!focusedDetails) {
113
- return (
114
- <div className="w-64 shrink-0 rounded-lg border border-border bg-background/95 overflow-hidden flex flex-col">
115
- <div className="flex-1 flex items-center justify-center p-4">
116
- <div className="text-center text-muted-foreground">
117
- <div className="w-12 h-12 mx-auto mb-3 rounded-full bg-muted/50 flex items-center justify-center">
118
- <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
119
- <path
120
- strokeLinecap="round"
121
- strokeLinejoin="round"
122
- strokeWidth={1.5}
123
- d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
124
- />
125
- </svg>
126
- </div>
127
- <p className="text-sm font-medium">{t('dependenciesPage.sidebar.emptyTitle')}</p>
128
- <p className="text-xs mt-1">{t('dependenciesPage.sidebar.emptyDescription')}</p>
129
- </div>
130
- </div>
131
- </div>
132
- );
133
- }
134
-
135
- const { node, upstream, downstream } = focusedDetails;
136
-
137
- const StatusIcon = statusIcons[node.status as keyof typeof statusIcons] || Clock;
138
- const PriorityIcon = priorityIcons[node.priority as keyof typeof priorityIcons] || Minus;
139
-
140
- return (
141
- <div className="w-64 shrink-0 rounded-lg border border-border bg-background/95 overflow-hidden flex flex-col">
142
- {/* Selected spec header */}
143
- <div className="p-3 border-b border-border bg-muted/30">
144
- <div className="flex items-center gap-2 mb-1">
145
- <span className="font-bold text-sm">#{node.number.toString().padStart(3, '0')}</span>
146
- {/* Status icon */}
147
- <div
148
- className={cn(
149
- 'rounded p-1 flex items-center justify-center',
150
- node.status === 'planned' && 'bg-blue-500/20',
151
- node.status === 'in-progress' && 'bg-orange-500/20',
152
- node.status === 'complete' && 'bg-green-500/20',
153
- node.status === 'archived' && 'bg-gray-500/20'
154
- )}
155
- title={node.status}
156
- >
157
- <StatusIcon
158
- className={cn(
159
- 'h-3 w-3',
160
- node.status === 'planned' && 'text-blue-600 dark:text-blue-300',
161
- node.status === 'in-progress' && 'text-orange-600 dark:text-orange-300',
162
- node.status === 'complete' && 'text-green-600 dark:text-green-300',
163
- node.status === 'archived' && 'text-gray-500 dark:text-gray-300'
164
- )}
165
- />
166
- </div>
167
- {/* Priority icon */}
168
- <div
169
- className={cn(
170
- 'rounded p-1 flex items-center justify-center',
171
- node.priority === 'critical' && 'bg-red-500/20',
172
- node.priority === 'high' && 'bg-orange-500/20',
173
- node.priority === 'medium' && 'bg-blue-500/20',
174
- node.priority === 'low' && 'bg-gray-500/20'
175
- )}
176
- title={node.priority}
177
- >
178
- <PriorityIcon
179
- className={cn(
180
- 'h-3 w-3',
181
- node.priority === 'critical' && 'text-red-600 dark:text-red-300',
182
- node.priority === 'high' && 'text-orange-600 dark:text-orange-300',
183
- node.priority === 'medium' && 'text-blue-600 dark:text-blue-300',
184
- node.priority === 'low' && 'text-gray-500 dark:text-gray-300'
185
- )}
186
- />
187
- </div>
188
- </div>
189
- <p className="text-sm font-medium text-foreground leading-snug">{node.name}</p>
190
- <button
191
- onClick={() => onOpenSpec(node.number)}
192
- className="mt-2 w-full rounded bg-primary/20 border border-primary/40 px-2 py-1.5 text-xs text-primary hover:bg-primary/30 font-medium"
193
- >
194
- {t('dependenciesPage.sidebar.openSpec')}
195
- </button>
196
- </div>
197
-
198
- {/* Scrollable spec lists */}
199
- <div className="flex-1 overflow-auto p-3 space-y-4">
200
- {/* Upstream Dependencies */}
201
- <div>
202
- <div className="flex items-center gap-2 mb-2">
203
- <span className="inline-block w-2 h-2 rounded-full bg-amber-500" />
204
- <span className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
205
- {t('dependenciesPage.sidebar.dependsOnHeading', { count: upstream.reduce((sum, g) => sum + g.specs.length, 0) })}
206
- </span>
207
- </div>
208
- {upstream.length > 0 ? (
209
- <div className="space-y-1.5">
210
- {upstream.flatMap((group) =>
211
- group.specs.map((spec) => (
212
- <SpecListItem
213
- key={spec.id}
214
- spec={spec}
215
- type="upstream"
216
- depth={group.depth}
217
- onClick={() => onSelectSpec(spec.id)}
218
- />
219
- ))
220
- )}
221
- </div>
222
- ) : (
223
- <p className="text-xs text-muted-foreground/60 italic">{t('dependenciesPage.sidebar.emptyUpstream')}</p>
224
- )}
225
- </div>
226
-
227
- {/* Downstream Dependents */}
228
- <div>
229
- <div className="flex items-center gap-2 mb-2">
230
- <span className="inline-block w-2 h-2 rounded-full bg-emerald-500" />
231
- <span className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
232
- {t('dependenciesPage.sidebar.requiredByHeading', { count: downstream.reduce((sum, g) => sum + g.specs.length, 0) })}
233
- </span>
234
- </div>
235
- {downstream.length > 0 ? (
236
- <div className="space-y-1.5">
237
- {downstream.flatMap((group) =>
238
- group.specs.map((spec) => (
239
- <SpecListItem
240
- key={spec.id}
241
- spec={spec}
242
- type="downstream"
243
- depth={group.depth}
244
- onClick={() => onSelectSpec(spec.id)}
245
- />
246
- ))
247
- )}
248
- </div>
249
- ) : (
250
- <p className="text-xs text-muted-foreground/60 italic">{t('dependenciesPage.sidebar.emptyDownstream')}</p>
251
- )}
252
- </div>
253
- </div>
254
- </div>
255
- );
256
- }
@@ -1,25 +0,0 @@
1
- // Node dimensions
2
- export const NODE_WIDTH = 180;
3
- export const NODE_HEIGHT = 60;
4
- export const COMPACT_NODE_WIDTH = 120;
5
- export const COMPACT_NODE_HEIGHT = 40;
6
-
7
- // Edge colors
8
- export const DEPENDS_ON_COLOR = '#f59e0b';
9
-
10
- // Status tone classes for node styling (aligned with StatusBadge component)
11
- // Using light/dark mode compatible colors
12
- export const toneClasses: Record<string, string> = {
13
- planned: 'border-blue-500 bg-blue-100 text-blue-800 dark:bg-blue-950/60 dark:text-blue-200',
14
- 'in-progress': 'border-orange-500 bg-orange-100 text-orange-800 dark:bg-orange-950/60 dark:text-orange-200',
15
- complete: 'border-green-500 bg-green-100 text-green-800 dark:bg-green-950/60 dark:text-green-200',
16
- archived: 'border-gray-400 bg-gray-100 text-gray-600 dark:border-gray-500/80 dark:bg-gray-900/60 dark:text-gray-400',
17
- };
18
-
19
- // Background colors for minimap (aligned with StatusBadge component)
20
- export const toneBgColors: Record<string, string> = {
21
- planned: '#3b82f6', // blue-500 (works in both themes)
22
- 'in-progress': '#f97316', // orange-500
23
- complete: '#22c55e', // green-500
24
- archived: '#6b7280', // gray-500
25
- };
@@ -1,38 +0,0 @@
1
- import type { DependencyGraph } from '../../types/api';
2
-
3
- export type GraphTone = 'planned' | 'in-progress' | 'complete' | 'archived';
4
-
5
- export interface SpecNodeData {
6
- label: string;
7
- shortLabel: string;
8
- badge: string;
9
- number: number;
10
- tone: GraphTone;
11
- priority: string;
12
- href?: string;
13
- interactive?: boolean;
14
- isFocused?: boolean;
15
- connectionDepth?: number;
16
- isDimmed?: boolean;
17
- isCompact?: boolean;
18
- isSecondary?: boolean; // Shown due to critical path, not primary filter
19
- }
20
-
21
- export type SpecNode = DependencyGraph['nodes'][0];
22
-
23
- // Specs grouped by their depth level from the focused node
24
- export interface SpecsByDepth {
25
- depth: number;
26
- specs: SpecNode[];
27
- }
28
-
29
- export interface FocusedNodeDetails {
30
- node: SpecNode;
31
- upstream: SpecsByDepth[]; // All transitive deps grouped by depth
32
- downstream: SpecsByDepth[]; // All transitive dependents grouped by depth
33
- }
34
-
35
- export interface ConnectionStats {
36
- connected: number;
37
- standalone: number;
38
- }
@@ -1,261 +0,0 @@
1
- import dagre from '@dagrejs/dagre';
2
- import type { Node, Edge } from 'reactflow';
3
- import type { SpecNodeData } from './types';
4
- import {
5
- NODE_WIDTH,
6
- NODE_HEIGHT,
7
- COMPACT_NODE_WIDTH,
8
- COMPACT_NODE_HEIGHT,
9
- } from './constants';
10
-
11
- /**
12
- * Get nodes at various depths from a starting node (directional BFS)
13
- * Only includes upstream (specs this depends on) and downstream (specs that depend on this)
14
- * Edge direction: source depends_on target (A→B means A depends on B)
15
- */
16
- export function getConnectionDepths(
17
- startId: string,
18
- edges: Array<{ source: string; target: string }>,
19
- maxDepth: number = 2
20
- ): Map<string, number> {
21
- const depths = new Map<string, number>();
22
- depths.set(startId, 0);
23
-
24
- // Build directional adjacency maps
25
- // upstreamMap: source → targets (specs that source depends on)
26
- // downstreamMap: target → sources (specs that depend on target)
27
- const upstreamMap = new Map<string, Set<string>>();
28
- const downstreamMap = new Map<string, Set<string>>();
29
-
30
- edges.forEach((e) => {
31
- // source depends on target, so target is upstream of source
32
- if (!upstreamMap.has(e.source)) upstreamMap.set(e.source, new Set());
33
- upstreamMap.get(e.source)!.add(e.target);
34
-
35
- // source depends on target, so source is downstream of target
36
- if (!downstreamMap.has(e.target)) downstreamMap.set(e.target, new Set());
37
- downstreamMap.get(e.target)!.add(e.source);
38
- });
39
-
40
- // BFS upstream (specs this depends on, directly or transitively)
41
- let currentLevel = new Set([startId]);
42
- let depth = 1;
43
- while (currentLevel.size > 0 && depth <= maxDepth) {
44
- const nextLevel = new Set<string>();
45
- currentLevel.forEach((nodeId) => {
46
- const upstreamNodes = upstreamMap.get(nodeId);
47
- if (upstreamNodes) {
48
- upstreamNodes.forEach((upstream) => {
49
- if (!depths.has(upstream)) {
50
- depths.set(upstream, depth);
51
- nextLevel.add(upstream);
52
- }
53
- });
54
- }
55
- });
56
- currentLevel = nextLevel;
57
- depth++;
58
- }
59
-
60
- // BFS downstream (specs that depend on this, directly or transitively)
61
- currentLevel = new Set([startId]);
62
- depth = 1;
63
- while (currentLevel.size > 0 && depth <= maxDepth) {
64
- const nextLevel = new Set<string>();
65
- currentLevel.forEach((nodeId) => {
66
- const downstreamNodes = downstreamMap.get(nodeId);
67
- if (downstreamNodes) {
68
- downstreamNodes.forEach((downstream) => {
69
- if (!depths.has(downstream)) {
70
- depths.set(downstream, depth);
71
- nextLevel.add(downstream);
72
- }
73
- });
74
- }
75
- });
76
- currentLevel = nextLevel;
77
- depth++;
78
- }
79
-
80
- return depths;
81
- }
82
-
83
- /**
84
- * Layout the graph using dagre (hierarchical DAG layout)
85
- */
86
- export function layoutGraph(
87
- nodes: Node<SpecNodeData>[],
88
- edges: Edge[],
89
- isCompact: boolean,
90
- showStandalone: boolean,
91
- options: {
92
- mode?: 'graph' | 'focus';
93
- focusedNodeId?: string | null;
94
- upstreamIds?: Set<string>;
95
- downstreamIds?: Set<string>;
96
- } = {}
97
- ): { nodes: Node<SpecNodeData>[]; edges: Edge[] } {
98
- if (nodes.length === 0) return { nodes: [], edges: [] };
99
-
100
- const mode = options.mode ?? 'graph';
101
-
102
- if (mode === 'focus' && options.focusedNodeId) {
103
- return layeredLayout(nodes, edges, isCompact);
104
- }
105
-
106
- const width = isCompact ? COMPACT_NODE_WIDTH : NODE_WIDTH;
107
- const height = isCompact ? COMPACT_NODE_HEIGHT : NODE_HEIGHT;
108
- const gap = isCompact ? 30 : 50;
109
-
110
- // Separate nodes with dependencies from standalone nodes
111
- const nodesWithDeps = new Set<string>();
112
- edges.forEach((e) => {
113
- nodesWithDeps.add(e.source);
114
- nodesWithDeps.add(e.target);
115
- });
116
-
117
- const connectedNodes = nodes.filter((n) => nodesWithDeps.has(n.id));
118
- const standaloneNodes = showStandalone ? nodes.filter((n) => !nodesWithDeps.has(n.id)) : [];
119
-
120
- const allLayoutedNodes: Node<SpecNodeData>[] = [];
121
-
122
- // DAG view: Layout connected nodes with dagre (left-to-right for dependency flow)
123
- if (connectedNodes.length > 0) {
124
- const graph = new dagre.graphlib.Graph();
125
- graph.setGraph({
126
- rankdir: 'LR',
127
- align: 'UL',
128
- nodesep: isCompact ? 30 : 50,
129
- ranksep: isCompact ? 80 : 120,
130
- marginx: 40,
131
- marginy: 40,
132
- });
133
- graph.setDefaultEdgeLabel(() => ({}));
134
-
135
- connectedNodes.forEach((node) => {
136
- graph.setNode(node.id, { width, height });
137
- });
138
- edges.forEach((edge) => {
139
- if (nodesWithDeps.has(edge.source) && nodesWithDeps.has(edge.target)) {
140
- graph.setEdge(edge.source, edge.target);
141
- }
142
- });
143
-
144
- dagre.layout(graph);
145
-
146
- // Find bounds for centering
147
- let minX = Infinity, minY = Infinity, maxX = 0, maxY = 0;
148
- connectedNodes.forEach((node) => {
149
- const pos = graph.node(node.id);
150
- minX = Math.min(minX, pos.x - width / 2);
151
- minY = Math.min(minY, pos.y - height / 2);
152
- maxX = Math.max(maxX, pos.x + width / 2);
153
- maxY = Math.max(maxY, pos.y + height / 2);
154
- });
155
-
156
- connectedNodes.forEach((node) => {
157
- const pos = graph.node(node.id);
158
- allLayoutedNodes.push({
159
- ...node,
160
- position: {
161
- x: pos.x - minX,
162
- y: pos.y - minY,
163
- },
164
- });
165
- });
166
-
167
- // Layout standalone nodes in a grid below the graph
168
- if (standaloneNodes.length > 0) {
169
- const graphHeight = maxY - minY;
170
- const graphWidth = maxX - minX;
171
- const gridStartY = graphHeight + gap * 2;
172
- const cols = Math.ceil(Math.sqrt(standaloneNodes.length * 1.5));
173
- const gridWidth = cols * (width + gap);
174
- const gridStartX = graphWidth > gridWidth ? Math.floor((graphWidth - gridWidth) / 2) : 0;
175
-
176
- standaloneNodes.forEach((node, i) => {
177
- const col = i % cols;
178
- const row = Math.floor(i / cols);
179
- allLayoutedNodes.push({
180
- ...node,
181
- position: {
182
- x: gridStartX + col * (width + gap),
183
- y: gridStartY + row * (height + gap),
184
- },
185
- });
186
- });
187
- }
188
- } else {
189
- // Only standalone nodes - arrange in a grid
190
- const cols = Math.ceil(Math.sqrt(standaloneNodes.length * 1.5));
191
-
192
- standaloneNodes.forEach((node, i) => {
193
- const col = i % cols;
194
- const row = Math.floor(i / cols);
195
- allLayoutedNodes.push({
196
- ...node,
197
- position: {
198
- x: col * (width + gap),
199
- y: row * (height + gap),
200
- },
201
- });
202
- });
203
- }
204
-
205
- return { nodes: allLayoutedNodes, edges };
206
- }
207
-
208
- function layeredLayout(
209
- nodes: Node<SpecNodeData>[],
210
- edges: Edge[],
211
- isCompact: boolean,
212
- ): { nodes: Node<SpecNodeData>[]; edges: Edge[] } {
213
- // Use dagre for consistent hierarchical layout
214
- // This preserves the structure of complex dependency chains (A->B->C)
215
- // instead of flattening them into just "upstream" and "downstream" buckets
216
-
217
- const width = isCompact ? COMPACT_NODE_WIDTH : NODE_WIDTH;
218
- const height = isCompact ? COMPACT_NODE_HEIGHT : NODE_HEIGHT;
219
-
220
- const graph = new dagre.graphlib.Graph();
221
- graph.setGraph({
222
- rankdir: 'LR', // Consistent with main graph
223
- align: 'UL',
224
- nodesep: isCompact ? 30 : 50,
225
- ranksep: isCompact ? 80 : 120,
226
- marginx: 40,
227
- marginy: 40,
228
- });
229
- graph.setDefaultEdgeLabel(() => ({}));
230
-
231
- nodes.forEach((node) => {
232
- graph.setNode(node.id, { width, height });
233
- });
234
-
235
- edges.forEach((edge) => {
236
- graph.setEdge(edge.source, edge.target);
237
- });
238
-
239
- dagre.layout(graph);
240
-
241
- // Find bounds to normalize coordinates (start at 0,0)
242
- let minX = Infinity, minY = Infinity;
243
- nodes.forEach((node) => {
244
- const pos = graph.node(node.id);
245
- minX = Math.min(minX, pos.x - width / 2);
246
- minY = Math.min(minY, pos.y - height / 2);
247
- });
248
-
249
- const layoutedNodes = nodes.map((node) => {
250
- const pos = graph.node(node.id);
251
- return {
252
- ...node,
253
- position: {
254
- x: pos.x - minX,
255
- y: pos.y - minY,
256
- },
257
- };
258
- });
259
-
260
- return { nodes: layoutedNodes, edges };
261
- }
@@ -1,89 +0,0 @@
1
- import { useState } from 'react';
2
- import { AlertCircle, ArrowDown, ArrowUp, Loader2, Minus } from 'lucide-react';
3
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@leanspec/ui-components';
4
- import { cn } from '../../lib/utils';
5
- import { api } from '../../lib/api';
6
- import type { Spec } from '../../types/api';
7
- import { useTranslation } from 'react-i18next';
8
-
9
- const PRIORITY_OPTIONS: Array<{ value: NonNullable<Spec['priority']>; labelKey: `priority.${string}`; className: string; Icon: React.ComponentType<{ className?: string }> }> = [
10
- { value: 'critical', labelKey: 'priority.critical', className: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400', Icon: AlertCircle },
11
- { value: 'high', labelKey: 'priority.high', className: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400', Icon: ArrowUp },
12
- { value: 'medium', labelKey: 'priority.medium', className: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400', Icon: Minus },
13
- { value: 'low', labelKey: 'priority.low', className: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-400', Icon: ArrowDown },
14
- ];
15
-
16
- interface PriorityEditorProps {
17
- specName: string;
18
- value: Spec['priority'];
19
- onChange?: (priority: NonNullable<Spec['priority']>) => void;
20
- disabled?: boolean;
21
- className?: string;
22
- }
23
-
24
- export function PriorityEditor({ specName, value, onChange, disabled = false, className }: PriorityEditorProps) {
25
- const initial = value || 'medium';
26
- const [priority, setPriority] = useState<NonNullable<Spec['priority']>>(initial as NonNullable<Spec['priority']>);
27
- const [updating, setUpdating] = useState(false);
28
- const [error, setError] = useState<string | null>(null);
29
- const { t } = useTranslation('common');
30
-
31
- const option = PRIORITY_OPTIONS.find((opt) => opt.value === priority) || PRIORITY_OPTIONS[1];
32
-
33
- const handleChange = async (next: NonNullable<Spec['priority']>) => {
34
- if (next === priority) return;
35
- const previous = priority;
36
- setPriority(next);
37
- setUpdating(true);
38
- setError(null);
39
-
40
- try {
41
- await api.updateSpec(specName, { priority: next });
42
- onChange?.(next);
43
- } catch (err) {
44
- setPriority(previous);
45
- const message = err instanceof Error ? err.message : t('editors.priorityError');
46
- setError(message);
47
- } finally {
48
- setUpdating(false);
49
- }
50
- };
51
-
52
- return (
53
- <div className="space-y-1">
54
- <Select
55
- value={priority}
56
- onValueChange={(value) => handleChange(value as NonNullable<Spec['priority']>)}
57
- disabled={disabled || updating}
58
- >
59
- <SelectTrigger
60
- className={cn(
61
- 'h-7 w-fit min-w-[100px] border-0 px-2 text-xs font-medium justify-start',
62
- option.className,
63
- className,
64
- updating && 'opacity-70'
65
- )}
66
- aria-label={t('editors.changePriority')}
67
- >
68
- <div className="flex items-center gap-1.5">
69
- {updating ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <option.Icon className="h-3.5 w-3.5" />}
70
- <SelectValue placeholder={t('specsPage.filters.priority')}>
71
- {t(option.labelKey)}
72
- </SelectValue>
73
- </div>
74
- </SelectTrigger>
75
- <SelectContent>
76
- {PRIORITY_OPTIONS.map((opt) => (
77
- <SelectItem key={opt.value} value={opt.value} className="flex items-center gap-2">
78
- <div className="flex items-center gap-2">
79
- <opt.Icon className="h-4 w-4" />
80
- <span>{t(opt.labelKey)}</span>
81
- </div>
82
- </SelectItem>
83
- ))}
84
- </SelectContent>
85
- </Select>
86
- {error && <p className="text-xs text-destructive">{error}</p>}
87
- </div>
88
- );
89
- }