@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.
Files changed (69) hide show
  1. package/dist/LikeC4Diagram.d.ts +1 -1
  2. package/dist/LikeC4Diagram.props.d.ts +27 -4
  3. package/dist/index.mjs +1543 -1346
  4. package/dist/state/DiagramContext.d.ts +2353 -264
  5. package/dist/state/diagramStore.d.ts +2360 -268
  6. package/dist/state/index.d.ts +0 -1
  7. package/dist/state/useDiagramStore.d.ts +3 -2534
  8. package/dist/state/useNavigationHistory.d.ts +5 -0
  9. package/dist/style.css +194 -131
  10. package/dist/theme.css.d.ts +5 -1
  11. package/dist/ui/BackwardForwardPanel.css.d.ts +1 -0
  12. package/dist/ui/BackwardForwardPanel.d.ts +1 -0
  13. package/dist/ui/DiagramTitlePanel.css.d.ts +1 -1
  14. package/dist/xyflow/XYFlow.d.ts +2 -1
  15. package/dist/xyflow/XYFlowEvents.d.ts +4 -4
  16. package/dist/xyflow/XYFlowInner.d.ts +2 -1
  17. package/dist/xyflow/const.d.ts +1 -1
  18. package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
  19. package/dist/xyflow/edges/RelationshipEdge.d.ts +4 -5
  20. package/dist/xyflow/edges/utils.d.ts +4 -2
  21. package/dist/xyflow/hooks/useXYFlow.d.ts +9 -9
  22. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +3 -1
  23. package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -1
  24. package/dist/xyflow/nodes/element/element.css.d.ts +1 -0
  25. package/dist/xyflow/nodes/element/index.d.ts +1 -1
  26. package/dist/xyflow/types.d.ts +3 -3
  27. package/dist/xyflow/utils.d.ts +3 -0
  28. package/package.json +18 -17
  29. package/src/LikeC4Diagram.css.ts +15 -5
  30. package/src/LikeC4Diagram.props.ts +33 -5
  31. package/src/LikeC4Diagram.tsx +103 -32
  32. package/src/globals.css.ts +1 -2
  33. package/src/mantine/theme.ts +1 -0
  34. package/src/state/DiagramContext.tsx +3 -4
  35. package/src/state/diagramStore.ts +177 -103
  36. package/src/state/index.ts +0 -1
  37. package/src/state/useDiagramStore.ts +6 -13
  38. package/src/state/useNavigationHistory.ts +39 -0
  39. package/src/theme.css.ts +5 -1
  40. package/src/ui/BackwardForwardPanel.css.ts +22 -0
  41. package/src/ui/BackwardForwardPanel.tsx +59 -0
  42. package/src/ui/DiagramTitlePanel.css.ts +11 -5
  43. package/src/ui/DiagramTitlePanel.tsx +108 -96
  44. package/src/ui/DynamicViewWalkthrough.css.ts +3 -2
  45. package/src/ui/DynamicViewWalkthrough.tsx +36 -17
  46. package/src/ui/OptionsPanel.tsx +28 -19
  47. package/src/ui/options/NodeOptions.tsx +16 -19
  48. package/src/xyflow/FitviewOnDiagramChange.tsx +15 -23
  49. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +3 -6
  50. package/src/xyflow/SyncWithDiagram.tsx +21 -2
  51. package/src/xyflow/XYFlow.tsx +20 -23
  52. package/src/xyflow/XYFlowEvents.tsx +26 -8
  53. package/src/xyflow/XYFlowInner.tsx +10 -6
  54. package/src/xyflow/const.ts +1 -1
  55. package/src/xyflow/diagram-to-xyflow.ts +4 -41
  56. package/src/xyflow/edges/RelationshipEdge.tsx +160 -111
  57. package/src/xyflow/edges/edges.css.ts +41 -26
  58. package/src/xyflow/edges/utils.ts +5 -2
  59. package/src/xyflow/hooks/useLayoutConstraints.ts +19 -4
  60. package/src/xyflow/nodes/compound/CompoundNode.css.ts +45 -37
  61. package/src/xyflow/nodes/compound/CompoundNode.tsx +36 -26
  62. package/src/xyflow/nodes/element/ElementNode.tsx +27 -14
  63. package/src/xyflow/nodes/element/element.css.ts +31 -18
  64. package/src/xyflow/nodes/element/index.ts +1 -1
  65. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +7 -3
  66. package/src/xyflow/types.ts +5 -2
  67. package/src/xyflow/utils.ts +48 -0
  68. package/dist/ui/index.d.ts +0 -2
  69. package/src/ui/index.ts +0 -3
