@likec4/diagram 1.24.0 → 1.24.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.
Files changed (116) hide show
  1. package/dist/LikeC4Diagram.css.d.ts +5 -0
  2. package/dist/LikeC4Diagram.css.js +1 -0
  3. package/dist/LikeC4Diagram.d.ts +1 -1
  4. package/dist/LikeC4Diagram.js +6 -2
  5. package/dist/LikeC4Diagram.props.d.ts +6 -0
  6. package/dist/base/BaseXYFlow.d.ts +1 -1
  7. package/dist/base/BaseXYFlow.js +12 -3
  8. package/dist/base/const.d.ts +1 -1
  9. package/dist/base/const.js +1 -1
  10. package/dist/base/primitives/compound/CompoundActionButton.css.js +2 -0
  11. package/dist/base/primitives/compound/CompoundDetailsButton.css.js +2 -0
  12. package/dist/base/primitives/compound/CompoundNodeContainer.css.d.ts +0 -1
  13. package/dist/base/primitives/compound/CompoundNodeContainer.css.js +4 -4
  14. package/dist/base/primitives/compound/CompoundNodeContainer.js +35 -22
  15. package/dist/base/primitives/compound/CompoundTitle.css.js +3 -1
  16. package/dist/base/primitives/customNode.js +1 -1
  17. package/dist/base/primitives/edge/EdgeContainer.js +5 -4
  18. package/dist/base/primitives/edge/EdgeLabel.css.d.ts +0 -1
  19. package/dist/base/primitives/edge/EdgeLabel.css.js +2 -5
  20. package/dist/base/primitives/edge/EdgeLabel.js +5 -5
  21. package/dist/base/primitives/edge/edge.css.d.ts +1 -1
  22. package/dist/base/primitives/edge/edge.css.js +4 -5
  23. package/dist/base/primitives/element/ElementActionButtons.css.js +2 -0
  24. package/dist/base/primitives/element/ElementActionButtons.js +4 -3
  25. package/dist/base/primitives/element/ElementDetailsButton.css.js +2 -0
  26. package/dist/base/primitives/element/ElementNodeContainer.css.js +2 -0
  27. package/dist/base/primitives/element/ElementNodeContainer.js +3 -1
  28. package/dist/base/primitives/element/ElementShape.css.js +4 -2
  29. package/dist/base/primitives/element/ElementShape.js +2 -3
  30. package/dist/base/primitives/element/ElementTitle.css.js +2 -0
  31. package/dist/base/primitives/element/styles.css.js +2 -0
  32. package/dist/components/SearchControl.css.js +2 -0
  33. package/dist/context/FramerMotionConfig.d.ts +9 -1
  34. package/dist/context/FramerMotionConfig.js +5 -2
  35. package/dist/context/RootContainer.js +13 -1
  36. package/dist/diagram.css +1 -1
  37. package/dist/globals.css.d.ts +2 -0
  38. package/dist/hooks/safeContext.js +1 -0
  39. package/dist/hooks/useDiagram.d.ts +554 -864
  40. package/dist/hooks/useDiagram.js +1 -0
  41. package/dist/hooks/useIsReducedGraphics.d.ts +4 -0
  42. package/dist/hooks/useIsReducedGraphics.js +8 -0
  43. package/dist/hooks/useXYFlow.js +1 -1
  44. package/dist/likec4diagram/DiagramUI.d.ts +2 -1
  45. package/dist/likec4diagram/DiagramUI.js +5 -3
  46. package/dist/likec4diagram/custom/nodes/nodeTypes.js +2 -2
  47. package/dist/likec4diagram/ui/controls/styles.css.js +2 -0
  48. package/dist/likec4diagram/ui/diagram-title/DiagramTitlePanel.css.js +2 -0
  49. package/dist/likec4diagram/ui/diagram-title/DiagramTitlePanel.js +3 -4
  50. package/dist/likec4diagram/ui/search/LikeC4Search.css.js +2 -0
  51. package/dist/overlays/element-details/ElementDetailsCard.js +12 -8
  52. package/dist/overlays/element-details/actor.d.ts +9084 -2569
  53. package/dist/overlays/overlay/Overlay.js +6 -3
  54. package/dist/overlays/relationship-details/RelationshipDetails.js +31 -17
  55. package/dist/overlays/relationship-details/actor.d.ts +228 -3
  56. package/dist/overlays/relationship-details/actor.js +86 -14
  57. package/dist/overlays/relationship-details/hooks.d.ts +3 -0
  58. package/dist/overlays/relationship-details/hooks.js +6 -2
  59. package/dist/overlays/relationships-browser/-useRelationshipsView.js +12 -3
  60. package/dist/overlays/relationships-browser/RelationshipsBrowser.js +12 -3
  61. package/dist/overlays/relationships-browser/actor.d.ts +271 -54
  62. package/dist/overlays/relationships-browser/actor.js +86 -23
  63. package/dist/state/DiagramActorProvider.js +12 -2
  64. package/dist/state/diagram-machine.d.ts +563 -867
  65. package/dist/state/diagram-machine.js +82 -81
  66. package/dist/state/persistence.d.ts +5 -0
  67. package/dist/state/persistence.js +23 -0
  68. package/package.json +9 -9
  69. package/src/LikeC4Diagram.css.ts +13 -5
  70. package/src/LikeC4Diagram.props.ts +7 -0
  71. package/src/LikeC4Diagram.tsx +33 -24
  72. package/src/base/BaseXYFlow.tsx +14 -9
  73. package/src/base/const.ts +1 -1
  74. package/src/base/primitives/compound/CompoundActionButton.css.ts +6 -0
  75. package/src/base/primitives/compound/CompoundDetailsButton.css.ts +4 -0
  76. package/src/base/primitives/compound/CompoundNodeContainer.css.ts +41 -59
  77. package/src/base/primitives/compound/CompoundNodeContainer.tsx +42 -22
  78. package/src/base/primitives/compound/CompoundTitle.css.ts +14 -13
  79. package/src/base/primitives/customNode.ts +3 -2
  80. package/src/base/primitives/edge/EdgeContainer.tsx +4 -3
  81. package/src/base/primitives/edge/EdgeLabel.css.ts +17 -11
  82. package/src/base/primitives/edge/EdgeLabel.tsx +4 -4
  83. package/src/base/primitives/edge/edge.css.ts +47 -27
  84. package/src/base/primitives/element/ElementActionButtons.css.ts +4 -0
  85. package/src/base/primitives/element/ElementActionButtons.tsx +4 -2
  86. package/src/base/primitives/element/ElementDetailsButton.css.ts +6 -8
  87. package/src/base/primitives/element/ElementNodeContainer.css.ts +18 -21
  88. package/src/base/primitives/element/ElementNodeContainer.tsx +3 -1
  89. package/src/base/primitives/element/ElementShape.css.ts +10 -6
  90. package/src/base/primitives/element/ElementShape.tsx +2 -2
  91. package/src/components/SearchControl.css.ts +11 -4
  92. package/src/context/FramerMotionConfig.tsx +13 -2
  93. package/src/context/RootContainer.tsx +9 -2
  94. package/src/globals.css.ts +3 -0
  95. package/src/hooks/safeContext.tsx +2 -1
  96. package/src/hooks/useDiagram.ts +1 -0
  97. package/src/hooks/useIsReducedGraphics.ts +5 -0
  98. package/src/hooks/useXYFlow.ts +1 -1
  99. package/src/likec4diagram/DiagramUI.tsx +5 -3
  100. package/src/likec4diagram/custom/nodes/nodeTypes.tsx +2 -2
  101. package/src/likec4diagram/ui/controls/styles.css.ts +11 -1
  102. package/src/likec4diagram/ui/diagram-title/DiagramTitlePanel.css.ts +10 -3
  103. package/src/likec4diagram/ui/diagram-title/DiagramTitlePanel.tsx +6 -3
  104. package/src/likec4diagram/ui/search/LikeC4Search.css.ts +1 -0
  105. package/src/overlays/element-details/ElementDetailsCard.tsx +238 -231
  106. package/src/overlays/overlay/Overlay.css.ts +1 -9
  107. package/src/overlays/overlay/Overlay.tsx +6 -3
  108. package/src/overlays/relationship-details/RelationshipDetails.tsx +83 -66
  109. package/src/overlays/relationship-details/actor.ts +106 -13
  110. package/src/overlays/relationship-details/hooks.ts +6 -2
  111. package/src/overlays/relationships-browser/-useRelationshipsView.ts +10 -0
  112. package/src/overlays/relationships-browser/RelationshipsBrowser.tsx +16 -8
  113. package/src/overlays/relationships-browser/actor.ts +92 -22
  114. package/src/state/DiagramActorProvider.tsx +13 -2
  115. package/src/state/diagram-machine.ts +80 -79
  116. package/src/state/persistence.ts +22 -0
