@likec4/diagram 1.17.0 → 1.18.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 (53) hide show
  1. package/package.json +24 -18
  2. package/src/LikeC4Diagram.props.ts +27 -12
  3. package/src/LikeC4Diagram.tsx +14 -9
  4. package/src/LikeC4Search.tsx +149 -0
  5. package/src/StaticLikeC4Diagram.tsx +1 -0
  6. package/src/hooks/useDiagramState.ts +8 -3
  7. package/src/hooks/useXYFlow.ts +2 -1
  8. package/src/likec4model/useLikeC4ElementsTree.ts +6 -13
  9. package/src/likec4model/useLikeC4Model.ts +21 -11
  10. package/src/overlays/Overlays.tsx +96 -63
  11. package/src/overlays/edge-details/EdgeDetailsXYFlow.tsx +3 -1
  12. package/src/overlays/edge-details/_types.ts +7 -3
  13. package/src/overlays/edge-details/use-layouted-edge-details.ts +21 -14
  14. package/src/overlays/edge-details/xyflow/ElementNode.tsx +4 -2
  15. package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +4 -4
  16. package/src/overlays/element-details/ElementDetailsCard.tsx +94 -81
  17. package/src/overlays/element-details/TabPanelDeployments.css.ts +69 -0
  18. package/src/overlays/element-details/TabPanelDeployments.tsx +217 -0
  19. package/src/overlays/element-details/TabPanelRelationships.tsx +8 -9
  20. package/src/overlays/element-details/TabPanelStructure.tsx +10 -10
  21. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +1 -1
  22. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +3 -11
  23. package/src/overlays/relationships-of/SelectElement.tsx +2 -2
  24. package/src/overlays/relationships-of/_types.ts +2 -2
  25. package/src/overlays/relationships-of/use-layouted-relationships.ts +18 -19
  26. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +1 -3
  27. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +12 -6
  28. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +0 -1
  29. package/src/state/aligners.ts +473 -0
  30. package/src/state/diagramStore.layout.ts +67 -0
  31. package/src/state/diagramStore.ts +31 -77
  32. package/src/ui/DiagramTitlePanel.tsx +2 -29
  33. package/src/ui/Link.tsx +40 -0
  34. package/src/ui/notation/NotationPanel.tsx +4 -2
  35. package/src/ui/top-left/ChangeAutoLayoutButton.tsx +2 -2
  36. package/src/ui/top-left/ManualLayoutToolsButton.tsx +15 -0
  37. package/src/ui/top-left/TopLeftPanel.tsx +16 -3
  38. package/src/xyflow/edges/EdgeLabel.tsx +4 -4
  39. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +20 -7
  40. package/src/xyflow/nodes/compound/CompoundNode.css.ts +28 -13
  41. package/src/xyflow/nodes/compound/CompoundNode.tsx +51 -19
  42. package/src/xyflow/nodes/element/ElementNode.tsx +153 -92
  43. package/src/xyflow/nodes/element/element.css.ts +53 -11
  44. package/src/xyflow/nodes/shared/ElementIcon.tsx +1 -1
  45. package/src/xyflow/nodes/shared/Toolbar.tsx +24 -8
  46. package/src/xyflow/useLayoutConstraints.ts +1 -1
  47. package/src/xyflow/useXYFlowEvents.ts +18 -5
  48. package/src/ui/OptionsPanel.css.ts +0 -9
  49. package/src/ui/OptionsPanel.tsx +0 -70
  50. package/src/ui/options/EdgeOptions.tsx +0 -142
  51. package/src/ui/options/NodeOptions.tsx +0 -362
  52. package/src/xyflow/nodes/element/ElementDetails.css.ts +0 -101
  53. package/src/xyflow/nodes/element/ElementDetails.tsx +0 -393
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  type BorderStyle,
3
3
  defaultTheme,
4
+ DiagramNode,
4
5
  type Element,
5
6
  ElementShapes,
6
7
  type Fqn,
@@ -40,6 +41,7 @@ import { IconCheck, IconFileSymlink, IconSelector, IconTransform } from '@tabler
40
41
  import { NodeToolbar, type NodeToolbarProps } from '@xyflow/react'
41
42
  import clsx from 'clsx'
42
43
  import { useEffect, useState } from 'react'
