@likec4/diagram 1.6.1 → 1.7.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 (49) hide show
  1. package/dist/LikeC4Diagram.d.ts +1 -1
  2. package/dist/LikeC4Diagram.props.d.ts +10 -11
  3. package/dist/hooks/useUpdateEffect.d.ts +1 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.mjs +572 -464
  6. package/dist/state/DiagramContext.d.ts +746 -181
  7. package/dist/state/diagram-to-xyflow.d.ts +10 -0
  8. package/dist/state/diagramStore.d.ts +782 -202
  9. package/dist/style.css +273 -275
  10. package/dist/types.d.ts +20 -0
  11. package/dist/xyflow/EdgeMarkers.d.ts +1 -0
  12. package/dist/xyflow/FitviewOnDiagramChange.d.ts +1 -1
  13. package/dist/xyflow/XYFlow.d.ts +1 -4
  14. package/dist/xyflow/XYFlowEvents.d.ts +41 -12
  15. package/dist/xyflow/XYFlowInner.d.ts +0 -1
  16. package/dist/xyflow/edges/RelationshipEdge.d.ts +3 -3
  17. package/dist/xyflow/hooks/index.d.ts +0 -1
  18. package/dist/xyflow/hooks/useXYFlow.d.ts +27 -27
  19. package/dist/xyflow/types.d.ts +11 -19
  20. package/dist/xyflow/utils.d.ts +1 -0
  21. package/package.json +6 -6
  22. package/src/LikeC4Diagram.props.ts +17 -13
  23. package/src/LikeC4Diagram.tsx +62 -96
  24. package/src/globals.css.ts +10 -6
  25. package/src/hooks/useUpdateEffect.ts +16 -1
  26. package/src/index.ts +1 -0
  27. package/src/state/DiagramContext.tsx +19 -27
  28. package/src/{xyflow → state}/diagram-to-xyflow.ts +19 -18
  29. package/src/state/diagramStore.ts +404 -192
  30. package/src/types.ts +22 -0
  31. package/src/ui/BackwardForwardPanel.tsx +24 -7
  32. package/src/xyflow/EdgeMarkers.tsx +2 -0
  33. package/src/xyflow/FitviewOnDiagramChange.tsx +42 -75
  34. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +1 -1
  35. package/src/xyflow/XYFlow.tsx +57 -32
  36. package/src/xyflow/XYFlowEvents.tsx +58 -10
  37. package/src/xyflow/XYFlowInner.tsx +8 -8
  38. package/src/xyflow/edges/RelationshipEdge.tsx +43 -29
  39. package/src/xyflow/edges/edges.css.ts +3 -3
  40. package/src/xyflow/edges/utils.ts +0 -1
  41. package/src/xyflow/hooks/index.ts +0 -1
  42. package/src/xyflow/hooks/useLayoutConstraints.ts +42 -65
  43. package/src/xyflow/nodes/element/ElementNode.tsx +0 -4
  44. package/src/xyflow/nodes/element/element.css.ts +8 -6
  45. package/src/xyflow/types.ts +14 -32
  46. package/src/xyflow/utils.ts +4 -0
  47. package/dist/xyflow/SyncWithDiagram.d.ts +0 -4
  48. package/dist/xyflow/diagram-to-xyflow.d.ts +0 -7
  49. package/src/xyflow/SyncWithDiagram.tsx +0 -85
@@ -2,18 +2,16 @@ import { ReactFlowProvider as XYFlowProvider } from '@xyflow/react'
2
2
  import clsx from 'clsx'
3
3
  import { shallowEqual } from 'fast-equals'
4
4
  import { domAnimation, LazyMotion } from 'framer-motion'
5
- import { memo, useEffect, useRef, useState } from 'react'
5
+ import { memo, useRef } from 'react'
6
6
  import { rootClassName } from './globals.css'
7
7
  import * as css from './LikeC4Diagram.css'
8
8
  import { type LikeC4DiagramEventHandlers, type LikeC4DiagramProperties } from './LikeC4Diagram.props'
9
9
  import { EnsureMantine } from './mantine/EnsureMantine'
