@likec4/diagram 1.7.4 → 1.8.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.
@@ -0,0 +1,6 @@
1
+ export declare const container: string;
2
+ export declare const card: string;
3
+ export declare const tabPanel: string;
4
+ export declare const elementNotation: string;
5
+ export declare const shapeSvg: string;
6
+ export declare const shapeBadge: string;
@@ -0,0 +1 @@
1
+ export declare function NotationPanel(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ export { NotationPanel as default } from './NotationPanel';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@likec4/diagram",
3
- "version": "1.7.4",
3
+ "version": "1.8.0",
4
4
  "license": "MIT",
5
5
  "bugs": "https://github.com/likec4/likec4/issues",
6
6
  "homepage": "https://likec4.dev",
@@ -50,31 +50,31 @@
50
50
  "dependencies": {
51
51
  "@lume/kiwi": "^0.4.3",
52
52
  "@react-hookz/web": "^24.0.4",
53
- "@tabler/icons-react": "^3.11.0",
53
+ "@tabler/icons-react": "^3.12.0",
54
54
  "@vanilla-extract/css": "^1.15.3",
55
55
  "@vanilla-extract/css-utils": "^0.1.4",
56
56
  "@vanilla-extract/dynamic": "^2.1.1",
57
- "@xyflow/react": "12.0.4",
58
- "@xyflow/system": "0.0.37",
57
+ "@xyflow/react": "12.1.1",
58
+ "@xyflow/system": "0.0.39",
59
59
  "bezier-js": "^6.1.4",
60
60
  "clsx": "^2.1.1",
61
61
  "d3-shape": "^3.2.0",
62
62
  "esm-env": "^1.0.0",
63
63
  "fast-equals": "^5.0.1",
64
- "framer-motion": "^11.3.24",
65
- "remeda": "^2.3.0",
66
- "zustand": "^4.5.4"
64
+ "framer-motion": "^11.3.28",
65
+ "remeda": "^2.11.0",
66
+ "zustand": "^4.5.5"
67
67
  },
68
68
  "peerDependencies": {
69
- "@likec4/core": "1.7.4",
69
+ "@likec4/core": "1.8.0",
70
70
  "@mantine/core": "^7.11.2",
71
71
  "@mantine/hooks": "^7.11.2",
72
72
  "react": "^18.3.1",
73
73
  "react-dom": "^18.3.1"
74
74
  },
75
75
  "devDependencies": {
76
- "@likec4/core": "1.7.4",
77
- "@likec4/tsconfig": "1.7.4",
76
+ "@likec4/core": "1.8.0",
77
+ "@likec4/tsconfig": "1.8.0",
78
78
  "@mantine/core": "7.11.2",
79
79
  "@mantine/hooks": "7.11.2",
80
80
  "@mantine/vanilla-extract": "7.11.2",
@@ -85,7 +85,7 @@
85
85
  "@types/react-dom": "^18.3.0",
86
86
  "@vanilla-extract/esbuild-plugin": "^2.3.8",
87
87
  "@vitejs/plugin-react": "^4.3.1",
88
- "execa": "^9.3.0",
88
+ "execa": "^9.3.1",
89
89
  "khroma": "^2.1.0",
90
90
  "react": "^18.3.1",
91
91
  "react-dom": "^18.3.1",
@@ -203,6 +203,12 @@ export interface LikeC4DiagramProperties {
203
203
  */
204
204
  showNavigationButtons?: undefined | boolean
205
205
 
206
+ /**
207
+ * Display notations of the view
208
+ * @default true
209
+ */
210
+ showNotations?: boolean | undefined
211
+
206
212
  /**
207
213
  * If double click on a node should enable focus mode, i.e. highlight incoming/outgoing edges
208
214
  * @default true
@@ -30,6 +30,7 @@ export function LikeC4Diagram({
30
30
  controls = false,
31
31
  showElementLinks = true,
32
32
  showDiagramTitle = true,
33
+ showNotations = true,
33
34
  enableDynamicViewWalkthrough = false,
34
35
  enableFocusMode = false,
35
36
  initialWidth,
@@ -80,6 +81,7 @@ export function LikeC4Diagram({
80
81
  fitViewPadding={fitViewPadding}
81
82
  showElementLinks={showElementLinks}
82
83
  showNavigationButtons={showNavigationButtons}
84
+ showNotations={showNotations}
83
85
  nodesDraggable={nodesDraggable}
84
86
  nodesSelectable={nodesSelectable}
85
87
  experimentalEdgeEditing={experimentalEdgeEditing}
@@ -34,6 +34,7 @@ export function StaticLikeC4Diagram({
34
34
  background={background}
35
35
  showElementLinks
36
36
  showDiagramTitle={false}
37
+ showNotations={false}
37
38
  enableDynamicViewWalkthrough={false}
38
39
  enableFocusMode={false}
39
40
  nodesSelectable={false}
@@ -2,7 +2,7 @@ import { defaultTheme } from '@likec4/core'
2
2
  import { createGlobalTheme, globalStyle } from '@vanilla-extract/css'
3
3
  import { scale, toHex } from 'khroma'
4
4
  import { keys, omit } from 'remeda'
5
- import { mantine } from './mantine.css'
5
+ import { mantine, whereDark } from './mantine.css'
6
6
  import { vars } from './theme.css'
7
7
 
8
8
  export const rootClassName = 'likec4-diagram-root'
@@ -18,6 +18,10 @@ globalStyle(`.${rootClassName}`, {
18
18
  WebkitFontSmoothing: mantine.webkitFontSmoothing,
19
19
  MozOsxFontSmoothing: mantine.mozFontSmoothing
20
20
  })
21
+ globalStyle(`.${rootClassName} .mantine-ActionIcon-icon .tabler-icon`, {
22
+ width: '75%',
23
+ height: '75%'
24
+ })
21
25
 
22
26
  createGlobalTheme(`.${rootClassName}`, {
23
27
  ...omit(vars, ['optionsPanel', 'navigationPanel', 'safariAnimationHook', 'default'])
@@ -28,7 +32,7 @@ createGlobalTheme(`.${rootClassName}`, {
28
32
  background: {
29
33
  color: mantine.colors.body,
30
34
  pattern: {
31
- color: mantine.colors.dark[3]
35
+ color: mantine.colors.gray[4]
32
36
  }
33
37
  }
34
38
  },
@@ -43,33 +47,9 @@ createGlobalTheme(`.${rootClassName}`, {
43
47
  relation: {
44
48
  ...defaultTheme.relationships.slate
45
49
  }
46
- // likec4: {
47
- // font: `var(--likec4-font-family,'ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"')`,
48
- // backgroundColor: fallbackVar(globalvars.likec4.backgroundColor, mantine.colors.body)
49
- // },
50
- // compound: {
51
- // font: fallbackVar(vars.compound.font, vars.likec4.font),
52
- // titleColor: fallbackVar(globalvars.compound.titleColor, globalvars.element.loContrast),
53
- // },
54
- // element: {
55
- // font: fallbackVar(globalvars.element.font, vars.likec4.font),
56
- // fill: fallbackVar(globalvars.element.fill, defaultTheme.elements.primary.fill),
57
- // stroke: fallbackVar(globalvars.element.stroke, defaultTheme.elements.primary.stroke),
58
- // hiContrast: fallbackVar(globalvars.element.hiContrast, defaultTheme.elements.primary.hiContrast),
59
- // loContrast: fallbackVar(globalvars.element.loContrast, defaultTheme.elements.primary.loContrast),
60
- // },
61
- // relation: {
62
- // lineColor: fallbackVar(globalvars.relation.lineColor, defaultTheme.relationships.slate.lineColor),
63
- // labelColor: fallbackVar(globalvars.relation.labelColor, defaultTheme.relationships.slate.labelColor),
64
- // labelBgColor: fallbackVar(globalvars.relation.labelBgColor, defaultTheme.relationships.slate.labelBgColor),
65
- // },
66
- // optionsPanel: {
67
- // top: globalvars.optionsPanel.top,
68
- // right: globalvars.optionsPanel.right
69
- // }
70
50
  })
71
51
 
72
- createGlobalTheme(`:where([data-mantine-color-scheme='light']) .${rootClassName}`, {
52
+ createGlobalTheme(`${whereDark} .${rootClassName}`, {
73
53
  likec4: {
74
54
  background: {
75
55
  pattern: {
@@ -81,7 +61,7 @@ createGlobalTheme(`:where([data-mantine-color-scheme='light']) .${rootClassName}
81
61
  likec4: {
82
62
  background: {
83
63
  pattern: {
84
- color: mantine.colors.gray[6]
64
+ color: mantine.colors.dark[5]
85
65
  }
86
66
  }
87
67
  }
@@ -138,7 +118,7 @@ for (const color of keys(defaultTheme.elements)) {
138
118
  )
139
119
 
140
120
  createGlobalTheme(
141
- `:where([data-mantine-color-scheme='dark']) :where([data-likec4-color='${color}'][data-compound-depth='${depth}'])`,
121
+ `${whereDark} :where([data-likec4-color='${color}'][data-compound-depth='${depth}'])`,
142
122
  compounds,
143
123
  {
144
124
  fill: compoundDarkColor(defaultTheme.elements[color].fill, depth),
@@ -1,4 +1,7 @@
1
1
  import { themeToVars } from '@mantine/vanilla-extract'
2
2
 
3
+ export const whereLight = ':where([data-mantine-color-scheme="light"])'
4
+ export const whereDark = ':where([data-mantine-color-scheme="dark"])'
5
+
3
6
  // we need only variables names
4
7
  export const mantine = themeToVars({})
@@ -55,10 +55,11 @@ export function diagramViewToXYFlowData(
55
55
  let next: typeof traverse[0] | undefined
56
56
  while ((next = traverse.shift())) {
57
57
  const { node, parent } = next
58
- if (node.children.length > 0) {
58
+ const isCompound = hasAtLeast(node.children, 1)
59
+ if (isCompound) {
59
60
  traverse.push(...node.children.map(child => ({ node: nodeById(child), parent: node })))
60
61
  }
61
- const isCompound = hasAtLeast(node.children, 1)
62
+
62
63
  const position = {
63
64
  x: node.position[0],
64
65
  y: node.position[1]
@@ -82,6 +83,7 @@ export function diagramViewToXYFlowData(
82
83
  },
83
84
  draggable,
84
85
  selectable: opts.selectable,
86
+ focusable: opts.selectable && !isCompound,
85
87
  deletable: false,
86
88
  position,
87
89
  zIndex: isCompound ? ZIndexes.Compound : ZIndexes.Element,
@@ -102,18 +104,11 @@ export function diagramViewToXYFlowData(
102
104
  const source = edge.source
103
105
  const target = edge.target
104
106
  const id = ns + edge.id
105
- // const points = isElkEdge(edge) ? edge.points : deriveEdgePoints(edge.points)
106
- // const controlPoints = deriveEdgePoints(edge.points)
107
- // if (edge.tailArrowPoint) {
108
- // controlPoints.unshift([...edge.tailArrowPoint])
109
- // }
110
- // if (edge.headArrowPoint) {
111
- // controlPoints.push([...edge.headArrowPoint])
112
- // }
113
- invariant(hasAtLeast(edge.points, 2), 'edge should have at least 2 points')
114
- // invariant(hasAtLeast(controlPoints, 2), 'edge controlPoints should have at least 2 points')
115
107
 
116
- // const level = Math.max(nodeZIndex(nodeById(source)), nodeZIndex(nodeById(target)))
108
+ if (!hasAtLeast(edge.points, 2)) {
109
+ console.error('edge should have at least 2 points', edge)
110
+ continue
111
+ }
117
112
 
118
113
  xyedges.push({
119
114
  id,
@@ -1,4 +1,13 @@
1
- import type { DiagramNode, DiagramView, Fqn, NodeId, ViewChange, ViewID } from '@likec4/core'
1
+ import type {
2
+ DiagramNode,
3
+ DiagramView,
4
+ ElementKind,
5
+ ElementNotation,
6
+ Fqn,
7
+ NodeId,
8
+ ViewChange,
9
+ ViewID
10
+ } from '@likec4/core'
2
11
  import { getBBoxCenter, invariant, nonexhaustive, nonNullable, StepEdgeId } from '@likec4/core'
3
12
  import {
4
13
  applyEdgeChanges,
@@ -11,8 +20,8 @@ import { boxToRect, getBoundsOfRects, getNodeDimensions } from '@xyflow/system'
11
20
  import { DEV } from 'esm-env'
12
21
  import { deepEqual as eq, shallowEqual } from 'fast-equals'
13
22
  import type { MouseEvent as ReactMouseEvent } from 'react'
14
- import { entries, hasAtLeast, isNullish, reduce } from 'remeda'
15
- import type { ConditionalKeys, Exact, Except, RequiredKeysOf, Simplify } from 'type-fest'
23
+ import { entries, hasAtLeast, isNullish, map, prop, reduce } from 'remeda'
24
+ import type { ConditionalKeys, Except, RequiredKeysOf, Simplify } from 'type-fest'
16
25
  import { devtools, subscribeWithSelector } from 'zustand/middleware'
17
26
  import { shallow } from 'zustand/shallow'
18
27
  import { createWithEqualityFn } from 'zustand/traditional'
@@ -37,6 +46,7 @@ export type DiagramInitialState = {
37
46
  readonly: boolean
38
47
  showElementLinks: boolean
39
48
  showNavigationButtons: boolean
49
+ showNotations: boolean
40
50
  fitViewEnabled: boolean
41
51
  fitViewPadding: number
42
52
  zoomable: boolean
@@ -132,6 +142,8 @@ export type DiagramState = Simplify<
132
142
  onInit: (xyflow: XYFlowInstance) => void
133
143
  onNodesChange: OnNodesChange<XYFlowNode>
134
144
  onEdgesChange: OnEdgesChange<XYFlowEdge>
145
+
146
+ highlightByElementNotation: (notation: ElementNotation, onlyOfKind?: ElementKind) => void
135
147
  }
136
148
  >
137
149
 
@@ -162,7 +174,7 @@ let StoreDevId = 1
162
174
 
163
175
  const EmptyStringSet: ReadonlySet<string> = new StringSet()
164
176
 
165
- export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(props: T) {
177
+ export function createDiagramStore(props: DiagramInitialState) {
166
178
  const storeDevId = 'DiagramStore' + String(StoreDevId++).padStart(2, '0')
167
179
  const {
168
180
  xynodes,
@@ -878,6 +890,21 @@ export function createDiagramStore<T extends Exact<DiagramInitialState, T>>(prop
878
890
  'onInit'
879
891
  )
880
892
  }
893
+ },
894
+
895
+ highlightByElementNotation: (notation: ElementNotation, onlyOfKind?: ElementKind) => {
896
+ const { xynodes, xyedges } = get()
897
+ const dimmed = new StringSet(map(xyedges, prop('id')))
898
+ xynodes.forEach(({ id, data }) => {
899
+ const node = data.element
900
+ if (
901
+ node.shape !== notation.shape || node.color !== notation.color || !notation.kinds.includes(node.kind)
902
+ || (onlyOfKind && node.kind !== onlyOfKind)
903
+ ) {
904
+ dimmed.add(id)
905
+ }
906
+ })
907
+ set({ dimmed }, noReplace, 'highlightByElementNotation')
881
908
  }
882
909
  }),
883
910
  {
@@ -1,5 +1,5 @@
1
1
  import { fallbackVar, globalStyle, style } from '@vanilla-extract/css'
2
- import { mantine } from '../mantine.css'
2
+ import { mantine, whereDark } from '../mantine.css'
3
3
  import { vars } from '../theme.css'
4
4
 
5
5
  export const container = style({
@@ -27,7 +27,7 @@ export const card = style({
27
27
  }
28
28
  },
29
29
  selectors: {
30
- [`:where([data-mantine-color-scheme='dark']) &`]: {
30
+ [`${whereDark} &`]: {
31
31
  backgroundColor: `color-mix(in srgb, ${mantine.colors.dark[6]}, transparent 20%)`
32
32
  }
33
33
  }
@@ -37,8 +37,10 @@ export const title = style({})
37
37
 
38
38
  export const description = style({
39
39
  whiteSpaceCollapse: 'preserve-breaks',
40
- color: mantine.colors.gray[7]
41
- })
42
- globalStyle(`:where([data-mantine-color-scheme="dark"]) ${description}`, {
43
- color: mantine.colors.gray[5]
40
+ color: mantine.colors.gray[7],
41
+ selectors: {
42
+ [`${whereDark} &`]: {
43
+ color: mantine.colors.gray[5]
44
+ }
45
+ }
44
46
  })
@@ -39,7 +39,7 @@ export default function DiagramTitlePanel() {
39
39
  <Card
40
40
  radius="sm"
41
41
  className={css.card}
42
- withBorder
42
+ withBorder={!isCollapsed}
43
43
  p={isCollapsed ? 'sm' : 'md'}
44
44
  onDoubleClick={e => e.stopPropagation()}>
45
45
  {isCollapsed && (
@@ -47,22 +47,21 @@ export default function DiagramTitlePanel() {
47
47
  <Button
48
48
  fullWidth
49
49
  justify="stretch"
50
- size="sm"
51
- py={2}
50
+ size="md"
52
51
  radius={'0'}
53
52
  variant={'subtle'}
54
- color="dark"
53
+ color="gray"
55
54
  onClick={toggle}
55
+ fw={500}
56
+ fz={'sm'}
56
57
  >
57
- <Text inline fw={500} fz={'xs'} c="gray">
58
- {title}
59
- </Text>
58
+ {title}
60
59
  </Button>
61
60
  </CardSection>
62
61
  )}
63
62
  {!isCollapsed && (
64
63
  <>
65
- <CardSection mb={'sm'}>
64
+ <CardSection mb={'xs'}>
66
65
  <Button
67
66
  fullWidth
68
67
  size="xs"
@@ -131,19 +130,19 @@ export default function DiagramTitlePanel() {
131
130
  justify="stretch"
132
131
  align="stretch">
133
132
  {links.map((link) => (
134
- <Group key={link} wrap="nowrap" align="center" gap={'sm'}>
133
+ <Group key={link.url} wrap="nowrap" align="center" gap={'sm'}>
135
134
  <Box flex={'1'} style={{ overflow: 'hidden' }}>
136
135
  <Anchor
137
- href={link}
136
+ href={link.url}
138
137
  target="_blank"
139
138
  fz="xs"
140
139
  truncate="end"
141
140
  display={'inline-block'}
142
141
  w={'100%'}>
143
- {link}
142
+ {link.title || link.url}
144
143
  </Anchor>
145
144
  </Box>
146
- <CopyButton value={link}>
145
+ <CopyButton value={link.url}>
147
146
  {({ copied, copy }) => (
148
147
  <Button
149
148
  size="compact-xs"
@@ -0,0 +1,103 @@
1
+ import { fallbackVar, globalStyle, style } from '@vanilla-extract/css'
2
+ import { calc } from '@vanilla-extract/css-utils'
3
+ import { mantine, whereDark } from '../../mantine.css'
4
+ import { vars } from '../../theme.css'
5
+ import { stokeFillMix } from '../../xyflow/nodes/element/element.css'
6
+
7
+ export const container = style({
8
+ bottom: 0,
9
+ right: 0,
10
+ padding: 8,
11
+ margin: 0
12
+ })
13
+
14
+ export const card = style({
15
+ cursor: 'default',
16
+ userSelect: 'none',
17
+ minWidth: 200,
18
+ maxWidth: 'calc(100vw - 20px)',
19
+ backgroundColor: `color-mix(in srgb, ${mantine.colors.body}, transparent 20%)`,
20
+ WebkitBackdropFilter: fallbackVar(vars.safariAnimationHook, 'blur(8px)'),
21
+ backdropFilter: fallbackVar(vars.safariAnimationHook, 'blur(8px)'),
22
+ '@media': {
23
+ [mantine.largerThan('sm')]: {
24
+ minWidth: 300,
25
+ maxWidth: `65vw`
26
+ },
27
+ [mantine.largerThan('md')]: {
28
+ // minWidth: 350,
29
+ maxWidth: `40vw`
30
+ }
31
+ },
32
+ selectors: {
33
+ [`${whereDark} &`]: {
34
+ backgroundColor: `color-mix(in srgb, ${mantine.colors.dark[6]}, transparent 20%)`
35
+ }
36
+ }
37
+ })
38
+
39
+ export const tabPanel = style({
40
+ padding: calc(mantine.spacing.xs).divide(2).toString()
41
+ })
42
+
43
+ // export const description = style({
44
+ // whiteSpaceCollapse: 'preserve-breaks',
45
+ // color: mantine.colors.gray[7],
46
+ // selectors: {
47
+ // [`${whereDark} &`]: {
48
+ // color: mantine.colors.gray[5]
49
+ // }
50
+ // }
51
+ // })
52
+
53
+ export const elementNotation = style({
54
+ backgroundColor: 'transparent',
55
+ transition: 'all 100ms ease-in',
56
+ // WebkitBackdropFilter: fallbackVar(vars.safariAnimationHook, 'blur(8px)'),
57
+ // backdropFilter: fallbackVar(vars.safariAnimationHook, 'blur(8px)'),
58
+ vars: {
59
+ [stokeFillMix]: `color-mix(in srgb, ${vars.element.stroke} 90%, ${vars.element.fill})`
60
+ },
61
+ ':hover': {
62
+ transition: 'all 120ms ease-out',
63
+ // backgroundColor:
64
+ backgroundColor: `color-mix(in srgb, ${mantine.colors.primaryColors[2]}, transparent 50%)`
65
+ },
66
+ selectors: {
67
+ [`${whereDark} &:hover`]: {
68
+ backgroundColor: `color-mix(in srgb, ${mantine.colors.dark[3]}, transparent 60%)`
69
+ }
70
+ }
71
+ })
72
+
73
+ export const shapeSvg = style({
74
+ fill: vars.element.fill,
75
+ stroke: vars.element.stroke,
76
+ strokeWidth: 1,
77
+ overflow: 'visible',
78
+ width: '100%',
79
+ height: 'auto',
80
+ filter: `
81
+ drop-shadow(0 2px 3px rgb(0 0 0 / 22%))
82
+ drop-shadow(0 1px 8px rgb(0 0 0 / 10%))
83
+ `
84
+ })
85
+
86
+ export const shapeBadge = style({
87
+ fontWeight: 500,
88
+ letterSpacing: '0.2px',
89
+ paddingTop: 0,
90
+ paddingBottom: 0,
91
+ textTransform: 'lowercase',
92
+ transition: 'all 150ms ease-in-out',
93
+ cursor: 'pointer',
94
+ vars: {
95
+ ['--badge-radius']: '2px',
96
+ ['--badge-fz']: '9.5px',
97
+ ['--badge-padding-x']: '3px',
98
+ ['--badge-height']: '13.5px',
99
+ ['--badge-lh']: '1'
100
+ // ['--badge-bg']: vars.element.fill,
101
+ // ['--badge-color']: vars.element.hiContrast
102
+ }
103
+ })