@likec4/diagram 1.16.0 → 1.17.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 +22 -18
- package/src/LikeC4CustomColors.tsx +24 -21
- package/src/LikeC4Diagram.props.ts +15 -9
- package/src/LikeC4Diagram.tsx +33 -27
- package/src/LikeC4Search.tsx +146 -0
- package/src/StaticLikeC4Diagram.tsx +10 -5
- 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/aligners.ts +473 -0
- package/src/state/diagramStore.layout.ts +67 -0
- package/src/state/diagramStore.ts +18 -73
- package/src/theme-vars.ts +9 -0
- package/src/ui/FramerMotionConfig.tsx +18 -0
- package/src/ui/notation/NotationPanel.tsx +3 -3
- package/src/ui/top-left/ManualLayoutToolsButton.tsx +15 -0
- package/src/ui/top-left/TopLeftPanel.tsx +14 -1
- 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 +165 -133
- package/src/xyflow/nodes/element/element.css.ts +46 -17
- 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
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ComputedView,
|
|
3
|
+
type DiagramNode,
|
|
4
|
+
type DiagramView,
|
|
5
|
+
type EdgeId,
|
|
6
|
+
type Element,
|
|
7
|
+
type Fqn,
|
|
8
|
+
invariant,
|
|
9
|
+
type Link,
|
|
10
|
+
type ViewID
|
|
11
|
+
} from '@likec4/core'
|
|
12
|
+
import {
|
|
13
|
+
ActionIcon,
|
|
14
|
+
ActionIconGroup,
|
|
15
|
+
Anchor,
|
|
16
|
+
Badge,
|
|
17
|
+
Box,
|
|
18
|
+
Card,
|
|
19
|
+
CloseButton,
|
|
20
|
+
Code,
|
|
21
|
+
CopyButton,
|
|
22
|
+
Divider as MantineDivider,
|
|
23
|
+
Flex,
|
|
24
|
+
FocusTrap,
|
|
25
|
+
FocusTrapInitialFocus,
|
|
26
|
+
Group,
|
|
27
|
+
RemoveScroll,
|
|
28
|
+
ScrollArea,
|
|
29
|
+
Stack,
|
|
30
|
+
Tabs,
|
|
31
|
+
TabsList,
|
|
32
|
+
TabsPanel,
|
|
33
|
+
TabsTab,
|
|
34
|
+
Text,
|
|
35
|
+
type TextProps,
|
|
36
|
+
ThemeIcon,
|
|
37
|
+
Tooltip as MantineTooltip,
|
|
38
|
+
UnstyledButton
|
|
39
|
+
} from '@mantine/core'
|
|
40
|
+
import { useViewportSize } from '@mantine/hooks'
|
|
41
|
+
import { IconCheck, IconCopy, IconExternalLink, IconFileSymlink, IconZoomScan } from '@tabler/icons-react'
|
|
42
|
+
import { useInternalNode } from '@xyflow/react'
|
|
43
|
+
import clsx from 'clsx'
|
|
44
|
+
import { m, type PanInfo, useDragControls, useMotionValue } from 'framer-motion'
|
|
45
|
+
import { type PropsWithChildren, type ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
|
46
|
+
import { clamp, find, isNullish, map, only, partition, pick, pipe, unique } from 'remeda'
|
|
47
|
+
import { useDiagramState, useDiagramStoreApi, useXYFlow } from '../../hooks'
|
|
48
|
+
import type { ElementIconRenderer, OnNavigateTo } from '../../LikeC4Diagram.props'
|
|
49
|
+
import { useLikeC4Model } from '../../likec4model'
|
|
50
|
+
import { useOverlayDialog } from '../OverlayContext'
|
|
51
|
+
import * as css from './ElementDetailsCard.css'
|
|
52
|
+
import { TabPanelRelationships } from './TabPanelRelationships'
|
|
53
|
+
import { TabElementStructure } from './TabPanelStructure'
|
|
54
|
+
|
|
55
|
+
const Divider = MantineDivider.withProps({
|
|
56
|
+
mb: 8,
|
|
57
|
+
labelPosition: 'left',
|
|
58
|
+
variant: 'dashed'
|
|
59
|
+
})
|
|
60
|
+
const Tooltip = MantineTooltip.withProps({
|
|
61
|
+
color: 'dark',
|
|
62
|
+
fz: 'xs',
|
|
63
|
+
openDelay: 400,
|
|
64
|
+
closeDelay: 150,
|
|
65
|
+
label: '',
|
|
66
|
+
children: null,
|
|
67
|
+
offset: 4
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const SmallLabel = Text.withProps({
|
|
71
|
+
component: 'div',
|
|
72
|
+
fz: 11,
|
|
73
|
+
fw: 500,
|
|
74
|
+
c: 'dimmed',
|
|
75
|
+
lh: 1
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const PropertyLabel = Text.withProps({
|
|
79
|
+
component: 'div',
|
|
80
|
+
fz: 'xs',
|
|
81
|
+
c: 'dimmed',
|
|
82
|
+
className: css.propertyLabel
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
type ElementDetailsCardProps = {
|
|
86
|
+
fqn: Fqn
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const MIN_PADDING = 24
|
|
90
|
+
|
|
91
|
+
export function ElementDetailsCard({ fqn }: ElementDetailsCardProps) {
|
|
92
|
+
const windowSize = useViewportSize()
|
|
93
|
+
const windowWidth = windowSize.width || window.innerWidth || 1200,
|
|
94
|
+
windowHeight = windowSize.height || window.innerHeight || 800
|
|
95
|
+
const xyflow = useXYFlow()
|
|
96
|
+
const xynode = useInternalNode(fqn)
|
|
97
|
+
invariant(xynode, `XYNode with id ${fqn} not found`)
|
|
98
|
+
|
|
99
|
+
const overlay = useOverlayDialog()
|
|
100
|
+
const [activeTab, setActiveTab] = useState<'Properties' | 'Relationships' | 'Views'>('Properties')
|
|
101
|
+
const diagramApi = useDiagramStoreApi()
|
|
102
|
+
const {
|
|
103
|
+
view: currentView,
|
|
104
|
+
renderIcon,
|
|
105
|
+
onNavigateTo,
|
|
106
|
+
onOpenSourceElement: onOpenSource
|
|
107
|
+
} = useDiagramState(pick(['view', 'renderIcon', 'onNavigateTo', 'onOpenSourceElement']))
|
|
108
|
+
const element = currentView.nodes.find(n => n.id === fqn)
|
|
109
|
+
invariant(element, `DiagramNode with fqn ${fqn} not found`)
|
|
110
|
+
|
|
111
|
+
const likec4Model = useLikeC4Model(true)
|
|
112
|
+
const elementModel = likec4Model.element(fqn)
|
|
113
|
+
const viewId = currentView.id
|
|
114
|
+
|
|
115
|
+
const elementIcon = ElementIcon({
|
|
116
|
+
element,
|
|
117
|
+
viewId,
|
|
118
|
+
renderIcon
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const incoming = elementModel.incoming().map(r => r.id)
|
|
122
|
+
const outgoing = elementModel.outgoing().map(r => r.id)
|
|
123
|
+
|
|
124
|
+
const findRelationIds = (edgeId: EdgeId) => currentView.edges.find((edge) => edge.id === edgeId)?.relations ?? []
|
|
125
|
+
|
|
126
|
+
const incomingInView = unique(element.inEdges.flatMap(findRelationIds))
|
|
127
|
+
const outgoingInView = unique(element.outEdges.flatMap(findRelationIds))
|
|
128
|
+
|
|
129
|
+
const notIncludedRelations = [
|
|
130
|
+
...incoming,
|
|
131
|
+
...outgoing
|
|
132
|
+
].filter(r => !incomingInView.includes(r) && !outgoingInView.includes(r)).length
|
|
133
|
+
|
|
134
|
+
const [viewsOf, otherViews] = pipe(
|
|
135
|
+
elementModel.views(),
|
|
136
|
+
map(v => v.view as ComputedView),
|
|
137
|
+
partition(v => v.__ !== 'dynamic' && v.viewOf === fqn)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const defaultView = element.navigateTo
|
|
141
|
+
? likec4Model.view(element.navigateTo).view
|
|
142
|
+
: (find(viewsOf, v => v.id !== currentView.id) ?? null)
|
|
143
|
+
|
|
144
|
+
const defaultLink = element.links && only(element.links)
|
|
145
|
+
|
|
146
|
+
const onNavigateToCb = useCallback((toView: ViewID, e?: React.MouseEvent): void => {
|
|
147
|
+
e?.stopPropagation()
|
|
148
|
+
const { onNavigateTo } = diagramApi.getState()
|
|
149
|
+
if (!onNavigateTo) {
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
overlay.close(() => {
|
|
153
|
+
diagramApi.setState({
|
|
154
|
+
lastOnNavigate: {
|
|
155
|
+
fromView: currentView.id,
|
|
156
|
+
toView,
|
|
157
|
+
fromNode: fqn
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
onNavigateTo(toView)
|
|
161
|
+
})
|
|
162
|
+
}, [fqn, currentView.id])
|
|
163
|
+
const controls = useDragControls()
|
|
164
|
+
|
|
165
|
+
const isCompound = element.children.length > 0
|
|
166
|
+
|
|
167
|
+
const fromPositon = xyflow.flowToScreenPosition({
|
|
168
|
+
x: xynode.internals.positionAbsolute.x + element.width / 2,
|
|
169
|
+
y: xynode.internals.positionAbsolute.y + (isCompound ? 0 : element.height / 2)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const _width = Math.min(700, windowWidth - MIN_PADDING * 2)
|
|
173
|
+
const _height = Math.min(650, windowHeight - MIN_PADDING * 2)
|
|
174
|
+
|
|
175
|
+
const left = Math.round(
|
|
176
|
+
clamp(fromPositon.x - _width / 2, {
|
|
177
|
+
min: MIN_PADDING,
|
|
178
|
+
max: windowWidth - _width - MIN_PADDING
|
|
179
|
+
})
|
|
180
|
+
)
|
|
181
|
+
const top = Math.round(
|
|
182
|
+
clamp(fromPositon.y - (isCompound ? 0 : 60), {
|
|
183
|
+
min: MIN_PADDING,
|
|
184
|
+
max: windowHeight - _height - MIN_PADDING
|
|
185
|
+
})
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
const width = useMotionValue(_width)
|
|
189
|
+
const height = useMotionValue(_height)
|
|
190
|
+
|
|
191
|
+
const handleDrag = useCallback((_: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => {
|
|
192
|
+
width.set(Math.max(width.get() + info.delta.x, 320))
|
|
193
|
+
height.set(Math.max(height.get() + info.delta.y, 300))
|
|
194
|
+
}, [])
|
|
195
|
+
|
|
196
|
+
const ref = useRef<HTMLDialogElement>(null)
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
ref.current?.showModal()
|
|
199
|
+
}, [])
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<m.dialog
|
|
203
|
+
ref={ref}
|
|
204
|
+
className={css.dialog}
|
|
205
|
+
initial={{
|
|
206
|
+
'--backdrop-blur': '0px',
|
|
207
|
+
'--backdrop-opacity': '0%'
|
|
208
|
+
}}
|
|
209
|
+
animate={{
|
|
210
|
+
'--backdrop-blur': '1px',
|
|
211
|
+
'--backdrop-opacity': '50%'
|
|
212
|
+
}}
|
|
213
|
+
exit={{
|
|
214
|
+
'--backdrop-blur': '0px',
|
|
215
|
+
'--backdrop-opacity': '0%'
|
|
216
|
+
}}
|
|
217
|
+
onClick={e => {
|
|
218
|
+
if ((e.target as any)?.nodeName?.toUpperCase() === 'DIALOG') {
|
|
219
|
+
e.stopPropagation()
|
|
220
|
+
overlay.close()
|
|
221
|
+
}
|
|
222
|
+
}}
|
|
223
|
+
onClose={e => {
|
|
224
|
+
e.stopPropagation()
|
|
225
|
+
overlay.close()
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
<RemoveScroll forwardProps>
|
|
229
|
+
<Card
|
|
230
|
+
drag
|
|
231
|
+
dragElastic={0}
|
|
232
|
+
dragMomentum={false}
|
|
233
|
+
dragListener={false}
|
|
234
|
+
dragControls={controls}
|
|
235
|
+
withBorder
|
|
236
|
+
shadow="md"
|
|
237
|
+
component={m.div}
|
|
238
|
+
className={css.card}
|
|
239
|
+
layoutId={`${viewId}:element:${fqn}`}
|
|
240
|
+
// initial={{
|
|
241
|
+
// opacity: 1,
|
|
242
|
+
// top,
|
|
243
|
+
// left,
|
|
244
|
+
// // transform: `translate(${left}px, ${top}px)`,
|
|
245
|
+
// // translateX: left,
|
|
246
|
+
// // translateY: top,
|
|
247
|
+
// // width: _width,
|
|
248
|
+
// // height: _height
|
|
249
|
+
// }}
|
|
250
|
+
initial={{
|
|
251
|
+
top,
|
|
252
|
+
left,
|
|
253
|
+
width: _width,
|
|
254
|
+
height: _height,
|
|
255
|
+
opacity: 0
|
|
256
|
+
}}
|
|
257
|
+
animate={{
|
|
258
|
+
opacity: 1
|
|
259
|
+
}}
|
|
260
|
+
exit={{
|
|
261
|
+
opacity: 0
|
|
262
|
+
}}
|
|
263
|
+
style={{
|
|
264
|
+
// `style` prop in Mantine doesn't accept motion values
|
|
265
|
+
width: width as any,
|
|
266
|
+
height: height as any
|
|
267
|
+
}}
|
|
268
|
+
onKeyDown={e => {
|
|
269
|
+
if (e.key === 'Escape') {
|
|
270
|
+
e.preventDefault()
|
|
271
|
+
e.stopPropagation()
|
|
272
|
+
overlay.close()
|
|
273
|
+
}
|
|
274
|
+
}}
|
|
275
|
+
data-likec4-color={element.color}>
|
|
276
|
+
<FocusTrap>
|
|
277
|
+
<FocusTrapInitialFocus />
|
|
278
|
+
<Box
|
|
279
|
+
className={css.cardHeader}
|
|
280
|
+
onPointerDown={e => controls.start(e)}>
|
|
281
|
+
<Group align="start" justify="space-between" gap={'sm'} mb={'sm'} wrap="nowrap">
|
|
282
|
+
<Group align="start" gap={'sm'} style={{ cursor: 'default' }} wrap="nowrap">
|
|
283
|
+
{elementIcon}
|
|
284
|
+
<Box>
|
|
285
|
+
<Text
|
|
286
|
+
component={m.div}
|
|
287
|
+
layout="position"
|
|
288
|
+
layoutId={`${viewId}:element:title:${fqn}`}
|
|
289
|
+
className={css.title}>
|
|
290
|
+
{elementModel.title}
|
|
291
|
+
</Text>
|
|
292
|
+
{element.notation && (
|
|
293
|
+
<Text component="div" c={'dimmed'} fz={'sm'} fw={500} lh={1.3} lineClamp={1}>
|
|
294
|
+
{element.notation}
|
|
295
|
+
</Text>
|
|
296
|
+
)}
|
|
297
|
+
</Box>
|
|
298
|
+
</Group>
|
|
299
|
+
<CloseButton
|
|
300
|
+
size={'lg'}
|
|
301
|
+
onClick={e => {
|
|
302
|
+
e.stopPropagation()
|
|
303
|
+
overlay.close()
|
|
304
|
+
}}
|
|
305
|
+
/>
|
|
306
|
+
</Group>
|
|
307
|
+
<Group align="baseline" gap={'sm'} wrap="nowrap">
|
|
308
|
+
<Box>
|
|
309
|
+
<SmallLabel>kind</SmallLabel>
|
|
310
|
+
<Badge radius={'sm'} size="sm" fw={600} color="gray">{element.kind}</Badge>
|
|
311
|
+
</Box>
|
|
312
|
+
<Box flex={1}>
|
|
313
|
+
<SmallLabel>tags</SmallLabel>
|
|
314
|
+
<Flex gap={4} flex={1} mt={6}>
|
|
315
|
+
{element.tags?.map((tag) => (
|
|
316
|
+
<Badge key={tag} radius={'sm'} size="sm" fw={600} variant="gradient">#{tag}</Badge>
|
|
317
|
+
))}
|
|
318
|
+
{!element.tags && <Badge radius={'sm'} size="sm" fw={600} color="gray">—</Badge>}
|
|
319
|
+
</Flex>
|
|
320
|
+
</Box>
|
|
321
|
+
<ActionIconGroup
|
|
322
|
+
style={{
|
|
323
|
+
alignSelf: 'flex-end'
|
|
324
|
+
}}>
|
|
325
|
+
{defaultLink && (
|
|
326
|
+
<ActionIcon
|
|
327
|
+
component="a"
|
|
328
|
+
href={defaultLink.url}
|
|
329
|
+
target="_blank"
|
|
330
|
+
size="lg"
|
|
331
|
+
variant="default"
|
|
332
|
+
radius="sm"
|
|
333
|
+
>
|
|
334
|
+
<IconExternalLink stroke={1.6} style={{ width: '65%' }} />
|
|
335
|
+
</ActionIcon>
|
|
336
|
+
)}
|
|
337
|
+
{onOpenSource && (
|
|
338
|
+
<Tooltip label="Open source">
|
|
339
|
+
<ActionIcon
|
|
340
|
+
size="lg"
|
|
341
|
+
variant="default"
|
|
342
|
+
radius="sm"
|
|
343
|
+
onClick={e => {
|
|
344
|
+
e.stopPropagation()
|
|
345
|
+
diagramApi.getState().onOpenSourceElement?.(fqn)
|
|
346
|
+
}}
|
|
347
|
+
>
|
|
348
|
+
<IconFileSymlink stroke={1.8} style={{ width: '62%' }} />
|
|
349
|
+
</ActionIcon>
|
|
350
|
+
</Tooltip>
|
|
351
|
+
)}
|
|
352
|
+
{defaultView && (
|
|
353
|
+
<Tooltip label="Open default view">
|
|
354
|
+
<ActionIcon
|
|
355
|
+
size="lg"
|
|
356
|
+
variant="default"
|
|
357
|
+
radius="sm"
|
|
358
|
+
onClick={e => {
|
|
359
|
+
onNavigateToCb(defaultView.id, e)
|
|
360
|
+
}}>
|
|
361
|
+
<IconZoomScan style={{ width: '70%' }} />
|
|
362
|
+
</ActionIcon>
|
|
363
|
+
</Tooltip>
|
|
364
|
+
)}
|
|
365
|
+
</ActionIconGroup>
|
|
366
|
+
</Group>
|
|
367
|
+
</Box>
|
|
368
|
+
|
|
369
|
+
<Tabs
|
|
370
|
+
value={activeTab}
|
|
371
|
+
onChange={v => setActiveTab(v as any)}
|
|
372
|
+
variant="none"
|
|
373
|
+
classNames={{
|
|
374
|
+
root: css.tabsRoot,
|
|
375
|
+
list: css.tabsList,
|
|
376
|
+
tab: css.tabsTab,
|
|
377
|
+
panel: css.tabsPanel
|
|
378
|
+
}}>
|
|
379
|
+
<TabsList>
|
|
380
|
+
<TabsTab value="Properties">
|
|
381
|
+
Properties
|
|
382
|
+
</TabsTab>
|
|
383
|
+
<TabsTab value="Relationships">
|
|
384
|
+
Relationships
|
|
385
|
+
</TabsTab>
|
|
386
|
+
<TabsTab value="Views">
|
|
387
|
+
Views
|
|
388
|
+
</TabsTab>
|
|
389
|
+
<TabsTab value="Structure">
|
|
390
|
+
Structure
|
|
391
|
+
</TabsTab>
|
|
392
|
+
</TabsList>
|
|
393
|
+
|
|
394
|
+
<TabsPanel value="Properties">
|
|
395
|
+
<ScrollArea scrollbars="y" type="auto">
|
|
396
|
+
<Box className={css.propertiesGrid} pt={'xs'}>
|
|
397
|
+
<ElementProperty title="description" emptyValue="no description">
|
|
398
|
+
{element.description}
|
|
399
|
+
</ElementProperty>
|
|
400
|
+
{element.technology && (
|
|
401
|
+
<ElementProperty title="technology">
|
|
402
|
+
{element.technology}
|
|
403
|
+
</ElementProperty>
|
|
404
|
+
)}
|
|
405
|
+
{element.links && (
|
|
406
|
+
<>
|
|
407
|
+
<PropertyLabel>links</PropertyLabel>
|
|
408
|
+
<Stack gap={'xs'} align="flex-start">
|
|
409
|
+
{element.links.map((link, i) => <ElementLink key={i} value={link} />)}
|
|
410
|
+
</Stack>
|
|
411
|
+
</>
|
|
412
|
+
)}
|
|
413
|
+
{elementModel.element.metadata && <ElementMetata value={elementModel.element.metadata} />}
|
|
414
|
+
</Box>
|
|
415
|
+
</ScrollArea>
|
|
416
|
+
</TabsPanel>
|
|
417
|
+
|
|
418
|
+
<TabsPanel value="Relationships">
|
|
419
|
+
{activeTab === 'Relationships' && (
|
|
420
|
+
<TabPanelRelationships
|
|
421
|
+
element={elementModel}
|
|
422
|
+
currentView={currentView} />
|
|
423
|
+
)}
|
|
424
|
+
</TabsPanel>
|
|
425
|
+
|
|
426
|
+
<TabsPanel value="Views">
|
|
427
|
+
<ScrollArea scrollbars="y" type="auto">
|
|
428
|
+
<Stack gap={'lg'}>
|
|
429
|
+
{viewsOf.length > 0 && (
|
|
430
|
+
<Box>
|
|
431
|
+
<Divider label="views of the element (scoped)" />
|
|
432
|
+
<Stack gap={'sm'}>
|
|
433
|
+
{viewsOf.map((view) => (
|
|
434
|
+
<ViewButton
|
|
435
|
+
key={view.id}
|
|
436
|
+
view={view}
|
|
437
|
+
onNavigateTo={onNavigateToCb}
|
|
438
|
+
/>
|
|
439
|
+
))}
|
|
440
|
+
</Stack>
|
|
441
|
+
</Box>
|
|
442
|
+
)}
|
|
443
|
+
{otherViews.length > 0 && (
|
|
444
|
+
<Box>
|
|
445
|
+
<Divider label="views including this element" />
|
|
446
|
+
<Stack gap={'sm'}>
|
|
447
|
+
{otherViews.map((view) => (
|
|
448
|
+
<ViewButton
|
|
449
|
+
key={view.id}
|
|
450
|
+
view={view}
|
|
451
|
+
onNavigateTo={onNavigateToCb}
|
|
452
|
+
/>
|
|
453
|
+
))}
|
|
454
|
+
</Stack>
|
|
455
|
+
</Box>
|
|
456
|
+
)}
|
|
457
|
+
</Stack>
|
|
458
|
+
</ScrollArea>
|
|
459
|
+
</TabsPanel>
|
|
460
|
+
|
|
461
|
+
<TabsPanel value="Structure">
|
|
462
|
+
<ScrollArea scrollbars="y" type="auto">
|
|
463
|
+
<TabElementStructure element={elementModel} />
|
|
464
|
+
</ScrollArea>
|
|
465
|
+
</TabsPanel>
|
|
466
|
+
</Tabs>
|
|
467
|
+
<m.div
|
|
468
|
+
className={css.resizeHandle}
|
|
469
|
+
drag
|
|
470
|
+
dragElastic={0}
|
|
471
|
+
dragMomentum={false}
|
|
472
|
+
onDrag={handleDrag}
|
|
473
|
+
dragConstraints={{ top: 0, left: 0, right: 0, bottom: 0 }}
|
|
474
|
+
/>
|
|
475
|
+
</FocusTrap>
|
|
476
|
+
</Card>
|
|
477
|
+
</RemoveScroll>
|
|
478
|
+
</m.dialog>
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const ElementIcon = (
|
|
483
|
+
{ element, viewId, renderIcon: RenderIcon }: {
|
|
484
|
+
element: DiagramNode
|
|
485
|
+
viewId: string
|
|
486
|
+
renderIcon: ElementIconRenderer | null
|
|
487
|
+
}
|
|
488
|
+
) => {
|
|
489
|
+
if (!element.icon) {
|
|
490
|
+
return null
|
|
491
|
+
}
|
|
492
|
+
let icon = null as React.ReactNode
|
|
493
|
+
if (element.icon.startsWith('http://') || element.icon.startsWith('https://')) {
|
|
494
|
+
icon = <img src={element.icon} alt={element.title} />
|
|
495
|
+
} else if (RenderIcon) {
|
|
496
|
+
icon = <RenderIcon node={element} />
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (!icon) {
|
|
500
|
+
return null
|
|
501
|
+
}
|
|
502
|
+
return (
|
|
503
|
+
<m.div
|
|
504
|
+
// layout="position"
|
|
505
|
+
layoutId={`${viewId}:element:icon:${element.id}`}
|
|
506
|
+
className={clsx(
|
|
507
|
+
css.elementIcon,
|
|
508
|
+
'likec4-element-icon'
|
|
509
|
+
)}
|
|
510
|
+
data-likec4-icon={element.icon}
|
|
511
|
+
>
|
|
512
|
+
{icon}
|
|
513
|
+
</m.div>
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const ViewButton = ({
|
|
518
|
+
view,
|
|
519
|
+
onNavigateTo
|
|
520
|
+
}: {
|
|
521
|
+
view: ComputedView | DiagramView
|
|
522
|
+
onNavigateTo: OnNavigateTo
|
|
523
|
+
}) => {
|
|
524
|
+
return (
|
|
525
|
+
<UnstyledButton className={css.viewButton} onClick={e => onNavigateTo(view.id, e)}>
|
|
526
|
+
<Group gap={6} align="start" wrap="nowrap">
|
|
527
|
+
<ThemeIcon size={'sm'} variant="transparent">
|
|
528
|
+
<IconZoomScan stroke={1.8} />
|
|
529
|
+
</ThemeIcon>
|
|
530
|
+
<Box>
|
|
531
|
+
<Text component="div" className={css.viewButtonTitle} lineClamp={1}>
|
|
532
|
+
{view.title || 'untitled'}
|
|
533
|
+
</Text>
|
|
534
|
+
{view.description && (
|
|
535
|
+
<Text component="div" mt={2} fz={'xs'} c={'dimmed'} lh={1.4} lineClamp={1}>
|
|
536
|
+
{view.description}
|
|
537
|
+
</Text>
|
|
538
|
+
)}
|
|
539
|
+
</Box>
|
|
540
|
+
</Group>
|
|
541
|
+
</UnstyledButton>
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function ElementProperty({
|
|
546
|
+
title,
|
|
547
|
+
emptyValue = `undefined`,
|
|
548
|
+
children,
|
|
549
|
+
style,
|
|
550
|
+
...props
|
|
551
|
+
}: PropsWithChildren<
|
|
552
|
+
Omit<TextProps, 'title'> & {
|
|
553
|
+
title: string
|
|
554
|
+
emptyValue?: string
|
|
555
|
+
}
|
|
556
|
+
>) {
|
|
557
|
+
return (
|
|
558
|
+
<>
|
|
559
|
+
<PropertyLabel>{title}</PropertyLabel>
|
|
560
|
+
<Text
|
|
561
|
+
component="div"
|
|
562
|
+
{...(isNullish(children) && { c: 'dimmed' })}
|
|
563
|
+
fz={'md'}
|
|
564
|
+
style={{
|
|
565
|
+
whiteSpace: 'preserve-breaks',
|
|
566
|
+
...style
|
|
567
|
+
}}
|
|
568
|
+
{...props}
|
|
569
|
+
>
|
|
570
|
+
{children || emptyValue}
|
|
571
|
+
</Text>
|
|
572
|
+
</>
|
|
573
|
+
)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function ElementLink({
|
|
577
|
+
value
|
|
578
|
+
}: {
|
|
579
|
+
value: Link
|
|
580
|
+
}) {
|
|
581
|
+
const url = new URL(value.url, window.location.href).toString()
|
|
582
|
+
return (
|
|
583
|
+
<CopyButton value={url}>
|
|
584
|
+
{({ copied, copy }) => (
|
|
585
|
+
<Anchor href={url} target="_blank" underline="never" className={css.elementLink}>
|
|
586
|
+
<Group gap={4} align="center" wrap="nowrap">
|
|
587
|
+
<ActionIcon
|
|
588
|
+
tabIndex={-1}
|
|
589
|
+
size={24}
|
|
590
|
+
variant={copied ? 'light' : 'subtle'}
|
|
591
|
+
color={copied ? 'teal' : 'gray'}
|
|
592
|
+
onClick={e => {
|
|
593
|
+
e.stopPropagation()
|
|
594
|
+
e.preventDefault()
|
|
595
|
+
copy()
|
|
596
|
+
}}
|
|
597
|
+
>
|
|
598
|
+
{copied ? <IconCheck /> : <IconCopy style={{ width: '65%', opacity: 0.8 }} />}
|
|
599
|
+
</ActionIcon>
|
|
600
|
+
<Box flex={1}>
|
|
601
|
+
<Text fz={'sm'} truncate lh={1.3} fw={value.title ? 500 : 400}>
|
|
602
|
+
{value.title || url}
|
|
603
|
+
</Text>
|
|
604
|
+
{value.title && (
|
|
605
|
+
<Text component="div" fz={10} c={'dimmed'} lh={1.2} truncate>
|
|
606
|
+
{url}
|
|
607
|
+
</Text>
|
|
608
|
+
)}
|
|
609
|
+
</Box>
|
|
610
|
+
</Group>
|
|
611
|
+
</Anchor>
|
|
612
|
+
)}
|
|
613
|
+
</CopyButton>
|
|
614
|
+
)
|
|
615
|
+
// <Anchor href={value.url} fz={'sm'}>
|
|
616
|
+
// {value.title || url}
|
|
617
|
+
// </Anchor><Button variant='default' size='sm'>
|
|
618
|
+
// {value.title || value.url}
|
|
619
|
+
// </Button>
|
|
620
|
+
// </Box>
|
|
621
|
+
// )
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function ElementMetata({
|
|
625
|
+
value
|
|
626
|
+
}: {
|
|
627
|
+
value: NonNullable<Element['metadata']>
|
|
628
|
+
}) {
|
|
629
|
+
return (
|
|
630
|
+
<>
|
|
631
|
+
<PropertyLabel>metadata</PropertyLabel>
|
|
632
|
+
<Box>
|
|
633
|
+
<Code block>
|
|
634
|
+
{JSON.stringify(value, null, 2)}
|
|
635
|
+
</Code>
|
|
636
|
+
</Box>
|
|
637
|
+
</>
|
|
638
|
+
)
|
|
639
|
+
}
|