@likec4/diagram 1.59.1 → 1.59.3

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 (73) hide show
  1. package/README.md +53 -0
  2. package/dist/LikeC4Styles.js +2 -1
  3. package/dist/base/BaseXYFlow.js +8 -1
  4. package/dist/base-primitives/compound/CompoundNodeContainer.js +2 -3
  5. package/dist/base-primitives/compound/CompoundTitle.js +5 -4
  6. package/dist/base-primitives/element/ElementData.js +6 -1
  7. package/dist/base-primitives/element/ElementShape.js +9 -10
  8. package/dist/context/CurrentViewModelProvider.js +2 -1
  9. package/dist/context/DiagramEventHandlers.js +2 -1
  10. package/dist/context/IconRenderer.js +42 -26
  11. package/dist/context/TagStylesContext.d.ts +2 -1
  12. package/dist/context/TagStylesContext.js +32 -20
  13. package/dist/custom/index.js +1 -1
  14. package/dist/editor/LikeC4EditorCallbacks.d.ts +20 -1
  15. package/dist/editor/LikeC4EditorCallbacks.js +4 -1
  16. package/dist/editor/actor/setup.d.ts +1 -0
  17. package/dist/editor/actor/setup.js +17 -12
  18. package/dist/editor/actor/state.sync-queue.js +1 -0
  19. package/dist/editor/applyChangesToManualLayout.js +1 -1
  20. package/dist/editor/index.d.ts +1 -1
  21. package/dist/editor/useEditorActorLogic.js +10 -3
  22. package/dist/hooks/useDiagram.js +4 -2
  23. package/dist/hooks/useDiagramCompareLayout.d.ts +13 -3
  24. package/dist/hooks/useDiagramCompareLayout.js +6 -1
  25. package/dist/hooks/useUpdateEffect.js +2 -1
  26. package/dist/index.d.ts +2 -1
  27. package/dist/index.js +3 -2
  28. package/dist/likec4diagram/DiagramUI.js +4 -3
  29. package/dist/likec4diagram/DiagramXYFlow.d.ts +1 -0
  30. package/dist/likec4diagram/DiagramXYFlow.js +14 -7
  31. package/dist/likec4diagram/custom/edges/RelationshipEdge.js +1 -3
  32. package/dist/likec4diagram/custom/edges/SequenceStepEdge.js +1 -3
  33. package/dist/likec4diagram/custom/edges/useRelationshipEdgePath.js +3 -2
  34. package/dist/likec4diagram/custom/nodes/nodes.js +6 -3
  35. package/dist/likec4diagram/custom/nodes/toolbar/Toolbar.js +4 -3
  36. package/dist/likec4diagram/relationship-popover/RelationshipPopover.js +1 -2
  37. package/dist/likec4diagram/state/assign.js +1 -1
  38. package/dist/likec4diagram/state/createViewChange.js +1 -6
  39. package/dist/likec4diagram/state/machine.actions.js +6 -2
  40. package/dist/likec4diagram/state/machine.setup.js +9 -8
  41. package/dist/likec4diagram/state/machine.state.ready.walkthrough.js +1 -1
  42. package/dist/likec4diagram/state/utils.js +7 -3
  43. package/dist/likec4diagram/ui/FloatingSequenceActors.js +2 -1
  44. package/dist/likec4diagram/ui/sequence-outline/SequenceOutlinePanel.js +4 -2
  45. package/dist/likec4diagram/useLayoutConstraints.js +19 -4
  46. package/dist/likec4diagram/xyflow-sequence/layouter.js +8 -10
  47. package/dist/navigationpanel/NavigationPanelDropdown.js +2 -1
  48. package/dist/navigationpanel/controls/DetailsControls.js +2 -1
  49. package/dist/navigationpanel/editorpanel/ApplySemanticLayout.js +2 -1
  50. package/dist/navigationpanel/editorpanel/ChangeAutoLayoutButton.js +2 -1
  51. package/dist/navigationpanel/editorpanel/ManualLayoutToolsButton.d.ts +2 -0
  52. package/dist/navigationpanel/editorpanel/ManualLayoutToolsButton.js +11 -3
  53. package/dist/navigationpanel/walkthrough/DynamicViewControls.js +1 -3
  54. package/dist/overlays/element-details/ElementDetailsCard.js +2 -2
  55. package/dist/overlays/overlaysActor.js +16 -17
  56. package/dist/overlays/relationship-details/RelationshipDetails.js +2 -1
  57. package/dist/overlays/relationship-details/hooks.js +2 -1
  58. package/dist/overlays/relationships-browser/RelationshipsBrowser.js +4 -2
  59. package/dist/overlays/relationships-browser/actor.js +2 -1
  60. package/dist/overlays/relationships-browser/custom/ElementActions.js +39 -38
  61. package/dist/overlays/relationships-browser/hooks.js +2 -1
  62. package/dist/projects-overview/ProjectsOverview.js +8 -7
  63. package/dist/projects-overview/hooks.js +2 -1
  64. package/dist/search/components/ViewsColum.js +10 -8
  65. package/dist/search/hooks.js +2 -1
  66. package/dist/shadowroot/ShadowRoot.js +4 -2
  67. package/dist/styles2.js +1 -1
  68. package/dist/utils/index.js +2 -2
  69. package/package.json +27 -27
  70. package/panda.buildinfo.json +2 -1
  71. package/styles-min.css +1 -1
  72. package/styles-xyflow.css +1 -1
  73. package/styles.css +1 -1
