@likec4/diagram 1.2.2 → 1.4.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/dist/LikeC4Diagram.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +27 -4
- package/dist/index.mjs +1543 -1346
- package/dist/state/DiagramContext.d.ts +2353 -264
- package/dist/state/diagramStore.d.ts +2360 -268
- package/dist/state/index.d.ts +0 -1
- package/dist/state/useDiagramStore.d.ts +3 -2534
- package/dist/state/useNavigationHistory.d.ts +5 -0
- package/dist/style.css +194 -131
- package/dist/theme.css.d.ts +5 -1
- package/dist/ui/BackwardForwardPanel.css.d.ts +1 -0
- package/dist/ui/BackwardForwardPanel.d.ts +1 -0
- package/dist/ui/DiagramTitlePanel.css.d.ts +1 -1
- package/dist/xyflow/XYFlow.d.ts +2 -1
- package/dist/xyflow/XYFlowEvents.d.ts +4 -4
- package/dist/xyflow/XYFlowInner.d.ts +2 -1
- package/dist/xyflow/const.d.ts +1 -1
- package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
- package/dist/xyflow/edges/RelationshipEdge.d.ts +4 -5
- package/dist/xyflow/edges/utils.d.ts +4 -2
- package/dist/xyflow/hooks/useXYFlow.d.ts +9 -9
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +3 -1
- package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -1
- package/dist/xyflow/nodes/element/element.css.d.ts +1 -0
- package/dist/xyflow/nodes/element/index.d.ts +1 -1
- package/dist/xyflow/types.d.ts +3 -3
- package/dist/xyflow/utils.d.ts +3 -0
- package/package.json +18 -17
- package/src/LikeC4Diagram.css.ts +15 -5
- package/src/LikeC4Diagram.props.ts +33 -5
- package/src/LikeC4Diagram.tsx +103 -32
- package/src/globals.css.ts +1 -2
- package/src/mantine/theme.ts +1 -0
- package/src/state/DiagramContext.tsx +3 -4
- package/src/state/diagramStore.ts +177 -103
- package/src/state/index.ts +0 -1
- package/src/state/useDiagramStore.ts +6 -13
- package/src/state/useNavigationHistory.ts +39 -0
- package/src/theme.css.ts +5 -1
- package/src/ui/BackwardForwardPanel.css.ts +22 -0
- package/src/ui/BackwardForwardPanel.tsx +59 -0
- package/src/ui/DiagramTitlePanel.css.ts +11 -5
- package/src/ui/DiagramTitlePanel.tsx +108 -96
- package/src/ui/DynamicViewWalkthrough.css.ts +3 -2
- package/src/ui/DynamicViewWalkthrough.tsx +36 -17
- package/src/ui/OptionsPanel.tsx +28 -19
- package/src/ui/options/NodeOptions.tsx +16 -19
- package/src/xyflow/FitviewOnDiagramChange.tsx +15 -23
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +3 -6
- package/src/xyflow/SyncWithDiagram.tsx +21 -2
- package/src/xyflow/XYFlow.tsx +20 -23
- package/src/xyflow/XYFlowEvents.tsx +26 -8
- package/src/xyflow/XYFlowInner.tsx +10 -6
- package/src/xyflow/const.ts +1 -1
- package/src/xyflow/diagram-to-xyflow.ts +4 -41
- package/src/xyflow/edges/RelationshipEdge.tsx +160 -111
- package/src/xyflow/edges/edges.css.ts +41 -26
- package/src/xyflow/edges/utils.ts +5 -2
- package/src/xyflow/hooks/useLayoutConstraints.ts +19 -4
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +45 -37
- package/src/xyflow/nodes/compound/CompoundNode.tsx +36 -26
- package/src/xyflow/nodes/element/ElementNode.tsx +27 -14
- package/src/xyflow/nodes/element/element.css.ts +31 -18
- package/src/xyflow/nodes/element/index.ts +1 -1
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +7 -3
- package/src/xyflow/types.ts +5 -2
- package/src/xyflow/utils.ts +48 -0
- package/dist/ui/index.d.ts +0 -2
- package/src/ui/index.ts +0 -3
package/src/mantine/theme.ts
CHANGED
|
@@ -4,12 +4,12 @@ import { useUpdateEffect } from '../hooks'
|
|
|
4
4
|
import { useXYFlow } from '../xyflow/hooks'
|
|
5
5
|
import { createDiagramStore, type DiagramInitialState } from './diagramStore'
|
|
6
6
|
|
|
7
|
-
type
|
|
8
|
-
export const DiagramContext = createContext<
|
|
7
|
+
export type DiagramZustandStore = ReturnType<typeof createDiagramStore>
|
|
8
|
+
export const DiagramContext = createContext<DiagramZustandStore | null>(null)
|
|
9
9
|
|
|
10
10
|
export function DiagramContextProvider({ children, view, ...props }: PropsWithChildren<DiagramInitialState>) {
|
|
11
11
|
const xyflow = useXYFlow()
|
|
12
|
-
const store = useRef<
|
|
12
|
+
const store = useRef<DiagramZustandStore>()
|
|
13
13
|
|
|
14
14
|
if (!store.current) {
|
|
15
15
|
store.current = createDiagramStore({
|
|
@@ -38,7 +38,6 @@ export function DiagramContextProvider({ children, view, ...props }: PropsWithCh
|
|
|
38
38
|
if (!hasSubObject(state, props)) {
|
|
39
39
|
store.current.setState(props, false, 'update incoming props')
|
|
40
40
|
}
|
|
41
|
-
isIncludedIn
|
|
42
41
|
},
|
|
43
42
|
[view, props]
|
|
44
43
|
)
|
|
@@ -1,29 +1,20 @@
|
|
|
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'
|
|
1
|
+
import type { DiagramNode, DiagramView, Fqn, ViewID } from '@likec4/core'
|
|
12
2
|
import { invariant, nonexhaustive, StepEdgeId } from '@likec4/core'
|
|
13
3
|
import { getViewportForBounds, type XYPosition } from '@xyflow/react'
|
|
14
4
|
import { getNodeDimensions } from '@xyflow/system'
|
|
15
5
|
import { DEV } from 'esm-env'
|
|
16
6
|
import { deepEqual, shallowEqual } from 'fast-equals'
|
|
17
7
|
import type { MouseEvent as ReactMouseEvent } from 'react'
|
|
18
|
-
import { entries,
|
|
8
|
+
import { entries, isEmpty, reduce } from 'remeda'
|
|
19
9
|
import type { Exact, Except, RequiredKeysOf, Simplify } from 'type-fest'
|
|
20
10
|
import { devtools, subscribeWithSelector } from 'zustand/middleware'
|
|
21
11
|
import { shallow } from 'zustand/shallow'
|
|
22
12
|
import { createWithEqualityFn } from 'zustand/traditional'
|
|
23
|
-
import type {
|
|
13
|
+
import type { Changes, DiagramNodeWithNavigate, LikeC4DiagramEventHandlers } from '../LikeC4Diagram.props'
|
|
24
14
|
import { MinZoom } from '../xyflow/const'
|
|
25
15
|
import type { XYStoreApi } from '../xyflow/hooks'
|
|
26
16
|
import type { XYFlowEdge, XYFlowInstance, XYFlowNode } from '../xyflow/types'
|
|
17
|
+
import { bezierControlPoints, isSamePoint } from '../xyflow/utils'
|
|
27
18
|
|
|
28
19
|
export type DiagramStore = {
|
|
29
20
|
// Incoming props
|
|
@@ -36,8 +27,11 @@ export type DiagramStore = {
|
|
|
36
27
|
pannable: boolean
|
|
37
28
|
nodesDraggable: boolean
|
|
38
29
|
nodesSelectable: boolean
|
|
30
|
+
experimentalEdgeEditing: boolean
|
|
39
31
|
|
|
40
32
|
// Internal state
|
|
33
|
+
viewSyncDebounceTimeout: number | null
|
|
34
|
+
|
|
41
35
|
xyflow: XYFlowInstance
|
|
42
36
|
initialized: boolean
|
|
43
37
|
xyflowSynced: boolean
|
|
@@ -62,7 +56,8 @@ export type DiagramStore = {
|
|
|
62
56
|
fromView: ViewID
|
|
63
57
|
toView: ViewID
|
|
64
58
|
element: DiagramNode
|
|
65
|
-
|
|
59
|
+
// Position of the element in the source view
|
|
60
|
+
elementScreenPosition: XYPosition
|
|
66
61
|
// If node from the target view is placed on the same position as the node from the source view
|
|
67
62
|
positionCorrected: boolean
|
|
68
63
|
}
|
|
@@ -85,6 +80,7 @@ export type DiagramInitialState = // Required properties
|
|
|
85
80
|
| 'pannable'
|
|
86
81
|
| 'nodesDraggable'
|
|
87
82
|
| 'nodesSelectable'
|
|
83
|
+
| 'experimentalEdgeEditing'
|
|
88
84
|
>
|
|
89
85
|
// Optional properties
|
|
90
86
|
// & Partial<
|
|
@@ -111,7 +107,10 @@ interface DiagramStoreActions {
|
|
|
111
107
|
resetLastClicked: () => void
|
|
112
108
|
|
|
113
109
|
getElement(id: Fqn): DiagramNode | null
|
|
114
|
-
|
|
110
|
+
triggerChangeElementStyle: (change: Changes.ChangeElementStyle) => void
|
|
111
|
+
|
|
112
|
+
cancelSaveManualLayout: () => void
|
|
113
|
+
triggerSaveManualLayout: (xystore: XYStoreApi) => void
|
|
115
114
|
triggerOnNavigateTo: (xynodeId: string, event: ReactMouseEvent) => void
|
|
116
115
|
fitDiagram: (xyStore: XYStoreApi) => void
|
|
117
116
|
|
|
@@ -122,6 +121,7 @@ interface DiagramStoreActions {
|
|
|
122
121
|
export type DiagramState = Simplify<DiagramStore & DiagramStoreActions>
|
|
123
122
|
|
|
124
123
|
const DEFAULT_PROPS: Except<DiagramStore, RequiredKeysOf<DiagramInitialState> | 'xyflow'> = {
|
|
124
|
+
viewSyncDebounceTimeout: null,
|
|
125
125
|
initialized: false,
|
|
126
126
|
xyflowSynced: false,
|
|
127
127
|
previousViews: [],
|
|
@@ -173,6 +173,8 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
173
173
|
|
|
174
174
|
updateView: (nextView) => {
|
|
175
175
|
let {
|
|
176
|
+
viewSyncDebounceTimeout,
|
|
177
|
+
xyflow,
|
|
176
178
|
dimmed,
|
|
177
179
|
xyflowSynced,
|
|
178
180
|
view: current,
|
|
@@ -186,10 +188,12 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
186
188
|
hoveredNodeId
|
|
187
189
|
} = get()
|
|
188
190
|
|
|
191
|
+
if (viewSyncDebounceTimeout !== null) {
|
|
192
|
+
clearTimeout(viewSyncDebounceTimeout)
|
|
193
|
+
viewSyncDebounceTimeout = null
|
|
194
|
+
}
|
|
195
|
+
|
|
189
196
|
if (shallowEqual(current, nextView)) {
|
|
190
|
-
if (!xyflowSynced) {
|
|
191
|
-
set({ xyflowSynced: true }, noReplace, 'updateView: xyflow synced')
|
|
192
|
-
}
|
|
193
197
|
DEV && console.debug('store: skip updateView')
|
|
194
198
|
return
|
|
195
199
|
}
|
|
@@ -218,8 +222,8 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
218
222
|
if (activeDynamicViewStep && !edgeIds.has(StepEdgeId(activeDynamicViewStep))) {
|
|
219
223
|
activeDynamicViewStep = null
|
|
220
224
|
}
|
|
221
|
-
xyflowSynced = deepEqual(current.nodes,
|
|
222
|
-
if (
|
|
225
|
+
xyflowSynced = deepEqual([current.nodes, current.edges], [nextView.nodes, nextView.edges])
|
|
226
|
+
if (dimmed.size > 0) {
|
|
223
227
|
let nextDimmed = new StringSet([...dimmed].filter(id => nodeIds.has(id) || edgeIds.has(id)))
|
|
224
228
|
if (nextDimmed.size !== dimmed.size) {
|
|
225
229
|
dimmed = nextDimmed
|
|
@@ -233,6 +237,11 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
233
237
|
}
|
|
234
238
|
}
|
|
235
239
|
|
|
240
|
+
if (!lastOnNavigate) {
|
|
241
|
+
const zoom = xyflow.getZoom()
|
|
242
|
+
xyflow.setCenter(nextView.width / 2, nextView.height / 2, { zoom })
|
|
243
|
+
}
|
|
244
|
+
|
|
236
245
|
// Reset hovered / clicked node/edge if the view is different
|
|
237
246
|
lastClickedEdgeId = null
|
|
238
247
|
lastClickedNodeId = null
|
|
@@ -252,6 +261,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
252
261
|
|
|
253
262
|
set(
|
|
254
263
|
{
|
|
264
|
+
viewSyncDebounceTimeout,
|
|
255
265
|
view: nextView,
|
|
256
266
|
activeDynamicViewStep,
|
|
257
267
|
xyflowSynced,
|
|
@@ -302,37 +312,39 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
302
312
|
|
|
303
313
|
setHoveredNode: (nodeId) => {
|
|
304
314
|
if (nodeId !== get().hoveredNodeId) {
|
|
305
|
-
set({ hoveredNodeId: nodeId }
|
|
315
|
+
set({ hoveredNodeId: nodeId })
|
|
306
316
|
}
|
|
307
317
|
},
|
|
308
318
|
|
|
309
319
|
setHoveredEdge: (edgeId) => {
|
|
310
320
|
if (edgeId !== get().hoveredEdgeId) {
|
|
311
|
-
set({ hoveredEdgeId: edgeId }
|
|
321
|
+
set({ hoveredEdgeId: edgeId })
|
|
312
322
|
}
|
|
313
323
|
},
|
|
314
324
|
|
|
315
325
|
setLastClickedNode: (nodeId) => {
|
|
316
326
|
if (nodeId !== get().lastClickedNodeId) {
|
|
317
|
-
set({ lastClickedNodeId: nodeId }
|
|
327
|
+
set({ lastClickedNodeId: nodeId })
|
|
318
328
|
}
|
|
319
329
|
},
|
|
320
330
|
|
|
321
331
|
setLastClickedEdge: (edgeId) => {
|
|
322
332
|
if (edgeId !== get().lastClickedEdgeId) {
|
|
323
|
-
set({ lastClickedEdgeId: edgeId }
|
|
333
|
+
set({ lastClickedEdgeId: edgeId })
|
|
324
334
|
}
|
|
325
335
|
},
|
|
326
336
|
|
|
327
337
|
resetLastClicked: () => {
|
|
328
338
|
let {
|
|
339
|
+
activeDynamicViewStep,
|
|
329
340
|
focusedNodeId,
|
|
330
341
|
lastClickedNodeId,
|
|
331
342
|
lastClickedEdgeId
|
|
332
343
|
} = get()
|
|
333
|
-
if (focusedNodeId || lastClickedNodeId || lastClickedEdgeId) {
|
|
344
|
+
if (activeDynamicViewStep || focusedNodeId || lastClickedNodeId || lastClickedEdgeId) {
|
|
334
345
|
set(
|
|
335
346
|
{
|
|
347
|
+
activeDynamicViewStep: null,
|
|
336
348
|
focusedNodeId: null,
|
|
337
349
|
lastClickedNodeId: null,
|
|
338
350
|
lastClickedEdgeId: null,
|
|
@@ -349,93 +361,153 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
349
361
|
return view.nodes.find(({ id }) => id === fqn) ?? null
|
|
350
362
|
},
|
|
351
363
|
|
|
352
|
-
|
|
364
|
+
triggerChangeElementStyle: (change) => {
|
|
353
365
|
if (DEV) {
|
|
354
|
-
console.debug('
|
|
366
|
+
console.debug('triggerChangeElementStyle', change)
|
|
355
367
|
}
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (change.
|
|
362
|
-
|
|
368
|
+
const { view, updateView, onChange } = get()
|
|
369
|
+
|
|
370
|
+
// Style changes we already apply to the view
|
|
371
|
+
let hasChanges = false
|
|
372
|
+
const nodes = view.nodes.map(origin => {
|
|
373
|
+
if (!change.targets.includes(origin.id)) {
|
|
374
|
+
return origin
|
|
363
375
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
376
|
+
let element = origin
|
|
377
|
+
for (const [key, value] of entries.strict(change.style)) {
|
|
378
|
+
switch (key) {
|
|
379
|
+
case 'shape':
|
|
380
|
+
if (value !== element.shape) {
|
|
381
|
+
element = {
|
|
382
|
+
...element,
|
|
383
|
+
shape: value
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
break
|
|
387
|
+
case 'color':
|
|
388
|
+
if (value !== element.color) {
|
|
389
|
+
element = {
|
|
390
|
+
...element,
|
|
391
|
+
color: value
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
break
|
|
395
|
+
case 'opacity':
|
|
396
|
+
if (value !== element.style?.opacity) {
|
|
397
|
+
element = {
|
|
398
|
+
...element,
|
|
399
|
+
style: {
|
|
400
|
+
...element.style,
|
|
401
|
+
opacity: value
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
break
|
|
406
|
+
case 'border':
|
|
407
|
+
if (value !== element.style?.border) {
|
|
408
|
+
element = {
|
|
409
|
+
...element,
|
|
410
|
+
style: {
|
|
411
|
+
...element.style,
|
|
412
|
+
border: value
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
break
|
|
417
|
+
default:
|
|
418
|
+
nonexhaustive(key)
|
|
382
419
|
}
|
|
383
420
|
}
|
|
421
|
+
if (element !== origin) {
|
|
422
|
+
hasChanges = true
|
|
423
|
+
return element
|
|
424
|
+
}
|
|
425
|
+
return origin
|
|
426
|
+
})
|
|
427
|
+
if (hasChanges) {
|
|
428
|
+
updateView({
|
|
429
|
+
...view,
|
|
430
|
+
nodes
|
|
431
|
+
})
|
|
384
432
|
}
|
|
433
|
+
// Trigger change event, even if there are no changes with local state
|
|
434
|
+
// but we maybe out of sync with the server
|
|
435
|
+
onChange?.({ change })
|
|
436
|
+
},
|
|
385
437
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
}
|
|
438
|
+
cancelSaveManualLayout: () => {
|
|
439
|
+
let { viewSyncDebounceTimeout } = get()
|
|
440
|
+
if (viewSyncDebounceTimeout !== null) {
|
|
441
|
+
clearTimeout(viewSyncDebounceTimeout)
|
|
442
|
+
set({ viewSyncDebounceTimeout: null })
|
|
443
|
+
}
|
|
444
|
+
},
|
|
403
445
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
446
|
+
triggerSaveManualLayout: (xystore) => {
|
|
447
|
+
let { viewSyncDebounceTimeout: debounced, onChange } = get()
|
|
448
|
+
if (!onChange) {
|
|
449
|
+
DEV && console.debug('ignore triggerSaveManualLayout, as no onChange handler is set')
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
if (debounced) {
|
|
453
|
+
clearTimeout(debounced)
|
|
454
|
+
}
|
|
455
|
+
debounced = setTimeout(() => {
|
|
456
|
+
const { nodeLookup } = xystore.getState()
|
|
457
|
+
const { xyflow, onChange } = get()
|
|
458
|
+
set({ viewSyncDebounceTimeout: null })
|
|
459
|
+
|
|
460
|
+
const movedNodes = new StringSet()
|
|
461
|
+
const nodes = reduce([...nodeLookup.values()], (acc, node) => {
|
|
462
|
+
const dimensions = getNodeDimensions(node)
|
|
463
|
+
if (!isSamePoint(node.internals.positionAbsolute, node.data.element.position)) {
|
|
464
|
+
movedNodes.add(node.id)
|
|
413
465
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
style: {
|
|
420
|
-
...element.style,
|
|
421
|
-
border
|
|
422
|
-
}
|
|
423
|
-
}
|
|
466
|
+
acc[node.data.fqn] = {
|
|
467
|
+
x: node.internals.positionAbsolute.x,
|
|
468
|
+
y: node.internals.positionAbsolute.y,
|
|
469
|
+
width: dimensions.width,
|
|
470
|
+
height: dimensions.height
|
|
424
471
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
472
|
+
return acc
|
|
473
|
+
}, {} as Changes.SaveManualLayout['nodes'])
|
|
474
|
+
const edges = reduce(xyflow.getEdges(), (acc, { source, target, data }) => {
|
|
475
|
+
let controlPoints = data.controlPoints
|
|
476
|
+
// If edge control points are not set, but the source or target node was moved
|
|
477
|
+
if (!controlPoints && (movedNodes.has(source) || movedNodes.has(target))) {
|
|
478
|
+
controlPoints = bezierControlPoints(data.edge)
|
|
428
479
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
...node.data,
|
|
433
|
-
element
|
|
480
|
+
if (controlPoints) {
|
|
481
|
+
acc[data.edge.id] = {
|
|
482
|
+
controlPoints
|
|
434
483
|
}
|
|
435
484
|
}
|
|
436
|
-
|
|
485
|
+
return acc
|
|
486
|
+
}, {} as Changes.SaveManualLayout['edges'])
|
|
487
|
+
if (movedNodes.size === 0 && isEmpty(edges)) {
|
|
488
|
+
DEV && console.debug('ignore triggerSaveManualLayout, as no changes detected')
|
|
489
|
+
return
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const change: Changes.SaveManualLayout = {
|
|
493
|
+
op: 'save-manual-layout',
|
|
494
|
+
nodes,
|
|
495
|
+
edges
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (DEV) {
|
|
499
|
+
console.debug('triggerSaveManualLayout', change)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
onChange?.({ change })
|
|
503
|
+
}, 2000)
|
|
504
|
+
set(
|
|
505
|
+
{
|
|
506
|
+
viewSyncDebounceTimeout: debounced
|
|
507
|
+
},
|
|
508
|
+
noReplace,
|
|
509
|
+
'debounce sync state'
|
|
437
510
|
)
|
|
438
|
-
get().onChange?.({ changes })
|
|
439
511
|
},
|
|
440
512
|
|
|
441
513
|
triggerOnNavigateTo: (xynodeId, event) => {
|
|
@@ -443,7 +515,6 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
443
515
|
const xynode = xyflow.getInternalNode(xynodeId)
|
|
444
516
|
const element = xynode?.data.element as DiagramNodeWithNavigate
|
|
445
517
|
invariant(!!xynode && element?.navigateTo, `node is not navigable: ${xynodeId}`)
|
|
446
|
-
const dimensions = getNodeDimensions(xynode)
|
|
447
518
|
set(
|
|
448
519
|
{
|
|
449
520
|
lastClickedNodeId: xynodeId,
|
|
@@ -451,9 +522,9 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
451
522
|
fromView: view.id,
|
|
452
523
|
toView: element.navigateTo,
|
|
453
524
|
element: element,
|
|
454
|
-
|
|
455
|
-
x: xynode.internals.positionAbsolute.x + dimensions.width / 2,
|
|
456
|
-
y: xynode.internals.positionAbsolute.y + dimensions.height / 2
|
|
525
|
+
elementScreenPosition: xyflow.flowToScreenPosition({
|
|
526
|
+
x: xynode.internals.positionAbsolute.x, // + dimensions.width / 2,
|
|
527
|
+
y: xynode.internals.positionAbsolute.y // + dimensions.height / 2
|
|
457
528
|
}),
|
|
458
529
|
positionCorrected: false
|
|
459
530
|
}
|
|
@@ -479,7 +550,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
479
550
|
height: view.height
|
|
480
551
|
}
|
|
481
552
|
const viewport = getViewportForBounds(bounds, width, height, MinZoom, 1, fitViewPadding)
|
|
482
|
-
panZoom?.setViewport(viewport, { duration:
|
|
553
|
+
panZoom?.setViewport(viewport, { duration: 500 })
|
|
483
554
|
if (!!focusedNodeId) {
|
|
484
555
|
set(
|
|
485
556
|
{
|
|
@@ -496,6 +567,9 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
496
567
|
const { view, activeDynamicViewStep, xyflow, fitViewPadding } = get()
|
|
497
568
|
invariant(view.__ === 'dynamic', 'view is not dynamic')
|
|
498
569
|
const nextStep = (activeDynamicViewStep ?? 0) + increment
|
|
570
|
+
if (nextStep <= 0 || nextStep > view.edges.length) {
|
|
571
|
+
return
|
|
572
|
+
}
|
|
499
573
|
const edgeId = StepEdgeId(nextStep)
|
|
500
574
|
const dimmed = new StringSet()
|
|
501
575
|
let edge: XYFlowEdge | null = null
|
package/src/state/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { shallowEqual } from 'fast-equals'
|
|
2
|
-
import { useContext
|
|
2
|
+
import { useContext } from 'react'
|
|
3
3
|
import { useStoreWithEqualityFn as useZustandStore } from 'zustand/traditional'
|
|
4
4
|
|
|
5
|
-
import { DiagramContext } from './DiagramContext'
|
|
5
|
+
import { DiagramContext, type DiagramZustandStore } from './DiagramContext'
|
|
6
6
|
import type { DiagramState } from './diagramStore'
|
|
7
7
|
|
|
8
8
|
function useDiagramState<StateSlice = unknown>(
|
|
9
9
|
selector: (state: DiagramState) => StateSlice,
|
|
10
10
|
equalityFn?: (a: StateSlice, b: StateSlice) => boolean
|
|
11
|
-
) {
|
|
11
|
+
): StateSlice {
|
|
12
12
|
const store = useContext(DiagramContext)
|
|
13
13
|
|
|
14
14
|
if (store === null) {
|
|
@@ -18,22 +18,15 @@ function useDiagramState<StateSlice = unknown>(
|
|
|
18
18
|
return useZustandStore(store, selector, equalityFn ?? shallowEqual)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
export type DiagramStoreApi = Readonly<Pick<DiagramZustandStore, 'getState' | 'setState' | 'subscribe'>>
|
|
22
|
+
function useDiagramStoreApi(): DiagramStoreApi {
|
|
22
23
|
const store = useContext(DiagramContext)
|
|
23
24
|
|
|
24
25
|
if (store === null) {
|
|
25
26
|
throw new Error('useDiagramStoreApi could be used only inside DiagramContext')
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
return
|
|
29
|
-
() => ({
|
|
30
|
-
getState: store.getState,
|
|
31
|
-
setState: store.setState,
|
|
32
|
-
subscribe: store.subscribe
|
|
33
|
-
}),
|
|
34
|
-
[store]
|
|
35
|
-
)
|
|
29
|
+
return store
|
|
36
30
|
}
|
|
37
|
-
export type DiagramStoreApi = ReturnType<typeof useDiagramStoreApi>
|
|
38
31
|
|
|
39
32
|
export { useDiagramState, useDiagramStoreApi }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useStateHistory } from '@mantine/hooks'
|
|
2
|
+
import { useMemo } from 'react'
|
|
3
|
+
import { useUpdateEffect } from '../hooks/useUpdateEffect'
|
|
4
|
+
import { useDiagramState, useDiagramStoreApi } from './useDiagramStore'
|
|
5
|
+
|
|
6
|
+
export function useNavigationHistory() {
|
|
7
|
+
const store = useDiagramStoreApi()
|
|
8
|
+
const viewId = useDiagramState(s => s.view.id)
|
|
9
|
+
|
|
10
|
+
const [historyViewId, historyOps, {
|
|
11
|
+
history,
|
|
12
|
+
current: historyIndex
|
|
13
|
+
}] = useStateHistory(viewId)
|
|
14
|
+
|
|
15
|
+
const hasBack = historyIndex > 0
|
|
16
|
+
const hasForward = historyIndex < history.length - 1
|
|
17
|
+
|
|
18
|
+
useUpdateEffect(() => {
|
|
19
|
+
if (viewId !== historyViewId) {
|
|
20
|
+
historyOps.set(viewId)
|
|
21
|
+
}
|
|
22
|
+
}, [viewId])
|
|
23
|
+
|
|
24
|
+
useUpdateEffect(() => {
|
|
25
|
+
if (viewId !== historyViewId) {
|
|
26
|
+
store.getState().onNavigateTo?.(historyViewId)
|
|
27
|
+
}
|
|
28
|
+
}, [historyViewId])
|
|
29
|
+
|
|
30
|
+
return useMemo(() => ({
|
|
31
|
+
hasBack,
|
|
32
|
+
hasForward,
|
|
33
|
+
ops: historyOps
|
|
34
|
+
}), [
|
|
35
|
+
hasBack,
|
|
36
|
+
hasForward,
|
|
37
|
+
historyOps
|
|
38
|
+
])
|
|
39
|
+
}
|
package/src/theme.css.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createGlobalThemeContract } from '@vanilla-extract/css'
|
|
2
2
|
|
|
3
3
|
export const vars = createGlobalThemeContract({
|
|
4
|
+
safariAnimationHook: 'safari-animation-hook',
|
|
4
5
|
default: {
|
|
5
6
|
font: 'default-font-family'
|
|
6
7
|
},
|
|
7
|
-
filterDimmed: 'filter-dimmed',
|
|
8
8
|
likec4: {
|
|
9
9
|
font: 'font-family',
|
|
10
10
|
background: {
|
|
@@ -33,6 +33,10 @@ export const vars = createGlobalThemeContract({
|
|
|
33
33
|
optionsPanel: {
|
|
34
34
|
top: 'options-panel-top',
|
|
35
35
|
right: 'options-panel-right'
|
|
36
|
+
},
|
|
37
|
+
navigationPanel: {
|
|
38
|
+
top: 'navigation-panel-top',
|
|
39
|
+
left: 'navigation-panel-left'
|
|
36
40
|
}
|
|
37
41
|
}, (value) => `likec4-${value}`)
|
|
38
42
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { fallbackVar, globalStyle, style } from '@vanilla-extract/css'
|
|
2
|
+
import { mantine } from '../mantine.css'
|
|
3
|
+
import { vars } from '../theme.css'
|
|
4
|
+
|
|
5
|
+
export const panel = style({
|
|
6
|
+
top: fallbackVar(vars.navigationPanel.top, '1rem'),
|
|
7
|
+
left: fallbackVar(vars.navigationPanel.left, '1rem'),
|
|
8
|
+
margin: 0
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
globalStyle(`${panel} .mantine-ActionIcon-root`, {
|
|
12
|
+
vars: {
|
|
13
|
+
['--ai-size']: 'var(--ai-size-md)'
|
|
14
|
+
},
|
|
15
|
+
'@media': {
|
|
16
|
+
[mantine.largerThan('md')]: {
|
|
17
|
+
vars: {
|
|
18
|
+
['--ai-size']: 'var(--ai-size-lg)'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ActionIcon, Group } from '@mantine/core'
|
|
2
|
+
import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react'
|
|
3
|
+
import clsx from 'clsx'
|
|
4
|
+
import { AnimatePresence, m } from 'framer-motion'
|
|
5
|
+
import { useNavigationHistory } from '../state/useNavigationHistory'
|
|
6
|
+
import * as css from './BackwardForwardPanel.css'
|
|
7
|
+
|
|
8
|
+
export function BackwardForwardPanel() {
|
|
9
|
+
const { hasBack, hasForward, ops } = useNavigationHistory()
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Group
|
|
13
|
+
className={clsx(
|
|
14
|
+
'react-flow__panel',
|
|
15
|
+
css.panel,
|
|
16
|
+
'likec4-navigation-panel'
|
|
17
|
+
)}
|
|
18
|
+
gap={'xs'}>
|
|
19
|
+
<AnimatePresence>
|
|
20
|
+
{hasBack && (
|
|
21
|
+
<m.div
|
|
22
|
+
layout
|
|
23
|
+
initial={{ opacity: 0.05, transform: 'translateX(-10px)' }}
|
|
24
|
+
animate={{ opacity: 1, transform: 'translateX(0)' }}
|
|
25
|
+
exit={{
|
|
26
|
+
opacity: 0.05,
|
|
27
|
+
transform: 'translateX(-10px)'
|
|
28
|
+
}}
|
|
29
|
+
key={'back'}>
|
|
30
|
+
<ActionIcon
|
|
31
|
+
variant="light"
|
|
32
|
+
color="gray"
|
|
33
|
+
onClick={() => ops.back()}>
|
|
34
|
+
<IconChevronLeft />
|
|
35
|
+
</ActionIcon>
|
|
36
|
+
</m.div>
|
|
37
|
+
)}
|
|
38
|
+
{hasForward && (
|
|
39
|
+
<m.div
|
|
40
|
+
layout
|
|
41
|
+
initial={{ opacity: 0.05, transform: 'translateX(10px)' }}
|
|
42
|
+
animate={{ opacity: 1, transform: 'translateX(0)' }}
|
|
43
|
+
exit={{
|
|
44
|
+
opacity: 0,
|
|
45
|
+
transform: 'translateX(10px)'
|
|
46
|
+
}}
|
|
47
|
+
key={'forward'}>
|
|
48
|
+
<ActionIcon
|
|
49
|
+
variant="light"
|
|
50
|
+
color="gray"
|
|
51
|
+
onClick={() => ops.forward()}>
|
|
52
|
+
<IconChevronRight />
|
|
53
|
+
</ActionIcon>
|
|
54
|
+
</m.div>
|
|
55
|
+
)}
|
|
56
|
+
</AnimatePresence>
|
|
57
|
+
</Group>
|
|
58
|
+
)
|
|
59
|
+
}
|