@likec4/diagram 1.6.0 → 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 +2 -0
  5. package/dist/index.mjs +769 -8587
  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 +11 -11
  22. package/src/LikeC4Diagram.props.ts +17 -13
  23. package/src/LikeC4Diagram.tsx +67 -106
  24. package/src/globals.css.ts +10 -6
  25. package/src/hooks/useUpdateEffect.ts +16 -1
  26. package/src/index.ts +3 -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 -35
  36. package/src/xyflow/XYFlowEvents.tsx +58 -10
  37. package/src/xyflow/XYFlowInner.tsx +9 -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 +8 -11
  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
@@ -1,22 +1,17 @@
1
1
  import { ReactFlowProvider as XYFlowProvider } from '@xyflow/react'
2
- import '@xyflow/react/dist/style.css'
3
2
  import clsx from 'clsx'
4
- import { DEV } from 'esm-env'
5
3
  import { shallowEqual } from 'fast-equals'
6
4
  import { domAnimation, LazyMotion } from 'framer-motion'
7
- import { memo, useEffect, useRef, useState } from 'react'
8
- import useTilg from 'tilg'
5
+ import { memo, useRef } from 'react'
9
6
  import { rootClassName } from './globals.css'
10
- import { cssDisablePan, cssNoControls, cssReactFlow, cssTransparentBg } from './LikeC4Diagram.css'
7
+ import * as css from './LikeC4Diagram.css'
11
8
  import { type LikeC4DiagramEventHandlers, type LikeC4DiagramProperties } from './LikeC4Diagram.props'
12
9
  import { EnsureMantine } from './mantine/EnsureMantine'
13
10
  import { DiagramContextProvider } from './state/DiagramContext'
14
- import { useDiagramStoreApi } from './state/useDiagramStore'
15
- import { diagramViewToXYFlowData } from './xyflow/diagram-to-xyflow'
11
+ import { useDiagramState } from './state/useDiagramStore'
16
12
  import { FitViewOnDiagramChange } from './xyflow/FitviewOnDiagramChange'
17
13
  import { SelectEdgesOnNodeFocus } from './xyflow/SelectEdgesOnNodeFocus'
18
- import { SyncWithDiagram } from './xyflow/SyncWithDiagram'
19
- import type { XYFlowData, XYFlowEdge, XYFlowNode } from './xyflow/types'
14
+ import type { XYFlowEdge, XYFlowNode } from './xyflow/types'
20
15
  import { XYFlow } from './xyflow/XYFlow'
21
16
  import { XYFlowInner } from './xyflow/XYFlowInner'
22
17
 
@@ -36,7 +31,8 @@ export function LikeC4Diagram({
36
31
  showElementLinks = true,
37
32
  showDiagramTitle = true,
38
33
  showNavigationButtons = false,
39
- enableDynamicViewWalkthrough = true,
34
+ enableDynamicViewWalkthrough = false,
35
+ enableFocusMode = readonly,
40
36
  initialWidth,
41
37
  initialHeight,
42
38
  keepAspectRatio = false,
@@ -50,145 +46,110 @@ export function LikeC4Diagram({
50
46
  onNavigateTo,
51
47
  onNodeClick,
52
48
  onNodeContextMenu,
49
+ onEditorCommand,
53
50
  renderIcon
54
51
  }: LikeC4DiagramProps) {
55
- DEV && useTilg()
56
- const initialRef = useRef<{
57
- defaultNodes: XYFlowData['nodes']
58
- defaultEdges: XYFlowData['edges']
59
- initialWidth: number
60
- initialHeight: number
61
- }>()
62
- if (!initialRef.current) {
63
- const initial = diagramViewToXYFlowData(view, {
64
- selectable: nodesSelectable,
65
- draggable: nodesDraggable
66
- })
67
- initialRef.current = {
68
- defaultNodes: initial.nodes,
69
- defaultEdges: initial.edges,
70
- initialWidth: initialWidth ?? view.width,
71
- initialHeight: initialHeight ?? view.height
72
- }
73
- }
74
-
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}])
75
59
  return (
76
60
  <EnsureMantine>
77
- <XYFlowProvider
78
- fitView={fitView}
79
- {...initialRef.current}
80
- >
81
- <DiagramContextProvider
82
- view={view}
83
- keepAspectRatio={keepAspectRatio}
84
- className={clsx(rootClassName, className)}
85
- readonly={readonly}
86
- pannable={pannable}
87
- zoomable={zoomable}
88
- fitViewEnabled={fitView}
89
- fitViewPadding={fitViewPadding}
90
- showElementLinks={showElementLinks}
91
- nodesDraggable={nodesDraggable}
92
- nodesSelectable={nodesSelectable}
93
- experimentalEdgeEditing={experimentalEdgeEditing}
94
- renderIcon={renderIcon ?? null}
95
- onCanvasClick={onCanvasClick}
96
- onCanvasContextMenu={onCanvasContextMenu}
97
- onEdgeClick={onEdgeClick}
98
- onEdgeContextMenu={onEdgeContextMenu}
99
- onNodeClick={onNodeClick}
100
- onNodeContextMenu={onNodeContextMenu}
101
- onChange={onChange}
102
- onNavigateTo={onNavigateTo}
103
- onCanvasDblClick={onCanvasDblClick}
61
+ <LazyMotion features={domAnimation} strict>
62
+ <XYFlowProvider
63
+ fitView={fitView}
64
+ {...initialRef.current}
104
65
  >
105
- <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
+ >
106
93
  <LikeC4DiagramInnerMemo
107
- defaultNodes={initialRef.current.defaultNodes}
108
- defaultEdges={initialRef.current.defaultEdges}
109
- fitView={fitView}
110
- zoomable={zoomable}
111
- background={background}
112
94
  controls={controls}
113
- pannable={pannable}
95
+ background={background}
114
96
  showDiagramTitle={showDiagramTitle}
115
97
  showNavigationButtons={showNavigationButtons}
116
- enableDynamicViewWalkthrough={enableDynamicViewWalkthrough}
117
98
  />
118
- </LazyMotion>
119
- </DiagramContextProvider>
120
- </XYFlowProvider>
99
+ </DiagramContextProvider>
100
+ </XYFlowProvider>
101
+ </LazyMotion>
121
102
  </EnsureMantine>
122
103
  )
