@likec4/diagram 1.7.2 → 1.7.4

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 (70) hide show
  1. package/dist/LikeC4Diagram.d.ts +1 -1
  2. package/dist/LikeC4Diagram.props.d.ts +38 -22
  3. package/dist/StaticLikeC4Diagram.d.ts +1 -1
  4. package/dist/hooks/index.d.ts +2 -0
  5. package/dist/{state/useDiagramStore.d.ts → hooks/useDiagramState.d.ts} +2 -3
  6. package/dist/{xyflow/hooks → hooks}/useXYFlow.d.ts +10 -10
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.mjs +818 -448
  9. package/dist/state/DiagramContext.d.ts +74 -30
  10. package/dist/state/diagram-to-xyflow.d.ts +2 -0
  11. package/dist/state/diagramStore.d.ts +71 -22
  12. package/dist/style.css +58 -37
  13. package/dist/ui/top-left/ChangeAutoLayoutButton.d.ts +3 -0
  14. package/dist/ui/top-left/TopLeftPanel.d.ts +4 -0
  15. package/dist/ui/top-left/_shared.d.ts +5 -0
  16. package/dist/ui/top-left/styles.css.d.ts +3 -0
  17. package/dist/xyflow/XYFlowInner.d.ts +0 -1
  18. package/dist/xyflow/const.d.ts +1 -0
  19. package/dist/xyflow/edges/edges.css.d.ts +1 -1
  20. package/dist/xyflow/{hooks/useLayoutConstraints.d.ts → useLayoutConstraints.d.ts} +1 -1
  21. package/dist/xyflow/utils.d.ts +11 -1
  22. package/package.json +6 -6
  23. package/src/LikeC4Diagram.props.ts +66 -32
  24. package/src/LikeC4Diagram.tsx +19 -12
  25. package/src/StaticLikeC4Diagram.tsx +2 -0
  26. package/src/hooks/index.ts +2 -0
  27. package/src/{state/useDiagramStore.ts → hooks/useDiagramState.ts} +3 -4
  28. package/src/{xyflow/hooks → hooks}/useXYFlow.ts +1 -1
  29. package/src/index.ts +2 -2
  30. package/src/state/DiagramContext.tsx +15 -7
  31. package/src/state/diagram-to-xyflow.ts +17 -3
  32. package/src/state/diagramStore.ts +73 -38
  33. package/src/ui/DiagramTitlePanel.css.ts +2 -2
  34. package/src/ui/DiagramTitlePanel.tsx +62 -44
  35. package/src/ui/DynamicViewWalkthrough.tsx +1 -2
  36. package/src/ui/OptionsPanel.tsx +1 -1
  37. package/src/ui/options/EdgeOptions.tsx +1 -16
  38. package/src/ui/options/NodeOptions.tsx +38 -13
  39. package/src/ui/top-left/ChangeAutoLayoutButton.tsx +131 -0
  40. package/src/ui/top-left/TopLeftPanel.tsx +213 -0
  41. package/src/ui/top-left/_shared.ts +19 -0
  42. package/src/ui/top-left/styles.css.ts +38 -0
  43. package/src/xyflow/FitviewOnDiagramChange.tsx +2 -2
  44. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +87 -59
  45. package/src/xyflow/XYFlow.tsx +11 -10
  46. package/src/xyflow/XYFlowEvents.tsx +44 -18
  47. package/src/xyflow/XYFlowInner.tsx +5 -7
  48. package/src/xyflow/const.ts +1 -0
  49. package/src/xyflow/edges/RelationshipEdge.tsx +60 -27
  50. package/src/xyflow/edges/edges.css.ts +29 -25
  51. package/src/xyflow/nodes/compound/CompoundNode.tsx +1 -1
  52. package/src/xyflow/nodes/element/ElementLink.tsx +1 -2
  53. package/src/xyflow/nodes/element/ElementNode.tsx +7 -5
  54. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
  55. package/src/xyflow/{hooks/useLayoutConstraints.ts → useLayoutConstraints.ts} +3 -3
  56. package/src/xyflow/utils.ts +25 -1
  57. package/dist/state/hooks.d.ts +0 -4
  58. package/dist/state/index.d.ts +0 -2
  59. package/dist/state/useNavigationHistory.d.ts +0 -5
  60. package/dist/types.d.ts +0 -20
  61. package/dist/ui/BackwardForwardPanel.css.d.ts +0 -1
  62. package/dist/ui/BackwardForwardPanel.d.ts +0 -1
  63. package/dist/xyflow/hooks/index.d.ts +0 -2
  64. package/src/state/hooks.ts +0 -5
  65. package/src/state/index.ts +0 -2
  66. package/src/state/useNavigationHistory.ts +0 -39
  67. package/src/types.ts +0 -22
  68. package/src/ui/BackwardForwardPanel.css.ts +0 -22
  69. package/src/ui/BackwardForwardPanel.tsx +0 -76
  70. package/src/xyflow/hooks/index.ts +0 -3