@@ -52,10 +52,29 @@ export function SyncWithDiagram() {
52
52
  setEdges(updates.edges.map(update => {
53
53
  const existing = edgeLookup.get(update.id)
54
54
  if (existing) {
55
- return eq(existing.data.edge, update.data.edge) ? existing : {
55
+ if (eq(existing.data.controlPoints, update.data.controlPoints) && eq(existing.data.edge, update.data.edge)) {
56
+ return existing
57
+ }
58
+ return {
56
59
  ...existing,
57
- ...update
60
+ ...update,
61
+ data: {
62
+ ...existing.data,
63
+ ...update.data
64
+ }
58
65
  }
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
+ // }
59
78
  }
60
79
  return update
61
80
  }))
@@ -1,10 +1,8 @@
1
- import { useMantineColorScheme } from '@mantine/core'
2
- import { Controls, ReactFlow, useOnViewportChange } from '@xyflow/react'
3
- import { deepEqual as eq, shallowEqual } from 'fast-equals'
4
- import { type CSSProperties, memo, type PropsWithChildren } from 'react'
5
- import { omit, omitBy } from 'remeda'
6
- import type { SetNonNullable, Simplify } from 'type-fest'
7
- import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
1
+ import { ReactFlow, useOnViewportChange } from '@xyflow/react'
2
+ import { DEV } from 'esm-env'
3
+ import { deepEqual as eq } from 'fast-equals'
4
+ import { type CSSProperties, type PropsWithChildren, useCallback } from 'react'
5
+ import useTilg from 'tilg'
8
6
  import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../state'
9
7
  import { MinZoom } from './const'
10
8
  import { RelationshipEdge } from './edges/RelationshipEdge'
@@ -13,7 +11,6 @@ import { useXYStoreApi } from './hooks/useXYFlow'
13
11
  import { CompoundNode } from './nodes/compound'
14
12
  import { ElementNode } from './nodes/element'
15
13
  import { XYFlowEdge, XYFlowNode } from './types'
16
- import { XYFlowBackground } from './XYFlowBackground'
17
14
  import { useXYFlowEvents } from './XYFlowEvents'
18
15
 
19
16
  const nodeTypes = {
@@ -25,6 +22,7 @@ const edgeTypes = {
25
22
  }
26
23
 
27
24
  type XYFlowWrapperProps = PropsWithChildren<{
25
+ colorMode?: 'system' | 'light' | 'dark'
28
26
  className?: string | undefined
29
27
  defaultNodes: XYFlowNode[]
30
28
  defaultEdges: XYFlowEdge[]
@@ -45,7 +43,12 @@ const selector = (s: DiagramState) => ({
45
43
  nodesSelectable: s.nodesSelectable || s.focusedNodeId !== null,
46
44
  nodesDraggable: s.nodesDraggable,
47
45
  fitView: s.fitViewEnabled,
48
- fitViewPadding: s.fitViewPadding,
46
+ fitViewOptions: {
47
+ minZoom: MinZoom,
48
+ maxZoom: 1,
49
+ padding: s.fitViewPadding,
50
+ includeHiddenNodes: true
51
+ },
49
52
  hasOnNavigateTo: !!s.onNavigateTo,
50
53
  hasOnNodeClick: !!s.onNodeClick,
51
54
  hasOnNodeContextMenu: !!s.onNodeContextMenu,
@@ -57,19 +60,21 @@ const selector = (s: DiagramState) => ({
57
60
  })
58
61
 
59
62
  export function XYFlow({
63
+ colorMode = 'system',
60
64
  className,
61
65
  children,
62
66
  defaultNodes,
63
67
  defaultEdges,
64
68
  style
65
69
  }: XYFlowWrapperProps) {
70
+ DEV && useTilg()
66
71
  const xyflowApi = useXYStoreApi()
67
72
  const diagramApi = useDiagramStoreApi()
68
73
  const {
69
74
  nodesSelectable,
70
75
  nodesDraggable,
71
76
  fitView,
72
- fitViewPadding,
77
+ fitViewOptions,
73
78
  pannable,
74
79
  zoomable,
75
80
  hasOnNodeClick,
@@ -78,15 +83,12 @@ export function XYFlow({
78
83
  hasOnCanvasContextMenu,
79
84
  hasOnEdgeContextMenu,
80
85
  hasOnEdgeClick
81
- } = useDiagramState(selector)
86
+ } = useDiagramState(selector, eq)
82
87
 
83
88
  const layoutConstraints = useLayoutConstraints()
84
89
 
85
90
  const handlers = useXYFlowEvents()
86
91
 
87
- const { colorScheme } = useMantineColorScheme()
88
- const colorMode = colorScheme !== 'auto' ? colorScheme : undefined
89
-
90
92
  /**
91
93
  * WORKAROUND - Called on viewport change
92
94
  * Viewport transform is not rounded to integers which results in blurry nodes on some resolution
@@ -122,12 +124,7 @@ export function XYFlow({
122
124
  maxZoom={zoomable ? 1.9 : 1}
123
125
  minZoom={zoomable ? MinZoom : 1}
124
126
  fitView={fitView}
125
- fitViewOptions={{
126
- minZoom: MinZoom,
127
- maxZoom: 1,
128
- padding: fitViewPadding,
129
- includeHiddenNodes: true
130
- }}
127
+ fitViewOptions={fitViewOptions}
131
128
  preventScrolling={zoomable || pannable}
132
129
  defaultMarkerColor="var(--xy-edge-stroke)"
133
130
  noDragClassName="nodrag"
@@ -159,9 +156,9 @@ export function XYFlow({
159
156
  onNodeClick={handlers.onNodeClick}
160
157
  onNodeDoubleClick={handlers.onNodeDoubleClick}
161
158
  onEdgeClick={handlers.onEdgeClick}
162
- onInit={() => {
159
+ onInit={useCallback(() => {
163
160
  diagramApi.setState({ initialized: true }, false, 'initialized')
164
- }}
161
+ }, [diagramApi])}
165
162
  {...(hasOnNodeContextMenu && {
166
163
  onNodeContextMenu: handlers.onNodeContextMenu
167
164
  })}
@@ -176,4 +173,4 @@ export function XYFlow({
176
173
  )
177
174
  }
178
175
 
179
- // export const XYFlow = memo(XYFlowWrapper, propsAreEqual) as typeof XYFlowWrapper
176
+ // export const XYFlow = memo(XYFlowWrapper) as typeof XYFlowWrapper
@@ -1,5 +1,6 @@
1
1
  import type { ReactFlowProps } from '@xyflow/react'
2
2
  import { useMemo, useRef } from 'react'
3
+ import { isNullish } from 'remeda'
3
4
  import type { Simplify } from 'type-fest'
4
5
  import { useDiagramStoreApi } from '../state'
5
6
  import { useXYStoreApi } from './hooks'
@@ -30,6 +31,8 @@ export function useXYFlowEvents() {
30
31
  const diagramApi = useDiagramStoreApi()
31
32
  const xyflowApi = useXYStoreApi()
32
33
 
34
+ const lastClickTimestamp = useRef<number>()
35
+
33
36
  const dblclickTimeout = useRef<number>()
34
37
 
35
38
  const dbclickLock = () => {
@@ -42,6 +45,10 @@ export function useXYFlowEvents() {
42
45
  return false
43
46
  }
44
47
 
48
+ const lastClickWasRecent = (ms = 1500) => {
49
+ return lastClickTimestamp.current && (Date.now() - lastClickTimestamp.current) < ms
50
+ }
51
+
45
52
  // If we are in focused mode, on edge enter we want to "highlight" the other node
46
53
  // This ref contains the id of this node
47
54
  const hoveredNodeFromOnEdgeEnterRef = useRef('')
@@ -68,8 +75,14 @@ export function useXYFlowEvents() {
68
75
  if (dbclickLock()) {
69
76
  return
70
77
  }
71
- const { focusedNodeId, fitDiagram, onCanvasClick, resetLastClicked } = diagramApi.getState()
72
- if (!!focusedNodeId) {
78
+ const {
79
+ focusedNodeId,
80
+ activeDynamicViewStep,
81
+ fitDiagram,
82
+ onCanvasClick,
83
+ resetLastClicked
84
+ } = diagramApi.getState()
85
+ if (focusedNodeId || activeDynamicViewStep) {
73
86
  fitDiagram(xyflowApi)
74
87
  if (!onCanvasClick) {
75
88
  event.stopPropagation()
@@ -100,20 +113,28 @@ export function useXYFlowEvents() {
100
113
  },
101
114
  onNodeClick: (event, xynode) => {
102
115
  const {
116
+ zoomable,
117
+ fitViewEnabled,
103
118
  focusedNodeId,
104
119
  focusOnNode,
105
120
  onNodeClick,
121
+ lastClickedNodeId,
106
122
  setLastClickedNode
107
123
  } = diagramApi.getState()
108
124
  setLastClickedNode(xynode.id)
125
+ const focusPossible = zoomable && fitViewEnabled
109
126
  // if we focused on a node, and clicked on another node - focus on the clicked node
110
- if (!!focusedNodeId && focusedNodeId !== xynode.id) {
127
+ const shallChangeFocus = !!focusedNodeId && focusedNodeId !== xynode.id
128
+ // 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)) {
111
131
  focusOnNode(xynode.id)
112
132
  if (!onNodeClick) {
113
133
  // user did not provide a custom handler, stop propagation
114
134
  event.stopPropagation()
115
135
  }
116
136
  }
137
+ lastClickTimestamp.current = Date.now()
117
138
  onNodeClick?.({
118
139
  element: xynode.data.element,
119
140
  xynode,
@@ -166,10 +187,7 @@ export function useXYFlowEvents() {
166
187
  },
167
188
  onNodeMouseEnter: (_event, xynode) => {
168
189
  hoveredNodeFromOnEdgeEnterRef.current = ''
169
- const { hoveredNodeId, setHoveredNode } = diagramApi.getState()
170
- if (hoveredNodeId !== xynode.id) {
171
- setHoveredNode(xynode.id)
172
- }
190
+ diagramApi.getState().setHoveredNode(xynode.id)
173
191
  },
174
192
  onNodeMouseLeave: (_event, xynode) => {
175
193
  const { hoveredNodeId, setHoveredNode } = diagramApi.getState()
@@ -191,9 +209,9 @@ export function useXYFlowEvents() {
191
209
  setHoveredEdge(null)
192
210
  }
193
211
  if (hoveredNodeId === hoveredNodeFromOnEdgeEnterRef.current) {
194
- hoveredNodeFromOnEdgeEnterRef.current = ''
195
212
  setHoveredNode(null)
196
213
  }
214
+ hoveredNodeFromOnEdgeEnterRef.current = ''
197
215
  }
198
216
  }) satisfies XYFlowEventHandlers, [diagramApi, xyflowApi])
199
217
  }
@@ -1,11 +1,12 @@
1
- import { isDynamicView } from '@likec4/core'
2
1
  import { Controls } from '@xyflow/react'
3
- import type { PropsWithChildren } from 'react'
4
- import { isNonNull, isNonNullish, isNullish } from 'remeda'
2
+ import { shallowEqual } from 'fast-equals'
3
+ import { memo, type PropsWithChildren } from 'react'
4
+ import { isNonNull } from 'remeda'
5
5
  import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
6
6
  import { useDiagramState } from '../state/useDiagramStore'
7
- import { DynamicViewWalkthrough } from '../ui'
7
+ import { BackwardForwardPanel } from '../ui/BackwardForwardPanel'
8
8
  import DiagramTitlePanel from '../ui/DiagramTitlePanel'
9
+ import { DynamicViewWalkthrough } from '../ui/DynamicViewWalkthrough'
9
10
  import OptionsPanel from '../ui/OptionsPanel'
10
11
  import { XYFlowBackground } from './XYFlowBackground'
11
12
 
@@ -13,14 +14,16 @@ type XYFlowInnerProps = PropsWithChildren<{
13
14
  background: NonNullable<LikeC4DiagramProperties['background']>
14
15
  controls: boolean
15
16
  showDiagramTitle: boolean
17
+ showNavigationButtons: boolean
16
18
  enableDynamicViewWalkthrough: boolean
17
19
  }>
18
20
 
19
- export function XYFlowInner({
21
+ export const XYFlowInner = memo(function XYFlowInnerR({
20
22
  children,
21
23
  background,
22
24
  controls,
23
25
  showDiagramTitle,
26
+ showNavigationButtons,
24
27
  enableDynamicViewWalkthrough
25
28
  }: XYFlowInnerProps) {
26
29
  const {
@@ -44,7 +47,8 @@ export function XYFlowInner({
44
47
  {readonly === false && <OptionsPanel />}
45
48
  {isDiagramTitleVisible && <DiagramTitlePanel />}
46
49
  {isDynamicView && enableDynamicViewWalkthrough && <DynamicViewWalkthrough />}
50
+ {showNavigationButtons && <BackwardForwardPanel />}
47
51
  {children}
48
52
  </>
49
53
  )
50
- }
54
+ }, shallowEqual)
@@ -4,4 +4,4 @@ export const ZIndexes = {
4
4
  Element: 3
5
5
  } as const
6
6
 
7
- export const MinZoom = 0.05
7
+ export const MinZoom = 0.1
@@ -1,49 +1,12 @@
1
- import {
2
- type DiagramNode,
3
- type DiagramView,
4
- extractStep,
5
- type Fqn,
6
- invariant,
7
- type NonEmptyArray,
8
- nonNullable,
9
- type Point
10
- } from '@likec4/core'
11
- import { Bezier } from 'bezier-js'
1
+ import { type DiagramNode, type DiagramView, extractStep, type Fqn, invariant, nonNullable } from '@likec4/core'
12
2
  import { hasAtLeast } from 'remeda'
13
3
  import type { XYFlowData } from '../xyflow/types'
14
4
 
15
- function deriveEdgePoints(bezierSpline: NonEmptyArray<Point>) {
16
- let [start, ...bezierPoints] = bezierSpline
17
- invariant(start, 'start should be defined')
18
- const handles = [
19
- // start
20
- ] as Point[]
21
-
22
- while (hasAtLeast(bezierPoints, 3)) {
23
- const [cp1, cp2, end, ...rest] = bezierPoints
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])
33
- })
34
- bezierPoints = rest
35
- start = end
36
- }
37
- invariant(bezierPoints.length === 0, 'all points should be consumed')
38
-
39
- return handles
40
- }
41
-
42
5
  // const nodeZIndex = (node: DiagramNode) => node.level + (hasAtLeast(node.children, 1) ? 2 : 1)
43
6
  const nodeZIndex = (node: DiagramNode) => node.level + 1
44
7
 
45
8
  export function diagramViewToXYFlowData(
46
- view: Pick<DiagramView, 'nodes' | 'edges' | '__'>,
9
+ view: Pick<DiagramView, 'id' | 'nodes' | 'edges' | '__' | 'manualLayout'>,
47
10
  opts: {
48
11
  draggable: boolean
49
12
  selectable: boolean
@@ -147,7 +110,7 @@ export function diagramViewToXYFlowData(
147
110
  const target = edge.target
148
111
  const id = ns + edge.id
149
112
  // const points = isElkEdge(edge) ? edge.points : deriveEdgePoints(edge.points)
150
- const controlPoints = deriveEdgePoints(edge.points)
113
+ // const controlPoints = deriveEdgePoints(edge.points)
151
114
  // if (edge.tailArrowPoint) {
152
115
  // controlPoints.unshift([...edge.tailArrowPoint])
153
116
  // }
@@ -169,7 +132,7 @@ export function diagramViewToXYFlowData(
169
132
  data: {
170
133
  edge,
171
134
  type: 'bezier',
172
- controlPoints,
135
+ controlPoints: view.manualLayout?.edges[edge.id]?.controlPoints || null,
173
136
  headPoint: edge.headArrowPoint ?? null,
174
137
  tailPoint: edge.tailArrowPoint ?? null,
175
138
  stepNum: isDynamicView ? extractStep(edge.id) : null,