@likec4/diagram 1.0.0-next.2 → 1.0.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 (124) hide show
  1. package/dist/LikeC4Diagram.css.d.ts +0 -1
  2. package/dist/LikeC4Diagram.d.ts +1 -2
  3. package/dist/LikeC4Diagram.props.d.ts +24 -25
  4. package/dist/StaticLikeC4Diagram.d.ts +1 -2
  5. package/dist/globals.css.d.ts +1 -0
  6. package/dist/hooks/index.d.ts +2 -3
  7. package/dist/hooks/useSetState.d.ts +3 -0
  8. package/dist/hooks/useUpdateEffect.d.ts +4 -0
  9. package/dist/icons/Clipboard.d.ts +0 -1
  10. package/dist/icons/Link.d.ts +0 -1
  11. package/dist/icons/ZoomIn.d.ts +0 -1
  12. package/dist/icons/index.d.ts +0 -1
  13. package/dist/index.d.ts +1 -4
  14. package/dist/index.mjs +9388 -0
  15. package/dist/mantine/EnsureMantine.d.ts +2 -4
  16. package/dist/mantine/theme.d.ts +3 -0
  17. package/dist/mantine.css.d.ts +1 -0
  18. package/dist/state/DiagramContext.d.ts +386 -0
  19. package/dist/state/WhenInitialized.d.ts +3 -0
  20. package/dist/state/diagramStore.d.ts +428 -0
  21. package/dist/state/index.d.ts +4 -2
  22. package/dist/state/useDiagramStore.d.ts +387 -0
  23. package/dist/style.css +1212 -1
  24. package/dist/theme.css.d.ts +16 -2
  25. package/dist/ui/Camera.d.ts +0 -1
  26. package/dist/ui/KeepAspectRatio.css.d.ts +0 -1
  27. package/dist/ui/KeepAspectRatio.d.ts +0 -1
  28. package/dist/ui/OptionsPanel.css.d.ts +0 -1
  29. package/dist/ui/OptionsPanel.d.ts +0 -1
  30. package/dist/ui/index.d.ts +0 -1
  31. package/dist/ui/options/NodeOptions.d.ts +0 -1
  32. package/dist/xyflow/FitviewOnDiagramChange.d.ts +1 -5
  33. package/dist/xyflow/SyncWithDiagram.d.ts +5 -0
  34. package/dist/xyflow/XYFlowBackground.d.ts +0 -1
  35. package/dist/xyflow/XYFlowEvents.d.ts +5 -15
  36. package/dist/xyflow/XYFlowWrapper.d.ts +4 -5
  37. package/dist/xyflow/const.d.ts +1 -2
  38. package/dist/xyflow/diagram-to-xyflow.d.ts +4 -2
  39. package/dist/xyflow/edges/RelationshipEdge.d.ts +7 -7
  40. package/dist/xyflow/edges/edges.css.d.ts +2 -1
  41. package/dist/xyflow/edges/utils.d.ts +3 -4
  42. package/dist/xyflow/hooks/index.d.ts +2 -37
  43. package/dist/xyflow/hooks/{use-layout-/321/201onstraints.d.ts → useLayoutConstraints.d.ts} +2 -4
  44. package/dist/xyflow/hooks/useXYFlow.d.ts +266 -0
  45. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +0 -1
  46. package/dist/xyflow/nodes/compound/CompoundNode.d.ts +0 -1
  47. package/dist/xyflow/nodes/compound/index.d.ts +0 -1
  48. package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -2
  49. package/dist/xyflow/nodes/element/ElementLink.css.d.ts +0 -1
  50. package/dist/xyflow/nodes/element/ElementLink.d.ts +1 -2
  51. package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -3
  52. package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +2 -3
  53. package/dist/xyflow/nodes/element/element.css.d.ts +0 -1
  54. package/dist/xyflow/nodes/element/index.d.ts +1 -2
  55. package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +0 -1
  56. package/dist/xyflow/types.d.ts +20 -8
  57. package/dist/xyflow/utils.d.ts +0 -1
  58. package/package.json +36 -34
  59. package/src/LikeC4Diagram.css.ts +31 -0
  60. package/src/LikeC4Diagram.props.ts +154 -0
  61. package/src/LikeC4Diagram.tsx +119 -0
  62. package/src/StaticLikeC4Diagram.tsx +39 -0
  63. package/src/globals.css.ts +128 -0
  64. package/src/hooks/index.ts +2 -0
  65. package/src/hooks/useSetState.ts +19 -0
  66. package/src/hooks/useUpdateEffect.ts +28 -0
  67. package/src/icons/Clipboard.tsx +24 -0
  68. package/src/icons/Link.tsx +23 -0
  69. package/src/icons/ZoomIn.tsx +25 -0
  70. package/src/icons/index.ts +3 -0
  71. package/src/index.ts +11 -0
  72. package/src/mantine/EnsureMantine.tsx +14 -0
  73. package/src/mantine/theme.ts +19 -0
  74. package/src/mantine.css.ts +4 -0
  75. package/src/reset.d.ts +2 -0
  76. package/src/state/DiagramContext.tsx +42 -0
  77. package/src/state/WhenInitialized.tsx +24 -0
  78. package/src/state/diagramStore.ts +358 -0
  79. package/src/state/index.ts +4 -0
  80. package/src/state/useDiagramStore.ts +39 -0
  81. package/src/theme.css.ts +64 -0
  82. package/src/ui/Camera.tsx +174 -0
  83. package/src/ui/KeepAspectRatio.css.ts +7 -0
  84. package/src/ui/KeepAspectRatio.tsx +35 -0
  85. package/src/ui/OptionsPanel.css.ts +10 -0
  86. package/src/ui/OptionsPanel.tsx +65 -0
  87. package/src/ui/index.ts +3 -0
  88. package/src/ui/options/NodeOptions.tsx +253 -0
  89. package/src/xyflow/FitviewOnDiagramChange.tsx +93 -0
  90. package/src/xyflow/SyncWithDiagram.tsx +113 -0
  91. package/src/xyflow/XYFlowBackground.tsx +33 -0
  92. package/src/xyflow/XYFlowEvents.tsx +100 -0
  93. package/src/xyflow/XYFlowWrapper.tsx +166 -0
  94. package/src/xyflow/const.ts +7 -0
  95. package/src/xyflow/diagram-to-xyflow.ts +197 -0
  96. package/src/xyflow/edges/RelationshipEdge.tsx +288 -0
  97. package/src/xyflow/edges/edges.css.ts +124 -0
  98. package/src/xyflow/edges/utils.ts +110 -0
  99. package/src/xyflow/hooks/index.ts +2 -0
  100. package/src/xyflow/hooks/useLayoutConstraints.ts +289 -0
  101. package/src/xyflow/hooks/useXYFlow.ts +20 -0
  102. package/src/xyflow/nodes/compound/CompoundNode.css.ts +191 -0
  103. package/src/xyflow/nodes/compound/CompoundNode.tsx +105 -0
  104. package/src/xyflow/nodes/compound/index.ts +1 -0
  105. package/src/xyflow/nodes/element/ElementIcon.tsx +44 -0
  106. package/src/xyflow/nodes/element/ElementLink.css.ts +67 -0
  107. package/src/xyflow/nodes/element/ElementLink.tsx +94 -0
  108. package/src/xyflow/nodes/element/ElementNode.tsx +225 -0
  109. package/src/xyflow/nodes/element/ElementShapeSvg.tsx +181 -0
  110. package/src/xyflow/nodes/element/element.css.ts +261 -0
  111. package/src/xyflow/nodes/element/index.ts +1 -0
  112. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +25 -0
  113. package/src/xyflow/types.ts +100 -0
  114. package/src/xyflow/utils.ts +12 -0
  115. package/dist/EmbeddedLikeC4Diagram.css.d.ts +0 -6
  116. package/dist/EmbeddedLikeC4Diagram.d.ts +0 -18
  117. package/dist/bundle.d.ts +0 -3
  118. package/dist/hooks/use-set-state.d.ts +0 -2
  119. package/dist/hooks/use-update-effect.d.ts +0 -4
  120. package/dist/index.css.d.ts +0 -2
  121. package/dist/index.js +0 -13043
  122. package/dist/mantine/index.d.ts +0 -3
  123. package/dist/state/DiagramState.d.ts +0 -162
  124. package/dist/xyflow/UpdateOnDiagramChange.d.ts +0 -10
