@likec4/diagram 1.21.0 → 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 (129) hide show
  1. package/dist/LikeC4Diagram.js +9 -13
  2. package/dist/base/BaseXYFlow.js +0 -1
  3. package/dist/base/index.d.ts +0 -1
  4. package/dist/base/primitives/compound/CompoundNodeContainer.js +2 -2
  5. package/dist/base/primitives/edge/EdgeLabel.js +2 -2
  6. package/dist/base/primitives/element/ElementNodeContainer.d.ts +6 -1
  7. package/dist/base/primitives/element/ElementNodeContainer.js +31 -13
  8. package/dist/base/primitives/element/ElementTitle.js +7 -3
  9. package/dist/base/primitives/element/styles.css.js +1 -1
  10. package/dist/{base → components}/Link.css.js +3 -3
  11. package/dist/components/SearchControl.css.d.ts +2 -0
  12. package/dist/components/SearchControl.css.js +7 -0
  13. package/dist/components/SearchControl.d.ts +5 -0
  14. package/dist/components/SearchControl.js +17 -0
  15. package/dist/context/IconRenderer.d.ts +10 -0
  16. package/dist/context/IconRenderer.js +34 -0
  17. package/dist/hooks/index.d.ts +0 -1
  18. package/dist/hooks/useDiagram.d.ts +4 -4
  19. package/dist/index.d.ts +1 -1
  20. package/dist/likec4diagram/DiagramUI.js +1 -4
  21. package/dist/likec4diagram/XYFlow.d.ts +6 -1
  22. package/dist/likec4diagram/XYFlow.js +28 -29
  23. package/dist/likec4diagram/custom/edges/RelationshipsDropdownMenu.js +3 -1
  24. package/dist/likec4diagram/state/actorContext.d.ts +16 -16
  25. package/dist/likec4diagram/state/assign.js +1 -1
  26. package/dist/likec4diagram/state/machine.d.ts +7 -7
  27. package/dist/likec4diagram/state/machine.js +10 -7
  28. package/dist/likec4diagram/ui/controls/Controls.js +63 -48
  29. package/dist/likec4diagram/ui/controls/NavigationButtons.js +44 -69
  30. package/dist/likec4diagram/ui/controls/_shared.js +1 -1
  31. package/dist/likec4diagram/ui/diagram-title/DiagramTitlePanel.js +5 -4
  32. package/dist/likec4diagram/ui/index.d.ts +2 -0
  33. package/dist/likec4diagram/ui/search/ElementsColumn.css.d.ts +15 -0
  34. package/dist/likec4diagram/ui/search/ElementsColumn.css.js +30 -0
  35. package/dist/likec4diagram/ui/search/ElementsColumn.d.ts +1 -0
  36. package/dist/likec4diagram/ui/search/ElementsColumn.js +243 -0
  37. package/dist/likec4diagram/ui/search/LikeC4Search.css.d.ts +8 -0
  38. package/dist/likec4diagram/ui/search/LikeC4Search.css.js +20 -0
  39. package/dist/likec4diagram/ui/search/LikeC4Search.d.ts +1 -0
  40. package/dist/likec4diagram/ui/search/LikeC4Search.js +265 -0
  41. package/dist/likec4diagram/ui/search/SearchInput.d.ts +1 -0
  42. package/dist/likec4diagram/ui/search/SearchInput.js +196 -0
  43. package/dist/likec4diagram/ui/search/ViewsColum.d.ts +9 -0
  44. package/dist/likec4diagram/ui/search/ViewsColum.js +130 -0
  45. package/dist/likec4diagram/ui/search/ViewsColumn.css.d.ts +5 -0
  46. package/dist/likec4diagram/ui/search/ViewsColumn.css.js +12 -0
  47. package/dist/likec4diagram/ui/search/_shared.css.d.ts +16 -0
  48. package/dist/likec4diagram/ui/search/_shared.css.js +5 -0
  49. package/dist/likec4diagram/ui/search/state.d.ts +19 -0
  50. package/dist/likec4diagram/ui/search/state.js +72 -0
  51. package/dist/likec4diagram/ui/search/utils.d.ts +3 -0
  52. package/dist/likec4diagram/ui/search/utils.js +23 -0
  53. package/dist/likec4diagram/useLayoutConstraints.js +4 -5
  54. package/dist/likec4model/useLikeC4ElementsTree.d.ts +1 -0
  55. package/dist/likec4model/useLikeC4ElementsTree.js +1 -0
  56. package/dist/overlays/Overlays.js +10 -10
  57. package/dist/overlays/element-details/ElementDetailsCard.js +20 -13
  58. package/dist/overlays/element-details/TabPanelDeployments.js +4 -2
  59. package/dist/overlays/overlay/Overlay.d.ts +6 -2
  60. package/dist/overlays/overlay/Overlay.js +18 -29
  61. package/dist/overlays/relationship-details/SelectEdge.js +1 -3
  62. package/dist/overlays/relationships-browser/RelationshipsBrowser.js +6 -5
  63. package/dist/overlays/relationships-browser/SelectElement.js +10 -6
  64. package/dist/overlays/relationships-browser/actor.d.ts +2 -2
  65. package/dist/overlays/relationships-browser/actor.js +8 -8
  66. package/dist/overlays/relationships-browser/custom/nodeTypes.js +2 -2
  67. package/dist/style.css +1 -1
  68. package/package.json +15 -14
  69. package/src/LikeC4Diagram.tsx +0 -1
  70. package/src/base/BaseXYFlow.tsx +0 -1
  71. package/src/base/index.ts +0 -2
  72. package/src/base/primitives/element/ElementNodeContainer.css.ts +4 -7
  73. package/src/base/primitives/element/ElementNodeContainer.tsx +36 -16
  74. package/src/base/primitives/element/ElementTitle.css.ts +12 -9
  75. package/src/base/primitives/element/ElementTitle.tsx +6 -2
  76. package/src/base/primitives/element/styles.css.ts +18 -40
  77. package/src/{base → components}/Link.css.ts +3 -3
  78. package/src/components/SearchControl.css.ts +69 -0
  79. package/src/components/SearchControl.tsx +33 -0
  80. package/src/context/IconRenderer.tsx +54 -1
  81. package/src/hooks/index.ts +0 -1
  82. package/src/index.ts +15 -1
  83. package/src/likec4diagram/DiagramUI.tsx +1 -8
  84. package/src/likec4diagram/XYFlow.tsx +30 -30
  85. package/src/likec4diagram/custom/edges/RelationshipEdge.tsx +0 -1
  86. package/src/likec4diagram/custom/edges/RelationshipsDropdownMenu.css.ts +1 -0
  87. package/src/likec4diagram/custom/edges/RelationshipsDropdownMenu.tsx +3 -1
  88. package/src/likec4diagram/state/assign.ts +1 -1
  89. package/src/likec4diagram/state/machine.ts +12 -10
  90. package/src/likec4diagram/ui/controls/Controls.tsx +35 -25
  91. package/src/likec4diagram/ui/controls/NavigationButtons.tsx +36 -58
  92. package/src/likec4diagram/ui/controls/styles.css.ts +3 -1
  93. package/src/likec4diagram/ui/diagram-title/DiagramTitlePanel.tsx +111 -108
  94. package/src/likec4diagram/ui/index.ts +2 -0
  95. package/src/likec4diagram/ui/notation/NotationPanel.tsx +2 -3
  96. package/src/likec4diagram/ui/search/ElementsColumn.css.ts +119 -0
  97. package/src/likec4diagram/ui/search/ElementsColumn.tsx +291 -0
  98. package/src/likec4diagram/ui/search/LikeC4Search.css.ts +117 -0
  99. package/src/likec4diagram/ui/search/LikeC4Search.tsx +312 -0
  100. package/src/likec4diagram/ui/search/SearchInput.tsx +235 -0
  101. package/src/likec4diagram/ui/search/ViewsColum.tsx +158 -0
  102. package/src/likec4diagram/ui/search/ViewsColumn.css.ts +37 -0
  103. package/src/likec4diagram/ui/search/_shared.css.ts +104 -0
  104. package/src/likec4diagram/ui/search/state.ts +76 -0
  105. package/src/likec4diagram/ui/search/utils.ts +20 -0
  106. package/src/likec4diagram/useLayoutConstraints.ts +4 -5
  107. package/src/likec4model/useLikeC4ElementsTree.ts +4 -4
  108. package/src/overlays/Overlays.tsx +32 -26
  109. package/src/overlays/element-details/ElementDetailsCard.css.ts +4 -4
  110. package/src/overlays/element-details/ElementDetailsCard.tsx +246 -244
  111. package/src/overlays/element-details/TabPanelDeployments.tsx +4 -4
  112. package/src/overlays/overlay/Overlay.css.ts +10 -9
  113. package/src/overlays/overlay/Overlay.tsx +38 -26
  114. package/src/overlays/relationship-details/SelectEdge.css.ts +1 -0
  115. package/src/overlays/relationship-details/SelectEdge.tsx +2 -4
  116. package/src/overlays/relationships-browser/RelationshipsBrowser.tsx +10 -5
  117. package/src/overlays/relationships-browser/SelectElement.tsx +88 -82
  118. package/src/overlays/relationships-browser/actor.ts +10 -10
  119. package/src/overlays/relationships-browser/custom/nodeTypes.tsx +2 -2
  120. package/dist/LikeC4Search.css.d.ts +0 -1
  121. package/dist/LikeC4Search.css.js +0 -5
  122. package/dist/LikeC4Search.d.ts +0 -2
  123. package/dist/LikeC4Search.js +0 -88
  124. package/src/LikeC4Search.css.ts +0 -12
  125. package/src/LikeC4Search.tsx +0 -91
  126. /package/dist/{base → components}/Link.css.d.ts +0 -0
  127. /package/dist/{base → components}/Link.d.ts +0 -0
  128. /package/dist/{base → components}/Link.js +0 -0
  129. /package/src/{base → components}/Link.tsx +0 -0