package/README.md CHANGED
@@ -272,6 +272,59 @@ function App() {
272
272
  }
273
273
  ```
274
274
 
275
+ ### Manual layout controls
276
+
277
+ Use `useDiagramCompareLayout` in a component rendered inside `LikeC4Diagram`
278
+ to place the built-in manual-layout behavior in custom application chrome. The
279
+ hook provides comparison availability and activity, drift reasons, the current
280
+ layout type, whether the latest model can be applied, and the corresponding
281
+ actions.
282
+
283
+ ```tsx
284
+ import { LikeC4Diagram, useDiagramCompareLayout } from '@likec4/diagram'
285
+
286
+ function LayoutControls() {
287
+ const [state, actions] = useDiagramCompareLayout()
288
+
289
+ if (!state.isEnabled) {
290
+ return null
291
+ }
292
+
293
+ return (
294
+ <div>
295
+ <button onClick={() => actions.toggleCompare()}>
296
+ {state.isActive ? 'Stop comparing' : 'Compare with latest'}
297
+ </button>
298
+ <button
299
+ disabled={state.layout === 'auto' ||
300
+ !state.canApplyLatest ||
301
+ !state.hasEditor ||
302
+ !state.isEditable}
303
+ onClick={actions.applyLatestToManual}
304
+ >
305
+ Apply latest
306
+ </button>
307
+ <button onClick={actions.resetManualLayout}>Reset manual layout</button>
308
+ </div>
309
+ )
310
+ }
311
+
312
+ function App() {
313
+ return (
314
+ <LikeC4Diagram
315
+ view={view}
316
+ onLayoutTypeChange={setLayoutType}
317
+ >
318
+ <LayoutControls />
319
+ </LikeC4Diagram>
320
+ )
321
+ }
322
+ ```
323
+
324
+ The hook reuses the same controller as LikeC4's built-in compare panel. It
325
+ requires comparison to be enabled for the diagram and an editor provider for
326
+ actions that modify a saved manual layout.
327
+
275
328
  ## Customization
276
329
 
277
330
  ### Custom node renderer
@@ -47,10 +47,11 @@ function LikeC4Styles({ id }) {
47
47
  const rootSelector = `#${id}`;
48
48
  const nonce = useMantineStyleNonce()?.();
49
49
  const $styles = useLikeC4Styles();
50
+ const colorsStyles = useMemo(() => generateBuiltInColorStyles($styles, rootSelector), [rootSelector, $styles.fingerprint]);
50
51
  return /* @__PURE__ */ jsx(MemoizedStyle, {
51
52
  id,
52
53
  nonce,
53
- colorsStyles: useMemo(() => generateBuiltInColorStyles($styles, rootSelector), [rootSelector, $styles.fingerprint])
54
+ colorsStyles
54
55
  });
55
56
  }
56
57
  /**
@@ -114,6 +114,12 @@ function BaseXYFlow({ nodes, edges, onEdgesChange, onNodesChange, className, pan
114
114
  onNodesChange,
115
115
  onEdgesChange,
116
116
  onMoveEnd: useCallbackRef((event, { x, y, zoom }) => {
117
+ /**
118
+ * WORKAROUND
119
+ * Viewport transform is not rounded to integers which results in blurry nodes on some resolution
120
+ * https://github.com/xyflow/xyflow/issues/3282
121
+ * https://github.com/likec4/likec4/issues/734
122
+ */
117
123
  const roundedX = roundDpr(x);
118
124
  const roundedY = roundDpr(y);
