@likec4/diagram 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/LikeC4Diagram.css.d.ts +0 -1
  2. package/dist/LikeC4Diagram.props.d.ts +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.mjs +1398 -1114
  5. package/dist/state/DiagramContext.d.ts +86 -4159
  6. package/dist/state/diagramStore.d.ts +88 -4162
  7. package/dist/state/hooks.d.ts +4 -0
  8. package/dist/state/index.d.ts +1 -2
  9. package/dist/style.css +182 -156
  10. package/dist/ui/options/EdgeOptions.d.ts +19 -0
  11. package/dist/ui/options/NodeOptions.d.ts +2 -2
  12. package/dist/xyflow/EdgeMarkers.d.ts +11 -0
  13. package/dist/xyflow/XYFlowEvents.d.ts +8 -5
  14. package/dist/xyflow/const.d.ts +3 -3
  15. package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
  16. package/dist/xyflow/edges/RelationshipEdge.d.ts +3 -2
  17. package/dist/xyflow/edges/edges.css.d.ts +1 -0
  18. package/dist/xyflow/edges/utils.d.ts +1 -0
  19. package/dist/xyflow/hooks/index.d.ts +3 -2
  20. package/dist/xyflow/hooks/useXYFlow.d.ts +13 -12
  21. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
  22. package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -4
  23. package/dist/xyflow/nodes/element/element.css.d.ts +2 -1
  24. package/dist/xyflow/types.d.ts +2 -32
  25. package/package.json +19 -19
  26. package/src/LikeC4Diagram.css.ts +0 -8
  27. package/src/LikeC4Diagram.props.ts +2 -2
  28. package/src/LikeC4Diagram.tsx +14 -20
  29. package/src/globals.css.ts +10 -3
  30. package/src/index.ts +1 -1
  31. package/src/mantine.css.ts +2 -2
  32. package/src/state/DiagramContext.tsx +44 -13
  33. package/src/state/diagramStore.ts +64 -52
  34. package/src/state/hooks.ts +5 -0
  35. package/src/state/index.ts +1 -2
  36. package/src/ui/BackwardForwardPanel.css.ts +9 -9
  37. package/src/ui/BackwardForwardPanel.tsx +1 -1
  38. package/src/ui/DiagramTitlePanel.css.ts +7 -2
  39. package/src/ui/DiagramTitlePanel.tsx +5 -3
  40. package/src/ui/DynamicViewWalkthrough.tsx +6 -4
  41. package/src/ui/OptionsPanel.tsx +37 -27
  42. package/src/ui/options/EdgeOptions.tsx +157 -0
  43. package/src/ui/options/NodeOptions.tsx +37 -29
  44. package/src/xyflow/EdgeMarkers.tsx +148 -0
  45. package/src/xyflow/FitviewOnDiagramChange.tsx +8 -4
  46. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +59 -55
  47. package/src/xyflow/SyncWithDiagram.tsx +13 -20
  48. package/src/xyflow/XYFlow.tsx +2 -4
  49. package/src/xyflow/XYFlowEvents.tsx +48 -33
  50. package/src/xyflow/XYFlowInner.tsx +1 -1
  51. package/src/xyflow/const.ts +3 -3
  52. package/src/xyflow/diagram-to-xyflow.ts +17 -41
  53. package/src/xyflow/edges/RelationshipEdge.tsx +207 -118
  54. package/src/xyflow/edges/edges.css.ts +36 -22
  55. package/src/xyflow/edges/utils.ts +10 -0
  56. package/src/xyflow/hooks/index.ts +4 -2
  57. package/src/xyflow/hooks/useLayoutConstraints.ts +3 -4
  58. package/src/xyflow/hooks/useXYFlow.ts +28 -1
  59. package/src/xyflow/nodes/compound/CompoundNode.css.ts +34 -24
  60. package/src/xyflow/nodes/compound/CompoundNode.tsx +52 -53
  61. package/src/xyflow/nodes/element/ElementIcon.tsx +11 -22
  62. package/src/xyflow/nodes/element/ElementLink.tsx +11 -16
  63. package/src/xyflow/nodes/element/ElementNode.tsx +64 -108
  64. package/src/xyflow/nodes/element/element.css.ts +30 -10
  65. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
  66. package/src/xyflow/types.ts +2 -4
  67. package/src/xyflow/utils.ts +3 -6
  68. package/dist/KeepAspectRatioContainer.d.ts +0 -9
  69. package/dist/icons/Clipboard.d.ts +0 -3
  70. package/dist/icons/Link.d.ts +0 -3
  71. package/dist/icons/ZoomIn.d.ts +0 -3
  72. package/dist/icons/index.d.ts +0 -3
  73. package/dist/mantine/theme.d.ts +0 -3
  74. package/dist/state/WhenInitialized.d.ts +0 -3
  75. package/dist/ui/Camera.d.ts +0 -3
  76. package/src/KeepAspectRatioContainer.tsx +0 -36
  77. package/src/icons/Clipboard.tsx +0 -24
  78. package/src/icons/Link.tsx +0 -23
  79. package/src/icons/ZoomIn.tsx +0 -25
  80. package/src/icons/index.ts +0 -3
  81. package/src/mantine/theme.ts +0 -20
  82. package/src/state/WhenInitialized.tsx +0 -24
  83. package/src/ui/Camera.tsx +0 -174
