likec4 0.48.0 → 0.50.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 (51) hide show
  1. package/README.md +12 -6
  2. package/dist/@likec4/diagrams/diagram/Diagram.js +55 -27
  3. package/dist/@likec4/diagrams/diagram/Edges.js +14 -11
  4. package/dist/@likec4/diagrams/diagram/Nodes.js +212 -23
  5. package/dist/@likec4/diagrams/diagram/icons/ZoomIn.js +2 -1
  6. package/dist/@likec4/diagrams/diagram/shapes/Compound.js +2 -1
  7. package/dist/@likec4/diagrams/diagram/shapes/Edge.js +1 -1
  8. package/dist/@likec4/diagrams/diagram/state/atoms.js +35 -60
  9. package/dist/@likec4/diagrams/diagram/utils.js +14 -0
  10. package/dist/@likec4/diagrams/hooks/useDiagramApi.js +4 -5
  11. package/dist/@likec4/diagrams/hooks/useImageLoader.js +1 -1
  12. package/dist/__app__/likec4.css +8 -5
  13. package/dist/__app__/src/App.jsx +9 -11
  14. package/dist/__app__/src/components/CopyToClipboard.jsx +29 -0
  15. package/dist/__app__/src/components/CopyToClipboard.module.css +16 -0
  16. package/dist/__app__/src/components/DiagramNotFound.jsx +2 -2
  17. package/dist/__app__/src/components/index.js +2 -1
  18. package/dist/__app__/src/components/sidebar/Sidebar.jsx +1 -1
  19. package/dist/__app__/src/components/sidebar/styles.module.css +4 -1
  20. package/dist/__app__/src/components/view-page/DisplayModeSelector.jsx +15 -9
  21. package/dist/__app__/src/components/view-page/Header.jsx +97 -0
  22. package/dist/__app__/src/components/view-page/Header.module.css +24 -0
  23. package/dist/__app__/src/components/view-page/ShareDialog.jsx +23 -15
  24. package/dist/__app__/src/components/view-page/ViewActions.jsx +69 -0
  25. package/dist/__app__/src/data/atoms.js +4 -22
  26. package/dist/__app__/src/data/hooks.js +5 -4
  27. package/dist/__app__/src/data/index-page.js +22 -0
  28. package/dist/__app__/src/likec4-views.js +1 -1
  29. package/dist/__app__/src/pages/index-page/index.jsx +99 -0
  30. package/dist/__app__/src/pages/index-page/index.module.css +20 -0
  31. package/dist/__app__/src/pages/index.js +1 -1
  32. package/dist/__app__/src/pages/useTransparentBackground.js +2 -2
  33. package/dist/__app__/src/pages/view-page/ViewAsReact.jsx +60 -0
  34. package/dist/__app__/src/pages/view-page/index.js +11 -0
  35. package/dist/__app__/src/pages/view-page/other-formats/ViewAsD2.jsx +18 -0
  36. package/dist/__app__/src/pages/view-page/other-formats/ViewAsDot.jsx +33 -0
  37. package/dist/__app__/src/pages/view-page/other-formats/ViewAsMmd.jsx +18 -0
  38. package/dist/__app__/src/pages/view-page/other-formats.jsx +43 -0
  39. package/dist/__app__/src/pages/view-page/view-page.module.css +81 -0
  40. package/dist/__app__/src/pages/view.page.jsx +12 -65
  41. package/dist/__app__/src/router.js +90 -20
  42. package/dist/__app__/src/utils/utils.js +1 -2
  43. package/dist/__app__/tsconfig.json +1 -0
  44. package/dist/cli/index.js +288 -207
  45. package/package.json +17 -17
  46. package/dist/__app__/postcss.config.cjs +0 -11
  47. package/dist/__app__/src/components/view-page/ViewActionsToolbar.jsx +0 -66
  48. package/dist/__app__/src/pages/index.module.css +0 -11
  49. package/dist/__app__/src/pages/index.page.jsx +0 -57
  50. package/dist/__app__/src/pages/view-page.module.css +0 -30
  51. package/dist/__app__/tailwind.config.cjs +0 -17
@@ -3,92 +3,67 @@ import { equals } from "rambdax";
3
3
  import { selectAtom } from "jotai/utils";
