@likec4/diagram 1.4.0 → 1.6.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 (87) hide show
  1. package/LICENSE +1 -1
  2. package/dist/LikeC4Diagram.css.d.ts +0 -1
  3. package/dist/LikeC4Diagram.d.ts +1 -1
  4. package/dist/LikeC4Diagram.props.d.ts +19 -4
  5. package/dist/StaticLikeC4Diagram.d.ts +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.mjs +3937 -3646
  8. package/dist/state/DiagramContext.d.ts +95 -4159
  9. package/dist/state/diagramStore.d.ts +100 -4164
  10. package/dist/state/hooks.d.ts +4 -0
  11. package/dist/state/index.d.ts +1 -2
  12. package/dist/style.css +281 -191
  13. package/dist/ui/options/EdgeOptions.d.ts +19 -0
  14. package/dist/ui/options/NodeOptions.d.ts +2 -2
  15. package/dist/xyflow/EdgeMarkers.d.ts +11 -0
  16. package/dist/xyflow/XYFlowEvents.d.ts +8 -5
  17. package/dist/xyflow/const.d.ts +3 -3
  18. package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
  19. package/dist/xyflow/edges/RelationshipEdge.d.ts +3 -2
  20. package/dist/xyflow/edges/edges.css.d.ts +1 -0
  21. package/dist/xyflow/edges/utils.d.ts +1 -0
  22. package/dist/xyflow/hooks/index.d.ts +3 -2
  23. package/dist/xyflow/hooks/useXYFlow.d.ts +13 -12
  24. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
  25. package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +5 -0
  26. package/dist/xyflow/nodes/element/element.css.d.ts +5 -2
  27. package/dist/xyflow/types.d.ts +2 -32
  28. package/package.json +19 -19
  29. package/src/LikeC4Diagram.css.ts +0 -8
  30. package/src/LikeC4Diagram.props.ts +23 -5
  31. package/src/LikeC4Diagram.tsx +18 -21
  32. package/src/StaticLikeC4Diagram.tsx +1 -0
  33. package/src/globals.css.ts +10 -3
  34. package/src/index.ts +1 -1
  35. package/src/mantine.css.ts +2 -2
  36. package/src/state/DiagramContext.tsx +44 -13
  37. package/src/state/diagramStore.ts +72 -53
  38. package/src/state/hooks.ts +5 -0
  39. package/src/state/index.ts +1 -2
  40. package/src/ui/BackwardForwardPanel.css.ts +9 -9
  41. package/src/ui/DiagramTitlePanel.css.ts +7 -2
  42. package/src/ui/DiagramTitlePanel.tsx +5 -3
  43. package/src/ui/DynamicViewWalkthrough.tsx +6 -4
  44. package/src/ui/OptionsPanel.tsx +37 -27
  45. package/src/ui/options/EdgeOptions.tsx +157 -0
  46. package/src/ui/options/NodeOptions.tsx +37 -29
  47. package/src/xyflow/EdgeMarkers.tsx +148 -0
  48. package/src/xyflow/FitviewOnDiagramChange.tsx +9 -5
  49. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +59 -55
  50. package/src/xyflow/SyncWithDiagram.tsx +13 -20
  51. package/src/xyflow/XYFlow.tsx +3 -4
  52. package/src/xyflow/XYFlowEvents.tsx +48 -33
  53. package/src/xyflow/const.ts +3 -3
  54. package/src/xyflow/diagram-to-xyflow.ts +17 -41
  55. package/src/xyflow/edges/RelationshipEdge.tsx +208 -119
  56. package/src/xyflow/edges/edges.css.ts +40 -26
  57. package/src/xyflow/edges/utils.ts +10 -0
  58. package/src/xyflow/hooks/index.ts +4 -2
  59. package/src/xyflow/hooks/useLayoutConstraints.ts +3 -4
  60. package/src/xyflow/hooks/useXYFlow.ts +28 -1
  61. package/src/xyflow/nodes/compound/CompoundNode.css.ts +34 -24
  62. package/src/xyflow/nodes/compound/CompoundNode.tsx +52 -53
  63. package/src/xyflow/nodes/element/ElementLink.tsx +12 -16
  64. package/src/xyflow/nodes/element/ElementNode.tsx +118 -114
  65. package/src/xyflow/nodes/element/ElementShapeSvg.tsx +2 -2
  66. package/src/xyflow/nodes/element/element.css.ts +147 -55
  67. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
  68. package/src/xyflow/types.ts +2 -4
  69. package/src/xyflow/utils.ts +3 -6
  70. package/dist/KeepAspectRatioContainer.d.ts +0 -9
  71. package/dist/icons/Clipboard.d.ts +0 -3
  72. package/dist/icons/Link.d.ts +0 -3
  73. package/dist/icons/ZoomIn.d.ts +0 -3
  74. package/dist/icons/index.d.ts +0 -3
  75. package/dist/mantine/theme.d.ts +0 -3
  76. package/dist/state/WhenInitialized.d.ts +0 -3
  77. package/dist/ui/Camera.d.ts +0 -3
  78. package/dist/xyflow/nodes/element/ElementIcon.d.ts +0 -10
  79. package/src/KeepAspectRatioContainer.tsx +0 -36
  80. package/src/icons/Clipboard.tsx +0 -24
  81. package/src/icons/Link.tsx +0 -23
  82. package/src/icons/ZoomIn.tsx +0 -25
  83. package/src/icons/index.ts +0 -3
  84. package/src/mantine/theme.ts +0 -20
  85. package/src/state/WhenInitialized.tsx +0 -24
  86. package/src/ui/Camera.tsx +0 -174
  87. package/src/xyflow/nodes/element/ElementIcon.tsx +0 -44
