@tldiagram/core-ui 1.87.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 (272) hide show
  1. package/dist/App.d.ts +1 -0
  2. package/dist/api/client.d.ts +143 -0
  3. package/dist/api/transport-vscode.d.ts +8 -0
  4. package/dist/api/transport.d.ts +1 -0
  5. package/dist/components/CodePreviewPanel-vscode.d.ts +7 -0
  6. package/dist/components/CodePreviewPanel.d.ts +9 -0
  7. package/dist/components/ConfirmDialog.d.ts +12 -0
  8. package/dist/components/ConnectorPanel.d.ts +21 -0
  9. package/dist/components/ContextBoundaryElement.d.ts +11 -0
  10. package/dist/components/ContextNeighborElement.d.ts +29 -0
  11. package/dist/components/ContextStraightConnector.d.ts +4 -0
  12. package/dist/components/CrossBranchControls.d.ts +9 -0
  13. package/dist/components/DependenciesOnboarding.d.ts +5 -0
  14. package/dist/components/DrawingCanvas.d.ts +39 -0
  15. package/dist/components/ElementLibrary-vscode.d.ts +7 -0
  16. package/dist/components/ElementLibrary.d.ts +22 -0
  17. package/dist/components/ElementNode.d.ts +36 -0
  18. package/dist/components/ElementPanel.d.ts +25 -0
  19. package/dist/components/ExploreOnboarding.d.ts +5 -0
  20. package/dist/components/ExplorePageOnboarding.d.ts +5 -0
  21. package/dist/components/ExportModal.d.ts +16 -0
  22. package/dist/components/FloatingEdge.d.ts +9 -0
  23. package/dist/components/GitSourceLinker.d.ts +8 -0
  24. package/dist/components/HeaderContext.d.ts +16 -0
  25. package/dist/components/Icons.d.ts +95 -0
  26. package/dist/components/ImportModal.d.ts +10 -0
  27. package/dist/components/InlineElementAdder.d.ts +17 -0
  28. package/dist/components/LayoutSection.d.ts +7 -0
  29. package/dist/components/LocalSourceLinker.d.ts +8 -0
  30. package/dist/components/MiniZoomOnboarding.d.ts +5 -0
  31. package/dist/components/NavBreadcrumb.d.ts +6 -0
  32. package/dist/components/NodeBody.d.ts +12 -0
  33. package/dist/components/NodeContainer.d.ts +8 -0
  34. package/dist/components/NodeHoverCard.d.ts +10 -0
  35. package/dist/components/PanelHeader.d.ts +8 -0
  36. package/dist/components/PanelUI.d.ts +3 -0
  37. package/dist/components/ProxyConnectorEdge.d.ts +4 -0
  38. package/dist/components/ProxyConnectorPanel.d.ts +9 -0
  39. package/dist/components/SafeBackground.d.ts +13 -0
  40. package/dist/components/ScrollIndicatorWrapper.d.ts +8 -0
  41. package/dist/components/SetChildModal.d.ts +10 -0
  42. package/dist/components/SetParentModal.d.ts +10 -0
  43. package/dist/components/SlidingPanel.d.ts +16 -0
  44. package/dist/components/TagUpsert.d.ts +8 -0
  45. package/dist/components/TopMenuBar.d.ts +8 -0
  46. package/dist/components/ViewBezierConnector.d.ts +4 -0
  47. package/dist/components/ViewDrawMenu.d.ts +22 -0
  48. package/dist/components/ViewEditorEdgeLabelLayout.d.ts +16 -0
  49. package/dist/components/ViewEditorOnboarding.d.ts +5 -0
  50. package/dist/components/ViewExplorer/TagManager/ColorPicker.d.ts +7 -0
  51. package/dist/components/ViewExplorer/TagManager/GroupNamingPopover.d.ts +10 -0
  52. package/dist/components/ViewExplorer/TagManager/LayerItem.d.ts +27 -0
  53. package/dist/components/ViewExplorer/TagManager/TagItem.d.ts +25 -0
  54. package/dist/components/ViewExplorer/TagManager/index.d.ts +21 -0
  55. package/dist/components/ViewExplorer/ViewNavigator.d.ts +11 -0
  56. package/dist/components/ViewExplorer/ViewSearch.d.ts +8 -0
  57. package/dist/components/ViewExplorer/ViewTree.d.ts +18 -0
  58. package/dist/components/ViewExplorer/index.d.ts +31 -0
  59. package/dist/components/ViewExplorer/types.d.ts +11 -0
  60. package/dist/components/ViewExplorer/utils.d.ts +6 -0
  61. package/dist/components/ViewExplorer-vscode.d.ts +6 -0
  62. package/dist/components/ViewFloatingMenu-vscode.d.ts +27 -0
  63. package/dist/components/ViewFloatingMenu.d.ts +39 -0
  64. package/dist/components/ViewGridNode.d.ts +29 -0
  65. package/dist/components/ViewHeaderButton.d.ts +11 -0
  66. package/dist/components/ViewPanel.d.ts +18 -0
  67. package/dist/components/ViewsGridOnboarding.d.ts +5 -0
  68. package/dist/components/ZUI/ZUICanvas.d.ts +18 -0
  69. package/dist/components/ZUI/index.d.ts +2 -0
  70. package/dist/components/ZUI/layout.d.ts +18 -0
  71. package/dist/components/ZUI/proxy.d.ts +25 -0
  72. package/dist/components/ZUI/renderer.d.ts +30 -0
  73. package/dist/components/ZUI/types.d.ts +140 -0
  74. package/dist/components/ZUI/useZUIInteraction.d.ts +21 -0
  75. package/dist/config/runtime-vscode.d.ts +22 -0
  76. package/dist/config/runtime.d.ts +5 -0
  77. package/dist/constants/colors.d.ts +27 -0
  78. package/dist/constants/diagramColors.d.ts +1 -0
  79. package/dist/context/ThemeContext.d.ts +27 -0
  80. package/dist/crossBranch/graph.d.ts +13 -0
  81. package/dist/crossBranch/resolve.d.ts +22 -0
  82. package/dist/crossBranch/settings.d.ts +6 -0
  83. package/dist/crossBranch/store.d.ts +11 -0
  84. package/dist/crossBranch/types.d.ts +96 -0
  85. package/dist/demo/DemoPage.d.ts +9 -0
  86. package/dist/demo/seed.d.ts +9 -0
  87. package/dist/demo/store.d.ts +137 -0
  88. package/dist/demo/viewEditor.d.ts +26 -0
  89. package/dist/favicon.svg +35 -0
  90. package/dist/hooks/useSafeFitView.d.ts +16 -0
  91. package/dist/index.css +1 -0
  92. package/dist/index.d.ts +115 -0
  93. package/dist/index.js +19966 -0
  94. package/dist/lib/vscodeBridge-vscode.d.ts +13 -0
  95. package/dist/lib/vscodeBridge.d.ts +5 -0
  96. package/dist/logo-120.png +0 -0
  97. package/dist/logo-bw.png +0 -0
  98. package/dist/logo-bw.svg +15 -0
  99. package/dist/logo-text.svg +51 -0
  100. package/dist/logo.svg +35 -0
  101. package/dist/pages/AppearanceSettings.d.ts +3 -0
  102. package/dist/pages/Dependencies.d.ts +1 -0
  103. package/dist/pages/InfiniteZoom.d.ts +7 -0
  104. package/dist/pages/Settings.d.ts +7 -0
  105. package/dist/pages/ViewEditor/components/EditorMenus.d.ts +24 -0
  106. package/dist/pages/ViewEditor/components/EditorOverlays.d.ts +30 -0
  107. package/dist/pages/ViewEditor/components/EmptyCanvasState.d.ts +7 -0
  108. package/dist/pages/ViewEditor/context.d.ts +13 -0
  109. package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +201 -0
  110. package/dist/pages/ViewEditor/hooks/useDrawingEngine.d.ts +40 -0
  111. package/dist/pages/ViewEditor/hooks/useViewContextNeighbours.d.ts +20 -0
  112. package/dist/pages/ViewEditor/hooks/useViewData.d.ts +74 -0
  113. package/dist/pages/ViewEditor/index.d.ts +8 -0
  114. package/dist/pages/ViewEditor/utils.d.ts +14 -0
  115. package/dist/pages/Views.d.ts +6 -0
  116. package/dist/pages/ViewsGrid.d.ts +6 -0
  117. package/dist/pkg/importer/mermaid.d.ts +7 -0
  118. package/dist/pkg/importer/mermaid.test.d.ts +1 -0
  119. package/dist/platform/PlatformContext.d.ts +6 -0
  120. package/dist/platform/context.d.ts +3 -0
  121. package/dist/platform/local.d.ts +2 -0
  122. package/dist/platform/types.d.ts +17 -0
  123. package/dist/slots.d.ts +67 -0
  124. package/dist/theme.d.ts +2 -0
  125. package/dist/types/index.d.ts +193 -0
  126. package/dist/types/vscode-messages.d.ts +60 -0
  127. package/dist/utils/edgeDistribution.d.ts +34 -0
  128. package/dist/utils/githubApi.d.ts +4 -0
  129. package/dist/utils/githubCache.d.ts +17 -0
  130. package/dist/utils/ids.d.ts +2 -0
  131. package/dist/utils/technologyCatalog.d.ts +15 -0
  132. package/dist/utils/toast.d.ts +15 -0
  133. package/dist/utils/treesitter.d.ts +13 -0
  134. package/dist/utils/url.d.ts +12 -0
  135. package/package.json +159 -0
  136. package/src/App.tsx +141 -0
  137. package/src/api/client.ts +618 -0
  138. package/src/api/transport-vscode.ts +28 -0
  139. package/src/api/transport.ts +7 -0
  140. package/src/assets/logo-mark.svg +31 -0
  141. package/src/assets/logo-wordmark.svg +22 -0
  142. package/src/assets/logo.svg +35 -0
  143. package/src/components/CodePreviewPanel-vscode.tsx +85 -0
  144. package/src/components/CodePreviewPanel.tsx +384 -0
  145. package/src/components/ConfirmDialog.tsx +66 -0
  146. package/src/components/ConnectorPanel.tsx +403 -0
  147. package/src/components/ContextBoundaryElement.tsx +35 -0
  148. package/src/components/ContextNeighborElement.tsx +282 -0
  149. package/src/components/ContextStraightConnector.tsx +144 -0
  150. package/src/components/CrossBranchControls.tsx +105 -0
  151. package/src/components/DependenciesOnboarding.tsx +427 -0
  152. package/src/components/DrawingCanvas.tsx +391 -0
  153. package/src/components/ElementLibrary-vscode.tsx +9 -0
  154. package/src/components/ElementLibrary.tsx +512 -0
  155. package/src/components/ElementNode.tsx +1033 -0
  156. package/src/components/ElementPanel.tsx +928 -0
  157. package/src/components/ExploreOnboarding.tsx +347 -0
  158. package/src/components/ExplorePageOnboarding.tsx +383 -0
  159. package/src/components/ExportModal.tsx +132 -0
  160. package/src/components/FloatingEdge.tsx +115 -0
  161. package/src/components/GitSourceLinker.tsx +1053 -0
  162. package/src/components/HeaderContext.tsx +30 -0
  163. package/src/components/Icons.tsx +245 -0
  164. package/src/components/ImportModal.tsx +219 -0
  165. package/src/components/InlineElementAdder.tsx +216 -0
  166. package/src/components/LayoutSection.tsx +624 -0
  167. package/src/components/LocalSourceLinker.tsx +330 -0
  168. package/src/components/MiniZoomOnboarding.tsx +78 -0
  169. package/src/components/NavBreadcrumb.tsx +24 -0
  170. package/src/components/NodeBody.tsx +89 -0
  171. package/src/components/NodeContainer.tsx +58 -0
  172. package/src/components/NodeHoverCard.tsx +135 -0
  173. package/src/components/PanelHeader.tsx +36 -0
  174. package/src/components/PanelUI.tsx +24 -0
  175. package/src/components/ProxyConnectorEdge.tsx +169 -0
  176. package/src/components/ProxyConnectorPanel.tsx +130 -0
  177. package/src/components/SafeBackground.tsx +19 -0
  178. package/src/components/ScrollIndicatorWrapper.tsx +117 -0
  179. package/src/components/SetChildModal.tsx +191 -0
  180. package/src/components/SetParentModal.tsx +187 -0
  181. package/src/components/SlidingPanel.tsx +114 -0
  182. package/src/components/TagUpsert.tsx +142 -0
  183. package/src/components/TopMenuBar.tsx +380 -0
  184. package/src/components/ViewBezierConnector.tsx +143 -0
  185. package/src/components/ViewDrawMenu.tsx +270 -0
  186. package/src/components/ViewEditorEdgeLabelLayout.ts +189 -0
  187. package/src/components/ViewEditorOnboarding.tsx +445 -0
  188. package/src/components/ViewExplorer/TagManager/ColorPicker.tsx +49 -0
  189. package/src/components/ViewExplorer/TagManager/GroupNamingPopover.tsx +96 -0
  190. package/src/components/ViewExplorer/TagManager/LayerItem.tsx +228 -0
  191. package/src/components/ViewExplorer/TagManager/TagItem.tsx +242 -0
  192. package/src/components/ViewExplorer/TagManager/index.tsx +418 -0
  193. package/src/components/ViewExplorer/ViewNavigator.tsx +121 -0
  194. package/src/components/ViewExplorer/ViewSearch.tsx +33 -0
  195. package/src/components/ViewExplorer/ViewTree.tsx +98 -0
  196. package/src/components/ViewExplorer/index.tsx +384 -0
  197. package/src/components/ViewExplorer/types.ts +13 -0
  198. package/src/components/ViewExplorer/utils.ts +56 -0
  199. package/src/components/ViewExplorer-vscode.tsx +8 -0
  200. package/src/components/ViewFloatingMenu-vscode.tsx +248 -0
  201. package/src/components/ViewFloatingMenu.tsx +379 -0
  202. package/src/components/ViewGridNode.tsx +451 -0
  203. package/src/components/ViewHeaderButton.tsx +60 -0
  204. package/src/components/ViewPanel.tsx +162 -0
  205. package/src/components/ViewsGridOnboarding.tsx +400 -0
  206. package/src/components/ZUI/ZUICanvas.tsx +853 -0
  207. package/src/components/ZUI/index.ts +3 -0
  208. package/src/components/ZUI/layout.ts +323 -0
  209. package/src/components/ZUI/proxy.ts +278 -0
  210. package/src/components/ZUI/renderer.ts +1189 -0
  211. package/src/components/ZUI/types.ts +150 -0
  212. package/src/components/ZUI/useZUIInteraction.ts +720 -0
  213. package/src/config/runtime-vscode.ts +46 -0
  214. package/src/config/runtime.ts +30 -0
  215. package/src/constants/colors.ts +80 -0
  216. package/src/constants/diagramColors.ts +9 -0
  217. package/src/context/ThemeContext.tsx +158 -0
  218. package/src/crossBranch/graph.ts +207 -0
  219. package/src/crossBranch/resolve.ts +643 -0
  220. package/src/crossBranch/settings.ts +59 -0
  221. package/src/crossBranch/store.ts +71 -0
  222. package/src/crossBranch/types.ts +102 -0
  223. package/src/demo/DemoPage.tsx +184 -0
  224. package/src/demo/seed.ts +67 -0
  225. package/src/demo/store.ts +536 -0
  226. package/src/demo/viewEditor.ts +110 -0
  227. package/src/hooks/useSafeFitView.ts +60 -0
  228. package/src/index.css +309 -0
  229. package/src/index.ts +184 -0
  230. package/src/kafka-ss.png +0 -0
  231. package/src/lib/vscodeBridge-vscode.ts +27 -0
  232. package/src/lib/vscodeBridge.ts +7 -0
  233. package/src/main.tsx +46 -0
  234. package/src/pages/AppearanceSettings.tsx +135 -0
  235. package/src/pages/Dependencies.tsx +926 -0
  236. package/src/pages/InfiniteZoom.tsx +404 -0
  237. package/src/pages/Settings.tsx +91 -0
  238. package/src/pages/ViewEditor/EDGE_DISTRIBUTION.md +64 -0
  239. package/src/pages/ViewEditor/components/EditorMenus.tsx +112 -0
  240. package/src/pages/ViewEditor/components/EditorOverlays.tsx +172 -0
  241. package/src/pages/ViewEditor/components/EmptyCanvasState.tsx +42 -0
  242. package/src/pages/ViewEditor/context.tsx +21 -0
  243. package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +1349 -0
  244. package/src/pages/ViewEditor/hooks/useDrawingEngine.ts +127 -0
  245. package/src/pages/ViewEditor/hooks/useViewContextNeighbours.ts +501 -0
  246. package/src/pages/ViewEditor/hooks/useViewData.ts +491 -0
  247. package/src/pages/ViewEditor/index.tsx +1366 -0
  248. package/src/pages/ViewEditor/utils.ts +88 -0
  249. package/src/pages/Views.tsx +171 -0
  250. package/src/pages/ViewsGrid.tsx +1310 -0
  251. package/src/pkg/importer/mermaid.test.ts +141 -0
  252. package/src/pkg/importer/mermaid.ts +76 -0
  253. package/src/platform/PlatformContext.tsx +17 -0
  254. package/src/platform/context.ts +9 -0
  255. package/src/platform/local.tsx +15 -0
  256. package/src/platform/types.ts +19 -0
  257. package/src/slots.ts +92 -0
  258. package/src/styles/editor-panels.css +66 -0
  259. package/src/styles/theme.css +56 -0
  260. package/src/theme.ts +336 -0
  261. package/src/types/index.ts +234 -0
  262. package/src/types/offline-ambient.d.ts +14 -0
  263. package/src/types/vscode-messages.ts +32 -0
  264. package/src/utils/edgeDistribution.ts +103 -0
  265. package/src/utils/githubApi.ts +121 -0
  266. package/src/utils/githubCache.ts +108 -0
  267. package/src/utils/ids.ts +9 -0
  268. package/src/utils/technologyCatalog.ts +143 -0
  269. package/src/utils/toast.ts +100 -0
  270. package/src/utils/treesitter.ts +147 -0
  271. package/src/utils/url.ts +72 -0
  272. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,536 @@
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
+
8
+ import type {
9
+ LibraryElement,
10
+ PlacedElement,
11
+ Connector,
12
+ ViewTreeNode,
13
+ ViewLayer,
14
+ Tag,
15
+ ElementPlacement,
16
+ ExploreData,
17
+ } from '../types'
18
+ import {
19
+ DEMO_ELEMENTS,
20
+ DEMO_VIEWS,
21
+ DEMO_PLACEMENTS,
22
+ DEMO_CONNECTORS,
23
+ DEMO_LAYERS,
24
+ } from './seed'
25
+
26
+ // ── Keys ──────────────────────────────────────────────────────────────────────
27
+
28
+ const K = {
29
+ elements: 'diag:demo:elements',
30
+ views: 'diag:demo:views',
31
+ placements: (viewId: number) => `diag:demo:placements:${viewId}`,
32
+ connectors: (viewId: number) => `diag:demo:connectors:${viewId}`,
33
+ layers: (viewId: number) => `diag:demo:layers:${viewId}`,
34
+ tagColors: 'diag:demo:tagColors',
35
+ nextId: 'diag:demo:nextId',
36
+ } as const
37
+
38
+ // ── ID generation ──────────────────────────────────────────────────────────────
39
+
40
+ function nextId(): number {
41
+ const current = Number(localStorage.getItem(K.nextId) ?? '9000')
42
+ const next = current + 1
43
+ localStorage.setItem(K.nextId, String(next))
44
+ return next
45
+ }
46
+
47
+ // ── Generic persistence helpers ────────────────────────────────────────────────
48
+
49
+ function load<T>(key: string, fallback: T): T {
50
+ try {
51
+ const raw = localStorage.getItem(key)
52
+ if (raw === null) return fallback
53
+ return JSON.parse(raw) as T
54
+ } catch {
55
+ return fallback
56
+ }
57
+ }
58
+
59
+ function save(key: string, value: unknown): void {
60
+ localStorage.setItem(key, JSON.stringify(value))
61
+ }
62
+
63
+ // ── Initialisation ─────────────────────────────────────────────────────────────
64
+ // Seed data is written once on first visit (when the store key is absent).
65
+
66
+ export function initDemoStore(): void {
67
+ if (localStorage.getItem(K.elements) === null) {
68
+ save(K.elements, DEMO_ELEMENTS)
69
+ }
70
+ if (localStorage.getItem(K.views) === null) {
71
+ save(K.views, DEMO_VIEWS)
72
+ }
73
+ for (const [rawId, placements] of Object.entries(DEMO_PLACEMENTS)) {
74
+ const id = Number(rawId)
75
+ if (localStorage.getItem(K.placements(id)) === null) {
76
+ save(K.placements(id), placements)
77
+ }
78
+ }
79
+ for (const [rawId, connectors] of Object.entries(DEMO_CONNECTORS)) {
80
+ const id = Number(rawId)
81
+ if (localStorage.getItem(K.connectors(id)) === null) {
82
+ save(K.connectors(id), connectors)
83
+ }
84
+ }
85
+ for (const [rawId, layers] of Object.entries(DEMO_LAYERS)) {
86
+ const id = Number(rawId)
87
+ if (localStorage.getItem(K.layers(id)) === null) {
88
+ save(K.layers(id), layers)
89
+ }
90
+ }
91
+ }
92
+
93
+ export function resetDemoStore(): void {
94
+ localStorage.removeItem(K.elements)
95
+ localStorage.removeItem(K.views)
96
+ localStorage.removeItem(K.tagColors)
97
+ localStorage.removeItem(K.nextId)
98
+ localStorage.removeItem('diag:demo:accent-color')
99
+ localStorage.removeItem('diag:demo:background-color')
100
+ localStorage.removeItem('diag:demo:element-color')
101
+ for (const viewId of getAllViewIds()) {
102
+ localStorage.removeItem(K.placements(viewId))
103
+ localStorage.removeItem(K.connectors(viewId))
104
+ localStorage.removeItem(K.layers(viewId))
105
+ }
106
+ initDemoStore()
107
+ }
108
+
109
+ // ── Tree helpers ──────────────────────────────────────────────────────────────
110
+
111
+ function getAllViews(): ViewTreeNode[] {
112
+ return load<ViewTreeNode[]>(K.views, DEMO_VIEWS)
113
+ }
114
+
115
+ function flattenTree(nodes: ViewTreeNode[]): ViewTreeNode[] {
116
+ return nodes.flatMap((n) => [n, ...flattenTree(n.children ?? [])])
117
+ }
118
+
119
+ function getAllViewIds(): number[] {
120
+ return flattenTree(getAllViews()).map((v) => v.id)
121
+ }
122
+
123
+ function findViewById(id: number): ViewTreeNode | null {
124
+ return flattenTree(getAllViews()).find((v) => v.id === id) ?? null
125
+ }
126
+
127
+ function saveViews(roots: ViewTreeNode[]): void {
128
+ save(K.views, roots)
129
+ }
130
+
131
+ function insertViewIntoTree(roots: ViewTreeNode[], newView: ViewTreeNode): ViewTreeNode[] {
132
+ if (newView.parent_view_id === null) return [...roots, newView]
133
+ return roots.map((n) => {
134
+ if (n.id === newView.parent_view_id) return { ...n, children: [...(n.children ?? []), newView] }
135
+ return { ...n, children: insertViewIntoTree(n.children ?? [], newView) }
136
+ })
137
+ }
138
+
139
+ function deleteViewFromTree(roots: ViewTreeNode[], id: number): ViewTreeNode[] {
140
+ return roots
141
+ .filter((n) => n.id !== id)
142
+ .map((n) => ({ ...n, children: deleteViewFromTree(n.children ?? [], id) }))
143
+ }
144
+
145
+ // ── api surface ───────────────────────────────────────────────────────────────
146
+
147
+ const NOW = () => new Date().toISOString()
148
+
149
+ export const demoApi = {
150
+ explore: {
151
+ load: async (): Promise<ExploreData> => {
152
+ const tree = getAllViews()
153
+ const flat = flattenTree(tree)
154
+ const views: ExploreData['views'] = {}
155
+ for (const v of flat) {
156
+ views[v.id] = {
157
+ placements: load<PlacedElement[]>(K.placements(v.id), []),
158
+ connectors: load<Connector[]>(K.connectors(v.id), []),
159
+ }
160
+ }
161
+ return { tree, views, navigations: [] }
162
+ },
163
+ },
164
+
165
+ elements: {
166
+ list: async (_params?: unknown): Promise<LibraryElement[]> => {
167
+ return load<LibraryElement[]>(K.elements, DEMO_ELEMENTS)
168
+ },
169
+
170
+ get: async (id: number): Promise<LibraryElement> => {
171
+ const elements = load<LibraryElement[]>(K.elements, DEMO_ELEMENTS)
172
+ const el = elements.find((e) => e.id === id)
173
+ if (!el) throw new Error(`Element ${id} not found`)
174
+ return el
175
+ },
176
+
177
+ create: async (data: Partial<LibraryElement>): Promise<LibraryElement> => {
178
+ const elements = load<LibraryElement[]>(K.elements, DEMO_ELEMENTS)
179
+ const newEl: LibraryElement = {
180
+ id: nextId(),
181
+ name: data.name ?? 'New Element',
182
+ kind: data.kind ?? null,
183
+ description: data.description ?? null,
184
+ technology: data.technology ?? null,
185
+ url: data.url ?? null,
186
+ logo_url: data.logo_url ?? null,
187
+ technology_connectors: data.technology_connectors ?? [],
188
+ tags: data.tags ?? [],
189
+ repo: data.repo ?? null,
190
+ branch: data.branch ?? null,
191
+ file_path: data.file_path ?? null,
192
+ language: data.language ?? null,
193
+ created_at: NOW(),
194
+ updated_at: NOW(),
195
+ has_view: false,
196
+ view_label: null,
197
+ }
198
+ save(K.elements, [...elements, newEl])
199
+ return newEl
200
+ },
201
+
202
+ update: async (id: number, data: Partial<LibraryElement>): Promise<LibraryElement> => {
203
+ const elements = load<LibraryElement[]>(K.elements, DEMO_ELEMENTS)
204
+ const idx = elements.findIndex((e) => e.id === id)
205
+ if (idx === -1) throw new Error(`Element ${id} not found`)
206
+ const updated = { ...elements[idx], ...data, id, updated_at: NOW() }
207
+ const next = [...elements]
208
+ next[idx] = updated
209
+ save(K.elements, next)
210
+ // Patch all view placements that reference this element
211
+ for (const viewId of getAllViewIds()) {
212
+ const placements = load<PlacedElement[]>(K.placements(viewId), [])
213
+ const changed = placements.map((p) =>
214
+ p.element_id === id
215
+ ? { ...p, name: updated.name, kind: updated.kind, description: updated.description, technology: updated.technology, tags: updated.tags }
216
+ : p,
217
+ )
218
+ save(K.placements(viewId), changed)
219
+ }
220
+ return updated
221
+ },
222
+
223
+ delete: async (_orgId: string, id: number): Promise<void> => {
224
+ const elements = load<LibraryElement[]>(K.elements, DEMO_ELEMENTS)
225
+ save(K.elements, elements.filter((e) => e.id !== id))
226
+ for (const viewId of getAllViewIds()) {
227
+ const placements = load<PlacedElement[]>(K.placements(viewId), [])
228
+ save(K.placements(viewId), placements.filter((p) => p.element_id !== id))
229
+ const connectors = load<Connector[]>(K.connectors(viewId), [])
230
+ save(K.connectors(viewId), connectors.filter((c) => c.source_element_id !== id && c.target_element_id !== id))
231
+ }
232
+ },
233
+
234
+ placements: async (id: number): Promise<ElementPlacement[]> => {
235
+ const result: ElementPlacement[] = []
236
+ for (const viewId of getAllViewIds()) {
237
+ const placements = load<PlacedElement[]>(K.placements(viewId), [])
238
+ const match = placements.find((p) => p.element_id === id)
239
+ if (match) result.push({ id: match.id, view_id: viewId, element_id: id, position_x: match.position_x, position_y: match.position_y })
240
+ }
241
+ return result
242
+ },
243
+ },
244
+
245
+ workspace: {
246
+ orgs: {
247
+ tagColors: {
248
+ list: async (): Promise<Record<string, Tag>> => {
249
+ return load<Record<string, Tag>>(K.tagColors, {})
250
+ },
251
+ set: async (tag: string, color: string, description?: string): Promise<void> => {
252
+ const tags = load<Record<string, Tag>>(K.tagColors, {})
253
+ save(K.tagColors, { ...tags, [tag]: { name: tag, color, description: description ?? null } })
254
+ },
255
+ },
256
+ },
257
+
258
+ views: {
259
+ list: async () => {
260
+ return flattenTree(getAllViews()).map((v) => ({
261
+ id: v.id,
262
+ owner_element_id: v.owner_element_id ?? null,
263
+ name: v.name,
264
+ label: v.level_label,
265
+ is_root: v.parent_view_id === null,
266
+ created_at: v.created_at,
267
+ updated_at: v.updated_at,
268
+ }))
269
+ },
270
+
271
+ get: async (id: number): Promise<ViewTreeNode> => {
272
+ const v = findViewById(id)
273
+ if (!v) throw new Error(`View ${id} not found`)
274
+ return v
275
+ },
276
+
277
+ content: async (id: number): Promise<{ placements: PlacedElement[]; connectors: Connector[] }> => {
278
+ return {
279
+ placements: load<PlacedElement[]>(K.placements(id), []),
280
+ connectors: load<Connector[]>(K.connectors(id), []),
281
+ }
282
+ },
283
+
284
+ tree: async (): Promise<ViewTreeNode[]> => {
285
+ return getAllViews()
286
+ },
287
+
288
+ create: async (data: { name: string; label?: string; parent_view_id?: number | null }) => {
289
+ const id = nextId()
290
+ const now = NOW()
291
+ const newView: ViewTreeNode = {
292
+ id,
293
+ name: data.name,
294
+ description: null,
295
+ level_label: data.label ?? null,
296
+ level: 0,
297
+ depth: 0,
298
+ owner_element_id: data.parent_view_id ?? null,
299
+ parent_view_id: data.parent_view_id ?? null,
300
+ created_at: now,
301
+ updated_at: now,
302
+ children: [],
303
+ }
304
+ const roots = getAllViews()
305
+ saveViews(insertViewIntoTree(roots, newView))
306
+ save(K.placements(id), [])
307
+ save(K.connectors(id), [])
308
+ save(K.layers(id), [])
309
+
310
+ // Mark the owning element as having a view
311
+ if (data.parent_view_id != null) {
312
+ const elements = load<LibraryElement[]>(K.elements, DEMO_ELEMENTS)
313
+ const idx = elements.findIndex((e) => e.id === data.parent_view_id)
314
+ if (idx !== -1) {
315
+ const next = [...elements]
316
+ next[idx] = { ...next[idx], has_view: true }
317
+ save(K.elements, next)
318
+ }
319
+ // Patch all placements of that element to reflect has_view
320
+ for (const viewId of getAllViewIds()) {
321
+ const placements = load<PlacedElement[]>(K.placements(viewId), [])
322
+ const changed = placements.map((p) =>
323
+ p.element_id === data.parent_view_id ? { ...p, has_view: true } : p,
324
+ )
325
+ save(K.placements(viewId), changed)
326
+ }
327
+ }
328
+
329
+ return {
330
+ id,
331
+ owner_element_id: data.parent_view_id ?? null,
332
+ name: data.name,
333
+ label: data.label ?? null,
334
+ is_root: data.parent_view_id === null,
335
+ created_at: now,
336
+ updated_at: now,
337
+ }
338
+ },
339
+
340
+ update: async (id: number, data: { name: string; label?: string }) => {
341
+ const roots = getAllViews()
342
+ const patchInTree = (nodes: ViewTreeNode[]): ViewTreeNode[] =>
343
+ nodes.map((n) =>
344
+ n.id === id
345
+ ? { ...n, name: data.name, level_label: data.label ?? n.level_label, updated_at: NOW() }
346
+ : { ...n, children: patchInTree(n.children ?? []) },
347
+ )
348
+ saveViews(patchInTree(roots))
349
+ const v = findViewById(id)
350
+ return {
351
+ id,
352
+ owner_element_id: v?.owner_element_id ?? null,
353
+ name: data.name,
354
+ label: data.label ?? null,
355
+ is_root: v?.parent_view_id === null,
356
+ created_at: v?.created_at ?? NOW(),
357
+ updated_at: NOW(),
358
+ }
359
+ },
360
+
361
+ delete: async (_orgId: string, id: number): Promise<void> => {
362
+ const roots = getAllViews()
363
+ saveViews(deleteViewFromTree(roots, id))
364
+ localStorage.removeItem(K.placements(id))
365
+ localStorage.removeItem(K.connectors(id))
366
+ localStorage.removeItem(K.layers(id))
367
+ },
368
+
369
+ placements: {
370
+ list: async (viewId: number): Promise<ElementPlacement[]> => {
371
+ const placements = load<PlacedElement[]>(K.placements(viewId), [])
372
+ return placements.map((p) => ({ id: p.id, view_id: viewId, element_id: p.element_id, position_x: p.position_x, position_y: p.position_y }))
373
+ },
374
+
375
+ add: async (viewId: number, elementId: number, x = 100, y = 100): Promise<ElementPlacement> => {
376
+ const elements = load<LibraryElement[]>(K.elements, DEMO_ELEMENTS)
377
+ const el = elements.find((e) => e.id === elementId)
378
+ if (!el) throw new Error(`Element ${elementId} not found`)
379
+ const placements = load<PlacedElement[]>(K.placements(viewId), [])
380
+ const id = nextId()
381
+ const newPlacement: PlacedElement = {
382
+ id,
383
+ view_id: viewId,
384
+ element_id: elementId,
385
+ position_x: x,
386
+ position_y: y,
387
+ name: el.name,
388
+ kind: el.kind,
389
+ description: el.description,
390
+ technology: el.technology,
391
+ url: el.url,
392
+ logo_url: el.logo_url,
393
+ technology_connectors: el.technology_connectors,
394
+ tags: el.tags,
395
+ repo: el.repo,
396
+ branch: el.branch,
397
+ file_path: el.file_path,
398
+ language: el.language,
399
+ has_view: el.has_view,
400
+ view_label: el.view_label,
401
+ }
402
+ save(K.placements(viewId), [...placements, newPlacement])
403
+ return { id, view_id: viewId, element_id: elementId, position_x: x, position_y: y }
404
+ },
405
+
406
+ updatePosition: async (viewId: number, elementId: number, x: number, y: number): Promise<void> => {
407
+ const placements = load<PlacedElement[]>(K.placements(viewId), [])
408
+ save(K.placements(viewId), placements.map((p) => p.element_id === elementId ? { ...p, position_x: x, position_y: y } : p))
409
+ },
410
+
411
+ remove: async (viewId: number, elementId: number): Promise<void> => {
412
+ const placements = load<PlacedElement[]>(K.placements(viewId), [])
413
+ save(K.placements(viewId), placements.filter((p) => p.element_id !== elementId))
414
+ },
415
+ },
416
+
417
+ layers: {
418
+ list: async (viewId: number): Promise<ViewLayer[]> => {
419
+ return load<ViewLayer[]>(K.layers(viewId), [])
420
+ },
421
+ create: async (viewId: number, data: { name: string; tags: string[]; color?: string }): Promise<ViewLayer> => {
422
+ const layers = load<ViewLayer[]>(K.layers(viewId), [])
423
+ const id = nextId()
424
+ const newLayer: ViewLayer = { id, diagram_id: viewId, name: data.name, tags: data.tags, color: data.color ?? '#888888', created_at: NOW(), updated_at: NOW() }
425
+ save(K.layers(viewId), [...layers, newLayer])
426
+ return newLayer
427
+ },
428
+ update: async (viewId: number, layerId: number, data: Partial<ViewLayer>): Promise<ViewLayer> => {
429
+ const layers = load<ViewLayer[]>(K.layers(viewId), [])
430
+ const idx = layers.findIndex((l) => l.id === layerId)
431
+ if (idx === -1) throw new Error(`Layer ${layerId} not found`)
432
+ const updated = { ...layers[idx], ...data, id: layerId, updated_at: NOW() }
433
+ const next = [...layers]
434
+ next[idx] = updated
435
+ save(K.layers(viewId), next)
436
+ return updated
437
+ },
438
+ delete: async (viewId: number, layerId: number): Promise<void> => {
439
+ const layers = load<ViewLayer[]>(K.layers(viewId), [])
440
+ save(K.layers(viewId), layers.filter((l) => l.id !== layerId))
441
+ },
442
+ },
443
+
444
+ reactions: {
445
+ list: async (_viewId: number) => [],
446
+ },
447
+
448
+ threads: {
449
+ listForElement: async () => [],
450
+ listForConnector: async () => [],
451
+ createForElement: async () => { throw new Error('Demo: threads not supported') },
452
+ createForConnector: async () => { throw new Error('Demo: threads not supported') },
453
+ addComment: async () => { throw new Error('Demo: comments not supported') },
454
+ resolve: async () => { /* no-op */ },
455
+ },
456
+
457
+ thumbnail: (_id: number) => Promise.resolve(null),
458
+ rename: async (id: number, name: string) => demoApi.workspace.views.update(id, { name }),
459
+ setLevel: async () => { /* no-op */ },
460
+ reparent: async () => { throw new Error('Demo: reparent not supported') },
461
+ },
462
+
463
+ connectors: {
464
+ list: async (viewId: number): Promise<Connector[]> => {
465
+ return load<Connector[]>(K.connectors(viewId), [])
466
+ },
467
+
468
+ create: async (
469
+ viewId: number,
470
+ data: {
471
+ source_element_id: number; target_element_id: number
472
+ label?: string; description?: string; relationship?: string
473
+ direction?: string; style?: string; url?: string
474
+ source_handle?: string | null; target_handle?: string | null
475
+ },
476
+ ): Promise<Connector> => {
477
+ const connectors = load<Connector[]>(K.connectors(viewId), [])
478
+ const id = nextId()
479
+ const now = NOW()
480
+ const newConnector: Connector = {
481
+ id,
482
+ view_id: viewId,
483
+ source_element_id: data.source_element_id,
484
+ target_element_id: data.target_element_id,
485
+ label: data.label ?? null,
486
+ description: data.description ?? null,
487
+ relationship: data.relationship ?? null,
488
+ direction: data.direction ?? 'forward',
489
+ style: data.style ?? 'bezier',
490
+ url: data.url ?? null,
491
+ source_handle: data.source_handle ?? null,
492
+ target_handle: data.target_handle ?? null,
493
+ created_at: now,
494
+ updated_at: now,
495
+ }
496
+ save(K.connectors(viewId), [...connectors, newConnector])
497
+ return newConnector
498
+ },
499
+
500
+ update: async (
501
+ viewId: number,
502
+ connectorId: number,
503
+ data: Partial<Connector>,
504
+ ): Promise<Connector> => {
505
+ const connectors = load<Connector[]>(K.connectors(viewId), [])
506
+ const idx = connectors.findIndex((c) => c.id === connectorId)
507
+ if (idx === -1) throw new Error(`Connector ${connectorId} not found`)
508
+ const updated = { ...connectors[idx], ...data, id: connectorId, updated_at: NOW() }
509
+ const next = [...connectors]
510
+ next[idx] = updated
511
+ save(K.connectors(viewId), next)
512
+ return updated
513
+ },
514
+
515
+ delete: async (_orgId: string, connectorId: number): Promise<void> => {
516
+ for (const viewId of getAllViewIds()) {
517
+ const connectors = load<Connector[]>(K.connectors(viewId), [])
518
+ const filtered = connectors.filter((c) => c.id !== connectorId)
519
+ if (filtered.length !== connectors.length) {
520
+ save(K.connectors(viewId), filtered)
521
+ break
522
+ }
523
+ }
524
+ },
525
+ },
526
+
527
+ elements: {
528
+ list: (params?: unknown) => demoApi.elements.list(params),
529
+ get: (id: number) => demoApi.elements.get(id),
530
+ create: (data: Partial<LibraryElement>) => demoApi.elements.create(data),
531
+ update: (id: number, data: Partial<LibraryElement>) => demoApi.elements.update(id, data),
532
+ delete: (orgId: string, id: number) => demoApi.elements.delete(orgId, id),
533
+ placements: (id: number) => demoApi.elements.placements(id),
534
+ },
535
+ },
536
+ }
@@ -0,0 +1,110 @@
1
+ import { useCallback, useEffect, useMemo, useRef, type MutableRefObject, type RefObject } from 'react'
2
+ import { getNodesBounds, getViewportForBounds, type Node as RFNode, type ReactFlowInstance } from 'reactflow'
3
+
4
+ export interface ViewEditorDemoOptions {
5
+ revealProgress?: number
6
+ disableImportExport?: boolean
7
+ hideFlowControls?: boolean
8
+ disableOnboarding?: boolean
9
+ }
10
+
11
+ export const DEMO_VIEW_EDITOR_OPTIONS: Omit<ViewEditorDemoOptions, 'revealProgress'> = {
12
+ disableImportExport: true,
13
+ hideFlowControls: true,
14
+ disableOnboarding: true,
15
+ }
16
+
17
+ function getCenteredViewport(bounds: { x: number; y: number; width: number; height: number }, width: number, height: number, zoom: number) {
18
+ const centerX = bounds.x + bounds.width / 2
19
+ const centerY = bounds.y + bounds.height / 2
20
+
21
+ return {
22
+ x: width / 2 - centerX * zoom,
23
+ y: height / 2 - centerY * zoom,
24
+ zoom,
25
+ }
26
+ }
27
+
28
+ interface UseDemoRevealViewportArgs {
29
+ demoOptions?: ViewEditorDemoOptions
30
+ containerRef: RefObject<HTMLDivElement | null>
31
+ rfNodesRef: MutableRefObject<RFNode[]>
32
+ rfReadyRef: MutableRefObject<boolean>
33
+ needsFitViewRef: MutableRefObject<boolean>
34
+ computedMinZoom: number
35
+ setViewport: ReactFlowInstance['setViewport']
36
+ resetKey: number | null
37
+ }
38
+
39
+ export function useDemoRevealViewport({
40
+ demoOptions,
41
+ containerRef,
42
+ rfNodesRef,
43
+ rfReadyRef,
44
+ needsFitViewRef,
45
+ computedMinZoom,
46
+ setViewport,
47
+ resetKey,
48
+ }: UseDemoRevealViewportArgs) {
49
+ const clampedRevealProgress = useMemo(() => {
50
+ if (typeof demoOptions?.revealProgress !== 'number') return null
51
+ return Math.max(0, Math.min(1, demoOptions.revealProgress))
52
+ }, [demoOptions?.revealProgress])
53
+
54
+ const revealZoomRef = useRef<number | null>(null)
55
+
56
+ useEffect(() => {
57
+ revealZoomRef.current = null
58
+ }, [resetKey, clampedRevealProgress])
59
+
60
+ const applyDemoRevealViewport = useCallback(() => {
61
+ if (clampedRevealProgress === null) return false
62
+
63
+ const el = containerRef.current
64
+ const nodes = rfNodesRef.current
65
+ if (!el || nodes.length === 0) return false
66
+ if (!nodes.every((node) => typeof node.width === 'number' && node.width > 0 && typeof node.height === 'number' && node.height > 0)) return false
67
+
68
+ const width = el.clientWidth
69
+ const height = el.clientHeight
70
+ if (width < 10 || height < 10) return false
71
+
72
+ const bounds = getNodesBounds(nodes)
73
+ const fittedViewport = getViewportForBounds(bounds, width, height, computedMinZoom, 2, 0.1)
74
+ if (![fittedViewport.x, fittedViewport.y, fittedViewport.zoom].every(Number.isFinite)) return false
75
+
76
+ if (clampedRevealProgress >= 0.999) {
77
+ revealZoomRef.current = fittedViewport.zoom
78
+ setViewport(fittedViewport, { duration: 0 })
79
+ return true
80
+ }
81
+
82
+ const fixedZoom = revealZoomRef.current ?? fittedViewport.zoom
83
+ revealZoomRef.current = fixedZoom
84
+
85
+ const centeredViewport = getCenteredViewport(bounds, width, height, fixedZoom)
86
+ const reveal = 1 - Math.pow(1 - clampedRevealProgress, 3)
87
+ const hiddenOffsetX = Math.max(width * 1.15, bounds.width * fixedZoom * 0.75)
88
+
89
+ setViewport({
90
+ x: centeredViewport.x + hiddenOffsetX * (1 - reveal),
91
+ y: centeredViewport.y,
92
+ zoom: fixedZoom,
93
+ }, { duration: 0 })
94
+
95
+ return true
96
+ }, [clampedRevealProgress, computedMinZoom, containerRef, rfNodesRef, setViewport])
97
+
98
+ useEffect(() => {
99
+ if (clampedRevealProgress === null || !rfReadyRef.current) return
100
+ const ok = applyDemoRevealViewport()
101
+ if (ok && clampedRevealProgress >= 0.999) needsFitViewRef.current = false
102
+ }, [applyDemoRevealViewport, clampedRevealProgress, needsFitViewRef, rfReadyRef])
103
+
104
+ return {
105
+ clampedRevealProgress,
106
+ applyDemoRevealViewport,
107
+ disableImportExport: demoOptions?.disableImportExport ?? false,
108
+ hideFlowControls: demoOptions?.hideFlowControls ?? false,
109
+ }
110
+ }