@likec4/diagram 1.21.1 → 1.22.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.
Files changed (120) hide show
  1. package/dist/LikeC4Diagram.js +9 -13
  2. package/dist/base/index.d.ts +0 -1
  3. package/dist/base/primitives/element/styles.css.js +1 -1
  4. package/dist/{base → components}/Link.css.js +3 -3
  5. package/dist/components/SearchControl.css.d.ts +2 -0
  6. package/dist/components/SearchControl.css.js +7 -0
  7. package/dist/components/SearchControl.d.ts +5 -0
  8. package/dist/components/SearchControl.js +17 -0
  9. package/dist/context/DiagramFeatures.d.ts +1 -1
  10. package/dist/context/IconRenderer.d.ts +10 -0
  11. package/dist/context/IconRenderer.js +34 -0
  12. package/dist/hooks/useCurrentViewId.js +8 -0
  13. package/dist/hooks/useDiagram.d.ts +108 -72
  14. package/dist/hooks/useDiagram.js +3 -0
  15. package/dist/likec4diagram/DiagramActor.js +13 -2
  16. package/dist/likec4diagram/DiagramUI.js +0 -2
  17. package/dist/likec4diagram/XYFlow.d.ts +6 -1
  18. package/dist/likec4diagram/XYFlow.js +37 -31
  19. package/dist/likec4diagram/custom/edges/RelationshipsDropdownMenu.js +2 -1
  20. package/dist/likec4diagram/state/actorContext.d.ts +620 -468
  21. package/dist/likec4diagram/state/assign.js +1 -1
  22. package/dist/likec4diagram/state/machine.d.ts +120 -74
  23. package/dist/likec4diagram/state/machine.js +32 -12
  24. package/dist/likec4diagram/state/syncManualLayoutActor.js +4 -8
  25. package/dist/likec4diagram/ui/controls/Controls.js +65 -48
  26. package/dist/likec4diagram/ui/controls/NavigationButtons.js +44 -69
  27. package/dist/likec4diagram/ui/controls/ToggleReadonly.d.ts +1 -0
  28. package/dist/likec4diagram/ui/controls/ToggleReadonly.js +29 -0
  29. package/dist/likec4diagram/ui/controls/_shared.js +1 -1
  30. package/dist/likec4diagram/ui/diagram-title/DiagramTitlePanel.js +1 -1
  31. package/dist/likec4diagram/ui/search/ElementsColumn.css.d.ts +15 -0
  32. package/dist/likec4diagram/ui/search/ElementsColumn.css.js +30 -0
  33. package/dist/likec4diagram/ui/search/ElementsColumn.d.ts +1 -0
  34. package/dist/likec4diagram/ui/search/ElementsColumn.js +243 -0
  35. package/dist/likec4diagram/ui/search/LikeC4Search.css.d.ts +8 -0
  36. package/dist/likec4diagram/ui/search/LikeC4Search.css.js +20 -0
  37. package/dist/likec4diagram/ui/search/LikeC4Search.d.ts +1 -0
  38. package/dist/likec4diagram/ui/search/LikeC4Search.js +265 -0
  39. package/dist/likec4diagram/ui/search/SearchInput.d.ts +1 -0
  40. package/dist/likec4diagram/ui/search/SearchInput.js +196 -0
  41. package/dist/likec4diagram/ui/search/ViewsColum.d.ts +9 -0
  42. package/dist/likec4diagram/ui/search/ViewsColum.js +130 -0
  43. package/dist/likec4diagram/ui/search/ViewsColumn.css.d.ts +5 -0
  44. package/dist/likec4diagram/ui/search/ViewsColumn.css.js +12 -0
  45. package/dist/likec4diagram/ui/search/_shared.css.d.ts +16 -0
  46. package/dist/likec4diagram/ui/search/_shared.css.js +5 -0
  47. package/dist/likec4diagram/ui/search/state.d.ts +19 -0
  48. package/dist/likec4diagram/ui/search/state.js +72 -0
  49. package/dist/likec4diagram/ui/search/utils.d.ts +3 -0
  50. package/dist/likec4diagram/ui/search/utils.js +23 -0
  51. package/dist/likec4diagram/useLayoutConstraints.js +4 -5
  52. package/dist/likec4model/useLikeC4ElementsTree.d.ts +1 -0
  53. package/dist/likec4model/useLikeC4ElementsTree.js +1 -0
  54. package/dist/overlays/Overlays.js +9 -9
  55. package/dist/overlays/element-details/ElementDetailsCard.js +20 -13
  56. package/dist/overlays/overlay/Overlay.d.ts +6 -2
  57. package/dist/overlays/overlay/Overlay.js +18 -29
  58. package/dist/overlays/relationship-details/SelectEdge.js +1 -3
  59. package/dist/overlays/relationships-browser/RelationshipsBrowser.js +6 -5
  60. package/dist/overlays/relationships-browser/SelectElement.js +10 -6
  61. package/dist/overlays/relationships-browser/actor.d.ts +2 -2
  62. package/dist/overlays/relationships-browser/actor.js +8 -8
  63. package/dist/overlays/relationships-browser/custom/nodeTypes.js +2 -2
  64. package/dist/style.css +1 -1
  65. package/package.json +13 -13
  66. package/src/LikeC4Diagram.tsx +0 -1
  67. package/src/base/index.ts +0 -2
  68. package/src/base/primitives/element/styles.css.ts +2 -2
  69. package/src/{base → components}/Link.css.ts +3 -3
  70. package/src/components/SearchControl.css.ts +69 -0
  71. package/src/components/SearchControl.tsx +33 -0
  72. package/src/context/DiagramFeatures.tsx +1 -1
  73. package/src/context/IconRenderer.tsx +54 -1
  74. package/src/globals.css.ts +0 -2
  75. package/src/hooks/useDiagram.ts +5 -0
  76. package/src/likec4diagram/DiagramActor.tsx +15 -2
  77. package/src/likec4diagram/DiagramUI.tsx +0 -4
  78. package/src/likec4diagram/XYFlow.tsx +39 -31
  79. package/src/likec4diagram/custom/edges/RelationshipsDropdownMenu.tsx +2 -1
  80. package/src/likec4diagram/state/assign.ts +1 -1
  81. package/src/likec4diagram/state/machine.ts +40 -14
  82. package/src/likec4diagram/state/syncManualLayoutActor.ts +6 -9
  83. package/src/likec4diagram/ui/controls/Controls.tsx +37 -25
  84. package/src/likec4diagram/ui/controls/NavigationButtons.tsx +36 -58
  85. package/src/likec4diagram/ui/controls/ToggleReadonly.tsx +31 -0
  86. package/src/likec4diagram/ui/controls/styles.css.ts +3 -1
  87. package/src/likec4diagram/ui/diagram-title/DiagramTitlePanel.tsx +1 -1
  88. package/src/likec4diagram/ui/search/ElementsColumn.css.ts +119 -0
  89. package/src/likec4diagram/ui/search/ElementsColumn.tsx +291 -0
  90. package/src/likec4diagram/ui/search/LikeC4Search.css.ts +117 -0
  91. package/src/likec4diagram/ui/search/LikeC4Search.tsx +312 -0
  92. package/src/likec4diagram/ui/search/SearchInput.tsx +235 -0
  93. package/src/likec4diagram/ui/search/ViewsColum.tsx +158 -0
  94. package/src/likec4diagram/ui/search/ViewsColumn.css.ts +37 -0
  95. package/src/likec4diagram/ui/search/_shared.css.ts +104 -0
  96. package/src/likec4diagram/ui/search/state.ts +76 -0
  97. package/src/likec4diagram/ui/search/utils.ts +20 -0
  98. package/src/likec4diagram/useLayoutConstraints.ts +4 -5
  99. package/src/likec4model/useLikeC4ElementsTree.ts +4 -4
  100. package/src/overlays/Overlays.tsx +31 -25
  101. package/src/overlays/element-details/ElementDetailsCard.css.ts +4 -4
  102. package/src/overlays/element-details/ElementDetailsCard.tsx +245 -240
  103. package/src/overlays/overlay/Overlay.css.ts +10 -9
  104. package/src/overlays/overlay/Overlay.tsx +38 -26
  105. package/src/overlays/relationship-details/SelectEdge.css.ts +1 -0
  106. package/src/overlays/relationship-details/SelectEdge.tsx +2 -4
  107. package/src/overlays/relationships-browser/RelationshipsBrowser.tsx +10 -5
  108. package/src/overlays/relationships-browser/SelectElement.tsx +88 -82
  109. package/src/overlays/relationships-browser/actor.ts +10 -10
  110. package/src/overlays/relationships-browser/custom/nodeTypes.tsx +2 -2
  111. package/dist/LikeC4Search.css.d.ts +0 -1
  112. package/dist/LikeC4Search.css.js +0 -5
  113. package/dist/LikeC4Search.d.ts +0 -2
  114. package/dist/LikeC4Search.js +0 -88
  115. package/src/LikeC4Search.css.ts +0 -12
  116. package/src/LikeC4Search.tsx +0 -91
  117. /package/dist/{base → components}/Link.css.d.ts +0 -0
  118. /package/dist/{base → components}/Link.d.ts +0 -0
  119. /package/dist/{base → components}/Link.js +0 -0
  120. /package/src/{base → components}/Link.tsx +0 -0
