@likec4/diagram 1.1.0 → 1.2.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 (76) hide show
  1. package/dist/KeepAspectRatioContainer.d.ts +1 -1
  2. package/dist/LikeC4Diagram.d.ts +1 -1
  3. package/dist/LikeC4Diagram.props.d.ts +22 -12
  4. package/dist/StaticLikeC4Diagram.d.ts +1 -1
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.mjs +2209 -706
  7. package/dist/mantine/EnsureMantine.d.ts +1 -1
  8. package/dist/state/DiagramContext.d.ts +2143 -19
  9. package/dist/state/WhenInitialized.d.ts +1 -1
  10. package/dist/state/diagramStore.d.ts +2152 -19
  11. package/dist/state/useDiagramStore.d.ts +2142 -18
  12. package/dist/style.css +256 -118
  13. package/dist/theme.css.d.ts +1 -0
  14. package/dist/ui/DiagramTitlePanel.css.d.ts +4 -0
  15. package/dist/ui/DiagramTitlePanel.d.ts +1 -0
  16. package/dist/ui/DynamicViewWalkthrough.css.d.ts +3 -0
  17. package/dist/ui/DynamicViewWalkthrough.d.ts +1 -0
  18. package/dist/ui/OptionsPanel.d.ts +1 -3
  19. package/dist/ui/index.d.ts +1 -0
  20. package/dist/ui/options/NodeOptions.d.ts +1 -2
  21. package/dist/xyflow/FitviewOnDiagramChange.d.ts +4 -2
  22. package/dist/xyflow/SelectEdgesOnNodeFocus.d.ts +1 -0
  23. package/dist/xyflow/SyncWithDiagram.d.ts +1 -2
  24. package/dist/xyflow/XYFlow.d.ts +4 -7
  25. package/dist/xyflow/XYFlowBackground.d.ts +5 -4
  26. package/dist/xyflow/XYFlowEvents.d.ts +84 -84
  27. package/dist/xyflow/XYFlowInner.d.ts +11 -0
  28. package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
  29. package/dist/xyflow/edges/RelationshipEdge.d.ts +14 -14
  30. package/dist/xyflow/edges/edges.css.d.ts +5 -2
  31. package/dist/xyflow/edges/utils.d.ts +2 -0
  32. package/dist/xyflow/hooks/useXYFlow.d.ts +126 -126
  33. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
  34. package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -1
  35. package/dist/xyflow/nodes/element/ElementLink.d.ts +1 -1
  36. package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -1
  37. package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +2 -2
  38. package/dist/xyflow/nodes/element/element.css.d.ts +2 -1
  39. package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +1 -2
  40. package/dist/xyflow/types.d.ts +18 -18
  41. package/package.json +21 -16
  42. package/src/KeepAspectRatioContainer.tsx +1 -1
  43. package/src/LikeC4Diagram.css.ts +4 -1
  44. package/src/LikeC4Diagram.props.ts +24 -14
  45. package/src/LikeC4Diagram.tsx +18 -9
  46. package/src/StaticLikeC4Diagram.tsx +2 -0
  47. package/src/globals.css.ts +7 -1
  48. package/src/index.ts +0 -3
  49. package/src/state/DiagramContext.tsx +21 -12
  50. package/src/state/diagramStore.ts +275 -98
  51. package/src/theme.css.ts +1 -0
  52. package/src/ui/DiagramTitlePanel.css.ts +33 -0
  53. package/src/ui/DiagramTitlePanel.tsx +138 -0
  54. package/src/ui/DynamicViewWalkthrough.css.ts +46 -0
  55. package/src/ui/DynamicViewWalkthrough.tsx +86 -0
  56. package/src/ui/OptionsPanel.tsx +7 -24
  57. package/src/ui/index.ts +1 -0
  58. package/src/ui/options/NodeOptions.tsx +103 -27
  59. package/src/xyflow/FitviewOnDiagramChange.tsx +89 -69
  60. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +75 -0
  61. package/src/xyflow/SyncWithDiagram.tsx +55 -101
  62. package/src/xyflow/XYFlow.tsx +52 -32
  63. package/src/xyflow/XYFlowBackground.tsx +7 -5
  64. package/src/xyflow/XYFlowEvents.tsx +150 -104
  65. package/src/xyflow/XYFlowInner.tsx +50 -0
  66. package/src/xyflow/diagram-to-xyflow.ts +16 -21
  67. package/src/xyflow/edges/RelationshipEdge.tsx +102 -70
  68. package/src/xyflow/edges/edges.css.ts +128 -53
  69. package/src/xyflow/edges/utils.ts +31 -0
  70. package/src/xyflow/nodes/compound/CompoundNode.css.ts +20 -12
  71. package/src/xyflow/nodes/compound/CompoundNode.tsx +6 -3
  72. package/src/xyflow/nodes/element/ElementLink.tsx +13 -3
  73. package/src/xyflow/nodes/element/ElementNode.tsx +24 -19
  74. package/src/xyflow/nodes/element/element.css.ts +10 -2
  75. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -0
  76. package/src/xyflow/types.ts +7 -8
