@voidzero-dev/vitepress-theme 4.4.2 → 4.5.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.
@@ -8,13 +8,16 @@
8
8
  */
9
9
  import { onKeyStroke } from '@vueuse/core'
10
10
  import type { DefaultTheme } from 'vitepress/theme'
11
- import { computed, defineAsyncComponent, onMounted, onUnmounted, ref } from 'vue'
11
+ import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
12
12
  import { useData } from '@vp-composables/data'
13
+ import { resolveMode, resolveOptionsForLanguage } from '@vp-support/docsearch'
14
+ import { smartComputed } from '@vp-support/reactivity'
15
+ import VPNavBarAskAiButton from './VPNavBarAskAiButton.vue'
13
16
  import VPNavBarSearchButton from './VPNavBarSearchButton.vue'
14
17
  import VPSearchError from './VPSearchError.vue'
15
18
  import { getSearchProvider, validateAlgoliaConfig } from '@vp-support/search-config'
16
19
 
17
- const { theme } = useData()
20
+ const { theme, localeIndex, lang } = useData()
18
21
 
19
22
  // Runtime-only provider detection
20
23
  // NOTE: We intentionally do NOT fall back to compile-time constants (__VP_LOCAL_SEARCH__, __ALGOLIA__)
@@ -23,10 +26,18 @@ const provider = computed(() => getSearchProvider(theme.value))
23
26
 
24
27
  const isLocal = computed(() => provider.value === 'local')
25
28
 
29
+ const algoliaOptions = smartComputed<DefaultTheme.AlgoliaSearchOptions>(() => {
30
+ return resolveOptionsForLanguage(
31
+ theme.value.search?.options || {},
32
+ localeIndex.value,
33
+ lang.value
34
+ )
35
+ })
36
+
26
37
  // Validate Algolia config before attempting to render
27
38
  const algoliaValidation = computed(() => {
28
39
  if (provider.value !== 'algolia') return null
29
- const options = theme.value.search?.options ?? (theme.value as any).algolia
40
+ const options = algoliaOptions.value
30
41
  return validateAlgoliaConfig(options)
31
42
  })
32
43
 
@@ -63,91 +74,101 @@ const VPAlgoliaSearchBox = defineAsyncComponent({
63
74
  }
64
75
  })
65
76
 
77
+ // #region Algolia Search
78
+
79
+ const resolvedMode = computed(() => resolveMode(algoliaOptions.value))
80
+
81
+ const askAiSidePanelConfig = computed(() => {
82
+ if (!resolvedMode.value.useSidePanel) return null
83
+ const askAi = algoliaOptions.value.askAi
84
+ if (!askAi || typeof askAi === 'string') return null
85
+ if (!askAi.sidePanel) return null
86
+ return askAi.sidePanel === true ? {} : askAi.sidePanel
87
+ })
88
+
89
+ const askAiShortcutEnabled = computed(() => {
90
+ return askAiSidePanelConfig.value?.keyboardShortcuts?.['Ctrl/Cmd+I'] !== false
91
+ })
92
+
93
+ type OpenTarget = 'search' | 'askAi' | 'toggleAskAi'
94
+ type OpenRequest = { target: OpenTarget; nonce: number }
95
+ const openRequest = ref<OpenRequest | null>(null)
96
+ let openNonce = 0
97
+
66
98
  // to avoid loading the docsearch js upfront (which is more than 1/3 of the
67
99
  // payload), we delay initializing it until the user has actually clicked or
68
100
  // hit the hotkey to invoke it.
69
101
  const loaded = ref(false)
