@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DiagramNode, DiagramView, Fqn, NodeId, ViewID } from '@likec4/core'
|
|
1
|
+
import type { DiagramNode, DiagramView, Fqn, NodeId, ViewChange, ViewID } from '@likec4/core'
|
|
2
2
|
import { getBBoxCenter, invariant, nonexhaustive, nonNullable, StepEdgeId } from '@likec4/core'
|
|
3
3
|
import {
|
|
4
4
|
applyEdgeChanges,
|
|
@@ -16,16 +16,16 @@ import type { ConditionalKeys, Exact, Except, RequiredKeysOf, Simplify } from 't
|
|
|
16
16
|
import { devtools, subscribeWithSelector } from 'zustand/middleware'
|
|
17
17
|
import { shallow } from 'zustand/shallow'
|
|
18
18
|
import { createWithEqualityFn } from 'zustand/traditional'
|
|
19
|
+
import type { XYStoreApi } from '../hooks/useXYFlow'
|
|
19
20
|
import type {
|
|
20
|
-
Changes,
|
|
21
21
|
DiagramNodeWithNavigate,
|
|
22
22
|
ElementIconRenderer,
|
|
23
|
-
LikeC4DiagramEventHandlers
|
|
23
|
+
LikeC4DiagramEventHandlers,
|
|
24
|
+
WhereOperator
|
|
24
25
|
} from '../LikeC4Diagram.props'
|
|
25
26
|
import { MinZoom } from '../xyflow/const'
|
|
26
|
-
import type { XYStoreApi } from '../xyflow/hooks'
|
|
27
27
|
import type { XYFlowEdge, XYFlowInstance, XYFlowNode } from '../xyflow/types'
|
|
28
|
-
import { bezierControlPoints, isSamePoint, toDomPrecision } from '../xyflow/utils'
|
|
28
|
+
import { bezierControlPoints, isInside, isSamePoint, toDomPrecision } from '../xyflow/utils'
|
|
29
29
|
import { diagramViewToXYFlowData } from './diagram-to-xyflow'
|
|
30
30
|
|
|
31
31
|
type RequiredOrNull<T> = {
|
|
@@ -36,6 +36,7 @@ export type DiagramInitialState = {
|
|
|
36
36
|
view: DiagramView
|
|
37
37
|
readonly: boolean
|
|
38
38
|
showElementLinks: boolean
|
|
39
|
+
showNavigationButtons: boolean
|
|
39
40
|
fitViewEnabled: boolean
|
|
40
41
|
fitViewPadding: number
|
|
41
42
|
zoomable: boolean
|
|
@@ -45,6 +46,7 @@ export type DiagramInitialState = {
|
|
|
45
46
|
experimentalEdgeEditing: boolean
|
|
46
47
|
enableFocusMode: boolean
|
|
47
48
|
renderIcon: ElementIconRenderer | null
|
|
49
|
+
whereFilter: WhereOperator<string, string> | null
|
|
48
50
|
// If Dynamic View
|
|
49
51
|
enableDynamicViewWalkthrough: boolean
|
|
50
52
|
|
|
@@ -108,7 +110,7 @@ export type DiagramState = Simplify<
|
|
|
108
110
|
resetLastClicked: () => void
|
|
109
111
|
|
|
110
112
|
getElement(id: Fqn): DiagramNode | null
|
|
111
|
-
triggerChangeElementStyle: (change:
|
|
113
|
+
triggerChangeElementStyle: (change: ViewChange.ChangeElementStyle) => void
|
|
112
114
|
|
|
113
115
|
/**
|
|
114
116
|
* @returns true if there was pending save layout
|
|
@@ -166,6 +168,7 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
|
|
|
166
168
|
xynodes,
|
|
167
169
|
xyedges
|
|
168
170
|
} = diagramViewToXYFlowData(props.view, {
|
|
171
|
+
where: props.whereFilter,
|
|
169
172
|
draggable: props.nodesDraggable,
|
|
170
173
|
selectable: props.nodesSelectable
|
|
171
174
|
})
|
|
@@ -208,6 +211,7 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
|
|
|
208
211
|
xyflow,
|
|
209
212
|
xystore,
|
|
210
213
|
dimmed,
|
|
214
|
+
whereFilter,
|
|
211
215
|
view: current,
|
|
212
216
|
lastOnNavigate,
|
|
213
217
|
navigationHistory,
|
|
@@ -224,11 +228,6 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
|
|
|
224
228
|
xynodes
|
|
225
229
|
} = get()
|
|
226
230
|
|
|
227
|
-
if (shallowEqual(current, nextView)) {
|
|
228
|
-
DEV && console.debug('store: skip updateView')
|
|
229
|
-
return
|
|
230
|
-
}
|
|
231
|
-
|
|
232
231
|
if (viewSyncDebounceTimeout !== null) {
|
|
233
232
|
clearTimeout(viewSyncDebounceTimeout)
|
|
234
233
|
viewSyncDebounceTimeout = null
|
|
@@ -329,14 +328,25 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
|
|
|
329
328
|
}
|
|
330
329
|
|
|
331
330
|
const update = diagramViewToXYFlowData(nextView, {
|
|
331
|
+
where: whereFilter,
|
|
332
332
|
draggable: nodesDraggable,
|
|
333
333
|
selectable: nodesSelectable
|
|
334
334
|
})
|
|
335
335
|
|
|
336
336
|
update.xynodes = update.xynodes.map(update => {
|
|
337
337
|
const existing = xynodes.find(n => n.id === update.id)
|
|
338
|
-
if (
|
|
339
|
-
|
|
338
|
+
if (
|
|
339
|
+
existing
|
|
340
|
+
&& existing.type === update.type
|
|
341
|
+
&& eq(existing.parentId ?? null, update.parentId ?? null)
|
|
342
|
+
) {
|
|
343
|
+
if (
|
|
344
|
+
existing.hidden === update.hidden
|
|
345
|
+
&& existing.width === update.width
|
|
346
|
+
&& existing.height === update.height
|
|
347
|
+
&& eq(existing.position, update.position)
|
|
348
|
+
&& eq(existing.data.element, update.data.element)
|
|
349
|
+
) {
|
|
340
350
|
return existing
|
|
341
351
|
}
|
|
342
352
|
return {
|
|
@@ -352,21 +362,27 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
|
|
|
352
362
|
update.xyedges = update.xyedges.map((update): XYFlowEdge => {
|
|
353
363
|
const existing = xyedges.find(n => n.id === update.id)
|
|
354
364
|
if (existing) {
|
|
355
|
-
if (
|
|
365
|
+
if (
|
|
366
|
+
existing.hidden === update.hidden
|
|
367
|
+
&& eq(existing.data.label, update.data.label)
|
|
368
|
+
&& eq(existing.data.controlPoints, update.data.controlPoints)
|
|
369
|
+
&& eq(existing.data.edge, update.data.edge)
|
|
370
|
+
) {
|
|
356
371
|
return existing
|
|
357
372
|
}
|
|
358
373
|
return {
|
|
359
374
|
...existing,
|
|
360
|
-
...update
|
|
375
|
+
...update,
|
|
376
|
+
data: {
|
|
377
|
+
...existing.data,
|
|
378
|
+
...update.data
|
|
379
|
+
}
|
|
361
380
|
}
|
|
362
381
|
}
|
|
363
382
|
return update
|
|
364
383
|
})
|
|
365
384
|
}
|
|
366
385
|
|
|
367
|
-
const hasChanges = !isSameView || !shallowEqual(update.xynodes, xynodes)
|
|
368
|
-
|| !shallowEqual(update.xyedges, xyedges)
|
|
369
|
-
|
|
370
386
|
set(
|
|
371
387
|
{
|
|
372
388
|
isDynamicView: nextView.__ === 'dynamic',
|
|
@@ -382,8 +398,8 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
|
|
|
382
398
|
navigationHistory,
|
|
383
399
|
navigationHistoryIndex,
|
|
384
400
|
dimmed,
|
|
385
|
-
xynodes:
|
|
386
|
-
xyedges:
|
|
401
|
+
xynodes: !isSameView || !shallowEqual(update.xynodes, xynodes) ? update.xynodes : xynodes,
|
|
402
|
+
xyedges: !isSameView || !shallowEqual(update.xyedges, xyedges) ? update.xyedges : xyedges
|
|
387
403
|
},
|
|
388
404
|
noReplace,
|
|
389
405
|
isSameView ? 'update-view [same]' : 'update-view [another]'
|
|
@@ -563,10 +579,6 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
|
|
|
563
579
|
clearTimeout(viewSyncDebounceTimeout)
|
|
564
580
|
set({ viewSyncDebounceTimeout: null })
|
|
565
581
|
}
|
|
566
|
-
if (!onChange) {
|
|
567
|
-
DEV && console.debug('ignore triggerSaveManualLayout, as no onChange handler is set')
|
|
568
|
-
return
|
|
569
|
-
}
|
|
570
582
|
const { nodeLookup } = xystore.getState()
|
|
571
583
|
const movedNodes = new StringSet()
|
|
572
584
|
let bounds = {
|
|
@@ -575,6 +587,7 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
|
|
|
575
587
|
width: 1,
|
|
576
588
|
height: 1
|
|
577
589
|
}
|
|
590
|
+
|
|
578
591
|
const nodes = reduce([...nodeLookup.values()], (acc, node) => {
|
|
579
592
|
const dimensions = getNodeDimensions(node)
|
|
580
593
|
if (!isSamePoint(node.internals.positionAbsolute, node.data.element.position)) {
|
|
@@ -589,7 +602,8 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
|
|
|
589
602
|
}
|
|
590
603
|
bounds = getBoundsOfRects(bounds, rect)
|
|
591
604
|
return acc
|
|
592
|
-
}, {} as
|
|
605
|
+
}, {} as ViewChange.SaveManualLayout['layout']['nodes'])
|
|
606
|
+
|
|
593
607
|
const edges = reduce(xyflow.getEdges(), (acc, { source, target, data }) => {
|
|
594
608
|
let controlPoints = data.controlPoints
|
|
595
609
|
const sourceOrTargetMoved = movedNodes.has(source) || movedNodes.has(target)
|
|
@@ -600,7 +614,7 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
|
|
|
600
614
|
if (data.edge.points.length === 0 && (!controlPoints || controlPoints.length === 0)) {
|
|
601
615
|
return acc
|
|
602
616
|
}
|
|
603
|
-
const _updated:
|
|
617
|
+
const _updated: ViewChange.SaveManualLayout['layout']['edges'][string] = acc[data.edge.id] = {
|
|
604
618
|
points: data.edge.points
|
|
605
619
|
}
|
|
606
620
|
if (data.label?.bbox) {
|
|
@@ -615,8 +629,16 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
|
|
|
615
629
|
if (!sourceOrTargetMoved && data.edge.dotpos) {
|
|
616
630
|
_updated.dotpos = data.edge.dotpos
|
|
617
631
|
}
|
|
618
|
-
const allX = [
|
|
619
|
-
|
|
632
|
+
const allX = [
|
|
633
|
+
...data.edge.points.map(p => p[0]),
|
|
634
|
+
...(controlPoints ?? []).map(p => p.x),
|
|
635
|
+
...(_updated.labelBBox ? [_updated.labelBBox.x, _updated.labelBBox.x + _updated.labelBBox.width] : [])
|
|
636
|
+
]
|
|
637
|
+
const allY = [
|
|
638
|
+
...data.edge.points.map(p => p[1]),
|
|
639
|
+
...(controlPoints ?? []).map(p => p.y),
|
|
640
|
+
...(_updated.labelBBox ? [_updated.labelBBox.y, _updated.labelBBox.y + _updated.labelBBox.height] : [])
|
|
641
|
+
]
|
|
620
642
|
const rect = boxToRect({
|
|
621
643
|
x: Math.floor(Math.min(...allX)),
|
|
622
644
|
y: Math.floor(Math.min(...allY)),
|
|
@@ -625,14 +647,13 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
|
|
|
625
647
|
})
|
|
626
648
|
bounds = getBoundsOfRects(bounds, rect)
|
|
627
649
|
return acc
|
|
628
|
-
}, {} as
|
|
650
|
+
}, {} as ViewChange.SaveManualLayout['layout']['edges'])
|
|
629
651
|
|
|
630
|
-
const change:
|
|
652
|
+
const change: ViewChange.SaveManualLayout = {
|
|
631
653
|
op: 'save-manual-layout',
|
|
632
654
|
layout: {
|
|
633
655
|
hash: view.hash,
|
|
634
656
|
autoLayout: view.autoLayout,
|
|
635
|
-
|
|
636
657
|
nodes,
|
|
637
658
|
edges,
|
|
638
659
|
...bounds
|
|
@@ -643,14 +664,24 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
|
|
|
643
664
|
console.debug('triggerSaveManualLayout', change)
|
|
644
665
|
}
|
|
645
666
|
|
|
646
|
-
|
|
667
|
+
// If new view bounds are outside of the diagram bounds, update the view bounds
|
|
668
|
+
// and update edges, as
|
|
669
|
+
if (!isInside(bounds, view.bounds)) {
|
|
670
|
+
set(
|
|
671
|
+
{
|
|
672
|
+
view: {
|
|
673
|
+
...view,
|
|
674
|
+
bounds
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
noReplace,
|
|
678
|
+
'update view bounds'
|
|
679
|
+
)
|
|
680
|
+
}
|
|
681
|
+
onChange?.({ change })
|
|
647
682
|
},
|
|
648
683
|
scheduleSaveManualLayout: () => {
|
|
649
|
-
let { viewSyncDebounceTimeout
|
|
650
|
-
if (!onChange) {
|
|
651
|
-
DEV && console.debug('ignore triggerSaveManualLayout, as no onChange handler is set')
|
|
652
|
-
return
|
|
653
|
-
}
|
|
684
|
+
let { viewSyncDebounceTimeout } = get()
|
|
654
685
|
if (viewSyncDebounceTimeout) {
|
|
655
686
|
clearTimeout(viewSyncDebounceTimeout)
|
|
656
687
|
}
|
|
@@ -666,7 +697,7 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
|
|
|
666
697
|
},
|
|
667
698
|
|
|
668
699
|
triggerOnNavigateTo: (xynodeId, event) => {
|
|
669
|
-
const { view, xynodes, onNavigateTo } = get()
|
|
700
|
+
const { view, xynodes, onNavigateTo, cancelSaveManualLayout } = get()
|
|
670
701
|
if (!onNavigateTo) {
|
|
671
702
|
return
|
|
672
703
|
}
|
|
@@ -674,6 +705,7 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
|
|
|
674
705
|
invariant(xynode, `xynode not found: ${xynodeId}`)
|
|
675
706
|
const element = view.nodes.find(({ id }) => id === xynodeId)
|
|
676
707
|
invariant(element?.navigateTo, `node is not navigable: ${xynodeId}`)
|
|
708
|
+
cancelSaveManualLayout()
|
|
677
709
|
set(
|
|
678
710
|
{
|
|
679
711
|
lastClickedNodeId: xynodeId,
|
|
@@ -857,3 +889,6 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
|
|
|
857
889
|
shallow
|
|
858
890
|
)
|
|
859
891
|
}
|
|
892
|
+
|
|
893
|
+
export type DiagramZustandStore = ReturnType<typeof createDiagramStore>
|
|
894
|
+
export type DiagramStoreApi = Readonly<Pick<DiagramZustandStore, 'getState' | 'setState' | 'subscribe'>>
|
|
@@ -3,7 +3,7 @@ import { useLocalStorage } from '@mantine/hooks'
|
|
|
3
3
|
import { IconMenu } from '@tabler/icons-react'
|
|
4
4
|
import clsx from 'clsx'
|
|
5
5
|
import { AnimatePresence, m } from 'framer-motion'
|
|
6
|
-
import { type DiagramState, useDiagramState } from '../
|
|
6
|
+
import { type DiagramState, useDiagramState } from '../hooks'
|
|
7
7
|
import * as css from './DiagramTitlePanel.css'
|
|
8
8
|
|
|
9
9
|
const selector = (s: DiagramState) => ({
|
|
@@ -8,8 +8,7 @@ import {
|
|
|
8
8
|
} from '@tabler/icons-react'
|
|
9
9
|
import clsx from 'clsx'
|
|
10
10
|
import { isNullish, isNumber } from 'remeda'
|
|
11
|
-
import { useDiagramState } from '../
|
|
12
|
-
import { useXYStore } from '../xyflow/hooks/useXYFlow'
|
|
11
|
+
import { useDiagramState, useXYStore } from '../hooks'
|
|
13
12
|
import * as css from './DynamicViewWalkthrough.css'
|
|
14
13
|
|
|
15
14
|
export function DynamicViewWalkthrough() {
|
package/src/ui/OptionsPanel.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import clsx from 'clsx'
|
|
|
4
4
|
import { deepEqual as eq } from 'fast-equals'
|
|
5
5
|
import { AnimatePresence, m } from 'framer-motion'
|
|
6
6
|
import { useState } from 'react'
|
|
7
|
-
import { useDiagramState } from '../
|
|
7
|
+
import { useDiagramState } from '../hooks/useDiagramState'
|
|
8
8
|
import { NodeOptions } from './options/NodeOptions'
|
|
9
9
|
import * as styles from './OptionsPanel.css'
|
|
10
10
|
|
|
@@ -3,8 +3,7 @@ import { CheckIcon, ColorSwatch, Divider, Flex, rem, Stack, Text, Tooltip, Toolt
|
|
|
3
3
|
import { shallowEqual } from 'fast-equals'
|
|
4
4
|
import { memo } from 'react'
|
|
5
5
|
import { hasAtLeast, keys, takeWhile } from 'remeda'
|
|
6
|
-
import { useDiagramStoreApi } from '../../
|
|
7
|
-
import { useXYEdgesData } from '../../xyflow/hooks/useXYFlow'
|
|
6
|
+
import { useDiagramStoreApi, useXYEdgesData } from '../../hooks'
|
|
8
7
|
import { type XYFlowEdge } from '../../xyflow/types'
|
|
9
8
|
|
|
10
9
|
const {
|
|
@@ -41,20 +40,6 @@ export const EdgeOptions = memo<{ selectedEdgeIds: string[] }>(({ selectedEdgeId
|
|
|
41
40
|
}
|
|
42
41
|
const [firstEdge, ...rest] = edges
|
|
43
42
|
|
|
44
|
-
// // Makes sense to show opacity option only if there is at least one compound node
|
|
45
|
-
// const showOpacityOption = firstNode.type === 'compound'
|
|
46
|
-
// && (rest.length === 0 || rest.every(node => node.type === 'compound'))
|
|
47
|
-
|
|
48
|
-
// const triggerChange = (style: Changes.ChangeElementStyle['style']) => {
|
|
49
|
-
// const targets = edges.map(node => node.data.element.id)
|
|
50
|
-
// invariant(hasAtLeast(targets, 1), 'At least one target is required')
|
|
51
|
-
// diagramApi.getState().triggerChangeElementStyle({
|
|
52
|
-
// op: 'change-element-style',
|
|
53
|
-
// style,
|
|
54
|
-
// targets
|
|
55
|
-
// })
|
|
56
|
-
// }
|
|
57
|
-
|
|
58
43
|
return (
|
|
59
44
|
<Stack gap={'xs'}>
|
|
60
45
|
<div>
|
|
@@ -5,14 +5,17 @@ import {
|
|
|
5
5
|
ElementShapes,
|
|
6
6
|
invariant,
|
|
7
7
|
type NonEmptyArray,
|
|
8
|
-
type ThemeColor
|
|
8
|
+
type ThemeColor,
|
|
9
|
+
type ViewChange
|
|
9
10
|
} from '@likec4/core'
|
|
10
11
|
import {
|
|
12
|
+
ActionIcon,
|
|
11
13
|
Box,
|
|
12
14
|
CheckIcon,
|
|
13
15
|
ColorSwatch,
|
|
14
16
|
Divider,
|
|
15
17
|
Flex,
|
|
18
|
+
Group,
|
|
16
19
|
rem,
|
|
17
20
|
SegmentedControl,
|
|
18
21
|
Select,
|
|
@@ -23,13 +26,12 @@ import {
|
|
|
23
26
|
Tooltip,
|
|
24
27
|
TooltipGroup
|
|
25
28
|
} from '@mantine/core'
|
|
29
|
+
import { IconFileSymlink } from '@tabler/icons-react'
|
|
26
30
|
import { shallowEqual } from 'fast-equals'
|
|
27
31
|
import { memo, useState } from 'react'
|
|
28
32
|
import { hasAtLeast, keys, takeWhile } from 'remeda'
|
|
33
|
+
import { useDiagramStoreApi, useXYNodesData } from '../../hooks'
|
|
29
34
|
import { useUpdateEffect } from '../../hooks/useUpdateEffect'
|
|
30
|
-
import type { Changes } from '../../LikeC4Diagram.props'
|
|
31
|
-
import { useDiagramStoreApi } from '../../state/hooks'
|
|
32
|
-
import { useXYNodesData } from '../../xyflow/hooks'
|
|
33
35
|
import { XYFlowNode } from '../../xyflow/types'
|
|
34
36
|
|
|
35
37
|
const {
|
|
@@ -71,11 +73,13 @@ export const NodeOptions = memo<{ selectedNodeIds: string[] }>(({ selectedNodeId
|
|
|
71
73
|
|
|
72
74
|
const [firstNode, ...rest] = nodes
|
|
73
75
|
|
|
76
|
+
const showGoToSource = rest.length === 0 && diagramApi.getState().onOpenSourceElement!!
|
|
77
|
+
|
|
74
78
|
// Makes sense to show opacity option only if there is at least one compound node
|
|
75
79
|
const showOpacityOption = firstNode.type === 'compound'
|
|
76
80
|
&& (rest.length === 0 || rest.every(node => node.type === 'compound'))
|
|
77
81
|
|
|
78
|
-
const triggerChange = (style:
|
|
82
|
+
const triggerChange = (style: ViewChange.ChangeElementStyle['style']) => {
|
|
79
83
|
const targets = nodes.map(node => node.data.element.id)
|
|
80
84
|
invariant(hasAtLeast(targets, 1), 'At least one target is required')
|
|
81
85
|
diagramApi.getState().triggerChangeElementStyle({
|
|
@@ -87,14 +91,35 @@ export const NodeOptions = memo<{ selectedNodeIds: string[] }>(({ selectedNodeId
|
|
|
87
91
|
|
|
88
92
|
return (
|
|
89
93
|
<Stack gap={'xs'}>
|
|
90
|
-
<
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{rest.length
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
<Group wrap="nowrap" align="flex-start">
|
|
95
|
+
<Box flex={1}>
|
|
96
|
+
<Text fz={rem(9)} fw={'500'} c={'dimmed'}>
|
|
97
|
+
ELEMENT{rest.length > 0 ? 'S' : ``}
|
|
98
|
+
</Text>
|
|
99
|
+
<Text size="xs" c={rest.length > 0 ? 'dimmed' : ''} truncate>
|
|
100
|
+
{rest.length === 0 ? firstNode.data.element.title : `[ multiple ]`}
|
|
101
|
+
</Text>
|
|
102
|
+
</Box>
|
|
103
|
+
{showGoToSource && (
|
|
104
|
+
<Box flex={0}>
|
|
105
|
+
<ActionIcon
|
|
106
|
+
size={'sm'}
|
|
107
|
+
variant="light"
|
|
108
|
+
color="gray"
|
|
109
|
+
onClick={e => {
|
|
110
|
+
e.stopPropagation()
|
|
111
|
+
diagramApi.getState().onOpenSourceElement?.(firstNode.data.element.id)
|
|
112
|
+
}}>
|
|
113
|
+
<IconFileSymlink
|
|
114
|
+
stroke={1.2}
|
|
115
|
+
style={{
|
|
116
|
+
width: '70%',
|
|
117
|
+
height: '70%'
|
|
118
|
+
}} />
|
|
119
|
+
</ActionIcon>
|
|
120
|
+
</Box>
|
|
121
|
+
)}
|
|
122
|
+
</Group>
|
|
98
123
|
{showShapeOption && (
|
|
99
124
|
<ShapeOption
|
|
100
125
|
nodes={nodes}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { AutoLayoutDirection } from '@likec4/core'
|
|
2
|
+
import {
|
|
3
|
+
ActionIcon as MantineActionIcon,
|
|
4
|
+
Box,
|
|
5
|
+
FloatingIndicator,
|
|
6
|
+
Group,
|
|
7
|
+
Popover,
|
|
8
|
+
PopoverDropdown,
|
|
9
|
+
type PopoverProps,
|
|
10
|
+
PopoverTarget,
|
|
11
|
+
Stack,
|
|
12
|
+
Text,
|
|
13
|
+
Tooltip as MantineTooltip,
|
|
14
|
+
TooltipGroup
|
|
15
|
+
} from '@mantine/core'
|
|
16
|
+
import { IconArrowBigDownLines, IconLayoutDashboard } from '@tabler/icons-react'
|
|
17
|
+
import { useState } from 'react'
|
|
18
|
+
import { useDiagramState, useDiagramStoreApi } from '../../hooks/useDiagramState'
|
|
19
|
+
import * as css from './styles.css'
|
|
20
|
+
|
|
21
|
+
const ActionIcon = MantineActionIcon.withProps({
|
|
22
|
+
size: 'md',
|
|
23
|
+
variant: 'subtle',
|
|
24
|
+
color: 'gray'
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const Tooltip = MantineTooltip.withProps({
|
|
28
|
+
color: 'gray',
|
|
29
|
+
fz: 'xs',
|
|
30
|
+
position: 'right',
|
|
31
|
+
openDelay: 300,
|
|
32
|
+
label: '',
|
|
33
|
+
children: null,
|
|
34
|
+
offset: 5,
|
|
35
|
+
transitionProps: { transition: 'fade', duration: 200 }
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export const ChangeAutoLayoutButton = (props: PopoverProps) => {
|
|
39
|
+
const store = useDiagramStoreApi()
|
|
40
|
+
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null)
|
|
41
|
+
const [controlsRefs, setControlsRefs] = useState<Record<AutoLayoutDirection, HTMLButtonElement | null>>({} as any)
|
|
42
|
+
const autoLayout = useDiagramState(s => s.view.autoLayout)
|
|
43
|
+
|
|
44
|
+
const setControlRef = (name: AutoLayoutDirection) => (node: HTMLButtonElement) => {
|
|
45
|
+
controlsRefs[name] = node
|
|
46
|
+
setControlsRefs(controlsRefs)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const setAutoLayout = (layout: AutoLayoutDirection) => (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
50
|
+
event.stopPropagation()
|
|
51
|
+
store.getState().onChange?.({
|
|
52
|
+
change: {
|
|
53
|
+
op: 'change-autolayout',
|
|
54
|
+
layout
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Popover
|
|
61
|
+
position="right-start"
|
|
62
|
+
clickOutsideEvents={[
|
|
63
|
+
'pointerdown'
|
|
64
|
+
]}
|
|
65
|
+
radius="xs"
|
|
66
|
+
shadow="lg"
|
|
67
|
+
offset={{
|
|
68
|
+
mainAxis: 10
|
|
69
|
+
}}
|
|
70
|
+
{...props}>
|
|
71
|
+
<PopoverTarget>
|
|
72
|
+
<MantineActionIcon
|
|
73
|
+
className="action-icon"
|
|
74
|
+
variant="light"
|
|
75
|
+
color="gray">
|
|
76
|
+
<IconLayoutDashboard stroke={1.3} />
|
|
77
|
+
</MantineActionIcon>
|
|
78
|
+
</PopoverTarget>
|
|
79
|
+
<PopoverDropdown className="likec4-top-left-panel" p={8} pt={4}>
|
|
80
|
+
<Box pos={'relative'} ref={setRootRef}>
|
|
81
|
+
<FloatingIndicator
|
|
82
|
+
target={controlsRefs[autoLayout]}
|
|
83
|
+
parent={rootRef}
|
|
84
|
+
className={css.autolayoutIndicator}
|
|
85
|
+
/>
|
|
86
|
+
<Box mb={10}>
|
|
87
|
+
<Text inline fz={'xs'} c={'dimmed'} fw={500}>Auto layout:</Text>
|
|
88
|
+
</Box>
|
|
89
|
+
<TooltipGroup openDelay={100}>
|
|
90
|
+
<Stack align="center" gap={1}>
|
|
91
|
+
<Tooltip label="Top to Bottom">
|
|
92
|
+
<ActionIcon
|
|
93
|
+
className={css.autolayoutIcon}
|
|
94
|
+
ref={setControlRef('TB')}
|
|
95
|
+
onClick={setAutoLayout('TB')}>
|
|
96
|
+
<IconArrowBigDownLines stroke={1.3} />
|
|
97
|
+
</ActionIcon>
|
|
98
|
+
</Tooltip>
|
|
99
|
+
<Group gap={30}>
|
|
100
|
+
<Tooltip label="Left to Right">
|
|
101
|
+
<ActionIcon
|
|
102
|
+
className={css.autolayoutIcon}
|
|
103
|
+
ref={setControlRef('LR')}
|
|
104
|
+
onClick={setAutoLayout('LR')}>
|
|
105
|
+
<IconArrowBigDownLines stroke={1.3} style={{ rotate: '270deg' }} />
|
|
106
|
+
</ActionIcon>
|
|
107
|
+
</Tooltip>
|
|
108
|
+
<Tooltip label="Right to Left">
|
|
109
|
+
<ActionIcon
|
|
110
|
+
className={css.autolayoutIcon}
|
|
111
|
+
ref={setControlRef('RL')}
|
|
112
|
+
onClick={setAutoLayout('RL')}>
|
|
113
|
+
<IconArrowBigDownLines stroke={1.3} style={{ rotate: '90deg' }} />
|
|
114
|
+
</ActionIcon>
|
|
115
|
+
</Tooltip>
|
|
116
|
+
</Group>
|
|
117
|
+
<Tooltip label="Bottom to Top">
|
|
118
|
+
<ActionIcon
|
|
119
|
+
className={css.autolayoutIcon}
|
|
120
|
+
ref={setControlRef('BT')}
|
|
121
|
+
onClick={setAutoLayout('BT')}>
|
|
122
|
+
<IconArrowBigDownLines stroke={1.3} style={{ rotate: '180deg' }} />
|
|
123
|
+
</ActionIcon>
|
|
124
|
+
</Tooltip>
|
|
125
|
+
</Stack>
|
|
126
|
+
</TooltipGroup>
|
|
127
|
+
</Box>
|
|
128
|
+
</PopoverDropdown>
|
|
129
|
+
</Popover>
|
|
130
|
+
)
|
|
131
|
+
}
|