archrip 0.2.2 → 0.2.3

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.
@@ -129,9 +129,9 @@ After writing the file:
129
129
  - UseCase: `id`, `name`, `nodeIds` — all required
130
130
 
131
131
  ### Node Rules
132
- - `id`: kebab-case, prefixed by category abbreviation (ctrl-, svc-, port-, adpt-, model-, ext-, job-, dto-)
132
+ - `id`: kebab-case, prefixed by category abbreviation (ctrl-, svc-, port-, adpt-, model-, db-, ext-, job-, dto-)
133
133
  - `layer`: non-negative integer. **Higher = closer to domain core / more stable. Lower = closer to external world / more volatile.** Dagre (TB) places higher layers lower on screen; concentric places them at center. Use as many layers as the architecture requires (typically 3-6). Example for DDD: 0=external, 1=adapters, 2=controllers, 3=app services, 4=ports, 5=domain entities. Example for MVC: 0=external, 1=controllers, 2=services, 3=models.
134
- - `category`: one of controller, service, port, adapter, model, external, job, dto (or custom)
134
+ - `category`: one of controller, service, port, adapter, model, database, external, job, dto (or custom). Use `model` for domain entities/value objects (core business logic). Use `database` for DB tables, migrations, ORMs, and infrastructure persistence.
135
135
  - `label`: display name for the node
136
136
  - `filePath`: relative from project root
137
137
  - `depth` (optional): 0=overview, 1=structure, 2=detail. Auto-inferred from `layer` if omitted: with 3+ unique layers, lowest → 0, middle → 1, highest → 2. With 1-2 layers, all nodes get depth 0 (always visible).
@@ -129,9 +129,9 @@ After writing the file:
129
129
  - UseCase: `id`, `name`, `nodeIds` — all required
130
130
 
131
131
  ### Node Rules
132
- - `id`: kebab-case, prefixed by category abbreviation (ctrl-, svc-, port-, adpt-, model-, ext-, job-, dto-)
132
+ - `id`: kebab-case, prefixed by category abbreviation (ctrl-, svc-, port-, adpt-, model-, db-, ext-, job-, dto-)
133
133
  - `layer`: non-negative integer. **Higher = closer to domain core / more stable. Lower = closer to external world / more volatile.** Dagre (TB) places higher layers lower on screen; concentric places them at center. Use as many layers as the architecture requires (typically 3-6). Example for DDD: 0=external, 1=adapters, 2=controllers, 3=app services, 4=ports, 5=domain entities. Example for MVC: 0=external, 1=controllers, 2=services, 3=models.
134
- - `category`: one of controller, service, port, adapter, model, external, job, dto (or custom)
134
+ - `category`: one of controller, service, port, adapter, model, database, external, job, dto (or custom). Use `model` for domain entities/value objects (core business logic). Use `database` for DB tables, migrations, ORMs, and infrastructure persistence.
135
135
  - `label`: display name for the node
136
136
  - `filePath`: relative from project root
137
137
  - `depth` (optional): 0=overview, 1=structure, 2=detail. Auto-inferred from `layer` if omitted: with 3+ unique layers, lowest → 0, middle → 1, highest → 2. With 1-2 layers, all nodes get depth 0 (always visible).
@@ -129,9 +129,9 @@ After writing the file:
129
129
  - UseCase: `id`, `name`, `nodeIds` — all required
130
130
 
131
131
  ### Node Rules
132
- - `id`: kebab-case, prefixed by category abbreviation (ctrl-, svc-, port-, adpt-, model-, ext-, job-, dto-)
132
+ - `id`: kebab-case, prefixed by category abbreviation (ctrl-, svc-, port-, adpt-, model-, db-, ext-, job-, dto-)
133
133
  - `layer`: non-negative integer. **Higher = closer to domain core / more stable. Lower = closer to external world / more volatile.** Dagre (TB) places higher layers lower on screen; concentric places them at center. Use as many layers as the architecture requires (typically 3-6). Example for DDD: 0=external, 1=adapters, 2=controllers, 3=app services, 4=ports, 5=domain entities. Example for MVC: 0=external, 1=controllers, 2=services, 3=models.
134
- - `category`: one of controller, service, port, adapter, model, external, job, dto (or custom)
134
+ - `category`: one of controller, service, port, adapter, model, database, external, job, dto (or custom). Use `model` for domain entities/value objects (core business logic). Use `database` for DB tables, migrations, ORMs, and infrastructure persistence.
135
135
  - `label`: display name for the node
136
136
  - `filePath`: relative from project root
137
137
  - `depth` (optional): 0=overview, 1=structure, 2=detail. Auto-inferred from `layer` if omitted: with 3+ unique layers, lowest → 0, middle → 1, highest → 2. With 1-2 layers, all nodes get depth 0 (always visible).
@@ -105,8 +105,9 @@ const CATEGORY_RING_PRIORITY = {
105
105
  dto: 3,
106
106
  controller: 4,
107
107
  adapter: 5,
108
- job: 6,
109
- external: 7,
108
+ database: 6,
109
+ job: 7,
110
+ external: 8,
110
111
  };
111
112
  const DEFAULT_RING_PRIORITY = 4; // unknown categories treated as controller-level
112
113
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/utils/layout.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/utils/layout.ts"],"names":[],"mappings":"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"}
@@ -16,13 +16,17 @@ import type { ArchFlowNode, ArchNodeData } from './types.ts';
16
16
  import { getCategoryColors } from './types.ts';
17
17
  import { ArchNode } from './components/nodes/ArchNode.tsx';
18
18
  import { GroupNode } from './components/nodes/GroupNode.tsx';
19
+ import { CommandPalette } from './components/CommandPalette.tsx';
19
20
  import { DetailPanel } from './components/DetailPanel.tsx';
20
21
  import { UseCaseFilter } from './components/UseCaseFilter.tsx';
21
22
  import { DepthFilter } from './components/DepthFilter.tsx';
22
23
  import { Legend } from './components/Legend.tsx';
23
24
  import { ThemeToggle } from './components/ThemeToggle.tsx';
24
25
  import { useArchitecture } from './hooks/useArchitecture.ts';
26
+ import { useCategoryFilter } from './hooks/useCategoryFilter.ts';
27
+ import { useCommandPalette } from './hooks/useCommandPalette.ts';
25
28
  import { useDepthFilter } from './hooks/useDepthFilter.ts';
