@tldiagram/core-ui 1.91.0 → 1.93.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.
Files changed (37) hide show
  1. package/dist/api/client.d.ts +13 -1
  2. package/dist/components/ElementNode.d.ts +9 -0
  3. package/dist/config/runtime-vscode.d.ts +1 -0
  4. package/dist/config/runtime.d.ts +1 -0
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.js +10063 -9512
  7. package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +2 -1
  8. package/dist/pages/ViewEditor/hooks/useViewData.d.ts +20 -21
  9. package/dist/shims/empty-node-module.d.ts +2 -0
  10. package/dist/store/useStore.d.ts +78 -0
  11. package/dist/store/useStore.test.d.ts +1 -0
  12. package/package.json +7 -4
  13. package/src/App.tsx +0 -4
  14. package/src/api/client.ts +39 -1
  15. package/src/components/ElementNode.tsx +11 -58
  16. package/src/components/ElementPanel.tsx +2 -2
  17. package/src/components/LayoutSection.tsx +68 -93
  18. package/src/components/ViewGridNode.tsx +1 -4
  19. package/src/components/ZUI/renderer.ts +166 -66
  20. package/src/components/ZUI/useZUIInteraction.ts +235 -81
  21. package/src/config/runtime-vscode.ts +6 -0
  22. package/src/config/runtime.ts +4 -0
  23. package/src/index.ts +0 -1
  24. package/src/main.tsx +26 -14
  25. package/src/pages/ViewEditor/context.tsx +12 -4
  26. package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +172 -121
  27. package/src/pages/ViewEditor/hooks/useViewData.ts +455 -253
  28. package/src/pages/ViewEditor/index.tsx +45 -32
  29. package/src/shims/empty-node-module.ts +1 -0
  30. package/src/store/useStore.test.ts +272 -0
  31. package/src/store/useStore.ts +285 -0
  32. package/dist/demo/DemoPage.d.ts +0 -9
  33. package/dist/demo/seed.d.ts +0 -9
  34. package/dist/demo/store.d.ts +0 -137
  35. package/src/demo/DemoPage.tsx +0 -184
  36. package/src/demo/seed.ts +0 -67
  37. package/src/demo/store.ts +0 -536
