likec4 0.44.2 → 0.46.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 (44) hide show
  1. package/dist/@likec4/core/utils/relations.js +11 -2
  2. package/dist/@likec4/diagrams/components/primitives/fullscreen/FullscreenDiagram.js +8 -7
  3. package/dist/@likec4/diagrams/diagram/Diagram.js +63 -45
  4. package/dist/@likec4/diagrams/diagram/Edges.js +32 -16
  5. package/dist/@likec4/diagrams/diagram/Nodes.js +66 -70
  6. package/dist/@likec4/diagrams/diagram/icons/ZoomIn.js +2 -3
  7. package/dist/@likec4/diagrams/diagram/shapes/Browser.js +43 -9
  8. package/dist/@likec4/diagrams/diagram/shapes/Compound.js +8 -10
  9. package/dist/@likec4/diagrams/diagram/shapes/Cylinder.js +3 -1
  10. package/dist/@likec4/diagrams/diagram/shapes/Edge.js +10 -10
  11. package/dist/@likec4/diagrams/diagram/shapes/Mobile.js +19 -4
  12. package/dist/@likec4/diagrams/diagram/shapes/NodeIcon.js +47 -9
  13. package/dist/@likec4/diagrams/diagram/shapes/NodeLabel.js +31 -54
  14. package/dist/@likec4/diagrams/diagram/shapes/Person.js +3 -1
  15. package/dist/@likec4/diagrams/diagram/shapes/Queue.js +3 -1
  16. package/dist/@likec4/diagrams/diagram/shapes/Rectangle.js +3 -1
  17. package/dist/@likec4/diagrams/diagram/shapes/index.js +1 -1
  18. package/dist/@likec4/diagrams/diagram/shapes/utils.js +1 -1
  19. package/dist/@likec4/diagrams/diagram/state/atoms.js +6 -0
  20. package/dist/@likec4/diagrams/diagram/state/hooks.js +10 -1
  21. package/dist/@likec4/diagrams/hooks/useDiagramApi.js +19 -22
  22. package/dist/@likec4/diagrams/hooks/useImageLoader.js +7 -1
  23. package/dist/__app__/index.html +1 -1
  24. package/dist/__app__/likec4.css +23 -0
  25. package/dist/__app__/src/App.jsx +27 -5
  26. package/dist/__app__/src/components/DiagramNotFound.jsx +10 -4
  27. package/dist/__app__/src/components/sidebar/Sidebar.jsx +1 -1
  28. package/dist/__app__/src/components/view-page/DisplayModeSelector.jsx +1 -1
  29. package/dist/__app__/src/components/view-page/ExportDiagram.jsx +13 -7
  30. package/dist/__app__/src/components/view-page/ViewActionsToolbar.jsx +12 -3
  31. package/dist/__app__/src/data/atoms.js +0 -11
  32. package/dist/__app__/src/pages/embed.page.jsx +14 -0
  33. package/dist/__app__/src/pages/export.page.jsx +4 -13
  34. package/dist/__app__/src/pages/index.js +1 -0
  35. package/dist/__app__/src/pages/useTransparentBackground.js +16 -0
  36. package/dist/__app__/src/pages/view-page.module.css +30 -0
  37. package/dist/__app__/src/pages/view.page.jsx +44 -8
  38. package/dist/__app__/src/router.js +14 -12
  39. package/dist/__app__/tailwind.config.cjs +1 -3
  40. package/dist/__app__/tsconfig.json +1 -7
  41. package/dist/cli/index.js +199 -206
  42. package/package.json +9 -10
  43. package/dist/@likec4/diagrams/hooks/useDarkMode.js +0 -5
  44. package/dist/@likec4/diagrams/index.mjs +0 -1927
@@ -3,6 +3,7 @@ import { useSpring } from "@react-spring/konva";
3
3
  import { AnimatedEllipse, AnimatedPath } from "../../konva.js";
4
4
  import { NodeLabels } from "./NodeLabel.js";
