@likec4/diagram 1.1.1 → 1.2.1

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 (80) hide show
  1. package/dist/KeepAspectRatioContainer.d.ts +1 -1
  2. package/dist/LikeC4Diagram.d.ts +1 -1
  3. package/dist/LikeC4Diagram.props.d.ts +10 -5
  4. package/dist/StaticLikeC4Diagram.d.ts +1 -1
  5. package/dist/hooks/useUpdateEffect.d.ts +1 -1
  6. package/dist/icons/Clipboard.d.ts +1 -1
  7. package/dist/icons/Link.d.ts +1 -1
  8. package/dist/icons/ZoomIn.d.ts +1 -1
  9. package/dist/index.d.ts +0 -1
  10. package/dist/index.mjs +2604 -1502
  11. package/dist/mantine/EnsureMantine.d.ts +1 -1
  12. package/dist/state/DiagramContext.d.ts +2307 -192
  13. package/dist/state/WhenInitialized.d.ts +1 -1
  14. package/dist/state/diagramStore.d.ts +2220 -97
  15. package/dist/state/useDiagramStore.d.ts +2124 -9
  16. package/dist/style.css +210 -136
  17. package/dist/theme.css.d.ts +1 -0
  18. package/dist/ui/DiagramTitlePanel.d.ts +1 -2
  19. package/dist/ui/DynamicViewWalkthrough.css.d.ts +3 -0
  20. package/dist/ui/DynamicViewWalkthrough.d.ts +1 -0
  21. package/dist/ui/OptionsPanel.d.ts +1 -2
  22. package/dist/ui/index.d.ts +1 -0
  23. package/dist/ui/options/NodeOptions.d.ts +1 -2
  24. package/dist/xyflow/FitviewOnDiagramChange.d.ts +4 -2
  25. package/dist/xyflow/SyncWithDiagram.d.ts +1 -2
  26. package/dist/xyflow/XYFlow.d.ts +5 -8
  27. package/dist/xyflow/XYFlowBackground.d.ts +6 -5
  28. package/dist/xyflow/XYFlowEvents.d.ts +60 -66
  29. package/dist/xyflow/XYFlowInner.d.ts +11 -0
  30. package/dist/xyflow/diagram-to-xyflow.d.ts +2 -2
  31. package/dist/xyflow/edges/RelationshipEdge.d.ts +17 -17
  32. package/dist/xyflow/edges/edges.css.d.ts +4 -2
  33. package/dist/xyflow/edges/utils.d.ts +3 -1
  34. package/dist/xyflow/hooks/useLayoutConstraints.d.ts +1 -1
  35. package/dist/xyflow/hooks/useXYFlow.d.ts +162 -162
  36. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
  37. package/dist/xyflow/nodes/compound/CompoundNode.d.ts +1 -1
  38. package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -1
  39. package/dist/xyflow/nodes/element/ElementLink.d.ts +1 -1
  40. package/dist/xyflow/nodes/element/ElementNode.d.ts +2 -2
  41. package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +2 -2
  42. package/dist/xyflow/nodes/element/element.css.d.ts +2 -1
  43. package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +1 -2
  44. package/dist/xyflow/types.d.ts +23 -23
  45. package/package.json +23 -21
  46. package/src/KeepAspectRatioContainer.tsx +1 -1
  47. package/src/LikeC4Diagram.css.ts +2 -0
  48. package/src/LikeC4Diagram.props.ts +9 -4
  49. package/src/LikeC4Diagram.tsx +17 -11
  50. package/src/StaticLikeC4Diagram.tsx +1 -0
  51. package/src/globals.css.ts +1 -0
  52. package/src/index.ts +0 -3
  53. package/src/state/DiagramContext.tsx +21 -12
  54. package/src/state/diagramStore.ts +177 -38
  55. package/src/theme.css.ts +1 -0
  56. package/src/ui/DiagramTitlePanel.css.ts +2 -1
  57. package/src/ui/DiagramTitlePanel.tsx +3 -4
  58. package/src/ui/DynamicViewWalkthrough.css.ts +46 -0
  59. package/src/ui/DynamicViewWalkthrough.tsx +86 -0
  60. package/src/ui/OptionsPanel.tsx +5 -3
  61. package/src/ui/index.ts +1 -0
  62. package/src/ui/options/NodeOptions.tsx +23 -24
  63. package/src/xyflow/FitviewOnDiagramChange.tsx +89 -69
  64. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +57 -39
  65. package/src/xyflow/SyncWithDiagram.tsx +55 -107
  66. package/src/xyflow/XYFlow.tsx +51 -35
  67. package/src/xyflow/XYFlowBackground.tsx +7 -5
  68. package/src/xyflow/XYFlowEvents.tsx +100 -95
  69. package/src/xyflow/XYFlowInner.tsx +50 -0
  70. package/src/xyflow/diagram-to-xyflow.ts +16 -21
  71. package/src/xyflow/edges/RelationshipEdge.tsx +70 -34
  72. package/src/xyflow/edges/edges.css.ts +68 -34
  73. package/src/xyflow/edges/utils.ts +31 -0
  74. package/src/xyflow/nodes/compound/CompoundNode.css.ts +6 -2
  75. package/src/xyflow/nodes/compound/CompoundNode.tsx +6 -3
  76. package/src/xyflow/nodes/element/ElementLink.tsx +13 -3
  77. package/src/xyflow/nodes/element/ElementNode.tsx +24 -19
  78. package/src/xyflow/nodes/element/element.css.ts +9 -2
  79. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -0
  80. package/src/xyflow/types.ts +7 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@likec4/diagram",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "license": "MIT",
