@likec4/diagram 1.1.0 → 1.2.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 (76) hide show
  1. package/dist/KeepAspectRatioContainer.d.ts +1 -1
  2. package/dist/LikeC4Diagram.d.ts +1 -1
  3. package/dist/LikeC4Diagram.props.d.ts +22 -12
  4. package/dist/StaticLikeC4Diagram.d.ts +1 -1
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.mjs +2209 -706
  7. package/dist/mantine/EnsureMantine.d.ts +1 -1
  8. package/dist/state/DiagramContext.d.ts +2143 -19
  9. package/dist/state/WhenInitialized.d.ts +1 -1
  10. package/dist/state/diagramStore.d.ts +2152 -19
  11. package/dist/state/useDiagramStore.d.ts +2142 -18
  12. package/dist/style.css +256 -118
  13. package/dist/theme.css.d.ts +1 -0
  14. package/dist/ui/DiagramTitlePanel.css.d.ts +4 -0
  15. package/dist/ui/DiagramTitlePanel.d.ts +1 -0
  16. package/dist/ui/DynamicViewWalkthrough.css.d.ts +3 -0
  17. package/dist/ui/DynamicViewWalkthrough.d.ts +1 -0
  18. package/dist/ui/OptionsPanel.d.ts +1 -3
  19. package/dist/ui/index.d.ts +1 -0
  20. package/dist/ui/options/NodeOptions.d.ts +1 -2
  21. package/dist/xyflow/FitviewOnDiagramChange.d.ts +4 -2
  22. package/dist/xyflow/SelectEdgesOnNodeFocus.d.ts +1 -0
  23. package/dist/xyflow/SyncWithDiagram.d.ts +1 -2
  24. package/dist/xyflow/XYFlow.d.ts +4 -7
  25. package/dist/xyflow/XYFlowBackground.d.ts +5 -4
  26. package/dist/xyflow/XYFlowEvents.d.ts +84 -84
  27. package/dist/xyflow/XYFlowInner.d.ts +11 -0
  28. package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
  29. package/dist/xyflow/edges/RelationshipEdge.d.ts +14 -14
  30. package/dist/xyflow/edges/edges.css.d.ts +5 -2
  31. package/dist/xyflow/edges/utils.d.ts +2 -0
  32. package/dist/xyflow/hooks/useXYFlow.d.ts +126 -126
  33. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
  34. package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -1
  35. package/dist/xyflow/nodes/element/ElementLink.d.ts +1 -1
  36. package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -1
  37. package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +2 -2
  38. package/dist/xyflow/nodes/element/element.css.d.ts +2 -1
  39. package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +1 -2
  40. package/dist/xyflow/types.d.ts +18 -18
  41. package/package.json +21 -16
  42. package/src/KeepAspectRatioContainer.tsx +1 -1
  43. package/src/LikeC4Diagram.css.ts +4 -1
  44. package/src/LikeC4Diagram.props.ts +24 -14
  45. package/src/LikeC4Diagram.tsx +18 -9
  46. package/src/StaticLikeC4Diagram.tsx +2 -0
  47. package/src/globals.css.ts +7 -1
  48. package/src/index.ts +0 -3
  49. package/src/state/DiagramContext.tsx +21 -12
  50. package/src/state/diagramStore.ts +275 -98
  51. package/src/theme.css.ts +1 -0
  52. package/src/ui/DiagramTitlePanel.css.ts +33 -0
  53. package/src/ui/DiagramTitlePanel.tsx +138 -0
  54. package/src/ui/DynamicViewWalkthrough.css.ts +46 -0
  55. package/src/ui/DynamicViewWalkthrough.tsx +86 -0
  56. package/src/ui/OptionsPanel.tsx +7 -24
  57. package/src/ui/index.ts +1 -0
  58. package/src/ui/options/NodeOptions.tsx +103 -27
  59. package/src/xyflow/FitviewOnDiagramChange.tsx +89 -69
  60. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +75 -0
  61. package/src/xyflow/SyncWithDiagram.tsx +55 -101
  62. package/src/xyflow/XYFlow.tsx +52 -32
  63. package/src/xyflow/XYFlowBackground.tsx +7 -5
  64. package/src/xyflow/XYFlowEvents.tsx +150 -104
  65. package/src/xyflow/XYFlowInner.tsx +50 -0
  66. package/src/xyflow/diagram-to-xyflow.ts +16 -21
  67. package/src/xyflow/edges/RelationshipEdge.tsx +102 -70
  68. package/src/xyflow/edges/edges.css.ts +128 -53
  69. package/src/xyflow/edges/utils.ts +31 -0
  70. package/src/xyflow/nodes/compound/CompoundNode.css.ts +20 -12
  71. package/src/xyflow/nodes/compound/CompoundNode.tsx +6 -3
  72. package/src/xyflow/nodes/element/ElementLink.tsx +13 -3
  73. package/src/xyflow/nodes/element/ElementNode.tsx +24 -19
  74. package/src/xyflow/nodes/element/element.css.ts +10 -2
  75. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -0
  76. package/src/xyflow/types.ts +7 -8
