@likec4/diagram 1.3.0 → 1.5.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.css.d.ts +0 -1
- package/dist/LikeC4Diagram.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +7 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +1580 -1194
- package/dist/state/DiagramContext.d.ts +97 -4169
- package/dist/state/diagramStore.d.ts +98 -4172
- package/dist/state/hooks.d.ts +4 -0
- package/dist/state/index.d.ts +1 -3
- package/dist/state/useDiagramStore.d.ts +3 -4622
- package/dist/state/useNavigationHistory.d.ts +5 -0
- package/dist/style.css +217 -159
- package/dist/theme.css.d.ts +5 -0
- 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/ui/OptionsPanel.d.ts +1 -3
- package/dist/ui/options/EdgeOptions.d.ts +19 -0
- package/dist/ui/options/NodeOptions.d.ts +2 -2
- package/dist/xyflow/EdgeMarkers.d.ts +11 -0
- package/dist/xyflow/XYFlowEvents.d.ts +12 -9
- package/dist/xyflow/XYFlowInner.d.ts +1 -0
- package/dist/xyflow/const.d.ts +3 -3
- package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
- package/dist/xyflow/edges/RelationshipEdge.d.ts +4 -3
- package/dist/xyflow/edges/edges.css.d.ts +1 -0
- package/dist/xyflow/edges/utils.d.ts +1 -0
- package/dist/xyflow/hooks/index.d.ts +3 -2
- package/dist/xyflow/hooks/useXYFlow.d.ts +22 -21
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
- package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -4
- package/dist/xyflow/nodes/element/element.css.d.ts +3 -1
- package/dist/xyflow/types.d.ts +2 -32
- package/package.json +21 -21
- package/src/LikeC4Diagram.css.ts +10 -8
- package/src/LikeC4Diagram.props.ts +12 -5
- package/src/LikeC4Diagram.tsx +43 -26
- package/src/globals.css.ts +11 -4
- package/src/index.ts +1 -1
- package/src/mantine.css.ts +2 -2
- package/src/state/DiagramContext.tsx +47 -16
- package/src/state/diagramStore.ts +89 -80
- package/src/state/hooks.ts +5 -0
- package/src/state/index.ts +1 -3
- package/src/state/useDiagramStore.ts +6 -13
- package/src/state/useNavigationHistory.ts +39 -0
- package/src/theme.css.ts +5 -0
- package/src/ui/BackwardForwardPanel.css.ts +22 -0
- package/src/ui/BackwardForwardPanel.tsx +59 -0
- package/src/ui/DiagramTitlePanel.css.ts +17 -6
- package/src/ui/DiagramTitlePanel.tsx +108 -94
- package/src/ui/DynamicViewWalkthrough.tsx +6 -4
- package/src/ui/OptionsPanel.tsx +37 -18
- package/src/ui/options/EdgeOptions.tsx +157 -0
- package/src/ui/options/NodeOptions.tsx +37 -29
- package/src/xyflow/EdgeMarkers.tsx +148 -0
- package/src/xyflow/FitviewOnDiagramChange.tsx +8 -4
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +59 -55
- package/src/xyflow/SyncWithDiagram.tsx +13 -20
- package/src/xyflow/XYFlow.tsx +2 -4
- package/src/xyflow/XYFlowEvents.tsx +48 -33
- package/src/xyflow/XYFlowInner.tsx +6 -3
- package/src/xyflow/const.ts +3 -3
- package/src/xyflow/diagram-to-xyflow.ts +18 -53
- package/src/xyflow/edges/RelationshipEdge.tsx +218 -126
- package/src/xyflow/edges/edges.css.ts +44 -26
- package/src/xyflow/edges/utils.ts +10 -0
- package/src/xyflow/hooks/index.ts +4 -2
- package/src/xyflow/hooks/useLayoutConstraints.ts +5 -5
- package/src/xyflow/hooks/useXYFlow.ts +28 -1
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +41 -31
- package/src/xyflow/nodes/compound/CompoundNode.tsx +52 -53
- package/src/xyflow/nodes/element/ElementIcon.tsx +11 -22
- package/src/xyflow/nodes/element/ElementLink.tsx +11 -16
- package/src/xyflow/nodes/element/ElementNode.tsx +69 -114
- package/src/xyflow/nodes/element/element.css.ts +48 -15
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
- package/src/xyflow/types.ts +2 -4
- package/src/xyflow/utils.ts +3 -6
- package/dist/KeepAspectRatioContainer.d.ts +0 -9
- package/dist/icons/Clipboard.d.ts +0 -3
- package/dist/icons/Link.d.ts +0 -3
- package/dist/icons/ZoomIn.d.ts +0 -3
- package/dist/icons/index.d.ts +0 -3
- package/dist/mantine/theme.d.ts +0 -3
- package/dist/state/WhenInitialized.d.ts +0 -3
- package/dist/ui/Camera.d.ts +0 -3
- package/src/KeepAspectRatioContainer.tsx +0 -36
- package/src/icons/Clipboard.tsx +0 -24
- package/src/icons/Link.tsx +0 -23
- package/src/icons/ZoomIn.tsx +0 -25
- package/src/icons/index.ts +0 -3
- package/src/mantine/theme.ts +0 -20
- package/src/state/WhenInitialized.tsx +0 -24
- package/src/ui/Camera.tsx +0 -174
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { DiagramNode, DiagramView, Fqn, ViewID } from '@likec4/core'
|
|
2
|
-
import { invariant, nonexhaustive,
|
|
2
|
+
import { invariant, nonexhaustive, StepEdgeId } from '@likec4/core'
|
|
3
3
|
import { getViewportForBounds, type XYPosition } from '@xyflow/react'
|
|
4
|
-
import {
|
|
4
|
+
import { getNodeDimensions } from '@xyflow/system'
|
|
5
5
|
import { DEV } from 'esm-env'
|
|
6
6
|
import { deepEqual, shallowEqual } from 'fast-equals'
|
|
7
7
|
import type { MouseEvent as ReactMouseEvent } from 'react'
|
|
8
|
-
import {
|
|
8
|
+
import { entries, isEmpty, reduce } from 'remeda'
|
|
9
9
|
import type { Exact, Except, RequiredKeysOf, Simplify } from 'type-fest'
|
|
10
10
|
import { devtools, subscribeWithSelector } from 'zustand/middleware'
|
|
11
11
|
import { shallow } from 'zustand/shallow'
|
|
@@ -17,6 +17,7 @@ import type { XYFlowEdge, XYFlowInstance, XYFlowNode } from '../xyflow/types'
|
|
|
17
17
|
import { bezierControlPoints, isSamePoint } from '../xyflow/utils'
|
|
18
18
|
|
|
19
19
|
export type DiagramStore = {
|
|
20
|
+
readonly storeDevId: string
|
|
20
21
|
// Incoming props
|
|
21
22
|
view: DiagramView
|
|
22
23
|
readonly: boolean
|
|
@@ -28,12 +29,14 @@ export type DiagramStore = {
|
|
|
28
29
|
nodesDraggable: boolean
|
|
29
30
|
nodesSelectable: boolean
|
|
30
31
|
experimentalEdgeEditing: boolean
|
|
32
|
+
// Diagram Container, see DiagramContainer.tsx
|
|
33
|
+
getContainer: () => HTMLDivElement | null
|
|
31
34
|
|
|
32
35
|
// Internal state
|
|
33
|
-
viewSyncState: 'synced' | 'changed'
|
|
34
36
|
viewSyncDebounceTimeout: number | null
|
|
35
37
|
|
|
36
38
|
xyflow: XYFlowInstance
|
|
39
|
+
xystore: XYStoreApi
|
|
37
40
|
initialized: boolean
|
|
38
41
|
xyflowSynced: boolean
|
|
39
42
|
|
|
@@ -83,13 +86,6 @@ export type DiagramInitialState = // Required properties
|
|
|
83
86
|
| 'nodesSelectable'
|
|
84
87
|
| 'experimentalEdgeEditing'
|
|
85
88
|
>
|
|
86
|
-
// Optional properties
|
|
87
|
-
// & Partial<
|
|
88
|
-
// Pick<
|
|
89
|
-
// DiagramStore,
|
|
90
|
-
// 'colorScheme'
|
|
91
|
-
// >
|
|
92
|
-
// >
|
|
93
89
|
& LikeC4DiagramEventHandlers
|
|
94
90
|
|
|
95
91
|
interface DiagramStoreActions {
|
|
@@ -109,9 +105,11 @@ interface DiagramStoreActions {
|
|
|
109
105
|
|
|
110
106
|
getElement(id: Fqn): DiagramNode | null
|
|
111
107
|
triggerChangeElementStyle: (change: Changes.ChangeElementStyle) => void
|
|
112
|
-
|
|
108
|
+
|
|
109
|
+
cancelSaveManualLayout: () => void
|
|
110
|
+
triggerSaveManualLayout: () => void
|
|
113
111
|
triggerOnNavigateTo: (xynodeId: string, event: ReactMouseEvent) => void
|
|
114
|
-
fitDiagram: (
|
|
112
|
+
fitDiagram: (duration?: number) => void
|
|
115
113
|
|
|
116
114
|
nextDynamicStep: (increment?: number) => void
|
|
117
115
|
stopDynamicView: () => void
|
|
@@ -119,8 +117,10 @@ interface DiagramStoreActions {
|
|
|
119
117
|
|
|
120
118
|
export type DiagramState = Simplify<DiagramStore & DiagramStoreActions>
|
|
121
119
|
|
|
122
|
-
const DEFAULT_PROPS: Except<
|
|
123
|
-
|
|
120
|
+
const DEFAULT_PROPS: Except<
|
|
121
|
+
DiagramStore,
|
|
122
|
+
RequiredKeysOf<DiagramInitialState> | 'xystore' | 'xyflow' | 'getContainer' | 'storeDevId'
|
|
123
|
+
> = {
|
|
124
124
|
viewSyncDebounceTimeout: null,
|
|
125
125
|
initialized: false,
|
|
126
126
|
xyflowSynced: false,
|
|
@@ -145,15 +145,16 @@ const DEFAULT_PROPS: Except<DiagramStore, RequiredKeysOf<DiagramInitialState> |
|
|
|
145
145
|
onCanvasDblClick: null
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
type CreateDiagramStore = DiagramInitialState & Pick<DiagramStore, 'xyflow'>
|
|
148
|
+
export type CreateDiagramStore = DiagramInitialState & Pick<DiagramStore, 'xystore' | 'xyflow' | 'getContainer'>
|
|
149
149
|
|
|
150
150
|
const noReplace = false
|
|
151
151
|
|
|
152
|
-
let
|
|
152
|
+
let StoreDevId = 1
|
|
153
153
|
|
|
154
154
|
const EmptyStringSet: ReadonlySet<string> = new StringSet()
|
|
155
155
|
|
|
156
156
|
export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props: T) {
|
|
157
|
+
const storeDevId = 'DiagramStore' + String(StoreDevId++).padStart(2, '0')
|
|
157
158
|
return createWithEqualityFn<
|
|
158
159
|
DiagramState,
|
|
159
160
|
[
|
|
@@ -164,9 +165,9 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
164
165
|
subscribeWithSelector(
|
|
165
166
|
devtools(
|
|
166
167
|
(set, get) => ({
|
|
168
|
+
storeDevId,
|
|
167
169
|
...DEFAULT_PROPS,
|
|
168
170
|
...(props as CreateDiagramStore),
|
|
169
|
-
|
|
170
171
|
isDynamicView: () => {
|
|
171
172
|
return get().view?.__ === 'dynamic'
|
|
172
173
|
},
|
|
@@ -174,7 +175,6 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
174
175
|
updateView: (nextView) => {
|
|
175
176
|
let {
|
|
176
177
|
viewSyncDebounceTimeout,
|
|
177
|
-
viewSyncState,
|
|
178
178
|
xyflow,
|
|
179
179
|
dimmed,
|
|
180
180
|
xyflowSynced,
|
|
@@ -189,26 +189,16 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
189
189
|
hoveredNodeId
|
|
190
190
|
} = get()
|
|
191
191
|
|
|
192
|
-
if (viewSyncDebounceTimeout) {
|
|
193
|
-
clearTimeout(viewSyncDebounceTimeout)
|
|
194
|
-
viewSyncDebounceTimeout = null
|
|
195
|
-
}
|
|
196
|
-
|
|
197
192
|
if (shallowEqual(current, nextView)) {
|
|
198
|
-
if (!xyflowSynced || viewSyncState === 'changed') {
|
|
199
|
-
set(
|
|
200
|
-
{
|
|
201
|
-
viewSyncState: 'synced',
|
|
202
|
-
xyflowSynced: true
|
|
203
|
-
},
|
|
204
|
-
noReplace,
|
|
205
|
-
'updateView: xyflow synced'
|
|
206
|
-
)
|
|
207
|
-
}
|
|
208
193
|
DEV && console.debug('store: skip updateView')
|
|
209
194
|
return
|
|
210
195
|
}
|
|
211
196
|
|
|
197
|
+
if (viewSyncDebounceTimeout !== null) {
|
|
198
|
+
clearTimeout(viewSyncDebounceTimeout)
|
|
199
|
+
viewSyncDebounceTimeout = null
|
|
200
|
+
}
|
|
201
|
+
|
|
212
202
|
const isSameView = current.id === nextView.id
|
|
213
203
|
|
|
214
204
|
if (isSameView) {
|
|
@@ -272,7 +262,6 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
272
262
|
|
|
273
263
|
set(
|
|
274
264
|
{
|
|
275
|
-
viewSyncState: 'synced',
|
|
276
265
|
viewSyncDebounceTimeout,
|
|
277
266
|
view: nextView,
|
|
278
267
|
activeDynamicViewStep,
|
|
@@ -287,7 +276,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
287
276
|
dimmed
|
|
288
277
|
},
|
|
289
278
|
noReplace,
|
|
290
|
-
'update-view'
|
|
279
|
+
isSameView ? 'update-view [same]' : 'update-view [another]'
|
|
291
280
|
)
|
|
292
281
|
},
|
|
293
282
|
|
|
@@ -324,25 +313,25 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
324
313
|
|
|
325
314
|
setHoveredNode: (nodeId) => {
|
|
326
315
|
if (nodeId !== get().hoveredNodeId) {
|
|
327
|
-
set({ hoveredNodeId: nodeId }
|
|
316
|
+
set({ hoveredNodeId: nodeId })
|
|
328
317
|
}
|
|
329
318
|
},
|
|
330
319
|
|
|
331
320
|
setHoveredEdge: (edgeId) => {
|
|
332
321
|
if (edgeId !== get().hoveredEdgeId) {
|
|
333
|
-
set({ hoveredEdgeId: edgeId }
|
|
322
|
+
set({ hoveredEdgeId: edgeId })
|
|
334
323
|
}
|
|
335
324
|
},
|
|
336
325
|
|
|
337
326
|
setLastClickedNode: (nodeId) => {
|
|
338
327
|
if (nodeId !== get().lastClickedNodeId) {
|
|
339
|
-
set({ lastClickedNodeId: nodeId }
|
|
328
|
+
set({ lastClickedNodeId: nodeId })
|
|
340
329
|
}
|
|
341
330
|
},
|
|
342
331
|
|
|
343
332
|
setLastClickedEdge: (edgeId) => {
|
|
344
333
|
if (edgeId !== get().lastClickedEdgeId) {
|
|
345
|
-
set({ lastClickedEdgeId: edgeId }
|
|
334
|
+
set({ lastClickedEdgeId: edgeId })
|
|
346
335
|
}
|
|
347
336
|
},
|
|
348
337
|
|
|
@@ -386,7 +375,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
386
375
|
return origin
|
|
387
376
|
}
|
|
388
377
|
let element = origin
|
|
389
|
-
for (const [key, value] of entries
|
|
378
|
+
for (const [key, value] of entries(change.style)) {
|
|
390
379
|
switch (key) {
|
|
391
380
|
case 'shape':
|
|
392
381
|
if (value !== element.shape) {
|
|
@@ -441,16 +430,21 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
441
430
|
...view,
|
|
442
431
|
nodes
|
|
443
432
|
})
|
|
444
|
-
if (onChange) {
|
|
445
|
-
set({ viewSyncState: 'changed' }, noReplace, 'change sync state')
|
|
446
|
-
}
|
|
447
433
|
}
|
|
448
434
|
// Trigger change event, even if there are no changes with local state
|
|
449
435
|
// but we maybe out of sync with the server
|
|
450
436
|
onChange?.({ change })
|
|
451
437
|
},
|
|
452
438
|
|
|
453
|
-
|
|
439
|
+
cancelSaveManualLayout: () => {
|
|
440
|
+
let { viewSyncDebounceTimeout } = get()
|
|
441
|
+
if (viewSyncDebounceTimeout !== null) {
|
|
442
|
+
clearTimeout(viewSyncDebounceTimeout)
|
|
443
|
+
set({ viewSyncDebounceTimeout: null })
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
triggerSaveManualLayout: () => {
|
|
454
448
|
let { viewSyncDebounceTimeout: debounced, onChange } = get()
|
|
455
449
|
if (!onChange) {
|
|
456
450
|
DEV && console.debug('ignore triggerSaveManualLayout, as no onChange handler is set')
|
|
@@ -460,8 +454,9 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
460
454
|
clearTimeout(debounced)
|
|
461
455
|
}
|
|
462
456
|
debounced = setTimeout(() => {
|
|
457
|
+
const { xyflow, onChange, xystore } = get()
|
|
463
458
|
const { nodeLookup } = xystore.getState()
|
|
464
|
-
|
|
459
|
+
set({ viewSyncDebounceTimeout: null })
|
|
465
460
|
|
|
466
461
|
const movedNodes = new StringSet()
|
|
467
462
|
const nodes = reduce([...nodeLookup.values()], (acc, node) => {
|
|
@@ -490,6 +485,10 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
490
485
|
}
|
|
491
486
|
return acc
|
|
492
487
|
}, {} as Changes.SaveManualLayout['edges'])
|
|
488
|
+
if (movedNodes.size === 0 && isEmpty(edges)) {
|
|
489
|
+
DEV && console.debug('ignore triggerSaveManualLayout, as no changes detected')
|
|
490
|
+
return
|
|
491
|
+
}
|
|
493
492
|
|
|
494
493
|
const change: Changes.SaveManualLayout = {
|
|
495
494
|
op: 'save-manual-layout',
|
|
@@ -500,24 +499,24 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
500
499
|
if (DEV) {
|
|
501
500
|
console.debug('triggerSaveManualLayout', change)
|
|
502
501
|
}
|
|
503
|
-
|
|
502
|
+
|
|
504
503
|
onChange?.({ change })
|
|
505
|
-
},
|
|
504
|
+
}, 2000) as any as number // explicit typecast to number to suppress TS error in astro build
|
|
506
505
|
set(
|
|
507
506
|
{
|
|
508
|
-
viewSyncDebounceTimeout: debounced
|
|
509
|
-
viewSyncState: 'changed'
|
|
507
|
+
viewSyncDebounceTimeout: debounced
|
|
510
508
|
},
|
|
511
509
|
noReplace,
|
|
512
|
-
'
|
|
510
|
+
'debounce sync state'
|
|
513
511
|
)
|
|
514
512
|
},
|
|
515
513
|
|
|
516
514
|
triggerOnNavigateTo: (xynodeId, event) => {
|
|
517
515
|
const { xyflow, view } = get()
|
|
518
516
|
const xynode = xyflow.getInternalNode(xynodeId)
|
|
519
|
-
|
|
520
|
-
|
|
517
|
+
invariant(!!xynode, `node not found: ${xynodeId}`)
|
|
518
|
+
const element = xynode.data.element as DiagramNodeWithNavigate
|
|
519
|
+
invariant(element.navigateTo, `node is not navigable: ${xynodeId}`)
|
|
521
520
|
set(
|
|
522
521
|
{
|
|
523
522
|
lastClickedNodeId: xynodeId,
|
|
@@ -543,20 +542,23 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
543
542
|
)
|
|
544
543
|
},
|
|
545
544
|
|
|
546
|
-
fitDiagram: (
|
|
547
|
-
const {
|
|
548
|
-
const {
|
|
545
|
+
fitDiagram: (duration = 500) => {
|
|
546
|
+
const { fitViewPadding, view, focusedNodeId, activeDynamicViewStep, xystore } = get()
|
|
547
|
+
const { width, height, panZoom, transform } = xystore.getState()
|
|
548
|
+
|
|
549
549
|
const bounds = {
|
|
550
550
|
x: 0,
|
|
551
551
|
y: 0,
|
|
552
552
|
width: view.width,
|
|
553
553
|
height: view.height
|
|
554
554
|
}
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
555
|
+
const maxZoom = Math.max(1, transform[2])
|
|
556
|
+
const viewport = getViewportForBounds(bounds, width, height, MinZoom, maxZoom, fitViewPadding)
|
|
557
|
+
panZoom?.setViewport(viewport, { duration })
|
|
558
|
+
if ((focusedNodeId ?? activeDynamicViewStep) !== null) {
|
|
558
559
|
set(
|
|
559
560
|
{
|
|
561
|
+
activeDynamicViewStep: null,
|
|
560
562
|
focusedNodeId: null,
|
|
561
563
|
dimmed: EmptyStringSet
|
|
562
564
|
},
|
|
@@ -567,7 +569,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
567
569
|
},
|
|
568
570
|
|
|
569
571
|
nextDynamicStep: (increment = 1) => {
|
|
570
|
-
const { view, activeDynamicViewStep, xyflow, fitViewPadding } = get()
|
|
572
|
+
const { view, activeDynamicViewStep, xyflow, xystore, fitViewPadding } = get()
|
|
571
573
|
invariant(view.__ === 'dynamic', 'view is not dynamic')
|
|
572
574
|
const nextStep = (activeDynamicViewStep ?? 0) + increment
|
|
573
575
|
if (nextStep <= 0 || nextStep > view.edges.length) {
|
|
@@ -592,36 +594,43 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
592
594
|
}
|
|
593
595
|
dimmed.add(n.id)
|
|
594
596
|
}
|
|
595
|
-
|
|
597
|
+
const { fitView, transform } = xystore.getState()
|
|
598
|
+
fitView({
|
|
596
599
|
duration: 400,
|
|
597
|
-
|
|
600
|
+
includeHiddenNodes: true,
|
|
601
|
+
maxZoom: Math.max(1, transform[2]),
|
|
598
602
|
minZoom: MinZoom,
|
|
599
|
-
padding: fitViewPadding,
|
|
603
|
+
padding: Math.max(fitViewPadding, 0.2),
|
|
600
604
|
nodes: selected
|
|
601
605
|
})
|
|
602
|
-
set(
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
606
|
+
set(
|
|
607
|
+
{
|
|
608
|
+
focusedNodeId: null,
|
|
609
|
+
activeDynamicViewStep: nextStep,
|
|
610
|
+
dimmed
|
|
611
|
+
},
|
|
612
|
+
noReplace,
|
|
613
|
+
'nextDynamicStep'
|
|
614
|
+
)
|
|
607
615
|
},
|
|
608
616
|
|
|
609
617
|
stopDynamicView: () => {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
618
|
+
if (get().activeDynamicViewStep !== null) {
|
|
619
|
+
set(
|
|
620
|
+
{
|
|
621
|
+
activeDynamicViewStep: null,
|
|
622
|
+
focusedNodeId: null,
|
|
623
|
+
dimmed: EmptyStringSet
|
|
624
|
+
},
|
|
625
|
+
noReplace,
|
|
626
|
+
'stopDynamicView'
|
|
627
|
+
)
|
|
628
|
+
get().fitDiagram()
|
|
629
|
+
}
|
|
621
630
|
}
|
|
622
631
|
}),
|
|
623
632
|
{
|
|
624
|
-
name:
|
|
633
|
+
name: `${storeDevId} - ${props.view.id}`,
|
|
625
634
|
enabled: DEV
|
|
626
635
|
}
|
|
627
636
|
)
|
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,6 +1,7 @@
|
|
|
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
|
},
|
|
@@ -32,6 +33,10 @@ export const vars = createGlobalThemeContract({
|
|
|
32
33
|
optionsPanel: {
|
|
33
34
|
top: 'options-panel-top',
|
|
34
35
|
right: 'options-panel-right'
|
|
36
|
+
},
|
|
37
|
+
navigationPanel: {
|
|
38
|
+
top: 'navigation-panel-top',
|
|
39
|
+
left: 'navigation-panel-left'
|
|
35
40
|
}
|
|
36
41
|
}, (value) => `likec4-${value}`)
|
|
37
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, '0.8rem'),
|
|
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-lg)'
|
|
14
|
+
}
|
|
15
|
+
// '@media': {
|
|
16
|
+
// [mantine.largerThan('lg')]: {
|
|
17
|
+
// vars: {
|
|
18
|
+
// ['--ai-size']: 'var(--ai-size-xl)'
|
|
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/hooks'
|
|
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
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { globalStyle, style } from '@vanilla-extract/css'
|
|
1
|
+
import { fallbackVar, globalStyle, style } from '@vanilla-extract/css'
|
|
2
2
|
import { mantine } from '../mantine.css'
|
|
3
|
+
import { vars } from '../theme.css'
|
|
3
4
|
|
|
4
5
|
export const container = style({
|
|
5
6
|
bottom: 0,
|
|
@@ -8,16 +9,26 @@ export const container = style({
|
|
|
8
9
|
margin: 0
|
|
9
10
|
})
|
|
10
11
|
|
|
11
|
-
export const
|
|
12
|
+
export const card = style({
|
|
12
13
|
cursor: 'default',
|
|
13
|
-
minWidth:
|
|
14
|
+
minWidth: 200,
|
|
14
15
|
maxWidth: 'calc(100vw - 16px)',
|
|
15
16
|
backgroundColor: `color-mix(in srgb, ${mantine.colors.body}, transparent 20%)`,
|
|
16
|
-
WebkitBackdropFilter: 'blur(8px)',
|
|
17
|
-
backdropFilter: 'blur(8px)',
|
|
17
|
+
WebkitBackdropFilter: fallbackVar(vars.safariAnimationHook, 'blur(8px)'),
|
|
18
|
+
backdropFilter: fallbackVar(vars.safariAnimationHook, 'blur(8px)'),
|
|
18
19
|
'@media': {
|
|
20
|
+
[mantine.largerThan('sm')]: {
|
|
21
|
+
minWidth: 300,
|
|
22
|
+
maxWidth: '90vw'
|
|
23
|
+
},
|
|
19
24
|
[mantine.largerThan('md')]: {
|
|
20
|
-
|
|
25
|
+
minWidth: 400,
|
|
26
|
+
maxWidth: '70vw'
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
selectors: {
|
|
30
|
+
[`:where([data-mantine-color-scheme='dark']) &`]: {
|
|
31
|
+
backgroundColor: `color-mix(in srgb, ${mantine.colors.dark[6]}, transparent 20%)`
|
|
21
32
|
}
|
|
22
33
|
}
|
|
23
34
|
})
|