5
5
  "bugs": "https://github.com/likec4/likec4/issues",
6
6
  "homepage": "https://likec4.dev",
@@ -40,18 +40,17 @@
40
40
  "./style.css": "./dist/style.css"
41
41
  }
42
42
  },
43
- "packageManager": "yarn@4.2.2",
43
+ "packageManager": "yarn@4.3.0",
44
44
  "scripts": {
45
- "typecheck": "tsc",
45
+ "typecheck": "tsc --noEmit",
46
46
  "prepack": "vite build --mode production",
47
- "build:turbo": "run -T turbo build --filter='diagram'",
48
- "build": "vite build --mode development && tsc -b",
49
47
  "lint": "run -T eslint src/ --fix",
50
48
  "clean": "run -T rimraf -v -g 'dist/*' .tsbuildinfo"
51
49
  },
52
50
  "dependencies": {
53
- "@likec4/core": "1.1.1",
51
+ "@likec4/core": "1.2.1",
54
52
  "@react-hookz/web": "^24.0.4",
53
+ "@tabler/icons-react": "^3.0.0",
55
54
  "d3-shape": "^3.2.0",
56
55
  "esm-env": "^1.0.0",
57
56
  "fast-equals": "^5.0.1",
@@ -60,35 +59,38 @@
60
59
  "zustand": "^4.5.2"
61
60
  },
62
61
  "peerDependencies": {
63
- "@mantine/core": "^7.9.2",
64
- "@mantine/hooks": "^7.9.2",
62
+ "@mantine/core": "^7.10.1",
63
+ "@mantine/hooks": "^7.10.1",
65
64
  "react": "^18.3.1",
66
65
  "react-dom": "^18.3.1"
67
66
  },
68
67
  "devDependencies": {
68
+ "@likec4/tsconfig": "1.2.1",
69
69
  "@lume/kiwi": "^0.4.3",
70
- "@mantine/core": "^7.9.2",
71
- "@mantine/hooks": "^7.9.2",
72
- "@mantine/vanilla-extract": "^7.9.2",
70
+ "@mantine/core": "^7.10.1",
71
+ "@mantine/hooks": "^7.10.1",
72
+ "@mantine/vanilla-extract": "^7.10.1",
73
73
  "@total-typescript/ts-reset": "^0.5.1",
74
+ "@types/bezier-js": "^4.1.3",
74
75
  "@types/d3-shape": "^3",
75
- "@types/react": "18.2.0",
76
- "@types/react-dom": "18.2.4",
77
- "@vanilla-extract/css": "^1.15.1",
78
- "@vanilla-extract/dynamic": "^2.1.0",
79
- "@vanilla-extract/esbuild-plugin": "^2.3.5",
80
- "@vitejs/plugin-react": "^4.2.1",
76
+ "@types/react": "^18.3.3",
77
+ "@types/react-dom": "^18.3.0",
78
+ "@vanilla-extract/css": "^1.15.2",
79
+ "@vanilla-extract/dynamic": "^2.1.1",
80
+ "@vanilla-extract/esbuild-plugin": "^2.3.6",
81
+ "@vitejs/plugin-react": "^4.3.1",
81
82
  "@xyflow/react": "^12.0.0-next",
83
+ "bezier-js": "^6.1.4",
82
84
  "clsx": "^2.1.1",
83
85
  "execa": "^9.1.0",
84
86
  "khroma": "^2.1.0",
85
- "react": "^18.2.0",
86
- "react-dom": "^18.2.0",
87
+ "react": "^18.3.1",
88
+ "react-dom": "^18.3.1",
87
89
  "tilg": "^0.1.1",
88
90
  "type-fest": "^4.18.2",
89
91
  "typescript": "^5.4.5",
90
92
  "unbuild": "^2.0.0",
91
- "vite": "^5.2.11",
92
- "vite-plugin-dts": "^3.8.1"
93
+ "vite": "5.3.0",
94
+ "vite-plugin-dts": "^3.9.1"
93
95
  }
