@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.
- package/package.json +24 -18
- package/src/LikeC4Diagram.props.ts +27 -12
- package/src/LikeC4Diagram.tsx +14 -9
- package/src/LikeC4Search.tsx +149 -0
- package/src/StaticLikeC4Diagram.tsx +1 -0
- package/src/hooks/useDiagramState.ts +8 -3
- package/src/hooks/useXYFlow.ts +2 -1
- package/src/likec4model/useLikeC4ElementsTree.ts +6 -13
- package/src/likec4model/useLikeC4Model.ts +21 -11
- package/src/overlays/Overlays.tsx +96 -63
- package/src/overlays/edge-details/EdgeDetailsXYFlow.tsx +3 -1
- package/src/overlays/edge-details/_types.ts +7 -3
- package/src/overlays/edge-details/use-layouted-edge-details.ts +21 -14
- package/src/overlays/edge-details/xyflow/ElementNode.tsx +4 -2
- package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +4 -4
- package/src/overlays/element-details/ElementDetailsCard.tsx +94 -81
- package/src/overlays/element-details/TabPanelDeployments.css.ts +69 -0
- package/src/overlays/element-details/TabPanelDeployments.tsx +217 -0
- package/src/overlays/element-details/TabPanelRelationships.tsx +8 -9
- package/src/overlays/element-details/TabPanelStructure.tsx +10 -10
- package/src/overlays/relationships-of/RelationshipsOverlay.tsx +1 -1
- package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +3 -11
- package/src/overlays/relationships-of/SelectElement.tsx +2 -2
- package/src/overlays/relationships-of/_types.ts +2 -2
- package/src/overlays/relationships-of/use-layouted-relationships.ts +18 -19
- package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +1 -3
- package/src/overlays/relationships-of/xyflow/ElementNode.tsx +12 -6
- package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +0 -1
- package/src/state/aligners.ts +473 -0
- package/src/state/diagramStore.layout.ts +67 -0
- package/src/state/diagramStore.ts +31 -77
- package/src/ui/DiagramTitlePanel.tsx +2 -29
- package/src/ui/Link.tsx +40 -0
- package/src/ui/notation/NotationPanel.tsx +4 -2
- package/src/ui/top-left/ChangeAutoLayoutButton.tsx +2 -2
- package/src/ui/top-left/ManualLayoutToolsButton.tsx +15 -0
- package/src/ui/top-left/TopLeftPanel.tsx +16 -3
- package/src/xyflow/edges/EdgeLabel.tsx +4 -4
- package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +20 -7
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +28 -13
- package/src/xyflow/nodes/compound/CompoundNode.tsx +51 -19
- package/src/xyflow/nodes/element/ElementNode.tsx +153 -92
- package/src/xyflow/nodes/element/element.css.ts +53 -11
- package/src/xyflow/nodes/shared/ElementIcon.tsx +1 -1
- package/src/xyflow/nodes/shared/Toolbar.tsx +24 -8
- package/src/xyflow/useLayoutConstraints.ts +1 -1
- package/src/xyflow/useXYFlowEvents.ts +18 -5
- package/src/ui/OptionsPanel.css.ts +0 -9
- package/src/ui/OptionsPanel.tsx +0 -70
- package/src/ui/options/EdgeOptions.tsx +0 -142
- package/src/ui/options/NodeOptions.tsx +0 -362
- package/src/xyflow/nodes/element/ElementDetails.css.ts +0 -101
- package/src/xyflow/nodes/element/ElementDetails.tsx +0 -393
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ComputedNode,
|
|
2
3
|
DiagramNode,
|
|
3
4
|
DiagramView,
|
|
4
5
|
EdgeId,
|
|
5
|
-
ElementKind,
|
|
6
6
|
ElementNotation,
|
|
7
7
|
Fqn,
|
|
8
8
|
NodeId,
|
|
9
9
|
ViewChange,
|
|
10
|
-
|
|
10
|
+
ViewId,
|
|
11
11
|
XYPoint
|
|
12
12
|
} from '@likec4/core'
|
|
13
13
|
import {
|
|
@@ -17,17 +17,16 @@ import {
|
|
|
17
17
|
isStepEdgeId,
|
|
18
18
|
nonexhaustive,
|
|
19
19
|
nonNullable,
|
|
20
|
-
StepEdgeId
|
|
20
|
+
type StepEdgeId
|
|
21
21
|
} from '@likec4/core'
|
|
22
22
|
import {
|
|
23
23
|
applyEdgeChanges,
|
|
24
24
|
applyNodeChanges,
|
|
25
25
|
getViewportForBounds,
|
|
26
|
-
type InternalNode,
|
|
27
26
|
type OnEdgesChange,
|
|
28
27
|
type OnNodesChange
|
|
29
28
|
} from '@xyflow/react'
|
|
30
|
-
import { boxToRect, getBoundsOfRects, getNodeDimensions
|
|
29
|
+
import { boxToRect, getBoundsOfRects, getNodeDimensions } from '@xyflow/system'
|
|
31
30
|
import { DEV } from 'esm-env'
|
|
32
31
|
import { deepEqual as eq, shallowEqual } from 'fast-equals'
|
|
33
32
|
import type { MouseEvent as ReactMouseEvent } from 'react'
|
|
@@ -46,9 +45,9 @@ import type {
|
|
|
46
45
|
import { type Vector, vector } from '../utils/vector'
|
|
47
46
|
import { MinZoom } from '../xyflow/const'
|
|
48
47
|
import type { XYFlowEdge, XYFlowInstance, XYFlowNode } from '../xyflow/types'
|
|
49
|
-
import { createLayoutConstraints } from '../xyflow/useLayoutConstraints'
|
|
50
48
|
import { bezierControlPoints, isInside, isSamePoint, toDomPrecision } from '../xyflow/utils'
|
|
51
49
|
import { diagramViewToXYFlowData } from './diagram-to-xyflow'
|
|
50
|
+
import { align, type AlignmentMode } from './diagramStore.layout'
|
|
52
51
|
|
|
53
52
|
type RequiredOrNull<T> = {
|
|
54
53
|
[P in keyof T]-?: NonNullable<T[P]> | null
|
|
@@ -73,6 +72,7 @@ export type DiagramInitialState = {
|
|
|
73
72
|
enableElementDetails: boolean
|
|
74
73
|
enableRelationshipBrowser: boolean
|
|
75
74
|
enableRelationshipDetails: boolean
|
|
75
|
+
enableSearch: boolean
|
|
76
76
|
renderIcon: ElementIconRenderer | null
|
|
77
77
|
whereFilter: WhereOperator<string, string> | null
|
|
78
78
|
// If Dynamic View
|
|
@@ -85,7 +85,7 @@ export type DiagramInitialState = {
|
|
|
85
85
|
getContainer: () => HTMLDivElement | null
|
|
86
86
|
} & RequiredOrNull<LikeC4DiagramEventHandlers>
|
|
87
87
|
|
|
88
|
-
type
|
|
88
|
+
type NodeKind = ComputedNode['kind']
|
|
89
89
|
|
|
90
90
|
const StringSet = Set<string>
|
|
91
91
|
|
|
@@ -132,12 +132,12 @@ export type DiagramState = Simplify<
|
|
|
132
132
|
dimmed: ReadonlySet<string>
|
|
133
133
|
|
|
134
134
|
lastOnNavigate: null | {
|
|
135
|
-
fromView:
|
|
136
|
-
toView:
|
|
137
|
-
fromNode: NodeId
|
|
135
|
+
fromView: ViewId
|
|
136
|
+
toView: ViewId
|
|
137
|
+
fromNode: NodeId | null
|
|
138
138
|
}
|
|
139
139
|
navigationHistory: Array<{
|
|
140
|
-
viewId:
|
|
140
|
+
viewId: ViewId
|
|
141
141
|
nodeId: NodeId | null
|
|
142
142
|
}>
|
|
143
143
|
navigationHistoryIndex: number
|
|
@@ -186,10 +186,12 @@ export type DiagramState = Simplify<
|
|
|
186
186
|
onNodesChange: OnNodesChange<XYFlowNode>
|
|
187
187
|
onEdgesChange: OnEdgesChange<XYFlowEdge>
|
|
188
188
|
|
|
189
|
-
highlightByElementNotation: (notation: ElementNotation, onlyOfKind?:
|
|
189
|
+
highlightByElementNotation: (notation: ElementNotation, onlyOfKind?: NodeKind) => void
|
|
190
190
|
|
|
191
191
|
resetEdgeControlPoints: () => void
|
|
192
192
|
align: (mode: AlignmentMode) => void
|
|
193
|
+
|
|
194
|
+
onOpenSourceView: () => void
|
|
193
195
|
}
|
|
194
196
|
>
|
|
195
197
|
|
|
@@ -850,10 +852,25 @@ export function createDiagramStore(props: DiagramInitialState) {
|
|
|
850
852
|
}
|
|
851
853
|
},
|
|
852
854
|
|
|
855
|
+
onOpenSourceView: () => {
|
|
856
|
+
const { view, onOpenSource } = get()
|
|
857
|
+
onOpenSource?.({
|
|
858
|
+
view: view.id
|
|
859
|
+
})
|
|
860
|
+
},
|
|
861
|
+
|
|
853
862
|
openOverlay: (overlay) => {
|
|
854
863
|
if (eq(overlay, get().activeOverlay)) {
|
|
855
864
|
return
|
|
856
865
|
}
|
|
866
|
+
if ('elementDetails' in overlay) {
|
|
867
|
+
const diagramNode = get().view.nodes.find(({ id }) => id === overlay.elementDetails)
|
|
868
|
+
if (!diagramNode || !diagramNode.modelRef) {
|
|
869
|
+
get().closeOverlay()
|
|
870
|
+
return
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
857
874
|
set(
|
|
858
875
|
{
|
|
859
876
|
activeWalkthrough: null,
|
|
@@ -992,7 +1009,7 @@ export function createDiagramStore(props: DiagramInitialState) {
|
|
|
992
1009
|
}
|
|
993
1010
|
},
|
|
994
1011
|
|
|
995
|
-
highlightByElementNotation: (notation: ElementNotation, onlyOfKind?:
|
|
1012
|
+
highlightByElementNotation: (notation: ElementNotation, onlyOfKind?: NodeKind) => {
|
|
996
1013
|
const { xynodes, xyedges } = get()
|
|
997
1014
|
const dimmed = new StringSet(map(xyedges, prop('id')))
|
|
998
1015
|
xynodes.forEach(({ id, data }) => {
|
|
@@ -1068,70 +1085,7 @@ export function createDiagramStore(props: DiagramInitialState) {
|
|
|
1068
1085
|
return vector(v).mul(scale).add(nodeCenter)
|
|
1069
1086
|
}
|
|
1070
1087
|
},
|
|
1071
|
-
align: (
|
|
1072
|
-
const { scheduleSaveManualLayout, xystore } = get()
|
|
1073
|
-
const { nodeLookup, parentLookup } = xystore.getState()
|
|
1074
|
-
|
|
1075
|
-
const selectedNodes = new Set(nodeLookup.values().filter(n => n.selected).map(n => n.id))
|
|
1076
|
-
const nodesToAlign = [...selectedNodes.difference(new Set(parentLookup.keys()))]
|
|
1077
|
-
|
|
1078
|
-
if (!hasAtLeast(nodesToAlign, 2)) {
|
|
1079
|
-
console.warn('At least 2 nodes must be selected to align')
|
|
1080
|
-
return
|
|
1081
|
-
}
|
|
1082
|
-
const constraints = createLayoutConstraints(xystore, nodesToAlign)
|
|
1083
|
-
|
|
1084
|
-
let getEdgePosition: (nodes: InternalNode<XYFlowNode>[]) => number
|
|
1085
|
-
let getPosition: (alignTo: number, node: InternalNode<XYFlowNode>) => number
|
|
1086
|
-
let propertyToEdit: keyof XYPosition
|
|
1087
|
-
|
|
1088
|
-
switch (mode) {
|
|
1089
|
-
case 'Left':
|
|
1090
|
-
getEdgePosition = nodes => Math.min(...nodes.map(n => n.internals.positionAbsolute.x))
|
|
1091
|
-
propertyToEdit = 'x'
|
|
1092
|
-
getPosition = (alignTo, _) => Math.floor(alignTo)
|
|
1093
|
-
break
|
|
1094
|
-
case 'Top':
|
|
1095
|
-
getEdgePosition = nodes => Math.min(...nodes.map(n => n.internals.positionAbsolute.y))
|
|
1096
|
-
propertyToEdit = 'y'
|
|
1097
|
-
getPosition = (alignTo, _) => Math.floor(alignTo)
|
|
1098
|
-
break
|
|
1099
|
-
case 'Right':
|
|
1100
|
-
getEdgePosition = nodes =>
|
|
1101
|
-
Math.max(...nodes.map(n => n.internals.positionAbsolute.x + getNodeDimensions(n).width))
|
|
1102
|
-
propertyToEdit = 'x'
|
|
1103
|
-
getPosition = (alignTo, node) => Math.floor(alignTo - node.width!)
|
|
1104
|
-
break
|
|
1105
|
-
case 'Bottom':
|
|
1106
|
-
getEdgePosition = nodes =>
|
|
1107
|
-
Math.max(...nodes.map(n => n.internals.positionAbsolute.y + getNodeDimensions(n).height))
|
|
1108
|
-
propertyToEdit = 'y'
|
|
1109
|
-
getPosition = (alignTo, node) => Math.floor(alignTo - node.height!)
|
|
1110
|
-
break
|
|
1111
|
-
case 'Center':
|
|
1112
|
-
getEdgePosition = nodes =>
|
|
1113
|
-
Math.max(...nodes.map(n => n.internals.positionAbsolute.x + getNodeDimensions(n).width / 2))
|
|
1114
|
-
propertyToEdit = 'x'
|
|
1115
|
-
getPosition = (alignTo, node) => Math.floor(alignTo - getNodeDimensions(node).width / 2)
|
|
1116
|
-
break
|
|
1117
|
-
case 'Middle':
|
|
1118
|
-
getEdgePosition = nodes =>
|
|
1119
|
-
Math.max(...nodes.map(n => n.internals.positionAbsolute.y + getNodeDimensions(n).height / 2))
|
|
1120
|
-
propertyToEdit = 'y'
|
|
1121
|
-
getPosition = (alignTo, node) => Math.floor(alignTo - getNodeDimensions(node).height / 2)
|
|
1122
|
-
break
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
constraints.onMove((nodes) => {
|
|
1126
|
-
const alignTo = getEdgePosition(nodes.map(({ node }) => node))
|
|
1127
|
-
|
|
1128
|
-
nodes.forEach(({ rect, node }) => {
|
|
1129
|
-
rect.positionAbsolute = { ...rect.positionAbsolute, [propertyToEdit]: getPosition(alignTo, node) }
|
|
1130
|
-
})
|
|
1131
|
-
})
|
|
1132
|
-
|
|
1133
|
-
scheduleSaveManualLayout()
|
|
1134
|
-
}
|
|
1088
|
+
align: align(get)
|
|
1135
1089
|
} satisfies DiagramState),
|
|
1136
1090
|
{
|
|
1137
1091
|
name: `${storeDevId} - ${props.view.id}`,
|
|
@@ -5,6 +5,7 @@ import clsx from 'clsx'
|
|
|
5
5
|
import { AnimatePresence, m } from 'framer-motion'
|
|
6
6
|
import { type DiagramState, useDiagramState } from '../hooks'
|
|
7
7
|
import * as css from './DiagramTitlePanel.css'
|
|
8
|
+
import { Link } from './Link'
|
|
8
9
|
|
|
9
10
|
const selector = (s: DiagramState) => ({
|
|
10
11
|
id: s.view.id,
|
|
@@ -151,35 +152,7 @@ export default function DiagramTitlePanel() {
|
|
|
151
152
|
gap={3}
|
|
152
153
|
justify="stretch"
|
|
153
154
|
align="stretch">
|
|
154
|
-
{links.map((link) =>
|
|
155
|
-
<Group key={link.url} wrap="nowrap" align="center" gap={'sm'}>
|
|
156
|
-
<Box flex={'1'} style={{ overflow: 'hidden' }}>
|
|
157
|
-
<Anchor
|
|
158
|
-
href={link.url}
|
|
159
|
-
target="_blank"
|
|
160
|
-
fz="xs"
|
|
161
|
-
truncate="end"
|
|
162
|
-
display={'inline-block'}
|
|
163
|
-
w={'100%'}>
|
|
164
|
-
{link.title || link.url}
|
|
165
|
-
</Anchor>
|
|
166
|
-
</Box>
|
|
167
|
-
<CopyButton value={link.url}>
|
|
168
|
-
{({ copied, copy }) => (
|
|
169
|
-
<Button
|
|
170
|
-
size="compact-xs"
|
|
171
|
-
fz={'10'}
|
|
172
|
-
variant="light"
|
|
173
|
-
onClick={copy}
|
|
174
|
-
color={copied
|
|
175
|
-
? 'teal'
|
|
176
|
-
: 'gray'}>
|
|
177
|
-
{copied ? 'copied' : 'copy'}
|
|
178
|
-
</Button>
|
|
179
|
-
)}
|
|
180
|
-
</CopyButton>
|
|
181
|
-
</Group>
|
|
182
|
-
))}
|
|
155
|
+
{links.map((link) => <Link link={link} />)}
|
|
183
156
|
</Stack>
|
|
184
157
|
)}
|
|
185
158
|
</>
|
package/src/ui/Link.tsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Link } from '@likec4/core'
|
|
2
|
+
import { Anchor, Box, Button, CopyButton, Group } from '@mantine/core'
|
|
3
|
+
|
|
4
|
+
export function Link({ link }: { link: Link }) {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
<Group key={link.url} wrap="nowrap" align="center" gap={'sm'}>
|
|
8
|
+
<Box flex={'1'} style={{ overflow: 'hidden' }}>
|
|
9
|
+
<Anchor
|
|
10
|
+
href={link.url}
|
|
11
|
+
target="_blank"
|
|
12
|
+
fz="xs"
|
|
13
|
+
truncate="end"
|
|
14
|
+
display={'inline-block'}
|
|
15
|
+
w={'100%'}
|
|
16
|
+
onClick={(e) => e.stopPropagation()}>
|
|
17
|
+
{link.title || link.url}
|
|
18
|
+
</Anchor>
|
|
19
|
+
</Box>
|
|
20
|
+
<CopyButton value={link.url}>
|
|
21
|
+
{({ copied, copy }) => (
|
|
22
|
+
<Button
|
|
23
|
+
size="compact-xs"
|
|
24
|
+
fz={'10'}
|
|
25
|
+
variant="light"
|
|
26
|
+
onClick={(e) => {
|
|
27
|
+
e.stopPropagation()
|
|
28
|
+
copy()
|
|
29
|
+
}}
|
|
30
|
+
color={copied
|
|
31
|
+
? 'teal'
|
|
32
|
+
: 'gray'}>
|
|
33
|
+
{copied ? 'copied' : 'copy'}
|
|
34
|
+
</Button>
|
|
35
|
+
)}
|
|
36
|
+
</CopyButton>
|
|
37
|
+
</Group>
|
|
38
|
+
</>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ComputedNode, type ElementNotation as ElementNotationData } from '@likec4/core'
|
|
2
2
|
import {
|
|
3
3
|
ActionIcon,
|
|
4
4
|
Badge,
|
|
@@ -29,13 +29,15 @@ import { vars } from '../../theme-vars'
|
|
|
29
29
|
import { ElementShapeSvg } from '../../xyflow/nodes/element/ElementShapeSvg'
|
|
30
30
|
import * as css from './NotationPanel.css'
|
|
31
31
|
|
|
32
|
+
type NodeKind = ComputedNode['kind']
|
|
33
|
+
|
|
32
34
|
const ElementNotation = ({ value }: { value: ElementNotationData }) => {
|
|
33
35
|
const {
|
|
34
36
|
title,
|
|
35
37
|
color = 'primary',
|
|
36
38
|
shape = 'rectangle'
|
|
37
39
|
} = value
|
|
38
|
-
const [onlyKind, setOnlyKind] = useState<
|
|
40
|
+
const [onlyKind, setOnlyKind] = useState<NodeKind | null>(null)
|
|
39
41
|
const diagramStore = useDiagramStoreApi()
|
|
40
42
|
const w = 300
|
|
41
43
|
const h = 200
|
|
@@ -137,7 +137,7 @@ export const ChangeAutoLayoutButton = (props: PopoverProps) => {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
const MAX_SPACING = 400
|
|
140
|
-
const SpacingSliders = forwardRef<HTMLDivElement, {
|
|
140
|
+
const SpacingSliders = forwardRef<HTMLDivElement | null, {
|
|
141
141
|
isVertical: boolean
|
|
142
142
|
nodeSep: number | undefined
|
|
143
143
|
rankSep: number | undefined
|
|
@@ -160,7 +160,7 @@ const SpacingSliders = forwardRef<HTMLDivElement, {
|
|
|
160
160
|
onChange(Math.round(x * MAX_SPACING), Math.round(y * MAX_SPACING))
|
|
161
161
|
},
|
|
162
162
|
[onChange, isVertical],
|
|
163
|
-
|
|
163
|
+
250,
|
|
164
164
|
2000
|
|
165
165
|
)
|
|
166
166
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
IconLayoutAlignMiddle,
|
|
7
7
|
IconLayoutAlignRight,
|
|
8
8
|
IconLayoutAlignTop,
|
|
9
|
+
IconLayoutBoardSplit,
|
|
9
10
|
IconLayoutCollage,
|
|
10
11
|
IconRouteOff
|
|
11
12
|
} from '@tabler/icons-react'
|
|
@@ -53,6 +54,13 @@ export const ManualLayoutToolsButton = (props: PopoverProps) => {
|
|
|
53
54
|
<PopoverDropdown p={0}>
|
|
54
55
|
<Group gap={'xs'}>
|
|
55
56
|
<ActionIconGroup pos={'relative'}>
|
|
57
|
+
<Action
|
|
58
|
+
label="Align in columns"
|
|
59
|
+
icon={<IconLayoutBoardSplit />}
|
|
60
|
+
onClick={e => {
|
|
61
|
+
e.stopPropagation()
|
|
62
|
+
store.getState().align('Column')
|
|
63
|
+
}} />
|
|
56
64
|
<Action
|
|
57
65
|
label="Align left"
|
|
58
66
|
icon={<IconLayoutAlignLeft />}
|
|
@@ -74,6 +82,13 @@ export const ManualLayoutToolsButton = (props: PopoverProps) => {
|
|
|
74
82
|
e.stopPropagation()
|
|
75
83
|
store.getState().align('Right')
|
|
76
84
|
}} />
|
|
85
|
+
<Action
|
|
86
|
+
label="Align in rows"
|
|
87
|
+
icon={<IconLayoutBoardSplit style={{ transform: 'rotate(90deg)' }} />}
|
|
88
|
+
onClick={e => {
|
|
89
|
+
e.stopPropagation()
|
|
90
|
+
store.getState().align('Row')
|
|
91
|
+
}} />
|
|
77
92
|
<Action
|
|
78
93
|
label="Align top"
|
|
79
94
|
icon={<IconLayoutAlignTop />}
|
|
@@ -12,13 +12,15 @@ import {
|
|
|
12
12
|
Text,
|
|
13
13
|
TooltipGroup
|
|
14
14
|
} from '@mantine/core'
|
|
15
|
+
import { openSpotlight } from '@mantine/spotlight'
|
|
15
16
|
import {
|
|
16
17
|
IconAlertTriangle,
|
|
17
18
|
IconChevronLeft,
|
|
18
19
|
IconChevronRight,
|
|
19
20
|
IconFileSymlink,
|
|
20
21
|
IconFocusCentered,
|
|
21
|
-
IconMenu2
|
|
22
|
+
IconMenu2,
|
|
23
|
+
IconSearch
|
|
22
24
|
} from '@tabler/icons-react'
|
|
23
25
|
import clsx from 'clsx'
|
|
24
26
|
import { AnimatePresence, LayoutGroup, m } from 'framer-motion'
|
|
@@ -138,6 +140,7 @@ const LayoutDriftNotification = (props: PopoverProps) => (
|
|
|
138
140
|
export const TopLeftPanel = () => {
|
|
139
141
|
const store = useDiagramStoreApi()
|
|
140
142
|
const {
|
|
143
|
+
enableSearch,
|
|
141
144
|
showNavigationButtons,
|
|
142
145
|
showFitDiagram,
|
|
143
146
|
showLayoutDriftWarning,
|
|
@@ -150,11 +153,12 @@ export const TopLeftPanel = () => {
|
|
|
150
153
|
const isNotFocused = isNullish(s.focusedNodeId)
|
|
151
154
|
const isNotActive = isNotWalkthrough && isNotFocused
|
|
152
155
|
return ({
|
|
156
|
+
enableSearch: s.enableSearch,
|
|
153
157
|
showNavigationButtons: !!s.onBurgerMenuClick || (s.showNavigationButtons && !!s.onNavigateTo),
|
|
154
158
|
showFitDiagram: s.controls && s.fitViewEnabled && s.zoomable && isNotWalkthrough,
|
|
155
159
|
showLayoutDriftWarning: s.controls && !s.readonly && s.view.hasLayoutDrift === true && isNotActive,
|
|
156
160
|
showChangeAutoLayout: s.controls && s.isEditable() && isNotActive,
|
|
157
|
-
showGoToSource: s.controls && !!s.
|
|
161
|
+
showGoToSource: s.controls && !!s.onOpenSource && isNotWalkthrough,
|
|
158
162
|
viewportChanged: s.viewportChanged,
|
|
159
163
|
showManualLayoutTools: s.controls && s.readonly !== true && s.experimentalEdgeEditing === true
|
|
160
164
|
})
|
|
@@ -174,12 +178,21 @@ export const TopLeftPanel = () => {
|
|
|
174
178
|
gap={'xs'}>
|
|
175
179
|
{showNavigationButtons && <BackwardForwardButtons />}
|
|
176
180
|
<ActionIconGroup className={css.actionIconGroup} orientation="vertical">
|
|
181
|
+
{enableSearch && (
|
|
182
|
+
<Tooltip label="Search (Ctrl+F or ⌘+F)">
|
|
183
|
+
<ActionIcon
|
|
184
|
+
onClick={() => openSpotlight()}
|
|
185
|
+
>
|
|
186
|
+
<IconSearch />
|
|
187
|
+
</ActionIcon>
|
|
188
|
+
</Tooltip>
|
|
189
|
+
)}
|
|
177
190
|
{showGoToSource && (
|
|
178
191
|
<Tooltip label="Open source" {...portalProps}>
|
|
179
192
|
<ActionIcon
|
|
180
193
|
onClick={e => {
|
|
181
194
|
e.stopPropagation()
|
|
182
|
-
store.getState().onOpenSourceView
|
|
195
|
+
store.getState().onOpenSourceView()
|
|
183
196
|
}}>
|
|
184
197
|
<IconFileSymlink stroke={1.5} />
|
|
185
198
|
</ActionIcon>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { extractStep, isStepEdgeId, type
|
|
1
|
+
import { extractStep, isStepEdgeId, type ViewId } from '@likec4/core'
|
|
2
2
|
import {
|
|
3
3
|
ActionIcon,
|
|
4
4
|
Box,
|
|
@@ -80,9 +80,9 @@ export const EdgeLabel = ({
|
|
|
80
80
|
</Box>
|
|
81
81
|
)}
|
|
82
82
|
{isTruthy(label?.text) && (
|
|
83
|
-
<
|
|
83
|
+
<Text component="div" className={edgesCss.edgeLabelText} lineClamp={5}>
|
|
84
84
|
{label.text}
|
|
85
|
-
</
|
|
85
|
+
</Text>
|
|
86
86
|
)}
|
|
87
87
|
{navigateTo && <NavigateToBtn viewId={navigateTo} />}
|
|
88
88
|
</Box>
|
|
@@ -162,7 +162,7 @@ const NotePopover = ({ notes, children }: PropsWithChildren<{ notes: string }>)
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
type NavigateToBtnProps = {
|
|
165
|
-
viewId:
|
|
165
|
+
viewId: ViewId
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
function NavigateToBtn({ viewId }: NavigateToBtnProps) {
|
|
@@ -16,11 +16,12 @@ import {
|
|
|
16
16
|
Tooltip as MantineTooltip,
|
|
17
17
|
TooltipGroup
|
|
18
18
|
} from '@mantine/core'
|
|
19
|
-
import { IconArrowRight,
|
|
19
|
+
import { IconArrowRight, IconFileSymlink, IconInfoCircle, IconZoomScan } from '@tabler/icons-react'
|
|
20
20
|
import clsx from 'clsx'
|
|
21
21
|
import { forwardRef, Fragment, type MouseEventHandler, type PropsWithChildren, useCallback } from 'react'
|
|
22
22
|
import { filter, isTruthy, map, partition, pick, pipe } from 'remeda'
|
|
23
23
|
import { useDiagramState, useDiagramStoreApi, useMantinePortalProps, useXYNodesData } from '../../hooks'
|
|
24
|
+
import { Link } from '../../ui/Link'
|
|
24
25
|
import type { RelationshipData } from '../types'
|
|
25
26
|
import * as css from './RelationshipsDropdownMenu.css'
|
|
26
27
|
|
|
@@ -72,7 +73,7 @@ export function RelationshipsDropdownMenu({
|
|
|
72
73
|
}
|
|
73
74
|
}),
|
|
74
75
|
filter(isTruthy),
|
|
75
|
-
partition(r => r.
|
|
76
|
+
partition(r => r.source.id === edge.source && r.target.id === edge.target)
|
|
76
77
|
)
|
|
77
78
|
|
|
78
79
|
const onClickOpenOverlay = useCallback((e: React.MouseEvent): void => {
|
|
@@ -84,7 +85,7 @@ export function RelationshipsDropdownMenu({
|
|
|
84
85
|
}
|
|
85
86
|
}, [edge.id, openOverlay, enableRelationshipBrowser])
|
|
86
87
|
|
|
87
|
-
const renderRelationship = (relationship: LikeC4Model.
|
|
88
|
+
const renderRelationship = (relationship: LikeC4Model.AnyRelation, index: number) => (
|
|
88
89
|
<Fragment key={relationship.id}>
|
|
89
90
|
{index > 0 && <MenuDivider opacity={0.65} />}
|
|
90
91
|
<MenuItem
|
|
@@ -155,7 +156,7 @@ export function RelationshipsDropdownMenu({
|
|
|
155
156
|
const Relationship = forwardRef<
|
|
156
157
|
HTMLDivElement,
|
|
157
158
|
StackProps & {
|
|
158
|
-
relationship: LikeC4Model.
|
|
159
|
+
relationship: LikeC4Model.AnyRelation
|
|
159
160
|
edge: RelationshipData['edge']
|
|
160
161
|
sourceNode: DiagramNode
|
|
161
162
|
targetNode: DiagramNode
|
|
@@ -175,12 +176,13 @@ const Relationship = forwardRef<
|
|
|
175
176
|
hasOnNavigateTo
|
|
176
177
|
} = useDiagramState(s => ({
|
|
177
178
|
viewId: s.view.id,
|
|
178
|
-
hasOnOpenSourceRelation: !!s.
|
|
179
|
+
hasOnOpenSourceRelation: !!s.onOpenSource,
|
|
179
180
|
hasOnNavigateTo: !!s.onNavigateTo
|
|
180
181
|
}))
|
|
181
182
|
const sourceId = nameFromFqn(edge.source) + r.source.id.slice(edge.source.length)
|
|
182
183
|
const targetId = nameFromFqn(edge.target) + r.target.id.slice(edge.target.length)
|
|
183
|
-
const navigateTo = hasOnNavigateTo && r.
|
|
184
|
+
const navigateTo = hasOnNavigateTo && r.navigateTo?.id !== viewId ? r.navigateTo?.id : undefined
|
|
185
|
+
const links = r.links
|
|
184
186
|
|
|
185
187
|
return (
|
|
186
188
|
<Stack ref={ref} className={clsx(css.menuItemRelationship, className)} {...props}>
|
|
@@ -223,7 +225,9 @@ const Relationship = forwardRef<
|
|
|
223
225
|
onPointerDownCapture={stopPropagation}
|
|
224
226
|
onClick={event => {
|
|
225
227
|
event.stopPropagation()
|
|
226
|
-
diagramApi.getState().
|
|
228
|
+
diagramApi.getState().onOpenSource?.({
|
|
229
|
+
relation: r.id
|
|
230
|
+
})
|
|
227
231
|
}}
|
|
228
232
|
role="button"
|
|
229
233
|
>
|
|
@@ -235,6 +239,15 @@ const Relationship = forwardRef<
|
|
|
235
239
|
)}
|
|
236
240
|
</Group>
|
|
237
241
|
<Box className={css.title}>{r.title || 'untitled'}</Box>
|
|
242
|
+
{r.description && <Text size="xs" c="dimmed">{r.description}</Text>}
|
|
243
|
+
{links.length > 0 && (
|
|
244
|
+
<Stack
|
|
245
|
+
gap={3}
|
|
246
|
+
justify="stretch"
|
|
247
|
+
align="stretch">
|
|
248
|
+
{links.map((link) => <Link key={link.url} link={link} />)}
|
|
249
|
+
</Stack>
|
|
250
|
+
)}
|
|
238
251
|
</Stack>
|
|
239
252
|
)
|
|
240
253
|
})
|
|
@@ -49,7 +49,20 @@ globalStyle(`.react-flow__node-compound:has(${dimmed})`, {
|
|
|
49
49
|
willChange: 'opacity, filter'
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
+
globalStyle(`:where([data-mantine-color-scheme='dark'] .likec4-compound-transparent)`, {
|
|
53
|
+
vars: {
|
|
54
|
+
['--_compound-border-color']: `color-mix(in srgb, ${vars.compound.titleColor} 25%, ${vars.element.stroke})`
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
globalStyle(`:where([data-mantine-color-scheme='light'] .likec4-compound-transparent)`, {
|
|
58
|
+
vars: {
|
|
59
|
+
['--_compound-title-color']: vars.element.stroke
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
52
63
|
export const varOpacity = createVar('opacity')
|
|
64
|
+
export const varBorderColor = createVar('border-color')
|
|
65
|
+
export const varBorderRadius = createVar('border-transparency')
|
|
53
66
|
export const varBorderTransparency = createVar('border-transparency')
|
|
54
67
|
|
|
55
68
|
const outlineColor = fallbackVar(
|
|
@@ -62,14 +75,17 @@ export const compoundBody = style({
|
|
|
62
75
|
width: '100%',
|
|
63
76
|
height: '100%',
|
|
64
77
|
position: 'relative',
|
|
65
|
-
borderRadius:
|
|
78
|
+
borderRadius: varBorderRadius,
|
|
66
79
|
boxShadow: '0 4px 10px 0.5px rgba(0,0,0,0.1) , 0 2px 2px -1px rgba(0,0,0,0.4)',
|
|
67
80
|
padding: 0,
|
|
68
81
|
margin: 0,
|
|
69
82
|
transition: 'all 200ms ease-out',
|
|
70
|
-
backgroundClip: 'padding-box',
|
|
83
|
+
// backgroundClip: 'padding-box',
|
|
71
84
|
// overflow: 'hidden',
|
|
72
85
|
cursor: 'default',
|
|
86
|
+
vars: {
|
|
87
|
+
[varBorderRadius]: '6px'
|
|
88
|
+
},
|
|
73
89
|
selectors: {
|
|
74
90
|
[`:where(.react-flow__node.selected) &`]: {
|
|
75
91
|
boxShadow: 'none'
|
|
@@ -88,8 +104,11 @@ export const compoundBody = style({
|
|
|
88
104
|
width: '100%',
|
|
89
105
|
height: '100%',
|
|
90
106
|
pointerEvents: 'none',
|
|
107
|
+
border: '1px solid transparent',
|
|
108
|
+
borderRadius: varBorderRadius,
|
|
91
109
|
transition: 'background 175ms ease-out, opacity 175ms ease-out',
|
|
92
|
-
background: vars.element.fill
|
|
110
|
+
background: vars.element.fill,
|
|
111
|
+
backgroundClip: 'border-box'
|
|
93
112
|
}
|
|
94
113
|
})
|
|
95
114
|
export const compoundTitle = style({
|
|
@@ -97,19 +116,20 @@ export const compoundTitle = style({
|
|
|
97
116
|
alignItems: 'center',
|
|
98
117
|
gap: 6,
|
|
99
118
|
paddingLeft: 14,
|
|
100
|
-
paddingTop:
|
|
119
|
+
paddingTop: 6,
|
|
101
120
|
minHeight: 30,
|
|
121
|
+
paddingRight: 2,
|
|
102
122
|
selectors: {
|
|
103
123
|
[`:where(.react-flow__node.draggable) &`]: {
|
|
104
124
|
cursor: 'grab'
|
|
105
125
|
},
|
|
106
126
|
[`:where(.likec4-compound-transparent) &`]: {
|
|
107
|
-
paddingTop:
|
|
127
|
+
paddingTop: 3
|
|
108
128
|
}
|
|
109
129
|
}
|
|
110
130
|
})
|
|
111
131
|
export const withNavigation = style({
|
|
112
|
-
paddingLeft:
|
|
132
|
+
paddingLeft: 32
|
|
113
133
|
})
|
|
114
134
|
|
|
115
135
|
const opacityDeltaOnHover = createVar('opacityDeltaOnHover')
|
|
@@ -117,8 +137,9 @@ export const transparent = style({
|
|
|
117
137
|
borderStyle: 'dashed',
|
|
118
138
|
borderWidth: 3,
|
|
119
139
|
boxShadow: 'none',
|
|
120
|
-
borderColor: `color-mix(in srgb , ${
|
|
140
|
+
borderColor: `color-mix(in srgb , ${varBorderColor}, transparent ${fallbackVar(varBorderTransparency, '5%')})`,
|
|
121
141
|
vars: {
|
|
142
|
+
[varBorderColor]: `var(--_compound-border-color,${vars.element.stroke})`,
|
|
122
143
|
[opacityDeltaOnHover]: '0'
|
|
123
144
|
},
|
|
124
145
|
':before': {
|
|
@@ -156,12 +177,6 @@ export const title = style({
|
|
|
156
177
|
mixBlendMode: 'screen'
|
|
157
178
|
})
|
|
158
179
|
|
|
159
|
-
globalStyle(`:where([data-mantine-color-scheme='light'] .likec4-compound-transparent)`, {
|
|
160
|
-
vars: {
|
|
161
|
-
['--_compound-title-color']: vars.element.stroke
|
|
162
|
-
}
|
|
163
|
-
})
|
|
164
|
-
|
|
165
180
|
const indicatorKeyframes = keyframes({
|
|
166
181
|
'from': {
|
|
167
182
|
opacity: 0.6
|