@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.
@@ -0,0 +1,120 @@
1
+ @import '@docsearch/css/dist/style.css';
2
+ @import '@docsearch/css/dist/sidepanel.css';
3
+
4
+ #vp-docsearch,
5
+ #vp-docsearch-sidepanel,
6
+ .DocSearch-SidepanelButton {
7
+ display: none;
8
+ }
9
+
10
+ :root:root {
11
+ --docsearch-actions-height: auto;
12
+ --docsearch-actions-width: auto;
13
+ --docsearch-background-color: var(--vp-c-bg-soft);
14
+ --docsearch-container-background: var(--vp-backdrop-bg-color);
15
+ --docsearch-dropdown-menu-background: var(--vp-c-bg-elv);
16
+ --docsearch-dropdown-menu-item-hover-background: var(--vp-c-default-soft);
17
+ --docsearch-focus-color: var(--vp-c-brand-1);
18
+ --docsearch-footer-background: var(--vp-c-bg-alt);
19
+ --docsearch-highlight-color: var(--vp-c-brand-1);
20
+ --docsearch-hit-background: var(--vp-c-bg);
21
+ --docsearch-hit-color: var(--vp-c-text-1);
22
+ --docsearch-hit-highlight-color: var(--vp-c-brand-soft);
23
+ --docsearch-icon-color: var(--vp-c-text-2);
24
+ --docsearch-key-background: var(--vp-code-bg);
25
+ --docsearch-modal-background: var(--vp-c-bg-soft);
26
+ --docsearch-muted-color: var(--vp-c-text-2);
27
+ --docsearch-primary-color: var(--vp-c-brand-1);
28
+ --docsearch-searchbox-background: var(--vp-c-bg-alt);
29
+ --docsearch-searchbox-focus-background: transparent;
30
+ --docsearch-secondary-text-color: var(--vp-c-text-2);
31
+ --docsearch-sidepanel-accent-muted: var(--vp-c-text-3);
32
+ --docsearch-sidepanel-text-base: var(--vp-c-text-1);
33
+ --docsearch-soft-muted-color: var(--vp-c-default-soft);
34
+ --docsearch-soft-primary-color: var(--vp-c-brand-soft);
35
+ --docsearch-subtle-color: var(--vp-c-divider);
36
+ --docsearch-success-color: var(--vp-c-brand-soft);
37
+ --docsearch-text-color: var(--vp-c-text-1);
38
+ }
39
+
40
+ :root.dark {
41
+ --docsearch-modal-shadow: none;
42
+ }
43
+
44
+ .DocSearch-AskAiScreen-RelatedSources-Item-Link {
45
+ padding: 8px 12px 8px 10px;
46
+ }
47
+
48
+ .DocSearch-AskAiScreen-RelatedSources-Item-Link svg {
49
+ width: 16px;
50
+ height: 16px;
51
+ }
52
+
53
+ .DocSearch-AskAiScreen-RelatedSources-Title {
54
+ padding-bottom: 0;
55
+ font-size: 12px;
56
+ }
57
+
58
+ .DocSearch-Clear {
59
+ padding-right: 6px;
60
+ }
61
+
62
+ .DocSearch-Commands-Key {
63
+ padding: 4px;
64
+ border: 1px solid var(--docsearch-subtle-color);
65
+ border-radius: 4px;
66
+ }
67
+
68
+ .DocSearch-Hit a:focus-visible {
69
+ outline: 2px solid var(--docsearch-focus-color);
70
+ }
71
+
72
+ .DocSearch-Logo [class^='cls-'] {
73
+ fill: currentColor;
74
+ }
75
+
76
+ .DocSearch-Markdown-Content code {
77
+ padding: 0.2em 0.4em;
78
+ }
79
+
80
+ .DocSearch-Menu-content {
81
+ margin-top: -4px;
82
+ padding: 6px;
83
+ border: 1px solid var(--vp-c-divider);
84
+ border-radius: 6px;
85
+ box-shadow: var(--vp-shadow-2);
86
+ }
87
+
88
+ .DocSearch-Menu-item {
89
+ border-radius: 4px;
90
+ }
91
+
92
+ .DocSearch-SearchBar + .DocSearch-Footer {
93
+ border-top-color: transparent;
94
+ }
95
+
96
+ .DocSearch-Sidepanel-Prompt--form {
97
+ border-color: var(--docsearch-subtle-color);
98
+ transition: border-color 0.2s;
99
+ }
100
+
101
+ .DocSearch-Sidepanel-Prompt--submit {
102
+ background-color: var(--docsearch-soft-primary-color);
103
+ color: var(--docsearch-primary-color);
104
+ }
105
+
106
+ .DocSearch-Sidepanel-Prompt--submit:hover {
107
+ background-color: var(--vp-button-brand-hover-bg);
108
+ color: var(--vp-button-brand-text);
109
+ }
110
+
111
+ .DocSearch-Sidepanel-Prompt--submit:disabled,
112
+ .DocSearch-Sidepanel-Prompt--submit[aria-disabled='true'] {
113
+ background-color: var(--docsearch-soft-muted-color);
114
+ color: var(--docsearch-muted-color);
115
+ }
116
+
117
+ .DocSearch-Title {
118
+ font-size: revert;
119
+ line-height: revert;
120
+ }
@@ -74,7 +74,10 @@
74
74
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2c-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z'/%3E%3C/svg%3E");
75
75
  }
