@likec4/diagram 1.0.2 → 1.1.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 (33) hide show
  1. package/dist/KeepAspectRatioContainer.d.ts +9 -0
  2. package/dist/LikeC4Diagram.css.d.ts +1 -0
  3. package/dist/index.mjs +248 -683
  4. package/dist/state/DiagramContext.d.ts +36 -9
  5. package/dist/state/diagramStore.d.ts +42 -12
  6. package/dist/state/useDiagramStore.d.ts +36 -9
  7. package/dist/style.css +600 -53
  8. package/dist/ui/index.d.ts +0 -1
  9. package/dist/xyflow/{XYFlowWrapper.d.ts → XYFlow.d.ts} +2 -2
  10. package/dist/xyflow/XYFlowEvents.d.ts +127 -13
  11. package/dist/xyflow/edges/RelationshipEdge.d.ts +2 -2
  12. package/dist/xyflow/hooks/useXYFlow.d.ts +20 -20
  13. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +7 -5
  14. package/dist/xyflow/types.d.ts +2 -2
  15. package/package.json +3 -3
  16. package/src/{ui/KeepAspectRatio.tsx → KeepAspectRatioContainer.tsx} +7 -6
  17. package/src/LikeC4Diagram.css.ts +6 -0
  18. package/src/LikeC4Diagram.tsx +6 -6
  19. package/src/globals.css.ts +42 -37
  20. package/src/state/diagramStore.ts +26 -7
  21. package/src/ui/index.ts +0 -1
  22. package/src/xyflow/FitviewOnDiagramChange.tsx +24 -18
  23. package/src/xyflow/SyncWithDiagram.tsx +23 -20
  24. package/src/xyflow/{XYFlowWrapper.tsx → XYFlow.tsx} +17 -24
  25. package/src/xyflow/XYFlowEvents.tsx +96 -43
  26. package/src/xyflow/diagram-to-xyflow.ts +4 -4
  27. package/src/xyflow/nodes/compound/CompoundNode.css.ts +85 -39
  28. package/src/xyflow/nodes/compound/CompoundNode.tsx +29 -31
  29. package/src/xyflow/nodes/element/ElementNode.tsx +2 -2
  30. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +2 -1
  31. package/dist/ui/KeepAspectRatio.css.d.ts +0 -1
  32. package/dist/ui/KeepAspectRatio.d.ts +0 -9
  33. package/src/ui/KeepAspectRatio.css.ts +0 -7
