@likec4/diagram 1.4.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.
Files changed (83) hide show
  1. package/dist/LikeC4Diagram.css.d.ts +0 -1
  2. package/dist/LikeC4Diagram.props.d.ts +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.mjs +1398 -1114
  5. package/dist/state/DiagramContext.d.ts +86 -4159
  6. package/dist/state/diagramStore.d.ts +88 -4162
  7. package/dist/state/hooks.d.ts +4 -0
  8. package/dist/state/index.d.ts +1 -2
  9. package/dist/style.css +182 -156
  10. package/dist/ui/options/EdgeOptions.d.ts +19 -0
  11. package/dist/ui/options/NodeOptions.d.ts +2 -2
  12. package/dist/xyflow/EdgeMarkers.d.ts +11 -0
  13. package/dist/xyflow/XYFlowEvents.d.ts +8 -5
  14. package/dist/xyflow/const.d.ts +3 -3
  15. package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
  16. package/dist/xyflow/edges/RelationshipEdge.d.ts +3 -2
  17. package/dist/xyflow/edges/edges.css.d.ts +1 -0
  18. package/dist/xyflow/edges/utils.d.ts +1 -0
  19. package/dist/xyflow/hooks/index.d.ts +3 -2
  20. package/dist/xyflow/hooks/useXYFlow.d.ts +13 -12
  21. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
  22. package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -4
  23. package/dist/xyflow/nodes/element/element.css.d.ts +2 -1
  24. package/dist/xyflow/types.d.ts +2 -32
  25. package/package.json +19 -19
  26. package/src/LikeC4Diagram.css.ts +0 -8
  27. package/src/LikeC4Diagram.props.ts +2 -2
  28. package/src/LikeC4Diagram.tsx +14 -20
  29. package/src/globals.css.ts +10 -3
  30. package/src/index.ts +1 -1
  31. package/src/mantine.css.ts +2 -2
  32. package/src/state/DiagramContext.tsx +44 -13
  33. package/src/state/diagramStore.ts +64 -52
  34. package/src/state/hooks.ts +5 -0
  35. package/src/state/index.ts +1 -2
  36. package/src/ui/BackwardForwardPanel.css.ts +9 -9
  37. package/src/ui/BackwardForwardPanel.tsx +1 -1
  38. package/src/ui/DiagramTitlePanel.css.ts +7 -2
  39. package/src/ui/DiagramTitlePanel.tsx +5 -3
  40. package/src/ui/DynamicViewWalkthrough.tsx +6 -4
  41. package/src/ui/OptionsPanel.tsx +37 -27
  42. package/src/ui/options/EdgeOptions.tsx +157 -0
  43. package/src/ui/options/NodeOptions.tsx +37 -29
  44. package/src/xyflow/EdgeMarkers.tsx +148 -0
  45. package/src/xyflow/FitviewOnDiagramChange.tsx +8 -4
  46. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +59 -55
  47. package/src/xyflow/SyncWithDiagram.tsx +13 -20
  48. package/src/xyflow/XYFlow.tsx +2 -4
  49. package/src/xyflow/XYFlowEvents.tsx +48 -33
  50. package/src/xyflow/XYFlowInner.tsx +1 -1
  51. package/src/xyflow/const.ts +3 -3
  52. package/src/xyflow/diagram-to-xyflow.ts +17 -41
  53. package/src/xyflow/edges/RelationshipEdge.tsx +207 -118
  54. package/src/xyflow/edges/edges.css.ts +36 -22
  55. package/src/xyflow/edges/utils.ts +10 -0
  56. package/src/xyflow/hooks/index.ts +4 -2
  57. package/src/xyflow/hooks/useLayoutConstraints.ts +3 -4
  58. package/src/xyflow/hooks/useXYFlow.ts +28 -1
  59. package/src/xyflow/nodes/compound/CompoundNode.css.ts +34 -24
  60. package/src/xyflow/nodes/compound/CompoundNode.tsx +52 -53
  61. package/src/xyflow/nodes/element/ElementIcon.tsx +11 -22
  62. package/src/xyflow/nodes/element/ElementLink.tsx +11 -16
  63. package/src/xyflow/nodes/element/ElementNode.tsx +64 -108
  64. package/src/xyflow/nodes/element/element.css.ts +30 -10
  65. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
  66. package/src/xyflow/types.ts +2 -4
  67. package/src/xyflow/utils.ts +3 -6
  68. package/dist/KeepAspectRatioContainer.d.ts +0 -9
  69. package/dist/icons/Clipboard.d.ts +0 -3
  70. package/dist/icons/Link.d.ts +0 -3
  71. package/dist/icons/ZoomIn.d.ts +0 -3
  72. package/dist/icons/index.d.ts +0 -3
  73. package/dist/mantine/theme.d.ts +0 -3
  74. package/dist/state/WhenInitialized.d.ts +0 -3
  75. package/dist/ui/Camera.d.ts +0 -3
  76. package/src/KeepAspectRatioContainer.tsx +0 -36
  77. package/src/icons/Clipboard.tsx +0 -24
  78. package/src/icons/Link.tsx +0 -23
  79. package/src/icons/ZoomIn.tsx +0 -25
  80. package/src/icons/index.ts +0 -3
  81. package/src/mantine/theme.ts +0 -20
  82. package/src/state/WhenInitialized.tsx +0 -24
  83. package/src/ui/Camera.tsx +0 -174
