prism-viz-engine 0.1.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 +104 -0
- package/package.json +82 -0
- package/scripts/emit-screen.mjs +192 -0
- package/src/core/json-canvas.ts +301 -0
- package/src/core/layer-roles.ts +77 -0
- package/src/core/motion.ts +390 -0
- package/src/core/mount.ts +130 -0
- package/src/index.ts +55 -0
- package/src/layers/01-generate/archify-ir.ts +234 -0
- package/src/layers/02-render/Canvas.tsx +211 -0
- package/src/layers/02-render/ComponentNode.tsx +103 -0
- package/src/layers/02-render/IsometricView.tsx +288 -0
- package/src/layers/02-render/icons.ts +199 -0
- package/src/layers/02-render/isometric.ts +149 -0
- package/src/layers/02-render/route.ts +297 -0
- package/src/layers/03-substrate/harvest-adapter.ts +161 -0
- package/src/layers/04-shell/Gallery.tsx +246 -0
- package/src/layers/04-shell/Inspector.tsx +153 -0
- package/src/layers/04-shell/Palette.tsx +149 -0
- package/src/layers/04-shell/Shell.tsx +306 -0
- package/src/layers/04-shell/mount-react.tsx +42 -0
- package/src/main.tsx +108 -0
- package/src/styles.css +410 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 04 — the interactive shell. The Waku mold, wired.
|
|
3
|
+
*
|
|
4
|
+
* waku-agent is on the shelf as "the model": a click-any-box LIVE architecture diagram
|
|
5
|
+
* where every box is a real module file. That is what this shell is — palette left,
|
|
6
|
+
* canvas centre, inspector right, and every box opens its real source.
|
|
7
|
+
*
|
|
8
|
+
* The renderer is NOT a user toggle. Layer 02's own rule is "the right canvas per
|
|
9
|
+
* shape": an infra topology routes to isometric, a process chain routes to the node
|
|
10
|
+
* graph. `route.ts` decides from the IR's declared `diagram_type`, and the decision is
|
|
11
|
+
* shown in the bar so the routing is legible rather than magic. An override exists for
|
|
12
|
+
* when the shape is genuinely ambiguous — it never silently overrules the declaration.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useCallback, useEffect, useMemo, useState } from "react"
|
|
16
|
+
import {
|
|
17
|
+
Canvas,
|
|
18
|
+
applyNodeChanges,
|
|
19
|
+
applyEdgeChanges,
|
|
20
|
+
addEdge,
|
|
21
|
+
LANE_H,
|
|
22
|
+
type Node,
|
|
23
|
+
type Edge,
|
|
24
|
+
type NodeChange,
|
|
25
|
+
type EdgeChange,
|
|
26
|
+
type Connection,
|
|
27
|
+
} from "../02-render/Canvas"
|
|
28
|
+
import { IsometricView } from "../02-render/IsometricView"
|
|
29
|
+
import { routeCanvas, resolveRenderer, IMPLEMENTED, type RendererId } from "../02-render/route"
|
|
30
|
+
import type { ComponentNodeData } from "../02-render/ComponentNode"
|
|
31
|
+
import { Palette } from "./Palette"
|
|
32
|
+
import { Inspector } from "./Inspector"
|
|
33
|
+
import { Gallery } from "./Gallery"
|
|
34
|
+
import {
|
|
35
|
+
type JSONCanvas,
|
|
36
|
+
type CanvasNode,
|
|
37
|
+
type GriotNodeMeta,
|
|
38
|
+
serialize,
|
|
39
|
+
validate,
|
|
40
|
+
explainViolations,
|
|
41
|
+
emptyCanvas,
|
|
42
|
+
} from "../../core/json-canvas"
|
|
43
|
+
import { ALL_SLOTS, type LayerSlot } from "../../core/layer-roles"
|
|
44
|
+
import { drive, type MountOptions, type VizHost } from "../../core/mount"
|
|
45
|
+
import { motionCssVars, type MotionMode } from "../../core/motion"
|
|
46
|
+
|
|
47
|
+
export interface VizSource {
|
|
48
|
+
id: string
|
|
49
|
+
label: string
|
|
50
|
+
note?: string
|
|
51
|
+
canvas: JSONCanvas
|
|
52
|
+
/** archify's declared diagram_type, when the source came from an IR. */
|
|
53
|
+
diagramType?: string
|
|
54
|
+
/** Sources with a palette are composable; example diagrams are view-only. */
|
|
55
|
+
composable?: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ShellProps {
|
|
59
|
+
host: VizHost
|
|
60
|
+
library: CanvasNode[]
|
|
61
|
+
sources?: VizSource[]
|
|
62
|
+
onChange?: MountOptions["onChange"]
|
|
63
|
+
reveal?: MountOptions["reveal"]
|
|
64
|
+
subscribeTrace?: MountOptions["subscribeTrace"]
|
|
65
|
+
registerHandle?: (h: { load: (c: JSONCanvas) => void; snapshot: () => JSONCanvas }) => void
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const RENDERER_LABEL: Record<RendererId, string> = {
|
|
69
|
+
isometric: "isometric",
|
|
70
|
+
nodegraph: "node-graph",
|
|
71
|
+
forcegraph: "prism-graph 3D",
|
|
72
|
+
excalidraw: "excalidraw",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const toFlowNode = (
|
|
76
|
+
n: CanvasNode,
|
|
77
|
+
reveal: ShellProps["reveal"],
|
|
78
|
+
tracing: boolean
|
|
79
|
+
): Node<ComponentNodeData> => ({
|
|
80
|
+
id: n.id,
|
|
81
|
+
type: "griot",
|
|
82
|
+
position: { x: n.x, y: n.y },
|
|
83
|
+
data: { label: (n as any).label ?? n.id, griot: n.griot as GriotNodeMeta, reveal, tracing },
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
export function Shell({ host, library, sources = [], onChange, reveal, subscribeTrace, registerHandle }: ShellProps) {
|
|
87
|
+
const [sourceId, setSourceId] = useState<string>(sources[0]?.id ?? "")
|
|
88
|
+
const [nodes, setNodes] = useState<Node<ComponentNodeData>[]>([])
|
|
89
|
+
const [edges, setEdges] = useState<Edge[]>([])
|
|
90
|
+
const [selected, setSelected] = useState<string | null>(null)
|
|
91
|
+
const [tracingIds, setTracingIds] = useState<Set<string>>(new Set())
|
|
92
|
+
const [override, setOverride] = useState<RendererId | null>(null)
|
|
93
|
+
/**
|
|
94
|
+
* `reveal`, not `none`.
|
|
95
|
+
*
|
|
96
|
+
* animation.md's default is `none`, and that is right for a PUBLISHED figure — it
|
|
97
|
+
* protects a reader who never asked for motion. This engine is not publishing; it is an
|
|
98
|
+
* instrument for comparing how systems move. Defaulting to silence means comparing still
|
|
99
|
+
* frames of things whose value IS the movement, which is most of what Lanshu is.
|
|
100
|
+
*
|
|
101
|
+
* `reveal` is animation.md's own sanctioned autoplay — one deterministic run that ends
|
|
102
|
+
* complete, never restarting on viewport re-entry. So this honours the law and still
|
|
103
|
+
* lets you see the thing. prefers-reduced-motion still wins over all of it.
|
|
104
|
+
*/
|
|
105
|
+
const [motionMode, setMotionMode] = useState<MotionMode>("reveal")
|
|
106
|
+
const [status, setStatus] = useState("")
|
|
107
|
+
// The reference gallery is a MODE, not a source: those files are rendered HTML, not IR,
|
|
108
|
+
// so they can never be a canvas. Making them a fake source would be a category error.
|
|
109
|
+
const [gallery, setGallery] = useState(false)
|
|
110
|
+
|
|
111
|
+
const source = useMemo(() => sources.find((s) => s.id === sourceId) ?? sources[0], [sources, sourceId])
|
|
112
|
+
const composable = source?.composable ?? false
|
|
113
|
+
|
|
114
|
+
const load = useCallback(
|
|
115
|
+
(c: JSONCanvas) => {
|
|
116
|
+
setNodes(c.nodes.filter((n) => n.griot).map((n) => toFlowNode(n, reveal, false)))
|
|
117
|
+
setEdges(c.edges.map((e) => ({ id: e.id, source: e.fromNode, target: e.toNode, label: e.label })))
|
|
118
|
+
},
|
|
119
|
+
[reveal]
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
// A composable source starts empty (palette is the shelf); an example opens rendered.
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (!source) return
|
|
125
|
+
setSelected(null)
|
|
126
|
+
setOverride(null)
|
|
127
|
+
load(source.composable ? emptyCanvas() : source.canvas)
|
|
128
|
+
}, [source, load])
|
|
129
|
+
|
|
130
|
+
const snapshot = useCallback((): JSONCanvas => {
|
|
131
|
+
const pool = source?.composable ? library : (source?.canvas.nodes ?? [])
|
|
132
|
+
return {
|
|
133
|
+
nodes: nodes.map((n) => {
|
|
134
|
+
const base = pool.find((l) => l.id === n.id)
|
|
135
|
+
return {
|
|
136
|
+
...(base ?? ({ id: n.id, type: "text", text: n.data.label } as any)),
|
|
137
|
+
id: n.id,
|
|
138
|
+
x: Math.round(n.position.x),
|
|
139
|
+
y: Math.round(n.position.y),
|
|
140
|
+
width: base?.width ?? 232,
|
|
141
|
+
height: base?.height ?? 96,
|
|
142
|
+
griot: n.data.griot,
|
|
143
|
+
label: n.data.label,
|
|
144
|
+
} as CanvasNode
|
|
145
|
+
}),
|
|
146
|
+
edges: edges.map((e) => ({
|
|
147
|
+
id: e.id,
|
|
148
|
+
fromNode: e.source,
|
|
149
|
+
fromSide: "bottom" as const,
|
|
150
|
+
toNode: e.target,
|
|
151
|
+
toSide: "top" as const,
|
|
152
|
+
label: typeof e.label === "string" ? e.label : undefined,
|
|
153
|
+
})),
|
|
154
|
+
}
|
|
155
|
+
}, [nodes, edges, library, source])
|
|
156
|
+
|
|
157
|
+
useEffect(() => { registerHandle?.({ load, snapshot }) }, [registerHandle, load, snapshot])
|
|
158
|
+
useEffect(() => { onChange?.(snapshot()) }, [nodes, edges]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
159
|
+
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (!subscribeTrace) return
|
|
162
|
+
return subscribeTrace((nodeId) => {
|
|
163
|
+
setTracingIds((p) => new Set(p).add(nodeId))
|
|
164
|
+
setTimeout(() => setTracingIds((p) => { const n = new Set(p); n.delete(nodeId); return n }), 1400)
|
|
165
|
+
})
|
|
166
|
+
}, [subscribeTrace])
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
setNodes((ns) => ns.map((n) => (n.data.tracing === tracingIds.has(n.id) ? n : { ...n, data: { ...n.data, tracing: tracingIds.has(n.id) } })))
|
|
170
|
+
}, [tracingIds])
|
|
171
|
+
|
|
172
|
+
const onNodesChange = useCallback((c: NodeChange[]) => setNodes((ns) => applyNodeChanges(c, ns) as Node<ComponentNodeData>[]), [])
|
|
173
|
+
const onEdgesChange = useCallback((c: EdgeChange[]) => setEdges((es) => applyEdgeChanges(c, es)), [])
|
|
174
|
+
const onConnect = useCallback((c: Connection) => setEdges((es) => addEdge({ ...c, id: `${c.source}->${c.target}` }, es)), [])
|
|
175
|
+
|
|
176
|
+
const onReroute = useCallback((nodeId: string, layer: LayerSlot) => {
|
|
177
|
+
setNodes((ns) => ns.map((n) => (n.id === nodeId ? { ...n, data: { ...n.data, griot: { ...n.data.griot, layer } } } : n)))
|
|
178
|
+
setStatus(`re-routed → ${layer}`)
|
|
179
|
+
}, [])
|
|
180
|
+
|
|
181
|
+
const onDropComponent = useCallback((payload: string, position: { x: number; y: number }, layer: LayerSlot) => {
|
|
182
|
+
let node: CanvasNode
|
|
183
|
+
try { node = JSON.parse(payload) } catch { return }
|
|
184
|
+
setNodes((ns) => {
|
|
185
|
+
if (ns.some((n) => n.id === node.id)) return ns
|
|
186
|
+
const griot = { ...(node.griot as GriotNodeMeta), layer }
|
|
187
|
+
return [...ns, { ...toFlowNode({ ...node, griot } as CanvasNode, reveal, false), position }]
|
|
188
|
+
})
|
|
189
|
+
setStatus(`placed ${(node as any).label} → ${layer}`)
|
|
190
|
+
}, [reveal])
|
|
191
|
+
|
|
192
|
+
const exportCanvas = useCallback(() => {
|
|
193
|
+
const c = snapshot()
|
|
194
|
+
const problems = validate(c)
|
|
195
|
+
if (problems.length) { setStatus(`${problems.length} violation(s) — nothing written`); console.warn(explainViolations(problems)); return }
|
|
196
|
+
const text = serialize(c)
|
|
197
|
+
try {
|
|
198
|
+
const url = URL.createObjectURL(new Blob([text], { type: "application/json" }))
|
|
199
|
+
const a = document.createElement("a"); a.href = url; a.download = `${source?.id ?? "prism-viz"}.canvas.json`; a.click()
|
|
200
|
+
setTimeout(() => URL.revokeObjectURL(url), 1500)
|
|
201
|
+
setStatus(`exported ${c.nodes.length} nodes · ${c.edges.length} edges — validated clean`)
|
|
202
|
+
} catch { void navigator.clipboard?.writeText(text); setStatus("export copied to clipboard") }
|
|
203
|
+
}, [snapshot, source])
|
|
204
|
+
|
|
205
|
+
const wake = useCallback(async () => {
|
|
206
|
+
const { rung, ok } = await drive("commit_canvas", { source: source?.id, nodes: nodes.length })
|
|
207
|
+
setStatus(ok ? `woke the agent via ${rung}` : "no drive rung on this host")
|
|
208
|
+
}, [nodes.length, source])
|
|
209
|
+
|
|
210
|
+
// ── the routing decision ───────────────────────────────────────────────────
|
|
211
|
+
const live = useMemo(
|
|
212
|
+
() => (composable ? snapshot() : (source?.canvas ?? emptyCanvas())),
|
|
213
|
+
[composable, snapshot, source]
|
|
214
|
+
)
|
|
215
|
+
const decision = useMemo(() => routeCanvas(live, source?.diagramType), [live, source])
|
|
216
|
+
const resolved = useMemo(() => resolveRenderer(decision), [decision])
|
|
217
|
+
const renderer: RendererId = override ?? resolved.renderer
|
|
218
|
+
|
|
219
|
+
const placed = useMemo(() => new Set(nodes.map((n) => n.id)), [nodes])
|
|
220
|
+
const selectedNode = useMemo(() => nodes.find((n) => n.id === selected) ?? null, [nodes, selected])
|
|
221
|
+
const filled = useMemo(() => ALL_SLOTS.filter((s) => nodes.some((n) => n.data.griot.layer === s)).length, [nodes])
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div className="vz-shell" style={{ ["--lane-h" as string]: `${LANE_H}px`, ...motionCssVars() }}>
|
|
225
|
+
<header className="vz-bar">
|
|
226
|
+
<span className="vz-brand">prism-viz-engine</span>
|
|
227
|
+
<span className="vz-host">{host}</span>
|
|
228
|
+
|
|
229
|
+
<select className="vz-picker" value={source?.id ?? ""} onChange={(e) => setSourceId(e.target.value)}>
|
|
230
|
+
{sources.map((s) => (
|
|
231
|
+
<option key={s.id} value={s.id}>{s.label}</option>
|
|
232
|
+
))}
|
|
233
|
+
</select>
|
|
234
|
+
|
|
235
|
+
{/* the routing decision, made legible */}
|
|
236
|
+
<span className="vz-route" title={decision.because}>
|
|
237
|
+
<b>{RENDERER_LABEL[renderer]}</b>
|
|
238
|
+
<em>{decision.empty ? "nothing placed" : decision.declared ? `declared ${decision.shape}` : `read as ${decision.shape}`}</em>
|
|
239
|
+
{resolved.fellBack && !override && (
|
|
240
|
+
<i className="vz-fellback">{RENDERER_LABEL[decision.renderer]} not built — using {RENDERER_LABEL[resolved.renderer]}</i>
|
|
241
|
+
)}
|
|
242
|
+
</span>
|
|
243
|
+
|
|
244
|
+
<span className="vz-stat">
|
|
245
|
+
{composable ? `${nodes.length} placed · ${filled}/11 layers · ` : ""}
|
|
246
|
+
{live.nodes.length} nodes · {live.edges.length} edges
|
|
247
|
+
</span>
|
|
248
|
+
|
|
249
|
+
<span className="vz-spacer" />
|
|
250
|
+
{status && <span className="vz-status">{status}</span>}
|
|
251
|
+
|
|
252
|
+
<select className="vz-picker" value={motionMode} onChange={(e) => setMotionMode(e.target.value as MotionMode)} title="diagram-design animation.md — one mode per figure">
|
|
253
|
+
<option value="none">motion: none</option>
|
|
254
|
+
<option value="reveal">reveal</option>
|
|
255
|
+
<option value="step">step</option>
|
|
256
|
+
<option value="loop">loop</option>
|
|
257
|
+
</select>
|
|
258
|
+
<select className="vz-picker" value={override ?? ""} onChange={(e) => setOverride((e.target.value || null) as RendererId | null)}>
|
|
259
|
+
<option value="">auto</option>
|
|
260
|
+
{(Object.keys(IMPLEMENTED) as RendererId[]).filter((r) => IMPLEMENTED[r]).map((r) => (
|
|
261
|
+
<option key={r} value={r}>{RENDERER_LABEL[r]}</option>
|
|
262
|
+
))}
|
|
263
|
+
</select>
|
|
264
|
+
|
|
265
|
+
<button className={gallery ? "on" : ""} onClick={() => setGallery((g) => !g)}>
|
|
266
|
+
{gallery ? "← canvas" : "Reference"}
|
|
267
|
+
</button>
|
|
268
|
+
<button onClick={wake}>Wake agent</button>
|
|
269
|
+
<button className="vz-pri" onClick={exportCanvas}>Export .canvas.json</button>
|
|
270
|
+
</header>
|
|
271
|
+
|
|
272
|
+
<div className="vz-body">
|
|
273
|
+
{gallery ? (
|
|
274
|
+
<Gallery sidecar={(import.meta as any).env?.VITE_SIDECAR ?? "http://127.0.0.1:5178"} />
|
|
275
|
+
) : (
|
|
276
|
+
<>
|
|
277
|
+
{composable && <Palette library={library} placed={placed} onReveal={reveal} />}
|
|
278
|
+
|
|
279
|
+
{renderer === "isometric" ? (
|
|
280
|
+
<IsometricView
|
|
281
|
+
canvas={live}
|
|
282
|
+
motionMode={motionMode}
|
|
283
|
+
selectedId={selected}
|
|
284
|
+
onSelect={setSelected}
|
|
285
|
+
onReveal={reveal}
|
|
286
|
+
/>
|
|
287
|
+
) : (
|
|
288
|
+
<Canvas
|
|
289
|
+
nodes={nodes}
|
|
290
|
+
edges={edges}
|
|
291
|
+
onNodesChange={onNodesChange}
|
|
292
|
+
onEdgesChange={onEdgesChange}
|
|
293
|
+
onConnect={onConnect}
|
|
294
|
+
onReroute={onReroute}
|
|
295
|
+
onDropComponent={onDropComponent}
|
|
296
|
+
onSelect={setSelected}
|
|
297
|
+
/>
|
|
298
|
+
)}
|
|
299
|
+
|
|
300
|
+
<Inspector node={selectedNode} reveal={reveal} onRelayer={onReroute} />
|
|
301
|
+
</>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
)
|
|
306
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The React half of the mount seam, kept behind a dynamic import so a host that only
|
|
3
|
+
* wants the format helpers never pays for React or xyflow.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createRoot, type Root } from "react-dom/client"
|
|
7
|
+
import { StrictMode } from "react"
|
|
8
|
+
import { Shell } from "./Shell"
|
|
9
|
+
import type { MountOptions, VizEngineHandle } from "../../core/mount"
|
|
10
|
+
import { emptyCanvas, type JSONCanvas, type CanvasNode } from "../../core/json-canvas"
|
|
11
|
+
import "../../styles.css"
|
|
12
|
+
|
|
13
|
+
export function mountReact(opts: MountOptions & { host: NonNullable<MountOptions["host"]> }): VizEngineHandle {
|
|
14
|
+
const root: Root = createRoot(opts.element)
|
|
15
|
+
const initial = opts.canvas ?? emptyCanvas()
|
|
16
|
+
|
|
17
|
+
// The palette shows everything harvested; the canvas shows what has been placed.
|
|
18
|
+
// They are the same objects — the library is the shelf, not a second copy.
|
|
19
|
+
const library: CanvasNode[] = initial.nodes.filter((n) => n.griot)
|
|
20
|
+
|
|
21
|
+
let handle: { load: (c: JSONCanvas) => void; snapshot: () => JSONCanvas } | null = null
|
|
22
|
+
|
|
23
|
+
root.render(
|
|
24
|
+
<StrictMode>
|
|
25
|
+
<Shell
|
|
26
|
+
host={opts.host}
|
|
27
|
+
library={library}
|
|
28
|
+
sources={(opts.sources as any) ?? []}
|
|
29
|
+
onChange={opts.onChange}
|
|
30
|
+
reveal={opts.reveal}
|
|
31
|
+
subscribeTrace={opts.subscribeTrace}
|
|
32
|
+
registerHandle={(h) => (handle = h)}
|
|
33
|
+
/>
|
|
34
|
+
</StrictMode>
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
load: (c) => handle?.load(c),
|
|
39
|
+
snapshot: () => handle?.snapshot() ?? emptyCanvas(),
|
|
40
|
+
destroy: () => root.unmount(),
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone entry — host #1 of three.
|
|
3
|
+
*
|
|
4
|
+
* Builds the source list the engine opens with. Two kinds, deliberately distinct:
|
|
5
|
+
*
|
|
6
|
+
* composable the Djeli UX/UI harvest — a palette you compose from. ONE source among
|
|
7
|
+
* several, not the engine's identity.
|
|
8
|
+
* examples archify's own IR files, straight out of the harvested repo. The engine
|
|
9
|
+
* demonstrates itself on the cluster's real data, so a broken adapter
|
|
10
|
+
* shows up as a broken example instead of a green fixture.
|
|
11
|
+
*
|
|
12
|
+
* Nothing is bundled. If the sidecar is down the palette stays empty and says so.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { mountVizEngine } from "./core/mount"
|
|
16
|
+
import { adaptHarvest, type HarvestedUxNode, type HarvestedCodeRow } from "./layers/03-substrate/harvest-adapter"
|
|
17
|
+
import { archifyToCanvas, type ArchifyIR } from "./layers/01-generate/archify-ir"
|
|
18
|
+
import { ALL_SLOTS } from "./core/layer-roles"
|
|
19
|
+
import { emptyCanvas, merge } from "./core/json-canvas"
|
|
20
|
+
import type { VizSource } from "./layers/04-shell/Shell"
|
|
21
|
+
|
|
22
|
+
const SIDECAR = import.meta.env.VITE_SIDECAR ?? "http://127.0.0.1:5178"
|
|
23
|
+
|
|
24
|
+
async function getJSON(path: string) {
|
|
25
|
+
const res = await fetch(`${SIDECAR}${path}`)
|
|
26
|
+
if (!res.ok) throw new Error(`${path} -> ${res.status}`)
|
|
27
|
+
return res.json()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function boot() {
|
|
31
|
+
const el = document.getElementById("root")!
|
|
32
|
+
const sources: VizSource[] = []
|
|
33
|
+
const notes: string[] = []
|
|
34
|
+
|
|
35
|
+
// ── the harvest (composable) ────────────────────────────────────────────────
|
|
36
|
+
let harvest = emptyCanvas()
|
|
37
|
+
try {
|
|
38
|
+
const b = await getJSON("/api/harvest")
|
|
39
|
+
const ux: HarvestedUxNode[] = b.ux ?? []
|
|
40
|
+
const code: HarvestedCodeRow[] = b.code ?? []
|
|
41
|
+
if (ux.length) {
|
|
42
|
+
harvest = adaptHarvest(ux, code, { slots: ALL_SLOTS })
|
|
43
|
+
sources.push({
|
|
44
|
+
id: "djeli-harvest",
|
|
45
|
+
label: `Djeli UX/UI harvest (${ux.length})`,
|
|
46
|
+
note: "composable — drag from the palette",
|
|
47
|
+
canvas: harvest,
|
|
48
|
+
composable: true,
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
if (b.notes?.length) notes.push(...b.notes)
|
|
52
|
+
} catch (e) {
|
|
53
|
+
notes.push(`harvest unavailable: ${(e as Error).message}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── archify's own examples (view-only) ──────────────────────────────────────
|
|
57
|
+
try {
|
|
58
|
+
const b = await getJSON("/api/examples")
|
|
59
|
+
for (const ex of b.examples ?? []) {
|
|
60
|
+
const ir = ex.ir as ArchifyIR
|
|
61
|
+
sources.push({
|
|
62
|
+
id: ex.id,
|
|
63
|
+
label: `${ex.title} · ${ex.diagramType}`,
|
|
64
|
+
note: `${ex.components} components · ${ex.connections} connections · ${ex.boundaries} boundaries`,
|
|
65
|
+
canvas: archifyToCanvas(ir, { harvestedBy: "archify-ir", harvestedAt: "2026-09-11" }),
|
|
66
|
+
diagramType: ex.diagramType,
|
|
67
|
+
composable: false,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
if (b.note) notes.push(b.note)
|
|
71
|
+
} catch (e) {
|
|
72
|
+
notes.push(`examples unavailable: ${(e as Error).message}`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const handle = await mountVizEngine({
|
|
76
|
+
element: el,
|
|
77
|
+
host: "standalone",
|
|
78
|
+
canvas: harvest, // the palette library comes from here
|
|
79
|
+
sources,
|
|
80
|
+
reveal: async (origin) => {
|
|
81
|
+
await fetch(`${SIDECAR}/api/reveal`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "content-type": "application/json" },
|
|
84
|
+
body: JSON.stringify(origin),
|
|
85
|
+
})
|
|
86
|
+
},
|
|
87
|
+
onChange: (c) => {
|
|
88
|
+
try { localStorage.setItem("prism-viz-engine:canvas", JSON.stringify(c)) } catch {}
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
if (!sources.length) {
|
|
93
|
+
const b = document.createElement("div")
|
|
94
|
+
b.style.cssText =
|
|
95
|
+
"position:fixed;bottom:14px;left:50%;transform:translateX(-50%);z-index:99;max-width:760px;" +
|
|
96
|
+
"background:#1a1206;border:1px solid #f59e0b;color:#f6d79a;border-radius:10px;padding:10px 14px;" +
|
|
97
|
+
"font:12px/1.5 Inter,system-ui,sans-serif"
|
|
98
|
+
b.textContent =
|
|
99
|
+
`No sources loaded. Start the sidecar: npm run dev:reveal. ` +
|
|
100
|
+
`Nothing is being invented to fill the gap. ${notes.join(" · ")}`
|
|
101
|
+
document.body.appendChild(b)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
void merge // kept exported-in-use for host code that merges canvases
|
|
105
|
+
;(window as any).__vizEngine = handle
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
void boot()
|