likec4 0.50.0 → 0.52.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.
@@ -67,10 +67,16 @@ export function ancestorsFqn(fqn) {
67
67
  export function compareFqnHierarchically(a, b) {
68
68
  const depthA = a.split(".").length;
69
69
  const depthB = b.split(".").length;
70
- if (depthA === depthB) {
71
- return 0;
72
- } else {
73
- return depthA - depthB;
70
+ switch (true) {
71
+ case depthA > depthB: {
72
+ return 1;
73
+ }
74
+ case depthA < depthB: {
75
+ return -1;
76
+ }
77
+ default: {
78
+ return 0;
79
+ }
74
80
  }
75
81
  }
76
82
  export function compareByFqnHierarchically(a, b) {
@@ -10,7 +10,14 @@ export const compareRelations = (a, b) => {
10
10
  return -1;
11
11
  }
12
12
  const compareParents = parentA && parentB ? compareFqnHierarchically(parentA, parentB) : 0;
13
- return compareParents || compareFqnHierarchically(a.source, b.source) || compareFqnHierarchically(a.target, b.target);
13
+ if (compareParents === 0) {
14
+ const compareSource = compareFqnHierarchically(a.source, b.source);
15
+ if (compareSource !== 0) {
16
+ return compareSource;
17
+ }
18
+ return compareFqnHierarchically(a.target, b.target);
19
+ }
20
+ return compareParents;
14
21
  };
15
22
  const isInside = (parent) => {
16
23
  const prefix = parent + ".";
@@ -3,8 +3,9 @@ import { nonexhaustive } from "@likec4/core";
3
3
  import { isEqualSimple } from "@react-hookz/deep-equal/esnext";
4
4
  import { useToggle } from "@react-hookz/web/esm";
5
5
  import { useSpring, useTransition } from "@react-spring/konva";
6
- import { mix, toHex, lighten } from "khroma";
6
+ import { lighten, mix, toHex } from "khroma";
7
7
  import { memo, useRef } from "react";
8
+ import { Group } from "react-konva";
8
9
  import { AnimatedCircle, AnimatedGroup, Rect } from "../konva.js";
9
10
  import { Portal } from "../konva-portal.js";
10
11
  import { ZoomInIcon } from "./icons/index.js";
@@ -14,7 +15,6 @@ import { CompoundShape } from "./shapes/Compound.js";
14
15
  import { mouseDefault, mousePointer } from "./shapes/utils.js";
15
16
  import { isCompound, useNodeSpringsFn } from "./springs.js";
16
17
  import { DiagramGesture, useHoveredEdge, useHoveredNodeId, useSetHoveredNode } from "./state/index.js";
17
- import { Group } from "react-konva";
18
18
  function nodeShape({ shape }) {
19
19
  switch (shape) {
20
20
  case "cylinder":
@@ -35,14 +35,15 @@ function EdgeLabelBg({
35
35
  isHovered,
36
36
  springs
37
37
  }) {
38
- const padding = 2;
38
+ const paddingX = 2;
39
+ const paddingY = 1;
39
40
  const props = useSpring({
40
41
  to: {
41
- x: labelBBox.x - padding,
42
- y: labelBBox.y - padding,
43
- width: labelBBox.width + padding * 2,
44
- height: labelBBox.height + padding * 2,
45
- opacity: isHovered ? 0.75 : 0.65
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
  });
@@ -52,7 +53,7 @@ function EdgeLabelBg({
52
53
  ...props,
53
54
  perfectDrawEnabled: false,
54
55
  fill: springs.labelBgColor,
55
- cornerRadius: 2,
56
+ cornerRadius: 3,
56
57
  globalCompositeOperation: "lighten",
57
58
  hitStrokeWidth: 20
58
59
  }
@@ -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, Code, Flex, ScrollArea } from '@radix-ui/themes';
2
+ import useSWR from 'swr';
2
3
  import { d2Source } from 'virtual:likec4/d2-sources';
3
4
  import { CopyToClipboard } from '../../../components';
5
+ const fetchFromKroki = async (d2) => {
6
+ const res = await fetch('https://kroki.io/d2/svg', {
7
+ method: 'POST',
8
+ cache: 'force-cache',
9
+ body: JSON.stringify({
10
+ diagram_source: d2,
11
+ // diagram_options: {
12
+ // theme: 'colorblind-clear'
13
+ // },
14
+ output_format: 'svg'
15
+ }),
16
+ headers: {
17
+ 'Content-Type': 'application/json'
18
+ }
19
+ });
20
+ return await res.text();
21
+ };
4
22
  export default function ViewAsD2({ viewId }) {
5
23
  const src = d2Source(viewId);
6
- return (<>
7
- <ScrollArea scrollbars='both'>
8
- <Box asChild display={'block'} p='2' style={{
9
- whiteSpace: 'pre'
24
+ const { data: krokiSvg } = useSWR(src, fetchFromKroki, {
25
+ keepPreviousData: true,
26
+ revalidateIfStale: false
27
+ });
28
+ return (<Flex gap='2' shrink='1' grow='1' align={'stretch'} wrap={'nowrap'} style={{
29
+ overflow: 'hidden'
10
30
  }}>
11
- <Code variant='soft' autoFocus>
12
- {src}
13
- </Code>
14
- </Box>
15
- </ScrollArea>
16
- <CopyToClipboard text={src}/>
17
- </>);
31
+ <Box py={'2'} position={'relative'} style={{
32
+ overflow: 'scroll'
33
+ }}>
34
+ <ScrollArea scrollbars='both'>
35
+ <Box asChild display={'block'} p='2' style={{
36
+ whiteSpace: 'pre',
37
+ minHeight: '100%'
38
+ }}>
39
+ <Code variant='soft' autoFocus>
40
+ {src}
41
+ </Code>
42
+ </Box>
43
+ </ScrollArea>
44
+ <CopyToClipboard text={src}/>
45
+ </Box>
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>);
18
57
  }
@@ -1,6 +1,5 @@
1
1
  import { Box, Code, Grid, 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';
5
4
  export default function ViewAsDot({ viewId }) {
6
5
  const dot = dotSource(viewId);
@@ -12,7 +11,8 @@ export default function ViewAsDot({ viewId }) {
12
11
  }}>
13
12
  <ScrollArea scrollbars='both'>
14
13
  <Box asChild display={'block'} p='2' style={{
15
- whiteSpace: 'pre'
14
+ whiteSpace: 'pre',
15
+ minHeight: '100%'
16
16
  }}>
17
17
  <Code variant='soft' autoFocus>
18
18
  {dot}
@@ -25,7 +25,7 @@ export default function ViewAsDot({ viewId }) {
25
25
  overflow: 'scroll',
26
26
  overscrollBehavior: 'none'
27
27
  }}>
28
- <Box asChild position={'relative'} className={styles.dotSvg}>
28
+ <Box asChild position={'relative'} className={'svg-container'}>
29
29
  <div dangerouslySetInnerHTML={{ __html: svgSource(viewId) }}></div>
30
30
  </Box>
31
31
  </Box>
@@ -1,18 +1,49 @@
1
- import { Box, Code, ScrollArea } from '@radix-ui/themes';
1
+ import { Box, Code, Flex, 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
+ const renderSvg = async (viewId, diagram) => {
7
+ const { default: mermaid } = await import('https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs');
8
+ mermaid.initialize({
9
+ theme: 'dark'
10
+ });
11
+ const { svg } = await mermaid.render(viewId, diagram);
12
+ return svg;
13
+ };
4
14
  export default function ViewAsMmd({ viewId }) {
5
15
  const src = mmdSource(viewId);
6
- return (<>
7
- <ScrollArea scrollbars='both'>
8
- <Box asChild display={'block'} p='2' style={{
9
- whiteSpace: 'pre'
16
+ const [mmdSvg, { execute }] = useAsync(renderSvg, null);
17
+ useEffect(() => {
18
+ void execute(viewId, src);
19
+ }, [src]);
20
+ return (<Flex gap='2' shrink='1' grow='1' align={'stretch'} wrap={'nowrap'} style={{
21
+ overflow: 'hidden'
10
22
  }}>
11
- <Code variant='soft' autoFocus>
12
- {src}
13
- </Code>
14
- </Box>
15
- </ScrollArea>
16
- <CopyToClipboard text={src}/>
17
- </>);
23
+ <Box py={'2'} position={'relative'} style={{
24
+ overflow: 'scroll'
25
+ }}>
26
+ <ScrollArea scrollbars='both'>
27
+ <Box asChild display={'block'} p='2' style={{
28
+ whiteSpace: 'pre',
29
+ minHeight: '100%'
30
+ }}>
31
+ <Code variant='soft' autoFocus>
32
+ {src}
33
+ </Code>
34
+ </Box>
35
+ <CopyToClipboard text={src}/>
36
+ </ScrollArea>
37
+ </Box>
38
+ <Box py={'2'} position={'relative'} grow={'1'} shrink={'0'} style={{
39
+ minWidth: '50vw',
40
+ overflow: 'scroll'
41
+ }}>
42
+ <ScrollArea scrollbars='both'>
43
+ {mmdSvg.result && (<Box grow={'1'} asChild position={'relative'} className={'svg-container'}>
44
+ <div dangerouslySetInnerHTML={{ __html: mmdSvg.result }}></div>
45
+ </Box>)}
46
+ </ScrollArea>
47
+ </Box>
48
+ </Flex>);
18
49
  }
@@ -4,32 +4,54 @@ 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
+ }
7
20
  export default function ViewDiagramInOtherFormats({ viewId, viewMode }) {
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='dot'>Graphviz</Tabs.Trigger>
13
- <Tabs.Trigger value='mmd'>Mermaid</Tabs.Trigger>
14
- <Tabs.Trigger value='d2'>D2</Tabs.Trigger>
15
- </Tabs.List>
16
- </Box>
21
+ return (<SWRConfig value={{
22
+ keepPreviousData: true,
23
+ errorRetryCount: 5,
24
+ provider: localStorageProvider
25
+ }}>
26
+ <Flex asChild position={'fixed'} inset={'0'} pt={'8'} pl={'8'} pr={'2'} align={'stretch'} direction={'column'}>
27
+ <Tabs.Root value={viewMode} onValueChange={mode => mode !== viewMode && updateSearchParams({ mode: mode })}>
28
+ <Box asChild shrink={'0'} grow={'0'}>
29
+ <Tabs.List>
30
+ <Tabs.Trigger value='react'>React</Tabs.Trigger>
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>
36
+
37
+ <Box p='2' className={styles.otherFormats} position={'relative'}>
38
+ <Tabs.Content value='react'>{''}</Tabs.Content>
17
39
 
18
- <Box p='2' className={styles.otherFormats} position={'relative'}>
19
- <Tabs.Content value='dot'>
20
- <ViewAsDot viewId={viewId}/>
21
- </Tabs.Content>
40
+ <Tabs.Content value='dot'>
41
+ <ViewAsDot viewId={viewId}/>
42
+ </Tabs.Content>
22
43
 
23
- <Tabs.Content value='mmd'>
24
- <ViewAsMmd viewId={viewId}/>
25
- </Tabs.Content>
44
+ <Tabs.Content value='mmd'>
45
+ <ViewAsMmd viewId={viewId}/>
46
+ </Tabs.Content>
26
47
 
27
- <Tabs.Content value='d2'>
28
- <ViewAsD2 viewId={viewId}/>
29
- </Tabs.Content>
30
- </Box>
31
- </Tabs.Root>
32
- </Flex>);
48
+ <Tabs.Content value='d2'>
49
+ <ViewAsD2 viewId={viewId}/>
50
+ </Tabs.Content>
51
+ </Box>
52
+ </Tabs.Root>
53
+ </Flex>
54
+ </SWRConfig>);
33
55
  // switch (viewMode) {
34
56
  // case 'dot':
35
57
  // return <ViewAsDot viewId={viewId} />
@@ -56,7 +56,8 @@
56
56
  }
57
57
  } */
58
58
 
59
- .dotSvg {
59
+ :global(.svg-container) {
60
+ min-width: 300px;
60
61
  & > svg {
61
62
  width: 100%;
62
63
  height: auto;
@@ -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
  }