76
76
  .vpi-search {
77
- --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.6'%3E%3Cpath d='m21 21l-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8' stroke-width='1.4'/%3E%3C/g%3E%3C/svg%3E");
77
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='m21 21l-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3C/g%3E%3C/svg%3E");
78
+ }
79
+ .vpi-sparkles {
80
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.8'%3E%3Cpath d='M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594zM20 2v4m2-2h-4'/%3E%3Ccircle cx='4' cy='20' r='2'/%3E%3C/g%3E%3C/svg%3E");
78
81
  }
79
82
  .vpi-layout-list {
80
83
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Crect width='7' height='7' x='3' y='3' rx='1'/%3E%3Crect width='7' height='7' x='3' y='14' rx='1'/%3E%3Cpath d='M14 4h7m-7 5h7m-7 6h7m-7 5h7'/%3E%3C/g%3E%3C/svg%3E");
@@ -396,6 +396,9 @@
396
396
  :lang(zh) {
397
397
  --vp-code-copy-copied-text-content: '已复制';
398
398
  }
399
+ :lang(ja) {
400
+ --vp-code-copy-copied-text-content: 'コピー完了';
401
+ }
399
402
 
400
403
  /**
401
404
  * Component: Button
@@ -537,7 +540,8 @@
537
540
  --vp-backdrop-bg-color: rgba(255, 255, 255, 0.7);
538
541
  }
539
542
 
540
- .dark {
543
+ .dark,
544
+ [data-theme="dark"] {
541
545
  --vp-backdrop-bg-color: rgba(22, 23, 29, 0.8);
542
546
  }
543
547
 
@@ -0,0 +1,253 @@
1
+ import type { DefaultTheme } from 'vitepress/theme'
2
+ import type { DocSearchAskAi } from '../../types/docsearch'
3
+ import { isObject } from '@vueuse/shared'
4
+
5
+ export type FacetFilter = string | string[] | FacetFilter[]
6
+
7
+ export interface ValidatedCredentials {
8
+ valid: boolean
9
+ appId?: string
10
+ apiKey?: string
11
+ indexName?: string
12
+ }
13
+
14
+ export type DocSearchMode = 'auto' | 'sidePanel' | 'hybrid' | 'modal'
15
+
16
+ export interface ResolvedMode {
17
+ mode: DocSearchMode
18
+ showKeywordSearch: boolean
19
+ useSidePanel: boolean
20
+ }
21
+
22
+ /**
23
+ * Resolves the effective mode based on config and available features.
24
+ *
25
+ * - 'auto': infer hybrid vs sidePanel-only from provided config
26
+ * - 'sidePanel': force sidePanel-only even if keyword search is configured
27
+ * - 'hybrid': force hybrid (error if keyword search is not configured)
28
+ * - 'modal': force modal even if sidePanel is configured
29
+ */
30
+ export function resolveMode(
31
+ options: Pick<
32
+ DefaultTheme.AlgoliaSearchOptions,
33
+ 'appId' | 'apiKey' | 'indexName' | 'askAi' | 'mode'
34
+ >
35
+ ): ResolvedMode {
36
+ const mode = options.mode ?? 'auto'
37
+ const hasKeyword = hasKeywordSearch(options)
38
+ const askAi = options.askAi
39
+ const hasSidePanelConfig = Boolean(
40
+ askAi && typeof askAi === 'object' && askAi.sidePanel
41
+ )
42
+
43
+ switch (mode) {
44
+ case 'sidePanel':
45
+ // Force sidePanel-only - hide keyword search
46
+ return {
47
+ mode,
48
+ showKeywordSearch: false,
49
+ useSidePanel: true
50
+ }
51
+
52
+ case 'hybrid':
53
+ // Force hybrid - keyword search must be configured
54
+ if (!hasKeyword) {
55
+ console.error(
56
+ '[vitepress] mode: "hybrid" requires keyword search credentials (appId, apiKey, indexName).'
57
+ )
58
+ }
59
+ return {
60
+ mode,
61
+ showKeywordSearch: hasKeyword,
62
+ useSidePanel: true
63
+ }
64
+
65
+ case 'modal':
66
+ // Force modal - don't use sidepanel for askai, even if configured
67
+ return {
68
+ mode,
69
+ showKeywordSearch: hasKeyword,
70
+ useSidePanel: false
71
+ }
72
+
73
+ case 'auto':
74
+ default:
75
+ // Auto-detect based on config
76
+ return {
77
+ mode: 'auto',
78
+ showKeywordSearch: hasKeyword,
79
+ useSidePanel: hasSidePanelConfig
80
+ }
81
+ }
82
+ }
83
+
84
+ export function hasKeywordSearch(
85
+ options: Pick<
86
+ DefaultTheme.AlgoliaSearchOptions,
87
+ 'appId' | 'apiKey' | 'indexName'
88
+ >
89
+ ): boolean {
90
+ return Boolean(options.appId && options.apiKey && options.indexName)
91
+ }
92
+
93
+ export function hasAskAi(
94
+ askAi: DefaultTheme.AlgoliaSearchOptions['askAi']
95
+ ): boolean {
96
+ if (!askAi) return false
97
+ if (typeof askAi === 'string') return askAi.length > 0
98
+ return Boolean(askAi.assistantId)
99
+ }
100
+
101
+ /**
102
+ * Removes existing `lang:` filters and appends `lang:${lang}`.
103
+ * Handles both flat arrays and nested arrays (for OR conditions).
104
+ */
105
+ export function mergeLangFacetFilters(
106
+ rawFacetFilters: FacetFilter | FacetFilter[] | undefined,
107
+ lang: string
108
+ ): FacetFilter[] {
109
+ const input = Array.isArray(rawFacetFilters)
110
+ ? rawFacetFilters
111
+ : rawFacetFilters
112
+ ? [rawFacetFilters]
113
+ : []
114
+
115
+ const filtered = input
116
+ .map((filter) => {
117
+ if (Array.isArray(filter)) {
118
+ // Handle nested arrays (OR conditions)
119
+ return filter.filter(
120
+ (f) => typeof f === 'string' && !f.startsWith('lang:')
121
+ )
122
+ }
123
+ return filter
124
+ })
125
+ .filter((filter) => {
126
+ if (typeof filter === 'string') {
127
+ return !filter.startsWith('lang:')
128
+ }
129
+ // Keep nested arrays with remaining filters
130
+ return Array.isArray(filter) && filter.length > 0
131
+ })
132
+
133
+ return [...filtered, `lang:${lang}`]
134
+ }
135
+
136
+ /**
137
+ * Validates that required Algolia credentials are present.
138
+ */
139
+ export function validateCredentials(
140
+ options: Pick<
141
+ DefaultTheme.AlgoliaSearchOptions,
142
+ 'appId' | 'apiKey' | 'indexName'
143
+ >
144
+ ): ValidatedCredentials {
145
+ const appId = options.appId
146
+ const apiKey = options.apiKey
147
+ const indexName = options.indexName
148
+
149
+ return {
150
+ valid: Boolean(appId && apiKey && indexName),
151
+ appId,
152
+ apiKey,
153
+ indexName
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Builds Ask AI configuration from various input formats.
159
+ */
160
+ export function buildAskAiConfig(
161
+ askAiProp: NonNullable<DefaultTheme.AlgoliaSearchOptions['askAi']>,
162
+ options: DefaultTheme.AlgoliaSearchOptions,
163
+ lang: string
164
+ ): DocSearchAskAi {
165
+ const isAskAiString = typeof askAiProp === 'string'
166
+
167
+ const askAiSearchParameters =
168
+ !isAskAiString && askAiProp.searchParameters
169
+ ? { ...askAiProp.searchParameters }
170
+ : undefined
171
+
172
+ // If Ask AI defines its own facetFilters, merge lang filtering into those.
173
+ // Otherwise, reuse the keyword search facetFilters so Ask AI follows the
174
+ // same language filtering behavior by default.
175
+ const askAiFacetFiltersSource =
176
+ askAiSearchParameters?.facetFilters ??
177
+ options.searchParameters?.facetFilters
178
+ const askAiFacetFilters = mergeLangFacetFilters(
179
+ askAiFacetFiltersSource as FacetFilter | FacetFilter[] | undefined,
180
+ lang
181
+ )
182
+
183
+ const mergedAskAiSearchParameters = {
184
+ ...askAiSearchParameters,
185
+ facetFilters: askAiFacetFilters.length ? askAiFacetFilters : undefined
186
+ }
187
+
188
+ const result: Record<string, any> = {
189
+ ...(isAskAiString ? {} : askAiProp),
190
+ indexName: isAskAiString ? options.indexName : askAiProp.indexName,
191
+ apiKey: isAskAiString ? options.apiKey : askAiProp.apiKey,
192
+ appId: isAskAiString ? options.appId : askAiProp.appId,
193
+ assistantId: isAskAiString ? askAiProp : askAiProp.assistantId
194
+ }
195
+
196
+ // Keep `searchParameters` undefined unless it has at least one key.
197
+ if (Object.values(mergedAskAiSearchParameters).some((v) => v != null)) {
198
+ result.searchParameters = mergedAskAiSearchParameters
199
+ }
200
+
201
+ return result
202
+ }
203
+
204
+ /**
205
+ * Resolves Algolia search options for the given language,
206
+ * merging in locale-specific overrides and language facet filters.
207
+ */
208
+ export function resolveOptionsForLanguage(
209
+ options: DefaultTheme.AlgoliaSearchOptions,
210
+ localeIndex: string,
211
+ lang: string
212
+ ): DefaultTheme.AlgoliaSearchOptions {
213
+ options = deepMerge(options, options.locales?.[localeIndex] || {})
214
+
215
+ const facetFilters = mergeLangFacetFilters(
216
+ options.searchParameters?.facetFilters,
217
+ lang
218
+ )
219
+ const askAi = options.askAi
220
+ ? buildAskAiConfig(options.askAi, options, lang)
221
+ : undefined
222
+
223
+ return {
224
+ ...options,
225
+ searchParameters: { ...options.searchParameters, facetFilters },
226
+ askAi
227
+ }
228
+ }
229
+
230
+ function deepMerge<T>(target: T, source: Partial<T>): T {
231
+ const result = { ...target } as any
232
+
233
+ for (const key in source) {
234
+ const value = source[key]
235
+ if (value === undefined) continue
236
+
237
+ // special case: replace entirely
238
+ if (key === 'searchParameters') {
239
+ result[key] = value
240
+ continue
241
+ }
242
+
243
+ // deep-merge only plain objects; arrays are replaced entirely
244
+ if (isObject(value) && isObject(result[key])) {
245
+ result[key] = deepMerge(result[key], value)
246
+ } else {
247
+ result[key] = value
248
+ }
249
+ }
250
+
251
+ delete result.locales
252
+ return result
253
+ }
@@ -0,0 +1,14 @@
1
+ import { type ComputedRef, computed } from 'vue'
2
+
3
+ export function smartComputed<T>(
4
+ getter: () => T,
5
+ comparator = (oldValue: T, newValue: T) =>
6
+ JSON.stringify(oldValue) === JSON.stringify(newValue)
7
+ ): ComputedRef<T> {
8
+ return computed((oldValue) => {
9
+ const newValue = getter()
10
+ return oldValue === undefined || !comparator(oldValue, newValue)
11
+ ? newValue
12
+ : oldValue
13
+ })
14
+ }
@@ -1,31 +1,63 @@
1
- /**
2
- * Type declarations for @docsearch packages.
3
- *
4
- * These are optional peer dependencies that may or may not be installed.
5
- * The declarations ensure TypeScript doesn't error on dynamic imports.
6
- */
1
+ import { type DocSearchProps as DocSearchPropsJS } from '@docsearch/js'
2
+ import { type SidepanelProps as SidepanelPropsBase } from '@docsearch/sidepanel-js'
7
3
 
8
- declare module '@docsearch/css' {
9
- // CSS module - no exports, just side effects
10
- const css: void
11
- export default css
4
+ export type DocSearchProps = Partial<
5
+ Pick<
6
+ DocSearchPropsJS,
7
+ | 'appId'
8
+ | 'apiKey'
9
+ | 'placeholder'
10
+ | 'maxResultsPerGroup'
11
+ | 'disableUserPersonalization'
12
+ | 'initialQuery'
13
+ | 'translations'
14
+ | 'recentSearchesLimit'
15
+ | 'recentSearchesWithFavoritesLimit'
16
+ >
17
+ > & {
18
+ /**
19
+ * Name of the algolia index to query.
20
+ */
21
+ indexName?: string
22
+ /**
23
+ * Additional algolia search parameters to merge into each query.
24
+ */
25
+ searchParameters?: DocSearchPropsJS['searchParameters']
26
+ /**
27
+ * Insights client integration options to send analytics events.
28
+ */
29
+ insights?: boolean
30
+ /**
31
+ * Configuration or assistant id to enable ask ai mode. Pass a string assistant id or a full config object.
32
+ */
33
+ askAi?: DocSearchAskAi | string
34
+ /**
35
+ * Ask AI side panel integration mode.
36
+ *
37
+ * - 'auto': infer hybrid vs sidePanel-only from provided config
38
+ * - 'sidePanel': force sidePanel-only even if keyword search is configured
39
+ * - 'hybrid': force hybrid (error if keyword search is not configured)
40
+ * - 'modal': force modal even if sidePanel is configured (ask ai in modal stays in modal)
41
+ *
42
+ * @default 'auto'
43
+ */
44
+ mode?: 'auto' | 'sidePanel' | 'hybrid' | 'modal'
12
45
  }
13
46
 
14
- declare module '@docsearch/js' {
15
- interface DocSearchProps {
16
- appId: string
17
- apiKey: string
18
- indexName: string
19
- container: string | HTMLElement
20
- placeholder?: string
21
- searchParameters?: Record<string, any>
22
- transformItems?: (items: any[]) => any[]
23
- navigator?: {
24
- navigate?: (params: { itemUrl: string }) => void
25
- }
26
- [key: string]: any
27
- }
47
+ export type DocSearchAskAi = Partial<
48
+ Exclude<DocSearchPropsJS['askAi'], string | undefined>
49
+ > & {
50
+ /**
51
+ * Ask AI side panel configuration.
52
+ */
53
+ sidePanel?: boolean | SidepanelProps
54
+ }
28
55
 
29
- function docsearch(props: DocSearchProps): void
30
- export default docsearch
56
+ export type SidepanelProps = Partial<
57
+ Pick<SidepanelPropsBase, 'button' | 'keyboardShortcuts'>
58
+ > & {
59
+ /**
60
+ * Props specific to the Sidepanel panel.
61
+ */
62
+ panel?: Omit<NonNullable<SidepanelPropsBase['panel']>, 'portalContainer'>
31
63
  }
@@ -1,38 +1,33 @@
1
- /**
2
- * Type definitions for local search functionality
3
- */
1
+ export interface LocalSearchTranslations {
2
+ button?: ButtonTranslations
3
+ modal?: ModalTranslations
4
+ }
5
+
6
+ export interface ButtonTranslations {
7
+ buttonText?: string
8
+ buttonAriaLabel?: string
9
+ }
4
10
 
5
11
  export interface ModalTranslations {
6
- modal?: {
7
- displayDetails?: string
8
- resetButtonTitle?: string
9
- backButtonTitle?: string
10
- noResultsText?: string
11
- footer?: {
12
- selectText?: string
13
- selectKeyAriaLabel?: string
14
- navigateText?: string
15
- navigateUpKeyAriaLabel?: string
16
- navigateDownKeyAriaLabel?: string
17
- closeText?: string
18
- closeKeyAriaLabel?: string
19
- }
20
- }
12
+ displayDetails?: string
13
+ resetButtonTitle?: string
14
+ backButtonTitle?: string
15
+ noResultsText?: string
16
+ footer?: FooterTranslations
21
17
  }
22
18
 
23
- export interface ButtonTranslations {
24
- button?: {
25
- buttonText?: string
26
- buttonAriaLabel?: string
27
- }
19
+ export interface FooterTranslations {
20
+ selectText?: string
21
+ selectKeyAriaLabel?: string
22
+ navigateText?: string
23
+ navigateUpKeyAriaLabel?: string
24
+ navigateDownKeyAriaLabel?: string
25
+ closeText?: string
26
+ closeKeyAriaLabel?: string
28
27
  }
29
28
 
30
- /**
31
- * Virtual module declaration for @localSearchIndex
32
- * This module is dynamically generated by VitePress's localSearchPlugin
33
- * during downstream project builds.
34
- */
35
- declare module '@localSearchIndex' {
36
- const data: Record<string, () => Promise<{ default: string }>>
37
- export default data
29
+ export interface PageSplitSection {
30
+ anchor?: string
31
+ titles: string[]
32
+ text: string
38
33
  }