@@ -0,0 +1,235 @@
1
+ import { compareNatural } from '@likec4/core'
2
+ import {
3
+ Combobox,
4
+ ComboboxDropdown,
5
+ ComboboxEmpty,
6
+ ComboboxOption,
7
+ ComboboxOptions,
8
+ ComboboxTarget,
9
+ Input,
10
+ rem,
11
+ ScrollAreaAutosize,
12
+ Text,
13
+ useCombobox,
14
+ } from '@mantine/core'
15
+ import { useDebouncedValue, useFocusWithin, useIsFirstRender, useWindowEvent } from '@mantine/hooks'
16
+ import { usePreviousDistinct } from '@react-hookz/web'
17
+ import { IconSearch } from '@tabler/icons-react'
18
+ import { type ReactNode } from 'react'
19
+ import { isString, keys, only } from 'remeda'
20
+ import { useLikeC4Model } from '../../../likec4model/useLikeC4Model'
21
+ import * as css from './LikeC4Search.css'
22
+ import { setSearch, useIsPickViewActive, useSearch } from './state'
23
+ import { moveFocusToSearchInput, stopAndPrevent } from './utils'
24
+
25
+ function startingWithKind(search: string) {
26
+ return search.match(/^(k|ki|kin|kind|kind:)$/) != null
27
+ }
28
+
29
+ const SEARCH_PREFIXES = ['#', 'kind:']
30
+
31
+ export function LikeC4SearchInput() {
32
+ const isPickViewActive = useIsPickViewActive()
33
+ const likec4model = useLikeC4Model(true)
34
+ const combobox = useCombobox({
35
+ scrollBehavior: 'smooth',
36
+ })
37
+ const { ref, focused } = useFocusWithin<HTMLInputElement>()
38
+ const search = useSearch()
39
+ const previous = usePreviousDistinct(search)
40
+ const isFirstRender = useIsFirstRender()
41
+
42
+ const [isEmptyForSomeTime] = useDebouncedValue(search === '' && !isFirstRender, isString(previous) ? 400 : 1000)
43
+
44
+ useWindowEvent(
45
+ 'keydown',
46
+ (event) => {
47
+ try {
48
+ if (
49
+ !focused && !isPickViewActive && (
50
+ event.key === 'Backspace' ||
51
+ event.key.startsWith('Arrow') ||
52
+ event.key.match(/^\p{L}$/u)
53
+ )
54
+ ) {
55
+ moveFocusToSearchInput()
56
+ }
57
+ } catch (e) {
58
+ console.warn(e)
59
+ }
60
+ },
61
+ )
62
+
63
+ let options = [] as ReactNode[]
64
+ let isExactMatch = false
65
+
66
+ switch (true) {
67
+ case search === '' && (isEmptyForSomeTime || SEARCH_PREFIXES.includes(previous ?? '')): {
68
+ options = SEARCH_PREFIXES.map((prefix) => (
69
+ <ComboboxOption value={prefix} key={prefix}>
70
+ <Text component="span" opacity={.5} mr={4} fz={'sm'}>Search by</Text>
71
+ {prefix}
72
+ </ComboboxOption>
73
+ ))
74
+ break
75
+ }
76
+ case search.startsWith('#'): {
77
+ const searchTag = search.toLocaleLowerCase().slice(1)
78
+ const alloptions = likec4model.allTags().filter((tag) => tag.toLocaleLowerCase().includes(searchTag)).sort(
79
+ compareNatural,
80
+ )
81
+ isExactMatch = only(alloptions)?.toLocaleLowerCase() === searchTag
82
+ if (alloptions.length === 0) {
83
+ options = [
84
+ <ComboboxEmpty key="empty-tags">
85
+ No tags found
86
+ </ComboboxEmpty>,
87
+ ]
88
+ } else {
89
+ options = alloptions.map((tag) => (
90
+ <ComboboxOption value={`#${tag}`} key={tag}>
91
+ <Text component="span" opacity={.5} mr={1} fz={'sm'}>#</Text>
92
+ {tag}
93
+ </ComboboxOption>
94
+ ))
95
+ }
96
+ break
97
+ }
98
+ case search.startsWith('kind:'):
99
+ case startingWithKind(search): {
100
+ const term = search.length > 5 ? search.slice(5).toLocaleLowerCase() : ''
101
+ let alloptions = keys(likec4model.$model.specification.elements)
102
+ if (term) {
103
+ alloptions = alloptions.filter((kind) => kind.toLocaleLowerCase().includes(term))
104
+ isExactMatch = only(alloptions)?.toLocaleLowerCase() === term
105
+ }
106
+ if (alloptions.length === 0) {
107
+ options = [
108
+ <ComboboxEmpty key="empty-kinds">
109
+ No kinds found
110
+ </ComboboxEmpty>,
111
+ ]
112
+ } else {
113
+ options = alloptions.map((kind) => (
114
+ <ComboboxOption value={`kind:${kind}`} key={kind}>
115
+ <Text component="span" opacity={.5} mr={1} fz={'sm'}>kind:</Text>
116
+ {kind}
117
+ </ComboboxOption>
118
+ ))
119
+ }
120
+ break
121
+ }
122
+ }
123
+
124
+ return (
125
+ <Combobox
126
+ onOptionSubmit={(optionValue) => {
127
+ setSearch(optionValue)
128
+ combobox.resetSelectedOption()
129
+ if (!SEARCH_PREFIXES.includes(optionValue)) {
130
+ combobox.closeDropdown()
131
+ // Let react to display filtered elements
132
+ setTimeout(() => {
133
+ document.querySelector<HTMLButtonElement>(`.${css.root} .${css.focusable}`)?.focus()
134
+ }, 50)
135
+ }
136
+ }}
137
+ width={'max-content'}
138
+ position="bottom-start"
139
+ shadow="md"
140
+ offset={{
141
+ mainAxis: 4,
142
+ crossAxis: 50,
143
+ }}
144
+ store={combobox}
145
+ withinPortal={false}
146
+ >
147
+ <ComboboxTarget>
148
+ <Input
149
+ ref={ref}
150
+ id="likec4searchinput"
151
+ placeholder="Search by title, description or start with # or kind:"
152
+ tabIndex={0}
153
+ classNames={{
154
+ input: css.input,
155
+ }}
156
+ size="xl"
157
+ value={search}
158
+ leftSection={<IconSearch style={{ width: rem(20) }} stroke={2} />}
159
+ onChange={(event) => {
160
+ setSearch(event.currentTarget.value)
161
+ combobox.openDropdown()
162
+ combobox.updateSelectedOptionIndex()
163
+ }}
164
+ onClick={() => combobox.openDropdown()}
165
+ onFocus={() => combobox.openDropdown()}
166
+ onBlur={() => combobox.closeDropdown()}
167
+ onKeyDownCapture={(e) => {
168
+ if (e.key === 'Tab') {
169
+ switch (true) {
170
+ case combobox.getSelectedOptionIndex() >= 0: {
171
+ combobox.clickSelectedOption()
172
+ return stopAndPrevent(e)
173
+ }
174
+ case options.length === 1: {
175
+ const firstOption = combobox.selectFirstOption()
176
+ if (firstOption) {
177
+ combobox.clickSelectedOption()
178
+ }
179
+ return stopAndPrevent(e)
180
+ }
181
+ case startingWithKind(search): {
182
+ setSearch('kind:')
183
+ return stopAndPrevent(e)
184
+ }
185
+ }
186
+ return
187
+ }
188
+ if (e.key === 'Backspace' && combobox.dropdownOpened) {
189
+ if (search === 'kind:') {
190
+ setSearch('')
191
+ combobox.resetSelectedOption()
192
+ return stopAndPrevent(e)
193
+ }
194
+ if (search.startsWith('kind:') && isExactMatch) {
195
+ setSearch('kind:')
196
+ combobox.resetSelectedOption()
197
+ return stopAndPrevent(e)
198
+ }
199
+ if (search.startsWith('#') && isExactMatch) {
200
+ setSearch('#')
201
+ combobox.resetSelectedOption()
202
+ return stopAndPrevent(e)
203
+ }
204
+ }
205
+ if (e.key === 'Escape' && combobox.dropdownOpened && options.length > 0) {
206
+ stopAndPrevent(e)
207
+ combobox.closeDropdown()
208
+ return
209
+ }
210
+ if (
211
+ e.key === 'ArrowDown' && (
212
+ !combobox.dropdownOpened ||
213
+ options.length === 0 || isExactMatch ||
214
+ // reached the last option and the search is empty
215
+ (search === '' && combobox.getSelectedOptionIndex() === options.length - 1)
216
+ )
217
+ ) {
218
+ combobox.closeDropdown()
219
+ stopAndPrevent(e)
220
+ document.querySelector<HTMLButtonElement>(`.${css.root} .${css.focusable}`)?.focus()
221
+ return
222
+ }
223
+ }} />
224
+ </ComboboxTarget>
225
+
226
+ <ComboboxDropdown hidden={options.length === 0} style={{ minWidth: 300 }}>
227
+ <ComboboxOptions>
228
+ <ScrollAreaAutosize mah={'min(322px, calc(100cqh - 50px))'} type="scroll">
229
+ {options}
230
+ </ScrollAreaAutosize>
231
+ </ComboboxOptions>
232
+ </ComboboxDropdown>
233
+ </Combobox>
234
+ )
235
+ }
@@ -0,0 +1,158 @@
1
+ import {
2
+ type LikeC4Model,
3
+ type Tag,
4
+ isDeploymentView,
5
+ } from '@likec4/core'
6
+ import {
7
+ type ElementProps,
8
+ type UnstyledButtonProps,
9
+ Badge,
10
+ Box,
11
+ createScopedKeydownHandler,
12
+ Group,
13
+ Highlight,
14
+ Stack,
15
+ ThemeIcon,
16
+ UnstyledButton,
17
+ VisuallyHidden,
18
+ } from '@mantine/core'
19
+ import { IconStack2, IconZoomScan } from '@tabler/icons-react'
20
+ import clsx from 'clsx'
21
+ import { m } from 'framer-motion'
22
+ import { first } from 'remeda'
23
+ import { useCurrentViewId } from '../../../hooks'
24
+ import { useLikeC4Model } from '../../../likec4model/useLikeC4Model'
25
+ import { emptyBoX } from './_shared.css'
26
+ import { useCloseSearchAndNavigateTo, useNormalizedSearch } from './state'
27
+ import { centerY, moveFocusToSearchInput } from './utils'
28
+ import * as css from './ViewsColumn.css'
29
+
30
+ export const NothingFound = () => (
31
+ <Box className={emptyBoX}>
32
+ Nothing found
33
+ </Box>
34
+ )
35
+
36
+ export function ViewsColumn() {
37
+ const search = useNormalizedSearch()
38
+ let views = [...useLikeC4Model(true).views()]
39
+ if (search) {
40
+ if (search.startsWith('kind:')) {
41
+ views = []
42
+ } else {
43
+ views = views.filter(view => {
44
+ if (search.startsWith('#')) {
45
+ return view.tags.some((tag: Tag) => tag.toLocaleLowerCase().includes(search.slice(1)))
46
+ }
47
+ return (view.title ?? '' + view.$view.description ?? '').toLocaleLowerCase().includes(search)
48
+ })
49
+ }
50
+ }
51
+
52
+ return (
53
+ <Stack
54
+ renderRoot={props => <m.div layout {...props} />}
55
+ gap={8}
56
+ data-likec4-search-views
57
+ onKeyDown={(e) => {
58
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
59
+ const maxY = (e.target as HTMLElement).getBoundingClientRect().y
60
+ const elementButtons = [...document.querySelectorAll<HTMLButtonElement>(
61
+ `[data-likec4-search-elements] .likec4-element-button`,
62
+ )]
63
+ let elementButton = elementButtons.length > 1
64
+ ? elementButtons.find((el, i, all) => centerY(el) > maxY || i === all.length - 1)
65
+ : null
66
+ elementButton ??= first(elementButtons)
67
+
68
+ if (elementButton) {
69
+ e.stopPropagation()
70
+ elementButton.focus()
71
+ }
72
+ return
73
+ }
74
+ }}>
75
+ {views.length === 0 && <NothingFound />}
76
+ {views.length > 0 && (
77
+ <VisuallyHidden>
78
+ <UnstyledButton
79
+ data-likec4-view
80
+ tabIndex={-1}
81
+ onFocus={e => {
82
+ e.stopPropagation()
83
+ moveFocusToSearchInput()
84
+ }} />
85
+ </VisuallyHidden>
86
+ )}
87
+ {views.map((view, i) => (
88
+ <m.div layoutId={`@view${view.id}`} key={view.id}>
89
+ <ViewButton
90
+ view={view}
91
+ search={search}
92
+ tabIndex={i === 0 ? 0 : -1}
93
+ />
94
+ </m.div>
95
+ ))}
96
+ </Stack>
97
+ )
98
+ }
99
+
100
+ export function ViewButton(
101
+ { className, view, loop = false, search, ...props }:
102
+ & {
103
+ view: LikeC4Model.View
104
+ search: string
105
+ loop?: boolean
106
+ }
107
+ & UnstyledButtonProps
108
+ & ElementProps<'button'>,
109
+ ) {
110
+ const navigateTo = useCloseSearchAndNavigateTo()
111
+ const currentViewId = useCurrentViewId()
112
+ const isCurrentView = view.id === currentViewId
113
+ return (
114
+ <UnstyledButton
115
+ {...props}
116
+ className={clsx(css.focusable, css.viewButton, className)}
117
+ data-likec4-view={view.id}
118
+ {...isCurrentView && { 'data-disabled': true }}
119
+ onClick={(e) => {
120
+ e.stopPropagation()
121
+ navigateTo(view.id)
122
+ }}
123
+ onKeyDown={createScopedKeydownHandler({
124
+ siblingSelector: '[data-likec4-view]',
125
+ parentSelector: '[data-likec4-search-views]',
126
+ activateOnFocus: false,
127
+ loop,
128
+ orientation: 'vertical',
129
+ onKeyDown: (e) => {
130
+ if (e.nativeEvent.code === 'Space') {
131
+ e.stopPropagation()
132
+ navigateTo(view.id)
133
+ }
134
+ },
135
+ })}>
136
+ <ThemeIcon variant="transparent">
137
+ {isDeploymentView(view)
138
+ ? <IconStack2 stroke={1.8} />
139
+ : <IconZoomScan stroke={1.8} />}
140
+ </ThemeIcon>
141
+ <Box style={{ flexGrow: 1 }}>
142
+ <Group gap={'xs'} wrap="nowrap" align="center">
143
+ <Highlight component="div" highlight={search} className={css.viewTitle}>
144
+ {view.title || 'untitled'}
145
+ </Highlight>
146
+ {isCurrentView && <Badge size="xs" fz={9} radius={'sm'}>current</Badge>}
147
+ </Group>
148
+ <Highlight
149
+ highlight={view.$view.description ? search : ''}
150
+ component="div"
151
+ className={css.viewDescription}
152
+ lineClamp={1}>
153
+ {view.$view.description || 'No description'}
154
+ </Highlight>
155
+ </Box>
156
+ </UnstyledButton>
157
+ )
158
+ }
@@ -0,0 +1,37 @@
1
+ import { fallbackVar, globalStyle, style } from '@vanilla-extract/css'
2
+ import { mantine } from '../../../theme-vars'
3
+ import { button, description, descriptionColor, iconColor, title } from './_shared.css'
4
+
5
+ export { focusable } from './_shared.css'
6
+
7
+ export const viewButton = style([button, {
8
+ flexWrap: 'nowrap',
9
+ display: 'flex',
10
+ }])
11
+
12
+ globalStyle(`${viewButton} .mantine-ThemeIcon-root`, {
13
+ // transition: transitions.fast,
14
+ color: fallbackVar(iconColor, mantine.colors.dark[2]),
15
+ vars: {
16
+ '--ti-size': '24px',
17
+ },
18
+ })
19
+ globalStyle(`${viewButton}[data-disabled] .mantine-ThemeIcon-root`, {
20
+ opacity: 0.45,
21
+ })
22
+
23
+ export const viewTitle = style([title])
24
+ export const viewDescription = style([description])
25
+
26
+ export const viewButtonDescription = style({
27
+ marginTop: 4,
28
+ // transition: transitions.fast,
29
+ color: fallbackVar(descriptionColor, mantine.colors.dimmed),
30
+ fontSize: 13,
31
+ lineHeight: 1.4,
32
+ selectors: {
33
+ [`:where(${viewButton}[data-disabled]) &`]: {
34
+ opacity: 0.85,
35
+ },
36
+ },
37
+ })
@@ -0,0 +1,104 @@
1
+ import { type StyleRule, createVar, fallbackVar, globalStyle, style } from '@vanilla-extract/css'
2
+ import { mantine } from '../../../theme-vars'
3
+ import { whereDark, whereLight } from '../../../theme-vars.css'
4
+
5
+ export const titleColor = createVar('title-color')
6
+ export const descriptionColor = createVar('description-color')
7
+ export const iconColor = createVar('icon-color')
8
+
9
+ export const buttonFocused = {
10
+ outline: 'none',
11
+ backgroundColor: mantine.colors.primaryColors[8],
12
+ borderColor: mantine.colors.primaryColors[9],
13
+ vars: {
14
+ [iconColor]: mantine.colors.primaryColors[2],
15
+ [titleColor]: mantine.colors.primaryColors[0],
16
+ [descriptionColor]: mantine.colors.primaryColors[1],
17
+ },
18
+ } satisfies StyleRule
19
+
20
+ export const button = style({
21
+ display: 'flex',
22
+ width: '100%',
23
+ background: mantine.colors.body,
24
+ borderRadius: mantine.radius.sm,
25
+ padding: `12px 8px 12px 14px`,
26
+ minHeight: 60,
27
+ gap: 10,
28
+ // alignItems: 'flex-start',
29
+ // transition: `all 50ms ${easings.inOut}`,
30
+ border: `1px solid ${mantine.colors.defaultBorder}`,
31
+ vars: {
32
+ [titleColor]: mantine.colors.dark[1],
33
+ [iconColor]: mantine.colors.dimmed,
34
+ [descriptionColor]: mantine.colors.dimmed,
35
+ },
36
+ ':hover': {
37
+ ...buttonFocused,
38
+ borderColor: mantine.colors.primaryColors[9],
39
+ backgroundColor: `color-mix(in srgb, ${buttonFocused.backgroundColor}, transparent 40%)`,
40
+ },
41
+ ':focus': buttonFocused,
42
+ selectors: {
43
+ [`${whereDark} &`]: {
44
+ borderColor: 'transparent',
45
+ backgroundColor: `color-mix(in srgb, ${mantine.colors.dark[6]}, transparent 20%)`,
46
+ // background: mantine.colors.dark[6],
47
+ },
48
+ [`${whereLight} &`]: {
49
+ backgroundColor: `color-mix(in srgb, ${mantine.colors.white}, transparent 10%)`,
50
+ vars: {
51
+ [iconColor]: mantine.colors.gray[6],
52
+ [titleColor]: mantine.colors.gray[7],
53
+ },
54
+ },
55
+ [`${whereLight} &:hover`]: {
56
+ borderColor: mantine.colors.primaryColors[6],
57
+ backgroundColor: mantine.colors.primaryColors[5],
58
+ vars: {
59
+ [iconColor]: mantine.colors.primaryColors[3],
60
+ [titleColor]: mantine.colors.primaryColors[0],
61
+ [descriptionColor]: mantine.colors.primaryColors[1],
62
+ },
63
+ },
64
+ },
65
+ })
66
+
67
+ export const focusable = style({})
68
+
69
+ export const title = style({
70
+ color: fallbackVar(titleColor, mantine.colors.gray[7]),
71
+ fontSize: 16,
72
+ fontWeight: 500,
73
+ lineHeight: 1.1,
74
+ selectors: {
75
+ [`:where([data-disabled]) &`]: {
76
+ opacity: 0.4,
77
+ },
78
+ },
79
+ })
80
+ export const description = style({
81
+ marginTop: 4,
82
+ color: fallbackVar(descriptionColor, mantine.colors.dimmed),
83
+ fontSize: 12,
84
+ lineHeight: 1.4,
85
+ selectors: {
86
+ [`:where([data-disabled]) &`]: {
87
+ opacity: 0.85,
88
+ },
89
+ },
90
+ })
91
+
92
+ export const emptyBoX = style({
93
+ width: '100%',
94
+ height: '100%',
95
+ border: `2px dashed ${mantine.colors.defaultBorder}`,
96
+ borderRadius: mantine.radius.md,
97
+ display: 'flex',
98
+ justifyContent: 'center',
99
+ alignItems: 'center',
100
+ fontSize: mantine.fontSizes.md,
101
+ color: mantine.colors.dimmed,
102
+ padding: mantine.spacing.md,
103
+ paddingBlock: mantine.spacing.xl,
104
+ })
@@ -0,0 +1,76 @@
1
+ import { type Fqn, type LikeC4Model, type ViewId, DiagramNode } from '@likec4/core'
2
+ import { createSafeContext } from '@mantine/core'
3
+ import { useCallbackRef } from '@mantine/hooks'
4
+ import { useStore } from '@nanostores/react'
5
+ import { atom, computed, onMount } from 'nanostores'
6
+ import { useDeferredValue } from 'react'
7
+ import { useDiagram } from '../../../hooks/useDiagram'
8
+
9
+ const $search = atom('')
10
+
11
+ export function setSearch(term: string) {
12
+ $search.set(term)
13
+ }
14
+
15
+ export function useSearch() {
16
+ return useStore($search)
17
+ }
18
+
19
+ const $normalizedSearch = computed($search, search => {
20
+ const v = search.trim().toLowerCase()
21
+ return v.length > 1 ? v : ''
22
+ })
23
+ export function useNormalizedSearch() {
24
+ return useDeferredValue(useStore($normalizedSearch))
25
+ }
26
+
27
+ export const [LikeC4SearchContext, useCloseSearch] = createSafeContext<(cb?: () => void) => void>('LikeC4Search')
28
+
29
+ type PickView = {
30
+ elementFqn: Fqn
31
+ scoped: LikeC4Model.View[]
32
+ others: LikeC4Model.View[]
33
+ }
34
+ const $pickView = atom<PickView | null>(null)
35
+ onMount($search, () => {
36
+ // reset search on mount
37
+ $pickView.set(null)
38
+ })
39
+
40
+ export function usePickView() {
41
+ return useStore($pickView)
42
+ }
43
+ export function setPickView(data: PickView | null) {
44
+ $pickView.set(data)
45
+ }
46
+ export function wasResetPickView() {
47
+ if ($pickView.get() !== null) {
48
+ $pickView.set(null)
49
+ return true
50
+ }
51
+ return false
52
+ }
53
+
54
+ const $pickViewActive = computed($pickView, pickView => pickView !== null)
55
+ export function useIsPickViewActive() {
56
+ return useStore($pickViewActive)
57
+ }
58
+
59
+ export function useCloseSearchAndNavigateTo() {
60
+ const diagram = useDiagram()
61
+ const close = useCloseSearch()
62
+ return useCallbackRef((viewId: ViewId, fromElementFqn?: Fqn) => {
63
+ close(() => {
64
+ fromElementFqn ??= $pickView.get()?.elementFqn
65
+ setPickView(null)
66
+ const fromNode = fromElementFqn
67
+ ? diagram.getContext().view.nodes.find(n => DiagramNode.modelRef(n) === fromElementFqn)?.id
68
+ : undefined
69
+ if (diagram.currentView().id === viewId && fromNode) {
70
+ diagram.focusNode(fromNode)
71
+ return
72
+ }
73
+ diagram.navigateTo(viewId, fromNode)
74
+ })
75
+ })
76
+ }
@@ -0,0 +1,20 @@
1
+ export function stopAndPrevent(e: React.KeyboardEvent | KeyboardEvent) {
2
+ e.stopPropagation()
3
+ e.preventDefault()
4
+ return
5
+ }
6
+
7
+ export function centerY(element: HTMLElement) {
8
+ const rect = element.getBoundingClientRect()
9
+ const y = rect.y + Math.floor(rect.height / 2)
10
+ return y
11
+ }
12
+
13
+ export function moveFocusToSearchInput() {
14
+ const input = document.getElementById('likec4searchinput') as HTMLInputElement | null
15
+ if (input) {
16
+ const length = input.value.length
17
+ input.focus()
18
+ input.setSelectionRange(length, length)
19
+ }
20
+ }
@@ -5,7 +5,6 @@ import { useMemo, useRef } from 'react'
5
5
  import { filter, hasAtLeast, isNullish, map, pipe } from 'remeda'
