likec4 0.43.0 → 0.44.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/README.md CHANGED
@@ -4,10 +4,10 @@
4
4
 
5
5
  Features:
6
6
 
7
- - preview diagrams in a local web server (with fast hot-reload on changes)
8
- - build a static website for sharing and embedding diagrams
9
- - export to PNG, Mermaid, Dot, D2
10
- - generate React components
7
+ - Preview diagrams in a local web server (with lightning fast updates) ⚡️
8
+ - Build a static .website (deploy to github pages, netlify...) 🔗
9
+ - Export to PNG, Mermaid, Dot, D2 (if you something static) 🖼️
10
+ - Generate React components (for custom integrations ) 🛠️
11
11
 
12
12
  ## Install
13
13
 
@@ -33,10 +33,6 @@ You can reference it directly in the `package.json#scripts` object:
33
33
  }
34
34
  ```
35
35
 
36
- > **Template:**
37
- > Check out the template repository [likec4/template](https://github.com/likec4/template)
38
- > with pre-configured CI for building and deploying to github pages.
39
-
40
36
  To use the binary, you can call it with [`npx`](https://docs.npmjs.com/cli/v10/commands/npx) while in the project directory:
41
37
 
42
38
  ```sh
@@ -88,14 +84,15 @@ Any changes in the sources trigger a super-fast hot update and you see changes i
88
84
 
89
85
  ### Build static website
90
86
 
