@likec4/diagram 1.17.1 → 1.18.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 (49) hide show
  1. package/package.json +17 -15
  2. package/src/LikeC4Diagram.props.ts +21 -12
  3. package/src/LikeC4Diagram.tsx +5 -7
  4. package/src/LikeC4Search.tsx +54 -51
  5. package/src/hooks/useDiagramState.ts +8 -3
  6. package/src/hooks/useXYFlow.ts +2 -1
  7. package/src/likec4model/useLikeC4ElementsTree.ts +6 -13
  8. package/src/likec4model/useLikeC4Model.ts +21 -11
  9. package/src/overlays/Overlays.tsx +96 -63
  10. package/src/overlays/edge-details/EdgeDetailsXYFlow.tsx +3 -1
  11. package/src/overlays/edge-details/_types.ts +7 -3
  12. package/src/overlays/edge-details/use-layouted-edge-details.ts +21 -14
  13. package/src/overlays/edge-details/xyflow/ElementNode.tsx +4 -2
  14. package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +4 -4
  15. package/src/overlays/element-details/ElementDetailsCard.tsx +91 -78
  16. package/src/overlays/element-details/TabPanelDeployments.css.ts +69 -0
  17. package/src/overlays/element-details/TabPanelDeployments.tsx +217 -0
  18. package/src/overlays/element-details/TabPanelRelationships.tsx +8 -9
  19. package/src/overlays/element-details/TabPanelStructure.tsx +10 -10
  20. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +1 -1
  21. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +3 -11
  22. package/src/overlays/relationships-of/SelectElement.tsx +2 -2
  23. package/src/overlays/relationships-of/_types.ts +2 -2
  24. package/src/overlays/relationships-of/use-layouted-relationships.ts +18 -19
  25. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +1 -3
  26. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +12 -6
  27. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +0 -1
  28. package/src/state/diagramStore.ts +28 -11
  29. package/src/ui/DiagramTitlePanel.tsx +2 -29
  30. package/src/ui/Link.tsx +40 -0
  31. package/src/ui/notation/NotationPanel.tsx +4 -2
  32. package/src/ui/top-left/ChangeAutoLayoutButton.tsx +2 -2
  33. package/src/ui/top-left/TopLeftPanel.tsx +5 -5
  34. package/src/xyflow/edges/EdgeLabel.tsx +4 -4
  35. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +20 -7
  36. package/src/xyflow/nodes/compound/CompoundNode.css.ts +28 -13
  37. package/src/xyflow/nodes/compound/CompoundNode.tsx +51 -19
  38. package/src/xyflow/nodes/element/ElementNode.tsx +147 -124
  39. package/src/xyflow/nodes/element/element.css.ts +52 -19
  40. package/src/xyflow/nodes/shared/ElementIcon.tsx +1 -1
  41. package/src/xyflow/nodes/shared/Toolbar.tsx +24 -8
  42. package/src/xyflow/useLayoutConstraints.ts +1 -1
  43. package/src/xyflow/useXYFlowEvents.ts +18 -5
  44. package/src/ui/OptionsPanel.css.ts +0 -9
  45. package/src/ui/OptionsPanel.tsx +0 -70
  46. package/src/ui/options/EdgeOptions.tsx +0 -142
  47. package/src/ui/options/NodeOptions.tsx +0 -362
  48. package/src/xyflow/nodes/element/ElementDetails.css.ts +0 -101
  49. package/src/xyflow/nodes/element/ElementDetails.tsx +0 -393
@@ -7,27 +7,27 @@ import * as css from './TabPanelStructure.css'
7
7
  interface ElementTreeNodeData {
8
8
  label: ReactNode
9
9
  value: string
10
- element: LikeC4Model.ElementModel
10
+ element: LikeC4Model.Element
11
11
  type: 'ancestor' | 'current' | 'descedant'
12
12
  children: TreeNodeData[]
13
13
  }
14
14
 