@@ -1,8 +1,8 @@
1
1
  import { deepEqual as eq } from 'fast-equals'
2
- import { useXYStoreApi } from './hooks'
3
2
 
4
3
  import { useEffect } from 'react'
5
- import { useDiagramState, useDiagramStoreApi } from '../state'
4
+ import { omit } from 'remeda'
5
+ import { useDiagramState, useDiagramStoreApi } from '../state/hooks'
6
6
  import { diagramViewToXYFlowData } from './diagram-to-xyflow'
7
7
 
8
8
  /**
@@ -10,7 +10,6 @@ import { diagramViewToXYFlowData } from './diagram-to-xyflow'
10
10
  */
11
11
  export function SyncWithDiagram() {
12
12
  const xyflowSynced = useDiagramState(s => s.xyflowSynced)
13
- const xyflowApi = useXYStoreApi()
14
13
  const diagramStoreApi = useDiagramStoreApi()
15
14
 
16
15
  useEffect(() => {
@@ -21,7 +20,8 @@ export function SyncWithDiagram() {
21
20
  view,
22
21
  nodesDraggable,
23
22
  nodesSelectable,
24
- focusedNodeId
23
+ focusedNodeId,
24
+ xystore
25
25
  } = diagramStoreApi.getState()
26
26
  const updates = diagramViewToXYFlowData(view, {
27
27
  draggable: nodesDraggable,
@@ -33,7 +33,7 @@ export function SyncWithDiagram() {
33
33
  edgeLookup,
34
34
  setNodes,
35
35
  setEdges
36
- } = xyflowApi.getState()
36
+ } = xystore.getState()
37
37
 
38
38
  setNodes(updates.nodes.map(update => {
39
39
  const existing = nodeLookup.get(update.id)?.internals.userNode
@@ -52,29 +52,22 @@ export function SyncWithDiagram() {
52
52
  setEdges(updates.edges.map(update => {
53
53
  const existing = edgeLookup.get(update.id)
54
54
  if (existing) {
55
- if (eq(existing.data.controlPoints, update.data.controlPoints) && eq(existing.data.edge, update.data.edge)) {
55
+ if (
56
+ eq(existing.data.controlPoints, update.data.controlPoints)
57
+ && eq(existing.markerStart, update.markerStart)
58
+ && eq(existing.markerEnd, update.markerEnd)
59
+ && eq(existing.data.edge, update.data.edge)
60
+ ) {
56
61
  return existing
57
62
  }
58
63
  return {
59
- ...existing,
64
+ ...omit(existing, ['data', 'markerStart', 'markerEnd']),
60
65
  ...update,
61
66
  data: {
62
67
  ...existing.data,
63
68
  ...update.data
64
69
  }
65
70
  }
66
- // if (update.data.controlPoints === null && existing.data.controlPoints !== null) {
67
- // // we don't want accidentally
68
- // // overwrite existing control points
69
- // return {
70
- // ...existing,
71
- // ...update,
72
- // data: {
73
- // ...update.data,
74
- // controlPoints: existing.data.controlPoints
75
- // }
76
- // }
77
- // }
78
71
  }
79
72
  return update
80
73
  }))
@@ -86,7 +79,7 @@ export function SyncWithDiagram() {
86
79
  false,
87
80
  'xyflowSynced'
88
81
  )
89
- }, [xyflowSynced, xyflowApi, diagramStoreApi])
82
+ }, [xyflowSynced, diagramStoreApi])
90
83
 
91
84
  return null
92
85
  }
@@ -3,7 +3,7 @@ import { DEV } from 'esm-env'
3
3
  import { deepEqual as eq } from 'fast-equals'
4
4
  import { type CSSProperties, type PropsWithChildren, useCallback } from 'react'
5
5
  import useTilg from 'tilg'
6
- import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../state'
6
+ import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../state/hooks'
7
7
  import { MinZoom } from './const'
8
8
  import { RelationshipEdge } from './edges/RelationshipEdge'
9
9
  import { useLayoutConstraints } from './hooks/useLayoutConstraints'
@@ -134,9 +134,6 @@ export function XYFlow({
134
134
  elementsSelectable={nodesSelectable}
135
135
  nodesFocusable={nodesDraggable || nodesSelectable || hasOnNodeClick || hasOnNavigateTo}
136
136
  edgesFocusable={hasOnEdgeClick}
137
- {...(!nodesSelectable && {
138
- selectionKeyCode: null
139
- })}
140
137
  nodesDraggable={nodesDraggable}
141
138
  {...nodesDraggable && {
142
139
  onNodeDragStart: layoutConstraints.onNodeDragStart,
@@ -146,6 +143,7 @@ export function XYFlow({
146
143
  zoomOnDoubleClick={false}
147
144
  elevateNodesOnSelect={false} // or edges are not visible after select
148
145
  selectNodesOnDrag={false} // or weird camera movement
146
+ selectionKeyCode={null}
149
147
  onDoubleClick={handlers.onDoubleClick}
150
148
  onPaneClick={handlers.onPaneClick}
151
149
  onMoveEnd={handlers.onMoveEnd}
@@ -1,12 +1,10 @@
1
1
  import type { ReactFlowProps } from '@xyflow/react'
2
2
  import { useMemo, useRef } from 'react'
3
- import { isNullish } from 'remeda'
4
3
  import type { Simplify } from 'type-fest'
5
- import { useDiagramStoreApi } from '../state'
6
- import { useXYStoreApi } from './hooks'
4
+ import { useDiagramStoreApi } from '../state/hooks'
7
5
  import type { XYFlowEdge, XYFlowNode } from './types'
8
6
 
9
- type XYFlowEventHandlers = Simplify<
7
+ export type XYFlowEventHandlers = Simplify<
10
8
  Required<
11
9
  Pick<
12
10
  ReactFlowProps<XYFlowNode, XYFlowEdge>,
@@ -29,32 +27,31 @@ type XYFlowEventHandlers = Simplify<
29
27
 
30
28
  export function useXYFlowEvents() {
31
29
  const diagramApi = useDiagramStoreApi()
32
- const xyflowApi = useXYStoreApi()
33
30
 
34
31
  const lastClickTimestamp = useRef<number>()
35
32
 
36
33
  const dblclickTimeout = useRef<number>()
37
34
 
38
- const dbclickLock = () => {
39
- if (dblclickTimeout.current !== undefined) {
40
- return true
41
- }
42
- dblclickTimeout.current = window.setTimeout(() => {
43
- dblclickTimeout.current = undefined
44
- }, 250)
45
- return false
46
- }
47
-
48
- const lastClickWasRecent = (ms = 1500) => {
49
- return lastClickTimestamp.current && (Date.now() - lastClickTimestamp.current) < ms
50
- }
51
-
52
35
  // If we are in focused mode, on edge enter we want to "highlight" the other node
53
36
  // This ref contains the id of this node
54
37
  const hoveredNodeFromOnEdgeEnterRef = useRef('')
55
38
 
56
- return useMemo(() =>
57
- ({
39
+ return useMemo(() => {
40
+ const dbclickLock = () => {
41
+ if (dblclickTimeout.current !== undefined) {
42
+ return true
43
+ }
44
+ dblclickTimeout.current = window.setTimeout(() => {
45
+ dblclickTimeout.current = undefined
46
+ }, 250)
47
+ return false
48
+ }
49
+
50
+ const lastClickWasRecent = (ms = 1500) => {
51
+ return lastClickTimestamp.current && (Date.now() - lastClickTimestamp.current) < ms
52
+ }
53
+
54
+ return ({
58
55
  onDoubleClick: (event) => {
59
56
  const {
60
57
  fitViewEnabled,
@@ -63,7 +60,7 @@ export function useXYFlowEvents() {
63
60
  fitDiagram
64
61
  } = diagramApi.getState()
65
62
  if (fitViewEnabled) {
66
- fitDiagram(xyflowApi)
63
+ fitDiagram()
67
64
  if (!onCanvasDblClick) {
68
65
  event.stopPropagation()
69
66
  }
@@ -82,8 +79,8 @@ export function useXYFlowEvents() {
82
79
  onCanvasClick,
83
80
  resetLastClicked
84
81
  } = diagramApi.getState()
85
- if (focusedNodeId || activeDynamicViewStep) {
86
- fitDiagram(xyflowApi)
82
+ if ((focusedNodeId ?? activeDynamicViewStep) !== null) {
83
+ fitDiagram()
87
84
  if (!onCanvasClick) {
88
85
  event.stopPropagation()
89
86
  }
@@ -106,7 +103,7 @@ export function useXYFlowEvents() {
106
103
  onEdgeContextMenu: (event, xyedge) => {
107
104
  diagramApi.getState().setLastClickedEdge(xyedge.id)
108
105
  diagramApi.getState().onEdgeContextMenu?.({
109
- relation: xyedge.data.edge,
106
+ edge: xyedge.data.edge,
110
107
  xyedge,
111
108
  event
112
109
  })
@@ -116,6 +113,7 @@ export function useXYFlowEvents() {
116
113
  zoomable,
117
114
  fitViewEnabled,
118
115
  focusedNodeId,
116
+ fitDiagram,
119
117
  focusOnNode,
120
118
  onNodeClick,
121
119
  lastClickedNodeId,
@@ -126,10 +124,21 @@ export function useXYFlowEvents() {
126
124
  // if we focused on a node, and clicked on another node - focus on the clicked node
127
125
  const shallChangeFocus = !!focusedNodeId && focusedNodeId !== xynode.id
128
126
  // if user clicked on the same node twice in a short time, focus on it
129
- const clickedRecently = isNullish(focusedNodeId) && lastClickedNodeId === xynode.id && lastClickWasRecent()
130
- if (focusPossible && (shallChangeFocus || clickedRecently)) {
131
- focusOnNode(xynode.id)
132
- if (!onNodeClick) {
127
+ const clickedRecently = lastClickedNodeId === xynode.id && lastClickWasRecent()
128
+ if (focusPossible) {
129
+ let stopPropagation = false
130
+ switch (true) {
131
+ case !focusedNodeId && clickedRecently:
132
+ case shallChangeFocus:
133
+ stopPropagation = true
134
+ focusOnNode(xynode.id)
135
+ break
136
+ case focusedNodeId === xynode.id && clickedRecently:
137
+ stopPropagation = true
138
+ fitDiagram()
139
+ break
140
+ }
141
+ if (!onNodeClick && stopPropagation) {
133
142
  // user did not provide a custom handler, stop propagation
134
143
  event.stopPropagation()
135
144
  }
@@ -151,10 +160,11 @@ export function useXYFlowEvents() {
151
160
  setLastClickedNode
152
161
  } = diagramApi.getState()
153
162
  setLastClickedNode(xynode.id)
163
+ lastClickTimestamp.current = Date.now()
154
164
  if (!!focusedNodeId || (zoomable && fitViewEnabled)) {
155
165
  // if we are already focused on the node, cancel
156
166
  if (focusedNodeId === xynode.id) {
157
- fitDiagram(xyflowApi)
167
+ fitDiagram()
158
168
  } else {
159
169
  focusOnNode(xynode.id)
160
170
  }
@@ -163,7 +173,11 @@ export function useXYFlowEvents() {
163
173
  },
164
174
  onEdgeClick: (event, xyedge) => {
165
175
  diagramApi.getState().setLastClickedEdge(xyedge.id)
166
- const { focusedNodeId, focusOnNode, onEdgeClick } = diagramApi.getState()
176
+ const {
177
+ focusedNodeId,
178
+ focusOnNode,
179
+ onEdgeClick
180
+ } = diagramApi.getState()
167
181
  // if we focused on a node, and clicked on an edge connected to it - focus on the other node
168
182
  if (focusedNodeId && (focusedNodeId === xyedge.source || focusedNodeId === xyedge.target)) {
169
183
  focusOnNode(focusedNodeId === xyedge.source ? xyedge.target : xyedge.source)
@@ -173,7 +187,7 @@ export function useXYFlowEvents() {
173
187
  }
174
188
  }
175
189
  onEdgeClick?.({
176
- relation: xyedge.data.edge,
190
+ edge: xyedge.data.edge,
177
191
  xyedge,
178
192
  event
179
193
  })
@@ -213,5 +227,6 @@ export function useXYFlowEvents() {
213
227
  }
214
228
  hoveredNodeFromOnEdgeEnterRef.current = ''
215
229
  }
216
- }) satisfies XYFlowEventHandlers, [diagramApi, xyflowApi])
230
+ }) satisfies XYFlowEventHandlers
231
+ }, [diagramApi])
217
232
  }
@@ -3,7 +3,7 @@ import { shallowEqual } from 'fast-equals'
3
3
  import { memo, type PropsWithChildren } from 'react'
4
4
  import { isNonNull } from 'remeda'
5
5
  import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
6
- import { useDiagramState } from '../state/useDiagramStore'
6
+ import { useDiagramState } from '../state/hooks'
7
7
  import { BackwardForwardPanel } from '../ui/BackwardForwardPanel'
8
8
  import DiagramTitlePanel from '../ui/DiagramTitlePanel'
9
9
  import { DynamicViewWalkthrough } from '../ui/DynamicViewWalkthrough'
@@ -1,7 +1,7 @@
1
1
  export const ZIndexes = {
2
- Compound: 1,
3
- Edge: 2,
4
- Element: 3
2
+ Compound: 2,
3
+ Edge: 4,
4
+ Element: 6
5
5
  } as const
6
6
 
7
7
  export const MinZoom = 0.1
@@ -1,9 +1,10 @@
1
- import { type DiagramNode, type DiagramView, extractStep, type Fqn, invariant, nonNullable } from '@likec4/core'
1
+ import { extractStep, invariant, nonNullable } from '@likec4/core'
2
+ import type { DiagramNode, DiagramView, Fqn } from '@likec4/core/types'
2
3
  import { hasAtLeast } from 'remeda'
3
4
  import type { XYFlowData } from '../xyflow/types'
5
+ import { ZIndexes } from './const'
4
6
 
5
- // const nodeZIndex = (node: DiagramNode) => node.level + (hasAtLeast(node.children, 1) ? 2 : 1)
6
- const nodeZIndex = (node: DiagramNode) => node.level + 1
7
+ // const nodeZIndex = (node: DiagramNode) => node.level - (node.children.length > 0 ? 1 : 0)
7
8
 
8
9
  export function diagramViewToXYFlowData(
9
10
  view: Pick<DiagramView, 'id' | 'nodes' | 'edges' | '__' | 'manualLayout'>,
@@ -39,10 +40,10 @@ export function diagramViewToXYFlowData(
39
40
  const nodeById = (id: Fqn) => nonNullable(nodeLookup.get(id), `Node not found: ${id}`)
40
41
 
41
42
  let next: typeof traverse[0] | undefined
42
- while ((next = traverse.pop())) {
43
+ while ((next = traverse.shift())) {
43
44
  const { node, parent } = next
44
45
  if (node.children.length > 0) {
45
- traverse.unshift(...node.children.map(child => ({ node: nodeById(child), parent: node })))
46
+ traverse.push(...node.children.map(child => ({ node: nodeById(child), parent: node })))
46
47
  }
47
48
  const isCompound = hasAtLeast(node.children, 1)
48
49
  const position = {
@@ -58,6 +59,7 @@ export function diagramViewToXYFlowData(
58
59
  // const inEdges = node.inEdges.map(e => view.edges.find(edge => edge.id === e)).filter(Boolean)
59
60
 
60
61
  const id = ns + node.id
62
+ const draggable = opts.draggable && (!parent || parent.children.length > 1)
61
63
  editor.nodes.push({
62
64
  id,
63
65
  type: isCompound ? 'compound' : 'element',
@@ -65,43 +67,22 @@ export function diagramViewToXYFlowData(
65
67
  fqn: node.id,
66
68
  element: node
67
69
  },
68
- draggable: opts.draggable && (!parent || parent.children.length > 1),
70
+ draggable,
69
71
  selectable: opts.selectable,
70
72
  deletable: false,
71
73
  position,
72
- zIndex: nodeZIndex(node),
74
+ zIndex: isCompound ? ZIndexes.Compound : ZIndexes.Element,
73
75
  hidden: false,
74
- /* initialWidth: node.width,
75
- initialHeight: node.height, */
76
+ initialWidth: node.width,
77
+ initialHeight: node.height,
76
78
  width: node.width,
77
79
  height: node.height,
78
- // measured: {
79
- // width: node.width,
80
- // height: node.height
81
- // },
82
80
  ...(parent && {
83
81
  parentId: ns + parent.id
82
+ }),
83
+ ...(isCompound && {
84
+ dragHandle: '.likec4-compound-title'
84
85
  })
85
- // handles: [
86
- // ...outEdges.map(out => ({
87
- // id: `${out.id}`,
88
- // type: 'source' as const,
89
- // position: Position.Bottom,
90
- // x: out.points[0][0],
91
- // y: out.points[0][1],
92
- // width: 10,
93
- // height: 10
94
- // })),
95
- // ...inEdges.map(out => ({
96
- // id: out.id,
97
- // type: 'target' as const,
98
- // position: Position.Top,
99
- // x: out.points[0][0],
100
- // y: out.points[0][1],
101
- // width: 10,
102
- // height: 10
103
- // }))
104
- // ],
105
86
  })
106
87
  }
107
88
 
@@ -120,33 +101,28 @@ export function diagramViewToXYFlowData(
120
101
  invariant(hasAtLeast(edge.points, 2), 'edge should have at least 2 points')
121
102
  // invariant(hasAtLeast(controlPoints, 2), 'edge controlPoints should have at least 2 points')
122
103
 
123
- const level = Math.max(nodeZIndex(nodeById(source)), nodeZIndex(nodeById(target)))
104
+ // const level = Math.max(nodeZIndex(nodeById(source)), nodeZIndex(nodeById(target)))
124
105
 
125
106
  editor.edges.push({
126
107
  id,
127
108
  type: 'relationship',
128
109
  source: ns + source,
129
110
  target: ns + target,
130
- zIndex: level,
111
+ zIndex: ZIndexes.Edge,
112
+ selectable: opts.selectable,
131
113
  deletable: false,
132
114
  data: {
133
115
  edge,
134
116
  type: 'bezier',
135
117
  controlPoints: view.manualLayout?.edges[edge.id]?.controlPoints || null,
136
- headPoint: edge.headArrowPoint ?? null,
137
- tailPoint: edge.tailArrowPoint ?? null,
138
118
  stepNum: isDynamicView ? extractStep(edge.id) : null,
139
119
  label: !!edge.labelBBox
140
120
  ? {
141
121
  bbox: edge.labelBBox,
142
- // text: edge.labels ? edge.labels.map(l => l.text).join('\n') : ''
143
122
  text: edge.label ?? ''
144
123
  }
145
124
  : null
146
125
  },
147
- // markerEnd: {
148
- // type: MarkerType.ArrowClosed
149
- // },
150
126
  interactionWidth: 20
151
127
  })
152
128
  }