@tldiagram/core-ui 1.90.1 → 1.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
- }