@@ -1,4 +1,4 @@
1
- import type { DiagramNode, DiagramView, Fqn, NodeId, ViewID } from '@likec4/core'
1
+ import type { DiagramNode, DiagramView, Fqn, NodeId, ViewChange, ViewID } from '@likec4/core'
2
2
  import { getBBoxCenter, invariant, nonexhaustive, nonNullable, StepEdgeId } from '@likec4/core'
3
3
  import {
4
4
  applyEdgeChanges,
@@ -16,16 +16,16 @@ import type { ConditionalKeys, Exact, Except, RequiredKeysOf, Simplify } from 't
16
16
  import { devtools, subscribeWithSelector } from 'zustand/middleware'
17
17
  import { shallow } from 'zustand/shallow'
18
18
  import { createWithEqualityFn } from 'zustand/traditional'
19
+ import type { XYStoreApi } from '../hooks/useXYFlow'
19
20
  import type {
20
- Changes,
21
21
  DiagramNodeWithNavigate,
22
22
  ElementIconRenderer,
23
- LikeC4DiagramEventHandlers
23
+ LikeC4DiagramEventHandlers,
24
+ WhereOperator
24
25
  } from '../LikeC4Diagram.props'
25
26
  import { MinZoom } from '../xyflow/const'
26
- import type { XYStoreApi } from '../xyflow/hooks'
27
27
  import type { XYFlowEdge, XYFlowInstance, XYFlowNode } from '../xyflow/types'
28
- import { bezierControlPoints, isSamePoint, toDomPrecision } from '../xyflow/utils'
28
+ import { bezierControlPoints, isInside, isSamePoint, toDomPrecision } from '../xyflow/utils'
29
29
  import { diagramViewToXYFlowData } from './diagram-to-xyflow'
30
30
 
31
31
  type RequiredOrNull<T> = {
@@ -36,6 +36,7 @@ export type DiagramInitialState = {
36
36
  view: DiagramView
37
37
  readonly: boolean
38
38
  showElementLinks: boolean
39
+ showNavigationButtons: boolean
39
40
  fitViewEnabled: boolean
40
41
  fitViewPadding: number
41
42
  zoomable: boolean
@@ -45,6 +46,7 @@ export type DiagramInitialState = {
45
46
  experimentalEdgeEditing: boolean
46
47
  enableFocusMode: boolean
47
48
  renderIcon: ElementIconRenderer | null
49
+ whereFilter: WhereOperator<string, string> | null
48
50
  // If Dynamic View
49
51
  enableDynamicViewWalkthrough: boolean
50
52
 
@@ -108,7 +110,7 @@ export type DiagramState = Simplify<
108
110
  resetLastClicked: () => void
109
111
 
110
112
  getElement(id: Fqn): DiagramNode | null
111
- triggerChangeElementStyle: (change: Changes.ChangeElementStyle) => void
113
+ triggerChangeElementStyle: (change: ViewChange.ChangeElementStyle) => void
112
114
 
113
115
  /**
114
116
  * @returns true if there was pending save layout
@@ -166,6 +168,7 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
166
168
  xynodes,
167
169
  xyedges
168
170
  } = diagramViewToXYFlowData(props.view, {
171
+ where: props.whereFilter,
169
172
  draggable: props.nodesDraggable,
170
173
  selectable: props.nodesSelectable
171
174
  })
@@ -208,6 +211,7 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
208
211
  xyflow,
209
212
  xystore,
210
213
  dimmed,
214
+ whereFilter,
211
215
  view: current,
212
216
  lastOnNavigate,
213
217
  navigationHistory,
@@ -224,11 +228,6 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
224
228
  xynodes
225
229
  } = get()
226
230
 
227
- if (shallowEqual(current, nextView)) {
228
- DEV && console.debug('store: skip updateView')
229
- return
230
- }
231
-
232
231
  if (viewSyncDebounceTimeout !== null) {
233
232
  clearTimeout(viewSyncDebounceTimeout)
234
233
  viewSyncDebounceTimeout = null
@@ -329,14 +328,25 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
329
328
  }
330
329
 
331
330
  const update = diagramViewToXYFlowData(nextView, {
331
+ where: whereFilter,
332
332
  draggable: nodesDraggable,
333
333
  selectable: nodesSelectable
334
334
  })
335
335
 
336
336
  update.xynodes = update.xynodes.map(update => {
337
337
  const existing = xynodes.find(n => n.id === update.id)
338
- if (existing && existing.type === update.type && eq(existing.parentId ?? null, update.parentId ?? null)) {
339
- if (eq(existing.data.element, update.data.element)) {
338
+ if (
339
+ existing
340
+ && existing.type === update.type
341
+ && eq(existing.parentId ?? null, update.parentId ?? null)
342
+ ) {
343
+ if (
344
+ existing.hidden === update.hidden
345
+ && existing.width === update.width
346
+ && existing.height === update.height
347
+ && eq(existing.position, update.position)
348
+ && eq(existing.data.element, update.data.element)
349
+ ) {
340
350
  return existing
341
351
  }
342
352
  return {
@@ -352,21 +362,27 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
352
362
  update.xyedges = update.xyedges.map((update): XYFlowEdge => {
353
363
  const existing = xyedges.find(n => n.id === update.id)
354
364
  if (existing) {
355
- if (eq(existing.data, update.data)) {
365
+ if (
366
+ existing.hidden === update.hidden
367
+ && eq(existing.data.label, update.data.label)
368
+ && eq(existing.data.controlPoints, update.data.controlPoints)
369
+ && eq(existing.data.edge, update.data.edge)
370
+ ) {
356
371
  return existing
357
372
  }
358
373
  return {
359
374
  ...existing,
360
- ...update
375
+ ...update,
376
+ data: {
377
+ ...existing.data,
378
+ ...update.data
379
+ }
361
380
  }
362
381
  }
363
382
  return update
364
383
  })
365
384
  }
366
385
 
367
- const hasChanges = !isSameView || !shallowEqual(update.xynodes, xynodes)
368
- || !shallowEqual(update.xyedges, xyedges)
369
-
370
386
  set(
371
387
  {
372
388
  isDynamicView: nextView.__ === 'dynamic',
@@ -382,8 +398,8 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
382
398
  navigationHistory,
383
399
  navigationHistoryIndex,
384
400
  dimmed,
385
- xynodes: hasChanges ? update.xynodes : xynodes,
386
- xyedges: hasChanges ? update.xyedges : xyedges
401
+ xynodes: !isSameView || !shallowEqual(update.xynodes, xynodes) ? update.xynodes : xynodes,
402
+ xyedges: !isSameView || !shallowEqual(update.xyedges, xyedges) ? update.xyedges : xyedges
387
403
  },
388
404
  noReplace,
389
405
  isSameView ? 'update-view [same]' : 'update-view [another]'
@@ -563,10 +579,6 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
563
579
  clearTimeout(viewSyncDebounceTimeout)
564
580
  set({ viewSyncDebounceTimeout: null })
565
581
  }
566
- if (!onChange) {
567
- DEV && console.debug('ignore triggerSaveManualLayout, as no onChange handler is set')
568
- return
569
- }
570
582
  const { nodeLookup } = xystore.getState()
571
583
  const movedNodes = new StringSet()
572
584
  let bounds = {
@@ -575,6 +587,7 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
575
587
  width: 1,
576
588
  height: 1
577
589
  }
590
+
578
591
  const nodes = reduce([...nodeLookup.values()], (acc, node) => {
579
592
  const dimensions = getNodeDimensions(node)
580
593
  if (!isSamePoint(node.internals.positionAbsolute, node.data.element.position)) {
@@ -589,7 +602,8 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
589
602
  }
590
603
  bounds = getBoundsOfRects(bounds, rect)
591
604
  return acc
592
- }, {} as Changes.SaveManualLayout['layout']['nodes'])
605
+ }, {} as ViewChange.SaveManualLayout['layout']['nodes'])
606
+
593
607
  const edges = reduce(xyflow.getEdges(), (acc, { source, target, data }) => {
594
608
  let controlPoints = data.controlPoints
595
609
  const sourceOrTargetMoved = movedNodes.has(source) || movedNodes.has(target)
@@ -600,7 +614,7 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
600
614
  if (data.edge.points.length === 0 && (!controlPoints || controlPoints.length === 0)) {
601
615
  return acc
602
616
  }
603
- const _updated: Changes.SaveManualLayout['layout']['edges'][string] = acc[data.edge.id] = {
617
+ const _updated: ViewChange.SaveManualLayout['layout']['edges'][string] = acc[data.edge.id] = {
604
618
  points: data.edge.points
605
619
  }
606
620
  if (data.label?.bbox) {
@@ -615,8 +629,16 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
615
629
  if (!sourceOrTargetMoved && data.edge.dotpos) {
616
630
  _updated.dotpos = data.edge.dotpos
617
631
  }
618
- const allX = [...data.edge.points.map(p => p[0]), ...(controlPoints ?? []).map(p => p.x)]
619
- const allY = [...data.edge.points.map(p => p[1]), ...(controlPoints ?? []).map(p => p.y)]
632
+ const allX = [
633
+ ...data.edge.points.map(p => p[0]),
634
+ ...(controlPoints ?? []).map(p => p.x),
635
+ ...(_updated.labelBBox ? [_updated.labelBBox.x, _updated.labelBBox.x + _updated.labelBBox.width] : [])
636
+ ]
637
+ const allY = [
638
+ ...data.edge.points.map(p => p[1]),
639
+ ...(controlPoints ?? []).map(p => p.y),
640
+ ...(_updated.labelBBox ? [_updated.labelBBox.y, _updated.labelBBox.y + _updated.labelBBox.height] : [])
641
+ ]
620
642
  const rect = boxToRect({
621
643
  x: Math.floor(Math.min(...allX)),
622
644
  y: Math.floor(Math.min(...allY)),
@@ -625,14 +647,13 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
625
647
  })
626
648
  bounds = getBoundsOfRects(bounds, rect)
627
649
  return acc
628
- }, {} as Changes.SaveManualLayout['layout']['edges'])
650
+ }, {} as ViewChange.SaveManualLayout['layout']['edges'])
629
651
 
630
- const change: Changes.SaveManualLayout = {
652
+ const change: ViewChange.SaveManualLayout = {
631
653
  op: 'save-manual-layout',
632
654
  layout: {
633
655
  hash: view.hash,
634
656
  autoLayout: view.autoLayout,
635
-
636
657
  nodes,
637
658
  edges,
638
659
  ...bounds
@@ -643,14 +664,24 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
643
664
  console.debug('triggerSaveManualLayout', change)
644
665
  }
645
666
 
646
- onChange({ change })
667
+ // If new view bounds are outside of the diagram bounds, update the view bounds
668
+ // and update edges, as
669
+ if (!isInside(bounds, view.bounds)) {
670
+ set(
671
+ {
672
+ view: {
673
+ ...view,
674
+ bounds
675
+ }
676
+ },
677
+ noReplace,
678
+ 'update view bounds'
679
+ )
680
+ }
681
+ onChange?.({ change })
647
682
  },
648
683
  scheduleSaveManualLayout: () => {
649
- let { viewSyncDebounceTimeout, onChange } = get()
650
- if (!onChange) {
651
- DEV && console.debug('ignore triggerSaveManualLayout, as no onChange handler is set')
652
- return
653
- }
684
+ let { viewSyncDebounceTimeout } = get()
654
685
  if (viewSyncDebounceTimeout) {
655
686
  clearTimeout(viewSyncDebounceTimeout)
656
687
  }
@@ -666,7 +697,7 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
666
697
  },
667
698
 
668
699
  triggerOnNavigateTo: (xynodeId, event) => {
669
- const { view, xynodes, onNavigateTo } = get()
700
+ const { view, xynodes, onNavigateTo, cancelSaveManualLayout } = get()
670
701
  if (!onNavigateTo) {
671
702
  return
672
703
  }
@@ -674,6 +705,7 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
674
705
  invariant(xynode, `xynode not found: ${xynodeId}`)
675
706
  const element = view.nodes.find(({ id }) => id === xynodeId)
676
707
  invariant(element?.navigateTo, `node is not navigable: ${xynodeId}`)
708
+ cancelSaveManualLayout()
677
709
  set(
678
710
  {
679
711
  lastClickedNodeId: xynodeId,
@@ -857,3 +889,6 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
857
889
  shallow
858
890
  )
859
891
  }
892
+
893
+ export type DiagramZustandStore = ReturnType<typeof createDiagramStore>
894
+ export type DiagramStoreApi = Readonly<Pick<DiagramZustandStore, 'getState' | 'setState' | 'subscribe'>>
@@ -18,11 +18,11 @@ export const card = style({
18
18
  backdropFilter: fallbackVar(vars.safariAnimationHook, 'blur(8px)'),
19
19
  '@media': {
20
20
  [mantine.largerThan('sm')]: {
21
- minWidth: 300,
21
+ minWidth: 250,
22
22
  maxWidth: '90vw'
23
23
  },
24
24
  [mantine.largerThan('md')]: {
25
- minWidth: 400,
25
+ minWidth: 350,
26
26
  maxWidth: '70vw'
27
27
  }
28
28
  },
@@ -3,7 +3,7 @@ import { useLocalStorage } from '@mantine/hooks'
3
3
  import { IconMenu } from '@tabler/icons-react'
4
4
  import clsx from 'clsx'
5
5
  import { AnimatePresence, m } from 'framer-motion'
6
- import { type DiagramState, useDiagramState } from '../state/hooks'
6
+ import { type DiagramState, useDiagramState } from '../hooks'
7
7
  import * as css from './DiagramTitlePanel.css'
8
8
 
9
9
  const selector = (s: DiagramState) => ({
@@ -40,52 +40,70 @@ export default function DiagramTitlePanel() {
40
40
  radius="sm"
41
41
  className={css.card}
42
42
  withBorder
43
- p={'md'}
44
- pb={isCollapsed ? 'sm' : 'md'}
43
+ p={isCollapsed ? 'sm' : 'md'}
45
44
  onDoubleClick={e => e.stopPropagation()}>
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>
45
+ {isCollapsed && (
46
+ <CardSection>
47
+ <Button
48
+ fullWidth
49
+ justify="stretch"
50
+ size="sm"
51
+ py={2}
52
+ radius={'0'}
53
+ variant={'subtle'}
54
+ color="dark"
55
+ onClick={toggle}
56
+ >
57
+ <Text inline fw={500} fz={'xs'} c="gray">
58
+ {title}
59
+ </Text>
60
+ </Button>
61
+ </CardSection>
62
+ )}
87
63
  {!isCollapsed && (
88
64
  <>
65
+ <CardSection mb={'sm'}>
66
+ <Button
67
+ fullWidth
68
+ size="xs"
69
+ h={'sm'}
70
+ py={2}
71
+ radius={'0'}
72
+ variant={'subtle'}
73
+ color="gray"
74
+ onClick={toggle}
75
+ >
76
+ <IconMenu size={11} opacity={0.7} />
77
+ </Button>
78
+ </CardSection>
79
+
80
+ <Group justify="stretch" wrap="nowrap" mb={'sm'}>
81
+ <Text
82
+ component={'div'}
83
+ flex={'1'}
84
+ size={'md'}
85
+ fw={500}
86
+ lh={1.1}
87
+ className={css.title}
88
+ >
89
+ {title}
90
+ </Text>
91
+ <Text
92
+ hidden={isCollapsed}
93
+ component={'div'}
94
+ flex={'0 0 auto'}
95
+ inline
96
+ size="xs"
97
+ fz={9}
98
+ fw={500}
99
+ c={'dimmed'}
100
+ style={{
101
+ userSelect: 'all'
102
+ }}>
103
+ <span style={{ userSelect: 'none' }}>id:{' '}</span>
104
+ {id}
105
+ </Text>
106
+ </Group>
89
107
  {description && (
90
108
  <Spoiler
91
109
  maxHeight={42}
@@ -8,8 +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/hooks'
12
- import { useXYStore } from '../xyflow/hooks/useXYFlow'
11
+ import { useDiagramState, useXYStore } from '../hooks'
13
12
  import * as css from './DynamicViewWalkthrough.css'
14
13
 
15
14
  export function DynamicViewWalkthrough() {
@@ -4,7 +4,7 @@ import clsx from 'clsx'
4
4
  import { deepEqual as eq } from 'fast-equals'
5
5
  import { AnimatePresence, m } from 'framer-motion'
6
6
  import { useState } from 'react'
7
- import { useDiagramState } from '../state/hooks'
7
+ import { useDiagramState } from '../hooks/useDiagramState'
8
8
  import { NodeOptions } from './options/NodeOptions'
9
9
  import * as styles from './OptionsPanel.css'
10
10
 
@@ -3,8 +3,7 @@ import { CheckIcon, ColorSwatch, Divider, Flex, rem, Stack, Text, Tooltip, Toolt
3
3
  import { shallowEqual } from 'fast-equals'
4
4
  import { memo } from 'react'
5
5
  import { hasAtLeast, keys, takeWhile } from 'remeda'
6
- import { useDiagramStoreApi } from '../../state/hooks'
7
- import { useXYEdgesData } from '../../xyflow/hooks/useXYFlow'
6
+ import { useDiagramStoreApi, useXYEdgesData } from '../../hooks'
8
7
  import { type XYFlowEdge } from '../../xyflow/types'
9
8
 
10
9
  const {
@@ -41,20 +40,6 @@ export const EdgeOptions = memo<{ selectedEdgeIds: string[] }>(({ selectedEdgeId
41
40
  }
42
41
  const [firstEdge, ...rest] = edges
43
42
 
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
43
  return (
59
44
  <Stack gap={'xs'}>
60
45
  <div>
@@ -5,14 +5,17 @@ import {
5
5
  ElementShapes,
6
6
  invariant,
7
7
  type NonEmptyArray,
8
- type ThemeColor
8
+ type ThemeColor,
9
+ type ViewChange
9
10
  } from '@likec4/core'
10
11
  import {
12
+ ActionIcon,
11
13
  Box,
12
14
  CheckIcon,
13
15
  ColorSwatch,
14
16
  Divider,
15
17
  Flex,
18
+ Group,
16
19
  rem,
17
20
  SegmentedControl,
18
21
  Select,
@@ -23,13 +26,12 @@ import {
23
26
  Tooltip,
24
27
  TooltipGroup
25
28
  } from '@mantine/core'
29
+ import { IconFileSymlink } from '@tabler/icons-react'
26
30
  import { shallowEqual } from 'fast-equals'
27
31
  import { memo, useState } from 'react'
28
32
  import { hasAtLeast, keys, takeWhile } from 'remeda'
33
+ import { useDiagramStoreApi, useXYNodesData } from '../../hooks'
29
34
  import { useUpdateEffect } from '../../hooks/useUpdateEffect'
30
- import type { Changes } from '../../LikeC4Diagram.props'
31
- import { useDiagramStoreApi } from '../../state/hooks'
32
- import { useXYNodesData } from '../../xyflow/hooks'
33
35
  import { XYFlowNode } from '../../xyflow/types'
34
36
 
35
37
  const {
@@ -71,11 +73,13 @@ export const NodeOptions = memo<{ selectedNodeIds: string[] }>(({ selectedNodeId
71
73
 
72
74
  const [firstNode, ...rest] = nodes
73
75
 
76
+ const showGoToSource = rest.length === 0 && diagramApi.getState().onOpenSourceElement!!
77
+
74
78
  // Makes sense to show opacity option only if there is at least one compound node
75
79
  const showOpacityOption = firstNode.type === 'compound'
76
80
  && (rest.length === 0 || rest.every(node => node.type === 'compound'))
77
81
 
78
- const triggerChange = (style: Changes.ChangeElementStyle['style']) => {
82
+ const triggerChange = (style: ViewChange.ChangeElementStyle['style']) => {
79
83
  const targets = nodes.map(node => node.data.element.id)
80
84
  invariant(hasAtLeast(targets, 1), 'At least one target is required')
81
85
  diagramApi.getState().triggerChangeElementStyle({
@@ -87,14 +91,35 @@ export const NodeOptions = memo<{ selectedNodeIds: string[] }>(({ selectedNodeId
87
91
 
88
92
  return (
89
93
  <Stack gap={'xs'}>
90
- <div>
91
- <Text fz={rem(9)} fw={'500'} c={'dimmed'}>
92
- ELEMENT{rest.length > 0 ? 'S' : ``}
93
- </Text>
94
- <Text size="xs" c={rest.length > 0 ? 'dimmed' : ''} truncate>
95
- {rest.length === 0 ? firstNode.data.element.title : `[ multiple ]`}
96
- </Text>
97
- </div>
94
+ <Group wrap="nowrap" align="flex-start">
95
+ <Box flex={1}>
96
+ <Text fz={rem(9)} fw={'500'} c={'dimmed'}>
97
+ ELEMENT{rest.length > 0 ? 'S' : ``}
98
+ </Text>
99
+ <Text size="xs" c={rest.length > 0 ? 'dimmed' : ''} truncate>
100
+ {rest.length === 0 ? firstNode.data.element.title : `[ multiple ]`}
101
+ </Text>
102
+ </Box>
103
+ {showGoToSource && (
104
+ <Box flex={0}>
105
+ <ActionIcon
106
+ size={'sm'}
107
+ variant="light"
108
+ color="gray"
109
+ onClick={e => {
110
+ e.stopPropagation()
111
+ diagramApi.getState().onOpenSourceElement?.(firstNode.data.element.id)
112
+ }}>
113
+ <IconFileSymlink
114
+ stroke={1.2}
115
+ style={{
116
+ width: '70%',
117
+ height: '70%'
118
+ }} />
119
+ </ActionIcon>
120
+ </Box>
121
+ )}
122
+ </Group>
98
123
  {showShapeOption && (
99
124
  <ShapeOption
100
125
  nodes={nodes}