@@ -2,19 +2,13 @@ import { IconChevronRight } from '@tabler/icons-react'
2
2
 
3
3
  import { AnimatePresence, m } from 'framer-motion'
4
4
 
5
- import { Group } from '@mantine/core'
6
- import { IconChevronLeft, IconMenu2 } from '@tabler/icons-react'
7
- import clsx from 'clsx'
8
- import { LayoutGroup } from 'framer-motion'
9
- import { useDiagramEventHandlers } from '../../../context'
5
+ import { IconChevronLeft } from '@tabler/icons-react'
10
6
  import { useDiagram } from '../../../hooks/useDiagram'
11
7
  import { useDiagramContext } from '../../../hooks/useDiagramContext'
12
8
  import { ActionIcon } from './_shared'
13
- import * as css from './styles.css'
14
9
 
15
10
  export const NavigationButtons = () => {
16
11
  const diagram = useDiagram()
17
- const { onBurgerMenuClick } = useDiagramEventHandlers()
18
12
  const {
19
13
  hasStepBack,
20
14
  hasStepForward,
@@ -23,61 +17,45 @@ export const NavigationButtons = () => {
23
17
  hasStepForward: s.navigationHistory.currentIndex < s.navigationHistory.history.length - 1,
24
18
  }))
25
19
  return (
26
- <LayoutGroup>
27
- <Group
28
- className={clsx(css.navigationButtons, 'likec4-navigation-webview')}
29
- align="flex-start"
30
- gap={'xs'}>
31
- {onBurgerMenuClick && (
20
+ <AnimatePresence>
21
+ {hasStepBack && (
22
+ <m.div
23
+ layout
24
+ initial={{ opacity: 0.05, transform: 'translateX(-10px)' }}
25
+ animate={{ opacity: 1, transform: 'translateX(0)' }}
26
+ exit={{
27
+ opacity: 0.05,
28
+ transform: 'translateX(-10px)',
29
+ }}
30
+ key={'back'}>
32
31
  <ActionIcon
33
32
  onClick={e => {
34
33
  e.stopPropagation()
35
- onBurgerMenuClick()
34
+ diagram.navigate('back')
36
35
  }}>
37
- <IconMenu2 />
36
+ <IconChevronLeft />
38
37
  </ActionIcon>
39
- )}
40
- <AnimatePresence>
41
- {hasStepBack && (
42
- <m.div
43
- layout
44
- initial={{ opacity: 0.05, transform: 'translateX(-10px)' }}
45
- animate={{ opacity: 1, transform: 'translateX(0)' }}
46
- exit={{
47
- opacity: 0.05,
48
- transform: 'translateX(-10px)',
49
- }}
50
- key={'back'}>
51
- <ActionIcon
52
- onClick={e => {
53
- e.stopPropagation()
54
- diagram.navigate('back')
55
- }}>
56
- <IconChevronLeft />
57
- </ActionIcon>
58
- </m.div>
59
- )}
60
- {hasStepForward && (
61
- <m.div
62
- layout
63
- initial={{ opacity: 0.05, transform: 'translateX(10px)' }}
64
- animate={{ opacity: 1, transform: 'translateX(0)' }}
65
- exit={{
66
- opacity: 0,
67
- transform: 'translateX(10px)',
68
- }}
69
- key={'forward'}>
70
- <ActionIcon
71
- onClick={e => {
72
- e.stopPropagation()
73
- diagram.navigate('forward')
74
- }}>
75
- <IconChevronRight />
76
- </ActionIcon>
77
- </m.div>
78
- )}
79
- </AnimatePresence>
80
- </Group>
81
- </LayoutGroup>
38
+ </m.div>
39
+ )}
40
+ {hasStepForward && (
41
+ <m.div
42
+ layout
43
+ initial={{ opacity: 0.05, transform: 'translateX(10px)' }}
44
+ animate={{ opacity: 1, transform: 'translateX(0)' }}
45
+ exit={{
46
+ opacity: 0,
47
+ transform: 'translateX(10px)',
48
+ }}
49
+ key={'forward'}>
50
+ <ActionIcon
51
+ onClick={e => {
52
+ e.stopPropagation()
53
+ diagram.navigate('forward')
54
+ }}>
55
+ <IconChevronRight />
56
+ </ActionIcon>
57
+ </m.div>
58
+ )}
59
+ </AnimatePresence>
82
60
  )