@@ -1,17 +1,29 @@
1
- import type { DiagramNode, DiagramView, ElementShape, Fqn, NonEmptyArray, ThemeColor, ViewID } from '@likec4/core'
2
- import { invariant, nonexhaustive } from '@likec4/core'
3
- import type { XYPosition } from '@xyflow/react'
1
+ import type {
2
+ BorderStyle,
3
+ DiagramEdge,
4
+ DiagramNode,
5
+ DiagramView,
6
+ ElementShape,
7
+ Fqn,
8
+ NonEmptyArray,
9
+ ThemeColor,
10
+ ViewID
11
+ } from '@likec4/core'
12
+ import { invariant, nonexhaustive, StepEdgeId } from '@likec4/core'
13
+ import { getViewportForBounds, type XYPosition } from '@xyflow/react'
4
14
  import { getNodeDimensions } from '@xyflow/system'
5
15
  import { DEV } from 'esm-env'
6
- import { shallowEqual } from 'fast-equals'
16
+ import { deepEqual, shallowEqual } from 'fast-equals'
7
17
  import type { MouseEvent as ReactMouseEvent } from 'react'
18
+ import { entries, filter, map, prop } from 'remeda'
8
19
  import type { Exact, Except, RequiredKeysOf, Simplify } from 'type-fest'
9
20
  import { devtools, subscribeWithSelector } from 'zustand/middleware'
10
21
  import { shallow } from 'zustand/shallow'
11
22
  import { createWithEqualityFn } from 'zustand/traditional'
12
23
  import type { Change, DiagramNodeWithNavigate, LikeC4DiagramEventHandlers } from '../LikeC4Diagram.props'
13
24
  import { MinZoom } from '../xyflow/const'
14
- import type { XYFlowInstance, XYFlowNode } from '../xyflow/types'
25
+ import type { XYStoreApi } from '../xyflow/hooks'
26
+ import type { XYFlowEdge, XYFlowInstance, XYFlowNode } from '../xyflow/types'
15
27
 
