@likec4/diagram 1.1.1 → 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.
- package/dist/KeepAspectRatioContainer.d.ts +1 -1
- package/dist/LikeC4Diagram.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +7 -2
- package/dist/StaticLikeC4Diagram.d.ts +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +2517 -1326
- package/dist/mantine/EnsureMantine.d.ts +1 -1
- package/dist/state/DiagramContext.d.ts +2125 -10
- package/dist/state/WhenInitialized.d.ts +1 -1
- package/dist/state/diagramStore.d.ts +2133 -10
- package/dist/state/useDiagramStore.d.ts +2124 -9
- package/dist/style.css +210 -136
- package/dist/theme.css.d.ts +1 -0
- package/dist/ui/DiagramTitlePanel.d.ts +1 -2
- package/dist/ui/DynamicViewWalkthrough.css.d.ts +3 -0
- package/dist/ui/DynamicViewWalkthrough.d.ts +1 -0
- package/dist/ui/OptionsPanel.d.ts +1 -2
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/options/NodeOptions.d.ts +1 -2
- package/dist/xyflow/FitviewOnDiagramChange.d.ts +4 -2
- package/dist/xyflow/SyncWithDiagram.d.ts +1 -2
- package/dist/xyflow/XYFlow.d.ts +4 -7
- package/dist/xyflow/XYFlowBackground.d.ts +5 -4
- package/dist/xyflow/XYFlowEvents.d.ts +57 -63
- package/dist/xyflow/XYFlowInner.d.ts +11 -0
- package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
- package/dist/xyflow/edges/RelationshipEdge.d.ts +14 -14
- package/dist/xyflow/edges/edges.css.d.ts +4 -2
- package/dist/xyflow/edges/utils.d.ts +2 -0
- package/dist/xyflow/hooks/useXYFlow.d.ts +126 -126
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
- package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -1
- package/dist/xyflow/nodes/element/ElementLink.d.ts +1 -1
- package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -1
- package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +2 -2
- package/dist/xyflow/nodes/element/element.css.d.ts +2 -1
- package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +1 -2
- package/dist/xyflow/types.d.ts +18 -18
- package/package.json +19 -16
- package/src/KeepAspectRatioContainer.tsx +1 -1
- package/src/LikeC4Diagram.css.ts +2 -0
- package/src/LikeC4Diagram.props.ts +9 -4
- package/src/LikeC4Diagram.tsx +17 -11
- package/src/StaticLikeC4Diagram.tsx +1 -0
- package/src/globals.css.ts +1 -0
- package/src/index.ts +0 -3
- package/src/state/DiagramContext.tsx +21 -12
- package/src/state/diagramStore.ts +177 -38
- package/src/theme.css.ts +1 -0
- package/src/ui/DiagramTitlePanel.css.ts +2 -1
- package/src/ui/DiagramTitlePanel.tsx +3 -4
- package/src/ui/DynamicViewWalkthrough.css.ts +46 -0
- package/src/ui/DynamicViewWalkthrough.tsx +86 -0
- package/src/ui/OptionsPanel.tsx +5 -3
- package/src/ui/index.ts +1 -0
- package/src/ui/options/NodeOptions.tsx +23 -24
- package/src/xyflow/FitviewOnDiagramChange.tsx +89 -69
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +57 -39
- package/src/xyflow/SyncWithDiagram.tsx +55 -107
- package/src/xyflow/XYFlow.tsx +51 -35
- package/src/xyflow/XYFlowBackground.tsx +7 -5
- package/src/xyflow/XYFlowEvents.tsx +97 -92
- package/src/xyflow/XYFlowInner.tsx +50 -0
- package/src/xyflow/diagram-to-xyflow.ts +16 -21
- package/src/xyflow/edges/RelationshipEdge.tsx +70 -34
- package/src/xyflow/edges/edges.css.ts +68 -34
- package/src/xyflow/edges/utils.ts +31 -0
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +6 -2
- package/src/xyflow/nodes/compound/CompoundNode.tsx +6 -3
- package/src/xyflow/nodes/element/ElementLink.tsx +13 -3
- package/src/xyflow/nodes/element/ElementNode.tsx +24 -19
- package/src/xyflow/nodes/element/element.css.ts +9 -2
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -0
- package/src/xyflow/types.ts +7 -8
package/src/LikeC4Diagram.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import '@xyflow/react/dist/style.css'
|
|
1
2
|
import { ReactFlowProvider as XYFlowProvider } from '@xyflow/react'
|
|
2
3
|
import clsx from 'clsx'
|
|
3
4
|
import { useRef } from 'react'
|
|
@@ -8,13 +9,13 @@ import { type LikeC4DiagramEventHandlers, type LikeC4DiagramProperties } from '.
|
|
|
8
9
|
import { EnsureMantine } from './mantine/EnsureMantine'
|
|
9
10
|
import { DiagramContextProvider } from './state/DiagramContext'
|
|
10
11
|
import { WhenInitialized } from './state/WhenInitialized'
|
|
11
|
-
import DiagramTitlePanel from './ui/DiagramTitlePanel'
|
|
12
|
-
import OptionsPanel from './ui/OptionsPanel'
|
|
13
12
|
import { diagramViewToXYFlowData } from './xyflow/diagram-to-xyflow'
|
|
14
13
|
import { FitViewOnDiagramChange } from './xyflow/FitviewOnDiagramChange'
|
|
14
|
+
import { SelectEdgesOnNodeFocus } from './xyflow/SelectEdgesOnNodeFocus'
|
|
15
15
|
import { SyncWithDiagram } from './xyflow/SyncWithDiagram'
|
|
16
16
|
import type { XYFlowData } from './xyflow/types'
|
|
17
17
|
import { XYFlow } from './xyflow/XYFlow'
|
|
18
|
+
import { XYFlowInner } from './xyflow/XYFlowInner'
|
|
18
19
|
|
|
19
20
|
export type LikeC4DiagramProps = LikeC4DiagramProperties & LikeC4DiagramEventHandlers
|
|
20
21
|
export function LikeC4Diagram({
|
|
@@ -28,9 +29,10 @@ export function LikeC4Diagram({
|
|
|
28
29
|
nodesSelectable = !readonly,
|
|
29
30
|
nodesDraggable = !readonly,
|
|
30
31
|
background = 'dots',
|
|
31
|
-
controls =
|
|
32
|
+
controls = !readonly,
|
|
32
33
|
showElementLinks = true,
|
|
33
34
|
showDiagramTitle = true,
|
|
35
|
+
enableDynamicViewWalkthrough = true,
|
|
34
36
|
initialWidth,
|
|
35
37
|
initialHeight,
|
|
36
38
|
keepAspectRatio = false,
|
|
@@ -44,6 +46,7 @@ export function LikeC4Diagram({
|
|
|
44
46
|
onNodeClick,
|
|
45
47
|
onNodeContextMenu
|
|
46
48
|
}: LikeC4DiagramProps) {
|
|
49
|
+
console.log('LikeC4Diagram')
|
|
47
50
|
const initialRef = useRef<{
|
|
48
51
|
defaultNodes: XYFlowData['nodes']
|
|
49
52
|
defaultEdges: XYFlowData['edges']
|
|
@@ -102,19 +105,22 @@ export function LikeC4Diagram({
|
|
|
102
105
|
pannable !== true && cssDisablePan,
|
|
103
106
|
background === 'transparent' && cssTransparentBg
|
|
104
107
|
)}
|
|
105
|
-
background={background}
|
|
106
|
-
controls={controls}
|
|
107
108
|
defaultNodes={initialRef.current.defaultNodes}
|
|
108
109
|
defaultEdges={initialRef.current.defaultEdges}
|
|
109
110
|
>
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
<XYFlowInner
|
|
112
|
+
showDiagramTitle={showDiagramTitle}
|
|
113
|
+
enableDynamicViewWalkthrough={enableDynamicViewWalkthrough}
|
|
114
|
+
background={background}
|
|
115
|
+
controls={controls}
|
|
116
|
+
/>
|
|
112
117
|
</XYFlow>
|
|
118
|
+
<WhenInitialized>
|
|
119
|
+
<SyncWithDiagram />
|
|
120
|
+
{fitView && <FitViewOnDiagramChange />}
|
|
121
|
+
{fitView && zoomable && <SelectEdgesOnNodeFocus />}
|
|
122
|
+
</WhenInitialized>
|
|
113
123
|
</KeepAspectRatioContainer>
|
|
114
|
-
<WhenInitialized>
|
|
115
|
-
<SyncWithDiagram />
|
|
116
|
-
<FitViewOnDiagramChange />
|
|
117
|
-
</WhenInitialized>
|
|
118
124
|
</DiagramContextProvider>
|
|
119
125
|
</XYFlowProvider>
|
|
120
126
|
</EnsureMantine>
|
package/src/globals.css.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createContext, type PropsWithChildren, useRef } from 'react'
|
|
1
|
+
import { createContext, type PropsWithChildren, useEffect, useRef } from 'react'
|
|
2
|
+
import { hasSubObject, isIncludedIn } from 'remeda'
|
|
2
3
|
import { useUpdateEffect } from '../hooks'
|
|
3
4
|
import { useXYFlow } from '../xyflow/hooks'
|
|
4
5
|
import { createDiagramStore, type DiagramInitialState } from './diagramStore'
|
|
@@ -18,20 +19,28 @@ export function DiagramContextProvider({ children, view, ...props }: PropsWithCh
|
|
|
18
19
|
})
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
() =>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
() => store.current?.getState().updateView(view),
|
|
29
|
-
[view]
|
|
22
|
+
useEffect(
|
|
23
|
+
() => {
|
|
24
|
+
if (store.current?.getState().xyflow !== xyflow) {
|
|
25
|
+
store.current?.setState({ xyflow }, false, 'update xyflow')
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
[xyflow]
|
|
30
29
|
)
|
|
31
30
|
|
|
32
31
|
useUpdateEffect(
|
|
33
|
-
() =>
|
|
34
|
-
|
|
32
|
+
() => {
|
|
33
|
+
if (!store.current) return
|
|
34
|
+
const state = store.current.getState()
|
|
35
|
+
if (state.view !== view) {
|
|
36
|
+
state.updateView(view)
|
|
37
|
+
}
|
|
38
|
+
if (!hasSubObject(state, props)) {
|
|
39
|
+
store.current.setState(props, false, 'update incoming props')
|
|
40
|
+
}
|
|
41
|
+
isIncludedIn
|
|
42
|
+
},
|
|
43
|
+
[view, props]
|
|
35
44
|
)
|
|
36
45
|
|
|
37
46
|
return (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
BorderStyle,
|
|
3
|
+
DiagramEdge,
|
|
3
4
|
DiagramNode,
|
|
4
5
|
DiagramView,
|
|
5
6
|
ElementShape,
|
|
@@ -8,20 +9,21 @@ import type {
|
|
|
8
9
|
ThemeColor,
|
|
9
10
|
ViewID
|
|
10
11
|
} from '@likec4/core'
|
|
11
|
-
import { invariant, nonexhaustive } from '@likec4/core'
|
|
12
|
-
import type
|
|
12
|
+
import { invariant, nonexhaustive, StepEdgeId } from '@likec4/core'
|
|
13
|
+
import { getViewportForBounds, type XYPosition } from '@xyflow/react'
|
|
13
14
|
import { getNodeDimensions } from '@xyflow/system'
|
|
14
15
|
import { DEV } from 'esm-env'
|
|
15
|
-
import { shallowEqual } from 'fast-equals'
|
|
16
|
+
import { deepEqual, shallowEqual } from 'fast-equals'
|
|
16
17
|
import type { MouseEvent as ReactMouseEvent } from 'react'
|
|
17
|
-
import { entries } from 'remeda'
|
|
18
|
+
import { entries, filter, map, prop } from 'remeda'
|
|
18
19
|
import type { Exact, Except, RequiredKeysOf, Simplify } from 'type-fest'
|
|
19
20
|
import { devtools, subscribeWithSelector } from 'zustand/middleware'
|
|
20
21
|
import { shallow } from 'zustand/shallow'
|
|
21
22
|
import { createWithEqualityFn } from 'zustand/traditional'
|
|
22
23
|
import type { Change, DiagramNodeWithNavigate, LikeC4DiagramEventHandlers } from '../LikeC4Diagram.props'
|
|
23
24
|
import { MinZoom } from '../xyflow/const'
|
|
24
|
-
import type {
|
|
25
|
+
import type { XYStoreApi } from '../xyflow/hooks'
|
|
26
|
+
import type { XYFlowEdge, XYFlowInstance, XYFlowNode } from '../xyflow/types'
|
|
25
27
|
|
|
26
28
|
export type DiagramStore = {
|
|
27
29
|
// Incoming props
|
|
@@ -38,6 +40,10 @@ export type DiagramStore = {
|
|
|
38
40
|
// Internal state
|
|
39
41
|
xyflow: XYFlowInstance
|
|
40
42
|
initialized: boolean
|
|
43
|
+
xyflowSynced: boolean
|
|
44
|
+
|
|
45
|
+
// If Dynamic View
|
|
46
|
+
activeDynamicViewStep: number | null
|
|
41
47
|
|
|
42
48
|
// This is XYFlow id's
|
|
43
49
|
lastClickedNodeId: string | null
|
|
@@ -46,6 +52,9 @@ export type DiagramStore = {
|
|
|
46
52
|
hoveredNodeId: string | null
|
|
47
53
|
hoveredEdgeId: string | null
|
|
48
54
|
|
|
55
|
+
// id's of nodes / edges that
|
|
56
|
+
dimmed: ReadonlySet<string>
|
|
57
|
+
|
|
49
58
|
// Stack of previous distinct views (unique id's)
|
|
50
59
|
previousViews: DiagramView[]
|
|
51
60
|
|
|
@@ -62,6 +71,8 @@ export type DiagramStore = {
|
|
|
62
71
|
viewportChanged: boolean
|
|
63
72
|
} & Required<LikeC4DiagramEventHandlers>
|
|
64
73
|
|
|
74
|
+
const StringSet = Set<string>
|
|
75
|
+
|
|
65
76
|
export type DiagramInitialState = // Required properties
|
|
66
77
|
& Pick<
|
|
67
78
|
DiagramStore,
|
|
@@ -85,8 +96,12 @@ export type DiagramInitialState = // Required properties
|
|
|
85
96
|
& LikeC4DiagramEventHandlers
|
|
86
97
|
|
|
87
98
|
interface DiagramStoreActions {
|
|
99
|
+
isDynamicView: () => boolean
|
|
100
|
+
|
|
88
101
|
updateView: (view: DiagramView) => void
|
|
89
102
|
|
|
103
|
+
focusOnNode: (nodeId: string) => void
|
|
104
|
+
|
|
90
105
|
setHoveredNode: (nodeId: string | null) => void
|
|
91
106
|
setHoveredEdge: (edgeId: string | null) => void
|
|
92
107
|
|
|
@@ -98,20 +113,26 @@ interface DiagramStoreActions {
|
|
|
98
113
|
getElement(id: Fqn): DiagramNode | null
|
|
99
114
|
triggerOnChange: (changes: NonEmptyArray<Change>) => void
|
|
100
115
|
triggerOnNavigateTo: (xynodeId: string, event: ReactMouseEvent) => void
|
|
101
|
-
fitDiagram: (
|
|
116
|
+
fitDiagram: (xyStore: XYStoreApi) => void
|
|
117
|
+
|
|
118
|
+
nextDynamicStep: (increment?: number) => void
|
|
119
|
+
stopDynamicView: () => void
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
export type DiagramState = Simplify<DiagramStore & DiagramStoreActions>
|
|
105
123
|
|
|
106
124
|
const DEFAULT_PROPS: Except<DiagramStore, RequiredKeysOf<DiagramInitialState> | 'xyflow'> = {
|
|
107
125
|
initialized: false,
|
|
126
|
+
xyflowSynced: false,
|
|
108
127
|
previousViews: [],
|
|
109
128
|
viewportChanged: false,
|
|
129
|
+
activeDynamicViewStep: null,
|
|
110
130
|
focusedNodeId: null,
|
|
111
131
|
hoveredNodeId: null,
|
|
112
132
|
hoveredEdgeId: null,
|
|
113
133
|
lastClickedNodeId: null,
|
|
114
134
|
lastClickedEdgeId: null,
|
|
135
|
+
dimmed: new StringSet(),
|
|
115
136
|
lastOnNavigate: null,
|
|
116
137
|
onChange: null,
|
|
117
138
|
onNavigateTo: null,
|
|
@@ -130,7 +151,7 @@ const noReplace = false
|
|
|
130
151
|
|
|
131
152
|
let storeDevId = 1
|
|
132
153
|
|
|
133
|
-
const
|
|
154
|
+
const EmptyStringSet: ReadonlySet<string> = new StringSet()
|
|
134
155
|
|
|
135
156
|
export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props: T) {
|
|
136
157
|
return createWithEqualityFn<
|
|
@@ -146,78 +167,139 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
146
167
|
...DEFAULT_PROPS,
|
|
147
168
|
...(props as CreateDiagramStore),
|
|
148
169
|
|
|
149
|
-
|
|
170
|
+
isDynamicView: () => {
|
|
171
|
+
return get().view?.__ === 'dynamic'
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
updateView: (nextView) => {
|
|
150
175
|
let {
|
|
151
|
-
|
|
176
|
+
dimmed,
|
|
177
|
+
xyflowSynced,
|
|
178
|
+
view: current,
|
|
152
179
|
lastOnNavigate,
|
|
153
180
|
previousViews,
|
|
154
181
|
focusedNodeId,
|
|
155
182
|
lastClickedNodeId,
|
|
156
183
|
lastClickedEdgeId,
|
|
184
|
+
activeDynamicViewStep,
|
|
157
185
|
hoveredEdgeId,
|
|
158
186
|
hoveredNodeId
|
|
159
187
|
} = get()
|
|
160
188
|
|
|
161
|
-
if (shallowEqual(
|
|
189
|
+
if (shallowEqual(current, nextView)) {
|
|
190
|
+
if (!xyflowSynced) {
|
|
191
|
+
set({ xyflowSynced: true }, noReplace, 'updateView: xyflow synced')
|
|
192
|
+
}
|
|
162
193
|
DEV && console.debug('store: skip updateView')
|
|
163
194
|
return
|
|
164
195
|
}
|
|
165
196
|
|
|
166
|
-
|
|
167
|
-
if (lastOnNavigate && lastOnNavigate.toView !== view.id && lastOnNavigate.fromView !== view.id) {
|
|
168
|
-
lastOnNavigate = null
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const isSameView = currentView.id === view.id
|
|
197
|
+
const isSameView = current.id === nextView.id
|
|
172
198
|
|
|
173
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))
|
|
174
202
|
// Reset clicked/hovered node/edge if the node/edge is not in the new view
|
|
175
|
-
if (lastClickedNodeId && !
|
|
203
|
+
if (lastClickedNodeId && !nodeIds.has(lastClickedNodeId)) {
|
|
176
204
|
lastClickedNodeId = null
|
|
177
205
|
}
|
|
178
|
-
if (hoveredNodeId && !
|
|
206
|
+
if (hoveredNodeId && !nodeIds.has(hoveredNodeId)) {
|
|
179
207
|
hoveredNodeId = null
|
|
180
208
|
}
|
|
181
|
-
if (focusedNodeId && !
|
|
209
|
+
if (focusedNodeId && !nodeIds.has(focusedNodeId)) {
|
|
182
210
|
focusedNodeId = null
|
|
183
211
|
}
|
|
184
|
-
if (lastClickedEdgeId && !
|
|
212
|
+
if (lastClickedEdgeId && !edgeIds.has(lastClickedEdgeId)) {
|
|
185
213
|
lastClickedEdgeId = null
|
|
186
214
|
}
|
|
187
|
-
if (hoveredEdgeId && !
|
|
215
|
+
if (hoveredEdgeId && !edgeIds.has(hoveredEdgeId)) {
|
|
188
216
|
hoveredEdgeId = null
|
|
189
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
|
+
}
|
|
190
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
|
+
|
|
191
236
|
// Reset hovered / clicked node/edge if the view is different
|
|
192
237
|
lastClickedEdgeId = null
|
|
193
238
|
lastClickedNodeId = null
|
|
194
239
|
hoveredEdgeId = null
|
|
195
240
|
hoveredNodeId = null
|
|
196
241
|
focusedNodeId = null
|
|
242
|
+
activeDynamicViewStep = null
|
|
243
|
+
dimmed = EmptyStringSet
|
|
197
244
|
|
|
198
245
|
// Update history stack (back button not implemented yet)
|
|
199
246
|
previousViews = [
|
|
200
|
-
|
|
201
|
-
...previousViews.filter((v) => v.id !==
|
|
247
|
+
current,
|
|
248
|
+
...previousViews.filter((v) => v.id !== nextView.id && v.id !== current.id)
|
|
202
249
|
]
|
|
250
|
+
xyflowSynced = false
|
|
203
251
|
}
|
|
204
252
|
|
|
205
253
|
set(
|
|
206
254
|
{
|
|
207
|
-
view,
|
|
255
|
+
view: nextView,
|
|
256
|
+
activeDynamicViewStep,
|
|
257
|
+
xyflowSynced,
|
|
208
258
|
lastOnNavigate,
|
|
209
259
|
previousViews,
|
|
210
260
|
lastClickedNodeId,
|
|
211
261
|
lastClickedEdgeId,
|
|
212
262
|
focusedNodeId,
|
|
213
263
|
hoveredEdgeId,
|
|
214
|
-
hoveredNodeId
|
|
264
|
+
hoveredNodeId,
|
|
265
|
+
dimmed
|
|
215
266
|
},
|
|
216
267
|
noReplace,
|
|
217
268
|
'update-view'
|
|
218
269
|
)
|
|
219
270
|
},
|
|
220
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
|
+
|
|
221
303
|
setHoveredNode: (nodeId) => {
|
|
222
304
|
if (nodeId !== get().hoveredNodeId) {
|
|
223
305
|
set({ hoveredNodeId: nodeId }, noReplace, nodeId ? 'setHoveredNode' : 'unsetHoveredNode')
|
|
@@ -253,7 +335,8 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
253
335
|
{
|
|
254
336
|
focusedNodeId: null,
|
|
255
337
|
lastClickedNodeId: null,
|
|
256
|
-
lastClickedEdgeId: null
|
|
338
|
+
lastClickedEdgeId: null,
|
|
339
|
+
dimmed: EmptyStringSet
|
|
257
340
|
},
|
|
258
341
|
noReplace,
|
|
259
342
|
'resetLastClicked'
|
|
@@ -386,22 +469,78 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
386
469
|
)
|
|
387
470
|
},
|
|
388
471
|
|
|
389
|
-
fitDiagram: (
|
|
390
|
-
const {
|
|
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
|
+
}
|
|
391
518
|
xyflow.fitView({
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
padding: fitViewPadding,
|
|
519
|
+
duration: 400,
|
|
520
|
+
maxZoom: 1,
|
|
395
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,
|
|
396
540
|
maxZoom: 1,
|
|
397
|
-
|
|
541
|
+
minZoom: MinZoom,
|
|
542
|
+
padding: fitViewPadding
|
|
398
543
|
})
|
|
399
|
-
if (centerNode && focusedNodeId !== centerNode.id) {
|
|
400
|
-
set({ focusedNodeId: centerNode.id }, noReplace, 'focus node')
|
|
401
|
-
}
|
|
402
|
-
if (!centerNode && !!focusedNodeId) {
|
|
403
|
-
set({ focusedNodeId: null }, noReplace, 'unfocus')
|
|
404
|
-
}
|
|
405
544
|
}
|
|
406
545
|
}),
|
|
407
546
|
{
|
package/src/theme.css.ts
CHANGED
|
@@ -10,9 +10,10 @@ export const container = style({
|
|
|
10
10
|
|
|
11
11
|
export const paper = style({
|
|
12
12
|
cursor: 'default',
|
|
13
|
-
minWidth:
|
|
13
|
+
minWidth: 100,
|
|
14
14
|
maxWidth: 'calc(100vw - 16px)',
|
|
15
15
|
backgroundColor: `color-mix(in srgb, ${mantine.colors.body}, transparent 20%)`,
|
|
16
|
+
WebkitBackdropFilter: 'blur(8px)',
|
|
16
17
|
backdropFilter: 'blur(8px)',
|
|
17
18
|
'@media': {
|
|
18
19
|
[mantine.largerThan('md')]: {
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Anchor, Box, Button, CopyButton, Group, Paper, Spoiler, Stack, Text } from '@mantine/core'
|
|
2
|
-
import {
|
|
2
|
+
import { useLocalStorage } from '@mantine/hooks'
|
|
3
3
|
import clsx from 'clsx'
|
|
4
4
|
import { AnimatePresence, motion } from 'framer-motion'
|
|
5
5
|
import { type DiagramState, useDiagramState } from '../state'
|
|
6
6
|
import * as css from './DiagramTitlePanel.css'
|
|
7
|
-
import { useLocalStorage } from '@mantine/hooks'
|
|
8
7
|
|
|
9
8
|
const selector = (s: DiagramState) => ({
|
|
10
9
|
id: s.view.id,
|
|
@@ -40,7 +39,7 @@ export default function DiagramTitlePanel() {
|
|
|
40
39
|
radius="sm"
|
|
41
40
|
withBorder={!isCollapsed}
|
|
42
41
|
className={css.paper}
|
|
43
|
-
p={isCollapsed ? '
|
|
42
|
+
p={isCollapsed ? 'xs' : 'lg'}
|
|
44
43
|
onDoubleClick={e => e.stopPropagation()}>
|
|
45
44
|
<Stack gap={'sm'} justify="stretch" align="stretch">
|
|
46
45
|
<Group justify="stretch" wrap="nowrap" onClick={toggle} style={{ cursor: 'pointer' }}>
|
|
@@ -99,7 +98,7 @@ export default function DiagramTitlePanel() {
|
|
|
99
98
|
justify="stretch"
|
|
100
99
|
align="stretch">
|
|
101
100
|
{links.map((link) => (
|
|
102
|
-
<Group key={link} wrap="nowrap" align=
|
|
101
|
+
<Group key={link} wrap="nowrap" align="center" gap={'sm'}>
|
|
103
102
|
<Box flex={'1'} style={{ overflow: 'hidden' }}>
|
|
104
103
|
<Anchor
|
|
105
104
|
href={link}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createVar, globalStyle, style } from '@vanilla-extract/css'
|
|
2
|
+
import { mantine } from '../mantine.css'
|
|
3
|
+
|
|
4
|
+
export const container = style({
|
|
5
|
+
position: 'absolute',
|
|
6
|
+
bottom: '0.5rem',
|
|
7
|
+
left: '50%',
|
|
8
|
+
transform: 'translateX(-50%)',
|
|
9
|
+
padding: 0,
|
|
10
|
+
margin: 0
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const buttons = style({
|
|
14
|
+
backdropFilter: 'blur(8px)',
|
|
15
|
+
transition: 'all 175ms ease-in',
|
|
16
|
+
':hover': {
|
|
17
|
+
transitionTimingFunction: 'ease-out',
|
|
18
|
+
transform: 'scale(1.1)'
|
|
19
|
+
},
|
|
20
|
+
':active': {
|
|
21
|
+
transitionDuration: '100ms',
|
|
22
|
+
transform: 'scale(0.98) translateY(3px)'
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
var transparency = createVar()
|
|
27
|
+
export const btn = style({
|
|
28
|
+
transition: 'all 175ms ease-in',
|
|
29
|
+
backgroundColor: 'var(--button-bg)',
|
|
30
|
+
vars: {
|
|
31
|
+
[transparency]: '35%',
|
|
32
|
+
['--button-bg']: `color-mix(in srgb, ${mantine.colors.primaryColors.filled}, transparent ${transparency})`,
|
|
33
|
+
['--button-hover']: `color-mix(in srgb, ${mantine.colors.primaryColors.filledHover}, transparent ${transparency})`
|
|
34
|
+
},
|
|
35
|
+
selectors: {
|
|
36
|
+
[mantine.lightSelector]: {
|
|
37
|
+
vars: {
|
|
38
|
+
[transparency]: '15%'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
globalStyle(`${btn} .tabler-icon`, {
|
|
45
|
+
width: '80%'
|
|
46
|
+
})
|