94
96
  }
@@ -27,7 +27,7 @@ export function KeepAspectRatioContainer({
27
27
  ? {
28
28
  ...style,
29
29
  aspectRatio: `${Math.ceil(width)}/${Math.ceil(height)}`,
30
- maxHeight: Math.ceil(height)
30
+ maxHeight: `var(--likec4-view-max-height, ${Math.ceil(height)}px)`
31
31
  }
32
32
  : style}>
33
33
  {children}
@@ -2,6 +2,8 @@ import { globalStyle, style } from '@vanilla-extract/css'
2
2
  import { vars, xyvars } from './theme.css'
3
3
 
4
4
  export const keepAspectRatioContainer = style({
5
+ overflow: 'hidden',
6
+ position: 'relative',
5
7
  width: '100%',
6
8
  height: '100%',
7
9
  padding: 0
@@ -11,8 +11,8 @@ import type {
11
11
  } from '@likec4/core'
12
12
  import type { MouseEvent as ReactMouseEvent } from 'react'
13
13
  import type { SetRequired, Simplify } from 'type-fest'
14
- import type { XYBackgroundProps } from './xyflow/XYFlowBackground'
15
14
  import type { XYFlowEdge, XYFlowNode } from './xyflow/types'
15
+ import type { XYBackground, XYBackgroundProps } from './xyflow/XYFlowBackground'
16
16
 
17
17
  export type DiagramNodeWithNavigate = Simplify<SetRequired<DiagramNode, 'navigateTo'>>
18
18
 
@@ -57,8 +57,7 @@ export namespace Changes {
57
57
  }
58
58
  }
59
59
 
60
- export type Change =
61
- | Changes.ChangeElementStyle
60
+ export type Change = Changes.ChangeElementStyle
62
61
 
63
62
  export type ChangeEvent = {
64
63
  changes: NonEmptyArray<Change>
@@ -131,7 +130,7 @@ export interface LikeC4DiagramProperties {
131
130
  * Background pattern
132
131
  * @default 'dots'
133
132
  */
134
- background?: 'transparent' | 'solid' | XYBackgroundProps | undefined
133
+ background?: 'transparent' | 'solid' | XYBackground | undefined
135
134
 
136
135
  /**
137
136
  * Display hovercards with element links
@@ -144,6 +143,12 @@ export interface LikeC4DiagramProperties {
144
143
  * @default true
145
144
  */
146
145
  showDiagramTitle?: boolean | undefined
146
+
147
+ /**
148
+ * If Walkthrough for dynamic views should be enabled
149
+ * @default false
150
+ */
151
+ enableDynamicViewWalkthrough?: boolean | undefined
147
152
  }
148
153
 
149
154
  export interface LikeC4DiagramEventHandlers {
@@ -1,3 +1,4 @@
1
+ import '@xyflow/react/dist/style.css'
1
2
  import { ReactFlowProvider as XYFlowProvider } from '@xyflow/react'
2
3
  import clsx from 'clsx'
3
4
  import { useRef } from 'react'
@@ -8,13 +9,13 @@ import { type LikeC4DiagramEventHandlers, type LikeC4DiagramProperties } from '.
8
9
  import { EnsureMantine } from './mantine/EnsureMantine'
9
10
  import { DiagramContextProvider } from './state/DiagramContext'
10
11
  import { WhenInitialized } from './state/WhenInitialized'
11
- import DiagramTitlePanel from './ui/DiagramTitlePanel'
12
- import OptionsPanel from './ui/OptionsPanel'
13
12
  import { diagramViewToXYFlowData } from './xyflow/diagram-to-xyflow'
14
13
  import { FitViewOnDiagramChange } from './xyflow/FitviewOnDiagramChange'
14
+ import { SelectEdgesOnNodeFocus } from './xyflow/SelectEdgesOnNodeFocus'
15
15
  import { SyncWithDiagram } from './xyflow/SyncWithDiagram'
16
16
  import type { XYFlowData } from './xyflow/types'
17
17
  import { XYFlow } from './xyflow/XYFlow'
18
+ import { XYFlowInner } from './xyflow/XYFlowInner'
18
19
 
19
20
  export type LikeC4DiagramProps = LikeC4DiagramProperties & LikeC4DiagramEventHandlers
20
21
  export function LikeC4Diagram({
@@ -28,9 +29,10 @@ export function LikeC4Diagram({
28
29
  nodesSelectable = !readonly,
29
30
  nodesDraggable = !readonly,
30
31
  background = 'dots',
31
- controls = false,
32
+ controls = !readonly,
32
33
  showElementLinks = true,
33
34
  showDiagramTitle = true,
35
+ enableDynamicViewWalkthrough = true,
34
36
  initialWidth,
35
37
  initialHeight,
36
38
  keepAspectRatio = false,
@@ -44,6 +46,7 @@ export function LikeC4Diagram({
44
46
  onNodeClick,
45
47
  onNodeContextMenu
46
48
  }: LikeC4DiagramProps) {
49
+ console.log('LikeC4Diagram')
47
50
  const initialRef = useRef<{
48
51
  defaultNodes: XYFlowData['nodes']
49
52
  defaultEdges: XYFlowData['edges']
@@ -102,19 +105,22 @@ export function LikeC4Diagram({
102
105
  pannable !== true && cssDisablePan,
103
106
  background === 'transparent' && cssTransparentBg
104
107
  )}
105
- background={background}
106
- controls={controls}
107
108
  defaultNodes={initialRef.current.defaultNodes}
108
109
  defaultEdges={initialRef.current.defaultEdges}
109
110
  >
110
- {readonly !== true && <OptionsPanel />}
111
- {showDiagramTitle === true && <DiagramTitlePanel />}
111
+ <XYFlowInner
112
+ showDiagramTitle={showDiagramTitle}
113
+ enableDynamicViewWalkthrough={enableDynamicViewWalkthrough}
114
+ background={background}
115
+ controls={controls}
116
+ />
112
117
  </XYFlow>
118
+ <WhenInitialized>
119
+ <SyncWithDiagram />
120
+ {fitView && <FitViewOnDiagramChange />}
121
+ {fitView && zoomable && <SelectEdgesOnNodeFocus />}
122
+ </WhenInitialized>
113
123
  </KeepAspectRatioContainer>
114
- <WhenInitialized>
115
- <SyncWithDiagram />
116
- <FitViewOnDiagramChange />
117
- </WhenInitialized>
118
124
  </DiagramContextProvider>
119
125
  </XYFlowProvider>
120
126
  </EnsureMantine>
@@ -32,6 +32,7 @@ export function StaticLikeC4Diagram({
32
32
  background={background}
33
33
  showElementLinks
34
34
  showDiagramTitle={false}
35
+ enableDynamicViewWalkthrough={false}
35
36
  nodesSelectable={false}
36
37
  nodesDraggable={false}
37
38
  {...rest}
@@ -25,6 +25,7 @@ createGlobalTheme(`.${rootClassName}`, {
25
25
  }
26
26
  }
27
27
  },
28
+ filterDimmed: 'brightness(.6) grayscale(.8) opacity(.5) blur(1px)',
28
29
  compound: {
29
30
  font: vars.likec4.font,
30
31
  titleColor: vars.element.loContrast
package/src/index.ts CHANGED
@@ -1,6 +1,3 @@
1
- import '@xyflow/react/dist/style.css'
2
- import './globals.css'
3
-
4
1
  export { LikeC4Diagram } from './LikeC4Diagram'
5
2
  export { StaticLikeC4Diagram } from './StaticLikeC4Diagram'
6
3
 
@@ -1,4 +1,5 @@
1
- import { createContext, type PropsWithChildren, useRef } from 'react'
1
+ import { createContext, type PropsWithChildren, useEffect, useRef } from 'react'
2
+ import { hasSubObject, isIncludedIn } from 'remeda'
2
3
  import { useUpdateEffect } from '../hooks'
3
4
  import { useXYFlow } from '../xyflow/hooks'
4
5
  import { createDiagramStore, type DiagramInitialState } from './diagramStore'
@@ -18,20 +19,28 @@ export function DiagramContextProvider({ children, view, ...props }: PropsWithCh
18
19
  })
19
20
  }
20
21
 
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]
22
+ useEffect(
23
+ () => {
24
+ if (store.current?.getState().xyflow !== xyflow) {
25
+ store.current?.setState({ xyflow }, false, 'update xyflow')
26
+ }
27
+ },
28
+ [xyflow]
30
29
  )
31
30
 
32
31
  useUpdateEffect(
33
- () => store.current?.setState(props, false, 'update incoming props'),
34
- [props]
32
+ () => {
33
+ if (!store.current) return
34
+ const state = store.current.getState()
35
+ if (state.view !== view) {
36
+ state.updateView(view)
37
+ }
38
+ if (!hasSubObject(state, props)) {
39
+ store.current.setState(props, false, 'update incoming props')
40
+ }
41
+ isIncludedIn
42
+ },
43
+ [view, props]
35
44
  )
36
45
 
37
46
  return (
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  BorderStyle,
3
+ DiagramEdge,
3
4
  DiagramNode,
4
5
  DiagramView,
5
6
  ElementShape,
@@ -8,20 +9,21 @@ import type {
8
9
  ThemeColor,
9
10
  ViewID
10
11
  } from '@likec4/core'
11
- import { invariant, nonexhaustive } from '@likec4/core'
12
- import type { XYPosition } from '@xyflow/react'
12
+ import { invariant, nonexhaustive, StepEdgeId } from '@likec4/core'
13
+ import { getViewportForBounds, type XYPosition } from '@xyflow/react'
13
14
  import { getNodeDimensions } from '@xyflow/system'
14
15
  import { DEV } from 'esm-env'
15
- import { shallowEqual } from 'fast-equals'
16
+ import { deepEqual, shallowEqual } from 'fast-equals'
16
17
  import type { MouseEvent as ReactMouseEvent } from 'react'
17
- import { entries } from 'remeda'
18
+ import { entries, filter, map, prop } from 'remeda'
18
19
  import type { Exact, Except, RequiredKeysOf, Simplify } from 'type-fest'
19
20
  import { devtools, subscribeWithSelector } from 'zustand/middleware'
20
21
  import { shallow } from 'zustand/shallow'
21
22
  import { createWithEqualityFn } from 'zustand/traditional'
22
23
  import type { Change, DiagramNodeWithNavigate, LikeC4DiagramEventHandlers } from '../LikeC4Diagram.props'
23
24
  import { MinZoom } from '../xyflow/const'
24
- import type { XYFlowInstance, XYFlowNode } from '../xyflow/types'
25
+ import type { XYStoreApi } from '../xyflow/hooks'
26
+ import type { XYFlowEdge, XYFlowInstance, XYFlowNode } from '../xyflow/types'
25
27
 
26
28
  export type DiagramStore = {
27
29
  // Incoming props
@@ -38,6 +40,10 @@ export type DiagramStore = {
38
40
  // Internal state
39
41
  xyflow: XYFlowInstance
40
42
  initialized: boolean
43
+ xyflowSynced: boolean
44
+
45
+ // If Dynamic View
46
+ activeDynamicViewStep: number | null
41
47
 
42
48
  // This is XYFlow id's
43
49
  lastClickedNodeId: string | null
@@ -46,6 +52,9 @@ export type DiagramStore = {
46
52
  hoveredNodeId: string | null
47
53
  hoveredEdgeId: string | null
48
54
 
55
+ // id's of nodes / edges that
56
+ dimmed: ReadonlySet<string>
57
+
49
58
  // Stack of previous distinct views (unique id's)
50
59
  previousViews: DiagramView[]
51
60
 
@@ -62,6 +71,8 @@ export type DiagramStore = {
62
71
  viewportChanged: boolean
63
72
  } & Required<LikeC4DiagramEventHandlers>
64
73
 
74
+ const StringSet = Set<string>
75
+
65
76
  export type DiagramInitialState = // Required properties
66
77
  & Pick<
67
78
  DiagramStore,
@@ -85,8 +96,12 @@ export type DiagramInitialState = // Required properties
85
96
  & LikeC4DiagramEventHandlers
86
97
 
87
98
  interface DiagramStoreActions {
99
+ isDynamicView: () => boolean
100
+
88
101
  updateView: (view: DiagramView) => void
89
102
 
103
+ focusOnNode: (nodeId: string) => void
104
+
90
105
  setHoveredNode: (nodeId: string | null) => void
91
106
  setHoveredEdge: (edgeId: string | null) => void
92
107
 
@@ -98,20 +113,26 @@ interface DiagramStoreActions {
98
113
  getElement(id: Fqn): DiagramNode | null
99
114
  triggerOnChange: (changes: NonEmptyArray<Change>) => void
100
115
  triggerOnNavigateTo: (xynodeId: string, event: ReactMouseEvent) => void
101
- fitDiagram: (centerNode?: XYFlowNode) => void
116
+ fitDiagram: (xyStore: XYStoreApi) => void
117
+
118
+ nextDynamicStep: (increment?: number) => void
119
+ stopDynamicView: () => void
102
120
  }
103
121
 
104
122
  export type DiagramState = Simplify<DiagramStore & DiagramStoreActions>
105
123
 
106
124
  const DEFAULT_PROPS: Except<DiagramStore, RequiredKeysOf<DiagramInitialState> | 'xyflow'> = {
107
125
  initialized: false,
126
+ xyflowSynced: false,
108
127
  previousViews: [],
109
128
  viewportChanged: false,
129
+ activeDynamicViewStep: null,
110
130
  focusedNodeId: null,
111
131
  hoveredNodeId: null,
112
132
  hoveredEdgeId: null,
113
133
  lastClickedNodeId: null,
114
134
  lastClickedEdgeId: null,
135
+ dimmed: new StringSet(),
115
136
  lastOnNavigate: null,
116
137
  onChange: null,
117
138
  onNavigateTo: null,
@@ -130,7 +151,7 @@ const noReplace = false
130
151
 
131
152
  let storeDevId = 1
132
153
 
133
- const containsWithId = <T extends { id: string }>(arr: T[], id: string) => arr.some((x) => x.id === id)
154
+ const EmptyStringSet: ReadonlySet<string> = new StringSet()
134
155
 
135
156
  export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props: T) {
136
157
  return createWithEqualityFn<
@@ -146,78 +167,139 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
146
167
  ...DEFAULT_PROPS,
147
168
  ...(props as CreateDiagramStore),
148
169
 
149
- updateView: (view) => {
170
+ isDynamicView: () => {
171
+ return get().view?.__ === 'dynamic'
172
+ },
173
+
174
+ updateView: (nextView) => {
150
175
  let {
151
- view: currentView,
176
+ dimmed,
177
+ xyflowSynced,
178
+ view: current,
152
179
  lastOnNavigate,
153
180
  previousViews,
154
181
  focusedNodeId,
155
182
  lastClickedNodeId,
156
183
  lastClickedEdgeId,
184
+ activeDynamicViewStep,
157
185
  hoveredEdgeId,
158
186
  hoveredNodeId
159
187
  } = get()
160
188
 
161
- if (shallowEqual(currentView, view)) {
189
+ if (shallowEqual(current, nextView)) {
190
+ if (!xyflowSynced) {
191
+ set({ xyflowSynced: true }, noReplace, 'updateView: xyflow synced')
192
+ }
162
193
  DEV && console.debug('store: skip updateView')
163
194
  return
164
195
  }
165
196
 
166
- // Reset lastOnNavigate if the view is not the source or target view
167
- if (lastOnNavigate && lastOnNavigate.toView !== view.id && lastOnNavigate.fromView !== view.id) {
168
- lastOnNavigate = null
169
- }
170
-
171
- const isSameView = currentView.id === view.id
197
+ const isSameView = current.id === nextView.id
172
198
 
173
199
  if (isSameView) {
200
+ const nodeIds = new StringSet(nextView.nodes.map((n) => n.id))
201
+ const edgeIds = new StringSet(nextView.edges.map((e) => e.id))
174
202
  // Reset clicked/hovered node/edge if the node/edge is not in the new view
175
- if (lastClickedNodeId && !containsWithId(view.nodes, lastClickedNodeId)) {
203
+ if (lastClickedNodeId && !nodeIds.has(lastClickedNodeId)) {
176
204
  lastClickedNodeId = null
177
205
  }
178
- if (hoveredNodeId && !containsWithId(view.nodes, hoveredNodeId)) {
206
+ if (hoveredNodeId && !nodeIds.has(hoveredNodeId)) {
179
207
  hoveredNodeId = null
180
208
  }
181
- if (focusedNodeId && !containsWithId(view.nodes, focusedNodeId)) {
209
+ if (focusedNodeId && !nodeIds.has(focusedNodeId)) {
182
210
  focusedNodeId = null
183
211
  }
184
- if (lastClickedEdgeId && !containsWithId(view.edges, lastClickedEdgeId)) {
212
+ if (lastClickedEdgeId && !edgeIds.has(lastClickedEdgeId)) {
185
213
  lastClickedEdgeId = null
186
214
  }
187
- if (hoveredEdgeId && !containsWithId(view.edges, hoveredEdgeId)) {
215
+ if (hoveredEdgeId && !edgeIds.has(hoveredEdgeId)) {
188
216
  hoveredEdgeId = null
189
217
  }
218
+ if (activeDynamicViewStep && !edgeIds.has(StepEdgeId(activeDynamicViewStep))) {
219
+ activeDynamicViewStep = null
220
+ }
221
+ xyflowSynced = deepEqual(current.nodes, nextView.nodes) && deepEqual(current.edges, nextView.edges)
222
+ if (!xyflowSynced) {
223
+ let nextDimmed = new StringSet([...dimmed].filter(id => nodeIds.has(id) || edgeIds.has(id)))
224
+ if (nextDimmed.size !== dimmed.size) {
225
+ dimmed = nextDimmed
226
+ }
227
+ }
190
228
  } else {
229
+ // Reset lastOnNavigate if the view is not the source or target view
230
+ if (lastOnNavigate) {
231
+ if (lastOnNavigate.toView !== nextView.id && lastOnNavigate.fromView !== nextView.id) {
232
+ lastOnNavigate = null
233
+ }
234
+ }
235
+
191
236
  // Reset hovered / clicked node/edge if the view is different
192
237
  lastClickedEdgeId = null
193
238
  lastClickedNodeId = null
194
239
  hoveredEdgeId = null
195
240
  hoveredNodeId = null
196
241
  focusedNodeId = null
242
+ activeDynamicViewStep = null
243
+ dimmed = EmptyStringSet
197
244
 
198
245
  // Update history stack (back button not implemented yet)
199
246
  previousViews = [
200
- currentView,
201
- ...previousViews.filter((v) => v.id !== view.id && v.id !== currentView.id)
247
+ current,
248
+ ...previousViews.filter((v) => v.id !== nextView.id && v.id !== current.id)
202
249
  ]
250
+ xyflowSynced = false
203
251
  }
204
252
 
205
253
  set(
206
254
  {
207
- view,
255
+ view: nextView,
256
+ activeDynamicViewStep,
257
+ xyflowSynced,
208
258
  lastOnNavigate,
209
259
  previousViews,
210
260
  lastClickedNodeId,
211
261
  lastClickedEdgeId,
212
262
  focusedNodeId,
213
263
  hoveredEdgeId,
214
- hoveredNodeId
264
+ hoveredNodeId,
265
+ dimmed
215
266
  },
216
267
  noReplace,
217
268
  'update-view'
218
269
  )
219
270
  },
220
271
 
272
+ focusOnNode: (nodeId) => {
273
+ const { focusedNodeId, view } = get()
274
+ if (nodeId !== focusedNodeId) {
275
+ const notDimmed = new StringSet([nodeId])
276
+ const dimmed = new StringSet()
277
+ for (const edge of view.edges) {
278
+ if (edge.source === nodeId || edge.target === nodeId) {
279
+ notDimmed.add(edge.source)
280
+ notDimmed.add(edge.target)
281
+ } else {
282
+ dimmed.add(edge.id)
283
+ }
284
+ }
285
+ for (const node of view.nodes) {
286
+ if (notDimmed.has(node.id)) {
287
+ continue
288
+ }
289
+ dimmed.add(node.id)
290
+ }
291
+ set(
292
+ {
293
+ activeDynamicViewStep: null,
294
+ focusedNodeId: nodeId,
295
+ dimmed
296
+ },
297
+ noReplace,
298
+ `focus on node: ${nodeId}`
299
+ )
300
+ }
301
+ },
302
+
221
303
  setHoveredNode: (nodeId) => {
222
304
  if (nodeId !== get().hoveredNodeId) {
223
305
  set({ hoveredNodeId: nodeId }, noReplace, nodeId ? 'setHoveredNode' : 'unsetHoveredNode')
@@ -253,7 +335,8 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
253
335
  {
254
336
  focusedNodeId: null,
255
337
  lastClickedNodeId: null,
256
- lastClickedEdgeId: null
338
+ lastClickedEdgeId: null,
339
+ dimmed: EmptyStringSet
257
340
  },
258
341
  noReplace,
259
342
  'resetLastClicked'
@@ -386,22 +469,78 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
386
469
  )
387
470
  },
388
471
 
389
- fitDiagram: (centerNode) => {
390
- const { fitViewPadding, xyflow, focusedNodeId } = get()
472
+ fitDiagram: (xyStore) => {
473
+ const { width, height, panZoom } = xyStore.getState()
474
+ const { fitViewPadding, view, focusedNodeId } = get()
475
+ const bounds = {
476
+ x: 0,
477
+ y: 0,
478
+ width: view.width,
479
+ height: view.height
480
+ }
481
+ const viewport = getViewportForBounds(bounds, width, height, MinZoom, 1, fitViewPadding)
482
+ panZoom?.setViewport(viewport, { duration: 400 })
483
+ if (!!focusedNodeId) {
484
+ set(
485
+ {
486
+ focusedNodeId: null,
487
+ dimmed: EmptyStringSet
488
+ },
489
+ noReplace,
490
+ 'unfocus'
491
+ )
492
+ }
493
+ },
494
+
495
+ nextDynamicStep: (increment = 1) => {
496
+ const { view, activeDynamicViewStep, xyflow, fitViewPadding } = get()
497
+ invariant(view.__ === 'dynamic', 'view is not dynamic')
498
+ const nextStep = (activeDynamicViewStep ?? 0) + increment
499
+ const edgeId = StepEdgeId(nextStep)
500
+ const dimmed = new StringSet()
501
+ let edge: XYFlowEdge | null = null
502
+ for (const e of xyflow.getEdges()) {
503
+ if (e.id === edgeId) {
504
+ edge = e
505
+ continue
506
+ }
507
+ dimmed.add(e.id)
508
+ }
509
+ invariant(!!edge, `edge not found: ${edgeId}`)
510
+ const selected = [] as XYFlowNode[]
511
+ for (const n of xyflow.getNodes()) {
512
+ if (n.id === edge.source || n.id === edge.target) {
513
+ selected.push(n)
514
+ continue
515
+ }
516
+ dimmed.add(n.id)
517
+ }
391
518
  xyflow.fitView({
392
- includeHiddenNodes: true,
393
- duration: (centerNode && focusedNodeId) ? 300 : 400,
394
- padding: fitViewPadding,
519
+ duration: 400,
520
+ maxZoom: 1,
395
521
  minZoom: MinZoom,
522
+ padding: fitViewPadding,
523
+ nodes: selected
524
+ })
525
+ set({
526
+ focusedNodeId: null,
527
+ activeDynamicViewStep: nextStep,
528
+ dimmed
529
+ })
530
+ },
531
+
532
+ stopDynamicView: () => {
533
+ const { xyflow, fitViewPadding } = get()
534
+ set({
535
+ activeDynamicViewStep: null,
536
+ dimmed: EmptyStringSet
537
+ })
538
+ xyflow.fitView({
539
+ duration: 400,
396
540
  maxZoom: 1,
397
- ...(centerNode && { nodes: [centerNode] })
541
+ minZoom: MinZoom,
542
+ padding: fitViewPadding
398
543
  })
399
- if (centerNode && focusedNodeId !== centerNode.id) {
400
- set({ focusedNodeId: centerNode.id }, noReplace, 'focus node')
401
- }
402
- if (!centerNode && !!focusedNodeId) {
403
- set({ focusedNodeId: null }, noReplace, 'unfocus')
404
- }
405
544
  }
406
545
  }),
407
546
  {
package/src/theme.css.ts CHANGED
@@ -4,6 +4,7 @@ export const vars = createGlobalThemeContract({
4
4
  default: {
5
5
  font: 'default-font-family'
6
6
  },
7
+ filterDimmed: 'filter-dimmed',
7
8
  likec4: {
8
9
  font: 'font-family',
9
10
  background: {