@@ -2,14 +2,15 @@ import { nonNullable, invariant, DiagramNode, nonexhaustive, getBBoxCenter, getP
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";
5
- import { setup, enqueueActions, assertEvent, stopChild, assign, sendTo, cancel, raise, spawnChild, and, or } from "xstate";
5
+ import { setup, enqueueActions, assign, assertEvent, stopChild, sendTo, cancel, raise, spawnChild, and, or } from "xstate";
6
6
  import { MinZoom } from "../base/const.js";
7
7
  import { AllDisabled } from "../context/DiagramFeatures.js";
8
8
  import { createLayoutConstraints } from "../likec4diagram/useLayoutConstraints.js";
9
9
  import { overlaysActorLogic } from "../overlays/overlaysActor.js";
10
10
  import { getAligner, toNodeRect } from "./aligners.js";
11
- import { resetEdgeControlPoints, updateEdgeData, updateNodeData, updateNavigationHistory, mergeXYNodesEdges, navigateForward, navigateBack, unfocusNodesEdges, updateActiveWalkthrough, focusNodesEdges, lastClickedNode } from "./assign.js";
11
+ import { lastClickedNode, resetEdgeControlPoints, updateEdgeData, updateNodeData, updateNavigationHistory, mergeXYNodesEdges, navigateForward, navigateBack, unfocusNodesEdges, updateActiveWalkthrough, focusNodesEdges } from "./assign.js";
12
12
  import { hotkeyActorLogic } from "./hotkeyActor.js";
13
+ import { DiagramToggledFeaturesPersistence } from "./persistence.js";
13
14
  import { syncManualLayoutActorLogic } from "./syncManualLayoutActor.js";
14
15
  import { typedSystem, findDiagramNode, focusedBounds } from "./utils.js";
15
16
  const diagramMachine = setup({
@@ -40,7 +41,7 @@ const diagramMachine = setup({
40
41
  }
41
42
  nonexhaustive(event.type);
42
43
  },
43
- "click: selected node ": ({ event }) => {
44
+ "click: selected node": ({ event }) => {
44
45
  assertEvent(event, "xyflow.nodeClick");
45
46
  return event.node.selected === true;
46
47
  },
@@ -52,6 +53,10 @@ const diagramMachine = setup({
52
53
  "click: focused node": ({ context, event }) => {
53
54
  assertEvent(event, "xyflow.nodeClick");
54
55
  return context.focusedNode === event.node.id;
56
+ },
57
+ "click: node has connections": ({ context, event }) => {
58
+ assertEvent(event, "xyflow.nodeClick");
59
+ return context.xyedges.some((e) => e.source === event.node.id || e.target === event.node.id);
55
60
  }
56
61
  },
57
62
  actions: {
@@ -61,21 +66,22 @@ const diagramMachine = setup({
61
66
  },
62
67
  "trigger:OpenSource": (_, _params) => {
63
68
  },
64
- "open source of focused node": enqueueActions(({ context, enqueue }) => {
65
- if (!context.focusedNode || !context.features.enableVscode) return;
66
- const diagramNode = findDiagramNode(context, context.focusedNode);
69
+ "assign lastClickedNode": assign(({ context, event }) => {
70
+ assertEvent(event, "xyflow.nodeClick");
71
+ return {
72
+ lastClickedNode: lastClickedNode({ context, event })
73
+ };
74
+ }),
75
+ "open source of focused or last clicked node": enqueueActions(({ context, enqueue }) => {
76
+ var _a;
77
+ const nodeId = context.focusedNode ?? ((_a = context.lastClickedNode) == null ? void 0 : _a.id);
78
+ if (!nodeId || !context.features.enableVscode) return;
79
+ const diagramNode = findDiagramNode(context, nodeId);
67
80
  if (!diagramNode) return;
68
- enqueue.cancel("openSource");
69
81
  if (DiagramNode.deploymentRef(diagramNode)) {
70
- enqueue.raise({ type: "open.source", deployment: DiagramNode.deploymentRef(diagramNode) }, {
71
- id: "openSource",
72
- delay: 100
73
- });
82
+ enqueue.raise({ type: "open.source", deployment: DiagramNode.deploymentRef(diagramNode) });
74
83
  } else if (DiagramNode.modelRef(diagramNode)) {
75
- enqueue.raise({ type: "open.source", element: DiagramNode.modelRef(diagramNode) }, {
76
- id: "openSource",
77
- delay: 100
78
- });
84
+ enqueue.raise({ type: "open.source", element: DiagramNode.modelRef(diagramNode) });
79
85
  }
80
86
  }),
81
87
  "xyflow:fitDiagram": ({ context }, params) => {
@@ -183,7 +189,7 @@ const diagramMachine = setup({
183
189
  context: ({ input, self, spawn }) => ({
184
190
  ...input,
185
191
  features: { ...AllDisabled },
186
- toggledFeatures: {
192
+ toggledFeatures: DiagramToggledFeaturesPersistence.read() ?? {
187
193
  enableReadOnly: true
188
194
  },
189
195
  initialized: false,
@@ -242,9 +248,10 @@ const diagramMachine = setup({
242
248
  // TODO: xstate fails to infer the type of the guard
243
249
  guard: and([
244
250
  "enabled: FocusMode",
251
+ "click: node has connections",
245
252
  or([
246
253
  "click: same node",
247
- "click: selected node "
254
+ "click: selected node"
248
255
  ])
249
256
  ]),
250
257
  actions: [
@@ -256,9 +263,10 @@ const diagramMachine = setup({
256
263
  target: "focused"
257
264
  },
258
265
  {
259
- actions: assign({
260
- lastClickedNode
261
- })
266
+ actions: [
267
+ "assign lastClickedNode",
268
+ "open source of focused or last clicked node"
269
+ ]
262
270
  }
263
271
  ],
264
272
  "xyflow.paneClick": {
@@ -301,7 +309,7 @@ const diagramMachine = setup({
301
309
  ...focusNodesEdges(s),
302
310
  viewportBeforeFocus: { ...s.context.viewport }
303
311
  })),
304
- "open source of focused node",
312
+ "open source of focused or last clicked node",
305
313
  spawnChild("hotkeyActorLogic", { id: "hotkey" }),
306
314
  {
307
315
  type: "xyflow:fitDiagram",
@@ -334,7 +342,7 @@ const diagramMachine = setup({
334
342
  focusedNode: ({ event }) => event.node.id
335
343
  }),
336
344
  assign(focusNodesEdges),
337
- "open source of focused node",
345
+ "open source of focused or last clicked node",
338
346
  {
339
347
  type: "xyflow:fitDiagram",
340
348
  params: focusedBounds
@@ -348,7 +356,7 @@ const diagramMachine = setup({
348
356
  focusedNode: ({ event }) => event.nodeId
349
357
  }),
350
358
  assign(focusNodesEdges),
351
- "open source of focused node",
359
+ "open source of focused or last clicked node",
352
360
  {
353
361
  type: "xyflow:fitDiagram",
354
362
  params: focusedBounds
@@ -380,69 +388,60 @@ const diagramMachine = setup({
380
388
  },
381
389
  // Navigating to another view (after `navigateTo` event)
382
390
  navigating: {
383
- initial: "pending",
384
- type: "compound",
385
- states: {
386
- pending: {
387
- entry: enqueueActions(({ enqueue }) => {
388
- enqueue("closeAllOverlays");
389
- enqueue("stopSyncLayout");
390
- enqueue({
391
- type: "trigger:NavigateTo",
392
- params: ({ context }) => ({
393
- viewId: nonNullable(context.lastOnNavigate, "Invalid state, lastOnNavigate is null").toView
394
- })
395
- });
396
- }),
397
- on: {
398
- "update.view": {
399
- actions: enqueueActions(({ enqueue, context, event }) => {
400
- const { fromNode, toNode } = findCorrespondingNode(context, event);
401
- if (fromNode && toNode) {
402
- enqueue({
403
- type: "xyflow:alignNodeFromToAfterNavigate",
404
- params: {
405
- fromNode: fromNode.id,
406
- toPosition: {
407
- x: toNode.position[0],
408
- y: toNode.position[1]
409
- }
410
- }
411
- });
412
- enqueue.raise({ type: "fitDiagram" }, { id: "fitDiagram", delay: 80 });
413
- } else {
414
- enqueue({
415
- type: "xyflow:setViewportCenter",
416
- params: getBBoxCenter(event.view.bounds)
417
- });
418
- enqueue.raise({ type: "fitDiagram", duration: 200 }, { id: "fitDiagram", delay: 25 });
419
- }
420
- enqueue.assign(updateNavigationHistory);
421
- enqueue.assign(mergeXYNodesEdges);
422
- }),
423
- target: "done"
424
- }
425
- }
426
- },
427
- done: {
428
- type: "final"
391
+ entry: [
392
+ "closeAllOverlays",
393
+ "stopSyncLayout",
394
+ {
395
+ type: "trigger:NavigateTo",
396
+ params: ({ context }) => ({
397
+ viewId: nonNullable(context.lastOnNavigate, "Invalid state, lastOnNavigate is null").toView
398
+ })
429
399
  }
430
- },
431
- onDone: {
432
- target: "idle"
433
- },
400
+ ],
434
401
  exit: assign({
435
402
  syncLayoutActorRef: ({ self, context, spawn }) => spawn("syncManualLayoutActorLogic", {
436
403
  id: "syncLayout",
437
404
  systemId: "syncLayout",
438
405
  input: { parent: self, viewId: context.view.id }
439
- }),
440
- lastOnNavigate: null
441
- })
406
+ })
407
+ // lastOnNavigate: null,
408
+ }),
409
+ on: {
410
+ "update.view": {
411
+ actions: enqueueActions(({ enqueue, context, event }) => {
412
+ const { fromNode, toNode } = findCorrespondingNode(context, event);
413
+ if (fromNode && toNode) {
414
+ enqueue({
415
+ type: "xyflow:alignNodeFromToAfterNavigate",
416
+ params: {
417
+ fromNode: fromNode.id,
418
+ toPosition: {
419
+ x: toNode.position[0],
420
+ y: toNode.position[1]
421
+ }
422
+ }
423
+ });
424
+ enqueue.raise({ type: "fitDiagram" }, { id: "fitDiagram", delay: 80 });
425
+ } else {
426
+ enqueue({
427
+ type: "xyflow:setViewportCenter",
428
+ params: getBBoxCenter(event.view.bounds)
429
+ });
430
+ enqueue.raise({ type: "fitDiagram", duration: 200 }, { id: "fitDiagram", delay: 25 });
431
+ }
432
+ enqueue.assign(updateNavigationHistory);
433
+ enqueue.assign(mergeXYNodesEdges);
434
+ enqueue.assign({
435
+ lastOnNavigate: null
436
+ });
437
+ }),
438
+ target: "#idle"
439
+ }
440
+ }
442
441
  },
443
442
  walkthrough: {
444
443
  entry: [
445
- // spawnChild('hotkeyActor', { id: 'hotkey' }),
444
+ spawnChild("hotkeyActorLogic", { id: "hotkey" }),
446
445
  assign({
447
446
  viewportBeforeFocus: ({ context }) => context.viewport,
448
447
  activeWalkthrough: ({ context, event }) => {
@@ -702,10 +701,12 @@ const diagramMachine = setup({
702
701
  },
703
702
  "toggle.feature": {
704
703
  actions: assign({
705
- toggledFeatures: ({ context, event }) => ({
706
- ...context.toggledFeatures,
707
- [`enable${event.feature}`]: event.forceValue ?? !(context.toggledFeatures[`enable${event.feature}`] ?? context.features[`enable${event.feature}`])
708
- })
704
+ toggledFeatures: ({ context, event }) => {
705
+ return DiagramToggledFeaturesPersistence.write({
706
+ ...context.toggledFeatures,
707
+ [`enable${event.feature}`]: event.forceValue ?? !(context.toggledFeatures[`enable${event.feature}`] ?? context.features[`enable${event.feature}`])
708
+ });
709
+ }
709
710
  })
710
711
  }
711
712
  },
@@ -0,0 +1,5 @@
1
+ import { EnabledFeatures } from '../context/DiagramFeatures';
2
+ export declare const DiagramToggledFeaturesPersistence: {
3
+ read(): Partial<EnabledFeatures> | null;
4
+ write<F extends Partial<EnabledFeatures>>(toggledFeatures: F): F;
5
+ };
@@ -0,0 +1,23 @@
1
+ import { pickBy, isBoolean } from "remeda";
2
+ const key = `likec4:diagram:toggledFeatures`;
3
+ const DiagramToggledFeaturesPersistence = {
4
+ read() {
5
+ try {
6
+ let fromStorage = sessionStorage.getItem(key);
7
+ if (fromStorage) {
8
+ return JSON.parse(fromStorage);
9
+ }
10
+ throw new Error(`Workspace ${key} not found`);
11
+ } catch (e) {
12
+ console.error(`Error reading fromStorage ${key}:`, e);
13
+ return null;
14
+ }
15
+ },
16
+ write(toggledFeatures) {
17
+ sessionStorage.setItem(key, JSON.stringify(pickBy(toggledFeatures, isBoolean)));
18
+ return toggledFeatures;
19
+ }
20
+ };
21
+ export {
22
+ DiagramToggledFeaturesPersistence
23
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@likec4/diagram",
3
- "version": "1.24.0",
3
+ "version": "1.24.1",
4
4
  "license": "MIT",
5
5
  "bugs": "https://github.com/likec4/likec4/issues",
6
6
  "homepage": "https://likec4.dev",
@@ -37,21 +37,21 @@
37
37
  "@tabler/icons-react": "3.29.0",
38
38
  "@vanilla-extract/dynamic": "^2.1.2",
39
39
  "@xstate/react": "^5.0.2",
40
- "@xyflow/react": "12.4.3",
41
- "@xyflow/system": "0.0.51",
40
+ "@xyflow/react": "12.4.4",
41
+ "@xyflow/system": "0.0.52",
42
42
  "bezier-js": "^6.1.4",
43
43
  "clsx": "^2.1.1",
44
44
  "d3-shape": "^3.2.0",
45
45
  "esm-env": "^1.2.2",
46
46
  "fast-equals": "^5.2.2",
47
- "framer-motion": "^12.4.2",
47
+ "framer-motion": "^12.4.7",
48
48
  "khroma": "^2.1.0",
49
49
  "nanostores": "0.11.3",
50
50
  "react-error-boundary": "^5.0.0",
51
- "remeda": "^2.20.1",
51
+ "remeda": "^2.20.2",
52
52
  "type-fest": "4.34.1",
53
53
  "xstate": "^5.19.2",
54
- "@likec4/core": "1.24.0"
54
+ "@likec4/core": "1.24.1"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "@mantine/core": "7.17.0",
@@ -72,14 +72,14 @@
72
72
  "typescript": "5.7.3",
73
73
  "vite": "6.1.0",
74
74
  "vite-plugin-dts": "^4.5.0",
75
- "vitest": "^3.0.4",
76
- "@likec4/tsconfig": "1.24.0"
75
+ "vitest": "^3.0.6",
76
+ "@likec4/tsconfig": "1.24.1"
77
77
  },
78
78
  "scripts": {
79
79
  "typecheck": "tsc --noEmit",
80
80
  "build": "vite build",
81
81
  "lint": "run -T eslint src/ --fix",
82
- "clean": "rm -r -f -g 'dist/*' .tsbuildinfo",
82
+ "clean": "rm -r -f -g \"dist/*\" .tsbuildinfo",
83
83
  "test": "vitest run --no-isolate"
84
84
  }
85
85
  }
@@ -1,4 +1,5 @@
1
1
  import { globalStyle, style } from '@vanilla-extract/css'
2
+ import { whereNotReducedGraphics, whereReducedGraphics } from './globals.css'
2
3
  import { vars, xyvars } from './theme-vars'
3
4
 
4
5
  export const cssReactFlow = style({
@@ -13,6 +14,10 @@ export const cssReactFlow = style({
13
14
  },
14
15
  })
15
16
 
17
+ export const reactFlowReducedGraphics = `${whereReducedGraphics} ${cssReactFlow}`
18
+ export const reactFlow = `${whereNotReducedGraphics} ${cssReactFlow}`
19
+ export { whereNotReducedGraphics, whereReducedGraphics }
20
+
16
21
  export const notInitialized = style({
17
22
  opacity: 0,
18
23
  })
@@ -30,11 +35,6 @@ globalStyle(`.react-flow${cssReactFlow}`, {
30
35
  globalStyle(`.react-flow${cssReactFlow} .react-flow__pane`, {
31
36
  WebkitUserSelect: 'none',
32
37
  })
33
-
34
- globalStyle(`.react-flow${cssReactFlow} .xyflow__viewport`, {
35
- willChange: 'transform',
36
- })
37
-
38
38
  export const cssDisablePan = style({})
39
39
 
40
40
  export const cssTransparentBg = style({})
@@ -58,3 +58,11 @@ export const hiddenIfZoomTooSmall = style({
58
58
  },
59
59
  },
60
60
  })
61
+
62
+ export const hiddenIfReducedGraphics = style({
63
+ selectors: {
64
+ [`${reactFlowReducedGraphics} &`]: {
65
+ visibility: 'hidden',
66
+ },
67
+ },
68
+ })
@@ -172,6 +172,13 @@ export interface LikeC4DiagramProperties {
172
172
  */
173
173
  experimentalEdgeEditing?: boolean | undefined
174
174
 
175
+ /**
176
+ * Improve performance by hiding certain elements and reducing visual effects (disable mix-blend, shadows, animations)
177
+ *
178
+ * @default 'auto' - will be set to true if view has more then 3000 * 2000 pixels
179
+ */
180
+ reduceGraphics?: 'auto' | boolean | undefined
181
+
175
182
  /**
176
183
  * Render icon for an element, bundled or remote
177
184
  * By default, if icon is http:// or https://, it will be rendered as an image
@@ -9,6 +9,7 @@ import {
9
9
  IconRendererProvider,
10
10
  RootContainer,
11
11
  } from './context'
12
+ import { ReducedGraphicsContext } from './hooks/useIsReducedGraphics'
12
13
  import { LikeC4CustomColors } from './LikeC4CustomColors'
13
14
  import { type LikeC4DiagramEventHandlers, type LikeC4DiagramProperties } from './LikeC4Diagram.props'
14
15
  import type { Types } from './likec4diagram/types'
@@ -41,6 +42,7 @@ export function LikeC4Diagram({
41
42
  initialWidth,
42
43
  initialHeight,
43
44
  experimentalEdgeEditing = false,
45
+ reduceGraphics = 'auto',
44
46
  onCanvasClick,
45
47
  onCanvasContextMenu,
46
48
  onCanvasDblClick,
@@ -81,6 +83,11 @@ export function LikeC4Diagram({
81
83
  }
82
84
  }
83
85
 
86
+ // If view has more then 3000 * 2000 pixels - assume it is a big diagram
87
+ const isReducedGraphicsMode = reduceGraphics === 'auto'
88
+ ? (view.bounds?.width ?? 1) * (view.bounds?.height ?? 1) > 6_000_000
89
+ : reduceGraphics
90
+
84
91
  return (
85
92
  <EnsureMantine>
86
93
  <FramerMotionConfig>
@@ -119,30 +126,32 @@ export function LikeC4Diagram({
119
126
  onOpenSource,
120
127
  onBurgerMenuClick,
121
128
  }}>
122
- <RootContainer className={className}>
123
- {!isEmpty(view.customColorDefinitions) && (
124
- <LikeC4CustomColors customColors={view.customColorDefinitions} />
125
- )}
126
- <XYFlowProvider
127
- fitView={fitView}
128
- {...initialRef.current}
129
- >
130
- <DiagramActorProvider
131
- input={{
132
- view,
133
- pannable,
134
- zoomable,
135
- fitViewPadding,
136
- ...xyNodesEdges,
137
- }}>
138
- <LikeC4DiagramXYFlow
139
- nodesDraggable={nodesDraggable}
140
- nodesSelectable={nodesSelectable}
141
- background={background}
142
- />
143
- </DiagramActorProvider>
144
- </XYFlowProvider>
145
- </RootContainer>
129
+ <ReducedGraphicsContext value={isReducedGraphicsMode}>
130
+ <RootContainer className={className}>
131
+ {!isEmpty(view.customColorDefinitions) && (
132
+ <LikeC4CustomColors customColors={view.customColorDefinitions} />
133
+ )}
134
+ <XYFlowProvider
135
+ fitView={fitView}
136
+ {...initialRef.current}
137
+ >
138
+ <DiagramActorProvider
139
+ input={{
140
+ view,
141
+ pannable,
142
+ zoomable,
143
+ fitViewPadding,
144
+ ...xyNodesEdges,
145
+ }}>
146
+ <LikeC4DiagramXYFlow
147
+ nodesDraggable={nodesDraggable}
148
+ nodesSelectable={nodesSelectable}
149
+ background={background}
150
+ />
151
+ </DiagramActorProvider>
152
+ </XYFlowProvider>
153
+ </RootContainer>
154
+ </ReducedGraphicsContext>
146
155
  </DiagramEventHandlers>
147
156
  </DiagramFeatures>
148
157
  </IconRendererProvider>
@@ -8,9 +8,11 @@ import {
8
8
  useStoreApi,
9
9
  } from '@xyflow/react'
10
10
  import clsx from 'clsx'
11
+ import { shallowEqual } from 'fast-equals'
11
12
  import { useMemo } from 'react'
12
13
  import type { SetRequired, Simplify } from 'type-fest'
13
- import { useUpdateEffect } from '../hooks'
14
+ import { useIsReducedGraphics } from '../hooks/useIsReducedGraphics'
15
+ import { useUpdateEffect } from '../hooks/useUpdateEffect'
14
16
  import { useIsZoomTooSmall } from '../hooks/useXYFlow'
15
17
  import * as css from '../LikeC4Diagram.css'
16
18
  import { stopPropagation } from '../utils/xyflow'
@@ -41,10 +43,8 @@ export type BaseXYFlowProps<NodeType extends Base.Node, EdgeType extends Base.Ed
41
43
  | 'zoomActivationKeyCode'
42
44
  | 'zoomOnScroll'
43
45
  | 'elementsSelectable'
44
- | 'onNodeMouseEnter'
45
- | 'onNodeMouseLeave'
46
- | 'onEdgeMouseEnter'
47
- | 'onEdgeMouseLeave'
46
+ | 'onNodeDoubleClick'
47
+ | 'onEdgeDoubleClick'
48
48
  | 'fitViewOptions'
49
49
  >,
50
50
  // Required props
@@ -79,6 +79,7 @@ export const BaseXYFlow = <
79
79
  }: BaseXYFlowProps<NodeType, EdgeType>) => {
80
80
  const isBgWithPattern = background !== 'transparent' && background !== 'solid'
81
81
  const isZoomTooSmall = useIsZoomTooSmall()
82
+ const reduceGraphics = useIsReducedGraphics()
82
83
  return (
83
84
  <ReactFlow<NodeType, EdgeType>
84
85
  colorMode={colorMode}
@@ -90,7 +91,12 @@ export const BaseXYFlow = <
90
91
  background === 'transparent' && css.cssTransparentBg,
91
92
  className,
92
93
  )}
93
- data-likec4-zoom-small={isZoomTooSmall}
94
+ {...isZoomTooSmall && {
95
+ ['data-likec4-zoom-small']: true,
96
+ }}
97
+ {...reduceGraphics && {
98
+ ['data-likec4-reduced-graphics']: true,
99
+ }}
94
100
  zoomOnPinch={zoomable}
95
101
  zoomOnScroll={!pannable && zoomable}
96
102
  {...(!zoomable && {
@@ -115,8 +121,7 @@ export const BaseXYFlow = <
115
121
  {...(!pannable && {
116
122
  selectionKeyCode: null,
117
123
  })}
118
- // TODO: benchmarks first
119
- //onlyRenderVisibleElements={nodes.length > 75}
124
+ onlyRenderVisibleElements={reduceGraphics}
120
125
  elementsSelectable={nodesSelectable}
121
126
  nodesFocusable={nodesDraggable || nodesSelectable}
122
127
  edgesFocusable={false}
@@ -202,7 +207,7 @@ const ViewportResizeHanlder = ({
202
207
  }: {
203
208
  onViewportResize: () => void
204
209
  }) => {
205
- const { width, height } = useStore(selectDimensions)
210
+ const { width, height } = useStore(selectDimensions, shallowEqual)
206
211
  useUpdateEffect(onViewportResize, [width, height])
207
212
 
208
213
  return <></>
package/src/base/const.ts CHANGED
@@ -5,5 +5,5 @@ export const ZIndexes = {
5
5
  Max: 8,
6
6
  } as const
7
7
 
8
- export const MinZoom = 0.1
8
+ export const MinZoom = 0.05
9
9
  export const MaxZoom = 2
@@ -1,4 +1,5 @@
1
1
  import { style } from '@vanilla-extract/css'
2
+ import { reactFlowReducedGraphics } from '../../../LikeC4Diagram.css'
2
3
  import { transitions, vars } from '../../../theme-vars'
3
4
 
4
5
  export const container = style({
@@ -45,11 +46,16 @@ export const actionIcon = style({
45
46
  },
46
47
  [`:where([data-hovered='true']) &`]: {
47
48
  opacity: 1,
49
+ transitionDelay: '100ms',
50
+ transitionDuration: '300ms',
48
51
  // boxShadow: '1px 1px 3px 0px rgba(0, 0, 0, 0.15)',
49
52
  // color: `var(--ai-color-hover)`,
50
53
  // vars: {
51
54
  // '--ai-bg': `var(--ai-bg-hover)`,
52
55
  // },
53
56
  },
57
+ [`${reactFlowReducedGraphics} &`]: {
58
+ boxShadow: 'none',
59
+ },
54
60
  },
55
61
  })
@@ -1,4 +1,5 @@
1
1
  import { createVar, globalStyle, style } from '@vanilla-extract/css'
2
+ import { reactFlowReducedGraphics } from '../../../LikeC4Diagram.css'
2
3
  import { vars } from '../../../theme-vars'
3
4
 
4
5
  export const container = style({
@@ -47,6 +48,9 @@ export const actionIcon = style({
47
48
  '--ai-bg': `color-mix(in srgb , ${vars.element.fill}, transparent 99%)`,
48
49
  },
49
50
  },
51
+ [`${reactFlowReducedGraphics} &`]: {
52
+ boxShadow: 'none',
53
+ },
50
54
  },
51
55
  })
52
56