@voidzero-dev/vitepress-theme 4.4.1 → 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.
- package/package.json +6 -4
- package/src/components/oss/Header.vue +1 -1
- package/src/components/vitepress-default/VPAlgoliaSearchBox.vue +211 -106
- package/src/components/vitepress-default/VPLocalSearchBox.vue +11 -33
- package/src/components/vitepress-default/VPMenuLink.vue +2 -1
- package/src/components/vitepress-default/VPNavBar.vue +14 -20
- package/src/components/vitepress-default/VPNavBarAskAiButton.vue +31 -0
- package/src/components/vitepress-default/VPNavBarExtra.vue +7 -1
- package/src/components/vitepress-default/VPNavBarSearch.vue +119 -70
- package/src/components/vitepress-default/VPNavBarSearchButton.vue +43 -152
- package/src/components/vitepress-default/VPNavBarTranslations.vue +7 -1
- package/src/components/vitepress-default/VPNavScreen.vue +1 -3
- package/src/shims.d.ts +10 -0
- package/src/styles/base.css +2 -0
- package/src/styles/vitepress-default/base-scoped.css +2 -0
- package/src/styles/vitepress-default/base.css +2 -0
- package/src/styles/vitepress-default/docsearch.css +120 -0
- package/src/styles/vitepress-default/icons.css +4 -1
- package/src/styles/vitepress-default/vars.css +5 -1
- package/src/support/vitepress-default/docsearch.ts +253 -0
- package/src/support/vitepress-default/reactivity.ts +14 -0
- package/src/types/docsearch.d.ts +58 -26
- package/src/types/local-search.d.ts +26 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidzero-dev/vitepress-theme",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "Shared VitePress theme for VoidZero projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
"*.css"
|
|
16
16
|
],
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"vitepress": "^2.0.0-alpha.
|
|
18
|
+
"vitepress": "^2.0.0-alpha.16",
|
|
19
19
|
"vue": "^3.5.0"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@docsearch/css": "^4.
|
|
23
|
-
"@docsearch/js": "^4.
|
|
22
|
+
"@docsearch/css": "^4.5.4",
|
|
23
|
+
"@docsearch/js": "^4.5.4",
|
|
24
|
+
"@docsearch/sidepanel-js": "^4.5.4",
|
|
24
25
|
"@rive-app/canvas-lite": "^2.33.3",
|
|
25
26
|
"@tailwindcss/typography": "^0.5.19",
|
|
26
27
|
"@tailwindcss/vite": "^4.1.18",
|
|
@@ -34,6 +35,7 @@
|
|
|
34
35
|
"tailwindcss": "^4.1.18"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
38
|
+
"@types/mark.js": "^8.11.12",
|
|
37
39
|
"typescript": "^5.9.3"
|
|
38
40
|
}
|
|
39
41
|
}
|
|
@@ -198,7 +198,7 @@ onUnmounted(() => {
|
|
|
198
198
|
class="wrapper px-6 py-5 flex items-center justify-between relative bg-white dark:bg-primary border-b border-stroke dark:border-nickel">
|
|
199
199
|
<!-- Left side: Logo + Nav -->
|
|
200
200
|
<div class="flex gap-10 self-stretch">
|
|
201
|
-
<a href="/" class="flex items-center -mx-2 px-2">
|
|
201
|
+
<a href="/" class="flex flex-col items-start justify-center -mx-2 px-2">
|
|
202
202
|
<slot name="nav-bar-title-before" />
|
|
203
203
|
<img class="h-4 block dark:hidden" :src="logoDark" :alt="logoAlt" />
|
|
204
204
|
<img class="h-4 hidden dark:block" :src="logoLight" :alt="logoAlt" />
|
|
@@ -1,133 +1,238 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* Key changes from original:
|
|
6
|
-
* 1. @docsearch/css and @docsearch/js are loaded dynamically (not static imports)
|
|
7
|
-
* 2. This allows local search users to avoid installing docsearch packages
|
|
8
|
-
* 3. Error handling prevents crashes when packages are missing or initialization fails
|
|
9
|
-
*/
|
|
10
|
-
import { useRouter } from 'vitepress'
|
|
2
|
+
import type { DocSearchInstance, DocSearchProps } from '@docsearch/js'
|
|
3
|
+
import type { SidepanelInstance, SidepanelProps } from '@docsearch/sidepanel-js'
|
|
4
|
+
import { inBrowser, useRouter } from 'vitepress'
|
|
11
5
|
import type { DefaultTheme } from 'vitepress/theme'
|
|
12
|
-
import { nextTick,
|
|
6
|
+
import { nextTick, onUnmounted, watch } from 'vue'
|
|
7
|
+
import type { DocSearchAskAi } from '../../types/docsearch'
|
|
13
8
|
import { useData } from '@vp-composables/data'
|
|
14
|
-
import {
|
|
9
|
+
import { resolveMode, validateCredentials } from '@vp-support/docsearch'
|
|
10
|
+
|
|
11
|
+
import '../../styles/vitepress-default/docsearch.css'
|
|
15
12
|
|
|
16
13
|
const props = defineProps<{
|
|
17
|
-
|
|
14
|
+
algoliaOptions: DefaultTheme.AlgoliaSearchOptions
|
|
15
|
+
openRequest?: {
|
|
16
|
+
target: 'search' | 'askAi' | 'toggleAskAi'
|
|
17
|
+
nonce: number
|
|
18
|
+
} | null
|
|
18
19
|
}>()
|
|
19
20
|
|
|
20
21
|
const router = useRouter()
|
|
21
|
-
const { site
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
let
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
const { site } = useData()
|
|
23
|
+
|
|
24
|
+
let cleanup = () => {}
|
|
25
|
+
let docsearchInstance: DocSearchInstance | undefined
|
|
26
|
+
let sidepanelInstance: SidepanelInstance | undefined
|
|
27
|
+
let openOnReady: 'search' | 'askAi' | null = null
|
|
28
|
+
let initializeCount = 0
|
|
29
|
+
let docsearchLoader: Promise<typeof import('@docsearch/js')> | undefined
|
|
30
|
+
let sidepanelLoader: Promise<typeof import('@docsearch/sidepanel-js')> | undefined
|
|
31
|
+
let lastFocusedElement: HTMLElement | null = null
|
|
32
|
+
let skipEventDocsearch = false
|
|
33
|
+
let skipEventSidepanel = false
|
|
34
|
+
|
|
35
|
+
watch(() => props.algoliaOptions, update, { immediate: true })
|
|
36
|
+
onUnmounted(cleanup)
|
|
37
|
+
|
|
38
|
+
watch(
|
|
39
|
+
() => props.openRequest?.nonce,
|
|
40
|
+
() => {
|
|
41
|
+
const req = props.openRequest
|
|
42
|
+
if (!req) return
|
|
43
|
+
if (req.target === 'search') {
|
|
44
|
+
if (docsearchInstance?.isReady) {
|
|
45
|
+
onBeforeOpen('docsearch', () => docsearchInstance?.open())
|
|
46
|
+
} else {
|
|
47
|
+
openOnReady = 'search'
|
|
48
|
+
}
|
|
49
|
+
} else if (req.target === 'toggleAskAi') {
|
|
50
|
+
if (sidepanelInstance?.isOpen) {
|
|
51
|
+
sidepanelInstance.close()
|
|
52
|
+
} else {
|
|
53
|
+
onBeforeOpen('sidepanel', () => sidepanelInstance?.open())
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
// askAi - open sidepanel or fallback to docsearch modal
|
|
57
|
+
if (sidepanelInstance?.isReady) {
|
|
58
|
+
onBeforeOpen('sidepanel', () => sidepanelInstance?.open())
|
|
59
|
+
} else if (sidepanelInstance) {
|
|
60
|
+
openOnReady = 'askAi'
|
|
61
|
+
} else if (docsearchInstance?.isReady) {
|
|
62
|
+
onBeforeOpen('docsearch', () => docsearchInstance?.openAskAi())
|
|
63
|
+
} else {
|
|
64
|
+
openOnReady = 'askAi'
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{ immediate: true }
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
async function update(options: DefaultTheme.AlgoliaSearchOptions) {
|
|
72
|
+
if (!inBrowser) return
|
|
73
|
+
await nextTick()
|
|
36
74
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
75
|
+
const askAi = options.askAi as DocSearchAskAi | undefined
|
|
76
|
+
|
|
77
|
+
const { valid, ...credentials } = validateCredentials({
|
|
78
|
+
appId: options.appId ?? askAi?.appId,
|
|
79
|
+
apiKey: options.apiKey ?? askAi?.apiKey,
|
|
80
|
+
indexName: options.indexName ?? askAi?.indexName
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
if (!valid) {
|
|
84
|
+
console.warn('[vitepress] Algolia search cannot be initialized: missing appId/apiKey/indexName.')
|
|
42
85
|
return
|
|
43
86
|
}
|
|
44
87
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
})
|
|
88
|
+
await initialize({ ...options, ...credentials })
|
|
89
|
+
}
|
|
48
90
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
91
|
+
async function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
|
|
92
|
+
const currentInitialize = ++initializeCount
|
|
93
|
+
|
|
94
|
+
// Always tear down previous instances first (e.g. on locale changes)
|
|
95
|
+
cleanup()
|
|
96
|
+
|
|
97
|
+
const { useSidePanel } = resolveMode(userOptions)
|
|
98
|
+
const askAi = userOptions.askAi as DocSearchAskAi | undefined
|
|
99
|
+
|
|
100
|
+
const { default: docsearch } = await loadDocsearch()
|
|
101
|
+
if (currentInitialize !== initializeCount) return
|
|
102
|
+
|
|
103
|
+
if (useSidePanel && askAi?.sidePanel) {
|
|
104
|
+
const { default: sidepanel } = await loadSidepanel()
|
|
105
|
+
if (currentInitialize !== initializeCount) return
|
|
106
|
+
|
|
107
|
+
sidepanelInstance = sidepanel({
|
|
108
|
+
...(askAi.sidePanel === true ? {} : askAi.sidePanel),
|
|
109
|
+
container: '#vp-docsearch-sidepanel',
|
|
110
|
+
indexName: askAi.indexName ?? userOptions.indexName,
|
|
111
|
+
appId: askAi.appId ?? userOptions.appId,
|
|
112
|
+
apiKey: askAi.apiKey ?? userOptions.apiKey,
|
|
113
|
+
assistantId: askAi.assistantId,
|
|
114
|
+
onOpen: focusInput,
|
|
115
|
+
onClose: onClose.bind(null, 'sidepanel'),
|
|
116
|
+
onReady: () => {
|
|
117
|
+
if (openOnReady === 'askAi') {
|
|
118
|
+
openOnReady = null
|
|
119
|
+
onBeforeOpen('sidepanel', () => sidepanelInstance?.open())
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
keyboardShortcuts: {
|
|
123
|
+
'Ctrl/Cmd+I': false
|
|
124
|
+
}
|
|
125
|
+
} as SidepanelProps)
|
|
52
126
|
}
|
|
53
|
-
})
|
|
54
127
|
|
|
55
|
-
async function update() {
|
|
56
|
-
if (!docsearchFn) return
|
|
57
|
-
|
|
58
|
-
await nextTick()
|
|
59
128
|
const options = {
|
|
60
|
-
...
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
...(Array.isArray(rawFacetFilters)
|
|
66
|
-
? rawFacetFilters
|
|
67
|
-
: [rawFacetFilters]
|
|
68
|
-
).filter((f) => !f.startsWith('lang:')),
|
|
69
|
-
`lang:${lang.value}`
|
|
70
|
-
]
|
|
71
|
-
|
|
72
|
-
// Rebuild the askAi prop as an object:
|
|
73
|
-
// If the askAi prop is a string, treat it as the assistantId and use
|
|
74
|
-
// the default indexName, apiKey and appId from the main options.
|
|
75
|
-
// If the askAi prop is an object, spread its explicit values.
|
|
76
|
-
const askAiProp = options.askAi
|
|
77
|
-
const isAskAiString = typeof askAiProp === 'string'
|
|
78
|
-
|
|
79
|
-
const askAi = askAiProp
|
|
80
|
-
? {
|
|
81
|
-
indexName: isAskAiString ? options.indexName : askAiProp.indexName,
|
|
82
|
-
apiKey: isAskAiString ? options.apiKey : askAiProp.apiKey,
|
|
83
|
-
appId: isAskAiString ? options.appId : askAiProp.appId,
|
|
84
|
-
assistantId: isAskAiString ? askAiProp : askAiProp.assistantId,
|
|
85
|
-
// Re-use the merged facetFilters from the search parameters so that
|
|
86
|
-
// Ask AI uses the same language filtering as the regular search.
|
|
87
|
-
searchParameters: facetFilters.length ? { facetFilters } : undefined
|
|
129
|
+
...userOptions,
|
|
130
|
+
container: '#vp-docsearch',
|
|
131
|
+
navigator: {
|
|
132
|
+
navigate(item) {
|
|
133
|
+
router.go(item.itemUrl)
|
|
88
134
|
}
|
|
89
|
-
: undefined
|
|
90
|
-
|
|
91
|
-
initialize({
|
|
92
|
-
...options,
|
|
93
|
-
searchParameters: {
|
|
94
|
-
...options.searchParameters,
|
|
95
|
-
facetFilters
|
|
96
135
|
},
|
|
97
|
-
|
|
136
|
+
transformItems: (items) => items.map((item) => ({ ...item, url: getRelativePath(item.url) })),
|
|
137
|
+
// When sidepanel is enabled, intercept Ask AI events to open it instead (hybrid mode)
|
|
138
|
+
...(useSidePanel && sidepanelInstance && {
|
|
139
|
+
interceptAskAiEvent: (initialMessage) => {
|
|
140
|
+
onBeforeOpen('sidepanel', () => sidepanelInstance?.open(initialMessage))
|
|
141
|
+
return true
|
|
142
|
+
}
|
|
143
|
+
}),
|
|
144
|
+
onOpen: focusInput,
|
|
145
|
+
onClose: onClose.bind(null, 'docsearch'),
|
|
146
|
+
onReady: () => {
|
|
147
|
+
if (openOnReady === 'search') {
|
|
148
|
+
openOnReady = null
|
|
149
|
+
onBeforeOpen('docsearch', () => docsearchInstance?.open())
|
|
150
|
+
} else if (openOnReady === 'askAi' && !sidepanelInstance) {
|
|
151
|
+
// No sidepanel configured, use docsearch modal for askAi
|
|
152
|
+
openOnReady = null
|
|
153
|
+
onBeforeOpen('docsearch', () => docsearchInstance?.openAskAi())
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
keyboardShortcuts: {
|
|
157
|
+
'/': false,
|
|
158
|
+
'Ctrl/Cmd+K': false
|
|
159
|
+
}
|
|
160
|
+
} as DocSearchProps
|
|
161
|
+
|
|
162
|
+
docsearchInstance = docsearch(options)
|
|
163
|
+
|
|
164
|
+
cleanup = () => {
|
|
165
|
+
docsearchInstance?.destroy()
|
|
166
|
+
sidepanelInstance?.destroy()
|
|
167
|
+
docsearchInstance = undefined
|
|
168
|
+
sidepanelInstance = undefined
|
|
169
|
+
openOnReady = null
|
|
170
|
+
lastFocusedElement = null
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function focusInput() {
|
|
175
|
+
requestAnimationFrame(() => {
|
|
176
|
+
const input =
|
|
177
|
+
document.querySelector<HTMLInputElement>('#docsearch-input') ||
|
|
178
|
+
document.querySelector<HTMLInputElement>('#docsearch-sidepanel textarea')
|
|
179
|
+
input?.focus()
|
|
98
180
|
})
|
|
99
181
|
}
|
|
100
182
|
|
|
101
|
-
function
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
183
|
+
function onBeforeOpen(target: 'docsearch' | 'sidepanel', cb: () => void) {
|
|
184
|
+
if (target === 'docsearch') {
|
|
185
|
+
if (sidepanelInstance?.isOpen) {
|
|
186
|
+
skipEventSidepanel = true
|
|
187
|
+
sidepanelInstance.close()
|
|
188
|
+
} else if (!docsearchInstance?.isOpen) {
|
|
189
|
+
if (document.activeElement instanceof HTMLElement) {
|
|
190
|
+
lastFocusedElement = document.activeElement
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} else if (target === 'sidepanel') {
|
|
194
|
+
if (docsearchInstance?.isOpen) {
|
|
195
|
+
skipEventDocsearch = true
|
|
196
|
+
docsearchInstance.close()
|
|
197
|
+
} else if (!sidepanelInstance?.isOpen) {
|
|
198
|
+
if (document.activeElement instanceof HTMLElement) {
|
|
199
|
+
lastFocusedElement = document.activeElement
|
|
200
|
+
}
|
|
201
|
+
}
|
|
105
202
|
}
|
|
203
|
+
setTimeout(cb, 0)
|
|
204
|
+
}
|
|
106
205
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
206
|
+
function onClose(target: 'docsearch' | 'sidepanel') {
|
|
207
|
+
if (target === 'docsearch') {
|
|
208
|
+
if (skipEventDocsearch) {
|
|
209
|
+
skipEventDocsearch = false
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
} else if (target === 'sidepanel') {
|
|
213
|
+
if (skipEventSidepanel) {
|
|
214
|
+
skipEventSidepanel = false
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (lastFocusedElement) {
|
|
219
|
+
lastFocusedElement.focus()
|
|
220
|
+
lastFocusedElement = null
|
|
221
|
+
}
|
|
222
|
+
}
|
|
116
223
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
})
|
|
224
|
+
function loadDocsearch() {
|
|
225
|
+
if (!docsearchLoader) {
|
|
226
|
+
docsearchLoader = import('@docsearch/js')
|
|
227
|
+
}
|
|
228
|
+
return docsearchLoader
|
|
229
|
+
}
|
|
125
230
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
hasError.value = true
|
|
231
|
+
function loadSidepanel() {
|
|
232
|
+
if (!sidepanelLoader) {
|
|
233
|
+
sidepanelLoader = import('@docsearch/sidepanel-js')
|
|
130
234
|
}
|
|
235
|
+
return sidepanelLoader
|
|
131
236
|
}
|
|
132
237
|
|
|
133
238
|
function getRelativePath(url: string) {
|
|
@@ -137,6 +242,6 @@ function getRelativePath(url: string) {
|
|
|
137
242
|
</script>
|
|
138
243
|
|
|
139
244
|
<template>
|
|
140
|
-
|
|
141
|
-
<div
|
|
245
|
+
<div id="vp-docsearch" />
|
|
246
|
+
<div id="vp-docsearch-sidepanel" />
|
|
142
247
|
</template>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {
|
|
8
8
|
computedAsync,
|
|
9
|
-
|
|
9
|
+
watchDebounced,
|
|
10
10
|
onKeyStroke,
|
|
11
11
|
useEventListener,
|
|
12
12
|
useLocalStorage,
|
|
@@ -35,23 +35,7 @@ import { escapeRegExp } from '@vp-support/shared-utils'
|
|
|
35
35
|
import { useData } from '@vp-composables/data'
|
|
36
36
|
import { LRUCache } from '@vp-support/lru'
|
|
37
37
|
import { createSearchTranslate } from '@vp-support/translation'
|
|
38
|
-
|
|
39
|
-
// Modal translations type
|
|
40
|
-
interface ModalTranslations {
|
|
41
|
-
displayDetails?: string
|
|
42
|
-
resetButtonTitle?: string
|
|
43
|
-
backButtonTitle?: string
|
|
44
|
-
noResultsText?: string
|
|
45
|
-
footer?: {
|
|
46
|
-
selectText?: string
|
|
47
|
-
selectKeyAriaLabel?: string
|
|
48
|
-
navigateText?: string
|
|
49
|
-
navigateUpKeyAriaLabel?: string
|
|
50
|
-
navigateDownKeyAriaLabel?: string
|
|
51
|
-
closeText?: string
|
|
52
|
-
closeKeyAriaLabel?: string
|
|
53
|
-
}
|
|
54
|
-
}
|
|
38
|
+
import type { LocalSearchTranslations } from '../../types/local-search'
|
|
55
39
|
|
|
56
40
|
const emit = defineEmits<{
|
|
57
41
|
(e: 'close'): void
|
|
@@ -177,16 +161,6 @@ const disableDetailedView = computed(() => {
|
|
|
177
161
|
)
|
|
178
162
|
})
|
|
179
163
|
|
|
180
|
-
const buttonText = computed(() => {
|
|
181
|
-
const options = theme.value.search?.options ?? theme.value.algolia
|
|
182
|
-
|
|
183
|
-
return (
|
|
184
|
-
options?.locales?.[localeIndex.value]?.translations?.button?.buttonText ||
|
|
185
|
-
options?.translations?.button?.buttonText ||
|
|
186
|
-
'Search'
|
|
187
|
-
)
|
|
188
|
-
})
|
|
189
|
-
|
|
190
164
|
watchEffect(() => {
|
|
191
165
|
if (disableDetailedView.value) {
|
|
192
166
|
showDetailedList.value = false
|
|
@@ -208,7 +182,7 @@ const mark = computedAsync(async () => {
|
|
|
208
182
|
|
|
209
183
|
const cache = new LRUCache<string, Map<string, string>>(16) // 16 files
|
|
210
184
|
|
|
211
|
-
|
|
185
|
+
watchDebounced(
|
|
212
186
|
() => [searchIndex.value, filterText.value, showDetailedList.value] as const,
|
|
213
187
|
async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => {
|
|
214
188
|
if (old?.[0] !== index) {
|
|
@@ -404,7 +378,10 @@ onKeyStroke('Escape', () => {
|
|
|
404
378
|
})
|
|
405
379
|
|
|
406
380
|
// Translations
|
|
407
|
-
const defaultTranslations:
|
|
381
|
+
const defaultTranslations: LocalSearchTranslations = {
|
|
382
|
+
button: {
|
|
383
|
+
buttonText: 'Search'
|
|
384
|
+
},
|
|
408
385
|
modal: {
|
|
409
386
|
displayDetails: 'Display detailed list',
|
|
410
387
|
resetButtonTitle: 'Reset search',
|
|
@@ -424,7 +401,7 @@ const defaultTranslations: { modal: ModalTranslations } = {
|
|
|
424
401
|
|
|
425
402
|
const translate = createSearchTranslate(defaultTranslations)
|
|
426
403
|
|
|
427
|
-
|
|
404
|
+
/* Back */
|
|
428
405
|
|
|
429
406
|
onMounted(() => {
|
|
430
407
|
// Prevents going to previous site
|
|
@@ -437,6 +414,7 @@ useEventListener('popstate', (event) => {
|
|
|
437
414
|
})
|
|
438
415
|
|
|
439
416
|
/** Lock body */
|
|
417
|
+
|
|
440
418
|
const isLocked = useScrollLock(inBrowser ? document.body : null)
|
|
441
419
|
|
|
442
420
|
onMounted(() => {
|
|
@@ -514,7 +492,7 @@ function onMouseMove(e: MouseEvent) {
|
|
|
514
492
|
@submit.prevent=""
|
|
515
493
|
>
|
|
516
494
|
<label
|
|
517
|
-
:title="buttonText"
|
|
495
|
+
:title="translate('button.buttonText')"
|
|
518
496
|
id="localsearch-label"
|
|
519
497
|
for="localsearch-input"
|
|
520
498
|
>
|
|
@@ -543,7 +521,7 @@ function onMouseMove(e: MouseEvent) {
|
|
|
543
521
|
id="localsearch-input"
|
|
544
522
|
enterkeyhint="go"
|
|
545
523
|
maxlength="64"
|
|
546
|
-
:placeholder="buttonText"
|
|
524
|
+
:placeholder="translate('button.buttonText')"
|
|
547
525
|
spellcheck="false"
|
|
548
526
|
type="search"
|
|
549
527
|
/>
|
|
@@ -7,6 +7,7 @@ import VPLink from './VPLink.vue'
|
|
|
7
7
|
|
|
8
8
|
const props = defineProps<{
|
|
9
9
|
item: T
|
|
10
|
+
rel?: string
|
|
10
11
|
}>()
|
|
11
12
|
|
|
12
13
|
const { page } = useData()
|
|
@@ -34,7 +35,7 @@ defineOptions({ inheritAttrs: false })
|
|
|
34
35
|
}"
|
|
35
36
|
:href
|
|
36
37
|
:target="item.target"
|
|
37
|
-
:rel="item.rel"
|
|
38
|
+
:rel="props.rel ?? item.rel"
|
|
38
39
|
:no-icon="item.noIcon"
|
|
39
40
|
>
|
|
40
41
|
<span v-html="item.text"></span>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import { useWindowScroll } from '@vueuse/core'
|
|
3
|
-
import { ref, watchPostEffect } from 'vue'
|
|
4
3
|
import { useLayout } from '@vp-composables/layout'
|
|
5
4
|
import VPNavBarSearch from './VPNavBarSearch.vue'
|
|
6
5
|
import VPNavBarAppearance from './VPNavBarAppearance.vue'
|
|
@@ -21,21 +20,18 @@ defineEmits<{
|
|
|
21
20
|
|
|
22
21
|
const { y } = useWindowScroll()
|
|
23
22
|
const { isHome, hasSidebar } = useLayout()
|
|
24
|
-
|
|
25
|
-
const classes = ref<Record<string, boolean>>({})
|
|
26
|
-
|
|
27
|
-
watchPostEffect(() => {
|
|
28
|
-
classes.value = {
|
|
29
|
-
'has-sidebar': hasSidebar.value,
|
|
30
|
-
'home': isHome.value,
|
|
31
|
-
'top': y.value === 0,
|
|
32
|
-
'screen-open': props.isScreenOpen
|
|
33
|
-
}
|
|
34
|
-
})
|
|
35
23
|
</script>
|
|
36
24
|
|
|
37
25
|
<template>
|
|
38
|
-
<div
|
|
26
|
+
<div
|
|
27
|
+
class="VPNavBar"
|
|
28
|
+
:class="{
|
|
29
|
+
'has-sidebar': hasSidebar,
|
|
30
|
+
'home': isHome,
|
|
31
|
+
'top': y === 0,
|
|
32
|
+
'screen-open': isScreenOpen
|
|
33
|
+
}"
|
|
34
|
+
>
|
|
39
35
|
<div class="wrapper">
|
|
40
36
|
<div class="container">
|
|
41
37
|
<div class="title">
|
|
@@ -55,7 +51,11 @@ watchPostEffect(() => {
|
|
|
55
51
|
<VPNavBarSocialLinks class="social-links" />
|
|
56
52
|
<VPNavBarExtra class="extra" />
|
|
57
53
|
<slot name="nav-bar-content-after" />
|
|
58
|
-
<VPNavBarHamburger
|
|
54
|
+
<VPNavBarHamburger
|
|
55
|
+
class="hamburger"
|
|
56
|
+
:active="isScreenOpen"
|
|
57
|
+
@click="$emit('toggle-screen')"
|
|
58
|
+
/>
|
|
59
59
|
</div>
|
|
60
60
|
</div>
|
|
61
61
|
</div>
|
|
@@ -203,12 +203,6 @@ watchPostEffect(() => {
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
@media (max-width: 767px) {
|
|
207
|
-
.content-body {
|
|
208
|
-
column-gap: 0.5rem;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
206
|
.menu + .translations::before,
|
|
213
207
|
.menu + .appearance::before,
|
|
214
208
|
.menu + .social-links::before,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button type="button" class="VPNavBarAskAiButton">
|
|
3
|
+
<span class="vpi-sparkles" aria-hidden="true"></span>
|
|
4
|
+
</button>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<style scoped>
|
|
8
|
+
.VPNavBarAskAiButton {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
height: var(--vp-nav-height);
|
|
12
|
+
padding: 8px 14px;
|
|
13
|
+
font-size: 20px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@media (min-width: 768px) {
|
|
17
|
+
.VPNavBarAskAiButton {
|
|
18
|
+
height: auto;
|
|
19
|
+
padding: 11.5px;
|
|
20
|
+
transition: color 0.3s ease;
|
|
21
|
+
background-color: var(--vp-c-bg-alt);
|
|
22
|
+
border-radius: 8px;
|
|
23
|
+
font-size: 15px;
|
|
24
|
+
color: var(--vp-c-text-2);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.VPNavBarAskAiButton:hover {
|
|
28
|
+
color: var(--vp-c-brand-1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
</style>
|
|
@@ -34,7 +34,13 @@ const hasExtraContent = computed(
|
|
|
34
34
|
<p class="trans-title">{{ currentLang.label }}</p>
|
|
35
35
|
|
|
36
36
|
<template v-for="locale in localeLinks" :key="locale.link">
|
|
37
|
-
<VPMenuLink
|
|
37
|
+
<VPMenuLink
|
|
38
|
+
:item="locale"
|
|
39
|
+
:lang="locale.lang"
|
|
40
|
+
:hreflang="locale.lang"
|
|
41
|
+
rel="alternate"
|
|
42
|
+
:dir="locale.dir"
|
|
43
|
+
/>
|
|
38
44
|
</template>
|
|
39
45
|
</div>
|
|
40
46
|
|