6
6
  import { type XYStoreApi, useXYStoreApi } from '../hooks'
7
7
  // import { type XYStoreApi } from '../hooks/useXYFlow'
8
- import { useSyncedRef } from '@react-hookz/web'
9
8
  import { useDiagram } from '../hooks/useDiagram'
10
9
  import type { Types } from './types'
11
10
 
@@ -255,11 +254,11 @@ type LayoutConstraints = {
255
254
  */
256
255
  export function useLayoutConstraints(): LayoutConstraints {
257
256
  const xystore = useXYStoreApi()
258
- const diagram = useSyncedRef(useDiagram())
257
+ const diagram = useDiagram()
259
258
  const solverRef = useRef<ReturnType<typeof createLayoutConstraints>>(undefined)
260
259
  return useMemo((): LayoutConstraints => ({
261
260
  onNodeDragStart: (_event, xynode) => {
262
- diagram.current.cancelSaveManualLayout()
261
+ diagram.cancelSaveManualLayout()
263
262
  const { nodeLookup } = xystore.getState()
264
263
  const draggingNodes = pipe(
265
264
  Array.from(nodeLookup.values()),
@@ -280,9 +279,9 @@ export function useLayoutConstraints(): LayoutConstraints {
280
279
  },
281
280
  onNodeDragStop: () => {
282
281
  solverRef.current?.updateXYFlowNodes()
283
- diagram.current.scheduleSaveManualLayout()
282
+ diagram.scheduleSaveManualLayout()
284
283
  // diagramApi.getState().scheduleSaveManualLayout()
285
284
  solverRef.current = undefined
286
285
  },
287
- }), [xystore])
286
+ }), [xystore, diagram])
288
287
  }
