@likec4/diagram 1.21.1 → 1.22.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 (120) hide show
  1. package/dist/LikeC4Diagram.js +9 -13
  2. package/dist/base/index.d.ts +0 -1
  3. package/dist/base/primitives/element/styles.css.js +1 -1
  4. package/dist/{base → components}/Link.css.js +3 -3
  5. package/dist/components/SearchControl.css.d.ts +2 -0
  6. package/dist/components/SearchControl.css.js +7 -0
  7. package/dist/components/SearchControl.d.ts +5 -0
  8. package/dist/components/SearchControl.js +17 -0
  9. package/dist/context/DiagramFeatures.d.ts +1 -1
  10. package/dist/context/IconRenderer.d.ts +10 -0
  11. package/dist/context/IconRenderer.js +34 -0
  12. package/dist/hooks/useCurrentViewId.js +8 -0
  13. package/dist/hooks/useDiagram.d.ts +108 -72
  14. package/dist/hooks/useDiagram.js +3 -0
  15. package/dist/likec4diagram/DiagramActor.js +13 -2
  16. package/dist/likec4diagram/DiagramUI.js +0 -2
  17. package/dist/likec4diagram/XYFlow.d.ts +6 -1
  18. package/dist/likec4diagram/XYFlow.js +37 -31
  19. package/dist/likec4diagram/custom/edges/RelationshipsDropdownMenu.js +2 -1
  20. package/dist/likec4diagram/state/actorContext.d.ts +620 -468
  21. package/dist/likec4diagram/state/assign.js +1 -1
  22. package/dist/likec4diagram/state/machine.d.ts +120 -74
  23. package/dist/likec4diagram/state/machine.js +32 -12
  24. package/dist/likec4diagram/state/syncManualLayoutActor.js +4 -8
  25. package/dist/likec4diagram/ui/controls/Controls.js +65 -48
  26. package/dist/likec4diagram/ui/controls/NavigationButtons.js +44 -69
  27. package/dist/likec4diagram/ui/controls/ToggleReadonly.d.ts +1 -0
  28. package/dist/likec4diagram/ui/controls/ToggleReadonly.js +29 -0
  29. package/dist/likec4diagram/ui/controls/_shared.js +1 -1
  30. package/dist/likec4diagram/ui/diagram-title/DiagramTitlePanel.js +1 -1
  31. package/dist/likec4diagram/ui/search/ElementsColumn.css.d.ts +15 -0
  32. package/dist/likec4diagram/ui/search/ElementsColumn.css.js +30 -0
  33. package/dist/likec4diagram/ui/search/ElementsColumn.d.ts +1 -0
  34. package/dist/likec4diagram/ui/search/ElementsColumn.js +243 -0
  35. package/dist/likec4diagram/ui/search/LikeC4Search.css.d.ts +8 -0
  36. package/dist/likec4diagram/ui/search/LikeC4Search.css.js +20 -0
  37. package/dist/likec4diagram/ui/search/LikeC4Search.d.ts +1 -0
  38. package/dist/likec4diagram/ui/search/LikeC4Search.js +265 -0
  39. package/dist/likec4diagram/ui/search/SearchInput.d.ts +1 -0
  40. package/dist/likec4diagram/ui/search/SearchInput.js +196 -0
  41. package/dist/likec4diagram/ui/search/ViewsColum.d.ts +9 -0
  42. package/dist/likec4diagram/ui/search/ViewsColum.js +130 -0
  43. package/dist/likec4diagram/ui/search/ViewsColumn.css.d.ts +5 -0
  44. package/dist/likec4diagram/ui/search/ViewsColumn.css.js +12 -0
  45. package/dist/likec4diagram/ui/search/_shared.css.d.ts +16 -0
  46. package/dist/likec4diagram/ui/search/_shared.css.js +5 -0
  47. package/dist/likec4diagram/ui/search/state.d.ts +19 -0
  48. package/dist/likec4diagram/ui/search/state.js +72 -0
  49. package/dist/likec4diagram/ui/search/utils.d.ts +3 -0
  50. package/dist/likec4diagram/ui/search/utils.js +23 -0
  51. package/dist/likec4diagram/useLayoutConstraints.js +4 -5
  52. package/dist/likec4model/useLikeC4ElementsTree.d.ts +1 -0
  53. package/dist/likec4model/useLikeC4ElementsTree.js +1 -0
  54. package/dist/overlays/Overlays.js +9 -9
  55. package/dist/overlays/element-details/ElementDetailsCard.js +20 -13
  56. package/dist/overlays/overlay/Overlay.d.ts +6 -2
  57. package/dist/overlays/overlay/Overlay.js +18 -29
  58. package/dist/overlays/relationship-details/SelectEdge.js +1 -3
  59. package/dist/overlays/relationships-browser/RelationshipsBrowser.js +6 -5
  60. package/dist/overlays/relationships-browser/SelectElement.js +10 -6
  61. package/dist/overlays/relationships-browser/actor.d.ts +2 -2
  62. package/dist/overlays/relationships-browser/actor.js +8 -8
  63. package/dist/overlays/relationships-browser/custom/nodeTypes.js +2 -2
  64. package/dist/style.css +1 -1
  65. package/package.json +13 -13
  66. package/src/LikeC4Diagram.tsx +0 -1
  67. package/src/base/index.ts +0 -2
  68. package/src/base/primitives/element/styles.css.ts +2 -2
  69. package/src/{base → components}/Link.css.ts +3 -3
  70. package/src/components/SearchControl.css.ts +69 -0
  71. package/src/components/SearchControl.tsx +33 -0
  72. package/src/context/DiagramFeatures.tsx +1 -1
  73. package/src/context/IconRenderer.tsx +54 -1
  74. package/src/globals.css.ts +0 -2
  75. package/src/hooks/useDiagram.ts +5 -0
  76. package/src/likec4diagram/DiagramActor.tsx +15 -2
  77. package/src/likec4diagram/DiagramUI.tsx +0 -4
  78. package/src/likec4diagram/XYFlow.tsx +39 -31
  79. package/src/likec4diagram/custom/edges/RelationshipsDropdownMenu.tsx +2 -1
  80. package/src/likec4diagram/state/assign.ts +1 -1
  81. package/src/likec4diagram/state/machine.ts +40 -14
  82. package/src/likec4diagram/state/syncManualLayoutActor.ts +6 -9
  83. package/src/likec4diagram/ui/controls/Controls.tsx +37 -25
  84. package/src/likec4diagram/ui/controls/NavigationButtons.tsx +36 -58
  85. package/src/likec4diagram/ui/controls/ToggleReadonly.tsx +31 -0
  86. package/src/likec4diagram/ui/controls/styles.css.ts +3 -1
  87. package/src/likec4diagram/ui/diagram-title/DiagramTitlePanel.tsx +1 -1
  88. package/src/likec4diagram/ui/search/ElementsColumn.css.ts +119 -0
  89. package/src/likec4diagram/ui/search/ElementsColumn.tsx +291 -0
  90. package/src/likec4diagram/ui/search/LikeC4Search.css.ts +117 -0
  91. package/src/likec4diagram/ui/search/LikeC4Search.tsx +312 -0
  92. package/src/likec4diagram/ui/search/SearchInput.tsx +235 -0
  93. package/src/likec4diagram/ui/search/ViewsColum.tsx +158 -0
  94. package/src/likec4diagram/ui/search/ViewsColumn.css.ts +37 -0
  95. package/src/likec4diagram/ui/search/_shared.css.ts +104 -0
  96. package/src/likec4diagram/ui/search/state.ts +76 -0
  97. package/src/likec4diagram/ui/search/utils.ts +20 -0
  98. package/src/likec4diagram/useLayoutConstraints.ts +4 -5
  99. package/src/likec4model/useLikeC4ElementsTree.ts +4 -4
  100. package/src/overlays/Overlays.tsx +31 -25
  101. package/src/overlays/element-details/ElementDetailsCard.css.ts +4 -4
  102. package/src/overlays/element-details/ElementDetailsCard.tsx +245 -240
  103. package/src/overlays/overlay/Overlay.css.ts +10 -9
  104. package/src/overlays/overlay/Overlay.tsx +38 -26
  105. package/src/overlays/relationship-details/SelectEdge.css.ts +1 -0
  106. package/src/overlays/relationship-details/SelectEdge.tsx +2 -4
  107. package/src/overlays/relationships-browser/RelationshipsBrowser.tsx +10 -5
  108. package/src/overlays/relationships-browser/SelectElement.tsx +88 -82
  109. package/src/overlays/relationships-browser/actor.ts +10 -10
  110. package/src/overlays/relationships-browser/custom/nodeTypes.tsx +2 -2
  111. package/dist/LikeC4Search.css.d.ts +0 -1
  112. package/dist/LikeC4Search.css.js +0 -5
  113. package/dist/LikeC4Search.d.ts +0 -2
  114. package/dist/LikeC4Search.js +0 -88
  115. package/src/LikeC4Search.css.ts +0 -12
  116. package/src/LikeC4Search.tsx +0 -91
  117. /package/dist/{base → components}/Link.css.d.ts +0 -0
  118. /package/dist/{base → components}/Link.d.ts +0 -0
  119. /package/dist/{base → components}/Link.js +0 -0
  120. /package/src/{base → components}/Link.tsx +0 -0
