@likec4/diagram 1.13.0 → 1.15.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 (43) hide show
  1. package/package.json +9 -9
  2. package/src/LikeC4CustomColors.tsx +6 -3
  3. package/src/LikeC4Diagram.css.ts +4 -0
  4. package/src/LikeC4Diagram.tsx +9 -7
  5. package/src/overlays/Overlays.css.ts +124 -0
  6. package/src/overlays/Overlays.tsx +210 -0
  7. package/src/overlays/edge-details/EdgeDetailsXYFlow.tsx +247 -0
  8. package/src/overlays/edge-details/SelectEdge.css.ts +99 -0
  9. package/src/overlays/edge-details/SelectEdge.tsx +153 -0
  10. package/src/overlays/edge-details/_types.ts +38 -0
  11. package/src/overlays/edge-details/use-layouted-edge-details.ts +356 -0
  12. package/src/overlays/edge-details/xyflow/CompoundNode.tsx +80 -0
  13. package/src/overlays/edge-details/xyflow/ElementNode.tsx +154 -0
  14. package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +81 -0
  15. package/src/overlays/edge-details/xyflow/styles.css.ts +196 -0
  16. package/src/overlays/index.tsx +1 -33
  17. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +124 -63
  18. package/src/overlays/relationships-of/_types.ts +13 -2
  19. package/src/overlays/relationships-of/use-layouted-relationships.ts +37 -37
  20. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +2 -2
  21. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +47 -26
  22. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +58 -22
  23. package/src/overlays/relationships-of/xyflow/styles.css.ts +53 -16
  24. package/src/state/DiagramContext.tsx +23 -5
  25. package/src/state/diagram-to-xyflow.ts +31 -20
  26. package/src/state/diagramStore.ts +30 -23
  27. package/src/xyflow/XYFlow.tsx +2 -1
  28. package/src/xyflow/XYFlowEvents.tsx +13 -1
  29. package/src/xyflow/edges/RelationshipEdge.tsx +10 -4
  30. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +24 -21
  31. package/src/xyflow/nodes/compound/CompoundNode.css.ts +1 -0
  32. package/src/xyflow/nodes/compound/CompoundNode.tsx +11 -11
  33. package/src/xyflow/nodes/shared/Toolbar.tsx +1 -1
  34. package/src/xyflow/types.ts +1 -0
  35. package/src/xyflow/useLayoutConstraints.ts +38 -19
  36. package/src/components/Overlay.css.ts +0 -78
  37. package/src/components/Overlay.tsx +0 -102
  38. package/src/components/index.ts +0 -2
  39. package/src/overlays/edge-details/EdgeDetailsOverlay.css.ts +0 -13
  40. package/src/overlays/edge-details/EdgeDetailsOverlay.tsx +0 -37
  41. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +0 -33
  42. package/src/overlays/relationships-of/styles.css.ts +0 -34
  43. /package/src/{components → overlays}/OverlayContext.tsx +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@likec4/diagram",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "license": "MIT",
5
5
  "bugs": "https://github.com/likec4/likec4/issues",
6
6
  "homepage": "https://likec4.dev",
@@ -23,7 +23,7 @@
23
23
  "exports": {
24
24
  ".": "./src/index.ts"
25
25
  },
26
- "packageManager": "yarn@4.5.0",
26
+ "packageManager": "yarn@4.5.1",
27
27
  "scripts": {
28
28
  "typecheck": "tsc --noEmit",
29
29
  "lint": "run -T eslint src/ --fix",
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@dagrejs/dagre": "^1.1.4",
34
- "@likec4/core": "1.13.0",
35
- "@mantine/vanilla-extract": "7.13.2",
34
+ "@likec4/core": "1.15.0",
35
+ "@mantine/vanilla-extract": "7.13.4",
36
36
  "@react-hookz/web": "^24.0.4",
37
37
  "@tabler/icons-react": "3.17.0",
38
38
  "@vanilla-extract/css": "^1.16.0",
@@ -45,9 +45,9 @@
45
45
  "d3-shape": "^3.2.0",
46
46
  "esm-env": "^1.0.0",
47
47
  "fast-equals": "^5.0.1",
48
- "framer-motion": "^11.11.1",
48
+ "framer-motion": "^11.11.9",
49
49
  "khroma": "^2.1.0",
50
- "remeda": "^2.14.0",
50
+ "remeda": "^2.15.0",
51
51
  "type-fest": "4.26.1",
52
52
  "zustand": "^4.5.5"
53
53
  },
@@ -58,9 +58,9 @@
58
58
  "react-dom": "^18.3.1"
59
59
  },