91
- Example [https://template.likec4.dev](https://template.likec4.dev/view/cloud)
92
- Build a single-page application with all diagrams:
87
+ Build a single HTML with diagrams, ready to be embedded into your website:
93
88
 
94
89
  ```sh
95
90
  likec4 build -o ./dist
96
91
  ```
97
92
 
98
- When you deploy the website, you can use "Share" button to get a link to a specific diagram.
93
+ Example [https://template.likec4.dev](https://template.likec4.dev/view/cloud)
94
+
95
+ When you deployed the website, you can use "Share" button to get a link to a specific diagram.
99
96
 
100
97
  > **Tip:**
101
98
  > [likec4/template](https://github.com/likec4/template) repository demonstrates how to deploy to github pages.
@@ -106,13 +103,16 @@ There is also a supplementary command to preview the build:
106
103
  likec4 preview -o ./dist
107
104
  ```
108
105
 
106
+ For example, this command can be used on CI, to compare diagrams with ones from the previous/main build.
107
+
109
108
  ### Export to PNG
110
109
 
111
110
  ```sh
112
111
  likec4 export png -o ./assets
113
112
  ```
114
113
 
115
- This command starts temporary local web server and uses [Playwright](https://playwright.dev/) to take screenshots of diagrams.
114
+ This command starts local web server and uses Playwright to take screenshots.
115
+ If you plan to use it on CI, refer to [Playwright documentation](https://playwright.dev/docs/ci) for details.
116
116
 
117
117
  ### Export to Mermaid, Dot, D2
118
118
 
@@ -131,11 +131,15 @@ likec4 codegen d2
131
131
  likec4 codegen react --outfile ./src/likec4.generated.tsx
132
132
  ```
133
133
 
134
+ Check [documentation](https://likec4.dev/docs/tools/react/)
135
+
134
136
  > Output file should have `.tsx` extension
135
137
  > By default, it generates `likec4.generated.tsx` in current directory
136
138
 
137
139
  ### Generate structured data
138
140
 
141
+ Generate a TypeScript file with `LikeC4Views` object, which contains all diagrams and their metadata.
142
+
139
143
  ```sh
140
144
  likec4 codegen views-data --outfile ./src/likec4.generated.ts
141
145
 
@@ -147,6 +151,18 @@ likec4 codegen ts ...
147
151
  > Output file should have `.ts` extension
148
152
  > By default, it generates `likec4.generated.ts` in current directory
149
153
 
154
+ ## Development
155
+
156
+ In root workspace:
157
+
158
+ ```sh
159
+ yarn install
160
+ yarn build
161
+
162
+ cd packages/likec4
163
+ yarn dev
164
+ ```
165
+
150
166
  ## Support
151
167
 
152
168
  If there's a problem you're encountering or something you need help with, don't hesitate to take advantage of my [_Priority Support_ service](https://github.com/sponsors/davydkov) where you can ask me questions in an exclusive forum. I'm well equppied to assist you with this project and would be happy to help you out! 🙂
@@ -86,24 +86,22 @@ export function Edges({ animate, theme, diagram, onEdgeClick }) {
86
86
  return edgeTransitions((springs, edge, { key }) => /* @__PURE__ */ jsx(
87
87
  Group,
88
88
  {
89
- ...onEdgeClick && {
90
- onPointerClick: (e) => {
91
- if (DiagramGesture.isDragging || e.evt.button !== 0) {
92
- return;
93
- }
94
- e.cancelBubble = true;
95
- onEdgeClick(edge, e);
96
- },
97
- onPointerEnter: (e) => {
98
- if (animate) {
99
- setHoveredEdge(edge);
100
- mousePointer(e);
101
- }
102
- },
103
- onPointerLeave: (e) => {
104
- setHoveredEdge(null);
105
- mouseDefault(e);
89
+ onPointerClick: (e) => {
90
+ if (!onEdgeClick || DiagramGesture.isDragging || e.evt.button !== 0) {
91
+ return;
106
92
  }
93
+ e.cancelBubble = true;
94
+ onEdgeClick(edge, e);
95
+ },
96
+ onPointerEnter: (e) => {
97
+ if (animate) {
98
+ setHoveredEdge(edge);
99
+ mousePointer(e);
100
+ }
101
+ },
102
+ onPointerLeave: (e) => {
103
+ setHoveredEdge(null);
104
+ mouseDefault(e);
107
105
  },
108
106
  children: /* @__PURE__ */ jsx(
109
107
  EdgeShape,
@@ -1,9 +1,10 @@
1
- import { jsx } from "react/jsx-runtime";
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { nonexhaustive } from "@likec4/core";
3
3
  import { useTransition } from "@react-spring/konva";
4
4
  import { useRef } from "react";
5
5
  import { AnimatedGroup } from "../konva.js";
6
6
  import { Portal } from "../konva-portal.js";
7
+ import { ZoomInIcon } from "./icons/index.js";
7
8
  import { CylinderShape, MobileShape, PersonShape, QueueShape, RectangleShape } from "./shapes/index.js";
8
9
  import { BrowserShape } from "./shapes/Browser.js";
9
10
  import { CompoundShape } from "./shapes/Compound.js";
@@ -137,9 +138,11 @@ function NodeSnape({
137
138
  onNodeClick
138
139
  }) {
139
140
  const setHoveredNode = useSetHoveredNode();
140
- const Shape = isCompound(node) ? CompoundShape : nodeShape(node);
141
+ const _isCompound = isCompound(node);
142
+ const isNavigatable = !!node.navigateTo && !!onNodeClick;
143
+ const Shape = nodeShape(node);
141
144
  const springs = ctrl.springs;
142
- return /* @__PURE__ */ jsx(Portal, { selector: ".top", enabled: isHovered && !isCompound(node), children: /* @__PURE__ */ jsx(
145
+ return /* @__PURE__ */ jsx(Portal, { selector: ".top", enabled: isHovered && !_isCompound, children: /* @__PURE__ */ jsxs(
143
146
  AnimatedGroup,
144
147
  {
145
148
  name: node.id,
@@ -172,7 +175,32 @@ function NodeSnape({
172
175
  scaleX: springs.scaleX,
173
176
  scaleY: springs.scaleY,
174
177
  opacity: springs.opacity,
175
- children: /* @__PURE__ */ jsx(Shape, { node, theme, springs, isHovered })
178
+ children: [
179
+ _isCompound && /* @__PURE__ */ jsxs(Fragment, { children: [
180
+ /* @__PURE__ */ jsx(
181
+ CompoundShape,
182
+ {
183
+ node,
184
+ theme,
185
+ springs,
186
+ labelOffsetX: isNavigatable ? -12 : 4
187
+ }
188
+ ),
189
+ isNavigatable && /* @__PURE__ */ jsx(ZoomInIcon, { fill: "#BABABA", opacity: 0.9, size: 16, x: 16, y: 18 })
190
+ ] }),
191
+ !_isCompound && /* @__PURE__ */ jsxs(Fragment, { children: [
192
+ /* @__PURE__ */ jsx(Shape, { node, theme, springs, isHovered }),
193
+ isNavigatable && /* @__PURE__ */ jsx(
194
+ ZoomInIcon,
195
+ {
196
+ fill: "#BABABA",
197
+ size: 16,
198
+ x: node.size.width / 2,
199
+ y: node.size.height - 20
200
+ }
201
+ )
202
+ ] })
203
+ ]
176
204
  }
177
205
  ) });
178
206
  }
@@ -0,0 +1,27 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { Path } from "../../konva.js";
3
+ export const ZoomInIcon = ({ fill, opacity = 1, size = 20, x, y }) => {
4
+ const originalSize = 15;
5
+ const scale = size / originalSize;
6
+ const offsetIcon = originalSize / 2;
7
+ return /* @__PURE__ */ jsx(
8
+ Path,
9
+ {
10
+ data: "M10 6.5C10 8.433 8.433 10 6.5 10C4.567 10 3 8.433 3 6.5C3 4.567 4.567 3 6.5 3C8.433 3 10 4.567 10 6.5ZM9.30884 10.0159C8.53901 10.6318 7.56251 11 6.5 11C4.01472 11 2 8.98528 2 6.5C2 4.01472 4.01472 2 6.5 2C8.98528 2 11 4.01472 11 6.5C11 7.56251 10.6318 8.53901 10.0159 9.30884L12.8536 12.1464C13.0488 12.3417 13.0488 12.6583 12.8536 12.8536C12.6583 13.0488 12.3417 13.0488 12.1464 12.8536L9.30884 10.0159ZM4.25 6.5C4.25 6.22386 4.47386 6 4.75 6H6V4.75C6 4.47386 6.22386 4.25 6.5 4.25C6.77614 4.25 7 4.47386 7 4.75V6H8.25C8.52614 6 8.75 6.22386 8.75 6.5C8.75 6.77614 8.52614 7 8.25 7H7V8.25C7 8.52614 6.77614 8.75 6.5 8.75C6.22386 8.75 6 8.52614 6 8.25V7H4.75C4.47386 7 4.25 6.77614 4.25 6.5Z",
11
+ fill,
12
+ fillRule: "evenodd",
13
+ strokeEnabled: false,
14
+ x,
15
+ y,
16
+ offsetX: offsetIcon,
17
+ offsetY: offsetIcon,
18
+ scaleX: scale,
19
+ scaleY: scale,
20
+ width: originalSize,
21
+ height: originalSize,
22
+ opacity,
23
+ globalCompositeOperation: "luminosity",
24
+ hitStrokeWidth: 5
25
+ }
26
+ );
27
+ };
@@ -1,2 +1,3 @@
1
1
  export * from "./ExternalLink.js";
2
2
  export * from "./BrainIcon.js";
3
+ export * from "./ZoomIn.js";
@@ -1,6 +1,6 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { AnimatedRect, AnimatedText } from "../../konva.js";
3
- export function CompoundShape({ node, theme, springs }) {
3
+ export function CompoundShape({ node, theme, springs, labelOffsetX = 4 }) {
4
4
  const { labels } = node;
5
5
  return /* @__PURE__ */ jsxs(Fragment, { children: [
6
6
  /* @__PURE__ */ jsx(
@@ -24,9 +24,9 @@ export function CompoundShape({ node, theme, springs }) {
24
24
  AnimatedText,
25
25
  {
26
26
  x,
27
- y: y - label.fontSize / 2,
28
- offsetX: 4,
29
- offsetY: 4,
27
+ y: y - 4,
28
+ offsetX: labelOffsetX,
29
+ offsetY: label.fontSize / 2,
30
30
  width: springs.width.to((v) => v - x - 4),
31
31
  fill: "#BABABA",
32
32
  fontFamily: theme.font,
@@ -7,6 +7,7 @@ export const hoveredNodeAtom = atom(
7
7
  (get) => get(currentHoveredNodeAtom),
8
8
  (get, set, update) => {
9
9
  clearTimeout(get(nodeTimeoutAtom));
10
+ clearTimeout(get(edgeTimeoutAtom));
10
11
  const _prev = get(currentHoveredNodeAtom);
11
12
  const _next = typeof update === "function" ? update(_prev) : update;
12
13
  if (equals(_prev, _next)) {
@@ -17,7 +18,6 @@ export const hoveredNodeAtom = atom(
17
18
  nodeTimeoutAtom,
18
19
  setTimeout(() => {
19
20
  set(currentHoveredNodeAtom, _next);
20
- clearTimeout(get(edgeTimeoutAtom));
21
21
  set(currentHoveredEdgeAtom, null);
22
22
  }, 200)
23
23
  );
@@ -28,6 +28,7 @@ export const hoveredNodeAtom = atom(
28
28
  nodeTimeoutAtom,
29
29
  setTimeout(() => {
30
30
  set(currentHoveredNodeAtom, _next);
31
+ set(currentHoveredEdgeAtom, null);
31
32
  }, 150)
32
33
  );
33
34
  return true;
@@ -37,7 +38,7 @@ export const hoveredNodeAtom = atom(
37
38
  nodeTimeoutAtom,
38
39
  setTimeout(() => {
39
40
  set(currentHoveredNodeAtom, null);
40
- }, 200)
41
+ }, 150)
41
42
  );
42
43
  return true;
43
44
  }
@@ -51,6 +52,7 @@ const edgeTimeoutAtom = atom(void 0);
51
52
  export const hoveredEdgeAtom = atom(
52
53
  (get) => get(currentHoveredEdgeAtom),
53
54
  (get, set, update) => {
55
+ clearTimeout(get(nodeTimeoutAtom));
54
56
  clearTimeout(get(edgeTimeoutAtom));
55
57
  const _prev = get(currentHoveredEdgeAtom);
56
58
  const _next = typeof update === "function" ? update(_prev) : update;
@@ -62,9 +64,8 @@ export const hoveredEdgeAtom = atom(
62
64
  edgeTimeoutAtom,
63
65
  setTimeout(() => {
64
66
  set(currentHoveredEdgeAtom, _next);
65
- clearTimeout(get(nodeTimeoutAtom));
66
67
  set(currentHoveredNodeAtom, null);
67
- }, 600)
68
+ }, 400)
68
69
  );
69
70
  return true;
70
71
  }
@@ -73,7 +74,8 @@ export const hoveredEdgeAtom = atom(
73
74
  edgeTimeoutAtom,
74
75
  setTimeout(() => {
75
76
  set(currentHoveredEdgeAtom, _next);
76
- }, 200)
77
+ set(currentHoveredNodeAtom, null);
78
+ }, 150)
77
79
  );
78
80
  return true;
79
81
  }
@@ -82,7 +84,7 @@ export const hoveredEdgeAtom = atom(
82
84
  edgeTimeoutAtom,
83
85
  setTimeout(() => {
84
86
  set(currentHoveredEdgeAtom, null);
85
- }, 300)
87
+ }, 150)
86
88
  );
87
89
  return true;
88
90
  }
@@ -0,0 +1,50 @@
1
+ import { CaretDownIcon } from '@radix-ui/react-icons';
2
+ import { Button, DropdownMenu, Flex, IconButton, Separator } from '@radix-ui/themes';
3
+ import { useState } from 'react';
4
+ import { keys } from 'remeda';
5
+ const Mode = {
6
+ react: 'React',
7
+ dot: 'Graphviz',
8
+ mmd: 'Mermaid',
9
+ d2: 'D2'
10
+ };
11
+ const mode_keys = keys.strict(Mode);
12
+ export const DisplayModeSelector = () => {
13
+ const [current, setCurrent] = useState('react');
14
+ const [[first, second, ...rest], setModes] = useState(mode_keys);
15
+ const changeMode = (mode) => () => {
16
+ if (mode === current) {
17
+ return;
18
+ }
19
+ setCurrent(mode);
20
+ if (mode === first || mode === second) {
21
+ return;
22
+ }
23
+ // change only second
24
+ setModes(([first, ...rest]) => [first, mode, ...rest.filter(m => m !== mode)]);
25
+ };
26
+ return (<Flex display={{
27
+ initial: 'none',
28
+ md: 'flex'
29
+ }} gap='3' align='center'>
30
+ <Button variant={current === first ? 'solid' : 'ghost'} size='1' onClick={changeMode(first)}>
31
+ {Mode[first]}
32
+ </Button>
33
+ <Button variant={current === second ? 'solid' : 'ghost'} size='1' onClick={changeMode(second)}>
34
+ {Mode[second]}
35
+ </Button>
36
+ <DropdownMenu.Root>
37
+ <DropdownMenu.Trigger>
38
+ <IconButton variant='ghost' size='1'>
39
+ <CaretDownIcon />
40
+ </IconButton>
41
+ </DropdownMenu.Trigger>
42
+ <DropdownMenu.Content>
43
+ {rest.map(mode => (<DropdownMenu.Item key={mode} onClick={changeMode(mode)}>
44
+ {Mode[mode]}
45
+ </DropdownMenu.Item>))}
46
+ </DropdownMenu.Content>
47
+ </DropdownMenu.Root>
48
+ <Separator orientation='vertical'/>
49
+ </Flex>);
50
+ };
@@ -1,7 +1,8 @@
1
1
  import { CaretDownIcon, Share1Icon as ShareIcon } from '@radix-ui/react-icons';
2
- import { Button, Dialog, DropdownMenu, Flex, IconButton, Separator, Text } from '@radix-ui/themes';
2
+ import { Button, Dialog, DropdownMenu, Flex, Text } from '@radix-ui/themes';
3
3
  import { useState } from 'react';
4
4
  // import { ThemePanelToggle } from '../ThemePanelToggle'
5
+ import { DisplayModeSelector } from './DisplayModeSelector';
5
6
  import ExportDiagram from './ExportDiagram';
6
7
  import { ShareDialog } from './ShareDialog';
7
8
  const ExportMenu = ({ onExport, children }) => (<DropdownMenu.Root>
@@ -13,29 +14,14 @@ const ExportMenu = ({ onExport, children }) => (<DropdownMenu.Root>
13
14
  <DropdownMenu.Group>
14
15
  <DropdownMenu.Item onClick={_ => {
15
16
  onExport('png');
16
- // const { boundingBox } = diagramApi.diagramView()
17
- // console.log('Serialized: ', diagramApi.stage.toObject())
18
- // const k = new KonvaCore.Canvas({
19
- // height: diagramApi.
20
- // })
21
- // diagramApi.stage().toBlob({
22
- // ...boundingBox,
23
- // callback(blob) {
24
- // const url = URL.createObjectURL(blob)
25
- // window.open(url)
26
- // // const a = document.createElement('a')
27
- // // a.href = url
28
- // // a.download = 'diagram.png'
29
- // // a.click()
30
- // URL.revokeObjectURL(url)
31
- // },
32
- // })
33
17
  }}>
34
18
  Export as .png
35
19
  </DropdownMenu.Item>
36
20
  <DropdownMenu.Item disabled>Export as .dot</DropdownMenu.Item>
37
21
  <DropdownMenu.Item disabled>Export as .mmd</DropdownMenu.Item>
38
22
  <DropdownMenu.Item disabled>Export as .d2</DropdownMenu.Item>
23
+ <DropdownMenu.Item disabled>Export to Draw.io</DropdownMenu.Item>
24
+ <DropdownMenu.Item disabled>Export to Miro</DropdownMenu.Item>
39
25
  </DropdownMenu.Group>
40
26
  <DropdownMenu.Separator />
41
27
  <DropdownMenu.Label>
@@ -49,24 +35,7 @@ const ExportMenu = ({ onExport, children }) => (<DropdownMenu.Root>
49
35
  export const ViewActionsToolbar = ({ diagram }) => {
50
36
  const [exportTo, setExportTo] = useState(null);
51
37
  return (<Flex position='fixed' top='0' right='0' p='2' gap={'3'} justify='end' align='center'>
52
- <Flex display={{
53
- initial: 'none',
54
- md: 'flex'
55
- }} gap='3' align='center'>
56
- <Button variant='solid' size='1'>
57
- React
58
- </Button>
59
- <Button variant='ghost' size='1'>
60
- Graphviz
61
- </Button>
62
- <Button variant='ghost' size='1'>
63
- Mermaid
64
- </Button>
65
- <IconButton variant='ghost' size='1'>
66
- <CaretDownIcon />
67
- </IconButton>
68
- <Separator orientation='vertical'/>
69
- </Flex>
38
+ <DisplayModeSelector />
70
39
  <Dialog.Root>
71
40
  <Dialog.Trigger>
72
41
  <Button variant='solid'>