102
+ const actuallyLoaded = ref(false)
103
+
104
+ onMounted(() => {
105
+ if (!isAlgolia.value) return
70
106
 
71
- const preconnect = () => {
72
107
  const id = 'VPAlgoliaPreconnect'
108
+ if (document.getElementById(id)) return
109
+
110
+ const appId =
111
+ algoliaOptions.value.appId ||
112
+ (typeof algoliaOptions.value.askAi === 'object'
113
+ ? algoliaOptions.value.askAi?.appId
114
+ : undefined)
115
+
116
+ if (!appId) return
73
117
 
74
118
  const rIC = window.requestIdleCallback || setTimeout
75
119
  rIC(() => {
76
120
  const preconnect = document.createElement('link')
77
121
  preconnect.id = id
78
122
  preconnect.rel = 'preconnect'
79
- preconnect.href = `https://${
80
- ((theme.value.search?.options as DefaultTheme.AlgoliaSearchOptions) ??
81
- (theme.value as any).algolia)!.appId
82
- }-dsn.algolia.net`
123
+ preconnect.href = `https://${appId}-dsn.algolia.net`
83
124
  preconnect.crossOrigin = ''
84
125
  document.head.appendChild(preconnect)
85
126
  })
86
- }
87
-
88
- onMounted(() => {
89
- if (!isAlgolia.value) {
90
- return
91
- }
92
-
93
- preconnect()
127
+ })
94
128
 
95
- const handleSearchHotKey = (event: KeyboardEvent) => {
129
+ if (isAlgolia.value) {
130
+ onKeyStroke('k', (event) => {
96
131
  if (
97
- (event.key?.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) ||
98
- (!isEditingContent(event) && event.key === '/')
132
+ resolvedMode.value.showKeywordSearch &&
133
+ (event.ctrlKey || event.metaKey)
99
134
  ) {
100
135
  event.preventDefault()
101
- load()
102
- remove()
136
+ loadAndOpen('search')
103
137
  }
104
- }
105
-
106
- const remove = () => {
107
- window.removeEventListener('keydown', handleSearchHotKey)
108
- }
138
+ })
109
139
 
110
- window.addEventListener('keydown', handleSearchHotKey)
140
+ onKeyStroke('i', (event) => {
141
+ if (
142
+ askAiSidePanelConfig.value &&
143
+ askAiShortcutEnabled.value &&
144
+ (event.ctrlKey || event.metaKey)
145
+ ) {
146
+ event.preventDefault()
147
+ loadAndOpen('askAi')
148
+ }
149
+ })
111
150
 
112
- onUnmounted(remove)
113
- })
151
+ onKeyStroke('/', (event) => {
152
+ if (resolvedMode.value.showKeywordSearch && !isEditingContent(event)) {
153
+ event.preventDefault()
154
+ loadAndOpen('search')
155
+ }
156
+ })
157
+ }
114
158
 
115
- function load() {
159
+ function loadAndOpen(target: OpenTarget) {
116
160
  if (!loaded.value) {
117
161
  loaded.value = true
118
- setTimeout(poll, 16)
119
162
  }
120
- }
121
-
122
- function poll() {
123
- // programmatically open the search box after initialize
124
- const e = new Event('keydown') as any
125
-
126
- e.key = 'k'
127
- e.metaKey = true
128
163
 
129
- window.dispatchEvent(e)
130
-
131
- setTimeout(() => {
132
- if (!document.querySelector('.DocSearch-Modal')) {
133
- poll()
134
- }
135
- }, 16)
164
+ // This will either be handled immediately if DocSearch is ready,
165
+ // or queued by the AlgoliaSearchBox until its instances become ready.
166
+ openRequest.value = { target, nonce: ++openNonce }
136
167
  }
137
168
 
138
- function isEditingContent(event: KeyboardEvent): boolean {
139
- const element = event.target as HTMLElement
140
- const tagName = element.tagName
169
+ // #endregion
141
170
 
142
- return (
143
- element.isContentEditable ||
144
- tagName === 'INPUT' ||
145
- tagName === 'SELECT' ||
146
- tagName === 'TEXTAREA'
147
- )
148
- }
149
-
150
- // Local search
171
+ // #region Local Search
151
172
 
152
173
  const showSearch = ref(false)
153
174
 
@@ -172,31 +193,58 @@ onKeyStroke('/', (event) => {
172
193
  showSearch.value = true
173
194
  }
174
195
  })
196
+
197
+ // #endregion
198
+
199
+ function isEditingContent(event: KeyboardEvent): boolean {
200
+ const element = event.target as HTMLElement
201
+ const tagName = element.tagName
202
+
203
+ return (
204
+ element.isContentEditable ||
205
+ tagName === 'INPUT' ||
206
+ tagName === 'SELECT' ||
207
+ tagName === 'TEXTAREA'
208
+ )
209
+ }
175
210
  </script>
176
211
 
177
212
  <template>
178
213
  <div class="VPNavBarSearch">
179
214
  <!-- Local Search -->
180
215
  <template v-if="provider === 'local'">
216
+ <VPNavBarSearchButton
217
+ :text="algoliaOptions.translations?.button?.buttonText || 'Search'"
218
+ :aria-label="algoliaOptions.translations?.button?.buttonAriaLabel || 'Search'"
219
+ :aria-keyshortcuts="'/ control+k meta+k'"
220
+ @click="showSearch = true"
221
+ />
181
222
  <VPLocalSearchBox
182
223
  v-if="showSearch"
183
224
  @close="showSearch = false"
184
225
  />
