themekit-js 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +27 -0
  3. package/bin/themekit.js +2 -0
  4. package/client.d.ts +5 -0
  5. package/dist/client/app/components/ClientOnly.js +10 -0
  6. package/dist/client/app/components/Content.js +23 -0
  7. package/dist/client/app/composables/codeGroups.js +40 -0
  8. package/dist/client/app/composables/copyCode.js +73 -0
  9. package/dist/client/app/composables/head.js +81 -0
  10. package/dist/client/app/composables/preFetch.js +99 -0
  11. package/dist/client/app/data.js +59 -0
  12. package/dist/client/app/devtools.js +29 -0
  13. package/dist/client/app/index.js +140 -0
  14. package/dist/client/app/router.js +233 -0
  15. package/dist/client/app/ssr.js +10 -0
  16. package/dist/client/app/theme.js +1 -0
  17. package/dist/client/app/utils.js +119 -0
  18. package/dist/client/index.d.ts +1446 -0
  19. package/dist/client/index.js +9 -0
  20. package/dist/client/shared.js +139 -0
  21. package/dist/client/theme-default/Layout.vue +94 -0
  22. package/dist/client/theme-default/NotFound.vue +109 -0
  23. package/dist/client/theme-default/components/VPAlgoliaSearchBox.vue +99 -0
  24. package/dist/client/theme-default/components/VPBackdrop.vue +41 -0
  25. package/dist/client/theme-default/components/VPBadge.vue +86 -0
  26. package/dist/client/theme-default/components/VPButton.vue +123 -0
  27. package/dist/client/theme-default/components/VPCarbonAds.vue +109 -0
  28. package/dist/client/theme-default/components/VPContent.vue +98 -0
  29. package/dist/client/theme-default/components/VPDoc.vue +193 -0
  30. package/dist/client/theme-default/components/VPDocAside.vue +46 -0
  31. package/dist/client/theme-default/components/VPDocAsideCarbonAds.vue +18 -0
  32. package/dist/client/theme-default/components/VPDocAsideOutline.vue +87 -0
  33. package/dist/client/theme-default/components/VPDocAsideSponsors.vue +17 -0
  34. package/dist/client/theme-default/components/VPDocFooter.vue +145 -0
  35. package/dist/client/theme-default/components/VPDocFooterLastUpdated.vue +50 -0
  36. package/dist/client/theme-default/components/VPDocOutlineItem.vue +59 -0
  37. package/dist/client/theme-default/components/VPFeature.vue +123 -0
  38. package/dist/client/theme-default/components/VPFeatures.vue +121 -0
  39. package/dist/client/theme-default/components/VPFlyout.vue +136 -0
  40. package/dist/client/theme-default/components/VPFooter.vue +60 -0
  41. package/dist/client/theme-default/components/VPHero.vue +336 -0
  42. package/dist/client/theme-default/components/VPHome.vue +43 -0
  43. package/dist/client/theme-default/components/VPHomeContent.vue +52 -0
  44. package/dist/client/theme-default/components/VPHomeFeatures.vue +14 -0
  45. package/dist/client/theme-default/components/VPHomeHero.vue +24 -0
  46. package/dist/client/theme-default/components/VPHomeSponsors.vue +116 -0
  47. package/dist/client/theme-default/components/VPImage.vue +46 -0
  48. package/dist/client/theme-default/components/VPLink.vue +33 -0
  49. package/dist/client/theme-default/components/VPLocalNav.vue +171 -0
  50. package/dist/client/theme-default/components/VPLocalNavOutlineDropdown.vue +190 -0
  51. package/dist/client/theme-default/components/VPLocalSearchBox.vue +856 -0
  52. package/dist/client/theme-default/components/VPMenu.vue +72 -0
  53. package/dist/client/theme-default/components/VPMenuGroup.vue +47 -0
  54. package/dist/client/theme-default/components/VPMenuLink.vue +54 -0
  55. package/dist/client/theme-default/components/VPNav.vue +57 -0
  56. package/dist/client/theme-default/components/VPNavBar.vue +267 -0
  57. package/dist/client/theme-default/components/VPNavBarAppearance.vue +25 -0
  58. package/dist/client/theme-default/components/VPNavBarExtra.vue +94 -0
  59. package/dist/client/theme-default/components/VPNavBarHamburger.vue +79 -0
  60. package/dist/client/theme-default/components/VPNavBarMenu.vue +29 -0
  61. package/dist/client/theme-default/components/VPNavBarMenuGroup.vue +42 -0
  62. package/dist/client/theme-default/components/VPNavBarMenuLink.vue +53 -0
  63. package/dist/client/theme-default/components/VPNavBarSearch.vue +194 -0
  64. package/dist/client/theme-default/components/VPNavBarSearchButton.vue +208 -0
  65. package/dist/client/theme-default/components/VPNavBarSocialLinks.vue +27 -0
  66. package/dist/client/theme-default/components/VPNavBarTitle.vue +76 -0
  67. package/dist/client/theme-default/components/VPNavBarTranslations.vue +47 -0
  68. package/dist/client/theme-default/components/VPNavScreen.vue +99 -0
  69. package/dist/client/theme-default/components/VPNavScreenAppearance.vue +33 -0
  70. package/dist/client/theme-default/components/VPNavScreenMenu.vue +23 -0
  71. package/dist/client/theme-default/components/VPNavScreenMenuGroup.vue +111 -0
  72. package/dist/client/theme-default/components/VPNavScreenMenuGroupLink.vue +39 -0
  73. package/dist/client/theme-default/components/VPNavScreenMenuGroupSection.vue +34 -0
  74. package/dist/client/theme-default/components/VPNavScreenMenuLink.vue +39 -0
  75. package/dist/client/theme-default/components/VPNavScreenSocialLinks.vue +14 -0
  76. package/dist/client/theme-default/components/VPNavScreenTranslations.vue +73 -0
  77. package/dist/client/theme-default/components/VPPage.vue +7 -0
  78. package/dist/client/theme-default/components/VPSidebar.vue +137 -0
  79. package/dist/client/theme-default/components/VPSidebarItem.vue +250 -0
  80. package/dist/client/theme-default/components/VPSkipLink.vue +68 -0
  81. package/dist/client/theme-default/components/VPSocialLink.vue +50 -0
  82. package/dist/client/theme-default/components/VPSocialLinks.vue +27 -0
  83. package/dist/client/theme-default/components/VPSponsors.vue +48 -0
  84. package/dist/client/theme-default/components/VPSponsorsGrid.vue +48 -0
  85. package/dist/client/theme-default/components/VPSwitch.vue +63 -0
  86. package/dist/client/theme-default/components/VPSwitchAppearance.vue +52 -0
  87. package/dist/client/theme-default/components/VPTeamMembers.vue +66 -0
  88. package/dist/client/theme-default/components/VPTeamMembersItem.vue +225 -0
  89. package/dist/client/theme-default/components/VPTeamPage.vue +58 -0
  90. package/dist/client/theme-default/components/VPTeamPageSection.vue +77 -0
  91. package/dist/client/theme-default/components/VPTeamPageTitle.vue +63 -0
  92. package/dist/client/theme-default/components/icons/VPIconAlignJustify.vue +8 -0
  93. package/dist/client/theme-default/components/icons/VPIconAlignLeft.vue +8 -0
  94. package/dist/client/theme-default/components/icons/VPIconAlignRight.vue +8 -0
  95. package/dist/client/theme-default/components/icons/VPIconArrowLeft.vue +7 -0
  96. package/dist/client/theme-default/components/icons/VPIconArrowRight.vue +7 -0
  97. package/dist/client/theme-default/components/icons/VPIconChevronDown.vue +5 -0
  98. package/dist/client/theme-default/components/icons/VPIconChevronLeft.vue +5 -0
  99. package/dist/client/theme-default/components/icons/VPIconChevronRight.vue +5 -0
  100. package/dist/client/theme-default/components/icons/VPIconChevronUp.vue +5 -0
  101. package/dist/client/theme-default/components/icons/VPIconEdit.vue +6 -0
  102. package/dist/client/theme-default/components/icons/VPIconHeart.vue +5 -0
  103. package/dist/client/theme-default/components/icons/VPIconLanguages.vue +9 -0
  104. package/dist/client/theme-default/components/icons/VPIconMinus.vue +5 -0
  105. package/dist/client/theme-default/components/icons/VPIconMinusSquare.vue +6 -0
  106. package/dist/client/theme-default/components/icons/VPIconMoon.vue +5 -0
  107. package/dist/client/theme-default/components/icons/VPIconMoreHorizontal.vue +7 -0
  108. package/dist/client/theme-default/components/icons/VPIconPlus.vue +5 -0
  109. package/dist/client/theme-default/components/icons/VPIconPlusSquare.vue +6 -0
  110. package/dist/client/theme-default/components/icons/VPIconSun.vue +13 -0
  111. package/dist/client/theme-default/composables/aside.js +17 -0
  112. package/dist/client/theme-default/composables/data.js +2 -0
  113. package/dist/client/theme-default/composables/edit-link.js +16 -0
  114. package/dist/client/theme-default/composables/flyout.js +41 -0
  115. package/dist/client/theme-default/composables/langs.js +26 -0
  116. package/dist/client/theme-default/composables/local-nav.js +18 -0
  117. package/dist/client/theme-default/composables/nav.js +30 -0
  118. package/dist/client/theme-default/composables/outline.js +178 -0
  119. package/dist/client/theme-default/composables/prev-next.js +57 -0
  120. package/dist/client/theme-default/composables/sidebar.js +136 -0
  121. package/dist/client/theme-default/composables/sponsor-grid.js +94 -0
  122. package/dist/client/theme-default/fonts/inter-italic-cyrillic-ext.woff2 +0 -0
  123. package/dist/client/theme-default/fonts/inter-italic-cyrillic.woff2 +0 -0
  124. package/dist/client/theme-default/fonts/inter-italic-greek-ext.woff2 +0 -0
  125. package/dist/client/theme-default/fonts/inter-italic-greek.woff2 +0 -0
  126. package/dist/client/theme-default/fonts/inter-italic-latin-ext.woff2 +0 -0
  127. package/dist/client/theme-default/fonts/inter-italic-latin.woff2 +0 -0
  128. package/dist/client/theme-default/fonts/inter-italic-vietnamese.woff2 +0 -0
  129. package/dist/client/theme-default/fonts/inter-roman-cyrillic-ext.woff2 +0 -0
  130. package/dist/client/theme-default/fonts/inter-roman-cyrillic.woff2 +0 -0
  131. package/dist/client/theme-default/fonts/inter-roman-greek-ext.woff2 +0 -0
  132. package/dist/client/theme-default/fonts/inter-roman-greek.woff2 +0 -0
  133. package/dist/client/theme-default/fonts/inter-roman-latin-ext.woff2 +0 -0
  134. package/dist/client/theme-default/fonts/inter-roman-latin.woff2 +0 -0
  135. package/dist/client/theme-default/fonts/inter-roman-vietnamese.woff2 +0 -0
  136. package/dist/client/theme-default/index.js +3 -0
  137. package/dist/client/theme-default/styles/base.css +252 -0
  138. package/dist/client/theme-default/styles/components/custom-block.css +208 -0
  139. package/dist/client/theme-default/styles/components/vp-code-group.css +85 -0
  140. package/dist/client/theme-default/styles/components/vp-code.css +7 -0
  141. package/dist/client/theme-default/styles/components/vp-doc.css +570 -0
  142. package/dist/client/theme-default/styles/components/vp-sponsor.css +155 -0
  143. package/dist/client/theme-default/styles/fonts.css +157 -0
  144. package/dist/client/theme-default/styles/icons.css +123 -0
  145. package/dist/client/theme-default/styles/utils.css +9 -0
  146. package/dist/client/theme-default/styles/vars.css +563 -0
  147. package/dist/client/theme-default/support/lru.js +33 -0
  148. package/dist/client/theme-default/support/sidebar.js +89 -0
  149. package/dist/client/theme-default/support/translation.js +49 -0
  150. package/dist/client/theme-default/support/utils.js +33 -0
  151. package/dist/client/theme-default/without-fonts.js +32 -0
  152. package/dist/node/cli.js +444 -0
  153. package/dist/node/index.d.ts +4588 -0
  154. package/dist/node/index.js +198 -0
  155. package/dist/node/serve-BjvG349_.js +50301 -0
  156. package/lib/vue-demi.mjs +34 -0
  157. package/package.json +223 -0
  158. package/template/.vitepress/config.js +28 -0
  159. package/template/.vitepress/theme/Layout.vue +21 -0
  160. package/template/.vitepress/theme/index.js +29 -0
  161. package/template/.vitepress/theme/style.css +143 -0
  162. package/template/api-examples.md +49 -0
  163. package/template/index.md +28 -0
  164. package/template/markdown-examples.md +85 -0
  165. package/theme-without-fonts.d.ts +2 -0
  166. package/theme.d.ts +30 -0
  167. package/types/default-theme.d.ts +533 -0
  168. package/types/docsearch.d.ts +144 -0
  169. package/types/index.d.ts +3 -0
  170. package/types/local-search.d.ts +33 -0
  171. package/types/shared.d.ts +199 -0