60
60
  "devDependencies": {
61
- "@likec4/tsconfig": "1.13.0",
62
- "@mantine/core": "7.13.2",
63
- "@mantine/hooks": "7.13.2",
61
+ "@likec4/tsconfig": "1.15.0",
62
+ "@mantine/core": "7.13.4",
63
+ "@mantine/hooks": "7.13.4",
64
64
  "@types/bezier-js": "^4.1.3",
65
65
  "@types/d3-shape": "^3.1.6",
66
66
  "@types/react": "^18.3.11",
@@ -1,12 +1,14 @@
1
1
  import { type ThemeColorValues } from '@likec4/core'
2
2
  import { useMantineStyleNonce } from '@mantine/core'
3
+ import { deepEqual } from 'fast-equals'
4
+ import { memo } from 'react'
3
5
  import { entries } from 'remeda'
4
6
  import type { LikeC4CustomColorsProperties } from './LikeC4CustomColors.props'
5
7
  import { vars } from './theme-vars'
6
8
 
7
9
  type CSSVarFunction = `var(--${string})` | `var(--${string}, ${string | number} )`
8
10
 
9
- export function LikeC4CustomColors({ customColors }: LikeC4CustomColorsProperties) {
11
+ export const LikeC4CustomColors = memo<LikeC4CustomColorsProperties>(({ customColors }) => {
10
12
  function toStyle(name: String, colorValues: ThemeColorValues): String {
11
13
  const rules = new Array<String>(
12
14
  ...entries(colorValues.elements).map(([key, value]) => `${stripCssVarReference(vars.element[key])}: ${value};`),
@@ -34,7 +36,8 @@ export function LikeC4CustomColors({ customColors }: LikeC4CustomColorsPropertie
34
36
 
35
37
  return (
36
38
  <>
37
- <style type="text/css" dangerouslySetInnerHTML={{ __html: styles }} {...(nonce && { nonce })} />
39
+ <style type="text/css" dangerouslySetInnerHTML={{ __html: styles }} nonce={nonce} />
38
40
  </>
39
41
  )
40
- }
42
+ }, deepEqual)
43
+ LikeC4CustomColors.displayName = 'LikeC4CustomColors'
@@ -13,6 +13,10 @@ export const cssReactFlow = style({
13
13
  }
14
14
  })
15
15
 
16
+ export const notInitialized = style({
17
+ opacity: 0
18
+ })
19
+
16
20
  globalStyle(`.react-flow${cssReactFlow}`, {
17
21
  vars: {
18
22
  [xyvars.background.color]: vars.likec4.background.color,
@@ -67,10 +67,10 @@ export function LikeC4Diagram({
67
67
  initialWidth: initialWidth ?? view.bounds.width,
68
68
  initialHeight: initialHeight ?? view.bounds.height
69
69
  })
70
- if (readonly !== true && !!where) {
71
- console.warn('where filter is only supported in readonly mode')
72
- }
73
70
  useEffect(() => {
71
+ if (readonly !== true && where != null) {
72
+ console.warn('Ignore filter, supported in readonly mode only\n', { where })
73
+ }
74
74
  if (hasLikec4model) {
75
75
  return
76
76
  }
@@ -80,7 +80,7 @@ export function LikeC4Diagram({
80
80
  if (enableRelationshipsBrowser) {
81
81
  console.warn('Invalid enableRelationshipsBrowser=true, requires LikeC4ModelProvider')
82
82
  }
83
- }, [])
83
+ })
84
84
 
85
85
  return (
86
86
  <EnsureMantine>
@@ -111,7 +111,8 @@ export function LikeC4Diagram({
111
111
  enableDynamicViewWalkthrough={enableDynamicViewWalkthrough}
112
112
  enableFocusMode={enableFocusMode}
113
113
  enableRelationshipsBrowser={enableRelationshipsBrowser && hasLikec4model}
114
- whereFilter={readonly !== true ? (where ?? null) : null}
114
+ // Apply where filter only in readonly mode
115
+ whereFilter={readonly ? (where ?? null) : null}
115
116
  renderIcon={renderIcon ?? null}
116
117
  onCanvasClick={onCanvasClick ?? null}
117
118
  onCanvasContextMenu={onCanvasContextMenu ?? null}
@@ -158,7 +159,7 @@ const LikeC4DiagramInnerMemo = /* @__PURE__ */ memo<LikeC4DiagramInnerProps>(fun
158
159
  pannable: s.pannable,
159
160
  fitView: s.fitViewEnabled,
160
161
  enableFocusMode: s.enableFocusMode,
161
- enableOverlays: s.enableRelationshipsBrowser
162
+ enableOverlays: s.hasLikeC4Model && (s.enableRelationshipsBrowser || s.showRelationshipDetails)
162
163
  }))
163
164
 
164
165
  return (
@@ -169,7 +170,8 @@ const LikeC4DiagramInnerMemo = /* @__PURE__ */ memo<LikeC4DiagramInnerProps>(fun
169
170
  css.cssReactFlow,
170
171
  css.cssNoControls,
171
172
  pannable !== true && css.cssDisablePan,
172
- background === 'transparent' && css.cssTransparentBg
173
+ background === 'transparent' && css.cssTransparentBg,
174
+ isInitialized ? 'initialized' : css.notInitialized
173
175
  )}
174
176
  >
175
177
  <XYFlowInner
@@ -0,0 +1,124 @@
1
+ import { createVar, globalStyle, keyframes, style } from '@vanilla-extract/css'
2
+ import { mantine, vars, whereDark, xyvars } from '../theme-vars'
3
+
4
+ const mixColor = createVar('mix-color')
5
+ export const container = style({
6
+ position: 'absolute',
7
+ top: 0,
8
+ left: 0,
9
+ padding: 0,
10
+ margin: 0,
11
+ boxSizing: 'border-box',
12
+ border: '0 solid transparent',
13
+ width: '100%',
14
+ height: '100%',
15
+ zIndex: 1000,
16
+ isolation: 'isolate',
17
+ WebkitBackdropFilter: 'blur(var(--backdrop-blur))',
18
+ backdropFilter: 'blur(var(--backdrop-blur))',
19
+ backgroundColor: `color-mix(in srgb, ${mantine.colors.body}, transparent var(--backdrop-opacity))`,
20
+ vars: {
21
+ '--backdrop-blur': '3px',
22
+ '--backdrop-opacity': '50%',
23
+ [mixColor]: `black`,
24
+ [xyvars.edge.stroke]: vars.relation.lineColor,
25
+ [xyvars.edge.strokeSelected]: `color-mix(in srgb, ${vars.relation.lineColor}, ${mixColor} 35%)`,
26
+ [xyvars.edge.labelColor]: `color-mix(in srgb, ${vars.relation.labelColor}, rgba(255 255 255 / 0.85) 50%)`,
27
+ [xyvars.edge.labelBgColor]: `color-mix(in srgb, ${vars.relation.labelBgColor}, transparent 30%)`
28
+ },
29
+ selectors: {
30
+ [`${whereDark} &`]: {
31
+ vars: {
32
+ [mixColor]: `white`,
33
+ [xyvars.edge.labelColor]: vars.relation.labelColor,
34
+ [xyvars.edge.labelBgColor]: `color-mix(in srgb, ${vars.relation.labelBgColor}, transparent 50%)`
35
+ }
36
+ }
37
+ }
38
+ })
39
+
40
+ globalStyle(`:where(${container}) .react-flow__attribution`, {
41
+ display: 'none'
42
+ })
43
+ globalStyle(`:where(${container}) .react-flow__edge-text`, {
44
+ fontSize: 14
45
+ })
46
+ globalStyle(`:where(${container}) .react-flow__node-compound`, {
47
+ mixBlendMode: 'hard-light'
48
+ })
49
+
50
+ // const dialogFadeIn = keyframes({
51
+ // '0%': {
52
+ // opacity: 0,
53
+ // display: 'none'
54
+ // },
55
+ // '100%': {
56
+ // opacity: 1,
57
+ // display: 'block'
58
+ // }
59
+ // })
60
+
61
+ // const dialogFadeOut = keyframes({
62
+ // '0%': {
63
+ // opacity: 1,
64
+ // transform: 'scale(1)',
65
+ // display: 'block'
66
+ // },
67
+ // '100%': {
68
+ // opacity: 0,
69
+ // transform: 'scale(0.98)',
70
+ // display: 'none'
71
+ // }
72
+ // })
73
+
74
+ // const backdropRgb = createVar('backdrop-rgb')
75
+
76
+ // const dialogBackdropFadeIn = keyframes({
77
+ // '0%': {
78
+ // WebkitBackdropFilter: 'blur(1px)',
79
+ // backdropFilter: 'blur(1px)',
80
+ // backgroundColor: `rgb(${backdropRgb} / 30%)`
81
+ // },
82
+ // '100%': {
83
+ // WebkitBackdropFilter: 'blur(8px)',
84
+ // backdropFilter: 'blur(8px)',
85
+ // backgroundColor: `rgb(${backdropRgb} / 85%)`
86
+ // }
87
+ // })
88
+
89
+ // export const dialog = style({
90
+ // top: 0,
91
+ // left: 0,
92
+ // padding: 0,
93
+ // margin: 0,
94
+ // boxSizing: 'border-box',
95
+ // border: '0 solid transparent',
96
+ // width: '100%',
97
+ // maxWidth: '100dvw',
98
+ // height: '100%',
99
+ // maxHeight: '100dvh',
100
+ // background: 'transparent',
101
+ // animation: `${dialogFadeOut} 130ms cubic-bezier(0.50, 0, 1, 1)`,
102
+ // transformOrigin: '50% 20%',
103
+ // vars: {
104
+ // [backdropRgb]: '255 255 255'
105
+ // },
106
+ // selectors: {
107
+ // [`${whereDark} &`]: {
108
+ // vars: {
109
+ // [backdropRgb]: '36 36 36'
110
+ // }
111
+ // },
112
+ // '&[open]': {
113
+ // animation: `${dialogFadeIn} 200ms cubic-bezier(0, 0, 0.40, 1) forwards`
114
+ // },
115
+ // '&::backdrop': {
116
+ // WebkitBackdropFilter: 'blur(1px)',
117
+ // backdropFilter: 'blur(1px)',
118
+ // backgroundColor: `rgb(${backdropRgb} / 30%)`
119
+ // },
120
+ // '&[open]::backdrop': {
121
+ // animation: `${dialogBackdropFadeIn} 350ms cubic-bezier(0, 0, 0.40, 1) forwards`
122
+ // }
123
+ // }
124
+ // })
@@ -0,0 +1,210 @@
1
+ import { ActionIcon, Box, FocusTrap } from '@mantine/core'
2
+ import { useHotkeys } from '@mantine/hooks'
3
+ import { IconX } from '@tabler/icons-react'
4
+ import { ReactFlowProvider as XYFlowProvider } from '@xyflow/react'
5
+ import { AnimatePresence, m } from 'framer-motion'
6
+ import { memo, useMemo } from 'react'
7
+ import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../hooks/useDiagramState'
8
+ import { EdgeDetailsXYFlow } from './edge-details/EdgeDetailsXYFlow'
9
+ import { OverlayContext } from './OverlayContext'
10
+ import * as css from './Overlays.css'
11
+ import { RelationshipsXYFlow } from './relationships-of/RelationshipsXYFlow'
12
+ export const Overlays = memo(() => {
13
+ const diagramStore = useDiagramStoreApi()
14
+ const activeOverlay = useDiagramState(s => s.activeOverlay)
15
+
16
+ const ctxValue = useMemo(() => ({
17
+ openOverlay: ((overlay) => {
18
+ diagramStore.getState().openOverlay(overlay)
19
+ }) as DiagramState['openOverlay'],
20
+ close: () => {
21
+ diagramStore.getState().closeOverlay()
22
+ }
23
+ }), [diagramStore])
24
+
25
+ const isActive = !!activeOverlay
26
+ useHotkeys(
27
+ isActive
28
+ ? [
29
+ ['Escape', (e) => {
30
+ e.stopImmediatePropagation()
31
+ diagramStore.getState().closeOverlay()
32
+ }, { preventDefault: true }]
33
+ ]
34
+ : []
35
+ )
36
+ return (
37
+ <OverlayContext.Provider value={ctxValue}>
38
+ <AnimatePresence>
39
+ {activeOverlay && (
40
+ <Box
41
+ component={m.div}
42
+ className={css.container}
43
+ data-likec4-color="gray"
44
+ initial={{
45
+ '--backdrop-blur': '0px',
46
+ '--backdrop-opacity': '60%',
47
+ opacity: 0,
48
+ translateY: -15
49
+ }}
50
+ animate={{
51
+ '--backdrop-blur': '10px',
52
+ '--backdrop-opacity': '25%',
53
+ opacity: 1,
54
+ translateY: 0
55
+ }}
56
+ exit={{
57
+ '--backdrop-blur': '1px',
58
+ '--backdrop-opacity': '90%',
59
+ translateY: -5,
60
+ opacity: 0
61
+ }}
62
+ >
63
+ <FocusTrap>
64
+ {activeOverlay.relationshipsOf && (
65
+ <XYFlowProvider
66
+ defaultNodes={[]}
67
+ defaultEdges={[]}>
68
+ <RelationshipsXYFlow subjectId={activeOverlay.relationshipsOf} />
69
+ </XYFlowProvider>
70
+ )}
71
+ {activeOverlay.edgeDetails && (
72
+ <XYFlowProvider
73
+ defaultNodes={[]}
74
+ defaultEdges={[]}>
75
+ <EdgeDetailsXYFlow edgeId={activeOverlay.edgeDetails} />
76
+ </XYFlowProvider>
77
+ )}
78
+ <Box pos={'absolute'} top={'1rem'} right={'1rem'}>
79
+ <ActionIcon
80
+ variant="default"
81
+ // color="gray"
82
+ size={'lg'}
83
+ data-autofocus
84
+ autoFocus
85
+ onClick={(e) => {
86
+ e.stopPropagation()
87
+ ctxValue.close()
88
+ }}>
89
+ <IconX />
90
+ </ActionIcon>
91
+ </Box>
92
+ </FocusTrap>
93
+ </Box>
94
+ )}
95
+ </AnimatePresence>
96
+ </OverlayContext.Provider>
97
+ )
98
+ })
99
+
100
+ // const OverlayDialogCloseButton = () => {
101
+ // const { close } = useOverlayDialog()
102
+ // return (
103
+ // <Box pos={'absolute'} top={'1rem'} right={'1rem'}>
104
+ // <ActionIcon
105
+ // variant="default"
106
+ // // color="gray"
107
+ // size={'lg'}
108
+ // autoFocus
109
+ // onClick={(e) => {
110
+ // e.stopPropagation()
111
+ // close()
112
+ // }}>
113
+ // <IconX />
114
+ // </ActionIcon>
115
+ // </Box>
116
+ // )
117
+ // }
118
+
119
+ // type OverlayDialogProps = Pick<HTMLAttributes<HTMLDialogElement>, 'style' | 'className'> & {
120
+ // onClose?: (() => void) | undefined
121
+ // children: (renderProps: {
122
+ // opened: boolean
123
+ // close: () => void
124
+ // }) => ReactNode
125
+ // }
126
+
127
+ // const OverlayDialog = forwardRef<HTMLDialogElement, OverlayDialogProps>(({
128
+ // className,
129
+ // children,
130
+ // onClose,
131
+ // ...props
132
+ // }, forwardedRef) => {
133
+ // const onCloseRef = useSyncedRef(onClose)
134
+ // const api = useDiagramStoreApi()
135
+ // const [opened, setOpened] = useState(false)
136
+ // const dialogRef = useRef<HTMLDialogElement>(null)
137
+ // const ref = useMergedRef(dialogRef, forwardedRef)
138
+
139
+ // // useDebouncedEffect(
140
+ // // () => {
141
+ // // dialogRef.current?.showModal()
142
+ // // },
143
+ // // [],
144
+ // // 30
145
+ // // )
146
+
147
+ // useDebouncedEffect(
148
+ // () => {
149
+ // setOpened(true)
150
+ // },
151
+ // [],
152
+ // 80
153
+ // )
154
+
155
+ // const { start: triggerOnClose } = useTimeout(() => {
156
+ // onCloseRef.current?.()
157
+ // }, 300)
158
+
159
+ // const ctxValue = useMemo(() => ({
160
+ // openOverlay: api.getState().openOverlay,
161
+ // close: () => {
162
+ // dialogRef.current?.close()
163
+ // }
164
+ // }), [api])
165
+
166
+ // return (
167
+ // <OverlayContext.Provider value={ctxValue}>
168
+ // <div
169
+ // aria-modal="true"
170
+ // ref={ref}
171
+ // className={clsx(css.dialog, className)}
172
+ // onClick={e => {
173
+ // if ((e.target as any)?.nodeName?.toUpperCase() === 'DIALOG') {
174
+ // e.stopPropagation()
175
+ // dialogRef.current?.close()
176
+ // }
177
+ // }}
178
+ // onClose={e => {
179
+ // e.stopPropagation()
180
+ // triggerOnClose()
181
+ // }}
182
+ // {...props}
183
+ // >
184
+ // {children({
185
+ // opened,
186
+ // ...ctxValue
187
+ // })}
188
+ // </div>
189
+ // </OverlayContext.Provider>
190
+ // )
191
+ // })
192
+
193
+ // const OverlayDialogCloseButton = () => {
194
+ // const { close } = useOverlayDialog()
195
+ // return (
196
+ // <Box pos={'absolute'} top={'1rem'} right={'1rem'}>
197
+ // <ActionIcon
198
+ // variant="default"
199
+ // // color="gray"
200
+ // size={'lg'}
201
+ // autoFocus
202
+ // onClick={(e) => {
203
+ // e.stopPropagation()
204
+ // close()
205
+ // }}>
206
+ // <IconX />
207
+ // </ActionIcon>
208
+ // </Box>
209
+ // )
210
+ // }