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
@@ -1,7 +1,7 @@
1
1
  import { Diagram, useDiagramApi } from '@likec4/diagrams';
2
2
  import { Box, Portal } from '@radix-ui/themes';
3
3
  import { useDebouncedEffect } from '@react-hookz/web/esm';
4
- import { memo } from 'react';
4
+ import { memo, useRef } from 'react';
5
5
  function downloadBlob(blob, name) {
6
6
  // Convert your blob into a Blob URL (a special url that points to an object in the browser's memory)
7
7
  const blobUrl = URL.createObjectURL(blob);
@@ -32,13 +32,19 @@ function downloadBlob(blob, name) {
32
32
  }
33
33
  const ExportDiagram = memo(({ diagram, onCompleted }) => {
34
34
  const id = diagram.id;
35
- const [ref, diagramApi] = useDiagramApi();
35
+ const [ref, api] = useDiagramApi();
36
36
  const padding = 20;
37
37
  const width = diagram.width + padding * 2;
38
38
  const height = diagram.height + padding * 2;
39
+ const onCompletedRef = useRef(onCompleted);
40
+ onCompletedRef.current = onCompleted;
39
41
  // To avoid flickering and double rendering
40
42
  useDebouncedEffect(() => {
41
- void diagramApi.stage
43
+ const stage = api.stage;
44
+ if (!stage) {
45
+ return;
46
+ }
47
+ void stage
42
48
  .toBlob({
43
49
  pixelRatio: 2,
44
50
  mimeType: 'image/png',
@@ -46,17 +52,17 @@ const ExportDiagram = memo(({ diagram, onCompleted }) => {
46
52
  if (blob) {
47
53
  downloadBlob(blob, `${diagram.id}.png`);
48
54
  }
49
- onCompleted();
55
+ onCompletedRef.current();
50
56
  }
51
57
  })
52
58
  .catch(err => {
53
- onCompleted();
59
+ onCompletedRef.current();
54
60
  // Show error after 100ms to avoid blocking the UI
55
61
  setTimeout(() => {
56
62
  window.alert(err);
57
63
  }, 100);
58
64
  });
59
- }, [id], 400);
65
+ }, [id, api], 400);
60
66
  return (<Portal>
61
67
  <Box position={'fixed'} style={{
62
68
  top: 0,
@@ -65,7 +71,7 @@ const ExportDiagram = memo(({ diagram, onCompleted }) => {
65
71
  height,
66
72
  transform: `translateY(${-height}px)`
67
73
  }}>
68
- <Diagram ref={ref} animate={false} pannable={false} zoomable={false} diagram={diagram} padding={padding} width={width} height={height}/>
74
+ <Diagram ref={ref} animate={false} pannable={false} zoomable={false} minZoom={1} maxZoom={1} diagram={diagram} padding={padding} width={width} height={height}/>
69
75
  </Box>
70
76
  </Portal>);
71
77
  });
@@ -34,11 +34,17 @@ const ExportMenu = ({ onExport, children }) => (<DropdownMenu.Root>
34
34
  </DropdownMenu.Root>);
35
35
  export const ViewActionsToolbar = ({ diagram }) => {
36
36
  const [exportTo, setExportTo] = useState(null);
37
- return (<Flex position='fixed' top='0' right='0' p='2' gap={'3'} justify='end' align='center'>
37
+ return (<Flex position='fixed' top='0' right='0' p={{
38
+ initial: '3',
39
+ md: '2'
40
+ }} gap={'3'} justify='end' align='center'>
38
41
  <DisplayModeSelector />
39
42
  <Dialog.Root>
40
43
  <Dialog.Trigger>
41
- <Button variant='solid'>
44
+ <Button variant='solid' size={{
45
+ initial: '1',
46
+ md: '2'
47
+ }}>
42
48
  <ShareIcon />
43
49
  <Text>Share</Text>
44
50
  </Button>
@@ -46,7 +52,10 @@ export const ViewActionsToolbar = ({ diagram }) => {
46
52
  <ShareDialog diagram={diagram}/>
47
53
  </Dialog.Root>
48
54
  <ExportMenu onExport={setExportTo}>
49
- <Button variant='soft' color='gray'>
55
+ <Button variant='soft' color='gray' size={{
56
+ initial: '1',
57
+ md: '2'
58
+ }}>
50
59
  <Text>Export</Text>
51
60
  <CaretDownIcon />
52
61
  </Button>
@@ -58,26 +58,15 @@ export const selectLikeC4ViewAtom = (viewId) => {
58
58
  if (import.meta.hot) {
59
59
  let $updateViews;
60
60
  viewsAtom.onMount = set => {
61
- console.log('mount viewsAtom');
62
61
  $updateViews = set;
63
62
  return () => {
64
- console.log('unmount viewsAtom');
65
63
  $updateViews = undefined;
66
64
  };
67
65
  };
68
66
  import.meta.hot.accept('/@vite-plugin-likec4/likec4-generated', md => {
69
67
  const update = md?.LikeC4Views;
70
- console.debug('accept ./data update');
71
- console.dir(update, {
72
- colors: true,
73
- compact: false,
74
- depth: 10
75
- });
76
68
  if (update) {
77
69
  $updateViews?.(update);
78
70
  }
79
- else {
80
- console.warn('no update', md);
81
- }
82
71
  });
83
72
  }
@@ -0,0 +1,14 @@
1
+ import { Diagram } from '@likec4/diagrams';
2
+ import { useWindowSize } from '@react-hookz/web/esm';
3
+ import { DiagramNotFound } from '../components';
4
+ import { useLikeC4View } from '../data';
5
+ import { useTransparentBackground } from './useTransparentBackground';
6
+ export function EmbedPage({ viewId, padding, transparentBg = true }) {
7
+ const { width, height } = useWindowSize();
8
+ const diagram = useLikeC4View(viewId);
9
+ useTransparentBackground(transparentBg && !!diagram);
10
+ if (!diagram) {
11
+ return <DiagramNotFound viewId={viewId}/>;
12
+ }
13
+ return (<Diagram animate={false} pannable={false} zoomable={false} diagram={diagram} padding={padding} width={width} height={height}/>);
14
+ }
@@ -1,23 +1,14 @@
1
1
  import { Diagram } from '@likec4/diagrams';
2
2
  import { useWindowSize } from '@react-hookz/web/esm';
3
- import { useLayoutEffect } from 'react';
4
3
  import { DiagramNotFound } from '../components';
5
4
  import { useLikeC4View } from '../data';
5
+ import { useTransparentBackground } from './useTransparentBackground';
6
6
  export function ExportPage({ viewId, padding }) {
7
7
  const { width, height } = useWindowSize();
8
8
  const diagram = useLikeC4View(viewId);
9
- // To get the transparent background
10
- // We need to add a class to the HTML element
11
- useLayoutEffect(() => {
12
- // see ../../likec4.css
13
- const classname = 'transparent-bg';
14
- document.body.parentElement?.classList.add(classname);
15
- return () => {
16
- document.body.parentElement?.classList.remove(classname);
17
- };
18
- }, []);
9
+ useTransparentBackground(!!diagram);
19
10
  if (!diagram) {
20
- return <DiagramNotFound />;
11
+ return <DiagramNotFound viewId={viewId}/>;
21
12
  }
22
- return (<Diagram animate={false} pannable={false} zoomable={false} diagram={diagram} padding={padding} width={width} height={height}/>);
13
+ return (<Diagram animate={false} pannable={false} zoomable={false} maxZoom={1} minZoom={1} diagram={diagram} padding={padding} width={width} height={height}/>);
23
14
  }
@@ -1,3 +1,4 @@
1
1
  export * from './index.page';
2
2
  export * from './export.page';
3
+ export * from './embed.page';
3
4
  export * from './view.page';
@@ -0,0 +1,16 @@
1
+ import { useEffect } from 'react';
2
+ // To get the transparent background
3
+ // We need to add a class to the HTML element
4
+ export function useTransparentBackground(enabled = true) {
5
+ useEffect(() => {
6
+ const htmlEl = document.body.parentElement;
7
+ if (!htmlEl || !enabled)
8
+ return;
9
+ // see ../../likec4.css
10
+ const classname = 'transparent-bg';
11
+ htmlEl.classList.add(classname);
12
+ return () => {
13
+ htmlEl.classList.remove(classname);
14
+ };
15
+ }, [enabled]);
16
+ }
@@ -0,0 +1,30 @@
1
+ .diagramBg {
2
+ overscroll-behavior: none;
3
+ overflow: hidden;
4
+ --diagram-bg-size: 24px;
5
+ --diagram-bg-position-x: 0;
6
+ --diagram-bg-position-y: 0;
7
+
8
+ :global(.konvajs-content) {
9
+
10
+ &::before {
11
+ content: '';
12
+ position: absolute;
13
+ padding: 0;
14
+ margin: 0;
15
+ top: 0;
16
+ left: 0;
17
+ width: 100%;
18
+ height: 100%;
19
+ pointer-events: none;
20
+ touch-action: none;
21
+ user-select: none;
22
+ background-origin: border-box;
23
+ background-attachment: fixed;
24
+ background-image: radial-gradient(var(--gray-a3) 12%, transparent 12%);
25
+ background-position: var(--diagram-bg-position-x) var(--diagram-bg-position-y);
26
+ background-size: var(--diagram-bg-size) var(--diagram-bg-size);
27
+ z-index: -1;
28
+ }
29
+ }
30
+ }
@@ -1,24 +1,60 @@
1
1
  import { Diagram, useDiagramApi } from '@likec4/diagrams';
2
2
  import { Box, Flex, Heading, Text } from '@radix-ui/themes';
3
3
  import { useWindowSize } from '@react-hookz/web/esm';
4
- import { $pages } from '~/router';
4
+ import { $pages } from '../router';
5
5
  import { DiagramNotFound, ViewActionsToolbar } from '../components';
6
6
  import { useLikeC4View } from '../data';
7
- const Paddings = [60, 20, 20, 20];
7
+ import { Fragment, useCallback, useEffect, useRef } from 'react';
8
+ import styles from './view-page.module.css';
9
+ const Paddings = [70, 20, 20, 40];
10
+ function round(n, d = 1) {
11
+ const m = Math.pow(10, d);
12
+ return Math.round(n * m) / m;
13
+ }
8
14
  export function ViewPage({ viewId, showUI = true }) {
9
15
  const { width, height } = useWindowSize();
10
- const [ref, diagramApi] = useDiagramApi();
11
16
  const diagram = useLikeC4View(viewId);
17
+ const pageDivRef = useRef(null);
18
+ const [ref, api] = useDiagramApi();
19
+ const handleTransform = useCallback(() => {
20
+ const stage = api.stage;
21
+ const style = pageDivRef.current?.style;
22
+ if (!stage || !style) {
23
+ return;
24
+ }
25
+ const pos = stage.getAbsolutePosition();
26
+ let scale = stage.scaleX();
27
+ while (scale < 0.5) {
28
+ scale = scale / 0.5;
29
+ }
30
+ const size = round(24 * scale);
31
+ const x = round(pos.x);
32
+ const y = round(pos.y);
33
+ style.setProperty('--diagram-bg-size', `${size}px`);
34
+ style.setProperty('--diagram-bg-position-x', `${x}px`);
35
+ style.setProperty('--diagram-bg-position-y', `${y}px`);
36
+ }, [api, pageDivRef]);
37
+ useEffect(() => {
38
+ const stage = api.stage;
39
+ if (!stage) {
40
+ return;
41
+ }
42
+ stage.on('absoluteTransformChange', handleTransform);
43
+ handleTransform();
44
+ return () => {
45
+ stage.off('absoluteTransformChange', handleTransform);
46
+ };
47
+ }, [api]);
12
48
  if (!diagram) {
13
- return <DiagramNotFound />;
49
+ return <DiagramNotFound viewId={viewId}/>;
14
50
  }
15
- return (<Box position={'fixed'} inset='0' className='overflow-hidden'>
51
+ return (<Box position={'fixed'} inset='0' className={styles.diagramBg} ref={pageDivRef}>
16
52
  <Diagram ref={ref} diagram={diagram} padding={showUI ? Paddings : undefined} width={width} height={height} onNodeClick={node => {
17
53
  if (node.navigateTo) {
18
54
  $pages.view.open(node.navigateTo);
19
55
  }
20
56
  }} onEdgeClick={_ => ({})}/>
21
- {showUI && (<>
57
+ {showUI && (<Fragment key='ui'>
22
58
  <Flex position={'fixed'} top='0' p='3' style={{
23
59
  left: 54
24
60
  }} direction={'column'}>
@@ -29,7 +65,7 @@ export function ViewPage({ viewId, showUI = true }) {
29
65
  {diagram.title || 'Untitled'}
30
66
  </Heading>
31
67
  </Flex>
32
- <ViewActionsToolbar diagramApi={diagramApi} diagram={diagram}/>
33
- </>)}
68
+ <ViewActionsToolbar diagram={diagram}/>
69
+ </Fragment>)}
34
70
  </Box>);
35
71
  }
@@ -1,5 +1,3 @@
1
- // stores/router.ts
2
- import { logger } from '@nanostores/logger';
3
1
  import { useStore } from '@nanostores/react';
4
2
  import { createSearchParams, createRouter, openPage, getPagePath } from '@nanostores/router';
5
3
  import { computed } from 'nanostores';
@@ -30,24 +28,28 @@ const $route = computed([$router, $searchParams], (r, v) => {
30
28
  return {
31
29
  route: 'view',
32
30
  params: {
33
- viewId: r.params.viewId ?? 'index'
31
+ viewId: r.params.viewId ?? 'index',
32
+ theme: asTheme(v.theme) ?? 'dark'
34
33
  },
35
34
  showUI: 'showUI' in v ? v.showUI === 'true' : true
36
35
  };
37
36
  }
38
37
  if (r?.route === 'export' || r?.route === 'embed') {
39
38
  return {
40
- route: 'export',
39
+ route: r.route,
41
40
  params: {
42
41
  viewId: r.params.viewId,
43
42
  padding: asPadding(v.padding),
44
- theme: asTheme(v.theme)
43
+ theme: r.route === 'embed' ? asTheme(v.theme) : undefined
45
44
  },
46
45
  showUI: false
47
46
  };
48
47
  }
49
48
  return {
50
49
  route: 'index',
50
+ params: {
51
+ theme: asTheme(v.theme) ?? 'dark'
52
+ },
51
53
  showUI: 'showUI' in v ? v.showUI === 'true' : true
52
54
  };
53
55
  });
@@ -67,10 +69,10 @@ export const $pages = {
67
69
  path: (viewId) => getPagePath($router, 'embed', { viewId })
68
70
  }
69
71
  };
70
- if (import.meta.env.DEV) {
71
- logger({
72
- $searchParams,
73
- $router,
74
- $route
75
- });
76
- }
72
+ // if (import.meta.env.DEV) {
73
+ // logger({
74
+ // $searchParams,
75
+ // $router,
76
+ // $route
77
+ // })
78
+ // }
@@ -1,5 +1,4 @@
1
1
  const { radixThemePreset } = require('radix-themes-tw');
2
- const tailwindcssAnimate = require('tailwindcss-animate');
3
2
  const { resolve } = require('node:path');
4
3
 
5
4
  /** @type {import('tailwindcss').Config} */
@@ -14,6 +13,5 @@ module.exports = {
14
13
  // ...
15
14
  // Preflight is enabled by default, we disable it here
16
15
  preflight: false
17
- },
18
- plugins: [tailwindcssAnimate],
16
+ }
19
17
  };
@@ -21,17 +21,11 @@
21
21
  "noFallthroughCasesInSwitch": true,
22
22
  "baseUrl": ".",
23
23
  "rootDir": ".",
24
- "paths": {
25
- "~/*": [
26
- "./src/*"
27
- ]
28
- },
29
24
  "types": [
30
25
  "vite/client"
31
26
  ]
32
27
  },
33
28
  "include": [
34
- "src",
35
- "src/likec4-views.ts"
29
+ "src"
36
30
  ]
37
31
  }