likec4 0.51.0 → 0.53.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.
- package/dist/@likec4/core/utils/fqn.js +10 -4
- package/dist/@likec4/core/utils/relations.js +8 -1
- package/dist/@likec4/diagrams/diagram/Nodes.js +75 -224
- package/dist/@likec4/diagrams/diagram/icons/LinkIcon.js +48 -0
- package/dist/@likec4/diagrams/diagram/icons/ZoomInIcon.js +74 -0
- package/dist/@likec4/diagrams/diagram/icons/index.js +2 -3
- package/dist/@likec4/diagrams/diagram/nodes/NodeLinkBtn.js +139 -0
- package/dist/@likec4/diagrams/diagram/nodes/NodeZoomBtn.js +174 -0
- package/dist/@likec4/diagrams/diagram/nodes/index.js +2 -0
- package/dist/@likec4/diagrams/diagram/shapes/Compound.js +2 -2
- package/dist/@likec4/diagrams/diagram/shapes/Edge.js +7 -6
- package/dist/@likec4/diagrams/diagram/springs.js +3 -3
- package/dist/@likec4/diagrams/diagram/state/atoms.js +2 -3
- package/dist/@likec4/diagrams/diagram/utils.js +33 -0
- package/dist/__app__/src/App.jsx +14 -21
- package/dist/__app__/src/pages/view-page/index.js +1 -1
- package/dist/__app__/src/pages/view-page/other-formats/ViewAsD2.jsx +51 -12
- package/dist/__app__/src/pages/view-page/other-formats/ViewAsDot.jsx +21 -17
- package/dist/__app__/src/pages/view-page/other-formats/ViewAsMmd.jsx +39 -11
- package/dist/__app__/src/pages/view-page/other-formats.jsx +3 -0
- package/dist/__app__/src/pages/view-page/view-page.module.css +7 -2
- package/dist/cli/index.js +232 -233
- package/package.json +22 -20
- package/dist/@likec4/diagrams/diagram/icons/ZoomIn.js +0 -28
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { isEqualSimple } from "@react-hookz/deep-equal/esnext";
|
|
3
|
+
import { useToggle } from "@react-hookz/web/esm";
|
|
4
|
+
import { useSpring } from "@react-spring/konva";
|
|
5
|
+
import { lighten, mix, toHex } from "khroma";
|
|
6
|
+
import { memo } from "react";
|
|
7
|
+
import { AnimatedCircle, AnimatedGroup, Group } from "../../konva.js";
|
|
8
|
+
import { ZoomInIcon } from "../icons/index.js";
|
|
9
|
+
import { DiagramGesture } from "../state/index.js";
|
|
10
|
+
import { mouseDefault, mousePointer } from "../utils.js";
|
|
11
|
+
export const NodeZoomBtn = memo(({ animate, node, theme, isHovered: _isHovered, onNodeClick }) => {
|
|
12
|
+
const size = 30;
|
|
13
|
+
const halfSize = size / 2;
|
|
14
|
+
const colors = theme.elements[node.color];
|
|
15
|
+
let zoomInIconY;
|
|
16
|
+
switch (node.shape) {
|
|
17
|
+
case "browser":
|
|
18
|
+
case "mobile":
|
|
19
|
+
zoomInIconY = node.size.height - 20;
|
|
20
|
+
break;
|
|
21
|
+
default:
|
|
22
|
+
zoomInIconY = node.size.height - 16;
|
|
23
|
+
}
|
|
24
|
+
const fill = toHex(mix(colors.fill, colors.stroke, 65));
|
|
25
|
+
const onOver = toHex(mix(colors.fill, colors.stroke, 75));
|
|
26
|
+
const [isOver, toggleOver] = useToggle(false);
|
|
27
|
+
const isHovered = _isHovered || isOver;
|
|
28
|
+
const props = useSpring({
|
|
29
|
+
to: {
|
|
30
|
+
fill: isOver ? onOver : fill,
|
|
31
|
+
opacity: isOver ? 1 : 0,
|
|
32
|
+
y: zoomInIconY + (isOver ? 2 : 0),
|
|
33
|
+
scale: isOver ? 1.38 : 1,
|
|
34
|
+
// shadowBlur: isOver ? 6 : 4,
|
|
35
|
+
shadowOpacity: isOver ? 0.3 : 0.15
|
|
36
|
+
// shadowOffsetY: isOver ? 8 : 6
|
|
37
|
+
},
|
|
38
|
+
delay: isHovered && !isOver ? 100 : 0,
|
|
39
|
+
immediate: !animate
|
|
40
|
+
});
|
|
41
|
+
return /* @__PURE__ */ jsxs(
|
|
42
|
+
AnimatedGroup,
|
|
43
|
+
{
|
|
44
|
+
x: node.size.width / 2,
|
|
45
|
+
y: props.y,
|
|
46
|
+
offsetX: halfSize,
|
|
47
|
+
offsetY: halfSize,
|
|
48
|
+
scaleX: props.scale,
|
|
49
|
+
scaleY: props.scale,
|
|
50
|
+
width: size,
|
|
51
|
+
height: size,
|
|
52
|
+
onPointerEnter: (e) => {
|
|
53
|
+
toggleOver(true);
|
|
54
|
+
mousePointer(e);
|
|
55
|
+
},
|
|
56
|
+
onPointerLeave: (e) => {
|
|
57
|
+
toggleOver(false);
|
|
58
|
+
mouseDefault(e);
|
|
59
|
+
},
|
|
60
|
+
onPointerClick: (e) => {
|
|
61
|
+
if (DiagramGesture.isDragging || e.evt.button !== 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
e.cancelBubble = true;
|
|
65
|
+
onNodeClick(node, e);
|
|
66
|
+
},
|
|
67
|
+
children: [
|
|
68
|
+
/* @__PURE__ */ jsx(
|
|
69
|
+
AnimatedCircle,
|
|
70
|
+
{
|
|
71
|
+
x: halfSize,
|
|
72
|
+
y: halfSize,
|
|
73
|
+
radius: halfSize,
|
|
74
|
+
fill: props.fill,
|
|
75
|
+
shadowBlur: 4,
|
|
76
|
+
shadowOpacity: props.shadowOpacity,
|
|
77
|
+
shadowOffsetX: 2,
|
|
78
|
+
shadowOffsetY: 6,
|
|
79
|
+
shadowColor: theme.shadow,
|
|
80
|
+
shadowEnabled: isHovered,
|
|
81
|
+
perfectDrawEnabled: false,
|
|
82
|
+
opacity: props.opacity,
|
|
83
|
+
hitStrokeWidth: halfSize
|
|
84
|
+
}
|
|
85
|
+
),
|
|
86
|
+
/* @__PURE__ */ jsx(ZoomInIcon, { size: 16, x: halfSize, y: halfSize })
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
}, isEqualSimple);
|
|
91
|
+
NodeZoomBtn.displayName = "NodeZoomBtn";
|
|
92
|
+
export const CompoundZoomBtn = memo(({
|
|
93
|
+
animate,
|
|
94
|
+
node,
|
|
95
|
+
theme,
|
|
96
|
+
ctrl,
|
|
97
|
+
isHovered: _isHovered,
|
|
98
|
+
onNodeClick
|
|
99
|
+
}) => {
|
|
100
|
+
const size = 28;
|
|
101
|
+
const [isOver, toggleOver] = useToggle(false);
|
|
102
|
+
const halfSize = size / 2;
|
|
103
|
+
const fill = toHex(lighten(ctrl.springs.fill.get(), 10));
|
|
104
|
+
const isHovered = _isHovered || isOver;
|
|
105
|
+
const props = useSpring({
|
|
106
|
+
to: {
|
|
107
|
+
opacity: isOver ? 1 : 0,
|
|
108
|
+
x: halfSize + 4 - (isOver ? 4 : 0),
|
|
109
|
+
y: halfSize + 6 - (isOver ? 4 : 0),
|
|
110
|
+
scale: isOver ? 1.35 : 1,
|
|
111
|
+
// shadowBlur: isOver ? 6 : 4,
|
|
112
|
+
shadowOpacity: isOver ? 0.3 : 0.15
|
|
113
|
+
// shadowOffsetY: isOver ? 8 : 6
|
|
114
|
+
},
|
|
115
|
+
delay: isHovered && !isOver ? 100 : 0,
|
|
116
|
+
// delay: isOver ? 150 : (isHovered ? 70 : 0),
|
|
117
|
+
immediate: !animate
|
|
118
|
+
});
|
|
119
|
+
return /* @__PURE__ */ jsx(
|
|
120
|
+
Group,
|
|
121
|
+
{
|
|
122
|
+
onPointerEnter: (e) => {
|
|
123
|
+
toggleOver(true);
|
|
124
|
+
mousePointer(e);
|
|
125
|
+
},
|
|
126
|
+
onPointerLeave: (e) => {
|
|
127
|
+
toggleOver(false);
|
|
128
|
+
mouseDefault(e);
|
|
129
|
+
},
|
|
130
|
+
onPointerClick: (e) => {
|
|
131
|
+
if (DiagramGesture.isDragging || e.evt.button !== 0) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
e.cancelBubble = true;
|
|
135
|
+
onNodeClick(node, e);
|
|
136
|
+
},
|
|
137
|
+
children: /* @__PURE__ */ jsxs(
|
|
138
|
+
AnimatedGroup,
|
|
139
|
+
{
|
|
140
|
+
x: props.x,
|
|
141
|
+
y: props.y,
|
|
142
|
+
offsetX: halfSize,
|
|
143
|
+
offsetY: halfSize,
|
|
144
|
+
scaleX: props.scale,
|
|
145
|
+
scaleY: props.scale,
|
|
146
|
+
width: size,
|
|
147
|
+
height: size,
|
|
148
|
+
children: [
|
|
149
|
+
/* @__PURE__ */ jsx(
|
|
150
|
+
AnimatedCircle,
|
|
151
|
+
{
|
|
152
|
+
x: halfSize,
|
|
153
|
+
y: halfSize,
|
|
154
|
+
radius: halfSize,
|
|
155
|
+
fill,
|
|
156
|
+
shadowBlur: 4,
|
|
157
|
+
shadowOpacity: props.shadowOpacity,
|
|
158
|
+
shadowOffsetX: 2,
|
|
159
|
+
shadowOffsetY: 6,
|
|
160
|
+
shadowColor: theme.shadow,
|
|
161
|
+
shadowEnabled: isHovered,
|
|
162
|
+
perfectDrawEnabled: false,
|
|
163
|
+
opacity: props.opacity,
|
|
164
|
+
hitStrokeWidth: halfSize
|
|
165
|
+
}
|
|
166
|
+
),
|
|
167
|
+
/* @__PURE__ */ jsx(ZoomInIcon, { size: 16, x: halfSize, y: halfSize })
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
}, isEqualSimple);
|
|
174
|
+
CompoundZoomBtn.displayName = "CompoundZoomBtn";
|
|
@@ -9,9 +9,9 @@ export function CompoundShape({ node, theme, springs, labelOffsetX = 0 }) {
|
|
|
9
9
|
cornerRadius: 4,
|
|
10
10
|
shadowColor: theme.shadow,
|
|
11
11
|
shadowBlur: node.level > 0 ? 20 : 10,
|
|
12
|
-
shadowOpacity: node.level > 0 ? 0.35 : 0.
|
|
12
|
+
shadowOpacity: node.level > 0 ? 0.35 : 0.6,
|
|
13
13
|
shadowOffsetX: 0,
|
|
14
|
-
shadowOffsetY:
|
|
14
|
+
shadowOffsetY: 5,
|
|
15
15
|
shadowEnabled: springs.opacity.to((v) => v > 0.7),
|
|
16
16
|
width: springs.width,
|
|
17
17
|
height: springs.height,
|
|
@@ -35,14 +35,15 @@ function EdgeLabelBg({
|
|
|
35
35
|
isHovered,
|
|
36
36
|
springs
|
|
37
37
|
}) {
|
|
38
|
-
const
|
|
38
|
+
const paddingX = 2;
|
|
39
|
+
const paddingY = 1;
|
|
39
40
|
const props = useSpring({
|
|
40
41
|
to: {
|
|
41
|
-
x: labelBBox.x -
|
|
42
|
-
y: labelBBox.y -
|
|
43
|
-
width: labelBBox.width +
|
|
44
|
-
height: labelBBox.height +
|
|
45
|
-
opacity: isHovered ? 0.
|
|
42
|
+
x: labelBBox.x - paddingX,
|
|
43
|
+
y: labelBBox.y - paddingY,
|
|
44
|
+
width: labelBBox.width + paddingX * 2,
|
|
45
|
+
height: labelBBox.height + paddingY * 2,
|
|
46
|
+
opacity: isHovered ? 0.8 : 0.55
|
|
46
47
|
},
|
|
47
48
|
immediate: !animate
|
|
48
49
|
});
|
|
@@ -42,10 +42,10 @@ export const useNodeSpringsFn = (theme) => {
|
|
|
42
42
|
export const useShadowSprings = (isHovered = false, theme, springs) => {
|
|
43
43
|
const [values] = useSpring(
|
|
44
44
|
{
|
|
45
|
-
shadowBlur: isHovered ? 30 :
|
|
46
|
-
shadowOpacity: isHovered ? 0.5 : 0.
|
|
45
|
+
shadowBlur: isHovered ? 30 : 10,
|
|
46
|
+
shadowOpacity: isHovered ? 0.5 : 0.3,
|
|
47
47
|
shadowOffsetX: 0,
|
|
48
|
-
shadowOffsetY: isHovered ? 16 :
|
|
48
|
+
shadowOffsetY: isHovered ? 16 : 5,
|
|
49
49
|
shadowColor: theme.shadow
|
|
50
50
|
},
|
|
51
51
|
[isHovered, theme]
|
|
@@ -24,7 +24,7 @@ export const hoveredNodeAtom = atom(
|
|
|
24
24
|
if (equals(_prev, _next)) {
|
|
25
25
|
return false;
|
|
26
26
|
}
|
|
27
|
-
const timeout = !!_next && !!_prev ?
|
|
27
|
+
const timeout = !!_next && !!_prev ? 50 : 120;
|
|
28
28
|
if (_next != null) {
|
|
29
29
|
clearTimeout(get(edgeTimeoutAtom));
|
|
30
30
|
scheduleHoveredEdge(set, null, timeout);
|
|
@@ -57,9 +57,8 @@ export const hoveredEdgeAtom = atom(
|
|
|
57
57
|
if (equals(_prev, _next)) {
|
|
58
58
|
return false;
|
|
59
59
|
}
|
|
60
|
-
|
|
60
|
+
const timeout = !!_next && !!_prev ? 50 : 120;
|
|
61
61
|
if (_next != null) {
|
|
62
|
-
timeout = _prev != null ? 120 : 300;
|
|
63
62
|
clearTimeout(get(nodeTimeoutAtom));
|
|
64
63
|
scheduleHoveredNode(set, null, timeout);
|
|
65
64
|
}
|
|
@@ -12,3 +12,36 @@ export function mouseDefault(e) {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
export const isNumber = is(Number);
|
|
15
|
+
export const getBoundingRect = (elements) => {
|
|
16
|
+
let minX = Infinity;
|
|
17
|
+
let minY = Infinity;
|
|
18
|
+
let maxX = -Infinity;
|
|
19
|
+
let maxY = -Infinity;
|
|
20
|
+
for (const element of elements) {
|
|
21
|
+
if ("size" in element && "position" in element) {
|
|
22
|
+
minX = Math.min(minX, element.position[0]);
|
|
23
|
+
minY = Math.min(minY, element.position[1]);
|
|
24
|
+
maxX = Math.max(maxX, element.position[0] + element.size.width);
|
|
25
|
+
maxY = Math.max(maxY, element.position[1] + element.size.height);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
element.points.forEach(([x, y]) => {
|
|
29
|
+
minX = Math.min(minX, x);
|
|
30
|
+
minY = Math.min(minY, y);
|
|
31
|
+
maxX = Math.max(maxX, x);
|
|
32
|
+
maxY = Math.max(maxY, y);
|
|
33
|
+
});
|
|
34
|
+
if (element.labelBBox) {
|
|
35
|
+
minX = Math.min(minX, element.labelBBox.x);
|
|
36
|
+
minY = Math.min(minY, element.labelBBox.y);
|
|
37
|
+
maxX = Math.max(maxX, element.labelBBox.x + element.labelBBox.width);
|
|
38
|
+
maxY = Math.max(maxY, element.labelBBox.y + element.labelBBox.height);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
x: minX,
|
|
43
|
+
y: minY,
|
|
44
|
+
width: maxX - minX,
|
|
45
|
+
height: maxY - minY
|
|
46
|
+
};
|
|
47
|
+
};
|
package/dist/__app__/src/App.jsx
CHANGED
|
@@ -9,29 +9,22 @@ import { useRoute } from './router';
|
|
|
9
9
|
const Routes = () => {
|
|
10
10
|
const r = useDeferredValue(useRoute());
|
|
11
11
|
const theme = r.params?.theme;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
const page = () => {
|
|
13
|
+
switch (r.route) {
|
|
14
|
+
case 'view':
|
|
15
|
+
return <ViewPage viewId={r.params.viewId} viewMode={r.params.mode} showUI={r.showUI}/>;
|
|
16
|
+
case 'export':
|
|
17
|
+
return <ExportPage viewId={r.params.viewId} padding={r.params.padding}/>;
|
|
18
|
+
case 'embed':
|
|
19
|
+
return (<EmbedPage viewId={r.params.viewId} padding={r.params.padding} transparentBg={isNil(r.params.theme)}/>);
|
|
20
|
+
case 'index':
|
|
21
|
+
return <IndexPage />;
|
|
22
|
+
default:
|
|
23
|
+
nonexhaustive(r);
|
|
17
24
|
}
|
|
18
|
-
|
|
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
|
-
}
|
|
25
|
+
};
|
|
33
26
|
return (<Theme hasBackground={!!theme} accentColor='indigo' radius='small' appearance={theme ?? 'inherit'}>
|
|
34
|
-
{page}
|
|
27
|
+
{page()}
|
|
35
28
|
<Fragment key='ui'>{r.showUI && <Sidebar />}</Fragment>
|
|
36
29
|
</Theme>);
|
|
37
30
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { lazy } from 'react';
|
|
3
3
|
export * from './ViewAsReact';
|
|
4
|
-
export const ViewAs = lazy(() => import('./other-formats'));
|
|
4
|
+
export const ViewAs = lazy(async () => await import('./other-formats'));
|
|
5
5
|
// export const ViewAs = {
|
|
6
6
|
// Dot: ViewAsDot,
|
|
7
7
|
// D2: ViewAsD2,
|
|
@@ -1,18 +1,57 @@
|
|
|
1
|
-
import { Box, Code, ScrollArea } from '@radix-ui/themes';
|
|
1
|
+
import { Box, Button, Code, ScrollArea } from '@radix-ui/themes';
|
|
2
|
+
import { useAsync } from '@react-hookz/web/esm';
|
|
2
3
|
import { d2Source } from 'virtual:likec4/d2-sources';
|
|
3
4
|
import { CopyToClipboard } from '../../../components';
|
|
5
|
+
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
|
6
|
+
const fetchFromKroki = async (d2) => {
|
|
7
|
+
const res = await fetch('https://kroki.io/d2/svg', {
|
|
8
|
+
method: 'POST',
|
|
9
|
+
cache: 'force-cache',
|
|
10
|
+
body: JSON.stringify({
|
|
11
|
+
diagram_source: d2,
|
|
12
|
+
// diagram_options: {
|
|
13
|
+
// theme: 'colorblind-clear'
|
|
14
|
+
// },
|
|
15
|
+
output_format: 'svg'
|
|
16
|
+
}),
|
|
17
|
+
headers: {
|
|
18
|
+
'Content-Type': 'application/json'
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
return await res.text();
|
|
22
|
+
};
|
|
4
23
|
export default function ViewAsD2({ viewId }) {
|
|
5
24
|
const src = d2Source(viewId);
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
25
|
+
const [krokiSvg, { execute }] = useAsync(fetchFromKroki, null);
|
|
26
|
+
return (<PanelGroup direction='horizontal' autoSaveId='ViewAsD2'>
|
|
27
|
+
<Panel minSizePixels={100}>
|
|
28
|
+
<ScrollArea scrollbars='both'>
|
|
29
|
+
<Box asChild display={'block'} p='2' style={{
|
|
30
|
+
whiteSpace: 'pre',
|
|
31
|
+
minHeight: '100%'
|
|
10
32
|
}}>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
33
|
+
<Code variant='soft' autoFocus>
|
|
34
|
+
{src}
|
|
35
|
+
</Code>
|
|
36
|
+
</Box>
|
|
37
|
+
<CopyToClipboard text={src}/>
|
|
38
|
+
</ScrollArea>
|
|
39
|
+
</Panel>
|
|
40
|
+
<PanelResizeHandle style={{
|
|
41
|
+
width: 10
|
|
42
|
+
}}/>
|
|
43
|
+
<Panel minSizePixels={100}>
|
|
44
|
+
<ScrollArea scrollbars='both'>
|
|
45
|
+
{krokiSvg.status !== 'success' && (<>
|
|
46
|
+
<Button disabled={krokiSvg.status === 'loading'} onClick={() => void execute(src)}>
|
|
47
|
+
{krokiSvg.status === 'loading' ? 'Loading...' : 'Render with Kroki'}
|
|
48
|
+
</Button>
|
|
49
|
+
{krokiSvg.status === 'error' && <Box>{krokiSvg.error?.message}</Box>}
|
|
50
|
+
</>)}
|
|
51
|
+
{krokiSvg.status === 'success' && (<Box grow={'1'} asChild className={'svg-container'}>
|
|
52
|
+
{!krokiSvg.result ? (<Box>Empty result</Box>) : (<div dangerouslySetInnerHTML={{ __html: krokiSvg.result }}></div>)}
|
|
53
|
+
</Box>)}
|
|
54
|
+
</ScrollArea>
|
|
55
|
+
</Panel>
|
|
56
|
+
</PanelGroup>);
|
|
18
57
|
}
|
|
@@ -1,33 +1,37 @@
|
|
|
1
|
-
import { Box, Code,
|
|
1
|
+
import { Box, Code, ScrollArea } from '@radix-ui/themes';
|
|
2
2
|
import { dotSource, svgSource } from 'virtual:likec4/dot-sources';
|
|
3
|
-
import styles from '../view-page.module.css';
|
|
4
3
|
import { CopyToClipboard } from '../../../components';
|
|
4
|
+
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
|
5
5
|
export default function ViewAsDot({ viewId }) {
|
|
6
6
|
const dot = dotSource(viewId);
|
|
7
|
-
return (<
|
|
8
|
-
|
|
9
|
-
columns='2' gap='2' shrink='1' grow='1'>
|
|
10
|
-
<Box py={'2'} position={'relative'} style={{
|
|
11
|
-
overflow: 'scroll'
|
|
12
|
-
}}>
|
|
7
|
+
return (<PanelGroup direction='horizontal' autoSaveId='viewAsDot'>
|
|
8
|
+
<Panel minSizePixels={100}>
|
|
13
9
|
<ScrollArea scrollbars='both'>
|
|
14
10
|
<Box asChild display={'block'} p='2' style={{
|
|
15
|
-
whiteSpace: 'pre'
|
|
11
|
+
whiteSpace: 'pre',
|
|
12
|
+
minHeight: '100%'
|
|
16
13
|
}}>
|
|
17
14
|
<Code variant='soft' autoFocus>
|
|
18
15
|
{dot}
|
|
19
16
|
</Code>
|
|
20
17
|
</Box>
|
|
18
|
+
<CopyToClipboard text={dot}/>
|
|
21
19
|
</ScrollArea>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
</Panel>
|
|
21
|
+
<PanelResizeHandle style={{
|
|
22
|
+
width: 10
|
|
23
|
+
}}/>
|
|
24
|
+
<Panel minSizePixels={100}>
|
|
25
|
+
<ScrollArea scrollbars='both'>
|
|
26
|
+
<Box py={'2'} style={{
|
|
25
27
|
overflow: 'scroll',
|
|
26
28
|
overscrollBehavior: 'none'
|
|
27
29
|
}}>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
<Box asChild position={'relative'} className={'svg-container'}>
|
|
31
|
+
<div dangerouslySetInnerHTML={{ __html: svgSource(viewId) }}></div>
|
|
32
|
+
</Box>
|
|
33
|
+
</Box>
|
|
34
|
+
</ScrollArea>
|
|
35
|
+
</Panel>
|
|
36
|
+
</PanelGroup>);
|
|
33
37
|
}
|
|
@@ -1,18 +1,46 @@
|
|
|
1
1
|
import { Box, Code, ScrollArea } from '@radix-ui/themes';
|
|
2
|
+
import { useAsync } from '@react-hookz/web/esm';
|
|
3
|
+
import { useEffect } from 'react';
|
|
2
4
|
import { mmdSource } from 'virtual:likec4/mmd-sources';
|
|
3
5
|
import { CopyToClipboard } from '../../../components';
|
|
6
|
+
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
|
7
|
+
const renderSvg = async (viewId, diagram) => {
|
|
8
|
+
const { default: mermaid } = await import('https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs');
|
|
9
|
+
mermaid.initialize({
|
|
10
|
+
theme: 'dark'
|
|
11
|
+
});
|
|
12
|
+
const { svg } = await mermaid.render(viewId, diagram);
|
|
13
|
+
return svg;
|
|
14
|
+
};
|
|
4
15
|
export default function ViewAsMmd({ viewId }) {
|
|
5
16
|
const src = mmdSource(viewId);
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
17
|
+
const [mmdSvg, { execute }] = useAsync(renderSvg, null);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
void execute(viewId, src);
|
|
20
|
+
}, [src]);
|
|
21
|
+
return (<PanelGroup direction='horizontal' autoSaveId='ViewAsD2'>
|
|
22
|
+
<Panel minSizePixels={100}>
|
|
23
|
+
<ScrollArea scrollbars='both'>
|
|
24
|
+
<Box asChild display={'block'} p='2' style={{
|
|
25
|
+
whiteSpace: 'pre',
|
|
26
|
+
minHeight: '100%'
|
|
10
27
|
}}>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
<Code variant='soft' autoFocus>
|
|
29
|
+
{src}
|
|
30
|
+
</Code>
|
|
31
|
+
</Box>
|
|
32
|
+
<CopyToClipboard text={src}/>
|
|
33
|
+
</ScrollArea>
|
|
34
|
+
</Panel>
|
|
35
|
+
<PanelResizeHandle style={{
|
|
36
|
+
width: 10
|
|
37
|
+
}}/>
|
|
38
|
+
<Panel minSizePixels={100}>
|
|
39
|
+
<ScrollArea scrollbars='both'>
|
|
40
|
+
{mmdSvg.result && (<Box grow={'1'} asChild position={'relative'} className={'svg-container'}>
|
|
41
|
+
<div dangerouslySetInnerHTML={{ __html: mmdSvg.result }}></div>
|
|
42
|
+
</Box>)}
|
|
43
|
+
</ScrollArea>
|
|
44
|
+
</Panel>
|
|
45
|
+
</PanelGroup>);
|
|
18
46
|
}
|
|
@@ -9,6 +9,7 @@ export default function ViewDiagramInOtherFormats({ viewId, viewMode }) {
|
|
|
9
9
|
<Tabs.Root value={viewMode} onValueChange={mode => mode !== viewMode && updateSearchParams({ mode: mode })}>
|
|
10
10
|
<Box asChild shrink={'0'} grow={'0'}>
|
|
11
11
|
<Tabs.List>
|
|
12
|
+
<Tabs.Trigger value='react'>React</Tabs.Trigger>
|
|
12
13
|
<Tabs.Trigger value='dot'>Graphviz</Tabs.Trigger>
|
|
13
14
|
<Tabs.Trigger value='mmd'>Mermaid</Tabs.Trigger>
|
|
14
15
|
<Tabs.Trigger value='d2'>D2</Tabs.Trigger>
|
|
@@ -16,6 +17,8 @@ export default function ViewDiagramInOtherFormats({ viewId, viewMode }) {
|
|
|
16
17
|
</Box>
|
|
17
18
|
|
|
18
19
|
<Box p='2' className={styles.otherFormats} position={'relative'}>
|
|
20
|
+
<Tabs.Content value='react'>{''}</Tabs.Content>
|
|
21
|
+
|
|
19
22
|
<Tabs.Content value='dot'>
|
|
20
23
|
<ViewAsDot viewId={viewId}/>
|
|
21
24
|
</Tabs.Content>
|
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
}
|
|
57
57
|
} */
|
|
58
58
|
|
|
59
|
-
.
|
|
59
|
+
:global(.svg-container) {
|
|
60
|
+
min-width: 300px;
|
|
60
61
|
& > svg {
|
|
61
62
|
width: 100%;
|
|
62
63
|
height: auto;
|
|
@@ -67,7 +68,7 @@
|
|
|
67
68
|
display: flex;
|
|
68
69
|
align-items: stretch;
|
|
69
70
|
flex: 1 1 auto;
|
|
70
|
-
overflow:
|
|
71
|
+
overflow: auto;
|
|
71
72
|
|
|
72
73
|
& > :global(.rt-TabsContent) {
|
|
73
74
|
display: flex;
|
|
@@ -77,5 +78,9 @@
|
|
|
77
78
|
&[hidden] {
|
|
78
79
|
display: none;
|
|
79
80
|
}
|
|
81
|
+
|
|
82
|
+
& :global(.rt-ScrollAreaViewport) > div {
|
|
83
|
+
height: inherit;
|
|
84
|
+
}
|
|
80
85
|
}
|
|
81
86
|
}
|