@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.
- package/dist/LikeC4Diagram.css.d.ts +0 -1
- package/dist/LikeC4Diagram.d.ts +1 -2
- package/dist/LikeC4Diagram.props.d.ts +24 -25
- package/dist/StaticLikeC4Diagram.d.ts +1 -2
- package/dist/globals.css.d.ts +1 -0
- package/dist/hooks/index.d.ts +2 -3
- package/dist/hooks/useSetState.d.ts +3 -0
- package/dist/hooks/useUpdateEffect.d.ts +4 -0
- package/dist/icons/Clipboard.d.ts +0 -1
- package/dist/icons/Link.d.ts +0 -1
- package/dist/icons/ZoomIn.d.ts +0 -1
- package/dist/icons/index.d.ts +0 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.mjs +9388 -0
- package/dist/mantine/EnsureMantine.d.ts +2 -4
- package/dist/mantine/theme.d.ts +3 -0
- package/dist/mantine.css.d.ts +1 -0
- package/dist/state/DiagramContext.d.ts +386 -0
- package/dist/state/WhenInitialized.d.ts +3 -0
- package/dist/state/diagramStore.d.ts +428 -0
- package/dist/state/index.d.ts +4 -2
- package/dist/state/useDiagramStore.d.ts +387 -0
- package/dist/style.css +1212 -1
- package/dist/theme.css.d.ts +16 -2
- package/dist/ui/Camera.d.ts +0 -1
- package/dist/ui/KeepAspectRatio.css.d.ts +0 -1
- package/dist/ui/KeepAspectRatio.d.ts +0 -1
- package/dist/ui/OptionsPanel.css.d.ts +0 -1
- package/dist/ui/OptionsPanel.d.ts +0 -1
- package/dist/ui/index.d.ts +0 -1
- package/dist/ui/options/NodeOptions.d.ts +0 -1
- package/dist/xyflow/FitviewOnDiagramChange.d.ts +1 -5
- package/dist/xyflow/SyncWithDiagram.d.ts +5 -0
- package/dist/xyflow/XYFlowBackground.d.ts +0 -1
- package/dist/xyflow/XYFlowEvents.d.ts +5 -15
- package/dist/xyflow/XYFlowWrapper.d.ts +4 -5
- package/dist/xyflow/const.d.ts +1 -2
- package/dist/xyflow/diagram-to-xyflow.d.ts +4 -2
- package/dist/xyflow/edges/RelationshipEdge.d.ts +7 -7
- package/dist/xyflow/edges/edges.css.d.ts +2 -1
- package/dist/xyflow/edges/utils.d.ts +3 -4
- package/dist/xyflow/hooks/index.d.ts +2 -37
- package/dist/xyflow/hooks/{use-layout-/321/201onstraints.d.ts → useLayoutConstraints.d.ts} +2 -4
- package/dist/xyflow/hooks/useXYFlow.d.ts +266 -0
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +0 -1
- package/dist/xyflow/nodes/compound/CompoundNode.d.ts +0 -1
- package/dist/xyflow/nodes/compound/index.d.ts +0 -1
- package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -2
- package/dist/xyflow/nodes/element/ElementLink.css.d.ts +0 -1
- package/dist/xyflow/nodes/element/ElementLink.d.ts +1 -2
- package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -3
- package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +2 -3
- package/dist/xyflow/nodes/element/element.css.d.ts +0 -1
- package/dist/xyflow/nodes/element/index.d.ts +1 -2
- package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +0 -1
- package/dist/xyflow/types.d.ts +20 -8
- package/dist/xyflow/utils.d.ts +0 -1
- package/package.json +36 -34
- package/src/LikeC4Diagram.css.ts +31 -0
- package/src/LikeC4Diagram.props.ts +154 -0
- package/src/LikeC4Diagram.tsx +119 -0
- package/src/StaticLikeC4Diagram.tsx +39 -0
- package/src/globals.css.ts +128 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useSetState.ts +19 -0
- package/src/hooks/useUpdateEffect.ts +28 -0
- package/src/icons/Clipboard.tsx +24 -0
- package/src/icons/Link.tsx +23 -0
- package/src/icons/ZoomIn.tsx +25 -0
- package/src/icons/index.ts +3 -0
- package/src/index.ts +11 -0
- package/src/mantine/EnsureMantine.tsx +14 -0
- package/src/mantine/theme.ts +19 -0
- package/src/mantine.css.ts +4 -0
- package/src/reset.d.ts +2 -0
- package/src/state/DiagramContext.tsx +42 -0
- package/src/state/WhenInitialized.tsx +24 -0
- package/src/state/diagramStore.ts +358 -0
- package/src/state/index.ts +4 -0
- package/src/state/useDiagramStore.ts +39 -0
- package/src/theme.css.ts +64 -0
- package/src/ui/Camera.tsx +174 -0
- package/src/ui/KeepAspectRatio.css.ts +7 -0
- package/src/ui/KeepAspectRatio.tsx +35 -0
- package/src/ui/OptionsPanel.css.ts +10 -0
- package/src/ui/OptionsPanel.tsx +65 -0
- package/src/ui/index.ts +3 -0
- package/src/ui/options/NodeOptions.tsx +253 -0
- package/src/xyflow/FitviewOnDiagramChange.tsx +93 -0
- package/src/xyflow/SyncWithDiagram.tsx +113 -0
- package/src/xyflow/XYFlowBackground.tsx +33 -0
- package/src/xyflow/XYFlowEvents.tsx +100 -0
- package/src/xyflow/XYFlowWrapper.tsx +166 -0
- package/src/xyflow/const.ts +7 -0
- package/src/xyflow/diagram-to-xyflow.ts +197 -0
- package/src/xyflow/edges/RelationshipEdge.tsx +288 -0
- package/src/xyflow/edges/edges.css.ts +124 -0
- package/src/xyflow/edges/utils.ts +110 -0
- package/src/xyflow/hooks/index.ts +2 -0
- package/src/xyflow/hooks/useLayoutConstraints.ts +289 -0
- package/src/xyflow/hooks/useXYFlow.ts +20 -0
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +191 -0
- package/src/xyflow/nodes/compound/CompoundNode.tsx +105 -0
- package/src/xyflow/nodes/compound/index.ts +1 -0
- package/src/xyflow/nodes/element/ElementIcon.tsx +44 -0
- package/src/xyflow/nodes/element/ElementLink.css.ts +67 -0
- package/src/xyflow/nodes/element/ElementLink.tsx +94 -0
- package/src/xyflow/nodes/element/ElementNode.tsx +225 -0
- package/src/xyflow/nodes/element/ElementShapeSvg.tsx +181 -0
- package/src/xyflow/nodes/element/element.css.ts +261 -0
- package/src/xyflow/nodes/element/index.ts +1 -0
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +25 -0
- package/src/xyflow/types.ts +100 -0
- package/src/xyflow/utils.ts +12 -0
- package/dist/EmbeddedLikeC4Diagram.css.d.ts +0 -6
- package/dist/EmbeddedLikeC4Diagram.d.ts +0 -18
- package/dist/bundle.d.ts +0 -3
- package/dist/hooks/use-set-state.d.ts +0 -2
- package/dist/hooks/use-update-effect.d.ts +0 -4
- package/dist/index.css.d.ts +0 -2
- package/dist/index.js +0 -13043
- package/dist/mantine/index.d.ts +0 -3
- package/dist/state/DiagramState.d.ts +0 -162
- 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
|
+
))
|
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
|
package/src/reset.d.ts
ADDED
|
@@ -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,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 }
|