@@ -0,0 +1,28 @@
1
+ import {
2
+ type DependenciesComparator,
3
+ type EffectHook,
4
+ useCustomCompareEffect,
5
+ useFirstMountState
6
+ } from '@react-hookz/web'
7
+ import { shallowEqual } from 'fast-equals'
8
+ import { type DependencyList, type EffectCallback } from 'react'
9
+
10
+ const noop = () => {}
11
+
12
+ export function useUpdateEffect<
13
+ Callback extends EffectCallback = EffectCallback,
14
+ Deps extends DependencyList = DependencyList
15
+ >(
16
+ callback: Callback,
17
+ deps: Deps,
18
+ equalityFn?: DependenciesComparator<Deps>,
19
+ effectHook?: EffectHook<Callback, Deps>
20
+ ) {
21
+ const isFirstMount = useFirstMountState()
22
+ useCustomCompareEffect(
23
+ isFirstMount ? noop as Callback : callback,
24
+ deps,
25
+ equalityFn ?? shallowEqual,
26
+ effectHook
27
+ )
28
+ }
@@ -0,0 +1,24 @@
1
+ import clsx from 'clsx'
2
+ import { forwardRef, type SVGProps } from 'react'
3
+
4
+ export const ClipboardCopy = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(({ className, ...props }, ref) => (
5
+ <svg
6
+ ref={ref}
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ className={clsx('icon icon-tabler icon-tabler-zoom-in', className)}
9
+ width="24"
10
+ height="24"
11
+ viewBox="0 0 24 24"
12
+ strokeWidth="1.5"
13
+ stroke="currentColor"
14
+ fill="none"
15
+ strokeLinecap="round"
16
+ strokeLinejoin="round"
17
+ {...props}
18
+ >
19
+ <path stroke="none" d="M0 0h24v24H0z" fill="none" />
20
+ <path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h3m9 -9v-5a2 2 0 0 0 -2 -2h-2" />
21
+ <path d="M13 17v-1a1 1 0 0 1 1 -1h1m3 0h1a1 1 0 0 1 1 1v1m0 3v1a1 1 0 0 1 -1 1h-1m-3 0h-1a1 1 0 0 1 -1 -1v-1" />
22
+ <path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z" />
23
+ </svg>
24
+ ))
@@ -0,0 +1,23 @@
1
+ import clsx from 'clsx'
2
+ import { forwardRef, type SVGProps } from 'react'
3
+
4
+ export const Link = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(({ className, ...props }, ref) => (
5
+ <svg
6
+ ref={ref}
7
+ className={clsx('icon icon-tabler', className)}
8
+ stroke="currentColor"
9
+ fill="currentColor"
10
+ strokeWidth="0"
11
+ viewBox="0 0 20 20"
12
+ height="20"
13
+ width="20"
14
+ xmlns="http://www.w3.org/2000/svg"
15
+ {...props}
16
+ >
17
+ <path
18
+ fillRule="evenodd"
19
+ d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z"
20
+ clipRule="evenodd">
21
+ </path>
22
+ </svg>
23
+ ))
@@ -0,0 +1,25 @@
1
+ import clsx from 'clsx'
2
+ import { forwardRef, type SVGProps } from 'react'
3
+
4
+ export const ZoomIn = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(({ className, ...props }, ref) => (
5
+ <svg
6
+ ref={ref}
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ className={clsx('icon icon-tabler icon-tabler-zoom-in', className)}
9
+ width="24"
10
+ height="24"
11
+ viewBox="0 0 24 24"
12
+ strokeWidth="1.5"
13
+ stroke="currentColor"
14
+ fill="none"
15
+ strokeLinecap="round"
16
+ strokeLinejoin="round"
17
+ {...props}
18
+ >
19
+ <path stroke="none" d="M0 0h24v24H0z" fill="none" />
20
+ <path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
21
+ <path d="M7 10l6 0" />
22
+ <path d="M10 7l0 6" />
23
+ <path d="M21 21l-6 -6" />
24
+ </svg>
25
+ ))
@@ -0,0 +1,3 @@
1
+ export * from './Clipboard'
2
+ export * from './Link'
3
+ export * from './ZoomIn'
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ import '@xyflow/react/dist/style.css'
2
+ import './globals.css'
3
+
4
+ export { LikeC4Diagram } from './LikeC4Diagram'
5
+ export { StaticLikeC4Diagram } from './StaticLikeC4Diagram'
6
+
7
+ export * from './hooks'
8
+ export type * from './LikeC4Diagram'
9
+ export type * from './LikeC4Diagram.props'
10
+ export type * from './StaticLikeC4Diagram'
11
+ export type * from './xyflow/types'
@@ -0,0 +1,14 @@
1
+ import { MantineContext } from '@mantine/core'
2
+ import { type PropsWithChildren, useContext } from 'react'
3
+
4
+ type EnsureMantineProps = PropsWithChildren<{}>
5
+
6
+ export function EnsureMantine({ children }: EnsureMantineProps) {
7
+ const mantineCtx = useContext(MantineContext)
8
+
9
+ if (!mantineCtx) {
10
+ throw new Error('LikeC4Diagram must be a child of MantineProvider')
11
+ }
12
+
13
+ return <>{children}</>
14
+ }
@@ -0,0 +1,19 @@
1
+ import { createTheme, type MantineTheme } from '@mantine/core'
2
+
3
+ export const theme = createTheme({
4
+ primaryColor: 'indigo',
5
+ cursorType: 'pointer',
6
+ defaultRadius: 'sm',
7
+ headings: {
8
+ fontWeight: '500',
9
+ sizes: {
10
+ h1: {
11
+ // fontSize: '2rem',
12
+ fontWeight: '600'
13
+ },
14
+ h2: {
15
+ // fontSize: '1.85rem',
16
+ }
17
+ }
18
+ }
19
+ }) as MantineTheme
@@ -0,0 +1,4 @@
1
+ import { themeToVars } from '@mantine/vanilla-extract'
2
+ import { theme } from './mantine/theme'
3
+
4
+ export const mantine = themeToVars(theme)
package/src/reset.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Do not add any other lines of code to this file!
2
+ import '@total-typescript/ts-reset'
@@ -0,0 +1,42 @@
1
+ import { createContext, type PropsWithChildren, useRef } from 'react'
2
+ import { useUpdateEffect } from '../hooks'
3
+ import { useXYFlow } from '../xyflow/hooks'
4
+ import { createDiagramStore, type DiagramInitialState } from './diagramStore'
5
+
6
+ type DiagramContextValue = ReturnType<typeof createDiagramStore>
7
+ export const DiagramContext = createContext<DiagramContextValue | null>(null)
8
+
9
+ export function DiagramContextProvider({ children, view, ...props }: PropsWithChildren<DiagramInitialState>) {
10
+ const xyflow = useXYFlow()
11
+ const store = useRef<DiagramContextValue>()
12
+
13
+ if (!store.current) {
14
+ store.current = createDiagramStore({
15
+ xyflow,
16
+ view,
17
+ ...props
18
+ })
19
+ }
20
+
21
+ useUpdateEffect(
22
+ () => store.current?.setState({ xyflow }, false, 'update xyflow'),
23
+ [xyflow],
24
+ Object.is
25
+ )
26
+
27
+ useUpdateEffect(
28
+ () => store.current?.getState().updateView(view),
29
+ [view]
30
+ )
31
+
32
+ useUpdateEffect(
33
+ () => store.current?.setState(props, false, 'update incoming props'),
34
+ [props]
35
+ )
36
+
37
+ return (
38
+ <DiagramContext.Provider value={store.current}>
39
+ {children}
40
+ </DiagramContext.Provider>
41
+ )
42
+ }
@@ -0,0 +1,24 @@
1
+ import { type PropsWithChildren, useEffect, useState } from 'react'
2
+ import { useDiagramStoreApi } from './useDiagramStore'
3
+
4
+ export function WhenInitialized({ children }: PropsWithChildren) {
5
+ const diagramApi = useDiagramStoreApi()
6
+
7
+ const [isInitialized, setIsInitialized] = useState(diagramApi.getState().initialized)
8
+
9
+ useEffect(() => {
10
+ if (isInitialized) {
11
+ return
12
+ }
13
+ return diagramApi.subscribe(
14
+ s => s.initialized,
15
+ setIsInitialized
16
+ )
17
+ }, [isInitialized])
18
+
19
+ if (!isInitialized) {
20
+ return null
21
+ }
22
+
23
+ return <>{children}</>
24
+ }
@@ -0,0 +1,358 @@
1
+ import type { DiagramNode, DiagramView, ElementShape, Fqn, NonEmptyArray, ThemeColor, ViewID } from '@likec4/core'
2
+ import { invariant, nonexhaustive } from '@likec4/core'
3
+ import type { XYPosition } from '@xyflow/react'
4
+ import { getNodeDimensions } from '@xyflow/system'
5
+ import { DEV } from 'esm-env'
6
+ import { shallowEqual } from 'fast-equals'
7
+ import type { MouseEvent as ReactMouseEvent } from 'react'
8
+ import type { Exact, Except, RequiredKeysOf, Simplify } from 'type-fest'
9
+ import { devtools, subscribeWithSelector } from 'zustand/middleware'
10
+ import { shallow } from 'zustand/shallow'
11
+ import { createWithEqualityFn } from 'zustand/traditional'
12
+ import type { Change, DiagramNodeWithNavigate, LikeC4DiagramEventHandlers } from '../LikeC4Diagram.props'
13
+ import { MinZoom } from '../xyflow/const'
14
+ import type { XYFlowInstance } from '../xyflow/types'
15
+
16
+ export type DiagramStore = {
17
+ // Incoming props
18
+ view: DiagramView
19
+ readonly: boolean
20
+ showElementLinks: boolean
21
+ fitViewEnabled: boolean
22
+ fitViewPadding: number
23
+ nodesDraggable: boolean
24
+ nodesSelectable: boolean
25
+
26
+ // Internal state
27
+ xyflow: XYFlowInstance
28
+ initialized: boolean
29
+
30
+ // This is XYFlow id's
31
+ lastClickedNodeId: string | null
32
+ lastClickedEdgeId: string | null
33
+ hoveredNodeId: string | null
34
+ hoveredEdgeId: string | null
35
+
36
+ // Stack of previous distinct views (unique id's)
37
+ previousViews: DiagramView[]
38
+
39
+ lastOnNavigate: null | {
40
+ fromView: ViewID
41
+ toView: ViewID
42
+ element: DiagramNode
43
+ elementCenterScreenPosition: XYPosition
44
+ // If node from the target view is placed on the same position as the node from the source view
45
+ positionCorrected: boolean
46
+ }
47
+
48
+ // User changed the viewport by dragging or zooming
49
+ viewportChanged: boolean
50
+ } & Required<LikeC4DiagramEventHandlers>
51
+
52
+ export type DiagramInitialState = // Required properties
53
+ & Pick<
54
+ DiagramStore,
55
+ | 'view'
56
+ | 'readonly'
57
+ | 'showElementLinks'
58
+ | 'fitViewEnabled'
59
+ | 'fitViewPadding'
60
+ | 'nodesDraggable'
61
+ | 'nodesSelectable'
62
+ >
63
+ // Optional properties
64
+ // & Partial<
65
+ // Pick<
66
+ // DiagramStore,
67
+ // 'colorScheme'
68
+ // >
69
+ // >
70
+ & LikeC4DiagramEventHandlers
71
+
72
+ interface DiagramStoreActions {
73
+ updateView: (view: DiagramView) => void
74
+
75
+ setHoveredNode: (nodeId: string | null) => void
76
+ setHoveredEdge: (edgeId: string | null) => void
77
+
78
+ setLastClickedNode: (nodeId: string | null) => void
79
+ setLastClickedEdge: (edgeId: string | null) => void
80
+
81
+ getElement(id: Fqn): DiagramNode | null
82
+ triggerOnChange: (changes: NonEmptyArray<Change>) => void
83
+ triggerOnNavigateTo: (xynodeId: string, event: ReactMouseEvent) => void
84
+ fitDiagram: () => void
85
+ }
86
+
87
+ export type DiagramState = Simplify<DiagramStore & DiagramStoreActions>
88
+
89
+ const DEFAULT_PROPS: Except<DiagramStore, RequiredKeysOf<DiagramInitialState> | 'xyflow'> = {
90
+ initialized: false,
91
+ previousViews: [],
92
+ viewportChanged: false,
93
+ hoveredNodeId: null,
94
+ hoveredEdgeId: null,
95
+ lastClickedNodeId: null,
96
+ lastClickedEdgeId: null,
97
+ lastOnNavigate: null,
98
+ onChange: null,
99
+ onNavigateTo: null,
100
+ onNodeClick: null,
101
+ onNodeContextMenu: null,
102
+ onCanvasContextMenu: null,
103
+ onEdgeClick: null,
104
+ onEdgeContextMenu: null,
105
+ onCanvasClick: null,
106
+ onCanvasDblClick: null
107
+ }
108
+
109
+ type CreateDiagramStore = DiagramInitialState & Pick<DiagramStore, 'xyflow'>
110
+
111
+ const noReplace = false
112
+
113
+ let storeDevId = 1
114
+
115
+ const containsWithId = <T extends { id: string }>(arr: T[], id: string) => arr.some((x) => x.id === id)
116
+
117
+ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props: T) {
118
+ return createWithEqualityFn<
119
+ DiagramState,
120
+ [
121
+ ['zustand/subscribeWithSelector', never],
122
+ ['zustand/devtools', never]
123
+ ]
124
+ >(
125
+ subscribeWithSelector(
126
+ devtools(
127
+ (set, get) => ({
128
+ ...DEFAULT_PROPS,
129
+ ...(props as CreateDiagramStore),
130
+
131
+ updateView: (view) => {
132
+ let {
133
+ view: currentView,
134
+ lastOnNavigate,
135
+ previousViews,
136
+ lastClickedNodeId,
137
+ lastClickedEdgeId,
138
+ hoveredEdgeId,
139
+ hoveredNodeId
140
+ } = get()
141
+
142
+ if (shallowEqual(currentView, view)) {
143
+ DEV && console.debug('store: skip updateView')
144
+ return
145
+ }
146
+
147
+ // Reset lastOnNavigate if the view is not the source or target view
148
+ if (lastOnNavigate && lastOnNavigate.toView !== view.id && lastOnNavigate.fromView !== view.id) {
149
+ lastOnNavigate = null
150
+ }
151
+
152
+ const isSameView = currentView.id === view.id
153
+
154
+ if (isSameView) {
155
+ // Reset clicked/hovered node/edge if the node/edge is not in the new view
156
+ if (lastClickedNodeId && !containsWithId(view.nodes, lastClickedNodeId)) {
157
+ lastClickedNodeId = null
158
+ }
159
+ if (hoveredNodeId && !containsWithId(view.nodes, hoveredNodeId)) {
160
+ hoveredNodeId = null
161
+ }
162
+ if (lastClickedEdgeId && !containsWithId(view.edges, lastClickedEdgeId)) {
163
+ lastClickedEdgeId = null
164
+ }
165
+ if (hoveredEdgeId && !containsWithId(view.edges, hoveredEdgeId)) {
166
+ hoveredEdgeId = null
167
+ }
168
+ } else {
169
+ // Reset hovered / clicked node/edge if the view is different
170
+ lastClickedEdgeId = null
171
+ lastClickedNodeId = null
172
+ hoveredEdgeId = null
173
+ hoveredNodeId = null
174
+
175
+ // Update history stack (back button not implemented yet)
176
+ previousViews = [
177
+ currentView,
178
+ ...previousViews.filter((v) => v.id !== view.id && v.id !== currentView.id)
179
+ ]
180
+ }
181
+
182
+ set(
183
+ {
184
+ view,
185
+ lastOnNavigate,
186
+ previousViews,
187
+ lastClickedNodeId,
188
+ lastClickedEdgeId,
189
+ hoveredEdgeId,
190
+ hoveredNodeId
191
+ },
192
+ noReplace,
193
+ 'update-view'
194
+ )
195
+ },
196
+
197
+ setHoveredNode: (nodeId) => {
198
+ if (nodeId !== get().hoveredNodeId) {
199
+ set({ hoveredNodeId: nodeId }, noReplace, nodeId ? 'setHoveredNode' : 'unsetHoveredNode')
200
+ }
201
+ },
202
+
203
+ setHoveredEdge: (edgeId) => {
204
+ if (edgeId !== get().hoveredEdgeId) {
205
+ set({ hoveredEdgeId: edgeId }, noReplace, edgeId ? 'setHoveredEdge' : 'unsetHoveredEdge')
206
+ }
207
+ },
208
+
209
+ setLastClickedNode: (nodeId) => {
210
+ if (nodeId !== get().lastClickedNodeId) {
211
+ set({ lastClickedNodeId: nodeId }, noReplace, nodeId ? 'setLastClickedNode' : 'unsetLastClickedNode')
212
+ }
213
+ },
214
+
215
+ setLastClickedEdge: (edgeId) => {
216
+ if (edgeId !== get().lastClickedEdgeId) {
217
+ set({ lastClickedEdgeId: edgeId }, noReplace, edgeId ? 'setLastClickedEdge' : 'unsetLastClickedEdge')
218
+ }
219
+ },
220
+
221
+ getElement: (fqn) => {
222
+ const { view } = get()
223
+ return view.nodes.find(({ id }) => id === fqn) ?? null
224
+ },
225
+
226
+ triggerOnChange: (changes: NonEmptyArray<Change>) => {
227
+ const newShapes = new Map<Fqn, ElementShape>()
228
+ const newColor = new Map<Fqn, ThemeColor>()
229
+ for (const change of changes) {
230
+ if (change.op === 'change-shape') {
231
+ for (const target of change.targets) {
232
+ newShapes.set(target, change.shape)
233
+ }
234
+ continue
235
+ }
236
+ if (change.op === 'change-color') {
237
+ for (const target of change.targets) {
238
+ newColor.set(target, change.color)
239
+ }
240
+ continue
241
+ }
242
+ nonexhaustive(change)
243
+ }
244
+ // TODO: Update positions/sizes
245
+ // const { view, updateView } = get()
246
+ // let hasChanges = false
247
+ // const nextNodes = view.nodes.map((node) => {
248
+ // const shape = newShapes.get(node.id)
249
+ // if (shape && shape !== node.shape) {
250
+ // hasChanges = true
251
+ // node = {
252
+ // ...node,
253
+ // shape
254
+ // }
255
+ // }
256
+ // const color = newColor.get(node.id)
257
+ // if (color && color !== node.color) {
258
+ // hasChanges = true
259
+ // node = {
260
+ // ...node,
261
+ // color
262
+ // }
263
+ // }
264
+ // return node
265
+ // })
266
+ // if (hasChanges) {
267
+ // updateView({
268
+ // ...view,
269
+ // nodes: nextNodes
270
+ // })
271
+ // }
272
+ get().xyflow.setNodes(nodes =>
273
+ nodes.map(node => {
274
+ let { data } = node
275
+ const shape = newShapes.get(data.fqn)
276
+ if (shape && shape !== data.element.shape) {
277
+ node = {
278
+ ...node,
279
+ data: {
280
+ ...data,
281
+ element: {
282
+ ...data.element,
283
+ shape
284
+ }
285
+ }
286
+ }
287
+ data = node.data
288
+ }
289
+ const color = newColor.get(data.fqn)
290
+ if (color && color !== data.element.color) {
291
+ node = {
292
+ ...node,
293
+ data: {
294
+ ...data,
295
+ element: {
296
+ ...data.element,
297
+ color
298
+ }
299
+ }
300
+ }
301
+ }
302
+ return node
303
+ })
304
+ )
305
+ get().onChange?.({ changes })
306
+ },
307
+
308
+ triggerOnNavigateTo: (xynodeId, event) => {
309
+ const { xyflow, view } = get()
310
+ const xynode = xyflow.getInternalNode(xynodeId)
311
+ const element = xynode?.data.element as DiagramNodeWithNavigate
312
+ invariant(!!xynode && element?.navigateTo, `node is not navigable: ${xynodeId}`)
313
+ const dimensions = getNodeDimensions(xynode)
314
+ set(
315
+ {
316
+ lastClickedNodeId: xynodeId,
317
+ lastOnNavigate: {
318
+ fromView: view.id,
319
+ toView: element.navigateTo,
320
+ element: element,
321
+ elementCenterScreenPosition: xyflow.flowToScreenPosition({
322
+ x: xynode.internals.positionAbsolute.x + dimensions.width / 2,
323
+ y: xynode.internals.positionAbsolute.y + dimensions.height / 2
324
+ }),
325
+ positionCorrected: false
326
+ }
327
+ },
328
+ noReplace,
329
+ 'triggerOnNavigateTo'
330
+ )
331
+ get().onNavigateTo?.(
332
+ element.navigateTo,
333
+ event,
334
+ element,
335
+ xynode.internals.userNode
336
+ )
337
+ },
338
+
339
+ fitDiagram: () => {
340
+ const { fitViewPadding, xyflow } = get()
341
+ xyflow.fitView({
342
+ includeHiddenNodes: true,
343
+ duration: 400,
344
+ padding: fitViewPadding,
345
+ minZoom: MinZoom,
346
+ maxZoom: 1
347
+ })
348
+ }
349
+ }),
350
+ {
351
+ name: `DiagramStore ${storeDevId++} - ${props.view.id}`,
352
+ enabled: DEV
353
+ }
354
+ )
355
+ ),
356
+ shallow
357
+ )
358
+ }
@@ -0,0 +1,4 @@
1
+ export { DiagramContextProvider } from './DiagramContext'
2
+ export type * from './diagramStore'
3
+ export * from './useDiagramStore'
4
+ export { WhenInitialized } from './WhenInitialized'
@@ -0,0 +1,39 @@
1
+ import { shallowEqual } from 'fast-equals'
2
+ import { useContext, useMemo } from 'react'
3
+ import { useStoreWithEqualityFn as useZustandStore } from 'zustand/traditional'
4
+
5
+ import { DiagramContext } from './DiagramContext'
6
+ import type { DiagramState } from './diagramStore'
7
+
8
+ function useDiagramState<StateSlice = unknown>(
9
+ selector: (state: DiagramState) => StateSlice,
10
+ equalityFn?: (a: StateSlice, b: StateSlice) => boolean
11
+ ) {
12
+ const store = useContext(DiagramContext)
13
+
14
+ if (store === null) {
15
+ throw new Error('useDiagramStore could be used only inside DiagramContext')
16
+ }
17
+
18
+ return useZustandStore(store, selector, equalityFn ?? shallowEqual)
19
+ }
20
+
21
+ function useDiagramStoreApi() {
22
+ const store = useContext(DiagramContext)
23
+
24
+ if (store === null) {
25
+ throw new Error('useDiagramStoreApi could be used only inside DiagramContext')
26
+ }
27
+
28
+ return useMemo(
29
+ () => ({
30
+ getState: store.getState,
31
+ setState: store.setState,
32
+ subscribe: store.subscribe
33
+ }),
34
+ [store]
35
+ )
36
+ }
37
+ export type DiagramStoreApi = ReturnType<typeof useDiagramStoreApi>
38
+
39
+ export { useDiagramState, useDiagramStoreApi }