15
- interface MessageNodeData {
15
+ export interface MessageNodeData {
16
16
  label: ReactNode
17
17
  value: string
18
18
  type: 'message'
19
19
  children: TreeNodeData[]
20
20
  }
21
21
 
22
- type TreeNodeData = ElementTreeNodeData | MessageNodeData
22
+ export type TreeNodeData = ElementTreeNodeData | MessageNodeData
23
23
 
24
- type TabElementStructureProps = {
25
- element: LikeC4Model.ElementModel
24
+ type TabPanelStructureProps = {
25
+ element: LikeC4Model.Element
26
26
  }
27
27
 
28
- const ElementLabel = ({
28
+ export const ElementLabel = ({
29
29
  element
30
- }: { element: LikeC4Model.ElementModel; type: 'ancestor' | 'current' | 'descedant' }) => (
30
+ }: { element: LikeC4Model.Element; type: 'ancestor' | 'current' | 'descedant' }) => (
31
31
  <Box className={css.elementLabel}>
32
32
  <Text component="div" fz={'sm'} fw={'500'}>{element.title}</Text>
33
33
  </Box>
@@ -38,9 +38,9 @@ const ElementLabel = ({
38
38
  // }: RenderTreeNodePayload) => {
39
39
  // }
40
40
 
41
- export function TabElementStructure({
41
+ export function TabPanelStructure({
42
42
  element
43
- }: TabElementStructureProps) {
43
+ }: TabPanelStructureProps) {
44
44
  const tree = useTree({
45
45
  multiple: false
46
46
  })
@@ -60,7 +60,7 @@ export function TabElementStructure({
60
60
  value: element.id,
61
61
  element,
62
62
  type: 'current',
63
- children: element.children().map((child): TreeNodeData => ({
63
+ children: [...element.children()].map((child): TreeNodeData => ({
64
64
  label: <ElementLabel type="descedant" element={child} />,
65
65
  value: child.id,
66
66
  element: child,
@@ -93,7 +93,7 @@ export const RelationshipsOverlay = memo<{ subjectId: Fqn }>(function Relationsh
93
93
  <Box flex={'1 0 auto'}>
94
94
  <SelectElement
95
95
  scope={scope}
96
- subject={subject.element}
96
+ subject={subject.$element}
97
97
  onSelect={fqn =>
98
98
  overlay.openOverlay({
99
99
  relationshipsOf: fqn
@@ -1,13 +1,8 @@
1
1
  import { delay, type DiagramView, type Fqn, isAncestor, type Relation } from '@likec4/core'
2
- import { Box, Button, Group, SegmentedControl, Space, Text } from '@mantine/core'
3
- import { useId, useLocalStorage, useStateHistory } from '@mantine/hooks'
2
+ import { useId } from '@mantine/hooks'
4
3
  import { useDebouncedCallback, useDeepCompareEffect, useSyncedRef } from '@react-hookz/web'
5
- import { IconArrowLeft, IconArrowRight } from '@tabler/icons-react'
6
4
  import {
7
- Background,
8
- BackgroundVariant,
9
5
  getViewportForBounds,
10
- Panel,
11
6
  ReactFlow,
12
7
  type ReactFlowInstance,
13
8
  type ReactFlowProps,
@@ -19,16 +14,13 @@ import {
19
14
  useStoreApi
20
15
  } from '@xyflow/react'
21
16
  import { getNodeDimensions } from '@xyflow/system'
22
- import { memo, type PropsWithChildren, useEffect, useRef, useState } from 'react'
17
+ import { type PropsWithChildren, useRef, useState } from 'react'
23
18
  import { isNullish, map, omit, only, prop, setPath, unique } from 'remeda'
24
19
  import { useUpdateEffect } from '../../hooks'
25
- import { useDiagramStoreApi } from '../../hooks/useDiagramState'
26
20
  import { centerXYInternalNode } from '../../xyflow/utils'
27
- import { useOverlayDialog } from '../OverlayContext'
28
21
  import { cssReactflowMarker } from '../Overlays.css'
29
22
  import type { XYFlowTypes } from './_types'
30
- import { SelectElement } from './SelectElement'
31
- import { useLayoutedRelationships, ZIndexes } from './use-layouted-relationships'
23
+ import { ZIndexes } from './use-layouted-relationships'
32
24
  import { CompoundNode } from './xyflow/CompoundNode'
33
25
  import { ElementNode } from './xyflow/ElementNode'
34
26
  import { EmptyNode } from './xyflow/EmptyNode'
@@ -1,4 +1,4 @@
1
- import { ancestorsFqn, type Element, type Fqn, type ViewID } from '@likec4/core'
1
+ import { ancestorsFqn, type Element, type Fqn, type ViewId } from '@likec4/core'
2
2
  import {
3
3
  ActionIcon,
4
4
  Box,
@@ -25,7 +25,7 @@ export const SelectElement = ({
25
25
  onSelect
26
26
  }: {
27
27
  subject: Element
28
- viewId: ViewID
28
+ viewId: ViewId
29
29
  scope: 'global' | 'view'
30
30
  onSelect: (id: Fqn) => void
31
31
  }) => {
@@ -1,4 +1,4 @@
1
- import type { ComputedNode, Fqn, Relation, ViewID } from '@likec4/core'
1
+ import type { ComputedNode, Fqn, Relation, ViewId } from '@likec4/core'
2
2
  import type {
3
3
  Edge as ReactFlowEdge,
4
4
  InternalNode as ReactFlowInternalNode,
@@ -26,7 +26,7 @@ export namespace XYFlowTypes {
26
26
  left: Port[]
27
27
  right: Port[]
28
28
  }
29
- navigateTo: ViewID | null
29
+ navigateTo: ViewId | null
30
30
  hovered?: boolean
31
31
  layoutId?: string
32
32
  leaving?: boolean
@@ -11,7 +11,7 @@ import {
11
11
  type LikeC4Model,
12
12
  nonNullable,
13
13
  parentFqn,
14
- type RelationID
14
+ type RelationId
15
15
  } from '@likec4/core'
16
16
  import { MarkerType } from '@xyflow/system'
17
17
  import { useMemo } from 'react'
@@ -118,15 +118,12 @@ function createGraph() {
118
118
  return g
119
119
  }
120
120
 
121
- type LikeC4ModelElement = LikeC4Model.ElementModel
122
- // type LikeC4ModelElement = LikeC4ViewModel.Element
123
-
124
121
  type Context = {
125
122
  scope: 'global' | 'view'
126
123
  g: dagre.graphlib.Graph
127
124
  subjectId: Fqn
128
125
  diagramNodes: Map<Fqn, DiagramNode>
129
- subjectElement: LikeC4ModelElement
126
+ subjectElement: LikeC4Model.Element
130
127
  // likec4model: LikeC4Model
131
128
  // subject: XYFlowTypes.ElementNode
132
129
  // Model
@@ -157,7 +154,7 @@ const graphId = (node: XYFlowTypes.Node) => ({
157
154
  })
158
155
 
159
156
  function nodeData(
160
- element: LikeC4Model.ElementModel,
157
+ element: LikeC4Model.Element,
161
158
  ctx: Context
162
159
  ): Omit<XYFlowTypes.NonEmptyNode['data'], 'column'> {
163
160
  // We try to inherit style from existing diagram node
@@ -167,7 +164,7 @@ function nodeData(
167
164
  // color from it if there is no diagram node
168
165
  const ancestor = diagramNode ?? (ctx.scope === 'view'
169
166
  ? pipe(
170
- element.ancestors(),
167
+ element.ancestors().toArray(),
171
168
  map(ancestor => ctx.diagramNodes.get(ancestor.id)),
172
169
  filter(isTruthy),
173
170
  first()
@@ -180,11 +177,11 @@ function nodeData(
180
177
  element: {
181
178
  kind: element.kind,
182
179
  title: diagramNode?.title ?? element.title,
183
- description: diagramNode?.description ?? element.element.description,
180
+ description: diagramNode?.description ?? element.description,
184
181
  color: diagramNode?.color ?? ancestor?.color ?? element.color,
185
182
  shape: diagramNode?.shape ?? element.shape
186
183
  },
187
- navigateTo: diagramNode?.navigateTo ?? first(element.viewsOf())?.id ?? null,
184
+ navigateTo: diagramNode?.navigateTo ?? element.defaultView?.id ?? null,
188
185
  ports: {
189
186
  left: [],
190
187
  right: []
@@ -228,7 +225,7 @@ function createEmptyNode(
228
225
  function createNode(
229
226
  column: ColumnKey,
230
227
  nodeType: Exclude<XYFlowTypes.Node['type'], 'empty'>,
231
- element: LikeC4Model.ElementModel,
228
+ element: LikeC4Model.Element,
232
229
  ctx: Context,
233
230
  depth: number = 0
234
231
  ): XYFlowTypes.ElementNode | XYFlowTypes.CompoundNode {
@@ -245,7 +242,7 @@ function createNode(
245
242
 
246
243
  // Create parent node
247
244
  const parent = pipe(
248
- element.ancestors(),
245
+ element.ancestors().toArray(),
249
246
  takeWhile(ancestor => !isAncestor(ancestor.id, ctx.subjectId)),
250
247
  find(ancestor =>
251
248
  ctx.diagramNodes.has(ancestor.id) || ctx.connected[column].has(ancestor.id)
@@ -363,7 +360,7 @@ function layout(
363
360
  ): {
364
361
  viewIncludesSubject: boolean
365
362
  notIncludedRelations: number
366
- subject: LikeC4ModelElement
363
+ subject: LikeC4Model.Element
367
364
  nodes: XYFlowTypes.Node[]
368
365
  edges: XYFlowTypes.Edge[]
369
366
  bounds: {
@@ -381,7 +378,7 @@ function layout(
381
378
  view.edges.flatMap(e => e.relations)
382
379
  )
383
380
  // Relations that are not included in the view
384
- const notIncludedRelations = new Set<RelationID>()
381
+ const notIncludedRelations = new Set<RelationId>()
385
382
 
386
383
  if (!viewIncludesSubject) {
387
384
  // Reset scope to global if subject is not in the view
@@ -401,16 +398,18 @@ function layout(
401
398
 
402
399
  if (scope === 'global') {
403
400
  relationships = {
404
- incoming: subjectElement.incoming().map(r => r.relationship),
405
- outgoing: subjectElement.outgoing().map(r => r.relationship)
401
+ incoming: subjectElement.incoming().map(r => r.$relationship).toArray(),
402
+ outgoing: subjectElement.outgoing().map(r => r.$relationship).toArray()
406
403
  }
407
404
  } else {
408
- const subjectViewModel = likec4model.view(view.id).element(subjectId)
405
+ const subjectViewModel = likec4model.view(view.id).node(subjectId)
409
406
  relationships = {
410
407
  incoming: subjectViewModel.incoming()
411
- .flatMap(c => c.relationships().map(r => r.relationship)),
408
+ .flatMap(c => c.relationships('model').map(r => r.$relationship))
409
+ .toArray(),
412
410
  outgoing: subjectViewModel.outgoing()
413
- .flatMap(c => c.relationships().map(r => r.relationship))
411
+ .flatMap(c => c.relationships('model').map(r => r.$relationship))
412
+ .toArray()
414
413
  }
415
414
  }
416
415
 
@@ -445,7 +444,7 @@ function layout(
445
444
  })
446
445
 
447
446
  if (viewIncludesSubject) {
448
- const subjectViewModel = likec4model.view(view.id).element(subjectId)
447
+ const subjectViewModel = likec4model.view(view.id).node(subjectId)
449
448
  subjectViewModel.incomers().forEach(incomer => {
450
449
  ctx.connected.incomers.add(incomer.id)
451
450
  })
@@ -86,14 +86,12 @@ export const CompoundNode = memo<CompoundNodeProps>(({
86
86
  {...(selectable && {
87
87
  whileHover: {
88
88
  ...scale(12),
89
- scaleX: (width + 12) / width,
90
- scaleY: (height + 12) / height,
91
89
  transition: {
92
90
  delay: 0.1
93
91
  }
94
92
  },
95
93
  whileTap: {
96
- ...scale(-12)
94
+ ...scale(-8)
97
95
  }
98
96
  })}
99
97
  >
@@ -30,7 +30,7 @@ function selector(s: DiagramState) {
30
30
  return {
31
31
  currentViewId: s.view.id,
32
32
  onNavigateTo: s.onNavigateTo,
33
- onOpenSource: s.onOpenSourceElement
33
+ onOpenSource: s.onOpenSource
34
34
  }
35
35
  }
36
36
 
@@ -56,10 +56,14 @@ export const ElementNode = memo<ElementNodeProps>(({
56
56
  onOpenSource
57
57
  } = useDiagramState(selector)
58
58
 
59
- const scale = (diff: number) => ({
60
- scaleX: (w + diff) / w,
61
- scaleY: (h + diff) / h
62
- })
59
+ const maxWH = Math.max(w, h)
60
+ const scale = (diff: number) => {
61
+ const s = (maxWH + diff) / maxWH
62
+ return ({
63
+ scaleX: s,
64
+ scaleY: s
65
+ })
66
+ }
63
67
 
64
68
  let opacity = 1
65
69
  if (data.dimmed) {
@@ -155,7 +159,9 @@ export const ElementNode = memo<ElementNodeProps>(({
155
159
  <Action
156
160
  onClick={(event) => {
157
161
  event.stopPropagation()
158
- onOpenSource(data.fqn)
162
+ onOpenSource?.({
163
+ element: data.fqn
164
+ })
159
165
  }}>
160
166
  <IconFileSymlink stroke={1.8} style={{ width: '72%' }} />
161
167
  </Action>
@@ -2,7 +2,6 @@ import { ActionIcon, Box, Group, Stack, Text, ThemeIcon, Tooltip as MantineToolt
2
2
  import { IconBoxMultipleFilled, IconZoomScan } from '@tabler/icons-react'
3
3
  import { BaseEdge, EdgeLabelRenderer, type EdgeProps, getBezierPath } from '@xyflow/react'
4
4
  import clsx from 'clsx'
5
- import { useEffect, useRef } from 'react'
6
5
  import { only } from 'remeda'
7
6
  import { useDiagramState } from '../../../hooks/useDiagramState'
8
7
  import { stopPropagation } from '../../../xyflow/utils'
@@ -1,13 +1,13 @@
1
1
  import type {
2
+ ComputedNode,
2
3
  DiagramNode,
3
4
  DiagramView,
4
5
  EdgeId,
5
- ElementKind,
6
6
  ElementNotation,
7
7
  Fqn,
8
8
  NodeId,
9
9
  ViewChange,
10
- ViewID,
10
+ ViewId,
11
11
  XYPoint
12
12
  } from '@likec4/core'
13
13
  import {
@@ -17,17 +17,16 @@ import {
17
17
  isStepEdgeId,
18
18
  nonexhaustive,
19
19
  nonNullable,
20
- StepEdgeId
20
+ type StepEdgeId
21
21
  } from '@likec4/core'
22
22
  import {
23
23
  applyEdgeChanges,
24
24
  applyNodeChanges,
25
25
  getViewportForBounds,
26
- type InternalNode,
27
26
  type OnEdgesChange,
28
27
  type OnNodesChange
29
28
  } from '@xyflow/react'
30
- import { boxToRect, getBoundsOfRects, getNodeDimensions, type XYPosition } from '@xyflow/system'
29
+ import { boxToRect, getBoundsOfRects, getNodeDimensions } from '@xyflow/system'
31
30
  import { DEV } from 'esm-env'
32
31
  import { deepEqual as eq, shallowEqual } from 'fast-equals'
33
32
  import type { MouseEvent as ReactMouseEvent } from 'react'
@@ -46,7 +45,6 @@ import type {
46
45
  import { type Vector, vector } from '../utils/vector'
47
46
  import { MinZoom } from '../xyflow/const'
48
47
  import type { XYFlowEdge, XYFlowInstance, XYFlowNode } from '../xyflow/types'
49
- import { createLayoutConstraints } from '../xyflow/useLayoutConstraints'
50
48
  import { bezierControlPoints, isInside, isSamePoint, toDomPrecision } from '../xyflow/utils'
51
49
  import { diagramViewToXYFlowData } from './diagram-to-xyflow'
52
50
  import { align, type AlignmentMode } from './diagramStore.layout'
@@ -87,6 +85,8 @@ export type DiagramInitialState = {
87
85
  getContainer: () => HTMLDivElement | null
88
86
  } & RequiredOrNull<LikeC4DiagramEventHandlers>
89
87
 
88
+ type NodeKind = ComputedNode['kind']
89
+
90
90
  const StringSet = Set<string>
91
91
 
92
92
  export type DiagramState = Simplify<
@@ -132,12 +132,12 @@ export type DiagramState = Simplify<
132
132
  dimmed: ReadonlySet<string>
133
133
 
134
134
  lastOnNavigate: null | {
135
- fromView: ViewID
136
- toView: ViewID
135
+ fromView: ViewId
136
+ toView: ViewId
137
137
  fromNode: NodeId | null
138
138
  }
139
139
  navigationHistory: Array<{
140
- viewId: ViewID
140
+ viewId: ViewId
141
141
  nodeId: NodeId | null
142
142
  }>
143
143
  navigationHistoryIndex: number
@@ -186,10 +186,12 @@ export type DiagramState = Simplify<
186
186
  onNodesChange: OnNodesChange<XYFlowNode>
187
187
  onEdgesChange: OnEdgesChange<XYFlowEdge>
188
188
 
189
- highlightByElementNotation: (notation: ElementNotation, onlyOfKind?: ElementKind) => void
189
+ highlightByElementNotation: (notation: ElementNotation, onlyOfKind?: NodeKind) => void
190
190
 
191
191
  resetEdgeControlPoints: () => void
192
192
  align: (mode: AlignmentMode) => void
193
+
194
+ onOpenSourceView: () => void
193
195
  }
194
196
  >
195
197
 
@@ -850,10 +852,25 @@ export function createDiagramStore(props: DiagramInitialState) {
850
852
  }
851
853
  },
852
854
 
855
+ onOpenSourceView: () => {
856
+ const { view, onOpenSource } = get()
857
+ onOpenSource?.({
858
+ view: view.id
859
+ })
860
+ },
861
+
853
862
  openOverlay: (overlay) => {
854
863
  if (eq(overlay, get().activeOverlay)) {
855
864
  return
856
865
  }
866
+ if ('elementDetails' in overlay) {
867
+ const diagramNode = get().view.nodes.find(({ id }) => id === overlay.elementDetails)
868
+ if (!diagramNode || !diagramNode.modelRef) {
869
+ get().closeOverlay()
870
+ return
871
+ }
872
+ }
873
+
857
874
  set(
858
875
  {
859
876
  activeWalkthrough: null,
@@ -992,7 +1009,7 @@ export function createDiagramStore(props: DiagramInitialState) {
992
1009
  }
993
1010
  },
994
1011
 
995
- highlightByElementNotation: (notation: ElementNotation, onlyOfKind?: ElementKind) => {
1012
+ highlightByElementNotation: (notation: ElementNotation, onlyOfKind?: NodeKind) => {
996
1013
  const { xynodes, xyedges } = get()
997
1014
  const dimmed = new StringSet(map(xyedges, prop('id')))
998
1015
  xynodes.forEach(({ id, data }) => {
@@ -5,6 +5,7 @@ import clsx from 'clsx'
5
5
  import { AnimatePresence, m } from 'framer-motion'
6
6
  import { type DiagramState, useDiagramState } from '../hooks'
7
7
  import * as css from './DiagramTitlePanel.css'
8
+ import { Link } from './Link'
8
9
 
9
10
  const selector = (s: DiagramState) => ({
10
11
  id: s.view.id,
@@ -151,35 +152,7 @@ export default function DiagramTitlePanel() {
151
152
  gap={3}
152
153
  justify="stretch"
153
154
  align="stretch">
154
- {links.map((link) => (
155
- <Group key={link.url} wrap="nowrap" align="center" gap={'sm'}>
156
- <Box flex={'1'} style={{ overflow: 'hidden' }}>
157
- <Anchor
158
- href={link.url}
159
- target="_blank"
160
- fz="xs"
161
- truncate="end"
162
- display={'inline-block'}
163
- w={'100%'}>
164
- {link.title || link.url}
165
- </Anchor>
166
- </Box>
167
- <CopyButton value={link.url}>
168
- {({ copied, copy }) => (
169
- <Button
170
- size="compact-xs"
171
- fz={'10'}
172
- variant="light"
173
- onClick={copy}
174
- color={copied
175
- ? 'teal'
176
- : 'gray'}>
177
- {copied ? 'copied' : 'copy'}
178
- </Button>
179
- )}
180
- </CopyButton>
181
- </Group>
182
- ))}
155
+ {links.map((link) => <Link link={link} />)}
183
156
  </Stack>
184
157
  )}
185
158
  </>
@@ -0,0 +1,40 @@
1
+ import type { Link } from '@likec4/core'
2
+ import { Anchor, Box, Button, CopyButton, Group } from '@mantine/core'
3
+
4
+ export function Link({ link }: { link: Link }) {
5
+ return (
6
+ <>
7
+ <Group key={link.url} wrap="nowrap" align="center" gap={'sm'}>
8
+ <Box flex={'1'} style={{ overflow: 'hidden' }}>
9
+ <Anchor
10
+ href={link.url}
11
+ target="_blank"
12
+ fz="xs"
13
+ truncate="end"
14
+ display={'inline-block'}
15
+ w={'100%'}
16
+ onClick={(e) => e.stopPropagation()}>
17
+ {link.title || link.url}
18
+ </Anchor>
19
+ </Box>
20
+ <CopyButton value={link.url}>
21
+ {({ copied, copy }) => (
22
+ <Button
23
+ size="compact-xs"
24
+ fz={'10'}
25
+ variant="light"
26
+ onClick={(e) => {
27
+ e.stopPropagation()
28
+ copy()
29
+ }}
30
+ color={copied
31
+ ? 'teal'
32
+ : 'gray'}>
33
+ {copied ? 'copied' : 'copy'}
34
+ </Button>
35
+ )}
36
+ </CopyButton>
37
+ </Group>
38
+ </>
39
+ )
40
+ }
@@ -1,4 +1,4 @@
1
- import { type ElementKind, type ElementNotation as ElementNotationData } from '@likec4/core'
1
+ import { type ComputedNode, type ElementNotation as ElementNotationData } from '@likec4/core'
2
2
  import {
3
3
  ActionIcon,
4
4
  Badge,
@@ -29,13 +29,15 @@ import { vars } from '../../theme-vars'
29
29
  import { ElementShapeSvg } from '../../xyflow/nodes/element/ElementShapeSvg'
30
30
  import * as css from './NotationPanel.css'
31
31
 
32
+ type NodeKind = ComputedNode['kind']
33
+
32
34
  const ElementNotation = ({ value }: { value: ElementNotationData }) => {
33
35
  const {
34
36
  title,
35
37
  color = 'primary',
36
38
  shape = 'rectangle'
37
39
  } = value
38
- const [onlyKind, setOnlyKind] = useState<ElementKind | null>(null)
40
+ const [onlyKind, setOnlyKind] = useState<NodeKind | null>(null)
39
41
  const diagramStore = useDiagramStoreApi()
40
42
  const w = 300
41
43
  const h = 200
@@ -137,7 +137,7 @@ export const ChangeAutoLayoutButton = (props: PopoverProps) => {
137
137
  }
138
138
 
139
139
  const MAX_SPACING = 400
140
- const SpacingSliders = forwardRef<HTMLDivElement, {
140
+ const SpacingSliders = forwardRef<HTMLDivElement | null, {
141
141
  isVertical: boolean
142
142
  nodeSep: number | undefined
143
143
  rankSep: number | undefined
@@ -160,7 +160,7 @@ const SpacingSliders = forwardRef<HTMLDivElement, {
160
160
  onChange(Math.round(x * MAX_SPACING), Math.round(y * MAX_SPACING))
161
161
  },
162
162
  [onChange, isVertical],
163
- 150,
163
+ 250,
164
164
  2000
165
165
  )
166
166
 
@@ -12,6 +12,7 @@ import {
12
12
  Text,
13
13
  TooltipGroup
14
14
  } from '@mantine/core'
15
+ import { openSpotlight } from '@mantine/spotlight'
15
16
  import {
16
17
  IconAlertTriangle,
17
18
  IconChevronLeft,
@@ -31,7 +32,6 @@ import { ActionIcon, Tooltip } from './_shared'
31
32
  import { ChangeAutoLayoutButton } from './ChangeAutoLayoutButton'
32
33
  import { ManualLayoutToolsButton } from './ManualLayoutToolsButton'
33
34
  import * as css from './styles.css'
34
- import { openSpotlight } from '@mantine/spotlight'
35
35
 
36
36
  const historySelector = (s: DiagramState) => ({
37
37
  showBurgerMenu: !!s.onBurgerMenuClick,
@@ -158,7 +158,7 @@ export const TopLeftPanel = () => {
158
158
  showFitDiagram: s.controls && s.fitViewEnabled && s.zoomable && isNotWalkthrough,
159
159
  showLayoutDriftWarning: s.controls && !s.readonly && s.view.hasLayoutDrift === true && isNotActive,
160
160
  showChangeAutoLayout: s.controls && s.isEditable() && isNotActive,
161
- showGoToSource: s.controls && !!s.onOpenSourceView && isNotWalkthrough,
161
+ showGoToSource: s.controls && !!s.onOpenSource && isNotWalkthrough,
162
162
  viewportChanged: s.viewportChanged,
163
163
  showManualLayoutTools: s.controls && s.readonly !== true && s.experimentalEdgeEditing === true
164
164
  })
@@ -179,10 +179,10 @@ export const TopLeftPanel = () => {
179
179
  {showNavigationButtons && <BackwardForwardButtons />}
180
180
  <ActionIconGroup className={css.actionIconGroup} orientation="vertical">
181
181
  {enableSearch && (
182
- <Tooltip label="Search">
182
+ <Tooltip label="Search (Ctrl+F or ⌘+F)">
183
183
  <ActionIcon
184
184
  onClick={() => openSpotlight()}
185
- >
185
+ >
186
186
  <IconSearch />
187
187
  </ActionIcon>
188
188
  </Tooltip>
@@ -192,7 +192,7 @@ export const TopLeftPanel = () => {
192
192
  <ActionIcon
193
193
  onClick={e => {
194
194
  e.stopPropagation()
195
- store.getState().onOpenSourceView?.()
195
+ store.getState().onOpenSourceView()
196
196
  }}>
197
197
  <IconFileSymlink stroke={1.5} />
198
198
  </ActionIcon>
@@ -1,4 +1,4 @@
1
- import { extractStep, isStepEdgeId, type ViewID } from '@likec4/core'
1
+ import { extractStep, isStepEdgeId, type ViewId } from '@likec4/core'
2
2
  import {
3
3
  ActionIcon,
4
4
  Box,
@@ -80,9 +80,9 @@ export const EdgeLabel = ({
80
80
  </Box>
81
81
  )}
82
82
  {isTruthy(label?.text) && (
83
- <Box className={edgesCss.edgeLabelText}>
83
+ <Text component="div" className={edgesCss.edgeLabelText} lineClamp={5}>
84
84
  {label.text}
85
- </Box>
85
+ </Text>
86
86
  )}
87
87
  {navigateTo && <NavigateToBtn viewId={navigateTo} />}
88
88
  </Box>
@@ -162,7 +162,7 @@ const NotePopover = ({ notes, children }: PropsWithChildren<{ notes: string }>)
162
162
  }
163
163
 
164
164
  type NavigateToBtnProps = {
165
- viewId: ViewID
165
+ viewId: ViewId
166
166
  }
167
167
 
168
168
  function NavigateToBtn({ viewId }: NavigateToBtnProps) {