@@ -1,4 +1,4 @@
1
- import { compareNatural, type Fqn, type LikeC4Model, type ViewId } from '@likec4/core'
1
+ import { type Fqn, type LikeC4Model, type ViewId, compareNatural } from '@likec4/core'
2
2
  import { useMemo } from 'react'
3
3
  import { useLikeC4Model } from './useLikeC4Model'
4
4
 
@@ -8,15 +8,15 @@ interface LikeC4ModelTreeNodeData {
8
8
  children: LikeC4ModelTreeNodeData[]
9
9
  }
10
10
 
11
- const sortByLabel = (a: LikeC4ModelTreeNodeData, b: LikeC4ModelTreeNodeData) => compareNatural(a.label, b.label)
11
+ export const sortByLabel = (a: LikeC4ModelTreeNodeData, b: LikeC4ModelTreeNodeData) => compareNatural(a.label, b.label)
12
12
 
13
13
  function buildNode(
14
- element: LikeC4Model.Node | LikeC4Model.Element
14
+ element: LikeC4Model.Node | LikeC4Model.Element,
15
15
  ): LikeC4ModelTreeNodeData {
16
16
  return {
17
17
  label: element.title,
18
18
  value: element.id,
19
- children: [...element.children()].map(buildNode).sort(sortByLabel)
19
+ children: [...element.children()].map(buildNode).sort(sortByLabel),
20
20
  }
21
21
  }
22
22