@neat.is/web 0.2.10

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.
@@ -0,0 +1,109 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useState } from 'react'
4
+ import { TopBar } from './TopBar'
5
+ import { Rail } from './Rail'
6
+ import { GraphCanvas } from './GraphCanvas'
7
+ import { Inspector } from './Inspector'
8
+ import { StatusBar } from './StatusBar'
9
+ import type { GraphNode, GraphEdge } from '@neat.is/types'
10
+
11
+ export interface GraphData {
12
+ nodes: GraphNode[]
13
+ edges: GraphEdge[]
14
+ }
15
+
16
+ interface ProjectEntry { name: string }
17
+
18
+ // ADR-057 #2 — resolution chain. URL → localStorage → first /projects → 'default'.
19
+ function readUrlProject(): string | null {
20
+ if (typeof window === 'undefined') return null
21
+ const v = new URLSearchParams(window.location.search).get('project')
22
+ return v && v.length > 0 ? v : null
23
+ }
24
+
25
+ function readStoredProject(): string | null {
26
+ if (typeof window === 'undefined') return null
27
+ try {
28
+ const v = window.localStorage.getItem('neat:lastProject')
29
+ return v && v.length > 0 ? v : null
30
+ } catch {
31
+ return null
32
+ }
33
+ }
34
+
35
+ export function AppShell() {
36
+ const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null)
37
+ const [graphData, setGraphData] = useState<GraphData | null>(null)
38
+ // ADR-057 #2 — start with URL or localStorage (synchronous), then resolve
39
+ // against /projects on mount if neither was set.
40
+ const [project, setProjectState] = useState<string>(() => {
41
+ return readUrlProject() ?? readStoredProject() ?? 'default'
42
+ })
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ const cyRef = useRef<any>(null)
45
+ const resolvedRef = useRef(readUrlProject() !== null || readStoredProject() !== null)
46
+
47
+ // ADR-057 #1, #4 — single source of truth + URL sync.
48
+ function setProject(name: string): void {
49
+ setProjectState(name)
50
+ if (typeof window === 'undefined') return
51
+ try {
52
+ window.localStorage.setItem('neat:lastProject', name)
53
+ } catch {
54
+ /* ignore quota errors */
55
+ }
56
+ const url = new URL(window.location.href)
57
+ url.searchParams.set('project', name)
58
+ window.history.replaceState({}, '', url)
59
+ }
60
+
61
+ // ADR-057 #2.3, #2.4 — if neither URL nor localStorage gave us a project,
62
+ // fetch /projects and use the first entry; fall back to 'default' if empty.
63
+ useEffect(() => {
64
+ if (resolvedRef.current) return
65
+ resolvedRef.current = true
66
+ fetch('/api/projects')
67
+ .then((r) => (r.ok ? r.json() : []))
68
+ .then((data: ProjectEntry[] | { projects?: ProjectEntry[] }) => {
69
+ const list = Array.isArray(data) ? data : Array.isArray(data?.projects) ? data.projects : []
70
+ if (list.length > 0 && list[0]?.name) {
71
+ setProject(list[0].name)
72
+ } else {
73
+ setProject('default')
74
+ }
75
+ })
76
+ .catch(() => {
77
+ /* registry unreachable — keep 'default' fallback */
78
+ })
79
+ }, [])
80
+
81
+ // Pre-select a node from the URL ?node= query param (e.g. from incidents back-link)
82
+ useEffect(() => {
83
+ const params = new URLSearchParams(window.location.search)
84
+ const nodeId = params.get('node')
85
+ if (nodeId) setSelectedNodeId(nodeId)
86
+ }, [])
87
+
88
+ return (
89
+ <div className="app">
90
+ <TopBar
91
+ project={project}
92
+ onProjectChange={setProject}
93
+ onNodeSelect={setSelectedNodeId}
94
+ onRelayout={() => cyRef.current?.layout({ name: 'cose', animate: true, randomize: false, idealEdgeLength: 90, nodeRepulsion: 9000, edgeElasticity: 80, gravity: 0.4, numIter: 1200 }).run()}
95
+ onToggleLock={() => { if (cyRef.current) cyRef.current.autoungrabify(!cyRef.current.autoungrabify()) }}
96
+ />
97
+ <Rail project={project} />
98
+ <GraphCanvas
99
+ project={project}
100
+ selectedNodeId={selectedNodeId}
101
+ onNodeSelect={setSelectedNodeId}
102
+ onGraphLoaded={setGraphData}
103
+ onCyReady={(cy) => { cyRef.current = cy }}
104
+ />
105
+ <Inspector project={project} selectedNodeId={selectedNodeId} graphData={graphData} />
106
+ <StatusBar project={project} graphData={graphData} />
107
+ </div>
108
+ )
109
+ }