@@ -1,31 +1,51 @@
1
+ import { shallowEqual } from 'fast-equals'
1
2
  import { createContext, type PropsWithChildren, useEffect, useRef } from 'react'
2
- import { hasSubObject, isIncludedIn } from 'remeda'
3
+ import { hasSubObject, isNonNullish, pickBy } from 'remeda'
3
4
  import { useUpdateEffect } from '../hooks'
4
- import { useXYFlow } from '../xyflow/hooks'
5
+ import { useXYFlow, useXYStoreApi } from '../xyflow/hooks'
5
6
  import { createDiagramStore, type DiagramInitialState } from './diagramStore'
6
7
 
7
8
  export type DiagramZustandStore = ReturnType<typeof createDiagramStore>
8
9
  export const DiagramContext = createContext<DiagramZustandStore | null>(null)
9
10
 
10
- export function DiagramContextProvider({ children, view, ...props }: PropsWithChildren<DiagramInitialState>) {
11
+ export type DiagramContextProviderProps = PropsWithChildren<
12
+ DiagramInitialState & {
13
+ className: string
14
+ keepAspectRatio: boolean
15
+ }
16
+ >
17
+
18
+ export function DiagramContextProvider({
19
+ children,
20
+ view,
21
+ className,
22
+ keepAspectRatio,
23
+ ...props
24
+ }: DiagramContextProviderProps) {
25
+ const containerRef = useRef<HTMLDivElement>(null)
26
+ const xystore = useXYStoreApi()
11
27
  const xyflow = useXYFlow()
12
28
  const store = useRef<DiagramZustandStore>()
13
29
 
14
30
  if (!store.current) {
15
31
  store.current = createDiagramStore({
32
+ xystore,
16
33
  xyflow,
17
34
  view,
35
+ getContainer: () => containerRef.current,
18
36
  ...props
19
37
  })
20
38
  }
21
39
 
22
40
  useEffect(
23
41
  () => {
24
- if (store.current?.getState().xyflow !== xyflow) {
25
- store.current?.setState({ xyflow }, false, 'update xyflow')
42
+ if (!store.current) return
43
+ const state = store.current.getState()
44
+ if (state.xyflow !== xyflow || state.xystore !== xystore) {
45
+ store.current.setState({ xyflow, xystore }, false, 'update xyflow and xystore')
26
46
  }
27
47
  },
28
- [xyflow]
48
+ [xyflow, xystore]
29
49
  )
30
50
 
31
51
  useUpdateEffect(
@@ -35,16 +55,27 @@ export function DiagramContextProvider({ children, view, ...props }: PropsWithCh
35
55
  if (state.view !== view) {
36
56
  state.updateView(view)
37
57
  }
38
- if (!hasSubObject(state, props)) {
39
- store.current.setState(props, false, 'update incoming props')
58
+ const newProps = pickBy(props, isNonNullish)
59
+ if (!hasSubObject(state, newProps)) {
60
+ store.current.setState(newProps, false, 'update incoming props')
40
61
  }
41
62
  },
42
- [view, props]
63
+ [view, props],
64
+ shallowEqual
43
65
  )
44
-
45
66
  return (
46
- <DiagramContext.Provider value={store.current}>
47
- {children}
48
- </DiagramContext.Provider>
67
+ <div
68
+ ref={containerRef}
69
+ className={className}
70
+ {...(keepAspectRatio && {
71
+ style: {
72
+ aspectRatio: `${Math.ceil(view.width)}/${Math.ceil(view.height)}`,
73
+ maxHeight: Math.ceil(view.height)
74
+ }
75
+ })}>
76
+ <DiagramContext.Provider value={store.current}>
77
+ {children}
78
+ </DiagramContext.Provider>
79
+ </div>
49
80
  )
50
81
  }
@@ -17,6 +17,7 @@ import type { XYFlowEdge, XYFlowInstance, XYFlowNode } from '../xyflow/types'
17
17
  import { bezierControlPoints, isSamePoint } from '../xyflow/utils'
18
18
 
19
19
  export type DiagramStore = {
20
+ readonly storeDevId: string
20
21
  // Incoming props
21
22
  view: DiagramView
22
23
  readonly: boolean
@@ -28,11 +29,14 @@ export type DiagramStore = {
28
29
  nodesDraggable: boolean
29
30
  nodesSelectable: boolean
30
31
  experimentalEdgeEditing: boolean
32
+ // Diagram Container, see DiagramContainer.tsx
33
+ getContainer: () => HTMLDivElement | null
31
34
 
32
35
  // Internal state
33
36
  viewSyncDebounceTimeout: number | null
34
37
 
35
38
  xyflow: XYFlowInstance
39
+ xystore: XYStoreApi
36
40
  initialized: boolean
37
41
  xyflowSynced: boolean
38
42
 
@@ -82,13 +86,6 @@ export type DiagramInitialState = // Required properties
82
86
  | 'nodesSelectable'
83
87
  | 'experimentalEdgeEditing'
84
88
  >
85
- // Optional properties
86
- // & Partial<
87
- // Pick<
88
- // DiagramStore,
89
- // 'colorScheme'
90
- // >
91
- // >
92
89
  & LikeC4DiagramEventHandlers
93
90
 
94
91
  interface DiagramStoreActions {
@@ -110,9 +107,9 @@ interface DiagramStoreActions {
110
107
  triggerChangeElementStyle: (change: Changes.ChangeElementStyle) => void
111
108
 
112
109
  cancelSaveManualLayout: () => void
113
- triggerSaveManualLayout: (xystore: XYStoreApi) => void
110
+ triggerSaveManualLayout: () => void
114
111
  triggerOnNavigateTo: (xynodeId: string, event: ReactMouseEvent) => void
115
- fitDiagram: (xyStore: XYStoreApi) => void
112
+ fitDiagram: (duration?: number) => void
116
113
 
117
114
  nextDynamicStep: (increment?: number) => void
118
115
  stopDynamicView: () => void
@@ -120,7 +117,10 @@ interface DiagramStoreActions {
120
117
 
121
118
  export type DiagramState = Simplify<DiagramStore & DiagramStoreActions>
122
119
 
123
- const DEFAULT_PROPS: Except<DiagramStore, RequiredKeysOf<DiagramInitialState> | 'xyflow'> = {
120
+ const DEFAULT_PROPS: Except<
121
+ DiagramStore,
122
+ RequiredKeysOf<DiagramInitialState> | 'xystore' | 'xyflow' | 'getContainer' | 'storeDevId'
123
+ > = {
124
124
  viewSyncDebounceTimeout: null,
125
125
  initialized: false,
126
126
  xyflowSynced: false,
@@ -145,15 +145,16 @@ const DEFAULT_PROPS: Except<DiagramStore, RequiredKeysOf<DiagramInitialState> |
145
145
  onCanvasDblClick: null
146
146
  }
147
147
 
148
- type CreateDiagramStore = DiagramInitialState & Pick<DiagramStore, 'xyflow'>
148
+ export type CreateDiagramStore = DiagramInitialState & Pick<DiagramStore, 'xystore' | 'xyflow' | 'getContainer'>
149
149
 
150
150
  const noReplace = false
151
151
 
152
- let storeDevId = 1
152
+ let StoreDevId = 1
153
153
 
154
154
  const EmptyStringSet: ReadonlySet<string> = new StringSet()
155
155
 
156
156
  export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props: T) {
157
+ const storeDevId = 'DiagramStore' + String(StoreDevId++).padStart(2, '0')
157
158
  return createWithEqualityFn<
158
159
  DiagramState,
159
160
  [
@@ -164,9 +165,9 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
164
165
  subscribeWithSelector(
165
166
  devtools(
166
167
  (set, get) => ({
168
+ storeDevId,
167
169
  ...DEFAULT_PROPS,
168
170
  ...(props as CreateDiagramStore),
169
-
170
171
  isDynamicView: () => {
171
172
  return get().view?.__ === 'dynamic'
172
173
  },
@@ -188,16 +189,16 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
188
189
  hoveredNodeId
189
190
  } = get()
190
191
 
191
- if (viewSyncDebounceTimeout !== null) {
192
- clearTimeout(viewSyncDebounceTimeout)
193
- viewSyncDebounceTimeout = null
194
- }
195
-
196
192
  if (shallowEqual(current, nextView)) {
197
193
  DEV && console.debug('store: skip updateView')
198
194
  return
199
195
  }
200
196
 
197
+ if (viewSyncDebounceTimeout !== null) {
198
+ clearTimeout(viewSyncDebounceTimeout)
199
+ viewSyncDebounceTimeout = null
200
+ }
201
+
201
202
  const isSameView = current.id === nextView.id
202
203
 
203
204
  if (isSameView) {
@@ -275,7 +276,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
275
276
  dimmed
276
277
  },
277
278
  noReplace,
278
- 'update-view'
279
+ isSameView ? 'update-view [same]' : 'update-view [another]'
279
280
  )
280
281
  },
281
282
 
@@ -374,7 +375,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
374
375
  return origin
375
376
  }
376
377
  let element = origin
377
- for (const [key, value] of entries.strict(change.style)) {
378
+ for (const [key, value] of entries(change.style)) {
378
379
  switch (key) {
379
380
  case 'shape':
380
381
  if (value !== element.shape) {
@@ -443,7 +444,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
443
444
  }
444
445
  },
445
446
 
446
- triggerSaveManualLayout: (xystore) => {
447
+ triggerSaveManualLayout: () => {
447
448
  let { viewSyncDebounceTimeout: debounced, onChange } = get()
448
449
  if (!onChange) {
449
450
  DEV && console.debug('ignore triggerSaveManualLayout, as no onChange handler is set')
@@ -453,8 +454,8 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
453
454
  clearTimeout(debounced)
454
455
  }
455
456
  debounced = setTimeout(() => {
457
+ const { xyflow, onChange, xystore } = get()
456
458
  const { nodeLookup } = xystore.getState()
457
- const { xyflow, onChange } = get()
458
459
  set({ viewSyncDebounceTimeout: null })
459
460
 
460
461
  const movedNodes = new StringSet()
@@ -500,7 +501,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
500
501
  }
501
502
 
502
503
  onChange?.({ change })
503
- }, 2000)
504
+ }, 2000) as any as number // explicit typecast to number to suppress TS error in astro build
504
505
  set(
505
506
  {
506
507
  viewSyncDebounceTimeout: debounced
@@ -513,8 +514,9 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
513
514
  triggerOnNavigateTo: (xynodeId, event) => {
514
515
  const { xyflow, view } = get()
515
516
  const xynode = xyflow.getInternalNode(xynodeId)
516
- const element = xynode?.data.element as DiagramNodeWithNavigate
517
- invariant(!!xynode && element?.navigateTo, `node is not navigable: ${xynodeId}`)
517
+ invariant(!!xynode, `node not found: ${xynodeId}`)
518
+ const element = xynode.data.element as DiagramNodeWithNavigate
519
+ invariant(element.navigateTo, `node is not navigable: ${xynodeId}`)
518
520
  set(
519
521
  {
520
522
  lastClickedNodeId: xynodeId,
@@ -540,20 +542,23 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
540
542
  )
541
543
  },
542
544
 
543
- fitDiagram: (xyStore) => {
544
- const { width, height, panZoom } = xyStore.getState()
545
- const { fitViewPadding, view, focusedNodeId } = get()
545
+ fitDiagram: (duration = 500) => {
546
+ const { fitViewPadding, view, focusedNodeId, activeDynamicViewStep, xystore } = get()
547
+ const { width, height, panZoom, transform } = xystore.getState()
548
+
546
549
  const bounds = {
547
550
  x: 0,
548
551
  y: 0,
549
552
  width: view.width,
550
553
  height: view.height
551
554
  }
552
- const viewport = getViewportForBounds(bounds, width, height, MinZoom, 1, fitViewPadding)
553
- panZoom?.setViewport(viewport, { duration: 500 })
554
- if (!!focusedNodeId) {
555
+ const maxZoom = Math.max(1, transform[2])
556
+ const viewport = getViewportForBounds(bounds, width, height, MinZoom, maxZoom, fitViewPadding)
557
+ panZoom?.setViewport(viewport, { duration })
558
+ if ((focusedNodeId ?? activeDynamicViewStep) !== null) {
555
559
  set(
556
560
  {
561
+ activeDynamicViewStep: null,
557
562
  focusedNodeId: null,
558
563
  dimmed: EmptyStringSet
559
564
  },
@@ -564,7 +569,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
564
569
  },
565
570
 
566
571
  nextDynamicStep: (increment = 1) => {
567
- const { view, activeDynamicViewStep, xyflow, fitViewPadding } = get()
572
+ const { view, activeDynamicViewStep, xyflow, xystore, fitViewPadding } = get()
568
573
  invariant(view.__ === 'dynamic', 'view is not dynamic')
569
574
  const nextStep = (activeDynamicViewStep ?? 0) + increment
570
575
  if (nextStep <= 0 || nextStep > view.edges.length) {
@@ -589,36 +594,43 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
589
594
  }
590
595
  dimmed.add(n.id)
591
596
  }
592
- xyflow.fitView({
597
+ const { fitView, transform } = xystore.getState()
598
+ fitView({
593
599
  duration: 400,
594
- maxZoom: 1,
600
+ includeHiddenNodes: true,
601
+ maxZoom: Math.max(1, transform[2]),
595
602
  minZoom: MinZoom,
596
- padding: fitViewPadding,
603
+ padding: Math.max(fitViewPadding, 0.2),
597
604
  nodes: selected
598
605
  })
599
- set({
600
- focusedNodeId: null,
601
- activeDynamicViewStep: nextStep,
602
- dimmed
603
- })
606
+ set(
607
+ {
608
+ focusedNodeId: null,
609
+ activeDynamicViewStep: nextStep,
610
+ dimmed
611
+ },
612
+ noReplace,
613
+ 'nextDynamicStep'
614
+ )
604
615
  },
605
616
 
606
617
  stopDynamicView: () => {
607
- const { xyflow, fitViewPadding } = get()
608
- set({
609
- activeDynamicViewStep: null,
610
- dimmed: EmptyStringSet
611
- })
612
- xyflow.fitView({
613
- duration: 400,
614
- maxZoom: 1,
615
- minZoom: MinZoom,
616
- padding: fitViewPadding
617
- })
618
+ if (get().activeDynamicViewStep !== null) {
619
+ set(
620
+ {
621
+ activeDynamicViewStep: null,
622
+ focusedNodeId: null,
623
+ dimmed: EmptyStringSet
624
+ },
625
+ noReplace,
626
+ 'stopDynamicView'
627
+ )
628
+ get().fitDiagram()
629
+ }
618
630
  }
619
631
  }),
620
632
  {
621
- name: `DiagramStore ${storeDevId++} - ${props.view.id}`,
633
+ name: `${storeDevId} - ${props.view.id}`,
622
634
  enabled: DEV
623
635
  }
624
636
  )
@@ -0,0 +1,5 @@
1
+ export { useDiagramState, useDiagramStoreApi } from './useDiagramStore'
2
+ export { useNavigationHistory } from './useNavigationHistory'
3
+
4
+ export type * from './diagramStore'
5
+ export type * from './useDiagramStore'
@@ -1,3 +1,2 @@
1
- export { DiagramContextProvider } from './DiagramContext'
2
1
  export type * from './diagramStore'
3
- export * from './useDiagramStore'
2
+ export type * from './useDiagramStore'
@@ -3,20 +3,20 @@ import { mantine } from '../mantine.css'
3
3
  import { vars } from '../theme.css'
4
4
 
5
5
  export const panel = style({
6
- top: fallbackVar(vars.navigationPanel.top, '1rem'),
6
+ top: fallbackVar(vars.navigationPanel.top, '0.8rem'),
7
7
  left: fallbackVar(vars.navigationPanel.left, '1rem'),
8
8
  margin: 0
9
9
  })
10
10
 
11
11
  globalStyle(`${panel} .mantine-ActionIcon-root`, {
12
12
  vars: {
13
- ['--ai-size']: 'var(--ai-size-md)'
14
- },
15
- '@media': {
16
- [mantine.largerThan('md')]: {
17
- vars: {
18
- ['--ai-size']: 'var(--ai-size-lg)'
19
- }
20
- }
13
+ ['--ai-size']: 'var(--ai-size-lg)'
21
14
  }
15
+ // '@media': {
16
+ // [mantine.largerThan('lg')]: {
17
+ // vars: {
18
+ // ['--ai-size']: 'var(--ai-size-xl)'
19
+ // }
20
+ // }
21
+ // }
22
22
  })
@@ -2,7 +2,7 @@ import { ActionIcon, Group } from '@mantine/core'
2
2
  import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react'
3
3
  import clsx from 'clsx'
4
4
  import { AnimatePresence, m } from 'framer-motion'
5
- import { useNavigationHistory } from '../state/useNavigationHistory'
5
+ import { useNavigationHistory } from '../state/hooks'
6
6
  import * as css from './BackwardForwardPanel.css'
7
7
 
8
8
  export function BackwardForwardPanel() {
@@ -11,14 +11,19 @@ export const container = style({
11
11
 
12
12
  export const card = style({
13
13
  cursor: 'default',
14
- minWidth: 150,
14
+ minWidth: 200,
15
15
  maxWidth: 'calc(100vw - 16px)',
16
16
  backgroundColor: `color-mix(in srgb, ${mantine.colors.body}, transparent 20%)`,
17
17
  WebkitBackdropFilter: fallbackVar(vars.safariAnimationHook, 'blur(8px)'),
18
18
  backdropFilter: fallbackVar(vars.safariAnimationHook, 'blur(8px)'),
19
19
  '@media': {
20
+ [mantine.largerThan('sm')]: {
21
+ minWidth: 300,
22
+ maxWidth: '90vw'
23
+ },
20
24
  [mantine.largerThan('md')]: {
21
- maxWidth: '45vw'
25
+ minWidth: 400,
26
+ maxWidth: '70vw'
22
27
  }
23
28
  },
24
29
  selectors: {
@@ -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 '../state'
6
+ import { type DiagramState, useDiagramState } from '../state/hooks'
7
7
  import * as css from './DiagramTitlePanel.css'
8
8
 
9
9
  const selector = (s: DiagramState) => ({
@@ -39,16 +39,18 @@ export default function DiagramTitlePanel() {
39
39
  <Card
40
40
  radius="sm"
41
41
  className={css.card}
42
+ withBorder
42
43
  p={'md'}
44
+ pb={isCollapsed ? 'sm' : 'md'}
43
45
  onDoubleClick={e => e.stopPropagation()}>
44
- <CardSection mb={4}>
46
+ <CardSection mb={isCollapsed ? 10 : 'sm'}>
45
47
  <Button
46
48
  fullWidth
47
49
  size="xs"
48
50
  h={'sm'}
49
51
  py={2}
50
52
  radius={'0'}
51
- variant="subtle"
53
+ variant={isCollapsed ? 'light' : 'subtle'}
52
54
  color="gray"
53
55
  onClick={toggle}
54
56
  >
@@ -1,4 +1,4 @@
1
- import { Box, Button } from '@mantine/core'
1
+ import { Box, Button, ButtonGroup } from '@mantine/core'
2
2
  import { useHotkeys } from '@mantine/hooks'
3
3
  import {
4
4
  IconPlayerPlayFilled,
@@ -8,7 +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 '../state/useDiagramStore'
11
+ import { useDiagramState } from '../state/hooks'
12
12
  import { useXYStore } from '../xyflow/hooks/useXYFlow'
13
13
  import * as css from './DynamicViewWalkthrough.css'
14
14
 
@@ -58,6 +58,8 @@ export function DynamicViewWalkthrough() {
58
58
  return (
59
59
  <Box
60
60
  className={clsx('react-flow__panel', css.container)}
61
+ onClick={(e) => e.stopPropagation()}
62
+ onDoubleClick={(e) => e.stopPropagation()}
61
63
  >
62
64
  {isNullish(activeDynamicViewStep)
63
65
  && (
@@ -71,7 +73,7 @@ export function DynamicViewWalkthrough() {
71
73
  </Button>
72
74
  )}
73
75
  {isNumber(activeDynamicViewStep) && (
74
- <Button.Group className={css.buttons}>
76
+ <ButtonGroup className={css.buttons}>
75
77
  <Button
76
78
  {...buttonProps}
77
79
  pl={'lg'}
@@ -98,7 +100,7 @@ export function DynamicViewWalkthrough() {
98
100
  <IconPlayerSkipForwardFilled />
99
101
  </Button>
100
102
  )}
101
- </Button.Group>
103
+ </ButtonGroup>
102
104
  )}
103
105
  </Box>
104
106
  )
@@ -1,38 +1,42 @@
1
1
  import { Card } from '@mantine/core'
2
+ import { useOnSelectionChange } from '@xyflow/react'
2
3
  import clsx from 'clsx'
3
- import { DEV } from 'esm-env'
4
- import { shallowEqual } from 'fast-equals'
4
+ import { deepEqual as eq } from 'fast-equals'
5
5
  import { AnimatePresence, m } from 'framer-motion'
6
- import { useCallback } from 'react'
7
- import { reduce } from 'remeda'
8
- import useTilg from 'tilg'
9
- import { useDiagramState } from '../state'
10
- import { useXYStore } from '../xyflow/hooks'
6
+ import { useState } from 'react'
7
+ import { useDiagramState } from '../state/hooks'
11
8
  import { NodeOptions } from './options/NodeOptions'
12
9
  import * as styles from './OptionsPanel.css'
13
10
 
14
- const Empty = [] as string[]
15
11
  export default function OptionsPanel() {
16
- const { isFocusDisabled, viewId } = useDiagramState(s => ({
12
+ const { isFocused, viewId } = useDiagramState(s => ({
17
13
  viewId: s.view.id,
18
- isFocusDisabled: s.focusedNodeId === null && s.activeDynamicViewStep === null
14
+ isFocused: s.focusedNodeId !== null || s.activeDynamicViewStep !== null
19
15
  }))
20
- const selectedNodes = useXYStore(
21
- useCallback(s =>
22
- s.elementsSelectable && isFocusDisabled
23
- ? reduce(s.nodes, (acc, n) => {
24
- if (n.selected) {
25
- acc.push(n.id)
26
- }
27
- return acc
28
- }, [] as string[])
29
- : Empty, [isFocusDisabled]),
30
- shallowEqual
31
- )
32
- DEV && useTilg()
16
+ const [selected, setSelected] = useState<{
17
+ nodes: string[]
18
+ edges: string[]
19
+ }>({
20
+ nodes: [],
21
+ edges: []
22
+ })
23
+ useOnSelectionChange({
24
+ onChange: ({ nodes, edges }) => {
25
+ const newSelected = {
26
+ nodes: nodes.map(n => n.id).sort(),
27
+ edges: edges.map(e => e.id).sort()
28
+ }
29
+ if (!eq(selected, newSelected)) {
30
+ setSelected(newSelected)
31
+ }
32
+ }
33
+ })
34
+ const { nodes } = selected
35
+ // const hasAnySelection = !isFocused && (nodes.length > 0 || edges.length > 0)
36
+ const hasAnySelection = !isFocused && nodes.length > 0
33
37
  return (
34
38
  <AnimatePresence mode="wait">
35
- {selectedNodes.length > 0 && (
39
+ {hasAnySelection && (
36
40
  <m.div
37
41
  key={viewId}
38
42
  initial={{ opacity: 0, scale: 0.85 }}
@@ -44,11 +48,17 @@ export default function OptionsPanel() {
44
48
  transition={{ duration: 0.14 }}
45
49
  className={clsx('react-flow__panel', styles.panel)}
46
50
  style={{
47
- transformOrigin: 'center right'
51
+ transformOrigin: 'center right',
52
+ maxWidth: '320px'
48
53
  }}
49
54
  >
50
- <Card shadow="sm">
51
- <NodeOptions selectedNodeIds={selectedNodes} />
55
+ <Card shadow="md">
56
+ {nodes.length > 0 && <NodeOptions selectedNodeIds={nodes} />}
57
+ {
58
+ /* nodes.length === 0 && edges.length > 0 && (
59
+ <EdgeOptions selectedEdgeIds={edges} />
60
+ ) */
61
+ }
52
62
  </Card>
53
63
  </m.div>
54
64
  )}