@likec4/diagram 1.13.0 → 1.15.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 +9 -9
- package/src/LikeC4CustomColors.tsx +6 -3
- package/src/LikeC4Diagram.css.ts +4 -0
- package/src/LikeC4Diagram.tsx +9 -7
- package/src/overlays/Overlays.css.ts +124 -0
- package/src/overlays/Overlays.tsx +210 -0
- package/src/overlays/edge-details/EdgeDetailsXYFlow.tsx +247 -0
- package/src/overlays/edge-details/SelectEdge.css.ts +99 -0
- package/src/overlays/edge-details/SelectEdge.tsx +153 -0
- package/src/overlays/edge-details/_types.ts +38 -0
- package/src/overlays/edge-details/use-layouted-edge-details.ts +356 -0
- package/src/overlays/edge-details/xyflow/CompoundNode.tsx +80 -0
- package/src/overlays/edge-details/xyflow/ElementNode.tsx +154 -0
- package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +81 -0
- package/src/overlays/edge-details/xyflow/styles.css.ts +196 -0
- package/src/overlays/index.tsx +1 -33
- package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +124 -63
- package/src/overlays/relationships-of/_types.ts +13 -2
- package/src/overlays/relationships-of/use-layouted-relationships.ts +37 -37
- package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +2 -2
- package/src/overlays/relationships-of/xyflow/ElementNode.tsx +47 -26
- package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +58 -22
- package/src/overlays/relationships-of/xyflow/styles.css.ts +53 -16
- package/src/state/DiagramContext.tsx +23 -5
- package/src/state/diagram-to-xyflow.ts +31 -20
- package/src/state/diagramStore.ts +30 -23
- package/src/xyflow/XYFlow.tsx +2 -1
- package/src/xyflow/XYFlowEvents.tsx +13 -1
- package/src/xyflow/edges/RelationshipEdge.tsx +10 -4
- package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +24 -21
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +1 -0
- package/src/xyflow/nodes/compound/CompoundNode.tsx +11 -11
- package/src/xyflow/nodes/shared/Toolbar.tsx +1 -1
- package/src/xyflow/types.ts +1 -0
- package/src/xyflow/useLayoutConstraints.ts +38 -19
- package/src/components/Overlay.css.ts +0 -78
- package/src/components/Overlay.tsx +0 -102
- package/src/components/index.ts +0 -2
- package/src/overlays/edge-details/EdgeDetailsOverlay.css.ts +0 -13
- package/src/overlays/edge-details/EdgeDetailsOverlay.tsx +0 -37
- package/src/overlays/relationships-of/RelationshipsOverlay.tsx +0 -33
- package/src/overlays/relationships-of/styles.css.ts +0 -34
- /package/src/{components → overlays}/OverlayContext.tsx +0 -0
|
@@ -28,7 +28,8 @@ import {
|
|
|
28
28
|
reverse,
|
|
29
29
|
sort,
|
|
30
30
|
sortBy,
|
|
31
|
-
takeWhile
|
|
31
|
+
takeWhile,
|
|
32
|
+
tap
|
|
32
33
|
} from 'remeda'
|
|
33
34
|
import { useDiagramState } from '../../hooks/useDiagramState'
|
|
34
35
|
import { useLikeC4Model } from '../../likec4model'
|
|
@@ -50,10 +51,10 @@ const Sizes = {
|
|
|
50
51
|
edgesep: 20
|
|
51
52
|
} satisfies GraphLabel,
|
|
52
53
|
edgeLabel: {
|
|
53
|
-
width:
|
|
54
|
+
width: 60
|
|
54
55
|
} satisfies Label,
|
|
55
56
|
|
|
56
|
-
emptyNodeOffset:
|
|
57
|
+
emptyNodeOffset: 150,
|
|
57
58
|
|
|
58
59
|
nodeWidth: 270,
|
|
59
60
|
hodeHeight: 160,
|
|
@@ -62,7 +63,7 @@ const Sizes = {
|
|
|
62
63
|
// 0 means no spacer
|
|
63
64
|
spacerHeight: 0,
|
|
64
65
|
|
|
65
|
-
compoundLabelHeight:
|
|
66
|
+
compoundLabelHeight: 1
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
export const ZIndexes = {
|
|
@@ -90,10 +91,12 @@ function createGraph() {
|
|
|
90
91
|
columns.reduce((prev, column) => {
|
|
91
92
|
const c = graphColumn(column)
|
|
92
93
|
g.setNode(c.id, {})
|
|
93
|
-
g.setNode(c.anchor, { width:
|
|
94
|
+
g.setNode(c.anchor, { width: 40, height: 2 })
|
|
94
95
|
g.setParent(c.anchor, c.id)
|
|
95
96
|
if (prev) {
|
|
96
|
-
g.setEdge(prev, c.anchor
|
|
97
|
+
g.setEdge(prev, c.anchor, {
|
|
98
|
+
width: 0
|
|
99
|
+
})
|
|
97
100
|
}
|
|
98
101
|
return c.anchor
|
|
99
102
|
}, null as string | null)
|
|
@@ -254,9 +257,6 @@ function createNode(
|
|
|
254
257
|
|
|
255
258
|
g.setNode(k.id, sized())
|
|
256
259
|
if (xynode.type === 'compound') {
|
|
257
|
-
g.setNode(k.id, {
|
|
258
|
-
padding: 40
|
|
259
|
-
})
|
|
260
260
|
g.setNode(k.port, {
|
|
261
261
|
width: Sizes.nodeWidth - Sizes.dagre.ranksep,
|
|
262
262
|
height: Sizes.compoundLabelHeight
|
|
@@ -397,6 +397,12 @@ function layout(
|
|
|
397
397
|
subject: LikeC4ModelElement
|
|
398
398
|
nodes: XYFlowTypes.Node[]
|
|
399
399
|
edges: XYFlowTypes.Edge[]
|
|
400
|
+
bounds: {
|
|
401
|
+
x: number
|
|
402
|
+
y: number
|
|
403
|
+
width: number
|
|
404
|
+
height: number
|
|
405
|
+
}
|
|
400
406
|
} {
|
|
401
407
|
const diagramNodes = new Map(view.nodes.map(n => [n.id, n]))
|
|
402
408
|
const subjectElement = likec4model.element(subjectId)
|
|
@@ -459,13 +465,11 @@ function layout(
|
|
|
459
465
|
|
|
460
466
|
forEachIncoming(sourceId, relations) {
|
|
461
467
|
const source = createNode('incomers', 'element', likec4model.element(sourceId), ctx)
|
|
462
|
-
g.setEdge(graphId(source).port, subject.id
|
|
463
|
-
weight: source.type === 'compound' ? 0 : 1
|
|
464
|
-
})
|
|
468
|
+
g.setEdge(graphId(source).port, subject.id)
|
|
465
469
|
|
|
466
|
-
const op = source.type === 'compound' ? 'unshift' : 'push'
|
|
470
|
+
// const op = source.type === 'compound' ? 'unshift' : 'push'
|
|
467
471
|
|
|
468
|
-
subject.data.ports.left
|
|
472
|
+
subject.data.ports.left.push({
|
|
469
473
|
id: source.id,
|
|
470
474
|
type: 'in'
|
|
471
475
|
})
|
|
@@ -482,12 +486,10 @@ function layout(
|
|
|
482
486
|
|
|
483
487
|
forEachOutgoing(targetId, relations) {
|
|
484
488
|
const target = createNode('outgoers', 'element', likec4model.element(targetId), ctx)
|
|
485
|
-
g.setEdge(subjectElement.id, graphId(target).port
|
|
486
|
-
weight: target.type === 'compound' ? 2 : 1
|
|
487
|
-
})
|
|
489
|
+
g.setEdge(subjectElement.id, graphId(target).port)
|
|
488
490
|
|
|
489
|
-
const op = target.type === 'compound' ? 'unshift' : 'push'
|
|
490
|
-
subject.data.ports.right
|
|
491
|
+
// const op = target.type === 'compound' ? 'unshift' : 'push'
|
|
492
|
+
subject.data.ports.right.push({
|
|
491
493
|
id: target.id,
|
|
492
494
|
type: 'out'
|
|
493
495
|
})
|
|
@@ -548,24 +550,25 @@ function layout(
|
|
|
548
550
|
)
|
|
549
551
|
}
|
|
550
552
|
|
|
551
|
-
// Sort ports by their position
|
|
552
553
|
for (const node of xynodes) {
|
|
554
|
+
// Grow empty nodes to fill the space
|
|
553
555
|
if (node.type === 'empty') {
|
|
554
556
|
const subjectPosition = nodebounds(subject.id)
|
|
555
557
|
node.position.y = subjectPosition.position.y + subjectPosition.height / 2 - node.height / 2
|
|
556
558
|
if (node.data.column === 'incomers') {
|
|
557
|
-
|
|
558
|
-
node.position.x = subjectPosition.position.x - node.width - Sizes.emptyNodeOffset
|
|
559
|
+
node.width = subjectPosition.position.x - Sizes.emptyNodeOffset - node.position.x
|
|
559
560
|
} else {
|
|
560
|
-
|
|
561
|
+
const rightX = node.position.x + node.width
|
|
561
562
|
node.position.x = subjectPosition.position.x + subjectPosition.width + Sizes.emptyNodeOffset
|
|
563
|
+
node.width = rightX - node.position.x
|
|
562
564
|
}
|
|
563
565
|
continue
|
|
564
566
|
}
|
|
565
|
-
|
|
567
|
+
// Sort ports by their position
|
|
568
|
+
if (node.data.ports.left.length > 1) {
|
|
566
569
|
node.data.ports.left = sortedPorts(node.data.ports.left)
|
|
567
570
|
}
|
|
568
|
-
if (node.data.ports.right.length >
|
|
571
|
+
if (node.data.ports.right.length > 1) {
|
|
569
572
|
node.data.ports.right = sortedPorts(node.data.ports.right)
|
|
570
573
|
}
|
|
571
574
|
}
|
|
@@ -575,21 +578,18 @@ function layout(
|
|
|
575
578
|
viewIncludesSubject: diagramNodes.has(subjectId),
|
|
576
579
|
subject: subjectElement,
|
|
577
580
|
edges: ctx.edges,
|
|
578
|
-
nodes: xynodes
|
|
581
|
+
nodes: xynodes,
|
|
582
|
+
bounds: {
|
|
583
|
+
x: 0,
|
|
584
|
+
y: 0,
|
|
585
|
+
width: g.graph().width ?? 0,
|
|
586
|
+
height: g.graph().height ?? 0
|
|
587
|
+
}
|
|
579
588
|
}
|
|
580
589
|
}
|
|
581
590
|
|
|
582
|
-
export function useLayoutedRelationships(scope: 'global' | 'view') {
|
|
583
|
-
const
|
|
584
|
-
subjectId,
|
|
585
|
-
view
|
|
586
|
-
} = useDiagramState(s => {
|
|
587
|
-
return {
|
|
588
|
-
subjectId: s.activeOverlay?.relationshipsOf,
|
|
589
|
-
view: s.view
|
|
590
|
-
}
|
|
591
|
-
})
|
|
592
|
-
invariant(subjectId, 'subject not found')
|
|
591
|
+
export function useLayoutedRelationships(subjectId: Fqn, scope: 'global' | 'view') {
|
|
592
|
+
const view = useDiagramState(s => s.view)
|
|
593
593
|
const likec4model = useLikeC4Model(true)
|
|
594
594
|
return useMemo(() =>
|
|
595
595
|
layout(
|
|
@@ -27,12 +27,12 @@ export function CompoundNode({
|
|
|
27
27
|
css.compoundNodeBody,
|
|
28
28
|
'likec4-compound-node'
|
|
29
29
|
])}
|
|
30
|
-
data-compound-depth={
|
|
30
|
+
data-compound-depth={3}
|
|
31
31
|
data-likec4-color={element.color}
|
|
32
32
|
animate={{
|
|
33
33
|
opacity: data.dimmed ? 0.15 : 1,
|
|
34
34
|
transition: {
|
|
35
|
-
delay: data.dimmed ? .
|
|
35
|
+
delay: data.dimmed === true ? .4 : 0
|
|
36
36
|
}
|
|
37
37
|
}}
|
|
38
38
|
{...(selectable && {
|
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import { ActionIcon, Box, Text as MantineText } from '@mantine/core'
|
|
2
|
-
import { IconZoomScan } from '@tabler/icons-react'
|
|
1
|
+
import { ActionIcon, Box, Group, Text as MantineText } from '@mantine/core'
|
|
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
5
|
import { m } from 'framer-motion'
|
|
6
|
-
import { useOverlayDialog } from '../../../components'
|
|
7
6
|
import { type DiagramState, useDiagramState } from '../../../hooks'
|
|
8
7
|
import { ElementShapeSvg } from '../../../xyflow/nodes/element/ElementShapeSvg'
|
|
9
8
|
import { stopPropagation } from '../../../xyflow/utils'
|
|
9
|
+
import { useOverlayDialog } from '../../OverlayContext'
|
|
10
10
|
import type { XYFlowTypes } from '../_types'
|
|
11
11
|
import * as css from './styles.css'
|
|
12
12
|
|
|
13
|
+
const Action = ActionIcon.withProps({
|
|
14
|
+
className: 'nodrag nopan ' + css.navigateBtn,
|
|
15
|
+
radius: 'md',
|
|
16
|
+
role: 'button',
|
|
17
|
+
onDoubleClick: stopPropagation,
|
|
18
|
+
onPointerDownCapture: stopPropagation
|
|
19
|
+
})
|
|
20
|
+
|
|
13
21
|
const Text = MantineText.withProps({
|
|
14
22
|
component: 'div'
|
|
15
23
|
})
|
|
@@ -18,8 +26,9 @@ type ElementNodeProps = NodeProps<XYFlowTypes.ElementNode>
|
|
|
18
26
|
|
|
19
27
|
function selector(s: DiagramState) {
|
|
20
28
|
return {
|
|
21
|
-
|
|
22
|
-
onNavigateTo: s.onNavigateTo
|
|
29
|
+
currentViewId: s.view.id,
|
|
30
|
+
onNavigateTo: s.onNavigateTo,
|
|
31
|
+
onOpenSource: s.onOpenSourceElement
|
|
23
32
|
}
|
|
24
33
|
}
|
|
25
34
|
|
|
@@ -36,8 +45,9 @@ export function ElementNode({
|
|
|
36
45
|
}: ElementNodeProps) {
|
|
37
46
|
const overlay = useOverlayDialog()
|
|
38
47
|
const {
|
|
39
|
-
|
|
40
|
-
onNavigateTo
|
|
48
|
+
currentViewId,
|
|
49
|
+
onNavigateTo,
|
|
50
|
+
onOpenSource
|
|
41
51
|
} = useDiagramState(selector)
|
|
42
52
|
return (
|
|
43
53
|
<>
|
|
@@ -50,7 +60,7 @@ export function ElementNode({
|
|
|
50
60
|
animate={{
|
|
51
61
|
opacity: data.dimmed ? 0.15 : 1,
|
|
52
62
|
transition: {
|
|
53
|
-
delay: data.dimmed ? .
|
|
63
|
+
delay: data.dimmed === true ? .4 : 0
|
|
54
64
|
}
|
|
55
65
|
}}
|
|
56
66
|
{...(selectable && {
|
|
@@ -73,10 +83,7 @@ export function ElementNode({
|
|
|
73
83
|
width={w}
|
|
74
84
|
height={h}
|
|
75
85
|
>
|
|
76
|
-
<ElementShapeSvg
|
|
77
|
-
shape={element.shape}
|
|
78
|
-
w={w}
|
|
79
|
-
h={h} />
|
|
86
|
+
<ElementShapeSvg shape={element.shape} w={w} h={h} />
|
|
80
87
|
</svg>
|
|
81
88
|
<Box className={css.elementNodeContent}>
|
|
82
89
|
<Text className={css.elementNodeTitle} lineClamp={2}>{element.title}</Text>
|
|
@@ -84,24 +91,38 @@ export function ElementNode({
|
|
|
84
91
|
<Text className={css.elementNodeDescription} lineClamp={4}>{element.description}</Text>
|
|
85
92
|
)}
|
|
86
93
|
</Box>
|
|
87
|
-
{
|
|
88
|
-
|
|
89
|
-
<
|
|
90
|
-
className={clsx('nodrag nopan', css.navigateBtn)}
|
|
91
|
-
radius="md"
|
|
94
|
+
<Group className={css.navigateBtnBox}>
|
|
95
|
+
{navigateTo && onNavigateTo && navigateTo !== currentViewId && (
|
|
96
|
+
<Action
|
|
92
97
|
onClick={(event) => {
|
|
93
98
|
event.stopPropagation()
|
|
94
|
-
setTimeout(() => onNavigateTo(navigateTo),
|
|
99
|
+
setTimeout(() => onNavigateTo(navigateTo), 100)
|
|
95
100
|
overlay.close()
|
|
96
|
-
}}
|
|
97
|
-
role="button"
|
|
98
|
-
onDoubleClick={stopPropagation}
|
|
99
|
-
onPointerDownCapture={stopPropagation}
|
|
100
|
-
>
|
|
101
|
+
}}>
|
|
101
102
|
<IconZoomScan stroke={1.8} style={{ width: '75%' }} />
|
|
102
|
-
</
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
</Action>
|
|
104
|
+
)}
|
|
105
|
+
{data.column !== 'subject' && (
|
|
106
|
+
<Action
|
|
107
|
+
onClick={(event) => {
|
|
108
|
+
event.stopPropagation()
|
|
109
|
+
overlay.openOverlay({
|
|
110
|
+
relationshipsOf: data.fqn
|
|
111
|
+
})
|
|
112
|
+
}}>
|
|
113
|
+
<IconTransform stroke={1.8} style={{ width: '72%' }} />
|
|
114
|
+
</Action>
|
|
115
|
+
)}
|
|
116
|
+
{onOpenSource && (
|
|
117
|
+
<Action
|
|
118
|
+
onClick={(event) => {
|
|
119
|
+
event.stopPropagation()
|
|
120
|
+
onOpenSource(data.fqn)
|
|
121
|
+
}}>
|
|
122
|
+
<IconFileSymlink stroke={1.8} style={{ width: '72%' }} />
|
|
123
|
+
</Action>
|
|
124
|
+
)}
|
|
125
|
+
</Group>
|
|
105
126
|
</m.div>
|
|
106
127
|
{ports.left.map(({ id, type }, i) => (
|
|
107
128
|
<Handle
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { Group, Text, ThemeIcon } from '@mantine/core'
|
|
2
|
-
import { IconBoxMultipleFilled } from '@tabler/icons-react'
|
|
1
|
+
import { ActionIcon, Box, Group, Stack, Text, ThemeIcon } from '@mantine/core'
|
|
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 { only } from 'remeda'
|
|
6
|
+
import { useDiagramState } from '../../../hooks/useDiagramState'
|
|
7
|
+
import { stopPropagation } from '../../../xyflow/utils'
|
|
5
8
|
import type { XYFlowTypes } from '../_types'
|
|
6
9
|
import { ZIndexes } from '../use-layouted-relationships'
|
|
7
10
|
import * as css from './styles.css'
|
|
@@ -11,8 +14,11 @@ export function RelationshipEdge({
|
|
|
11
14
|
label,
|
|
12
15
|
...props
|
|
13
16
|
}: EdgeProps<XYFlowTypes.Edge>) {
|
|
17
|
+
const onNavigateTo = useDiagramState(s => s.onNavigateTo)
|
|
14
18
|
const [edgePath, labelX, labelY] = getBezierPath(props)
|
|
19
|
+
const navigateTo = onNavigateTo ? only(data.relations)?.navigateTo : undefined
|
|
15
20
|
const isMultiRelation = data.relations.length > 1
|
|
21
|
+
const technology = only(data.relations)?.technology
|
|
16
22
|
return (
|
|
17
23
|
<>
|
|
18
24
|
<g
|
|
@@ -25,35 +31,65 @@ export function RelationshipEdge({
|
|
|
25
31
|
{...props}
|
|
26
32
|
/>
|
|
27
33
|
</g>
|
|
28
|
-
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
<EdgeLabelRenderer>
|
|
35
|
+
<Stack
|
|
36
|
+
gap={2}
|
|
37
|
+
style={{
|
|
38
|
+
position: 'absolute',
|
|
39
|
+
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
|
40
|
+
maxWidth: Math.abs(props.targetX - props.sourceX - 40),
|
|
41
|
+
zIndex: ZIndexes.max
|
|
42
|
+
}}
|
|
43
|
+
className={clsx([
|
|
44
|
+
css.edgeLabel,
|
|
45
|
+
'nodrag nopan'
|
|
46
|
+
])}
|
|
47
|
+
data-edge-dimmed={data.dimmed}
|
|
48
|
+
data-edge-hovered={data.hovered}
|
|
49
|
+
>
|
|
50
|
+
{label && (
|
|
44
51
|
<Group gap={6} wrap="nowrap">
|
|
45
52
|
{isMultiRelation && (
|
|
46
53
|
<ThemeIcon size={'sm'} variant="transparent" color="orange">
|
|
47
54
|
<IconBoxMultipleFilled style={{ width: '80%' }} />
|
|
48
55
|
</ThemeIcon>
|
|
49
56
|
)}
|
|
50
|
-
<Text
|
|
57
|
+
<Text
|
|
58
|
+
fw={isMultiRelation ? '500' : '400'}
|
|
59
|
+
component={'div'}
|
|
60
|
+
className={css.edgeLabelText}
|
|
61
|
+
lineClamp={3}>
|
|
51
62
|
{label}
|
|
52
63
|
</Text>
|
|
53
64
|
</Group>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
)}
|
|
66
|
+
{technology && (
|
|
67
|
+
<Text component={'div'} className={css.edgeLabelTechnology}>
|
|
68
|
+
{'[ '}
|
|
69
|
+
{technology}
|
|
70
|
+
{' ]'}
|
|
71
|
+
</Text>
|
|
72
|
+
)}
|
|
73
|
+
{navigateTo && (
|
|
74
|
+
<Box ta={'center'} mt={4}>
|
|
75
|
+
<ActionIcon
|
|
76
|
+
variant="default"
|
|
77
|
+
size={'sm'}
|
|
78
|
+
radius="sm"
|
|
79
|
+
onPointerDownCapture={stopPropagation}
|
|
80
|
+
onClick={event => {
|
|
81
|
+
event.stopPropagation()
|
|
82
|
+
onNavigateTo?.(navigateTo, event)
|
|
83
|
+
}}
|
|
84
|
+
role="button"
|
|
85
|
+
onDoubleClick={stopPropagation}
|
|
86
|
+
>
|
|
87
|
+
<IconZoomScan />
|
|
88
|
+
</ActionIcon>
|
|
89
|
+
</Box>
|
|
90
|
+
)}
|
|
91
|
+
</Stack>
|
|
92
|
+
</EdgeLabelRenderer>
|
|
57
93
|
</>
|
|
58
94
|
)
|
|
59
95
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { rem } from '@mantine/core'
|
|
2
|
+
import { globalStyle, style } from '@vanilla-extract/css'
|
|
2
3
|
import { mantine, vars, whereLight, xyvars } from '../../../theme-vars'
|
|
3
4
|
|
|
4
5
|
export const elementNode = style({
|
|
@@ -13,6 +14,7 @@ export const elementNodeContent = style({
|
|
|
13
14
|
flexDirection: 'column',
|
|
14
15
|
justifyContent: 'center',
|
|
15
16
|
alignItems: 'center',
|
|
17
|
+
overflow: 'hidden',
|
|
16
18
|
gap: 6,
|
|
17
19
|
padding: 16
|
|
18
20
|
})
|
|
@@ -48,7 +50,7 @@ export const compoundNodeBody = style({
|
|
|
48
50
|
width: '100%',
|
|
49
51
|
height: '100%',
|
|
50
52
|
boxShadow: '0 4px 10px 0.5px rgba(0,0,0,0.1) , 0 2px 2px -1px rgba(0,0,0,0.4)',
|
|
51
|
-
background: `color-mix(in srgb , ${vars.element.fill}, transparent
|
|
53
|
+
background: `color-mix(in srgb , ${vars.element.fill}, transparent 10%)`,
|
|
52
54
|
borderRadius: 4
|
|
53
55
|
})
|
|
54
56
|
|
|
@@ -63,13 +65,7 @@ export const compoundNodeTitle = style({
|
|
|
63
65
|
paddingTop: 12,
|
|
64
66
|
paddingLeft: 10,
|
|
65
67
|
mixBlendMode: 'screen',
|
|
66
|
-
color: vars.compound.titleColor
|
|
67
|
-
selectors: {
|
|
68
|
-
[`${whereLight} &`]: {
|
|
69
|
-
mixBlendMode: 'darken',
|
|
70
|
-
color: vars.element.stroke
|
|
71
|
-
}
|
|
72
|
-
}
|
|
68
|
+
color: vars.compound.titleColor
|
|
73
69
|
})
|
|
74
70
|
|
|
75
71
|
export const cssShapeSvg = style({
|
|
@@ -88,6 +84,8 @@ export const cssShapeSvg = style({
|
|
|
88
84
|
zIndex: -1
|
|
89
85
|
})
|
|
90
86
|
|
|
87
|
+
const DimmedTransitionDelay = '400ms'
|
|
88
|
+
|
|
91
89
|
export const edgePath = style({
|
|
92
90
|
opacity: 1,
|
|
93
91
|
transition: 'all 0.2s ease-in-out',
|
|
@@ -95,7 +93,7 @@ export const edgePath = style({
|
|
|
95
93
|
selectors: {
|
|
96
94
|
[`&[data-edge-dimmed='true']`]: {
|
|
97
95
|
opacity: 0.1,
|
|
98
|
-
transitionDelay:
|
|
96
|
+
transitionDelay: DimmedTransitionDelay
|
|
99
97
|
}
|
|
100
98
|
}
|
|
101
99
|
})
|
|
@@ -119,8 +117,7 @@ export const edgeLabel = style({
|
|
|
119
117
|
selectors: {
|
|
120
118
|
[`&[data-edge-dimmed='true']`]: {
|
|
121
119
|
opacity: 0.1,
|
|
122
|
-
|
|
123
|
-
transitionDelay: '800ms'
|
|
120
|
+
transitionDelay: DimmedTransitionDelay
|
|
124
121
|
}
|
|
125
122
|
}
|
|
126
123
|
})
|
|
@@ -128,10 +125,17 @@ export const edgeLabel = style({
|
|
|
128
125
|
export const edgeLabelText = style({
|
|
129
126
|
textAlign: 'center',
|
|
130
127
|
whiteSpaceCollapse: 'preserve-breaks',
|
|
131
|
-
fontSize:
|
|
128
|
+
fontSize: rem(15),
|
|
132
129
|
lineHeight: mantine.lineHeights.xs
|
|
133
130
|
})
|
|
134
131
|
|
|
132
|
+
export const edgeLabelTechnology = style({
|
|
133
|
+
textAlign: 'center',
|
|
134
|
+
whiteSpaceCollapse: 'preserve-breaks',
|
|
135
|
+
fontSize: mantine.fontSizes.sm,
|
|
136
|
+
lineHeight: 1
|
|
137
|
+
})
|
|
138
|
+
|
|
135
139
|
export const emptyNode = style({
|
|
136
140
|
width: '100%',
|
|
137
141
|
height: '100%',
|
|
@@ -148,23 +152,56 @@ export const navigateBtnBox = style({
|
|
|
148
152
|
left: '50%',
|
|
149
153
|
bottom: 2,
|
|
150
154
|
transform: 'translate(-50%, 0%)',
|
|
155
|
+
gap: 0,
|
|
156
|
+
transition: 'all 190ms cubic-bezier(0.5, 0, 0.4, 1)',
|
|
151
157
|
selectors: {
|
|
152
158
|
[`:where([data-likec4-shape='browser']) &`]: {
|
|
153
159
|
bottom: 4
|
|
154
160
|
}
|
|
155
161
|
}
|
|
156
162
|
})
|
|
163
|
+
globalStyle(`:where(${elementNode}:hover) ${navigateBtnBox}`, {
|
|
164
|
+
transitionDelay: '20ms',
|
|
165
|
+
gap: 8,
|
|
166
|
+
transform: 'translate(-50%, 5px)'
|
|
167
|
+
})
|
|
168
|
+
|
|
157
169
|
export const navigateBtn = style({
|
|
170
|
+
opacity: 0.7,
|
|
158
171
|
pointerEvents: 'all',
|
|
159
|
-
color: vars.element.loContrast,
|
|
160
172
|
cursor: 'pointer',
|
|
161
|
-
|
|
173
|
+
transform: 'scale(0.9)',
|
|
174
|
+
transition: 'all 190ms cubic-bezier(0.5, 0, 0.4, 1)',
|
|
162
175
|
'vars': {
|
|
163
176
|
'--ai-bg': `color-mix(in srgb , ${vars.element.fill}, transparent 99%)`,
|
|
164
177
|
'--ai-bg-hover': `color-mix(in srgb , ${vars.element.fill} 65%, ${vars.element.stroke})`,
|
|
165
178
|
'--ai-hover': `color-mix(in srgb , ${vars.element.fill} 50%, ${vars.element.stroke})`
|
|
166
179
|
},
|
|
167
180
|
':hover': {
|
|
168
|
-
|
|
181
|
+
transitionDelay: '0ms',
|
|
182
|
+
transform: 'scale(1.25)',
|
|
183
|
+
boxShadow: mantine.shadows.lg
|
|
184
|
+
},
|
|
185
|
+
':active': {
|
|
186
|
+
transform: 'scale(0.975)'
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
globalStyle(`:where(${elementNode}:hover) ${navigateBtn}`, {
|
|
191
|
+
transitionDelay: '40ms',
|
|
192
|
+
transitionTimingFunction: 'cubic-bezier(0, 0, 0.40, 1)',
|
|
193
|
+
opacity: 1,
|
|
194
|
+
transform: 'scale(1.07)',
|
|
195
|
+
boxShadow: mantine.shadows.md,
|
|
196
|
+
vars: {
|
|
197
|
+
'--ai-bg': `var(--ai-bg-hover)`
|
|
169
198
|
}
|
|
170
199
|
})
|
|
200
|
+
// globalStyle(`${elementNode}:hover ${navigateBtn}`, {
|
|
201
|
+
// opacity: 1,
|
|
202
|
+
// transform: 'scale(1.05)',
|
|
203
|
+
// boxShadow: mantine.shadows.md,
|
|
204
|
+
// vars: {
|
|
205
|
+
// '--ai-bg': `var(--ai-bg-hover)`
|
|
206
|
+
// }
|
|
207
|
+
// })
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { invariant } from '@likec4/core'
|
|
2
|
+
import { useMergedRef } from '@mantine/hooks'
|
|
2
3
|
import { deepEqual, shallowEqual } from 'fast-equals'
|
|
3
|
-
import {
|
|
4
|
+
import { useAnimate } from 'framer-motion'
|
|
5
|
+
import { createContext, type PropsWithChildren, useCallback, useEffect, useRef } from 'react'
|
|
6
|
+
import { clone } from 'remeda'
|
|
4
7
|
import { useUpdateEffect } from '../hooks/useUpdateEffect'
|
|
5
8
|
import { useXYFlow, useXYStoreApi } from '../hooks/useXYFlow'
|
|
6
9
|
import { createDiagramStore, type DiagramInitialState, type DiagramZustandStore } from './diagramStore'
|
|
@@ -22,7 +25,9 @@ export function DiagramContextProvider({
|
|
|
22
25
|
whereFilter,
|
|
23
26
|
...props
|
|
24
27
|
}: DiagramContextProviderProps) {
|
|
28
|
+
const [scope, animate] = useAnimate()
|
|
25
29
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
30
|
+
const ref = useMergedRef(scope, containerRef)
|
|
26
31
|
const xystore = useXYStoreApi()
|
|
27
32
|
const xyflow = useXYFlow()
|
|
28
33
|
const store = useRef<DiagramZustandStore>()
|
|
@@ -35,7 +40,7 @@ export function DiagramContextProvider({
|
|
|
35
40
|
xyflow,
|
|
36
41
|
view,
|
|
37
42
|
getContainer,
|
|
38
|
-
whereFilter:
|
|
43
|
+
whereFilter: clone(whereFilter),
|
|
39
44
|
...props
|
|
40
45
|
})
|
|
41
46
|
}
|
|
@@ -55,7 +60,7 @@ export function DiagramContextProvider({
|
|
|
55
60
|
const current = store.current
|
|
56
61
|
invariant(current, 'DiagramContext.store.current is not defined')
|
|
57
62
|
if (!deepEqual(whereFilter, current.getState().whereFilter)) {
|
|
58
|
-
current.setState({ whereFilter:
|
|
63
|
+
current.setState({ whereFilter: clone(whereFilter) }, false, 'update where filter')
|
|
59
64
|
}
|
|
60
65
|
current.getState().updateView(view)
|
|
61
66
|
},
|
|
@@ -63,9 +68,22 @@ export function DiagramContextProvider({
|
|
|
63
68
|
(a, b) => shallowEqual(a[0], b[0]) && deepEqual(a[1], b[1])
|
|
64
69
|
)
|
|
65
70
|
|
|
71
|
+
const api = store.current
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
return api.subscribe(s => !!s.activeOverlay, (isActiveOverlay) => {
|
|
74
|
+
animate('.likec4-diagram .react-flow__renderer', {
|
|
75
|
+
filter: isActiveOverlay ? 'grayscale(0.85) brightness(0.5)' : 'grayscale(0) brightness()',
|
|
76
|
+
scale: isActiveOverlay ? 0.97 : 1,
|
|
77
|
+
transform: isActiveOverlay ? `translateY(8px)` : `translateY(0)`
|
|
78
|
+
}, {
|
|
79
|
+
duration: isActiveOverlay ? 0.5 : 0.16
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
}, [api])
|
|
83
|
+
|
|
66
84
|
return (
|
|
67
85
|
<div
|
|
68
|
-
ref={
|
|
86
|
+
ref={ref}
|
|
69
87
|
className={className}
|
|
70
88
|
{...(keepAspectRatio && {
|
|
71
89
|
style: {
|
|
@@ -73,7 +91,7 @@ export function DiagramContextProvider({
|
|
|
73
91
|
maxHeight: Math.ceil(view.bounds.height)
|
|
74
92
|
}
|
|
75
93
|
})}>
|
|
76
|
-
<DiagramContext.Provider value={
|
|
94
|
+
<DiagramContext.Provider value={api}>
|
|
77
95
|
{children}
|
|
78
96
|
</DiagramContext.Provider>
|
|
79
97
|
</div>
|