123
104
  }
124
105
 
125
106
  type LikeC4DiagramInnerProps = {
126
107
  background: NonNullable<LikeC4DiagramProperties['background']>
127
- fitView: boolean
128
- zoomable: boolean
129
- pannable: boolean
130
108
  controls: boolean
131
- defaultNodes: XYFlowNode[]
132
- defaultEdges: XYFlowEdge[]
133
109
  showDiagramTitle: boolean
134
110
  showNavigationButtons: boolean
135
- enableDynamicViewWalkthrough: boolean
136
111
  }
137
112
  const LikeC4DiagramInnerMemo = memo<LikeC4DiagramInnerProps>(function LikeC4DiagramInner({
138
113
  background,
139
- fitView,
140
- zoomable,
141
114
  controls,
142
- pannable,
143
- defaultNodes,
144
- defaultEdges,
145
115
  showDiagramTitle,
146
- showNavigationButtons,
147
- enableDynamicViewWalkthrough
116
+ showNavigationButtons
148
117
  }) {
149
- DEV && useTilg()
150
- const diagramApi = useDiagramStoreApi()
151
- const [isInitialized, setIsInitialized] = useState(diagramApi.getState().initialized)
152
-
153
- useEffect(() => {
154
- if (isInitialized) {
155
- return
156
- }
157
- return diagramApi.subscribe(
158
- s => s.initialized,
159
- setIsInitialized,
160
- {
161
- fireImmediately: true
162
- }
163
- )
164
- }, [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
+ }))
165
130
 
166
131
  return (
167
132
  <>
168
133
  <XYFlow
169
- defaultNodes={defaultNodes}
170
- defaultEdges={defaultEdges}
171
134
  className={clsx(
172
135
  'likec4-diagram',
173
- cssReactFlow,
174
- controls === false && cssNoControls,
175
- pannable !== true && cssDisablePan,
176
- background === 'transparent' && cssTransparentBg
136
+ css.cssReactFlow,
137
+ controls === false && css.cssNoControls,
138
+ pannable !== true && css.cssDisablePan,
139
+ background === 'transparent' && css.cssTransparentBg
177
140
  )}
178
141
  >
179
142
  <XYFlowInner
180
143
  showNavigationButtons={showNavigationButtons}
181
144
  showDiagramTitle={showDiagramTitle}
182
- enableDynamicViewWalkthrough={enableDynamicViewWalkthrough}
183
145
  background={background}
184
146
  controls={controls}
185
147
  />
186
148
  </XYFlow>
187
149
  {isInitialized && (
188
150
  <>
189
- <SyncWithDiagram />
190
151
  {fitView && <FitViewOnDiagramChange />}
191
- {fitView && zoomable && <SelectEdgesOnNodeFocus />}
152
+ {enableFocusMode && <SelectEdgesOnNodeFocus />}
192
153
  </>
193
154
  )}
194
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
@@ -1,3 +1,5 @@
1
+ import '@xyflow/react/dist/style.css'
2
+
1
3
  export { LikeC4Diagram } from './LikeC4Diagram'
2
4
  export { StaticLikeC4Diagram } from './StaticLikeC4Diagram'
3
5
 
@@ -5,4 +7,5 @@ export { useSetState, useUpdateEffect } from './hooks'
5
7
  export type * from './LikeC4Diagram'
6
8
  export type * from './LikeC4Diagram.props'
7
9
  export type * from './StaticLikeC4Diagram'
10
+ export type * from './types'
8
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
- if (state.view !== view) {
56
- state.updateView(view)
57
- }
58
- const newProps = pickBy(props, isNonNullish)
59
- if (!hasSubObject(state, newProps)) {
60
- store.current.setState(newProps, false, 'update incoming props')
61
- }
53
+ store.current?.getState().updateView(view)
62
54
  },
63
- [view, props],
64
- shallowEqual
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
  }