@likec4/diagram 1.7.2 → 1.7.3
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/LikeC4Diagram.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +38 -22
- package/dist/StaticLikeC4Diagram.d.ts +1 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/{state/useDiagramStore.d.ts → hooks/useDiagramState.d.ts} +2 -3
- package/dist/{xyflow/hooks → hooks}/useXYFlow.d.ts +10 -10
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +755 -401
- package/dist/state/DiagramContext.d.ts +74 -30
- package/dist/state/diagram-to-xyflow.d.ts +2 -0
- package/dist/state/diagramStore.d.ts +71 -22
- package/dist/style.css +56 -35
- package/dist/ui/top-left/ChangeAutoLayoutButton.d.ts +3 -0
- package/dist/ui/top-left/TopLeftPanel.d.ts +4 -0
- package/dist/ui/top-left/_shared.d.ts +5 -0
- package/dist/ui/top-left/styles.css.d.ts +3 -0
- package/dist/xyflow/XYFlowInner.d.ts +0 -1
- package/dist/xyflow/edges/edges.css.d.ts +1 -1
- package/dist/xyflow/{hooks/useLayoutConstraints.d.ts → useLayoutConstraints.d.ts} +1 -1
- package/dist/xyflow/utils.d.ts +11 -1
- package/package.json +6 -6
- package/src/LikeC4Diagram.props.ts +66 -32
- package/src/LikeC4Diagram.tsx +19 -12
- package/src/StaticLikeC4Diagram.tsx +2 -0
- package/src/hooks/index.ts +2 -0
- package/src/{state/useDiagramStore.ts → hooks/useDiagramState.ts} +3 -4
- package/src/{xyflow/hooks → hooks}/useXYFlow.ts +1 -1
- package/src/index.ts +2 -2
- package/src/state/DiagramContext.tsx +15 -7
- package/src/state/diagram-to-xyflow.ts +17 -3
- package/src/state/diagramStore.ts +73 -38
- package/src/ui/DiagramTitlePanel.tsx +1 -1
- package/src/ui/DynamicViewWalkthrough.tsx +1 -2
- package/src/ui/OptionsPanel.tsx +1 -1
- package/src/ui/options/EdgeOptions.tsx +1 -16
- package/src/ui/options/NodeOptions.tsx +38 -13
- package/src/ui/top-left/ChangeAutoLayoutButton.tsx +131 -0
- package/src/ui/top-left/TopLeftPanel.tsx +213 -0
- package/src/ui/top-left/_shared.ts +19 -0
- package/src/ui/top-left/styles.css.ts +38 -0
- package/src/xyflow/FitviewOnDiagramChange.tsx +2 -2
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +87 -59
- package/src/xyflow/XYFlow.tsx +7 -8
- package/src/xyflow/XYFlowEvents.tsx +39 -19
- package/src/xyflow/XYFlowInner.tsx +5 -7
- package/src/xyflow/edges/RelationshipEdge.tsx +60 -27
- package/src/xyflow/edges/edges.css.ts +29 -25
- package/src/xyflow/nodes/compound/CompoundNode.tsx +1 -1
- package/src/xyflow/nodes/element/ElementLink.tsx +1 -2
- package/src/xyflow/nodes/element/ElementNode.tsx +7 -5
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
- package/src/xyflow/{hooks/useLayoutConstraints.ts → useLayoutConstraints.ts} +3 -3
- package/src/xyflow/utils.ts +25 -1
- package/dist/state/hooks.d.ts +0 -4
- package/dist/state/index.d.ts +0 -2
- package/dist/state/useNavigationHistory.d.ts +0 -5
- package/dist/types.d.ts +0 -20
- package/dist/ui/BackwardForwardPanel.css.d.ts +0 -1
- package/dist/ui/BackwardForwardPanel.d.ts +0 -1
- package/dist/xyflow/hooks/index.d.ts +0 -2
- package/src/state/hooks.ts +0 -5
- package/src/state/index.ts +0 -2
- package/src/state/useNavigationHistory.ts +0 -39
- package/src/types.ts +0 -22
- package/src/ui/BackwardForwardPanel.css.ts +0 -22
- package/src/ui/BackwardForwardPanel.tsx +0 -76
- package/src/xyflow/hooks/index.ts +0 -3
|
@@ -16,10 +16,10 @@ import { curveCatmullRomOpen, line as d3line } from 'd3-shape'
|
|
|
16
16
|
import { deepEqual, deepEqual as eq } from 'fast-equals'
|
|
17
17
|
import { memo, type PointerEvent as ReactPointerEvent, useEffect, useRef, useState } from 'react'
|
|
18
18
|
import { first, hasAtLeast, isArray, isTruthy, last } from 'remeda'
|
|
19
|
-
import { useDiagramState, useDiagramStoreApi } from '../../
|
|
19
|
+
import { useDiagramState, useDiagramStoreApi } from '../../hooks/useDiagramState'
|
|
20
|
+
import { useXYStoreApi } from '../../hooks/useXYFlow'
|
|
20
21
|
import { ZIndexes } from '../const'
|
|
21
22
|
import { EdgeMarkers, type EdgeMarkerType } from '../EdgeMarkers'
|
|
22
|
-
import { useXYStoreApi } from '../hooks/useXYFlow'
|
|
23
23
|
import { type RelationshipData, type XYFlowEdge } from '../types'
|
|
24
24
|
import { bezierControlPoints } from '../utils'
|
|
25
25
|
import * as edgesCss from './edges.css'
|
|
@@ -97,9 +97,9 @@ function bezierPath(bezierSpline: NonEmptyArray<Point>) {
|
|
|
97
97
|
return path
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
// If points are within
|
|
100
|
+
// If points are within 1px, consider them the same
|
|
101
101
|
const isSame = (a: number, b: number) => {
|
|
102
|
-
return Math.abs(a - b) <
|
|
102
|
+
return Math.abs(a - b) < 2.5
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
const isSamePoint = (a: XYPosition | Point, b: XYPosition | Point) => {
|
|
@@ -150,12 +150,14 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
150
150
|
const [isControlPointDragging, setIsControlPointDragging] = useState(false)
|
|
151
151
|
const diagramStore = useDiagramStoreApi()
|
|
152
152
|
const xyflowStore = useXYStoreApi()
|
|
153
|
-
const { isActive, isEdgePathEditable, isHovered, isDimmed } = useDiagramState(s => ({
|
|
154
|
-
isEdgePathEditable: s.readonly !== true && s.experimentalEdgeEditing === true
|
|
153
|
+
const { isActive, isEdgePathEditable, isHovered, isDimmed, hasOpenSourceRelation } = useDiagramState(s => ({
|
|
154
|
+
isEdgePathEditable: s.readonly !== true && s.experimentalEdgeEditing === true && s.focusedNodeId === null
|
|
155
|
+
&& s.activeDynamicViewStep === null,
|
|
155
156
|
isActive: s.focusedNodeId === source || s.focusedNodeId === target
|
|
156
157
|
|| (s.activeDynamicViewStep !== null && s.activeDynamicViewStep === data.stepNum),
|
|
157
158
|
isHovered: s.hoveredEdgeId === id,
|
|
158
|
-
isDimmed: s.dimmed.has(id)
|
|
159
|
+
isDimmed: s.dimmed.has(id),
|
|
160
|
+
hasOpenSourceRelation: !!s.onOpenSourceRelation
|
|
159
161
|
}))
|
|
160
162
|
const { nodeLookup, edgeLookup } = xyflowStore.getState()
|
|
161
163
|
const sourceNode = nonNullable(nodeLookup.get(source)!, `source node ${source} not found`)
|
|
@@ -200,8 +202,6 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
200
202
|
let edgePath: string
|
|
201
203
|
|
|
202
204
|
if (isModified) {
|
|
203
|
-
labelX = labelPos.x
|
|
204
|
-
labelY = labelPos.y
|
|
205
205
|
const sourceCenterPos = { x: sourceX, y: sourceY }
|
|
206
206
|
const targetCenterPos = { x: targetX, y: targetY }
|
|
207
207
|
|
|
@@ -254,10 +254,15 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
254
254
|
})
|
|
255
255
|
},
|
|
256
256
|
[labelPos],
|
|
257
|
-
|
|
257
|
+
50,
|
|
258
258
|
300
|
|
259
259
|
)
|
|
260
260
|
|
|
261
|
+
if (isModified || isControlPointDragging) {
|
|
262
|
+
labelX = labelPos.x
|
|
263
|
+
labelY = labelPos.y
|
|
264
|
+
}
|
|
265
|
+
|
|
261
266
|
const onControlPointerDown = (index: number, e: ReactPointerEvent<SVGCircleElement>) => {
|
|
262
267
|
const { domNode } = xyflowStore.getState()
|
|
263
268
|
if (!domNode || e.pointerType !== 'mouse') {
|
|
@@ -281,30 +286,47 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
281
286
|
e.stopPropagation()
|
|
282
287
|
let hasMoved = false
|
|
283
288
|
let pointer = { x: e.clientX, y: e.clientY }
|
|
289
|
+
|
|
284
290
|
const onPointerMove = (e: PointerEvent) => {
|
|
285
|
-
if (!isSamePoint(pointer, [e.clientX, e.clientY])) {
|
|
291
|
+
if (hasMoved || !isSamePoint(pointer, [e.clientX, e.clientY])) {
|
|
286
292
|
setIsControlPointDragging(true)
|
|
287
293
|
hasMoved = true
|
|
288
294
|
pointer = { x: e.clientX, y: e.clientY }
|
|
289
295
|
const { x, y } = xyflow.screenToFlowPosition(pointer, { snapToGrid: false })
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
+
xyflow.updateEdgeData(id, xyedge => {
|
|
297
|
+
const cp = (xyedge.data.controlPoints ?? controlPoints).slice()
|
|
298
|
+
cp[index] = {
|
|
299
|
+
x: Math.round(x),
|
|
300
|
+
y: Math.round(y)
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
controlPoints: cp
|
|
304
|
+
}
|
|
305
|
+
})
|
|
296
306
|
}
|
|
307
|
+
e.stopPropagation()
|
|
297
308
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
domNode.removeEventListener('
|
|
309
|
+
|
|
310
|
+
const onPointerUp = (e: PointerEvent) => {
|
|
311
|
+
domNode.removeEventListener('pointermove', onPointerMove, {
|
|
312
|
+
capture: true
|
|
313
|
+
})
|
|
314
|
+
if (hasMoved) {
|
|
315
|
+
e.stopPropagation()
|
|
316
|
+
}
|
|
301
317
|
if (hasMoved || wasCanceled) {
|
|
302
318
|
diagramStore.getState().scheduleSaveManualLayout()
|
|
303
319
|
}
|
|
304
320
|
setIsControlPointDragging(false)
|
|
305
321
|
}
|
|
306
|
-
|
|
307
|
-
domNode.addEventListener('
|
|
322
|
+
|
|
323
|
+
domNode.addEventListener('pointermove', onPointerMove, {
|
|
324
|
+
capture: true
|
|
325
|
+
})
|
|
326
|
+
domNode.addEventListener('pointerup', onPointerUp, {
|
|
327
|
+
once: true,
|
|
328
|
+
capture: true
|
|
329
|
+
})
|
|
308
330
|
}
|
|
309
331
|
|
|
310
332
|
let markerStartName = toMarker(diagramEdge.tail)
|
|
@@ -318,6 +340,9 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
318
340
|
|
|
319
341
|
const labelZIndex = 1 + (isHovered ? ZIndexes.Element : (edgeLookup.get(id)!.zIndex ?? ZIndexes.Edge))
|
|
320
342
|
|
|
343
|
+
const relationId = first(data.edge.relations)
|
|
344
|
+
const showOpenSourceRelation = hasOpenSourceRelation && relationId !== undefined
|
|
345
|
+
|
|
321
346
|
return (
|
|
322
347
|
<g
|
|
323
348
|
className={clsx(
|
|
@@ -393,7 +418,12 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
393
418
|
isHovered,
|
|
394
419
|
isActive,
|
|
395
420
|
isStepEdge
|
|
396
|
-
})}
|
|
421
|
+
})}
|
|
422
|
+
onClick={showOpenSourceRelation && ((e) => {
|
|
423
|
+
e.stopPropagation()
|
|
424
|
+
diagramStore.getState().onOpenSourceRelation?.(relationId)
|
|
425
|
+
})}
|
|
426
|
+
/>
|
|
397
427
|
)}
|
|
398
428
|
</g>
|
|
399
429
|
)
|
|
@@ -412,9 +442,10 @@ type EdgeLabelProps = {
|
|
|
412
442
|
isHovered: boolean
|
|
413
443
|
isActive: boolean
|
|
414
444
|
zIndex: number
|
|
445
|
+
onClick: false | ((e: ReactPointerEvent<HTMLDivElement>) => void)
|
|
415
446
|
}
|
|
416
447
|
|
|
417
|
-
const EdgeLabel =
|
|
448
|
+
const EdgeLabel = ({
|
|
418
449
|
isDimmed,
|
|
419
450
|
color,
|
|
420
451
|
isModified,
|
|
@@ -426,8 +457,9 @@ const EdgeLabel = memo<EdgeLabelProps>(({
|
|
|
426
457
|
stepNum,
|
|
427
458
|
isHovered,
|
|
428
459
|
isActive,
|
|
429
|
-
zIndex
|
|
430
|
-
|
|
460
|
+
zIndex,
|
|
461
|
+
onClick
|
|
462
|
+
}: EdgeLabelProps) => {
|
|
431
463
|
return (
|
|
432
464
|
<EdgeLabelRenderer>
|
|
433
465
|
<Box
|
|
@@ -455,6 +487,7 @@ const EdgeLabel = memo<EdgeLabelProps>(({
|
|
|
455
487
|
'data-edge-hovered': isHovered,
|
|
456
488
|
'data-edge-active': isActive
|
|
457
489
|
}}
|
|
490
|
+
onClick={onClick || undefined}
|
|
458
491
|
>
|
|
459
492
|
{stepNum !== null && (
|
|
460
493
|
<Box className={edgesCss.stepEdgeNumber}>
|
|
@@ -469,4 +502,4 @@ const EdgeLabel = memo<EdgeLabelProps>(({
|
|
|
469
502
|
</Box>
|
|
470
503
|
</EdgeLabelRenderer>
|
|
471
504
|
)
|
|
472
|
-
}
|
|
505
|
+
}
|
|
@@ -10,7 +10,7 @@ export const container = style({
|
|
|
10
10
|
[mixColor]: `black`,
|
|
11
11
|
[xyvars.edge.stroke]: vars.relation.lineColor,
|
|
12
12
|
[xyvars.edge.strokeSelected]: `color-mix(in srgb, ${vars.relation.lineColor}, ${mixColor} 35%)`,
|
|
13
|
-
[xyvars.edge.labelColor]: `color-mix(in srgb, ${vars.relation.labelColor}, rgba(255 255 255 / 0.85)
|
|
13
|
+
[xyvars.edge.labelColor]: `color-mix(in srgb, ${vars.relation.labelColor}, rgba(255 255 255 / 0.85) 40%)`,
|
|
14
14
|
[xyvars.edge.labelBgColor]: `color-mix(in srgb, ${vars.relation.labelBgColor}, transparent 40%)`,
|
|
15
15
|
[xyvars.edge.strokeWidth]: '3'
|
|
16
16
|
}
|
|
@@ -159,6 +159,24 @@ export const cssEdgePath = style({
|
|
|
159
159
|
}
|
|
160
160
|
})
|
|
161
161
|
|
|
162
|
+
const labelBorderRadius = 2
|
|
163
|
+
export const stepEdgeNumber = style({
|
|
164
|
+
flex: '0 0 auto',
|
|
165
|
+
fontWeight: 600,
|
|
166
|
+
fontSize: rem(14),
|
|
167
|
+
lineHeight: 1,
|
|
168
|
+
padding: '5px 5px',
|
|
169
|
+
textAlign: 'center',
|
|
170
|
+
minWidth: 22,
|
|
171
|
+
borderTopLeftRadius: labelBorderRadius,
|
|
172
|
+
borderBottomLeftRadius: labelBorderRadius,
|
|
173
|
+
backgroundColor: `color-mix(in srgb, ${vars.relation.labelBgColor}, ${mixColor} 5%)`,
|
|
174
|
+
fontVariantNumeric: 'tabular-nums'
|
|
175
|
+
})
|
|
176
|
+
globalStyle(`:where([data-mantine-color-scheme="dark"]) :where([data-likec4-color="gray"]) ${stepEdgeNumber}`, {
|
|
177
|
+
backgroundColor: `color-mix(in srgb, ${vars.relation.labelBgColor}, ${mixColor} 15%)`
|
|
178
|
+
})
|
|
179
|
+
|
|
162
180
|
export const varLabelX = createVar('label-x')
|
|
163
181
|
export const varLabelY = createVar('label-y')
|
|
164
182
|
|
|
@@ -169,6 +187,7 @@ export const edgeLabel = style({
|
|
|
169
187
|
left: 0,
|
|
170
188
|
padding: '2px 4px 4px 4px',
|
|
171
189
|
fontFamily: vars.likec4.font,
|
|
190
|
+
display: 'flex',
|
|
172
191
|
position: 'absolute',
|
|
173
192
|
pointerEvents: 'all',
|
|
174
193
|
cursor: 'pointer',
|
|
@@ -177,7 +196,7 @@ export const edgeLabel = style({
|
|
|
177
196
|
mixBlendMode: 'plus-lighter',
|
|
178
197
|
color: xyvars.edge.labelColor,
|
|
179
198
|
backgroundColor: xyvars.edge.labelBgColor,
|
|
180
|
-
borderRadius:
|
|
199
|
+
borderRadius: labelBorderRadius,
|
|
181
200
|
transform: varTranslate,
|
|
182
201
|
vars: {
|
|
183
202
|
[varTranslate]: `translate(${fallbackVar(varLabelX, '-50%')}, ${fallbackVar(varLabelY, '-50%')})`
|
|
@@ -187,6 +206,10 @@ export const edgeLabel = style({
|
|
|
187
206
|
transition: 'all 140ms ease-in-out',
|
|
188
207
|
transform: `${varTranslate} scale(1.12)`
|
|
189
208
|
},
|
|
209
|
+
[`&:has(${stepEdgeNumber})`]: {
|
|
210
|
+
padding: 0,
|
|
211
|
+
gap: 2
|
|
212
|
+
},
|
|
190
213
|
[`&:is(${dimmed})`]: {
|
|
191
214
|
opacity: 0.3,
|
|
192
215
|
transition: 'opacity 600ms ease-in-out, filter 600ms ease-in-out',
|
|
@@ -197,32 +220,13 @@ export const edgeLabel = style({
|
|
|
197
220
|
}
|
|
198
221
|
})
|
|
199
222
|
|
|
200
|
-
export const stepEdgeNumber = style({
|
|
201
|
-
position: 'absolute',
|
|
202
|
-
top: -2,
|
|
203
|
-
left: -2,
|
|
204
|
-
fontWeight: 500,
|
|
205
|
-
fontSize: rem(16),
|
|
206
|
-
lineHeight: 0.9,
|
|
207
|
-
padding: '4px 6px',
|
|
208
|
-
borderRadius: 99999,
|
|
209
|
-
textAlign: 'center',
|
|
210
|
-
minWidth: 22,
|
|
211
|
-
backgroundColor: xyvars.edge.labelBgColor,
|
|
212
|
-
transition: 'all 120ms ease-out',
|
|
213
|
-
transform: 'translateX(-100%)',
|
|
214
|
-
fontVariantNumeric: 'tabular-nums',
|
|
215
|
-
selectors: {
|
|
216
|
-
':where([data-edge-hovered="true"],[data-edge-active="true"]) &': {
|
|
217
|
-
fontWeight: 600,
|
|
218
|
-
backgroundColor: `color-mix(in srgb, ${vars.relation.labelBgColor}, transparent 5%)`
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
})
|
|
222
|
-
|
|
223
223
|
export const edgeLabelText = style({
|
|
224
224
|
textAlign: 'left',
|
|
225
225
|
whiteSpaceCollapse: 'preserve-breaks',
|
|
226
226
|
fontSize: rem(14),
|
|
227
227
|
lineHeight: 1.185
|
|
228
228
|
})
|
|
229
|
+
|
|
230
|
+
globalStyle(`${edgeLabel}:has(${stepEdgeNumber}) ${edgeLabelText}`, {
|
|
231
|
+
padding: '2px 5px 4px 2px'
|
|
232
|
+
})
|
|
@@ -5,7 +5,7 @@ import clsx from 'clsx'
|
|
|
5
5
|
import { deepEqual as eq } from 'fast-equals'
|
|
6
6
|
import { memo } from 'react'
|
|
7
7
|
import { clamp } from 'remeda'
|
|
8
|
-
import { useDiagramState } from '../../../
|
|
8
|
+
import { useDiagramState } from '../../../hooks/useDiagramState'
|
|
9
9
|
import type { CompoundXYFlowNode } from '../../types'
|
|
10
10
|
import { NavigateToBtn } from '../shared/NavigateToBtn'
|
|
11
11
|
import * as css from './CompoundNode.css'
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
Group,
|
|
8
8
|
HoverCard,
|
|
9
9
|
HoverCardDropdown,
|
|
10
|
-
type HoverCardProps,
|
|
11
10
|
HoverCardTarget,
|
|
12
11
|
Stack,
|
|
13
12
|
UnstyledButton
|
|
@@ -16,8 +15,8 @@ import { IconLink } from '@tabler/icons-react'
|
|
|
16
15
|
import clsx from 'clsx'
|
|
17
16
|
import { useId } from 'react'
|
|
18
17
|
import { clamp } from 'remeda'
|
|
18
|
+
import { useDiagramState } from '../../../hooks/useDiagramState'
|
|
19
19
|
import { type DiagramState } from '../../../state/diagramStore'
|
|
20
|
-
import { useDiagramState } from '../../../state/useDiagramStore'
|
|
21
20
|
import type { XYFlowNode } from '../../types'
|
|
22
21
|
import { elementLink, trigger } from './ElementLink.css'
|
|
23
22
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { DiagramNode } from '@likec4/core/types'
|
|
2
|
-
import { Text as MantineText } from '@mantine/core'
|
|
3
|
-
import {
|
|
2
|
+
import { ActionIcon, Text as MantineText, Tooltip } from '@mantine/core'
|
|
3
|
+
import { IconFileSymlink } from '@tabler/icons-react'
|
|
4
|
+
import { Handle, type NodeProps, NodeToolbar, Position } from '@xyflow/react'
|
|
4
5
|
import clsx from 'clsx'
|
|
5
6
|
import { deepEqual as eq } from 'fast-equals'
|
|
6
7
|
import { m, type Variants } from 'framer-motion'
|
|
7
8
|
import { memo } from 'react'
|
|
8
9
|
import { isNumber, isTruthy } from 'remeda'
|
|
9
|
-
import { type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../../../
|
|
10
|
+
import { type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../../../hooks/useDiagramState'
|
|
10
11
|
import type { ElementXYFlowNode } from '../../types'
|
|
11
12
|
import { toDomPrecision } from '../../utils'
|
|
12
13
|
import { NavigateToBtn } from '../shared/NavigateToBtn'
|
|
@@ -89,12 +90,13 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
|
|
|
89
90
|
height
|
|
90
91
|
}) {
|
|
91
92
|
const store = useDiagramStoreApi()
|
|
92
|
-
const { isHovered, isDimmed, hasOnNavigateTo, isHovercards, isInteractive } = useDiagramState(s => ({
|
|
93
|
+
const { isHovered, isDimmed, hasOnNavigateTo, isHovercards, isInteractive, hasGoToSource } = useDiagramState(s => ({
|
|
93
94
|
isHovered: s.hoveredNodeId === id,
|
|
94
95
|
isDimmed: s.dimmed.has(id),
|
|
95
96
|
isInteractive: s.nodesDraggable || s.nodesSelectable || !!s.onNavigateTo,
|
|
96
97
|
isHovercards: s.showElementLinks,
|
|
97
|
-
hasOnNavigateTo: !!s.onNavigateTo
|
|
98
|
+
hasOnNavigateTo: !!s.onNavigateTo,
|
|
99
|
+
hasGoToSource: !!s.onOpenSourceElement
|
|
98
100
|
}))
|
|
99
101
|
const isNavigable = hasOnNavigateTo && !!element.navigateTo
|
|
100
102
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ActionIcon } from '@mantine/core'
|
|
2
2
|
import { IconZoomScan } from '@tabler/icons-react'
|
|
3
3
|
import clsx from 'clsx'
|
|
4
|
-
import { useDiagramStoreApi } from '../../../
|
|
4
|
+
import { useDiagramStoreApi } from '../../../hooks/useDiagramState'
|
|
5
5
|
|
|
6
6
|
export type NavigateToBtnProps = {
|
|
7
7
|
xynodeId: string
|
|
@@ -4,9 +4,9 @@ import type { InternalNode, NodeChange, ReactFlowProps, XYPosition } from '@xyfl
|
|
|
4
4
|
import { getNodeDimensions } from '@xyflow/system'
|
|
5
5
|
import { useMemo, useRef } from 'react'
|
|
6
6
|
import { isNullish } from 'remeda'
|
|
7
|
-
import { useDiagramStoreApi } from '
|
|
8
|
-
import type
|
|
9
|
-
import {
|
|
7
|
+
import { useDiagramStoreApi } from '../hooks'
|
|
8
|
+
import { type XYStoreApi } from '../hooks/useXYFlow'
|
|
9
|
+
import type { XYFlowNode } from './types'
|
|
10
10
|
|
|
11
11
|
abstract class Rect {
|
|
12
12
|
id!: string
|
package/src/xyflow/utils.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { hasAtLeast, invariant } from '@likec4/core'
|
|
2
2
|
import type { DiagramEdge, Point } from '@likec4/core/types'
|
|
3
|
-
import type { XYPosition } from '@xyflow/react'
|
|
3
|
+
import type { Rect, XYPosition } from '@xyflow/react'
|
|
4
4
|
import { Bezier } from 'bezier-js'
|
|
5
5
|
import { isArray } from 'remeda'
|
|
6
|
+
import type { InternalXYFlowNode } from './types'
|
|
6
7
|
|
|
7
8
|
export function toDomPrecision(v: number | null) {
|
|
8
9
|
if (v === null) {
|
|
@@ -15,6 +16,29 @@ export function distance(a: XYPosition, b: XYPosition) {
|
|
|
15
16
|
return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2))
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
export const nodeToRect = (nd: InternalXYFlowNode): Rect => ({
|
|
20
|
+
x: nd.internals.positionAbsolute.x,
|
|
21
|
+
y: nd.internals.positionAbsolute.y,
|
|
22
|
+
width: nd.width ?? nd.data.element.width,
|
|
23
|
+
height: nd.height ?? nd.data.element.height
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Checks if a rectangle is completely inside another rectangle.
|
|
28
|
+
*
|
|
29
|
+
* @param test - The rectangle to test.
|
|
30
|
+
* @param target - The target rectangle.
|
|
31
|
+
* @returns `true` if the `test` rectangle is completely inside the `target` rectangle, otherwise `false`.
|
|
32
|
+
*/
|
|
33
|
+
export const isInside = (test: Rect, target: Rect) => {
|
|
34
|
+
return (
|
|
35
|
+
test.x >= target.x
|
|
36
|
+
&& test.y >= target.y
|
|
37
|
+
&& test.x + test.width <= target.x + target.width
|
|
38
|
+
&& test.y + test.height <= target.y + target.height
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
18
42
|
export function bezierControlPoints(diagramEdge: DiagramEdge) {
|
|
19
43
|
let [start, ...bezierPoints] = diagramEdge.points
|
|
20
44
|
invariant(start, 'start should be defined')
|
package/dist/state/hooks.d.ts
DELETED
package/dist/state/index.d.ts
DELETED
package/dist/types.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { Fqn, RelationID, ViewID } from '@likec4/core';
|
|
2
|
-
import { Simplify } from 'type-fest';
|
|
3
|
-
|
|
4
|
-
type TaggedUnion<TagKey extends string, UnionMembers extends Record<string, Record<string, unknown>>> = {
|
|
5
|
-
[Name in keyof UnionMembers]: Simplify<{
|
|
6
|
-
[Key in TagKey]: Name;
|
|
7
|
-
} & UnionMembers[Name]>;
|
|
8
|
-
}[keyof UnionMembers];
|
|
9
|
-
export type DiagramEditorCommand = TaggedUnion<'type', {
|
|
10
|
-
showElement: {
|
|
11
|
-
element: Fqn;
|
|
12
|
-
};
|
|
13
|
-
showRelation: {
|
|
14
|
-
relation: RelationID;
|
|
15
|
-
};
|
|
16
|
-
showView: {
|
|
17
|
-
view: ViewID;
|
|
18
|
-
};
|
|
19
|
-
}>;
|
|
20
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const panel: string;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function BackwardForwardPanel(): import("react/jsx-runtime").JSX.Element;
|
package/src/state/hooks.ts
DELETED
package/src/state/index.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { useStateHistory } from '@mantine/hooks'
|
|
2
|
-
import { useMemo } from 'react'
|
|
3
|
-
import { useUpdateEffect } from '../hooks/useUpdateEffect'
|
|
4
|
-
import { useDiagramState, useDiagramStoreApi } from './useDiagramStore'
|
|
5
|
-
|
|
6
|
-
export function useNavigationHistory() {
|
|
7
|
-
const store = useDiagramStoreApi()
|
|
8
|
-
const viewId = useDiagramState(s => s.view.id)
|
|
9
|
-
|
|
10
|
-
const [historyViewId, historyOps, {
|
|
11
|
-
history,
|
|
12
|
-
current: historyIndex
|
|
13
|
-
}] = useStateHistory(viewId)
|
|
14
|
-
|
|
15
|
-
const hasBack = historyIndex > 0
|
|
16
|
-
const hasForward = historyIndex < history.length - 1
|
|
17
|
-
|
|
18
|
-
useUpdateEffect(() => {
|
|
19
|
-
if (viewId !== historyViewId) {
|
|
20
|
-
historyOps.set(viewId)
|
|
21
|
-
}
|
|
22
|
-
}, [viewId])
|
|
23
|
-
|
|
24
|
-
useUpdateEffect(() => {
|
|
25
|
-
if (viewId !== historyViewId) {
|
|
26
|
-
store.getState().onNavigateTo?.(historyViewId)
|
|
27
|
-
}
|
|
28
|
-
}, [historyViewId])
|
|
29
|
-
|
|
30
|
-
return useMemo(() => ({
|
|
31
|
-
hasBack,
|
|
32
|
-
hasForward,
|
|
33
|
-
ops: historyOps
|
|
34
|
-
}), [
|
|
35
|
-
hasBack,
|
|
36
|
-
hasForward,
|
|
37
|
-
historyOps
|
|
38
|
-
])
|
|
39
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { Fqn, RelationID, ViewID } from '@likec4/core'
|
|
2
|
-
import type { Simplify } from 'type-fest'
|
|
3
|
-
|
|
4
|
-
type TaggedUnion<
|
|
5
|
-
TagKey extends string,
|
|
6
|
-
UnionMembers extends Record<string, Record<string, unknown>>
|
|
7
|
-
> = {
|
|
8
|
-
[Name in keyof UnionMembers]: Simplify<{ [Key in TagKey]: Name } & UnionMembers[Name]>
|
|
9
|
-
}[keyof UnionMembers]
|
|
10
|
-
|
|
11
|
-
export type DiagramEditorCommand = TaggedUnion<'type', {
|
|
12
|
-
// Sources
|
|
13
|
-
showElement: {
|
|
14
|
-
element: Fqn
|
|
15
|
-
}
|
|
16
|
-
showRelation: {
|
|
17
|
-
relation: RelationID
|
|
18
|
-
}
|
|
19
|
-
showView: {
|
|
20
|
-
view: ViewID
|
|
21
|
-
}
|
|
22
|
-
}>
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { fallbackVar, globalStyle, style } from '@vanilla-extract/css'
|
|
2
|
-
import { mantine } from '../mantine.css'
|
|
3
|
-
import { vars } from '../theme.css'
|
|
4
|
-
|
|
5
|
-
export const panel = style({
|
|
6
|
-
top: fallbackVar(vars.navigationPanel.top, '0.8rem'),
|
|
7
|
-
left: fallbackVar(vars.navigationPanel.left, '1rem'),
|
|
8
|
-
margin: 0
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
globalStyle(`${panel} .mantine-ActionIcon-root`, {
|
|
12
|
-
vars: {
|
|
13
|
-
['--ai-size']: 'var(--ai-size-lg)'
|
|
14
|
-
}
|
|
15
|
-
// '@media': {
|
|
16
|
-
// [mantine.largerThan('lg')]: {
|
|
17
|
-
// vars: {
|
|
18
|
-
// ['--ai-size']: 'var(--ai-size-xl)'
|
|
19
|
-
// }
|
|
20
|
-
// }
|
|
21
|
-
// }
|
|
22
|
-
})
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { ActionIcon, Group } from '@mantine/core'
|
|
2
|
-
import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react'
|
|
3
|
-
import clsx from 'clsx'
|
|
4
|
-
import { AnimatePresence, m } from 'framer-motion'
|
|
5
|
-
import { type DiagramState, useDiagramState } from '../state/hooks'
|
|
6
|
-
import * as css from './BackwardForwardPanel.css'
|
|
7
|
-
|
|
8
|
-
const historySelector = (s: DiagramState) => ({
|
|
9
|
-
hasStepBack: s.navigationHistoryIndex > 0,
|
|
10
|
-
hasStepForward: s.navigationHistoryIndex < s.navigationHistory.length - 1,
|
|
11
|
-
goBack: s.goBack,
|
|
12
|
-
goForward: s.goForward
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
export function BackwardForwardPanel() {
|
|
16
|
-
const {
|
|
17
|
-
hasStepBack,
|
|
18
|
-
hasStepForward,
|
|
19
|
-
goBack,
|
|
20
|
-
goForward
|
|
21
|
-
} = useDiagramState(historySelector)
|
|
22
|
-
return (
|
|
23
|
-
<Group
|
|
24
|
-
className={clsx(
|
|
25
|
-
'react-flow__panel',
|
|
26
|
-
css.panel,
|
|
27
|
-
'likec4-navigation-panel'
|
|
28
|
-
)}
|
|
29
|
-
gap={'xs'}>
|
|
30
|
-
<AnimatePresence>
|
|
31
|
-
{hasStepBack && (
|
|
32
|
-
<m.div
|
|
33
|
-
layout
|
|
34
|
-
initial={{ opacity: 0.05, transform: 'translateX(-10px)' }}
|
|
35
|
-
animate={{ opacity: 1, transform: 'translateX(0)' }}
|
|
36
|
-
exit={{
|
|
37
|
-
opacity: 0.05,
|
|
38
|
-
transform: 'translateX(-10px)'
|
|
39
|
-
}}
|
|
40
|
-
key={'back'}>
|
|
41
|
-
<ActionIcon
|
|
42
|
-
variant="light"
|
|
43
|
-
color="gray"
|
|
44
|
-
onClick={e => {
|
|
45
|
-
e.stopPropagation()
|
|
46
|
-
goBack()
|
|
47
|
-
}}>
|
|
48
|
-
<IconChevronLeft />
|
|
49
|
-
</ActionIcon>
|
|
50
|
-
</m.div>
|
|
51
|
-
)}
|
|
52
|
-
{hasStepForward && (
|
|
53
|
-
<m.div
|
|
54
|
-
layout
|
|
55
|
-
initial={{ opacity: 0.05, transform: 'translateX(10px)' }}
|
|
56
|
-
animate={{ opacity: 1, transform: 'translateX(0)' }}
|
|
57
|
-
exit={{
|
|
58
|
-
opacity: 0,
|
|
59
|
-
transform: 'translateX(10px)'
|
|
60
|
-
}}
|
|
61
|
-
key={'forward'}>
|
|
62
|
-
<ActionIcon
|
|
63
|
-
variant="light"
|
|
64
|
-
color="gray"
|
|
65
|
-
onClick={e => {
|
|
66
|
-
e.stopPropagation()
|
|
67
|
-
goForward()
|
|
68
|
-
}}>
|
|
69
|
-
<IconChevronRight />
|
|
70
|
-
</ActionIcon>
|
|
71
|
-
</m.div>
|
|
72
|
-
)}
|
|
73
|
-
</AnimatePresence>
|
|
74
|
-
</Group>
|
|
75
|
-
)
|
|
76
|
-
}
|