@likec4/diagram 1.15.1 → 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.
- package/package.json +7 -7
- package/src/LikeC4CustomColors.tsx +24 -21
- package/src/LikeC4Diagram.props.ts +9 -9
- package/src/LikeC4Diagram.tsx +25 -26
- package/src/StaticLikeC4Diagram.tsx +9 -3
- package/src/overlays/OverlayContext.tsx +1 -1
- package/src/overlays/Overlays.css.ts +28 -23
- package/src/overlays/Overlays.tsx +116 -64
- package/src/overlays/edge-details/xyflow/ElementNode.tsx +15 -12
- package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +7 -3
- package/src/overlays/edge-details/xyflow/styles.css.ts +22 -16
- package/src/overlays/element-details/ElementDetailsCard.css.ts +322 -0
- package/src/overlays/element-details/ElementDetailsCard.tsx +639 -0
- package/src/overlays/element-details/TabPanelRelationships.css.ts +106 -0
- package/src/overlays/element-details/TabPanelRelationships.tsx +201 -0
- package/src/overlays/element-details/TabPanelStructure.css.ts +36 -0
- package/src/overlays/element-details/TabPanelStructure.tsx +120 -0
- package/src/overlays/relationships-of/RelationshipsOverlay.tsx +185 -0
- package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +179 -213
- package/src/overlays/relationships-of/_types.ts +15 -4
- package/src/overlays/relationships-of/use-layouted-relationships.ts +301 -207
- package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +68 -14
- package/src/overlays/relationships-of/xyflow/ElementNode.tsx +54 -14
- package/src/overlays/relationships-of/xyflow/EmptyNode.tsx +1 -1
- package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +51 -23
- package/src/overlays/relationships-of/xyflow/styles.css.ts +34 -27
- package/src/state/DiagramContext.tsx +15 -8
- package/src/state/diagramStore.ts +14 -6
- package/src/theme-vars.ts +9 -0
- package/src/ui/FramerMotionConfig.tsx +18 -0
- package/src/ui/notation/NotationPanel.tsx +3 -3
- package/src/xyflow/XYFlow.tsx +18 -35
- package/src/xyflow/XYFlowInner.tsx +2 -2
- package/src/xyflow/edges/EdgeLabel.tsx +3 -3
- package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +25 -18
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +96 -30
- package/src/xyflow/nodes/compound/CompoundNode.tsx +149 -165
- package/src/xyflow/nodes/element/ElementDetails.css.ts +101 -0
- package/src/xyflow/nodes/element/ElementDetails.tsx +393 -0
- package/src/xyflow/nodes/element/ElementNode.tsx +110 -116
- package/src/xyflow/nodes/element/element.css.ts +38 -18
- package/src/xyflow/nodes/shared/ElementIcon.tsx +43 -0
- package/src/xyflow/nodes/shared/Toolbar.tsx +6 -6
- package/src/xyflow/{XYFlowEvents.tsx → useXYFlowEvents.ts} +26 -22
- package/src/xyflow/utils.ts +12 -3
- package/src/LikeC4CustomColors.props.ts +0 -5
- /package/src/{mantine → ui}/EnsureMantine.tsx +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Text as MantineText } from '@mantine/core'
|
|
2
|
-
import { Handle, type NodeProps, Position } from '@xyflow/react'
|
|
2
|
+
import { Handle, type NodeProps, Position, useStore } from '@xyflow/react'
|
|
3
3
|
import clsx from 'clsx'
|
|
4
|
+
import { deepEqual, shallowEqual } from 'fast-equals'
|
|
4
5
|
import { m } from 'framer-motion'
|
|
6
|
+
import { memo, useCallback } from 'react'
|
|
5
7
|
import type { XYFlowTypes } from '../_types'
|
|
6
8
|
import * as css from './styles.css'
|
|
7
9
|
|
|
@@ -11,15 +13,44 @@ const Text = MantineText.withProps({
|
|
|
11
13
|
|
|
12
14
|
type CompoundNodeProps = NodeProps<XYFlowTypes.CompoundNode>
|
|
13
15
|
|
|
14
|
-
export
|
|
16
|
+
export const CompoundNode = memo<CompoundNodeProps>(({
|
|
17
|
+
id,
|
|
15
18
|
data: {
|
|
16
19
|
element,
|
|
17
20
|
ports,
|
|
21
|
+
layoutId = id,
|
|
22
|
+
leaving = false,
|
|
23
|
+
initialAnimation = true,
|
|
18
24
|
...data
|
|
19
25
|
},
|
|
20
26
|
width = 200,
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
height = 100,
|
|
28
|
+
...props
|
|
29
|
+
}) => {
|
|
30
|
+
const scale = (diff: number) => ({
|
|
31
|
+
scaleX: (width + diff) / width,
|
|
32
|
+
scaleY: (height + diff) / height
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
let opacity = 1
|
|
36
|
+
if (data.dimmed) {
|
|
37
|
+
opacity = data.dimmed === 'immediate' ? 0.05 : 0.15
|
|
38
|
+
}
|
|
39
|
+
if (leaving) {
|
|
40
|
+
opacity = 0
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const {
|
|
44
|
+
elementsSelectable
|
|
45
|
+
} = useStore(
|
|
46
|
+
useCallback((s) => ({
|
|
47
|
+
elementsSelectable: s.elementsSelectable
|
|
48
|
+
}), []),
|
|
49
|
+
shallowEqual
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const selectable = props.selectable ?? elementsSelectable
|
|
53
|
+
|
|
23
54
|
return (
|
|
24
55
|
<>
|
|
25
56
|
<m.div
|
|
@@ -27,27 +58,48 @@ export function CompoundNode({
|
|
|
27
58
|
css.compoundNodeBody,
|
|
28
59
|
'likec4-compound-node'
|
|
29
60
|
])}
|
|
30
|
-
|
|
61
|
+
layoutId={layoutId}
|
|
62
|
+
data-compound-depth={data.depth ?? 1}
|
|
31
63
|
data-likec4-color={element.color}
|
|
64
|
+
initial={(layoutId === id && initialAnimation)
|
|
65
|
+
? {
|
|
66
|
+
...scale(-20),
|
|
67
|
+
opacity: 0,
|
|
68
|
+
width,
|
|
69
|
+
height
|
|
70
|
+
}
|
|
71
|
+
: false}
|
|
32
72
|
animate={{
|
|
33
|
-
|
|
73
|
+
...scale(0),
|
|
74
|
+
opacity,
|
|
75
|
+
width,
|
|
76
|
+
height,
|
|
34
77
|
transition: {
|
|
35
|
-
|
|
78
|
+
opacity: {
|
|
79
|
+
delay: !leaving && data.dimmed === true ? .4 : 0,
|
|
80
|
+
...((leaving || data.dimmed === 'immediate') && {
|
|
81
|
+
duration: 0.09
|
|
82
|
+
})
|
|
83
|
+
}
|
|
36
84
|
}
|
|
37
85
|
}}
|
|
38
86
|
{...(selectable && {
|
|
39
87
|
whileHover: {
|
|
40
|
-
scale
|
|
88
|
+
...scale(12),
|
|
89
|
+
scaleX: (width + 12) / width,
|
|
90
|
+
scaleY: (height + 12) / height,
|
|
41
91
|
transition: {
|
|
42
|
-
delay: 0.
|
|
92
|
+
delay: 0.1
|
|
43
93
|
}
|
|
44
94
|
},
|
|
45
95
|
whileTap: {
|
|
46
|
-
scale
|
|
96
|
+
...scale(-12)
|
|
47
97
|
}
|
|
48
98
|
})}
|
|
49
99
|
>
|
|
50
|
-
<Text className={css.compoundNodeTitle} maw={width - 20}>
|
|
100
|
+
<Text className={css.compoundNodeTitle} maw={width - 20}>
|
|
101
|
+
{element.title}
|
|
102
|
+
</Text>
|
|
51
103
|
</m.div>
|
|
52
104
|
{ports.left.map(({ id, type }, i) => (
|
|
53
105
|
<Handle
|
|
@@ -57,7 +109,7 @@ export function CompoundNode({
|
|
|
57
109
|
position={Position.Left}
|
|
58
110
|
style={{
|
|
59
111
|
visibility: 'hidden',
|
|
60
|
-
top: `${
|
|
112
|
+
top: `${20 * (i + 1)}px`
|
|
61
113
|
}} />
|
|
62
114
|
))}
|
|
63
115
|
{ports.right.map(({ id, type }, i) => (
|
|
@@ -68,9 +120,11 @@ export function CompoundNode({
|
|
|
68
120
|
position={Position.Right}
|
|
69
121
|
style={{
|
|
70
122
|
visibility: 'hidden',
|
|
71
|
-
top: `${
|
|
123
|
+
top: `${20 * (i + 1)}px`
|
|
72
124
|
}} />
|
|
73
125
|
))}
|
|
74
126
|
</>
|
|
75
127
|
)
|
|
76
|
-
}
|
|
128
|
+
}, (prev, next) => {
|
|
129
|
+
return deepEqual(prev.data, next.data)
|
|
130
|
+
})
|
|
@@ -2,7 +2,9 @@ import { ActionIcon, Box, Group, Text as MantineText } from '@mantine/core'
|
|
|
2
2
|
import { IconFileSymlink, IconTransform, IconZoomScan } from '@tabler/icons-react'
|
|
3
3
|
import { Handle, type NodeProps, Position } from '@xyflow/react'
|
|
4
4
|
import clsx from 'clsx'
|
|
5
|
+
import { deepEqual } from 'fast-equals'
|
|
5
6
|
import { m } from 'framer-motion'
|
|
7
|
+
import { memo } from 'react'
|
|
6
8
|
import { type DiagramState, useDiagramState } from '../../../hooks'
|
|
7
9
|
import { ElementShapeSvg } from '../../../xyflow/nodes/element/ElementShapeSvg'
|
|
8
10
|
import { stopPropagation } from '../../../xyflow/utils'
|
|
@@ -32,23 +34,41 @@ function selector(s: DiagramState) {
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
export
|
|
37
|
+
export const ElementNode = memo<ElementNodeProps>(({
|
|
38
|
+
id,
|
|
36
39
|
data: {
|
|
37
40
|
element,
|
|
38
41
|
ports,
|
|
39
42
|
navigateTo,
|
|
43
|
+
layoutId = id,
|
|
44
|
+
leaving = false,
|
|
45
|
+
initialAnimation = true,
|
|
40
46
|
...data
|
|
41
47
|
},
|
|
42
48
|
selectable = true,
|
|
43
49
|
width: w = 100,
|
|
44
50
|
height: h = 100
|
|
45
|
-
}
|
|
51
|
+
}) => {
|
|
46
52
|
const overlay = useOverlayDialog()
|
|
47
53
|
const {
|
|
48
54
|
currentViewId,
|
|
49
55
|
onNavigateTo,
|
|
50
56
|
onOpenSource
|
|
51
57
|
} = useDiagramState(selector)
|
|
58
|
+
|
|
59
|
+
const scale = (diff: number) => ({
|
|
60
|
+
scaleX: (w + diff) / w,
|
|
61
|
+
scaleY: (h + diff) / h
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
let opacity = 1
|
|
65
|
+
if (data.dimmed) {
|
|
66
|
+
opacity = data.dimmed === 'immediate' ? 0.05 : 0.15
|
|
67
|
+
}
|
|
68
|
+
if (leaving) {
|
|
69
|
+
opacity = 0
|
|
70
|
+
}
|
|
71
|
+
|
|
52
72
|
return (
|
|
53
73
|
<>
|
|
54
74
|
<m.div
|
|
@@ -56,22 +76,39 @@ export function ElementNode({
|
|
|
56
76
|
css.elementNode,
|
|
57
77
|
'likec4-element-node'
|
|
58
78
|
])}
|
|
79
|
+
layoutId={layoutId}
|
|
59
80
|
data-likec4-color={element.color}
|
|
81
|
+
initial={(layoutId === id && initialAnimation)
|
|
82
|
+
? {
|
|
83
|
+
...scale(-20),
|
|
84
|
+
opacity: 0,
|
|
85
|
+
width: w,
|
|
86
|
+
height: h
|
|
87
|
+
}
|
|
88
|
+
: false}
|
|
60
89
|
animate={{
|
|
61
|
-
|
|
90
|
+
...scale(0),
|
|
91
|
+
opacity,
|
|
92
|
+
width: w,
|
|
93
|
+
height: h,
|
|
62
94
|
transition: {
|
|
63
|
-
|
|
95
|
+
opacity: {
|
|
96
|
+
delay: !leaving && data.dimmed === true ? .4 : 0,
|
|
97
|
+
...((leaving || data.dimmed === 'immediate') && {
|
|
98
|
+
duration: 0.09
|
|
99
|
+
})
|
|
100
|
+
}
|
|
64
101
|
}
|
|
65
102
|
}}
|
|
66
103
|
{...(selectable && {
|
|
67
104
|
whileHover: {
|
|
68
|
-
scale
|
|
69
|
-
transition: {
|
|
70
|
-
|
|
71
|
-
}
|
|
105
|
+
...scale(16)
|
|
106
|
+
// transition: {
|
|
107
|
+
// delay: 0.1
|
|
108
|
+
// }
|
|
72
109
|
},
|
|
73
110
|
whileTap: {
|
|
74
|
-
scale
|
|
111
|
+
...scale(-8)
|
|
75
112
|
}
|
|
76
113
|
})}
|
|
77
114
|
>
|
|
@@ -86,7 +123,9 @@ export function ElementNode({
|
|
|
86
123
|
<ElementShapeSvg shape={element.shape} w={w} h={h} />
|
|
87
124
|
</svg>
|
|
88
125
|
<Box className={css.elementNodeContent}>
|
|
89
|
-
<Text className={css.elementNodeTitle} lineClamp={2}>
|
|
126
|
+
<Text className={css.elementNodeTitle} lineClamp={2}>
|
|
127
|
+
{element.title}
|
|
128
|
+
</Text>
|
|
90
129
|
{element.description && (
|
|
91
130
|
<Text className={css.elementNodeDescription} lineClamp={4}>{element.description}</Text>
|
|
92
131
|
)}
|
|
@@ -96,13 +135,12 @@ export function ElementNode({
|
|
|
96
135
|
<Action
|
|
97
136
|
onClick={(event) => {
|
|
98
137
|
event.stopPropagation()
|
|
99
|
-
|
|
100
|
-
overlay.close()
|
|
138
|
+
overlay.close(() => onNavigateTo(navigateTo))
|
|
101
139
|
}}>
|
|
102
140
|
<IconZoomScan stroke={1.8} style={{ width: '75%' }} />
|
|
103
141
|
</Action>
|
|
104
142
|
)}
|
|
105
|
-
{data.column !== '
|
|
143
|
+
{data.column !== 'subjects' && (
|
|
106
144
|
<Action
|
|
107
145
|
onClick={(event) => {
|
|
108
146
|
event.stopPropagation()
|
|
@@ -148,4 +186,6 @@ export function ElementNode({
|
|
|
148
186
|
))}
|
|
149
187
|
</>
|
|
150
188
|
)
|
|
151
|
-
}
|
|
189
|
+
}, (prev, next) => {
|
|
190
|
+
return deepEqual(prev.data, next.data)
|
|
191
|
+
})
|
|
@@ -16,7 +16,7 @@ export function EmptyNode({
|
|
|
16
16
|
}: EmptyNodeProps) {
|
|
17
17
|
return (
|
|
18
18
|
<Box className={css.emptyNode}>
|
|
19
|
-
<Text c={'dimmed'} fw={500}>No {column === 'incomers' ? 'incoming' : 'outgoing'}</Text>
|
|
19
|
+
<Text c={'dimmed'} fz={'lg'} fw={500}>No {column === 'incomers' ? 'incoming' : 'outgoing'}</Text>
|
|
20
20
|
</Box>
|
|
21
21
|
)
|
|
22
22
|
}
|
|
@@ -1,30 +1,52 @@
|
|
|
1
|
-
import { ActionIcon, Box, Group, Stack, Text, ThemeIcon } from '@mantine/core'
|
|
1
|
+
import { ActionIcon, Box, Group, Stack, Text, ThemeIcon, Tooltip as MantineTooltip } from '@mantine/core'
|
|
2
2
|
import { IconBoxMultipleFilled, IconZoomScan } from '@tabler/icons-react'
|
|
3
3
|
import { BaseEdge, EdgeLabelRenderer, type EdgeProps, getBezierPath } from '@xyflow/react'
|
|
4
4
|
import clsx from 'clsx'
|
|
5
|
+
import { useEffect, useRef } from 'react'
|
|
5
6
|
import { only } from 'remeda'
|
|
6
7
|
import { useDiagramState } from '../../../hooks/useDiagramState'
|
|
7
8
|
import { stopPropagation } from '../../../xyflow/utils'
|
|
9
|
+
import { useOverlayDialog } from '../../OverlayContext'
|
|
8
10
|
import type { XYFlowTypes } from '../_types'
|
|
9
11
|
import { ZIndexes } from '../use-layouted-relationships'
|
|
10
12
|
import * as css from './styles.css'
|
|
11
13
|
|
|
14
|
+
const Tooltip = MantineTooltip.withProps({
|
|
15
|
+
color: 'dark',
|
|
16
|
+
fz: 'sm',
|
|
17
|
+
openDelay: 400,
|
|
18
|
+
closeDelay: 150,
|
|
19
|
+
withinPortal: false,
|
|
20
|
+
label: '',
|
|
21
|
+
children: null,
|
|
22
|
+
offset: 4
|
|
23
|
+
})
|
|
24
|
+
|
|
12
25
|
export function RelationshipEdge({
|
|
13
26
|
data,
|
|
14
27
|
label,
|
|
15
28
|
...props
|
|
16
29
|
}: EdgeProps<XYFlowTypes.Edge>) {
|
|
17
|
-
const
|
|
30
|
+
const {
|
|
31
|
+
viewId,
|
|
32
|
+
onNavigateTo
|
|
33
|
+
} = useDiagramState(s => ({
|
|
34
|
+
viewId: s.view.id,
|
|
35
|
+
onNavigateTo: s.onNavigateTo
|
|
36
|
+
}))
|
|
37
|
+
const overlay = useOverlayDialog()
|
|
18
38
|
const [edgePath, labelX, labelY] = getBezierPath(props)
|
|
19
39
|
const navigateTo = onNavigateTo ? only(data.relations)?.navigateTo : undefined
|
|
20
40
|
const isMultiRelation = data.relations.length > 1
|
|
21
41
|
const technology = only(data.relations)?.technology
|
|
42
|
+
|
|
22
43
|
return (
|
|
23
44
|
<>
|
|
24
45
|
<g
|
|
25
|
-
className={css.
|
|
46
|
+
className={css.edgeContainer}
|
|
26
47
|
data-edge-dimmed={data.dimmed}
|
|
27
48
|
data-edge-hovered={data.hovered}
|
|
49
|
+
data-likec4-color={data.includedInCurrentView ? 'gray' : 'amber'}
|
|
28
50
|
>
|
|
29
51
|
<BaseEdge
|
|
30
52
|
path={edgePath}
|
|
@@ -37,7 +59,7 @@ export function RelationshipEdge({
|
|
|
37
59
|
style={{
|
|
38
60
|
position: 'absolute',
|
|
39
61
|
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
|
40
|
-
maxWidth: Math.abs(props.targetX - props.sourceX -
|
|
62
|
+
maxWidth: Math.abs(props.targetX - props.sourceX - 70),
|
|
41
63
|
zIndex: ZIndexes.max
|
|
42
64
|
}}
|
|
43
65
|
className={clsx([
|
|
@@ -46,25 +68,29 @@ export function RelationshipEdge({
|
|
|
46
68
|
])}
|
|
47
69
|
data-edge-dimmed={data.dimmed}
|
|
48
70
|
data-edge-hovered={data.hovered}
|
|
71
|
+
data-likec4-color={data.includedInCurrentView ? 'gray' : 'amber'}
|
|
72
|
+
// {...data.includedInCurrentView === false && { 'data-likec4-color': 'amber' }}
|
|
49
73
|
>
|
|
50
74
|
{label && (
|
|
51
|
-
<
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
<Tooltip label="Not included in current view" disabled={data.includedInCurrentView} color={'orange'}>
|
|
76
|
+
<Group gap={6} wrap="nowrap">
|
|
77
|
+
{isMultiRelation && (
|
|
78
|
+
<ThemeIcon size={'sm'} variant="transparent" color="orange">
|
|
79
|
+
<IconBoxMultipleFilled style={{ width: '80%' }} />
|
|
80
|
+
</ThemeIcon>
|
|
81
|
+
)}
|
|
82
|
+
<Text
|
|
83
|
+
fw={isMultiRelation ? '500' : '400'}
|
|
84
|
+
style={{
|
|
85
|
+
whiteSpace: isMultiRelation ? 'nowrap' : undefined
|
|
86
|
+
}}
|
|
87
|
+
component={'div'}
|
|
88
|
+
className={css.edgeLabelText}
|
|
89
|
+
lineClamp={3}>
|
|
90
|
+
{label}
|
|
91
|
+
</Text>
|
|
92
|
+
</Group>
|
|
93
|
+
</Tooltip>
|
|
68
94
|
)}
|
|
69
95
|
{technology && (
|
|
70
96
|
<Text component={'div'} className={css.edgeLabelTechnology}>
|
|
@@ -73,7 +99,7 @@ export function RelationshipEdge({
|
|
|
73
99
|
{' ]'}
|
|
74
100
|
</Text>
|
|
75
101
|
)}
|
|
76
|
-
{navigateTo && (
|
|
102
|
+
{navigateTo && viewId !== navigateTo && (
|
|
77
103
|
<Box ta={'center'} mt={4}>
|
|
78
104
|
<ActionIcon
|
|
79
105
|
variant="default"
|
|
@@ -82,7 +108,9 @@ export function RelationshipEdge({
|
|
|
82
108
|
onPointerDownCapture={stopPropagation}
|
|
83
109
|
onClick={event => {
|
|
84
110
|
event.stopPropagation()
|
|
85
|
-
|
|
111
|
+
overlay.close(() => {
|
|
112
|
+
onNavigateTo?.(navigateTo, event)
|
|
113
|
+
})
|
|
86
114
|
}}
|
|
87
115
|
role="button"
|
|
88
116
|
onDoubleClick={stopPropagation}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { rem } from '@mantine/core'
|
|
2
2
|
import { globalStyle, style } from '@vanilla-extract/css'
|
|
3
|
-
import { mantine, vars,
|
|
3
|
+
import { mantine, transitions, vars, whereDark, xyvars } from '../../../theme-vars'
|
|
4
|
+
import { mixColor } from '../../Overlays.css'
|
|
4
5
|
|
|
5
6
|
export const elementNode = style({
|
|
7
|
+
position: 'relative',
|
|
6
8
|
width: '100%',
|
|
7
9
|
height: '100%'
|
|
8
10
|
})
|
|
@@ -27,7 +29,7 @@ export const elementNodeTitle = style({
|
|
|
27
29
|
fontOpticalSizing: 'auto',
|
|
28
30
|
fontStyle: 'normal',
|
|
29
31
|
fontWeight: 500,
|
|
30
|
-
fontSize:
|
|
32
|
+
fontSize: 18,
|
|
31
33
|
lineHeight: 1.25,
|
|
32
34
|
textWrap: 'balance',
|
|
33
35
|
color: vars.element.hiContrast
|
|
@@ -40,30 +42,30 @@ export const elementNodeDescription = style({
|
|
|
40
42
|
fontFamily: vars.element.font,
|
|
41
43
|
fontOpticalSizing: 'auto',
|
|
42
44
|
fontStyle: 'normal',
|
|
43
|
-
fontSize: mantine.fontSizes.
|
|
45
|
+
fontSize: mantine.fontSizes.sm,
|
|
44
46
|
lineHeight: 1.25,
|
|
45
47
|
textWrap: 'balance',
|
|
46
48
|
color: vars.element.loContrast
|
|
47
49
|
})
|
|
48
50
|
|
|
49
51
|
export const compoundNodeBody = style({
|
|
52
|
+
position: 'relative',
|
|
50
53
|
width: '100%',
|
|
51
54
|
height: '100%',
|
|
52
|
-
boxShadow: '0 4px 10px 0.5px rgba(0,0,0,0.1) ,
|
|
53
|
-
background: `color-mix(in srgb , ${vars.element.fill}, transparent 10%)`,
|
|
54
|
-
|
|
55
|
+
boxShadow: '0 4px 10px 0.5px rgba(0,0,0,0.1) , 1px 1px 4px -1px rgba(0,0,0,0.3)',
|
|
56
|
+
// background: `color-mix(in srgb , ${vars.element.fill}, transparent 10%)`,
|
|
57
|
+
background: vars.element.fill,
|
|
58
|
+
borderRadius: 6
|
|
55
59
|
})
|
|
56
60
|
|
|
57
61
|
export const compoundNodeTitle = style({
|
|
58
62
|
fontFamily: vars.compound.font,
|
|
59
|
-
fontOpticalSizing: 'auto',
|
|
60
|
-
fontStyle: 'normal',
|
|
61
63
|
fontWeight: 600,
|
|
62
|
-
fontSize:
|
|
64
|
+
fontSize: 15,
|
|
63
65
|
lineHeight: 1,
|
|
64
66
|
textTransform: 'uppercase',
|
|
65
|
-
paddingTop:
|
|
66
|
-
paddingLeft:
|
|
67
|
+
paddingTop: 14,
|
|
68
|
+
paddingLeft: 12,
|
|
67
69
|
mixBlendMode: 'screen',
|
|
68
70
|
color: vars.compound.titleColor
|
|
69
71
|
})
|
|
@@ -86,19 +88,32 @@ export const cssShapeSvg = style({
|
|
|
86
88
|
|
|
87
89
|
const DimmedTransitionDelay = '400ms'
|
|
88
90
|
|
|
89
|
-
export const
|
|
91
|
+
export const edgeContainer = style({
|
|
90
92
|
opacity: 1,
|
|
91
|
-
transition:
|
|
92
|
-
|
|
93
|
+
transition: transitions.fast,
|
|
94
|
+
vars: {
|
|
95
|
+
[xyvars.edge.stroke]: vars.relation.lineColor,
|
|
96
|
+
[xyvars.edge.strokeSelected]: `color-mix(in srgb, ${vars.relation.lineColor}, ${mixColor} 35%)`,
|
|
97
|
+
[xyvars.edge.labelColor]: `color-mix(in srgb, ${vars.relation.labelColor}, rgba(255 255 255 / 0.85) 50%)`,
|
|
98
|
+
[xyvars.edge.labelBgColor]: `color-mix(in srgb, ${vars.relation.labelBgColor}, transparent 20%)`
|
|
99
|
+
},
|
|
93
100
|
selectors: {
|
|
94
101
|
[`&[data-edge-dimmed='true']`]: {
|
|
95
|
-
opacity: 0.
|
|
102
|
+
opacity: 0.2,
|
|
96
103
|
transitionDelay: DimmedTransitionDelay
|
|
104
|
+
},
|
|
105
|
+
[`&[data-edge-dimmed='immediate']`]: {
|
|
106
|
+
opacity: 0.2
|
|
107
|
+
},
|
|
108
|
+
[`${whereDark} &`]: {
|
|
109
|
+
vars: {
|
|
110
|
+
[xyvars.edge.labelBgColor]: `color-mix(in srgb, ${vars.relation.labelBgColor}, transparent 50%)`
|
|
111
|
+
}
|
|
97
112
|
}
|
|
98
113
|
}
|
|
99
114
|
})
|
|
100
115
|
|
|
101
|
-
export const edgeLabel = style({
|
|
116
|
+
export const edgeLabel = style([edgeContainer, {
|
|
102
117
|
padding: '2px 5px',
|
|
103
118
|
fontFamily: vars.likec4.font,
|
|
104
119
|
position: 'absolute',
|
|
@@ -111,21 +126,13 @@ export const edgeLabel = style({
|
|
|
111
126
|
// if you have an interactive element, set pointer-events: all
|
|
112
127
|
pointerEvents: 'all',
|
|
113
128
|
textWrap: 'pretty',
|
|
114
|
-
whiteSpace: 'preserve-breaks'
|
|
115
|
-
|
|
116
|
-
transition: 'all 0.2s ease-in-out',
|
|
117
|
-
selectors: {
|
|
118
|
-
[`&[data-edge-dimmed='true']`]: {
|
|
119
|
-
opacity: 0.1,
|
|
120
|
-
transitionDelay: DimmedTransitionDelay
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
})
|
|
129
|
+
whiteSpace: 'preserve-breaks'
|
|
130
|
+
}])
|
|
124
131
|
|
|
125
132
|
export const edgeLabelText = style({
|
|
126
133
|
textAlign: 'center',
|
|
127
134
|
whiteSpaceCollapse: 'preserve-breaks',
|
|
128
|
-
fontSize:
|
|
135
|
+
fontSize: mantine.fontSizes.md,
|
|
129
136
|
lineHeight: mantine.lineHeights.xs
|
|
130
137
|
})
|
|
131
138
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { invariant } from '@likec4/core'
|
|
2
|
-
import {
|
|
2
|
+
import { Box } from '@mantine/core'
|
|
3
|
+
import { useMergedRef, useReducedMotion } from '@mantine/hooks'
|
|
3
4
|
import { deepEqual, shallowEqual } from 'fast-equals'
|
|
4
5
|
import { useAnimate } from 'framer-motion'
|
|
5
6
|
import { createContext, type PropsWithChildren, useCallback, useEffect, useRef } from 'react'
|
|
@@ -25,6 +26,7 @@ export function DiagramContextProvider({
|
|
|
25
26
|
whereFilter,
|
|
26
27
|
...props
|
|
27
28
|
}: DiagramContextProviderProps) {
|
|
29
|
+
const isMotionReduced = useReducedMotion() ?? false
|
|
28
30
|
const [scope, animate] = useAnimate()
|
|
29
31
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
30
32
|
const ref = useMergedRef(scope, containerRef)
|
|
@@ -70,19 +72,24 @@ export function DiagramContextProvider({
|
|
|
70
72
|
|
|
71
73
|
const api = store.current
|
|
72
74
|
useEffect(() => {
|
|
75
|
+
if (isMotionReduced) {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
73
78
|
return api.subscribe(s => !!s.activeOverlay, (isActiveOverlay) => {
|
|
74
79
|
animate('.likec4-diagram .react-flow__renderer', {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
transform: isActiveOverlay ? `translateY(
|
|
80
|
+
opacity: isActiveOverlay ? 0.7 : 1,
|
|
81
|
+
filter: isActiveOverlay ? 'grayscale(1)' : 'grayscale(0)',
|
|
82
|
+
transform: isActiveOverlay ? `perspective(300px) translateZ(-10px) translateY(2px)` : `translateY(0)`
|
|
78
83
|
}, {
|
|
79
|
-
duration: isActiveOverlay ? 0.
|
|
84
|
+
duration: isActiveOverlay ? 0.4 : 0.2
|
|
80
85
|
})
|
|
86
|
+
}, {
|
|
87
|
+
fireImmediately: true
|
|
81
88
|
})
|
|
82
|
-
}, [api])
|
|
89
|
+
}, [api, isMotionReduced])
|
|
83
90
|
|
|
84
91
|
return (
|
|
85
|
-
<
|
|
92
|
+
<Box
|
|
86
93
|
ref={ref}
|
|
87
94
|
className={className}
|
|
88
95
|
{...(keepAspectRatio && {
|
|
@@ -94,7 +101,7 @@ export function DiagramContextProvider({
|
|
|
94
101
|
<DiagramContext.Provider value={api}>
|
|
95
102
|
{children}
|
|
96
103
|
</DiagramContext.Provider>
|
|
97
|
-
</
|
|
104
|
+
</Box>
|
|
98
105
|
)
|
|
99
106
|
}
|
|
100
107
|
DiagramContextProvider.displayName = 'DiagramContextProvider'
|
|
@@ -60,10 +60,8 @@ export type DiagramInitialState = {
|
|
|
60
60
|
// If LikeC4Model provided
|
|
61
61
|
hasLikeC4Model: boolean
|
|
62
62
|
controls: boolean
|
|
63
|
-
showElementLinks: boolean
|
|
64
63
|
showNavigationButtons: boolean
|
|
65
64
|
showNotations: boolean
|
|
66
|
-
showRelationshipDetails: boolean
|
|
67
65
|
fitViewEnabled: boolean
|
|
68
66
|
fitViewPadding: number
|
|
69
67
|
zoomable: boolean
|
|
@@ -72,7 +70,9 @@ export type DiagramInitialState = {
|
|
|
72
70
|
nodesSelectable: boolean
|
|
73
71
|
experimentalEdgeEditing: boolean
|
|
74
72
|
enableFocusMode: boolean
|
|
75
|
-
|
|
73
|
+
enableElementDetails: boolean
|
|
74
|
+
enableRelationshipBrowser: boolean
|
|
75
|
+
enableRelationshipDetails: boolean
|
|
76
76
|
renderIcon: ElementIconRenderer | null
|
|
77
77
|
whereFilter: WhereOperator<string, string> | null
|
|
78
78
|
// If Dynamic View
|
|
@@ -104,6 +104,7 @@ export type DiagramState = Simplify<
|
|
|
104
104
|
activeOverlay:
|
|
105
105
|
| null
|
|
106
106
|
| RequireExactlyOne<{
|
|
107
|
+
elementDetails: Fqn
|
|
107
108
|
relationshipsOf: Fqn
|
|
108
109
|
edgeDetails: EdgeId
|
|
109
110
|
}>
|
|
@@ -776,7 +777,7 @@ export function createDiagramStore(props: DiagramInitialState) {
|
|
|
776
777
|
}
|
|
777
778
|
const xynode = xynodes.find(({ id }) => id === xynodeId)
|
|
778
779
|
invariant(xynode, `xynode not found: ${xynodeId}`)
|
|
779
|
-
const element =
|
|
780
|
+
const element = xynode.data.element
|
|
780
781
|
invariant(element?.navigateTo, `node is not navigable: ${xynodeId}`)
|
|
781
782
|
cancelSaveManualLayout()
|
|
782
783
|
set(
|
|
@@ -853,7 +854,14 @@ export function createDiagramStore(props: DiagramInitialState) {
|
|
|
853
854
|
if (eq(overlay, get().activeOverlay)) {
|
|
854
855
|
return
|
|
855
856
|
}
|
|
856
|
-
set(
|
|
857
|
+
set(
|
|
858
|
+
{
|
|
859
|
+
activeWalkthrough: null,
|
|
860
|
+
activeOverlay: overlay
|
|
861
|
+
},
|
|
862
|
+
noReplace,
|
|
863
|
+
'openOverlay'
|
|
864
|
+
)
|
|
857
865
|
},
|
|
858
866
|
closeOverlay: () => {
|
|
859
867
|
if (get().activeOverlay !== null) {
|
|
@@ -862,7 +870,7 @@ export function createDiagramStore(props: DiagramInitialState) {
|
|
|
862
870
|
activeOverlay: null
|
|
863
871
|
},
|
|
864
872
|
noReplace,
|
|
865
|
-
'
|
|
873
|
+
'closeOverlay'
|
|
866
874
|
)
|
|
867
875
|
}
|
|
868
876
|
},
|
package/src/theme-vars.ts
CHANGED
|
@@ -72,5 +72,14 @@ export const xyvars = createGlobalThemeContract({
|
|
|
72
72
|
export const whereLight = ':where([data-mantine-color-scheme="light"])'
|
|
73
73
|
export const whereDark = ':where([data-mantine-color-scheme="dark"])'
|
|
74
74
|
|
|
75
|
+
export const easings = {
|
|
76
|
+
out: 'cubic-bezier(0, 0, 0.40, 1)',
|
|
77
|
+
inOut: 'cubic-bezier(0.50, 0, 0.2, 1)'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const transitions = {
|
|
81
|
+
fast: `all 130ms ${easings.inOut}`
|
|
82
|
+
}
|
|
83
|
+
|
|
75
84
|
// we need only variables names
|
|
76
85
|
export const mantine = themeToVars({})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useMantineStyleNonce } from '@mantine/core'
|
|
2
|
+
import { useId } from '@mantine/hooks'
|
|
3
|
+
import { domMax, LayoutGroup, LazyMotion, MotionConfig } from 'framer-motion'
|
|
4
|
+
import { type PropsWithChildren } from 'react'
|
|
5
|
+
|
|
6
|
+
export const FramerMotionConfig = ({ children }: PropsWithChildren) => {
|
|
7
|
+
const layoutId = useId()
|
|
8
|
+
const nonce = useMantineStyleNonce()?.()
|
|
9
|
+
return (
|
|
10
|
+
<LazyMotion features={domMax} strict>
|
|
11
|
+
<MotionConfig reducedMotion="user" {...nonce && { nonce }}>
|
|
12
|
+
<LayoutGroup id={layoutId}>
|
|
13
|
+
{children}
|
|
14
|
+
</LayoutGroup>
|
|
15
|
+
</MotionConfig>
|
|
16
|
+
</LazyMotion>
|
|
17
|
+
)
|
|
18
|
+
}
|