@likec4/diagram 1.21.1 → 1.22.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 (109) 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/IconRenderer.d.ts +10 -0
  10. package/dist/context/IconRenderer.js +34 -0
  11. package/dist/hooks/useCurrentViewId.js +8 -0
  12. package/dist/hooks/useDiagram.d.ts +4 -4
  13. package/dist/likec4diagram/DiagramUI.js +0 -2
  14. package/dist/likec4diagram/XYFlow.d.ts +6 -1
  15. package/dist/likec4diagram/XYFlow.js +28 -29
  16. package/dist/likec4diagram/custom/edges/RelationshipsDropdownMenu.js +2 -1
  17. package/dist/likec4diagram/state/actorContext.d.ts +16 -16
  18. package/dist/likec4diagram/state/assign.js +1 -1
  19. package/dist/likec4diagram/state/machine.d.ts +7 -7
  20. package/dist/likec4diagram/state/machine.js +10 -7
  21. package/dist/likec4diagram/ui/controls/Controls.js +63 -48
  22. package/dist/likec4diagram/ui/controls/NavigationButtons.js +44 -69
  23. package/dist/likec4diagram/ui/controls/_shared.js +1 -1
  24. package/dist/likec4diagram/ui/diagram-title/DiagramTitlePanel.js +1 -1
  25. package/dist/likec4diagram/ui/search/ElementsColumn.css.d.ts +15 -0
  26. package/dist/likec4diagram/ui/search/ElementsColumn.css.js +30 -0
  27. package/dist/likec4diagram/ui/search/ElementsColumn.d.ts +1 -0
  28. package/dist/likec4diagram/ui/search/ElementsColumn.js +243 -0
  29. package/dist/likec4diagram/ui/search/LikeC4Search.css.d.ts +8 -0
  30. package/dist/likec4diagram/ui/search/LikeC4Search.css.js +20 -0
  31. package/dist/likec4diagram/ui/search/LikeC4Search.d.ts +1 -0
  32. package/dist/likec4diagram/ui/search/LikeC4Search.js +265 -0
  33. package/dist/likec4diagram/ui/search/SearchInput.d.ts +1 -0
  34. package/dist/likec4diagram/ui/search/SearchInput.js +196 -0
  35. package/dist/likec4diagram/ui/search/ViewsColum.d.ts +9 -0
  36. package/dist/likec4diagram/ui/search/ViewsColum.js +130 -0
  37. package/dist/likec4diagram/ui/search/ViewsColumn.css.d.ts +5 -0
  38. package/dist/likec4diagram/ui/search/ViewsColumn.css.js +12 -0
  39. package/dist/likec4diagram/ui/search/_shared.css.d.ts +16 -0
  40. package/dist/likec4diagram/ui/search/_shared.css.js +5 -0
  41. package/dist/likec4diagram/ui/search/state.d.ts +19 -0
  42. package/dist/likec4diagram/ui/search/state.js +72 -0
  43. package/dist/likec4diagram/ui/search/utils.d.ts +3 -0
  44. package/dist/likec4diagram/ui/search/utils.js +23 -0
  45. package/dist/likec4diagram/useLayoutConstraints.js +4 -5
  46. package/dist/likec4model/useLikeC4ElementsTree.d.ts +1 -0
  47. package/dist/likec4model/useLikeC4ElementsTree.js +1 -0
  48. package/dist/overlays/Overlays.js +9 -9
  49. package/dist/overlays/element-details/ElementDetailsCard.js +20 -13
  50. package/dist/overlays/overlay/Overlay.d.ts +6 -2
  51. package/dist/overlays/overlay/Overlay.js +18 -29
  52. package/dist/overlays/relationship-details/SelectEdge.js +1 -3
  53. package/dist/overlays/relationships-browser/RelationshipsBrowser.js +6 -5
  54. package/dist/overlays/relationships-browser/SelectElement.js +10 -6
  55. package/dist/overlays/relationships-browser/actor.d.ts +2 -2
  56. package/dist/overlays/relationships-browser/actor.js +8 -8
  57. package/dist/overlays/relationships-browser/custom/nodeTypes.js +2 -2
  58. package/dist/style.css +1 -1
  59. package/package.json +13 -12
  60. package/src/LikeC4Diagram.tsx +0 -1
  61. package/src/base/index.ts +0 -2
  62. package/src/base/primitives/element/styles.css.ts +2 -2
  63. package/src/{base → components}/Link.css.ts +3 -3
  64. package/src/components/SearchControl.css.ts +69 -0
  65. package/src/components/SearchControl.tsx +33 -0
  66. package/src/context/IconRenderer.tsx +54 -1
  67. package/src/globals.css.ts +0 -2
  68. package/src/likec4diagram/DiagramUI.tsx +0 -4
  69. package/src/likec4diagram/XYFlow.tsx +30 -30
  70. package/src/likec4diagram/custom/edges/RelationshipsDropdownMenu.tsx +2 -1
  71. package/src/likec4diagram/state/assign.ts +1 -1
  72. package/src/likec4diagram/state/machine.ts +12 -10
  73. package/src/likec4diagram/ui/controls/Controls.tsx +35 -25
  74. package/src/likec4diagram/ui/controls/NavigationButtons.tsx +36 -58
  75. package/src/likec4diagram/ui/controls/styles.css.ts +3 -1
  76. package/src/likec4diagram/ui/diagram-title/DiagramTitlePanel.tsx +1 -1
  77. package/src/likec4diagram/ui/search/ElementsColumn.css.ts +119 -0
  78. package/src/likec4diagram/ui/search/ElementsColumn.tsx +291 -0
  79. package/src/likec4diagram/ui/search/LikeC4Search.css.ts +117 -0
  80. package/src/likec4diagram/ui/search/LikeC4Search.tsx +312 -0
  81. package/src/likec4diagram/ui/search/SearchInput.tsx +235 -0
  82. package/src/likec4diagram/ui/search/ViewsColum.tsx +158 -0
  83. package/src/likec4diagram/ui/search/ViewsColumn.css.ts +37 -0
  84. package/src/likec4diagram/ui/search/_shared.css.ts +104 -0
  85. package/src/likec4diagram/ui/search/state.ts +76 -0
  86. package/src/likec4diagram/ui/search/utils.ts +20 -0
  87. package/src/likec4diagram/useLayoutConstraints.ts +4 -5
  88. package/src/likec4model/useLikeC4ElementsTree.ts +4 -4
  89. package/src/overlays/Overlays.tsx +31 -25
  90. package/src/overlays/element-details/ElementDetailsCard.css.ts +4 -4
  91. package/src/overlays/element-details/ElementDetailsCard.tsx +245 -240
  92. package/src/overlays/overlay/Overlay.css.ts +10 -9
  93. package/src/overlays/overlay/Overlay.tsx +38 -26
  94. package/src/overlays/relationship-details/SelectEdge.css.ts +1 -0
  95. package/src/overlays/relationship-details/SelectEdge.tsx +2 -4
  96. package/src/overlays/relationships-browser/RelationshipsBrowser.tsx +10 -5
  97. package/src/overlays/relationships-browser/SelectElement.tsx +88 -82
  98. package/src/overlays/relationships-browser/actor.ts +10 -10
  99. package/src/overlays/relationships-browser/custom/nodeTypes.tsx +2 -2
  100. package/dist/LikeC4Search.css.d.ts +0 -1
  101. package/dist/LikeC4Search.css.js +0 -5
  102. package/dist/LikeC4Search.d.ts +0 -2
  103. package/dist/LikeC4Search.js +0 -88
  104. package/src/LikeC4Search.css.ts +0 -12
  105. package/src/LikeC4Search.tsx +0 -91
  106. /package/dist/{base → components}/Link.css.d.ts +0 -0
  107. /package/dist/{base → components}/Link.d.ts +0 -0
  108. /package/dist/{base → components}/Link.js +0 -0
  109. /package/src/{base → components}/Link.tsx +0 -0
