likec4 0.52.0 → 0.54.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/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 +1 -1
- 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/other-formats/ViewAsD2.jsx +25 -25
- package/dist/__app__/src/pages/view-page/other-formats/ViewAsDot.jsx +19 -15
- package/dist/__app__/src/pages/view-page/other-formats/ViewAsMmd.jsx +11 -14
- package/dist/__app__/src/pages/view-page/other-formats.jsx +24 -43
- package/dist/__app__/src/pages/view-page/view-page.module.css +1 -1
- package/dist/cli/index.js +193 -191
- package/package.json +12 -12
- 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,
|
|
@@ -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,8 @@
|
|
|
1
|
-
import { Box,
|
|
2
|
-
import
|
|
1
|
+
import { Box, Button, Code, ScrollArea } from '@radix-ui/themes';
|
|
2
|
+
import { useAsync } from '@react-hookz/web/esm';
|
|
3
3
|
import { d2Source } from 'virtual:likec4/d2-sources';
|
|
4
4
|
import { CopyToClipboard } from '../../../components';
|
|
5
|
+
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
|
5
6
|
const fetchFromKroki = async (d2) => {
|
|
6
7
|
const res = await fetch('https://kroki.io/d2/svg', {
|
|
7
8
|
method: 'POST',
|
|
@@ -21,16 +22,9 @@ const fetchFromKroki = async (d2) => {
|
|
|
21
22
|
};
|
|
22
23
|
export default function ViewAsD2({ viewId }) {
|
|
23
24
|
const src = d2Source(viewId);
|
|
24
|
-
const {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
});
|
|
28
|
-
return (<Flex gap='2' shrink='1' grow='1' align={'stretch'} wrap={'nowrap'} style={{
|
|
29
|
-
overflow: 'hidden'
|
|
30
|
-
}}>
|
|
31
|
-
<Box py={'2'} position={'relative'} style={{
|
|
32
|
-
overflow: 'scroll'
|
|
33
|
-
}}>
|
|
25
|
+
const [krokiSvg, { execute }] = useAsync(fetchFromKroki, null);
|
|
26
|
+
return (<PanelGroup direction='horizontal' autoSaveId='ViewAsD2'>
|
|
27
|
+
<Panel minSizePixels={100}>
|
|
34
28
|
<ScrollArea scrollbars='both'>
|
|
35
29
|
<Box asChild display={'block'} p='2' style={{
|
|
36
30
|
whiteSpace: 'pre',
|
|
@@ -40,18 +34,24 @@ export default function ViewAsD2({ viewId }) {
|
|
|
40
34
|
{src}
|
|
41
35
|
</Code>
|
|
42
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>)}
|
|
43
54
|
</ScrollArea>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
{krokiSvg && (<Box py={'2'} grow={'1'} shrink={'0'} style={{
|
|
47
|
-
width: '50%',
|
|
48
|
-
overflow: 'scroll'
|
|
49
|
-
}}>
|
|
50
|
-
<ScrollArea scrollbars='both'>
|
|
51
|
-
<Box grow={'1'} asChild className={'svg-container'}>
|
|
52
|
-
<div dangerouslySetInnerHTML={{ __html: krokiSvg }}></div>
|
|
53
|
-
</Box>
|
|
54
|
-
</ScrollArea>
|
|
55
|
-
</Box>)}
|
|
56
|
-
</Flex>);
|
|
55
|
+
</Panel>
|
|
56
|
+
</PanelGroup>);
|
|
57
57
|
}
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import { Box, Code,
|
|
1
|
+
import { Box, Code, ScrollArea } from '@radix-ui/themes';
|
|
2
2
|
import { dotSource, svgSource } from 'virtual:likec4/dot-sources';
|
|
3
3
|
import { CopyToClipboard } from '../../../components';
|
|
4
|
+
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
|
4
5
|
export default function ViewAsDot({ viewId }) {
|
|
5
6
|
const dot = dotSource(viewId);
|
|
6
|
-
return (<
|
|
7
|
-
|
|
8
|
-
columns='2' gap='2' shrink='1' grow='1'>
|
|
9
|
-
<Box py={'2'} position={'relative'} style={{
|
|
10
|
-
overflow: 'scroll'
|
|
11
|
-
}}>
|
|
7
|
+
return (<PanelGroup direction='horizontal' autoSaveId='viewAsDot'>
|
|
8
|
+
<Panel minSizePixels={100}>
|
|
12
9
|
<ScrollArea scrollbars='both'>
|
|
13
10
|
<Box asChild display={'block'} p='2' style={{
|
|
14
11
|
whiteSpace: 'pre',
|
|
@@ -18,16 +15,23 @@ export default function ViewAsDot({ viewId }) {
|
|
|
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,8 +1,9 @@
|
|
|
1
|
-
import { Box, Code,
|
|
1
|
+
import { Box, Code, ScrollArea } from '@radix-ui/themes';
|
|
2
2
|
import { useAsync } from '@react-hookz/web/esm';
|
|
3
3
|
import { useEffect } from 'react';
|
|
4
4
|
import { mmdSource } from 'virtual:likec4/mmd-sources';
|
|
5
5
|
import { CopyToClipboard } from '../../../components';
|
|
6
|
+
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
|
6
7
|
const renderSvg = async (viewId, diagram) => {
|
|
7
8
|
const { default: mermaid } = await import('https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs');
|
|
8
9
|
mermaid.initialize({
|
|
@@ -17,12 +18,8 @@ export default function ViewAsMmd({ viewId }) {
|
|
|
17
18
|
useEffect(() => {
|
|
18
19
|
void execute(viewId, src);
|
|
19
20
|
}, [src]);
|
|
20
|
-
return (<
|
|
21
|
-
|
|
22
|
-
}}>
|
|
23
|
-
<Box py={'2'} position={'relative'} style={{
|
|
24
|
-
overflow: 'scroll'
|
|
25
|
-
}}>
|
|
21
|
+
return (<PanelGroup direction='horizontal' autoSaveId='ViewAsD2'>
|
|
22
|
+
<Panel minSizePixels={100}>
|
|
26
23
|
<ScrollArea scrollbars='both'>
|
|
27
24
|
<Box asChild display={'block'} p='2' style={{
|
|
28
25
|
whiteSpace: 'pre',
|
|
@@ -34,16 +31,16 @@ export default function ViewAsMmd({ viewId }) {
|
|
|
34
31
|
</Box>
|
|
35
32
|
<CopyToClipboard text={src}/>
|
|
36
33
|
</ScrollArea>
|
|
37
|
-
</
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
</Panel>
|
|
35
|
+
<PanelResizeHandle style={{
|
|
36
|
+
width: 10
|
|
37
|
+
}}/>
|
|
38
|
+
<Panel minSizePixels={100}>
|
|
42
39
|
<ScrollArea scrollbars='both'>
|
|
43
40
|
{mmdSvg.result && (<Box grow={'1'} asChild position={'relative'} className={'svg-container'}>
|
|
44
41
|
<div dangerouslySetInnerHTML={{ __html: mmdSvg.result }}></div>
|
|
45
42
|
</Box>)}
|
|
46
43
|
</ScrollArea>
|
|
47
|
-
</
|
|
48
|
-
</
|
|
44
|
+
</Panel>
|
|
45
|
+
</PanelGroup>);
|
|
49
46
|
}
|
|
@@ -4,54 +4,35 @@ import ViewAsD2 from './other-formats/ViewAsD2';
|
|
|
4
4
|
import ViewAsDot from './other-formats/ViewAsDot';
|
|
5
5
|
import ViewAsMmd from './other-formats/ViewAsMmd';
|
|
6
6
|
import styles from './view-page.module.css';
|
|
7
|
-
import { SWRConfig } from 'swr';
|
|
8
|
-
function localStorageProvider() {
|
|
9
|
-
// When initializing, we restore the data from `localStorage` into a map.
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
-
const map = new Map(JSON.parse(localStorage.getItem('swr-cache') || '[]'));
|
|
12
|
-
// Before unloading the app, we write back all the data into `localStorage`.
|
|
13
|
-
window.addEventListener('beforeunload', () => {
|
|
14
|
-
const appCache = JSON.stringify(Array.from(map.entries()));
|
|
15
|
-
localStorage.setItem('swr-cache', appCache);
|
|
16
|
-
});
|
|
17
|
-
// We still use the map for write & read for performance.
|
|
18
|
-
return map;
|
|
19
|
-
}
|
|
20
7
|
export default function ViewDiagramInOtherFormats({ viewId, viewMode }) {
|
|
21
|
-
return (<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<Tabs.Trigger value='dot'>Graphviz</Tabs.Trigger>
|
|
32
|
-
<Tabs.Trigger value='mmd'>Mermaid</Tabs.Trigger>
|
|
33
|
-
<Tabs.Trigger value='d2'>D2</Tabs.Trigger>
|
|
34
|
-
</Tabs.List>
|
|
35
|
-
</Box>
|
|
8
|
+
return (<Flex asChild position={'fixed'} inset={'0'} pt={'8'} pl={'8'} pr={'2'} align={'stretch'} direction={'column'}>
|
|
9
|
+
<Tabs.Root value={viewMode} onValueChange={mode => mode !== viewMode && updateSearchParams({ mode: mode })}>
|
|
10
|
+
<Box asChild shrink={'0'} grow={'0'}>
|
|
11
|
+
<Tabs.List>
|
|
12
|
+
<Tabs.Trigger value='react'>React</Tabs.Trigger>
|
|
13
|
+
<Tabs.Trigger value='dot'>Graphviz</Tabs.Trigger>
|
|
14
|
+
<Tabs.Trigger value='mmd'>Mermaid</Tabs.Trigger>
|
|
15
|
+
<Tabs.Trigger value='d2'>D2</Tabs.Trigger>
|
|
16
|
+
</Tabs.List>
|
|
17
|
+
</Box>
|
|
36
18
|
|
|
37
|
-
|
|
38
|
-
|
|
19
|
+
<Box p='2' className={styles.otherFormats} position={'relative'}>
|
|
20
|
+
<Tabs.Content value='react'>{''}</Tabs.Content>
|
|
39
21
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
22
|
+
<Tabs.Content value='dot'>
|
|
23
|
+
<ViewAsDot viewId={viewId}/>
|
|
24
|
+
</Tabs.Content>
|
|
43
25
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
26
|
+
<Tabs.Content value='mmd'>
|
|
27
|
+
<ViewAsMmd viewId={viewId}/>
|
|
28
|
+
</Tabs.Content>
|
|
47
29
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
</SWRConfig>);
|
|
30
|
+
<Tabs.Content value='d2'>
|
|
31
|
+
<ViewAsD2 viewId={viewId}/>
|
|
32
|
+
</Tabs.Content>
|
|
33
|
+
</Box>
|
|
34
|
+
</Tabs.Root>
|
|
35
|
+
</Flex>);
|
|
55
36
|
// switch (viewMode) {
|
|
56
37
|
// case 'dot':
|
|
57
38
|
// return <ViewAsDot viewId={viewId} />
|