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.
Files changed (103) hide show
  1. package/dist/commands/build.d.ts +2 -0
  2. package/dist/commands/build.d.ts.map +1 -0
  3. package/dist/commands/build.js +88 -0
  4. package/dist/commands/build.js.map +1 -0
  5. package/dist/commands/init.d.ts +2 -0
  6. package/dist/commands/init.d.ts.map +1 -0
  7. package/dist/commands/init.js +58 -0
  8. package/dist/commands/init.js.map +1 -0
  9. package/dist/commands/serve.d.ts +2 -0
  10. package/dist/commands/serve.d.ts.map +1 -0
  11. package/dist/commands/serve.js +27 -0
  12. package/dist/commands/serve.js.map +1 -0
  13. package/dist/index.d.ts +3 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +64 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/install/slash-commands.d.ts +6 -0
  18. package/dist/install/slash-commands.d.ts.map +1 -0
  19. package/dist/install/slash-commands.js +31 -0
  20. package/dist/install/slash-commands.js.map +1 -0
  21. package/dist/install/viewer.d.ts +5 -0
  22. package/dist/install/viewer.d.ts.map +1 -0
  23. package/dist/install/viewer.js +40 -0
  24. package/dist/install/viewer.js.map +1 -0
  25. package/dist/schema/architecture.schema.json +144 -0
  26. package/dist/templates/skeleton.json +15 -0
  27. package/dist/templates/slash-commands/claude/archrip-refine.md +17 -0
  28. package/dist/templates/slash-commands/claude/archrip-scan.md +140 -0
  29. package/dist/templates/slash-commands/claude/archrip-update.md +34 -0
  30. package/dist/templates/slash-commands/claude/archrips-refine.md +17 -0
  31. package/dist/templates/slash-commands/claude/archrips-scan.md +97 -0
  32. package/dist/templates/slash-commands/claude/archrips-update.md +18 -0
  33. package/dist/templates/slash-commands/codex/archrip-refine.md +17 -0
  34. package/dist/templates/slash-commands/codex/archrip-scan.md +140 -0
  35. package/dist/templates/slash-commands/codex/archrip-update.md +34 -0
  36. package/dist/templates/slash-commands/codex/archrips-refine.md +17 -0
  37. package/dist/templates/slash-commands/codex/archrips-scan.md +97 -0
  38. package/dist/templates/slash-commands/codex/archrips-update.md +18 -0
  39. package/dist/templates/slash-commands/gemini/archrip-refine.md +17 -0
  40. package/dist/templates/slash-commands/gemini/archrip-scan.md +140 -0
  41. package/dist/templates/slash-commands/gemini/archrip-update.md +34 -0
  42. package/dist/templates/slash-commands/gemini/archrips-refine.md +17 -0
  43. package/dist/templates/slash-commands/gemini/archrips-scan.md +97 -0
  44. package/dist/templates/slash-commands/gemini/archrips-update.md +18 -0
  45. package/dist/utils/detect-agents.d.ts +15 -0
  46. package/dist/utils/detect-agents.d.ts.map +1 -0
  47. package/dist/utils/detect-agents.js +29 -0
  48. package/dist/utils/detect-agents.js.map +1 -0
  49. package/dist/utils/gitignore.d.ts +5 -0
  50. package/dist/utils/gitignore.d.ts.map +1 -0
  51. package/dist/utils/gitignore.js +21 -0
  52. package/dist/utils/gitignore.js.map +1 -0
  53. package/dist/utils/layout.d.ts +17 -0
  54. package/dist/utils/layout.d.ts.map +1 -0
  55. package/dist/utils/layout.js +121 -0
  56. package/dist/utils/layout.js.map +1 -0
  57. package/dist/utils/layout.spec.d.ts +2 -0
  58. package/dist/utils/layout.spec.d.ts.map +1 -0
  59. package/dist/utils/layout.spec.js +176 -0
  60. package/dist/utils/layout.spec.js.map +1 -0
  61. package/dist/utils/paths.d.ts +3 -0
  62. package/dist/utils/paths.d.ts.map +1 -0
  63. package/dist/utils/paths.js +28 -0
  64. package/dist/utils/paths.js.map +1 -0
  65. package/dist/utils/project-info.d.ts +9 -0
  66. package/dist/utils/project-info.d.ts.map +1 -0
  67. package/dist/utils/project-info.js +86 -0
  68. package/dist/utils/project-info.js.map +1 -0
  69. package/dist/utils/validate.d.ts +88 -0
  70. package/dist/utils/validate.d.ts.map +1 -0
  71. package/dist/utils/validate.js +238 -0
  72. package/dist/utils/validate.js.map +1 -0
  73. package/dist/utils/validate.spec.d.ts +2 -0
  74. package/dist/utils/validate.spec.d.ts.map +1 -0
  75. package/dist/utils/validate.spec.js +424 -0
  76. package/dist/utils/validate.spec.js.map +1 -0
  77. package/dist/utils/verbose.d.ts +3 -0
  78. package/dist/utils/verbose.d.ts.map +1 -0
  79. package/dist/utils/verbose.js +8 -0
  80. package/dist/utils/verbose.js.map +1 -0
  81. package/dist/viewer-template/index.html +15 -0
  82. package/dist/viewer-template/package-lock.json +2714 -0
  83. package/dist/viewer-template/package.json +26 -0
  84. package/dist/viewer-template/src/App.tsx +168 -0
  85. package/dist/viewer-template/src/components/DepthFilter.tsx +43 -0
  86. package/dist/viewer-template/src/components/DetailPanel.tsx +261 -0
  87. package/dist/viewer-template/src/components/Legend.tsx +41 -0
  88. package/dist/viewer-template/src/components/ThemeToggle.tsx +37 -0
  89. package/dist/viewer-template/src/components/UseCaseFilter.tsx +59 -0
  90. package/dist/viewer-template/src/components/nodes/ArchNode.tsx +37 -0
  91. package/dist/viewer-template/src/data/loader.ts +140 -0
  92. package/dist/viewer-template/src/hooks/useArchitecture.ts +32 -0
  93. package/dist/viewer-template/src/hooks/useDepthFilter.ts +37 -0
  94. package/dist/viewer-template/src/hooks/useTheme.ts +39 -0
  95. package/dist/viewer-template/src/hooks/useUseCaseFilter.ts +56 -0
  96. package/dist/viewer-template/src/index.css +130 -0
  97. package/dist/viewer-template/src/main.tsx +13 -0
  98. package/dist/viewer-template/src/types.ts +125 -0
  99. package/dist/viewer-template/tsconfig.app.json +24 -0
  100. package/dist/viewer-template/tsconfig.json +7 -0
  101. package/dist/viewer-template/tsconfig.node.json +22 -0
  102. package/dist/viewer-template/vite.config.ts +7 -0
  103. 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
+ &times;
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
+ }