4
4
  const currentHoveredNodeAtom = atom(null);
5
5
  const nodeTimeoutAtom = atom(void 0);
6
+ const scheduleHoveredNode = (set, node = null, timeout = 175) => {
7
+ if (timeout <= 0) {
8
+ set(currentHoveredNodeAtom, node);
9
+ return;
10
+ }
11
+ set(
12
+ nodeTimeoutAtom,
13
+ setTimeout(() => {
14
+ set(currentHoveredNodeAtom, node);
15
+ }, timeout)
16
+ );
17
+ };
6
18
  export const hoveredNodeAtom = atom(
7
19
  (get) => get(currentHoveredNodeAtom),
8
20
  (get, set, update) => {
9
21
  clearTimeout(get(nodeTimeoutAtom));
10
- clearTimeout(get(edgeTimeoutAtom));
11
22
  const _prev = get(currentHoveredNodeAtom);
12
23
  const _next = typeof update === "function" ? update(_prev) : update;
13
24
  if (equals(_prev, _next)) {
14
25
  return false;
15
26
  }
16
- if (_next != null && _prev == null) {
17
- set(
18
- nodeTimeoutAtom,
19
- setTimeout(() => {
20
- set(currentHoveredNodeAtom, _next);
21
- set(currentHoveredEdgeAtom, null);
22
- }, 200)
23
- );
24
- return true;
25
- }
26
- if (_next != null && _prev != null) {
27
- set(
28
- nodeTimeoutAtom,
29
- setTimeout(() => {
30
- set(currentHoveredNodeAtom, _next);
31
- set(currentHoveredEdgeAtom, null);
32
- }, 150)
33
- );
34
- return true;
35
- }
36
- if (_next == null && _prev != null) {
37
- set(
38
- nodeTimeoutAtom,
39
- setTimeout(() => {
40
- set(currentHoveredNodeAtom, null);
41
- }, 150)
42
- );
43
- return true;
27
+ const timeout = !!_next && !!_prev ? 120 : 175;
28
+ if (_next != null) {
29
+ clearTimeout(get(edgeTimeoutAtom));
30
+ scheduleHoveredEdge(set, null, timeout);
44
31
  }
45
- set(currentHoveredNodeAtom, _next);
32
+ scheduleHoveredNode(set, _next, timeout);
46
33
  return true;
47
34
  }
48
35
  );
49
36
  export const hoveredNodeIdAtom = selectAtom(hoveredNodeAtom, (node) => node?.id ?? null);
50
37
  const currentHoveredEdgeAtom = atom(null);
51
38
  const edgeTimeoutAtom = atom(void 0);
39
+ const scheduleHoveredEdge = (set, edge = null, timeout = 175) => {
40
+ if (timeout <= 0) {
41
+ set(currentHoveredEdgeAtom, edge);
42
+ return;
43
+ }
44
+ set(
45
+ edgeTimeoutAtom,
46
+ setTimeout(() => {
47
+ set(currentHoveredEdgeAtom, edge);
48
+ }, timeout)
49
+ );
50
+ };
52
51
  export const hoveredEdgeAtom = atom(
53
52
  (get) => get(currentHoveredEdgeAtom),
54
53
  (get, set, update) => {
55
- clearTimeout(get(nodeTimeoutAtom));
56
54
  clearTimeout(get(edgeTimeoutAtom));
57
55
  const _prev = get(currentHoveredEdgeAtom);
58
56
  const _next = typeof update === "function" ? update(_prev) : update;
59
57
  if (equals(_prev, _next)) {
60
58
  return false;
61
59
  }
62
- if (_next != null && _prev == null) {
63
- set(
64
- edgeTimeoutAtom,
65
- setTimeout(() => {
66
- set(currentHoveredEdgeAtom, _next);
67
- set(currentHoveredNodeAtom, null);
68
- }, 400)
69
- );
70
- return true;
71
- }
72
- if (_next != null && _prev != null) {
73
- set(
74
- edgeTimeoutAtom,
75
- setTimeout(() => {
76
- set(currentHoveredEdgeAtom, _next);
77
- set(currentHoveredNodeAtom, null);
78
- }, 150)
79
- );
80
- return true;
81
- }
82
- if (_next == null && _prev != null) {
83
- set(
84
- edgeTimeoutAtom,
85
- setTimeout(() => {
86
- set(currentHoveredEdgeAtom, null);
87
- }, 150)
88
- );
89
- return true;
60
+ let timeout = 175;
61
+ if (_next != null) {
62
+ timeout = _prev != null ? 120 : 300;
63
+ clearTimeout(get(nodeTimeoutAtom));
64
+ scheduleHoveredNode(set, null, timeout);
90
65
  }
91
- set(currentHoveredEdgeAtom, null);
66
+ scheduleHoveredEdge(set, _next, timeout);
92
67
  return true;
93
68
  }
94
69
  );
@@ -0,0 +1,14 @@
1
+ import { is } from "rambdax";
2
+ export function mousePointer(e) {
3
+ const container = e.target.getStage()?.container();
4
+ if (container) {
5
+ container.style.cursor = "pointer";
6
+ }
7
+ }
8
+ export function mouseDefault(e) {
9
+ const container = e.target.getStage()?.container();
10
+ if (container) {
11
+ container.style.cursor = "";
12
+ }
13
+ }
14
+ export const isNumber = is(Number);
@@ -13,11 +13,10 @@ export function useDiagramApi() {
13
13
  get container() {
14
14
  return ref.current?.container ?? null;
15
15
  },
16
- resetStageZoom: (_immediate) => {
17
- nonNullable(ref.current, "not mounted, use ref").resetStageZoom(_immediate);
18
- },
19
- centerOnNode: (node) => nonNullable(ref.current, "not mounted, use ref").centerOnNode(node),
20
- centerAndFit: () => nonNullable(ref.current, "not mounted, use ref").centerAndFit()
16
+ resetStageZoom: (_immediate) => nonNullable(ref.current, "not mounted, use ref").resetStageZoom(_immediate),
17
+ centerOnNode: (node, opts) => nonNullable(ref.current, "not mounted, use ref").centerOnNode(node, opts),
18
+ centerOnRect: (rect, opts) => nonNullable(ref.current, "not mounted, use ref").centerOnRect(rect, opts),
19
+ centerAndFit: (opts) => nonNullable(ref.current, "not mounted, use ref").centerAndFit(opts)
21
20
  })
22
21
  );