29
+ import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts.ts';
26
30
  import { useUseCaseFilter } from './hooks/useUseCaseFilter.ts';
27
31
  import { useTheme } from './hooks/useTheme.ts';
28
32
 
@@ -31,10 +35,11 @@ const nodeTypes = { archNode: ArchNode, groupNode: GroupNode };
31
35
  function AppInner() {
32
36
  const { nodes, edges, useCases, projectName, layoutType, loading, error, onNodesChange, onEdgesChange } = useArchitecture();
33
37
  const { depthLevel, setDepthLevel, visibleNodes, visibleEdges } = useDepthFilter(nodes, edges, layoutType);
34
- const { selectedUseCase, setSelectedUseCase, categories, filteredNodes, filteredEdges } = useUseCaseFilter(visibleNodes, visibleEdges, useCases);
38
+ const { selectedUseCase, setSelectedUseCase, categories, filteredNodes, filteredEdges, flowInfo } = useUseCaseFilter(visibleNodes, visibleEdges, useCases);
39
+ const categoryFilter = useCategoryFilter();
35
40
  const [selectedNodeId, setSelectedNodeId] = useQueryState('node', parseAsString.withOptions({ history: 'replace' }));
36
41
  const { theme, toggleTheme } = useTheme();
37
- const { fitView } = useReactFlow();
42
+ const { fitView, setCenter, getNodes } = useReactFlow();
38
43
 
39
44
  const selectedNodeData: ArchNodeData | null = useMemo(() => {
40
45
  if (!selectedNodeId) return null;
@@ -66,6 +71,70 @@ function AppInner() {
66
71
  return () => cancelAnimationFrame(id);
67
72
  }, [depthLevel, selectedUseCase, fitView]);
68
73
 
74
+ // Auto-center viewport on active flow step
75
+ useEffect(() => {
76
+ if (!flowInfo.flowNodeIds || flowInfo.activeStep < 0) return;
77
+ const activeNodeId = flowInfo.flowNodeIds[flowInfo.activeStep];
78
+ if (!activeNodeId) return;
79
+ const rfNodes = getNodes();
80
+ const target = rfNodes.find((n) => n.id === activeNodeId);
81
+ if (!target) return;
82
+ const x = target.position.x + (target.measured?.width ?? 180) / 2;
83
+ const y = target.position.y + (target.measured?.height ?? 80) / 2;
84
+ void setCenter(x, y, { zoom: 1.2, duration: 400 });
85
+ }, [flowInfo.activeStep, flowInfo.flowNodeIds, getNodes, setCenter]);
86
+
87
+ // Category filter: dim hidden categories
88
+ const displayNodes = useMemo(() => {
89
+ if (categoryFilter.hiddenCategories.size === 0) return filteredNodes;
90
+ return filteredNodes.map((node) => {
91
+ if (categoryFilter.hiddenCategories.has(node.data.category)) {
92
+ return { ...node, style: { ...node.style, opacity: 0.08, transition: 'opacity 0.3s' } };
93
+ }
94
+ return node;
95
+ });
96
+ }, [filteredNodes, categoryFilter.hiddenCategories]);
97
+
98
+ const displayEdges = useMemo(() => {
99
+ if (categoryFilter.hiddenCategories.size === 0) return filteredEdges;
100
+ const hiddenNodeIds = new Set<string>();
101
+ for (const node of filteredNodes) {
102
+ if (categoryFilter.hiddenCategories.has(node.data.category)) {
103
+ hiddenNodeIds.add(node.id);
104
+ }
105
+ }
106
+ return filteredEdges.map((edge) => {
107
+ if (hiddenNodeIds.has(edge.source) || hiddenNodeIds.has(edge.target)) {
108
+ return {
109
+ ...edge,
110
+ style: { ...edge.style, opacity: 0.08, transition: 'opacity 0.3s' },
111
+ labelStyle: { ...((edge.labelStyle as Record<string, unknown>) ?? {}), opacity: 0 },
112
+ };
113
+ }
114
+ return edge;
115
+ });
116
+ }, [filteredEdges, filteredNodes, categoryFilter.hiddenCategories]);
117
+
118
+ // Command palette
119
+ const palette = useCommandPalette(displayNodes, categoryFilter.hiddenCategories);
120
+
121
+ const handlePaletteSelect = useCallback((nodeId: string) => {
122
+ palette.close();
123
+ void setSelectedNodeId(nodeId);
124
+ }, [palette, setSelectedNodeId]);
125
+
126
+ const handleEscape = useCallback(() => {
127
+ if (palette.isOpen) { palette.close(); return; }
128
+ if (selectedNodeId) { void setSelectedNodeId(null); return; }
129
+ if (selectedUseCase) { void setSelectedUseCase(null); return; }
130
+ }, [palette, selectedNodeId, setSelectedNodeId, selectedUseCase, setSelectedUseCase]);
131
+
132
+ useKeyboardShortcuts({
133
+ onTogglePalette: palette.toggle,
134
+ onEscape: handleEscape,
135
+ isPaletteOpen: palette.isOpen,
136
+ });
137
+
69
138
  if (loading) {
70
139
  return (
71
140
  <div className="w-full h-screen flex items-center justify-center" style={{ color: 'var(--color-content-tertiary)', background: 'var(--color-surface-canvas)' }}>
@@ -88,8 +157,8 @@ function AppInner() {
88
157
  return (
89
158
  <div className="w-full h-screen relative">
90
159
  <ReactFlow
91
- nodes={filteredNodes}
92
- edges={filteredEdges}
160
+ nodes={displayNodes}
161
+ edges={displayEdges}
93
162
  onNodesChange={onNodesChange}
94
163
  onEdgesChange={onEdgesChange}
95
164
  onNodeClick={onNodeClick}
@@ -120,11 +189,17 @@ function AppInner() {
120
189
  useCases={useCases}
121
190
  selectedUseCase={selectedUseCase}
122
191
  onSelect={handleUseCaseSelect}
192
+ flowInfo={flowInfo}
123
193
  />
124
194
  )}
125
195
  <DepthFilter depthLevel={depthLevel} onSelect={setDepthLevel} />
126
196
  </div>
127
- <Legend categories={categories} />
197
+ <Legend
198
+ categories={categories}
199
+ hiddenCategories={categoryFilter.hiddenCategories}
200
+ onToggleCategory={categoryFilter.toggleCategory}
201
+ onShowAll={categoryFilter.showAll}
202
+ />
128
203
 
129
204
  {/* Title + Theme Toggle */}
130
205
  <div className="absolute top-3 right-3 z-10 flex items-center gap-2">
@@ -157,6 +232,19 @@ function AppInner() {
157
232
  />
158
233
  </>
159
234
  )}
235
+
236
+ {palette.isOpen && (
237
+ <CommandPalette
238
+ query={palette.query}
239
+ onQueryChange={palette.setQuery}
240
+ results={palette.results}
241
+ activeIndex={palette.activeIndex}
242
+ onMoveUp={palette.moveUp}
243
+ onMoveDown={palette.moveDown}
244
+ onSelect={handlePaletteSelect}
245
+ onClose={palette.close}
246
+ />
247
+ )}
160
248
  </div>
161
249
  );
162
250
  }
@@ -0,0 +1,174 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ import { getCategoryColors, getCategoryLabel } from '../types.ts';
4
+ import type { CommandPaletteResult } from '../hooks/useCommandPalette.ts';
5
+
6
+ interface CommandPaletteProps {
7
+ query: string;
8
+ onQueryChange: (q: string) => void;
9
+ results: CommandPaletteResult[];
10
+ activeIndex: number;
11
+ onMoveUp: () => void;
12
+ onMoveDown: () => void;
13
+ onSelect: (id: string) => void;
14
+ onClose: () => void;
15
+ }
16
+
17
+ export function CommandPalette({
18
+ query,
19
+ onQueryChange,
20
+ results,
21
+ activeIndex,
22
+ onMoveUp,
23
+ onMoveDown,
24
+ onSelect,
25
+ onClose,
26
+ }: CommandPaletteProps) {
27
+ const inputRef = useRef<HTMLInputElement>(null);
28
+ const listRef = useRef<HTMLDivElement>(null);
29
+
30
+ useEffect(() => {
31
+ inputRef.current?.focus();
32
+ }, []);
33
+
34
+ // Scroll active item into view
35
+ useEffect(() => {
36
+ const list = listRef.current;
37
+ if (!list) return;
38
+ const item = list.children[activeIndex] as HTMLElement | undefined;
39
+ item?.scrollIntoView({ block: 'nearest' });
40
+ }, [activeIndex]);
41
+
42
+ function handleKeyDown(e: React.KeyboardEvent) {
43
+ if (e.key === 'ArrowUp') {
44
+ e.preventDefault();
45
+ onMoveUp();
46
+ } else if (e.key === 'ArrowDown') {
47
+ e.preventDefault();
48
+ onMoveDown();
49
+ } else if (e.key === 'Enter') {
50
+ e.preventDefault();
51
+ const item = results[activeIndex];
52
+ if (item) onSelect(item.id);
53
+ } else if (e.key === 'Escape') {
54
+ e.preventDefault();
55
+ onClose();
56
+ }
57
+ }
58
+
59
+ return (
60
+ <>
61
+ {/* Overlay */}
62
+ <div
63
+ className="fixed inset-0 z-60"
64
+ style={{ background: 'rgba(0, 0, 0, 0.3)' }}
65
+ onClick={onClose}
66
+ />
67
+
68
+ {/* Palette card */}
69
+ <div
70
+ className="fixed z-60 rounded-xl border overflow-hidden"
71
+ style={{
72
+ top: '20%',
73
+ left: '50%',
74
+ transform: 'translateX(-50%)',
75
+ width: 480,
76
+ maxWidth: 'calc(100vw - 32px)',
77
+ background: 'var(--color-surface-primary)',
78
+ borderColor: 'var(--color-border-primary)',
79
+ boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
80
+ }}
81
+ >
82
+ {/* Search input */}
83
+ <div
84
+ className="border-b"
85
+ style={{ borderColor: 'var(--color-border-primary)' }}
86
+ >
87
+ <input
88
+ ref={inputRef}
89
+ type="text"
90
+ value={query}
91
+ onChange={(e) => onQueryChange(e.target.value)}
92
+ onKeyDown={handleKeyDown}
93
+ placeholder="Search nodes..."
94
+ className="w-full px-4 py-3 text-sm bg-transparent outline-none"
95
+ style={{ color: 'var(--color-content-primary)' }}
96
+ />
97
+ </div>
98
+
99
+ {/* Results list */}
100
+ <div
101
+ ref={listRef}
102
+ className="overflow-y-auto"
103
+ style={{ maxHeight: 320 }}
104
+ >
105
+ {results.length === 0 && (
106
+ <div
107
+ className="px-4 py-6 text-center text-sm"
108
+ style={{ color: 'var(--color-content-tertiary)' }}
109
+ >
110
+ No nodes found
111
+ </div>
112
+ )}
113
+ {results.map((item, index) => {
114
+ const colors = getCategoryColors(item.category);
115
+ const isActive = index === activeIndex;
116
+ return (
117
+ <button
118
+ key={item.id}
119
+ onClick={() => onSelect(item.id)}
120
+ className="w-full px-4 py-2.5 flex items-start gap-3 text-left cursor-pointer border-none"
121
+ style={{
122
+ background: isActive ? 'var(--color-surface-tertiary)' : 'transparent',
123
+ }}
124
+ >
125
+ <div
126
+ className="w-3 h-3 rounded-sm border mt-0.5 shrink-0"
127
+ style={{ background: colors.bg, borderColor: colors.border }}
128
+ />
129
+ <div className="min-w-0 flex-1">
130
+ <div className="flex items-center gap-2">
131
+ <span
132
+ className="text-sm font-medium truncate"
133
+ style={{ color: 'var(--color-content-primary)' }}
134
+ >
135
+ {item.label}
136
+ </span>
137
+ <span
138
+ className="text-xs shrink-0"
139
+ style={{ color: 'var(--color-content-tertiary)' }}
140
+ >
141
+ {getCategoryLabel(item.category)}
142
+ </span>
143
+ </div>
144
+ {item.description && (
145
+ <p
146
+ className="text-xs mt-0.5 truncate"
147
+ style={{ color: 'var(--color-content-secondary)' }}
148
+ >
149
+ {item.description}
150
+ </p>
151
+ )}
152
+ </div>
153
+ </button>
154
+ );
155
+ })}
156
+ </div>
157
+
158
+ {/* Footer */}
159
+ <div
160
+ className="px-4 py-2 border-t flex items-center gap-4 text-xs"
161
+ style={{
162
+ borderColor: 'var(--color-border-primary)',
163
+ color: 'var(--color-content-tertiary)',
164
+ background: 'var(--color-surface-secondary)',
165
+ }}
166
+ >
167
+ <span><kbd className="font-mono">{'\u2191\u2193'}</kbd> Navigate</span>
168
+ <span><kbd className="font-mono">{'\u21B5'}</kbd> Select</span>
169
+ <span><kbd className="font-mono">Esc</kbd> Close</span>
170
+ </div>
171
+ </div>
172
+ </>
173
+ );
174
+ }
@@ -2,9 +2,12 @@ import { getCategoryColors, getCategoryLabel } from '../types.ts';
2
2
 
3
3
  interface LegendProps {
4
4
  categories: string[];
5
+ hiddenCategories: Set<string>;
6
+ onToggleCategory: (category: string) => void;
7
+ onShowAll: () => void;
5
8
  }
6
9
 
7
- export function Legend({ categories }: LegendProps) {
10
+ export function Legend({ categories, hiddenCategories, onToggleCategory, onShowAll }: LegendProps) {
8
11
  return (
9
12
  <div
10
13
  className="absolute bottom-3 right-3 z-10 rounded-lg p-3 border"
@@ -23,19 +26,46 @@ export function Legend({ categories }: LegendProps) {
23
26
  <div className="grid grid-cols-2 gap-x-4 gap-y-1">
24
27
  {categories.map((cat) => {
25
28
  const colors = getCategoryColors(cat);
29
+ const hidden = hiddenCategories.has(cat);
26
30
  return (
27
- <div key={cat} className="flex items-center gap-1.5">
31
+ <button
32
+ key={cat}
33
+ onClick={() => onToggleCategory(cat)}
34
+ className="flex items-center gap-1.5 cursor-pointer bg-transparent border-none p-0 text-left"
35
+ >
28
36
  <div
29
37
  className="w-3 h-3 rounded-sm border"
30
- style={{ background: colors.bg, borderColor: colors.border }}
38
+ style={{
39
+ background: colors.bg,
40
+ borderColor: colors.border,
41
+ opacity: hidden ? 0.3 : 1,
42
+ transition: 'opacity 0.2s',
43
+ }}
31
44
  />
32
- <span className="text-xs" style={{ color: 'var(--color-content-secondary)' }}>
45
+ <span
46
+ className="text-xs"
47
+ style={{
48
+ color: 'var(--color-content-secondary)',
49
+ opacity: hidden ? 0.4 : 1,
50
+ textDecoration: hidden ? 'line-through' : 'none',
51
+ transition: 'opacity 0.2s',
52
+ }}
53
+ >
33
54
  {getCategoryLabel(cat)}
34
55
  </span>
35
- </div>
56
+ </button>
36
57
  );
37
58
  })}
38
59
  </div>
60
+ {hiddenCategories.size > 0 && (
61
+ <button
62
+ onClick={onShowAll}
63
+ className="mt-2 text-xs cursor-pointer hover:underline bg-transparent border-none p-0"
64
+ style={{ color: 'var(--color-interactive-primary)' }}
65
+ >
66
+ Show All
67
+ </button>
68
+ )}
39
69
  </div>
40
70
  );
41
71
  }
@@ -1,12 +1,53 @@
1
1
  import type { UseCase } from '../types.ts';
2
+ import type { FlowAnimationState } from '../hooks/useFlowAnimation.ts';
2
3
 
3
4
  interface UseCaseFilterProps {
4
5
  useCases: UseCase[];
5
6
  selectedUseCase: string | null;
6
7
  onSelect: (useCaseId: string | null) => void;
8
+ flowInfo: FlowAnimationState;
7
9
  }
8
10
 
9
- export function UseCaseFilter({ useCases, selectedUseCase, onSelect }: UseCaseFilterProps) {
11
+ function FlowButton({ label, onClick }: { label: string; onClick: () => void }) {
12
+ return (
13
+ <button
14
+ onClick={onClick}
15
+ className="w-7 h-7 flex items-center justify-center rounded border text-xs cursor-pointer"
16
+ style={{
17
+ background: 'var(--color-surface-secondary)',
18
+ borderColor: 'var(--color-border-primary)',
19
+ color: 'var(--color-content-secondary)',
20
+ }}
21
+ aria-label={label}
22
+ >
23
+ {label === 'Previous' && (
24
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
25
+ <path d="M8 1.5L3.5 6L8 10.5" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
26
+ </svg>
27
+ )}
28
+ {label === 'Play' && (
29
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
30
+ <path d="M3 1.5L10 6L3 10.5V1.5Z" />
31
+ </svg>
32
+ )}
33
+ {label === 'Pause' && (
34
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
35
+ <rect x="2.5" y="1.5" width="2.5" height="9" rx="0.5" />
36
+ <rect x="7" y="1.5" width="2.5" height="9" rx="0.5" />
37
+ </svg>
38
+ )}
39
+ {label === 'Next' && (
40
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
41
+ <path d="M4 1.5L8.5 6L4 10.5" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
42
+ </svg>
43
+ )}
44
+ </button>
45
+ );
46
+ }
47
+
48
+ export function UseCaseFilter({ useCases, selectedUseCase, onSelect, flowInfo }: UseCaseFilterProps) {
49
+ const selectedUc = selectedUseCase ? useCases.find((uc) => uc.id === selectedUseCase) : undefined;
50
+
10
51
  return (
11
52
  <div
12
53
  className="rounded-lg p-3 w-64 border"
@@ -40,11 +81,29 @@ export function UseCaseFilter({ useCases, selectedUseCase, onSelect }: UseCaseFi
40
81
  </option>
41
82
  ))}
42
83
  </select>
43
- {selectedUseCase && (
84
+ {selectedUc && (
44
85
  <div className="mt-2">
45
86
  <p className="text-xs" style={{ color: 'var(--color-content-secondary)' }}>
46
- {useCases.find((uc) => uc.id === selectedUseCase)?.description}
87
+ {selectedUc.description}
47
88
  </p>
89
+ {flowInfo.flowNodeIds && (
90
+ <div className="mt-1.5">
91
+ <p
92
+ className="text-xs font-mono mb-1.5"
93
+ style={{ color: 'var(--color-interactive-primary)' }}
94
+ >
95
+ Step {flowInfo.activeStep + 1}/{flowInfo.flowNodeIds.length}
96
+ </p>
97
+ <div className="flex items-center gap-1">
98
+ <FlowButton label="Previous" onClick={flowInfo.prev} />
99
+ <FlowButton
100
+ label={flowInfo.isPlaying ? 'Pause' : 'Play'}
101
+ onClick={flowInfo.isPlaying ? flowInfo.pause : flowInfo.play}
102
+ />
103
+ <FlowButton label="Next" onClick={flowInfo.next} />
104
+ </div>
105
+ </div>
106
+ )}
48
107
  <button
49
108
  onClick={() => onSelect(null)}
50
109
  className="mt-1.5 text-xs cursor-pointer hover:underline"
@@ -0,0 +1,36 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import { useQueryState, parseAsString } from 'nuqs';
3
+
4
+ interface CategoryFilterState {
5
+ hiddenCategories: Set<string>;
6
+ toggleCategory: (category: string) => void;
7
+ showAll: () => void;
8
+ }
9
+
10
+ export function useCategoryFilter(): CategoryFilterState {
11
+ const [hideParam, setHideParam] = useQueryState('hide', parseAsString.withOptions({ history: 'replace' }));
12
+
13
+ const hiddenCategories = useMemo(() => {
14
+ if (!hideParam) return new Set<string>();
15
+ return new Set(hideParam.split(',').filter(Boolean));
16
+ }, [hideParam]);
17
+
18
+ const toggleCategory = useCallback(
19
+ (category: string) => {
20
+ const next = new Set(hiddenCategories);
21
+ if (next.has(category)) {
22
+ next.delete(category);
23
+ } else {
24
+ next.add(category);
25
+ }
26
+ void setHideParam(next.size > 0 ? [...next].join(',') : null);
27
+ },
28
+ [hiddenCategories, setHideParam],
29
+ );
30
+
31
+ const showAll = useCallback(() => {
32
+ void setHideParam(null);
33
+ }, [setHideParam]);
34
+
35
+ return { hiddenCategories, toggleCategory, showAll };
36
+ }
@@ -0,0 +1,91 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+
3
+ import type { ArchFlowNode } from '../types.ts';
4
+
5
+ export interface CommandPaletteResult {
6
+ id: string;
7
+ label: string;
8
+ category: string;
9
+ description: string;
10
+ }
11
+
12
+ interface CommandPaletteState {
13
+ isOpen: boolean;
14
+ toggle: () => void;
15
+ close: () => void;
16
+ query: string;
17
+ setQuery: (q: string) => void;
18
+ results: CommandPaletteResult[];
19
+ activeIndex: number;
20
+ moveUp: () => void;
21
+ moveDown: () => void;
22
+ activeItem: CommandPaletteResult | null;
23
+ }
24
+
25
+ const MAX_RESULTS = 20;
26
+
27
+ export function useCommandPalette(
28
+ nodes: ArchFlowNode[],
29
+ hiddenCategories: Set<string>,
30
+ ): CommandPaletteState {
31
+ const [isOpen, setIsOpen] = useState(false);
32
+ const [query, setQueryRaw] = useState('');
33
+ const [activeIndex, setActiveIndex] = useState(0);
34
+
35
+ const toggle = useCallback(() => {
36
+ setIsOpen((prev) => {
37
+ if (!prev) {
38
+ setQueryRaw('');
39
+ setActiveIndex(0);
40
+ }
41
+ return !prev;
42
+ });
43
+ }, []);
44
+
45
+ const close = useCallback(() => {
46
+ setIsOpen(false);
47
+ }, []);
48
+
49
+ const setQuery = useCallback((q: string) => {
50
+ setQueryRaw(q);
51
+ setActiveIndex(0);
52
+ }, []);
53
+
54
+ const results = useMemo(() => {
55
+ const searchable = nodes.filter(
56
+ (n) => !n.hidden && !hiddenCategories.has(n.data.category),
57
+ );
58
+
59
+ if (!query.trim()) {
60
+ return searchable.slice(0, MAX_RESULTS).map((n) => ({
61
+ id: n.id,
62
+ label: n.data.label,
63
+ category: n.data.category,
64
+ description: n.data.description,
65
+ }));
66
+ }
67
+
68
+ const q = query.toLowerCase();
69
+ return searchable
70
+ .filter((n) => n.data.label.toLowerCase().includes(q))
71
+ .slice(0, MAX_RESULTS)
72
+ .map((n) => ({
73
+ id: n.id,
74
+ label: n.data.label,
75
+ category: n.data.category,
76
+ description: n.data.description,
77
+ }));
78
+ }, [nodes, hiddenCategories, query]);
79
+
80
+ const moveUp = useCallback(() => {
81
+ setActiveIndex((prev) => (prev <= 0 ? prev : prev - 1));
82
+ }, []);
83
+
84
+ const moveDown = useCallback(() => {
85
+ setActiveIndex((prev) => (prev >= results.length - 1 ? prev : prev + 1));
86
+ }, [results.length]);
87
+
88
+ const activeItem = results[activeIndex] ?? null;
89
+
90
+ return { isOpen, toggle, close, query, setQuery, results, activeIndex, moveUp, moveDown, activeItem };
91
+ }
@@ -0,0 +1,82 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ import type { UseCase } from '../types.ts';
4
+
5
+ export interface FlowAnimationState {
6
+ activeStep: number;
7
+ flowNodeIds: string[] | null;
8
+ isPlaying: boolean;
9
+ play: () => void;
10
+ pause: () => void;
11
+ next: () => void;
12
+ prev: () => void;
13
+ }
14
+
15
+ const NOOP = () => {};
16
+
17
+ const IDLE_STATE: FlowAnimationState = {
18
+ activeStep: -1,
19
+ flowNodeIds: null,
20
+ isPlaying: false,
21
+ play: NOOP,
22
+ pause: NOOP,
23
+ next: NOOP,
24
+ prev: NOOP,
25
+ };
26
+
27
+ export function useFlowAnimation(
28
+ useCases: UseCase[],
29
+ selectedUseCase: string | null,
30
+ ): FlowAnimationState {
31
+ const [activeStep, setActiveStep] = useState(0);
32
+ const [isPlaying, setIsPlaying] = useState(true);
33
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
34
+
35
+ const uc = selectedUseCase ? useCases.find((u) => u.id === selectedUseCase) : undefined;
36
+ const flow = uc?.flow;
37
+ const flowLength = flow?.length ?? 0;
38
+
39
+ // Reset on use case change
40
+ useEffect(() => {
41
+ setActiveStep(0);
42
+ setIsPlaying(true);
43
+ }, [selectedUseCase]);
44
+
45
+ // Auto-advance timer
46
+ useEffect(() => {
47
+ if (!flow || flowLength < 2 || !isPlaying) return;
48
+
49
+ timerRef.current = setTimeout(() => {
50
+ setActiveStep((prev) => {
51
+ const next = prev + 1;
52
+ return next >= flowLength ? 0 : next;
53
+ });
54
+ }, 800);
55
+
56
+ return () => {
57
+ if (timerRef.current !== null) {
58
+ clearTimeout(timerRef.current);
59
+ timerRef.current = null;
60
+ }
61
+ };
62
+ }, [flow, flowLength, isPlaying, activeStep]);
63
+
64
+ const play = useCallback(() => setIsPlaying(true), []);
65
+ const pause = useCallback(() => setIsPlaying(false), []);
66
+
67
+ const next = useCallback(() => {
68
+ setIsPlaying(false);
69
+ setActiveStep((prev) => (prev + 1 >= flowLength ? prev : prev + 1));
70
+ }, [flowLength]);
71
+
72
+ const prev = useCallback(() => {
73
+ setIsPlaying(false);
74
+ setActiveStep((prev) => (prev <= 0 ? 0 : prev - 1));
75
+ }, []);
76
+
77
+ if (!flow || flowLength < 2) {
78
+ return IDLE_STATE;
79
+ }
80
+
81
+ return { activeStep, flowNodeIds: flow, isPlaying, play, pause, next, prev };
82
+ }
@@ -0,0 +1,39 @@
1
+ import { useEffect } from 'react';
2
+
3
+ interface KeyboardShortcutActions {
4
+ onTogglePalette: () => void;
5
+ onEscape: () => void;
6
+ isPaletteOpen: boolean;
7
+ }
8
+
9
+ export function useKeyboardShortcuts(actions: KeyboardShortcutActions): void {
10
+ useEffect(() => {
11
+ function handleKeyDown(e: KeyboardEvent) {
12
+ // Skip when focused on form elements
13
+ const tag = (e.target as HTMLElement).tagName;
14
+ if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') {
15
+ // Allow Escape even in input
16
+ if (e.key === 'Escape') {
17
+ actions.onEscape();
18
+ return;
19
+ }
20
+ return;
21
+ }
22
+
23
+ // Cmd/Ctrl+K: toggle palette
24
+ if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
25
+ e.preventDefault();
26
+ actions.onTogglePalette();
27
+ return;
28
+ }
29
+
30
+ if (e.key === 'Escape') {
31
+ actions.onEscape();
32
+ return;
33
+ }
34
+ }
35
+
36
+ window.addEventListener('keydown', handleKeyDown);
37
+ return () => window.removeEventListener('keydown', handleKeyDown);
38
+ }, [actions]);
39
+ }
@@ -3,6 +3,7 @@ import { useQueryState, parseAsString } from 'nuqs';
3
3
  import type { Edge } from '@xyflow/react';
4
4
 
5
5
  import type { ArchFlowNode, UseCase } from '../types.ts';
6
+ import { useFlowAnimation } from './useFlowAnimation.ts';
6
7
 
7
8
  function isNodeActive(node: ArchFlowNode, activeIds: Set<string>): boolean {
8
9
  if (node.data.isGroup && node.data.memberNodes) {
@@ -11,8 +12,20 @@ function isNodeActive(node: ArchFlowNode, activeIds: Set<string>): boolean {
11
12
  return activeIds.has(node.id);
12
13
  }
13
14
 
15
+ /** Resolve a node's effective ID for flow matching (group → first active member) */
16
+ function getFlowId(node: ArchFlowNode, flowSet: Set<string>): string | null {
17
+ if (flowSet.has(node.id)) return node.id;
18
+ if (node.data.isGroup && node.data.memberNodes) {
19
+ const member = node.data.memberNodes.find((m) => flowSet.has(m.id));
20
+ if (member) return member.id;
21
+ }
22
+ return null;
23
+ }
24
+
14
25
  export function useUseCaseFilter(nodes: ArchFlowNode[], edges: Edge[], useCases: UseCase[]) {
15
26
  const [selectedUseCase, setSelectedUseCase] = useQueryState('uc', parseAsString.withOptions({ history: 'replace' }));
27
+ const flowState = useFlowAnimation(useCases, selectedUseCase);
28
+ const { activeStep, flowNodeIds } = flowState;
16
29
 
17
30
  const categories = useMemo(() => {
18
31
  const set = new Set<string>();
@@ -27,19 +40,77 @@ export function useUseCaseFilter(nodes: ArchFlowNode[], edges: Edge[], useCases:
27
40
  const uc = useCases.find((u) => u.id === selectedUseCase);
28
41
  if (!uc) return nodes;
29
42
  const activeIds = new Set(uc.nodeIds);
43
+
44
+ // No flow — static highlight (backward compat)
45
+ if (!flowNodeIds) {
46
+ return nodes.map((node) => {
47
+ if (node.hidden) return node;
48
+ const active = isNodeActive(node, activeIds);
49
+ return {
50
+ ...node,
51
+ style: {
52
+ ...node.style,
53
+ opacity: active ? 1 : 0.15,
54
+ transition: 'opacity 0.3s',
55
+ },
56
+ };
57
+ });
58
+ }
59
+
60
+ // Flow animation mode
61
+ const flowSet = new Set(flowNodeIds);
62
+ const activeFlowId = flowNodeIds[activeStep];
63
+ const passedIds = new Set(flowNodeIds.slice(0, activeStep));
64
+
30
65
  return nodes.map((node) => {
31
66
  if (node.hidden) return node;
32
67
  const active = isNodeActive(node, activeIds);
68
+ if (!active) {
69
+ // Non-participating node
70
+ return {
71
+ ...node,
72
+ style: { ...node.style, opacity: 0.08, transition: 'opacity 0.3s' },
73
+ };
74
+ }
75
+
76
+ const flowId = getFlowId(node, flowSet);
77
+ if (!flowId) {
78
+ // In use case but not in flow — dim slightly
79
+ return {
80
+ ...node,
81
+ style: { ...node.style, opacity: 0.3, transition: 'opacity 0.3s' },
82
+ };
83
+ }
84
+
85
+ if (flowId === activeFlowId) {
86
+ // Active step — glow + scale
87
+ return {
88
+ ...node,
89
+ style: {
90
+ ...node.style,
91
+ opacity: 1,
92
+ boxShadow: 'var(--shadow-node-flow-active)',
93
+ transform: 'scale(1.02)',
94
+ transition: 'opacity 0.3s, box-shadow 0.3s, transform 0.3s',
95
+ },
96
+ };
97
+ }
98
+
99
+ if (passedIds.has(flowId)) {
100
+ // Already passed
101
+ return {
102
+ ...node,
103
+ style: { ...node.style, opacity: 1, transition: 'opacity 0.3s' },
104
+ };
105
+ }
106
+
107
+ // Not yet reached
33
108
  return {
34
109
  ...node,
35
- style: {
36
- ...node.style,
37
- opacity: active ? 1 : 0.15,
38
- transition: 'opacity 0.3s',
39
- },
110
+ style: { ...node.style, opacity: 0.3, transition: 'opacity 0.3s' },
40
111
  };
41
112
  });
42
- }, [nodes, selectedUseCase, useCases]);
113
+ }, [nodes, selectedUseCase, useCases, flowNodeIds, activeStep]);
43
114
 
44
115
  const filteredEdges = useMemo(() => {
45
116
  if (!selectedUseCase) return edges;
@@ -55,19 +126,93 @@ export function useUseCaseFilter(nodes: ArchFlowNode[], edges: Edge[], useCases:
55
126
  }
56
127
  }
57
128
 
58
- return edges.map((edge) => ({
59
- ...edge,
60
- style: {
61
- ...edge.style,
62
- opacity: activeNodeIds.has(edge.source) && activeNodeIds.has(edge.target) ? 1 : 0.08,
63
- transition: 'opacity 0.3s',
64
- },
65
- labelStyle: {
66
- ...((edge.labelStyle as Record<string, unknown>) ?? {}),
67
- opacity: activeNodeIds.has(edge.source) && activeNodeIds.has(edge.target) ? 1 : 0,
68
- },
69
- }));
70
- }, [edges, nodes, selectedUseCase, useCases]);
71
-
72
- return { selectedUseCase, setSelectedUseCase, categories, filteredNodes, filteredEdges };
129
+ // No flow — static highlight (backward compat)
130
+ if (!flowNodeIds) {
131
+ return edges.map((edge) => ({
132
+ ...edge,
133
+ style: {
134
+ ...edge.style,
135
+ opacity: activeNodeIds.has(edge.source) && activeNodeIds.has(edge.target) ? 1 : 0.08,
136
+ transition: 'opacity 0.3s',
137
+ },
138
+ labelStyle: {
139
+ ...((edge.labelStyle as Record<string, unknown>) ?? {}),
140
+ opacity: activeNodeIds.has(edge.source) && activeNodeIds.has(edge.target) ? 1 : 0,
141
+ },
142
+ }));
143
+ }
144
+
145
+ // Flow animation mode — build flow edge lookup
146
+ // A flow edge is one connecting flow[i] → flow[i+1]
147
+ const flowEdgeMap = new Map<string, 'active' | 'passed' | 'upcoming'>();
148
+ for (let i = 0; i < flowNodeIds.length - 1; i++) {
149
+ const src = flowNodeIds[i];
150
+ const tgt = flowNodeIds[i + 1];
151
+ // Check both edge ID conventions
152
+ const key1 = `${src}->${tgt}`;
153
+ const key2 = `${tgt}->${src}`;
154
+ let status: 'active' | 'passed' | 'upcoming';
155
+ if (i + 1 === activeStep) {
156
+ status = 'active';
157
+ } else if (i + 1 <= activeStep) {
158
+ status = 'passed';
159
+ } else {
160
+ status = 'upcoming';
161
+ }
162
+ flowEdgeMap.set(key1, status);
163
+ flowEdgeMap.set(key2, status);
164
+ }
165
+
166
+ return edges.map((edge) => {
167
+ const bothActive = activeNodeIds.has(edge.source) && activeNodeIds.has(edge.target);
168
+ if (!bothActive) {
169
+ return {
170
+ ...edge,
171
+ style: { ...edge.style, opacity: 0.08, transition: 'opacity 0.3s' },
172
+ className: '',
173
+ labelStyle: {
174
+ ...((edge.labelStyle as Record<string, unknown>) ?? {}),
175
+ opacity: 0,
176
+ },
177
+ };
178
+ }
179
+
180
+ const flowStatus = flowEdgeMap.get(edge.id);
181
+ if (flowStatus === 'active') {
182
+ return {
183
+ ...edge,
184
+ style: { ...edge.style, opacity: 1, stroke: undefined, strokeWidth: undefined },
185
+ className: 'flow-edge-active',
186
+ labelStyle: {
187
+ ...((edge.labelStyle as Record<string, unknown>) ?? {}),
188
+ opacity: 1,
189
+ },
190
+ };
191
+ }
192
+ if (flowStatus === 'passed') {
193
+ return {
194
+ ...edge,
195
+ style: { ...edge.style, opacity: 1, transition: 'opacity 0.3s' },
196
+ className: '',
197
+ labelStyle: {
198
+ ...((edge.labelStyle as Record<string, unknown>) ?? {}),
199
+ opacity: 1,
200
+ },
201
+ };
202
+ }
203
+
204
+ // In use case but not a flow edge, or upcoming flow edge
205
+ return {
206
+ ...edge,
207
+ style: { ...edge.style, opacity: 0.3, transition: 'opacity 0.3s' },
208
+ className: '',
209
+ labelStyle: {
210
+ ...((edge.labelStyle as Record<string, unknown>) ?? {}),
211
+ opacity: 0.3,
212
+ },
213
+ };
214
+ });
215
+ }, [edges, nodes, selectedUseCase, useCases, flowNodeIds, activeStep]);
216
+
217
+ return { selectedUseCase, setSelectedUseCase, categories, filteredNodes, filteredEdges, flowInfo: flowState };
73
218
  }
@@ -33,6 +33,7 @@
33
33
  --shadow-panel: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
34
34
  --shadow-node: 0 1px 3px rgba(0, 0, 0, 0.1);
35
35
  --shadow-node-selected: 0 0 0 2px rgba(37, 99, 235, 0.25);
36
+ --shadow-node-flow-active: 0 0 12px 2px rgba(59, 130, 246, 0.5);
36
37
 
37
38
  /* Category colors (light) */
38
39
  --cat-controller-bg: #dbeafe;
@@ -50,6 +51,9 @@
50
51
  --cat-model-bg: #fee2e2;
51
52
  --cat-model-border: #ef4444;
52
53
  --cat-model-text: #991b1b;
54
+ --cat-database-bg: #fef3c7;
55
+ --cat-database-border: #d97706;
56
+ --cat-database-text: #92400e;
53
57
  --cat-external-bg: #f3f4f6;
54
58
  --cat-external-border: #6b7280;
55
59
  --cat-external-text: #374151;
@@ -88,6 +92,7 @@
88
92
  --shadow-panel: 0 1px 3px 0 rgba(0, 0, 0, 0.4), 0 1px 2px -1px rgba(0, 0, 0, 0.4);
89
93
  --shadow-node: 0 1px 3px rgba(0, 0, 0, 0.3);
90
94
  --shadow-node-selected: 0 0 0 2px rgba(96, 165, 250, 0.3);
95
+ --shadow-node-flow-active: 0 0 12px 2px rgba(96, 165, 250, 0.4);
91
96
 
92
97
  /* Category colors (dark) */
93
98
  --cat-controller-bg: #1e293b;
@@ -105,6 +110,9 @@
105
110
  --cat-model-bg: #2b1111;
106
111
  --cat-model-border: #f87171;
107
112
  --cat-model-text: #fca5a5;
113
+ --cat-database-bg: #2b1f06;
114
+ --cat-database-border: #fbbf24;
115
+ --cat-database-text: #fde68a;
108
116
  --cat-external-bg: #1e2330;
109
117
  --cat-external-border: #9ca3af;
110
118
  --cat-external-text: #d1d5db;
@@ -119,6 +127,17 @@
119
127
  --cat-fallback-text: #d6d3d1;
120
128
  }
121
129
 
130
+ /* ─── Flow Animation ─── */
131
+ @keyframes dash-flow {
132
+ to { stroke-dashoffset: -20; }
133
+ }
134
+ .react-flow__edge.flow-edge-active .react-flow__edge-path {
135
+ stroke: var(--color-interactive-primary) !important;
136
+ stroke-width: 2.5 !important;
137
+ stroke-dasharray: 8 4;
138
+ animation: dash-flow 0.6s linear infinite;
139
+ }
140
+
122
141
  html,
123
142
  body,
124
143
  #root {
@@ -62,7 +62,8 @@ const CATEGORY_META = {
62
62
  service: { label: 'Service', icon: '\u{2699}\u{FE0F}', color: { bg: '#dcfce7', border: '#22c55e', text: '#166534' } },
63
63
  port: { label: 'Port', icon: '\u{1F50C}', color: { bg: '#ede9fe', border: '#8b5cf6', text: '#5b21b6' } },
64
64
  adapter: { label: 'Adapter', icon: '\u{1F527}', color: { bg: '#ffedd5', border: '#f97316', text: '#9a3412' } },
65
- model: { label: 'Model / DB', icon: '\u{1F4BE}', color: { bg: '#fee2e2', border: '#ef4444', text: '#991b1b' } },
65
+ model: { label: 'Entity', icon: '\u{1F4BE}', color: { bg: '#fee2e2', border: '#ef4444', text: '#991b1b' } },
66
+ database: { label: 'DB / Infra', icon: '\u{1F5C4}\u{FE0F}', color: { bg: '#fef3c7', border: '#d97706', text: '#92400e' } },
66
67
  external: { label: 'External', icon: '\u{2601}\u{FE0F}', color: { bg: '#f3f4f6', border: '#6b7280', text: '#374151' } },
67
68
  job: { label: 'Job', icon: '\u{23F0}', color: { bg: '#fef9c3', border: '#eab308', text: '#854d0e' } },
68
69
  dto: { label: 'DTO', icon: '\u{1F4E6}', color: { bg: '#cffafe', border: '#06b6d4', text: '#155e75' } },
@@ -76,7 +77,7 @@ function getMeta(category: string): CategoryMeta {
76
77
  return CATEGORY_META[category as StandardCategory] ?? FALLBACK_META;
77
78
  }
78
79
 
79
- const STANDARD_CATEGORIES = ['controller', 'service', 'port', 'adapter', 'model', 'external', 'job', 'dto'] as const;
80
+ const STANDARD_CATEGORIES = ['controller', 'service', 'port', 'adapter', 'model', 'database', 'external', 'job', 'dto'] as const;
80
81
 
81
82
  export function getCategoryColors(category: string): CategoryStyle {
82
83
  const key = (STANDARD_CATEGORIES as readonly string[]).includes(category) ? category : 'fallback';
@@ -125,8 +125,9 @@ const CATEGORY_RING_PRIORITY: Record<string, number> = {
125
125
  dto: 3,
126
126
  controller: 4,
127
127
  adapter: 5,
128
- job: 6,
129
- external: 7,
128
+ database: 6,
129
+ job: 7,
130
+ external: 8,
130
131
  };
131
132
 
132
133
  const DEFAULT_RING_PRIORITY = 4;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archrip",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Generate interactive architecture diagrams from your codebase using AI agents",
5
5
  "type": "module",
6
6
  "bin": {