@likec4/diagram 1.16.0 → 1.17.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 (47) hide show
  1. package/package.json +7 -7
  2. package/src/LikeC4CustomColors.tsx +24 -21
  3. package/src/LikeC4Diagram.props.ts +9 -9
  4. package/src/LikeC4Diagram.tsx +25 -26
  5. package/src/StaticLikeC4Diagram.tsx +9 -5
  6. package/src/overlays/OverlayContext.tsx +1 -1
  7. package/src/overlays/Overlays.css.ts +28 -23
  8. package/src/overlays/Overlays.tsx +116 -64
  9. package/src/overlays/edge-details/xyflow/ElementNode.tsx +15 -12
  10. package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +7 -3
  11. package/src/overlays/edge-details/xyflow/styles.css.ts +22 -16
  12. package/src/overlays/element-details/ElementDetailsCard.css.ts +322 -0
  13. package/src/overlays/element-details/ElementDetailsCard.tsx +639 -0
  14. package/src/overlays/element-details/TabPanelRelationships.css.ts +106 -0
  15. package/src/overlays/element-details/TabPanelRelationships.tsx +201 -0
  16. package/src/overlays/element-details/TabPanelStructure.css.ts +36 -0
  17. package/src/overlays/element-details/TabPanelStructure.tsx +120 -0
  18. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +185 -0
  19. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +179 -213
  20. package/src/overlays/relationships-of/_types.ts +15 -4
  21. package/src/overlays/relationships-of/use-layouted-relationships.ts +301 -207
  22. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +68 -14
  23. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +54 -14
  24. package/src/overlays/relationships-of/xyflow/EmptyNode.tsx +1 -1
  25. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +51 -23
  26. package/src/overlays/relationships-of/xyflow/styles.css.ts +34 -27
  27. package/src/state/DiagramContext.tsx +15 -8
  28. package/src/state/diagramStore.ts +14 -6
  29. package/src/theme-vars.ts +9 -0
  30. package/src/ui/FramerMotionConfig.tsx +18 -0
  31. package/src/ui/notation/NotationPanel.tsx +3 -3
  32. package/src/xyflow/XYFlow.tsx +18 -35
  33. package/src/xyflow/XYFlowInner.tsx +2 -2
  34. package/src/xyflow/edges/EdgeLabel.tsx +3 -3
  35. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +25 -18
  36. package/src/xyflow/nodes/compound/CompoundNode.css.ts +96 -30
  37. package/src/xyflow/nodes/compound/CompoundNode.tsx +149 -165
  38. package/src/xyflow/nodes/element/ElementDetails.css.ts +101 -0
  39. package/src/xyflow/nodes/element/ElementDetails.tsx +393 -0
  40. package/src/xyflow/nodes/element/ElementNode.tsx +110 -116
  41. package/src/xyflow/nodes/element/element.css.ts +38 -18
  42. package/src/xyflow/nodes/shared/ElementIcon.tsx +43 -0
  43. package/src/xyflow/nodes/shared/Toolbar.tsx +6 -6
  44. package/src/xyflow/{XYFlowEvents.tsx → useXYFlowEvents.ts} +26 -22
  45. package/src/xyflow/utils.ts +12 -3
  46. package/src/LikeC4CustomColors.props.ts +0 -5
  47. /package/src/{mantine → ui}/EnsureMantine.tsx +0 -0
@@ -22,6 +22,7 @@ import clsx from 'clsx'
22
22
  import { AnimatePresence, m } from 'framer-motion'
23
23
  import { useState } from 'react'
24
24
  import { ceil, isNonNullish, isNullish } from 'remeda'
25
+ import { useXYStore } from '../../hooks'
25
26
  import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../../hooks/useDiagramState'
26
27
  import { useMantinePortalProps } from '../../hooks/useMantinePortalProps'
27
28
  import { vars } from '../../theme-vars'