83
61
  }
@@ -0,0 +1,31 @@
1
+ import { IconLock, IconLockOpen2 } from '@tabler/icons-react'
2
+ import { useDiagram } from '../../../hooks/useDiagram'
3
+ import { useDiagramContext } from '../../../hooks/useDiagramContext'
4
+ import type { DiagramContext } from '../../state/machine'
5
+ import { ActionIcon, Tooltip } from './_shared'
6
+
7
+ const selector = (state: DiagramContext) => ({
8
+ visible: state.features.enableReadOnly !== true,
9
+ isReadOnly: state.toggledFeatures.enableReadOnly ?? state.features.enableReadOnly,
10
+ })
11
+
12
+ export const ToggleReadonly = () => {
13
+ const { visible, isReadOnly } = useDiagramContext(selector)
14
+ const diagram = useDiagram()
15
+
16
+ if (!visible) {
17
+ return null
18
+ }
19
+
20
+ return (
21
+ <Tooltip label={isReadOnly ? 'Enable editing' : 'Disable editing'}>
22
+ <ActionIcon
23
+ onClick={e => {
24
+ e.stopPropagation()
25
+ diagram.toggleFeature('ReadOnly')
26
+ }}>
27
+ {isReadOnly ? <IconLock /> : <IconLockOpen2 />}
28
+ </ActionIcon>
29
+ </Tooltip>
30
+ )
31
+ }
@@ -5,12 +5,14 @@ import { mantine, vars } from '../../../theme-vars'
5
5
 