119
125
  if (x !== roundedX || y !== roundedY) xystore.setState({ transform: [
@@ -190,7 +196,8 @@ function BaseXYFlow({ nodes, edges, onEdgesChange, onNodesChange, className, pan
190
196
  }
191
197
  var selectDimensions = ({ width, height }) => (width || 1) * (height || 1);
192
198
  var ViewportResizeHanlder = ({ onViewportResize }) => {
193
- useUpdateEffect(onViewportResize, [useStore(selectDimensions)]);
199
+ const square = useStore(selectDimensions);
200
+ useUpdateEffect(onViewportResize, [square]);
194
201
  return null;
195
202
  };
196
203
  //#endregion
@@ -13,10 +13,9 @@ function CompoundNodeContainer({ nodeProps: { data: { hovered: isHovered = false
13
13
  max: 100
14
14
  });
15
15
  const isTransparent = opacity < 98;
16
- const MIN_OPACITY = 50;
17
- const borderOpacity = MIN_OPACITY + clamp((100 - MIN_OPACITY) * (opacity / 100), {
16
+ const borderOpacity = 50 + clamp(50 * (opacity / 100), {
18
17
  min: 0,
19
- max: 100 - MIN_OPACITY
18
+ max: 50
20
19
  });
21
20
  const compoundClass = compoundNode({
22
21
  isTransparent,
@@ -3,13 +3,14 @@ import { IconRenderer } from "../../context/IconRenderer.js";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/base-primitives/compound/CompoundTitle.tsx
5
5
  function CompoundTitle({ data, "aria-hidden": ariaHidden }) {
6
+ const elementIcon = IconRenderer({
7
+ element: data,
8
+ className: "likec4-compound-icon"
9
+ });
6
10
  return /* @__PURE__ */ jsxs("div", {
7
11
  className: "likec4-compound-title-container",
8
12
  "aria-hidden": ariaHidden,
9
- children: [IconRenderer({
10
- element: data,
11
- className: "likec4-compound-icon"
12
- }), /* @__PURE__ */ jsx("div", {
13
+ children: [elementIcon, /* @__PURE__ */ jsx("div", {
13
14
  className: "likec4-compound-title",
14
15
  children: data.title
15
16
  })]
@@ -22,16 +22,21 @@ var resolveIconColor = (styles, data) => {
22
22
  const colors = styles.colors(iconColor).elements;
23
23
  return iconColor === data.color ? colors.stroke : colors.fill;
24
24
  };
25
+ function shouldBlendIconWithPalette(icon, hasIconColor) {
26
+ return !hasIconColor && icon?.startsWith("bootstrap:") === true;
27
+ }
25
28
  var Root = forwardRef(({ className, style, data, ...props }, ref) => {
26
29
  const styles = useLikeC4Styles();
27
30
  const iconSize = data.style.iconSize ? styles.nodeSizes(data.style).values.iconSize : void 0;
28
31
  const resolvedIconColor = resolveIconColor(styles, data);
32
+ const hasIconColor = !!resolvedIconColor;
29
33
  return /* @__PURE__ */ jsx("div", {
30
34
  ...props,
31
35
  ref,
32
36
  className: cx(className, elementNodeData({
33
37
  iconPosition: data.style.iconPosition,
34
- withIconColor: !!resolvedIconColor
38
+ withIconColor: hasIconColor,
39
+ withIconBlend: shouldBlendIconWithPalette(data.icon, hasIconColor)
35
40
  }), "likec4-element"),
36
41
  style: {
37
42
  ...style,
@@ -22,7 +22,8 @@ function cylinderSVGPath(diameter, height, tilt = .07) {
22
22
  };
23
23
  }
24
24
  function docSVGPath(width, height) {
25
- const baseY = roundDpr(height - height / 8 / 2);
25
+ const waveHeight = height / 8;
26
+ const baseY = roundDpr(height - waveHeight / 2);
26
27
  const amplitude = roundDpr(height / 6);
27
28
  const radius = 6;
28
29
  return { path: `
@@ -302,15 +303,13 @@ function ShapeSvgOutline({ shape, w, h }) {
302
303
  children: /* @__PURE__ */ jsx("path", { d: cylinderSVGPath(w + 6, h + 6).path })
303
304
  });
304
305
  break;
305
- default:
306
- svg = /* @__PURE__ */ jsx("rect", {
307
- x: -3,
308
- y: -3,
309
- width: w + 6,
310
- height: h + 6,
311
- rx: 8
312
- });
313
- break;
306
+ default: svg = /* @__PURE__ */ jsx("rect", {
307
+ x: -3,
308
+ y: -3,
309
+ width: w + 6,
310
+ height: h + 6,
311
+ rx: 8
312
+ });
314
313
  }
315
314
  return /* @__PURE__ */ jsx("g", {
316
315
  className: "likec4-shape-outline",
@@ -5,8 +5,9 @@ import { jsx } from "react/jsx-runtime";
5
5
  //#region src/context/CurrentViewModelProvider.tsx
6
6
  function CurrentViewModelProvider({ children }) {
7
7
  const viewId = useCurrentViewId();
8
+ const viewmodel = useOptionalLikeC4Model()?.findView(viewId) ?? null;
8
9
  return /* @__PURE__ */ jsx(CurrentViewModelContext, {
9
- value: useOptionalLikeC4Model()?.findView(viewId) ?? null,
10
+ value: viewmodel,
10
11
  children
11
12
  });
12
13
  }
@@ -26,13 +26,14 @@ var DiagramEventHandlersReactContext = createContext({
26
26
  });
27
27
  function DiagramEventHandlers({ handlers, children }) {
28
28
  const handlersRef = useSyncedRef(handlers);
29
+ const deps = HandlerNames.map((name) => isFunction(handlers[name]));
29
30
  const value = useMemo(() => ({
30
31
  ...mapToObj(HandlerNames, (name) => {
31
32
  if (handlersRef.current[name]) return [name, (...args) => handlersRef.current[name]?.(...args)];
32
33
  return [name, null];
33
34
  }),
34
35
  handlersRef
35
- }), [handlersRef, ...HandlerNames.map((name) => isFunction(handlers[name]))]);
36
+ }), [handlersRef, ...deps]);
36
37
  return /* @__PURE__ */ jsx(DiagramEventHandlersRefContext.Provider, {
37
38
  value: handlersRef,
38
39
  children: /* @__PURE__ */ jsx(DiagramEventHandlersReactContext.Provider, {
@@ -26,23 +26,6 @@ function IconRendererProvider({ value, children }) {
26
26
  children
27
27
  });
28
28
  }
29
- /**
30
- * Attempts to extract and decode SVG content from a data URL.
31
- * Returns the decoded SVG string if successful, or null if not an SVG data URL.
32
- */
33
- function decodeSvgDataUrl(dataUrl) {
34
- if (!dataUrl.startsWith("data:image/svg+xml")) return null;
35
- try {
36
- if (dataUrl.includes(";base64,")) {
37
- const base64Content = dataUrl.split(";base64,")[1];
38
- if (base64Content) return atob(base64Content);
39
- } else {
40
- const encodedContent = dataUrl.split(",")[1];
41
- if (encodedContent) return decodeURIComponent(encodedContent);
42
- }
43
- } catch {}
44
- return null;
45
- }
46
29
  function IconRenderer({ element, className, style }) {
47
30
  const RenderIcon = useContext(IconRendererContext);
48
31
  if (!element || !element.icon || element.icon === "none") return null;
@@ -65,9 +48,7 @@ function IconRenderer({ element, className, style }) {
65
48
  case !!RenderIcon:
66
49
  icon = /* @__PURE__ */ jsx(RenderIcon, { node: element });
67
50
  break;
68
- default:
69
- icon = null;
70
- break;
51
+ default: icon = null;
71
52
  }
72
53
  if (!icon) return null;
73
54
  return /* @__PURE__ */ jsx("div", {
@@ -104,14 +85,49 @@ function IconOrShapeRenderer({ element, className, style }) {
104
85
  style
105
86
  });
106
87
  }
107
- function SvgDataUrlIcon({ dataUrl, ...props }) {
108
- const svgContent = useMemo(() => decodeSvgDataUrl(dataUrl), [dataUrl]);
109
- if (!svgContent) return null;
88
+ function SvgDataUrlIcon({ dataUrl, alt }) {
89
+ if (!useMemo(() => hasCurrentColorReference(dataUrl), [dataUrl])) return /* @__PURE__ */ jsx("img", {
90
+ src: dataUrl,
91
+ alt
92
+ });
93
+ const maskUrl = `url(${JSON.stringify(dataUrl)})`;
110
94
  return /* @__PURE__ */ jsx("span", {
111
- ...props,
112
- style: { display: "contents" },
113
- dangerouslySetInnerHTML: { __html: svgContent }
95
+ role: alt ? "img" : void 0,
96
+ "aria-label": alt,
97
+ style: {
98
+ display: "inline-block",
99
+ width: "100%",
100
+ height: "100%",
101
+ backgroundColor: "currentColor",
102
+ maskImage: maskUrl,
103
+ maskRepeat: "no-repeat",
104
+ maskPosition: "center",
105
+ maskSize: "contain",
106
+ WebkitMaskImage: maskUrl,
107
+ WebkitMaskRepeat: "no-repeat",
108
+ WebkitMaskPosition: "center",
109
+ WebkitMaskSize: "contain"
110
+ }
114
111
  });
115
112
  }
113
+ function hasCurrentColorReference(dataUrl) {
114
+ return /currentcolor/i.test(decodeSvgDataUrl(dataUrl) ?? "");
115
+ }
116
+ function decodeSvgDataUrl(dataUrl) {
117
+ if (!dataUrl.startsWith("data:image/svg+xml")) return null;
118
+ try {
119
+ const comma = dataUrl.indexOf(",");
120
+ if (comma === -1) return null;
121
+ const payload = dataUrl.slice(comma + 1);
122
+ if (dataUrl.slice(0, comma).endsWith(";base64")) return new TextDecoder().decode(Uint8Array.from(atob(payload), (c) => c.charCodeAt(0)));
123
+ try {
124
+ return decodeURIComponent(payload);
125
+ } catch {
126
+ return payload;
127
+ }
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
116
132
  //#endregion
117
133
  export { IconOrShapeRenderer, IconRenderer, IconRendererProvider };
@@ -1,6 +1,7 @@
1
1
  import { TagSpecification } from '@likec4/core';
2
+ import { LikeC4Styles } from '@likec4/core/styles';
2
3
  import { PropsWithChildren } from 'react';
3
- export declare function generateColorVars(spec: TagSpecification): string;
4
+ export declare function generateColorVars(spec: TagSpecification, styles?: LikeC4Styles): string;
4
5
  export declare function TagStylesProvider({ children, id }: PropsWithChildren<{
5
6
  id: string;
6
7
  }>): import("react").JSX.Element;
@@ -1,49 +1,61 @@
1
1
  import "../_virtual/_rolldown/runtime.js";
2
2
  import { useLikeC4Specification } from "../hooks/useLikeC4Model.js";
3
+ import { useLikeC4Styles } from "../hooks/useLikeC4Styles.js";
3
4
  import { createContext, memo } from "react";
4
5
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
6
  import { entries, flatMap, isEmpty, join, pipe } from "remeda";
6
7
  import { isTagColorSpecified } from "@likec4/core";
7
8
  import { useMantineStyleNonce } from "@mantine/core";
8
- import { DefaultTagColors, getContrastedColorsAPCA, isValidColor } from "@likec4/core/styles";
9
+ import { DefaultTagColors, LikeC4Styles, getContrastedColorsAPCA, isValidColor } from "@likec4/core/styles";
9
10
  //#region src/context/TagStylesContext.tsx
10
11
  var radixColors = DefaultTagColors;
11
- function generateColorVars(spec) {
12
+ function generateColorVars(spec, styles = LikeC4Styles.DEFAULT) {
12
13
  const color = spec.color;
13
14
  if (isTagColorSpecified(spec) && isValidColor(color)) return `
14
15
  --colors-likec4-tag-bg: ${color};
15
16
  --colors-likec4-tag-bg-hover: color-mix(in oklab, ${color}, var(--colors-likec4-mix-color) 20%);
16
17
  --colors-likec4-tag-text: ${getContrastedColorsAPCA(color).hiContrast};
17
18
  `;
18
- if (!radixColors.includes(color)) return "";
19
- let textcolor = `var(--colors-${color}-12)`;
20
- if ([
21
- "mint",
22
- "grass",
23
- "lime",
24
- "yellow",
25
- "amber"
26
- ].includes(color)) textcolor = "rgba(0 0 0 / 0.85)";
27
- return `
28
- --colors-likec4-tag-border: var(--colors-${color}-8);
29
- --colors-likec4-tag-bg: var(--colors-${color}-9);
30
- --colors-likec4-tag-bg-hover: var(--colors-${color}-10);
31
- --colors-likec4-tag-text: ${textcolor};
32
- `;
19
+ if (radixColors.includes(color)) {
20
+ let textcolor = `var(--colors-${color}-12)`;
21
+ if ([
22
+ "mint",
23
+ "grass",
24
+ "lime",
25
+ "yellow",
26
+ "amber"
27
+ ].includes(color)) textcolor = "rgba(0 0 0 / 0.85)";
28
+ return `
29
+ --colors-likec4-tag-border: var(--colors-${color}-8);
30
+ --colors-likec4-tag-bg: var(--colors-${color}-9);
31
+ --colors-likec4-tag-bg-hover: var(--colors-${color}-10);
32
+ --colors-likec4-tag-text: ${textcolor};
33
+ `;
34
+ }
35
+ if (styles.isThemeColor(color)) {
36
+ const { fill, hiContrast } = styles.tagColor(color);
37
+ return `
38
+ --colors-likec4-tag-bg: ${fill};
39
+ --colors-likec4-tag-bg-hover: color-mix(in oklab, ${fill}, var(--colors-likec4-mix-color) 20%);
40
+ --colors-likec4-tag-text: ${hiContrast};
41
+ `;
42
+ }
43
+ return "";
33
44
  }
34
45
  var TagStylesContext = createContext({});
35
- function generateStylesheet(tags, rootSelector) {
46
+ function generateStylesheet(tags, rootSelector, styles) {
36
47
  if (!tags || isEmpty(tags)) return "";
37
48
  return pipe(entries(tags), flatMap(([tag, spec]) => [
38
49
  `:is(${rootSelector} [data-likec4-tag="${tag}"]) {`,
39
- generateColorVars(spec),
50
+ generateColorVars(spec, styles),
40
51
  "}"
41
52
  ]), join("\n"));
42
53
  }
43
54
  function TagStylesProvider({ children, id }) {
44
55
  const tags = useLikeC4Specification().tags;
56
+ const styles = useLikeC4Styles();
45
57
  const nonce = useMantineStyleNonce()?.();
46
- const stylesheet = generateStylesheet(tags, `#${id}`);
58
+ const stylesheet = generateStylesheet(tags, `#${id}`, styles);
47
59
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [stylesheet !== "" && /* @__PURE__ */ jsx(TagStylesheet, {
48
60
  nonce,
49
61
  stylesheet
@@ -9,10 +9,10 @@ import { useDiagramContext, useOnDiagramEvent } from "../hooks/useDiagram.js";
9
9
  import { useCurrentView, useCurrentViewId } from "../hooks/useCurrentView.js";
10
10
  import { useOptionalCurrentViewModel, useOptionalLikeC4Model } from "../context/LikeC4ModelContext.js";
11
11
  import { useLikeC4Model, useLikeC4Specification, useLikeC4ViewModel } from "../hooks/useLikeC4Model.js";
12
+ import { useLikeC4Styles } from "../hooks/useLikeC4Styles.js";
12
13
  import { ErrorBoundary, ErrorFallback } from "../components/ErrorFallback.js";
13
14
  import { useCurrentViewModel } from "../hooks/useCurrentViewModel.js";
14
15
  import { useHasProjects, useLikeC4Project, useLikeC4ProjectId, useLikeC4Projects, useLikeC4ProjectsContext } from "../hooks/useLikeC4Project.js";
15
- import { useLikeC4Styles } from "../hooks/useLikeC4Styles.js";
16
16
  import { CompoundActionButton } from "../base-primitives/compound/CompoundActionButton.js";
17
17
  import { CompoundDetailsButton } from "../base-primitives/compound/CompoundDetailsButton.js";
18
18
  import { CompoundNodeContainer } from "../base-primitives/compound/CompoundNodeContainer.js";
@@ -1,4 +1,19 @@
1
1
  import type * as t from '@likec4/core/types';
2
+ /**
3
+ * Result of applying a change from the LikeC4 Editor.
4
+ */
5
+ export interface LikeC4EditorChangeResult {
6
+ /**
7
+ * Whether applying the change will publish a refreshed view to the diagram
8
+ * that the editor should wait for.
9
+ *
10
+ * Returning no result from `handleChange` defaults this to `true`,
11
+ * preserving the existing behavior for integrations that persist changes
12
+ * through an external model service.
13
+ */
14
+ waitForViewSync: boolean;
15
+ }
16
+ export declare function shouldWaitForViewSync(result?: void | LikeC4EditorChangeResult): boolean;
2
17
  /**
3
18
  * Callbacks from LikeC4 Editor.
4
19
  */
@@ -17,7 +32,11 @@ export interface LikeC4EditorCallbacks {
17
32
  fetchView(viewId: t.ViewId, layout?: t.LayoutType): t.LayoutedView | Promise<t.LayoutedView>;
18
33
  /**
19
34
  * Callback invoked when the view changes.
35
+ *
36
+ * Return `{ waitForViewSync: false }` when the integration applies the
37
+ * change locally and will not publish a refreshed view. When changes are
38
+ * batched, the editor waits if any callback invocation expects a refresh.
20
39
  */
21
- handleChange(viewId: t.ViewId, change: t.ViewChange): void | Promise<void>;
40
+ handleChange(viewId: t.ViewId, change: t.ViewChange): void | LikeC4EditorChangeResult | Promise<void | LikeC4EditorChangeResult>;
22
41
  }
23
42
  export declare function createLikeC4Editor(callbacks: LikeC4EditorCallbacks): LikeC4EditorCallbacks;
@@ -1,7 +1,10 @@
1
1
  import "../_virtual/_rolldown/runtime.js";
2
2
  //#region src/editor/LikeC4EditorCallbacks.tsx
3
+ function shouldWaitForViewSync(result) {
4
+ return result?.waitForViewSync ?? true;
5
+ }
3
6
  function createLikeC4Editor(callbacks) {
4
7
  return callbacks;
5
8
  }
6
9
  //#endregion
7
- export { createLikeC4Editor };
10
+ export { createLikeC4Editor, shouldWaitForViewSync };
@@ -39,6 +39,7 @@ export declare namespace EditorCalls {
39
39
  type Output = {
40
40
  requested: t.ViewChange[];
41
41
  applied: t.ViewChange[];
42
+ waitForViewSync: boolean;
42
43
  };
43
44
  }
44
45
  }
@@ -2,6 +2,22 @@ import "../../_virtual/_rolldown/runtime.js";
2
2
  import { defineActors } from "../../utils/defineActors.js";
3
3
  import { hotkey } from "./hotkey.js";
4
4
  import { fromPromise, setup } from "xstate";
5
+ //#region src/editor/actor/setup.ts
6
+ var applyLatest = fromPromise(() => {
7
+ throw new Error("Not implemented");
8
+ });
9
+ var executeChange = fromPromise(() => {
10
+ throw new Error("Not implemented");
11
+ });
12
+ var applySemanticLayout = fromPromise(() => {
13
+ throw new Error("Not implemented");
14
+ });
15
+ var actors = defineActors({
16
+ hotkey,
17
+ applyLatest,
18
+ executeChange,
19
+ applySemanticLayout
20
+ });
5
21
  var machine = setup({
6
22
  types: {
7
23
  context: {},
@@ -11,18 +27,7 @@ var machine = setup({
11
27
  children: {},
12
28
  tags: ""
13
29
  },
14
- actors: defineActors({
15
- hotkey,
16
- applyLatest: fromPromise(() => {
17
- throw new Error("Not implemented");
18
- }),
19
- executeChange: fromPromise(() => {
20
- throw new Error("Not implemented");
21
- }),
22
- applySemanticLayout: fromPromise(() => {
23
- throw new Error("Not implemented");
24
- })
25
- }),
30
+ actors,
26
31
  delays: {
27
32
  "500ms": 500,
28
33
  "wait-after-edit": 1e3
@@ -229,6 +229,7 @@ var process = machine.createStateConfig({
229
229
  type: "update.view-bounds",
230
230
  bounds: lastSyncSnapshot.layout.bounds
231
231
  });
232
+ if (!event.output.waitForViewSync) enqueue.raise({ type: "view.synched" });
232
233
  }),
233
234
  target: "waitViewSynced"
234
235
  },
@@ -153,7 +153,7 @@ function edgeControlPoints(source, target) {
153
153
  if (source === target) {
154
154
  const loopSize = 80;
155
155
  const centerOfTopBoundary = vector(0, source.height || 0).multiply(-.5).add(sourceCenter);
156
- return [centerOfTopBoundary.add(vector(-80 / 2.5, -80)).trunc().toObject(), centerOfTopBoundary.add(vector(loopSize / 2.5, -80)).trunc().toObject()];
156
+ return [centerOfTopBoundary.add(vector(-32, -80)).trunc().toObject(), centerOfTopBoundary.add(vector(loopSize / 2.5, -80)).trunc().toObject()];
157
157
  }
158
158
  const sourceToTargetVector = targetCenter.subtract(sourceCenter);
159
159
  const sourceBorderPoint = getBorderPointOnVector(source, sourceCenter, sourceToTargetVector);
@@ -1,5 +1,5 @@
1
1
  export type * from './actor/machine';
2
2
  export type * from './actor/types';
3
3
  export { createLikeC4Editor } from './LikeC4EditorCallbacks';
4
- export type { LikeC4EditorCallbacks } from './LikeC4EditorCallbacks';
4
+ export type { LikeC4EditorCallbacks, LikeC4EditorChangeResult, } from './LikeC4EditorCallbacks';
5
5
  export { EnsureEditorContext, LikeC4EditorProvider, useOptionalLikeC4Editor, } from './LikeC4EditorProvider';
@@ -1,5 +1,6 @@
1
1
  import "../_virtual/_rolldown/runtime.js";
2
2
  import { useCallbackRef } from "../hooks/useCallbackRef.js";
3
+ import { shouldWaitForViewSync } from "./LikeC4EditorCallbacks.js";
3
4
  import { useOptionalLikeC4Editor } from "./LikeC4EditorProvider.js";
4
5
  import { editorActorLogic } from "./actor/machine.js";
5
6
  import { applyChangesToManualLayout } from "./applyChangesToManualLayout.js";
@@ -18,7 +19,9 @@ function useEditorActorLogic() {
18
19
  console.error("No editor port available for applying latest to manual layout");
19
20
  throw new Error("No editor port");
20
21
  }
21
- return { updated: applyChangesToManualLayout(await promisify(() => current ?? port.fetchView(viewId, "manual")), await promisify(() => port.fetchView(viewId, "auto"))) };
22
+ const manual = await promisify(() => current ?? port.fetchView(viewId, "manual"));
23
+ const latest = await promisify(() => port.fetchView(viewId, "auto"));
24
+ return { updated: applyChangesToManualLayout(manual, latest) };
22
25
  });
23
26
  const executeChange = useCallbackRef(async ({ input }) => {
24
27
  if (!port) {
@@ -26,18 +29,22 @@ function useEditorActorLogic() {
26
29
  throw new Error("No editor port");
27
30
  }
28
31
  const applied = [];
32
+ let waitForViewSync = false;
29
33
  for (const change of input.changes) try {
30
- await promisify(() => port.handleChange(input.viewId, change));
34
+ const result = await promisify(() => port.handleChange(input.viewId, change));
31
35
  applied.push(change);
36
+ waitForViewSync ||= shouldWaitForViewSync(result);
32
37
  } catch (error) {
33
38
  console.error("Failed to execute change", {
34
39
  change,
35
40
  error
36
41
  });
42
+ waitForViewSync ||= shouldWaitForViewSync();
37
43
  }
38
44
  return {
39
45
  requested: input.changes,
40
- applied
46
+ applied,
47
+ waitForViewSync
41
48
  };
42
49
  });
43
50
  const applySemanticLayout = useCallbackRef(async ({ input }) => {
@@ -17,7 +17,8 @@ function selectDiagramActor(selector) {
17
17
  * @deprecated use {@link useDiagramSelector}
18
18
  */
19
19
  function useDiagramSnapshot(selector, compare = shallowEqual) {
20
- return useSelector(useDiagramActorRef(), selector, compare);
20
+ const actorRef = useDiagramActorRef();
21
+ return useSelector(actorRef, selector, compare);
21
22
  }
22
23
  /**
23
24
  * Helper to create a selector for diagram actor snapshot
@@ -33,7 +34,8 @@ function selectDiagramActorContext(selector) {
33
34
  function useDiagramContext(selector, compare = shallowEqual, deps = []) {
34
35
  const actorRef = useDiagramActorRef();
35
36
  const selectorRef = useCallbackRef(selector);
36
- return useSelector(actorRef, useCallback((s) => selectorRef(s.context), deps), compare);
37
+ const select = useCallback((s) => selectorRef(s.context), deps);
38
+ return useSelector(actorRef, select, compare);
37
39
  }
38
40
  /**
39
41
  * Subscribe to diagram emitted events
@@ -1,11 +1,11 @@
1
1
  import { DiagramActorSnapshot } from '../likec4diagram/state/types';
2
2
  import type * as t from '@likec4/core/types';
3
- declare const selectCompareLayoutState: ({ context }: DiagramActorSnapshot) => {
3
+ export declare const selectCompareLayoutState: ({ context }: DiagramActorSnapshot) => {
4
4
  isEnabled: false;
5
5
  hasEditor: false;
6
6
  isEditable: false;
7
7
  isActive: false;
8
- drifts: never;
8
+ drifts: null;
9
9
  canApplyLatest: boolean;
10
10
  layout: t.LayoutType;
11
11
  } | {
@@ -17,7 +17,13 @@ declare const selectCompareLayoutState: ({ context }: DiagramActorSnapshot) => {
17
17
  canApplyLatest: boolean;
18
18
  layout: t.LayoutType;
19
19
  };
20
+ /**
21
+ * Current manual-layout comparison state exposed by {@link useDiagramCompareLayout}.
22
+ */
20
23
  export type DiagramCompareLayoutState = ReturnType<typeof selectCompareLayoutState>;
24
+ /**
25
+ * Manual-layout actions exposed by {@link useDiagramCompareLayout}.
26
+ */
21
27
  export type DiagramCompareLayoutOps = {
22
28
  /**
23
29
  * Toggles the compare mode on or off.
@@ -36,8 +42,12 @@ export type DiagramCompareLayoutOps = {
36
42
  */
37
43
  resetManualLayout: () => void;
38
44
  };
45
+ /**
46
+ * Controls LikeC4's manual-layout comparison from custom diagram children.
47
+ *
48
+ * The hook must be called by a component rendered inside {@link LikeC4Diagram}.
49
+ */
39
50
  export declare function useDiagramCompareLayout(): [
40
51
  DiagramCompareLayoutState,
41
52
  DiagramCompareLayoutOps
42
53
  ];
43
- export {};
@@ -28,6 +28,11 @@ var selectCompareLayoutState = ({ context }) => {
28
28
  layout: context.view._layout ?? "auto"
29
29
  };
30
30
  };
31
+ /**
32
+ * Controls LikeC4's manual-layout comparison from custom diagram children.
33
+ *
34
+ * The hook must be called by a component rendered inside {@link LikeC4Diagram}.
35
+ */
31
36
  function useDiagramCompareLayout() {
32
37
  const actorRef = useDiagramActorRef();
33
38
  const state = useSelector(actorRef, selectCompareLayoutState, shallowEqual);
@@ -78,4 +83,4 @@ function useDiagramCompareLayout() {
78
83
  }];
79
84
  }
80
85
  //#endregion
81
- export { useDiagramCompareLayout };
86
+ export { selectCompareLayoutState, useDiagramCompareLayout };