@@ -0,0 +1,285 @@
1
+ import { create } from 'zustand'
2
+ import type { Edge as RFEdge, Node as RFNode } from 'reactflow'
3
+ import type {
4
+ Connector,
5
+ IncomingViewConnector,
6
+ LibraryElement,
7
+ PlacedElement,
8
+ ViewConnector,
9
+ ViewTreeNode,
10
+ } from '../types'
11
+
12
+ export type StoreSetter<T> = T | ((previous: T) => T)
13
+
14
+ export type ViewEditorUiState = {
15
+ viewId: number | null
16
+ canEdit: boolean
17
+ isOwner: boolean
18
+ isFreePlan: boolean
19
+ snapToGrid: boolean
20
+ selectedElement: LibraryElement | null
21
+ selectedConnector: Connector | null
22
+ }
23
+
24
+ export type ViewContentLinks = {
25
+ linksMap: Record<number, ViewConnector[]>
26
+ parentLinksMap: Record<number, ViewConnector[]>
27
+ incomingLinks: IncomingViewConnector[]
28
+ }
29
+
30
+ export type ViewContentPayload = ViewContentLinks & {
31
+ view: ViewTreeNode | null
32
+ viewElements: PlacedElement[]
33
+ connectors: Connector[]
34
+ treeData: ViewTreeNode[]
35
+ }
36
+
37
+ export type CanvasStoreState = ViewEditorUiState & {
38
+ view: ViewTreeNode | null | undefined
39
+ viewElements: PlacedElement[]
40
+ connectors: Connector[]
41
+ nodes: RFNode[]
42
+ edges: RFEdge[]
43
+ linksMap: Record<number, ViewConnector[]>
44
+ parentLinksMap: Record<number, ViewConnector[]>
45
+ incomingLinks: IncomingViewConnector[]
46
+ treeData: ViewTreeNode[]
47
+ allElements: LibraryElement[]
48
+ libraryRefresh: number
49
+
50
+ setViewEditorUi: (patch: Partial<ViewEditorUiState>) => void
51
+ setSnapToGrid: (snapToGrid: boolean) => void
52
+ setSelectedElement: (selectedElement: LibraryElement | null) => void
53
+ setSelectedConnector: (selectedConnector: Connector | null) => void
54
+ setView: (view: ViewTreeNode | null | undefined) => void
55
+ setViewElements: (next: StoreSetter<PlacedElement[]>) => void
56
+ setConnectors: (next: StoreSetter<Connector[]>) => void
57
+ setNodes: (next: StoreSetter<RFNode[]>) => void
58
+ setEdges: (next: StoreSetter<RFEdge[]>) => void
59
+ setLinksMap: (next: StoreSetter<Record<number, ViewConnector[]>>) => void
60
+ setParentLinksMap: (next: StoreSetter<Record<number, ViewConnector[]>>) => void
61
+ setIncomingLinks: (next: StoreSetter<IncomingViewConnector[]>) => void
62
+ setTreeData: (next: StoreSetter<ViewTreeNode[]>) => void
63
+ setAllElements: (next: StoreSetter<LibraryElement[]>) => void
64
+ setLibraryRefresh: (next: StoreSetter<number>) => void
65
+ resetCanvas: () => void
66
+ hydrateViewContent: (payload: ViewContentPayload) => void
67
+ updateElementPosition: (elementId: number, x: number, y: number) => void
68
+ removeElementPlacement: (elementId: number) => void
69
+ removeElementEverywhere: (elementId: number) => void
70
+ mergeSavedElement: (saved: LibraryElement) => void
71
+ upsertConnector: (connector: Connector) => void
72
+ replaceConnector: (connector: Connector) => void
73
+ removeConnector: (connectorId: number) => void
74
+ }
75
+
76
+ export const emptyViewEditorUiState: ViewEditorUiState = {
77
+ viewId: null,
78
+ canEdit: false,
79
+ isOwner: false,
80
+ isFreePlan: false,
81
+ snapToGrid: false,
82
+ selectedElement: null,
83
+ selectedConnector: null,
84
+ }
85
+
86
+ function resolveSetter<T>(next: StoreSetter<T>, previous: T): T {
87
+ return typeof next === 'function' ? (next as (previous: T) => T)(previous) : next
88
+ }
89
+
90
+ export function findViewByOwner(nodes: ViewTreeNode[], elementId: number): ViewTreeNode | null {
91
+ for (const node of nodes) {
92
+ if (node.owner_element_id !== null && Number(node.owner_element_id) === Number(elementId)) return node
93
+ const found = findViewByOwner(node.children, elementId)
94
+ if (found) return found
95
+ }
96
+ return null
97
+ }
98
+
99
+ export function findViewPath(nodes: ViewTreeNode[], targetId: number, path: ViewTreeNode[] = []): ViewTreeNode[] | null {
100
+ for (const node of nodes) {
101
+ if (node.id === targetId) return [...path, node]
102
+ const found = findViewPath(node.children, targetId, [...path, node])
103
+ if (found) return found
104
+ }
105
+ return null
106
+ }
107
+
108
+ export function buildViewContentLinks(tree: ViewTreeNode[], viewId: number, viewElements: PlacedElement[]): ViewContentLinks {
109
+ const linksMap: Record<number, ViewConnector[]> = {}
110
+ const parentLinksMap: Record<number, ViewConnector[]> = {}
111
+
112
+ const viewPath = findViewPath(tree, viewId)
113
+ const parentView = viewPath && viewPath.length > 1 ? viewPath[viewPath.length - 2] : null
114
+ const currentViewInTree = viewPath ? viewPath[viewPath.length - 1] : null
115
+
116
+ const incomingLinks: IncomingViewConnector[] = []
117
+ if (parentView && currentViewInTree?.owner_element_id) {
118
+ incomingLinks.push({
119
+ id: 0,
120
+ element_id: currentViewInTree.owner_element_id,
121
+ element_name: 'Parent',
122
+ from_view_id: parentView.id,
123
+ from_view_name: parentView.name,
124
+ to_view_id: viewId,
125
+ })
126
+ }
127
+
128
+ for (const element of viewElements) {
129
+ const childView = findViewByOwner(tree, element.element_id)
130
+ if (childView) {
131
+ linksMap[element.element_id] = [{
132
+ id: 0,
133
+ element_id: element.element_id,
134
+ from_view_id: viewId,
135
+ to_view_id: childView.id,
136
+ to_view_name: childView.name,
137
+ relation_type: 'child',
138
+ }]
139
+ }
140
+
141
+ if (parentView) {
142
+ parentLinksMap[element.element_id] = [{
143
+ id: 0,
144
+ element_id: element.element_id,
145
+ from_view_id: parentView.id,
146
+ to_view_id: parentView.id,
147
+ to_view_name: parentView.name,
148
+ relation_type: 'parent',
149
+ }]
150
+ }
151
+ }
152
+
153
+ return { linksMap, parentLinksMap, incomingLinks }
154
+ }
155
+
156
+ export function selectExistingElementIds(state: Pick<CanvasStoreState, 'viewElements'>): Set<number> {
157
+ return new Set(state.viewElements.map((element) => element.element_id))
158
+ }
159
+
160
+ export function selectElementById(state: Pick<CanvasStoreState, 'viewElements'>, elementId: number): PlacedElement | undefined {
161
+ return state.viewElements.find((element) => element.element_id === elementId)
162
+ }
163
+
164
+ export function selectConnectorById(state: Pick<CanvasStoreState, 'connectors'>, connectorId: number): Connector | undefined {
165
+ return state.connectors.find((connector) => connector.id === connectorId)
166
+ }
167
+
168
+ export function updatePlacedElementPosition(elements: PlacedElement[], elementId: number, x: number, y: number): PlacedElement[] {
169
+ let changed = false
170
+ const next = elements.map((element) => {
171
+ if (element.element_id !== elementId) return element
172
+ if (element.position_x === x && element.position_y === y) return element
173
+ changed = true
174
+ return { ...element, position_x: x, position_y: y }
175
+ })
176
+ return changed ? next : elements
177
+ }
178
+
179
+ export function removePlacedElement(elements: PlacedElement[], elementId: number): PlacedElement[] {
180
+ return elements.filter((element) => element.element_id !== elementId)
181
+ }
182
+
183
+ export function mergeSavedElementIntoPlacements(elements: PlacedElement[], saved: LibraryElement): PlacedElement[] {
184
+ return elements.map((element) =>
185
+ element.element_id === saved.id
186
+ ? {
187
+ ...element,
188
+ name: saved.name,
189
+ description: saved.description,
190
+ kind: saved.kind,
191
+ technology: saved.technology,
192
+ url: saved.url,
193
+ logo_url: saved.logo_url,
194
+ technology_connectors: saved.technology_connectors,
195
+ tags: saved.tags,
196
+ repo: saved.repo,
197
+ branch: saved.branch,
198
+ file_path: saved.file_path,
199
+ language: saved.language,
200
+ }
201
+ : element,
202
+ )
203
+ }
204
+
205
+ export function upsertConnectorInList(connectors: Connector[], connector: Connector): Connector[] {
206
+ const index = connectors.findIndex((candidate) => candidate.id === connector.id)
207
+ if (index === -1) return [...connectors, connector]
208
+ const next = connectors.slice()
209
+ next[index] = connector
210
+ return next
211
+ }
212
+
213
+ export function removeConnectorFromList(connectors: Connector[], connectorId: number): Connector[] {
214
+ return connectors.filter((connector) => connector.id !== connectorId)
215
+ }
216
+
217
+ export const useStore = create<CanvasStoreState>((set) => ({
218
+ ...emptyViewEditorUiState,
219
+ view: undefined,
220
+ viewElements: [],
221
+ connectors: [],
222
+ nodes: [],
223
+ edges: [],
224
+ linksMap: {},
225
+ parentLinksMap: {},
226
+ incomingLinks: [],
227
+ treeData: [],
228
+ allElements: [],
229
+ libraryRefresh: 0,
230
+
231
+ setViewEditorUi: (patch) => set((state) => ({ ...state, ...patch })),
232
+ setSnapToGrid: (snapToGrid) => set({ snapToGrid }),
233
+ setSelectedElement: (selectedElement) => set({ selectedElement }),
234
+ setSelectedConnector: (selectedConnector) => set({ selectedConnector }),
235
+ setView: (view) => set({ view }),
236
+ setViewElements: (next) => set((state) => ({ viewElements: resolveSetter(next, state.viewElements) })),
237
+ setConnectors: (next) => set((state) => ({ connectors: resolveSetter(next, state.connectors) })),
238
+ setNodes: (next) => set((state) => ({ nodes: resolveSetter(next, state.nodes) })),
239
+ setEdges: (next) => set((state) => ({ edges: resolveSetter(next, state.edges) })),
240
+ setLinksMap: (next) => set((state) => ({ linksMap: resolveSetter(next, state.linksMap) })),
241
+ setParentLinksMap: (next) => set((state) => ({ parentLinksMap: resolveSetter(next, state.parentLinksMap) })),
242
+ setIncomingLinks: (next) => set((state) => ({ incomingLinks: resolveSetter(next, state.incomingLinks) })),
243
+ setTreeData: (next) => set((state) => ({ treeData: resolveSetter(next, state.treeData) })),
244
+ setAllElements: (next) => set((state) => ({ allElements: resolveSetter(next, state.allElements) })),
245
+ setLibraryRefresh: (next) => set((state) => ({ libraryRefresh: resolveSetter(next, state.libraryRefresh) })),
246
+ resetCanvas: () => set({ nodes: [], edges: [] }),
247
+ hydrateViewContent: (payload) => set({
248
+ view: payload.view,
249
+ viewElements: payload.viewElements,
250
+ connectors: payload.connectors,
251
+ linksMap: payload.linksMap,
252
+ parentLinksMap: payload.parentLinksMap,
253
+ incomingLinks: payload.incomingLinks,
254
+ treeData: payload.treeData,
255
+ }),
256
+ updateElementPosition: (elementId, x, y) => set((state) => ({
257
+ viewElements: updatePlacedElementPosition(state.viewElements, elementId, x, y),
258
+ })),
259
+ removeElementPlacement: (elementId) => set((state) => ({
260
+ viewElements: removePlacedElement(state.viewElements, elementId),
261
+ })),
262
+ removeElementEverywhere: (elementId) => set((state) => ({
263
+ viewElements: removePlacedElement(state.viewElements, elementId),
264
+ libraryRefresh: state.libraryRefresh + 1,
265
+ })),
266
+ mergeSavedElement: (saved) => set((state) => ({
267
+ viewElements: mergeSavedElementIntoPlacements(state.viewElements, saved),
268
+ libraryRefresh: state.libraryRefresh + 1,
269
+ })),
270
+ upsertConnector: (connector) => set((state) => ({
271
+ connectors: upsertConnectorInList(state.connectors, connector),
272
+ })),
273
+ replaceConnector: (connector) => set((state) => ({
274
+ connectors: upsertConnectorInList(state.connectors, connector),
275
+ })),
276
+ removeConnector: (connectorId) => set((state) => ({
277
+ connectors: removeConnectorFromList(state.connectors, connectorId),
278
+ })),
279
+ }))
280
+
281
+ export const canvasSelectors = {
282
+ existingElementIds: selectExistingElementIds,
283
+ elementById: selectElementById,
284
+ connectorById: selectConnectorById,
285
+ }
@@ -1,9 +0,0 @@
1
- /**
2
- * Demo entry point.
3
- * Overrides the real `api` singleton with localStorage-backed implementations
4
- * and patches window.history to redirect /views/:id → /demo/:id, all for the
5
- * lifetime of this component. Restores everything on unmount.
6
- * No auth required; served at /demo and /demo/:id routes.
7
- */
8
- export declare function DemoNavigator(): null;
9
- export default function DemoPage(): import("react/jsx-runtime").JSX.Element;
@@ -1,9 +0,0 @@
1
- import type { LibraryElement, PlacedElement, Connector, ViewTreeNode, ViewLayer } from '../types';
2
- export declare const DEMO_ELEMENTS: LibraryElement[];
3
- export declare const DEMO_VIEWS: ViewTreeNode[];
4
- type ViewPlacements = Record<number, PlacedElement[]>;
5
- type ViewConnectors = Record<number, Connector[]>;
6
- export declare const DEMO_PLACEMENTS: ViewPlacements;
7
- export declare const DEMO_CONNECTORS: ViewConnectors;
8
- export declare const DEMO_LAYERS: Record<number, ViewLayer[]>;
9
- export {};
@@ -1,137 +0,0 @@
1
- /**
2
- * localStorage-backed store for the demo mode.
3
- * Implements the subset of the `api` interface used by ViewEditor and its hooks.
4
- * Data is scoped under the `diag:demo:*` key namespace to avoid colliding
5
- * with a real logged-in session.
6
- */
7
- import type { LibraryElement, PlacedElement, Connector, ViewTreeNode, ViewLayer, Tag, ElementPlacement, ExploreData } from '../types';
8
- export declare function initDemoStore(): void;
9
- export declare function resetDemoStore(): void;
10
- export declare const demoApi: {
11
- explore: {
12
- load: () => Promise<ExploreData>;
13
- };
14
- elements: {
15
- list: (_params?: unknown) => Promise<LibraryElement[]>;
16
- get: (id: number) => Promise<LibraryElement>;
17
- create: (data: Partial<LibraryElement>) => Promise<LibraryElement>;
18
- update: (id: number, data: Partial<LibraryElement>) => Promise<LibraryElement>;
19
- delete: (_orgId: string, id: number) => Promise<void>;
20
- placements: (id: number) => Promise<ElementPlacement[]>;
21
- };
22
- workspace: {
23
- orgs: {
24
- tagColors: {
25
- list: () => Promise<Record<string, Tag>>;
26
- set: (tag: string, color: string, description?: string) => Promise<void>;
27
- };
28
- };
29
- views: {
30
- list: () => Promise<{
31
- id: number;
32
- owner_element_id: number | null;
33
- name: string;
34
- label: string | null;
35
- is_root: boolean;
36
- created_at: string;
37
- updated_at: string;
38
- }[]>;
39
- get: (id: number) => Promise<ViewTreeNode>;
40
- content: (id: number) => Promise<{
41
- placements: PlacedElement[];
42
- connectors: Connector[];
43
- }>;
44
- tree: () => Promise<ViewTreeNode[]>;
45
- create: (data: {
46
- name: string;
47
- label?: string;
48
- parent_view_id?: number | null;
49
- }) => Promise<{
50
- id: number;
51
- owner_element_id: number | null;
52
- name: string;
53
- label: string | null;
54
- is_root: boolean;
55
- created_at: string;
56
- updated_at: string;
57
- }>;
58
- update: (id: number, data: {
59
- name: string;
60
- label?: string;
61
- }) => Promise<{
62
- id: number;
63
- owner_element_id: number | null;
64
- name: string;
65
- label: string | null;
66
- is_root: boolean;
67
- created_at: string;
68
- updated_at: string;
69
- }>;
70
- delete: (_orgId: string, id: number) => Promise<void>;
71
- placements: {
72
- list: (viewId: number) => Promise<ElementPlacement[]>;
73
- add: (viewId: number, elementId: number, x?: number, y?: number) => Promise<ElementPlacement>;
74
- updatePosition: (viewId: number, elementId: number, x: number, y: number) => Promise<void>;
75
- remove: (viewId: number, elementId: number) => Promise<void>;
76
- };
77
- layers: {
78
- list: (viewId: number) => Promise<ViewLayer[]>;
79
- create: (viewId: number, data: {
80
- name: string;
81
- tags: string[];
82
- color?: string;
83
- }) => Promise<ViewLayer>;
84
- update: (viewId: number, layerId: number, data: Partial<ViewLayer>) => Promise<ViewLayer>;
85
- delete: (viewId: number, layerId: number) => Promise<void>;
86
- };
87
- reactions: {
88
- list: (_viewId: number) => Promise<never[]>;
89
- };
90
- threads: {
91
- listForElement: () => Promise<never[]>;
92
- listForConnector: () => Promise<never[]>;
93
- createForElement: () => Promise<never>;
94
- createForConnector: () => Promise<never>;
95
- addComment: () => Promise<never>;
96
- resolve: () => Promise<void>;
97
- };
98
- thumbnail: (_id: number) => Promise<null>;
99
- rename: (id: number, name: string) => Promise<{
100
- id: number;
101
- owner_element_id: number | null;
102
- name: string;
103
- label: string | null;
104
- is_root: boolean;
105
- created_at: string;
106
- updated_at: string;
107
- }>;
108
- setLevel: () => Promise<void>;
109
- reparent: () => Promise<never>;
110
- };
111
- connectors: {
112
- list: (viewId: number) => Promise<Connector[]>;
113
- create: (viewId: number, data: {
114
- source_element_id: number;
115
- target_element_id: number;
116
- label?: string;
117
- description?: string;
118
- relationship?: string;
119
- direction?: string;
120
- style?: string;
121
- url?: string;
122
- source_handle?: string | null;
123
- target_handle?: string | null;
124
- }) => Promise<Connector>;
125
- update: (viewId: number, connectorId: number, data: Partial<Connector>) => Promise<Connector>;
126
- delete: (_orgId: string, connectorId: number) => Promise<void>;
127
- };
128
- elements: {
129
- list: (params?: unknown) => Promise<LibraryElement[]>;
130
- get: (id: number) => Promise<LibraryElement>;
131
- create: (data: Partial<LibraryElement>) => Promise<LibraryElement>;
132
- update: (id: number, data: Partial<LibraryElement>) => Promise<LibraryElement>;
133
- delete: (orgId: string, id: number) => Promise<void>;
134
- placements: (id: number) => Promise<ElementPlacement[]>;
135
- };
136
- };
137
- };
@@ -1,184 +0,0 @@
1
- /**
2
- * Demo entry point.
3
- * Overrides the real `api` singleton with localStorage-backed implementations
4
- * and patches window.history to redirect /views/:id → /demo/:id, all for the
5
- * lifetime of this component. Restores everything on unmount.
6
- * No auth required; served at /demo and /demo/:id routes.
7
- */
8
-
9
- import React, { useEffect, useState } from 'react'
10
- import { useNavigate } from 'react-router-dom'
11
- import { Box } from '@chakra-ui/react'
12
- import { api } from '../api/client'
13
- import ViewEditor from '../pages/ViewEditor'
14
- import { HeaderProvider } from '../components/HeaderContext'
15
- import { ThemeProvider } from '../context/ThemeContext'
16
- import { demoApi, initDemoStore } from './store'
17
- import { DEMO_VIEW_EDITOR_OPTIONS } from './viewEditor'
18
-
19
- // ── Override helpers ──────────────────────────────────────────────────────────
20
-
21
- type ApiOverride = Record<string, unknown>
22
-
23
- /**
24
- * Swap api methods + patch window.history so any /views/:id navigation is
25
- * silently rewritten to /demo/:id before React Router processes it.
26
- * Returns a function that restores everything.
27
- */
28
- function applyOverrides(): () => void {
29
- const originals: [obj: ApiOverride, key: string, original: unknown][] = []
30
-
31
- function swap(obj: ApiOverride, key: string, replacement: unknown) {
32
- originals.push([obj, key, obj[key]])
33
- obj[key] = replacement
34
- }
35
-
36
- // elements
37
- const realElements = api.elements as unknown as ApiOverride
38
- const demoElements = demoApi.elements as ApiOverride
39
- for (const key of ['list', 'get', 'create', 'update', 'delete', 'placements']) {
40
- swap(realElements, key, demoElements[key])
41
- }
42
-
43
- // workspace.elements
44
- const realWsElements = (api.workspace as unknown as ApiOverride).elements as ApiOverride
45
- const demoWsElements = demoApi.workspace.elements as ApiOverride
46
- for (const key of ['list', 'get', 'create', 'update', 'delete', 'placements']) {
47
- swap(realWsElements, key, demoWsElements[key])
48
- }
49
-
50
- // workspace.orgs.tagColors
51
- const realTagColors = ((api.workspace as unknown as ApiOverride).orgs as ApiOverride).tagColors as ApiOverride
52
- const demoTagColors = demoApi.workspace.orgs.tagColors as ApiOverride
53
- for (const key of ['list', 'set']) {
54
- swap(realTagColors, key, demoTagColors[key])
55
- }
56
-
57
- // workspace.views top-level methods + nested namespaces
58
- const realViews = (api.workspace as unknown as ApiOverride).views as ApiOverride
59
- const demoViews = demoApi.workspace.views as ApiOverride
60
- for (const key of ['list', 'get', 'content', 'tree', 'create', 'update', 'delete', 'thumbnail', 'rename', 'setLevel', 'reparent']) {
61
- swap(realViews, key, demoViews[key])
62
- }
63
- for (const ns of ['placements', 'layers', 'reactions', 'threads']) {
64
- const realNs = realViews[ns] as ApiOverride
65
- const demoNs = demoViews[ns] as ApiOverride
66
- for (const key of Object.keys(demoNs)) {
67
- swap(realNs, key, demoNs[key])
68
- }
69
- }
70
-
71
- // workspace.connectors
72
- const realConnectors = (api.workspace as unknown as ApiOverride).connectors as ApiOverride
73
- const demoConnectors = demoApi.workspace.connectors as ApiOverride
74
- for (const key of ['list', 'create', 'update', 'delete']) {
75
- swap(realConnectors, key, demoConnectors[key])
76
- }
77
-
78
- // explore.load (cross-branch graph snapshot)
79
- const realExplore = (api as unknown as ApiOverride).explore as ApiOverride
80
- swap(realExplore, 'load', demoApi.explore.load)
81
-
82
- // ── Patch window.history so /views/:id → /demo/:id before React Router sees it
83
- const origPush = window.history.pushState.bind(window.history)
84
- const origReplace = window.history.replaceState.bind(window.history)
85
-
86
- function rewriteUrl(url: string | URL | null | undefined): string | URL | null | undefined {
87
- if (typeof url !== 'string') return url
88
- return url.replace(/\/views\/(\d+)/, '/demo/$1')
89
- }
90
-
91
- window.history.pushState = (state, title, url) => origPush(state, title, rewriteUrl(url))
92
- window.history.replaceState = (state, title, url) => origReplace(state, title, rewriteUrl(url))
93
-
94
- return () => {
95
- for (const [obj, key, original] of originals) {
96
- obj[key] = original
97
- }
98
- window.history.pushState = origPush
99
- window.history.replaceState = origReplace
100
- }
101
- }
102
-
103
- // ── Inner component overrides applied before children mount ─────────────────
104
-
105
- function DemoApp({
106
- revealProgress,
107
- }: {
108
- revealProgress: number
109
- }) {
110
- // useState initializer runs synchronously during the first render of this
111
- // component instance, before any child component mounts or any hook effect
112
- // runs. This guarantees api overrides are in place for ViewEditor's first fetch.
113
- const [restore] = useState<() => void>(() => {
114
- initDemoStore()
115
- return applyOverrides()
116
- })
117
-
118
- useEffect(() => restore, [restore])
119
-
120
- return (
121
- <ViewEditor
122
- demoOptions={{ ...DEMO_VIEW_EDITOR_OPTIONS, revealProgress }}
123
- />
124
- )
125
- }
126
-
127
- // ── DemoNavigator: redirects bare /demo to the first root view ────────────────
128
-
129
- export function DemoNavigator() {
130
- const navigate = useNavigate()
131
-
132
- useEffect(() => {
133
- // Call demoApi directly no need to apply global overrides here, which
134
- // would otherwise be cleaned up after DemoApp mounts and clobber its patches.
135
- initDemoStore()
136
- demoApi.workspace.views.tree().then((tree) => {
137
- const root = tree.find((v) => v.parent_view_id === null)
138
- navigate(root ? `/demo/${root.id}` : '/demo/1', { replace: true })
139
- }).catch(() => navigate('/demo/1', { replace: true }))
140
- }, [navigate])
141
-
142
- return null
143
- }
144
-
145
- // ── DemoPage ──────────────────────────────────────────────────────────────────
146
-
147
- export default function DemoPage() {
148
- const [revealProgress, setRevealProgress] = useState(() => (window.self === window.top ? 1 : 0))
149
-
150
- useEffect(() => {
151
- if (window.self === window.top) {
152
- setRevealProgress(1)
153
- return
154
- }
155
-
156
- const handleMessage = (event: MessageEvent) => {
157
- const data = event.data as { type?: unknown; progress?: unknown } | null
158
- if (!data || data.type !== 'tldiagram-demo-progress') return
159
-
160
- const nextProgress = Number(data.progress)
161
- if (!Number.isFinite(nextProgress)) return
162
-
163
- setRevealProgress(Math.max(0, Math.min(1, nextProgress)))
164
- }
165
-
166
- window.addEventListener('message', handleMessage)
167
- return () => window.removeEventListener('message', handleMessage)
168
- }, [])
169
-
170
- return (
171
- <ThemeProvider
172
- storagePrefix="diag:demo"
173
- defaultAccent="#63b3ed"
174
- defaultBackground="#0d121e"
175
- defaultElementColor="#2d3748"
176
- >
177
- <Box minH="100vh" bg="var(--bg-canvas)" style={{ '--editor-top-offset': '0px' } as React.CSSProperties}>
178
- <HeaderProvider>
179
- <DemoApp revealProgress={revealProgress} />
180
- </HeaderProvider>
181
- </Box>
182
- </ThemeProvider>
183
- )
184
- }