@likec4/diagram 1.28.1 → 1.29.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.
@@ -1,4 +1,4 @@
1
- import { nonNullable, invariant, DiagramNode, nonexhaustive, getBBoxCenter, getParallelStepsPrefix } from '@likec4/core';
1
+ import { nonNullable, invariant, DiagramNode, nonexhaustive, getBBoxCenter, isStepEdgeId, getParallelStepsPrefix } from '@likec4/core';
2
2
  import { getViewportForBounds, applyEdgeChanges, applyNodeChanges } from '@xyflow/react';
3
3
  import { nodeToRect } from '@xyflow/system';
4
4
  import { hasAtLeast, prop, clamp, first } from 'remeda';
@@ -32,6 +32,7 @@ const diagramMachine = setup({
32
32
  "enabled: FitView": ({ context }) => context.features.enableFitView,
33
33
  "enabled: FocusMode": ({ context }) => context.features.enableFocusMode,
34
34
  "enabled: Readonly": ({ context }) => context.features.enableReadOnly,
35
+ "enabled: RelationshipDetails": ({ context }) => context.features.enableRelationshipDetails,
35
36
  "not readonly": ({ context }) => !context.features.enableReadOnly,
36
37
  "is dynamic view": ({ context }) => context.view.__ === "dynamic",
37
38
  "is another view": ({ context, event }) => {
@@ -59,6 +60,10 @@ const diagramMachine = setup({
59
60
  "click: node has connections": ({ context, event }) => {
60
61
  assertEvent(event, "xyflow.nodeClick");
61
62
  return context.xyedges.some((e) => e.source === event.node.id || e.target === event.node.id);
63
+ },
64
+ "click: selected edge": ({ event }) => {
65
+ assertEvent(event, "xyflow.edgeClick");
66
+ return event.edge.selected === true || event.edge.data.active === true;
62
67
  }
63
68
  },
64
69
  actions: {
@@ -562,7 +567,27 @@ const diagramMachine = setup({
562
567
  focusedNode: ({ event }) => event.nodeId
563
568
  }),
564
569
  target: "focused"
565
- }
570
+ },
571
+ "xyflow.edgeClick": [{
572
+ guard: and([
573
+ "is dynamic view",
574
+ "click: selected edge"
575
+ ]),
576
+ actions: raise(({ event }) => ({
577
+ type: "walkthrough.start",
578
+ stepId: event.edge.id
579
+ }))
580
+ }, {
581
+ guard: and([
582
+ "enabled: RelationshipDetails",
583
+ "click: selected edge"
584
+ ]),
585
+ actions: sendTo(({ system }) => typedSystem(system).overlaysActorRef, ({ context, event }) => ({
586
+ type: "open.relationshipDetails",
587
+ viewId: context.view.id,
588
+ edgeId: event.edge.id
589
+ }))
590
+ }]
566
591
  }
567
592
  },
568
593
  focused: {
@@ -575,18 +600,18 @@ const diagramMachine = setup({
575
600
  spawnChild("hotkeyActorLogic", { id: "hotkey" }),
576
601
  "xyflow:fitFocusedBounds"
577
602
  ],
578
- exit: enqueueActions(({ enqueue, context }) => {
603
+ exit: enqueueActions(({ enqueue, context, event }) => {
579
604
  enqueue.stopChild("hotkey");
580
605
  if (context.viewportBeforeFocus) {
581
606
  enqueue({ type: "xyflow:setViewport", params: { viewport: context.viewportBeforeFocus } });
582
607
  } else {
583
608
  enqueue({ type: "xyflow:fitDiagram" });
584
609
  }
585
- enqueue.assign((s) => ({
586
- ...unfocusNodesEdges(s),
610
+ enqueue.assign({
611
+ ...unfocusNodesEdges({ context}),
587
612
  viewportBeforeFocus: null,
588
613
  focusedNode: null
589
- }));
614
+ });
590
615
  }),
591
616
  on: {
592
617
  "xyflow.nodeClick": [
@@ -681,6 +706,23 @@ const diagramMachine = setup({
681
706
  enqueue("xyflow:fitFocusedBounds");
682
707
  })
683
708
  },
709
+ "xyflow.edgeClick": {
710
+ actions: [
711
+ assign(({ event, context }) => {
712
+ if (!isStepEdgeId(event.edge.id) || event.edge.id === context.activeWalkthrough?.stepId) {
713
+ return {};
714
+ }
715
+ return {
716
+ activeWalkthrough: {
717
+ stepId: event.edge.id,
718
+ parallelPrefix: getParallelStepsPrefix(event.edge.id)
719
+ }
720
+ };
721
+ }),
722
+ assign(updateActiveWalkthrough),
723
+ "xyflow:fitFocusedBounds"
724
+ ]
725
+ },
684
726
  "notations.unhighlight": {
685
727
  actions: assign((s) => ({
686
728
  ...updateActiveWalkthrough(s)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@likec4/diagram",
3
- "version": "1.28.1",
3
+ "version": "1.29.0",
4
4
  "license": "MIT",
5
5
  "bugs": "https://github.com/likec4/likec4/issues",
6
6
  "homepage": "https://likec4.dev",
@@ -44,7 +44,7 @@
44
44
  "@dagrejs/dagre": "1.1.4",
45
45
  "@mantine/core": "7.17.4",
46
46
  "@mantine/hooks": "7.17.4",
47
- "@nanostores/react": "0.8.4",
47
+ "@nanostores/react": "1.0.0",
48
48
  "@react-hookz/web": "^25.1.1",
49
49
  "@tabler/icons-react": "3.31.0",
50
50
  "@xstate/react": "^5.0.3",
@@ -54,15 +54,15 @@
54
54
  "clsx": "^2.1.1",
55
55
  "d3-shape": "^3.2.0",
56
56
  "fast-equals": "^5.2.2",
57
- "motion": "^12.6.3",
58
- "nanostores": "0.11.4",
57
+ "motion": "^12.7.3",
58
+ "nanostores": "1.0.1",
59
59
  "react-error-boundary": "^5.0.0",
60
60
  "react-shadow": "^20.6.0",
61
61
  "remeda": "^2.21.2",
62
62
  "type-fest": "^4.39.1",
63
63
  "xstate": "^5.19.2",
64
- "@likec4/core": "1.28.1",
65
- "@likec4/styles": "1.28.1"
64
+ "@likec4/core": "1.29.0",
65
+ "@likec4/styles": "1.29.0"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "react": "^18.x || ^19.x",
@@ -84,8 +84,8 @@
84
84
  "vite": "^6.2.5",
85
85
  "vite-plugin-dts": "^4.5.3",
86
86
  "vitest": "^3.1.1",
87
- "wireit": "^0.14.11",
88
- "@likec4/tsconfig": "1.28.1"
87
+ "wireit": "^0.14.12",
88
+ "@likec4/tsconfig": "1.29.0"
89
89
  },
90
90
  "scripts": {
91
91
  "generate": "panda codegen",
@@ -15,7 +15,6 @@ import { LikeC4CustomColors } from './LikeC4CustomColors'
15
15
  import { type LikeC4DiagramEventHandlers, type LikeC4DiagramProperties } from './LikeC4Diagram.props'
16
16
  import { LikeC4DiagramXYFlow } from './likec4diagram/DiagramXYFlow'
17
17
  import type { Types } from './likec4diagram/types'
18
- import { useViewToNodesEdges } from './likec4diagram/useViewToNodesEdges'
19
18
  import { useLikeC4Model } from './likec4model'
20
19
  import { DiagramActorProvider } from './state/DiagramActorProvider'
21
20
 
@@ -13,6 +13,7 @@ export const edgePropsEqual = <P extends Record<string, unknown> = Base.EdgeData
13
13
  ) => (
14
14
  prev.id === next.id
15
15
  && eq(prev.selected ?? false, next.selected ?? false)
16
+ && eq(prev.animated ?? false, next.animated ?? false)
16
17
  && eq(prev.source, next.source)
17
18
  && eq(prev.sourceHandleId ?? null, next.sourceHandleId ?? null)
18
19
  && eq(prev.sourcePosition, next.sourcePosition)
@@ -125,6 +125,9 @@ export const labelsva = sva({
125
125
  padding: '0px',
126
126
  },
127
127
  labelContents: {
128
+ display: 'flex',
129
+ flexDirection: 'column',
130
+ alignItems: 'center',
128
131
  padding: '2px 5px 4px 0px',
129
132
  },
130
133
  labelText: {
@@ -1,6 +1,7 @@
1
1
  import { type DiagramEdge, extractStep, isStepEdgeId } from '@likec4/core'
2
2
  import { cx } from '@likec4/styles/css'
3
- import { type BoxProps, Box, Text } from '@mantine/core'
3
+ import { type BoxProps, Box } from '@likec4/styles/jsx'
4
+ import { Text } from '@mantine/core'
4
5
  import { type PropsWithChildren } from 'react'
5
6
  import { isTruthy } from 'remeda'
6
7
  import type { UndefinedOnPartialDeep } from 'type-fest'
@@ -4,9 +4,11 @@ import type { DiagramActorRef } from '../state/types'
4
4
  /**
5
5
  * To improve experience with HMR, we use `createSafeContext` as a boundary for hoooks
6
6
  */
7
- export const DiagramActorSafeContext = createContext<DiagramActorRef>(null as any)
7
+ const DiagramActorSafeContext = createContext<DiagramActorRef>(null as any)
8
8
  DiagramActorSafeContext.displayName = 'DiagramActorSafeContext'
9
9
 
10
+ export const DiagramActorContextProvider = DiagramActorSafeContext.Provider
11
+
10
12
  export const useDiagramActorRef = () => {
11
13
  const ctx = useContext(DiagramActorSafeContext)
12
14
  if (ctx === null) {
@@ -1,6 +1,6 @@
1
+ import { cx } from '@likec4/styles/css'
1
2
  import { ActionIconGroup, Badge, Box, Group, Loader, Stack } from '@mantine/core'
2
3
  import { IconFileSymlink, IconFocusCentered, IconMenu2 } from '@tabler/icons-react'
3
- import { clsx } from 'clsx'
4
4
  import { LayoutGroup, m } from 'motion/react'
5
5
  import { memo } from 'react'
6
6
  import { useDiagramEventHandlers, useEnabledFeatures } from '../../../context'
@@ -42,7 +42,7 @@ const ControlsDefaultLayout: ControlsCustomLayout = ({
42
42
  translateY: -10,
43
43
  }}
44
44
  align="flex-start"
45
- className={clsx(
45
+ className={cx(
46
46
  'react-flow__panel',
47
47
  css.panel,
48
48
  'likec4-top-left-panel',
@@ -53,7 +53,7 @@ const ControlsDefaultLayout: ControlsCustomLayout = ({
53
53
  <Stack align="flex-start" gap="xs">
54
54
  <LayoutGroup>
55
55
  <Group
56
- className={clsx(css.navigationButtons, 'likec4-navigation-webview')}
56
+ className={cx(css.navigationButtons, 'likec4-navigation-webview')}
57
57
  gap={'xs'}>
58
58
  {burgerMenu}
59
59
  {navigationButtons}
@@ -24,10 +24,10 @@ export function ElementDetails({
24
24
  shallowEqual,
25
25
  )
26
26
  return (
27
- <ElementDetailsActorContext value={actorRef}>
27
+ <ElementDetailsActorContext.Provider value={actorRef}>
28
28
  <ElementDetailsCard
29
29
  onClose={onClose}
30
30
  {...props} />
31
- </ElementDetailsActorContext>
31
+ </ElementDetailsActorContext.Provider>
32
32
  )
33
33
  }
@@ -6,7 +6,7 @@ import { shallowEqual } from 'fast-equals'
6
6
  import { type PropsWithChildren, useEffect } from 'react'
7
7
  import { useDiagramEventHandlersRef } from '../context/DiagramEventHandlers'
8
8
  import { DiagramFeatures, useEnabledFeatures } from '../context/DiagramFeatures'
9
- import { DiagramActorSafeContext } from '../hooks/safeContext'
9
+ import { DiagramActorContextProvider } from '../hooks/safeContext'
10
10
  import { useUpdateEffect } from '../hooks/useUpdateEffect'
11
11
  import type { Types } from '../likec4diagram/types'
12
12
  import { useViewToNodesEdges } from '../likec4diagram/useViewToNodesEdges'
@@ -92,10 +92,10 @@ export function DiagramActorProvider({
92
92
  const toggledFeatures = useSelector(actorRef, selectToggledFeatures, shallowEqual)
93
93
 
94
94
  return (
95
- <DiagramActorSafeContext value={actorRef}>
95
+ <DiagramActorContextProvider value={actorRef}>
96
96
  <DiagramFeatures overrides={toggledFeatures}>
97
97
  {children}
98
98
  </DiagramFeatures>
99
- </DiagramActorSafeContext>
99
+ </DiagramActorContextProvider>
100
100
  )
101
101
  }
@@ -13,6 +13,7 @@ import {
13
13
  getBBoxCenter,
14
14
  getParallelStepsPrefix,
15
15
  invariant,
16
+ isStepEdgeId,
16
17
  nonexhaustive,
17
18
  nonNullable,
18
19
  } from '@likec4/core'
@@ -24,7 +25,7 @@ import {
24
25
  useStoreApi,
25
26
  } from '@xyflow/react'
26
27
  import { type EdgeChange, type NodeChange, type Rect, type Viewport, nodeToRect } from '@xyflow/system'
27
- import { clamp, first, hasAtLeast, prop, reduce } from 'remeda'
28
+ import { clamp, first, hasAtLeast, prop } from 'remeda'
28
29
  import {
29
30
  type ActorLogicFrom,
30
31
  type ActorRef,
@@ -199,6 +200,7 @@ export const diagramMachine = setup({
199
200
  'enabled: FitView': ({ context }) => context.features.enableFitView,
200
201
  'enabled: FocusMode': ({ context }) => context.features.enableFocusMode,
201
202
  'enabled: Readonly': ({ context }) => context.features.enableReadOnly,
203
+ 'enabled: RelationshipDetails': ({ context }) => context.features.enableRelationshipDetails,
202
204
  'not readonly': ({ context }) => !context.features.enableReadOnly,
203
205
  'is dynamic view': ({ context }) => context.view.__ === 'dynamic',
204
206
  'is another view': ({ context, event }) => {
@@ -227,6 +229,10 @@ export const diagramMachine = setup({
227
229
  assertEvent(event, 'xyflow.nodeClick')
228
230
  return context.xyedges.some(e => e.source === event.node.id || e.target === event.node.id)
229
231
  },
232
+ 'click: selected edge': ({ event }) => {
233
+ assertEvent(event, 'xyflow.edgeClick')
234
+ return event.edge.selected === true || event.edge.data.active === true
235
+ },
230
236
  },
231
237
  actions: {
232
238
  'trigger:NavigateTo': (_, _params: { viewId: ViewId }) => {
@@ -767,6 +773,26 @@ export const diagramMachine = setup({
767
773
  }),
768
774
  target: 'focused',
769
775
  },
776
+ 'xyflow.edgeClick': [{
777
+ guard: and([
778
+ 'is dynamic view',
779
+ 'click: selected edge',
780
+ ]) as any,
781
+ actions: raise(({ event }) => ({
782
+ type: 'walkthrough.start',
783
+ stepId: event.edge.id as StepEdgeId,
784
+ })),
785
+ }, {
786
+ guard: and([
787
+ 'enabled: RelationshipDetails',
788
+ 'click: selected edge',
789
+ ]) as any,
790
+ actions: sendTo(({ system }) => typedSystem(system).overlaysActorRef!, ({ context, event }) => ({
791
+ type: 'open.relationshipDetails',
792
+ viewId: context.view.id,
793
+ edgeId: event.edge.id as EdgeId,
794
+ })),
795
+ }],
770
796
  },
771
797
  },
772
798
  focused: {
@@ -779,18 +805,18 @@ export const diagramMachine = setup({
779
805
  spawnChild('hotkeyActorLogic', { id: 'hotkey' }),
780
806
  'xyflow:fitFocusedBounds',
781
807
  ],
782
- exit: enqueueActions(({ enqueue, context }) => {
808
+ exit: enqueueActions(({ enqueue, context, event }) => {
783
809
  enqueue.stopChild('hotkey')
784
810
  if (context.viewportBeforeFocus) {
785
811
  enqueue({ type: 'xyflow:setViewport', params: { viewport: context.viewportBeforeFocus } })
786
812
  } else {
787
813
  enqueue({ type: 'xyflow:fitDiagram' })
788
814
  }
789
- enqueue.assign((s) => ({
790
- ...unfocusNodesEdges(s),
815
+ enqueue.assign({
816
+ ...unfocusNodesEdges({ context, event }),
791
817
  viewportBeforeFocus: null,
792
818
  focusedNode: null,
793
- }))
819
+ })
794
820
  }),
795
821
  on: {
796
822
  'xyflow.nodeClick': [
@@ -885,6 +911,23 @@ export const diagramMachine = setup({
885
911
  enqueue('xyflow:fitFocusedBounds')
886
912
  }),
887
913
  },
914
+ 'xyflow.edgeClick': {
915
+ actions: [
916
+ assign(({ event, context }) => {
917
+ if (!isStepEdgeId(event.edge.id) || event.edge.id === context.activeWalkthrough?.stepId) {
918
+ return {}
919
+ }
920
+ return {
921
+ activeWalkthrough: {
922
+ stepId: event.edge.id,
923
+ parallelPrefix: getParallelStepsPrefix(event.edge.id),
924
+ },
925
+ }
926
+ }),
927
+ assign(updateActiveWalkthrough),
928
+ 'xyflow:fitFocusedBounds',
929
+ ],
930
+ },
888
931
  'notations.unhighlight': {
889
932
  actions: assign(s => ({
890
933
  ...updateActiveWalkthrough(s),