6
6
  export const navigationButtons = style({
7
7
  gap: calc(mantine.spacing.xs).divide(1.5).toString(),
8
+ ':empty': {
9
+ display: 'none',
10
+ },
8
11
  })
9
12
 
10
13
  export const panel = style({
11
14
  top: fallbackVar(vars.navigationPanel.top, '1rem'),
12
15
  left: fallbackVar(vars.navigationPanel.left, '1rem'),
13
- gap: calc(mantine.spacing.xs).divide(1.5).toString(),
14
16
  margin: 0,
15
17
  })
16
18
 
@@ -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 { Link } from '../../../base'
6
+ import { Link } from '../../../components/Link'
7
7
  import { useDiagramContext } from '../../../hooks/useDiagramContext'
8
8
  import type { DiagramContext } from '../../state/machine'
9
9
  import * as css from './DiagramTitlePanel.css'
@@ -0,0 +1,119 @@
1
+ import { createContainer, createGlobalVar, fallbackVar, globalStyle, style } from '@vanilla-extract/css'
2
+ import { mantine } from '../../../theme-vars'
3
+ import { whereLight } from '../../../theme-vars.css'
4
+ import { button, buttonFocused, description, descriptionColor, iconColor, title } from './_shared.css'
5
+
6
+ export { focusable } from './_shared.css'
7
+
8
+ export const treeContainer = createContainer('likec4-elements-tree')
9
+
10
+ const whenContainerIsNarrow = `${treeContainer} (width < 450px)`
11
+
12
+ export const treeNode = style({
13
+ outline: 'none',
14
+ marginBottom: 8,
15
+ })
16
+ export const treeRoot = style({
17
+ overflow: 'hidden',
18
+ containerName: treeContainer,
19
+ containerType: 'inline-size',
20
+ height: '100%',
21
+ })
22
+ export const treeLabel = style({
23
+ display: 'flex',
24
+ alignItems: 'baseline',
25
+ outline: 'none !important',
26
+ gap: 4,
27
+ })
28
+ export const treeSubtree = style({
29
+ marginTop: 8,
30
+ })
31
+
32
+ export const iconSize = createGlobalVar('likec4-icon-size')
33
+ export const elementExpandIcon = style({
34
+ color: mantine.colors.dimmed,
35
+ })
36
+
37
+ export const elementButton = style([button, {
38
+ flexGrow: 1,
39
+ vars: {
40
+ [iconSize]: '24px',
41
+ },
42
+ '@container': {
43
+ [`${whenContainerIsNarrow}`]: {
44
+ vars: {
45
+ [iconSize]: '18px',
46
+ },
47
+ },
48
+ },
49
+ }])
50
+
51
+ globalStyle(`${treeNode}:focus > ${treeLabel} ${elementButton}`, buttonFocused)
52
+
53
+ export const elementTitleAndId = style({
54
+ '@container': {
55
+ [`${whenContainerIsNarrow}`]: {
56
+ flexDirection: 'column-reverse',
57
+ alignItems: 'flex-start',
58
+ gap: 2,
59
+ },
60
+ },
61
+ })
62
+
63
+ export const elementTitle = style([title])
64
+ export const elementId = style({
65
+ color: fallbackVar(descriptionColor, mantine.colors.dimmed),
66
+ fontSize: 10,
67
+ lineHeight: 1.3,
68
+ display: 'block',
69
+ fontWeight: 500,
70
+ whiteSpace: 'nowrap',
71
+ padding: '1px 5px',
72
+ borderRadius: 4,
73
+ background: `color-mix(in srgb, ${mantine.colors.dark[9]}, transparent 70%)`,
74
+ selectors: {
75
+ [`${whereLight} &`]: {
76
+ backgroundColor: `color-mix(in srgb, ${mantine.colors.gray[3]}, transparent 80%)`,
77
+ },
78
+ },
79
+ })
80
+ export const elementDescription = style([description])
81
+
82
+ export const elementIcon = style({
83
+ flex: `0 0 ${iconSize}`,
84
+ height: iconSize,
85
+ width: iconSize,
86
+ display: 'flex',
87
+ alignItems: 'center',
88
+ justifyContent: 'center',
89
+ alignSelf: 'flex-start',
90
+ marginTop: 4,
91
+ })
92
+ globalStyle(`${elementIcon} svg, ${elementIcon} img`, {
93
+ width: '100%',
94
+ height: 'auto',
95
+ maxHeight: '100%',
96
+ pointerEvents: 'none',
97
+ })
98
+ globalStyle(`${elementIcon} img`, {
99
+ objectFit: 'contain',
100
+ })
101
+ globalStyle(`${elementIcon}.likec4-shape-icon svg`, {
102
+ color: iconColor,
103
+ width: '90%',
104
+ strokeWidth: 1.5,
105
+ })
106
+
107
+ export const elementViewsCount = style({
108
+ flex: 0,
109
+ color: fallbackVar(descriptionColor, mantine.colors.dimmed),
110
+ fontSize: 10,
111
+ fontWeight: 500,
112
+ whiteSpace: 'nowrap',
113
+ lineHeight: 1.1,
114
+ '@container': {
115
+ [`${whenContainerIsNarrow}`]: {
116
+ display: 'none',
117
+ },
118
+ },
119
+ })
@@ -0,0 +1,291 @@
1
+ import {
2
+ type Fqn,
3
+ ifilter,
4
+ isAncestor,
5
+ LikeC4Model,
6
+ nameFromFqn,
7
+ sortParentsFirst,
8
+ toArray,
9
+ } from '@likec4/core'
10
+ import {
11
+ type RenderTreeNodePayload,
12
+ ActionIcon,
13
+ Box,
14
+ Group,
15
+ Highlight,
16
+ Text,
17
+ Tooltip,
18
+ Tree,
19
+ UnstyledButton,
20
+ useTree,
21
+ } from '@mantine/core'
22
+ import { useCallbackRef } from '@mantine/hooks'
23
+ import { IconChevronRight } from '@tabler/icons-react'
24
+ import clsx from 'clsx'
25
+ import { m } from 'framer-motion'
26
+ import { useEffect, useMemo } from 'react'
27
+ import { first, indexBy, isEmpty, only, partition, pipe, prop, reduce } from 'remeda'
28
+ import { IconOrShapeRenderer } from '../../../context/IconRenderer'
29
+ import { sortByLabel } from '../../../likec4model/useLikeC4ElementsTree'
30
+ import { useLikeC4Model } from '../../../likec4model/useLikeC4Model'
31
+ import * as css from './ElementsColumn.css'
32
+ import { setPickView, useCloseSearchAndNavigateTo, useNormalizedSearch } from './state'
33
+ import { centerY, moveFocusToSearchInput, stopAndPrevent } from './utils'
34
+ import { NothingFound } from './ViewsColum'
35
+
36
+ interface LikeC4ModelTreeNodeData {
37
+ label: string
38
+ value: Fqn
39
+ element: LikeC4Model.Element
40
+ searchTerms: string[]
41
+ children: LikeC4ModelTreeNodeData[]
42
+ }
43
+
44
+ function buildNode(
45
+ element: LikeC4Model.Element,
46
+ searchTerms: string[] = [],
47
+ ): LikeC4ModelTreeNodeData {
48
+ return {
49
+ label: element.title,
50
+ value: element.id,
51
+ element,
52
+ searchTerms,
53
+ children: [...element.children()].map(e => buildNode(e, searchTerms)).sort(sortByLabel),
54
+ }
55
+ }
56
+
57
+ export function ElementsColumn() {
58
+ const search = useNormalizedSearch()
59
+ const model = useLikeC4Model(true)
60
+ const {
61
+ all,
62
+ byid,
63
+ roots: data,
64
+ } = useMemo(() => {
65
+ const searchTerms = search.split('.')
66
+ let elements
67
+ if (isEmpty(search) || search === 'kind:') {
68
+ elements = model.elements()
69
+ } else {
70
+ elements = ifilter(model.elements(), element => {
71
+ if (search.startsWith('kind:')) {
72
+ return element.kind.toLocaleLowerCase().startsWith(search.slice(5))
73
+ }
74
+ if (search.startsWith('#')) {
75
+ return element.tags.some((tag) => tag.toLocaleLowerCase().includes(search.slice(1)))
76
+ }
77
+ return (element.title + ' ' + element.id + ' ' + (element.description ?? ''))
78
+ .toLocaleLowerCase().includes(search)
79
+ })
80
+ }
81
+ const { all, roots } = pipe(
82
+ elements,
83
+ toArray(),
84
+ sortParentsFirst,
85
+ reduce((acc, element) => {
86
+ const treeItem: LikeC4ModelTreeNodeData = {
87
+ label: element.title,
88
+ value: element.id,
89
+ element,
90
+ searchTerms,
91
+ children: [],
92
+ }
93
+ const parent = acc.all.findLast((root) => isAncestor(root.value, treeItem.value))
94
+ if (parent) {
95
+ parent.children.push(treeItem)
96
+ if (parent.children.length > 1) {
97
+ parent.children.sort(sortByLabel)
98
+ }
99
+ } else {
100
+ acc.roots.push(treeItem)
101
+ }
102
+ acc.all.push(treeItem)
103
+ return acc
104
+ }, {
105
+ all: [] as LikeC4ModelTreeNodeData[],
106
+ roots: [] as LikeC4ModelTreeNodeData[],
107
+ }),
108
+ )
109
+ return {
110
+ all,
111
+ byid: indexBy(all, prop('value')),
112
+ roots: roots.sort(sortByLabel),
113
+ }
114
+ }, [model, search])
115
+
116
+ const tree = useTree({
117
+ multiple: false,
118
+ })
119
+ useEffect(() => {
120
+ tree.collapseAllNodes()
121
+ for (const nd of all) {
122
+ if (nd.children.length > 0) {
123
+ tree.expand(nd.value)
124
+ }
125
+ }
126
+ }, [all])
127
+
128
+ const handleClick = useHandleElementSelection()
129
+
130
+ return (
131
+ <>
132
+ {data.length === 0 && <NothingFound />}
133
+ <Tree
134
+ data-likec4-search-elements
135
+ allowRangeSelection={false}
136
+ clearSelectionOnOutsideClick
137
+ selectOnClick={false}
138
+ tree={tree}
139
+ data={data}
140
+ levelOffset={'lg'}
141
+ classNames={{
142
+ root: css.treeRoot,
143
+ node: clsx(css.focusable, css.treeNode),
144
+ label: css.treeLabel,
145
+ subtree: css.treeSubtree,
146
+ }}
147
+ onKeyDownCapture={(e) => {
148
+ const target = e.target as HTMLElement
149
+ const id = target.getAttribute('data-value')
150
+ const node = !!id && byid[id as Fqn]
151
+ if (!node) return
152
+ if (e.key === 'ArrowUp') {
153
+ if (id === data[0]?.value) {
154
+ stopAndPrevent(e)
155
+ moveFocusToSearchInput()
156
+ }
157
+ return
158
+ }
159
+ if (e.key === 'ArrowRight') {
160
+ const hasChildren = node.children.length > 0
161
+ if (hasChildren && tree.expandedState[id] === false) {
162
+ return
163
+ }
164
+ const label = (e.target as HTMLLIElement).querySelector<HTMLLIElement>('.mantine-Tree-label') ?? target
165
+ const maxY = label.getBoundingClientRect().y
166
+ const viewButtons = [...document.querySelectorAll<HTMLButtonElement>(
167
+ `[data-likec4-search-views] .${css.focusable}`,
168
+ )]
169
+ let view = viewButtons.length > 1
170
+ ? viewButtons.find((el, i, all) => centerY(el) > maxY || i === all.length - 1)
171
+ : null
172
+ view ??= first(viewButtons)
173
+ if (view) {
174
+ stopAndPrevent(e)
175
+ view.focus()
176
+ }
177
+ return
178
+ }
179
+ if (e.key === ' ' || e.key === 'Enter') {
180
+ stopAndPrevent(e)
181
+ handleClick(node.element)
182
+ return
183
+ }
184
+ }}
185
+ renderNode={ElementTreeNode}
186
+ />
187
+ </>
188
+ )
189
+ }
190
+
191
+ function ElementTreeNode(
192
+ { node, elementProps, hasChildren, expanded }: RenderTreeNodePayload,
193
+ ) {
194
+ const { element, searchTerms } = node as LikeC4ModelTreeNodeData
195
+ const elementIcon = IconOrShapeRenderer({
196
+ element: {
197
+ id: element.id,
198
+ title: element.title,
199
+ shape: element.shape,
200
+ icon: element.icon,
201
+ },
202
+ className: css.elementIcon,
203
+ })
204
+ const views = [...element.views()]
205
+
206
+ const handleClick = useHandleElementSelection()
207
+
208
+ const key = `@tree.${node.value}`
209
+
210
+ return (
211
+ <m.div layoutId={key} key={key} {...elementProps as any}>
212
+ <ActionIcon
213
+ variant="transparent"
214
+ size={16}
215
+ tabIndex={-1}
216
+ className={clsx(css.elementExpandIcon)}
217
+ style={{
218
+ visibility: hasChildren ? 'visible' : 'hidden',
219
+ }}>
220
+ <IconChevronRight
221
+ stroke={3.5}
222
+ style={{
223
+ transition: 'transform 150ms ease',
224
+ transform: `rotate(${expanded ? '90deg' : '0'})`,
225
+ width: '100%',
226
+ }} />
227
+ </ActionIcon>
228
+ <UnstyledButton
229
+ component={m.button}
230
+ layout
231
+ tabIndex={-1}
232
+ className={clsx(css.elementButton, 'likec4-element-button')}
233
+ {...views.length > 0 && {
234
+ onClick: (e) => {
235
+ if (!hasChildren || expanded) {
236
+ e.stopPropagation()
237
+ handleClick(element)
238
+ }
239
+ },
240
+ }}
241
+ >
242
+ {elementIcon}
243
+ <Box style={{ flexGrow: 1 }}>
244
+ <Group gap={'xs'} wrap="nowrap" align="center" className={css.elementTitleAndId}>
245
+ <Highlight component="div" highlight={searchTerms} className={css.elementTitle}>
246
+ {node.label as any}
247
+ </Highlight>
248
+ <Tooltip label={element.id} withinPortal={false} fz={'xs'} disabled={!element.id.includes('.')}>
249
+ <Highlight component="div" highlight={searchTerms} className={css.elementId}>
250
+ {nameFromFqn(element.id)}
251
+ </Highlight>
252
+ </Tooltip>
253
+ </Group>
254
+ <Highlight component="div" highlight={searchTerms} className={css.elementDescription} lineClamp={1}>
255
+ {element.description || 'No description'}
256
+ </Highlight>
257
+ </Box>
258
+
259
+ <Text component="div" className={css.elementViewsCount}>
260
+ {views.length === 0 ? 'No views' : (
261
+ <>
262
+ {views.length} view{views.length > 1 ? 's' : ''}
263
+ </>
264
+ )}
265
+ </Text>
266
+ </UnstyledButton>
267
+ </m.div>
268
+ )
269
+ }
270
+
271
+ function useHandleElementSelection() {
272
+ const navigateTo = useCloseSearchAndNavigateTo()
273
+
274
+ return useCallbackRef((element: LikeC4Model.Element) => {
275
+ const views = [...element.views()]
276
+ if (views.length === 0) {
277
+ return
278
+ }
279
+ const singleView = only(views)
280
+ if (singleView) {
281
+ navigateTo(singleView.id, element.id)
282
+ return
283
+ }
284
+ const [scoped, others] = partition(views, v => v.viewOf?.id === element.id)
285
+ setPickView({
286
+ elementFqn: element.id,
287
+ scoped,
288
+ others,
289
+ })
290
+ })
291
+ }
@@ -0,0 +1,117 @@
1
+ import { rem } from '@mantine/core'
2
+ import { fallbackVar, globalStyle, style } from '@vanilla-extract/css'
3
+ import { mantine } from '../../../theme-vars'
4
+ import { vars, whereDark, whereLight } from '../../../theme-vars.css'
5
+ import { button, buttonFocused } from './_shared.css'
6
+ export { focusable } from './_shared.css'
7
+
8
+ export const backdrop = style({
9
+ position: 'fixed',
10
+ zIndex: 900,
11
+ inset: 0,
12
+ backgroundColor: 'rgb(34 34 34 / 0.95)',
13
+ WebkitBackdropFilter: fallbackVar(vars.safariAnimationHook, 'blur(6px)'),
14
+ backdropFilter: fallbackVar(vars.safariAnimationHook, 'blur(6px)'),
15
+
16
+ selectors: {
17
+ [`${whereLight} &`]: {
18
+ backgroundColor: 'rgb(255 255 255 / 0.92 )',
19
+ },
20
+ },
21
+ })
22
+ export const root = style({
23
+ containerName: 'likec4-search',
24
+ containerType: 'size',
25
+ position: 'fixed',
26
+ zIndex: 901,
27
+ top: 0,
28
+ left: 0,
29
+ width: '100%',
30
+ height: '100%',
31
+ maxHeight: '100vh',
32
+ overflow: 'hidden',
33
+ display: 'flex',
34
+ flexDirection: 'column',
35
+ justifyContent: 'stretch',
36
+ gap: mantine.spacing.md,
37
+ paddingTop: rem(32),
38
+ paddingLeft: rem(16),
39
+ paddingRight: rem(16),
40
+ paddingBottom: 0,
41
+ })
42
+ export const input = style({
43
+ borderColor: 'transparent',
44
+ WebkitBackdropFilter: fallbackVar(vars.safariAnimationHook, 'blur(6px)'),
45
+ backdropFilter: fallbackVar(vars.safariAnimationHook, 'blur(6px)'),
46
+ backgroundColor: 'transparent',
47
+ ':focus-within': {
48
+ backgroundColor: `color-mix(in srgb, ${mantine.colors.gray[3]}, transparent 45%)`,
49
+ },
50
+ selectors: {
51
+ [`${whereDark} &`]: {},
52
+ [`${whereDark} &:focus-within`]: {
53
+ backgroundColor: `color-mix(in srgb, ${mantine.colors.dark[4]}, transparent 45%)`,
54
+ },
55
+ },
56
+ })
57
+
58
+ export const pickviewBackdrop = style({
59
+ position: 'absolute',
60
+ inset: 0,
61
+ width: '100%',
62
+ height: '100%',
63
+ backgroundColor: 'rgb(34 34 34 / 0.5)',
64
+ zIndex: 902,
65
+ WebkitBackdropFilter: fallbackVar(vars.safariAnimationHook, 'blur(6px)'),
66
+ backdropFilter: fallbackVar(vars.safariAnimationHook, 'blur(6px)'),
67
+ selectors: {
68
+ [`${whereLight} &`]: {
69
+ backgroundColor: 'rgb(255 255 255 / 0.6 )',
70
+ },
71
+ },
72
+ })
73
+ export const pickview = style({
74
+ position: 'absolute',
75
+ top: '2rem',
76
+ left: '50%',
77
+ width: '100%',
78
+ maxWidth: 600,
79
+ minWidth: 200,
80
+ transform: 'translateX(-50%)',
81
+ zIndex: 903,
82
+ })
83
+
84
+ export const pickviewGroup = style({
85
+ marginTop: rem(8),
86
+ selectors: {
87
+ '& + &': {
88
+ marginTop: rem(32),
89
+ },
90
+ },
91
+ })
92
+
93
+ globalStyle(`${whereDark} ${pickview} ${button}`, {
94
+ borderColor: mantine.colors.dark[5],
95
+ backgroundColor: mantine.colors.dark[6],
96
+ })
97
+ globalStyle(`${whereDark} ${pickview} ${button}:hover`, {
98
+ ...buttonFocused,
99
+ backgroundColor: `color-mix(in srgb, ${buttonFocused.backgroundColor}, transparent 40%)`,
100
+ })
101
+ globalStyle(`${whereDark} ${pickview} ${button}:focus`, buttonFocused)
102
+
103
+ export const scrollArea = style({
104
+ height: [
105
+ '100%',
106
+ '100cqh',
107
+ ],
108
+ })
109
+
110
+ globalStyle(`.${scrollArea} .mantine-ScrollArea-viewport`, {
111
+ minHeight: '100%',
112
+ })
113
+
114
+ globalStyle(`.${scrollArea} .mantine-ScrollArea-viewport > div`, {
115
+ minHeight: '100%',
116
+ height: '100%',
117
+ })