@@ -28,7 +28,7 @@ const FeatureNames = [
28
28
  */
29
29
  'Vscode',
30
30
  ] as const
31
- type FeatureName = typeof FeatureNames[number]
31
+ export type FeatureName = typeof FeatureNames[number]
32
32
  export type EnabledFeatures = {
33
33
  [P in `enable${FeatureName}`]: boolean
34
34
  }
@@ -1,5 +1,16 @@
1
+ import type { ElementShape } from '@likec4/core'
2
+ import {
3
+ type Icon,
4
+ type IconProps,
5
+ IconBrowser,
6
+ IconCylinder,
7
+ IconDeviceMobile,
8
+ IconRectangularPrism,
9
+ IconReorder,
10
+ IconUser,
11
+ } from '@tabler/icons-react'
1
12
  import clsx from 'clsx'
2
- import { type ReactNode, createContext, useContext } from 'react'
13
+ import { type ForwardRefExoticComponent, type ReactNode, type RefAttributes, createContext, useContext } from 'react'
3
14
  import type { ElementIconRenderer } from '../LikeC4Diagram.props'
4
15
 
5
16
  const IconRendererContext = createContext<ElementIconRenderer | null>(null)
@@ -43,3 +54,45 @@ export function IconRenderer({
43
54
  </div>
44
55
  )
45
56
  }
