archrip 0.1.7 → 0.1.9

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.
@@ -1,5 +1,5 @@
1
1
  import type { ArchNodeData, UseCase } from '../types.ts';
2
- import { getCategoryColors, getCategoryLabel } from '../types.ts';
2
+ import { getCategoryColors, getCategoryLabel, isGroupNode } from '../types.ts';
3
3
 
4
4
  interface DetailPanelProps {
5
5
  data: ArchNodeData;
@@ -48,192 +48,270 @@ export function DetailPanel({ data, useCases, onClose, onUseCaseClick }: DetailP
48
48
  </div>
49
49
 
50
50
  <div className="p-4 space-y-5 text-sm">
51
- {/* Description */}
52
- {data.description && (
53
- <Section title="Description">
54
- <p style={{ color: 'var(--color-content-secondary)' }}>{data.description}</p>
55
- </Section>
56
- )}
51
+ {isGroupNode(data) ? (
52
+ <>
53
+ {/* Group node: show contained nodes */}
54
+ <Section title="Contained Nodes">
55
+ <div className="space-y-2">
56
+ {data.memberNodes?.map((member) => (
57
+ <div
58
+ key={member.id}
59
+ className="rounded-lg p-3 border"
60
+ style={{
61
+ background: 'var(--color-surface-secondary)',
62
+ borderColor: 'var(--color-border-primary)',
63
+ }}
64
+ >
65
+ <div className="font-semibold text-sm" style={{ color: 'var(--color-content-primary)' }}>
66
+ {member.label}
67
+ </div>
68
+ {member.description && (
69
+ <div className="text-xs mt-0.5" style={{ color: 'var(--color-content-secondary)' }}>
70
+ {member.description}
71
+ </div>
72
+ )}
73
+ {member.filePath && (
74
+ <div className="text-xs mt-1">
75
+ {member.sourceUrl ? (
76
+ <a
77
+ href={member.sourceUrl}
78
+ target="_blank"
79
+ rel="noopener noreferrer"
80
+ className="underline break-all"
81
+ style={{ color: 'var(--color-interactive-primary)' }}
82
+ >
83
+ {member.filePath}
84
+ </a>
85
+ ) : (
86
+ <code
87
+ className="px-1 py-0.5 rounded break-all"
88
+ style={{ color: 'var(--color-content-tertiary)', background: 'var(--color-surface-primary)' }}
89
+ >
90
+ {member.filePath}
91
+ </code>
92
+ )}
93
+ </div>
94
+ )}
95
+ </div>
96
+ ))}
97
+ </div>
98
+ </Section>
57
99
 
58
- {/* Source Link */}
59
- {data.filePath && (
60
- <Section title="Source">
61
- {data.sourceUrl ? (
62
- <a
63
- href={data.sourceUrl}
64
- target="_blank"
65
- rel="noopener noreferrer"
66
- className="underline break-all"
67
- style={{ color: 'var(--color-interactive-primary)' }}
68
- >
69
- {data.filePath}
70
- </a>
71
- ) : (
72
- <code
73
- className="px-1.5 py-0.5 rounded text-xs break-all"
74
- style={{ color: 'var(--color-content-secondary)', background: 'var(--color-surface-secondary)' }}
75
- >
76
- {data.filePath}
77
- </code>
100
+ {/* Use Cases for group */}
101
+ {data.useCases.length > 0 && (
102
+ <Section title="Use Cases">
103
+ <div className="flex flex-wrap gap-1.5">
104
+ {data.useCases.map((ucId) => {
105
+ const ucName = useCases.find(uc => uc.id === ucId)?.name ?? ucId;
106
+ return (
107
+ <button
108
+ key={ucId}
109
+ onClick={() => onUseCaseClick(ucId)}
110
+ className="px-2 py-1 rounded text-xs transition-colors cursor-pointer border"
111
+ style={{
112
+ background: 'var(--color-surface-secondary)',
113
+ color: 'var(--color-interactive-primary)',
114
+ borderColor: 'var(--color-border-primary)',
115
+ }}
116
+ >
117
+ {ucName}
118
+ </button>
119
+ );
120
+ })}
121
+ </div>
122
+ </Section>
123
+ )}
124
+ </>
125
+ ) : (
126
+ <>
127
+ {/* Description */}
128
+ {data.description && (
129
+ <Section title="Description">
130
+ <p style={{ color: 'var(--color-content-secondary)' }}>{data.description}</p>
131
+ </Section>
78
132
  )}
79
- </Section>
80
- )}
81
-
82
- {/* Implements */}
83
- {data.implements && (
84
- <Section title="Implements">
85
- <code
86
- className="px-1.5 py-0.5 rounded text-xs"
87
- style={{ color: 'var(--cat-port-text)', background: 'var(--cat-port-bg)' }}
88
- >
89
- {data.implements}
90
- </code>
91
- </Section>
92
- )}
93
-
94
- {/* External Service */}
95
- {data.externalService && (
96
- <Section title="External Service">
97
- <span style={{ color: 'var(--color-content-secondary)' }}>{data.externalService}</span>
98
- </Section>
99
- )}
100
133
 
101
- {/* Routes */}
102
- {data.routes && data.routes.length > 0 && (
103
- <Section title="Routes">
104
- <div className="space-y-1">
105
- {data.routes.map((route) => {
106
- const [method, ...pathParts] = route.split(' ');
107
- const path = pathParts.join(' ');
108
- return (
109
- <div key={route} className="font-mono text-xs">
110
- <span className={`inline-block w-14 font-bold ${methodColor(method ?? '')}`}>
111
- {method}
112
- </span>
113
- <span style={{ color: 'var(--color-content-secondary)' }}>{path}</span>
114
- </div>
115
- );
116
- })}
117
- </div>
118
- </Section>
119
- )}
134
+ {/* Source Link */}
135
+ {data.filePath && (
136
+ <Section title="Source">
137
+ {data.sourceUrl ? (
138
+ <a
139
+ href={data.sourceUrl}
140
+ target="_blank"
141
+ rel="noopener noreferrer"
142
+ className="underline break-all"
143
+ style={{ color: 'var(--color-interactive-primary)' }}
144
+ >
145
+ {data.filePath}
146
+ </a>
147
+ ) : (
148
+ <code
149
+ className="px-1.5 py-0.5 rounded text-xs break-all"
150
+ style={{ color: 'var(--color-content-secondary)', background: 'var(--color-surface-secondary)' }}
151
+ >
152
+ {data.filePath}
153
+ </code>
154
+ )}
155
+ </Section>
156
+ )}
120
157
 
121
- {/* Methods */}
122
- {data.methods && data.methods.length > 0 && (
123
- <Section title="Methods">
124
- <div className="flex flex-wrap gap-1.5">
125
- {data.methods.map((m) => (
158
+ {/* Implements */}
159
+ {data.implements && (
160
+ <Section title="Implements">
126
161
  <code
127
- key={m}
128
162
  className="px-1.5 py-0.5 rounded text-xs"
129
- style={{ background: 'var(--color-surface-secondary)', color: 'var(--color-content-secondary)' }}
163
+ style={{ color: 'var(--cat-port-text)', background: 'var(--cat-port-bg)' }}
130
164
  >
131
- {m}()
165
+ {data.implements}
132
166
  </code>
133
- ))}
134
- </div>
135
- </Section>
136
- )}
167
+ </Section>
168
+ )}
137
169
 
138
- {/* Schema */}
139
- {data.schema && (
140
- <Section title={`Table: ${data.schema.tableName}`}>
141
- <div className="overflow-x-auto">
142
- <table className="w-full text-xs border-collapse">
143
- <thead>
144
- <tr style={{ background: 'var(--color-surface-secondary)' }}>
145
- <th className="text-left p-1.5 font-semibold" style={{ borderBottom: '1px solid var(--color-border-primary)' }}>Column</th>
146
- <th className="text-left p-1.5 font-semibold" style={{ borderBottom: '1px solid var(--color-border-primary)' }}>Type</th>
147
- <th className="text-left p-1.5 font-semibold" style={{ borderBottom: '1px solid var(--color-border-primary)' }}>Null</th>
148
- <th className="text-left p-1.5 font-semibold" style={{ borderBottom: '1px solid var(--color-border-primary)' }}>Key</th>
149
- </tr>
150
- </thead>
151
- <tbody>
152
- {data.schema.columns.map((col) => (
153
- <tr key={col.name} style={{ borderBottom: '1px solid var(--color-border-primary)' }}>
154
- <td className="p-1.5 font-mono" style={{ color: 'var(--color-content-primary)' }}>
155
- {col.name}
156
- {col.foreignKey && (
157
- <span className="ml-1" style={{ color: 'var(--color-interactive-primary)' }} title={`FK: ${col.foreignKey.table}.${col.foreignKey.column}${col.foreignKey.onDelete ? ` (${col.foreignKey.onDelete})` : ''}`}>
158
- FK
159
- </span>
160
- )}
161
- </td>
162
- <td className="p-1.5" style={{ color: 'var(--color-content-secondary)' }}>{col.type}</td>
163
- <td className="p-1.5">{col.nullable ? <span className="text-yellow-600 dark:text-yellow-400">YES</span> : '-'}</td>
164
- <td className="p-1.5" style={{ color: 'var(--color-content-tertiary)' }}>{col.index ?? '-'}</td>
165
- </tr>
166
- ))}
167
- </tbody>
168
- </table>
169
- </div>
170
+ {/* External Service */}
171
+ {data.externalService && (
172
+ <Section title="External Service">
173
+ <span style={{ color: 'var(--color-content-secondary)' }}>{data.externalService}</span>
174
+ </Section>
175
+ )}
176
+
177
+ {/* Routes */}
178
+ {data.routes && data.routes.length > 0 && (
179
+ <Section title="Routes">
180
+ <div className="space-y-1">
181
+ {data.routes.map((route) => {
182
+ const [method, ...pathParts] = route.split(' ');
183
+ const path = pathParts.join(' ');
184
+ return (
185
+ <div key={route} className="font-mono text-xs">
186
+ <span className={`inline-block w-14 font-bold ${methodColor(method ?? '')}`}>
187
+ {method}
188
+ </span>
189
+ <span style={{ color: 'var(--color-content-secondary)' }}>{path}</span>
190
+ </div>
191
+ );
192
+ })}
193
+ </div>
194
+ </Section>
195
+ )}
170
196
 
171
- {/* Enum Values */}
172
- {data.schema.enumValues && Object.entries(data.schema.enumValues).map(([field, values]) => (
173
- <div key={field} className="mt-2">
174
- <div className="text-xs font-semibold mb-1" style={{ color: 'var(--color-content-secondary)' }}>{field} values:</div>
175
- <div className="flex flex-wrap gap-1">
176
- {Object.entries(values).map(([k, v]) => (
177
- <span
178
- key={k}
197
+ {/* Methods */}
198
+ {data.methods && data.methods.length > 0 && (
199
+ <Section title="Methods">
200
+ <div className="flex flex-wrap gap-1.5">
201
+ {data.methods.map((m) => (
202
+ <code
203
+ key={m}
179
204
  className="px-1.5 py-0.5 rounded text-xs"
180
- style={{ background: 'var(--cat-controller-bg)', color: 'var(--cat-controller-text)' }}
205
+ style={{ background: 'var(--color-surface-secondary)', color: 'var(--color-content-secondary)' }}
181
206
  >
182
- {k}={v}
183
- </span>
207
+ {m}()
208
+ </code>
184
209
  ))}
185
210
  </div>
186
- </div>
187
- ))}
211
+ </Section>
212
+ )}
188
213
 
189
- {/* Indexes */}
190
- {data.schema.indexes && data.schema.indexes.length > 0 && (
191
- <div className="mt-2">
192
- <div className="text-xs font-semibold mb-1" style={{ color: 'var(--color-content-secondary)' }}>Indexes:</div>
193
- <ul className="text-xs space-y-0.5" style={{ color: 'var(--color-content-secondary)' }}>
194
- {data.schema.indexes.map((idx) => (
195
- <li key={idx} className="font-mono">{idx}</li>
196
- ))}
197
- </ul>
198
- </div>
214
+ {/* Schema */}
215
+ {data.schema && (
216
+ <Section title={`Table: ${data.schema.tableName}`}>
217
+ <div className="overflow-x-auto">
218
+ <table className="w-full text-xs border-collapse">
219
+ <thead>
220
+ <tr style={{ background: 'var(--color-surface-secondary)' }}>
221
+ <th className="text-left p-1.5 font-semibold" style={{ borderBottom: '1px solid var(--color-border-primary)' }}>Column</th>
222
+ <th className="text-left p-1.5 font-semibold" style={{ borderBottom: '1px solid var(--color-border-primary)' }}>Type</th>
223
+ <th className="text-left p-1.5 font-semibold" style={{ borderBottom: '1px solid var(--color-border-primary)' }}>Null</th>
224
+ <th className="text-left p-1.5 font-semibold" style={{ borderBottom: '1px solid var(--color-border-primary)' }}>Key</th>
225
+ </tr>
226
+ </thead>
227
+ <tbody>
228
+ {data.schema.columns.map((col) => (
229
+ <tr key={col.name} style={{ borderBottom: '1px solid var(--color-border-primary)' }}>
230
+ <td className="p-1.5 font-mono" style={{ color: 'var(--color-content-primary)' }}>
231
+ {col.name}
232
+ {col.foreignKey && (
233
+ <span className="ml-1" style={{ color: 'var(--color-interactive-primary)' }} title={`FK: ${col.foreignKey.table}.${col.foreignKey.column}${col.foreignKey.onDelete ? ` (${col.foreignKey.onDelete})` : ''}`}>
234
+ FK
235
+ </span>
236
+ )}
237
+ </td>
238
+ <td className="p-1.5" style={{ color: 'var(--color-content-secondary)' }}>{col.type}</td>
239
+ <td className="p-1.5">{col.nullable ? <span className="text-yellow-600 dark:text-yellow-400">YES</span> : '-'}</td>
240
+ <td className="p-1.5" style={{ color: 'var(--color-content-tertiary)' }}>{col.index ?? '-'}</td>
241
+ </tr>
242
+ ))}
243
+ </tbody>
244
+ </table>
245
+ </div>
246
+
247
+ {/* Enum Values */}
248
+ {data.schema.enumValues && Object.entries(data.schema.enumValues).map(([field, values]) => (
249
+ <div key={field} className="mt-2">
250
+ <div className="text-xs font-semibold mb-1" style={{ color: 'var(--color-content-secondary)' }}>{field} values:</div>
251
+ <div className="flex flex-wrap gap-1">
252
+ {Object.entries(values).map(([k, v]) => (
253
+ <span
254
+ key={k}
255
+ className="px-1.5 py-0.5 rounded text-xs"
256
+ style={{ background: 'var(--cat-controller-bg)', color: 'var(--cat-controller-text)' }}
257
+ >
258
+ {k}={v}
259
+ </span>
260
+ ))}
261
+ </div>
262
+ </div>
263
+ ))}
264
+
265
+ {/* Indexes */}
266
+ {data.schema.indexes && data.schema.indexes.length > 0 && (
267
+ <div className="mt-2">
268
+ <div className="text-xs font-semibold mb-1" style={{ color: 'var(--color-content-secondary)' }}>Indexes:</div>
269
+ <ul className="text-xs space-y-0.5" style={{ color: 'var(--color-content-secondary)' }}>
270
+ {data.schema.indexes.map((idx) => (
271
+ <li key={idx} className="font-mono">{idx}</li>
272
+ ))}
273
+ </ul>
274
+ </div>
275
+ )}
276
+ </Section>
199
277
  )}
200
- </Section>
201
- )}
202
278
 
203
- {/* SQL Examples */}
204
- {data.sqlExamples && data.sqlExamples.length > 0 && (
205
- <Section title="SQL Examples">
206
- {data.sqlExamples.map((sql) => (
207
- <pre key={sql} className="bg-gray-900 text-green-400 p-2 rounded text-xs overflow-x-auto mb-1.5 whitespace-pre-wrap">
208
- {sql}
209
- </pre>
210
- ))}
211
- </Section>
212
- )}
279
+ {/* SQL Examples */}
280
+ {data.sqlExamples && data.sqlExamples.length > 0 && (
281
+ <Section title="SQL Examples">
282
+ {data.sqlExamples.map((sql) => (
283
+ <pre key={sql} className="bg-gray-900 text-green-400 p-2 rounded text-xs overflow-x-auto mb-1.5 whitespace-pre-wrap">
284
+ {sql}
285
+ </pre>
286
+ ))}
287
+ </Section>
288
+ )}
213
289
 
214
- {/* Use Cases */}
215
- {data.useCases.length > 0 && (
216
- <Section title="Use Cases">
217
- <div className="flex flex-wrap gap-1.5">
218
- {data.useCases.map((ucId) => {
219
- const ucName = useCases.find(uc => uc.id === ucId)?.name ?? ucId;
220
- return (
221
- <button
222
- key={ucId}
223
- onClick={() => onUseCaseClick(ucId)}
224
- className="px-2 py-1 rounded text-xs transition-colors cursor-pointer border"
225
- style={{
226
- background: 'var(--color-surface-secondary)',
227
- color: 'var(--color-interactive-primary)',
228
- borderColor: 'var(--color-border-primary)',
229
- }}
230
- >
231
- {ucName}
232
- </button>
233
- );
234
- })}
235
- </div>
236
- </Section>
290
+ {/* Use Cases */}
291
+ {data.useCases.length > 0 && (
292
+ <Section title="Use Cases">
293
+ <div className="flex flex-wrap gap-1.5">
294
+ {data.useCases.map((ucId) => {
295
+ const ucName = useCases.find(uc => uc.id === ucId)?.name ?? ucId;
296
+ return (
297
+ <button
298
+ key={ucId}
299
+ onClick={() => onUseCaseClick(ucId)}
300
+ className="px-2 py-1 rounded text-xs transition-colors cursor-pointer border"
301
+ style={{
302
+ background: 'var(--color-surface-secondary)',
303
+ color: 'var(--color-interactive-primary)',
304
+ borderColor: 'var(--color-border-primary)',
305
+ }}
306
+ >
307
+ {ucName}
308
+ </button>
309
+ );
310
+ })}
311
+ </div>
312
+ </Section>
313
+ )}
314
+ </>
237
315
  )}
238
316
  </div>
239
317
  </div>
@@ -0,0 +1,62 @@
1
+ import { Handle, Position } from '@xyflow/react';
2
+ import type { NodeProps } from '@xyflow/react';
3
+ import type { ArchFlowNode } from '../../types.ts';
4
+ import { getCategoryColors, getCategoryIcon } from '../../types.ts';
5
+
6
+ export function GroupNode({ data, selected }: NodeProps<ArchFlowNode>) {
7
+ const d = data;
8
+ const colors = getCategoryColors(d.category);
9
+ const icon = getCategoryIcon(d.category);
10
+
11
+ return (
12
+ <>
13
+ <Handle type="target" position={Position.Top} style={{ background: colors.border }} />
14
+ <div
15
+ style={{
16
+ background: colors.bg,
17
+ border: `2px dashed ${selected ? 'var(--color-border-focus)' : colors.border}`,
18
+ borderRadius: 12,
19
+ padding: '8px 12px',
20
+ minWidth: 160,
21
+ maxWidth: 220,
22
+ cursor: 'pointer',
23
+ boxShadow: selected ? 'var(--shadow-node-selected)' : 'var(--shadow-node)',
24
+ transition: 'box-shadow 0.15s, border-color 0.15s',
25
+ position: 'relative',
26
+ }}
27
+ >
28
+ {/* Count badge */}
29
+ {d.memberCount != null && (
30
+ <div
31
+ style={{
32
+ position: 'absolute',
33
+ top: -8,
34
+ right: -8,
35
+ background: colors.border,
36
+ color: '#fff',
37
+ fontSize: 10,
38
+ fontWeight: 700,
39
+ width: 20,
40
+ height: 20,
41
+ borderRadius: '50%',
42
+ display: 'flex',
43
+ alignItems: 'center',
44
+ justifyContent: 'center',
45
+ lineHeight: 1,
46
+ }}
47
+ >
48
+ {d.memberCount}
49
+ </div>
50
+ )}
51
+
52
+ <div style={{ fontSize: 11, color: colors.text, opacity: 0.7, marginBottom: 2 }}>
53
+ {icon} {d.category.toUpperCase()}
54
+ </div>
55
+ <div style={{ fontSize: 13, fontWeight: 600, color: colors.text, lineHeight: 1.3, wordBreak: 'break-word' }}>
56
+ {d.label}
57
+ </div>
58
+ </div>
59
+ <Handle type="source" position={Position.Bottom} style={{ background: colors.border }} />
60
+ </>
61
+ );
62
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Edge } from '@xyflow/react';
2
2
  import type { ArchFlowNode, ArchNodeData, UseCase, TableSchema, DepthLevel } from '../types.ts';
3
- import { getDefaultDepth } from '../types.ts';
3
+ import { computeDepths } from '../types.ts';
4
4
 
5
5
  interface RawArchData {
6
6
  version: string;
@@ -10,6 +10,7 @@ interface RawArchData {
10
10
  language?: string;
11
11
  framework?: string;
12
12
  sourceUrl?: string;
13
+ layout?: string;
13
14
  };
14
15
  nodes: RawNode[];
15
16
  edges: RawEdge[];
@@ -55,6 +56,7 @@ export interface LoadedArchitecture {
55
56
  nodes: ArchFlowNode[];
56
57
  edges: Edge[];
57
58
  useCases: UseCase[];
59
+ layoutType: 'dagre' | 'concentric';
58
60
  }
59
61
 
60
62
  function resolveSourceUrl(template: string | undefined, filePath: string): string {
@@ -83,6 +85,8 @@ export async function loadArchitecture(): Promise<LoadedArchitecture> {
83
85
  const layout = raw._layout ?? {};
84
86
  const sourceUrlTemplate = raw.project.sourceUrl;
85
87
 
88
+ const depthMap = computeDepths(raw.nodes);
89
+
86
90
  // Convert raw nodes to React Flow nodes
87
91
  const nodes: ArchFlowNode[] = raw.nodes.map((n) => {
88
92
  const pos = layout[n.id] ?? { x: 0, y: 0 };
@@ -91,10 +95,11 @@ export async function loadArchitecture(): Promise<LoadedArchitecture> {
91
95
  const data: ArchNodeData = {
92
96
  label: n.label,
93
97
  category: n.category,
94
- depth: (n.depth ?? getDefaultDepth(n.category)) as DepthLevel,
98
+ depth: (n.depth ?? depthMap.get(n.layer) ?? 1) as DepthLevel,
95
99
  description: n.description ?? '',
96
100
  filePath: n.filePath ?? '',
97
101
  sourceUrl: resolveSourceUrl(sourceUrlTemplate, n.filePath ?? ''),
102
+ layer: n.layer,
98
103
  methods: n.methods,
99
104
  routes: n.routes,
100
105
  useCases: n.useCases ?? [],
@@ -136,5 +141,6 @@ export async function loadArchitecture(): Promise<LoadedArchitecture> {
136
141
  nodes,
137
142
  edges,
138
143
  useCases,
144
+ layoutType: raw.project.layout === 'concentric' ? 'concentric' as const : 'dagre' as const,
139
145
  };
140
146
  }
@@ -10,6 +10,7 @@ export function useArchitecture() {
10
10
  const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
11
11
  const [useCases, setUseCases] = useState<UseCase[]>([]);
12
12
  const [projectName, setProjectName] = useState('Architecture Viewer');
13
+ const [layoutType, setLayoutType] = useState<'dagre' | 'concentric'>('dagre');
13
14
  const [loading, setLoading] = useState(true);
14
15
  const [error, setError] = useState<string | null>(null);
15
16
 
@@ -20,6 +21,7 @@ export function useArchitecture() {
20
21
  setEdges(arch.edges);
21
22
  setUseCases(arch.useCases);
22
23
  setProjectName(arch.projectName);
24
+ setLayoutType(arch.layoutType);
23
25
  setLoading(false);
24
26
  })
25
27
  .catch((err: unknown) => {
@@ -28,5 +30,5 @@ export function useArchitecture() {
28
30
  });
29
31
  }, [setNodes, setEdges]);
30
32
 
31
- return { nodes, edges, useCases, projectName, loading, error, setNodes, setEdges, onNodesChange, onEdgesChange };
33
+ return { nodes, edges, useCases, projectName, layoutType, loading, error, setNodes, setEdges, onNodesChange, onEdgesChange };
32
34
  }