@@ -0,0 +1,138 @@
1
+ import { Anchor, Box, Button, CopyButton, Group, Paper, Spoiler, Stack, Text } from '@mantine/core'
2
+ import { useLocalStorage } from '@mantine/hooks'
3
+ import clsx from 'clsx'
4
+ import { AnimatePresence, motion } from 'framer-motion'
5
+ import { type DiagramState, useDiagramState } from '../state'
6
+ import * as css from './DiagramTitlePanel.css'
7
+
8
+ const selector = (s: DiagramState) => ({
9
+ id: s.view.id,
10
+ title: s.view.title ?? 'untitled',
11
+ description: s.view.description,
12
+ links: s.view.links
13
+ })
14
+
15
+ export default function DiagramTitlePanel() {
16
+ const { id, title, description, links } = useDiagramState(selector)
17
+ const [isCollapsed, setCollapsed] = useLocalStorage({
18
+ key: 'diagram-title-panel-collapsed',
19
+ defaultValue: false
20
+ })
21
+ const toggle = () => setCollapsed(v => !v)
22
+
23
+ return (
24
+ <AnimatePresence mode="wait">
25
+ <motion.div
26
+ key={id}
27
+ initial={{ opacity: 0.05, scale: 0.7 }}
28
+ animate={{ opacity: 1, scale: 1 }}
29
+ exit={{
30
+ opacity: 0.05,
31
+ scale: 0.6
32
+ }}
33
+ className={clsx('react-flow__panel', css.container)}
34
+ style={{
35
+ transformOrigin: 'left center'
36
+ }}
37
+ >
38
+ <Paper
39
+ radius="sm"
40
+ withBorder={!isCollapsed}
41
+ className={css.paper}
42
+ p={isCollapsed ? 'xs' : 'lg'}
43
+ onDoubleClick={e => e.stopPropagation()}>
44
+ <Stack gap={'sm'} justify="stretch" align="stretch">
45
+ <Group justify="stretch" wrap="nowrap" onClick={toggle} style={{ cursor: 'pointer' }}>
46
+ <Text
47
+ component={'div'}
48
+ flex={'1'}
49
+ size={isCollapsed ? 'sm' : 'lg'}
50
+ fw={500}
51
+ lh={1.1}
52
+ className={css.title}
53
+ >
54
+ {title}
55
+ </Text>
56
+ <Text
57
+ hidden={isCollapsed}
58
+ component={'div'}
59
+ flex={'0 0 auto'}
60
+ inline
61
+ size="xs"
62
+ fz={9}
63
+ fw={500}
64
+ c={'dimmed'}
65
+ style={{
66
+ userSelect: 'all'
67
+ }}>
68
+ <span style={{ userSelect: 'none' }}>id:{' '}</span>
69
+ {id}
70
+ </Text>
71
+ </Group>
72
+ {!isCollapsed && (
73
+ <>
74
+ {description && (
75
+ <Spoiler
76
+ maxHeight={42}
77
+ showLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">show more</Button>}
78
+ hideLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">hide</Button>}>
79
+ <Text
80
+ component={'div'}
81
+ size="sm"
82
+ className={css.description}>
83
+ {description || 'no description'}
84
+ </Text>
85
+ </Spoiler>
86
+ )}
87
+ {!description && (
88
+ <Text
89
+ component={'div'}
90
+ size="xs"
91
+ c={'dimmed'}>
92
+ no description
93
+ </Text>
94
+ )}
95
+ {links && (
96
+ <Stack
97
+ gap={3}
98
+ justify="stretch"
99
+ align="stretch">
100
+ {links.map((link) => (
101
+ <Group key={link} wrap="nowrap" align="center" gap={'sm'}>
102
+ <Box flex={'1'} style={{ overflow: 'hidden' }}>
103
+ <Anchor
104
+ href={link}
105
+ target="_blank"
106
+ fz="xs"
107
+ truncate="end"
108
+ display={'inline-block'}
109
+ w={'100%'}>
110
+ {link}
111
+ </Anchor>
112
+ </Box>
113
+ <CopyButton value={link}>
114
+ {({ copied, copy }) => (
115
+ <Button
116
+ size="compact-xs"
117
+ fz={'10'}
118
+ variant="light"
119
+ onClick={copy}
120
+ color={copied
121
+ ? 'teal'
122
+ : 'gray'}>
123
+ {copied ? 'copied' : 'copy'}
124
+ </Button>
125
+ )}
126
+ </CopyButton>
127
+ </Group>
128
+ ))}
129
+ </Stack>
130
+ )}
131
+ </>
132
+ )}
133
+ </Stack>
134
+ </Paper>
135
+ </motion.div>
136
+ </AnimatePresence>
137
+ )
138
+ }
@@ -0,0 +1,46 @@
1
+ import { createVar, globalStyle, style } from '@vanilla-extract/css'
2
+ import { mantine } from '../mantine.css'
3
+
4
+ export const container = style({
5
+ position: 'absolute',
6
+ bottom: '0.5rem',
7
+ left: '50%',
8
+ transform: 'translateX(-50%)',
9
+ padding: 0,
10
+ margin: 0
11
+ })
12
+
13
+ export const buttons = style({
14
+ backdropFilter: 'blur(8px)',
15
+ transition: 'all 175ms ease-in',
16
+ ':hover': {
17
+ transitionTimingFunction: 'ease-out',
18
+ transform: 'scale(1.1)'
19
+ },
20
+ ':active': {
21
+ transitionDuration: '100ms',
22
+ transform: 'scale(0.98) translateY(3px)'
23
+ }
24
+ })
25
+
26
+ var transparency = createVar()
27
+ export const btn = style({
28
+ transition: 'all 175ms ease-in',
29
+ backgroundColor: 'var(--button-bg)',
30
+ vars: {
31
+ [transparency]: '35%',
32
+ ['--button-bg']: `color-mix(in srgb, ${mantine.colors.primaryColors.filled}, transparent ${transparency})`,
33
+ ['--button-hover']: `color-mix(in srgb, ${mantine.colors.primaryColors.filledHover}, transparent ${transparency})`
34
+ },
35
+ selectors: {
36
+ [mantine.lightSelector]: {
37
+ vars: {
38
+ [transparency]: '15%'
39
+ }
40
+ }
41
+ }
42
+ })
43
+
44
+ globalStyle(`${btn} .tabler-icon`, {
45
+ width: '80%'
46
+ })
@@ -0,0 +1,86 @@
1
+ import { ActionIcon, Box, Button, rem, useComputedColorScheme } from '@mantine/core'
2
+ import {
3
+ IconHeart,
4
+ IconPhoto,
5
+ IconPlayerPlayFilled,
6
+ IconPlayerSkipBackFilled,
7
+ IconPlayerSkipForwardFilled,
8
+ IconPlayerStopFilled,
9
+ IconSettings
10
+ } from '@tabler/icons-react'
11
+ import clsx from 'clsx'
12
+ import { isNullish, isNumber } from 'remeda'
13
+ import { useDiagramState } from '../state/useDiagramStore'
14
+ import * as css from './DynamicViewWalkthrough.css'
15
+
16
+ export function DynamicViewWalkthrough() {
17
+ const {
18
+ nextDynamicStep,
19
+ stopDynamicView,
20
+ activeDynamicViewStep,
21
+ hasNextSteps
22
+ } = useDiagramState(s => ({
23
+ nextDynamicStep: s.nextDynamicStep,
24
+ stopDynamicView: s.stopDynamicView,
25
+ activeDynamicViewStep: s.activeDynamicViewStep,
26
+ hasNextSteps: (s.activeDynamicViewStep ?? 1) < s.view.edges.length
27
+ }))
28
+
29
+ const buttonProps = {
30
+ className: css.btn,
31
+ size: 'compact-xl',
32
+ radius: 'xl'
33
+ }
34
+
35
+ const nextStep = (increment = 1) => (e: React.MouseEvent) => {
36
+ e.stopPropagation()
37
+ nextDynamicStep(increment)
38
+ }
39
+
40
+ return (
41
+ <Box
42
+ className={clsx('react-flow__panel', css.container)}
43
+ >
44
+ {isNullish(activeDynamicViewStep)
45
+ && (
46
+ <Button
47
+ {...buttonProps}
48
+ className={clsx(css.buttons, css.btn)}
49
+ rightSection={<IconPlayerPlayFilled />}
50
+ onClick={nextStep()}
51
+ px={'xl'}>
52
+ Start
53
+ </Button>
54
+ )}
55
+ {isNumber(activeDynamicViewStep) && (
56
+ <Button.Group className={css.buttons}>
57
+ <Button
58
+ {...buttonProps}
59
+ pl={'lg'}
60
+ disabled={activeDynamicViewStep === 1}
61
+ onClick={nextStep(-1)}>
62
+ <IconPlayerSkipBackFilled />
63
+ </Button>
64
+
65
+ <Button
66
+ {...buttonProps}
67
+ px={'sm'}
68
+ onClick={e => {
69
+ e.stopPropagation()
70
+ stopDynamicView()
71
+ }}>
72
+ <IconPlayerStopFilled />
73
+ </Button>
74
+
75
+ <Button
76
+ {...buttonProps}
77
+ pr={'lg'}
78
+ disabled={!hasNextSteps}
79
+ onClick={nextStep()}>
80
+ <IconPlayerSkipForwardFilled />
81
+ </Button>
82
+ </Button.Group>
83
+ )}
84
+ </Box>
85
+ )
86
+ }
@@ -3,11 +3,14 @@ import { useOnSelectionChange } from '@xyflow/react'
3
3
  import clsx from 'clsx'
