@likec4/diagram 1.15.1 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/package.json +7 -7
  2. package/src/LikeC4CustomColors.tsx +24 -21
  3. package/src/LikeC4Diagram.props.ts +9 -9
  4. package/src/LikeC4Diagram.tsx +25 -26
  5. package/src/StaticLikeC4Diagram.tsx +9 -3
  6. package/src/overlays/OverlayContext.tsx +1 -1
  7. package/src/overlays/Overlays.css.ts +28 -23
  8. package/src/overlays/Overlays.tsx +116 -64
  9. package/src/overlays/edge-details/xyflow/ElementNode.tsx +15 -12
  10. package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +7 -3
  11. package/src/overlays/edge-details/xyflow/styles.css.ts +22 -16
  12. package/src/overlays/element-details/ElementDetailsCard.css.ts +322 -0
  13. package/src/overlays/element-details/ElementDetailsCard.tsx +639 -0
  14. package/src/overlays/element-details/TabPanelRelationships.css.ts +106 -0
  15. package/src/overlays/element-details/TabPanelRelationships.tsx +201 -0
  16. package/src/overlays/element-details/TabPanelStructure.css.ts +36 -0
  17. package/src/overlays/element-details/TabPanelStructure.tsx +120 -0
  18. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +185 -0
  19. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +179 -213
  20. package/src/overlays/relationships-of/_types.ts +15 -4
  21. package/src/overlays/relationships-of/use-layouted-relationships.ts +301 -207
  22. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +68 -14
  23. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +54 -14
  24. package/src/overlays/relationships-of/xyflow/EmptyNode.tsx +1 -1
  25. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +51 -23
  26. package/src/overlays/relationships-of/xyflow/styles.css.ts +34 -27
  27. package/src/state/DiagramContext.tsx +15 -8
  28. package/src/state/diagramStore.ts +14 -6
  29. package/src/theme-vars.ts +9 -0
  30. package/src/ui/FramerMotionConfig.tsx +18 -0
  31. package/src/ui/notation/NotationPanel.tsx +3 -3
  32. package/src/xyflow/XYFlow.tsx +18 -35
  33. package/src/xyflow/XYFlowInner.tsx +2 -2
  34. package/src/xyflow/edges/EdgeLabel.tsx +3 -3
  35. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +25 -18
  36. package/src/xyflow/nodes/compound/CompoundNode.css.ts +96 -30
  37. package/src/xyflow/nodes/compound/CompoundNode.tsx +149 -165
  38. package/src/xyflow/nodes/element/ElementDetails.css.ts +101 -0
  39. package/src/xyflow/nodes/element/ElementDetails.tsx +393 -0
  40. package/src/xyflow/nodes/element/ElementNode.tsx +110 -116
  41. package/src/xyflow/nodes/element/element.css.ts +38 -18
  42. package/src/xyflow/nodes/shared/ElementIcon.tsx +43 -0
  43. package/src/xyflow/nodes/shared/Toolbar.tsx +6 -6
  44. package/src/xyflow/{XYFlowEvents.tsx → useXYFlowEvents.ts} +26 -22
  45. package/src/xyflow/utils.ts +12 -3
  46. package/src/LikeC4CustomColors.props.ts +0 -5
  47. /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'}>
282
+ <Group align="start" gap={'sm'} style={{ cursor: 'default' }}>
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'}>
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
+ }