@likec4/diagram 1.1.0 → 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.
@@ -1,10 +1,11 @@
1
1
  import { XYFlowNode } from './types';
2
2
 
3
3
  export declare function useXYFlowEvents(): {
4
- onDoubleClick: import('react').MouseEventHandler<HTMLDivElement> | undefined;
5
- onNodeClick: import('@xyflow/react').NodeMouseHandler<XYFlowNode>;
6
- onNodeContextMenu: import('@xyflow/react').NodeMouseHandler<XYFlowNode>;
7
- onEdgeClick: (event: import('react').MouseEvent<Element, MouseEvent>, edge: {
4
+ onDoubleClick: (event: import('react').MouseEvent<HTMLDivElement, MouseEvent>) => void;
5
+ onPaneClick: (event: import('react').MouseEvent<Element, MouseEvent>) => void;
6
+ onNodeContextMenu: (event: import('react').MouseEvent<Element, MouseEvent>, xynode: XYFlowNode) => void;
7
+ onPaneContextMenu: (event: MouseEvent | import('react').MouseEvent<Element, MouseEvent>) => void;
8
+ onEdgeContextMenu: (event: import('react').MouseEvent<Element, MouseEvent>, xyedge: {
8
9
  source: string;
9
10
  target: string;
10
11
  style?: import('react').CSSProperties;
@@ -33,17 +34,19 @@ export declare function useXYFlowEvents(): {
33
34
  data: import('./types').RelationshipData;
34
35
  type: "relationship";
35
36
  }) => void;
36
- onEdgeContextMenu: import('@xyflow/react').EdgeMouseHandler<{
37
+ onNodeClick: (event: import('react').MouseEvent<Element, MouseEvent>, xynode: XYFlowNode) => void;
38
+ onNodeDoubleClick: (event: import('react').MouseEvent<Element, MouseEvent>, xynode: XYFlowNode) => void;
39
+ onEdgeClick: (event: import('react').MouseEvent<Element, MouseEvent>, xyedge: {
37
40
  source: string;
38
41
  target: string;
39
- style?: import("react").CSSProperties;
42
+ style?: import('react').CSSProperties;
40
43
  id: string;
41
44
  zIndex?: number;
42
- markerEnd?: import("@xyflow/react").EdgeMarkerType;
43
- markerStart?: import("@xyflow/react").EdgeMarkerType;
45
+ markerEnd?: import('@xyflow/react').EdgeMarkerType;
46
+ markerStart?: import('@xyflow/react').EdgeMarkerType;
44
47
  hidden?: boolean;
45
48
  className?: string;
46
- label?: import("react").ReactNode;
49
+ label?: import('react').ReactNode;
47
50
  selected?: boolean;
48
51
  selectable?: boolean;
49
52
  deletable?: boolean;
@@ -53,30 +56,29 @@ export declare function useXYFlowEvents(): {
53
56
  targetHandle?: string | null;
54
57
  animated?: boolean;
55
58
  interactionWidth?: number;
56
- labelStyle?: import("react").CSSProperties;
59
+ labelStyle?: import('react').CSSProperties;
57
60
  labelShowBg?: boolean;
58
- labelBgStyle?: import("react").CSSProperties;
61
+ labelBgStyle?: import('react').CSSProperties;
59
62
  labelBgPadding?: [number, number];
60
63
  labelBgBorderRadius?: number;
61
- updatable?: import("@xyflow/react").EdgeUpdatable;
62
- data: import("./types").RelationshipData;
64
+ updatable?: import('@xyflow/react').EdgeUpdatable;
65
+ data: import('./types').RelationshipData;
63
66
  type: "relationship";
64
- }>;
65
- onMoveEnd: import('@xyflow/react').OnMove;
66
- onNodeDoubleClick: import('@xyflow/react').NodeMouseHandler<XYFlowNode>;
67
- onNodeMouseEnter: import('@xyflow/react').NodeMouseHandler<XYFlowNode>;
68
- onNodeMouseLeave: import('@xyflow/react').NodeMouseHandler<XYFlowNode>;
69
- onEdgeMouseEnter: import('@xyflow/react').EdgeMouseHandler<{
67
+ }) => void;
68
+ onMoveEnd: (event: MouseEvent | TouchEvent | null, _viewport: import('@xyflow/react').Viewport) => void;
69
+ onNodeMouseEnter: (event: import('react').MouseEvent<Element, MouseEvent>, xynode: XYFlowNode) => void;
70
+ onNodeMouseLeave: (event: import('react').MouseEvent<Element, MouseEvent>, xynode: XYFlowNode) => void;
71
+ onEdgeMouseEnter: (event: import('react').MouseEvent<Element, MouseEvent>, xyedge: {
70
72
  source: string;
71
73
  target: string;
72
- style?: import("react").CSSProperties;
74
+ style?: import('react').CSSProperties;
73
75
  id: string;
74
76
  zIndex?: number;
75
- markerEnd?: import("@xyflow/react").EdgeMarkerType;
76
- markerStart?: import("@xyflow/react").EdgeMarkerType;
77
+ markerEnd?: import('@xyflow/react').EdgeMarkerType;
78
+ markerStart?: import('@xyflow/react').EdgeMarkerType;
77
79
  hidden?: boolean;
78
80
  className?: string;
79
- label?: import("react").ReactNode;
81
+ label?: import('react').ReactNode;
80
82
  selected?: boolean;
81
83
  selectable?: boolean;
82
84
  deletable?: boolean;
@@ -86,26 +88,26 @@ export declare function useXYFlowEvents(): {
86
88
  targetHandle?: string | null;
87
89
  animated?: boolean;
88
90
  interactionWidth?: number;
89
- labelStyle?: import("react").CSSProperties;
91
+ labelStyle?: import('react').CSSProperties;
90
92
  labelShowBg?: boolean;
91
- labelBgStyle?: import("react").CSSProperties;
93
+ labelBgStyle?: import('react').CSSProperties;
92
94
  labelBgPadding?: [number, number];
93
95
  labelBgBorderRadius?: number;
94
- updatable?: import("@xyflow/react").EdgeUpdatable;
95
- data: import("./types").RelationshipData;
96
+ updatable?: import('@xyflow/react').EdgeUpdatable;
97
+ data: import('./types').RelationshipData;
96
98
  type: "relationship";
97
- }>;
98
- onEdgeMouseLeave: import('@xyflow/react').EdgeMouseHandler<{
99
+ }) => void;
100
+ onEdgeMouseLeave: (event: import('react').MouseEvent<Element, MouseEvent>, xyedge: {
99
101
  source: string;
100
102
  target: string;
101
- style?: import("react").CSSProperties;
103
+ style?: import('react').CSSProperties;
102
104
  id: string;
103
105
  zIndex?: number;
104
- markerEnd?: import("@xyflow/react").EdgeMarkerType;
105
- markerStart?: import("@xyflow/react").EdgeMarkerType;
106
+ markerEnd?: import('@xyflow/react').EdgeMarkerType;
107
+ markerStart?: import('@xyflow/react').EdgeMarkerType;
106
108
  hidden?: boolean;
107
109
  className?: string;
108
- label?: import("react").ReactNode;
110
+ label?: import('react').ReactNode;
109
111
  selected?: boolean;
110
112
  selectable?: boolean;
111
113
  deletable?: boolean;
@@ -115,15 +117,19 @@ export declare function useXYFlowEvents(): {
115
117
  targetHandle?: string | null;
116
118
  animated?: boolean;
117
119
  interactionWidth?: number;
118
- labelStyle?: import("react").CSSProperties;
120
+ labelStyle?: import('react').CSSProperties;
119
121
  labelShowBg?: boolean;
120
- labelBgStyle?: import("react").CSSProperties;
122
+ labelBgStyle?: import('react').CSSProperties;
121
123
  labelBgPadding?: [number, number];
122
124
  labelBgBorderRadius?: number;
123
- updatable?: import("@xyflow/react").EdgeUpdatable;
124
- data: import("./types").RelationshipData;
125
+ updatable?: import('@xyflow/react').EdgeUpdatable;
126
+ data: import('./types').RelationshipData;
125
127
  type: "relationship";
126
- }>;
127
- onPaneClick: (event: import('react').MouseEvent<Element, MouseEvent>) => void;
128
- onPaneContextMenu: (event: MouseEvent | import('react').MouseEvent<Element, MouseEvent>) => void;
128
+ }) => void;
129
+ /**
130
+ * WOKAROUND:
131
+ * Viewport transform is not rounded to integers which results in blurry nodes on some resolution
132
+ * https://github.com/xyflow/xyflow/issues/3282
133
+ */
134
+ onViewportChange: ({ x, y, zoom }: import('@xyflow/react').Viewport) => void;
129
135
  };
@@ -1,6 +1,7 @@
1
1
  export declare const container: string;
2
2
  export declare const edgePathBg: string;
3
3
  export declare const fillStrokeCtx: string;
4
+ export declare const controlPoint: string;
4
5
  export declare const cssEdgePath: string;
5
6
  export declare const varLabelX: `var(--${string})` | `var(--${string}, ${string})` | `var(--${string}, ${number})`;
6
7
  export declare const varLabelY: `var(--${string})` | `var(--${string}, ${string})` | `var(--${string}, ${number})`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@likec4/diagram",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "license": "MIT",
5
5
  "bugs": "https://github.com/likec4/likec4/issues",
6
6
  "homepage": "https://likec4.dev",
@@ -50,8 +50,9 @@
50
50
  "clean": "run -T rimraf -v -g 'dist/*' .tsbuildinfo"
51
51
  },
52
52
  "dependencies": {
53
- "@likec4/core": "1.1.0",
53
+ "@likec4/core": "1.1.1",
54
54
  "@react-hookz/web": "^24.0.4",
55
+ "d3-shape": "^3.2.0",
55
56
  "esm-env": "^1.0.0",
56
57
  "fast-equals": "^5.0.1",
57
58
  "framer-motion": "^11.2.2",
@@ -70,6 +71,7 @@
70
71
  "@mantine/hooks": "^7.9.2",
71
72
  "@mantine/vanilla-extract": "^7.9.2",
72
73
  "@total-typescript/ts-reset": "^0.5.1",
74
+ "@types/d3-shape": "^3",
73
75
  "@types/react": "18.2.0",
74
76
  "@types/react-dom": "18.2.4",
75
77
  "@vanilla-extract/css": "^1.15.1",
@@ -16,7 +16,8 @@ globalStyle(`.react-flow${cssReactFlow}`, {
16
16
  }
17
17
  })
18
18
  globalStyle(`.react-flow${cssReactFlow} .react-flow__viewport`, {
19
- willChange: 'transform'
19
+ // willChange: 'transform'
20
+ backfaceVisibility: 'hidden'
20
21
  })
21
22
 
22
23
  export const cssDisablePan = style({})
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ BorderStyle,
2
3
  DiagramEdge,
3
4
  DiagramNode,
4
5
  DiagramView,
@@ -10,8 +11,8 @@ import type {
10
11
  } from '@likec4/core'
11
12
  import type { MouseEvent as ReactMouseEvent } from 'react'
12
13
  import type { SetRequired, Simplify } from 'type-fest'
13
- import type { XYFlowEdge, XYFlowNode } from './xyflow/types'
14
14
  import type { XYBackgroundProps } from './xyflow/XYFlowBackground'
15
+ import type { XYFlowEdge, XYFlowNode } from './xyflow/types'
15
16
 
16
17
  export type DiagramNodeWithNavigate = Simplify<SetRequired<DiagramNode, 'navigateTo'>>
17
18
 
@@ -44,22 +45,20 @@ export type OnEdgeClick = (
44
45
  export type OnCanvasClick = (event: ReactMouseEvent) => void
45
46
 
46
47
  export namespace Changes {
47
- export interface ChangeColor {
48
- op: 'change-color'
49
- color: ThemeColor
50
- targets: NonEmptyArray<Fqn>
51
- }
52
-
53
- export interface ChangeShape {
54
- op: 'change-shape'
55
- shape: ElementShape
48
+ export interface ChangeElementStyle {
49
+ op: 'change-element-style'
50
+ style: {
51
+ border?: BorderStyle
52
+ opacity?: number
53
+ shape?: ElementShape
54
+ color?: ThemeColor
55
+ }
56
56
  targets: NonEmptyArray<Fqn>
57
57
  }
58
58
  }
59
59
 
60
60
  export type Change =
61
- | Changes.ChangeColor
62
- | Changes.ChangeShape
61
+ | Changes.ChangeElementStyle
63
62
 
64
63
  export type ChangeEvent = {
65
64
  changes: NonEmptyArray<Change>
@@ -139,6 +138,12 @@ export interface LikeC4DiagramProperties {
139
138
  * @default true
140
139
  */
141
140
  showElementLinks?: boolean | undefined
141
+
142
+ /**
143
+ * Display panel with diagram title / description
144
+ * @default true
145
+ */
146
+ showDiagramTitle?: boolean | undefined
142
147
  }
143
148
 
144
149
  export interface LikeC4DiagramEventHandlers {
@@ -8,6 +8,7 @@ import { type LikeC4DiagramEventHandlers, type LikeC4DiagramProperties } from '.
8
8
  import { EnsureMantine } from './mantine/EnsureMantine'
9
9
  import { DiagramContextProvider } from './state/DiagramContext'
10
10
  import { WhenInitialized } from './state/WhenInitialized'
11
+ import DiagramTitlePanel from './ui/DiagramTitlePanel'
11
12
  import OptionsPanel from './ui/OptionsPanel'
12
13
  import { diagramViewToXYFlowData } from './xyflow/diagram-to-xyflow'
13
14
  import { FitViewOnDiagramChange } from './xyflow/FitviewOnDiagramChange'
@@ -29,6 +30,7 @@ export function LikeC4Diagram({
29
30
  background = 'dots',
30
31
  controls = false,
31
32
  showElementLinks = true,
33
+ showDiagramTitle = true,
32
34
  initialWidth,
33
35
  initialHeight,
34
36
  keepAspectRatio = false,
@@ -106,6 +108,7 @@ export function LikeC4Diagram({
106
108
  defaultEdges={initialRef.current.defaultEdges}
107
109
  >
108
110
  {readonly !== true && <OptionsPanel />}
111
+ {showDiagramTitle === true && <DiagramTitlePanel />}
109
112
  </XYFlow>
110
113
  </KeepAspectRatioContainer>
111
114
  <WhenInitialized>
@@ -31,6 +31,7 @@ export function StaticLikeC4Diagram({
31
31
  controls={false}
32
32
  background={background}
33
33
  showElementLinks
34
+ showDiagramTitle={false}
34
35
  nodesSelectable={false}
35
36
  nodesDraggable={false}
36
37
  {...rest}
@@ -1,5 +1,5 @@
1
1
  import { defaultTheme } from '@likec4/core'
2
- import { createGlobalTheme } from '@vanilla-extract/css'
2
+ import { createGlobalTheme, globalStyle } from '@vanilla-extract/css'
3
3
  import { scale, toHex } from 'khroma'
4
4
  import { keys, omit } from 'remeda'
5
5
  import { mantine } from './mantine.css'
@@ -7,6 +7,11 @@ 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
  }, {
@@ -1,10 +1,20 @@
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'
@@ -83,6 +93,8 @@ interface DiagramStoreActions {
83
93
  setLastClickedNode: (nodeId: string | null) => void
84
94
  setLastClickedEdge: (edgeId: string | null) => void
85
95
 
96
+ resetLastClicked: () => void
97
+
86
98
  getElement(id: Fqn): DiagramNode | null
87
99
  triggerOnChange: (changes: NonEmptyArray<Change>) => void
88
100
  triggerOnNavigateTo: (xynodeId: string, event: ReactMouseEvent) => void
@@ -230,88 +242,114 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
230
242
  }
231
243
  },
232
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
+
233
264
  getElement: (fqn) => {
234
265
  const { view } = get()
235
266
  return view.nodes.find(({ id }) => id === fqn) ?? null
236
267
  },
237
268
 
238
269
  triggerOnChange: (changes: NonEmptyArray<Change>) => {
270
+ if (DEV) {
271
+ console.debug('triggerOnChange', changes)
272
+ }
239
273
  const newShapes = new Map<Fqn, ElementShape>()
240
274
  const newColor = new Map<Fqn, ThemeColor>()
275
+ const newOpacity = new Map<Fqn, number>()
276
+ const newBorder = new Map<Fqn, BorderStyle>()
241
277
  for (const change of changes) {
242
- if (change.op === 'change-shape') {
243
- for (const target of change.targets) {
244
- newShapes.set(target, change.shape)
245
- }
246
- continue
278
+ if (change.op !== 'change-element-style') {
279
+ nonexhaustive(change.op)
247
280
  }
248
- if (change.op === 'change-color') {
249
- for (const target of change.targets) {
250
- 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
+ }
251
299
  }
252
- continue
253
300
  }
254
- nonexhaustive(change)
255
301
  }
256
- // TODO: Update positions/sizes
257
- // const { view, updateView } = get()
258
- // let hasChanges = false
259
- // const nextNodes = view.nodes.map((node) => {
260
- // const shape = newShapes.get(node.id)
261
- // if (shape && shape !== node.shape) {
262
- // hasChanges = true
263
- // node = {
264
- // ...node,
265
- // shape
266
- // }
267
- // }
268
- // const color = newColor.get(node.id)
269
- // if (color && color !== node.color) {
270
- // hasChanges = true
271
- // node = {
272
- // ...node,
273
- // color
274
- // }
275
- // }
276
- // return node
277
- // })
278
- // if (hasChanges) {
279
- // updateView({
280
- // ...view,
281
- // nodes: nextNodes
282
- // })
283
- // }
302
+
284
303
  get().xyflow.setNodes(nodes =>
285
304
  nodes.map(node => {
286
- let { data } = node
287
- const shape = newShapes.get(data.fqn)
288
- if (shape && shape !== data.element.shape) {
289
- node = {
290
- ...node,
291
- data: {
292
- ...data,
293
- element: {
294
- ...data.element,
295
- shape
296
- }
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
297
328
  }
298
329
  }
299
- data = node.data
300
330
  }
301
- const color = newColor.get(data.fqn)
302
- if (color && color !== data.element.color) {
303
- node = {
304
- ...node,
305
- data: {
306
- ...data,
307
- element: {
308
- ...data.element,
309
- color
310
- }
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
311
339
  }
312
340
  }
313
341
  }
314
- 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
+ }
315
353
  })
316
354
  )
317
355
  get().onChange?.({ changes })
@@ -353,7 +391,7 @@ export function createDiagramStore<T extends Exact<CreateDiagramStore, T>>(props
353
391
  xyflow.fitView({
354
392
  includeHiddenNodes: true,
355
393
  duration: (centerNode && focusedNodeId) ? 300 : 400,
356
- padding: centerNode ? 0 : fitViewPadding,
394
+ padding: fitViewPadding,
357
395
  minZoom: MinZoom,
358
396
  maxZoom: 1,
359
397
  ...(centerNode && { nodes: [centerNode] })
@@ -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
+ })