10
10
  import { DiagramContextProvider } from './state/DiagramContext'
11
- import { useDiagramStoreApi } from './state/useDiagramStore'
12
- import { diagramViewToXYFlowData } from './xyflow/diagram-to-xyflow'
11
+ import { useDiagramState } from './state/useDiagramStore'
13
12
  import { FitViewOnDiagramChange } from './xyflow/FitviewOnDiagramChange'
14
13
  import { SelectEdgesOnNodeFocus } from './xyflow/SelectEdgesOnNodeFocus'
15
- import { SyncWithDiagram } from './xyflow/SyncWithDiagram'
16
- import type { XYFlowData, XYFlowEdge, XYFlowNode } from './xyflow/types'
14
+ import type { XYFlowEdge, XYFlowNode } from './xyflow/types'
17
15
  import { XYFlow } from './xyflow/XYFlow'
18
16
  import { XYFlowInner } from './xyflow/XYFlowInner'
19
17
 
@@ -33,7 +31,8 @@ export function LikeC4Diagram({
33
31
  showElementLinks = true,
34
32
  showDiagramTitle = true,
35
33
  showNavigationButtons = false,
36
- enableDynamicViewWalkthrough = true,
34
+ enableDynamicViewWalkthrough = false,
35
+ enableFocusMode = readonly,
37
36
  initialWidth,
38
37
  initialHeight,
39
38
  keepAspectRatio = false,
@@ -47,122 +46,91 @@ export function LikeC4Diagram({
47
46
  onNavigateTo,
48
47
  onNodeClick,
49
48
  onNodeContextMenu,
49
+ onEditorCommand,
50
50
  renderIcon
51
51
  }: LikeC4DiagramProps) {
52
- const initialRef = useRef<{
53
- defaultNodes: XYFlowData['nodes']
54
- defaultEdges: XYFlowData['edges']
55
- initialWidth: number
56
- initialHeight: number
57
- }>()
58
- if (!initialRef.current) {
59
- const initial = diagramViewToXYFlowData(view, {
60
- selectable: nodesSelectable,
61
- draggable: nodesDraggable
62
- })
63
- initialRef.current = {
64
- defaultNodes: initial.nodes,
65
- defaultEdges: initial.edges,
66
- initialWidth: initialWidth ?? view.width,
67
- initialHeight: initialHeight ?? view.height
68
- }
69
- }
70
-
52
+ const initialRef = useRef({
53
+ defaultNodes: [] as XYFlowNode[],
54
+ defaultEdges: [] as XYFlowEdge[],
55
+ initialWidth: initialWidth ?? view.bounds.width,
56
+ initialHeight: initialHeight ?? view.bounds.height
57
+ })
58
+ // useLogger('LikeC4Diagram', [{view}])
71
59
  return (
72
60
  <EnsureMantine>
73
- <XYFlowProvider
74
- fitView={fitView}
75
- {...initialRef.current}
76
- >
77
- <DiagramContextProvider
78
- view={view}
79
- keepAspectRatio={keepAspectRatio}
80
- className={clsx(rootClassName, className)}
81
- readonly={readonly}
82
- pannable={pannable}
83
- zoomable={zoomable}
84
- fitViewEnabled={fitView}
85
- fitViewPadding={fitViewPadding}
86
- showElementLinks={showElementLinks}
87
- nodesDraggable={nodesDraggable}
88
- nodesSelectable={nodesSelectable}
89
- experimentalEdgeEditing={experimentalEdgeEditing}
90
- renderIcon={renderIcon ?? null}
91
- onCanvasClick={onCanvasClick}
92
- onCanvasContextMenu={onCanvasContextMenu}
93
- onEdgeClick={onEdgeClick}
94
- onEdgeContextMenu={onEdgeContextMenu}
95
- onNodeClick={onNodeClick}
96
- onNodeContextMenu={onNodeContextMenu}
97
- onChange={onChange}
98
- onNavigateTo={onNavigateTo}
99
- onCanvasDblClick={onCanvasDblClick}
61
+ <LazyMotion features={domAnimation} strict>
62
+ <XYFlowProvider
63
+ fitView={fitView}
64
+ {...initialRef.current}
100
65
  >
101
- <LazyMotion features={domAnimation} strict>
66
+ <DiagramContextProvider
67
+ view={view}
68
+ keepAspectRatio={keepAspectRatio}
69
+ className={clsx(rootClassName, className)}
70
+ readonly={readonly}
71
+ pannable={pannable}
72
+ zoomable={zoomable}
73
+ fitViewEnabled={fitView}
74
+ fitViewPadding={fitViewPadding}
75
+ showElementLinks={showElementLinks}
76
+ nodesDraggable={nodesDraggable}
77
+ nodesSelectable={nodesSelectable}
78
+ experimentalEdgeEditing={experimentalEdgeEditing}
79
+ enableDynamicViewWalkthrough={enableDynamicViewWalkthrough}
80
+ enableFocusMode={enableFocusMode}
81
+ renderIcon={renderIcon ?? null}
82
+ onCanvasClick={onCanvasClick ?? null}
83
+ onCanvasContextMenu={onCanvasContextMenu ?? null}
84
+ onEdgeClick={onEdgeClick ?? null}
85
+ onEdgeContextMenu={onEdgeContextMenu ?? null}
86
+ onNodeClick={onNodeClick ?? null}
87
+ onNodeContextMenu={onNodeContextMenu ?? null}
88
+ onChange={onChange ?? null}
89
+ onNavigateTo={onNavigateTo ?? null}
90
+ onCanvasDblClick={onCanvasDblClick ?? null}
91
+ onEditorCommand={onEditorCommand ?? null}
92
+ >
102
93
  <LikeC4DiagramInnerMemo
103
- defaultNodes={initialRef.current.defaultNodes}
104
- defaultEdges={initialRef.current.defaultEdges}
105
- fitView={fitView}
106
- zoomable={zoomable}
107
- background={background}
108
94
  controls={controls}
109
- pannable={pannable}
95
+ background={background}
110
96
  showDiagramTitle={showDiagramTitle}
111
97
  showNavigationButtons={showNavigationButtons}
112
- enableDynamicViewWalkthrough={enableDynamicViewWalkthrough}
113
98
  />
114
- </LazyMotion>
115
- </DiagramContextProvider>
116
- </XYFlowProvider>
99
+ </DiagramContextProvider>
100
+ </XYFlowProvider>
101
+ </LazyMotion>
117
102
  </EnsureMantine>
118
103
  )
119
104
  }
120
105
 
121
106
  type LikeC4DiagramInnerProps = {
122
107
  background: NonNullable<LikeC4DiagramProperties['background']>
123
- fitView: boolean
124
- zoomable: boolean
125
- pannable: boolean
126
108
  controls: boolean
127
- defaultNodes: XYFlowNode[]
128
- defaultEdges: XYFlowEdge[]
129
109
  showDiagramTitle: boolean
130
110
  showNavigationButtons: boolean
131
- enableDynamicViewWalkthrough: boolean
132
111
  }
133
112
  const LikeC4DiagramInnerMemo = memo<LikeC4DiagramInnerProps>(function LikeC4DiagramInner({
134
113
  background,
135
- fitView,
136
- zoomable,
137
114
  controls,
138
- pannable,
139
- defaultNodes,
140
- defaultEdges,
141
115
  showDiagramTitle,
142
- showNavigationButtons,
143
- enableDynamicViewWalkthrough
116
+ showNavigationButtons
144
117
  }) {
145
- const diagramApi = useDiagramStoreApi()
146
- const [isInitialized, setIsInitialized] = useState(diagramApi.getState().initialized)
147
-
148
- useEffect(() => {
149
- if (isInitialized) {
150
- return
151
- }
152
- return diagramApi.subscribe(
153
- s => s.initialized,
154
- setIsInitialized,
155
- {
156
- fireImmediately: true
157
- }
158
- )
159
- }, [isInitialized])
118
+ const {
119
+ isInitialized,
120
+ pannable,
121
+ fitView,
122
+ enableFocusMode
123
+ } = useDiagramState(s => ({
124
+ isInitialized: s.initialized,
125
+ zoomable: s.zoomable,
126
+ pannable: s.pannable,
127
+ fitView: s.fitViewEnabled,
128
+ enableFocusMode: s.enableFocusMode
129
+ }))
160
130
 
161
131
  return (
162
132
  <>
163
133
  <XYFlow
164
- defaultNodes={defaultNodes}
165
- defaultEdges={defaultEdges}
166
134
  className={clsx(
167
135
  'likec4-diagram',
168
136
  css.cssReactFlow,
@@ -174,16 +142,14 @@ const LikeC4DiagramInnerMemo = memo<LikeC4DiagramInnerProps>(function LikeC4Diag
174
142
  <XYFlowInner
175
143
  showNavigationButtons={showNavigationButtons}
176
144
  showDiagramTitle={showDiagramTitle}
177
- enableDynamicViewWalkthrough={enableDynamicViewWalkthrough}
178
145
  background={background}
179
146
  controls={controls}
180
147
  />
181
148
  </XYFlow>
182
149
  {isInitialized && (
183
150
  <>
184
- <SyncWithDiagram />
185
151
  {fitView && <FitViewOnDiagramChange />}
186
- {fitView && zoomable && <SelectEdgesOnNodeFocus />}
152
+ {enableFocusMode && <SelectEdgesOnNodeFocus />}
187
153
  </>
188
154
  )}
189
155
  </>
@@ -128,18 +128,22 @@ for (const color of keys(defaultTheme.elements)) {
128
128
  }
129
129
 
130
130
  for (let depth = 1; depth <= 6; depth++) {
131
- createGlobalTheme(`:where([data-likec4-color='${color}'][data-compound-depth='${depth}'])`, compounds, {
132
- fill: compoundDarkColor(defaultTheme.elements[color].fill, depth),
133
- stroke: compoundDarkColor(defaultTheme.elements[color].stroke, depth)
134
- })
135
-
136
131
  createGlobalTheme(
137
- `:where([data-mantine-color-scheme='light']) :where([data-likec4-color='${color}'][data-compound-depth='${depth}'])`,
132
+ `:where([data-likec4-color='${color}'][data-compound-depth='${depth}'])`,
138
133
  compounds,
139
134
  {
140
135
  fill: compoundLightColor(defaultTheme.elements[color].fill, depth),
141
136
  stroke: compoundLightColor(defaultTheme.elements[color].stroke, depth)
142
137
  }
143
138
  )
139
+
140
+ createGlobalTheme(
141
+ `:where([data-mantine-color-scheme='dark']) :where([data-likec4-color='${color}'][data-compound-depth='${depth}'])`,
142
+ compounds,
143
+ {
144
+ fill: compoundDarkColor(defaultTheme.elements[color].fill, depth),
145
+ stroke: compoundDarkColor(defaultTheme.elements[color].stroke, depth)
146
+ }
147
+ )
144
148
  }
145
149
  }
@@ -9,6 +9,21 @@ import { type DependencyList, type EffectCallback } from 'react'
9
9
 
10
10
  const noop = () => {}
11
11
 
12
+ export const depsShallowEqual: DependenciesComparator = (d1, d2) => {
13
+ if (d1 === d2) {
14
+ return true
15
+ }
16
+ if (d1.length !== d2.length) {
17
+ return false
18
+ }
19
+ for (const [i, element] of d1.entries()) {
20
+ if (!shallowEqual(element, d2[i])) {
21
+ return false
22
+ }
23
+ }
24
+ return true
25
+ }
26
+
12
27
  export function useUpdateEffect<
13
28
  Callback extends EffectCallback = EffectCallback,
14
29
  Deps extends DependencyList = DependencyList
@@ -22,7 +37,7 @@ export function useUpdateEffect<
22
37
  useCustomCompareEffect(
23
38
  isFirstMount ? noop as Callback : callback,
24
39
  deps,
25
- equalityFn ?? shallowEqual,
40
+ equalityFn ?? depsShallowEqual,
26
41
  effectHook
27
42
  )
28
43
  }
package/src/index.ts CHANGED
@@ -7,4 +7,5 @@ export { useSetState, useUpdateEffect } from './hooks'
7
7
  export type * from './LikeC4Diagram'
8
8
  export type * from './LikeC4Diagram.props'
9
9
  export type * from './StaticLikeC4Diagram'
10
+ export type * from './types'
10
11
  export type * from './xyflow/types'
@@ -1,6 +1,5 @@
1
- import { shallowEqual } from 'fast-equals'
2
- import { createContext, type PropsWithChildren, useEffect, useRef } from 'react'
3
- import { hasSubObject, isNonNullish, pickBy } from 'remeda'
1
+ import { deepEqual, shallowEqual } from 'fast-equals'
2
+ import { createContext, type PropsWithChildren, useCallback, useEffect, useRef } from 'react'
4
3
  import { useUpdateEffect } from '../hooks'
5
4
  import { useXYFlow, useXYStoreApi } from '../xyflow/hooks/useXYFlow'
6
5
  import { createDiagramStore, type DiagramInitialState } from './diagramStore'
@@ -9,7 +8,7 @@ export type DiagramZustandStore = ReturnType<typeof createDiagramStore>
9
8
  export const DiagramContext = createContext<DiagramZustandStore | null>(null)
10
9
 
11
10
  export type DiagramContextProviderProps = PropsWithChildren<
12
- DiagramInitialState & {
11
+ Omit<DiagramInitialState, 'xystore' | 'xyflow' | 'getContainer'> & {
13
12
  className: string
14
13
  keepAspectRatio: boolean
15
14
  }
@@ -27,50 +26,43 @@ export function DiagramContextProvider({
27
26
  const xyflow = useXYFlow()
28
27
  const store = useRef<DiagramZustandStore>()
29
28
 
29
+ const getContainer = useCallback(() => containerRef.current, [containerRef])
30
+
30
31
  if (!store.current) {
31
32
  store.current = createDiagramStore({
32
33
  xystore,
33
34
  xyflow,
34
35
  view,
35
- getContainer: () => containerRef.current,
36
+ getContainer,
36
37
  ...props
37
38
  })
38
39
  }
39
40
 
40
- useEffect(
41
- () => {
42
- if (!store.current) return
43
- const state = store.current.getState()
44
- if (state.xyflow !== xyflow || state.xystore !== xystore) {
45
- store.current.setState({ xyflow, xystore }, false, 'update xyflow and xystore')
46
- }
47
- },
48
- [xyflow, xystore]
41
+ useUpdateEffect(
42
+ () => store.current?.setState({ xyflow, xystore, getContainer }, false, 'update xyflow and xystore'),
43
+ [xyflow, xystore, getContainer]
44
+ )
45
+
46
+ useUpdateEffect(
47
+ () => store.current?.setState(props, false, 'update incoming props'),
48
+ [props]
49
49
  )
50
50
 
51
51
  useUpdateEffect(
52
52
  () => {
53
- if (!store.current) return
54
- const state = store.current.getState()
55
- const newProps = pickBy(props, isNonNullish)
56
- if (!hasSubObject(state, newProps)) {
57
- store.current.setState(newProps, false, 'update incoming props')
58
- }
59
- if (state.view !== view) {
60
- state.updateView(view)
61
- }
53
+ store.current?.getState().updateView(view)
62
54
  },
63
- [view, props],
64
- (prev, next) => shallowEqual(prev[0], next[0]) && shallowEqual(prev[1], next[1])
55
+ [view]
65
56
  )
57
+
66
58
  return (
67
59
  <div
68
60
  ref={containerRef}
69
61
  className={className}
70
62
  {...(keepAspectRatio && {
71
63
  style: {
72
- aspectRatio: `${Math.ceil(view.width)}/${Math.ceil(view.height)}`,
73
- maxHeight: Math.ceil(view.height)
64
+ aspectRatio: `${Math.ceil(view.bounds.width)}/${Math.ceil(view.bounds.height)}`,
65
+ maxHeight: Math.ceil(view.bounds.height)
74
66
  }
75
67
  })}>
76
68
  <DiagramContext.Provider value={store.current}>
@@ -1,25 +1,25 @@
1
1
  import { extractStep, invariant, nonNullable } from '@likec4/core'
2
2
  import type { DiagramNode, DiagramView, Fqn } from '@likec4/core/types'
3
3
  import { hasAtLeast } from 'remeda'
4
- import type { XYFlowData } from '../xyflow/types'
5
- import { ZIndexes } from './const'
4
+ import { ZIndexes } from '../xyflow/const'
5
+ import type { XYFlowEdge, XYFlowNode } from '../xyflow/types'
6
6
 
7
7
  // const nodeZIndex = (node: DiagramNode) => node.level - (node.children.length > 0 ? 1 : 0)
8
8
 
9
9
  export function diagramViewToXYFlowData(
10
- view: Pick<DiagramView, 'id' | 'nodes' | 'edges' | '__' | 'manualLayout'>,
10
+ view: Pick<DiagramView, 'id' | 'nodes' | 'edges' | '__'>,
11
11
  opts: {
12
12
  draggable: boolean
13
13
  selectable: boolean
14
14
  }
15
- ): XYFlowData {
16
- const isDynamicView = view.__ === 'dynamic'
17
- const editor: XYFlowData = {
18
- nodes: [],
19
- edges: []
20
- }
21
-
22
- const nodeLookup = new Map<Fqn, DiagramNode>()
15
+ ): {
16
+ xynodes: XYFlowNode[]
17
+ xyedges: XYFlowEdge[]
18
+ } {
19
+ const isDynamicView = view.__ === 'dynamic',
20
+ xynodes = [] as XYFlowNode[],
21
+ xyedges = [] as XYFlowEdge[],
22
+ nodeLookup = new Map<Fqn, DiagramNode>()
23
23
 
24
24
  const traverse = view.nodes.reduce(
25
25
  (acc, node) => {
@@ -60,7 +60,7 @@ export function diagramViewToXYFlowData(
60
60
 
61
61
  const id = ns + node.id
62
62
  const draggable = opts.draggable && (!parent || parent.children.length > 1)
63
- editor.nodes.push({
63
+ xynodes.push({
64
64
  id,
65
65
  type: isCompound ? 'compound' : 'element',
66
66
  data: {
@@ -73,10 +73,9 @@ export function diagramViewToXYFlowData(
73
73
  position,
74
74
  zIndex: isCompound ? ZIndexes.Compound : ZIndexes.Element,
75
75
  hidden: false,
76
- initialWidth: node.width,
77
- initialHeight: node.height,
78
76
  width: node.width,
79
77
  height: node.height,
78
+ // parentId: parent ? ns + parent.id : null,
80
79
  ...(parent && {
81
80
  parentId: ns + parent.id
82
81
  }),
@@ -103,7 +102,7 @@ export function diagramViewToXYFlowData(
103
102
 
104
103
  // const level = Math.max(nodeZIndex(nodeById(source)), nodeZIndex(nodeById(target)))
105
104
 
106
- editor.edges.push({
105
+ xyedges.push({
107
106
  id,
108
107
  type: 'relationship',
109
108
  source: ns + source,
@@ -113,8 +112,7 @@ export function diagramViewToXYFlowData(
113
112
  deletable: false,
114
113
  data: {
115
114
  edge,
116
- type: 'bezier',
117
- controlPoints: view.manualLayout?.edges[edge.id]?.controlPoints || null,
115
+ controlPoints: edge.controlPoints || null,
118
116
  stepNum: isDynamicView ? extractStep(edge.id) : null,
119
117
  label: !!edge.labelBBox
120
118
  ? {
@@ -127,5 +125,8 @@ export function diagramViewToXYFlowData(
127
125
  })
128
126
  }
129
127
 
130
- return editor
128
+ return {
129
+ xynodes,
130
+ xyedges
131
+ }
131
132
  }