57
+
58
+ const ShapeIcons = {
59
+ browser: IconBrowser,
60
+ cylinder: IconCylinder,
61
+ mobile: IconDeviceMobile,
62
+ person: IconUser,
63
+ queue: IconReorder,
64
+ rectangle: IconRectangularPrism,
65
+ storage: IconCylinder,
66
+ } satisfies Record<ElementShape, ForwardRefExoticComponent<IconProps & RefAttributes<Icon>>>
67
+
68
+ export function IconOrShapeRenderer({
69
+ element,
70
+ className,
71
+ }: {
72
+ element: {
73
+ id: string
74
+ title: string
75
+ shape: ElementShape
76
+ icon?: string | null | undefined
77
+ }
78
+ className: string
79
+ }) {
80
+ const icon = IconRenderer({
81
+ element,
82
+ className,
83
+ })
84
+ if (icon) {
85
+ return icon
86
+ }
87
+ const ShapeIcon = ShapeIcons[element.shape]
88
+ return (
89
+ <div
90
+ className={clsx(
91
+ className,
92
+ 'likec4-shape-icon',
93
+ )}
94
+ >
95
+ <ShapeIcon />
96
+ </div>
97
+ )
98
+ }
@@ -7,8 +7,6 @@ import { mantine, vars, whereDark } from './theme-vars'
7
7
  export const rootClassName = 'likec4-diagram-root'
8
8
 
9
9
  globalStyle(`.${rootClassName}`, {
10
- containerName: 'likec4-diagram-root',
11
- containerType: 'size',
12
10
  overflow: 'hidden',
13
11
  position: 'relative',
14
12
  width: '100%',
@@ -1,6 +1,7 @@
1
1
  import type { EdgeId, Fqn, NodeId, ViewId } from '@likec4/core'
2
2
  import { useMemo, useTransition } from 'react'
3
3
  import type { PartialDeep } from 'type-fest'
4
+ import type { FeatureName } from '../context/DiagramFeatures'
4
5
  import { useDiagramActor } from '../hooks/useDiagramActor'
5
6
  import type { OpenSourceParams } from '../LikeC4Diagram.props'
6
7
  import type { AlignmentMode } from '../likec4diagram/state/aligners'
@@ -125,5 +126,9 @@ export function useDiagram() {
125
126
  stopWalkthrough: () => {
126
127
  actor.send({ type: 'walkthrough.end' })
127
128
  },
129
+
130
+ toggleFeature: (feature: FeatureName, forceValue?: boolean) => {
131
+ actor.send({ type: 'toggle.feature', feature, ...(forceValue !== undefined && { forceValue }) })
132
+ },
128
133
  }), [actor])
129
134
  }
@@ -2,11 +2,12 @@ import type { ViewId } from '@likec4/core'
2
2
  import { useCallbackRef } from '@mantine/hooks'
3
3
  import { useStoreApi } from '@xyflow/react'
4
4
  import { type PropsWithChildren, useEffect } from 'react'
5
- import { useDiagramEventHandlers, useEnabledFeatures } from '../context'
5
+ import { DiagramFeatures, useDiagramEventHandlers, useEnabledFeatures } from '../context'
6
6
  import { useDiagramActor } from '../hooks/useDiagramActor'
7
7
  import { useUpdateEffect } from '../hooks/useUpdateEffect'
8
8
  import { LikeC4ViewMachineContextProvider } from './state/actorContext'
9
9
  // import { useInspector } from './state/inspector'
10
+ import { useDiagramContext } from '../hooks/useDiagramContext'
10
11
  import { type Input, diagramMachine } from './state/machine'
11
12
  import { syncManualLayoutActor } from './state/syncManualLayoutActor'
12
13
  import type { Types } from './types'
@@ -54,7 +55,9 @@ export function DiagramActor({ input, children }: PropsWithChildren<{ input: Act
54
55
  }}
55
56
  >
56
57
  <SyncStore input={input} />
57
- {children}
58
+ <DiagramActorToggledFeatures>
59
+ {children}
60
+ </DiagramActorToggledFeatures>
58
61
  </LikeC4ViewMachineContextProvider>
59
62
  )
60
63
  )