@@ -3,7 +3,7 @@ import { createVar, fallbackVar, generateIdentifier, globalKeyframes, globalStyl
3
3
  import { mantine } from '../../../mantine.css'
4
4
  import { vars } from '../../../theme.css'
5
5
 
6
- export const cssContainer = style({
6
+ export const container = style({
7
7
  width: '100%',
8
8
  height: '100%',
9
9
  position: 'relative',
@@ -11,7 +11,7 @@ export const cssContainer = style({
11
11
  margin: 0
12
12
  })
13
13
 
14
- const bgTransparency = createVar('bgTransparency')
14
+ export const varTransparency = createVar('transparency')
15
15
 
16
16
  const outlineColor = fallbackVar(
17
17
  mantine.colors.primaryColors.outline,
@@ -19,15 +19,15 @@ const outlineColor = fallbackVar(
19
19
  vars.element.stroke
20
20
  )
21
21
 
22
- export const cssCompound = style({
22
+ export const compoundBody = style({
23
23
  width: '100%',
24
24
  height: '100%',
25
25
  position: 'relative',
26
26
  borderRadius: rem(6),
27
27
  boxShadow: '0 4px 10px 0.5px rgba(0,0,0,0.1) , 0 2px 2px -1px rgba(0,0,0,0.4)',
28
- padding: rem(8),
28
+ padding: 8,
29
29
  background: vars.element.fill,
30
- transition: 'all 300ms ease-out',
30
+ transition: 'all 200ms ease-out',
31
31
  selectors: {
32
32
  [`:where(.react-flow__node.selected) &`]: {
33
33
  boxShadow: 'none'
@@ -40,62 +40,72 @@ export const cssCompound = style({
40
40
  }
41
41
  })
42
42
 
43
- // @ts-ignore
44
- const cssTransparentCompound = style({
45
- width: '100%',
46
- height: '100%',
47
- position: 'relative',
48
- borderRadius: rem(8),
43
+ const bgTransparency = createVar('bgTransparency')
44
+ const bgTransparencyDelta = createVar('bgTransparencyDelta')
45
+ export const transparent = style({
46
+ borderRadius: 8,
49
47
  borderStyle: 'dashed',
50
- borderWidth: rem(3.5),
51
- borderColor: vars.element.fill,
52
- boxShadow: '0 4px 10px 0.5px rgba(0,0,0,0.05) , 0 2px 2px -1px rgba(0,0,0,0.04)',
53
- padding: rem(8),
48
+ borderWidth: 3,
49
+ padding: 6,
50
+ borderColor: `color-mix(in srgb , ${vars.element.stroke}, transparent min(${bgTransparency}, 30%))`,
54
51
  background: `color-mix(in srgb , ${vars.element.fill}, transparent ${bgTransparency})`,
55
- transition: 'all 200ms ease-out',
52
+ backgroundClip: 'padding-box',
53
+ // transitionTimingFunction: 'ease-in',
54
+ // transition: 'all 150ms ease-in-out',
56
55
  vars: {
57
- [bgTransparency]: '90%'
56
+ [bgTransparency]: `calc(${fallbackVar(varTransparency, '80%')} - ${bgTransparencyDelta})`,
57
+ [bgTransparencyDelta]: '0%',
58
+ '--ai-bg': `color-mix(in srgb , ${vars.element.fill}, transparent 99%)`
58
59
  },
59
60
  selectors: {
60
- [`:where(${cssContainer}[data-hovered]) &`]: {
61
- transitionDelay: '300ms',
61
+ [`:where(${container}[data-hovered]) &`]: {
62
+ transitionDelay: '175ms',
63
+ // transitionTimingFunction: 'ease-out',
62
64
  vars: {
63
- [bgTransparency]: '65%'
65
+ [bgTransparencyDelta]: '15%'
64
66
  }
65
67
  },
66
68
  [`:where(.react-flow__node.selected) &`]: {
67
69
  transitionDelay: '50ms',
68
- boxShadow: 'none',
69
70
  borderColor: 'transparent',
71
+ // transitionTimingFunction: 'ease-out',
70
72
  vars: {
71
- [bgTransparency]: '15%'
73
+ [bgTransparencyDelta]: '25%'
72
74
  }
73
75
  }
74
76
  }
75
77
  })
76
78
 
77
- export const cssTitle = style({
79
+ export const title = style({
78
80
  fontFamily: vars.compound.font,
79
81
  fontOpticalSizing: 'auto',
80
82
  fontStyle: 'normal',
81
83
  textAlign: 'left',
82
84
  display: 'inline-block',
83
85
  fontWeight: 600,
84
- fontSize: '13px',
86
+ fontSize: '14px',
85
87
  textTransform: 'uppercase',
86
88
  letterSpacing: '0.2px',
87
89
  lineHeight: 1,
88
90
  opacity: 0.75,
89
- color: vars.compound.titleColor,
90
- paddingLeft: rem(12),
91
- mixBlendMode: 'lighten',
91
+ color: `var(--_compound-title-color,${vars.compound.titleColor})`,
92
+ paddingLeft: 12,
92
93
  selectors: {
93
94
  [`:where([data-likec4-navigable='true']) &`]: {
94
- paddingLeft: rem(26)
95
+ paddingLeft: 26
96
+ },
97
+ [`:where([data-mantine-color-scheme='light']) &`]: {
98
+ opacity: 1
95
99
  }
96
100
  }
97
101
  })
98
102
 
103
+ globalStyle(`:where([data-mantine-color-scheme='light'] .likec4-compound-transparent)`, {
104
+ vars: {
105
+ ['--_compound-title-color']: vars.element.stroke
106
+ }
107
+ })
108
+
99
109
  const indicatorKeyframes = generateIdentifier('indicator')
100
110
  globalKeyframes(indicatorKeyframes, {
101
111
  'from': {
@@ -106,7 +116,9 @@ globalKeyframes(indicatorKeyframes, {
106
116
  }
107
117
  })
108
118
 
109
- export const cssIndicator = style({
119
+ const indicatorStroke = createVar('indicator-stroke')
120
+ const indicatorStrokeWidth = createVar('indicator-stroke-width')
121
+ export const indicator = style({
110
122
  position: 'absolute',
111
123
  top: '0',
112
124
  left: '0',
@@ -115,17 +127,32 @@ export const cssIndicator = style({
115
127
  pointerEvents: 'none',
116
128
  overflow: 'visible',
117
129
  visibility: 'hidden',
130
+ vars: {
131
+ [indicatorStroke]: vars.element.loContrast,
132
+ [indicatorStrokeWidth]: '6'
133
+ },
118
134
  selectors: {
119
135
  ':where(.react-flow__node.selected:not(:focus-visible)) &': {
120
136
  visibility: 'visible'
137
+ },
138
+ ':where(.react-flow__node:focus-within:not(.selected)) &': {
139
+ vars: {
140
+ [indicatorStroke]: `color-mix(in srgb, ${vars.element.stroke} 30%, ${vars.element.loContrast})`,
141
+ [indicatorStrokeWidth]: '8'
142
+ }
143
+ },
144
+ [`:where([data-mantine-color-scheme='light']) &`]: {
145
+ vars: {
146
+ [indicatorStroke]: `color-mix(in srgb, ${vars.element.stroke} 80%, #000)`
147
+ }
121
148
  }
122
149
  }
123
150
  })
124
151
 
125
- globalStyle(`${cssIndicator} rect`, {
126
- stroke: vars.element.loContrast,
152
+ globalStyle(`${indicator} rect`, {
153
+ stroke: indicatorStroke,
127
154
  transformOrigin: 'center center',
128
- strokeWidth: 6,
155
+ strokeWidth: indicatorStrokeWidth,
129
156
  animationDuration: '800ms',
130
157
  animationName: indicatorKeyframes,
131
158
  animationIterationCount: 'infinite',
@@ -133,18 +160,30 @@ globalStyle(`${cssIndicator} rect`, {
133
160
  fill: 'none'
134
161
  })
135
162
 
136
- globalStyle(`:where(.react-flow__node:focus-within:not(.selected)) ${cssIndicator} rect`, {
137
- stroke: `color-mix(in srgb, ${vars.element.stroke} 30%, ${vars.element.loContrast})`,
138
- strokeWidth: 8
163
+ // globalStyle(`:where(.react-flow__node:focus-within:not(.selected)) ${indicator} rect`, {
164
+ // stroke: `color-mix(in srgb, ${vars.element.stroke} 30%, ${vars.element.loContrast})`,
165
+ // strokeWidth: 8
166
+ // })
167
+ const navigateBtnColor = createVar('navigateBtnColor')
168
+
169
+ globalStyle(`${container}`, {
170
+ vars: {
171
+ [navigateBtnColor]: vars.element.loContrast
172
+ }
173
+ })
174
+ globalStyle(`:where([data-mantine-color-scheme='light'] .likec4-compound-transparent)`, {
175
+ vars: {
176
+ [navigateBtnColor]: vars.element.stroke
177
+ }
139
178
  })
140
179
 
141
- export const cssNavigateBtn = style({
180
+ export const navigateBtn = style({
142
181
  position: 'absolute',
143
182
  pointerEvents: 'all',
144
183
  left: 3,
145
184
  top: 7,
146
185
  cursor: 'pointer',
147
- color: vars.element.loContrast,
186
+ color: `var(--_compound-title-color,${navigateBtnColor})`,
148
187
  transformOrigin: '90% 70%',
149
188
  opacity: 0.75,
150
189
  transition: 'all 150ms ease-out',
@@ -167,7 +206,14 @@ export const cssNavigateBtn = style({
167
206
  transitionDelay: '0'
168
207
  },
169
208
  selectors: {
170
- [`:where(.react-flow__node:not(.dragging) ${cssContainer}:hover) &:not(:hover)`]: {
209
+ [`:where([data-mantine-color-scheme='light'] .likec4-compound-transparent) &`]: {
210
+ opacity: 0.85,
211
+ vars: {
212
+ '--ai-bg-hover': `color-mix(in srgb , ${vars.element.fill}, transparent 50%)`,
213
+ '--ai-hover': `color-mix(in srgb , ${vars.element.fill}, transparent 20%)`
214
+ }
215
+ },
216
+ [`:where(.react-flow__node:not(.dragging) ${container}:hover) &:not(:hover)`]: {
171
217
  boxShadow: mantine.shadows.lg,
172
218
  transform: 'scale(1.223)',
173
219
  opacity: 1,
@@ -178,7 +224,7 @@ export const cssNavigateBtn = style({
178
224
  }
179
225
  }
180
226
  })
181
- globalStyle(`${cssNavigateBtn} svg.icon`, {
227
+ globalStyle(`${navigateBtn} svg.icon`, {
182
228
  width: '70%',
183
229
  height: '70%',
184
230
  strokeWidth: 1.5
@@ -1,16 +1,15 @@
1
- import { defaultTheme } from '@likec4/core'
2
1
  import { Box, Text } from '@mantine/core'
3
2
  import { assignInlineVars } from '@vanilla-extract/dynamic'
4
3
  import { Handle, type NodeProps, Position } from '@xyflow/react'
5
4
  import clsx from 'clsx'
6
5
  import { deepEqual as eq } from 'fast-equals'
7
- import { scale, toHex } from 'khroma'
8
- import { memo, useMemo } from 'react'
6
+ import { memo } from 'react'
7
+ import { clamp } from 'remeda'
9
8
  import { useDiagramState } from '../../../state'
10
- import { vars } from '../../../theme.css'
11
9
  import type { CompoundXYFlowNode } from '../../types'
12
10
  import { NavigateToBtn } from '../shared/NavigateToBtn'
13
- import { cssCompound, cssContainer, cssIndicator, cssNavigateBtn, cssTitle } from './CompoundNode.css'
11
+ import { compoundBody, container, indicator, navigateBtn, title } from './CompoundNode.css'
12
+ import * as css from './CompoundNode.css'
14
13
 
15
14
  type CompoundNodeProps = Pick<
16
15
  NodeProps<CompoundXYFlowNode>,
@@ -26,14 +25,6 @@ const isEqualProps = (prev: CompoundNodeProps, next: CompoundNodeProps) => (
26
25
  && eq(prev.data, next.data)
27
26
  )
28
27
 
29
- const compoundColor = (color: string, depth: number) =>
30
- toHex(
31
- scale(color, {
32
- l: -35 - 5 * depth,
33
- s: -15 - 5 * depth
34
- })
35
- )
36
-
37
28
  export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function CompoundNode({
38
29
  id,
39
30
  data: {
@@ -41,17 +32,10 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
41
32
  }
42
33
  }) {
43
34
  // useTilg()
44
- const { color, depth = 0, ...compound } = element
45
- const colors = useMemo(() => {
46
- const colors = defaultTheme.elements[color]
47
- return {
48
- fill: compoundColor(colors.fill, depth),
49
- stroke: compoundColor(colors.stroke, depth)
50
- }
51
- }, [color, depth])
52
-
35
+ const { color, style, depth = 1, ...compound } = element
53
36
  // const w = toDomPrecision(width ?? compound.width)
54
37
  // const h = toDomPrecision(height ?? compound.height)
38
+ const opacity = style.opacity ?? 100
55
39
 
56
40
  const { isHovered, hasOnNavigateTo } = useDiagramState(s => ({
57
41
  isHovered: s.hoveredNodeId === id,
@@ -59,26 +43,35 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
59
43
  }))
60
44
  const isnavigable = !!compound.navigateTo && hasOnNavigateTo
61
45
 
46
+ const transparency = clamp(100 - opacity, {
47
+ min: 0,
48
+ max: 98
49
+ })
50
+
62
51
  return (
63
52
  <Box
64
- className={clsx(cssContainer, 'likec4-compound-node')}
65
- style={assignInlineVars({ fill: vars.element.fill, stroke: vars.element.stroke }, {
66
- fill: colors.fill,
67
- stroke: colors.stroke
68
- })}
53
+ className={clsx(
54
+ css.container,
55
+ 'likec4-compound-node',
56
+ opacity < 100 && 'likec4-compound-transparent'
57
+ )}
69
58
  mod={{
59
+ 'compound-depth': depth,
70
60
  'likec4-color': color,
71
61
  'likec4-shape': compound.shape,
72
62
  'likec4-navigable': isnavigable,
73
63
  hovered: isHovered
74
64
  }}
65
+ style={assignInlineVars({
66
+ [css.varTransparency]: transparency + '%'
67
+ })}
75
68
  >
76
69
  <Handle
77
70
  type="target"
78
71
  position={Position.Top}
79
72
  style={{ visibility: 'hidden' }}
80
73
  />
81
- <svg className={cssIndicator}>
74
+ <svg className={css.indicator}>
82
75
  <rect
83
76
  x={0}
84
77
  y={0}
@@ -87,14 +80,19 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
87
80
  rx={6}
88
81
  />
89
82
  </svg>
90
- <div className={clsx(cssCompound, 'likec4-compound')}>
83
+ <div
84
+ className={clsx(css.compoundBody, opacity < 100 && css.transparent, 'likec4-compound')}
85
+ style={{
86
+ borderStyle: opacity < 100 ? style.border : undefined
87
+ }}
88
+ >
91
89
  <Text
92
90
  component="div"
93
- className={clsx(cssTitle, 'likec4-compound-title')}>
91
+ className={clsx(css.title, 'likec4-compound-title')}>
94
92
  {compound.title}
95
93
  </Text>
96
94
  </div>
97
- {isnavigable && <NavigateToBtn xynodeId={id} className={cssNavigateBtn} />}
95
+ {isnavigable && <NavigateToBtn xynodeId={id} className={css.navigateBtn} />}
98
96
  <Handle
99
97
  type="source"
100
98
  position={Position.Bottom}
@@ -78,7 +78,7 @@ export function ElementNode({
78
78
  }: ElementNodeProps) {
79
79
  const { isHovered, hasOnNavigateTo, isHovercards, isInteractive } = useDiagramState(s => ({
80
80
  isHovered: s.hoveredNodeId === id,
81
- isInteractive: s.nodesDraggable || s.nodesSelectable,
81
+ isInteractive: s.nodesDraggable || s.nodesSelectable || !!s.onNavigateTo,
82
82
  isHovercards: s.showElementLinks,
83
83
  hasOnNavigateTo: !!s.onNavigateTo
84
84
  }))
@@ -99,7 +99,7 @@ export function ElementNode({
99
99
  case dragging:
100
100
  animate = 'idle'
101
101
  break
102
- case (isInteractive || hasOnNavigateTo) && isHovered:
102
+ case isInteractive && isHovered:
103
103
  animate = 'hovered'
104
104
  break
105
105
  case selected:
@@ -15,9 +15,10 @@ export function NavigateToBtn({ xynodeId, className }: NavigateToBtnProps) {
15
15
  className={clsx('nodrag nopan', className)}
16
16
  radius="xl"
17
17
  onClick={event => {
18
- diagramApi.getState().triggerOnNavigateTo(xynodeId, event)
19
18
  event.stopPropagation()
19
+ diagramApi.getState().triggerOnNavigateTo(xynodeId, event)
20
20
  }}
21
+ onDoubleClick={event => event.stopPropagation()}
21
22
  >
22
23
  <ZoomIn />
23
24
  </ActionIcon>
@@ -1 +0,0 @@
1
- export declare const wrapper: string;
@@ -1,9 +0,0 @@
1
- import { HTMLAttributes } from 'react';
2
-
3
- type KeepAspectRatioProps = HTMLAttributes<HTMLDivElement> & {
4
- enabled: boolean;
5
- width: number;
6
- height: number;
7
- };
8
- export declare function KeepAspectRatio({ children, width, height, enabled, className, style, ...rest }: KeepAspectRatioProps): JSX.Element;
9
- export {};
@@ -1,7 +0,0 @@
1
- import { style } from '@vanilla-extract/css'
2
-
3
- export const wrapper = style({
4
- width: '100%',
5
- height: '100%',
6
- padding: 0
7
- })