185
-
186
- <div id="local-search">
187
- <VPNavBarSearchButton @click="showSearch = true" />
188
- </div>
189
226
  </template>
190
227
 
191
228
  <!-- Algolia Search - only render if config is valid -->
192
- <template v-else-if="isAlgolia">
193
- <!-- Show button until Algolia is loaded, then Algolia takes over the #docsearch container -->
194
- <div v-if="!loaded" id="docsearch">
195
- <VPNavBarSearchButton @click="load" />
196
- </div>
229
+ <template v-if="isAlgolia">
230
+ <VPNavBarSearchButton
231
+ v-if="resolvedMode.showKeywordSearch"
232
+ :text="algoliaOptions.translations?.button?.buttonText || 'Search'"
233
+ :aria-label="algoliaOptions.translations?.button?.buttonAriaLabel || 'Search'"
234
+ :aria-keyshortcuts="'/ control+k meta+k'"
235
+ @click="loadAndOpen('search')"
236
+ />
237
+ <VPNavBarAskAiButton
238
+ v-if="askAiSidePanelConfig"
239
+ :aria-label="askAiSidePanelConfig.button?.translations?.buttonAriaLabel || 'Ask AI'"
240
+ :aria-keyshortcuts="askAiShortcutEnabled ? 'control+i meta+i' : undefined"
241
+ @click="actuallyLoaded ? loadAndOpen('toggleAskAi') : loadAndOpen('askAi')"
242
+ />
197
243
  <VPAlgoliaSearchBox
198
- v-else
199
- :algolia="algoliaValidation!.config!"
244
+ v-if="loaded"
245
+ :algolia-options
246
+ :open-request
247
+ @vue:beforeMount="actuallyLoaded = true"
200
248
  />
201
249
  </template>
202
250
 
