@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
@@ -32,7 +32,7 @@ export function useXYFlowEvents() {
32
32
 
33
33
  const dblclickTimeout = useRef<number>()
34
34
 
35
- const dbclickGuarg = () => {
35
+ const dbclickLock = () => {
36
36
  if (dblclickTimeout.current !== undefined) {
37
37
  return true
38
38
  }
@@ -42,112 +42,158 @@ export function useXYFlowEvents() {
42
42
  return false
43
43
  }
44
44
 
45
- return useMemo<XYFlowEventHandlers>(() => ({
46
- onDoubleClick: (event) => {
47
- diagramApi.getState().setLastClickedNode(null)
48
- diagramApi.getState().setLastClickedEdge(null)
49
- const diagramState = diagramApi.getState()
50
- if (diagramState.onCanvasDblClick) {
51
- diagramState.onCanvasDblClick(event)
52
- return
53
- }
54
- if (diagramState.fitViewEnabled && diagramState.zoomable) {
55
- diagramState.fitDiagram()
56
- event.stopPropagation()
57
- return
58
- }
59
- },
60
- onPaneClick: (event) => {
61
- if (dbclickGuarg()) {
62
- return
63
- }
64
- diagramApi.getState().setLastClickedNode(null)
65
- diagramApi.getState().setLastClickedEdge(null)
66
- const { focusedNodeId, fitDiagram, onCanvasClick } = diagramApi.getState()
67
- if (onCanvasClick) {
68
- onCanvasClick(event)
69
- return
70
- }
71
- // reset focus if clicked on empty space
72
- if (focusedNodeId) {
73
- fitDiagram()
74
- event.stopPropagation()
75
- }
76
- },
77
- onNodeContextMenu: (event, xynode) => {
78
- diagramApi.getState().setLastClickedNode(xynode.id)
79
- diagramApi.getState().onNodeContextMenu?.({
80
- element: xynode.data.element,
81
- xynode,
82
- event
83
- })
84
- },
85
- onPaneContextMenu: (event) => {
86
- diagramApi.getState().setLastClickedNode(null)
87
- diagramApi.getState().setLastClickedEdge(null)
88
- diagramApi.getState().onCanvasContextMenu?.(event as any)
89
- },
90
- onEdgeContextMenu: (event, xyedge) => {
91
- diagramApi.getState().setLastClickedEdge(xyedge.id)
92
- diagramApi.getState().onEdgeContextMenu?.({
93
- relation: xyedge.data.edge,
94
- xyedge,
95
- event
96
- })
97
- },
98
- onNodeClick: (event, xynode) => {
99
- diagramApi.getState().setLastClickedNode(xynode.id)
100
- const { focusedNodeId, fitDiagram, onNodeClick } = diagramApi.getState()
101
- if (onNodeClick) {
102
- onNodeClick({
45
+ // If we are in focused mode, on edge enter we want to "highlight" the other node
46
+ // This ref contains the id of this node
47
+ const hoveredNodeFromOnEdgeEnterRef = useRef('')
48
+
49
+ return useMemo(() =>
50
+ ({
51
+ onDoubleClick: (event) => {
52
+ const {
53
+ fitViewEnabled,
54
+ onCanvasDblClick,
55
+ resetLastClicked,
56
+ fitDiagram
57
+ } = diagramApi.getState()
58
+ if (fitViewEnabled) {
59
+ fitDiagram(xyflowApi)
60
+ if (!onCanvasDblClick) {
61
+ event.stopPropagation()
62
+ }
63
+ }
64
+ resetLastClicked()
65
+ onCanvasDblClick?.(event)
66
+ },
67
+ onPaneClick: (event) => {
68
+ if (dbclickLock()) {
69
+ return
70
+ }
71
+ const { focusedNodeId, fitDiagram, onCanvasClick, resetLastClicked } = diagramApi.getState()
72
+ if (!!focusedNodeId) {
73
+ fitDiagram(xyflowApi)
74
+ if (!onCanvasClick) {
75
+ event.stopPropagation()
76
+ }
77
+ }
78
+ resetLastClicked()
79
+ onCanvasClick?.(event)
80
+ },
81
+ onNodeContextMenu: (event, xynode) => {
82
+ diagramApi.getState().setLastClickedNode(xynode.id)
83
+ diagramApi.getState().onNodeContextMenu?.({
103
84
  element: xynode.data.element,
104
85
  xynode,
105
86
  event
106
87
  })
107
- return
108
- }
109
- // if we clicked on a node that is not focused, fit the diagram
110
- if (!!focusedNodeId && focusedNodeId !== xynode.id) {
111
- fitDiagram(xynode)
112
- event.stopPropagation()
113
- }
114
- },
115
- onNodeDoubleClick: (event, xynode) => {
116
- const { zoomable, fitDiagram, onNodeClick } = diagramApi.getState()
117
- if (onNodeClick) {
118
- return
119
- }
120
- if (zoomable) {
121
- fitDiagram(xynode)
122
- event.stopPropagation()
123
- }
124
- },
125
- onEdgeClick: (event, xyedge) => {
126
- diagramApi.getState().setLastClickedEdge(xyedge.id)
127
- diagramApi.getState().onEdgeClick?.({
128
- relation: xyedge.data.edge,
129
- xyedge,
130
- event
131
- })
132
- },
133
- onMoveEnd: (event, _viewport) => {
134
- // if event is present, the move was triggered by user
135
- const viewportChanged = !!event
136
- if (viewportChanged !== diagramApi.getState().viewportChanged) {
137
- diagramApi.setState({ viewportChanged }, false, `viewport-changed: ${viewportChanged}`)
88
+ },
89
+ onPaneContextMenu: (event) => {
90
+ diagramApi.getState().resetLastClicked()
91
+ diagramApi.getState().onCanvasContextMenu?.(event as any)
92
+ },
93
+ onEdgeContextMenu: (event, xyedge) => {
94
+ diagramApi.getState().setLastClickedEdge(xyedge.id)
95
+ diagramApi.getState().onEdgeContextMenu?.({
96
+ relation: xyedge.data.edge,
97
+ xyedge,
98
+ event
99
+ })
100
+ },
101
+ onNodeClick: (event, xynode) => {
102
+ const {
103
+ focusedNodeId,
104
+ focusOnNode,
105
+ onNodeClick,
106
+ setLastClickedNode
107
+ } = diagramApi.getState()
108
+ setLastClickedNode(xynode.id)
109
+ // if we focused on a node, and clicked on another node - focus on the clicked node
110
+ if (!!focusedNodeId && focusedNodeId !== xynode.id) {
111
+ focusOnNode(xynode.id)
112
+ if (!onNodeClick) {
113
+ // user did not provide a custom handler, stop propagation
114
+ event.stopPropagation()
115
+ }
116
+ }
117
+ onNodeClick?.({
118
+ element: xynode.data.element,
119
+ xynode,
120
+ event
121
+ })
122
+ },
123
+ onNodeDoubleClick: (event, xynode) => {
124
+ const {
125
+ focusedNodeId,
126
+ zoomable,
127
+ fitViewEnabled,
128
+ fitDiagram,
129
+ focusOnNode,
130
+ setLastClickedNode
131
+ } = diagramApi.getState()
132
+ setLastClickedNode(xynode.id)
133
+ if (!!focusedNodeId || (zoomable && fitViewEnabled)) {
134
+ // if we are already focused on the node, cancel
135
+ if (focusedNodeId === xynode.id) {
136
+ fitDiagram(xyflowApi)
137
+ } else {
138
+ focusOnNode(xynode.id)
139
+ }
140
+ event.stopPropagation()
141
+ }
142
+ },
143
+ onEdgeClick: (event, xyedge) => {
144
+ diagramApi.getState().setLastClickedEdge(xyedge.id)
145
+ const { focusedNodeId, focusOnNode, onEdgeClick } = diagramApi.getState()
146
+ // if we focused on a node, and clicked on an edge connected to it - focus on the other node
147
+ if (focusedNodeId && (focusedNodeId === xyedge.source || focusedNodeId === xyedge.target)) {
148
+ focusOnNode(focusedNodeId === xyedge.source ? xyedge.target : xyedge.source)
149
+ if (!onEdgeClick) {
150
+ // user did not provide a custom handler, stop propagation
151
+ event.stopPropagation()
152
+ }
153
+ }
154
+ onEdgeClick?.({
155
+ relation: xyedge.data.edge,
156
+ xyedge,
157
+ event
158
+ })
159
+ },
160
+ onMoveEnd: (event, _viewport) => {
161
+ // if event is present, the move was triggered by user
162
+ const viewportChanged = !!event
163
+ if (viewportChanged !== diagramApi.getState().viewportChanged) {
164
+ diagramApi.setState({ viewportChanged }, false, `viewport-changed: ${viewportChanged}`)
165
+ }
166
+ },
167
+ onNodeMouseEnter: (event, xynode) => {
168
+ hoveredNodeFromOnEdgeEnterRef.current = ''
169
+ const { hoveredNodeId, setHoveredNode } = diagramApi.getState()
170
+ if (hoveredNodeId !== xynode.id) {
171
+ setHoveredNode(xynode.id)
172
+ }
173
+ },
174
+ onNodeMouseLeave: (event, xynode) => {
175
+ const { hoveredNodeId, setHoveredNode } = diagramApi.getState()
176
+ if (hoveredNodeId === xynode.id) {
177
+ setHoveredNode(null)
178
+ }
179
+ },
180
+ onEdgeMouseEnter: (event, { id, source, target }) => {
181
+ const { hoveredNodeId, focusedNodeId, setHoveredEdge, setHoveredNode } = diagramApi.getState()
182
+ setHoveredEdge(id)
183
+ if ((focusedNodeId === source || focusedNodeId === target) && focusedNodeId !== hoveredNodeId) {
184
+ const next = hoveredNodeFromOnEdgeEnterRef.current = source === focusedNodeId ? target : source
185
+ setHoveredNode(next)
186
+ }
187
+ },
188
+ onEdgeMouseLeave: (event, xyedge) => {
189
+ const { hoveredEdgeId, setHoveredEdge, hoveredNodeId, setHoveredNode } = diagramApi.getState()
190
+ if (hoveredEdgeId === xyedge.id) {
191
+ setHoveredEdge(null)
192
+ }
193
+ if (hoveredNodeId === hoveredNodeFromOnEdgeEnterRef.current) {
194
+ hoveredNodeFromOnEdgeEnterRef.current = ''
195
+ setHoveredNode(null)
196
+ }
138
197
  }
139
- },
140
- onNodeMouseEnter: (event, xynode) => {
141
- diagramApi.getState().setHoveredNode(xynode.id)
142
- },
143
- onNodeMouseLeave: (event, xynode) => {
144
- diagramApi.getState().setHoveredNode(null)
145
- },
146
- onEdgeMouseEnter: (event, xyedge) => {
147
- diagramApi.getState().setHoveredEdge(xyedge.id)
148
- },
149
- onEdgeMouseLeave: (event, xyedge) => {
150
- diagramApi.getState().setHoveredEdge(null)
151
- }
152
- }), [diagramApi, xyflowApi])
198
+ }) satisfies XYFlowEventHandlers, [diagramApi, xyflowApi])
153
199
  }
@@ -0,0 +1,50 @@
1
+ import { isDynamicView } from '@likec4/core'
2
+ import { Controls } from '@xyflow/react'
3
+ import type { PropsWithChildren } from 'react'
4
+ import { isNonNull, isNonNullish, isNullish } from 'remeda'
5
+ import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
6
+ import { useDiagramState } from '../state/useDiagramStore'
7
+ import { DynamicViewWalkthrough } from '../ui'
8
+ import DiagramTitlePanel from '../ui/DiagramTitlePanel'
9
+ import OptionsPanel from '../ui/OptionsPanel'
10
+ import { XYFlowBackground } from './XYFlowBackground'
11
+
12
+ type XYFlowInnerProps = PropsWithChildren<{
13
+ background: NonNullable<LikeC4DiagramProperties['background']>
14
+ controls: boolean
15
+ showDiagramTitle: boolean
16
+ enableDynamicViewWalkthrough: boolean
17
+ }>
18
+
19
+ export function XYFlowInner({
20
+ children,
21
+ background,
22
+ controls,
23
+ showDiagramTitle,
24
+ enableDynamicViewWalkthrough
25
+ }: XYFlowInnerProps) {
26
+ const {
27
+ isDynamicView,
28
+ isDynamicViewActive,
29
+ readonly
30
+ } = useDiagramState(s => ({
31
+ isDynamicView: s.isDynamicView(),
32
+ readonly: s.readonly,
33
+ isDynamicViewActive: isNonNull(s.activeDynamicViewStep)
34
+ }))
35
+
36
+ const isBgWithPattern = background !== 'transparent' && background !== 'solid'
37
+
38
+ const isDiagramTitleVisible = !isDynamicViewActive && showDiagramTitle === true
39
+
40
+ return (
41
+ <>
42
+ {isBgWithPattern && <XYFlowBackground background={background} />}
43
+ {controls && <Controls position={'bottom-right'} />}
44
+ {readonly === false && <OptionsPanel />}
45
+ {isDiagramTitleVisible && <DiagramTitlePanel />}
46
+ {isDynamicView && enableDynamicViewWalkthrough && <DynamicViewWalkthrough />}
47
+ {children}
48
+ </>
49
+ )
50
+ }
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  type DiagramNode,
3
3
  type DiagramView,
4
+ extractStep,
4
5
  type Fqn,
5
6
  invariant,
6
7
  type NonEmptyArray,
7
8
  nonNullable,
8
9
  type Point
9
10
  } from '@likec4/core'
10
- import { getBezierEdgeCenter } from '@xyflow/react'
11
+ import { Bezier } from 'bezier-js'
11
12
  import { hasAtLeast } from 'remeda'
12
13
  import type { XYFlowData } from '../xyflow/types'
13
14
 
@@ -20,24 +21,16 @@ function deriveEdgePoints(bezierSpline: NonEmptyArray<Point>) {
20
21
 
21
22
  while (hasAtLeast(bezierPoints, 3)) {
22
23
  const [cp1, cp2, end, ...rest] = bezierPoints
23
- const [centerX, centerY] = getBezierEdgeCenter({
24
- sourceX: start[0],
25
- sourceY: start[1],
26
- targetX: end[0],
27
- targetY: end[1],
28
- sourceControlX: cp1[0],
29
- sourceControlY: cp1[1],
30
- targetControlX: cp2[0],
31
- targetControlY: cp2[1]
24
+ const bezier = new Bezier(start[0], start[1], cp1[0], cp1[1], cp2[0], cp2[1], end[0], end[1])
25
+ // const { x, y } = bezier.get(0.5)
26
+ const inflections = bezier.inflections()
27
+ if (inflections.length === 0) {
28
+ inflections.push(0.5)
29
+ }
30
+ inflections.forEach(t => {
31
+ const { x, y } = bezier.get(t)
32
+ handles.push([x, y])
32
33
  })
33
- handles.push(
34
- [centerX, centerY]
35
- )
36
- // if (rest.length === 0) {
37
- // handles.push(
38
- // end
39
- // )
40
- // }
41
34
  bezierPoints = rest
42
35
  start = end
43
36
  }
@@ -50,12 +43,13 @@ function deriveEdgePoints(bezierSpline: NonEmptyArray<Point>) {
50
43
  const nodeZIndex = (node: DiagramNode) => node.level + 1
51
44
 
52
45
  export function diagramViewToXYFlowData(
53
- view: Pick<DiagramView, 'nodes' | 'edges'>,
46
+ view: Pick<DiagramView, 'nodes' | 'edges' | '__'>,
54
47
  opts: {
55
48
  draggable: boolean
56
49
  selectable: boolean
57
50
  }
58
51
  ): XYFlowData {
52
+ const isDynamicView = view.__ === 'dynamic'
59
53
  const editor: XYFlowData = {
60
54
  nodes: [],
61
55
  edges: []
@@ -178,11 +172,12 @@ export function diagramViewToXYFlowData(
178
172
  controlPoints,
179
173
  headPoint: edge.headArrowPoint ?? null,
180
174
  tailPoint: edge.tailArrowPoint ?? null,
181
- label: edge.labelBBox && edge.label
175
+ stepNum: isDynamicView ? extractStep(edge.id) : null,
176
+ label: !!edge.labelBBox
182
177
  ? {
183
178
  bbox: edge.labelBBox,
184
179
  // text: edge.labels ? edge.labels.map(l => l.text).join('\n') : ''
185
- text: edge.label
180
+ text: edge.label ?? ''
186
181
  }
187
182
  : null
188
183
  },