@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
package/src/demo/seed.ts DELETED
@@ -1,67 +0,0 @@
1
- import type { LibraryElement, PlacedElement, Connector, ViewTreeNode, ViewLayer } from '../types'
2
-
3
- const NOW = new Date().toISOString()
4
-
5
- export const DEMO_ELEMENTS: LibraryElement[] = [
6
- { id: 1, name: 'User', kind: 'person', description: 'End user of the system', technology: null, url: null, logo_url: "https://tldiagram.com/app/icons/azure-users.png", technology_connectors: [], tags: ['external'], has_view: false, view_label: null, created_at: NOW, updated_at: NOW },
7
- { id: 2, name: 'Web App', kind: 'service', description: 'React single-page application', technology: 'React', url: null, logo_url: "https://tldiagram.com/app/icons/react.png", technology_connectors: [], tags: ['frontend'], has_view: true, view_label: null, created_at: NOW, updated_at: NOW },
8
- { id: 3, name: 'API Gateway', kind: 'service', description: 'REST API gateway', technology: 'Golang', url: null, logo_url: "https://tldiagram.com/app/icons/golang.png", technology_connectors: [], tags: ['backend'], has_view: false, view_label: null, created_at: NOW, updated_at: NOW },
9
- { id: 4, name: 'Auth Service', kind: 'service', description: 'Handles authentication & sessions', technology: 'Go', url: null, logo_url: "https://tldiagram.com/app/icons/golang.png", technology_connectors: [], tags: ['backend'], has_view: false, view_label: null, created_at: NOW, updated_at: NOW },
10
- { id: 5, name: 'Frontend', kind: 'service', description: 'React frontend bundle', technology: 'React', url: null, logo_url: null, technology_connectors: [], tags: ['frontend'], has_view: false, view_label: null, created_at: NOW, updated_at: NOW },
11
- { id: 6, name: 'Backend', kind: 'service', description: 'Core business logic API', technology: 'Go', url: null, logo_url: "https://tldiagram.com/app/icons/golang.png", technology_connectors: [], tags: ['backend'], has_view: false, view_label: null, created_at: NOW, updated_at: NOW },
12
- { id: 7, name: 'PostgreSQL', kind: 'database', description: 'Primary relational database', technology: 'PostgreSQL', url: null, logo_url: null, technology_connectors: [], tags: ['data'], has_view: false, view_label: null, created_at: NOW, updated_at: NOW },
13
- { id: 8, name: 'Redis Cache', kind: 'database', description: 'In-memory cache and session store', technology: 'Redis', url: null, logo_url: null, technology_connectors: [], tags: ['data'], has_view: false, view_label: null, created_at: NOW, updated_at: NOW },
14
- { id: 9, name: 'CDN', kind: 'service', description: 'Content delivery network', technology: 'Cloudflare', url: null, logo_url: null, technology_connectors: [], tags: ['external', 'infrastructure'], has_view: false, view_label: null, created_at: NOW, updated_at: NOW },
15
- ]
16
-
17
- export const DEMO_VIEWS: ViewTreeNode[] = [
18
- {
19
- id: 1, name: 'System Context', description: 'Top-level system context view', level_label: 'Context', level: 1, depth: 0,
20
- owner_element_id: null, parent_view_id: null, created_at: NOW, updated_at: NOW, children: [
21
- {
22
- id: 2, name: 'Web App – Containers', description: 'Container-level view of the Web App', level_label: 'Container', level: 2, depth: 1,
23
- owner_element_id: 2, parent_view_id: 1, created_at: NOW, updated_at: NOW, children: [],
24
- },
25
- ],
26
- },
27
- ]
28
-
29
- type ViewPlacements = Record<number, PlacedElement[]>
30
- type ViewConnectors = Record<number, Connector[]>
31
-
32
- export const DEMO_PLACEMENTS: ViewPlacements = {
33
- 1: [
34
- { id: 101, view_id: 1, element_id: 1, position_x: 80, position_y: 200, name: 'User', kind: 'person', description: 'End user of the system', technology: null, url: null, logo_url: "https://tldiagram.com/app/icons/azure-users.png", technology_connectors: [], tags: ['external'], has_view: false, view_label: null },
35
- { id: 102, view_id: 1, element_id: 2, position_x: 380, position_y: 200, name: 'Web App', kind: 'service', description: 'React single-page application', technology: 'React', url: null, logo_url: "https://tldiagram.com/app/icons/react.png", technology_connectors: [], tags: ['frontend'], has_view: true, view_label: null },
36
- { id: 103, view_id: 1, element_id: 3, position_x: 680, position_y: 200, name: 'API Gateway', kind: 'service', description: 'REST API gateway', technology: 'Go', url: null, logo_url: "https://tldiagram.com/app/icons/golang.png", technology_connectors: [], tags: ['backend'], has_view: false, view_label: null },
37
- { id: 104, view_id: 1, element_id: 4, position_x: 380, position_y: 400, name: 'Auth Service', kind: 'service', description: 'Handles authentication & sessions', technology: 'Auth0', url: null, logo_url: "https://tldiagram.com/app/icons/auth0.png", technology_connectors: [], tags: ['backend'], has_view: false, view_label: null },
38
- { id: 105, view_id: 1, element_id: 9, position_x: 380, position_y: 0, name: 'CDN', kind: 'service', description: 'Content delivery network', technology: 'Cloudflare', url: null, logo_url: "https://tldiagram.com/app/icons/cloudflare.png", technology_connectors: [], tags: ['external', 'infrastructure'], has_view: false, view_label: null },
39
- { id: 106, view_id: 1, element_id: 10, position_x: 680, position_y: 0, name: 'Stripe', kind: 'service', description: 'Payment', technology: 'Stripe', url: null, logo_url: "https://tldiagram.com/app/icons/stripe.png", technology_connectors: [], tags: ['external', 'billing'], has_view: false, view_label: null },
40
- ],
41
- 2: [
42
- { id: 201, view_id: 2, element_id: 5, position_x: 80, position_y: 200, name: 'Frontend', kind: 'service', description: 'React frontend bundle', technology: 'React', url: null, logo_url: "https://tldiagram.com/app/icons/react.png", technology_connectors: [], tags: ['frontend'], has_view: false, view_label: null },
43
- { id: 202, view_id: 2, element_id: 6, position_x: 380, position_y: 200, name: 'Backend', kind: 'service', description: 'Core business logic API', technology: 'Go', url: null, logo_url: "https://tldiagram.com/app/icons/golang.png", technology_connectors: [], tags: ['backend'], has_view: false, view_label: null },
44
- { id: 203, view_id: 2, element_id: 7, position_x: 680, position_y: 200, name: 'PostgreSQL', kind: 'database', description: 'Primary relational database', technology: 'PostgreSQL', url: null, logo_url: "https://tldiagram.com/app/icons/postgresql.png", technology_connectors: [], tags: ['data'], has_view: false, view_label: null },
45
- { id: 204, view_id: 2, element_id: 8, position_x: 680, position_y: 400, name: 'Redis Cache', kind: 'database', description: 'In-memory cache and session store', technology: 'Redis', url: null, logo_url: "https://tldiagram.com/app/icons/redis.png", technology_connectors: [], tags: ['data'], has_view: false, view_label: null },
46
- ],
47
- }
48
-
49
- export const DEMO_CONNECTORS: ViewConnectors = {
50
- 1: [
51
- { id: 1001, view_id: 1, source_element_id: 1, target_element_id: 2, label: 'Uses', description: null, relationship: null, direction: 'forward', style: 'bezier', url: null, source_handle: 'right', target_handle: 'left', created_at: NOW, updated_at: NOW },
52
- { id: 1002, view_id: 1, source_element_id: 2, target_element_id: 3, label: 'API calls', description: null, relationship: null, direction: 'forward', style: 'bezier', url: null, source_handle: 'right', target_handle: 'left', created_at: NOW, updated_at: NOW },
53
- { id: 1003, view_id: 1, source_element_id: 2, target_element_id: 4, label: 'Auth', description: null, relationship: null, direction: 'forward', style: 'bezier', url: null, source_handle: 'bottom', target_handle: 'top', created_at: NOW, updated_at: NOW },
54
- { id: 1004, view_id: 1, source_element_id: 2, target_element_id: 9, label: 'Serves via', description: null, relationship: null, direction: 'backward', style: 'bezier', url: null, source_handle: 'top', target_handle: 'bottom', created_at: NOW, updated_at: NOW },
55
- { id: 1005, view_id: 1, source_element_id: 3, target_element_id: 10, label: 'Billing', description: null, relationship: null, direction: 'both', style: 'bezier', url: null, source_handle: 'top', target_handle: 'bottom', created_at: NOW, updated_at: NOW },
56
- ],
57
- 2: [
58
- { id: 2001, view_id: 2, source_element_id: 5, target_element_id: 6, label: 'HTTP/JSON', description: null, relationship: null, direction: 'forward', style: 'bezier', url: null, source_handle: 'right', target_handle: 'left', created_at: NOW, updated_at: NOW },
59
- { id: 2002, view_id: 2, source_element_id: 6, target_element_id: 7, label: 'Reads / Writes', description: null, relationship: null, direction: 'forward', style: 'bezier', url: null, source_handle: 'right', target_handle: 'left', created_at: NOW, updated_at: NOW },
60
- { id: 2003, view_id: 2, source_element_id: 6, target_element_id: 8, label: 'Cache', description: null, relationship: null, direction: 'forward', style: 'bezier', url: null, source_handle: 'bottom', target_handle: 'left', created_at: NOW, updated_at: NOW },
61
- ],
62
- }
63
-
64
- export const DEMO_LAYERS: Record<number, ViewLayer[]> = {
65
- 1: [],
66
- 2: [],
67
- }
package/src/demo/store.ts DELETED
@@ -1,536 +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
-
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
- }