@likec4/diagram 1.3.0 → 1.5.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/dist/LikeC4Diagram.css.d.ts +0 -1
- package/dist/LikeC4Diagram.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +7 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +1580 -1194
- package/dist/state/DiagramContext.d.ts +97 -4169
- package/dist/state/diagramStore.d.ts +98 -4172
- package/dist/state/hooks.d.ts +4 -0
- package/dist/state/index.d.ts +1 -3
- package/dist/state/useDiagramStore.d.ts +3 -4622
- package/dist/state/useNavigationHistory.d.ts +5 -0
- package/dist/style.css +217 -159
- package/dist/theme.css.d.ts +5 -0
- package/dist/ui/BackwardForwardPanel.css.d.ts +1 -0
- package/dist/ui/BackwardForwardPanel.d.ts +1 -0
- package/dist/ui/DiagramTitlePanel.css.d.ts +1 -1
- package/dist/ui/OptionsPanel.d.ts +1 -3
- package/dist/ui/options/EdgeOptions.d.ts +19 -0
- package/dist/ui/options/NodeOptions.d.ts +2 -2
- package/dist/xyflow/EdgeMarkers.d.ts +11 -0
- package/dist/xyflow/XYFlowEvents.d.ts +12 -9
- package/dist/xyflow/XYFlowInner.d.ts +1 -0
- package/dist/xyflow/const.d.ts +3 -3
- package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
- package/dist/xyflow/edges/RelationshipEdge.d.ts +4 -3
- package/dist/xyflow/edges/edges.css.d.ts +1 -0
- package/dist/xyflow/edges/utils.d.ts +1 -0
- package/dist/xyflow/hooks/index.d.ts +3 -2
- package/dist/xyflow/hooks/useXYFlow.d.ts +22 -21
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
- package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -4
- package/dist/xyflow/nodes/element/element.css.d.ts +3 -1
- package/dist/xyflow/types.d.ts +2 -32
- package/package.json +21 -21
- package/src/LikeC4Diagram.css.ts +10 -8
- package/src/LikeC4Diagram.props.ts +12 -5
- package/src/LikeC4Diagram.tsx +43 -26
- package/src/globals.css.ts +11 -4
- package/src/index.ts +1 -1
- package/src/mantine.css.ts +2 -2
- package/src/state/DiagramContext.tsx +47 -16
- package/src/state/diagramStore.ts +89 -80
- package/src/state/hooks.ts +5 -0
- package/src/state/index.ts +1 -3
- package/src/state/useDiagramStore.ts +6 -13
- package/src/state/useNavigationHistory.ts +39 -0
- package/src/theme.css.ts +5 -0
- package/src/ui/BackwardForwardPanel.css.ts +22 -0
- package/src/ui/BackwardForwardPanel.tsx +59 -0
- package/src/ui/DiagramTitlePanel.css.ts +17 -6
- package/src/ui/DiagramTitlePanel.tsx +108 -94
- package/src/ui/DynamicViewWalkthrough.tsx +6 -4
- package/src/ui/OptionsPanel.tsx +37 -18
- package/src/ui/options/EdgeOptions.tsx +157 -0
- package/src/ui/options/NodeOptions.tsx +37 -29
- package/src/xyflow/EdgeMarkers.tsx +148 -0
- package/src/xyflow/FitviewOnDiagramChange.tsx +8 -4
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +59 -55
- package/src/xyflow/SyncWithDiagram.tsx +13 -20
- package/src/xyflow/XYFlow.tsx +2 -4
- package/src/xyflow/XYFlowEvents.tsx +48 -33
- package/src/xyflow/XYFlowInner.tsx +6 -3
- package/src/xyflow/const.ts +3 -3
- package/src/xyflow/diagram-to-xyflow.ts +18 -53
- package/src/xyflow/edges/RelationshipEdge.tsx +218 -126
- package/src/xyflow/edges/edges.css.ts +44 -26
- package/src/xyflow/edges/utils.ts +10 -0
- package/src/xyflow/hooks/index.ts +4 -2
- package/src/xyflow/hooks/useLayoutConstraints.ts +5 -5
- package/src/xyflow/hooks/useXYFlow.ts +28 -1
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +41 -31
- package/src/xyflow/nodes/compound/CompoundNode.tsx +52 -53
- package/src/xyflow/nodes/element/ElementIcon.tsx +11 -22
- package/src/xyflow/nodes/element/ElementLink.tsx +11 -16
- package/src/xyflow/nodes/element/ElementNode.tsx +69 -114
- package/src/xyflow/nodes/element/element.css.ts +48 -15
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
- package/src/xyflow/types.ts +2 -4
- package/src/xyflow/utils.ts +3 -6
- package/dist/KeepAspectRatioContainer.d.ts +0 -9
- package/dist/icons/Clipboard.d.ts +0 -3
- package/dist/icons/Link.d.ts +0 -3
- package/dist/icons/ZoomIn.d.ts +0 -3
- package/dist/icons/index.d.ts +0 -3
- package/dist/mantine/theme.d.ts +0 -3
- package/dist/state/WhenInitialized.d.ts +0 -3
- package/dist/ui/Camera.d.ts +0 -3
- package/src/KeepAspectRatioContainer.tsx +0 -36
- package/src/icons/Clipboard.tsx +0 -24
- package/src/icons/Link.tsx +0 -23
- package/src/icons/ZoomIn.tsx +0 -25
- package/src/icons/index.ts +0 -3
- package/src/mantine/theme.ts +0 -20
- package/src/state/WhenInitialized.tsx +0 -24
- package/src/ui/Camera.tsx +0 -174
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { Anchor, Box, Button, CopyButton, Group,
|
|
1
|
+
import { Anchor, Box, Button, Card, CardSection, CopyButton, Group, Spoiler, Stack, Text } from '@mantine/core'
|
|
2
2
|
import { useLocalStorage } from '@mantine/hooks'
|
|
3
|
+
import { IconMenu } from '@tabler/icons-react'
|
|
3
4
|
import clsx from 'clsx'
|
|
4
5
|
import { AnimatePresence, m } from 'framer-motion'
|
|
5
|
-
import { type DiagramState, useDiagramState } from '../state'
|
|
6
|
+
import { type DiagramState, useDiagramState } from '../state/hooks'
|
|
6
7
|
import * as css from './DiagramTitlePanel.css'
|
|
7
8
|
|
|
8
9
|
const selector = (s: DiagramState) => ({
|
|
@@ -35,103 +36,116 @@ export default function DiagramTitlePanel() {
|
|
|
35
36
|
transformOrigin: 'left center'
|
|
36
37
|
}}
|
|
37
38
|
>
|
|
38
|
-
<
|
|
39
|
+
<Card
|
|
39
40
|
radius="sm"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
p={
|
|
41
|
+
className={css.card}
|
|
42
|
+
withBorder
|
|
43
|
+
p={'md'}
|
|
44
|
+
pb={isCollapsed ? 'sm' : 'md'}
|
|
43
45
|
onDoubleClick={e => e.stopPropagation()}>
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
46
|
+
<CardSection mb={isCollapsed ? 10 : 'sm'}>
|
|
47
|
+
<Button
|
|
48
|
+
fullWidth
|
|
49
|
+
size="xs"
|
|
50
|
+
h={'sm'}
|
|
51
|
+
py={2}
|
|
52
|
+
radius={'0'}
|
|
53
|
+
variant={isCollapsed ? 'light' : 'subtle'}
|
|
54
|
+
color="gray"
|
|
55
|
+
onClick={toggle}
|
|
56
|
+
>
|
|
57
|
+
<IconMenu size={11} opacity={0.7} />
|
|
58
|
+
</Button>
|
|
59
|
+
</CardSection>
|
|
60
|
+
<Group justify="stretch" wrap="nowrap" mb={isCollapsed ? 0 : 'sm'}>
|
|
61
|
+
<Text
|
|
62
|
+
component={'div'}
|
|
63
|
+
flex={'1'}
|
|
64
|
+
size={isCollapsed ? 'sm' : 'lg'}
|
|
65
|
+
fw={500}
|
|
66
|
+
lh={1.1}
|
|
67
|
+
className={css.title}
|
|
68
|
+
>
|
|
69
|
+
{title}
|
|
70
|
+
</Text>
|
|
71
|
+
<Text
|
|
72
|
+
hidden={isCollapsed}
|
|
73
|
+
component={'div'}
|
|
74
|
+
flex={'0 0 auto'}
|
|
75
|
+
inline
|
|
76
|
+
size="xs"
|
|
77
|
+
fz={9}
|
|
78
|
+
fw={500}
|
|
79
|
+
c={'dimmed'}
|
|
80
|
+
style={{
|
|
81
|
+
userSelect: 'all'
|
|
82
|
+
}}>
|
|
83
|
+
<span style={{ userSelect: 'none' }}>id:{' '}</span>
|
|
84
|
+
{id}
|
|
85
|
+
</Text>
|
|
86
|
+
</Group>
|
|
87
|
+
{!isCollapsed && (
|
|
88
|
+
<>
|
|
89
|
+
{description && (
|
|
90
|
+
<Spoiler
|
|
91
|
+
maxHeight={42}
|
|
92
|
+
showLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">show more</Button>}
|
|
93
|
+
hideLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">hide</Button>}>
|
|
88
94
|
<Text
|
|
89
95
|
component={'div'}
|
|
90
|
-
size="
|
|
91
|
-
|
|
92
|
-
no description
|
|
96
|
+
size="sm"
|
|
97
|
+
className={css.description}>
|
|
98
|
+
{description || 'no description'}
|
|
93
99
|
</Text>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
100
|
+
</Spoiler>
|
|
101
|
+
)}
|
|
102
|
+
{!description && (
|
|
103
|
+
<Text
|
|
104
|
+
component={'div'}
|
|
105
|
+
size="xs"
|
|
106
|
+
c={'dimmed'}>
|
|
107
|
+
no description
|
|
108
|
+
</Text>
|
|
109
|
+
)}
|
|
110
|
+
{links && (
|
|
111
|
+
<Stack
|
|
112
|
+
gap={3}
|
|
113
|
+
justify="stretch"
|
|
114
|
+
align="stretch">
|
|
115
|
+
{links.map((link) => (
|
|
116
|
+
<Group key={link} wrap="nowrap" align="center" gap={'sm'}>
|
|
117
|
+
<Box flex={'1'} style={{ overflow: 'hidden' }}>
|
|
118
|
+
<Anchor
|
|
119
|
+
href={link}
|
|
120
|
+
target="_blank"
|
|
121
|
+
fz="xs"
|
|
122
|
+
truncate="end"
|
|
123
|
+
display={'inline-block'}
|
|
124
|
+
w={'100%'}>
|
|
125
|
+
{link}
|
|
126
|
+
</Anchor>
|
|
127
|
+
</Box>
|
|
128
|
+
<CopyButton value={link}>
|
|
129
|
+
{({ copied, copy }) => (
|
|
130
|
+
<Button
|
|
131
|
+
size="compact-xs"
|
|
132
|
+
fz={'10'}
|
|
133
|
+
variant="light"
|
|
134
|
+
onClick={copy}
|
|
135
|
+
color={copied
|
|
136
|
+
? 'teal'
|
|
137
|
+
: 'gray'}>
|
|
138
|
+
{copied ? 'copied' : 'copy'}
|
|
139
|
+
</Button>
|
|
140
|
+
)}
|
|
141
|
+
</CopyButton>
|
|
142
|
+
</Group>
|
|
143
|
+
))}
|
|
144
|
+
</Stack>
|
|
145
|
+
)}
|
|
146
|
+
</>
|
|
147
|
+
)}
|
|
148
|
+
</Card>
|
|
135
149
|
</m.div>
|
|
136
150
|
</AnimatePresence>
|
|
137
151
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box, Button } from '@mantine/core'
|
|
1
|
+
import { Box, Button, ButtonGroup } from '@mantine/core'
|
|
2
2
|
import { useHotkeys } from '@mantine/hooks'
|
|
3
3
|
import {
|
|
4
4
|
IconPlayerPlayFilled,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from '@tabler/icons-react'
|
|
9
9
|
import clsx from 'clsx'
|
|
10
10
|
import { isNullish, isNumber } from 'remeda'
|
|
11
|
-
import { useDiagramState } from '../state/
|
|
11
|
+
import { useDiagramState } from '../state/hooks'
|
|
12
12
|
import { useXYStore } from '../xyflow/hooks/useXYFlow'
|
|
13
13
|
import * as css from './DynamicViewWalkthrough.css'
|
|
14
14
|
|
|
@@ -58,6 +58,8 @@ export function DynamicViewWalkthrough() {
|
|
|
58
58
|
return (
|
|
59
59
|
<Box
|
|
60
60
|
className={clsx('react-flow__panel', css.container)}
|
|
61
|
+
onClick={(e) => e.stopPropagation()}
|
|
62
|
+
onDoubleClick={(e) => e.stopPropagation()}
|
|
61
63
|
>
|
|
62
64
|
{isNullish(activeDynamicViewStep)
|
|
63
65
|
&& (
|
|
@@ -71,7 +73,7 @@ export function DynamicViewWalkthrough() {
|
|
|
71
73
|
</Button>
|
|
72
74
|
)}
|
|
73
75
|
{isNumber(activeDynamicViewStep) && (
|
|
74
|
-
<
|
|
76
|
+
<ButtonGroup className={css.buttons}>
|
|
75
77
|
<Button
|
|
76
78
|
{...buttonProps}
|
|
77
79
|
pl={'lg'}
|
|
@@ -98,7 +100,7 @@ export function DynamicViewWalkthrough() {
|
|
|
98
100
|
<IconPlayerSkipForwardFilled />
|
|
99
101
|
</Button>
|
|
100
102
|
)}
|
|
101
|
-
</
|
|
103
|
+
</ButtonGroup>
|
|
102
104
|
)}
|
|
103
105
|
</Box>
|
|
104
106
|
)
|
package/src/ui/OptionsPanel.tsx
CHANGED
|
@@ -3,27 +3,42 @@ import { useOnSelectionChange } from '@xyflow/react'
|
|
|
3
3
|
import clsx from 'clsx'
|
|
4
4
|
import { deepEqual as eq } from 'fast-equals'
|
|
5
5
|
import { AnimatePresence, m } from 'framer-motion'
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { useDiagramState } from '../state'
|
|
6
|
+
import { useState } from 'react'
|
|
7
|
+
import { useDiagramState } from '../state/hooks'
|
|
9
8
|
import { NodeOptions } from './options/NodeOptions'
|
|
10
9
|
import * as styles from './OptionsPanel.css'
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
export default function OptionsPanel() {
|
|
12
|
+
const { isFocused, viewId } = useDiagramState(s => ({
|
|
13
|
+
viewId: s.view.id,
|
|
14
|
+
isFocused: s.focusedNodeId !== null || s.activeDynamicViewStep !== null
|
|
15
|
+
}))
|
|
16
|
+
const [selected, setSelected] = useState<{
|
|
17
|
+
nodes: string[]
|
|
18
|
+
edges: string[]
|
|
19
|
+
}>({
|
|
20
|
+
nodes: [],
|
|
21
|
+
edges: []
|
|
22
|
+
})
|
|
16
23
|
useOnSelectionChange({
|
|
17
|
-
onChange: ({ nodes }) => {
|
|
18
|
-
const
|
|
19
|
-
|
|
24
|
+
onChange: ({ nodes, edges }) => {
|
|
25
|
+
const newSelected = {
|
|
26
|
+
nodes: nodes.map(n => n.id).sort(),
|
|
27
|
+
edges: edges.map(e => e.id).sort()
|
|
28
|
+
}
|
|
29
|
+
if (!eq(selected, newSelected)) {
|
|
30
|
+
setSelected(newSelected)
|
|
31
|
+
}
|
|
20
32
|
}
|
|
21
33
|
})
|
|
22
|
-
|
|
34
|
+
const { nodes } = selected
|
|
35
|
+
// const hasAnySelection = !isFocused && (nodes.length > 0 || edges.length > 0)
|
|
36
|
+
const hasAnySelection = !isFocused && nodes.length > 0
|
|
23
37
|
return (
|
|
24
38
|
<AnimatePresence mode="wait">
|
|
25
|
-
{
|
|
39
|
+
{hasAnySelection && (
|
|
26
40
|
<m.div
|
|
41
|
+
key={viewId}
|
|
27
42
|
initial={{ opacity: 0, scale: 0.85 }}
|
|
28
43
|
animate={{ opacity: 1, scale: 1 }}
|
|
29
44
|
exit={{
|
|
@@ -33,16 +48,20 @@ const OptionsPanelMemo = memo(function OptionsPanel() {
|
|
|
33
48
|
transition={{ duration: 0.14 }}
|
|
34
49
|
className={clsx('react-flow__panel', styles.panel)}
|
|
35
50
|
style={{
|
|
36
|
-
transformOrigin: 'center right'
|
|
51
|
+
transformOrigin: 'center right',
|
|
52
|
+
maxWidth: '320px'
|
|
37
53
|
}}
|
|
38
54
|
>
|
|
39
|
-
<Card shadow="
|
|
40
|
-
<NodeOptions selectedNodeIds={
|
|
55
|
+
<Card shadow="md">
|
|
56
|
+
{nodes.length > 0 && <NodeOptions selectedNodeIds={nodes} />}
|
|
57
|
+
{
|
|
58
|
+
/* nodes.length === 0 && edges.length > 0 && (
|
|
59
|
+
<EdgeOptions selectedEdgeIds={edges} />
|
|
60
|
+
) */
|
|
61
|
+
}
|
|
41
62
|
</Card>
|
|
42
63
|
</m.div>
|
|
43
64
|
)}
|
|
44
65
|
</AnimatePresence>
|
|
45
66
|
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export default OptionsPanelMemo
|
|
67
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { DefaultRelationshipColor, defaultTheme, type NonEmptyArray, type ThemeColor } from '@likec4/core'
|
|
2
|
+
import { CheckIcon, ColorSwatch, Divider, Flex, rem, Stack, Text, Tooltip, TooltipGroup } from '@mantine/core'
|
|
3
|
+
import { shallowEqual } from 'fast-equals'
|
|
4
|
+
import { memo } from 'react'
|
|
5
|
+
import { hasAtLeast, keys, takeWhile } from 'remeda'
|
|
6
|
+
import { useDiagramStoreApi } from '../../state/hooks'
|
|
7
|
+
import { useXYEdgesData } from '../../xyflow/hooks/useXYFlow'
|
|
8
|
+
import { type XYFlowEdge } from '../../xyflow/types'
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
primary,
|
|
12
|
+
secondary,
|
|
13
|
+
muted,
|
|
14
|
+
...otherColors
|
|
15
|
+
} = defaultTheme.elements
|
|
16
|
+
|
|
17
|
+
export const themedColors = [
|
|
18
|
+
{ key: 'primary', value: primary.fill },
|
|
19
|
+
{ key: 'secondary', value: secondary.fill },
|
|
20
|
+
{ key: 'muted', value: muted.fill }
|
|
21
|
+
] satisfies Array<{ key: ThemeColor; value: string }>
|
|
22
|
+
|
|
23
|
+
export const colors = keys(otherColors).map(key => ({
|
|
24
|
+
key,
|
|
25
|
+
value: defaultTheme.elements[key].fill
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
export type ThemeColorKey = typeof themedColors[0]['key']
|
|
29
|
+
export type ColorKey = typeof colors[0]['key']
|
|
30
|
+
|
|
31
|
+
type XYEdgeData = Pick<XYFlowEdge, 'id' | 'data'>
|
|
32
|
+
|
|
33
|
+
export const EdgeOptions = memo<{ selectedEdgeIds: string[] }>(({ selectedEdgeIds }) => {
|
|
34
|
+
const diagramApi = useDiagramStoreApi()
|
|
35
|
+
const edges = useXYEdgesData(selectedEdgeIds)
|
|
36
|
+
if (!hasAtLeast(edges, 1)) {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
if (edges.length !== selectedEdgeIds.length) {
|
|
40
|
+
throw new Error('EdgeOptions: edges and props.edges should have the same length')
|
|
41
|
+
}
|
|
42
|
+
const [firstEdge, ...rest] = edges
|
|
43
|
+
|
|
44
|
+
// // Makes sense to show opacity option only if there is at least one compound node
|
|
45
|
+
// const showOpacityOption = firstNode.type === 'compound'
|
|
46
|
+
// && (rest.length === 0 || rest.every(node => node.type === 'compound'))
|
|
47
|
+
|
|
48
|
+
// const triggerChange = (style: Changes.ChangeElementStyle['style']) => {
|
|
49
|
+
// const targets = edges.map(node => node.data.element.id)
|
|
50
|
+
// invariant(hasAtLeast(targets, 1), 'At least one target is required')
|
|
51
|
+
// diagramApi.getState().triggerChangeElementStyle({
|
|
52
|
+
// op: 'change-element-style',
|
|
53
|
+
// style,
|
|
54
|
+
// targets
|
|
55
|
+
// })
|
|
56
|
+
// }
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Stack gap={'xs'}>
|
|
60
|
+
<div>
|
|
61
|
+
<Text fz={rem(9)} fw={'500'} c={'dimmed'}>
|
|
62
|
+
RELATIONSHIP{rest.length > 0 ? 'S' : ''}
|
|
63
|
+
</Text>
|
|
64
|
+
<Text size="xs" c={rest.length > 0 ? 'dimmed' : ''} truncate>
|
|
65
|
+
{rest.length === 0 ? firstEdge.data.edge.label : `[ multiple ]`}
|
|
66
|
+
</Text>
|
|
67
|
+
</div>
|
|
68
|
+
{
|
|
69
|
+
/* {showShapeOption && (
|
|
70
|
+
<ShapeOption
|
|
71
|
+
nodes={edges}
|
|
72
|
+
onShapeChange={(shape: ElementShape) => {
|
|
73
|
+
triggerChange({ shape })
|
|
74
|
+
}} />
|
|
75
|
+
)} */
|
|
76
|
+
}
|
|
77
|
+
<Colors
|
|
78
|
+
edges={edges}
|
|
79
|
+
onColorChange={(_color: ColorKey | ThemeColorKey) => {
|
|
80
|
+
// triggerChange({ color })
|
|
81
|
+
}} />
|
|
82
|
+
</Stack>
|
|
83
|
+
)
|
|
84
|
+
}, (prevProps, nextProps) => shallowEqual(prevProps.selectedEdgeIds, nextProps.selectedEdgeIds))
|
|
85
|
+
|
|
86
|
+
function Colors({
|
|
87
|
+
edges: [firstEdge, ...edges],
|
|
88
|
+
onColorChange
|
|
89
|
+
}: {
|
|
90
|
+
edges: NonEmptyArray<XYEdgeData>
|
|
91
|
+
onColorChange: (color: ColorKey | ThemeColorKey) => void
|
|
92
|
+
}) {
|
|
93
|
+
let selectedColor = (firstEdge.data.edge.color ?? DefaultRelationshipColor) as ThemeColor | null
|
|
94
|
+
takeWhile(edges, edge => {
|
|
95
|
+
const color = edge.data.edge.color ?? DefaultRelationshipColor
|
|
96
|
+
if (color !== selectedColor) {
|
|
97
|
+
selectedColor = null
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
return true
|
|
101
|
+
})
|
|
102
|
+
const changeColor = (color: ColorKey | ThemeColorKey) => () => {
|
|
103
|
+
if (selectedColor === color) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
onColorChange(color)
|
|
107
|
+
}
|
|
108
|
+
return (
|
|
109
|
+
<div>
|
|
110
|
+
<TooltipGroup openDelay={400} closeDelay={300}>
|
|
111
|
+
<Divider label="color" labelPosition="left" />
|
|
112
|
+
<Flex mt={'xs'} maw={150} gap="xs" justify="flex-start" align="flex-start" direction="row" wrap="wrap">
|
|
113
|
+
{themedColors.map(({ key, value }) => (
|
|
114
|
+
<Tooltip
|
|
115
|
+
key={key}
|
|
116
|
+
label={key}
|
|
117
|
+
fz={'xs'}
|
|
118
|
+
color="dark"
|
|
119
|
+
offset={2}
|
|
120
|
+
transitionProps={{ duration: 140, transition: 'slide-up' }}>
|
|
121
|
+
<ColorSwatch
|
|
122
|
+
color={value}
|
|
123
|
+
size={20}
|
|
124
|
+
withShadow
|
|
125
|
+
onClick={changeColor(key)}
|
|
126
|
+
style={{ color: '#fff', cursor: 'pointer' }}
|
|
127
|
+
>
|
|
128
|
+
{selectedColor === key && <CheckIcon style={{ width: rem(12), height: rem(12) }} />}
|
|
129
|
+
</ColorSwatch>
|
|
130
|
+
</Tooltip>
|
|
131
|
+
))}
|
|
132
|
+
</Flex>
|
|
133
|
+
|
|
134
|
+
<Flex mt="sm" maw={150} gap="xs" justify="flex-start" align="flex-start" direction="row" wrap="wrap">
|
|
135
|
+
{colors.map(({ key, value }) => (
|
|
136
|
+
<Tooltip
|
|
137
|
+
key={key}
|
|
138
|
+
label={key}
|
|
139
|
+
fz={'xs'}
|
|
140
|
+
color="dark"
|
|
141
|
+
offset={2}
|
|
142
|
+
transitionProps={{ duration: 140, transition: 'slide-up' }}>
|
|
143
|
+
<ColorSwatch
|
|
144
|
+
color={value}
|
|
145
|
+
size={20}
|
|
146
|
+
onClick={changeColor(key)}
|
|
147
|
+
style={{ color: '#fff', cursor: 'pointer' }}
|
|
148
|
+
>
|
|
149
|
+
{selectedColor === key && <CheckIcon style={{ width: rem(12), height: rem(12) }} />}
|
|
150
|
+
</ColorSwatch>
|
|
151
|
+
</Tooltip>
|
|
152
|
+
))}
|
|
153
|
+
</Flex>
|
|
154
|
+
</TooltipGroup>
|
|
155
|
+
</div>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
@@ -17,16 +17,18 @@ import {
|
|
|
17
17
|
SegmentedControl,
|
|
18
18
|
Select,
|
|
19
19
|
Slider,
|
|
20
|
+
Space,
|
|
20
21
|
Stack,
|
|
21
22
|
Text,
|
|
22
23
|
Tooltip,
|
|
23
24
|
TooltipGroup
|
|
24
25
|
} from '@mantine/core'
|
|
25
|
-
import {
|
|
26
|
+
import { shallowEqual } from 'fast-equals'
|
|
27
|
+
import { memo, useState } from 'react'
|
|
26
28
|
import { hasAtLeast, keys, takeWhile } from 'remeda'
|
|
27
29
|
import { useUpdateEffect } from '../../hooks/useUpdateEffect'
|
|
28
30
|
import type { Changes } from '../../LikeC4Diagram.props'
|
|
29
|
-
import { useDiagramStoreApi } from '../../state'
|
|
31
|
+
import { useDiagramStoreApi } from '../../state/hooks'
|
|
30
32
|
import { useXYNodesData } from '../../xyflow/hooks'
|
|
31
33
|
import { XYFlowNode } from '../../xyflow/types'
|
|
32
34
|
|
|
@@ -43,7 +45,7 @@ export const themedColors = [
|
|
|
43
45
|
{ key: 'muted', value: muted.fill }
|
|
44
46
|
] satisfies Array<{ key: ThemeColor; value: string }>
|
|
45
47
|
|
|
46
|
-
export const colors = keys
|
|
48
|
+
export const colors = keys(otherColors).map(key => ({
|
|
47
49
|
key,
|
|
48
50
|
value: defaultTheme.elements[key].fill
|
|
49
51
|
}))
|
|
@@ -53,7 +55,7 @@ export type ColorKey = typeof colors[0]['key']
|
|
|
53
55
|
|
|
54
56
|
type XYNodesData = Pick<XYFlowNode, 'id' | 'data' | 'type'>
|
|
55
57
|
|
|
56
|
-
export
|
|
58
|
+
export const NodeOptions = memo<{ selectedNodeIds: string[] }>(({ selectedNodeIds }) => {
|
|
57
59
|
// export function NodeOptions(props: { nodes: XYFlowNode[] }) {
|
|
58
60
|
const diagramApi = useDiagramStoreApi()
|
|
59
61
|
// const nodes = useXYNodesData(props.nodes.map(node => node.id))
|
|
@@ -70,7 +72,8 @@ export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] })
|
|
|
70
72
|
const [firstNode, ...rest] = nodes
|
|
71
73
|
|
|
72
74
|
// Makes sense to show opacity option only if there is at least one compound node
|
|
73
|
-
const showOpacityOption =
|
|
75
|
+
const showOpacityOption = firstNode.type === 'compound'
|
|
76
|
+
&& (rest.length === 0 || rest.every(node => node.type === 'compound'))
|
|
74
77
|
|
|
75
78
|
const triggerChange = (style: Changes.ChangeElementStyle['style']) => {
|
|
76
79
|
const targets = nodes.map(node => node.data.element.id)
|
|
@@ -83,8 +86,11 @@ export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] })
|
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
return (
|
|
86
|
-
<Stack>
|
|
89
|
+
<Stack gap={'xs'}>
|
|
87
90
|
<div>
|
|
91
|
+
<Text fz={rem(9)} fw={'500'} c={'dimmed'}>
|
|
92
|
+
ELEMENT{rest.length > 0 ? 'S' : ``}
|
|
93
|
+
</Text>
|
|
88
94
|
<Text size="xs" c={rest.length > 0 ? 'dimmed' : ''} truncate>
|
|
89
95
|
{rest.length === 0 ? firstNode.data.element.title : `[ multiple ]`}
|
|
90
96
|
</Text>
|
|
@@ -105,11 +111,13 @@ export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] })
|
|
|
105
111
|
{showOpacityOption && (
|
|
106
112
|
<div key={firstNode.id}>
|
|
107
113
|
<Divider label="opacity and border" labelPosition="left" />
|
|
114
|
+
<Space h={'xs'} />
|
|
108
115
|
<OpacityOption
|
|
109
116
|
node={firstNode}
|
|
110
117
|
onOpacityChange={(opacity: number) => {
|
|
111
118
|
triggerChange({ opacity })
|
|
112
119
|
}} />
|
|
120
|
+
<Space h={'sm'} />
|
|
113
121
|
<BorderStyleOption
|
|
114
122
|
node={firstNode}
|
|
115
123
|
onChange={(border: BorderStyle) => {
|
|
@@ -121,7 +129,7 @@ export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] })
|
|
|
121
129
|
<NavigateToOption nodes={nodes} />
|
|
122
130
|
</Stack>
|
|
123
131
|
)
|
|
124
|
-
}
|
|
132
|
+
}, (prevProps, nextProps) => shallowEqual(prevProps.selectedNodeIds, nextProps.selectedNodeIds))
|
|
125
133
|
|
|
126
134
|
function Colors({
|
|
127
135
|
nodes: [firstNode, ...nodes],
|
|
@@ -242,26 +250,27 @@ function NavigateToOption({
|
|
|
242
250
|
return (
|
|
243
251
|
<div>
|
|
244
252
|
<Divider label="navigate to" labelPosition="left" />
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
253
|
+
{isMultiple && (
|
|
254
|
+
<Text size="xs" c={'dimmed'}>
|
|
255
|
+
{'[ multiple ]'}
|
|
256
|
+
</Text>
|
|
257
|
+
)}
|
|
258
|
+
{!isMultiple && (
|
|
259
|
+
<>
|
|
260
|
+
<Space h={'xs'} />
|
|
261
|
+
<Select
|
|
262
|
+
size="xs"
|
|
263
|
+
variant="filled"
|
|
264
|
+
value={node.data.element.navigateTo ?? null}
|
|
265
|
+
placeholder={'select'}
|
|
266
|
+
data={node.data.element.navigateTo
|
|
267
|
+
? [{ value: node.data.element.navigateTo, label: node.data.element.navigateTo }]
|
|
268
|
+
: []}
|
|
269
|
+
allowDeselect={true}
|
|
270
|
+
checkIconPosition="right"
|
|
271
|
+
/>
|
|
272
|
+
</>
|
|
273
|
+
)}
|
|
265
274
|
</div>
|
|
266
275
|
)
|
|
267
276
|
}
|
|
@@ -281,7 +290,6 @@ function OpacityOption({
|
|
|
281
290
|
|
|
282
291
|
return (
|
|
283
292
|
<Slider
|
|
284
|
-
mt={'xs'}
|
|
285
293
|
size={'sm'}
|
|
286
294
|
color={'dark'}
|
|
287
295
|
value={value}
|
|
@@ -304,7 +312,7 @@ function BorderStyleOption({
|
|
|
304
312
|
}, [selecteBorderStyle])
|
|
305
313
|
|
|
306
314
|
return (
|
|
307
|
-
<Box
|
|
315
|
+
<Box>
|
|
308
316
|
<SegmentedControl
|
|
309
317
|
size="xs"
|
|
310
318
|
fullWidth
|