4
4
  import { shallowEqual } from 'fast-equals'
5
5
  import { AnimatePresence, motion } from 'framer-motion'
6
- import { memo, useState } from 'react'
6
+ import { useState } from 'react'
7
+ import { isNullish, isTruthy } from 'remeda'
8
+ import { useDiagramState } from '../state'
7
9
  import { NodeOptions } from './options/NodeOptions'
8
10
  import * as styles from './OptionsPanel.css'
9
11
 
10
- const OptionsPanelMemo = /* @__PURE__ */ memo(function OptionsPanel() {
12
+ export default function OptionsPanel() {
13
+ const isFocusDisabled = useDiagramState(s => isNullish(s.focusedNodeId))
11
14
  const [selectedNodes, setSelectedNodes] = useState([] as string[])
12
15
 
13
16
  useOnSelectionChange({
@@ -16,30 +19,12 @@ const OptionsPanelMemo = /* @__PURE__ */ memo(function OptionsPanel() {
16
19
  setSelectedNodes(prev => {
17
20
  return shallowEqual(prev, next) ? prev : next
18
21
  })
19
- // if (nodes.length === 0 && edges.length === 0) {
20
- // setSelectedNodes([])
21
- // // setSelectedEdges([])
22
- // return
23
- // }
24
- // const selected = new Set([
25
- // ...nodes.map((n) => n.id),
26
- // ...edges.flatMap((edge) => [
27
- // edge.source,
28
- // edge.target
29
- // ])
30
- // ])
31
- // setSelectedNodes([...selected])
32
- // setSelectedEdges(edges.map(e => e.id))
33
22
  }
34
23
  })
35
24
 
36
- // useUpdateEffect(() => {
37
- // setSelectedNodes([])
38
- // }, [viewId])
39
-
40
25
  return (
41
26
  <AnimatePresence mode="wait">
42
- {selectedNodes.length > 0 && (
27
+ {isFocusDisabled && selectedNodes.length > 0 && (
43
28
  <motion.div
44
29
  initial={{ opacity: 0, scale: 0.85 }}
45
30
  animate={{ opacity: 1, scale: 1 }}
@@ -60,6 +45,4 @@ const OptionsPanelMemo = /* @__PURE__ */ memo(function OptionsPanel() {
60
45
  )}
61
46
  </AnimatePresence>
62
47
  )
63
- })
64
-
65
- export default OptionsPanelMemo
48
+ }
package/src/ui/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  // export { default as Camera } from './Camera'
2
+ export * from './DynamicViewWalkthrough'
2
3
  export { default as OptionsPanel } from './OptionsPanel'
@@ -1,8 +1,9 @@
1
1
  import {
2
+ type BorderStyle,
2
3
  defaultTheme,
3
4
  type ElementShape,
4
5
  ElementShapes,
5
- type Fqn,
6
+ invariant,
6
7
  type NonEmptyArray,
7
8
  type ThemeColor
8
9
  } from '@likec4/core'
@@ -13,13 +14,17 @@ import {
13
14
  Divider,
14
15
  Flex,
15
16
  rem,
17
+ SegmentedControl,
16
18
  Select,
19
+ Slider,
17
20
  Stack,
18
21
  Text,
19
22
  Tooltip,
20
23
  TooltipGroup
21
24
  } from '@mantine/core'
25
+ import { useEffect, useState } from 'react'
22
26
  import { hasAtLeast, keys, takeWhile } from 'remeda'
27
+ import type { Changes } from '../../LikeC4Diagram.props'
23
28
  import { useDiagramStoreApi } from '../../state'
24
29
  import { useXYNodesData } from '../../xyflow/hooks'
25
30
  import { XYFlowNode } from '../../xyflow/types'
@@ -52,15 +57,34 @@ export type ColorKey = typeof colors[0]['key']
52
57
  type XYNodesData = Pick<XYFlowNode, 'id' | 'data' | 'type'>
53
58
 
54
59
  export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] }) {
60
+ // export function NodeOptions(props: { nodes: XYFlowNode[] }) {
55
61
  const diagramApi = useDiagramStoreApi()
62
+ // const nodes = useXYNodesData(props.nodes.map(node => node.id))
56
63
  const nodes = useXYNodesData(selectedNodeIds)
57
64
  if (!hasAtLeast(nodes, 1)) {
58
65
  return null
59
66
  }
67
+ if (nodes.length !== selectedNodeIds.length) {
68
+ throw new Error('NodeOptions: nodes and props.nodes should have the same length')
69
+ }
60
70
  // Makes sense to show shape option only if there is at least one element node
61
71
  const showShapeOption = nodes.some(node => node.type === 'element')
62
72
 
63
73
  const [firstNode, ...rest] = nodes
74
+
75
+ // Makes sense to show opacity option only if there is at least one compound node
76
+ const showOpacityOption = rest.length === 0 && firstNode.type === 'compound'
77
+
78
+ const triggerChange = (style: Changes.ChangeElementStyle['style']) => {
79
+ const targets = nodes.map(node => node.data.element.id)
80
+ invariant(hasAtLeast(targets, 1), 'At least one target is required')
81
+ diagramApi.getState().triggerOnChange([{
82
+ op: 'change-element-style',
83
+ style,
84
+ targets
85
+ }])
86
+ }
87
+
64
88
  return (
65
89
  <Stack>
66
90
  <Box maw={200}>
@@ -72,38 +96,31 @@ export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] })
72
96
  <ShapeOption
73
97
  nodes={nodes}
74
98
  onShapeChange={(shape: ElementShape) => {
75
- const targets = [] as Fqn[]
76
- for (const nd of nodes) {
77
- if (nd.data.element.shape !== shape) {
78
- targets.push(nd.data.element.id)
79
- }
80
- }
81
- if (hasAtLeast(targets, 1)) {
82
- diagramApi.getState().triggerOnChange([{
83
- op: 'change-shape',
84
- shape,
85
- targets
86
- }])
87
- }
99
+ triggerChange({ shape })
88
100
  }} />
89
101
  )}