@@ -204,7 +252,7 @@ onKeyStroke('/', (event) => {
204
252
  </div>
205
253
  </template>
206
254
 
207
- <style>
255
+ <style scoped>
208
256
  .VPNavBarSearch {
209
257
  display: flex;
210
258
  align-items: center;
@@ -212,6 +260,7 @@ onKeyStroke('/', (event) => {
212
260
 
213
261
  @media (min-width: 768px) {
214
262
  .VPNavBarSearch {
263
+ gap: 8px;
215
264
  flex-grow: 1;
216
265
  padding-left: 8px;
217
266
  }
@@ -1,176 +1,67 @@
1
1
  <script lang="ts" setup>
2
- import { createSearchTranslate } from '@vp-support/translation'
3
-
4
- // Button translations
5
- interface ButtonTranslations {
6
- buttonText?: string
7
- buttonAriaLabel?: string
8
- }
9
-
10
- const defaultTranslations: { button: ButtonTranslations } = {
11
- button: {
12
- buttonText: 'Search',
13
- buttonAriaLabel: 'Search'
14
- }
15
- }
16
-
17
- const translate = createSearchTranslate(defaultTranslations)
2
+ defineProps<{
3
+ text: string
4
+ }>()
18
5
  </script>
19
6
 
20
7
  <template>
21
- <button
22
- type="button"
23
- :aria-label="translate('button.buttonAriaLabel')"
24
- aria-keyshortcuts="/ control+k meta+k"
25
- class="DocSearch DocSearch-Button VPSearchButton"
26
- >
27
- <span class="DocSearch-Button-Container">
28
- <span class="vpi-search DocSearch-Search-Icon"></span>
29
- <span class="DocSearch-Button-Placeholder">{{ translate('button.buttonText') }}</span>
30
- </span>
31
- <span class="DocSearch-Button-Keys">
32
- <kbd class="DocSearch-Button-Key"></kbd>
33
- <kbd class="DocSearch-Button-Key"></kbd>
8
+ <button type="button" class="VPNavBarSearchButton">
9
+ <span class="vpi-search" aria-hidden="true"></span>
10
+ <span class="text">{{ text }}</span>
11
+ <span class="keys" aria-hidden="true">
12
+ <kbd class="key-cmd">&#x2318;</kbd>
13
+ <kbd class="key-ctrl">Ctrl</kbd>
14
+ <kbd>K</kbd>
34
15
  </span>
35
16
  </button>
36
17
  </template>
37
18
 
38
- <style>
39
- [class*='DocSearch'] {
40
- --docsearch-actions-height: auto;
41
- --docsearch-actions-width: auto;
42
- --docsearch-background-color: var(--vp-c-bg-soft);
43
- --docsearch-container-background: var(--vp-backdrop-bg-color);
44
- --docsearch-focus-color: var(--vp-c-brand-1);
45
- --docsearch-footer-background: var(--vp-c-bg);
46
- --docsearch-highlight-color: var(--vp-c-brand-1);
47
- --docsearch-hit-background: var(--vp-c-default-soft);
48
- --docsearch-hit-color: var(--vp-c-text-1);
49
- --docsearch-hit-highlight-color: var(--vp-c-brand-soft);
50
- --docsearch-icon-color: var(--vp-c-text-2);
51
- --docsearch-key-background: transparent;
52
- --docsearch-key-color: var(--vp-c-text-2);
53
- --docsearch-modal-background: var(--vp-c-bg-soft);
54
- --docsearch-muted-color: var(--vp-c-text-2);
55
- --docsearch-primary-color: var(--vp-c-brand-1);
56
- --docsearch-searchbox-focus-background: transparent;
57
- --docsearch-secondary-text-color: var(--vp-c-text-2);
58
- --docsearch-soft-primary-color: var(--vp-c-brand-soft);
59
- --docsearch-subtle-color: var(--vp-c-divider);
60
- --docsearch-success-color: var(--vp-c-brand-soft);
61
- --docsearch-text-color: var(--vp-c-text-1);
62
- }
63
-
64
- .dark [class*='DocSearch'] {
65
- --docsearch-modal-shadow: none;
66
- }
67
-
68
- .DocSearch-Clear {
69
- padding: 0 8px;
70
- }
71
-
72
- .DocSearch-Commands-Key {
73
- padding: 4px;
74
- border: 1px solid var(--docsearch-subtle-color);
75
- border-radius: 4px;
76
- }
77
-
78
- .DocSearch-Hit a:focus-visible {
79
- outline: 2px solid var(--docsearch-focus-color);
80
- }
81
-
82
- .DocSearch-Logo [class^='cls-'] {
83
- fill: currentColor;
84
- }
85
-
86
- .DocSearch-SearchBar + .DocSearch-Footer {
87
- border-top-color: transparent;
88
- }
89
-
90
- .DocSearch-Title {
91
- font-size: revert;
92
- line-height: revert;
93
- }
94
-
95
- .DocSearch-Button {
96
- --docsearch-muted-color: var(--docsearch-text-color);
97
- --docsearch-searchbox-background: transparent;
19
+ <style scoped>
20
+ .VPNavBarSearchButton {
98
21
  display: flex;
99
22
  align-items: center;
100
- gap: 4px;
101
- width: auto;
102
- height: 40px;
103
- padding: 0 12px;
104
- margin: 0;
105
- border: none;
106
- border-radius: 8px;
107
- background: var(--docsearch-searchbox-background);
108
- color: var(--docsearch-muted-color);
109
- font-size: 14px;
110
- font-weight: 500;
111
- cursor: pointer;
112
- transition: background 0.1s;
113
- }
114
-
115
- .DocSearch-Button:hover {
116
- background: var(--vp-c-default-soft);
23
+ gap: 8px;
24
+ height: var(--vp-nav-height);
25
+ padding: 8px 14px;
26
+ font-size: 20px;
117
27
  }
118
28
 
119
- .DocSearch-Button-Container {
120
- display: flex;
121
- align-items: center;
122
- gap: 8px;
29
+ .text,
30
+ .keys,
31
+ :root.mac .key-ctrl,
32
+ :root:not(.mac) .key-cmd {
33
+ display: none;
123
34
  }
124
35
 
125
- .DocSearch-Search-Icon {
126
- color: inherit !important;
127
- width: 20px;
128
- height: 20px;
36
+ kbd {
37
+ font-family: inherit;
38
+ font-weight: 500;
129
39
  }
130
40
 
131
41
  @media (min-width: 768px) {
132
- .DocSearch-Button {
133
- --docsearch-muted-color: var(--docsearch-secondary-text-color);
134
- --docsearch-searchbox-background: var(--vp-c-bg-alt);
42
+ .VPNavBarSearchButton {
43
+ height: auto;
44
+ padding: 8px 12px;
45
+ background-color: var(--vp-c-bg-alt);
46
+ border-radius: 8px;
47
+ font-size: 14px;
48
+ line-height: 1;
49
+ color: var(--vp-c-text-2);
135
50
  }
136
51
 
137
- .DocSearch-Search-Icon {
138
- width: 15px;
139
- height: 15px;
140
- }
141
-
142
- .DocSearch-Button-Placeholder {
52
+ .text {
53
+ display: inline;
143
54
  font-size: 13px;
144
55
  }
145
- }
146
-
147
- /*
148
- * Placeholder button keyboard shortcut styles.
149
- * Scoped to .VPSearchButton so they don't conflict with DocSearch's real button.
150
- */
151
- .VPSearchButton .DocSearch-Button-Keys {
152
- min-width: auto;
153
- margin: 0;
154
- padding: 4px 6px;
155
- background-color: var(--docsearch-key-background);
156
- border: 1px solid var(--docsearch-subtle-color);
157
- border-radius: 4px;
158
- font-size: 12px;
159
- line-height: 1;
160
- color: var(--docsearch-key-color);
161
- }
162
-
163
- .VPSearchButton .DocSearch-Button-Keys > * {
164
- display: none;
165
- }
166
56
 
167
- .VPSearchButton .DocSearch-Button-Keys:after {
168
- /*rtl:ignore*/
169
- direction: ltr;
170
- content: 'Ctrl K';
171
- }
172
-
173
- .mac .VPSearchButton .DocSearch-Button-Keys:after {
174
- content: '\2318 K';
57
+ .keys {
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 4px;
61
+ padding: 4px 6px;
62
+ border: 1px solid var(--vp-c-divider);
63
+ border-radius: 4px;
64
+ font-size: 12px;
65
+ }
175
66
  }
176
67
  </style>
@@ -19,7 +19,13 @@ const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
19
19
  <p class="title">{{ currentLang.label }}</p>
20
20
 
21
21
  <template v-for="locale in localeLinks" :key="locale.link">
22
- <VPMenuLink :item="locale" :lang="locale.lang" :dir="locale.dir" />
22
+ <VPMenuLink
23
+ :item="locale"
24
+ :lang="locale.lang"
25
+ :hreflang="locale.lang"
26
+ rel="alternate"
27
+ :dir="locale.dir"
28
+ />
23
29
  </template>
24
30
  </div>
25
31
  </VPFlyout>
@@ -1,7 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { useScrollLock } from '@vueuse/core'
3
3
  import { inBrowser } from 'vitepress'
4
- import { ref } from 'vue'
5
4
  import VPNavScreenAppearance from './VPNavScreenAppearance.vue'
6
5
  import VPNavScreenMenu from './VPNavScreenMenu.vue'
7
6
  import VPNavScreenSocialLinks from './VPNavScreenSocialLinks.vue'
@@ -11,7 +10,6 @@ defineProps<{
11
10
  open: boolean
12
11
  }>()
13
12
 
14
- const screen = ref<HTMLElement | null>(null)
15
13
  const isLocked = useScrollLock(inBrowser ? document.body : null)
16
14
  </script>
17
15
 
@@ -21,7 +19,7 @@ const isLocked = useScrollLock(inBrowser ? document.body : null)
21
19
  @enter="isLocked = true"
22
20
  @after-leave="isLocked = false"
23
21
  >
24
- <div v-if="open" class="VPNavScreen" ref="screen" id="VPNavScreen">
22
+ <div v-if="open" class="VPNavScreen" id="VPNavScreen">
25
23
  <div class="container">
26
24
  <slot name="nav-screen-content-before" />
27
25
  <VPNavScreenMenu class="menu" />
package/src/shims.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ declare module '@localSearchIndex' {
2
+ const data: Record<string, () => Promise<{ default: string }>>
3
+ export default data
4
+ }
5
+
6
+ declare module 'mark.js/src/vanilla.js' {
7
+ import type Mark from 'mark.js'
8
+ const mark: typeof Mark
9
+ export default mark
10
+ }
@@ -55,6 +55,8 @@ body {
55
55
  image-rendering: high-quality;
56
56
  -webkit-font-smoothing: antialiased;
57
57
  -moz-osx-font-smoothing: grayscale;
58
+ text-autospace: normal;
59
+ text-spacing-trim: normal;
58
60
  background-color: var(--vp-c-bg);
59
61
  }
60
62
 
@@ -49,6 +49,8 @@
49
49
  text-rendering: optimizeLegibility;
50
50
  -webkit-font-smoothing: antialiased;
51
51
  -moz-osx-font-smoothing: grayscale;
52
+ text-autospace: normal;
53
+ text-spacing-trim: normal;
52
54
  }
53
55
 
54
56
  .vp-doc main {
@@ -44,6 +44,8 @@ body {
44
44
  text-rendering: optimizeLegibility;
45
45
  -webkit-font-smoothing: antialiased;
46
46
  -moz-osx-font-smoothing: grayscale;
47
+ text-autospace: normal;
48
+ text-spacing-trim: normal;
47
49
  }
48
50
 
49
51
  main {