5
5
  import { useShadowSprings } from "../springs.js";
6
+ import { NodeIcon } from "./NodeIcon.js";
6
7
  function cylinderSVGPath(diameter, height, tilt = 0.0825) {
7
8
  const radius = Math.round(diameter / 2);
8
9
  const rx = radius;
@@ -51,6 +52,7 @@ export function CylinderShape({ node, theme, springs, isHovered }) {
51
52
  fill: springs.stroke
52
53
  }
53
54
  ),
54
- /* @__PURE__ */ jsx(NodeLabels, { node, offsetY: -ry * (node.icon ? 1.5 : 0.5), theme })
55
+ /* @__PURE__ */ jsx(NodeLabels, { node, offsetY: -4 - ry * (node.icon ? 1 : 0), theme }),
56
+ /* @__PURE__ */ jsx(NodeIcon, { node, paddingY: ry + 4, offsetY: -ry - 8 })
55
57
  ] });
56
58
  }
@@ -16,15 +16,15 @@ function EdgeArrow({
16
16
  return /* @__PURE__ */ jsx(
17
17
  AnimatedLine,
18
18
  {
19
- opacity: springs.opacity,
20
19
  points: points.flat(),
21
20
  closed: true,
22
21
  fill: isOutline ? void 0 : springs.lineColor,
23
22
  stroke: springs.lineColor,
24
- strokeWidth: 1.6,
25
- hitStrokeWidth: 5,
23
+ strokeWidth: 1,
26
24
  lineCap: "round",
27
25
  lineJoin: "miter",
26
+ perfectDrawEnabled: false,
27
+ listening: false,
28
28
  globalCompositeOperation
29
29
  }
30
30
  );
@@ -35,14 +35,14 @@ function EdgeLabelBg({
35
35
  isHovered,
36
36
  springs
37
37
  }) {
38
- const padding = 4;
38
+ const padding = 2;
39
39
  const props = useSpring({
40
40
  to: {
41
41
  x: labelBBox.x - padding,
42
42
  y: labelBBox.y - padding,
43
43
  width: labelBBox.width + padding * 2,
44
44
  height: labelBBox.height + padding * 2,
45
- opacity: isHovered ? 0.25 : 0.1
45
+ opacity: isHovered ? 0.5 : 0.12
46
46
  },
47
47
  immediate: !animate
48
48
  });
@@ -50,6 +50,7 @@ function EdgeLabelBg({
50
50
  AnimatedRect,
51
51
  {
52
52
  ...props,
53
+ perfectDrawEnabled: false,
53
54
  fill: springs.labelBgColor,
54
55
  cornerRadius: 2,
55
56
  globalCompositeOperation: "darken",
@@ -57,7 +58,7 @@ function EdgeLabelBg({
57
58
  }
58
59
  );
59
60
  }
60
- export function EdgeShape({ animate = true, edge, theme, isHovered, springs }) {
61
+ export function Edge({ animate = true, edge, theme, isHovered, springs }) {
61
62
  const {
62
63
  points,
63
64
  head,
@@ -82,7 +83,6 @@ export function EdgeShape({ animate = true, edge, theme, isHovered, springs }) {
82
83
  /* @__PURE__ */ jsx(
83
84
  AnimatedLine,
84
85
  {
85
- opacity: springs.opacity,
86
86
  bezier: true,
87
87
  dashEnabled: isDashed,
88
88
  dashOffset: 1,
@@ -93,6 +93,7 @@ export function EdgeShape({ animate = true, edge, theme, isHovered, springs }) {
93
93
  hitStrokeWidth: 20,
94
94
  lineCap: "round",
95
95
  lineJoin: "round",
96
+ perfectDrawEnabled: false,
96
97
  globalCompositeOperation
97
98
  }
98
99
  ),
@@ -129,14 +130,13 @@ export function EdgeShape({ animate = true, edge, theme, isHovered, springs }) {
129
130
  AnimatedText,
130
131
  {
131
132
  x: label.pt[0],
132
- y: label.pt[1],
133
- offsetY: label.fontSize / 2,
134
- opacity: springs.opacity,
133
+ y: label.pt[1] - label.fontSize,
135
134
  fill: springs.labelColor,
136
135
  fontFamily: theme.font,
137
136
  fontSize: label.fontSize,
138
137
  fontStyle: label.fontStyle ?? "normal",
139
138
  text: label.text,
139
+ perfectDrawEnabled: false,
140
140
  listening: false,
141
141
  globalCompositeOperation,
142
142
  shadowEnabled: springs.opacity.to((o) => o > 0.5),
@@ -1,9 +1,11 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { AnimatedRect, Circle } from "../../konva.js";
2
+ import { AnimatedCircle, AnimatedRect } from "../../konva.js";
3
3
  import { NodeLabels } from "./NodeLabel.js";
4
4
  import { useShadowSprings } from "../springs.js";
5
+ import { NodeIcon } from "./NodeIcon.js";
5
6
  export function MobileShape({ node, theme, springs, isHovered }) {
6
7
  const colors = theme.elements[node.color];
8
+ const maxWidth = node.size.width - 40 - 30;
7
9
  return /* @__PURE__ */ jsxs(Fragment, { children: [
8
10
  /* @__PURE__ */ jsx(
9
11
  AnimatedRect,
@@ -12,10 +14,21 @@ export function MobileShape({ node, theme, springs, isHovered }) {
12
14
  cornerRadius: 6,
13
15
  width: springs.width,
14
16
  height: springs.height,
15
- fill: springs.stroke
17
+ fill: springs.stroke,
18
+ perfectDrawEnabled: false
19
+ }
20
+ ),
21
+ /* @__PURE__ */ jsx(
22
+ AnimatedCircle,
23
+ {
24
+ x: 16,
25
+ y: node.size.height / 2,
26
+ radius: 10,
27
+ fill: springs.fill,
28
+ listening: false,
29
+ perfectDrawEnabled: false
16
30
  }
17
31
  ),
18
- /* @__PURE__ */ jsx(Circle, { x: 16, y: node.size.height / 2, radius: 10, fill: colors.fill, listening: false }),
19
32
  /* @__PURE__ */ jsx(
20
33
  AnimatedRect,
21
34
  {
@@ -25,9 +38,11 @@ export function MobileShape({ node, theme, springs, isHovered }) {
25
38
  width: springs.width.to((w) => w - 43),
26
39
  height: springs.height.to((h) => h - 24),
27
40
  fill: springs.fill,
41
+ perfectDrawEnabled: false,
28
42
  listening: false
29
43
  }
30
44
  ),
31
- /* @__PURE__ */ jsx(NodeLabels, { node, theme, offsetX: -6 })
45
+ /* @__PURE__ */ jsx(NodeLabels, { node, theme, offsetX: -10, maxWidth }),
46
+ /* @__PURE__ */ jsx(NodeIcon, { node, offsetX: -10, maxWidth })
32
47
  ] });
33
48
  }
@@ -1,23 +1,28 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import useImageLoader from "../../hooks/useImageLoader.js";
3
3
  import { Image } from "../../konva.js";
4
- export function NodeIcon({ icon, maxWidth, maxHeight, offsetX = 0, offsetY = 0 }) {
4
+ function IconImage({
5
+ icon,
6
+ centerX,
7
+ centerY,
8
+ maxWidth,
9
+ maxHeight,
10
+ offsetX = 0,
11
+ offsetY = 0
12
+ }) {
5
13
  const [image] = useImageLoader(icon);
6
14
  if (!image) {
7
15
  return null;
8
16
  }
9
- const padding = 16;
10
- const maxIconWidth = Math.round(maxWidth - padding * 2);
11
- const maxIconHeight = Math.round(maxHeight - padding * 2);
12
- const scale = Math.min(maxIconWidth / image.width, maxIconHeight / image.height, 1);
13
- const iconWidth = Math.floor(image.width * scale);
14
- const iconHeight = Math.floor(image.height * scale);
17
+ const scale = Math.min(maxWidth / image.naturalWidth, maxHeight / image.naturalHeight);
18
+ const iconWidth = Math.floor(image.naturalWidth * scale);
19
+ const iconHeight = Math.floor(image.naturalHeight * scale);
15
20
  return /* @__PURE__ */ jsx(
16
21
  Image,
17
22
  {
18
23
  image,
19
- x: padding + (maxIconWidth - iconWidth) / 2,
20
- y: padding + (maxIconHeight - iconHeight) / 2,
24
+ x: centerX - iconWidth / 2,
25
+ y: centerY - iconHeight / 2,
21
26
  offsetX,
22
27
  offsetY,
23
28
  width: iconWidth,
@@ -27,3 +32,36 @@ export function NodeIcon({ icon, maxWidth, maxHeight, offsetX = 0, offsetY = 0 }
27
32
  }
28
33
  );
29
34
  }
35
+ export function NodeIcon({
36
+ node,
37
+ maxWidth,
38
+ paddingX = 24,
39
+ paddingY = 24,
40
+ offsetX = 0,
41
+ offsetY = 0
42
+ }) {
43
+ if (!node.icon) {
44
+ return null;
45
+ }
46
+ const firstLabel = node.labels[0];
47
+ if (!firstLabel) {
48
+ return null;
49
+ }
50
+ const firstLabelY = Math.floor(firstLabel.pt[1] - firstLabel.fontSize * 1.25);
51
+ const maxIconHeight = Math.round(firstLabelY - paddingY - 8);
52
+ const maxIconWidth = maxWidth ?? node.size.width - paddingX * 2;
53
+ const centerY = paddingY + Math.floor(maxIconHeight / 2);
54
+ const centerX = Math.floor(node.size.width / 2);
55
+ return /* @__PURE__ */ jsx(
56
+ IconImage,
57
+ {
58
+ icon: node.icon,
59
+ centerX,
60
+ centerY,
61
+ maxWidth: maxIconWidth,
62
+ maxHeight: maxIconHeight,
63
+ offsetX,
64
+ offsetY
65
+ }
66
+ );
67
+ }
@@ -1,59 +1,36 @@
1
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { Text } from "../../konva.js";
3
- import { NodeIcon } from "./NodeIcon.js";
4
- export function NodeLabels({
5
- node: { icon, labels, size, color },
6
- theme,
7
- offsetX = 0,
8
- offsetY = 0,
9
- maxWidth
10
- }) {
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { Group, Text } from "../../konva.js";
3
+ export function NodeLabels({ node, theme, offsetX = 0, offsetY = 0, maxWidth }) {
4
+ const { labels, size, color } = node;
11
5
  const colors = theme.elements[color];
12
- const width = maxWidth ?? size.width;
13
- const firstLabel = labels[0];
14
- const titleFontSize = firstLabel?.fontSize ?? 12;
15
- let nodeIcon;
16
- if (icon) {
17
- const maxHeight = firstLabel ? Math.floor(firstLabel.pt[1] - firstLabel.fontSize / 2) : size.height;
18
- nodeIcon = /* @__PURE__ */ jsx(
19
- NodeIcon,
6
+ const width = maxWidth ?? size.width - 40;
7
+ const titleFontSize = labels[0]?.fontSize ?? 18;
8
+ const x = Math.ceil((size.width - width) / 2);
9
+ return /* @__PURE__ */ jsx(Group, { x, y: 3, offsetX, offsetY, children: labels.map((label, i) => {
10
+ const isTitle = label.fontSize === titleFontSize;
11
+ let color2 = colors.hiContrast;
12
+ if (!isTitle) {
13
+ color2 = colors.loContrast;
14
+ }
15
+ return /* @__PURE__ */ jsx(
16
+ Text,
20
17
  {
21
- icon,
22
- maxWidth: width,
23
- maxHeight,
24
- offsetX,
25
- offsetY
26
- }
18
+ text: label.text,
19
+ x: 0,
20
+ y: label.pt[1] - label.fontSize,
21
+ width,
22
+ fill: color2,
23
+ fontFamily: theme.font,
24
+ fontSize: label.fontSize,
25
+ fontStyle: label.fontStyle ?? "normal",
26
+ align: "center",
27
+ wrap: "none",
28
+ strokeEnablaed: false,
29
+ perfectDrawEnabled: false,
30
+ listening: false
31
+ },
32
+ label.text + i
27
33
  );
28
- }
29
- return /* @__PURE__ */ jsxs(Fragment, { children: [
30
- nodeIcon,
31
- labels.map((label, i) => {
32
- let color2 = colors.hiContrast;
33
- if (label.fontSize !== titleFontSize) {
34
- color2 = colors.loContrast;
35
- }
36
- return /* @__PURE__ */ jsx(
37
- Text,
38
- {
39
- x: 8,
40
- width: width - 16,
41
- y: label.pt[1],
42
- offsetY: offsetY + label.fontSize / 2,
43
- offsetX,
44
- fill: color2,
45
- fontFamily: theme.font,
46
- fontSize: label.fontSize,
47
- fontStyle: label.fontStyle ?? "normal",
48
- align: "center",
49
- text: label.text,
50
- strokeEnabled: false,
51
- perfectDrawEnabled: false,
52
- listening: false
53
- },
54
- label.text + i
55
- );
56
- })
57
- ] });
34
+ }) });
58
35
  }
59
36
  NodeLabels.displayName = "NodeLabels";
@@ -2,6 +2,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useShadowSprings } from "../springs.js";
3
3
  import { AnimatedPath, AnimatedRect } from "../../konva.js";
4
4
  import { NodeLabels } from "./NodeLabel.js";
5
+ import { NodeIcon } from "./NodeIcon.js";
5
6
  const PersonIcon = {
6
7
  width: 115,
7
8
  height: 120,
@@ -37,6 +38,7 @@ export function PersonShape({ node, theme, springs, isHovered }) {
37
38
  listening: false
38
39
  }
39
40
  ),
40
- /* @__PURE__ */ jsx(NodeLabels, { node, theme })
41
+ /* @__PURE__ */ jsx(NodeLabels, { node, theme }),
42
+ /* @__PURE__ */ jsx(NodeIcon, { node })
41
43
  ] });
42
44
  }
@@ -3,6 +3,7 @@ import { useShadowSprings } from "../springs.js";
3
3
  import { AnimatedEllipse, AnimatedPath } from "../../konva.js";
4
4
  import { NodeLabels } from "./NodeLabel.js";
5
5
  import { useSpring } from "@react-spring/konva";
6
+ import { NodeIcon } from "./NodeIcon.js";
6
7
  function queueSVGPath(width, height, tilt = 0.2) {
7
8
  const diameter = height;
8
9
  const radius = Math.round(diameter / 2);
@@ -52,6 +53,7 @@ export function QueueShape({ node, theme, springs, isHovered }) {
52
53
  fill: springs.stroke
53
54
  }
54
55
  ),
55
- /* @__PURE__ */ jsx(NodeLabels, { node, maxWidth: width - rx * 2, theme })
56
+ /* @__PURE__ */ jsx(NodeLabels, { node, maxWidth: width - 2 * rx - 10, offsetX: rx, theme }),
57
+ /* @__PURE__ */ jsx(NodeIcon, { node, maxWidth: width - 2 * rx - 10, offsetX: rx })
56
58
  ] });
57
59
  }
@@ -1,6 +1,7 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { AnimatedRect } from "../../konva.js";
3
3
  import { useShadowSprings } from "../springs.js";
4
+ import { NodeIcon } from "./NodeIcon.js";
4
5
  import { NodeLabels } from "./NodeLabel.js";
5
6
  export function RectangleShape({ node, theme, springs, isHovered }) {
6
7
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -15,6 +16,7 @@ export function RectangleShape({ node, theme, springs, isHovered }) {
15
16
  fill: springs.fill
16
17
  }
17
18
  ),
18
- /* @__PURE__ */ jsx(NodeLabels, { node, theme })
19
+ /* @__PURE__ */ jsx(NodeLabels, { node, theme }),
20
+ /* @__PURE__ */ jsx(NodeIcon, { node })
19
21
  ] });
20
22
  }
@@ -4,4 +4,4 @@ export { CylinderShape } from "./Cylinder.js";
4
4
  export { MobileShape } from "./Mobile.js";
5
5
  export { QueueShape } from "./Queue.js";
6
6
  export { PersonShape } from "./Person.js";
7
- export { EdgeShape } from "./Edge.js";
7
+ export { Edge } from "./Edge.js";
@@ -7,6 +7,6 @@ export function mousePointer(e) {
7
7
  export function mouseDefault(e) {
8
8
  const container = e.target.getStage()?.container();
9
9
  if (container) {
10
- container.style.cursor = "auto";
10
+ container.style.cursor = "";
11
11
  }
12
12
  }
@@ -93,3 +93,9 @@ export const hoveredEdgeAtom = atom(
93
93
  }
94
94
  );
95
95
  export const hoveredEdgeIdAtom = selectAtom(hoveredEdgeAtom, (edge) => edge?.id ?? null);
96
+ export const resetHoveredStatesAtom = atom(void 0, (get, set) => {
97
+ clearTimeout(get(nodeTimeoutAtom));
98
+ clearTimeout(get(edgeTimeoutAtom));
99
+ set(currentHoveredNodeAtom, null);
100
+ set(currentHoveredEdgeAtom, null);
101
+ });
@@ -1,5 +1,11 @@
1
1
  import { useAtom, useAtomValue, useSetAtom } from "jotai";
2
- import { hoveredEdgeAtom, hoveredEdgeIdAtom, hoveredNodeAtom, hoveredNodeIdAtom } from "./atoms.js";
2
+ import {
3
+ hoveredEdgeAtom,
4
+ hoveredEdgeIdAtom,
5
+ hoveredNodeAtom,
6
+ hoveredNodeIdAtom,
7
+ resetHoveredStatesAtom
8
+ } from "./atoms.js";
3
9
  export function useHoveredNode() {
4
10
  return useAtom(hoveredNodeAtom);
5
11
  }
@@ -18,3 +24,6 @@ export function useHoveredEdgeId() {
18
24
  export function useSetHoveredEdge() {
19
25
  return useSetAtom(hoveredEdgeAtom);
20
26
  }
27
+ export function useResetHoveredStates() {
28
+ return useSetAtom(resetHoveredStatesAtom);
29
+ }
@@ -1,27 +1,24 @@
1
- import { useRef, useMemo } from "react";
2
1
  import { nonNullable } from "@likec4/core";
2
+ import { useRef, useState } from "react";
3
3
  export function useDiagramApi() {
4
4
  const ref = useRef(null);
5
- return useMemo(
6
- () => [
7
- ref,
8
- {
9
- get stage() {
10
- return nonNullable(ref.current, "not mounted, use ref").stage;
11
- },
12
- get diagramView() {
13
- return nonNullable(ref.current, "not mounted, use ref").diagramView;
14
- },
15
- get container() {
16
- return nonNullable(ref.current, "not mounted, use ref").container;
17
- },
18
- resetStageZoom: (_immediate) => {
19
- nonNullable(ref.current, "not mounted, use ref").resetStageZoom(_immediate);
20
- },
21
- centerOnNode: (node) => nonNullable(ref.current, "not mounted, use ref").centerOnNode(node),
22
- centerAndFit: () => nonNullable(ref.current, "not mounted, use ref").centerAndFit()
23
- }
24
- ],
25
- [ref]
5
+ const [api] = useState(
6
+ () => ({
7
+ get stage() {
8
+ return ref.current?.stage ?? null;
9
+ },
10
+ get diagramView() {
11
+ return nonNullable(ref.current, "not mounted, use ref").diagramView;
12
+ },
13
+ get container() {
14
+ return ref.current?.container ?? null;
15
+ },
16
+ resetStageZoom: (_immediate) => {
17
+ nonNullable(ref.current, "not mounted, use ref").resetStageZoom(_immediate);
18
+ },
19
+ centerOnNode: (node) => nonNullable(ref.current, "not mounted, use ref").centerOnNode(node),
20
+ centerAndFit: () => nonNullable(ref.current, "not mounted, use ref").centerAndFit()
21
+ })
26
22
  );
23
+ return [ref, api];
27
24
  }
@@ -8,10 +8,14 @@ export default function useImageLoader(url, crossOrigin, referrerpolicy) {
8
8
  const statusRef = useRef(imageRef.current ? "loaded" : "loading");
9
9
  const [_, setStateToken] = useState(0);
10
10
  const urlRef = useRef(url);
11
- if (!url || urlRef.current !== url) {
11
+ const crossOriginRef = useRef(crossOrigin);
12
+ const referrerPolicyRef = useRef(referrerpolicy);
13
+ if (!url || urlRef.current !== url || crossOriginRef.current !== crossOrigin || referrerPolicyRef.current !== referrerpolicy) {
12
14
  statusRef.current = "loading";
13
15
  imageRef.current = void 0;
14
16
  urlRef.current = url;
17
+ crossOriginRef.current = crossOrigin;
18
+ referrerPolicyRef.current = referrerpolicy;
15
19
  }
16
20
  useLayoutEffect(() => {
17
21
  if (!url) {
@@ -28,6 +32,8 @@ export default function useImageLoader(url, crossOrigin, referrerpolicy) {
28
32
  return;
29
33
  }
30
34
  const img = document.createElement("img");
35
+ img.style.width = "100%";
36
+ img.style.height = "auto";
31
37
  function onload() {
32
38
  if (!isMounted())
33
39
  return;
@@ -8,7 +8,7 @@
8
8
  <link rel="stylesheet" type="text/css" href="/likec4.css" />
9
9
  </head>
10
10
  <body>
11
- <div id="like4-root" class="w-screen h-screen m-0 p-0"></div>
11
+ <div id="like4-root"></div>
12
12
  <script type="module" src="/src/main"></script>
13
13
  </body>
14
14
  </html>
@@ -8,11 +8,34 @@
8
8
  --font-weight-bold: 600;
9
9
  }
10
10
 
11
+ *,
12
+ :before,
13
+ :after {
14
+ box-sizing: border-box;
15
+ outline: none;
16
+ border-width: 0;
17
+ border-style: solid;
18
+ border-color: transparent;
19
+ }
20
+
11
21
  html, body {
12
22
  margin: 0;
13
23
  padding: 0;
24
+ width: 100%;
25
+ height: 100%;
26
+ }
27
+
28
+ #like4-root {
29
+ margin: 0;
30
+ padding: 0;
31
+ width: 100vw;
32
+ height: 100vh;
14
33
  }
15
34
 
16
35
  .transparent-bg {
17
36
  --color-page-background: transparent !important;
37
+
38
+ body {
39
+ background-color: transparent !important;
40
+ }
18
41
  }
@@ -1,15 +1,37 @@
1
1
  import { Provider } from 'jotai';
2
2
  import { Fragment } from 'react';
3
3
  import { Sidebar } from './components';
4
- import { ExportPage, IndexPage, ViewPage } from './pages';
4
+ import { ExportPage, IndexPage, EmbedPage, ViewPage } from './pages';
5
5
  import { useRoute } from './router';
6
6
  import { Theme } from '@radix-ui/themes';
7
+ import { nonexhaustive } from '@likec4/core';
8
+ import { isNil } from 'remeda';
7
9
  const Routes = () => {
8
10
  const r = useRoute();
9
- return (<Theme hasBackground={r.route !== 'export'} accentColor='indigo' radius='small' appearance={r.params?.theme}>
10
- {r.route === 'index' && <IndexPage key='index'/>}
11
- {r.route === 'view' && <ViewPage key='view' viewId={r.params.viewId} showUI={r.showUI}/>}
12
- {r.route === 'export' && (<ExportPage key='export' viewId={r.params.viewId} padding={r.params.padding}/>)}
11
+ const theme = r.params?.theme;
12
+ let page = null;
13
+ switch (r.route) {
14
+ case 'view': {
15
+ page = <ViewPage key='view' viewId={r.params.viewId} showUI={r.showUI}/>;
16
+ break;
17
+ }
18
+ case 'export': {
19
+ page = <ExportPage key='export' viewId={r.params.viewId} padding={r.params.padding}/>;
20
+ break;
21
+ }
22
+ case 'embed': {
23
+ page = (<EmbedPage key='embed' viewId={r.params.viewId} padding={r.params.padding} transparentBg={isNil(r.params.theme)}/>);
24
+ break;
25
+ }
26
+ case 'index': {
27
+ page = <IndexPage key='index'/>;
28
+ break;
29
+ }
30
+ default:
31
+ nonexhaustive(r);
32
+ }
33
+ return (<Theme hasBackground={!!theme} accentColor='indigo' radius='small' appearance={theme}>
34
+ {page}
13
35
  {r.showUI && (<Fragment key='ui'>
14
36
  <Sidebar />
15
37
  </Fragment>)}
@@ -1,7 +1,7 @@
1
1
  import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
2
- import { Box, Button, Card, Flex, Heading, IconButton, Text } from '@radix-ui/themes';
2
+ import { Box, Button, Card, Code, Flex, Heading, IconButton, Text } from '@radix-ui/themes';
3
3
  import { $pages } from '../router';
4
- export const DiagramNotFound = () => {
4
+ export const DiagramNotFound = ({ viewId }) => {
5
5
  return (<Flex position='fixed' inset='0' align='center' justify='center'>
6
6
  <Card color='red' size='3'>
7
7
  <Flex gap='4' direction='row' align='start'>
@@ -14,9 +14,15 @@ export const DiagramNotFound = () => {
14
14
  <Heading trim='both' color='amber' size='4'>
15
15
  Diagram not found
16
16
  </Heading>
17
- <Text as='div'>The diagram you are looking for does not exist.</Text>
17
+ <Text as='div'>
18
+ The diagram{' '}
19
+ <Code color='amber' variant='soft'>
20
+ {viewId}
21
+ </Code>{' '}
22
+ does not exist
23
+ </Text>
18
24
  <Box pt='2'>
19
- <Button variant='soft' color='amber' onClick={() => $pages.index.open()}>
25
+ <Button variant='soft' color='amber' className='cursor-pointer' onClick={() => $pages.index.open()}>
20
26
  Go to overview
21
27
  </Button>
22
28
  </Box>
@@ -2,7 +2,7 @@ import { Box, Button, Flex, IconButton, ScrollArea, Separator } from '@radix-ui/
2
2
  import { useClickOutside, useToggle } from '@react-hookz/web/esm';
3
3
  import { HamburgerMenuIcon, ArrowLeftIcon } from '@radix-ui/react-icons';
4
4
  import { useRef } from 'react';
5
- import { cn } from '~/utils';
5
+ import { cn } from '../../utils';
6
6
  import { DiagramsTree } from './DiagramsTree';
7
7
  import styles from './styles.module.css';
8
8
  import { $pages } from '../../router';
@@ -25,7 +25,7 @@ export const DisplayModeSelector = () => {
25
25
  };
26
26
  return (<Flex display={{
27
27
  initial: 'none',
28
- md: 'flex'
28
+ sm: 'flex'
29
29
  }} gap='3' align='center'>
30
30
  <Button variant={current === first ? 'solid' : 'ghost'} size='1' onClick={changeMode(first)}>
31
31
  {Mode[first]}