90
102
  <Colors
91
103
  nodes={nodes}
92
104
  onColorChange={(color: ColorKey | ThemeColorKey) => {
93
- const targets = [] as Fqn[]
94
- for (const nd of nodes) {
95
- if (nd.data.element.color !== color) {
96
- targets.push(nd.data.element.id)
97
- }
98
- }
99
- if (hasAtLeast(targets, 1)) {
100
- diagramApi.getState().triggerOnChange([{
101
- op: 'change-color',
102
- color,
103
- targets
104
- }])
105
- }
105
+ triggerChange({ color })
106
106
  }} />
107
+
108
+ {showOpacityOption && (
109
+ <Box key={firstNode.id}>
110
+ <Divider label="opacity and border" labelPosition="left" />
111
+ <OpacityOption
112
+ node={firstNode}
113
+ onOpacityChange={(opacity: number) => {
114
+ triggerChange({ opacity })
115
+ }} />
116
+ <BorderStyleOption
117
+ node={firstNode}
118
+ onChange={(border: BorderStyle) => {
119
+ triggerChange({ border })
120
+ }}
121
+ />
122
+ </Box>
123
+ )}
107
124
  <NavigateToOption nodes={nodes} />
108
125
  </Stack>
109
126
  )
@@ -251,3 +268,62 @@ function NavigateToOption({
251
268
  </Box>
252
269
  )
253
270
  }
