create-project-arch 1.0.0
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/README.md +58 -0
- package/dist/cli.js +232 -0
- package/dist/cli.test.js +8 -0
- package/package.json +29 -0
- package/templates/arch-ui/.arch/edges/decision_to_domain.json +4 -0
- package/templates/arch-ui/.arch/edges/milestone_to_task.json +4 -0
- package/templates/arch-ui/.arch/edges/task_to_decision.json +4 -0
- package/templates/arch-ui/.arch/edges/task_to_module.json +4 -0
- package/templates/arch-ui/.arch/graph.json +17 -0
- package/templates/arch-ui/.arch/nodes/decisions.json +4 -0
- package/templates/arch-ui/.arch/nodes/domains.json +4 -0
- package/templates/arch-ui/.arch/nodes/milestones.json +4 -0
- package/templates/arch-ui/.arch/nodes/modules.json +4 -0
- package/templates/arch-ui/.arch/nodes/tasks.json +4 -0
- package/templates/arch-ui/app/api/architecture/map/route.ts +13 -0
- package/templates/arch-ui/app/api/decisions/route.ts +23 -0
- package/templates/arch-ui/app/api/domain-docs/route.ts +89 -0
- package/templates/arch-ui/app/api/domains/route.ts +10 -0
- package/templates/arch-ui/app/api/graph/route.ts +16 -0
- package/templates/arch-ui/app/api/health/route.ts +44 -0
- package/templates/arch-ui/app/api/node-files/route.ts +173 -0
- package/templates/arch-ui/app/api/phases/route.ts +10 -0
- package/templates/arch-ui/app/api/route.ts +22 -0
- package/templates/arch-ui/app/api/search/route.ts +56 -0
- package/templates/arch-ui/app/api/task-doc/[taskId]/route.ts +60 -0
- package/templates/arch-ui/app/api/tasks/route.ts +36 -0
- package/templates/arch-ui/app/api/trace/file/route.ts +40 -0
- package/templates/arch-ui/app/api/trace/task/[taskId]/route.ts +12 -0
- package/templates/arch-ui/app/architecture/page.tsx +5 -0
- package/templates/arch-ui/app/globals.css +240 -0
- package/templates/arch-ui/app/health/page.tsx +48 -0
- package/templates/arch-ui/app/layout.tsx +19 -0
- package/templates/arch-ui/app/page.tsx +5 -0
- package/templates/arch-ui/app/work/page.tsx +265 -0
- package/templates/arch-ui/components/app-shell.tsx +171 -0
- package/templates/arch-ui/components/error-boundary.tsx +53 -0
- package/templates/arch-ui/components/graph/arch-node.tsx +77 -0
- package/templates/arch-ui/components/graph/build-graph-from-dataset.ts +196 -0
- package/templates/arch-ui/components/graph/build-initial-graph.ts +245 -0
- package/templates/arch-ui/components/graph/graph-context-menu.tsx +84 -0
- package/templates/arch-ui/components/graph/graph-doc-panel.tsx +46 -0
- package/templates/arch-ui/components/graph/graph-types.ts +82 -0
- package/templates/arch-ui/components/graph/use-auto-layout.ts +65 -0
- package/templates/arch-ui/components/graph/use-connection-validation.ts +62 -0
- package/templates/arch-ui/components/graph/use-flow-persistence.ts +48 -0
- package/templates/arch-ui/components/graph-canvas.tsx +670 -0
- package/templates/arch-ui/components/health-panel.tsx +49 -0
- package/templates/arch-ui/components/inspector-context.tsx +35 -0
- package/templates/arch-ui/components/inspector.tsx +895 -0
- package/templates/arch-ui/components/markdown-viewer.tsx +74 -0
- package/templates/arch-ui/components/sidebar.tsx +531 -0
- package/templates/arch-ui/components/topbar.tsx +187 -0
- package/templates/arch-ui/components/work-table.tsx +57 -0
- package/templates/arch-ui/components/workspace-context.tsx +274 -0
- package/templates/arch-ui/eslint.config.js +2 -0
- package/templates/arch-ui/global.d.ts +1 -0
- package/templates/arch-ui/lib/api.ts +93 -0
- package/templates/arch-ui/lib/arch-model.ts +113 -0
- package/templates/arch-ui/lib/graph-dataset.ts +756 -0
- package/templates/arch-ui/lib/graph-schema.ts +408 -0
- package/templates/arch-ui/lib/project-root.ts +52 -0
- package/templates/arch-ui/lib/types.ts +116 -0
- package/templates/arch-ui/next-env.d.ts +6 -0
- package/templates/arch-ui/next.config.js +17 -0
- package/templates/arch-ui/package.json +38 -0
- package/templates/arch-ui/postcss.config.mjs +6 -0
- package/templates/arch-ui/tailwind.config.ts +11 -0
- package/templates/arch-ui/tsconfig.json +21 -0
- package/templates/ui-package/eslint.config.mjs +4 -0
- package/templates/ui-package/package.json +26 -0
- package/templates/ui-package/src/accordion.tsx +10 -0
- package/templates/ui-package/src/badge.tsx +12 -0
- package/templates/ui-package/src/button.tsx +32 -0
- package/templates/ui-package/src/card.tsx +22 -0
- package/templates/ui-package/src/code.tsx +6 -0
- package/templates/ui-package/src/command.tsx +18 -0
- package/templates/ui-package/src/dialog.tsx +6 -0
- package/templates/ui-package/src/dropdown-menu.tsx +10 -0
- package/templates/ui-package/src/input.tsx +6 -0
- package/templates/ui-package/src/navigation-menu.tsx +6 -0
- package/templates/ui-package/src/scroll-area.tsx +6 -0
- package/templates/ui-package/src/select.tsx +6 -0
- package/templates/ui-package/src/separator.tsx +6 -0
- package/templates/ui-package/src/sheet.tsx +6 -0
- package/templates/ui-package/src/skeleton.tsx +6 -0
- package/templates/ui-package/src/table.tsx +26 -0
- package/templates/ui-package/src/tabs.tsx +14 -0
- package/templates/ui-package/src/toggle-group.tsx +10 -0
- package/templates/ui-package/src/utils.ts +3 -0
- package/templates/ui-package/tsconfig.json +10 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Code } from "@repo/ui/code";
|
|
4
|
+
import ReactMarkdown from "react-markdown";
|
|
5
|
+
|
|
6
|
+
type MarkdownViewerProps = {
|
|
7
|
+
markdown: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function MarkdownViewer({ markdown }: MarkdownViewerProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="rounded-xl border border-slate-600 bg-slate-950 p-3.5">
|
|
13
|
+
<ReactMarkdown
|
|
14
|
+
components={{
|
|
15
|
+
h1({ children }) {
|
|
16
|
+
return <h1 className="mb-3 mt-1 text-xl font-semibold text-slate-100">{children}</h1>;
|
|
17
|
+
},
|
|
18
|
+
h2({ children }) {
|
|
19
|
+
return <h2 className="mb-2 mt-4 text-lg font-semibold text-slate-100">{children}</h2>;
|
|
20
|
+
},
|
|
21
|
+
h3({ children }) {
|
|
22
|
+
return <h3 className="mb-2 mt-3 text-base font-semibold text-slate-100">{children}</h3>;
|
|
23
|
+
},
|
|
24
|
+
p({ children }) {
|
|
25
|
+
return <p className="mb-2 leading-relaxed text-slate-200">{children}</p>;
|
|
26
|
+
},
|
|
27
|
+
ul({ children }) {
|
|
28
|
+
return <ul className="mb-2 list-disc space-y-1 pl-5 text-slate-200">{children}</ul>;
|
|
29
|
+
},
|
|
30
|
+
ol({ children }) {
|
|
31
|
+
return <ol className="mb-2 list-decimal space-y-1 pl-5 text-slate-200">{children}</ol>;
|
|
32
|
+
},
|
|
33
|
+
li({ children }) {
|
|
34
|
+
return <li className="leading-relaxed">{children}</li>;
|
|
35
|
+
},
|
|
36
|
+
a({ href, children }) {
|
|
37
|
+
return (
|
|
38
|
+
<a
|
|
39
|
+
href={href}
|
|
40
|
+
className="text-blue-300 underline underline-offset-2 hover:text-blue-200"
|
|
41
|
+
target="_blank"
|
|
42
|
+
rel="noreferrer"
|
|
43
|
+
>
|
|
44
|
+
{children}
|
|
45
|
+
</a>
|
|
46
|
+
);
|
|
47
|
+
},
|
|
48
|
+
blockquote({ children }) {
|
|
49
|
+
return (
|
|
50
|
+
<blockquote className="my-2 border-l-2 border-slate-600 pl-3 italic text-slate-300">
|
|
51
|
+
{children}
|
|
52
|
+
</blockquote>
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
hr() {
|
|
56
|
+
return <hr className="my-3 border-slate-700" />;
|
|
57
|
+
},
|
|
58
|
+
pre({ children }) {
|
|
59
|
+
return (
|
|
60
|
+
<pre className="my-2 overflow-x-auto rounded-md border border-slate-700 bg-slate-900 p-2.5 text-xs text-slate-100">
|
|
61
|
+
{children}
|
|
62
|
+
</pre>
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
code({ children }) {
|
|
66
|
+
return <Code>{children}</Code>;
|
|
67
|
+
},
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{markdown}
|
|
71
|
+
</ReactMarkdown>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Badge } from "@repo/ui/badge";
|
|
4
|
+
import { NavigationMenu } from "@repo/ui/navigation-menu";
|
|
5
|
+
import {
|
|
6
|
+
ChevronDown,
|
|
7
|
+
ChevronRight,
|
|
8
|
+
FileText,
|
|
9
|
+
Folder,
|
|
10
|
+
FolderOpen,
|
|
11
|
+
FolderTree,
|
|
12
|
+
Boxes,
|
|
13
|
+
ListChecks,
|
|
14
|
+
Network,
|
|
15
|
+
Search,
|
|
16
|
+
} from "lucide-react";
|
|
17
|
+
import Link from "next/link";
|
|
18
|
+
import { usePathname, useSearchParams } from "next/navigation";
|
|
19
|
+
import { useEffect, useMemo, useState } from "react";
|
|
20
|
+
import { getDomainDocs } from "../lib/api";
|
|
21
|
+
import { GRAPH_VIEW_BEHAVIOR, GraphViewMode } from "../lib/graph-schema";
|
|
22
|
+
import { DomainDocsData } from "../lib/types";
|
|
23
|
+
import { useInspector } from "./inspector-context";
|
|
24
|
+
import { useWorkspace, WorkspaceView } from "./workspace-context";
|
|
25
|
+
|
|
26
|
+
const viewRoutes: Record<WorkspaceView, string> = {
|
|
27
|
+
"architecture-map": "/work?view=architecture",
|
|
28
|
+
decisions: "/architecture?view=decisions",
|
|
29
|
+
"tasks-roadmap": "/work?view=tasks",
|
|
30
|
+
"project-map": "/work?view=project",
|
|
31
|
+
};
|
|
32
|
+
const selectableViews: WorkspaceView[] = ["architecture-map", "tasks-roadmap", "project-map"];
|
|
33
|
+
|
|
34
|
+
const nodeTypeFilters = [
|
|
35
|
+
{ key: "domains", label: "Domains" },
|
|
36
|
+
{ key: "modules", label: "Modules" },
|
|
37
|
+
{ key: "tasks", label: "Tasks" },
|
|
38
|
+
{ key: "decisions", label: "Decisions" },
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
const edgeTypeFilters = [
|
|
42
|
+
{ key: "dependency", label: "Dependency" },
|
|
43
|
+
{ key: "data-flow", label: "Data Flow" },
|
|
44
|
+
{ key: "blocking", label: "Blocking" },
|
|
45
|
+
] as const;
|
|
46
|
+
const authorityFilters = [
|
|
47
|
+
{ key: "authoritative", label: "Authoritative" },
|
|
48
|
+
{ key: "manual", label: "Manual" },
|
|
49
|
+
{ key: "inferred", label: "Inferred" },
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
type TreeNode = {
|
|
53
|
+
key: string;
|
|
54
|
+
label: string;
|
|
55
|
+
kind: "domain-doc" | "folder";
|
|
56
|
+
children?: TreeNode[];
|
|
57
|
+
meta?: string;
|
|
58
|
+
id?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function humanizeSegment(value: string): string {
|
|
62
|
+
const cleaned = value.replace(/\.[a-z0-9]+$/i, "").replace(/[_-]+/g, " ");
|
|
63
|
+
return cleaned
|
|
64
|
+
.split(/\s+/)
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.map((part) => part[0]?.toUpperCase() + part.slice(1))
|
|
67
|
+
.join(" ");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sortTree(nodes: TreeNode[]): TreeNode[] {
|
|
71
|
+
const sorted = [...nodes].sort((a, b) => {
|
|
72
|
+
const aFolder = !!a.children?.length || a.kind === "folder";
|
|
73
|
+
const bFolder = !!b.children?.length || b.kind === "folder";
|
|
74
|
+
if (aFolder !== bFolder) return aFolder ? -1 : 1;
|
|
75
|
+
return a.label.localeCompare(b.label);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return sorted.map((node) => ({
|
|
79
|
+
...node,
|
|
80
|
+
children: node.children ? sortTree(node.children) : undefined,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildDocNodes(scope: DomainDocsData["docs"][number]["scope"], docs: DomainDocsData["docs"]): TreeNode[] {
|
|
85
|
+
const scopedDocs = docs.filter((doc) => doc.scope === scope);
|
|
86
|
+
const roots: TreeNode[] = [];
|
|
87
|
+
|
|
88
|
+
for (const doc of scopedDocs) {
|
|
89
|
+
const relativePath = doc.path.startsWith(`${scope}/`) ? doc.path.slice(scope.length + 1) : doc.path;
|
|
90
|
+
const parts = relativePath.split("/").filter(Boolean);
|
|
91
|
+
if (parts.length === 0) continue;
|
|
92
|
+
|
|
93
|
+
let cursor = roots;
|
|
94
|
+
let cursorPath: string = scope;
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
97
|
+
const part = parts[i] ?? "";
|
|
98
|
+
cursorPath = `${cursorPath}/${part}`;
|
|
99
|
+
let folderNode = cursor.find((node) => node.key === `folder:${cursorPath}`);
|
|
100
|
+
if (!folderNode) {
|
|
101
|
+
folderNode = {
|
|
102
|
+
key: `folder:${cursorPath}`,
|
|
103
|
+
label: humanizeSegment(part),
|
|
104
|
+
kind: "folder",
|
|
105
|
+
children: [],
|
|
106
|
+
meta: cursorPath,
|
|
107
|
+
};
|
|
108
|
+
cursor.push(folderNode);
|
|
109
|
+
}
|
|
110
|
+
if (!folderNode.children) folderNode.children = [];
|
|
111
|
+
cursor = folderNode.children;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const fileName = parts[parts.length - 1] ?? doc.file;
|
|
115
|
+
cursor.push({
|
|
116
|
+
key: `domain-doc:${doc.path}`,
|
|
117
|
+
label: doc.title || humanizeSegment(fileName),
|
|
118
|
+
kind: "domain-doc",
|
|
119
|
+
id: doc.path,
|
|
120
|
+
meta: doc.path,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return sortTree(roots);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function viewModeForWorkspaceView(view: WorkspaceView): GraphViewMode {
|
|
128
|
+
if (view === "tasks-roadmap") return "tasks";
|
|
129
|
+
if (view === "project-map") return "project";
|
|
130
|
+
return "architecture-map";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function Sidebar() {
|
|
134
|
+
const pathname = usePathname();
|
|
135
|
+
const searchParams = useSearchParams();
|
|
136
|
+
const { setSelection } = useInspector();
|
|
137
|
+
const {
|
|
138
|
+
leftCollapsed,
|
|
139
|
+
setLeftCollapsed,
|
|
140
|
+
filters,
|
|
141
|
+
toggleNodeType,
|
|
142
|
+
toggleEdgeType,
|
|
143
|
+
toggleAuthorityType,
|
|
144
|
+
setHideCompletedTasks,
|
|
145
|
+
setShowExternalDependencies,
|
|
146
|
+
setHopDepth,
|
|
147
|
+
} = useWorkspace();
|
|
148
|
+
const [domainDocs, setDomainDocs] = useState<DomainDocsData["docs"]>([]);
|
|
149
|
+
const [expandedKeys, setExpandedKeys] = useState<Set<string>>(
|
|
150
|
+
new Set([
|
|
151
|
+
"domain-docs-root",
|
|
152
|
+
"arch-model-docs-root",
|
|
153
|
+
"architecture-docs-root",
|
|
154
|
+
"roadmap-docs-root",
|
|
155
|
+
]),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
void getDomainDocs()
|
|
160
|
+
.then((result) => setDomainDocs(result.docs))
|
|
161
|
+
.catch(() => setDomainDocs([]));
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
const activeView = useMemo<WorkspaceView>(() => {
|
|
165
|
+
if (pathname === "/work" && searchParams.get("view") === "architecture") return "architecture-map";
|
|
166
|
+
if (pathname === "/work" && searchParams.get("view") === "project") return "project-map";
|
|
167
|
+
if (pathname === "/work") return "tasks-roadmap";
|
|
168
|
+
const view = searchParams.get("view");
|
|
169
|
+
if (pathname === "/architecture" && view === "decisions") return "decisions";
|
|
170
|
+
return "architecture-map";
|
|
171
|
+
}, [pathname, searchParams]);
|
|
172
|
+
|
|
173
|
+
const activeGraphView = useMemo(() => viewModeForWorkspaceView(activeView), [activeView]);
|
|
174
|
+
const hopDepthBounds = useMemo(() => {
|
|
175
|
+
const allowed = GRAPH_VIEW_BEHAVIOR[activeGraphView].allowedHopDepths;
|
|
176
|
+
return {
|
|
177
|
+
min: Math.min(...allowed),
|
|
178
|
+
max: Math.max(...allowed),
|
|
179
|
+
};
|
|
180
|
+
}, [activeGraphView]);
|
|
181
|
+
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (filters.hopDepth < hopDepthBounds.min) {
|
|
184
|
+
setHopDepth(hopDepthBounds.min);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (filters.hopDepth > hopDepthBounds.max) {
|
|
188
|
+
setHopDepth(hopDepthBounds.max);
|
|
189
|
+
}
|
|
190
|
+
}, [filters.hopDepth, hopDepthBounds.max, hopDepthBounds.min, setHopDepth]);
|
|
191
|
+
|
|
192
|
+
const tree = useMemo<TreeNode[]>(() => {
|
|
193
|
+
const domainDocNodes = buildDocNodes("arch-domains", domainDocs);
|
|
194
|
+
const modelDocNodes = buildDocNodes("arch-model", domainDocs);
|
|
195
|
+
const architectureDocNodes = buildDocNodes("architecture", domainDocs);
|
|
196
|
+
const roadmapDocNodes = buildDocNodes("roadmap", domainDocs);
|
|
197
|
+
|
|
198
|
+
return [
|
|
199
|
+
{
|
|
200
|
+
key: "domain-docs-root",
|
|
201
|
+
label: "Domain Docs",
|
|
202
|
+
kind: "folder",
|
|
203
|
+
children: domainDocNodes,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
key: "arch-model-docs-root",
|
|
207
|
+
label: "Arch Model",
|
|
208
|
+
kind: "folder",
|
|
209
|
+
children: modelDocNodes,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
key: "architecture-docs-root",
|
|
213
|
+
label: "Architecture Docs",
|
|
214
|
+
kind: "folder",
|
|
215
|
+
children: architectureDocNodes,
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
key: "roadmap-docs-root",
|
|
219
|
+
label: "Roadmap",
|
|
220
|
+
kind: "folder",
|
|
221
|
+
children: roadmapDocNodes,
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
}, [domainDocs]);
|
|
225
|
+
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
setExpandedKeys((prev) => {
|
|
228
|
+
const next = new Set(prev);
|
|
229
|
+
if (!tree.some((node) => next.has(node.key))) {
|
|
230
|
+
tree.forEach((node) => {
|
|
231
|
+
if (node.children?.length) next.add(node.key);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return next;
|
|
235
|
+
});
|
|
236
|
+
}, [tree]);
|
|
237
|
+
|
|
238
|
+
function toggleExpanded(key: string) {
|
|
239
|
+
setExpandedKeys((prev) => {
|
|
240
|
+
const next = new Set(prev);
|
|
241
|
+
if (next.has(key)) {
|
|
242
|
+
next.delete(key);
|
|
243
|
+
} else {
|
|
244
|
+
next.add(key);
|
|
245
|
+
}
|
|
246
|
+
return next;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function selectTreeNode(node: TreeNode) {
|
|
251
|
+
if (node.kind === "folder" || (node.children?.length && !node.id)) {
|
|
252
|
+
toggleExpanded(node.key);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (node.kind === "domain-doc") {
|
|
256
|
+
setSelection({
|
|
257
|
+
type: "file",
|
|
258
|
+
title: node.label,
|
|
259
|
+
id: node.id ?? node.label,
|
|
260
|
+
metadata: [{ label: "Path", value: node.meta ?? "arch-domains" }],
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function iconForNode(node: TreeNode, expanded: boolean) {
|
|
267
|
+
if (node.kind === "folder") {
|
|
268
|
+
return expanded ? <FolderOpen size={14} /> : <Folder size={14} />;
|
|
269
|
+
}
|
|
270
|
+
if (node.kind === "domain-doc") return <FileText size={14} />;
|
|
271
|
+
return <FileText size={14} />;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function renderTree(nodes: TreeNode[], depth = 0) {
|
|
275
|
+
return nodes.map((node) => (
|
|
276
|
+
<div key={node.key} className="grid gap-0.5" style={{ paddingLeft: `${depth * 0.5}px` }}>
|
|
277
|
+
<div className="group flex items-center gap-1 rounded px-1 py-0.5 hover:bg-slate-800/70">
|
|
278
|
+
{node.children?.length ? (
|
|
279
|
+
<button
|
|
280
|
+
type="button"
|
|
281
|
+
className="grid h-4 w-4 place-items-center text-slate-400 hover:text-slate-200"
|
|
282
|
+
onClick={(event) => {
|
|
283
|
+
event.stopPropagation();
|
|
284
|
+
toggleExpanded(node.key);
|
|
285
|
+
}}
|
|
286
|
+
aria-label={`Toggle ${node.label}`}
|
|
287
|
+
>
|
|
288
|
+
{expandedKeys.has(node.key) ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
289
|
+
</button>
|
|
290
|
+
) : (
|
|
291
|
+
<span className="h-4 w-4" />
|
|
292
|
+
)}
|
|
293
|
+
<span className="text-slate-300">
|
|
294
|
+
{iconForNode(node, expandedKeys.has(node.key))}
|
|
295
|
+
</span>
|
|
296
|
+
<button
|
|
297
|
+
type="button"
|
|
298
|
+
className="min-w-0 flex-1 truncate text-left text-[12px] text-slate-200"
|
|
299
|
+
onClick={() => selectTreeNode(node)}
|
|
300
|
+
title={node.meta ? `${node.label} - ${node.meta}` : node.label}
|
|
301
|
+
>
|
|
302
|
+
{node.label}
|
|
303
|
+
</button>
|
|
304
|
+
{node.meta ? (
|
|
305
|
+
<span className="max-w-[40%] truncate text-[10px] text-slate-500">{node.meta}</span>
|
|
306
|
+
) : null}
|
|
307
|
+
</div>
|
|
308
|
+
{node.children?.length && expandedKeys.has(node.key) ? (
|
|
309
|
+
<div className="ml-1 border-l border-slate-700/70 pl-1">
|
|
310
|
+
{renderTree(node.children, depth + 1)}
|
|
311
|
+
</div>
|
|
312
|
+
) : null}
|
|
313
|
+
</div>
|
|
314
|
+
));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (leftCollapsed) {
|
|
318
|
+
return (
|
|
319
|
+
<aside className="grid h-full min-h-0 content-start justify-items-center gap-2 overflow-y-auto border-r border-slate-800 bg-slate-950/85 p-2">
|
|
320
|
+
<button
|
|
321
|
+
className="rounded-lg border border-slate-600 px-3 py-5 text-sm"
|
|
322
|
+
type="button"
|
|
323
|
+
onClick={() => setLeftCollapsed(false)}
|
|
324
|
+
title="Expand sidebar (Ctrl/Cmd+B)"
|
|
325
|
+
>
|
|
326
|
+
<FolderTree size={14} />
|
|
327
|
+
</button>
|
|
328
|
+
{selectableViews.map((view) => {
|
|
329
|
+
const isActive = activeView === view;
|
|
330
|
+
return (
|
|
331
|
+
<Link
|
|
332
|
+
key={view}
|
|
333
|
+
href={viewRoutes[view]}
|
|
334
|
+
title={
|
|
335
|
+
view === "architecture-map"
|
|
336
|
+
? "Architecture Map"
|
|
337
|
+
: view === "tasks-roadmap"
|
|
338
|
+
? "Tasks / Roadmap"
|
|
339
|
+
: view === "project-map"
|
|
340
|
+
? "Project"
|
|
341
|
+
: "Decisions"
|
|
342
|
+
}
|
|
343
|
+
className={
|
|
344
|
+
isActive
|
|
345
|
+
? "rounded-lg border border-blue-700 bg-slate-800 p-2 text-slate-100"
|
|
346
|
+
: "rounded-lg border border-transparent p-2 text-slate-300 hover:border-slate-700 hover:bg-slate-900"
|
|
347
|
+
}
|
|
348
|
+
>
|
|
349
|
+
{view === "architecture-map" ? <Network size={16} /> : null}
|
|
350
|
+
{view === "tasks-roadmap" ? <ListChecks size={16} /> : null}
|
|
351
|
+
{view === "project-map" ? <Boxes size={16} /> : null}
|
|
352
|
+
</Link>
|
|
353
|
+
);
|
|
354
|
+
})}
|
|
355
|
+
</aside>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<aside className="h-full min-h-0 overflow-y-auto border-r border-slate-800 bg-slate-950/85 p-4">
|
|
361
|
+
<div className="mb-4">
|
|
362
|
+
<p className="m-0 text-[11px] uppercase tracking-[0.1em] text-slate-400">Architecture</p>
|
|
363
|
+
<div className="mt-1 flex flex-wrap items-center gap-2">
|
|
364
|
+
<p className="m-0 text-base font-semibold">Control Panel</p>
|
|
365
|
+
<button
|
|
366
|
+
className="rounded-lg border border-transparent px-3 py-1.5 text-sm hover:border-slate-700"
|
|
367
|
+
type="button"
|
|
368
|
+
title="Collapse sidebar (Ctrl/Cmd+B)"
|
|
369
|
+
onClick={() => setLeftCollapsed(true)}
|
|
370
|
+
>
|
|
371
|
+
Collapse
|
|
372
|
+
</button>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<div className="mb-4 grid gap-2 rounded-xl border border-slate-700 bg-slate-900/90 p-3">
|
|
377
|
+
<p className="m-0 text-[11px] uppercase tracking-[0.1em] text-slate-400">Views</p>
|
|
378
|
+
<div className="flex flex-wrap gap-2">
|
|
379
|
+
{selectableViews.map((view) => (
|
|
380
|
+
<Link
|
|
381
|
+
key={view}
|
|
382
|
+
href={viewRoutes[view]}
|
|
383
|
+
className={
|
|
384
|
+
activeView === view
|
|
385
|
+
? "flex items-center gap-2 rounded-lg border border-blue-700 bg-slate-800 px-3 py-2 text-sm text-slate-200"
|
|
386
|
+
: "flex items-center gap-2 rounded-lg border border-transparent px-3 py-2 text-sm text-slate-300 hover:border-slate-700 hover:bg-slate-900"
|
|
387
|
+
}
|
|
388
|
+
>
|
|
389
|
+
{view === "architecture-map" ? <Network size={14} /> : null}
|
|
390
|
+
{view === "tasks-roadmap" ? <ListChecks size={14} /> : null}
|
|
391
|
+
{view === "project-map" ? <Boxes size={14} /> : null}
|
|
392
|
+
<span>
|
|
393
|
+
{view === "architecture-map"
|
|
394
|
+
? "Architecture Map"
|
|
395
|
+
: view === "tasks-roadmap"
|
|
396
|
+
? "Tasks / Roadmap"
|
|
397
|
+
: view === "project-map"
|
|
398
|
+
? "Project"
|
|
399
|
+
: "Decisions"}
|
|
400
|
+
</span>
|
|
401
|
+
</Link>
|
|
402
|
+
))}
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
<NavigationMenu>
|
|
407
|
+
<section className="mb-4">
|
|
408
|
+
<p className="mb-2 text-[11px] uppercase tracking-[0.1em] text-slate-400">Unified Explorer</p>
|
|
409
|
+
{tree.length > 0 ? renderTree(tree) : <p className="text-sm text-slate-400">Loading explorer...</p>}
|
|
410
|
+
</section>
|
|
411
|
+
<section className="mb-4">
|
|
412
|
+
<p className="mb-2 text-[11px] uppercase tracking-[0.1em] text-slate-400">Node Filters</p>
|
|
413
|
+
<div className="flex flex-wrap gap-2">
|
|
414
|
+
{nodeTypeFilters.map(({ key, label }) => (
|
|
415
|
+
<button
|
|
416
|
+
key={key}
|
|
417
|
+
className={
|
|
418
|
+
filters.nodeTypes[key]
|
|
419
|
+
? "rounded-lg border border-blue-700 bg-blue-700 px-3 py-2 text-sm"
|
|
420
|
+
: "rounded-lg border border-slate-600 px-3 py-2 text-sm"
|
|
421
|
+
}
|
|
422
|
+
type="button"
|
|
423
|
+
onClick={() => toggleNodeType(key)}
|
|
424
|
+
>
|
|
425
|
+
{label}
|
|
426
|
+
</button>
|
|
427
|
+
))}
|
|
428
|
+
</div>
|
|
429
|
+
</section>
|
|
430
|
+
<section className="mb-4">
|
|
431
|
+
<p className="mb-2 text-[11px] uppercase tracking-[0.1em] text-slate-400">Status</p>
|
|
432
|
+
<label className="flex items-center gap-2 text-sm text-slate-400">
|
|
433
|
+
<input
|
|
434
|
+
type="checkbox"
|
|
435
|
+
checked={filters.hideCompletedTasks}
|
|
436
|
+
onChange={(event) => setHideCompletedTasks(event.target.checked)}
|
|
437
|
+
/>
|
|
438
|
+
Hide Completed Tasks
|
|
439
|
+
</label>
|
|
440
|
+
<label className="mt-2 flex items-center gap-2 text-sm text-slate-400">
|
|
441
|
+
<input
|
|
442
|
+
type="checkbox"
|
|
443
|
+
checked={filters.showExternalDependencies}
|
|
444
|
+
onChange={(event) => setShowExternalDependencies(event.target.checked)}
|
|
445
|
+
/>
|
|
446
|
+
Show External Dependencies
|
|
447
|
+
</label>
|
|
448
|
+
<label className="mt-2 grid gap-1 text-sm text-slate-400">
|
|
449
|
+
<span>Hop Depth</span>
|
|
450
|
+
<input
|
|
451
|
+
type="range"
|
|
452
|
+
min={hopDepthBounds.min}
|
|
453
|
+
max={hopDepthBounds.max}
|
|
454
|
+
value={filters.hopDepth}
|
|
455
|
+
onChange={(event) => setHopDepth(Number(event.target.value))}
|
|
456
|
+
/>
|
|
457
|
+
<span className="text-xs text-slate-500">
|
|
458
|
+
Current: {filters.hopDepth} (allowed {hopDepthBounds.min}-{hopDepthBounds.max})
|
|
459
|
+
</span>
|
|
460
|
+
</label>
|
|
461
|
+
</section>
|
|
462
|
+
<section className="mb-4">
|
|
463
|
+
<p className="mb-2 text-[11px] uppercase tracking-[0.1em] text-slate-400">Edge Filters</p>
|
|
464
|
+
<div className="flex flex-wrap gap-2">
|
|
465
|
+
{edgeTypeFilters.map(({ key, label }) => (
|
|
466
|
+
<button
|
|
467
|
+
key={key}
|
|
468
|
+
className={
|
|
469
|
+
filters.edgeTypes[key]
|
|
470
|
+
? "rounded-lg border border-slate-700 bg-slate-700 px-3 py-2 text-sm"
|
|
471
|
+
: "rounded-lg border border-slate-600 px-3 py-2 text-sm"
|
|
472
|
+
}
|
|
473
|
+
type="button"
|
|
474
|
+
onClick={() => toggleEdgeType(key)}
|
|
475
|
+
>
|
|
476
|
+
{label}
|
|
477
|
+
</button>
|
|
478
|
+
))}
|
|
479
|
+
</div>
|
|
480
|
+
<Badge variant="secondary">
|
|
481
|
+
{pathname === "/work" ? "Applied in active view" : "Stored for active view"}
|
|
482
|
+
</Badge>
|
|
483
|
+
</section>
|
|
484
|
+
<section className="mb-4">
|
|
485
|
+
<p className="mb-2 text-[11px] uppercase tracking-[0.1em] text-slate-400">
|
|
486
|
+
Edge Authority
|
|
487
|
+
</p>
|
|
488
|
+
<div className="flex flex-wrap gap-2">
|
|
489
|
+
{authorityFilters.map(({ key, label }) => (
|
|
490
|
+
<button
|
|
491
|
+
key={key}
|
|
492
|
+
className={
|
|
493
|
+
filters.authorityTypes[key]
|
|
494
|
+
? "rounded-lg border border-slate-700 bg-slate-700 px-3 py-2 text-sm"
|
|
495
|
+
: "rounded-lg border border-slate-600 px-3 py-2 text-sm"
|
|
496
|
+
}
|
|
497
|
+
type="button"
|
|
498
|
+
onClick={() => toggleAuthorityType(key)}
|
|
499
|
+
>
|
|
500
|
+
{label}
|
|
501
|
+
</button>
|
|
502
|
+
))}
|
|
503
|
+
</div>
|
|
504
|
+
</section>
|
|
505
|
+
<section className="mb-4">
|
|
506
|
+
<p className="mb-2 text-[11px] uppercase tracking-[0.1em] text-slate-400">Quick Links</p>
|
|
507
|
+
<div className="grid gap-1">
|
|
508
|
+
<Link
|
|
509
|
+
href="/work?view=architecture"
|
|
510
|
+
className={
|
|
511
|
+
pathname === "/work" && searchParams.get("view") === "architecture"
|
|
512
|
+
? "flex items-center gap-2 rounded-lg border border-slate-600 bg-slate-800 px-3 py-2 text-sm"
|
|
513
|
+
: "flex items-center gap-2 rounded-lg border border-transparent px-3 py-2 text-sm hover:border-slate-700 hover:bg-slate-900"
|
|
514
|
+
}
|
|
515
|
+
>
|
|
516
|
+
<Network size={14} />
|
|
517
|
+
<span>Map</span>
|
|
518
|
+
</Link>
|
|
519
|
+
<Link
|
|
520
|
+
href="/health?view=trace"
|
|
521
|
+
className="flex items-center gap-2 rounded-lg border border-transparent px-3 py-2 text-sm hover:border-slate-700 hover:bg-slate-900"
|
|
522
|
+
>
|
|
523
|
+
<Search size={14} />
|
|
524
|
+
<span>Trace</span>
|
|
525
|
+
</Link>
|
|
526
|
+
</div>
|
|
527
|
+
</section>
|
|
528
|
+
</NavigationMenu>
|
|
529
|
+
</aside>
|
|
530
|
+
);
|
|
531
|
+
}
|