@likec4/diagram 1.9.0 → 1.12.1
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 +20 -43
- package/src/LikeC4CustomColors.tsx +5 -2
- package/src/LikeC4Diagram.css.ts +1 -1
- package/src/LikeC4Diagram.props.ts +40 -94
- package/src/LikeC4Diagram.tsx +14 -4
- package/src/LikeC4ModelProvider.tsx +21 -0
- package/src/StaticLikeC4Diagram.tsx +1 -0
- package/src/globals.css.ts +2 -3
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useMantinePortalProps.ts +8 -0
- package/src/hooks/useXYFlow.ts +1 -2
- package/src/index.ts +9 -0
- package/src/likec4model/LikeC4ModelContext.ts +4 -0
- package/src/likec4model/index.ts +1 -0
- package/src/likec4model/useLikeC4Model.ts +58 -0
- package/src/state/diagram-to-xyflow.ts +1 -2
- package/src/state/diagramStore.ts +185 -66
- package/src/{theme.css.ts → theme-vars.ts} +11 -4
- package/src/ui/DiagramTitlePanel.css.ts +2 -3
- package/src/ui/DiagramTitlePanel.tsx +25 -3
- package/src/ui/DynamicViewWalkthrough.css.ts +28 -2
- package/src/ui/DynamicViewWalkthrough.tsx +97 -52
- package/src/ui/OptionsPanel.css.ts +1 -2
- package/src/ui/OptionsPanel.tsx +5 -2
- package/src/ui/notation/NotationPanel.css.ts +2 -3
- package/src/ui/notation/NotationPanel.tsx +10 -11
- package/src/ui/options/NodeOptions.tsx +4 -1
- package/src/ui/top-left/ChangeAutoLayoutButton.tsx +122 -71
- package/src/ui/top-left/TopLeftPanel.tsx +128 -117
- package/src/ui/top-left/_shared.ts +7 -6
- package/src/ui/top-left/styles.css.ts +48 -8
- package/src/utils/vector.ts +57 -0
- package/src/xyflow/FitviewOnDiagramChange.tsx +5 -4
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +7 -5
- package/src/xyflow/XYFlow.tsx +13 -17
- package/src/xyflow/XYFlowEvents.tsx +58 -58
- package/src/xyflow/XYFlowInner.tsx +4 -8
- package/src/xyflow/edges/EdgeLabel.tsx +185 -0
- package/src/xyflow/edges/RelationshipEdge.tsx +170 -137
- package/src/xyflow/edges/RelationshipsDropdownMenu.css.ts +60 -0
- package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +214 -0
- package/src/xyflow/edges/edges.css.ts +75 -7
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +1 -2
- package/src/xyflow/nodes/compound/CompoundNode.tsx +25 -8
- package/src/xyflow/nodes/element/ElementLink.css.ts +1 -1
- package/src/xyflow/nodes/element/ElementLink.tsx +4 -8
- package/src/xyflow/nodes/element/ElementNode.tsx +47 -8
- package/src/xyflow/nodes/element/element.css.ts +1 -2
- package/src/xyflow/nodes/shared/Toolbar.tsx +426 -0
- package/src/xyflow/types.ts +1 -24
- package/src/xyflow/useLayoutConstraints.ts +10 -6
- package/dist/LikeC4CustomColors.d.ts +0 -3
- package/dist/LikeC4CustomColors.props.d.ts +0 -5
- package/dist/LikeC4Diagram.css.d.ts +0 -4
- package/dist/LikeC4Diagram.d.ts +0 -7
- package/dist/LikeC4Diagram.props.d.ts +0 -154
- package/dist/StaticLikeC4Diagram.d.ts +0 -4
- package/dist/globals.css.d.ts +0 -1
- package/dist/hooks/index.d.ts +0 -4
- package/dist/hooks/useDiagramState.d.ts +0 -6
- package/dist/hooks/useSetState.d.ts +0 -3
- package/dist/hooks/useUpdateEffect.d.ts +0 -5
- package/dist/hooks/useXYFlow.d.ts +0 -267
- package/dist/index.d.ts +0 -9
- package/dist/index.mjs +0 -3684
- package/dist/mantine/EnsureMantine.d.ts +0 -8
- package/dist/mantine.css.d.ts +0 -3
- package/dist/state/DiagramContext.d.ts +0 -1192
- package/dist/state/diagram-to-xyflow.d.ts +0 -12
- package/dist/state/diagramStore.d.ts +0 -1268
- package/dist/style.css +0 -2139
- package/dist/theme.css.d.ts +0 -63
- package/dist/ui/DiagramTitlePanel.css.d.ts +0 -4
- package/dist/ui/DiagramTitlePanel.d.ts +0 -1
- package/dist/ui/DynamicViewWalkthrough.css.d.ts +0 -3
- package/dist/ui/DynamicViewWalkthrough.d.ts +0 -1
- package/dist/ui/OptionsPanel.css.d.ts +0 -1
- package/dist/ui/OptionsPanel.d.ts +0 -1
- package/dist/ui/notation/NotationPanel.css.d.ts +0 -6
- package/dist/ui/notation/NotationPanel.d.ts +0 -1
- package/dist/ui/notation/index.d.ts +0 -1
- package/dist/ui/options/EdgeOptions.d.ts +0 -19
- package/dist/ui/options/NodeOptions.d.ts +0 -19
- package/dist/ui/top-left/ChangeAutoLayoutButton.d.ts +0 -3
- package/dist/ui/top-left/TopLeftPanel.d.ts +0 -4
- package/dist/ui/top-left/_shared.d.ts +0 -5
- package/dist/ui/top-left/styles.css.d.ts +0 -3
- package/dist/xyflow/EdgeMarkers.d.ts +0 -12
- package/dist/xyflow/FitviewOnDiagramChange.d.ts +0 -4
- package/dist/xyflow/SelectEdgesOnNodeFocus.d.ts +0 -1
- package/dist/xyflow/XYFlow.d.ts +0 -12
- package/dist/xyflow/XYFlowBackground.d.ts +0 -11
- package/dist/xyflow/XYFlowEvents.d.ts +0 -161
- package/dist/xyflow/XYFlowInner.d.ts +0 -10
- package/dist/xyflow/const.d.ts +0 -7
- package/dist/xyflow/edges/RelationshipEdge.d.ts +0 -32
- package/dist/xyflow/edges/edges.css.d.ts +0 -12
- package/dist/xyflow/edges/utils.d.ts +0 -17
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +0 -11
- package/dist/xyflow/nodes/compound/CompoundNode.d.ts +0 -6
- package/dist/xyflow/nodes/compound/index.d.ts +0 -1
- package/dist/xyflow/nodes/element/ElementLink.css.d.ts +0 -3
- package/dist/xyflow/nodes/element/ElementLink.d.ts +0 -7
- package/dist/xyflow/nodes/element/ElementNode.d.ts +0 -6
- package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +0 -18
- package/dist/xyflow/nodes/element/element.css.d.ts +0 -19
- package/dist/xyflow/nodes/element/index.d.ts +0 -1
- package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +0 -5
- package/dist/xyflow/types.d.ts +0 -50
- package/dist/xyflow/useLayoutConstraints.d.ts +0 -9
- package/dist/xyflow/utils.d.ts +0 -18
- package/src/mantine.css.ts +0 -7
- package/src/reset.d.ts +0 -2
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { type DiagramNode, LikeC4Model, nameFromFqn } from '@likec4/core'
|
|
2
|
+
import {
|
|
3
|
+
ActionIcon,
|
|
4
|
+
Box,
|
|
5
|
+
Group,
|
|
6
|
+
Menu,
|
|
7
|
+
MenuDivider,
|
|
8
|
+
MenuDropdown,
|
|
9
|
+
MenuItem,
|
|
10
|
+
MenuLabel,
|
|
11
|
+
MenuTarget,
|
|
12
|
+
Space,
|
|
13
|
+
Stack,
|
|
14
|
+
type StackProps,
|
|
15
|
+
Text,
|
|
16
|
+
Tooltip as MantineTooltip,
|
|
17
|
+
TooltipGroup
|
|
18
|
+
} from '@mantine/core'
|
|
19
|
+
import { IconArrowRight, IconFileSymlink, IconZoomScan } from '@tabler/icons-react'
|
|
20
|
+
import clsx from 'clsx'
|
|
21
|
+
import { forwardRef, Fragment, type MouseEventHandler, type PropsWithChildren } from 'react'
|
|
22
|
+
import { filter, isTruthy, map, partition, pipe } from 'remeda'
|
|
23
|
+
import { useDiagramState, useDiagramStoreApi, useMantinePortalProps, useXYNodesData } from '../../hooks'
|
|
24
|
+
import type { RelationshipData } from '../types'
|
|
25
|
+
import * as css from './RelationshipsDropdownMenu.css'
|
|
26
|
+
|
|
27
|
+
const stopPropagation: MouseEventHandler = (e) => e.stopPropagation()
|
|
28
|
+
|
|
29
|
+
export const Tooltip = MantineTooltip.withProps({
|
|
30
|
+
color: 'gray',
|
|
31
|
+
fz: 'xs',
|
|
32
|
+
openDelay: 300,
|
|
33
|
+
closeDelay: 150,
|
|
34
|
+
label: '',
|
|
35
|
+
children: null,
|
|
36
|
+
offset: 8,
|
|
37
|
+
withinPortal: false
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export function RelationshipsDropdownMenu({
|
|
41
|
+
edge,
|
|
42
|
+
disabled = false,
|
|
43
|
+
likec4model,
|
|
44
|
+
children
|
|
45
|
+
}: PropsWithChildren<{
|
|
46
|
+
edge: RelationshipData['edge']
|
|
47
|
+
disabled?: boolean | undefined
|
|
48
|
+
likec4model: LikeC4Model
|
|
49
|
+
}>) {
|
|
50
|
+
const portalProps = useMantinePortalProps()
|
|
51
|
+
const [sourceXYNode, targetXYNode] = useXYNodesData([edge.source, edge.target])
|
|
52
|
+
|
|
53
|
+
if (!sourceXYNode || !targetXYNode) return children
|
|
54
|
+
|
|
55
|
+
const [direct, nested] = pipe(
|
|
56
|
+
edge.relations,
|
|
57
|
+
map(id => {
|
|
58
|
+
try {
|
|
59
|
+
return likec4model.relationship(id)
|
|
60
|
+
} catch (e) {
|
|
61
|
+
// View was cached, but likec4model based on new data
|
|
62
|
+
console.error(
|
|
63
|
+
`View is cached and likec4model missing relationship ${id} from ${edge.source} -> ${edge.target}`,
|
|
64
|
+
e
|
|
65
|
+
)
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}),
|
|
69
|
+
filter(isTruthy),
|
|
70
|
+
partition(r => r.relationship.source === edge.source && r.relationship.target === edge.target)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if (direct.length + nested.length === 0) {
|
|
74
|
+
return <>{children}</>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const renderRelationship = (relationship: LikeC4Model.ViewModel.Relationship, index: number) => (
|
|
78
|
+
<Fragment key={relationship.id}>
|
|
79
|
+
{index > 0 && <MenuDivider opacity={0.65} />}
|
|
80
|
+
<MenuItem
|
|
81
|
+
component={Relationship}
|
|
82
|
+
relationship={relationship}
|
|
83
|
+
sourceNode={sourceXYNode.data.element}
|
|
84
|
+
targetNode={targetXYNode.data.element}
|
|
85
|
+
edge={edge} />
|
|
86
|
+
</Fragment>
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Menu
|
|
91
|
+
trigger={'click-hover'}
|
|
92
|
+
openDelay={300}
|
|
93
|
+
closeDelay={450}
|
|
94
|
+
floatingStrategy={'fixed'}
|
|
95
|
+
closeOnClickOutside
|
|
96
|
+
clickOutsideEvents={['pointerdown', 'mousedown', 'click']}
|
|
97
|
+
closeOnEscape
|
|
98
|
+
closeOnItemClick={false}
|
|
99
|
+
disabled={disabled}
|
|
100
|
+
{...portalProps}
|
|
101
|
+
>
|
|
102
|
+
<MenuTarget>
|
|
103
|
+
{children}
|
|
104
|
+
</MenuTarget>
|
|
105
|
+
<MenuDropdown
|
|
106
|
+
className={css.menuDropdown}
|
|
107
|
+
onPointerDownCapture={stopPropagation}
|
|
108
|
+
onPointerDown={stopPropagation}
|
|
109
|
+
onClick={stopPropagation}
|
|
110
|
+
onDoubleClick={stopPropagation}
|
|
111
|
+
>
|
|
112
|
+
{direct.length > 0 && (
|
|
113
|
+
<>
|
|
114
|
+
<MenuLabel>direct relationships</MenuLabel>
|
|
115
|
+
{direct.map(renderRelationship)}
|
|
116
|
+
</>
|
|
117
|
+
)}
|
|
118
|
+
{nested.length > 0 && (
|
|
119
|
+
<>
|
|
120
|
+
{direct.length > 0 && <MenuDivider />}
|
|
121
|
+
<MenuLabel>resolved from nested</MenuLabel>
|
|
122
|
+
{nested.map(renderRelationship)}
|
|
123
|
+
</>
|
|
124
|
+
)}
|
|
125
|
+
</MenuDropdown>
|
|
126
|
+
</Menu>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const Relationship = forwardRef<
|
|
131
|
+
HTMLDivElement,
|
|
132
|
+
StackProps & {
|
|
133
|
+
relationship: LikeC4Model.ViewModel.Relationship
|
|
134
|
+
edge: RelationshipData['edge']
|
|
135
|
+
sourceNode: DiagramNode
|
|
136
|
+
targetNode: DiagramNode
|
|
137
|
+
}
|
|
138
|
+
>(({
|
|
139
|
+
className,
|
|
140
|
+
relationship: r,
|
|
141
|
+
edge,
|
|
142
|
+
sourceNode,
|
|
143
|
+
targetNode,
|
|
144
|
+
...props
|
|
145
|
+
}, ref) => {
|
|
146
|
+
const diagramApi = useDiagramStoreApi()
|
|
147
|
+
const {
|
|
148
|
+
viewId,
|
|
149
|
+
hasOnOpenSourceRelation,
|
|
150
|
+
hasOnNavigateTo
|
|
151
|
+
} = useDiagramState(s => ({
|
|
152
|
+
viewId: s.view.id,
|
|
153
|
+
hasOnOpenSourceRelation: !!s.onOpenSourceRelation,
|
|
154
|
+
hasOnNavigateTo: !!s.onNavigateTo
|
|
155
|
+
}))
|
|
156
|
+
const sourceId = nameFromFqn(edge.source) + r.source.id.slice(edge.source.length)
|
|
157
|
+
const targetId = nameFromFqn(edge.target) + r.target.id.slice(edge.target.length)
|
|
158
|
+
const navigateTo = hasOnNavigateTo && r.relationship.navigateTo !== viewId ? r.relationship.navigateTo : undefined
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<Stack ref={ref} className={clsx(css.menuItemRelationship, className)} {...props}>
|
|
162
|
+
<Group gap={4}>
|
|
163
|
+
<Text component="div" className={css.endpoint} data-likec4-color={sourceNode.color}>
|
|
164
|
+
{sourceId}
|
|
165
|
+
</Text>
|
|
166
|
+
<IconArrowRight stroke={2.5} size={11} />
|
|
167
|
+
<Text component="div" className={css.endpoint} data-likec4-color={targetNode.color}>
|
|
168
|
+
{targetId}
|
|
169
|
+
</Text>
|
|
170
|
+
{(navigateTo || hasOnOpenSourceRelation) && (
|
|
171
|
+
<TooltipGroup openDelay={100}>
|
|
172
|
+
<Space w={'xs'} />
|
|
173
|
+
{navigateTo && (
|
|
174
|
+
<Tooltip label={'Open dynamic view'}>
|
|
175
|
+
<ActionIcon
|
|
176
|
+
className={clsx('nodrag nopan')}
|
|
177
|
+
size={'sm'}
|
|
178
|
+
radius="sm"
|
|
179
|
+
variant="default"
|
|
180
|
+
onPointerDownCapture={stopPropagation}
|
|
181
|
+
onClick={event => {
|
|
182
|
+
diagramApi.getState().onNavigateTo?.(navigateTo, event)
|
|
183
|
+
}}
|
|
184
|
+
role="button"
|
|
185
|
+
>
|
|
186
|
+
<IconZoomScan size="80%" stroke={2} />
|
|
187
|
+
</ActionIcon>
|
|
188
|
+
</Tooltip>
|
|
189
|
+
)}
|
|
190
|
+
{hasOnOpenSourceRelation && (
|
|
191
|
+
<Tooltip label={'Open source'}>
|
|
192
|
+
<ActionIcon
|
|
193
|
+
className={clsx('nodrag nopan')}
|
|
194
|
+
size={'sm'}
|
|
195
|
+
radius="sm"
|
|
196
|
+
variant="default"
|
|
197
|
+
onPointerDownCapture={stopPropagation}
|
|
198
|
+
onClick={event => {
|
|
199
|
+
event.stopPropagation()
|
|
200
|
+
diagramApi.getState().onOpenSourceRelation?.(r.id)
|
|
201
|
+
}}
|
|
202
|
+
role="button"
|
|
203
|
+
>
|
|
204
|
+
<IconFileSymlink size="80%" stroke={2} />
|
|
205
|
+
</ActionIcon>
|
|
206
|
+
</Tooltip>
|
|
207
|
+
)}
|
|
208
|
+
</TooltipGroup>
|
|
209
|
+
)}
|
|
210
|
+
</Group>
|
|
211
|
+
<Box className={css.title}>{r.title || 'untitled'}</Box>
|
|
212
|
+
</Stack>
|
|
213
|
+
)
|
|
214
|
+
})
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { rem } from '@mantine/core'
|
|
2
2
|
import { createVar, fallbackVar, globalStyle, keyframes, style } from '@vanilla-extract/css'
|
|
3
|
-
import { mantine } from '../../
|
|
4
|
-
import { vars, xyvars } from '../../theme.css'
|
|
3
|
+
import { mantine, vars, xyvars } from '../../theme-vars'
|
|
5
4
|
|
|
6
5
|
const mixColor = createVar('mix-color')
|
|
7
6
|
|
|
@@ -145,7 +144,7 @@ export const cssEdgePath = style({
|
|
|
145
144
|
animationDelay: '450ms',
|
|
146
145
|
transition: 'stroke 130ms ease-out,stroke-width 130ms ease-out'
|
|
147
146
|
},
|
|
148
|
-
[`:where(${isSelected}, [data-edge-active='true']) &`]: {
|
|
147
|
+
[`:where(${isSelected}, [data-edge-active='true'], [data-edge-animated='true']) &`]: {
|
|
149
148
|
animationName: strokeKeyframes,
|
|
150
149
|
animationDelay: '0ms',
|
|
151
150
|
transition: 'stroke 130ms ease-out,stroke-width 130ms ease-out'
|
|
@@ -171,10 +170,15 @@ export const stepEdgeNumber = style({
|
|
|
171
170
|
borderTopLeftRadius: labelBorderRadius,
|
|
172
171
|
borderBottomLeftRadius: labelBorderRadius,
|
|
173
172
|
backgroundColor: `color-mix(in srgb, ${vars.relation.labelBgColor}, ${mixColor} 5%)`,
|
|
174
|
-
fontVariantNumeric: 'tabular-nums'
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
173
|
+
fontVariantNumeric: 'tabular-nums',
|
|
174
|
+
selectors: {
|
|
175
|
+
[`:where([data-mantine-color-scheme="dark"]) :where([data-likec4-color="gray"]) &`]: {
|
|
176
|
+
backgroundColor: `color-mix(in srgb, ${vars.relation.labelBgColor}, ${mixColor} 15%)`
|
|
177
|
+
},
|
|
178
|
+
[`:where([data-edge-active='true']) &`]: {
|
|
179
|
+
backgroundColor: 'transparent'
|
|
180
|
+
}
|
|
181
|
+
}
|
|
178
182
|
})
|
|
179
183
|
|
|
180
184
|
export const varLabelX = createVar('label-x')
|
|
@@ -230,3 +234,67 @@ export const edgeLabelText = style({
|
|
|
230
234
|
globalStyle(`${edgeLabel}:has(${stepEdgeNumber}) ${edgeLabelText}`, {
|
|
231
235
|
padding: '2px 5px 4px 2px'
|
|
232
236
|
})
|
|
237
|
+
|
|
238
|
+
export const edgeNoteCloseButton = style({
|
|
239
|
+
position: 'absolute',
|
|
240
|
+
top: 4,
|
|
241
|
+
right: 4,
|
|
242
|
+
zIndex: 9
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
export const edgeNoteText = style({
|
|
246
|
+
userSelect: 'all',
|
|
247
|
+
textAlign: 'left',
|
|
248
|
+
whiteSpaceCollapse: 'preserve-breaks',
|
|
249
|
+
textWrap: 'pretty',
|
|
250
|
+
lineHeight: 1.25,
|
|
251
|
+
vars: {
|
|
252
|
+
'--text-fz': mantine.fontSizes.sm
|
|
253
|
+
},
|
|
254
|
+
'@media': {
|
|
255
|
+
[mantine.largerThan('md')]: {
|
|
256
|
+
vars: {
|
|
257
|
+
'--text-fz': mantine.fontSizes.md
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
export const cssNavigateBtn = style({
|
|
264
|
+
zIndex: 'calc(var(--layer-overlays, 1) + 1)',
|
|
265
|
+
position: 'absolute',
|
|
266
|
+
pointerEvents: 'all',
|
|
267
|
+
left: '50%',
|
|
268
|
+
top: 'calc(100% + 1px)',
|
|
269
|
+
color: xyvars.edge.labelColor,
|
|
270
|
+
cursor: 'pointer',
|
|
271
|
+
transformOrigin: 'top center',
|
|
272
|
+
opacity: 0.8,
|
|
273
|
+
transition: 'all 150ms ease-in-out',
|
|
274
|
+
transform: 'translate(-50%, 0)',
|
|
275
|
+
transitionDelay: '0ms',
|
|
276
|
+
backgroundColor: 'var(--ai-bg)',
|
|
277
|
+
vars: {
|
|
278
|
+
'--ai-bg': vars.relation.labelBgColor,
|
|
279
|
+
'--ai-hover': `color-mix(in srgb , ${vars.relation.labelBgColor}, ${mixColor} 10%)`
|
|
280
|
+
},
|
|
281
|
+
selectors: {
|
|
282
|
+
[`:is(${dimmed}) &`]: {
|
|
283
|
+
display: 'none'
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
':hover': {
|
|
287
|
+
boxShadow: mantine.shadows.lg,
|
|
288
|
+
transform: 'translate(-50%, 0) scale(1.3)',
|
|
289
|
+
opacity: 1
|
|
290
|
+
},
|
|
291
|
+
':active': {
|
|
292
|
+
transform: 'translate(-50%, 0) scale(1.02)'
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
globalStyle(`${cssNavigateBtn} .tabler-icon`, {
|
|
296
|
+
width: '75%',
|
|
297
|
+
height: '75%',
|
|
298
|
+
strokeWidth: 2
|
|
299
|
+
})
|
|
300
|
+
export const cssNavigateBtnIcon = style({})
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { rem } from '@mantine/core'
|
|
2
2
|
import { createVar, fallbackVar, globalStyle, keyframes, style } from '@vanilla-extract/css'
|
|
3
3
|
import { calc } from '@vanilla-extract/css-utils'
|
|
4
|
-
import { mantine } from '../../../
|
|
5
|
-
import { vars } from '../../../theme.css'
|
|
4
|
+
import { mantine, vars } from '../../../theme-vars'
|
|
6
5
|
|
|
7
6
|
export const container = style({
|
|
8
7
|
width: '100%',
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import type { ThemeColor } from '@likec4/core'
|
|
1
2
|
import { Box, Text } from '@mantine/core'
|
|
3
|
+
import { useDebouncedValue } from '@mantine/hooks'
|
|
2
4
|
import { assignInlineVars } from '@vanilla-extract/dynamic'
|
|
3
5
|
import { Handle, type NodeProps, Position } from '@xyflow/react'
|
|
4
6
|
import clsx from 'clsx'
|
|
5
7
|
import { deepEqual as eq } from 'fast-equals'
|
|
6
|
-
import { memo } from 'react'
|
|
8
|
+
import { memo, useState } from 'react'
|
|
7
9
|
import { clamp } from 'remeda'
|
|
8
10
|
import { useDiagramState } from '../../../hooks/useDiagramState'
|
|
9
11
|
import type { CompoundXYFlowNode } from '../../types'
|
|
10
12
|
import { NavigateToBtn } from '../shared/NavigateToBtn'
|
|
13
|
+
import { CompoundToolbar } from '../shared/Toolbar'
|
|
11
14
|
import * as css from './CompoundNode.css'
|
|
12
15
|
|
|
13
16
|
type CompoundNodeProps = Pick<
|
|
@@ -17,7 +20,7 @@ type CompoundNodeProps = Pick<
|
|
|
17
20
|
|
|
18
21
|
const isEqualProps = (prev: CompoundNodeProps, next: CompoundNodeProps) => (
|
|
19
22
|
prev.id === next.id
|
|
20
|
-
|
|
23
|
+
&& eq(prev.selected ?? false, next.selected ?? false)
|
|
21
24
|
// && eq(prev.width, next.width)
|
|
22
25
|
// && eq(prev.height, next.height)
|
|
23
26
|
&& eq(prev.data.element, next.data.element)
|
|
@@ -25,6 +28,7 @@ const isEqualProps = (prev: CompoundNodeProps, next: CompoundNodeProps) => (
|
|
|
25
28
|
|
|
26
29
|
export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function CompoundNode({
|
|
27
30
|
id,
|
|
31
|
+
selected = false,
|
|
28
32
|
data: {
|
|
29
33
|
element
|
|
30
34
|
}
|
|
@@ -41,15 +45,28 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
|
|
|
41
45
|
max: 50
|
|
42
46
|
})
|
|
43
47
|
|
|
44
|
-
const { isHovered, isDimmed, hasOnNavigateTo } = useDiagramState(s => ({
|
|
48
|
+
const { isEditable, isHovered, isDimmed, hasOnNavigateTo } = useDiagramState(s => ({
|
|
49
|
+
isEditable: s.readonly !== true,
|
|
45
50
|
isHovered: s.hoveredNodeId === id,
|
|
46
51
|
isDimmed: s.dimmed.has(id),
|
|
47
52
|
hasOnNavigateTo: !!s.onNavigateTo
|
|
48
53
|
}))
|
|
49
54
|
const isnavigable = !!compound.navigateTo && hasOnNavigateTo
|
|
50
55
|
|
|
56
|
+
const _isToolbarVisible = isEditable && (isHovered || (import.meta.env.DEV && selected))
|
|
57
|
+
const [isToolbarVisible] = useDebouncedValue(_isToolbarVisible, _isToolbarVisible ? 500 : 300)
|
|
58
|
+
|
|
59
|
+
const [previewColor, setPreviewColor] = useState<ThemeColor | null>(null)
|
|
60
|
+
|
|
51
61
|
return (
|
|
52
62
|
<>
|
|
63
|
+
{isToolbarVisible && (
|
|
64
|
+
<CompoundToolbar
|
|
65
|
+
isVisible={isToolbarVisible}
|
|
66
|
+
element={element}
|
|
67
|
+
align="start"
|
|
68
|
+
onColorPreview={setPreviewColor} />
|
|
69
|
+
)}
|
|
53
70
|
<Handle
|
|
54
71
|
type="target"
|
|
55
72
|
position={Position.Top}
|
|
@@ -64,7 +81,7 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
|
|
|
64
81
|
)}
|
|
65
82
|
mod={{
|
|
66
83
|
'compound-depth': depth,
|
|
67
|
-
'likec4-color': color,
|
|
84
|
+
'likec4-color': previewColor ?? color,
|
|
68
85
|
hovered: isHovered
|
|
69
86
|
}}
|
|
70
87
|
>
|
|
@@ -83,15 +100,15 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
|
|
|
83
100
|
opacity < 1 && css.transparent,
|
|
84
101
|
'likec4-compound'
|
|
85
102
|
)}
|
|
86
|
-
style={
|
|
87
|
-
|
|
103
|
+
style={{
|
|
104
|
+
...(opacity < 1 && {
|
|
88
105
|
...assignInlineVars({
|
|
89
106
|
[css.varBorderTransparency]: `${borderTransparency}%`,
|
|
90
107
|
[css.varOpacity]: opacity.toFixed(2)
|
|
91
108
|
}),
|
|
92
109
|
borderStyle: style.border ?? 'dashed'
|
|
93
|
-
}
|
|
94
|
-
|
|
110
|
+
})
|
|
111
|
+
}}
|
|
95
112
|
>
|
|
96
113
|
<Text
|
|
97
114
|
component="div"
|
|
@@ -15,7 +15,7 @@ import { IconLink } from '@tabler/icons-react'
|
|
|
15
15
|
import clsx from 'clsx'
|
|
16
16
|
import { useId } from 'react'
|
|
17
17
|
import { clamp } from 'remeda'
|
|
18
|
-
import {
|
|
18
|
+
import { useMantinePortalProps } from '../../../hooks/useMantinePortalProps'
|
|
19
19
|
import { type DiagramState } from '../../../state/diagramStore'
|
|
20
20
|
import type { XYFlowNode } from '../../types'
|
|
21
21
|
import { elementLink, trigger } from './ElementLink.css'
|
|
@@ -24,17 +24,12 @@ type ElementLinkProps = {
|
|
|
24
24
|
element: XYFlowNode['data']['element']
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const selector = (s: DiagramState) => {
|
|
28
|
-
const target = s.getContainer()
|
|
29
|
-
return target ? { target } : null
|
|
30
|
-
}
|
|
31
|
-
|
|
32
27
|
export function ElementLink({
|
|
33
28
|
element
|
|
34
29
|
}: ElementLinkProps) {
|
|
35
30
|
invariant(element.links, 'ElementLink: links are required')
|
|
36
31
|
const id = useId()
|
|
37
|
-
const portalProps =
|
|
32
|
+
const portalProps = useMantinePortalProps()
|
|
38
33
|
return (
|
|
39
34
|
<div className={elementLink} data-likec4-linkid={id}>
|
|
40
35
|
<HoverCard
|
|
@@ -47,7 +42,7 @@ export function ElementLink({
|
|
|
47
42
|
transitionProps={{
|
|
48
43
|
transition: 'pop'
|
|
49
44
|
}}
|
|
50
|
-
{...
|
|
45
|
+
{...portalProps}
|
|
51
46
|
floatingStrategy={'fixed'}
|
|
52
47
|
openDelay={350}
|
|
53
48
|
closeDelay={800}
|
|
@@ -57,6 +52,7 @@ export function ElementLink({
|
|
|
57
52
|
}}>
|
|
58
53
|
<HoverCardTarget>
|
|
59
54
|
<UnstyledButton
|
|
55
|
+
tabIndex={-1}
|
|
60
56
|
className={clsx('nodrag nopan', trigger)}
|
|
61
57
|
autoFocus={false}>
|
|
62
58
|
<IconLink size={12} />
|
|
@@ -1,19 +1,42 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { defaultTheme, type DiagramNode, type ThemeColor } from '@likec4/core'
|
|
2
2
|
import { Text as MantineText } from '@mantine/core'
|
|
3
|
+
import { useDebouncedValue } from '@mantine/hooks'
|
|
3
4
|
import { Handle, type NodeProps, Position } from '@xyflow/react'
|
|
4
5
|
import clsx from 'clsx'
|
|
5
6
|
import { deepEqual as eq } from 'fast-equals'
|
|
6
7
|
import { m, type Variants } from 'framer-motion'
|
|
7
|
-
import { memo } from 'react'
|
|
8
|
-
import { isNumber, isTruthy } from 'remeda'
|
|
8
|
+
import { memo, useState } from 'react'
|
|
9
|
+
import { isNumber, isTruthy, keys } from 'remeda'
|
|
9
10
|
import { type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../../../hooks/useDiagramState'
|
|
10
11
|
import type { ElementXYFlowNode } from '../../types'
|
|
11
12
|
import { toDomPrecision } from '../../utils'
|
|
12
13
|
import { NavigateToBtn } from '../shared/NavigateToBtn'
|
|
14
|
+
import { ElementToolbar } from '../shared/Toolbar'
|
|
13
15
|
import * as css from './element.css'
|
|
14
16
|
import { ElementLink } from './ElementLink'
|
|
15
17
|
import { ElementShapeSvg, SelectedIndicator } from './ElementShapeSvg'
|
|
16
18
|
|
|
19
|
+
const {
|
|
20
|
+
primary,
|
|
21
|
+
secondary,
|
|
22
|
+
muted,
|
|
23
|
+
...otherColors
|
|
24
|
+
} = defaultTheme.elements
|
|
25
|
+
|
|
26
|
+
export const themedColors = [
|
|
27
|
+
{ key: 'primary', value: primary.fill },
|
|
28
|
+
{ key: 'secondary', value: secondary.fill },
|
|
29
|
+
{ key: 'muted', value: muted.fill }
|
|
30
|
+
] satisfies Array<{ key: ThemeColor; value: string }>
|
|
31
|
+
|
|
32
|
+
export const colors = keys(otherColors).map(key => ({
|
|
33
|
+
key,
|
|
34
|
+
value: defaultTheme.elements[key].fill
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
export type ThemeColorKey = typeof themedColors[0]['key']
|
|
38
|
+
export type ColorKey = typeof colors[0]['key']
|
|
39
|
+
|
|
17
40
|
const Text = MantineText.withProps({
|
|
18
41
|
component: 'div'
|
|
19
42
|
})
|
|
@@ -89,15 +112,21 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
|
|
|
89
112
|
height
|
|
90
113
|
}) {
|
|
91
114
|
const store = useDiagramStoreApi()
|
|
92
|
-
const { isHovered, isDimmed, hasOnNavigateTo, isHovercards, isInteractive
|
|
115
|
+
const { isEditable, isHovered, isDimmed, hasOnNavigateTo, isHovercards, isInteractive } = useDiagramState(s => ({
|
|
116
|
+
isEditable: s.readonly !== true,
|
|
93
117
|
isHovered: s.hoveredNodeId === id,
|
|
94
118
|
isDimmed: s.dimmed.has(id),
|
|
95
|
-
isInteractive: s.nodesDraggable || s.nodesSelectable || !!s.onNavigateTo,
|
|
119
|
+
isInteractive: s.nodesDraggable || s.nodesSelectable || (!!s.onNavigateTo && !!element.navigateTo),
|
|
96
120
|
isHovercards: s.showElementLinks,
|
|
97
|
-
hasOnNavigateTo: !!s.onNavigateTo
|
|
98
|
-
hasGoToSource: !!s.onOpenSourceElement
|
|
121
|
+
hasOnNavigateTo: !!s.onNavigateTo
|
|
122
|
+
// hasGoToSource: !!s.onOpenSourceElement
|
|
99
123
|
}))
|
|
100
124
|
const isNavigable = hasOnNavigateTo && !!element.navigateTo
|
|
125
|
+
// const isIdle = useIdle(300)
|
|
126
|
+
// const [isToolbarVisible] = useDebouncedValue(isEditable && (isHovered || (import.meta.env.DEV && selected)), isIdle ? 50 : 350)
|
|
127
|
+
const _isToolbarVisible = isEditable && (isHovered || (import.meta.env.DEV && selected))
|
|
128
|
+
|
|
129
|
+
const [isToolbarVisible] = useDebouncedValue(_isToolbarVisible, _isToolbarVisible ? 500 : 300)
|
|
101
130
|
|
|
102
131
|
const w = toDomPrecision(width ?? element.width)
|
|
103
132
|
const h = toDomPrecision(height ?? element.height)
|
|
@@ -120,8 +149,17 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
|
|
|
120
149
|
|
|
121
150
|
const elementIcon = ElementIcon({ node: element, store })
|
|
122
151
|
|
|
152
|
+
const [previewColor, setPreviewColor] = useState<ThemeColor | null>(null)
|
|
153
|
+
|
|
123
154
|
return (
|
|
124
155
|
<>
|
|
156
|
+
{isToolbarVisible && (
|
|
157
|
+
<ElementToolbar
|
|
158
|
+
element={element}
|
|
159
|
+
isVisible={isToolbarVisible}
|
|
160
|
+
onColorPreview={setPreviewColor}
|
|
161
|
+
/>
|
|
162
|
+
)}
|
|
125
163
|
<Handle
|
|
126
164
|
type="target"
|
|
127
165
|
position={Position.Top}
|
|
@@ -136,11 +174,12 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
|
|
|
136
174
|
'likec4-element-node'
|
|
137
175
|
])}
|
|
138
176
|
data-hovered={!dragging && isHovered}
|
|
139
|
-
data-likec4-color={element.color}
|
|
177
|
+
data-likec4-color={previewColor ?? element.color}
|
|
140
178
|
data-likec4-shape={element.shape}
|
|
141
179
|
variants={variants}
|
|
142
180
|
initial={false}
|
|
143
181
|
animate={animate}
|
|
182
|
+
tabIndex={-1}
|
|
144
183
|
{...(isInteractive && {
|
|
145
184
|
whileTap: dragging ? animate : 'tap'
|
|
146
185
|
})}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { rem } from '@mantine/core'
|
|
2
2
|
import { createVar, fallbackVar, globalStyle, keyframes, style } from '@vanilla-extract/css'
|
|
3
|
-
import { mantine } from '../../../
|
|
4
|
-
import { vars } from '../../../theme.css'
|
|
3
|
+
import { mantine, vars } from '../../../theme-vars'
|
|
5
4
|
|
|
6
5
|
export const stokeFillMix = createVar('stroke-fill-mix')
|
|
7
6
|
|