271
+
272
+ function OpacityOption({
273
+ node,
274
+ onOpacityChange
275
+ }: {
276
+ node: XYNodesData
277
+ onOpacityChange: (opacity: number) => void
278
+ }) {
279
+ let selectedOpacity = node.data.element.style.opacity ?? 100
280
+ const [value, setValue] = useState(selectedOpacity)
281
+ useEffect(() => {
282
+ setValue(selectedOpacity)
283
+ }, [selectedOpacity])
284
+
285
+ return (
286
+ <Slider
287
+ mt={'xs'}
288
+ size={'sm'}
289
+ color={'dark'}
290
+ value={value}
291
+ onChange={setValue}
292
+ onChangeEnd={onOpacityChange} />
293
+ )
294
+ }
295
+
296
+ function BorderStyleOption({
297
+ node,
298
+ onChange
299
+ }: {
300
+ node: XYNodesData
301
+ onChange: (borderStyle: BorderStyle) => void
302
+ }) {
303
+ let selecteBorderStyle = node.data.element.style.border ?? 'dashed'
304
+ const [value, setValue] = useState(selecteBorderStyle)
305
+ useEffect(() => {
306
+ setValue(selecteBorderStyle)
307
+ }, [selecteBorderStyle])
308
+
309
+ return (
310
+ <Box mt={'md'}>
311
+ <SegmentedControl
312
+ size="xs"
313
+ fullWidth
314
+ withItemsBorders={false}
315
+ value={value}
316
+ onChange={v => {
317
+ setValue(v as BorderStyle)
318
+ onChange(v as BorderStyle)
319
+ }}
320
+ data={[
321
+ { label: 'Solid', value: 'solid' },
322
+ { label: 'Dashed', value: 'dashed' },
323
+ { label: 'Dotted', value: 'dotted' },
324
+ { label: 'None', value: 'none' }
325
+ ]}
326
+ />
327
+ </Box>
328
+ )
329
+ }