16
28
  export type DiagramStore = {
17
29
  // Incoming props
@@ -28,6 +40,10 @@ export type DiagramStore = {
28
40
  // Internal state
29
41
  xyflow: XYFlowInstance
30
42
  initialized: boolean
43
+ xyflowSynced: boolean
44
+
45
+ // If Dynamic View
46
+ activeDynamicViewStep: number | null
31
47
 
32
48
  // This is XYFlow id's
33
49
  lastClickedNodeId: string | null
@@ -36,6 +52,9 @@ export type DiagramStore = {
36
52
  hoveredNodeId: string | null
37
53
  hoveredEdgeId: string | null
38
54
 
55
+ // id's of nodes / edges that
56
+ dimmed: ReadonlySet<string>
57
+
39
58
  // Stack of previous distinct views (unique id's)
40
59
  previousViews: DiagramView[]
41
60
 
@@ -52,6 +71,8 @@ export type DiagramStore = {
52
71
  viewportChanged: boolean
53
72
  } & Required<LikeC4DiagramEventHandlers>
54
73
 
74
+ const StringSet = Set<string>
75
+
55
76
  export type DiagramInitialState = // Required properties
56
77
  & Pick<
57
78
  DiagramStore,
@@ -75,31 +96,43 @@ export type DiagramInitialState = // Required properties
75
96
  & LikeC4DiagramEventHandlers
76
97
 
77
98
  interface DiagramStoreActions {
99
+ isDynamicView: () => boolean
100
+
78
101
  updateView: (view: DiagramView) => void
79
102
 
103
+ focusOnNode: (nodeId: string) => void
104
+
80
105
  setHoveredNode: (nodeId: string | null) => void
81
106
  setHoveredEdge: (edgeId: string | null) => void
82
107
 
83
108
  setLastClickedNode: (nodeId: string | null) => void
84
109
  setLastClickedEdge: (edgeId: string | null) => void
85
110
 
111
+ resetLastClicked: () => void
112
+
86
113
  getElement(id: Fqn): DiagramNode | null
87
114
  triggerOnChange: (changes: NonEmptyArray<Change>) => void
88
115
  triggerOnNavigateTo: (xynodeId: string, event: ReactMouseEvent) => void
89
- fitDiagram: (centerNode?: XYFlowNode) => void
116
+ fitDiagram: (xyStore: XYStoreApi) => void
117
+
118
+ nextDynamicStep: (increment?: number) => void
119
+ stopDynamicView: () => void
90
120
  }
91
121
 
92
122
  export type DiagramState = Simplify<DiagramStore & DiagramStoreActions>
93
123
 
94
124
  const DEFAULT_PROPS: Except<DiagramStore, RequiredKeysOf<DiagramInitialState> | 'xyflow'> = {
95
125
  initialized: false,
126
+ xyflowSynced: false,
96
127
  previousViews: [],
97
128
  viewportChanged: false,
129
+ activeDynamicViewStep: null,
98
130
  focusedNodeId: null,
99
131
  hoveredNodeId: null,
100
132
  hoveredEdgeId: null,
101
133
  lastClickedNodeId: null,
102
134
  lastClickedEdgeId: null,
135
+ dimmed: new StringSet(),
103
136
  lastOnNavigate: null,
104
137
  onChange: null,
105
138
  onNavigateTo: null,
@@ -118,7 +151,7 @@ const noReplace = false
118
151
 
119
152
  let storeDevId = 1
120
153
 
121
- const containsWithId = <T extends { id: string }>(arr: T[], id: string) => arr.some((x) => x.id === id)
154
+ const EmptyStringSet: ReadonlySet<string> = new StringSet()
122
155
 
123
156
  export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props: T) {
124
157
  return createWithEqualityFn<
@@ -134,78 +167,139 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
134
167
  ...DEFAULT_PROPS,
135
168
  ...(props as CreateDiagramStore),
136
169
 
137
- updateView: (view) => {
170
+ isDynamicView: () => {
171
+ return get().view?.__ === 'dynamic'
172
+ },
173
+
174
+ updateView: (nextView) => {
138
175
  let {
139
- view: currentView,
176
+ dimmed,
177
+ xyflowSynced,
178
+ view: current,
140
179
  lastOnNavigate,
141
180
  previousViews,
142
181
  focusedNodeId,
143
182
  lastClickedNodeId,
144
183
  lastClickedEdgeId,
184
+ activeDynamicViewStep,
145
185
  hoveredEdgeId,
146
186
  hoveredNodeId
147
187
  } = get()
148
188
 
149
- if (shallowEqual(currentView, view)) {
189
+ if (shallowEqual(current, nextView)) {
190
+ if (!xyflowSynced) {
191
+ set({ xyflowSynced: true }, noReplace, 'updateView: xyflow synced')
192
+ }
150
193
  DEV && console.debug('store: skip updateView')
151
194
  return
152
195
  }
153
196
 
154
- // Reset lastOnNavigate if the view is not the source or target view
155
- if (lastOnNavigate && lastOnNavigate.toView !== view.id && lastOnNavigate.fromView !== view.id) {
156
- lastOnNavigate = null
157
- }
158
-
159
- const isSameView = currentView.id === view.id
197
+ const isSameView = current.id === nextView.id
160
198
 
161
199
  if (isSameView) {
200
+ const nodeIds = new StringSet(nextView.nodes.map((n) => n.id))
201
+ const edgeIds = new StringSet(nextView.edges.map((e) => e.id))
162
202
  // Reset clicked/hovered node/edge if the node/edge is not in the new view
163
- if (lastClickedNodeId && !containsWithId(view.nodes, lastClickedNodeId)) {
203
+ if (lastClickedNodeId && !nodeIds.has(lastClickedNodeId)) {
164
204
  lastClickedNodeId = null
165
205
  }
166
- if (hoveredNodeId && !containsWithId(view.nodes, hoveredNodeId)) {
206
+ if (hoveredNodeId && !nodeIds.has(hoveredNodeId)) {
167
207
  hoveredNodeId = null
168
208
  }
169
- if (focusedNodeId && !containsWithId(view.nodes, focusedNodeId)) {
209
+ if (focusedNodeId && !nodeIds.has(focusedNodeId)) {
170
210
  focusedNodeId = null
171
211
  }
172
- if (lastClickedEdgeId && !containsWithId(view.edges, lastClickedEdgeId)) {
212
+ if (lastClickedEdgeId && !edgeIds.has(lastClickedEdgeId)) {
173
213
  lastClickedEdgeId = null
174
214
  }
175
- if (hoveredEdgeId && !containsWithId(view.edges, hoveredEdgeId)) {
215
+ if (hoveredEdgeId && !edgeIds.has(hoveredEdgeId)) {
176
216
  hoveredEdgeId = null
177
217
  }
218
+ if (activeDynamicViewStep && !edgeIds.has(StepEdgeId(activeDynamicViewStep))) {
219
+ activeDynamicViewStep = null
220
+ }
221
+ xyflowSynced = deepEqual(current.nodes, nextView.nodes) && deepEqual(current.edges, nextView.edges)
222
+ if (!xyflowSynced) {
223
+ let nextDimmed = new StringSet([...dimmed].filter(id => nodeIds.has(id) || edgeIds.has(id)))
224
+ if (nextDimmed.size !== dimmed.size) {
225
+ dimmed = nextDimmed
226
+ }
227
+ }
178
228
  } else {
229
+ // Reset lastOnNavigate if the view is not the source or target view
230
+ if (lastOnNavigate) {
231
+ if (lastOnNavigate.toView !== nextView.id && lastOnNavigate.fromView !== nextView.id) {
232
+ lastOnNavigate = null
233
+ }
234
+ }
235
+
179
236
  // Reset hovered / clicked node/edge if the view is different
180
237
  lastClickedEdgeId = null
181
238
  lastClickedNodeId = null
182
239
  hoveredEdgeId = null
183
240
  hoveredNodeId = null
184
241
  focusedNodeId = null
242
+ activeDynamicViewStep = null
243
+ dimmed = EmptyStringSet
185
244
 
186
245
  // Update history stack (back button not implemented yet)
187
246
  previousViews = [
188
- currentView,
189
- ...previousViews.filter((v) => v.id !== view.id && v.id !== currentView.id)
247
+ current,
248
+ ...previousViews.filter((v) => v.id !== nextView.id && v.id !== current.id)
190
249
  ]
250
+ xyflowSynced = false
191
251
  }
192
252
 
193
253
  set(
194
254
  {
195
- view,
255
+ view: nextView,
256
+ activeDynamicViewStep,
257
+ xyflowSynced,
196
258
  lastOnNavigate,
197
259
  previousViews,
198
260
  lastClickedNodeId,
199
261
  lastClickedEdgeId,
200
262
  focusedNodeId,
201
263
  hoveredEdgeId,
202
- hoveredNodeId
264
+ hoveredNodeId,
265
+ dimmed
203
266
  },
204
267
  noReplace,
205
268
  'update-view'
206
269
  )
207
270
  },
208
271
 
272
+ focusOnNode: (nodeId) => {
273
+ const { focusedNodeId, view } = get()
274
+ if (nodeId !== focusedNodeId) {
275
+ const notDimmed = new StringSet([nodeId])
276
+ const dimmed = new StringSet()
277
+ for (const edge of view.edges) {
278
+ if (edge.source === nodeId || edge.target === nodeId) {
279
+ notDimmed.add(edge.source)
280
+ notDimmed.add(edge.target)
281
+ } else {
282
+ dimmed.add(edge.id)
283
+ }
284
+ }
285
+ for (const node of view.nodes) {
286
+ if (notDimmed.has(node.id)) {
287
+ continue
288
+ }
289
+ dimmed.add(node.id)
290
+ }
291
+ set(
292
+ {
293
+ activeDynamicViewStep: null,
294
+ focusedNodeId: nodeId,
295
+ dimmed
296
+ },
297
+ noReplace,
298
+ `focus on node: ${nodeId}`
299
+ )
300
+ }
301
+ },
302
+
209
303
  setHoveredNode: (nodeId) => {
210
304
  if (nodeId !== get().hoveredNodeId) {
211
305
  set({ hoveredNodeId: nodeId }, noReplace, nodeId ? 'setHoveredNode' : 'unsetHoveredNode')
@@ -230,88 +324,115 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
230
324
  }
231
325
  },
232
326
 
327
+ resetLastClicked: () => {
328
+ let {
329
+ focusedNodeId,
330
+ lastClickedNodeId,
331
+ lastClickedEdgeId
332
+ } = get()
333
+ if (focusedNodeId || lastClickedNodeId || lastClickedEdgeId) {
334
+ set(
335
+ {
336
+ focusedNodeId: null,
337
+ lastClickedNodeId: null,
338
+ lastClickedEdgeId: null,
339
+ dimmed: EmptyStringSet
340
+ },
341
+ noReplace,
342
+ 'resetLastClicked'
343
+ )
344
+ }
345
+ },
346
+
233
347
  getElement: (fqn) => {
234
348
  const { view } = get()
235
349
  return view.nodes.find(({ id }) => id === fqn) ?? null
236
350
  },
237
351
 
238
352
  triggerOnChange: (changes: NonEmptyArray<Change>) => {
353
+ if (DEV) {
354
+ console.debug('triggerOnChange', changes)
355
+ }
239
356
  const newShapes = new Map<Fqn, ElementShape>()
240
357
  const newColor = new Map<Fqn, ThemeColor>()
358
+ const newOpacity = new Map<Fqn, number>()
359
+ const newBorder = new Map<Fqn, BorderStyle>()
241
360
  for (const change of changes) {
242
- if (change.op === 'change-shape') {
243
- for (const target of change.targets) {
244
- newShapes.set(target, change.shape)
245
- }
246
- continue
361
+ if (change.op !== 'change-element-style') {
362
+ nonexhaustive(change.op)
247
363
  }
248
- if (change.op === 'change-color') {
249
- for (const target of change.targets) {
250
- newColor.set(target, change.color)
364
+ for (const target of change.targets) {
365
+ for (const [key, value] of entries.strict(change.style)) {
366
+ switch (key) {
367
+ case 'shape':
368
+ newShapes.set(target, value)
369
+ break
370
+ case 'color':
371
+ newColor.set(target, value)
372
+ break
373
+ case 'opacity':
374
+ newOpacity.set(target, value)
375
+ break
376
+ case 'border':
377
+ newBorder.set(target, value)
378
+ break
379
+ default:
380
+ nonexhaustive(key)
381
+ }
251
382
  }
252
- continue
253
383
  }
254
- nonexhaustive(change)
255
384
  }
256
- // TODO: Update positions/sizes
257
- // const { view, updateView } = get()
258
- // let hasChanges = false
259
- // const nextNodes = view.nodes.map((node) => {
260
- // const shape = newShapes.get(node.id)
261
- // if (shape && shape !== node.shape) {
262
- // hasChanges = true
263
- // node = {
264
- // ...node,
265
- // shape
266
- // }
267
- // }
268
- // const color = newColor.get(node.id)
269
- // if (color && color !== node.color) {
270
- // hasChanges = true
271
- // node = {
272
- // ...node,
273
- // color
274
- // }
275
- // }
276
- // return node
277
- // })
278
- // if (hasChanges) {
279
- // updateView({
280
- // ...view,
281
- // nodes: nextNodes
282
- // })
283
- // }
385
+
284
386
  get().xyflow.setNodes(nodes =>
285
387
  nodes.map(node => {
286
- let { data } = node
287
- const shape = newShapes.get(data.fqn)
288
- if (shape && shape !== data.element.shape) {
289
- node = {
290
- ...node,
291
- data: {
292
- ...data,
293
- element: {
294
- ...data.element,
295
- shape
296
- }
388
+ let element = node.data.element
389
+ const shape = newShapes.get(element.id)
390
+ if (shape && shape !== element.shape) {
391
+ element = {
392
+ ...element,
393
+ shape
394
+ }
395
+ }
396
+ const color = newColor.get(element.id)
397
+ if (color && color !== element.color) {
398
+ element = {
399
+ ...element,
400
+ color
401
+ }
402
+ }
403
+
404
+ const opacity = newOpacity.get(element.id)
405
+ if (!!opacity && opacity !== element.style?.opacity) {
406
+ element = {
407
+ ...element,
408
+ style: {
409
+ ...element.style,
410
+ opacity
297
411
  }
298
412
  }
299
- data = node.data
300
413
  }
301
- const color = newColor.get(data.fqn)
302
- if (color && color !== data.element.color) {
303
- node = {
304
- ...node,
305
- data: {
306
- ...data,
307
- element: {
308
- ...data.element,
309
- color
310
- }
414
+
415
+ const border = newBorder.get(element.id)
416
+ if (border && border !== element.style?.border) {
417
+ element = {
418
+ ...element,
419
+ style: {
420
+ ...element.style,
421
+ border
311
422
  }
312
423
  }
313
424
  }
314
- return node
425
+
426
+ if (element === node.data.element) {
427
+ return node
428
+ }
429
+ return {
430
+ ...node,
431
+ data: {
432
+ ...node.data,
433
+ element
434
+ }
435
+ }
315
436
  })
316
437
  )
317
438
  get().onChange?.({ changes })
@@ -348,22 +469,78 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
348
469
  )
349
470
  },
350
471
 
351
- fitDiagram: (centerNode) => {
352
- const { fitViewPadding, xyflow, focusedNodeId } = get()
472
+ fitDiagram: (xyStore) => {
473
+ const { width, height, panZoom } = xyStore.getState()
474
+ const { fitViewPadding, view, focusedNodeId } = get()
475
+ const bounds = {
476
+ x: 0,
477
+ y: 0,
478
+ width: view.width,
479
+ height: view.height
480
+ }
481
+ const viewport = getViewportForBounds(bounds, width, height, MinZoom, 1, fitViewPadding)
482
+ panZoom?.setViewport(viewport, { duration: 400 })
483
+ if (!!focusedNodeId) {
484
+ set(
485
+ {
486
+ focusedNodeId: null,
487
+ dimmed: EmptyStringSet
488
+ },
489
+ noReplace,
490
+ 'unfocus'
491
+ )
492
+ }
493
+ },
494
+
495
+ nextDynamicStep: (increment = 1) => {
496
+ const { view, activeDynamicViewStep, xyflow, fitViewPadding } = get()
497
+ invariant(view.__ === 'dynamic', 'view is not dynamic')
498
+ const nextStep = (activeDynamicViewStep ?? 0) + increment
499
+ const edgeId = StepEdgeId(nextStep)
500
+ const dimmed = new StringSet()
501
+ let edge: XYFlowEdge | null = null
502
+ for (const e of xyflow.getEdges()) {
503
+ if (e.id === edgeId) {
504
+ edge = e
505
+ continue
506
+ }
507
+ dimmed.add(e.id)
508
+ }
509
+ invariant(!!edge, `edge not found: ${edgeId}`)
510
+ const selected = [] as XYFlowNode[]
511
+ for (const n of xyflow.getNodes()) {
512
+ if (n.id === edge.source || n.id === edge.target) {
513
+ selected.push(n)
514
+ continue
515
+ }
516
+ dimmed.add(n.id)
517
+ }
353
518
  xyflow.fitView({
354
- includeHiddenNodes: true,
355
- duration: (centerNode && focusedNodeId) ? 300 : 400,
356
- padding: centerNode ? 0 : fitViewPadding,
519
+ duration: 400,
520
+ maxZoom: 1,
357
521
  minZoom: MinZoom,
522
+ padding: fitViewPadding,
523
+ nodes: selected
524
+ })
525
+ set({
526
+ focusedNodeId: null,
527
+ activeDynamicViewStep: nextStep,
528
+ dimmed
529
+ })
530
+ },
531
+
532
+ stopDynamicView: () => {
533
+ const { xyflow, fitViewPadding } = get()
534
+ set({
535
+ activeDynamicViewStep: null,
536
+ dimmed: EmptyStringSet
537
+ })
538
+ xyflow.fitView({
539
+ duration: 400,
358
540
  maxZoom: 1,
359
- ...(centerNode && { nodes: [centerNode] })
541
+ minZoom: MinZoom,
542
+ padding: fitViewPadding
360
543
  })
361
- if (centerNode && focusedNodeId !== centerNode.id) {
362
- set({ focusedNodeId: centerNode.id }, noReplace, 'focus node')
363
- }
364
- if (!centerNode && !!focusedNodeId) {
365
- set({ focusedNodeId: null }, noReplace, 'unfocus')
366
- }
367
544
  }
368
545
  }),
369
546
  {
package/src/theme.css.ts CHANGED
@@ -4,6 +4,7 @@ export const vars = createGlobalThemeContract({
4
4
  default: {
5
5
  font: 'default-font-family'
6
6
  },
7
+ filterDimmed: 'filter-dimmed',
7
8
  likec4: {
8
9
  font: 'font-family',
9
10
  background: {
@@ -0,0 +1,33 @@
1
+ import { globalStyle, style } from '@vanilla-extract/css'
2
+ import { mantine } from '../mantine.css'
3
+
4
+ export const container = style({
5
+ bottom: 0,
6
+ left: 0,
7
+ padding: 8,
8
+ margin: 0
9
+ })
10
+
11
+ export const paper = style({
12
+ cursor: 'default',
13
+ minWidth: 100,
14
+ maxWidth: 'calc(100vw - 16px)',
15
+ backgroundColor: `color-mix(in srgb, ${mantine.colors.body}, transparent 20%)`,
16
+ WebkitBackdropFilter: 'blur(8px)',
17
+ backdropFilter: 'blur(8px)',
18
+ '@media': {
19
+ [mantine.largerThan('md')]: {
20
+ maxWidth: '45vw'
21
+ }
22
+ }
23
+ })
24
+
25
+ export const title = style({})
26
+
27
+ export const description = style({
28
+ whiteSpaceCollapse: 'preserve-breaks',
29
+ color: mantine.colors.gray[7]
30
+ })
31
+ globalStyle(`:where([data-mantine-color-scheme="dark"]) ${description}`, {
32
+ color: mantine.colors.gray[5]
33
+ })