@@ -0,0 +1,856 @@
1
+ <script lang="ts" setup>
2
+ import localSearchIndex from '@localSearchIndex'
3
+ import {
4
+ computedAsync,
5
+ debouncedWatch,
6
+ onKeyStroke,
7
+ useEventListener,
8
+ useLocalStorage,
9
+ useScrollLock,
10
+ useSessionStorage
11
+ } from '@vueuse/core'
12
+ import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
13
+ import Mark from 'mark.js/src/vanilla.js'
14
+ import MiniSearch, { type SearchResult } from 'minisearch'
15
+ import { dataSymbol, inBrowser, useRouter } from 'vitepress'
16
+ import {
17
+ computed,
18
+ createApp,
19
+ markRaw,
20
+ nextTick,
21
+ onBeforeUnmount,
22
+ onMounted,
23
+ ref,
24
+ shallowRef,
25
+ watch,
26
+ watchEffect,
27
+ type Ref
28
+ } from 'vue'
29
+ import type { ModalTranslations } from '../../../../types/local-search'
30
+ import { pathToFile } from '../../app/utils'
31
+ import { escapeRegExp } from '../../shared'
32
+ import { useData } from '../composables/data'
33
+ import { LRUCache } from '../support/lru'
34
+ import { createSearchTranslate } from '../support/translation'
35
+
36
+ const emit = defineEmits<{
37
+ (e: 'close'): void
38
+ }>()
39
+
40
+ const el = shallowRef<HTMLElement>()
41
+ const resultsEl = shallowRef<HTMLElement>()
42
+
43
+ /* Search */
44
+
45
+ const searchIndexData = shallowRef(localSearchIndex)
46
+
47
+ // hmr
48
+ if (import.meta.hot) {
49
+ import.meta.hot.accept('/@localSearchIndex', (m) => {
50
+ if (m) {
51
+ searchIndexData.value = m.default
52
+ }
53
+ })
54
+ }
55
+
56
+ interface Result {
57
+ title: string
58
+ titles: string[]
59
+ text?: string
60
+ }
61
+
62
+ const vitePressData = useData()
63
+ const { activate } = useFocusTrap(el, {
64
+ immediate: true,
65
+ allowOutsideClick: true,
66
+ clickOutsideDeactivates: true,
67
+ escapeDeactivates: true
68
+ })
69
+ const { localeIndex, theme } = vitePressData
70
+ const searchIndex = computedAsync(async () =>
71
+ markRaw(
72
+ MiniSearch.loadJSON<Result>(
73
+ (await searchIndexData.value[localeIndex.value]?.())?.default,
74
+ {
75
+ fields: ['title', 'titles', 'text'],
76
+ storeFields: ['title', 'titles'],
77
+ searchOptions: {
78
+ fuzzy: 0.2,
79
+ prefix: true,
80
+ boost: { title: 4, text: 2, titles: 1 },
81
+ ...(theme.value.search?.provider === 'local' &&
82
+ theme.value.search.options?.miniSearch?.searchOptions)
83
+ },
84
+ ...(theme.value.search?.provider === 'local' &&
85
+ theme.value.search.options?.miniSearch?.options)
86
+ }
87
+ )
88
+ )
89
+ )
90
+
91
+ const disableQueryPersistence = computed(() => {
92
+ return (
93
+ theme.value.search?.provider === 'local' &&
94
+ theme.value.search.options?.disableQueryPersistence === true
95
+ )
96
+ })
97
+
98
+ const filterText = disableQueryPersistence.value
99
+ ? ref('')
100
+ : useSessionStorage('vitepress:local-search-filter', '')
101
+
102
+ const showDetailedList = useLocalStorage(
103
+ 'vitepress:local-search-detailed-list',
104
+ theme.value.search?.provider === 'local' &&
105
+ theme.value.search.options?.detailedView === true
106
+ )
107
+
108
+ const disableDetailedView = computed(() => {
109
+ return (
110
+ theme.value.search?.provider === 'local' &&
111
+ (theme.value.search.options?.disableDetailedView === true ||
112
+ theme.value.search.options?.detailedView === false)
113
+ )
114
+ })
115
+
116
+ const buttonText = computed(() => {
117
+ const options = theme.value.search?.options ?? theme.value.algolia
118
+
119
+ return (
120
+ options?.locales?.[localeIndex.value]?.translations?.button?.buttonText ||
121
+ options?.translations?.button?.buttonText ||
122
+ 'Search'
123
+ )
124
+ })
125
+
126
+ watchEffect(() => {
127
+ if (disableDetailedView.value) {
128
+ showDetailedList.value = false
129
+ }
130
+ })
131
+
132
+ const results: Ref<(SearchResult & Result)[]> = shallowRef([])
133
+
134
+ const enableNoResults = ref(false)
135
+
136
+ watch(filterText, () => {
137
+ enableNoResults.value = false
138
+ })
139
+
140
+ const mark = computedAsync(async () => {
141
+ if (!resultsEl.value) return
142
+ return markRaw(new Mark(resultsEl.value))
143
+ }, null)
144
+
145
+ const cache = new LRUCache<string, Map<string, string>>(16) // 16 files
146
+
147
+ debouncedWatch(
148
+ () => [searchIndex.value, filterText.value, showDetailedList.value] as const,
149
+ async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => {
150
+ if (old?.[0] !== index) {
151
+ // in case of hmr
152
+ cache.clear()
153
+ }
154
+
155
+ let canceled = false
156
+ onCleanup(() => {
157
+ canceled = true
158
+ })
159
+
160
+ if (!index) return
161
+
162
+ // Search
163
+ results.value = index
164
+ .search(filterTextValue)
165
+ .slice(0, 16) as (SearchResult & Result)[]
166
+ enableNoResults.value = true
167
+
168
+ // Highlighting
169
+ const mods = showDetailedListValue
170
+ ? await Promise.all(results.value.map((r) => fetchExcerpt(r.id)))
171
+ : []
172
+ if (canceled) return
173
+ for (const { id, mod } of mods) {
174
+ const mapId = id.slice(0, id.indexOf('#'))
175
+ let map = cache.get(mapId)
176
+ if (map) continue
177
+ map = new Map()
178
+ cache.set(mapId, map)
179
+ const comp = mod.default ?? mod
180
+ if (comp?.render || comp?.setup) {
181
+ const app = createApp(comp)
182
+ // Silence warnings about missing components
183
+ app.config.warnHandler = () => {}
184
+ app.provide(dataSymbol, vitePressData)
185
+ Object.defineProperties(app.config.globalProperties, {
186
+ $frontmatter: {
187
+ get() {
188
+ return vitePressData.frontmatter.value
189
+ }
190
+ },
191
+ $params: {
192
+ get() {
193
+ return vitePressData.page.value.params
194
+ }
195
+ }
196
+ })
197
+ const div = document.createElement('div')
198
+ app.mount(div)
199
+ const headings = div.querySelectorAll('h1, h2, h3, h4, h5, h6')
200
+ headings.forEach((el) => {
201
+ const href = el.querySelector('a')?.getAttribute('href')
202
+ const anchor = href?.startsWith('#') && href.slice(1)
203
+ if (!anchor) return
204
+ let html = ''
205
+ while ((el = el.nextElementSibling!) && !/^h[1-6]$/i.test(el.tagName))
206
+ html += el.outerHTML
207
+ map!.set(anchor, html)
208
+ })
209
+ app.unmount()
210
+ }
211
+ if (canceled) return
212
+ }
213
+
214
+ const terms = new Set<string>()
215
+
216
+ results.value = results.value.map((r) => {
217
+ const [id, anchor] = r.id.split('#')
218
+ const map = cache.get(id)
219
+ const text = map?.get(anchor) ?? ''
220
+ for (const term in r.match) {
221
+ terms.add(term)
222
+ }
223
+ return { ...r, text }
224
+ })
225
+
226
+ await nextTick()
227
+ if (canceled) return
228
+
229
+ await new Promise((r) => {
230
+ mark.value?.unmark({
231
+ done: () => {
232
+ mark.value?.markRegExp(formMarkRegex(terms), { done: r })
233
+ }
234
+ })
235
+ })
236
+
237
+ const excerpts = el.value?.querySelectorAll('.result .excerpt') ?? []
238
+ for (const excerpt of excerpts) {
239
+ excerpt
240
+ .querySelector('mark[data-markjs="true"]')
241
+ ?.scrollIntoView({ block: 'center' })
242
+ }
243
+ // FIXME: without this whole page scrolls to the bottom
244
+ resultsEl.value?.firstElementChild?.scrollIntoView({ block: 'start' })
245
+ },
246
+ { debounce: 200, immediate: true }
247
+ )
248
+
249
+ async function fetchExcerpt(id: string) {
250
+ const file = pathToFile(id.slice(0, id.indexOf('#')))
251
+ try {
252
+ if (!file) throw new Error(`Cannot find file for id: ${id}`)
253
+ return { id, mod: await import(/*@vite-ignore*/ file) }
254
+ } catch (e) {
255
+ console.error(e)
256
+ return { id, mod: {} }
257
+ }
258
+ }
259
+
260
+ /* Search input focus */
261
+
262
+ const searchInput = ref<HTMLInputElement>()
263
+ const disableReset = computed(() => {
264
+ return filterText.value?.length <= 0
265
+ })
266
+ function focusSearchInput(select = true) {
267
+ searchInput.value?.focus()
268
+ select && searchInput.value?.select()
269
+ }
270
+
271
+ onMounted(() => {
272
+ focusSearchInput()
273
+ })
274
+
275
+ function onSearchBarClick(event: PointerEvent) {
276
+ if (event.pointerType === 'mouse') {
277
+ focusSearchInput()
278
+ }
279
+ }
280
+
281
+ /* Search keyboard selection */
282
+
283
+ const selectedIndex = ref(-1)
284
+ const disableMouseOver = ref(false)
285
+
286
+ watch(results, (r) => {
287
+ selectedIndex.value = r.length ? 0 : -1
288
+ scrollToSelectedResult()
289
+ })
290
+
291
+ function scrollToSelectedResult() {
292
+ nextTick(() => {
293
+ const selectedEl = document.querySelector('.result.selected')
294
+ if (selectedEl) {
295
+ selectedEl.scrollIntoView({
296
+ block: 'nearest'
297
+ })
298
+ }
299
+ })
300
+ }
301
+
302
+ onKeyStroke('ArrowUp', (event) => {
303
+ event.preventDefault()
304
+ selectedIndex.value--
305
+ if (selectedIndex.value < 0) {
306
+ selectedIndex.value = results.value.length - 1
307
+ }
308
+ disableMouseOver.value = true
309
+ scrollToSelectedResult()
310
+ })
311
+
312
+ onKeyStroke('ArrowDown', (event) => {
313
+ event.preventDefault()
314
+ selectedIndex.value++
315
+ if (selectedIndex.value >= results.value.length) {
316
+ selectedIndex.value = 0
317
+ }
318
+ disableMouseOver.value = true
319
+ scrollToSelectedResult()
320
+ })
321
+
322
+ const router = useRouter()
323
+
324
+ onKeyStroke('Enter', (e) => {
325
+ if (e.isComposing) return
326
+
327
+ if (e.target instanceof HTMLButtonElement && e.target.type !== 'submit')
328
+ return
329
+
330
+ const selectedPackage = results.value[selectedIndex.value]
331
+ if (e.target instanceof HTMLInputElement && !selectedPackage) {
332
+ e.preventDefault()
333
+ return
334
+ }
335
+
336
+ if (selectedPackage) {
337
+ router.go(selectedPackage.id)
338
+ emit('close')
339
+ }
340
+ })
341
+
342
+ onKeyStroke('Escape', () => {
343
+ emit('close')
344
+ })
345
+
346
+ // Translations
347
+ const defaultTranslations: { modal: ModalTranslations } = {
348
+ modal: {
349
+ displayDetails: 'Display detailed list',
350
+ resetButtonTitle: 'Reset search',
351
+ backButtonTitle: 'Close search',
352
+ noResultsText: 'No results for',
353
+ footer: {
354
+ selectText: 'to select',
355
+ selectKeyAriaLabel: 'enter',
356
+ navigateText: 'to navigate',
357
+ navigateUpKeyAriaLabel: 'up arrow',
358
+ navigateDownKeyAriaLabel: 'down arrow',
359
+ closeText: 'to close',
360
+ closeKeyAriaLabel: 'escape'
361
+ }
362
+ }
363
+ }
364
+
365
+ const translate = createSearchTranslate(defaultTranslations)
366
+
367
+ // Back
368
+
369
+ onMounted(() => {
370
+ // Prevents going to previous site
371
+ window.history.pushState(null, '', null)
372
+ })
373
+
374
+ useEventListener('popstate', (event) => {
375
+ event.preventDefault()
376
+ emit('close')
377
+ })
378
+
379
+ /** Lock body */
380
+ const isLocked = useScrollLock(inBrowser ? document.body : null)
381
+
382
+ onMounted(() => {
383
+ nextTick(() => {
384
+ isLocked.value = true
385
+ nextTick().then(() => activate())
386
+ })
387
+ })
388
+
389
+ onBeforeUnmount(() => {
390
+ isLocked.value = false
391
+ })
392
+
393
+ function resetSearch() {
394
+ filterText.value = ''
395
+ nextTick().then(() => focusSearchInput(false))
396
+ }
397
+
398
+ function formMarkRegex(terms: Set<string>) {
399
+ return new RegExp(
400
+ [...terms]
401
+ .sort((a, b) => b.length - a.length)
402
+ .map((term) => `(${escapeRegExp(term)})`)
403
+ .join('|'),
404
+ 'gi'
405
+ )
406
+ }
407
+ </script>
408
+
409
+ <template>
410
+ <Teleport to="body">
411
+ <div
412
+ ref="el"
413
+ role="button"
414
+ :aria-owns="results?.length ? 'localsearch-list' : undefined"
415
+ aria-expanded="true"
416
+ aria-haspopup="listbox"
417
+ aria-labelledby="localsearch-label"
418
+ class="VPLocalSearchBox"
419
+ >
420
+ <div class="backdrop" @click="$emit('close')" />
421
+
422
+ <div class="shell">
423
+ <form
424
+ class="search-bar"
425
+ @pointerup="onSearchBarClick($event)"
426
+ @submit.prevent=""
427
+ >
428
+ <label
429
+ :title="buttonText"
430
+ id="localsearch-label"
431
+ for="localsearch-input"
432
+ >
433
+ <span aria-hidden="true" class="vpi-search search-icon local-search-icon" />
434
+ </label>
435
+ <div class="search-actions before">
436
+ <button
437
+ class="back-button"
438
+ :title="translate('modal.backButtonTitle')"
439
+ @click="$emit('close')"
440
+ >
441
+ <span class="vpi-arrow-left local-search-icon" />
442
+ </button>
443
+ </div>
444
+ <input
445
+ ref="searchInput"
446
+ v-model="filterText"
447
+ :placeholder="buttonText"
448
+ id="localsearch-input"
449
+ aria-labelledby="localsearch-label"
450
+ class="search-input"
451
+ />
452
+ <div class="search-actions">
453
+ <button
454
+ v-if="!disableDetailedView"
455
+ class="toggle-layout-button"
456
+ type="button"
457
+ :class="{ 'detailed-list': showDetailedList }"
458
+ :title="translate('modal.displayDetails')"
459
+ @click="
460
+ selectedIndex > -1 && (showDetailedList = !showDetailedList)
461
+ "
462
+ >
463
+ <span class="vpi-layout-list local-search-icon" />
464
+ </button>
465
+
466
+ <button
467
+ class="clear-button"
468
+ type="reset"
469
+ :disabled="disableReset"
470
+ :title="translate('modal.resetButtonTitle')"
471
+ @click="resetSearch"
472
+ >
473
+ <span class="vpi-delete local-search-icon" />
474
+ </button>
475
+ </div>
476
+ </form>
477
+
478
+ <ul
479
+ ref="resultsEl"
480
+ :id="results?.length ? 'localsearch-list' : undefined"
481
+ :role="results?.length ? 'listbox' : undefined"
482
+ :aria-labelledby="results?.length ? 'localsearch-label' : undefined"
483
+ class="results"
484
+ @mousemove="disableMouseOver = false"
485
+ >
486
+ <li
487
+ v-for="(p, index) in results"
488
+ :key="p.id"
489
+ role="option"
490
+ :aria-selected="selectedIndex === index ? 'true' : 'false'"
491
+ >
492
+ <a
493
+ :href="p.id"
494
+ class="result"
495
+ :class="{
496
+ selected: selectedIndex === index
497
+ }"
498
+ :aria-label="[...p.titles, p.title].join(' > ')"
499
+ @mouseenter="!disableMouseOver && (selectedIndex = index)"
500
+ @focusin="selectedIndex = index"
501
+ @click="$emit('close')"
502
+ >
503
+ <div>
504
+ <div class="titles">
505
+ <span class="title-icon">#</span>
506
+ <span
507
+ v-for="(t, index) in p.titles"
508
+ :key="index"
509
+ class="title"
510
+ >
511
+ <span class="text" v-html="t" />
512
+ <span class="vpi-chevron-right local-search-icon" />
513
+ </span>
514
+ <span class="title main">
515
+ <span class="text" v-html="p.title" />
516
+ </span>
517
+ </div>
518
+
519
+ <div v-if="showDetailedList" class="excerpt-wrapper">
520
+ <div v-if="p.text" class="excerpt" inert>
521
+ <div class="vp-doc" v-html="p.text" />
522
+ </div>
523
+ <div class="excerpt-gradient-bottom" />
524
+ <div class="excerpt-gradient-top" />
525
+ </div>
526
+ </div>
527
+ </a>
528
+ </li>
529
+ <li
530
+ v-if="filterText && !results.length && enableNoResults"
531
+ class="no-results"
532
+ >
533
+ {{ translate('modal.noResultsText') }} "<strong>{{ filterText }}</strong
534
+ >"
535
+ </li>
536
+ </ul>
537
+
538
+ <div class="search-keyboard-shortcuts">
539
+ <span>
540
+ <kbd :aria-label="translate('modal.footer.navigateUpKeyAriaLabel')">
541
+ <span class="vpi-arrow-up navigate-icon" />
542
+ </kbd>
543
+ <kbd :aria-label="translate('modal.footer.navigateDownKeyAriaLabel')">
544
+ <span class="vpi-arrow-down navigate-icon" />
545
+ </kbd>
546
+ {{ translate('modal.footer.navigateText') }}
547
+ </span>
548
+ <span>
549
+ <kbd :aria-label="translate('modal.footer.selectKeyAriaLabel')">
550
+ <span class="vpi-corner-down-left navigate-icon" />
551
+ </kbd>
552
+ {{ translate('modal.footer.selectText') }}
553
+ </span>
554
+ <span>
555
+ <kbd :aria-label="translate('modal.footer.closeKeyAriaLabel')">esc</kbd>
556
+ {{ translate('modal.footer.closeText') }}
557
+ </span>
558
+ </div>
559
+ </div>
560
+ </div>
561
+ </Teleport>
562
+ </template>
563
+
564
+ <style scoped>
565
+ .VPLocalSearchBox {
566
+ position: fixed;
567
+ z-index: 100;
568
+ inset: 0;
569
+ display: flex;
570
+ }
571
+
572
+ .backdrop {
573
+ position: absolute;
574
+ inset: 0;
575
+ background: var(--vp-backdrop-bg-color);
576
+ transition: opacity 0.5s;
577
+ }
578
+
579
+ .shell {
580
+ position: relative;
581
+ padding: 12px;
582
+ margin: 64px auto;
583
+ display: flex;
584
+ flex-direction: column;
585
+ gap: 16px;
586
+ background: var(--vp-local-search-bg);
587
+ width: min(100vw - 60px, 900px);
588
+ height: min-content;
589
+ max-height: min(100vh - 128px, 900px);
590
+ border-radius: 6px;
591
+ }
592
+
593
+ @media (max-width: 767px) {
594
+ .shell {
595
+ margin: 0;
596
+ width: 100vw;
597
+ height: 100vh;
598
+ max-height: none;
599
+ border-radius: 0;
600
+ }
601
+ }
602
+
603
+ .search-bar {
604
+ border: 1px solid var(--vp-c-divider);
605
+ border-radius: 4px;
606
+ display: flex;
607
+ align-items: center;
608
+ padding: 0 12px;
609
+ cursor: text;
610
+ }
611
+
612
+ @media (max-width: 767px) {
613
+ .search-bar {
614
+ padding: 0 8px;
615
+ }
616
+ }
617
+
618
+ .search-bar:focus-within {
619
+ border-color: var(--vp-c-brand-1);
620
+ }
621
+
622
+ .local-search-icon {
623
+ display: block;
624
+ font-size: 18px;
625
+ }
626
+
627
+ .navigate-icon {
628
+ display: block;
629
+ font-size: 14px;
630
+ }
631
+
632
+ .search-icon {
633
+ margin: 8px;
634
+ }
635
+
636
+ @media (max-width: 767px) {
637
+ .search-icon {
638
+ display: none;
639
+ }
640
+ }
641
+
642
+ .search-input {
643
+ padding: 6px 12px;
644
+ font-size: inherit;
645
+ width: 100%;
646
+ }
647
+
648
+ @media (max-width: 767px) {
649
+ .search-input {
650
+ padding: 6px 4px;
651
+ }
652
+ }
653
+
654
+ .search-actions {
655
+ display: flex;
656
+ gap: 4px;
657
+ }
658
+
659
+ @media (any-pointer: coarse) {
660
+ .search-actions {
661
+ gap: 8px;
662
+ }
663
+ }
664
+
665
+ @media (min-width: 769px) {
666
+ .search-actions.before {
667
+ display: none;
668
+ }
669
+ }
670
+
671
+ .search-actions button {
672
+ padding: 8px;
673
+ }
674
+
675
+ .search-actions button:not([disabled]):hover,
676
+ .toggle-layout-button.detailed-list {
677
+ color: var(--vp-c-brand-1);
678
+ }
679
+
680
+ .search-actions button.clear-button:disabled {
681
+ opacity: 0.37;
682
+ }
683
+
684
+ .search-keyboard-shortcuts {
685
+ font-size: 0.8rem;
686
+ opacity: 75%;
687
+ display: flex;
688
+ flex-wrap: wrap;
689
+ gap: 16px;
690
+ line-height: 14px;
691
+ }
692
+
693
+ .search-keyboard-shortcuts span {
694
+ display: flex;
695
+ align-items: center;
696
+ gap: 4px;
697
+ }
698
+
699
+ @media (max-width: 767px) {
700
+ .search-keyboard-shortcuts {
701
+ display: none;
702
+ }
703
+ }
704
+
705
+ .search-keyboard-shortcuts kbd {
706
+ background: rgba(128, 128, 128, 0.1);
707
+ border-radius: 4px;
708
+ padding: 3px 6px;
709
+ min-width: 24px;
710
+ display: inline-block;
711
+ text-align: center;
712
+ vertical-align: middle;
713
+ border: 1px solid rgba(128, 128, 128, 0.15);
714
+ box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.1);
715
+ }
716
+
717
+ .results {
718
+ display: flex;
719
+ flex-direction: column;
720
+ gap: 6px;
721
+ overflow-x: hidden;
722
+ overflow-y: auto;
723
+ overscroll-behavior: contain;
724
+ }
725
+
726
+ .result {
727
+ display: flex;
728
+ align-items: center;
729
+ gap: 8px;
730
+ border-radius: 4px;
731
+ transition: none;
732
+ line-height: 1rem;
733
+ border: solid 2px var(--vp-local-search-result-border);
734
+ outline: none;
735
+ }
736
+
737
+ .result > div {
738
+ margin: 12px;
739
+ width: 100%;
740
+ overflow: hidden;
741
+ }
742
+
743
+ @media (max-width: 767px) {
744
+ .result > div {
745
+ margin: 8px;
746
+ }
747
+ }
748
+
749
+ .titles {
750
+ display: flex;
751
+ flex-wrap: wrap;
752
+ gap: 4px;
753
+ position: relative;
754
+ z-index: 1001;
755
+ padding: 2px 0;
756
+ }
757
+
758
+ .title {
759
+ display: flex;
760
+ align-items: center;
761
+ gap: 4px;
762
+ }
763
+
764
+ .title.main {
765
+ font-weight: 500;
766
+ }
767
+
768
+ .title-icon {
769
+ opacity: 0.5;
770
+ font-weight: 500;
771
+ color: var(--vp-c-brand-1);
772
+ }
773
+
774
+ .title svg {
775
+ opacity: 0.5;
776
+ }
777
+
778
+ .result.selected {
779
+ --vp-local-search-result-bg: var(--vp-local-search-result-selected-bg);
780
+ border-color: var(--vp-local-search-result-selected-border);
781
+ }
782
+
783
+ .excerpt-wrapper {
784
+ position: relative;
785
+ }
786
+
787
+ .excerpt {
788
+ opacity: 75%;
789
+ pointer-events: none;
790
+ max-height: 140px;
791
+ overflow: hidden;
792
+ position: relative;
793
+ opacity: 0.5;
794
+ margin-top: 4px;
795
+ }
796
+
797
+ .result.selected .excerpt {
798
+ opacity: 1;
799
+ }
800
+
801
+ .excerpt :deep(*) {
802
+ font-size: 0.8rem !important;
803
+ line-height: 130% !important;
804
+ }
805
+
806
+ .titles :deep(mark),
807
+ .excerpt :deep(mark) {
808
+ background-color: var(--vp-local-search-highlight-bg);
809
+ color: var(--vp-local-search-highlight-text);
810
+ border-radius: 2px;
811
+ padding: 0 2px;
812
+ }
813
+
814
+ .excerpt :deep(.vp-code-group) .tabs {
815
+ display: none;
816
+ }
817
+
818
+ .excerpt :deep(.vp-code-group) div[class*='language-'] {
819
+ border-radius: 8px !important;
820
+ }
821
+
822
+ .excerpt-gradient-bottom {
823
+ position: absolute;
824
+ bottom: -1px;
825
+ left: 0;
826
+ width: 100%;
827
+ height: 8px;
828
+ background: linear-gradient(transparent, var(--vp-local-search-result-bg));
829
+ z-index: 1000;
830
+ }
831
+
832
+ .excerpt-gradient-top {
833
+ position: absolute;
834
+ top: -1px;
835
+ left: 0;
836
+ width: 100%;
837
+ height: 8px;
838
+ background: linear-gradient(var(--vp-local-search-result-bg), transparent);
839
+ z-index: 1000;
840
+ }
841
+
842
+ .result.selected .titles,
843
+ .result.selected .title-icon {
844
+ color: var(--vp-c-brand-1) !important;
845
+ }
846
+
847
+ .no-results {
848
+ font-size: 0.9rem;
849
+ text-align: center;
850
+ padding: 12px;
851
+ }
852
+
853
+ svg {
854
+ flex: none;
855
+ }
856
+ </style>