@@ -120,16 +121,15 @@ const ElementNotation = ({ value }: { value: ElementNotationData }) => {
120
121
  const selector = (s: DiagramState) => ({
121
122
  id: s.view.id,
122
123
  notations: s.view.notation?.elements ?? [],
123
- height: s.xystore.getState().height,
124
124
  isVisible: isNullish(s.focusedNodeId ?? s.activeWalkthrough)
125
125
  })
126
126
 
127
127
  export function NotationPanel() {
128
+ const height = useXYStore(s => s.height)
128
129
  const {
129
130
  id,
130
131
  notations,
131
- isVisible,
132
- height
132
+ isVisible
133
133
  } = useDiagramState(selector)
134
134
  const [isCollapsed, setCollapsed] = useLocalStorage({
135
135
  key: 'notation-webview-collapsed',
@@ -10,7 +10,7 @@ import { CompoundNode } from './nodes/compound'
10
10
  import { ElementNode } from './nodes/element'
11
11
  import { XYFlowEdge, XYFlowNode } from './types'
12
12
  import { useLayoutConstraints } from './useLayoutConstraints'
13
- import { useXYFlowEvents } from './XYFlowEvents'
13
+ import { useXYFlowEvents } from './useXYFlowEvents'
14
14
 
15
15
  const nodeTypes = {
16
16
  element: ElementNode,
@@ -87,7 +87,12 @@ export function XYFlow({
87
87
  } = useDiagramState(selector, shallowEqual)
88
88
  // const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState(zoomable)
89
89
  const layoutConstraints = useLayoutConstraints()
90
- const handlers = useXYFlowEvents()
90
+ const {
91
+ onNodeContextMenu,
92
+ onPaneContextMenu,
93
+ onEdgeContextMenu,
94
+ ...handlers
95
+ } = useXYFlowEvents()
91
96
 
92
97
  /**
93
98
  * WORKAROUND - Called on viewport change
@@ -118,6 +123,9 @@ export function XYFlow({
118
123
  {...colorMode && { colorMode }}
119
124
  nodes={nodes}
120
125
  edges={edges}
126
+ onNodesChange={onNodesChange}
127
+ onEdgesChange={onEdgesChange}
128
+ onInit={onInit}
121
129
  nodeTypes={nodeTypes}
122
130
  edgeTypes={edgeTypes}
123
131
  zoomOnPinch={zoomable}
@@ -130,12 +138,12 @@ export function XYFlow({
130
138
  minZoom={zoomable ? MinZoom : 1}
131
139
  // Fitview is handled by store in onInit
132
140
  fitView={false}
133
- fitViewOptions={useMemo(() => ({
141
+ fitViewOptions={{
134
142
  minZoom: MinZoom,
135
143
  maxZoom: 1,
136
144
  padding: fitViewPadding,
137
145
  includeHiddenNodes: true
138
- }), [fitViewPadding])}
146
+ }}
139
147
  {...(!fitView && {
140
148
  viewport: {
141
149
  x: translateX,
@@ -156,41 +164,16 @@ export function XYFlow({
156
164
  nodesFocusable={nodesDraggable || nodesSelectable}
157
165
  edgesFocusable={false}
158
166
  nodesDraggable={nodesDraggable}
159
- {...nodesDraggable && {
160
- onNodeDragStart: layoutConstraints.onNodeDragStart,
161
- onNodeDrag: layoutConstraints.onNodeDrag,
162
- onNodeDragStop: layoutConstraints.onNodeDragStop
163
- }}
167
+ {...nodesDraggable && layoutConstraints}
164
168
  nodeDragThreshold={4}
165
169
  elevateNodesOnSelect={false} // or edges are not visible after select\
166
170
  selectNodesOnDrag={false}
167
- onNodesChange={onNodesChange}
168
- onEdgesChange={onEdgesChange}
169
- onDoubleClick={handlers.onDoubleClick}
170
- onPaneClick={handlers.onPaneClick}
171
- onMoveEnd={handlers.onMoveEnd}
172
- onNodeMouseEnter={handlers.onNodeMouseEnter}
173
- onNodeMouseLeave={handlers.onNodeMouseLeave}
174
- onEdgeMouseEnter={handlers.onEdgeMouseEnter}
175
- onEdgeMouseLeave={handlers.onEdgeMouseLeave}
176
- onNodeClick={handlers.onNodeClick}
177
- onNodeDoubleClick={handlers.onNodeDoubleClick}
178
- onEdgeClick={handlers.onEdgeClick}
179
- onEdgeDoubleClick={handlers.onEdgeDoubleClick}
180
- onInit={onInit}
181
- {...(hasOnNodeContextMenu && {
182
- onNodeContextMenu: handlers.onNodeContextMenu
183
- })}
184
- {...(hasOnEdgeContextMenu && {
185
- onEdgeContextMenu: handlers.onEdgeContextMenu
186
- })}
187
- {...(hasOnCanvasContextMenu && {
188
- onPaneContextMenu: handlers.onPaneContextMenu
189
- })}>
171
+ {...hasOnNodeContextMenu && { onNodeContextMenu }}
172
+ {...hasOnCanvasContextMenu && { onPaneContextMenu }}
173
+ {...hasOnEdgeContextMenu && { onEdgeContextMenu }}
174
+ {...handlers}
175
+ >
190
176
  {children}
191
177
  </ReactFlow>
192
178
  )
193
179
  }
194
- XYFlow.displayName = 'XYFlow'
195
-
196
- // export const XYFlow = memo(XYFlowWrapper) as typeof XYFlowWrapper
@@ -1,4 +1,4 @@
1
- import { shallowEqual } from 'fast-equals'
1
+ import { deepEqual, shallowEqual } from 'fast-equals'
2
2
  import { memo } from 'react'
3
3
  import { isNonNullish } from 'remeda'
4
4
  import { useDiagramState } from '../hooks/useDiagramState'
@@ -44,5 +44,5 @@ export const XYFlowInner = /* @__PURE__ */ memo(function XYFlowInnerR({
44
44
  {showNotations && <NotationPanel />}
45
45
  </>
46
46
  )
47
- }, shallowEqual)
47
+ }, deepEqual)
48
48
  XYFlowInner.displayName = 'XYFlowInner'
@@ -41,11 +41,11 @@ export const EdgeLabel = ({
41
41
  }: EdgeLabelProps) => {
42
42
  const likec4model = useLikeC4Model()
43
43
  const {
44
- showRelationshipDetails,
44
+ enableRelationshipDetails,
45
45
  isActiveWalkthroughStep,
46
46
  hasOnNavigateTo
47
47
  } = useDiagramState(s => ({
48
- showRelationshipDetails: s.showRelationshipDetails,
48
+ enableRelationshipDetails: s.enableRelationshipDetails,
49
49
  isActiveWalkthroughStep: s.activeWalkthrough?.stepId === edge.id,
50
50
  hasOnNavigateTo: !!s.onNavigateTo
51
51
  }))
@@ -57,7 +57,7 @@ export const EdgeLabel = ({
57
57
  if (isTruthy(notes)) {
58
58
  return <NotePopover notes={notes}>{node}</NotePopover>
59
59
  }
60
- if (showRelationshipDetails && likec4model && edge.relations.length > 0) {
60
+ if (enableRelationshipDetails && likec4model && edge.relations.length > 0) {
61
61
  return (
62
62
  <RelationshipsDropdownMenu
63
63
  disabled={isDimmed}
@@ -19,7 +19,7 @@ import {
19
19
  import { IconArrowRight, IconArrowsMaximize, IconFileSymlink, IconInfoCircle, IconZoomScan } from '@tabler/icons-react'
20
20
  import clsx from 'clsx'
21
21
  import { forwardRef, Fragment, type MouseEventHandler, type PropsWithChildren, useCallback } from 'react'
22
- import { filter, isTruthy, map, partition, pipe } from 'remeda'
22
+ import { filter, isTruthy, map, partition, pick, pipe } from 'remeda'
23
23
  import { useDiagramState, useDiagramStoreApi, useMantinePortalProps, useXYNodesData } from '../../hooks'
24
24
  import type { RelationshipData } from '../types'
25
25
  import * as css from './RelationshipsDropdownMenu.css'
@@ -47,7 +47,10 @@ export function RelationshipsDropdownMenu({
47
47
  disabled?: boolean | undefined
48
48
  likec4model: LikeC4Model
49
49
  }>) {
50
- const openOverlay = useDiagramState(s => s.openOverlay)
50
+ const {
51
+ openOverlay,
52
+ enableRelationshipBrowser
53
+ } = useDiagramState(pick(['openOverlay', 'enableRelationshipBrowser']))
51
54
  const portalProps = useMantinePortalProps()
52
55
  const [sourceXYNode, targetXYNode] = useXYNodesData([edge.source, edge.target])
53
56
 
@@ -72,6 +75,15 @@ export function RelationshipsDropdownMenu({
72
75
  partition(r => r.relationship.source === edge.source && r.relationship.target === edge.target)
73
76
  )
74
77
 
78
+ const onClickOpenOverlay = useCallback((e: React.MouseEvent): void => {
79
+ e.stopPropagation()
80
+ if (enableRelationshipBrowser) {
81
+ openOverlay({
82
+ edgeDetails: edge.id
83
+ })
84
+ }
85
+ }, [edge.id, openOverlay, enableRelationshipBrowser])
86
+
75
87
  const renderRelationship = (relationship: LikeC4Model.ViewModel.Relationship, index: number) => (
76
88
  <Fragment key={relationship.id}>
77
89
  {index > 0 && <MenuDivider opacity={0.65} />}
@@ -85,13 +97,6 @@ export function RelationshipsDropdownMenu({
85
97
  </Fragment>
86
98
  )
87
99
 
88
- const onClickOpenOverlay = useCallback((e: React.MouseEvent): void => {
89
- e.stopPropagation()
90
- openOverlay({
91
- edgeDetails: edge.id
92
- })
93
- }, [edge.id, openOverlay])
94
-
95
100
  if (direct.length + nested.length === 0) {
96
101
  return <>{children}</>
97
102
  }
@@ -131,15 +136,17 @@ export function RelationshipsDropdownMenu({
131
136
  {nested.map(renderRelationship)}
132
137
  </>
133
138
  )}
134
- <Box pos={'absolute'} top={5} right={6}>
135
- <ActionIcon
136
- size={24}
137
- variant="subtle"
138
- onClick={onClickOpenOverlay}
139
- >
140
- <IconInfoCircle style={{ width: '70%' }} />
141
- </ActionIcon>
142
- </Box>
139
+ {enableRelationshipBrowser && (
140
+ <Box pos={'absolute'} top={5} right={6}>
141
+ <ActionIcon
142
+ size={24}
143
+ variant="subtle"
144
+ onClick={onClickOpenOverlay}
145
+ >
146
+ <IconInfoCircle style={{ width: '70%' }} />
147
+ </ActionIcon>
148
+ </Box>
149
+ )}
143
150
  </MenuDropdown>
144
151
  </Menu>
145
152
  )
@@ -1,14 +1,26 @@
1
1
  import { rem } from '@mantine/core'
2
2
  import { createVar, fallbackVar, globalStyle, keyframes, style } from '@vanilla-extract/css'
3
3
  import { calc } from '@vanilla-extract/css-utils'
4
- import { mantine, vars } from '../../../theme-vars'
4
+ import { mantine, vars, whereLight } from '../../../theme-vars'
5
5
 
6
+ // For framer motion
7
+ export const containerForFramer = style({
8
+ position: 'relative',
9
+ width: '100%',
10
+ height: '100%'
11
+ // transformOrigin: 'center center'
12
+ })
13
+
14
+ const iconSize = createVar('icon-size')
6
15
  export const container = style({
7
16
  width: '100%',
8
17
  height: '100%',
9
18
  position: 'relative',
10
19
  padding: 0,
11
20
  margin: 0,
21
+ vars: {
22
+ [iconSize]: '18px'
23
+ },
12
24
  ':before': {
13
25
  content: ' ',
14
26
  position: 'absolute',
@@ -56,7 +68,7 @@ export const compoundBody = style({
56
68
  margin: 0,
57
69
  transition: 'all 200ms ease-out',
58
70
  backgroundClip: 'padding-box',
59
- overflow: 'hidden',
71
+ // overflow: 'hidden',
60
72
  cursor: 'default',
61
73
  selectors: {
62
74
  [`:where(.react-flow__node.selected) &`]: {
@@ -80,6 +92,25 @@ export const compoundBody = style({
80
92
  background: vars.element.fill
81
93
  }
82
94
  })
95
+ export const compoundTitle = style({
96
+ display: 'flex',
97
+ alignItems: 'center',
98
+ gap: 6,
99
+ paddingLeft: 14,
100
+ paddingTop: 9,
101
+ minHeight: 30,
102
+ selectors: {
103
+ [`:where(.react-flow__node.draggable) &`]: {
104
+ cursor: 'grab'
105
+ },
106
+ [`:where(.likec4-compound-transparent) &`]: {
107
+ paddingTop: 4
108
+ }
109
+ }
110
+ })
111
+ export const withNavigation = style({
112
+ paddingLeft: 30
113
+ })
83
114
 
84
115
  const opacityDeltaOnHover = createVar('opacityDeltaOnHover')
85
116
  export const transparent = style({
@@ -88,8 +119,7 @@ export const transparent = style({
88
119
  boxShadow: 'none',
89
120
  borderColor: `color-mix(in srgb , ${vars.element.stroke}, transparent ${fallbackVar(varBorderTransparency, '10%')})`,
90
121
  vars: {
91
- [opacityDeltaOnHover]: '0',
92
- '--ai-bg': `color-mix(in srgb , ${vars.element.fill}, transparent 99%)`
122
+ [opacityDeltaOnHover]: '0'
93
123
  },
94
124
  ':before': {
95
125
  borderRadius: 'unset',
@@ -111,30 +141,19 @@ export const transparent = style({
111
141
  })
112
142
 
113
143
  export const title = style({
144
+ flex: 1,
114
145
  fontFamily: vars.compound.font,
115
- textAlign: 'left',
116
146
  fontWeight: 600,
117
147
  fontSize: rem(15),
118
148
  textTransform: 'uppercase',
119
149
  letterSpacing: '0.2px',
120
- lineHeight: 1,
150
+ // lineHeight: 1,
121
151
  color: `var(--_compound-title-color,${vars.compound.titleColor})`,
122
- paddingLeft: 14,
123
- paddingTop: 13,
124
- paddingBottom: 6,
125
- minHeight: 30,
126
- mixBlendMode: 'screen',
127
- selectors: {
128
- [`:where(.react-flow__node.draggable) &`]: {
129
- cursor: 'grab'
130
- },
131
- [`:where(.likec4-compound-transparent) &`]: {
132
- paddingTop: 10
133
- }
134
- }
135
- })
136
- export const titleWithNavigation = style({
137
- paddingLeft: 32
152
+ // paddingLeft: 0,
153
+ // paddingTop: 13,
154
+ // paddingBottom: 6,
155
+ // minHeight: 20,
156
+ mixBlendMode: 'screen'
138
157
  })
139
158
 
140
159
  globalStyle(`:where([data-mantine-color-scheme='light'] .likec4-compound-transparent)`, {
@@ -209,17 +228,14 @@ globalStyle(`:where([data-mantine-color-scheme='light'] .likec4-compound-transpa
209
228
  }
210
229
  })
211
230
 
212
- export const navigateBtn = style({
213
- position: 'absolute',
231
+ const btn = style({
214
232
  pointerEvents: 'all',
215
- left: 3,
216
- top: 6,
217
233
  cursor: 'pointer',
218
234
  color: `var(--_compound-title-color,${navigateBtnColor})`,
219
235
  opacity: 'var(--ai-opacity)',
220
236
  backgroundColor: 'var(--ai-bg)',
221
237
  vars: {
222
- '--ai-opacity': '0.75',
238
+ '--ai-opacity': '1',
223
239
  '--ai-bg-idle': `color-mix(in srgb , ${vars.element.fill}, transparent 99%)`,
224
240
  '--ai-bg': `var(--ai-bg-idle)`,
225
241
  '--ai-bg-hover': `color-mix(in srgb , ${vars.element.fill} 65%, ${vars.element.stroke})`,
@@ -229,12 +245,62 @@ export const navigateBtn = style({
229
245
  boxShadow: mantine.shadows.md
230
246
  },
231
247
  selectors: {
232
- [`:where([data-mantine-color-scheme='light'] .likec4-compound-transparent) &`]: {
248
+ [`${whereLight} .likec4-compound-transparent &`]: {
233
249
  opacity: 0.85,
234
250
  vars: {
235
- '--ai-bg-hover': `color-mix(in srgb , ${vars.element.fill}, transparent 60%)`,
236
- '--ai-hover': `color-mix(in srgb , ${vars.element.fill}, transparent 20%)`
251
+ '--ai-bg-hover': `color-mix(in srgb , ${vars.element.fill}, transparent 20%)`,
252
+ '--ai-hover': `color-mix(in srgb , ${vars.element.fill}, transparent 10%)`,
253
+ '--ai-bg': `color-mix(in srgb , ${vars.element.fill}, transparent 99%)`
237
254
  }
238
255
  }
239
256
  }
240
257
  })
258
+
259
+ export const navigateBtn = style([btn, {
260
+ position: 'absolute',
261
+ left: 3,
262
+ top: 6
263
+ }])
264
+
265
+ export const detailsBtn = style([btn, {
266
+ // position: 'absolute',
267
+ // top: 2,
268
+ // right: 2,
269
+ // selectors: {
270
+ // [`:where([data-likec4-shape='browser']) &`]: {
271
+ // top: 3,
272
+ // right: 5
273
+ // },
274
+ // ':where([data-likec4-shape="cylinder"], [data-likec4-shape="storage"]) &': {
275
+ // top: 14
276
+ // },
277
+ // ':where([data-likec4-shape="queue"]) &': {
278
+ // top: 1,
279
+ // right: 12
280
+ // }
281
+ // }
282
+ }])
283
+
284
+ export const elementIcon = style({
285
+ flex: `0 0 ${iconSize}`,
286
+ height: iconSize,
287
+ width: iconSize,
288
+ display: 'flex',
289
+ alignItems: 'center',
290
+ justifyContent: 'center',
291
+ mixBlendMode: 'luminosity'
292
+ })
293
+ globalStyle(`${elementIcon} svg, ${elementIcon} img`, {
294
+ width: '100%',
295
+ height: 'auto',
296
+ maxHeight: '100%',
297
+ pointerEvents: 'none',
298
+ filter: `
299
+ drop-shadow(0 0 3px rgb(0 0 0 / 10%))
300
+ drop-shadow(0 1px 8px rgb(0 0 0 / 8%))
301
+ drop-shadow(1px 1px 16px rgb(0 0 0 / 3%))
302
+ `
303
+ })
304
+ globalStyle(`${elementIcon} img`, {
305
+ objectFit: 'contain'
306
+ })