@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.
Files changed (50) hide show
  1. package/dist/KeepAspectRatioContainer.d.ts +9 -0
  2. package/dist/LikeC4Diagram.css.d.ts +1 -0
  3. package/dist/LikeC4Diagram.d.ts +1 -1
  4. package/dist/LikeC4Diagram.props.d.ts +16 -11
  5. package/dist/index.mjs +639 -762
  6. package/dist/state/DiagramContext.d.ts +54 -18
  7. package/dist/state/diagramStore.d.ts +61 -21
  8. package/dist/state/useDiagramStore.d.ts +54 -18
  9. package/dist/style.css +690 -79
  10. package/dist/ui/DiagramTitlePanel.css.d.ts +4 -0
  11. package/dist/ui/DiagramTitlePanel.d.ts +2 -0
  12. package/dist/ui/OptionsPanel.d.ts +1 -2
  13. package/dist/ui/index.d.ts +0 -1
  14. package/dist/xyflow/SelectEdgesOnNodeFocus.d.ts +1 -0
  15. package/dist/xyflow/{XYFlowWrapper.d.ts → XYFlow.d.ts} +2 -2
  16. package/dist/xyflow/XYFlowEvents.d.ts +133 -13
  17. package/dist/xyflow/edges/RelationshipEdge.d.ts +2 -2
  18. package/dist/xyflow/edges/edges.css.d.ts +1 -0
  19. package/dist/xyflow/hooks/useXYFlow.d.ts +20 -20
  20. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +7 -5
  21. package/dist/xyflow/types.d.ts +2 -2
  22. package/package.json +5 -3
  23. package/src/{ui/KeepAspectRatio.tsx → KeepAspectRatioContainer.tsx} +7 -6
  24. package/src/LikeC4Diagram.css.ts +8 -1
  25. package/src/LikeC4Diagram.props.ts +17 -12
  26. package/src/LikeC4Diagram.tsx +9 -6
  27. package/src/StaticLikeC4Diagram.tsx +1 -0
  28. package/src/globals.css.ts +48 -38
  29. package/src/state/diagramStore.ts +125 -68
  30. package/src/ui/DiagramTitlePanel.css.ts +32 -0
  31. package/src/ui/DiagramTitlePanel.tsx +139 -0
  32. package/src/ui/OptionsPanel.tsx +4 -23
  33. package/src/ui/index.ts +0 -1
  34. package/src/ui/options/NodeOptions.tsx +107 -30
  35. package/src/xyflow/FitviewOnDiagramChange.tsx +26 -20
  36. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +57 -0
  37. package/src/xyflow/SyncWithDiagram.tsx +35 -26
  38. package/src/xyflow/{XYFlowWrapper.tsx → XYFlow.tsx} +24 -27
  39. package/src/xyflow/XYFlowEvents.tsx +175 -81
  40. package/src/xyflow/diagram-to-xyflow.ts +4 -4
  41. package/src/xyflow/edges/RelationshipEdge.tsx +43 -47
  42. package/src/xyflow/edges/edges.css.ts +77 -36
  43. package/src/xyflow/nodes/compound/CompoundNode.css.ts +94 -44
  44. package/src/xyflow/nodes/compound/CompoundNode.tsx +29 -31
  45. package/src/xyflow/nodes/element/ElementNode.tsx +2 -2
  46. package/src/xyflow/nodes/element/element.css.ts +1 -0
  47. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +2 -1
  48. package/dist/ui/KeepAspectRatio.css.d.ts +0 -1
  49. package/dist/ui/KeepAspectRatio.d.ts +0 -9
  50. package/src/ui/KeepAspectRatio.css.ts +0 -7
@@ -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 { omit } from 'remeda'
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
- // globalStyle(`${scope} *, ${scope} *::before, ${scope} *::after`, {
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 as ThemeColor],
96
+ element: defaultTheme.elements[color],
123
97
  compound: {
124
- titleColor: defaultTheme.elements[color as ThemeColor].loContrast
98
+ titleColor: defaultTheme.elements[color].loContrast
125
99
  },
126
- relation: defaultTheme.relationships[color as ThemeColor]
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 { DiagramNode, DiagramView, ElementShape, Fqn, NonEmptyArray, ThemeColor, ViewID } from '@likec4/core'
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 === 'change-shape') {
231
- for (const target of change.targets) {
232
- newShapes.set(target, change.shape)
233
- }
234
- continue
278
+ if (change.op !== 'change-element-style') {
279
+ nonexhaustive(change.op)
235
280
  }
236
- if (change.op === 'change-color') {
237
- for (const target of change.targets) {
238
- newColor.set(target, change.color)
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
- // TODO: Update positions/sizes
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 { data } = node
275
- const shape = newShapes.get(data.fqn)
276
- if (shape && shape !== data.element.shape) {
277
- node = {
278
- ...node,
279
- data: {
280
- ...data,
281
- element: {
282
- ...data.element,
283
- shape
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
- const color = newColor.get(data.fqn)
290
- if (color && color !== data.element.color) {
291
- node = {
292
- ...node,
293
- data: {
294
- ...data,
295
- element: {
296
- ...data.element,
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
- return node
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
+ }
@@ -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 { memo, useState } from 'react'
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
- const OptionsPanelMemo = /* @__PURE__ */ memo(function OptionsPanel() {
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
@@ -1,3 +1,2 @@
1
1
  // export { default as Camera } from './Camera'
2
- export { KeepAspectRatio } from './KeepAspectRatio'
3
2
  export { default as OptionsPanel } from './OptionsPanel'