23
22
  return [ref, api];
@@ -1,5 +1,5 @@
1
1
  import { useLayoutEffect, useRef, useState } from "react";
2
- import { useIsMounted } from "@react-hookz/web/esm/useIsMounted";
2
+ import { useIsMounted } from "@react-hookz/web/esm";
3
3
  import { invariant } from "@likec4/core";
4
4
  const imageElements = /* @__PURE__ */ new Map();
5
5
  export default function useImageLoader(url, crossOrigin, referrerpolicy) {
@@ -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 {
@@ -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, EmbedPage, ViewPage } from './pages';
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 && (<Fragment key='ui'>
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' className='cursor-pointer' onClick={() => $pages.index.open()}>
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 { ViewActionsToolbar } from './view-page/ViewActionsToolbar';
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' p={'2'} className={cn(styles.trigger, 'inset-y-0 cursor-pointer items-start', isOpened && 'display-none')} onClick={toggle}>
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,10 @@
1
1
  .trigger {
2
-
3
2
  cursor: pointer;
4
3
 
4
+ &[data-opened='true'] {
5
+ visibility: hidden;
6
+ }
7
+
5
8
  &::before {
6
9
  transition-property: all;
7
10
  transition-timing-function: cubic-bezier(0, 0.31, 0, 1.03);
@@ -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 [current, setCurrent] = useState('react');
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
- 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)]);
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
- const params = new URLSearchParams();
8
- params.set('padding', `${padding}`);
9
+ url.searchParams.set('padding', `${padding}`);
9
10
  if (theme !== 'system') {
10
- params.set('theme', theme);
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};max-width:${width}px;width:100%;height:auto;padding:0;margin-left:auto;margin-right:auto">
20
- ${iframe}
21
- </div>`.trim();
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 asChild display={'block'} my='2' p='2' className='whitespace-pre-wrap overflow-scroll select-all'>
68
- <Code variant='soft' autoFocus>
69
- {code}
70
- </Code>
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
+ };