likec4 0.43.1 → 0.44.1

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
@@ -84,18 +80,19 @@ This recursively searchs for `*.c4`, `*.likec4` files in current folder, parses
84
80
  Any changes in the sources trigger a super-fast hot update and you see changes in the browser immediately.
85
81
 
86
82
  > **Tip:**
87
- > You can use `likec4 serve [path]` in a separate terminal window and keep it running while you're editing diagrams in editor, or even serve multiple projects at once.
83
+ > You can use `likec4 start [path]` in a separate terminal window and keep it running while you're editing diagrams in editor, or even serve multiple projects at once.
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! 🙂
@@ -8,13 +8,13 @@ const sky = {
8
8
  fill: "#0284c7",
9
9
  stroke: "#0369a1",
10
10
  hiContrast: "#f0f9ff",
11
- loContrast: "#e0f2fe"
11
+ loContrast: "#B6ECF7"
12
12
  };
13
13
  const slate = {
14
14
  fill: "#64748b",
15
15
  stroke: "#475569",
16
16
  hiContrast: "#f8fafc",
17
- loContrast: "#e2e8f0"
17
+ loContrast: "#cbd5e1"
18
18
  };
19
19
  export const ElementColors = {
20
20
  primary: blue,
@@ -31,23 +31,24 @@ export const ElementColors = {
31
31
  fill: "#737373",
32
32
  stroke: "#525252",
33
33
  hiContrast: "#fafafa",
34
- loContrast: "#e5e5e5"
34
+ loContrast: "#d4d4d4"
35
35
  },
36
36
  red: {
37
37
  // fill: colors.red[500],
38
38
  // stroke: colors.red[600],
39
39
  // hiContrast: colors.red[50],
40
40
  // loContrast: colors.red[200],
41
- fill: "#b54548",
42
- stroke: "#8c333a",
41
+ fill: "#AC4D39",
42
+ // fill: '#b54548',
43
+ stroke: "#853A2D",
43
44
  // hiContrast: '#fef2f2',
44
45
  // loContrast: '#fecaca',
45
46
  // hiContrast: '#191111', // colors.gray[900],
46
47
  // loContrast: '#3b1219' // colors.gray[800],
47
- hiContrast: "#f8fafc",
48
+ hiContrast: "#FBD3CB",
48
49
  // hiContrast: '#f8fafc',
49
50
  // loContrast: '#fdd8d8' // radix black red 12
50
- loContrast: "#F9C6C6"
51
+ loContrast: "#FF977D"
51
52
  },
52
53
  green: {
53
54
  fill: "#428a4f",
@@ -56,18 +57,10 @@ export const ElementColors = {
56
57
  loContrast: "#c2f0c2"
57
58
  },
58
59
  amber: {
59
- // fill: colors.amber[600],
60
- // stroke: colors.amber[700],
61
- // hiContrast: colors.amber[50],
62
- // loContrast: colors.amber[200],
63
- fill: "#d97706",
64
- stroke: "#b45309",
65
- // hiContrast: '#fffbeb',
66
- // loContrast: '#fde68a',
67
- hiContrast: "#f8fafc",
68
- // colors.gray[900],
69
- loContrast: "#ffe0c2"
70
- // colors.gray[800],
60
+ fill: "#A35829",
61
+ stroke: "#7E451D",
62
+ hiContrast: "#FFE0C2",
63
+ loContrast: "#FFA057"
71
64
  },
72
65
  indigo: {
73
66
  // fill: colors.indigo[500],
@@ -31,7 +31,7 @@ export const RelationshipColors = {
31
31
  amber: {
32
32
  lineColor: "#b45309",
33
33
  labelBgColor: "#78350f",
34
- labelColor: "#f59e0b"
34
+ labelColor: "#FFE0C2"
35
35
  },
36
36
  blue,
37
37
  gray,
@@ -54,9 +54,9 @@ export const RelationshipColors = {
54
54
  muted: slate,
55
55
  primary: blue,
56
56
  red: {
57
- lineColor: "#b91c1c",
57
+ lineColor: "#AC4D39",
58
58
  labelBgColor: "#b91c1c",
59
- labelColor: "#dc2626"
59
+ labelColor: "#FF977D"
60
60
  },
61
61
  secondary: sky,
62
62
  sky,
@@ -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,23 +138,27 @@ 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,
146
149
  visible: expired !== true,
147
- onPointerEnter: (e) => {
148
- if (animate) {
150
+ ...animate && {
151
+ onPointerEnter: (e) => {
149
152
  setHoveredNode(node);
150
- onNodeClick && mousePointer(e);
153
+ if (isNavigatable) {
154
+ mousePointer(e);
155
+ }
156
+ },
157
+ onPointerLeave: (e) => {
158
+ setHoveredNode(null);
159
+ mouseDefault(e);
151
160
  }
152
161
  },
153
- onPointerLeave: (e) => {
154
- setHoveredNode(null);
155
- mouseDefault(e);
156
- },
157
162
  ...onNodeClick && {
158
163
  onPointerClick: (e) => {
159
164
  if (DiagramGesture.isDragging || e.evt.button !== 0) {
@@ -172,7 +177,32 @@ function NodeSnape({
172
177
  scaleX: springs.scaleX,
173
178
  scaleY: springs.scaleY,
174
179
  opacity: springs.opacity,
175
- children: /* @__PURE__ */ jsx(Shape, { node, theme, springs, isHovered })
180
+ children: [
181
+ _isCompound && /* @__PURE__ */ jsxs(Fragment, { children: [
182
+ /* @__PURE__ */ jsx(
183
+ CompoundShape,
184
+ {
185
+ node,
186
+ theme,
187
+ springs,
188
+ labelOffsetX: isNavigatable ? -12 : 4
189
+ }
190
+ ),
191
+ isNavigatable && /* @__PURE__ */ jsx(ZoomInIcon, { fill: "#BABABA", opacity: 0.9, size: 16, x: 16, y: 17 })
192
+ ] }),
193
+ !_isCompound && /* @__PURE__ */ jsxs(Fragment, { children: [
194
+ /* @__PURE__ */ jsx(Shape, { node, theme, springs, isHovered }),
195
+ isNavigatable && /* @__PURE__ */ jsx(
196
+ ZoomInIcon,
197
+ {
198
+ fill: "#BABABA",
199
+ size: 16,
200
+ x: node.size.width / 2,
201
+ y: node.size.height - 20
202
+ }
203
+ )
204
+ ] })
205
+ ]
176
206
  }
177
207
  ) });
178
208
  }
@@ -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,
@@ -1,4 +1,4 @@
1
- export * from "./useDarkMode.js";
1
+ export * from "./usePrefersLightMode.js";
2
2
  export * from "./useDiagramApi.js";
3
3
  export * from "./useImageLoader.js";
4
4
  export * from "./useViewIdFromHash.js";
@@ -0,0 +1,5 @@
1
+ import { useMediaQuery } from "@react-hookz/web/esm";
2
+ const COLOR_SCHEME_QUERY = "(prefers-color-scheme: light)";
3
+ export function usePrefersLightMode() {
4
+ return useMediaQuery(COLOR_SCHEME_QUERY);
5
+ }
@@ -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'>