@@ -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
+ })
@@ -0,0 +1,312 @@
1
+ import {
2
+ ActionIcon,
3
+ Box,
4
+ Grid,
5
+ GridCol,
6
+ Group,
7
+ Portal,
8
+ RemoveScroll,
9
+ ScrollArea,
10
+ ScrollAreaAutosize,
11
+ Stack,
12
+ Title,
13
+ } from '@mantine/core'
14
+ import {
15
+ useCallbackRef,
16
+ useDisclosure,
17
+ useFocusReturn,
18
+ useFocusTrap,
19
+ useFocusWithin,
20
+ useHotkeys,
21
+ useMergedRef,
22
+ useWindowEvent,
23
+ } from '@mantine/hooks'
24
+ import { IconX } from '@tabler/icons-react'
25
+ import { AnimatePresence, LayoutGroup, m } from 'framer-motion'
26
+ import { useRef } from 'react'
27
+ import { SearchControl } from '../../../components/SearchControl'
28
+ import { useMantinePortalProps } from '../../../hooks'
29
+ import { ElementsColumn } from './ElementsColumn'
30
+ import * as css from './LikeC4Search.css'
31
+ import { LikeC4SearchInput } from './SearchInput'
32
+ import {
33
+ LikeC4SearchContext,
34
+ setPickView,
35
+ useCloseSearch,
36
+ usePickView,
37
+ wasResetPickView,
38
+ } from './state'
39
+ import { stopAndPrevent } from './utils'
40
+ import { ViewButton, ViewsColumn } from './ViewsColum'
41
+
42
+ export function LikeC4Search() {
43
+ const [searchOpened, searchOps] = useDisclosure(false)
44
+ useHotkeys([
45
+ ['mod+k', () => searchOps.toggle(), {
46
+ preventDefault: true,
47
+ }],
48
+ ['mod+f', () => searchOps.toggle(), {
49
+ preventDefault: true,
50
+ }],
51
+ ])
52
+
53
+ const afterCloseCbRef = useRef<(() => void)>(null)
54
+ const close = useCallbackRef((cb?: () => void) => {
55
+ afterCloseCbRef.current = cb ?? null
56
+ searchOps.close()
57
+ })
58
+ const onExitComplete = useCallbackRef(() => {
59
+ if (afterCloseCbRef.current) {
60
+ afterCloseCbRef.current()
61
+ afterCloseCbRef.current = null
62
+ }
63
+ })
64
+
65
+ const { portalProps } = useMantinePortalProps()
66
+
67
+ return (
68
+ <LikeC4SearchContext value={close}>
69
+ <SearchControl
70
+ onClick={e => {
71
+ e.stopPropagation()
72
+ searchOps.toggle()
73
+ }} />
74
+ <Portal {...portalProps}>
75
+ <RemoveScroll enabled={searchOpened}>
76
+ <AnimatePresence onExitComplete={onExitComplete}>
77
+ {searchOpened && (
78
+ <>
79
+ <m.div
80
+ key={'backdrop'}
81
+ className={css.backdrop}
82
+ initial={{ opacity: 0.7 }}
83
+ animate={{
84
+ opacity: 1,
85
+ transition: {
86
+ duration: 0.1,
87
+ },
88
+ }}
89
+ exit={{
90
+ opacity: 0,
91
+ transition: {
92
+ duration: 0.075,
93
+ },
94
+ }}>
95
+ </m.div>
96
+ <LikeC4SearchOverlay key="overlay" />
97
+ </>
98
+ )}
99
+ </AnimatePresence>
100
+ </RemoveScroll>
101
+ </Portal>
102
+ </LikeC4SearchContext>
103
+ )
104
+ }
105
+
106
+ function LikeC4SearchOverlay() {
107
+ const close = useCloseSearch()
108
+ const focusTrapRef = useFocusTrap()
109
+ const { ref: forcusRef, focused } = useFocusWithin()
110
+
111
+ const ref = useMergedRef(forcusRef, focusTrapRef)
112
+
113
+ useWindowEvent(
114
+ 'keydown',
115
+ (event) => {
116
+ if (event.key === 'Escape') {
117
+ stopAndPrevent(event)
118
+ if (!wasResetPickView()) {
119
+ close()
120
+ }
121
+ return
122
+ }
123
+ },
124
+ )
125
+
126
+ return (
127
+ <m.div
128
+ ref={ref}
129
+ className={css.root}
130
+ // initial={{
131
+ // opacity: 0,
132
+ // // scale: 0.9,
133
+ // translateY: -20,
134
+ // }}
135
+ animate={{
136
+ opacity: 1,
137
+ scale: 1,
138
+ translateY: 0,
139
+ }}
140
+ exit={{
141
+ opacity: 0,
142
+ scale: 0.9,
143
+ transition: {
144
+ duration: 0.1,
145
+ },
146
+ }}
147
+ >
148
+ <Group wrap="nowrap">
149
+ <Box flex={1} px={'sm'}>
150
+ <LikeC4SearchInput />
151
+ </Box>
152
+ <Box flex={'0 0 auto'}>
153
+ <ActionIcon
154
+ size={'lg'}
155
+ variant="default"
156
+ onClick={(e) => {
157
+ e.stopPropagation()
158
+ close()
159
+ }}>
160
+ <IconX />
161
+ </ActionIcon>
162
+ </Box>
163
+ </Group>
164
+ <Grid>
165
+ <GridCol span={6}>
166
+ <Title component={'div'} order={6} c="dimmed" pl="sm">Elements</Title>
167
+ </GridCol>
168
+ <GridCol span={6}>
169
+ <Title component={'div'} order={6} c="dimmed" pl="sm">Views</Title>
170
+ </GridCol>
171
+ </Grid>
172
+ <Grid
173
+ style={{
174
+ containerName: 'likec4-search-elements',
175
+ containerType: 'size',
176
+ overflow: 'hidden',
177
+ flexGrow: 1,
178
+ }}>
179
+ <GridCol span={6}>
180
+ <ScrollArea
181
+ type="hover"
182
+ className={css.scrollArea}
183
+ pr="xs"
184
+ scrollbars="y">
185
+ <AnimatePresence>
186
+ <LayoutGroup id="likec4-search-elements">
187
+ <ElementsColumn />
188
+ </LayoutGroup>
189
+ </AnimatePresence>
190
+ </ScrollArea>
191
+ </GridCol>
192
+ <GridCol span={6}>
193
+ <ScrollArea
194
+ type="hover"
195
+ className={css.scrollArea}
196
+ pr="xs"
197
+ scrollbars="y">
198
+ <AnimatePresence>
199
+ <LayoutGroup id="likec4-search-views">
200
+ <ViewsColumn />
201
+ </LayoutGroup>
202
+ </AnimatePresence>
203
+ </ScrollArea>
204
+ </GridCol>
205
+ </Grid>
206
+ <Box></Box>
207
+ <PickView />
208
+ </m.div>
209
+ )
210
+ }
211
+
212
+ function PickView() {
213
+ const pickview = usePickView()
214
+ // const navigateTo = useCloseSearchAndNavigateTo()
215
+ const focusTrapRef = useFocusTrap(!!pickview)
216
+ useFocusReturn({
217
+ opened: !!pickview,
218
+ })
219
+
220
+ return (
221
+ <AnimatePresence>
222
+ <LayoutGroup id="likec4-search-pickview">
223
+ {pickview && (
224
+ <>
225
+ <m.div
226
+ key="backdrop"
227
+ className={css.pickviewBackdrop}
228
+ onClick={e => {
229
+ e.stopPropagation()
230
+ setPickView(null)
231
+ }}>
232
+ </m.div>
233
+ <m.div
234
+ key="pickview"
235
+ initial={{
236
+ opacity: 0,
237
+ scale: 0.95,
238
+ originY: 0,
239
+ translateX: '-50%',
240
+ translateY: -20,
241
+ }}
242
+ animate={{
243
+ opacity: 1,
244
+ scale: 1,
245
+ translateY: 0,
246
+ }}
247
+ exit={{
248
+ opacity: 0,
249
+ scale: 0.98,
250
+ translateY: -20,
251
+ transition: {
252
+ duration: 0.1,
253
+ },
254
+ }}
255
+ className={css.pickview}
256
+ data-likec4-search-views
257
+ ref={focusTrapRef}>
258
+ <Group px="sm" py="md" justify="space-between">
259
+ <Title order={2} lh={1}>Select view</Title>
260
+ <ActionIcon
261
+ size={'md'}
262
+ variant="default"
263
+ onClick={(e) => {
264
+ e.stopPropagation()
265
+ setPickView(null)
266
+ }}>
267
+ <IconX />
268
+ </ActionIcon>
269
+ </Group>
270
+
271
+ <ScrollAreaAutosize mah={'calc(100vh - 8rem - 50px)'} type="never">
272
+ {pickview.scoped.length > 0 && (
273
+ <Stack gap={'sm'} px={'sm'} className={css.pickviewGroup}>
274
+ <Title order={6} c={'dimmed'}>scoped views of the element</Title>
275
+ {pickview.scoped.map((view, i) => (
276
+ <ViewButton
277
+ key={view.id}
278
+ view={view}
279
+ search={''}
280
+ loop
281
+ mod={{
282
+ autofocus: i === 0,
283
+ }}
284
+ />
285
+ ))}
286
+ </Stack>
287
+ )}
288
+
289
+ {pickview.others.length > 0 && (
290
+ <Stack gap={'sm'} px={'sm'} className={css.pickviewGroup}>
291
+ <Title order={6} c={'dimmed'}>views including this element</Title>
292
+ {pickview.others.map((view, i) => (
293
+ <ViewButton
294
+ key={view.id}
295
+ view={view}
296
+ search={''}
297
+ loop
298
+ mod={{
299
+ autofocus: i === 0 && pickview.scoped.length === 0,
300
+ }}
301
+ />
302
+ ))}
303
+ </Stack>
304
+ )}
305
+ </ScrollAreaAutosize>
306
+ </m.div>
307
+ </>
308
+ )}
309
+ </LayoutGroup>
310
+ </AnimatePresence>
311
+ )
312
+ }