@likec4/diagram 1.16.0 → 1.17.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 (47) hide show
  1. package/package.json +7 -7
  2. package/src/LikeC4CustomColors.tsx +24 -21
  3. package/src/LikeC4Diagram.props.ts +9 -9
  4. package/src/LikeC4Diagram.tsx +25 -26
  5. package/src/StaticLikeC4Diagram.tsx +9 -5
  6. package/src/overlays/OverlayContext.tsx +1 -1
  7. package/src/overlays/Overlays.css.ts +28 -23
  8. package/src/overlays/Overlays.tsx +116 -64
  9. package/src/overlays/edge-details/xyflow/ElementNode.tsx +15 -12
  10. package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +7 -3
  11. package/src/overlays/edge-details/xyflow/styles.css.ts +22 -16
  12. package/src/overlays/element-details/ElementDetailsCard.css.ts +322 -0
  13. package/src/overlays/element-details/ElementDetailsCard.tsx +639 -0
  14. package/src/overlays/element-details/TabPanelRelationships.css.ts +106 -0
  15. package/src/overlays/element-details/TabPanelRelationships.tsx +201 -0
  16. package/src/overlays/element-details/TabPanelStructure.css.ts +36 -0
  17. package/src/overlays/element-details/TabPanelStructure.tsx +120 -0
  18. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +185 -0
  19. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +179 -213
  20. package/src/overlays/relationships-of/_types.ts +15 -4
  21. package/src/overlays/relationships-of/use-layouted-relationships.ts +301 -207
  22. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +68 -14
  23. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +54 -14
  24. package/src/overlays/relationships-of/xyflow/EmptyNode.tsx +1 -1
  25. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +51 -23
  26. package/src/overlays/relationships-of/xyflow/styles.css.ts +34 -27
  27. package/src/state/DiagramContext.tsx +15 -8
  28. package/src/state/diagramStore.ts +14 -6
  29. package/src/theme-vars.ts +9 -0
  30. package/src/ui/FramerMotionConfig.tsx +18 -0
  31. package/src/ui/notation/NotationPanel.tsx +3 -3
  32. package/src/xyflow/XYFlow.tsx +18 -35
  33. package/src/xyflow/XYFlowInner.tsx +2 -2
  34. package/src/xyflow/edges/EdgeLabel.tsx +3 -3
  35. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +25 -18
  36. package/src/xyflow/nodes/compound/CompoundNode.css.ts +96 -30
  37. package/src/xyflow/nodes/compound/CompoundNode.tsx +149 -165
  38. package/src/xyflow/nodes/element/ElementDetails.css.ts +101 -0
  39. package/src/xyflow/nodes/element/ElementDetails.tsx +393 -0
  40. package/src/xyflow/nodes/element/ElementNode.tsx +110 -116
  41. package/src/xyflow/nodes/element/element.css.ts +38 -18
  42. package/src/xyflow/nodes/shared/ElementIcon.tsx +43 -0
  43. package/src/xyflow/nodes/shared/Toolbar.tsx +6 -6
  44. package/src/xyflow/{XYFlowEvents.tsx → useXYFlowEvents.ts} +26 -22
  45. package/src/xyflow/utils.ts +12 -3
  46. package/src/LikeC4CustomColors.props.ts +0 -5
  47. /package/src/{mantine → ui}/EnsureMantine.tsx +0 -0
