@likec4/diagram 1.0.2 → 1.1.1
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/KeepAspectRatioContainer.d.ts +9 -0
- package/dist/LikeC4Diagram.css.d.ts +1 -0
- package/dist/LikeC4Diagram.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +16 -11
- package/dist/index.mjs +639 -762
- package/dist/state/DiagramContext.d.ts +54 -18
- package/dist/state/diagramStore.d.ts +61 -21
- package/dist/state/useDiagramStore.d.ts +54 -18
- package/dist/style.css +690 -79
- package/dist/ui/DiagramTitlePanel.css.d.ts +4 -0
- package/dist/ui/DiagramTitlePanel.d.ts +2 -0
- package/dist/ui/OptionsPanel.d.ts +1 -2
- package/dist/ui/index.d.ts +0 -1
- package/dist/xyflow/SelectEdgesOnNodeFocus.d.ts +1 -0
- package/dist/xyflow/{XYFlowWrapper.d.ts → XYFlow.d.ts} +2 -2
- package/dist/xyflow/XYFlowEvents.d.ts +133 -13
- package/dist/xyflow/edges/RelationshipEdge.d.ts +2 -2
- package/dist/xyflow/edges/edges.css.d.ts +1 -0
- package/dist/xyflow/hooks/useXYFlow.d.ts +20 -20
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +7 -5
- package/dist/xyflow/types.d.ts +2 -2
- package/package.json +5 -3
- package/src/{ui/KeepAspectRatio.tsx → KeepAspectRatioContainer.tsx} +7 -6
- package/src/LikeC4Diagram.css.ts +8 -1
- package/src/LikeC4Diagram.props.ts +17 -12
- package/src/LikeC4Diagram.tsx +9 -6
- package/src/StaticLikeC4Diagram.tsx +1 -0
- package/src/globals.css.ts +48 -38
- package/src/state/diagramStore.ts +125 -68
- package/src/ui/DiagramTitlePanel.css.ts +32 -0
- package/src/ui/DiagramTitlePanel.tsx +139 -0
- package/src/ui/OptionsPanel.tsx +4 -23
- package/src/ui/index.ts +0 -1
- package/src/ui/options/NodeOptions.tsx +107 -30
- package/src/xyflow/FitviewOnDiagramChange.tsx +26 -20
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +57 -0
- package/src/xyflow/SyncWithDiagram.tsx +35 -26
- package/src/xyflow/{XYFlowWrapper.tsx → XYFlow.tsx} +24 -27
- package/src/xyflow/XYFlowEvents.tsx +175 -81
- package/src/xyflow/diagram-to-xyflow.ts +4 -4
- package/src/xyflow/edges/RelationshipEdge.tsx +43 -47
- package/src/xyflow/edges/edges.css.ts +77 -36
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +94 -44
- package/src/xyflow/nodes/compound/CompoundNode.tsx +29 -31
- package/src/xyflow/nodes/element/ElementNode.tsx +2 -2
- package/src/xyflow/nodes/element/element.css.ts +1 -0
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +2 -1
- package/dist/ui/KeepAspectRatio.css.d.ts +0 -1
- package/dist/ui/KeepAspectRatio.d.ts +0 -9
- package/src/ui/KeepAspectRatio.css.ts +0 -7
package/src/globals.css.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import type { ThemeColor } from '@likec4/core'
|
|
2
1
|
import { defaultTheme } from '@likec4/core'
|
|
3
|
-
import { createGlobalTheme } from '@vanilla-extract/css'
|
|
4
|
-
import {
|
|
2
|
+
import { createGlobalTheme, globalStyle } from '@vanilla-extract/css'
|
|
3
|
+
import { scale, toHex } from 'khroma'
|
|
4
|
+
import { keys, omit } from 'remeda'
|
|
5
5
|
import { mantine } from './mantine.css'
|
|
6
6
|
import { vars } from './theme.css'
|
|
7
7
|
|
|
8
8
|
export const rootClassName = 'likec4-diagram-root'
|
|
9
9
|
|
|
10
|
+
globalStyle(`.${rootClassName}`, {
|
|
11
|
+
WebkitFontSmoothing: mantine.webkitFontSmoothing,
|
|
12
|
+
MozOsxFontSmoothing: mantine.mozFontSmoothing
|
|
13
|
+
})
|
|
14
|
+
|
|
10
15
|
createGlobalTheme(`.${rootClassName}`, {
|
|
11
16
|
...omit(vars, ['optionsPanel', 'default'])
|
|
12
17
|
}, {
|
|
@@ -75,38 +80,7 @@ createGlobalTheme(`:where([data-mantine-color-scheme='light']) .${rootClassName}
|
|
|
75
80
|
}
|
|
76
81
|
})
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
// boxSizing: 'border-box',
|
|
80
|
-
// outline: 'none',
|
|
81
|
-
// borderWidth: 0,
|
|
82
|
-
// borderStyle: 'solid',
|
|
83
|
-
// borderColor: 'transparent',
|
|
84
|
-
// })
|
|
85
|
-
|
|
86
|
-
// createGlobalTheme(':where(.likec4-diagram-root)', {
|
|
87
|
-
// ...omit(vars, ['optionsPanel', 'defaults']),
|
|
88
|
-
// }, {
|
|
89
|
-
// likec4: {
|
|
90
|
-
// font: `var(--likec4-default-font-family,'ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"')`,
|
|
91
|
-
// backgroundColor: mantine.colors.body
|
|
92
|
-
// },
|
|
93
|
-
// compound: {
|
|
94
|
-
// font: vars.likec4.font,
|
|
95
|
-
// titleColor: vars.element.loContrast
|
|
96
|
-
// },
|
|
97
|
-
// element: {
|
|
98
|
-
// font: vars.likec4.font,
|
|
99
|
-
// ...defaultTheme.elements.primary
|
|
100
|
-
// },
|
|
101
|
-
// relation: {
|
|
102
|
-
// ...defaultTheme.relationships.slate
|
|
103
|
-
// }
|
|
104
|
-
// })
|
|
105
|
-
|
|
106
|
-
for (const color of Object.keys(defaultTheme.elements)) {
|
|
107
|
-
if (!(color in defaultTheme.elements) || !(color in defaultTheme.relationships)) {
|
|
108
|
-
continue
|
|
109
|
-
}
|
|
83
|
+
for (const color of keys.strict(defaultTheme.elements)) {
|
|
110
84
|
createGlobalTheme(`:where([data-likec4-color='${color}'])`, {
|
|
111
85
|
element: {
|
|
112
86
|
fill: vars.element.fill,
|
|
@@ -119,10 +93,46 @@ for (const color of Object.keys(defaultTheme.elements)) {
|
|
|
119
93
|
},
|
|
120
94
|
relation: vars.relation
|
|
121
95
|
}, {
|
|
122
|
-
element: defaultTheme.elements[color
|
|
96
|
+
element: defaultTheme.elements[color],
|
|
123
97
|
compound: {
|
|
124
|
-
titleColor: defaultTheme.elements[color
|
|
98
|
+
titleColor: defaultTheme.elements[color].loContrast
|
|
125
99
|
},
|
|
126
|
-
relation: defaultTheme.relationships[color
|
|
100
|
+
relation: defaultTheme.relationships[color]
|
|
127
101
|
})
|
|
102
|
+
|
|
103
|
+
const compoundDarkColor = (color: string, depth: number) =>
|
|
104
|
+
toHex(
|
|
105
|
+
scale(color, {
|
|
106
|
+
l: -22 - 5 * depth,
|
|
107
|
+
s: -10 - 6 * depth
|
|
108
|
+
})
|
|
109
|
+
)
|
|
110
|
+
const compoundLightColor = (color: string, depth: number) =>
|
|
111
|
+
toHex(
|
|
112
|
+
scale(color, {
|
|
113
|
+
l: -20 - 3 * depth,
|
|
114
|
+
s: -3 - 6 * depth
|
|
115
|
+
})
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const compounds = {
|
|
119
|
+
fill: vars.element.fill,
|
|
120
|
+
stroke: vars.element.stroke
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (let depth = 1; depth <= 6; depth++) {
|
|
124
|
+
createGlobalTheme(`:where([data-likec4-color='${color}'][data-compound-depth='${depth}'])`, compounds, {
|
|
125
|
+
fill: compoundDarkColor(defaultTheme.elements[color].fill, depth),
|
|
126
|
+
stroke: compoundDarkColor(defaultTheme.elements[color].stroke, depth)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
createGlobalTheme(
|
|
130
|
+
`:where([data-mantine-color-scheme='light']) :where([data-likec4-color='${color}'][data-compound-depth='${depth}'])`,
|
|
131
|
+
compounds,
|
|
132
|
+
{
|
|
133
|
+
fill: compoundLightColor(defaultTheme.elements[color].fill, depth),
|
|
134
|
+
stroke: compoundLightColor(defaultTheme.elements[color].stroke, depth)
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
}
|
|
128
138
|
}
|
|
@@ -1,17 +1,27 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BorderStyle,
|
|
3
|
+
DiagramNode,
|
|
4
|
+
DiagramView,
|
|
5
|
+
ElementShape,
|
|
6
|
+
Fqn,
|
|
7
|
+
NonEmptyArray,
|
|
8
|
+
ThemeColor,
|
|
9
|
+
ViewID
|
|
10
|
+
} from '@likec4/core'
|
|
2
11
|
import { invariant, nonexhaustive } from '@likec4/core'
|
|
3
12
|
import type { XYPosition } from '@xyflow/react'
|
|
4
13
|
import { getNodeDimensions } from '@xyflow/system'
|
|
5
14
|
import { DEV } from 'esm-env'
|
|
6
15
|
import { shallowEqual } from 'fast-equals'
|
|
7
16
|
import type { MouseEvent as ReactMouseEvent } from 'react'
|
|
17
|
+
import { entries } from 'remeda'
|
|
8
18
|
import type { Exact, Except, RequiredKeysOf, Simplify } from 'type-fest'
|
|
9
19
|
import { devtools, subscribeWithSelector } from 'zustand/middleware'
|
|
10
20
|
import { shallow } from 'zustand/shallow'
|
|
11
21
|
import { createWithEqualityFn } from 'zustand/traditional'
|
|
12
22
|
import type { Change, DiagramNodeWithNavigate, LikeC4DiagramEventHandlers } from '../LikeC4Diagram.props'
|
|
13
23
|
import { MinZoom } from '../xyflow/const'
|
|
14
|
-
import type { XYFlowInstance } from '../xyflow/types'
|
|
24
|
+
import type { XYFlowInstance, XYFlowNode } from '../xyflow/types'
|
|
15
25
|
|
|
16
26
|
export type DiagramStore = {
|
|
17
27
|
// Incoming props
|
|
@@ -20,6 +30,8 @@ export type DiagramStore = {
|
|
|
20
30
|
showElementLinks: boolean
|
|
21
31
|
fitViewEnabled: boolean
|
|
22
32
|
fitViewPadding: number
|
|
33
|
+
zoomable: boolean
|
|
34
|
+
pannable: boolean
|
|
23
35
|
nodesDraggable: boolean
|
|
24
36
|
nodesSelectable: boolean
|
|
25
37
|
|
|
@@ -30,6 +42,7 @@ export type DiagramStore = {
|
|
|
30
42
|
// This is XYFlow id's
|
|
31
43
|
lastClickedNodeId: string | null
|
|
32
44
|
lastClickedEdgeId: string | null
|
|
45
|
+
focusedNodeId: string | null
|
|
33
46
|
hoveredNodeId: string | null
|
|
34
47
|
hoveredEdgeId: string | null
|
|
35
48
|
|
|
@@ -57,6 +70,8 @@ export type DiagramInitialState = // Required properties
|
|
|
57
70
|
| 'showElementLinks'
|
|
58
71
|
| 'fitViewEnabled'
|
|
59
72
|
| 'fitViewPadding'
|
|
73
|
+
| 'zoomable'
|
|
74
|
+
| 'pannable'
|
|
60
75
|
| 'nodesDraggable'
|
|
61
76
|
| 'nodesSelectable'
|
|
62
77
|
>
|
|
@@ -78,10 +93,12 @@ interface DiagramStoreActions {
|
|
|
78
93
|
setLastClickedNode: (nodeId: string | null) => void
|
|
79
94
|
setLastClickedEdge: (edgeId: string | null) => void
|
|
80
95
|
|
|
96
|
+
resetLastClicked: () => void
|
|
97
|
+
|
|
81
98
|
getElement(id: Fqn): DiagramNode | null
|
|
82
99
|
triggerOnChange: (changes: NonEmptyArray<Change>) => void
|
|
83
100
|
triggerOnNavigateTo: (xynodeId: string, event: ReactMouseEvent) => void
|
|
84
|
-
fitDiagram: () => void
|
|
101
|
+
fitDiagram: (centerNode?: XYFlowNode) => void
|
|
85
102
|
}
|
|
86
103
|
|
|
87
104
|
export type DiagramState = Simplify<DiagramStore & DiagramStoreActions>
|
|
@@ -90,6 +107,7 @@ const DEFAULT_PROPS: Except<DiagramStore, RequiredKeysOf<DiagramInitialState> |
|
|
|
90
107
|
initialized: false,
|
|
91
108
|
previousViews: [],
|
|
92
109
|
viewportChanged: false,
|
|
110
|
+
focusedNodeId: null,
|
|
93
111
|
hoveredNodeId: null,
|
|
94
112
|
hoveredEdgeId: null,
|
|
95
113
|
lastClickedNodeId: null,
|
|
@@ -133,6 +151,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
133
151
|
view: currentView,
|
|
134
152
|
lastOnNavigate,
|
|
135
153
|
previousViews,
|
|
154
|
+
focusedNodeId,
|
|
136
155
|
lastClickedNodeId,
|
|
137
156
|
lastClickedEdgeId,
|
|
138
157
|
hoveredEdgeId,
|
|
@@ -159,6 +178,9 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
159
178
|
if (hoveredNodeId && !containsWithId(view.nodes, hoveredNodeId)) {
|
|
160
179
|
hoveredNodeId = null
|
|
161
180
|
}
|
|
181
|
+
if (focusedNodeId && !containsWithId(view.nodes, focusedNodeId)) {
|
|
182
|
+
focusedNodeId = null
|
|
183
|
+
}
|
|
162
184
|
if (lastClickedEdgeId && !containsWithId(view.edges, lastClickedEdgeId)) {
|
|
163
185
|
lastClickedEdgeId = null
|
|
164
186
|
}
|
|
@@ -171,6 +193,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
171
193
|
lastClickedNodeId = null
|
|
172
194
|
hoveredEdgeId = null
|
|
173
195
|
hoveredNodeId = null
|
|
196
|
+
focusedNodeId = null
|
|
174
197
|
|
|
175
198
|
// Update history stack (back button not implemented yet)
|
|
176
199
|
previousViews = [
|
|
@@ -186,6 +209,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
186
209
|
previousViews,
|
|
187
210
|
lastClickedNodeId,
|
|
188
211
|
lastClickedEdgeId,
|
|
212
|
+
focusedNodeId,
|
|
189
213
|
hoveredEdgeId,
|
|
190
214
|
hoveredNodeId
|
|
191
215
|
},
|
|
@@ -218,88 +242,114 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
218
242
|
}
|
|
219
243
|
},
|
|
220
244
|
|
|
245
|
+
resetLastClicked: () => {
|
|
246
|
+
let {
|
|
247
|
+
focusedNodeId,
|
|
248
|
+
lastClickedNodeId,
|
|
249
|
+
lastClickedEdgeId
|
|
250
|
+
} = get()
|
|
251
|
+
if (focusedNodeId || lastClickedNodeId || lastClickedEdgeId) {
|
|
252
|
+
set(
|
|
253
|
+
{
|
|
254
|
+
focusedNodeId: null,
|
|
255
|
+
lastClickedNodeId: null,
|
|
256
|
+
lastClickedEdgeId: null
|
|
257
|
+
},
|
|
258
|
+
noReplace,
|
|
259
|
+
'resetLastClicked'
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
|
|
221
264
|
getElement: (fqn) => {
|
|
222
265
|
const { view } = get()
|
|
223
266
|
return view.nodes.find(({ id }) => id === fqn) ?? null
|
|
224
267
|
},
|
|
225
268
|
|
|
226
269
|
triggerOnChange: (changes: NonEmptyArray<Change>) => {
|
|
270
|
+
if (DEV) {
|
|
271
|
+
console.debug('triggerOnChange', changes)
|
|
272
|
+
}
|
|
227
273
|
const newShapes = new Map<Fqn, ElementShape>()
|
|
228
274
|
const newColor = new Map<Fqn, ThemeColor>()
|
|
275
|
+
const newOpacity = new Map<Fqn, number>()
|
|
276
|
+
const newBorder = new Map<Fqn, BorderStyle>()
|
|
229
277
|
for (const change of changes) {
|
|
230
|
-
if (change.op
|
|
231
|
-
|
|
232
|
-
newShapes.set(target, change.shape)
|
|
233
|
-
}
|
|
234
|
-
continue
|
|
278
|
+
if (change.op !== 'change-element-style') {
|
|
279
|
+
nonexhaustive(change.op)
|
|
235
280
|
}
|
|
236
|
-
|
|
237
|
-
for (const
|
|
238
|
-
|
|
281
|
+
for (const target of change.targets) {
|
|
282
|
+
for (const [key, value] of entries.strict(change.style)) {
|
|
283
|
+
switch (key) {
|
|
284
|
+
case 'shape':
|
|
285
|
+
newShapes.set(target, value)
|
|
286
|
+
break
|
|
287
|
+
case 'color':
|
|
288
|
+
newColor.set(target, value)
|
|
289
|
+
break
|
|
290
|
+
case 'opacity':
|
|
291
|
+
newOpacity.set(target, value)
|
|
292
|
+
break
|
|
293
|
+
case 'border':
|
|
294
|
+
newBorder.set(target, value)
|
|
295
|
+
break
|
|
296
|
+
default:
|
|
297
|
+
nonexhaustive(key)
|
|
298
|
+
}
|
|
239
299
|
}
|
|
240
|
-
continue
|
|
241
300
|
}
|
|
242
|
-
nonexhaustive(change)
|
|
243
301
|
}
|
|
244
|
-
|
|
245
|
-
// const { view, updateView } = get()
|
|
246
|
-
// let hasChanges = false
|
|
247
|
-
// const nextNodes = view.nodes.map((node) => {
|
|
248
|
-
// const shape = newShapes.get(node.id)
|
|
249
|
-
// if (shape && shape !== node.shape) {
|
|
250
|
-
// hasChanges = true
|
|
251
|
-
// node = {
|
|
252
|
-
// ...node,
|
|
253
|
-
// shape
|
|
254
|
-
// }
|
|
255
|
-
// }
|
|
256
|
-
// const color = newColor.get(node.id)
|
|
257
|
-
// if (color && color !== node.color) {
|
|
258
|
-
// hasChanges = true
|
|
259
|
-
// node = {
|
|
260
|
-
// ...node,
|
|
261
|
-
// color
|
|
262
|
-
// }
|
|
263
|
-
// }
|
|
264
|
-
// return node
|
|
265
|
-
// })
|
|
266
|
-
// if (hasChanges) {
|
|
267
|
-
// updateView({
|
|
268
|
-
// ...view,
|
|
269
|
-
// nodes: nextNodes
|
|
270
|
-
// })
|
|
271
|
-
// }
|
|
302
|
+
|
|
272
303
|
get().xyflow.setNodes(nodes =>
|
|
273
304
|
nodes.map(node => {
|
|
274
|
-
let
|
|
275
|
-
const shape = newShapes.get(
|
|
276
|
-
if (shape && shape !==
|
|
277
|
-
|
|
278
|
-
...
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
305
|
+
let element = node.data.element
|
|
306
|
+
const shape = newShapes.get(element.id)
|
|
307
|
+
if (shape && shape !== element.shape) {
|
|
308
|
+
element = {
|
|
309
|
+
...element,
|
|
310
|
+
shape
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const color = newColor.get(element.id)
|
|
314
|
+
if (color && color !== element.color) {
|
|
315
|
+
element = {
|
|
316
|
+
...element,
|
|
317
|
+
color
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const opacity = newOpacity.get(element.id)
|
|
322
|
+
if (!!opacity && opacity !== element.style?.opacity) {
|
|
323
|
+
element = {
|
|
324
|
+
...element,
|
|
325
|
+
style: {
|
|
326
|
+
...element.style,
|
|
327
|
+
opacity
|
|
285
328
|
}
|
|
286
329
|
}
|
|
287
|
-
data = node.data
|
|
288
330
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
element
|
|
296
|
-
|
|
297
|
-
color
|
|
298
|
-
}
|
|
331
|
+
|
|
332
|
+
const border = newBorder.get(element.id)
|
|
333
|
+
if (border && border !== element.style?.border) {
|
|
334
|
+
element = {
|
|
335
|
+
...element,
|
|
336
|
+
style: {
|
|
337
|
+
...element.style,
|
|
338
|
+
border
|
|
299
339
|
}
|
|
300
340
|
}
|
|
301
341
|
}
|
|
302
|
-
|
|
342
|
+
|
|
343
|
+
if (element === node.data.element) {
|
|
344
|
+
return node
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
...node,
|
|
348
|
+
data: {
|
|
349
|
+
...node.data,
|
|
350
|
+
element
|
|
351
|
+
}
|
|
352
|
+
}
|
|
303
353
|
})
|
|
304
354
|
)
|
|
305
355
|
get().onChange?.({ changes })
|
|
@@ -336,15 +386,22 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
|
|
|
336
386
|
)
|
|
337
387
|
},
|
|
338
388
|
|
|
339
|
-
fitDiagram: () => {
|
|
340
|
-
const { fitViewPadding, xyflow } = get()
|
|
389
|
+
fitDiagram: (centerNode) => {
|
|
390
|
+
const { fitViewPadding, xyflow, focusedNodeId } = get()
|
|
341
391
|
xyflow.fitView({
|
|
342
392
|
includeHiddenNodes: true,
|
|
343
|
-
duration: 400,
|
|
393
|
+
duration: (centerNode && focusedNodeId) ? 300 : 400,
|
|
344
394
|
padding: fitViewPadding,
|
|
345
395
|
minZoom: MinZoom,
|
|
346
|
-
maxZoom: 1
|
|
396
|
+
maxZoom: 1,
|
|
397
|
+
...(centerNode && { nodes: [centerNode] })
|
|
347
398
|
})
|
|
399
|
+
if (centerNode && focusedNodeId !== centerNode.id) {
|
|
400
|
+
set({ focusedNodeId: centerNode.id }, noReplace, 'focus node')
|
|
401
|
+
}
|
|
402
|
+
if (!centerNode && !!focusedNodeId) {
|
|
403
|
+
set({ focusedNodeId: null }, noReplace, 'unfocus')
|
|
404
|
+
}
|
|
348
405
|
}
|
|
349
406
|
}),
|
|
350
407
|
{
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { globalStyle, style } from '@vanilla-extract/css'
|
|
2
|
+
import { mantine } from '../mantine.css'
|
|
3
|
+
|
|
4
|
+
export const container = style({
|
|
5
|
+
bottom: 0,
|
|
6
|
+
left: 0,
|
|
7
|
+
padding: 8,
|
|
8
|
+
margin: 0
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const paper = style({
|
|
12
|
+
cursor: 'default',
|
|
13
|
+
minWidth: 200,
|
|
14
|
+
maxWidth: 'calc(100vw - 16px)',
|
|
15
|
+
backgroundColor: `color-mix(in srgb, ${mantine.colors.body}, transparent 20%)`,
|
|
16
|
+
backdropFilter: 'blur(8px)',
|
|
17
|
+
'@media': {
|
|
18
|
+
[mantine.largerThan('md')]: {
|
|
19
|
+
maxWidth: '45vw'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const title = style({})
|
|
25
|
+
|
|
26
|
+
export const description = style({
|
|
27
|
+
whiteSpaceCollapse: 'preserve-breaks',
|
|
28
|
+
color: mantine.colors.gray[7]
|
|
29
|
+
})
|
|
30
|
+
globalStyle(`:where([data-mantine-color-scheme="dark"]) ${description}`, {
|
|
31
|
+
color: mantine.colors.gray[5]
|
|
32
|
+
})
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Anchor, Box, Button, CopyButton, Group, Paper, Spoiler, Stack, Text } from '@mantine/core'
|
|
2
|
+
import { useToggle } from '@react-hookz/web'
|
|
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
|
+
import { useLocalStorage } from '@mantine/hooks'
|
|
8
|
+
|
|
9
|
+
const selector = (s: DiagramState) => ({
|
|
10
|
+
id: s.view.id,
|
|
11
|
+
title: s.view.title ?? 'untitled',
|
|
12
|
+
description: s.view.description,
|
|
13
|
+
links: s.view.links
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export default function DiagramTitlePanel() {
|
|
17
|
+
const { id, title, description, links } = useDiagramState(selector)
|
|
18
|
+
const [isCollapsed, setCollapsed] = useLocalStorage({
|
|
19
|
+
key: 'diagram-title-panel-collapsed',
|
|
20
|
+
defaultValue: false
|
|
21
|
+
})
|
|
22
|
+
const toggle = () => setCollapsed(v => !v)
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<AnimatePresence mode="wait">
|
|
26
|
+
<motion.div
|
|
27
|
+
key={id}
|
|
28
|
+
initial={{ opacity: 0.05, scale: 0.7 }}
|
|
29
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
30
|
+
exit={{
|
|
31
|
+
opacity: 0.05,
|
|
32
|
+
scale: 0.6
|
|
33
|
+
}}
|
|
34
|
+
className={clsx('react-flow__panel', css.container)}
|
|
35
|
+
style={{
|
|
36
|
+
transformOrigin: 'left center'
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<Paper
|
|
40
|
+
radius="sm"
|
|
41
|
+
withBorder={!isCollapsed}
|
|
42
|
+
className={css.paper}
|
|
43
|
+
p={isCollapsed ? 'sm' : 'md'}
|
|
44
|
+
onDoubleClick={e => e.stopPropagation()}>
|
|
45
|
+
<Stack gap={'sm'} justify="stretch" align="stretch">
|
|
46
|
+
<Group justify="stretch" wrap="nowrap" onClick={toggle} style={{ cursor: 'pointer' }}>
|
|
47
|
+
<Text
|
|
48
|
+
component={'div'}
|
|
49
|
+
flex={'1'}
|
|
50
|
+
size={isCollapsed ? 'sm' : 'lg'}
|
|
51
|
+
fw={500}
|
|
52
|
+
lh={1.1}
|
|
53
|
+
className={css.title}
|
|
54
|
+
>
|
|
55
|
+
{title}
|
|
56
|
+
</Text>
|
|
57
|
+
<Text
|
|
58
|
+
hidden={isCollapsed}
|
|
59
|
+
component={'div'}
|
|
60
|
+
flex={'0 0 auto'}
|
|
61
|
+
inline
|
|
62
|
+
size="xs"
|
|
63
|
+
fz={9}
|
|
64
|
+
fw={500}
|
|
65
|
+
c={'dimmed'}
|
|
66
|
+
style={{
|
|
67
|
+
userSelect: 'all'
|
|
68
|
+
}}>
|
|
69
|
+
<span style={{ userSelect: 'none' }}>id:{' '}</span>
|
|
70
|
+
{id}
|
|
71
|
+
</Text>
|
|
72
|
+
</Group>
|
|
73
|
+
{!isCollapsed && (
|
|
74
|
+
<>
|
|
75
|
+
{description && (
|
|
76
|
+
<Spoiler
|
|
77
|
+
maxHeight={42}
|
|
78
|
+
showLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">show more</Button>}
|
|
79
|
+
hideLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">hide</Button>}>
|
|
80
|
+
<Text
|
|
81
|
+
component={'div'}
|
|
82
|
+
size="sm"
|
|
83
|
+
className={css.description}>
|
|
84
|
+
{description || 'no description'}
|
|
85
|
+
</Text>
|
|
86
|
+
</Spoiler>
|
|
87
|
+
)}
|
|
88
|
+
{!description && (
|
|
89
|
+
<Text
|
|
90
|
+
component={'div'}
|
|
91
|
+
size="xs"
|
|
92
|
+
c={'dimmed'}>
|
|
93
|
+
no description
|
|
94
|
+
</Text>
|
|
95
|
+
)}
|
|
96
|
+
{links && (
|
|
97
|
+
<Stack
|
|
98
|
+
gap={3}
|
|
99
|
+
justify="stretch"
|
|
100
|
+
align="stretch">
|
|
101
|
+
{links.map((link) => (
|
|
102
|
+
<Group key={link} wrap="nowrap" align='center' gap={'sm'}>
|
|
103
|
+
<Box flex={'1'} style={{ overflow: 'hidden' }}>
|
|
104
|
+
<Anchor
|
|
105
|
+
href={link}
|
|
106
|
+
target="_blank"
|
|
107
|
+
fz="xs"
|
|
108
|
+
truncate="end"
|
|
109
|
+
display={'inline-block'}
|
|
110
|
+
w={'100%'}>
|
|
111
|
+
{link}
|
|
112
|
+
</Anchor>
|
|
113
|
+
</Box>
|
|
114
|
+
<CopyButton value={link}>
|
|
115
|
+
{({ copied, copy }) => (
|
|
116
|
+
<Button
|
|
117
|
+
size="compact-xs"
|
|
118
|
+
fz={'10'}
|
|
119
|
+
variant="light"
|
|
120
|
+
onClick={copy}
|
|
121
|
+
color={copied
|
|
122
|
+
? 'teal'
|
|
123
|
+
: 'gray'}>
|
|
124
|
+
{copied ? 'copied' : 'copy'}
|
|
125
|
+
</Button>
|
|
126
|
+
)}
|
|
127
|
+
</CopyButton>
|
|
128
|
+
</Group>
|
|
129
|
+
))}
|
|
130
|
+
</Stack>
|
|
131
|
+
)}
|
|
132
|
+
</>
|
|
133
|
+
)}
|
|
134
|
+
</Stack>
|
|
135
|
+
</Paper>
|
|
136
|
+
</motion.div>
|
|
137
|
+
</AnimatePresence>
|
|
138
|
+
)
|
|
139
|
+
}
|
package/src/ui/OptionsPanel.tsx
CHANGED
|
@@ -3,11 +3,11 @@ 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 {
|
|
7
|
-
import { NodeOptions } from './options/NodeOptions'
|
|
6
|
+
import { useState } from 'react'
|
|
8
7
|
import * as styles from './OptionsPanel.css'
|
|
8
|
+
import { NodeOptions } from './options/NodeOptions'
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
export default function OptionsPanel() {
|
|
11
11
|
const [selectedNodes, setSelectedNodes] = useState([] as string[])
|
|
12
12
|
|
|
13
13
|
useOnSelectionChange({
|
|
@@ -16,26 +16,9 @@ const OptionsPanelMemo = /* @__PURE__ */ memo(function OptionsPanel() {
|
|
|
16
16
|
setSelectedNodes(prev => {
|
|
17
17
|
return shallowEqual(prev, next) ? prev : next
|
|
18
18
|
})
|
|
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
19
|
}
|
|
34
20
|
})
|
|
35
21
|
|
|
36
|
-
// useUpdateEffect(() => {
|
|
37
|
-
// setSelectedNodes([])
|
|
38
|
-
// }, [viewId])
|
|
39
22
|
|
|
40
23
|
return (
|
|
41
24
|
<AnimatePresence mode="wait">
|
|
@@ -60,6 +43,4 @@ const OptionsPanelMemo = /* @__PURE__ */ memo(function OptionsPanel() {
|
|
|
60
43
|
)}
|
|
61
44
|
</AnimatePresence>
|
|
62
45
|
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export default OptionsPanelMemo
|
|
46
|
+
}
|
package/src/ui/index.ts
CHANGED