likec4 0.49.0 → 0.51.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/Diagram.js +55 -27
- package/dist/@likec4/diagrams/diagram/Edges.js +14 -11
- package/dist/@likec4/diagrams/diagram/Nodes.js +212 -23
- package/dist/@likec4/diagrams/diagram/icons/ZoomIn.js +2 -1
- package/dist/@likec4/diagrams/diagram/shapes/Compound.js +2 -1
- package/dist/@likec4/diagrams/diagram/shapes/Edge.js +1 -1
- package/dist/@likec4/diagrams/diagram/state/atoms.js +35 -60
- package/dist/@likec4/diagrams/diagram/utils.js +14 -0
- package/dist/@likec4/diagrams/hooks/useDiagramApi.js +4 -5
- package/dist/@likec4/diagrams/hooks/useImageLoader.js +1 -1
- package/dist/__app__/likec4.css +8 -5
- package/dist/__app__/src/App.jsx +9 -11
- package/dist/__app__/src/components/CopyToClipboard.jsx +29 -0
- package/dist/__app__/src/components/CopyToClipboard.module.css +16 -0
- package/dist/__app__/src/components/DiagramNotFound.jsx +2 -2
- package/dist/__app__/src/components/index.js +2 -1
- package/dist/__app__/src/components/sidebar/Sidebar.jsx +1 -1
- package/dist/__app__/src/components/sidebar/styles.module.css +4 -1
- package/dist/__app__/src/components/view-page/DisplayModeSelector.jsx +15 -9
- package/dist/__app__/src/components/view-page/Header.jsx +97 -0
- package/dist/__app__/src/components/view-page/Header.module.css +24 -0
- package/dist/__app__/src/components/view-page/ShareDialog.jsx +23 -15
- package/dist/__app__/src/components/view-page/ViewActions.jsx +69 -0
- package/dist/__app__/src/data/atoms.js +4 -22
- package/dist/__app__/src/data/hooks.js +5 -4
- package/dist/__app__/src/data/index-page.js +22 -0
- package/dist/__app__/src/likec4-views.js +1 -1
- package/dist/__app__/src/pages/index-page/index.jsx +99 -0
- package/dist/__app__/src/pages/index-page/index.module.css +20 -0
- package/dist/__app__/src/pages/index.js +1 -1
- package/dist/__app__/src/pages/useTransparentBackground.js +2 -2
- package/dist/__app__/src/pages/view-page/ViewAsReact.jsx +60 -0
- package/dist/__app__/src/pages/view-page/index.js +11 -0
- package/dist/__app__/src/pages/view-page/other-formats/ViewAsD2.jsx +18 -0
- package/dist/__app__/src/pages/view-page/other-formats/ViewAsDot.jsx +33 -0
- package/dist/__app__/src/pages/view-page/other-formats/ViewAsMmd.jsx +18 -0
- package/dist/__app__/src/pages/view-page/other-formats.jsx +43 -0
- package/dist/__app__/src/pages/view-page/view-page.module.css +81 -0
- package/dist/__app__/src/pages/view.page.jsx +12 -65
- package/dist/__app__/src/router.js +90 -20
- package/dist/__app__/src/utils/utils.js +1 -2
- package/dist/__app__/tsconfig.json +1 -0
- package/dist/cli/index.js +286 -212
- package/package.json +19 -20
- package/dist/__app__/postcss.config.cjs +0 -11
- package/dist/__app__/src/components/view-page/ViewActionsToolbar.jsx +0 -66
- package/dist/__app__/src/pages/index.module.css +0 -11
- package/dist/__app__/src/pages/index.page.jsx +0 -57
- package/dist/__app__/src/pages/view-page.module.css +0 -30
- package/dist/__app__/tailwind.config.cjs +0 -17
package/dist/__app__/likec4.css
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
@import '@radix-ui/themes/styles.css';
|
|
2
2
|
|
|
3
|
-
@tailwind base;
|
|
4
|
-
@tailwind components;
|
|
5
|
-
@tailwind utilities;
|
|
6
|
-
|
|
7
3
|
.radix-themes {
|
|
8
4
|
--font-weight-bold: 600;
|
|
5
|
+
--color-text-dimmed: var(--gray-a8);
|
|
9
6
|
}
|
|
10
7
|
|
|
11
8
|
*,
|
|
@@ -26,10 +23,16 @@ html, body {
|
|
|
26
23
|
}
|
|
27
24
|
|
|
28
25
|
#like4-root {
|
|
29
|
-
margin: 0;
|
|
26
|
+
/* margin: 0;
|
|
30
27
|
padding: 0;
|
|
31
28
|
width: 100vw;
|
|
32
29
|
height: 100vh;
|
|
30
|
+
overscroll-behavior: none;
|
|
31
|
+
overflow: hidden; */
|
|
32
|
+
/* & > div {
|
|
33
|
+
position: fixed;
|
|
34
|
+
inset: 0;
|
|
35
|
+
} */
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
.transparent-bg {
|
package/dist/__app__/src/App.jsx
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
+
import { nonexhaustive } from '@likec4/core';
|
|
2
|
+
import { Theme } from '@radix-ui/themes';
|
|
1
3
|
import { Provider } from 'jotai';
|
|
2
|
-
import { Fragment } from 'react';
|
|
4
|
+
import { Fragment, useDeferredValue } from 'react';
|
|
5
|
+
import { isNil } from 'remeda';
|
|
3
6
|
import { Sidebar } from './components';
|
|
4
|
-
import { ExportPage, IndexPage,
|
|
7
|
+
import { EmbedPage, ExportPage, IndexPage, ViewPage } from './pages';
|
|
5
8
|
import { useRoute } from './router';
|
|
6
|
-
import { Theme } from '@radix-ui/themes';
|
|
7
|
-
import { nonexhaustive } from '@likec4/core';
|
|
8
|
-
import { isNil } from 'remeda';
|
|
9
9
|
const Routes = () => {
|
|
10
|
-
const r = useRoute();
|
|
10
|
+
const r = useDeferredValue(useRoute());
|
|
11
11
|
const theme = r.params?.theme;
|
|
12
12
|
let page = null;
|
|
13
13
|
switch (r.route) {
|
|
14
14
|
case 'view': {
|
|
15
|
-
page = <ViewPage key='view' viewId={r.params.viewId} showUI={r.showUI}
|
|
15
|
+
page = (<ViewPage key='view' viewId={r.params.viewId} viewMode={r.params.mode} showUI={r.showUI}/>);
|
|
16
16
|
break;
|
|
17
17
|
}
|
|
18
18
|
case 'export': {
|
|
@@ -30,11 +30,9 @@ const Routes = () => {
|
|
|
30
30
|
default:
|
|
31
31
|
nonexhaustive(r);
|
|
32
32
|
}
|
|
33
|
-
return (<Theme hasBackground={!!theme} accentColor='indigo' radius='small' appearance={theme}>
|
|
33
|
+
return (<Theme hasBackground={!!theme} accentColor='indigo' radius='small' appearance={theme ?? 'inherit'}>
|
|
34
34
|
{page}
|
|
35
|
-
{r.showUI &&
|
|
36
|
-
<Sidebar />
|
|
37
|
-
</Fragment>)}
|
|
35
|
+
<Fragment key='ui'>{r.showUI && <Sidebar />}</Fragment>
|
|
38
36
|
</Theme>);
|
|
39
37
|
};
|
|
40
38
|
export default function App() {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CheckCircledIcon, CopyIcon } from '@radix-ui/react-icons';
|
|
2
|
+
import { Box, IconButton, Tooltip } from '@radix-ui/themes';
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
4
|
+
import styles from './CopyToClipboard.module.css';
|
|
5
|
+
export function CopyToClipboard({ text }) {
|
|
6
|
+
const [copied, setCopied] = useState(false);
|
|
7
|
+
const copy = useCallback(() => {
|
|
8
|
+
void navigator.clipboard.writeText(text);
|
|
9
|
+
setCopied(true);
|
|
10
|
+
}, [text]);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
setCopied(false);
|
|
13
|
+
}, [text]);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (copied) {
|
|
16
|
+
const timeout = setTimeout(() => {
|
|
17
|
+
setCopied(false);
|
|
18
|
+
}, 800);
|
|
19
|
+
return () => clearTimeout(timeout);
|
|
20
|
+
}
|
|
21
|
+
}, [copied]);
|
|
22
|
+
return (<Box position='absolute' top={'0'} right={'0'} p={'4'}>
|
|
23
|
+
<Tooltip content={copied ? 'Copied!' : 'Copy to clipboard'} {...(copied ? { open: true } : {})}>
|
|
24
|
+
<IconButton variant='soft' color={copied ? 'green' : undefined} size={'2'} radius='large' onClick={copy} data-copied={copied} className={styles.copyButton}>
|
|
25
|
+
{copied ? (<CheckCircledIcon width={16} height={16}/>) : (<CopyIcon width={16} height={16}/>)}
|
|
26
|
+
</IconButton>
|
|
27
|
+
</Tooltip>
|
|
28
|
+
</Box>);
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
@keyframes copyButton {
|
|
2
|
+
0% { transform: scale(1); opacity: 1 }
|
|
3
|
+
40% { transform: scale(1.4); opacity: .7; }
|
|
4
|
+
60% { transform: scale(0.6); opacity: 1 }
|
|
5
|
+
80% { transform: scale(0.95) }
|
|
6
|
+
100% { transform: scale(0.85) }
|
|
7
|
+
}
|
|
8
|
+
.copyButton {
|
|
9
|
+
animation-timing-function: ease-in;
|
|
10
|
+
backdrop-filter: blur(2px);
|
|
11
|
+
|
|
12
|
+
&:where([data-copied='true']) {
|
|
13
|
+
animation-name: copyButton;
|
|
14
|
+
animation-duration: .7s;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -22,8 +22,8 @@ export const DiagramNotFound = ({ viewId }) => {
|
|
|
22
22
|
does not exist
|
|
23
23
|
</Text>
|
|
24
24
|
<Box pt='2'>
|
|
25
|
-
<Button variant='soft' color='amber'
|
|
26
|
-
Go to overview
|
|
25
|
+
<Button asChild variant='soft' color='amber'>
|
|
26
|
+
<a href={$pages.index.url()}>Go to overview</a>
|
|
27
27
|
</Button>
|
|
28
28
|
</Box>
|
|
29
29
|
</Flex>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Sidebar } from './sidebar/Sidebar';
|
|
2
|
-
export {
|
|
2
|
+
export { ViewActions } from './view-page/ViewActions';
|
|
3
3
|
export { ThemePanelToggle } from './ThemePanelToggle';
|
|
4
4
|
export { DiagramNotFound } from './DiagramNotFound';
|
|
5
|
+
export { CopyToClipboard } from './CopyToClipboard';
|
|
@@ -11,7 +11,7 @@ export const Sidebar = () => {
|
|
|
11
11
|
const [isOpened, toggle] = useToggle(false, true);
|
|
12
12
|
useClickOutside(ref, () => isOpened && toggle());
|
|
13
13
|
return (<>
|
|
14
|
-
<Flex position='fixed' left='0'
|
|
14
|
+
<Flex position='fixed' left='0' top={'0'} bottom={'0'} p={'2'} justify={'start'} data-opened={isOpened} className={cn(styles.trigger)} onClick={toggle}>
|
|
15
15
|
<IconButton size='2' color='gray' variant='soft'>
|
|
16
16
|
<HamburgerMenuIcon width={22} height={22}/>
|
|
17
17
|
</IconButton>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CaretDownIcon } from '@radix-ui/react-icons';
|
|
2
2
|
import { Button, DropdownMenu, Flex, IconButton, Separator } from '@radix-ui/themes';
|
|
3
|
-
import { useState } from 'react';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
4
|
import { keys } from 'remeda';
|
|
5
|
+
import { updateSearchParams, useSearchParams } from '../../router';
|
|
5
6
|
const Mode = {
|
|
6
7
|
react: 'React',
|
|
7
8
|
dot: 'Graphviz',
|
|
@@ -10,19 +11,24 @@ const Mode = {
|
|
|
10
11
|
};
|
|
11
12
|
const mode_keys = keys.strict(Mode);
|
|
12
13
|
export const DisplayModeSelector = () => {
|
|
13
|
-
const
|
|
14
|
+
const current = useSearchParams().mode ?? 'react';
|
|
14
15
|
const [[first, second, ...rest], setModes] = useState(mode_keys);
|
|
15
16
|
const changeMode = (mode) => () => {
|
|
16
17
|
if (mode === current) {
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
-
if (mode === first || mode === second) {
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
// change only second
|
|
24
|
-
setModes(([first, ...rest]) => [first, mode, ...rest.filter(m => m !== mode)]);
|
|
20
|
+
updateSearchParams({ mode });
|
|
25
21
|
};
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
// change only second
|
|
24
|
+
setModes(modes => {
|
|
25
|
+
if (modes[0] === current || modes[1] === current) {
|
|
26
|
+
return modes;
|
|
27
|
+
}
|
|
28
|
+
const [first, ...rest] = modes;
|
|
29
|
+
return [first, current, ...rest.filter(m => m !== current)];
|
|
30
|
+
});
|
|
31
|
+
}, [current]);
|
|
26
32
|
return (<Flex display={{
|
|
27
33
|
initial: 'none',
|
|
28
34
|
sm: 'flex'
|
|
@@ -39,7 +45,7 @@ export const DisplayModeSelector = () => {
|
|
|
39
45
|
<CaretDownIcon />
|
|
40
46
|
</IconButton>
|
|
41
47
|
</DropdownMenu.Trigger>
|
|
42
|
-
<DropdownMenu.Content>
|
|
48
|
+
<DropdownMenu.Content size={'1'} side='bottom' align='end'>
|
|
43
49
|
{rest.map(mode => (<DropdownMenu.Item key={mode} onClick={changeMode(mode)}>
|
|
44
50
|
{Mode[mode]}
|
|
45
51
|
</DropdownMenu.Item>))}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { ExternalLinkIcon, Link2Icon } from '@radix-ui/react-icons';
|
|
2
|
+
import { Box, Code, Flex, Heading, HoverCard, IconButton, Link, Text, Tooltip } from '@radix-ui/themes';
|
|
3
|
+
import { isEmpty } from 'remeda';
|
|
4
|
+
import { ViewActions } from './ViewActions';
|
|
5
|
+
import styles from './Header.module.css';
|
|
6
|
+
export function Header({ diagram }) {
|
|
7
|
+
return (<Flex position={'fixed'} top='0' left='0' width={'100%'} className={styles.header} justify='between' align={'stretch'} gap={'4'} p={'2'}>
|
|
8
|
+
<Flex pl='7' grow='1' gap={'2'} shrink='1' align={'stretch'} wrap={'nowrap'}>
|
|
9
|
+
<DiagramTitle diagram={diagram}/>
|
|
10
|
+
<DiagramLinks diagram={diagram}/>
|
|
11
|
+
</Flex>
|
|
12
|
+
<ViewActions diagram={diagram}/>
|
|
13
|
+
</Flex>);
|
|
14
|
+
}
|
|
15
|
+
function DiagramTitle({ diagram }) {
|
|
16
|
+
const hasDescription = !isEmpty(diagram.description?.trim());
|
|
17
|
+
return (<HoverCard.Root closeDelay={500}>
|
|
18
|
+
<HoverCard.Trigger>
|
|
19
|
+
<Flex px={'3'} className={styles.title} align={'center'}>
|
|
20
|
+
<Heading size={{
|
|
21
|
+
initial: '2',
|
|
22
|
+
sm: '3',
|
|
23
|
+
md: '4'
|
|
24
|
+
}} trim={'both'} weight={'medium'}>
|
|
25
|
+
{diagram.title || 'Untitled'}
|
|
26
|
+
</Heading>
|
|
27
|
+
</Flex>
|
|
28
|
+
</HoverCard.Trigger>
|
|
29
|
+
<HoverCard.Content size={'2'} className={styles.titleHoverCardContent}>
|
|
30
|
+
<Flex direction='column' gap='3'>
|
|
31
|
+
<HoverCardItem title='view id'>
|
|
32
|
+
<Code color='gray' size='2'>
|
|
33
|
+
{diagram.id}
|
|
34
|
+
</Code>
|
|
35
|
+
</HoverCardItem>
|
|
36
|
+
{diagram.viewOf && (<HoverCardItem title='view of'>
|
|
37
|
+
<Code size='2'>{diagram.viewOf}</Code>
|
|
38
|
+
</HoverCardItem>)}
|
|
39
|
+
<HoverCardItem title='description'>
|
|
40
|
+
{hasDescription ? (<Text as='p' size='2' style={{ whiteSpace: 'pre-line' }}>
|
|
41
|
+
{diagram.description?.trim()}
|
|
42
|
+
</Text>) : (<Text as='p' size='2' className={styles.dimmed}>
|
|
43
|
+
no description
|
|
44
|
+
</Text>)}
|
|
45
|
+
</HoverCardItem>
|
|
46
|
+
</Flex>
|
|
47
|
+
</HoverCard.Content>
|
|
48
|
+
</HoverCard.Root>);
|
|
49
|
+
}
|
|
50
|
+
function HoverCardItem({ title, children }) {
|
|
51
|
+
return (<Box>
|
|
52
|
+
<Text as='p' size='1' color='gray'>
|
|
53
|
+
{title}
|
|
54
|
+
</Text>
|
|
55
|
+
{children}
|
|
56
|
+
</Box>);
|
|
57
|
+
}
|
|
58
|
+
function DiagramLinks({ diagram: { links } }) {
|
|
59
|
+
if (!links) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (links.length > 1) {
|
|
63
|
+
return (<Flex align={'center'}>
|
|
64
|
+
<Box grow={'0'} height={'4'}>
|
|
65
|
+
<HoverCard.Root closeDelay={500}>
|
|
66
|
+
<HoverCard.Trigger>
|
|
67
|
+
<IconButton color='gray' variant='ghost' size={'2'}>
|
|
68
|
+
<Link2Icon width={16} height={16}/>
|
|
69
|
+
</IconButton>
|
|
70
|
+
</HoverCard.Trigger>
|
|
71
|
+
<HoverCard.Content size={'2'} align='center'>
|
|
72
|
+
<Flex direction='column' gap='2'>
|
|
73
|
+
{links.map(link => (<Flex asChild align={'center'} gap={'2'} key={link}>
|
|
74
|
+
<Link href={link} target='_blank'>
|
|
75
|
+
<ExternalLinkIcon width={13} height={13}/>
|
|
76
|
+
<Text size='2'>{link}</Text>
|
|
77
|
+
</Link>
|
|
78
|
+
</Flex>))}
|
|
79
|
+
</Flex>
|
|
80
|
+
</HoverCard.Content>
|
|
81
|
+
</HoverCard.Root>
|
|
82
|
+
</Box>
|
|
83
|
+
</Flex>);
|
|
84
|
+
}
|
|
85
|
+
const link = links[0];
|
|
86
|
+
return (<Flex align={'center'}>
|
|
87
|
+
<Tooltip content={link}>
|
|
88
|
+
<Box grow={'0'}>
|
|
89
|
+
<IconButton asChild color='gray' variant='ghost' size={'2'}>
|
|
90
|
+
<Link href={link} target='_blank'>
|
|
91
|
+
<Link2Icon width={16} height={16}/>
|
|
92
|
+
</Link>
|
|
93
|
+
</IconButton>
|
|
94
|
+
</Box>
|
|
95
|
+
</Tooltip>
|
|
96
|
+
</Flex>);
|
|
97
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
.header {
|
|
2
|
+
background: var(--color-panel-translucent);
|
|
3
|
+
border-bottom: 1px solid var(--gray-a2);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.dimmed {
|
|
7
|
+
color: var(--color-text-dimmed);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* diagram title */
|
|
11
|
+
.title {
|
|
12
|
+
border-radius: var(--radius-2);
|
|
13
|
+
cursor: var(--cursor-link);
|
|
14
|
+
|
|
15
|
+
&:hover {
|
|
16
|
+
background-color: var(--gray-a3);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.titleHoverCardContent {
|
|
21
|
+
width: var(--radix-hover-card-trigger-width);
|
|
22
|
+
min-width: 200px;
|
|
23
|
+
max-width: 500px;
|
|
24
|
+
}
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { ExclamationTriangleIcon, InfoCircledIcon, OpenInNewWindowIcon } from '@radix-ui/react-icons';
|
|
2
|
-
import { Box, Button, Callout, Code, Dialog, Flex, Link, Select, Tabs, Text } from '@radix-ui/themes';
|
|
2
|
+
import { Box, Button, Callout, Code, Dialog, Flex, Link, ScrollArea, Select, Tabs, Text } from '@radix-ui/themes';
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { $pages } from '../../router';
|
|
5
|
+
import { CopyToClipboard } from '../CopyToClipboard';
|
|
5
6
|
const embedCode = (diagram, theme) => {
|
|
7
|
+
const url = new URL($pages.embed.path(diagram.id), window.location.href);
|
|
6
8
|
const padding = 20;
|
|
7
|
-
|
|
8
|
-
params.set('padding', `${padding}`);
|
|
9
|
+
url.searchParams.set('padding', `${padding}`);
|
|
9
10
|
if (theme !== 'system') {
|
|
10
|
-
|
|
11
|
+
url.searchParams.set('theme', theme);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
url.searchParams.delete('theme');
|
|
11
15
|
}
|
|
12
16
|
const width = diagram.width + padding * 2;
|
|
13
17
|
const height = diagram.height + padding * 2;
|
|
14
|
-
const url = new URL(window.location.href);
|
|
15
|
-
url.pathname = $pages.embed.path(diagram.id);
|
|
16
|
-
url.search = params.toString();
|
|
17
|
-
const iframe = `<iframe src="${url.href}" width="100%" height="100%" style="border:0;background:transparent;"></iframe>`;
|
|
18
18
|
const code = `
|
|
19
|
-
<div style="aspect-ratio:${width}/${height};
|
|
20
|
-
${iframe
|
|
21
|
-
</div
|
|
19
|
+
<div style="aspect-ratio:${width}/${height};width:100%;height:auto;max-width:${width}px;margin:0 auto">
|
|
20
|
+
<iframe src="${url.href}" width="100%" height="100%" style="border:0;background:transparent;"></iframe>
|
|
21
|
+
</div>
|
|
22
|
+
`.trim();
|
|
22
23
|
return {
|
|
23
24
|
code,
|
|
24
25
|
href: url.href
|
|
@@ -64,10 +65,17 @@ export const ShareDialog = ({ diagram }) => {
|
|
|
64
65
|
</Link>
|
|
65
66
|
</Flex>
|
|
66
67
|
</Flex>
|
|
67
|
-
<Box
|
|
68
|
-
<
|
|
69
|
-
{
|
|
70
|
-
|
|
68
|
+
<Box position={'relative'} mt={'1'}>
|
|
69
|
+
<ScrollArea scrollbars='both' style={{ maxHeight: 200 }}>
|
|
70
|
+
<Box asChild display={'block'} px='2' py='3' style={{
|
|
71
|
+
whiteSpace: 'pre'
|
|
72
|
+
}}>
|
|
73
|
+
<Code variant='soft' autoFocus>
|
|
74
|
+
{code}
|
|
75
|
+
</Code>
|
|
76
|
+
</Box>
|
|
77
|
+
</ScrollArea>
|
|
78
|
+
<CopyToClipboard text={code}/>
|
|
71
79
|
</Box>
|
|
72
80
|
</label>
|
|
73
81
|
<Text as='div' size='2' color='gray' trim={'start'}>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { CaretDownIcon, Share1Icon as ShareIcon } from '@radix-ui/react-icons';
|
|
2
|
+
import { Button, Dialog, DropdownMenu, Flex, Text } from '@radix-ui/themes';
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
// import { ThemePanelToggle } from '../ThemePanelToggle'
|
|
5
|
+
import { DisplayModeSelector } from './DisplayModeSelector';
|
|
6
|
+
import ExportDiagram from './ExportDiagram';
|
|
7
|
+
import { ShareDialog } from './ShareDialog';
|
|
8
|
+
import { updateSearchParams } from '../../router';
|
|
9
|
+
const ExportMenu = ({ onExport, children }) => {
|
|
10
|
+
const changeMode = (mode) => (_) => {
|
|
11
|
+
updateSearchParams({ mode });
|
|
12
|
+
};
|
|
13
|
+
return (<DropdownMenu.Root>
|
|
14
|
+
<DropdownMenu.Trigger>{children}</DropdownMenu.Trigger>
|
|
15
|
+
<DropdownMenu.Content variant='soft'>
|
|
16
|
+
<DropdownMenu.Label>
|
|
17
|
+
<Text weight='medium'>Current view</Text>
|
|
18
|
+
</DropdownMenu.Label>
|
|
19
|
+
<DropdownMenu.Group>
|
|
20
|
+
<DropdownMenu.Item onClick={_ => {
|
|
21
|
+
onExport('png');
|
|
22
|
+
}}>
|
|
23
|
+
Export as .png
|
|
24
|
+
</DropdownMenu.Item>
|
|
25
|
+
<DropdownMenu.Item onClick={changeMode('dot')}>Export as .dot</DropdownMenu.Item>
|
|
26
|
+
<DropdownMenu.Item onClick={changeMode('mmd')}>Export as .mmd</DropdownMenu.Item>
|
|
27
|
+
<DropdownMenu.Item onClick={changeMode('d2')}>Export as .d2</DropdownMenu.Item>
|
|
28
|
+
<DropdownMenu.Item disabled>Export to Draw.io</DropdownMenu.Item>
|
|
29
|
+
<DropdownMenu.Item disabled>Export to Miro</DropdownMenu.Item>
|
|
30
|
+
</DropdownMenu.Group>
|
|
31
|
+
<DropdownMenu.Separator />
|
|
32
|
+
<DropdownMenu.Label>
|
|
33
|
+
<Text weight='medium'>All views</Text>
|
|
34
|
+
</DropdownMenu.Label>
|
|
35
|
+
<DropdownMenu.Group>
|
|
36
|
+
<DropdownMenu.Item disabled>Download as ZIP</DropdownMenu.Item>
|
|
37
|
+
</DropdownMenu.Group>
|
|
38
|
+
</DropdownMenu.Content>
|
|
39
|
+
</DropdownMenu.Root>);
|
|
40
|
+
};
|
|
41
|
+
export const ViewActions = ({ diagram }) => {
|
|
42
|
+
const [exportTo, setExportTo] = useState(null);
|
|
43
|
+
return (<Flex shrink={'0'} grow={'0'} gap={'3'} align='center' wrap={'nowrap'}>
|
|
44
|
+
<DisplayModeSelector />
|
|
45
|
+
<Dialog.Root>
|
|
46
|
+
<Dialog.Trigger>
|
|
47
|
+
<Button variant='solid' size={{
|
|
48
|
+
initial: '1',
|
|
49
|
+
md: '2'
|
|
50
|
+
}}>
|
|
51
|
+
<ShareIcon />
|
|
52
|
+
<Text>Share</Text>
|
|
53
|
+
</Button>
|
|
54
|
+
</Dialog.Trigger>
|
|
55
|
+
<ShareDialog diagram={diagram}/>
|
|
56
|
+
</Dialog.Root>
|
|
57
|
+
<ExportMenu onExport={setExportTo}>
|
|
58
|
+
<Button variant='soft' color='gray' size={{
|
|
59
|
+
initial: '1',
|
|
60
|
+
md: '2'
|
|
61
|
+
}}>
|
|
62
|
+
<Text>Export</Text>
|
|
63
|
+
<CaretDownIcon />
|
|
64
|
+
</Button>
|
|
65
|
+
</ExportMenu>
|
|
66
|
+
{exportTo === 'png' && (<ExportDiagram key={'export-diagram-png'} diagram={diagram} onCompleted={() => setExportTo(null)}/>)}
|
|
67
|
+
{/* <ThemePanelToggle /> */}
|
|
68
|
+
</Flex>);
|
|
69
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { atom } from 'jotai';
|
|
2
|
-
import { atomFamily, atomWithReducer
|
|
3
|
-
import { equals,
|
|
4
|
-
import { LikeC4Views } from '
|
|
2
|
+
import { atomFamily, atomWithReducer } from 'jotai/utils';
|
|
3
|
+
import { equals, mapObject, values } from 'rambdax';
|
|
4
|
+
import { LikeC4Views } from 'virtual:likec4/views';
|
|
5
5
|
import { buildDiagramTreeAtom } from './sidebar-diagram-tree';
|
|
6
6
|
function atomWithCompare(initialValue) {
|
|
7
7
|
return atomWithReducer(initialValue, (prev, next) => {
|
|
@@ -27,24 +27,6 @@ export const viewsAtom = atom(get => get(_viewsAtom), (_, set, update) => {
|
|
|
27
27
|
}, update));
|
|
28
28
|
});
|
|
29
29
|
viewsAtom.debugLabel = 'views';
|
|
30
|
-
const indexPageTilesAtom = atom(get => {
|
|
31
|
-
const views = values(get(viewsAtom));
|
|
32
|
-
const byPath = groupBy(v => get(v).relativePath ?? '', views);
|
|
33
|
-
return Object.entries(byPath)
|
|
34
|
-
.map(([path, views]) => ({
|
|
35
|
-
path,
|
|
36
|
-
isRoot: path === '',
|
|
37
|
-
views
|
|
38
|
-
}))
|
|
39
|
-
.sort((a, b) => {
|
|
40
|
-
return a.path.localeCompare(b.path);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
const byPath = (tile) => tile.path;
|
|
44
|
-
export const indexPageTilesAtomsAtom = splitAtom(indexPageTilesAtom, byPath);
|
|
45
|
-
// export type DashboardTile = ExtractAtomValue<typeof dashboardTilesAtom>[number]
|
|
46
|
-
// const key = (tile: DashboardTile) => tile.path
|
|
47
|
-
// export const diagramsTreeAtom = atom(buildDiagramTreeAtom(LikeC4Views))
|
|
48
30
|
export const diagramsTreeAtom = atom(get => {
|
|
49
31
|
const views = values(get(viewsAtom));
|
|
50
32
|
return buildDiagramTreeAtom(views.map(v => get(v)));
|
|
@@ -63,7 +45,7 @@ if (import.meta.hot) {
|
|
|
63
45
|
$updateViews = undefined;
|
|
64
46
|
};
|
|
65
47
|
};
|
|
66
|
-
import.meta.hot.accept('/@vite-plugin-likec4/likec4-
|
|
48
|
+
import.meta.hot.accept('/@vite-plugin-likec4/likec4-views', md => {
|
|
67
49
|
const update = md?.LikeC4Views;
|
|
68
50
|
if (update) {
|
|
69
51
|
$updateViews?.(update);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { useAtomValue } from 'jotai';
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
|
-
import { diagramsTreeAtom,
|
|
3
|
+
import { diagramsTreeAtom, selectLikeC4ViewAtom } from './atoms';
|
|
4
|
+
import { viewsGroupAtomsAtom } from './index-page';
|
|
4
5
|
export const useLikeC4View = (viewId) => {
|
|
5
6
|
const anAtom = useMemo(() => selectLikeC4ViewAtom(viewId), [viewId]);
|
|
6
7
|
return useAtomValue(anAtom);
|
|
7
8
|
};
|
|
8
|
-
export const useIndexPageTileAtoms = () => {
|
|
9
|
-
return useAtomValue(indexPageTilesAtomsAtom);
|
|
10
|
-
};
|
|
11
9
|
export const useDiagramsTree = () => useAtomValue(diagramsTreeAtom);
|
|
10
|
+
export const useViewGroupsAtoms = () => {
|
|
11
|
+
return useAtomValue(viewsGroupAtomsAtom);
|
|
12
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { atom } from 'jotai';
|
|
2
|
+
import { viewsAtom } from './atoms';
|
|
3
|
+
import { groupBy, values } from 'remeda';
|
|
4
|
+
import { splitAtom } from 'jotai/utils';
|
|
5
|
+
/**
|
|
6
|
+
* Views grouped by folder
|
|
7
|
+
*/
|
|
8
|
+
const viewGroupsAtom = atom(get => {
|
|
9
|
+
const views = values(get(viewsAtom));
|
|
10
|
+
const byPath = groupBy(views, v => get(v).relativePath ?? '');
|
|
11
|
+
return Object.entries(byPath)
|
|
12
|
+
.map(([path, views]) => ({
|
|
13
|
+
path,
|
|
14
|
+
isRoot: path === '',
|
|
15
|
+
views
|
|
16
|
+
}))
|
|
17
|
+
.sort((a, b) => {
|
|
18
|
+
return a.path.localeCompare(b.path);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
const byPath = (tile) => tile.path;
|
|
22
|
+
export const viewsGroupAtomsAtom = splitAtom(viewGroupsAtom, byPath);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Diagram } from '@likec4/diagrams';
|
|
2
|
+
import { Box, Card, Container, Flex, Heading, IconButton, Inset, Section, Separator, Text } from '@radix-ui/themes';
|
|
3
|
+
import { useDebouncedEffect } from '@react-hookz/web/esm';
|
|
4
|
+
import { useAtomValue } from 'jotai';
|
|
5
|
+
import { memo, useState } from 'react';
|
|
6
|
+
import { isEmpty } from 'remeda';
|
|
7
|
+
import { useViewGroupsAtoms } from '../../data';
|
|
8
|
+
import { $pages } from '../../router';
|
|
9
|
+
import { cn } from '../../utils';
|
|
10
|
+
import styles from './index.module.css';
|
|
11
|
+
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
|
12
|
+
const DiagramPreview = memo((props) => {
|
|
13
|
+
const [diagram, setDiagram] = useState(null);
|
|
14
|
+
// defer rendering to update to avoid flickering
|
|
15
|
+
useDebouncedEffect(() => {
|
|
16
|
+
setDiagram(props.diagram);
|
|
17
|
+
}, [props.diagram], 50);
|
|
18
|
+
return (<Box className={styles.previewBg} style={{ width: 350, height: 175 }}>
|
|
19
|
+
{diagram && (<Diagram animate={false} pannable={false} zoomable={false} minZoom={0.1} maxZoom={1} diagram={diagram} padding={[4, 6, 4, 6]} width={350} height={175}/>)}
|
|
20
|
+
</Box>);
|
|
21
|
+
});
|
|
22
|
+
const ViewCard = ({ atom }) => {
|
|
23
|
+
// const diagram =
|
|
24
|
+
const diagram = useAtomValue(atom);
|
|
25
|
+
const { id, title, description } = diagram;
|
|
26
|
+
return (<Box asChild shrink='0' grow='1'>
|
|
27
|
+
<Card asChild style={{ width: 350, maxWidth: 350 }} variant='surface' size='1'>
|
|
28
|
+
<a href={$pages.view.url(id)}>
|
|
29
|
+
<Inset clip='padding-box' side='top' pb='current'>
|
|
30
|
+
<DiagramPreview diagram={diagram}/>
|
|
31
|
+
</Inset>
|
|
32
|
+
<Text as='div' size='2' weight='bold' trim='start'>
|
|
33
|
+
{title || id}
|
|
34
|
+
</Text>
|
|
35
|
+
<Text as='div' color='gray' size='2' my='1' className={cn(isEmpty(description?.trim()) && styles.dimmed)} style={{
|
|
36
|
+
whiteSpace: 'pre-line'
|
|
37
|
+
}}>
|
|
38
|
+
{description?.trim() || 'no description'}
|
|
39
|
+
</Text>
|
|
40
|
+
</a>
|
|
41
|
+
</Card>
|
|
42
|
+
</Box>);
|
|
43
|
+
};
|
|
44
|
+
function ViewsGroup({ atom }) {
|
|
45
|
+
const { path, views, isRoot } = useAtomValue(atom);
|
|
46
|
+
return (<Flex asChild gap={'4'} direction={'column'}>
|
|
47
|
+
<Section size='2'>
|
|
48
|
+
<Flex gap='2'>
|
|
49
|
+
<Heading color={isRoot ? undefined : 'gray'} className={cn(isRoot || styles.dimmed)} trim='end'>
|
|
50
|
+
views
|
|
51
|
+
</Heading>
|
|
52
|
+
{!isRoot && (<>
|
|
53
|
+
<Heading color='gray' className={styles.dimmed} trim={'end'}>
|
|
54
|
+
/
|
|
55
|
+
</Heading>
|
|
56
|
+
<Heading trim={'end'}>{path}</Heading>
|
|
57
|
+
</>)}
|
|
58
|
+
</Flex>
|
|
59
|
+
<Separator orientation='horizontal' my='2' size={'4'}/>
|
|
60
|
+
<Flex gap={{
|
|
61
|
+
initial: '4',
|
|
62
|
+
md: '6'
|
|
63
|
+
}} wrap={{
|
|
64
|
+
initial: 'nowrap',
|
|
65
|
+
md: 'wrap'
|
|
66
|
+
}} direction={{
|
|
67
|
+
initial: 'column',
|
|
68
|
+
md: 'row'
|
|
69
|
+
}} align='stretch'>
|
|
70
|
+
{views.map(v => (<ViewCard key={v.toString()} atom={v}/>))}
|
|
71
|
+
</Flex>
|
|
72
|
+
</Section>
|
|
73
|
+
</Flex>);
|
|
74
|
+
}
|
|
75
|
+
export function IndexPage() {
|
|
76
|
+
const viewGroupsAtoms = useViewGroupsAtoms();
|
|
77
|
+
return (<Container size={'4'} px={{
|
|
78
|
+
initial: '3',
|
|
79
|
+
lg: '1'
|
|
80
|
+
}}>
|
|
81
|
+
{viewGroupsAtoms.map(g => (<ViewsGroup key={g.toString()} atom={g}/>))}
|
|
82
|
+
{viewGroupsAtoms.length === 0 && (<Flex position='fixed' inset='0' align='center' justify='center'>
|
|
83
|
+
<Card color='red' size='4'>
|
|
84
|
+
<Flex gap='4' direction='row' align='center'>
|
|
85
|
+
<Box grow='0' shrink='0' pt='1'>
|
|
86
|
+
<IconButton variant='ghost' color='red'>
|
|
87
|
+
<ExclamationTriangleIcon width={20} height={20}/>
|
|
88
|
+
</IconButton>
|
|
89
|
+
</Box>
|
|
90
|
+
<Flex gap='3' direction='column'>
|
|
91
|
+
<Heading trim='both' color='red' size='4'>
|
|
92
|
+
No diagrams found
|
|
93
|
+
</Heading>
|
|
94
|
+
</Flex>
|
|
95
|
+
</Flex>
|
|
96
|
+
</Card>
|
|
97
|
+
</Flex>)}
|
|
98
|
+
</Container>);
|
|
99
|
+
}
|