@@ -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/useXYFlow'
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
  }
@@ -10,13 +10,19 @@ import type { Exact, Except, RequiredKeysOf, Simplify } from 'type-fest'
10
10
  import { devtools, subscribeWithSelector } from 'zustand/middleware'
11
11
  import { shallow } from 'zustand/shallow'
12
12
  import { createWithEqualityFn } from 'zustand/traditional'
13
- import type { Changes, DiagramNodeWithNavigate, LikeC4DiagramEventHandlers } from '../LikeC4Diagram.props'
13
+ import type {
14
+ Changes,
15
+ DiagramNodeWithNavigate,
16
+ ElementIconRenderer,
17
+ LikeC4DiagramEventHandlers
18
+ } from '../LikeC4Diagram.props'
14
19
  import { MinZoom } from '../xyflow/const'
15
20
  import type { XYStoreApi } from '../xyflow/hooks'
16
21
  import type { XYFlowEdge, XYFlowInstance, XYFlowNode } from '../xyflow/types'
17
22
  import { bezierControlPoints, isSamePoint } from '../xyflow/utils'
18
23
 
19
24
  export type DiagramStore = {
25
+ readonly storeDevId: string
20
26
  // Incoming props
21
27
  view: DiagramView
22
28
  readonly: boolean
@@ -28,11 +34,15 @@ export type DiagramStore = {
28
34
  nodesDraggable: boolean
29
35
  nodesSelectable: boolean
30
36
  experimentalEdgeEditing: boolean
37
+ renderIcon: ElementIconRenderer | null
38
+ // Diagram Container, see DiagramContainer.tsx
39
+ getContainer: () => HTMLDivElement | null
31
40
 
32
41
  // Internal state
33
42
  viewSyncDebounceTimeout: number | null
34
43
 
35
44
  xyflow: XYFlowInstance
45
+ xystore: XYStoreApi
36
46
  initialized: boolean
37
47
  xyflowSynced: boolean
38
48
 
@@ -72,6 +82,7 @@ export type DiagramInitialState = // Required properties
72
82
  & Pick<
73
83
  DiagramStore,
74
84
  | 'view'
85
+ | 'renderIcon'
75
86
  | 'readonly'
76
87
  | 'showElementLinks'
77
88
  | 'fitViewEnabled'
@@ -82,13 +93,6 @@ export type DiagramInitialState = // Required properties
82
93
  | 'nodesSelectable'
83
94
  | 'experimentalEdgeEditing'
84
95
  >
85
- // Optional properties
86
- // & Partial<
87
- // Pick<
88
- // DiagramStore,
89
- // 'colorScheme'
90
- // >
91
- // >
92
96
  & LikeC4DiagramEventHandlers
93
97
 
94
98
  interface DiagramStoreActions {
@@ -110,9 +114,9 @@ interface DiagramStoreActions {
110
114
  triggerChangeElementStyle: (change: Changes.ChangeElementStyle) => void
111
115
 
112
116
  cancelSaveManualLayout: () => void
113
- triggerSaveManualLayout: (xystore: XYStoreApi) => void
117
+ triggerSaveManualLayout: () => void
114
118
  triggerOnNavigateTo: (xynodeId: string, event: ReactMouseEvent) => void
115
- fitDiagram: (xyStore: XYStoreApi) => void
119
+ fitDiagram: (duration?: number) => void
116
120
 
117
121
  nextDynamicStep: (increment?: number) => void
118
122
  stopDynamicView: () => void
@@ -120,7 +124,10 @@ interface DiagramStoreActions {
120
124
 
121
125
  export type DiagramState = Simplify<DiagramStore & DiagramStoreActions>
122
126
 
123
- const DEFAULT_PROPS: Except<DiagramStore, RequiredKeysOf<DiagramInitialState> | 'xyflow'> = {
127
+ const DEFAULT_PROPS: Except<
128
+ DiagramStore,
129
+ RequiredKeysOf<DiagramInitialState> | 'xystore' | 'xyflow' | 'getContainer' | 'storeDevId'
130
+ > = {
124
131
  viewSyncDebounceTimeout: null,
125
132
  initialized: false,
126
133
  xyflowSynced: false,
@@ -145,15 +152,16 @@ const DEFAULT_PROPS: Except<DiagramStore, RequiredKeysOf<DiagramInitialState> |
145
152
  onCanvasDblClick: null
146
153
  }
147
154
 
148
- type CreateDiagramStore = DiagramInitialState & Pick<DiagramStore, 'xyflow'>
155
+ export type CreateDiagramStore = DiagramInitialState & Pick<DiagramStore, 'xystore' | 'xyflow' | 'getContainer'>
149
156
 
150
157
  const noReplace = false
151
158
 
152
- let storeDevId = 1
159
+ let StoreDevId = 1
153
160
 
154
161
  const EmptyStringSet: ReadonlySet<string> = new StringSet()
155
162
 
156
163
  export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props: T) {
164
+ const storeDevId = 'DiagramStore' + String(StoreDevId++).padStart(2, '0')
157
165
  return createWithEqualityFn<
158
166
  DiagramState,
159
167
  [
@@ -164,9 +172,9 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
164
172
  subscribeWithSelector(
165
173
  devtools(
166
174
  (set, get) => ({
175
+ storeDevId,
167
176
  ...DEFAULT_PROPS,
168
177
  ...(props as CreateDiagramStore),
169
-
170
178
  isDynamicView: () => {
171
179
  return get().view?.__ === 'dynamic'
172
180
  },
@@ -188,16 +196,16 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
188
196
  hoveredNodeId
189
197
  } = get()
190
198
 
191
- if (viewSyncDebounceTimeout !== null) {
192
- clearTimeout(viewSyncDebounceTimeout)
193
- viewSyncDebounceTimeout = null
194
- }
195
-
196
199
  if (shallowEqual(current, nextView)) {
197
200
  DEV && console.debug('store: skip updateView')
198
201
  return
199
202
  }
200
203
 
204
+ if (viewSyncDebounceTimeout !== null) {
205
+ clearTimeout(viewSyncDebounceTimeout)
206
+ viewSyncDebounceTimeout = null
207
+ }
208
+
201
209
  const isSameView = current.id === nextView.id
202
210
 
203
211
  if (isSameView) {
@@ -275,7 +283,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
275
283
  dimmed
276
284
  },
277
285
  noReplace,
278
- 'update-view'
286
+ isSameView ? 'update-view [same]' : 'update-view [another]'
279
287
  )
280
288
  },
281
289
 
@@ -374,7 +382,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
374
382
  return origin
375
383
  }
376
384
  let element = origin
377
- for (const [key, value] of entries.strict(change.style)) {
385
+ for (const [key, value] of entries(change.style)) {
378
386
  switch (key) {
379
387
  case 'shape':
380
388
  if (value !== element.shape) {
@@ -443,7 +451,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
443
451
  }
444
452
  },
445
453
 
446
- triggerSaveManualLayout: (xystore) => {
454
+ triggerSaveManualLayout: () => {
447
455
  let { viewSyncDebounceTimeout: debounced, onChange } = get()
448
456
  if (!onChange) {
449
457
  DEV && console.debug('ignore triggerSaveManualLayout, as no onChange handler is set')
@@ -453,8 +461,8 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
453
461
  clearTimeout(debounced)
454
462
  }
455
463
  debounced = setTimeout(() => {
464
+ const { xyflow, onChange, xystore } = get()
456
465
  const { nodeLookup } = xystore.getState()
457
- const { xyflow, onChange } = get()
458
466
  set({ viewSyncDebounceTimeout: null })
459
467
 
460
468
  const movedNodes = new StringSet()
@@ -500,7 +508,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
500
508
  }
501
509
 
502
510
  onChange?.({ change })
503
- }, 2000)
511
+ }, 2000) as any as number // explicit typecast to number to suppress TS error in astro build
504
512
  set(
505
513
  {
506
514
  viewSyncDebounceTimeout: debounced
@@ -513,8 +521,9 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
513
521
  triggerOnNavigateTo: (xynodeId, event) => {
514
522
  const { xyflow, view } = get()
515
523
  const xynode = xyflow.getInternalNode(xynodeId)
516
- const element = xynode?.data.element as DiagramNodeWithNavigate
517
- invariant(!!xynode && element?.navigateTo, `node is not navigable: ${xynodeId}`)
524
+ invariant(!!xynode, `node not found: ${xynodeId}`)
525
+ const element = xynode.data.element as DiagramNodeWithNavigate
526
+ invariant(element.navigateTo, `node is not navigable: ${xynodeId}`)
518
527
  set(
519
528
  {
520
529
  lastClickedNodeId: xynodeId,
@@ -540,20 +549,23 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
540
549
  )
541
550
  },
542
551
 
543
- fitDiagram: (xyStore) => {
544
- const { width, height, panZoom } = xyStore.getState()
545
- const { fitViewPadding, view, focusedNodeId } = get()
552
+ fitDiagram: (duration = 500) => {
553
+ const { fitViewPadding, view, focusedNodeId, activeDynamicViewStep, xystore } = get()
554
+ const { width, height, panZoom, transform } = xystore.getState()
555
+
546
556
  const bounds = {
547
557
  x: 0,
548
558
  y: 0,
549
559
  width: view.width,
550
560
  height: view.height
551
561
  }
552
- const viewport = getViewportForBounds(bounds, width, height, MinZoom, 1, fitViewPadding)
553
- panZoom?.setViewport(viewport, { duration: 500 })
554
- if (!!focusedNodeId) {
562
+ const maxZoom = Math.max(1, transform[2])
563
+ const viewport = getViewportForBounds(bounds, width, height, MinZoom, maxZoom, fitViewPadding)
564
+ panZoom?.setViewport(viewport, { duration })
565
+ if ((focusedNodeId ?? activeDynamicViewStep) !== null) {
555
566
  set(
556
567
  {
568
+ activeDynamicViewStep: null,
557
569
  focusedNodeId: null,
558
570
  dimmed: EmptyStringSet
559
571
  },
@@ -564,7 +576,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
564
576
  },
565
577
 
566
578
  nextDynamicStep: (increment = 1) => {
567
- const { view, activeDynamicViewStep, xyflow, fitViewPadding } = get()
579
+ const { view, activeDynamicViewStep, xyflow, xystore, fitViewPadding } = get()
568
580
  invariant(view.__ === 'dynamic', 'view is not dynamic')
569
581
  const nextStep = (activeDynamicViewStep ?? 0) + increment
570
582
  if (nextStep <= 0 || nextStep > view.edges.length) {
@@ -589,36 +601,43 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
589
601
  }
590
602
  dimmed.add(n.id)
591
603
  }
592
- xyflow.fitView({
604
+ const { fitView, transform } = xystore.getState()
605
+ fitView({
593
606
  duration: 400,
594
- maxZoom: 1,
607
+ includeHiddenNodes: true,
608
+ maxZoom: Math.max(1, transform[2]),
595
609
  minZoom: MinZoom,
596
- padding: fitViewPadding,
610
+ padding: Math.max(fitViewPadding, 0.2),
597
611
  nodes: selected
598
612
  })
599
- set({
600
- focusedNodeId: null,
601
- activeDynamicViewStep: nextStep,
602
- dimmed
603
- })
613
+ set(
614
+ {
615
+ focusedNodeId: null,
616
+ activeDynamicViewStep: nextStep,
617
+ dimmed
618
+ },
619
+ noReplace,
620
+ 'nextDynamicStep'
621
+ )
604
622
  },
605
623
 
606
624
  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
- })
625
+ if (get().activeDynamicViewStep !== null) {
626
+ set(
627
+ {
628
+ activeDynamicViewStep: null,
629
+ focusedNodeId: null,
630
+ dimmed: EmptyStringSet
631
+ },
632
+ noReplace,
633
+ 'stopDynamicView'
634
+ )
635
+ get().fitDiagram()
636
+ }
618
637
  }
619
638
  }),
620
639
  {
621
- name: `DiagramStore ${storeDevId++} - ${props.view.id}`,
640
+ name: `${storeDevId} - ${props.view.id}`,
622
641
  enabled: DEV
623
642
  }
624
643
  )
@@ -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
  })
@@ -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
  )}