archrip 0.1.2
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.
- package/dist/commands/build.d.ts +2 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +88 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +58 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/serve.d.ts +2 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +27 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/install/slash-commands.d.ts +6 -0
- package/dist/install/slash-commands.d.ts.map +1 -0
- package/dist/install/slash-commands.js +31 -0
- package/dist/install/slash-commands.js.map +1 -0
- package/dist/install/viewer.d.ts +5 -0
- package/dist/install/viewer.d.ts.map +1 -0
- package/dist/install/viewer.js +40 -0
- package/dist/install/viewer.js.map +1 -0
- package/dist/schema/architecture.schema.json +144 -0
- package/dist/templates/skeleton.json +15 -0
- package/dist/templates/slash-commands/claude/archrip-refine.md +17 -0
- package/dist/templates/slash-commands/claude/archrip-scan.md +140 -0
- package/dist/templates/slash-commands/claude/archrip-update.md +34 -0
- package/dist/templates/slash-commands/claude/archrips-refine.md +17 -0
- package/dist/templates/slash-commands/claude/archrips-scan.md +97 -0
- package/dist/templates/slash-commands/claude/archrips-update.md +18 -0
- package/dist/templates/slash-commands/codex/archrip-refine.md +17 -0
- package/dist/templates/slash-commands/codex/archrip-scan.md +140 -0
- package/dist/templates/slash-commands/codex/archrip-update.md +34 -0
- package/dist/templates/slash-commands/codex/archrips-refine.md +17 -0
- package/dist/templates/slash-commands/codex/archrips-scan.md +97 -0
- package/dist/templates/slash-commands/codex/archrips-update.md +18 -0
- package/dist/templates/slash-commands/gemini/archrip-refine.md +17 -0
- package/dist/templates/slash-commands/gemini/archrip-scan.md +140 -0
- package/dist/templates/slash-commands/gemini/archrip-update.md +34 -0
- package/dist/templates/slash-commands/gemini/archrips-refine.md +17 -0
- package/dist/templates/slash-commands/gemini/archrips-scan.md +97 -0
- package/dist/templates/slash-commands/gemini/archrips-update.md +18 -0
- package/dist/utils/detect-agents.d.ts +15 -0
- package/dist/utils/detect-agents.d.ts.map +1 -0
- package/dist/utils/detect-agents.js +29 -0
- package/dist/utils/detect-agents.js.map +1 -0
- package/dist/utils/gitignore.d.ts +5 -0
- package/dist/utils/gitignore.d.ts.map +1 -0
- package/dist/utils/gitignore.js +21 -0
- package/dist/utils/gitignore.js.map +1 -0
- package/dist/utils/layout.d.ts +17 -0
- package/dist/utils/layout.d.ts.map +1 -0
- package/dist/utils/layout.js +121 -0
- package/dist/utils/layout.js.map +1 -0
- package/dist/utils/layout.spec.d.ts +2 -0
- package/dist/utils/layout.spec.d.ts.map +1 -0
- package/dist/utils/layout.spec.js +176 -0
- package/dist/utils/layout.spec.js.map +1 -0
- package/dist/utils/paths.d.ts +3 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +28 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/project-info.d.ts +9 -0
- package/dist/utils/project-info.d.ts.map +1 -0
- package/dist/utils/project-info.js +86 -0
- package/dist/utils/project-info.js.map +1 -0
- package/dist/utils/validate.d.ts +88 -0
- package/dist/utils/validate.d.ts.map +1 -0
- package/dist/utils/validate.js +238 -0
- package/dist/utils/validate.js.map +1 -0
- package/dist/utils/validate.spec.d.ts +2 -0
- package/dist/utils/validate.spec.d.ts.map +1 -0
- package/dist/utils/validate.spec.js +424 -0
- package/dist/utils/validate.spec.js.map +1 -0
- package/dist/utils/verbose.d.ts +3 -0
- package/dist/utils/verbose.d.ts.map +1 -0
- package/dist/utils/verbose.js +8 -0
- package/dist/utils/verbose.js.map +1 -0
- package/dist/viewer-template/index.html +15 -0
- package/dist/viewer-template/package-lock.json +2714 -0
- package/dist/viewer-template/package.json +26 -0
- package/dist/viewer-template/src/App.tsx +168 -0
- package/dist/viewer-template/src/components/DepthFilter.tsx +43 -0
- package/dist/viewer-template/src/components/DetailPanel.tsx +261 -0
- package/dist/viewer-template/src/components/Legend.tsx +41 -0
- package/dist/viewer-template/src/components/ThemeToggle.tsx +37 -0
- package/dist/viewer-template/src/components/UseCaseFilter.tsx +59 -0
- package/dist/viewer-template/src/components/nodes/ArchNode.tsx +37 -0
- package/dist/viewer-template/src/data/loader.ts +140 -0
- package/dist/viewer-template/src/hooks/useArchitecture.ts +32 -0
- package/dist/viewer-template/src/hooks/useDepthFilter.ts +37 -0
- package/dist/viewer-template/src/hooks/useTheme.ts +39 -0
- package/dist/viewer-template/src/hooks/useUseCaseFilter.ts +56 -0
- package/dist/viewer-template/src/index.css +130 -0
- package/dist/viewer-template/src/main.tsx +13 -0
- package/dist/viewer-template/src/types.ts +125 -0
- package/dist/viewer-template/tsconfig.app.json +24 -0
- package/dist/viewer-template/tsconfig.json +7 -0
- package/dist/viewer-template/tsconfig.node.json +22 -0
- package/dist/viewer-template/vite.config.ts +7 -0
- package/package.json +45 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "archrip-viewer",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@xyflow/react": "^12.10.0",
|
|
13
|
+
"nuqs": "^2.8.8",
|
|
14
|
+
"react": "^19.2.0",
|
|
15
|
+
"react-dom": "^19.2.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
19
|
+
"@types/react": "^19.2.7",
|
|
20
|
+
"@types/react-dom": "^19.2.3",
|
|
21
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
22
|
+
"tailwindcss": "^4.1.18",
|
|
23
|
+
"typescript": "~5.9.3",
|
|
24
|
+
"vite": "^7.3.1"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ReactFlow,
|
|
4
|
+
ReactFlowProvider,
|
|
5
|
+
Background,
|
|
6
|
+
Controls,
|
|
7
|
+
MiniMap,
|
|
8
|
+
useReactFlow,
|
|
9
|
+
} from '@xyflow/react';
|
|
10
|
+
import type { NodeMouseHandler } from '@xyflow/react';
|
|
11
|
+
import { useQueryState, parseAsString } from 'nuqs';
|
|
12
|
+
import '@xyflow/react/dist/style.css';
|
|
13
|
+
|
|
14
|
+
import type { ArchFlowNode, ArchNodeData } from './types.ts';
|
|
15
|
+
import { getCategoryColors } from './types.ts';
|
|
16
|
+
import { ArchNode } from './components/nodes/ArchNode.tsx';
|
|
17
|
+
import { DetailPanel } from './components/DetailPanel.tsx';
|
|
18
|
+
import { UseCaseFilter } from './components/UseCaseFilter.tsx';
|
|
19
|
+
import { DepthFilter } from './components/DepthFilter.tsx';
|
|
20
|
+
import { Legend } from './components/Legend.tsx';
|
|
21
|
+
import { ThemeToggle } from './components/ThemeToggle.tsx';
|
|
22
|
+
import { useArchitecture } from './hooks/useArchitecture.ts';
|
|
23
|
+
import { useDepthFilter } from './hooks/useDepthFilter.ts';
|
|
24
|
+
import { useUseCaseFilter } from './hooks/useUseCaseFilter.ts';
|
|
25
|
+
import { useTheme } from './hooks/useTheme.ts';
|
|
26
|
+
|
|
27
|
+
const nodeTypes = { archNode: ArchNode };
|
|
28
|
+
|
|
29
|
+
function AppInner() {
|
|
30
|
+
const { nodes, edges, useCases, projectName, loading, error, onNodesChange, onEdgesChange } = useArchitecture();
|
|
31
|
+
const { depthLevel, setDepthLevel, visibleNodes, visibleEdges } = useDepthFilter(nodes, edges);
|
|
32
|
+
const { selectedUseCase, setSelectedUseCase, categories, filteredNodes, filteredEdges } = useUseCaseFilter(visibleNodes, visibleEdges, useCases);
|
|
33
|
+
const [selectedNodeId, setSelectedNodeId] = useQueryState('node', parseAsString.withOptions({ history: 'replace' }));
|
|
34
|
+
const { theme, toggleTheme } = useTheme();
|
|
35
|
+
const { fitView } = useReactFlow();
|
|
36
|
+
|
|
37
|
+
const selectedNodeData: ArchNodeData | null = useMemo(() => {
|
|
38
|
+
if (!selectedNodeId) return null;
|
|
39
|
+
const found = nodes.find((n) => n.id === selectedNodeId);
|
|
40
|
+
return found?.data ?? null;
|
|
41
|
+
}, [selectedNodeId, nodes]);
|
|
42
|
+
|
|
43
|
+
const onNodeClick: NodeMouseHandler<ArchFlowNode> = useCallback((_event, node) => {
|
|
44
|
+
void setSelectedNodeId(node.id);
|
|
45
|
+
}, [setSelectedNodeId]);
|
|
46
|
+
|
|
47
|
+
const onPaneClick = useCallback(() => {
|
|
48
|
+
void setSelectedNodeId(null);
|
|
49
|
+
}, [setSelectedNodeId]);
|
|
50
|
+
|
|
51
|
+
const handleUseCaseSelect = useCallback((ucId: string | null) => {
|
|
52
|
+
void setSelectedUseCase(ucId);
|
|
53
|
+
}, [setSelectedUseCase]);
|
|
54
|
+
|
|
55
|
+
const handleUseCaseClickFromPanel = useCallback((ucId: string) => {
|
|
56
|
+
void setSelectedUseCase(ucId);
|
|
57
|
+
void setSelectedNodeId(null);
|
|
58
|
+
}, [setSelectedUseCase, setSelectedNodeId]);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const id = requestAnimationFrame(() => {
|
|
62
|
+
fitView({ padding: 0.15, duration: 300 });
|
|
63
|
+
});
|
|
64
|
+
return () => cancelAnimationFrame(id);
|
|
65
|
+
}, [depthLevel, selectedUseCase, fitView]);
|
|
66
|
+
|
|
67
|
+
if (loading) {
|
|
68
|
+
return (
|
|
69
|
+
<div className="w-full h-screen flex items-center justify-center" style={{ color: 'var(--color-content-tertiary)', background: 'var(--color-surface-canvas)' }}>
|
|
70
|
+
Loading architecture data...
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (error) {
|
|
76
|
+
return (
|
|
77
|
+
<div className="w-full h-screen flex items-center justify-center" style={{ background: 'var(--color-surface-canvas)' }}>
|
|
78
|
+
<div className="text-center">
|
|
79
|
+
<p className="font-semibold mb-2" style={{ color: 'var(--color-interactive-primary)' }}>Failed to load</p>
|
|
80
|
+
<p className="text-sm" style={{ color: 'var(--color-content-tertiary)' }}>{error}</p>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className="w-full h-screen relative">
|
|
88
|
+
<ReactFlow
|
|
89
|
+
nodes={filteredNodes}
|
|
90
|
+
edges={filteredEdges}
|
|
91
|
+
onNodesChange={onNodesChange}
|
|
92
|
+
onEdgesChange={onEdgesChange}
|
|
93
|
+
onNodeClick={onNodeClick}
|
|
94
|
+
onPaneClick={onPaneClick}
|
|
95
|
+
nodeTypes={nodeTypes}
|
|
96
|
+
colorMode={theme}
|
|
97
|
+
fitView
|
|
98
|
+
fitViewOptions={{ padding: 0.15 }}
|
|
99
|
+
minZoom={0.05}
|
|
100
|
+
maxZoom={2}
|
|
101
|
+
proOptions={{ hideAttribution: true }}
|
|
102
|
+
>
|
|
103
|
+
<Background gap={20} size={1} />
|
|
104
|
+
<Controls showInteractive={false} />
|
|
105
|
+
<MiniMap
|
|
106
|
+
nodeColor={(node) => {
|
|
107
|
+
const data = node.data as ArchNodeData;
|
|
108
|
+
return getCategoryColors(data.category).border;
|
|
109
|
+
}}
|
|
110
|
+
maskColor="rgba(0,0,0,0.08)"
|
|
111
|
+
style={{ border: '1px solid var(--color-border-primary)' }}
|
|
112
|
+
/>
|
|
113
|
+
</ReactFlow>
|
|
114
|
+
|
|
115
|
+
<div className="absolute top-3 left-3 z-10 flex flex-col gap-2">
|
|
116
|
+
{useCases.length > 0 && (
|
|
117
|
+
<UseCaseFilter
|
|
118
|
+
useCases={useCases}
|
|
119
|
+
selectedUseCase={selectedUseCase}
|
|
120
|
+
onSelect={handleUseCaseSelect}
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
<DepthFilter depthLevel={depthLevel} onSelect={setDepthLevel} />
|
|
124
|
+
</div>
|
|
125
|
+
<Legend categories={categories} />
|
|
126
|
+
|
|
127
|
+
{/* Title + Theme Toggle */}
|
|
128
|
+
<div className="absolute top-3 right-3 z-10 flex items-center gap-2">
|
|
129
|
+
<div
|
|
130
|
+
className="backdrop-blur-sm rounded-lg px-4 py-2 border"
|
|
131
|
+
style={{
|
|
132
|
+
background: 'color-mix(in srgb, var(--color-surface-primary) 90%, transparent)',
|
|
133
|
+
borderColor: 'var(--color-border-primary)',
|
|
134
|
+
boxShadow: 'var(--shadow-panel)',
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<h1 className="text-sm font-bold" style={{ color: 'var(--color-content-primary)' }}>
|
|
138
|
+
{projectName} — Architecture
|
|
139
|
+
</h1>
|
|
140
|
+
</div>
|
|
141
|
+
<ThemeToggle theme={theme} onToggle={toggleTheme} />
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{selectedNodeData && (
|
|
145
|
+
<>
|
|
146
|
+
<div
|
|
147
|
+
className="fixed inset-0 z-40"
|
|
148
|
+
onClick={() => void setSelectedNodeId(null)}
|
|
149
|
+
/>
|
|
150
|
+
<DetailPanel
|
|
151
|
+
data={selectedNodeData}
|
|
152
|
+
useCases={useCases}
|
|
153
|
+
onClose={() => void setSelectedNodeId(null)}
|
|
154
|
+
onUseCaseClick={handleUseCaseClickFromPanel}
|
|
155
|
+
/>
|
|
156
|
+
</>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export default function App() {
|
|
163
|
+
return (
|
|
164
|
+
<ReactFlowProvider>
|
|
165
|
+
<AppInner />
|
|
166
|
+
</ReactFlowProvider>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { DEPTH_LEVELS } from '../types.ts';
|
|
2
|
+
import type { DepthLevel } from '../types.ts';
|
|
3
|
+
|
|
4
|
+
interface DepthFilterProps {
|
|
5
|
+
depthLevel: DepthLevel;
|
|
6
|
+
onSelect: (level: DepthLevel) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function DepthFilter({ depthLevel, onSelect }: DepthFilterProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
className="rounded-lg p-3 w-64 border"
|
|
13
|
+
style={{
|
|
14
|
+
background: 'var(--color-surface-primary)',
|
|
15
|
+
borderColor: 'var(--color-border-primary)',
|
|
16
|
+
boxShadow: 'var(--shadow-panel)',
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
<h3
|
|
20
|
+
className="text-xs font-semibold uppercase tracking-wider mb-2"
|
|
21
|
+
style={{ color: 'var(--color-content-tertiary)' }}
|
|
22
|
+
>
|
|
23
|
+
Depth
|
|
24
|
+
</h3>
|
|
25
|
+
<div className="flex gap-1">
|
|
26
|
+
{DEPTH_LEVELS.map(({ level, label }) => (
|
|
27
|
+
<button
|
|
28
|
+
key={level}
|
|
29
|
+
onClick={() => onSelect(level)}
|
|
30
|
+
className="flex-1 text-xs py-1.5 px-2 rounded font-medium transition-colors cursor-pointer"
|
|
31
|
+
style={
|
|
32
|
+
depthLevel === level
|
|
33
|
+
? { background: 'var(--color-interactive-primary)', color: 'var(--color-content-inverse)' }
|
|
34
|
+
: { background: 'var(--color-surface-secondary)', color: 'var(--color-content-secondary)' }
|
|
35
|
+
}
|
|
36
|
+
>
|
|
37
|
+
{label}
|
|
38
|
+
</button>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { ArchNodeData, UseCase } from '../types.ts';
|
|
2
|
+
import { getCategoryColors, getCategoryLabel } from '../types.ts';
|
|
3
|
+
|
|
4
|
+
interface DetailPanelProps {
|
|
5
|
+
data: ArchNodeData;
|
|
6
|
+
useCases: UseCase[];
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
onUseCaseClick: (useCaseId: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DetailPanel({ data, useCases, onClose, onUseCaseClick }: DetailPanelProps) {
|
|
12
|
+
const colors = getCategoryColors(data.category);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
className="fixed top-0 right-0 h-full w-[400px] z-50 overflow-y-auto border-l"
|
|
17
|
+
style={{
|
|
18
|
+
background: 'var(--color-surface-primary)',
|
|
19
|
+
borderColor: 'var(--color-border-primary)',
|
|
20
|
+
boxShadow: 'var(--shadow-panel)',
|
|
21
|
+
}}
|
|
22
|
+
>
|
|
23
|
+
{/* Header */}
|
|
24
|
+
<div
|
|
25
|
+
className="sticky top-0 border-b p-4 flex items-start justify-between"
|
|
26
|
+
style={{
|
|
27
|
+
background: 'var(--color-surface-primary)',
|
|
28
|
+
borderColor: 'var(--color-border-primary)',
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
<div>
|
|
32
|
+
<span
|
|
33
|
+
className="inline-block text-xs font-semibold px-2 py-0.5 rounded-full mb-2"
|
|
34
|
+
style={{ background: colors.bg, color: colors.text, border: `1px solid ${colors.border}` }}
|
|
35
|
+
>
|
|
36
|
+
{getCategoryLabel(data.category)}
|
|
37
|
+
</span>
|
|
38
|
+
<h2 className="text-lg font-bold" style={{ color: 'var(--color-content-primary)' }}>{data.label}</h2>
|
|
39
|
+
</div>
|
|
40
|
+
<button
|
|
41
|
+
onClick={onClose}
|
|
42
|
+
className="text-xl leading-none p-1 cursor-pointer transition-colors"
|
|
43
|
+
style={{ color: 'var(--color-content-tertiary)' }}
|
|
44
|
+
aria-label="Close"
|
|
45
|
+
>
|
|
46
|
+
×
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
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
|
+
)}
|
|
57
|
+
|
|
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>
|
|
78
|
+
)}
|
|
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
|
+
|
|
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
|
+
)}
|
|
120
|
+
|
|
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) => (
|
|
126
|
+
<code
|
|
127
|
+
key={m}
|
|
128
|
+
className="px-1.5 py-0.5 rounded text-xs"
|
|
129
|
+
style={{ background: 'var(--color-surface-secondary)', color: 'var(--color-content-secondary)' }}
|
|
130
|
+
>
|
|
131
|
+
{m}()
|
|
132
|
+
</code>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
</Section>
|
|
136
|
+
)}
|
|
137
|
+
|
|
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
|
+
|
|
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}
|
|
179
|
+
className="px-1.5 py-0.5 rounded text-xs"
|
|
180
|
+
style={{ background: 'var(--cat-controller-bg)', color: 'var(--cat-controller-text)' }}
|
|
181
|
+
>
|
|
182
|
+
{k}={v}
|
|
183
|
+
</span>
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
))}
|
|
188
|
+
|
|
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>
|
|
199
|
+
)}
|
|
200
|
+
</Section>
|
|
201
|
+
)}
|
|
202
|
+
|
|
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
|
+
)}
|
|
213
|
+
|
|
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>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
|
244
|
+
return (
|
|
245
|
+
<div>
|
|
246
|
+
<h3 className="text-xs font-semibold uppercase tracking-wider mb-1.5" style={{ color: 'var(--color-content-tertiary)' }}>{title}</h3>
|
|
247
|
+
{children}
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function methodColor(method: string): string {
|
|
253
|
+
switch (method) {
|
|
254
|
+
case 'GET': return 'text-green-600 dark:text-green-400';
|
|
255
|
+
case 'POST': return 'text-blue-600 dark:text-blue-400';
|
|
256
|
+
case 'PUT': return 'text-yellow-600 dark:text-yellow-400';
|
|
257
|
+
case 'PATCH': return 'text-orange-600 dark:text-orange-400';
|
|
258
|
+
case 'DELETE': return 'text-red-600 dark:text-red-400';
|
|
259
|
+
default: return '';
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getCategoryColors, getCategoryLabel } from '../types.ts';
|
|
2
|
+
|
|
3
|
+
interface LegendProps {
|
|
4
|
+
categories: string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Legend({ categories }: LegendProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
className="absolute bottom-3 right-3 z-10 rounded-lg p-3 border"
|
|
11
|
+
style={{
|
|
12
|
+
background: 'var(--color-surface-primary)',
|
|
13
|
+
borderColor: 'var(--color-border-primary)',
|
|
14
|
+
boxShadow: 'var(--shadow-panel)',
|
|
15
|
+
}}
|
|
16
|
+
>
|
|
17
|
+
<h3
|
|
18
|
+
className="text-xs font-semibold uppercase tracking-wider mb-2"
|
|
19
|
+
style={{ color: 'var(--color-content-tertiary)' }}
|
|
20
|
+
>
|
|
21
|
+
Legend
|
|
22
|
+
</h3>
|
|
23
|
+
<div className="grid grid-cols-2 gap-x-4 gap-y-1">
|
|
24
|
+
{categories.map((cat) => {
|
|
25
|
+
const colors = getCategoryColors(cat);
|
|
26
|
+
return (
|
|
27
|
+
<div key={cat} className="flex items-center gap-1.5">
|
|
28
|
+
<div
|
|
29
|
+
className="w-3 h-3 rounded-sm border"
|
|
30
|
+
style={{ background: colors.bg, borderColor: colors.border }}
|
|
31
|
+
/>
|
|
32
|
+
<span className="text-xs" style={{ color: 'var(--color-content-secondary)' }}>
|
|
33
|
+
{getCategoryLabel(cat)}
|
|
34
|
+
</span>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
})}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
interface ThemeToggleProps {
|
|
2
|
+
theme: 'light' | 'dark';
|
|
3
|
+
onToggle: () => void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function ThemeToggle({ theme, onToggle }: ThemeToggleProps) {
|
|
7
|
+
return (
|
|
8
|
+
<button
|
|
9
|
+
onClick={onToggle}
|
|
10
|
+
className="p-2 rounded-lg border cursor-pointer transition-colors"
|
|
11
|
+
style={{
|
|
12
|
+
background: 'var(--color-surface-primary)',
|
|
13
|
+
borderColor: 'var(--color-border-primary)',
|
|
14
|
+
color: 'var(--color-content-secondary)',
|
|
15
|
+
}}
|
|
16
|
+
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
|
|
17
|
+
>
|
|
18
|
+
{theme === 'light' ? (
|
|
19
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
20
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
21
|
+
</svg>
|
|
22
|
+
) : (
|
|
23
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
24
|
+
<circle cx="12" cy="12" r="5" />
|
|
25
|
+
<line x1="12" y1="1" x2="12" y2="3" />
|
|
26
|
+
<line x1="12" y1="21" x2="12" y2="23" />
|
|
27
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
|
28
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
|
29
|
+
<line x1="1" y1="12" x2="3" y2="12" />
|
|
30
|
+
<line x1="21" y1="12" x2="23" y2="12" />
|
|
31
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
|
32
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
|
33
|
+
</svg>
|
|
34
|
+
)}
|
|
35
|
+
</button>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { UseCase } from '../types.ts';
|
|
2
|
+
|
|
3
|
+
interface UseCaseFilterProps {
|
|
4
|
+
useCases: UseCase[];
|
|
5
|
+
selectedUseCase: string | null;
|
|
6
|
+
onSelect: (useCaseId: string | null) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function UseCaseFilter({ useCases, selectedUseCase, onSelect }: UseCaseFilterProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
className="rounded-lg p-3 w-64 border"
|
|
13
|
+
style={{
|
|
14
|
+
background: 'var(--color-surface-primary)',
|
|
15
|
+
borderColor: 'var(--color-border-primary)',
|
|
16
|
+
boxShadow: 'var(--shadow-panel)',
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
<h3
|
|
20
|
+
className="text-xs font-semibold uppercase tracking-wider mb-2"
|
|
21
|
+
style={{ color: 'var(--color-content-tertiary)' }}
|
|
22
|
+
>
|
|
23
|
+
Use Case Filter
|
|
24
|
+
</h3>
|
|
25
|
+
<select
|
|
26
|
+
value={selectedUseCase ?? ''}
|
|
27
|
+
onChange={(e) => onSelect(e.target.value || null)}
|
|
28
|
+
className="w-full text-sm rounded px-2 py-1.5 focus:outline-none focus:ring-2 border"
|
|
29
|
+
style={{
|
|
30
|
+
background: 'var(--color-surface-primary)',
|
|
31
|
+
borderColor: 'var(--color-border-secondary)',
|
|
32
|
+
color: 'var(--color-content-primary)',
|
|
33
|
+
'--tw-ring-color': 'var(--color-border-focus)',
|
|
34
|
+
} as React.CSSProperties}
|
|
35
|
+
>
|
|
36
|
+
<option value="">Show All</option>
|
|
37
|
+
{useCases.map((uc) => (
|
|
38
|
+
<option key={uc.id} value={uc.id}>
|
|
39
|
+
{uc.name}
|
|
40
|
+
</option>
|
|
41
|
+
))}
|
|
42
|
+
</select>
|
|
43
|
+
{selectedUseCase && (
|
|
44
|
+
<div className="mt-2">
|
|
45
|
+
<p className="text-xs" style={{ color: 'var(--color-content-secondary)' }}>
|
|
46
|
+
{useCases.find((uc) => uc.id === selectedUseCase)?.description}
|
|
47
|
+
</p>
|
|
48
|
+
<button
|
|
49
|
+
onClick={() => onSelect(null)}
|
|
50
|
+
className="mt-1.5 text-xs cursor-pointer hover:underline"
|
|
51
|
+
style={{ color: 'var(--color-interactive-primary)' }}
|
|
52
|
+
>
|
|
53
|
+
Clear filter
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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 ArchNode({ 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 solid ${selected ? 'var(--color-border-focus)' : colors.border}`,
|
|
18
|
+
borderRadius: 8,
|
|
19
|
+
padding: '8px 12px',
|
|
20
|
+
minWidth: 140,
|
|
21
|
+
maxWidth: 200,
|
|
22
|
+
cursor: 'pointer',
|
|
23
|
+
boxShadow: selected ? 'var(--shadow-node-selected)' : 'var(--shadow-node)',
|
|
24
|
+
transition: 'box-shadow 0.15s, border-color 0.15s',
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
<div style={{ fontSize: 11, color: colors.text, opacity: 0.7, marginBottom: 2 }}>
|
|
28
|
+
{icon} {d.category.toUpperCase()}
|
|
29
|
+
</div>
|
|
30
|
+
<div style={{ fontSize: 13, fontWeight: 600, color: colors.text, lineHeight: 1.3, wordBreak: 'break-word' }}>
|
|
31
|
+
{d.label}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<Handle type="source" position={Position.Bottom} style={{ background: colors.border }} />
|
|
35
|
+
</>
|
|
36
|
+
);
|
|
37
|
+
}
|