@@ -77,3 +80,13 @@ const SyncStore = ({ input: { view, xyedges, xynodes, ...inputs } }: { input: Ac
77
80
 
78
81
  return null
79
82
  }
83
+
84
+ function DiagramActorToggledFeatures({ children }: PropsWithChildren) {
85
+ const toggledFeatures = useDiagramContext(s => s.toggledFeatures)
86
+ return (
87
+ <DiagramFeatures
88
+ overrides={toggledFeatures}>
89
+ {children}
90
+ </DiagramFeatures>
91
+ )
92
+ }
@@ -1,5 +1,4 @@
1
1
  import { IfEnabled } from '../context'
2
- import { LikeC4Search } from '../LikeC4Search'
3
2
  import { Overlays } from '../overlays/Overlays'
4
3
  import { Controls, DiagramTitlePanel, DynamicViewWalkthrough, NotationPanel } from './ui'
5
4
 
@@ -8,9 +7,6 @@ export function DiagramUI() {
8
7
  <>
9
8
  <Controls />
10
9
  <Overlays />
11
- <IfEnabled feature="Search">
12
- <LikeC4Search />
13
- </IfEnabled>
14
10
  <IfEnabled feature="ViewTitle">
15
11
  <DiagramTitlePanel />
16
12
  </IfEnabled>
@@ -2,6 +2,7 @@ import { type EdgeId, type NodeId, nonNullable } from '@likec4/core'
2
2
  import { useCallbackRef } from '@mantine/hooks'
3
3
  import clsx from 'clsx'
4
4
  import { shallowEqual } from 'fast-equals'
5
+ import { memo } from 'react'
5
6
  import type { EnforceOptional } from 'type-fest/source/enforce-optional'
6
7
  import { BaseXYFlow } from '../base/BaseXYFlow'
7
8
  import { useDiagramEventHandlers } from '../context'
@@ -9,6 +10,7 @@ import { useDiagram } from '../hooks/useDiagram'
9
10
  import { useDiagramContext } from '../hooks/useDiagramContext'
10
11
  import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
11
12
  import { edgeTypes, nodeTypes } from './custom'
13
+ import { DiagramUI } from './DiagramUI'
12
14
  import type { DiagramContext } from './state/machine'
13
15
  import type { Types } from './types'
14
16
  import { useLayoutConstraints } from './useLayoutConstraints'
@@ -22,6 +24,13 @@ const selectXYProps = (ctx: DiagramContext) => ({
22
24
  fitViewPadding: ctx.fitViewPadding,
23
25
  enableFitView: ctx.features.enableFitView,
24
26
  enableReadOnly: ctx.features.enableReadOnly,
27
+ ...(!ctx.features.enableFitView && {
28
+ viewport: {
29
+ x: -Math.min(ctx.view.bounds.x, 0),
30
+ y: -Math.min(ctx.view.bounds.y, 0),
31
+ zoom: 1,
32
+ },
33
+ }),
25
34
  })
26
35
  const equalsXYProps = (a: ReturnType<typeof selectXYProps>, b: ReturnType<typeof selectXYProps>): boolean =>
27
36
  a.initialized === b.initialized &&
@@ -31,7 +40,8 @@ const equalsXYProps = (a: ReturnType<typeof selectXYProps>, b: ReturnType<typeof
31
40
  a.enableFitView === b.enableFitView &&
32
41
  a.enableReadOnly === b.enableReadOnly &&
33
42
  shallowEqual(a.nodes, b.nodes) &&
34
- shallowEqual(a.edges, b.edges)
43
+ shallowEqual(a.edges, b.edges) &&
44
+ shallowEqual(a.viewport, b.viewport)
35
45
 
36
46
  type Picked = EnforceOptional<
37
47
  Pick<
@@ -43,7 +53,7 @@ type Picked = EnforceOptional<
43
53
  >
44
54
  export type LikeC4DiagramXYFlowProps = Required<Picked>
45
55
 
46
- export const LikeC4DiagramXYFlow = ({ background, ...rest }: LikeC4DiagramXYFlowProps) => {
56
+ export const LikeC4DiagramXYFlow = memo<LikeC4DiagramXYFlowProps>(({ background, nodesDraggable, nodesSelectable }) => {
47
57
  const diagram = useDiagram()
48
58
  const {
49
59
  initialized,
@@ -80,10 +90,10 @@ export const LikeC4DiagramXYFlow = ({ background, ...rest }: LikeC4DiagramXYFlow
80
90
  nodeTypes={nodeTypes}
81
91
  edgeTypes={edgeTypes}
82
92
  onNodesChange={useCallbackRef((changes) => {
83
- diagram.send({ type: 'xyflow.applyNodeChages', changes })
93
+ diagram.send({ type: 'xyflow.applyNodeChanges', changes })
84
94
  })}
85
95
  onEdgesChange={useCallbackRef((changes) => {
86
- diagram.send({ type: 'xyflow.applyEdgeChages', changes })
96
+ diagram.send({ type: 'xyflow.applyEdgeChanges', changes })
87
97
  })}
88
98
  background={initialized ? background : 'transparent'}
89
99
  // Fitview is handled in onInit
@@ -105,6 +115,7 @@ export const LikeC4DiagramXYFlow = ({ background, ...rest }: LikeC4DiagramXYFlow
105
115
  })}
106
116
  onDoubleClick={useCallbackRef(e => {
107
117
  e.stopPropagation()
118
+ e.preventDefault()
108
119
  diagram.send({ type: 'xyflow.paneDblClick' })
109
120
  onCanvasDblClick?.(e as any)
110
121
  })}
@@ -115,34 +126,31 @@ export const LikeC4DiagramXYFlow = ({ background, ...rest }: LikeC4DiagramXYFlow
115
126
  onInit={useCallbackRef((instance) => {
116
127
  diagram.send({ type: 'xyflow.init', instance })
117
128
  })}
118
- {...onNodeContextMenu && {
119
- onNodeContextMenu: useCallbackRef((event, node) => {
120
- const diagramNode = nonNullable(
121
- diagram.findDiagramNode(node.id as NodeId),
122
- `diagramNode ${node.id} not found`,
123
- )
124
- onNodeContextMenu(diagramNode, event)
125
- }),
126
- }}
127
- {...onEdgeContextMenu && {
128
- onEdgeContextMenu: useCallbackRef((event, edge) => {
129
- const diagramEdge = nonNullable(
130
- diagram.findDiagramEdge(edge.id as EdgeId),
131
- `diagramEdge ${edge.id} not found`,
132
- )
133
- onEdgeContextMenu(diagramEdge, event)
134
- }),
135
- }}
136
- {...onCanvasContextMenu && {
137
- onPaneContextMenu: useCallbackRef((event) => {
138
- onCanvasContextMenu(event as any)
139
- }),
140
- }}
129
+ onNodeContextMenu={useCallbackRef((event, node) => {
130
+ const diagramNode = nonNullable(
131
+ diagram.findDiagramNode(node.id as NodeId),
132
+ `diagramNode ${node.id} not found`,
133
+ )
134
+ onNodeContextMenu?.(diagramNode, event)
135
+ })}
136
+ onEdgeContextMenu={useCallbackRef((event, edge) => {
137
+ const diagramEdge = nonNullable(
138
+ diagram.findDiagramEdge(edge.id as EdgeId),
139
+ `diagramEdge ${edge.id} not found`,
140
+ )
141
+ onEdgeContextMenu?.(diagramEdge, event)
142
+ })}
143
+ onPaneContextMenu={useCallbackRef((event) => {
144
+ onCanvasContextMenu?.(event as any)
145
+ })}
141
146
  {...enableFitView && {
142
147
  onViewportResize,
143
148
  }}
144
- {...(notReadOnly && rest.nodesDraggable && layoutConstraints)}
145
- {...props}
146
- {...rest} />
149
+ nodesDraggable={nodesDraggable}
150
+ nodesSelectable={nodesSelectable}
151
+ {...(notReadOnly && nodesDraggable && layoutConstraints)}
152
+ {...props}>
153
+ <DiagramUI />
154
+ </BaseXYFlow>
147
155
  )
148
- }
156
+ }, shallowEqual)
@@ -27,7 +27,7 @@ import { IconArrowRight, IconFileSymlink, IconInfoCircle, IconZoomScan } from '@
27
27
  import clsx from 'clsx'
28
28
  import { type MouseEventHandler, type PropsWithChildren, forwardRef, Fragment, useCallback } from 'react'
29
29
  import { filter, isTruthy, map, partition, pipe } from 'remeda'
30
- import { Link } from '../../../base/Link'
30
+ import { Link } from '../../../components/Link'
31
31
  import { IfEnabled, useDiagramEventHandlers, useEnabledFeature } from '../../../context'
32
32
  import { useMantinePortalProps } from '../../../hooks'
33
33
  import { useDiagram } from '../../../hooks/useDiagram'
@@ -127,6 +127,7 @@ export function RelationshipsDropdownMenu({
127
127
  closeOnClickOutside
128
128
  clickOutsideEvents={['pointerdown', 'mousedown', 'click']}
129
129
  closeOnEscape
130
+ keepMounted={false}
130
131
  closeOnItemClick={false}
131
132
  disabled={disabled}
132
133
  position="bottom-start"
@@ -321,7 +321,7 @@ export function resetEdgeControlPoints({ context }: ActionArg): Partial<DiagramC
321
321
  }
322
322
 
323
323
  // Edge is a loop
324
- if (source == target) {
324
+ if (source === target) {
325
325
  const loopSize = 80
326
326
  const centerOfTopBoundary = new VectorImpl(0, source.height || 0)
327
327
  .mul(-0.5)
@@ -4,7 +4,6 @@ import {
4
4
  type EdgeId,
5
5
  type Fqn,
6
6
  type NodeId,
7
- type RelationId,
8
7
  type StepEdgeId,
9
8
  type ViewChange,
10
9
  type ViewId,
@@ -42,7 +41,7 @@ import {
42
41
  } from 'xstate'
43
42
  import { MinZoom } from '../../base/const'
44
43
  import type { EnabledFeatures } from '../../context'
45
- import { AllDisabled } from '../../context/DiagramFeatures'
44
+ import { type FeatureName, AllDisabled } from '../../context/DiagramFeatures'
46
45
  import type { OpenSourceParams } from '../../LikeC4Diagram.props'
47
46
  import { relationshipDetailsActor } from '../../overlays/relationship-details/actor'
48
47
  import { relationshipsBrowserActor } from '../../overlays/relationships-browser/actor'
@@ -87,8 +86,12 @@ export interface Input {
87
86
  fitViewPadding: number
88
87
  }
89
88
 
89
+ type ToggledFeatures = Partial<EnabledFeatures>
90
+
90
91
  export interface DiagramContext extends Input {
91
92
  features: EnabledFeatures
93
+ // This is used to override features from props
94
+ toggledFeatures: ToggledFeatures
92
95
  initialized: boolean
93
96
  viewport: Viewport
94
97
  viewportChangedManually: boolean
@@ -136,8 +139,8 @@ export namespace DiagramContext {
136
139
  export type Events =
137
140
  | HotKeyEvent
138
141
  | { type: 'xyflow.init'; instance: ReactFlowInstance<Types.Node, Types.Edge> }
139
- | { type: 'xyflow.applyNodeChages'; changes: NodeChange<Types.Node>[] }
140
- | { type: 'xyflow.applyEdgeChages'; changes: EdgeChange<Types.Edge>[] }
142
+ | { type: 'xyflow.applyNodeChanges'; changes: NodeChange<Types.Node>[] }
143
+ | { type: 'xyflow.applyEdgeChanges'; changes: EdgeChange<Types.Edge>[] }
141
144
  | { type: 'xyflow.viewportMoved'; viewport: Viewport; manually: boolean }
142
145
  | { type: 'xyflow.nodeClick'; node: Types.Node }
143
146
  | { type: 'xyflow.edgeClick'; edge: Types.Edge }
@@ -166,6 +169,7 @@ export type Events =
166
169
  | { type: 'walkthrough.start'; stepId?: StepEdgeId }
167
170
  | { type: 'walkthrough.step'; direction: 'next' | 'previous' }
168
171
  | { type: 'walkthrough.end' }
172
+ | { type: 'toggle.feature'; feature: FeatureName; forceValue?: boolean }
169
173
 
170
174
  export type ActionArg = { context: DiagramContext; event: Events }
171
175
 
@@ -307,12 +311,24 @@ export const diagramMachine = setup({
307
311
  })
308
312
  })
309
313
  },
314
+
315
+ 'updateFeatures': assign(({ event }) => {
316
+ assertEvent(event, 'update.features')
317
+ return {
318
+ features: {
319
+ ...event.features,
320
+ },
321
+ }
322
+ }),
310
323
  },
311
324
  }).createMachine({
312
325
  initial: 'initializing',
313
326
  context: ({ input, self, spawn }) => ({
314
327
  ...input,
315
328
  features: { ...AllDisabled },
329
+ toggledFeatures: {
330
+ enableReadOnly: true,
331
+ },
316
332
  initialized: false,
317
333
  viewportChangedManually: false,
318
334
  lastOnNavigate: null,
@@ -634,7 +650,7 @@ export const diagramMachine = setup({
634
650
  pending: {
635
651
  entry: enqueueActions(({ enqueue }) => {
636
652
  enqueue.sendTo(c => c.context.syncLayoutActorRef, { type: 'stop' })
637
- enqueue.stopChild('layout')
653
+ enqueue.stopChild('syncLayout')
638
654
  enqueue({
639
655
  type: 'trigger:NavigateTo',
640
656
  params: ({ context }) => ({
@@ -657,15 +673,16 @@ export const diagramMachine = setup({
657
673
  },
658
674
  },
659
675
  })
676
+ enqueue.raise({ type: 'fitDiagram' }, { id: 'fitDiagram', delay: 80 })
660
677
  } else {
661
678
  enqueue({
662
679
  type: 'xyflow:setViewportCenter',
663
680
  params: getBBoxCenter(event.view.bounds),
664
681
  })
682
+ enqueue.raise({ type: 'fitDiagram', duration: 200 }, { id: 'fitDiagram', delay: 25 })
665
683
  }
666
684
  enqueue.assign(updateNavigationHistory)
667
685
  enqueue.assign(mergeXYNodesEdges)
668
- enqueue.raise({ type: 'fitDiagram' }, { id: 'fitDiagram', delay: 75 })
669
686
  }),
670
687
  target: 'done',
671
688
  },
@@ -765,12 +782,12 @@ export const diagramMachine = setup({
765
782
  },
766
783
  },
767
784
  on: {
768
- 'xyflow.applyNodeChages': {
785
+ 'xyflow.applyNodeChanges': {
769
786
  actions: assign({
770
787
  xynodes: ({ context, event }) => applyNodeChanges(event.changes, context.xynodes),
771
788
  }),
772
789
  },
773
- 'xyflow.applyEdgeChages': {
790
+ 'xyflow.applyEdgeChanges': {
774
791
  actions: assign({
775
792
  xyedges: ({ context, event }) => applyEdgeChanges(event.changes, context.xyedges),
776
793
  }),
@@ -838,17 +855,19 @@ export const diagramMachine = setup({
838
855
  },
839
856
  },
840
857
  })
858
+ enqueue.raise({ type: 'fitDiagram' }, { id: 'fitDiagram', delay: 80 })
841
859
  } else {
842
860
  enqueue({
843
861
  type: 'xyflow:setViewportCenter',
844
862
  params: getBBoxCenter(event.view.bounds),
845
863
  })
864
+ enqueue.raise({ type: 'fitDiagram', duration: 200 }, { id: 'fitDiagram', delay: 25 })
846
865
  }
847
866
  } else {
848
867
  enqueue.sendTo(c => c.context.syncLayoutActorRef, { type: 'synced' })
849
- }
850
- if (isAnotherView || !context.viewportChangedManually) {
851
- enqueue.raise({ type: 'fitDiagram' }, { id: 'fitDiagram', delay: 90 })
868
+ if (!context.viewportChangedManually) {
869
+ enqueue.raise({ type: 'fitDiagram' }, { id: 'fitDiagram', delay: 50 })
870
+ }
852
871
  }
853
872
  enqueue.assign(mergeXYNodesEdges)
854
873
  enqueue.assign({
@@ -861,9 +880,7 @@ export const diagramMachine = setup({
861
880
  actions: assign(({ event }) => ({ ...event.inputs })),
862
881
  },
863
882
  'update.features': {
864
- actions: assign({
865
- features: ({ event }) => event.features,
866
- }),
883
+ actions: 'updateFeatures',
867
884
  },
868
885
  'update.nodeData': {
869
886
  actions: assign(updateNodeData),
@@ -919,6 +936,15 @@ export const diagramMachine = setup({
919
936
  guard: 'is dynamic view',
920
937
  target: '.walkthrough',
921
938
  },
939
+ 'toggle.feature': {
940
+ actions: assign({
941
+ toggledFeatures: ({ context, event }) => ({
942
+ ...context.toggledFeatures,
943
+ [`enable${event.feature}`]: event.forceValue ??
944
+ !(context.toggledFeatures[`enable${event.feature}`] ?? context.features[`enable${event.feature}`]),
945
+ }),
946
+ }),
947
+ },
922
948
  },
923
949
  exit: [
924
950
  stopChild('layout'),
@@ -1,5 +1,5 @@
1
1
  import type { ViewChange, ViewId } from '@likec4/core'
2
- import { boxToRect, getBoundsOfRects, getNodeDimensions } from '@xyflow/system'
2
+ import { type Rect, boxToRect, getBoundsOfRects, getNodeDimensions } from '@xyflow/system'
3
3
  import { hasAtLeast, reduce } from 'remeda'
4
4
  import {
5
5
  type ActorRef,
@@ -132,12 +132,7 @@ function createViewChange(parentContext: Machine.DiagramContext): ViewChange.Sav
132
132
 
133
133
  const { nodeLookup } = xystore.getState()
134
134
  const movedNodes = new Set<string>()
135
- let bounds = {
136
- x: 0,
137
- y: 0,
138
- width: 1,
139
- height: 1,
140
- }
135
+ let bounds: Rect | undefined
141
136
 
142
137
  const nodes = reduce([...nodeLookup.values()], (acc, node) => {
143
138
  const dimensions = getNodeDimensions(node)
@@ -154,7 +149,7 @@ function createViewChange(parentContext: Machine.DiagramContext): ViewChange.Sav
154
149
  width: Math.ceil(dimensions.width),
155
150
  height: Math.ceil(dimensions.height),
156
151
  }
157
- bounds = getBoundsOfRects(bounds, rect)
152
+ bounds = bounds ? getBoundsOfRects(bounds, rect) : rect
158
153
  return acc
159
154
  }, {} as ViewChange.SaveManualLayout['layout']['nodes'])
160
155
 
@@ -202,10 +197,12 @@ function createViewChange(parentContext: Machine.DiagramContext): ViewChange.Sav
202
197
  x2: Math.ceil(Math.max(...allX)),
203
198
  y2: Math.ceil(Math.max(...allY)),
204
199
  })
205
- bounds = getBoundsOfRects(bounds, rect)
200
+ bounds = bounds ? getBoundsOfRects(bounds, rect) : rect
206
201
  return acc
207
202
  }, {} as ViewChange.SaveManualLayout['layout']['edges'])
208
203
 
204
+ bounds ??= view.bounds
205
+
209
206
  return {
210
207
  op: 'save-manual-layout',
211
208
  layout: {
@@ -1,20 +1,22 @@
1
- import { ActionIconGroup, Badge, Loader, Stack } from '@mantine/core'
2
- import { openSpotlight } from '@mantine/spotlight'
3
- import { IconFileSymlink, IconFocusCentered, IconSearch } from '@tabler/icons-react'
1
+ import { ActionIconGroup, Badge, Group, Loader, Stack } from '@mantine/core'
2
+ import { IconFileSymlink, IconFocusCentered, IconMenu2 } from '@tabler/icons-react'
4
3
  import { clsx } from 'clsx'
4
+ import { LayoutGroup } from 'framer-motion'
5
5
  import { memo } from 'react'
6
- import { IfNotEnabled, useDiagramEventHandlers, useEnabledFeatures } from '../../../context'
6
+ import { useDiagramEventHandlers, useEnabledFeatures } from '../../../context'
7
7
  import { useMantinePortalProps } from '../../../hooks'
8
8
  import { useDiagram } from '../../../hooks/useDiagram'
9
9
  import { useDiagramSyncLayoutState } from '../../../hooks/useDiagramActor'
10
10
  import { useDiagramContext } from '../../../hooks/useDiagramContext'
11
11
  import { stopPropagation } from '../../../utils'
12
+ import { LikeC4Search } from '../search/LikeC4Search'
12
13
  import { ActionIcon, Tooltip } from './_shared'
13
14
  import { ChangeAutoLayoutButton } from './ChangeAutoLayoutButton'
14
15
  import { LayoutDriftNotification } from './LayoutDriftNotification'
15
16
  import { ManualLayoutToolsButton } from './ManualLayoutToolsButton'
16
17
  import { NavigationButtons } from './NavigationButtons'
17
18
  import * as css from './styles.css'
19
+ import { ToggleReadonly } from './ToggleReadonly'
18
20
 
19
21
  export const Controls = memo(() => {
20
22
  const { viewId, hasLayoutDrift, viewportChanged } = useDiagramContext(c => ({
@@ -25,41 +27,47 @@ export const Controls = memo(() => {
25
27
  const diagram = useDiagram()
26
28
  const {
27
29
  onOpenSource,
30
+ onBurgerMenuClick,
28
31
  } = useDiagramEventHandlers()
29
32
  const {
30
33
  enableControls,
31
34
  enableSearch,
32
35
  enableNavigationButtons,
33
36
  enableReadOnly,
34
- enableEdgeEditing,
35
37
  enableVscode,
36
38
  } = useEnabledFeatures()
37
39
  const notReadOnly = !enableReadOnly
38
40
  const portalProps = useMantinePortalProps()
41
+
39
42
  return (
40
- <>
41
- <Stack
42
- className={clsx(
43
- 'react-flow__panel',
44
- css.panel,
45
- 'likec4-top-left-panel',
46
- )}
47
- align="flex-start"
48
- onClick={stopPropagation}
49
- gap={'xs'}>
50
- {enableNavigationButtons && <NavigationButtons />}
51
- <ActionIconGroup className={css.actionIconGroup} orientation="vertical">
52
- {enableControls && enableSearch && (
53
- <Tooltip label="Search (Ctrl+F or ⌘+F)" {...portalProps}>
43
+ <Group
44
+ align="flex-start"
45
+ className={clsx(
46
+ 'react-flow__panel',
47
+ css.panel,
48
+ 'likec4-top-left-panel',
49
+ )}
50
+ gap="xs"
51
+ onClick={stopPropagation}
52
+ >
53
+ <Stack align="flex-start" gap="xs">
54
+ <LayoutGroup>
55
+ <Group
56
+ className={clsx(css.navigationButtons, 'likec4-navigation-webview')}
57
+ gap={'xs'}>
58
+ {onBurgerMenuClick && (
54
59
  <ActionIcon
55
60
  onClick={e => {
56
61
  e.stopPropagation()
57
- openSpotlight()
62
+ onBurgerMenuClick()
58
63
  }}>
59
- <IconSearch />
64
+ <IconMenu2 />
60
65
  </ActionIcon>
61
- </Tooltip>
62
- )}
66
+ )}
67
+ {enableNavigationButtons && <NavigationButtons />}
68
+ </Group>
69
+ </LayoutGroup>
70
+ <ActionIconGroup className={css.actionIconGroup} orientation="vertical">
63
71
  {enableVscode && (
64
72
  <Tooltip label="Open source" {...portalProps}>
65
73
  <ActionIcon
@@ -71,6 +79,7 @@ export const Controls = memo(() => {
71
79
  </ActionIcon>
72
80
  </Tooltip>
73
81
  )}
82
+ {enableControls && <ToggleReadonly />}
74
83
  {enableControls && hasLayoutDrift && <LayoutDriftNotification {...portalProps} />}
75
84
  {enableControls && notReadOnly && (
76
85
  <>
@@ -90,9 +99,12 @@ export const Controls = memo(() => {
90
99
  </Tooltip>
91
100
  )}
92
101
  </ActionIconGroup>
93
- {notReadOnly && <SyncLayoutBadge />}
94
102
  </Stack>
95
- </>
103
+ <Group>
104
+ {enableSearch && <LikeC4Search />}
105
+ {notReadOnly && <SyncLayoutBadge />}
106
+ </Group>
107
+ </Group>
96
108
  )
97
109
  })
98
110