44
+ import type { MergeExclusive } from 'type-fest'
43
45
  import { useDiagramState, useDiagramStoreApi, useMantinePortalProps, useUpdateEffect } from '../../../hooks'
44
46
  import { stopPropagation } from '../../utils'
45
47
 
@@ -75,7 +77,7 @@ const Tooltip = MantineTooltip.withProps({
75
77
  })
76
78
 
77
79
  type ToolbarProps = NodeToolbarProps & {
78
- element: Element
80
+ element: DiagramNode
79
81
  onColorPreview: (color: ThemeColor | null) => void
80
82
  }
81
83
 
@@ -86,13 +88,15 @@ export function CompoundToolbar({
86
88
  onColorPreview,
87
89
  ...props
88
90
  }: ToolbarProps) {
91
+ const modelRef = DiagramNode.modelRef(element)
92
+ const deploymentRef = DiagramNode.deploymentRef(element)
89
93
  const targets = [element.id] as NonEmptyArray<Fqn>
90
94
  const diagramApi = useDiagramStoreApi()
91
95
  const {
92
96
  hasGoToSource,
93
97
  enableRelationshipBrowser
94
98
  } = useDiagramState(s => ({
95
- hasGoToSource: !!s.onOpenSourceElement,
99
+ hasGoToSource: !!s.onOpenSource,
96
100
  enableRelationshipBrowser: s.enableRelationshipBrowser
97
101
  }))
98
102
 
@@ -117,7 +121,8 @@ export function CompoundToolbar({
117
121
  elementBorderStyle={element.style?.border}
118
122
  onChange={onChange}
119
123
  />
120
- {hasGoToSource && <GoToSourceButton elementId={element.id} />}
124
+ {hasGoToSource && modelRef && <GoToSourceButton elementId={modelRef} />}
125
+ {hasGoToSource && !modelRef && deploymentRef && <GoToSourceButton deploymentId={deploymentRef} />}
121
126
  {enableRelationshipBrowser && <BrowseRelationshipsButton elementId={element.id} />}
122
127
  </Toolbar>
123
128
  )
@@ -156,13 +161,15 @@ export function ElementToolbar({
156
161
  onColorPreview,
157
162
  ...props
158
163
  }: ToolbarProps) {
164
+ const modelRef = DiagramNode.modelRef(element)
165
+ const deploymentRef = DiagramNode.deploymentRef(element)
159
166
  const targets = [element.id] as NonEmptyArray<Fqn>
160
167
  const diagramApi = useDiagramStoreApi()
161
168
  const {
162
169
  hasGoToSource,
163
170
  enableRelationshipBrowser
164
171
  } = useDiagramState(s => ({
165
- hasGoToSource: !!s.onOpenSourceElement,
172
+ hasGoToSource: !!s.onOpenSource,
166
173
  enableRelationshipBrowser: s.enableRelationshipBrowser
167
174
  }))
168
175
  const portalProps = useMantinePortalProps()
@@ -234,20 +241,21 @@ export function ElementToolbar({
234
241
  onChange={onChange}
235
242
  position="right-end"
236
243
  />
237
- {hasGoToSource && <GoToSourceButton elementId={element.id} />}
244
+ {hasGoToSource && modelRef && <GoToSourceButton elementId={modelRef} />}
245
+ {hasGoToSource && !modelRef && deploymentRef && <GoToSourceButton deploymentId={deploymentRef} />}
238
246
  {enableRelationshipBrowser && <BrowseRelationshipsButton elementId={element.id} />}
239
247
  </Toolbar>
240
248
  )
241
249
  }
242
250
 
243
251
  type ColorButtonProps = Omit<PopoverProps, 'onChange'> & {
244
- element: Element
252
+ element: DiagramNode
245
253
  isOpacityEditable?: boolean
246
254
  onColorPreview: (color: ThemeColor | null) => void
247
255
  onChange: OnStyleChange
248
256
  }
249
257
 
250
- function GoToSourceButton({ elementId }: { elementId: Fqn }) {
258
+ function GoToSourceButton(props: MergeExclusive<{ elementId: Fqn }, { deploymentId: Fqn }>) {
251
259
  const diagramApi = useDiagramStoreApi()
252
260
  const portalProps = useMantinePortalProps()
253
261
  return (
@@ -258,7 +266,15 @@ function GoToSourceButton({ elementId }: { elementId: Fqn }) {
258
266
  color="gray"
259
267
  onClick={e => {
260
268
  e.stopPropagation()
261
- diagramApi.getState().onOpenSourceElement?.(elementId)
269
+ if (props.elementId) {
270
+ diagramApi.getState().onOpenSource?.({
271
+ element: props.elementId
272
+ })
273
+ } else if (props.deploymentId) {
274
+ diagramApi.getState().onOpenSource?.({
275
+ deployment: props.deploymentId
276
+ })
277
+ }
262
278
  }}>
263
279
  <IconFileSymlink stroke={1.8} style={{ width: '70%' }} />
264
280
  </ActionIcon>
@@ -1,4 +1,4 @@
1
- import { invariant, isAncestor, type NodeId, type NonEmptyArray, nonNullable } from '@likec4/core'
1
+ import { type NodeId, type NonEmptyArray, nonNullable } from '@likec4/core'
2
2
  import type { InternalNode, NodeChange, ReactFlowProps, XYPosition } from '@xyflow/react'
3
3
  import { getNodeDimensions } from '@xyflow/system'
4
4
  import { useMemo, useRef } from 'react'
@@ -1,3 +1,4 @@
1
+ import { DiagramNode } from '@likec4/core'
1
2
  import type { ReactFlowProps } from '@xyflow/react'
2
3
  import { useMemo, useRef } from 'react'
3
4
  import { isNonNullish, isTruthy } from 'remeda'
@@ -123,9 +124,11 @@ export function useXYFlowEvents() {
123
124
  nodesSelectable,
124
125
  enableElementDetails,
125
126
  setLastClickedNode,
126
- onOpenSourceElement,
127
+ onOpenSource,
127
128
  openOverlay
128
129
  } = diagramApi.getState()
130
+ const modelRef = DiagramNode.modelRef(xynode.data.element)
131
+ const deploymentRef = DiagramNode.deploymentRef(xynode.data.element)
129
132
  setLastClickedNode(xynode.id)
130
133
  // if we focused on a node, and clicked on another node - focus on the clicked node
131
134
  const shallChangeFocus = !!focusedNodeId && focusedNodeId !== xynode.id
@@ -134,8 +137,16 @@ export function useXYFlowEvents() {
134
137
 
135
138
  let stopPropagation = false
136
139
 
137
- if (clickedRecently && !!onOpenSourceElement) {
138
- onOpenSourceElement(xynode.data.element.id)
140
+ if (clickedRecently && !!onOpenSource && (modelRef || deploymentRef)) {
141
+ if (modelRef) {
142
+ onOpenSource({
143
+ element: modelRef
144
+ })
145
+ } else if (deploymentRef) {
146
+ onOpenSource({
147
+ deployment: deploymentRef
148
+ })
149
+ }
139
150
  stopPropagation = true
140
151
  }
141
152
 
@@ -152,7 +163,7 @@ export function useXYFlowEvents() {
152
163
  stopPropagation = true
153
164
  break
154
165
  }
155
- case !clickedRecently && focusedNodeId === xynode.id && enableElementDetails: {
166
+ case !clickedRecently && modelRef && focusedNodeId === xynode.id && enableElementDetails: {
156
167
  openOverlay({
157
168
  elementDetails: xynode.data.element.id
158
169
  })
@@ -160,7 +171,9 @@ export function useXYFlowEvents() {
160
171
  break
161
172
  }
162
173
  }
163
- } else if (enableElementDetails && (clickedRecently || focusedNodeId === xynode.id) && !onNodeClick) {
174
+ } else if (
175
+ enableElementDetails && modelRef && (clickedRecently || focusedNodeId === xynode.id) && !onNodeClick
176
+ ) {
164
177
  openOverlay({
165
178
  elementDetails: xynode.data.element.id
166
179
  })
@@ -1,9 +0,0 @@
1
- import { fallbackVar, style } from '@vanilla-extract/css'
2
- import { mantine, vars } from '../theme-vars'
3
-
4
- export const panel = style({
5
- top: fallbackVar(vars.optionsPanel.top, '1rem'),
6
- right: fallbackVar(vars.optionsPanel.right, '1rem'),
7
- margin: 0,
8
- boxShadow: mantine.shadows.xl
9
- })
@@ -1,70 +0,0 @@
1
- import { Card } from '@mantine/core'
2
- import { useOnSelectionChange } from '@xyflow/react'
3
- import clsx from 'clsx'
4
- import { deepEqual as eq } from 'fast-equals'
5
- import { AnimatePresence, m } from 'framer-motion'
6
- import { useState } from 'react'
7
- import { useDiagramState } from '../hooks/useDiagramState'
8
- import { NodeOptions } from './options/NodeOptions'
9
- import * as styles from './OptionsPanel.css'
10
-
11
- /**
12
- * @deprecated
13
- */
14
- export function OptionsPanel() {
15
- const { isFocused, viewId } = useDiagramState(s => ({
16
- viewId: s.view.id,
17
- isFocused: s.focusedNodeId !== null || s.activeWalkthrough !== null
18
- }))
19
- const [selected, setSelected] = useState<{
20
- nodes: string[]
21
- edges: string[]
22
- }>({
23
- nodes: [],
24
- edges: []
25
- })
26
- useOnSelectionChange({
27
- onChange: ({ nodes, edges }) => {
28
- const newSelected = {
29
- nodes: nodes.map(n => n.id).sort(),
30
- edges: edges.map(e => e.id).sort()
31
- }
32
- if (!eq(selected, newSelected)) {
33
- setSelected(newSelected)
34
- }
35
- }
36
- })
37
- const { nodes } = selected
38
- // const hasAnySelection = !isFocused && (nodes.length > 0 || edges.length > 0)
39
- const hasAnySelection = !isFocused && nodes.length > 0
40
- return (
41
- <AnimatePresence mode="wait">
42
- {hasAnySelection && (
43
- <m.div
44
- key={viewId}
45
- initial={{ opacity: 0, scale: 0.85 }}
46
- animate={{ opacity: 1, scale: 1 }}
47
- exit={{
48
- opacity: 0,
49
- scale: 0.85
50
- }}
51
- transition={{ duration: 0.14 }}
52
- className={clsx('react-flow__panel', styles.panel)}
53
- style={{
54
- transformOrigin: 'center right',
55
- maxWidth: '320px'
56
- }}
57
- >
58
- <Card shadow="md">
59
- {nodes.length > 0 && <NodeOptions selectedNodeIds={nodes} />}
60
- {
61
- /* nodes.length === 0 && edges.length > 0 && (
62
- <EdgeOptions selectedEdgeIds={edges} />
63
- ) */
64
- }
65
- </Card>
66
- </m.div>
67
- )}
68
- </AnimatePresence>
69
- )
70
- }
@@ -1,142 +0,0 @@
1
- import { DefaultRelationshipColor, defaultTheme, type NonEmptyArray, type ThemeColor } from '@likec4/core'
2
- import { CheckIcon, ColorSwatch, Divider, Flex, rem, Stack, Text, Tooltip, TooltipGroup } from '@mantine/core'
3
- import { shallowEqual } from 'fast-equals'
4
- import { memo } from 'react'
5
- import { hasAtLeast, keys, takeWhile } from 'remeda'
6
- import { useDiagramStoreApi, useXYEdgesData } from '../../hooks'
7
- import { type XYFlowEdge } from '../../xyflow/types'
8
-
9
- const {
10
- primary,
11
- secondary,
12
- muted,
13
- ...otherColors
14
- } = defaultTheme.elements
15
-
16
- export const themedColors = [
17
- { key: 'primary', value: primary.fill },
18
- { key: 'secondary', value: secondary.fill },
19
- { key: 'muted', value: muted.fill }
20
- ] satisfies Array<{ key: ThemeColor; value: string }>
21
-
22
- export const colors = keys(otherColors).map(key => ({
23
- key,
24
- value: defaultTheme.elements[key].fill
25
- }))
26
-
27
- export type ThemeColorKey = typeof themedColors[0]['key']
28
- export type ColorKey = typeof colors[0]['key']
29
-
30
- type XYEdgeData = Pick<XYFlowEdge, 'id' | 'data'>
31
-
32
- export const EdgeOptions = memo<{ selectedEdgeIds: string[] }>(({ selectedEdgeIds }) => {
33
- const diagramApi = useDiagramStoreApi()
34
- const edges = useXYEdgesData(selectedEdgeIds)
35
- if (!hasAtLeast(edges, 1)) {
36
- return null
37
- }
38
- if (edges.length !== selectedEdgeIds.length) {
39
- throw new Error('EdgeOptions: edges and props.edges should have the same length')
40
- }
41
- const [firstEdge, ...rest] = edges
42
-
43
- return (
44
- <Stack gap={'xs'}>
45
- <div>
46
- <Text fz={rem(9)} fw={'500'} c={'dimmed'}>
47
- RELATIONSHIP{rest.length > 0 ? 'S' : ''}
48
- </Text>
49
- <Text size="xs" c={rest.length > 0 ? 'dimmed' : ''} truncate>
50
- {rest.length === 0 ? firstEdge.data.edge.label : `[ multiple ]`}
51
- </Text>
52
- </div>
53
- {
54
- /* {showShapeOption && (
55
- <ShapeOption
56
- nodes={edges}
57
- onShapeChange={(shape: ElementShape) => {
58
- triggerChange({ shape })
59
- }} />
60
- )} */
61
- }
62
- <Colors
63
- edges={edges}
64
- onColorChange={(_color: ColorKey | ThemeColorKey) => {
65
- // triggerChange({ color })
66
- }} />
67
- </Stack>
68
- )
69
- }, (prevProps, nextProps) => shallowEqual(prevProps.selectedEdgeIds, nextProps.selectedEdgeIds))
70
-
71
- function Colors({
72
- edges: [firstEdge, ...edges],
73
- onColorChange
74
- }: {
75
- edges: NonEmptyArray<XYEdgeData>
76
- onColorChange: (color: ColorKey | ThemeColorKey) => void
77
- }) {
78
- let selectedColor = (firstEdge.data.edge.color ?? DefaultRelationshipColor) as ThemeColor | null
79
- takeWhile(edges, edge => {
80
- const color = edge.data.edge.color ?? DefaultRelationshipColor
81
- if (color !== selectedColor) {
82
- selectedColor = null
83
- return false
84
- }
85
- return true
86
- })
87
- const changeColor = (color: ColorKey | ThemeColorKey) => () => {
88
- if (selectedColor === color) {
89
- return
90
- }
91
- onColorChange(color)
92
- }
93
- return (
94
- <div>
95
- <TooltipGroup openDelay={400} closeDelay={300}>
96
- <Divider label="color" labelPosition="left" />
97
- <Flex mt={'xs'} maw={150} gap="xs" justify="flex-start" align="flex-start" direction="row" wrap="wrap">
98
- {themedColors.map(({ key, value }) => (
99
- <Tooltip
100
- key={key}
101
- label={key}
102
- fz={'xs'}
103
- color="dark"
104
- offset={2}
105
- transitionProps={{ duration: 140, transition: 'slide-up' }}>
106
- <ColorSwatch
107
- color={value}
108
- size={20}
109
- withShadow
110
- onClick={changeColor(key)}
111
- style={{ color: '#fff', cursor: 'pointer' }}
112
- >
113
- {selectedColor === key && <CheckIcon style={{ width: rem(12), height: rem(12) }} />}
114
- </ColorSwatch>
115
- </Tooltip>
116
- ))}
117
- </Flex>
118
-
119
- <Flex mt="sm" maw={150} gap="xs" justify="flex-start" align="flex-start" direction="row" wrap="wrap">
120
- {colors.map(({ key, value }) => (
121
- <Tooltip
122
- key={key}
123
- label={key}
124
- fz={'xs'}
125
- color="dark"
126
- offset={2}
127
- transitionProps={{ duration: 140, transition: 'slide-up' }}>
128
- <ColorSwatch
129
- color={value}
130
- size={20}
131
- onClick={changeColor(key)}
132
- style={{ color: '#fff', cursor: 'pointer' }}
133
- >
134
- {selectedColor === key && <CheckIcon style={{ width: rem(12), height: rem(12) }} />}
135
- </ColorSwatch>
136
- </Tooltip>
137
- ))}
138
- </Flex>
139
- </TooltipGroup>
140
- </div>
141
- )
142
- }