@@ -0,0 +1,106 @@
1
+ import { globalStyle, style } from '@vanilla-extract/css'
2
+ import { mantine, vars, whereDark, whereLight } from '../../theme-vars'
3
+
4
+ export const fqn = style({
5
+ display: 'inline-block',
6
+ fontSize: mantine.fontSizes.sm,
7
+ fontWeight: 500,
8
+ whiteSpace: 'nowrap',
9
+ padding: '3px 6px',
10
+ borderRadius: 3,
11
+ background: `color-mix(in srgb , ${vars.element.fill}, transparent 25%)`,
12
+ lineHeight: 1.2,
13
+ color: vars.element.hiContrast
14
+ // selectors: {
15
+ // [`${whereDark} &`]: {
16
+ // }
17
+ // }
18
+ })
19
+
20
+ export const relationshipStat = style({
21
+ selectors: {
22
+ [`${whereLight} &`]: {
23
+ background: mantine.colors.gray[1]
24
+ },
25
+ // [`&[data-zero]`]: {
26
+ // color: mantine.colors.dimmed
27
+ // },
28
+ // [`${whereLight} &[data-zero]`]: {
29
+ // background: mantine.colors.gray[3]
30
+ // },
31
+ [`&[data-missing]`]: {
32
+ color: mantine.colors.orange[4],
33
+ background: `color-mix(in srgb, ${mantine.colors.orange[8]} 15%, transparent)`,
34
+ borderColor: `color-mix(in srgb, ${mantine.colors.orange[5]} 20%, transparent)`
35
+ },
36
+ [`${whereLight} &[data-missing]`]: {
37
+ color: mantine.colors.orange[8]
38
+ }
39
+ }
40
+ })
41
+
42
+ export const xyflow = style({
43
+ flex: '1 1 100%',
44
+ position: 'relative',
45
+ width: '100%',
46
+ height: '100%',
47
+ background: mantine.colors.body,
48
+ border: `1px solid ${mantine.colors.defaultBorder}`,
49
+ borderRadius: mantine.radius.sm,
50
+ selectors: {
51
+ [`${whereLight} &`]: {
52
+ borderColor: mantine.colors.gray[3],
53
+ background: mantine.colors.gray[1]
54
+ }
55
+ }
56
+ })
57
+
58
+ export const panelScope = style({
59
+ ':before': {
60
+ content: 'scope:',
61
+ position: 'absolute',
62
+ top: 0,
63
+ left: 8,
64
+ fontSize: 9,
65
+ fontWeight: 500,
66
+ lineHeight: 1,
67
+ color: mantine.colors.dimmed,
68
+ opacity: 0.85,
69
+ transform: 'translateY(-100%) translateY(-2px)'
70
+ }
71
+ })
72
+
73
+ globalStyle(`${whereLight} ${panelScope} .mantine-SegmentedControl-root`, {
74
+ background: mantine.colors.gray[3]
75
+ })
76
+
77
+ export const edgeNum = style({
78
+ display: 'inline-block',
79
+ fontSize: mantine.fontSizes.xl,
80
+ fontWeight: 600,
81
+ padding: '1px 5px',
82
+ minWidth: 24,
83
+ textAlign: 'center',
84
+ borderRadius: mantine.radius.sm,
85
+ background: mantine.colors.dark[7],
86
+ color: mantine.colors.defaultColor,
87
+ selectors: {
88
+ [`${whereLight} &`]: {
89
+ background: mantine.colors.gray[4],
90
+ color: mantine.colors.dark[6]
91
+ },
92
+ [`&[data-zero]`]: {
93
+ color: mantine.colors.dimmed
94
+ },
95
+ [`${whereLight} &[data-zero]`]: {
96
+ background: mantine.colors.gray[3]
97
+ },
98
+ [`&[data-missing]`]: {
99
+ color: mantine.colors.orange[4],
100
+ background: `color-mix(in srgb, ${mantine.colors.orange[8]} 20%, transparent)`
101
+ },
102
+ [`${whereLight} &[data-missing]`]: {
103
+ color: mantine.colors.orange[8]
104
+ }
105
+ }
106
+ })
@@ -0,0 +1,201 @@
1
+ import { type DiagramView, type EdgeId, type LikeC4Model, nameFromFqn, nonNullable } from '@likec4/core'
2
+ import { Box, Button, Group, Paper, SegmentedControl, Stack, Text, ThemeIcon } from '@mantine/core'
3
+ import { useId } from '@mantine/hooks'
4
+ import { IconArrowRight, IconExternalLink, IconInfoCircle } from '@tabler/icons-react'
5
+ import { Panel } from '@xyflow/react'
6
+ import { LayoutGroup } from 'framer-motion'
7
+ import { useState } from 'react'
8
+ import { unique } from 'remeda'
9
+ import { useDiagramState } from '../../hooks'
10
+ import { useOverlayDialog } from '../OverlayContext'
11
+ import { RelationshipsXYFlow } from '../relationships-of/RelationshipsXYFlow'
12
+ import { useLayoutedRelationships } from '../relationships-of/use-layouted-relationships'
13
+ import * as css from './TabPanelRelationships.css'
14
+
15
+ type RelationshipsTabPanelProps = {
16
+ currentView: DiagramView
17
+ element: LikeC4Model.ElementModel
18
+ }
19
+
20
+ export function TabPanelRelationships({
21
+ currentView,
22
+ element
23
+ }: RelationshipsTabPanelProps) {
24
+ const layoutId = useId()
25
+ const enableRelationshipBrowser = useDiagramState(s => s.enableRelationshipBrowser)
26
+ const overlay = useOverlayDialog()
27
+ const [scope, setScope] = useState<'global' | 'view'>('view')
28
+ const node = nonNullable(currentView.nodes.find((n) => n.id === element.id))
29
+
30
+ const incoming = element.incoming().map(r => r.id)
31
+ const outgoing = element.outgoing().map(r => r.id)
32
+
33
+ const findRelationIds = (edgeId: EdgeId) => currentView.edges.find((edge) => edge.id === edgeId)?.relations ?? []
34
+
35
+ const incomingInView = unique(node.inEdges.flatMap(findRelationIds))
36
+ const outgoingInView = unique(node.outEdges.flatMap(findRelationIds))
37
+
38
+ const notIncludedRelations = [
39
+ ...incoming,
40
+ ...outgoing
41
+ ].filter(r => !incomingInView.includes(r) && !outgoingInView.includes(r)).length
42
+
43
+ const {
44
+ edges,
45
+ nodes,
46
+ bounds
47
+ } = useLayoutedRelationships(element.id, currentView, scope)
48
+
49
+ return (
50
+ <Stack gap={'xs'} pos={'relative'} w={'100%'} h={'100%'}>
51
+ {(incoming.length + outgoing.length) > 0 && (
52
+ <Group gap={'xs'} wrap="nowrap" align="center">
53
+ <Box>
54
+ <Group gap={8} mb={4} wrap="nowrap">
55
+ <RelationshipsStat
56
+ title="incoming"
57
+ total={incoming.length}
58
+ included={incomingInView.length}
59
+ />
60
+ <ThemeIcon size={'sm'} variant="transparent" c="dimmed">
61
+ <IconArrowRight style={{ width: 16 }} />
62
+ </ThemeIcon>
63
+ <Text className={css.fqn}>{nameFromFqn(element.id)}</Text>
64
+ <ThemeIcon size={'sm'} variant="transparent" c="dimmed">
65
+ <IconArrowRight style={{ width: 16 }} />
66
+ </ThemeIcon>
67
+ <RelationshipsStat
68
+ title="outgoing"
69
+ total={outgoing.length}
70
+ included={outgoingInView.length}
71
+ />
72
+ </Group>
73
+ </Box>
74
+ {notIncludedRelations > 0 && (
75
+ <Group
76
+ mt={'xs'}
77
+ gap={6}
78
+ c="orange"
79
+ style={{ cursor: 'pointer' }}>
80
+ <IconInfoCircle style={{ width: 14 }} />
81
+ <Text fz="sm">
82
+ {notIncludedRelations} relationship{notIncludedRelations > 1 ? 's are' : ' is'} hidden
83
+ </Text>
84
+ </Group>
85
+ )}
86
+ </Group>
87
+ )}
88
+
89
+ <Box className={css.xyflow}>
90
+ <LayoutGroup id={layoutId}>
91
+ <RelationshipsXYFlow
92
+ subjectId={element.id}
93
+ bounds={bounds}
94
+ nodes={nodes}
95
+ edges={edges}
96
+ view={currentView}
97
+ elementsSelectable={false}
98
+ >
99
+ <Panel position="top-left" className={css.panelScope}>
100
+ <SegmentedControl
101
+ size="xs"
102
+ withItemsBorders={false}
103
+ value={scope}
104
+ onChange={setScope as any}
105
+ data={[
106
+ { label: 'Global', value: 'global' },
107
+ { label: 'View', value: 'view' }
108
+ ]}
109
+ />
110
+ </Panel>
111
+ {enableRelationshipBrowser && (
112
+ <Panel position="top-right">
113
+ <Button
114
+ size="compact-sm"
115
+ variant="default"
116
+ fz={'xs'}
117
+ fw={500}
118
+ rightSection={<IconExternalLink stroke={1.6} style={{ width: 16 }} />}
119
+ onClick={(e) => {
120
+ e.stopPropagation()
121
+ overlay.close(() => {
122
+ overlay.openOverlay({
123
+ relationshipsOf: element.id
124
+ })
125
+ })
126
+ }}
127
+ >
128
+ Open
129
+ </Button>
130
+ </Panel>
131
+ )}
132
+ </RelationshipsXYFlow>
133
+ </LayoutGroup>
134
+ </Box>
135
+ </Stack>
136
+ )
137
+ }
138
+
139
+ function RelationshipsStat({
140
+ title,
141
+ total,
142
+ included
143
+ }: {
144
+ title: string
145
+ total: number
146
+ included: number
147
+ }) {
148
+ return (
149
+ <Paper
150
+ withBorder
151
+ shadow="none"
152
+ className={css.relationshipStat}
153
+ px="md"
154
+ py="xs"
155
+ radius="md"
156
+ mod={{
157
+ zero: total === 0,
158
+ missing: total !== included
159
+ }}>
160
+ <Stack gap={4} align="flex-end">
161
+ <Text component="div" c={total !== included ? 'orange' : 'dimmed'} tt="uppercase" fw={600} fz={10} lh={1}>
162
+ {title}
163
+ </Text>
164
+ <Text fw={600} fz={'xl'} component="div" lh={1}>
165
+ {total !== included
166
+ ? (
167
+ <>
168
+ {included} / {total}
169
+ </>
170
+ )
171
+ : (
172
+ <>
173
+ {total}
174
+ </>
175
+ )}
176
+ </Text>
177
+ {
178
+ /* <ThemeIcon
179
+ color="gray"
180
+ variant="light"
181
+ style={{
182
+ color: stat.diff > 0 ? 'var(--mantine-color-teal-6)' : 'var(--mantine-color-red-6)'
183
+ }}
184
+ size={38}
185
+ radius="md"
186
+ >
187
+ <DiffIcon size="1.8rem" stroke={1.5} />
188
+ </ThemeIcon> */
189
+ }
190
+ </Stack>
191
+ {
192
+ /* <Text c="dimmed" fz="sm" mt="md">
193
+ <Text component="span" c={stat.diff > 0 ? 'teal' : 'red'} fw={700}>
194
+ {stat.diff}%
195
+ </Text>{' '}
196
+ {stat.diff > 0 ? 'increase' : 'decrease'} compared to last month
197
+ </Text> */
198
+ }
199
+ </Paper>
200
+ )
201
+ }
@@ -0,0 +1,36 @@
1
+ import { globalStyle, style } from '@vanilla-extract/css'
2
+ import { easings, mantine, transitions, whereDark } from '../../theme-vars'
3
+
4
+ export const treeNodeLabel = style({
5
+ marginTop: mantine.spacing.sm,
6
+ marginBottom: mantine.spacing.sm
7
+ })
8
+
9
+ export const elementLabel = style({
10
+ display: 'inline-flex',
11
+ transition: transitions.fast,
12
+ border: `1px dashed ${mantine.colors.defaultBorder}`,
13
+ borderRadius: mantine.radius.sm,
14
+ padding: `${mantine.spacing.xs} ${mantine.spacing.md}`,
15
+ alignItems: 'center',
16
+ cursor: 'pointer',
17
+ color: mantine.colors.gray[7],
18
+ ':hover': {
19
+ transitionTimingFunction: easings.out,
20
+ borderStyle: 'solid',
21
+ color: mantine.colors.defaultColor,
22
+ background: mantine.colors.defaultHover
23
+ },
24
+ selectors: {
25
+ [`${whereDark} &`]: {
26
+ color: mantine.colors.dark[1]
27
+ }
28
+ }
29
+ })
30
+ globalStyle(`${elementLabel} > *`, {
31
+ transition: transitions.fast
32
+ })
33
+ globalStyle(`${elementLabel}:hover > *`, {
34
+ transitionTimingFunction: easings.out,
35
+ transform: 'translateX(1px)'
36
+ })
@@ -0,0 +1,120 @@
1
+ import type { LikeC4Model } from '@likec4/core'
2
+ import { Alert, Anchor, Box, Pill, Text, Tree, useTree } from '@mantine/core'
3
+ import { IconInfoCircle } from '@tabler/icons-react'
4
+ import { type ReactNode, useEffect, useMemo } from 'react'
5
+ import * as css from './TabPanelStructure.css'
6
+
7
+ interface ElementTreeNodeData {
8
+ label: ReactNode
9
+ value: string
10
+ element: LikeC4Model.ElementModel
11
+ type: 'ancestor' | 'current' | 'descedant'
12
+ children: TreeNodeData[]
13
+ }
14
+
15
+ interface MessageNodeData {
16
+ label: ReactNode
17
+ value: string
18
+ type: 'message'
19
+ children: TreeNodeData[]
20
+ }
21
+
22
+ type TreeNodeData = ElementTreeNodeData | MessageNodeData
23
+
24
+ type TabElementStructureProps = {
25
+ element: LikeC4Model.ElementModel
26
+ }
27
+
28
+ const ElementLabel = ({
29
+ element
30
+ }: { element: LikeC4Model.ElementModel; type: 'ancestor' | 'current' | 'descedant' }) => (
31
+ <Box className={css.elementLabel}>
32
+ <Text component="div" fz={'sm'} fw={'500'}>{element.title}</Text>
33
+ </Box>
34
+ )
35
+
36
+ // const Render = ({
37
+ // node
38
+ // }: RenderTreeNodePayload) => {
39
+ // }
40
+
41
+ export function TabElementStructure({
42
+ element
43
+ }: TabElementStructureProps) {
44
+ const tree = useTree({
45
+ multiple: false
46
+ })
47
+
48
+ const data = useMemo(() => {
49
+ let seq = 1
50
+
51
+ const messageNode = (label: ReactNode): MessageNodeData => ({
52
+ label,
53
+ value: `msg${seq++}`,
54
+ type: 'message',
55
+ children: []
56
+ })
57
+
58
+ const current: TreeNodeData = {
59
+ label: <ElementLabel type="current" element={element} />,
60
+ value: element.id,
61
+ element,
62
+ type: 'current',
63
+ children: element.children().map((child): TreeNodeData => ({
64
+ label: <ElementLabel type="descedant" element={child} />,
65
+ value: child.id,
66
+ element: child,
67
+ type: 'descedant',
68
+ children: []
69
+ }))
70
+ }
71
+ if (current.children.length === 0) {
72
+ current.children.push(
73
+ messageNode(<Pill radius={'sm'}>no nested</Pill>)
74
+ )
75
+ }
76
+ let ancestor = element.ancestors().reduce((acc, parent) => ({
77
+ label: <ElementLabel type="ancestor" element={parent} />,
78
+ value: parent.id,
79
+ element: parent,
80
+ type: 'ancestor' as const,
81
+ children: [acc]
82
+ }), current)
83
+
84
+ return [
85
+ ancestor
86
+ ]
87
+ }, [element])
88
+
89
+ useEffect(() => {
90
+ tree.expandAllNodes()
91
+ }, [data])
92
+
93
+ return (
94
+ <>
95
+ <Alert variant="light" color="orange" title="In development" icon={<IconInfoCircle />}>
96
+ We need your feedback. Share your thoughts and ideas -{' '}
97
+ <Anchor
98
+ fz="sm"
99
+ fw={500}
100
+ underline="hover"
101
+ c={'orange'}
102
+ href="https://github.com/likec4/likec4/discussions/"
103
+ target="_blank">
104
+ GitHub discussions
105
+ </Anchor>
106
+ </Alert>
107
+ <Tree
108
+ levelOffset={'xl'}
109
+ allowRangeSelection={false}
110
+ expandOnClick={false}
111
+ expandOnSpace={false}
112
+ classNames={{
113
+ label: css.treeNodeLabel
114
+ }}
115
+ data={data}
116
+ tree={tree}
117
+ />
118
+ </>
119
+ )
120
+ }
@@ -0,0 +1,185 @@
1
+ import { type Fqn } from '@likec4/core'
2
+ import { Box, Button, Group, SegmentedControl, Space, Text } from '@mantine/core'
3
+ import { useLocalStorage, useStateHistory } from '@mantine/hooks'
4
+ import { IconArrowLeft, IconArrowRight } from '@tabler/icons-react'
5
+ import { Panel } from '@xyflow/react'
6
+ import { memo, useEffect } from 'react'
7
+ import { useDiagramState } from '../../hooks/useDiagramState'
8
+ import { useOverlayDialog } from '../OverlayContext'
9
+ import { RelationshipsXYFlow } from './RelationshipsXYFlow'
10
+ import { SelectElement } from './SelectElement'
11
+ import { useLayoutedRelationships } from './use-layouted-relationships'
12
+
13
+ export const RelationshipsOverlay = memo<{ subjectId: Fqn }>(function RelationshipsOverlay({ subjectId }) {
14
+ const view = useDiagramState(s => s.view)
15
+ const [_scope, setScope] = useLocalStorage<'global' | 'view'>({
16
+ key: 'likec4:scope-relationships-of',
17
+ getInitialValueInEffect: false,
18
+ defaultValue: 'view'
19
+ })
20
+ const {
21
+ notIncludedRelations,
22
+ viewIncludesSubject,
23
+ edges,
24
+ nodes,
25
+ subject,
26
+ bounds
27
+ } = useLayoutedRelationships(subjectId, view, _scope)
28
+ const scope = viewIncludesSubject ? _scope : 'global'
29
+ const showSubjectWarning = !viewIncludesSubject && _scope === 'view'
30
+ const [historySubjectId, historyOps, { history, current }] = useStateHistory(subjectId)
31
+
32
+ const overlay = useOverlayDialog()
33
+
34
+ useEffect(() => {
35
+ if (historySubjectId !== subjectId) {
36
+ historyOps.set(subjectId)
37
+ }
38
+ }, [subjectId])
39
+
40
+ useEffect(() => {
41
+ if (historySubjectId !== subjectId) {
42
+ overlay.openOverlay({
43
+ relationshipsOf: historySubjectId
44
+ })
45
+ }
46
+ }, [historySubjectId])
47
+
48
+ return (
49
+ <RelationshipsXYFlow
50
+ nodes={nodes}
51
+ edges={edges}
52
+ bounds={bounds}
53
+ view={view}
54
+ subjectId={subjectId}
55
+ viewportPadding={0.2}
56
+ onNodeClick={(e, node) => {
57
+ e.stopPropagation()
58
+ if (node.type === 'empty') return
59
+ // lastClickedNodeRef.current = node
60
+ overlay.openOverlay({
61
+ relationshipsOf: node.data.fqn
62
+ })
63
+ }}
64
+ >
65
+ <Panel position="top-center">
66
+ <Group gap={'xs'} wrap={'nowrap'}>
67
+ <Button
68
+ leftSection={<IconArrowLeft stroke={3} size={14} />}
69
+ color="dimmed"
70
+ variant="subtle"
71
+ style={{
72
+ visibility: current > 0 ? 'visible' : 'hidden',
73
+ padding: '0.25rem 0.75rem'
74
+ }}
75
+ styles={{
76
+ label: {
77
+ fontWeight: 400
78
+ },
79
+ section: {
80
+ marginInlineEnd: 6
81
+ }
82
+ }}
83
+ size="sm"
84
+ onClick={(e) => {
85
+ e.stopPropagation()
86
+ historyOps.back()
87
+ }}>
88
+ Back
89
+ </Button>
90
+ <Space w={2} />
91
+ <Group gap={'xs'} pos={'relative'} wrap="nowrap" flex={'1 0 auto'}>
92
+ <Box fz={'sm'} fw={'400'} style={{ whiteSpace: 'nowrap', userSelect: 'none' }}>Relationships of</Box>
93
+ <Box flex={'1 0 auto'}>
94
+ <SelectElement
95
+ scope={scope}
96
+ subject={subject.element}
97
+ onSelect={fqn =>
98
+ overlay.openOverlay({
99
+ relationshipsOf: fqn
100
+ })}
101
+ viewId={view.id} />
102
+ </Box>
103
+ <SegmentedControl
104
+ flex={'1 0 auto'}
105
+ size="xs"
106
+ withItemsBorders={false}
107
+ value={scope}
108
+ onChange={setScope as any}
109
+ data={[
110
+ { label: 'Global', value: 'global' },
111
+ { label: 'Current view', value: 'view', disabled: !viewIncludesSubject }
112
+ ]}
113
+ />
114
+ {showSubjectWarning && (
115
+ <Box
116
+ pos={'absolute'}
117
+ top={'calc(100% + .5rem)'}
118
+ left={'50%'}
119
+ w={'max-content'}
120
+ style={{
121
+ transform: 'translateX(-50%)',
122
+ textAlign: 'center',
123
+ cursor: 'pointer'
124
+ }}
125
+ onClick={(e) => {
126
+ e.stopPropagation()
127
+ setScope('global')
128
+ }}
129
+ >
130
+ <Text fw={500} size="xs" c="orange" component="div">
131
+ Current view doesn't include this element, switched to Global
132
+ </Text>
133
+ </Box>
134
+ )}
135
+ {viewIncludesSubject && scope === 'view' && notIncludedRelations > 0 && (
136
+ <Box
137
+ pos={'absolute'}
138
+ top={'calc(100% + .5rem)'}
139
+ left={'50%'}
140
+ w={'max-content'}
141
+ style={{
142
+ transform: 'translateX(-50%)',
143
+ textAlign: 'center',
144
+ cursor: 'pointer'
145
+ }}
146
+ onClick={(e) => {
147
+ e.stopPropagation()
148
+ setScope('global')
149
+ }}
150
+ >
151
+ <Text fw={500} size="xs" c="orange" component="div">
152
+ View does not include {notIncludedRelations}{' '}
153
+ relationship{notIncludedRelations > 1 ? 's' : ''}. Switch to Global to compare
154
+ </Text>
155
+ </Box>
156
+ )}
157
+ </Group>
158
+ <Button
159
+ rightSection={<IconArrowRight stroke={3} size={14} />}
160
+ color="dimmed"
161
+ variant="subtle"
162
+ style={{
163
+ visibility: current + 1 < history.length ? 'visible' : 'hidden',
164
+ padding: '0.25rem 0.75rem'
165
+ }}
166
+ styles={{
167
+ label: {
168
+ fontWeight: 400
169
+ },
170
+ section: {
171
+ marginInlineStart: 4
172
+ }
173
+ }}
174
+ size="sm"
175
+ onClick={(e) => {
176
+ e.stopPropagation()
177
+ historyOps.forward()
178
+ }}>
179
+ Forward
180
+ </Button>
181
+ </Group>
182
+ </Panel>
183
+ </RelationshipsXYFlow>
184
+ )
185
+ })