picocrank 1.21.4 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -8
- package/public/themes/README.md +29 -0
- package/public/themes/catppuccin/theme.css +195 -0
- package/public/themes/dracula/theme.css +181 -0
- package/public/themes/index.json +7 -0
- package/public/themes/waffles/theme.css +143 -0
- package/router.js +20 -0
- package/styles/dark-theme.css +15 -1
- package/styles.css +18 -2
- package/vite.config.js +57 -0
- package/vue/components/CheckGroup.vue +156 -0
- package/vue/components/FormField.vue +14 -2
- package/vue/components/Header.vue +164 -35
- package/vue/components/QuickSearch.vue +325 -348
- package/vue/components/RadioGroup.vue +191 -0
- package/vue/components/Sidebar.vue +226 -262
- package/vue/components/SidebarNavLink.vue +161 -0
- package/vue/composables/useCustomTheme.js +162 -0
- package/vue/composables/useTheme.js +43 -10
- package/vue/data/examplePeople.js +45 -0
- package/vue/views/App.vue +85 -15
- package/vue/views/FormExample.vue +64 -29
- package/vue/views/PageStructure.vue +168 -95
- package/vue/views/TableExample.vue +2 -76
- package/vue/views/ThemePreview.vue +172 -0
- package/vue/views/UserDetails.vue +39 -0
- package/vue/views/Welcome.vue +9 -6
|
@@ -1,47 +1,25 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
>
|
|
12
|
-
<HugeiconsIcon :icon="SearchIcon" width="1em" height="1em" />
|
|
13
|
-
</button>
|
|
14
|
-
|
|
15
|
-
<Teleport to="body" :disabled="!isMobile">
|
|
16
|
-
<div
|
|
17
|
-
v-if="!isMobile || overlayOpen"
|
|
18
|
-
:class="{ 'quick-search-overlay-root': isMobile }"
|
|
2
|
+
<div class="quick-search">
|
|
3
|
+
<button
|
|
4
|
+
type="button"
|
|
5
|
+
class="neutral search-trigger"
|
|
6
|
+
:aria-label="buttonLabel"
|
|
7
|
+
:title="buttonTitle"
|
|
8
|
+
aria-haspopup="dialog"
|
|
9
|
+
:aria-expanded="isOpen"
|
|
10
|
+
@click="open"
|
|
19
11
|
>
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
:aria-label="isMobile ? 'Search' : undefined"
|
|
32
|
-
>
|
|
33
|
-
<div v-if="isMobile" class="quick-search-overlay-header">
|
|
34
|
-
<span class="quick-search-overlay-title">Search</span>
|
|
35
|
-
<button
|
|
36
|
-
type="button"
|
|
37
|
-
class="quick-search-overlay-close neutral"
|
|
38
|
-
aria-label="Close search"
|
|
39
|
-
@click="closeOverlay"
|
|
40
|
-
>
|
|
41
|
-
<HugeiconsIcon :icon="Cancel01Icon" width="1em" height="1em" />
|
|
42
|
-
</button>
|
|
43
|
-
</div>
|
|
44
|
-
|
|
12
|
+
<HugeiconsIcon :icon="SearchIcon" width="1em" height="1em" />
|
|
13
|
+
</button>
|
|
14
|
+
|
|
15
|
+
<dialog
|
|
16
|
+
ref="dialogRef"
|
|
17
|
+
class="quick-search-dialog"
|
|
18
|
+
:aria-busy="hasAsyncSearch && isLoading ? 'true' : undefined"
|
|
19
|
+
@close="onDialogClose"
|
|
20
|
+
@click="onDialogBackdropClick"
|
|
21
|
+
>
|
|
22
|
+
<div class="dialog-body" @click.stop>
|
|
45
23
|
<div class="search-container">
|
|
46
24
|
<input
|
|
47
25
|
ref="searchInput"
|
|
@@ -49,21 +27,41 @@
|
|
|
49
27
|
type="text"
|
|
50
28
|
class="search-input"
|
|
51
29
|
:placeholder="placeholder"
|
|
52
|
-
@input="
|
|
53
|
-
@focus="onFocus"
|
|
54
|
-
@blur="onBlur"
|
|
30
|
+
@input="onSearchInput"
|
|
55
31
|
@keydown="onKeydown"
|
|
56
32
|
/>
|
|
57
|
-
<div class="search-icon">
|
|
58
|
-
<HugeiconsIcon
|
|
33
|
+
<div class="search-icon" aria-hidden="true">
|
|
34
|
+
<HugeiconsIcon
|
|
35
|
+
:icon="hasAsyncSearch && isLoading ? Loading03Icon : SearchIcon"
|
|
36
|
+
width="1em"
|
|
37
|
+
height="1em"
|
|
38
|
+
:class="{ 'is-spinning': hasAsyncSearch && isLoading }"
|
|
39
|
+
/>
|
|
59
40
|
</div>
|
|
60
41
|
</div>
|
|
61
42
|
|
|
62
|
-
<
|
|
43
|
+
<p
|
|
44
|
+
v-if="hasAsyncSearch && isLoading"
|
|
45
|
+
class="search-status subtle"
|
|
46
|
+
aria-live="polite"
|
|
47
|
+
>
|
|
48
|
+
Searching…
|
|
49
|
+
</p>
|
|
50
|
+
<p
|
|
51
|
+
v-else-if="hasAsyncSearch && searchError"
|
|
52
|
+
class="search-status subtle"
|
|
53
|
+
role="alert"
|
|
54
|
+
>
|
|
55
|
+
{{ searchErrorMessage }}
|
|
56
|
+
</p>
|
|
57
|
+
|
|
58
|
+
<div v-if="displayedItems.length > 0" class="search-results" role="listbox">
|
|
63
59
|
<div
|
|
64
|
-
v-for="(item, index) in
|
|
60
|
+
v-for="(item, index) in displayedItems"
|
|
65
61
|
:key="item.id"
|
|
66
62
|
:class="['search-result-item', { active: selectedIndex === index }]"
|
|
63
|
+
role="option"
|
|
64
|
+
:aria-selected="selectedIndex === index"
|
|
67
65
|
@click="selectItem(item)"
|
|
68
66
|
@mouseenter="selectedIndex = index"
|
|
69
67
|
>
|
|
@@ -72,36 +70,56 @@
|
|
|
72
70
|
</div>
|
|
73
71
|
<div class="result-content">
|
|
74
72
|
<div class="result-title" v-html="highlightText(item.title, searchQuery)"></div>
|
|
75
|
-
<div
|
|
73
|
+
<div
|
|
74
|
+
v-if="item.description"
|
|
75
|
+
class="result-description"
|
|
76
|
+
v-html="highlightText(item.description, searchQuery)"
|
|
77
|
+
></div>
|
|
76
78
|
<div v-if="item.category" class="result-category">{{ item.category }}</div>
|
|
77
79
|
</div>
|
|
78
80
|
</div>
|
|
79
81
|
</div>
|
|
80
82
|
|
|
81
|
-
<div
|
|
83
|
+
<div
|
|
84
|
+
v-else-if="searchQuery && !(hasAsyncSearch && isLoading)"
|
|
85
|
+
class="no-results"
|
|
86
|
+
>
|
|
82
87
|
<div class="no-results-content">
|
|
83
88
|
<HugeiconsIcon :icon="SearchRemoveIcon" width="2em" height="2em" />
|
|
84
89
|
<p>No results found for "{{ searchQuery }}"</p>
|
|
85
90
|
</div>
|
|
86
91
|
</div>
|
|
92
|
+
|
|
93
|
+
<p v-else-if="!searchQuery" class="search-hint subtle">
|
|
94
|
+
Type to search. Use ↑ ↓ and Enter to choose.
|
|
95
|
+
</p>
|
|
87
96
|
</div>
|
|
88
|
-
</
|
|
89
|
-
</
|
|
97
|
+
</dialog>
|
|
98
|
+
</div>
|
|
90
99
|
</template>
|
|
91
100
|
|
|
92
101
|
<script setup>
|
|
93
102
|
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
|
94
103
|
import { useRouter } from 'vue-router'
|
|
95
104
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
96
|
-
import { SearchIcon
|
|
97
|
-
import {
|
|
98
|
-
import {
|
|
105
|
+
import { SearchIcon } from '@hugeicons/core-free-icons'
|
|
106
|
+
import { SearchRemoveIcon } from '@hugeicons/core-free-icons'
|
|
107
|
+
import { Loading03Icon } from '@hugeicons/core-free-icons'
|
|
108
|
+
import { ViewIcon } from '@hugeicons/core-free-icons'
|
|
99
109
|
|
|
100
110
|
const props = defineProps({
|
|
101
111
|
placeholder: {
|
|
102
112
|
type: String,
|
|
103
113
|
default: 'Search...'
|
|
104
114
|
},
|
|
115
|
+
buttonLabel: {
|
|
116
|
+
type: String,
|
|
117
|
+
default: 'Search',
|
|
118
|
+
},
|
|
119
|
+
buttonTitle: {
|
|
120
|
+
type: String,
|
|
121
|
+
default: 'Search (Ctrl+K)',
|
|
122
|
+
},
|
|
105
123
|
items: {
|
|
106
124
|
type: Array,
|
|
107
125
|
default: () => []
|
|
@@ -125,24 +143,56 @@ const props = defineProps({
|
|
|
125
143
|
autoImportRoutes: {
|
|
126
144
|
type: Boolean,
|
|
127
145
|
default: true
|
|
128
|
-
}
|
|
146
|
+
},
|
|
147
|
+
/**
|
|
148
|
+
* Optional async search provider.
|
|
149
|
+
* When set, QuickSearch shows loading feedback and cancels in-flight requests.
|
|
150
|
+
* Signature: async (query, { signal }) => Item[]
|
|
151
|
+
*/
|
|
152
|
+
fetchResults: {
|
|
153
|
+
type: Function,
|
|
154
|
+
default: null,
|
|
155
|
+
},
|
|
129
156
|
})
|
|
130
157
|
|
|
131
|
-
const emit = defineEmits([
|
|
158
|
+
const emit = defineEmits([
|
|
159
|
+
'select',
|
|
160
|
+
'search',
|
|
161
|
+
'search-error',
|
|
162
|
+
'focus',
|
|
163
|
+
'blur',
|
|
164
|
+
'open',
|
|
165
|
+
'close',
|
|
166
|
+
])
|
|
132
167
|
|
|
133
168
|
const router = useRouter()
|
|
134
|
-
const
|
|
135
|
-
const overlayOpen = ref(false)
|
|
169
|
+
const dialogRef = ref(null)
|
|
136
170
|
const searchInput = ref(null)
|
|
137
171
|
const searchQuery = ref('')
|
|
138
|
-
const
|
|
172
|
+
const isOpen = ref(false)
|
|
139
173
|
const selectedIndex = ref(-1)
|
|
140
174
|
const items = ref([...props.items])
|
|
175
|
+
const remoteItems = ref([])
|
|
176
|
+
const isLoading = ref(false)
|
|
177
|
+
const searchError = ref(null)
|
|
141
178
|
const debounceTimer = ref(null)
|
|
179
|
+
let abortController = null
|
|
180
|
+
|
|
181
|
+
const hasAsyncSearch = computed(() => typeof props.fetchResults === 'function')
|
|
182
|
+
|
|
183
|
+
const searchErrorMessage = computed(() => {
|
|
184
|
+
if (!searchError.value) {
|
|
185
|
+
return ''
|
|
186
|
+
}
|
|
187
|
+
if (typeof searchError.value === 'string') {
|
|
188
|
+
return searchError.value
|
|
189
|
+
}
|
|
190
|
+
return searchError.value.message || 'Search failed. Try again.'
|
|
191
|
+
})
|
|
142
192
|
|
|
143
193
|
function importRoutesFromRouter() {
|
|
144
194
|
const routeItems = router.getRoutes()
|
|
145
|
-
.filter(route => route.name)
|
|
195
|
+
.filter(route => route.name)
|
|
146
196
|
.map(route => ({
|
|
147
197
|
id: `route-${route.name}`,
|
|
148
198
|
title: route.meta.title || route.name,
|
|
@@ -152,106 +202,133 @@ function importRoutesFromRouter() {
|
|
|
152
202
|
icon: route.meta.icon || ViewIcon,
|
|
153
203
|
type: 'route'
|
|
154
204
|
}))
|
|
155
|
-
|
|
156
|
-
// Add the items to the search
|
|
205
|
+
|
|
157
206
|
routeItems.forEach(item => addItem(item))
|
|
158
207
|
}
|
|
159
208
|
|
|
160
|
-
const
|
|
209
|
+
const localFilteredItems = computed(() => {
|
|
161
210
|
if (!searchQuery.value.trim()) {
|
|
162
211
|
return []
|
|
163
212
|
}
|
|
164
|
-
|
|
213
|
+
|
|
165
214
|
const query = searchQuery.value.toLowerCase()
|
|
166
|
-
|
|
215
|
+
return items.value.filter(item => {
|
|
167
216
|
return props.searchFields.some(field => {
|
|
168
217
|
const value = item[field]
|
|
169
|
-
return value && value.toLowerCase().includes(query)
|
|
218
|
+
return value && value.toString().toLowerCase().includes(query)
|
|
170
219
|
})
|
|
171
220
|
})
|
|
172
|
-
|
|
173
|
-
return filtered.slice(0, props.maxResults)
|
|
174
221
|
})
|
|
175
222
|
|
|
176
|
-
|
|
177
|
-
if (!
|
|
178
|
-
return
|
|
223
|
+
const displayedItems = computed(() => {
|
|
224
|
+
if (!searchQuery.value.trim()) {
|
|
225
|
+
return []
|
|
179
226
|
}
|
|
180
|
-
overlayOpen.value = true
|
|
181
|
-
nextTick(() => {
|
|
182
|
-
searchInput.value?.focus()
|
|
183
|
-
showResults.value = true
|
|
184
|
-
})
|
|
185
|
-
}
|
|
186
227
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
228
|
+
const seen = new Set()
|
|
229
|
+
const merged = []
|
|
230
|
+
|
|
231
|
+
for (const item of [...localFilteredItems.value, ...remoteItems.value]) {
|
|
232
|
+
if (!item || item.id == null || seen.has(item.id)) {
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
seen.add(item.id)
|
|
236
|
+
merged.push(item)
|
|
237
|
+
if (merged.length >= props.maxResults) {
|
|
238
|
+
break
|
|
239
|
+
}
|
|
190
240
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
241
|
+
|
|
242
|
+
return merged
|
|
243
|
+
})
|
|
194
244
|
|
|
195
245
|
function handleGlobalKeydown(event) {
|
|
196
|
-
// Check if Ctrl+K is pressed
|
|
197
246
|
if (event.ctrlKey && event.key === 'k') {
|
|
198
247
|
event.preventDefault()
|
|
199
248
|
|
|
200
|
-
// Don't trigger if user is typing in an input/textarea
|
|
201
249
|
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
|
|
202
250
|
return
|
|
203
251
|
}
|
|
204
252
|
|
|
205
|
-
|
|
253
|
+
open()
|
|
206
254
|
}
|
|
207
255
|
}
|
|
208
256
|
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}, props.debounceMs)
|
|
257
|
+
function cancelSearch() {
|
|
258
|
+
if (abortController) {
|
|
259
|
+
abortController.abort()
|
|
260
|
+
abortController = null
|
|
261
|
+
}
|
|
262
|
+
isLoading.value = false
|
|
216
263
|
}
|
|
217
264
|
|
|
218
|
-
function
|
|
219
|
-
|
|
220
|
-
|
|
265
|
+
function clearRemoteResults() {
|
|
266
|
+
remoteItems.value = []
|
|
267
|
+
searchError.value = null
|
|
221
268
|
}
|
|
222
269
|
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
270
|
+
async function runSearch(query) {
|
|
271
|
+
selectedIndex.value = -1
|
|
272
|
+
|
|
273
|
+
if (!hasAsyncSearch.value) {
|
|
274
|
+
emit('search', query)
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
cancelSearch()
|
|
279
|
+
|
|
280
|
+
if (!query.trim()) {
|
|
281
|
+
clearRemoteResults()
|
|
282
|
+
emit('search', query, { signal: null })
|
|
227
283
|
return
|
|
228
284
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
285
|
+
|
|
286
|
+
abortController = new AbortController()
|
|
287
|
+
const { signal } = abortController
|
|
288
|
+
|
|
289
|
+
isLoading.value = true
|
|
290
|
+
searchError.value = null
|
|
291
|
+
emit('search', query, { signal })
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const results = await props.fetchResults(query, { signal })
|
|
295
|
+
if (signal.aborted) {
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
remoteItems.value = Array.isArray(results) ? results : []
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (error?.name === 'AbortError' || signal.aborted) {
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
searchError.value = error
|
|
304
|
+
remoteItems.value = []
|
|
305
|
+
emit('search-error', error)
|
|
306
|
+
} finally {
|
|
307
|
+
if (!signal.aborted) {
|
|
308
|
+
isLoading.value = false
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function onSearchInput() {
|
|
314
|
+
clearTimeout(debounceTimer.value)
|
|
315
|
+
debounceTimer.value = setTimeout(() => {
|
|
316
|
+
runSearch(searchQuery.value)
|
|
317
|
+
}, props.debounceMs)
|
|
234
318
|
}
|
|
235
319
|
|
|
236
320
|
function onKeydown(event) {
|
|
237
|
-
if (
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
closeOverlay()
|
|
241
|
-
return
|
|
321
|
+
if (displayedItems.value.length === 0) {
|
|
322
|
+
if (event.key === 'Escape') {
|
|
323
|
+
close()
|
|
242
324
|
}
|
|
243
|
-
showResults.value = false
|
|
244
|
-
selectedIndex.value = -1
|
|
245
|
-
searchInput.value?.blur()
|
|
246
325
|
return
|
|
247
326
|
}
|
|
248
327
|
|
|
249
|
-
if (!showResults.value || filteredItems.value.length === 0) return
|
|
250
|
-
|
|
251
328
|
switch (event.key) {
|
|
252
329
|
case 'ArrowDown':
|
|
253
330
|
event.preventDefault()
|
|
254
|
-
selectedIndex.value = Math.min(selectedIndex.value + 1,
|
|
331
|
+
selectedIndex.value = Math.min(selectedIndex.value + 1, displayedItems.value.length - 1)
|
|
255
332
|
break
|
|
256
333
|
case 'ArrowUp':
|
|
257
334
|
event.preventDefault()
|
|
@@ -260,14 +337,17 @@ function onKeydown(event) {
|
|
|
260
337
|
case 'Enter':
|
|
261
338
|
event.preventDefault()
|
|
262
339
|
if (selectedIndex.value >= 0) {
|
|
263
|
-
selectItem(
|
|
340
|
+
selectItem(displayedItems.value[selectedIndex.value])
|
|
264
341
|
}
|
|
265
342
|
break
|
|
343
|
+
case 'Escape':
|
|
344
|
+
close()
|
|
345
|
+
break
|
|
266
346
|
}
|
|
267
347
|
}
|
|
268
348
|
|
|
269
349
|
function selectItem(item) {
|
|
270
|
-
switch(item.type) {
|
|
350
|
+
switch (item.type) {
|
|
271
351
|
case 'route':
|
|
272
352
|
router.push({ path: item.path })
|
|
273
353
|
break
|
|
@@ -277,24 +357,18 @@ function selectItem(item) {
|
|
|
277
357
|
default:
|
|
278
358
|
emit('select', item)
|
|
279
359
|
}
|
|
280
|
-
|
|
281
|
-
searchQuery.value = ''
|
|
282
|
-
showResults.value = false
|
|
283
|
-
selectedIndex.value = -1
|
|
284
360
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
361
|
+
clear()
|
|
362
|
+
close()
|
|
288
363
|
}
|
|
289
364
|
|
|
290
365
|
function highlightText(text, query) {
|
|
291
366
|
if (!query || !text) return text
|
|
292
|
-
|
|
367
|
+
|
|
293
368
|
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi')
|
|
294
369
|
return text.replace(regex, '<mark>$1</mark>')
|
|
295
370
|
}
|
|
296
371
|
|
|
297
|
-
// API Methods
|
|
298
372
|
function addItem(item) {
|
|
299
373
|
const existingIndex = items.value.findIndex(i => i.id === item.id)
|
|
300
374
|
if (existingIndex >= 0) {
|
|
@@ -320,26 +394,62 @@ function setItems(newItems) {
|
|
|
320
394
|
items.value = [...newItems]
|
|
321
395
|
}
|
|
322
396
|
|
|
323
|
-
function
|
|
324
|
-
|
|
325
|
-
|
|
397
|
+
function setRemoteItems(newItems) {
|
|
398
|
+
remoteItems.value = Array.isArray(newItems) ? [...newItems] : []
|
|
399
|
+
isLoading.value = false
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function open() {
|
|
403
|
+
if (!dialogRef.value) {
|
|
326
404
|
return
|
|
327
405
|
}
|
|
406
|
+
|
|
407
|
+
if (!dialogRef.value.open) {
|
|
408
|
+
dialogRef.value.showModal()
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
isOpen.value = true
|
|
412
|
+
emit('open')
|
|
413
|
+
emit('focus')
|
|
414
|
+
|
|
415
|
+
await nextTick()
|
|
328
416
|
searchInput.value?.focus()
|
|
329
417
|
}
|
|
330
418
|
|
|
331
|
-
function
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
|
|
419
|
+
function close() {
|
|
420
|
+
if (dialogRef.value?.open) {
|
|
421
|
+
dialogRef.value.close()
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function onDialogClose() {
|
|
426
|
+
cancelSearch()
|
|
427
|
+
clearTimeout(debounceTimer.value)
|
|
428
|
+
isOpen.value = false
|
|
429
|
+
selectedIndex.value = -1
|
|
430
|
+
emit('close')
|
|
431
|
+
emit('blur')
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function onDialogBackdropClick(event) {
|
|
435
|
+
if (event.target === dialogRef.value) {
|
|
436
|
+
close()
|
|
335
437
|
}
|
|
336
|
-
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function focus() {
|
|
441
|
+
open()
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function blur() {
|
|
445
|
+
close()
|
|
337
446
|
}
|
|
338
447
|
|
|
339
448
|
function clear() {
|
|
340
449
|
searchQuery.value = ''
|
|
341
|
-
showResults.value = false
|
|
342
450
|
selectedIndex.value = -1
|
|
451
|
+
clearRemoteResults()
|
|
452
|
+
cancelSearch()
|
|
343
453
|
}
|
|
344
454
|
|
|
345
455
|
function refreshRoutes() {
|
|
@@ -348,26 +458,11 @@ function refreshRoutes() {
|
|
|
348
458
|
}
|
|
349
459
|
}
|
|
350
460
|
|
|
351
|
-
watch(isMobile, (mobile) => {
|
|
352
|
-
if (!mobile && overlayOpen.value) {
|
|
353
|
-
closeOverlay()
|
|
354
|
-
}
|
|
355
|
-
})
|
|
356
|
-
|
|
357
|
-
watch(overlayOpen, (open) => {
|
|
358
|
-
if (typeof document === 'undefined') {
|
|
359
|
-
return
|
|
360
|
-
}
|
|
361
|
-
document.body.style.overflow = open ? 'hidden' : ''
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
// Global shortcut management
|
|
365
461
|
onMounted(() => {
|
|
366
462
|
if (props.enableGlobalShortcut) {
|
|
367
463
|
document.addEventListener('keydown', handleGlobalKeydown)
|
|
368
464
|
}
|
|
369
|
-
|
|
370
|
-
// Auto-import routes on mount
|
|
465
|
+
|
|
371
466
|
if (props.autoImportRoutes) {
|
|
372
467
|
importRoutesFromRouter()
|
|
373
468
|
}
|
|
@@ -377,12 +472,10 @@ onUnmounted(() => {
|
|
|
377
472
|
if (props.enableGlobalShortcut) {
|
|
378
473
|
document.removeEventListener('keydown', handleGlobalKeydown)
|
|
379
474
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
475
|
+
clearTimeout(debounceTimer.value)
|
|
476
|
+
cancelSearch()
|
|
383
477
|
})
|
|
384
478
|
|
|
385
|
-
// Watch for external items changes
|
|
386
479
|
watch(() => props.items, (newItems) => {
|
|
387
480
|
items.value = [...newItems]
|
|
388
481
|
}, { deep: true })
|
|
@@ -393,89 +486,58 @@ defineExpose({
|
|
|
393
486
|
clearItems,
|
|
394
487
|
getItems,
|
|
395
488
|
setItems,
|
|
489
|
+
setRemoteItems,
|
|
490
|
+
cancelSearch,
|
|
396
491
|
focus,
|
|
397
492
|
blur,
|
|
493
|
+
open,
|
|
494
|
+
close,
|
|
398
495
|
clear,
|
|
399
496
|
refreshRoutes,
|
|
400
|
-
openOverlay,
|
|
401
|
-
closeOverlay,
|
|
402
497
|
searchQuery,
|
|
403
|
-
filteredItems,
|
|
404
|
-
|
|
405
|
-
|
|
498
|
+
filteredItems: displayedItems,
|
|
499
|
+
displayedItems,
|
|
500
|
+
remoteItems,
|
|
501
|
+
isLoading,
|
|
502
|
+
searchError,
|
|
503
|
+
hasAsyncSearch,
|
|
504
|
+
isOpen,
|
|
505
|
+
showResults: isOpen,
|
|
406
506
|
})
|
|
407
507
|
</script>
|
|
408
508
|
|
|
409
509
|
<style scoped>
|
|
410
|
-
.quick-search
|
|
411
|
-
flex-shrink: 0;
|
|
510
|
+
.quick-search {
|
|
412
511
|
display: inline-flex;
|
|
413
512
|
align-items: center;
|
|
414
|
-
|
|
415
|
-
border: 0;
|
|
416
|
-
color: #fff;
|
|
417
|
-
padding: 0.5em;
|
|
418
|
-
cursor: pointer;
|
|
419
|
-
background: transparent;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
.quick-search-trigger:hover {
|
|
423
|
-
background-color: var(--header-hover-background-color);
|
|
424
|
-
color: var(--header-hover-text-color);
|
|
513
|
+
flex-shrink: 0;
|
|
425
514
|
}
|
|
426
515
|
|
|
427
|
-
.
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
z-index: 2000;
|
|
431
|
-
display: flex;
|
|
432
|
-
align-items: flex-start;
|
|
516
|
+
.search-trigger {
|
|
517
|
+
display: inline-flex;
|
|
518
|
+
align-items: center;
|
|
433
519
|
justify-content: center;
|
|
434
|
-
padding: 1rem;
|
|
435
|
-
padding-top: max(1rem, env(safe-area-inset-top));
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
.quick-search-backdrop {
|
|
439
|
-
position: absolute;
|
|
440
|
-
inset: 0;
|
|
441
|
-
background: rgba(0, 0, 0, 0.45);
|
|
442
520
|
}
|
|
443
521
|
|
|
444
|
-
.quick-search {
|
|
445
|
-
|
|
446
|
-
width:
|
|
522
|
+
.quick-search-dialog {
|
|
523
|
+
width: min(32rem, calc(100vw - 2rem));
|
|
524
|
+
max-width: 32rem;
|
|
525
|
+
padding: 0;
|
|
526
|
+
border: 1px solid var(--border-color, #d7d7d7);
|
|
527
|
+
background: var(--standout-bg-color, #fff);
|
|
528
|
+
/* Dialog is rendered under <header>, so do not inherit header's white text */
|
|
529
|
+
color: var(--text-color, #334155);
|
|
447
530
|
}
|
|
448
531
|
|
|
449
|
-
.quick-search
|
|
450
|
-
|
|
451
|
-
z-index: 1;
|
|
452
|
-
width: min(100%, 28rem);
|
|
453
|
-
margin-top: 10vh;
|
|
454
|
-
padding: 0.75rem;
|
|
455
|
-
border-radius: 0.5rem;
|
|
456
|
-
background: #fff;
|
|
457
|
-
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
|
|
532
|
+
.quick-search-dialog::backdrop {
|
|
533
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
458
534
|
}
|
|
459
535
|
|
|
460
|
-
.
|
|
536
|
+
.dialog-body {
|
|
461
537
|
display: flex;
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
margin-bottom: 0.5rem;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
.quick-search-overlay-title {
|
|
469
|
-
font-weight: 600;
|
|
470
|
-
color: #1f2937;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
.quick-search-overlay-close {
|
|
474
|
-
border: 0;
|
|
475
|
-
padding: 0.35em;
|
|
476
|
-
cursor: pointer;
|
|
477
|
-
color: #1f2937;
|
|
478
|
-
background: transparent;
|
|
538
|
+
flex-direction: column;
|
|
539
|
+
gap: 0.75rem;
|
|
540
|
+
padding: 1rem;
|
|
479
541
|
}
|
|
480
542
|
|
|
481
543
|
.search-container {
|
|
@@ -486,28 +548,13 @@ defineExpose({
|
|
|
486
548
|
|
|
487
549
|
.search-input {
|
|
488
550
|
width: 100%;
|
|
489
|
-
padding: 0.
|
|
490
|
-
border:
|
|
491
|
-
border-radius: .4em;
|
|
551
|
+
padding: 0.75em 2.25em 0.75em 0.75em;
|
|
552
|
+
border: 1px solid var(--border-color, #d7d7d7);
|
|
553
|
+
border-radius: 0.4em;
|
|
492
554
|
font-size: 1rem;
|
|
493
555
|
outline: none;
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
color: #fff;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
.quick-search--overlay .search-input {
|
|
500
|
-
background-color: #f3f4f6;
|
|
501
|
-
color: #111827;
|
|
502
|
-
padding: 0.65em 2.25em 0.65em 0.75em;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
.quick-search--overlay .search-input::placeholder {
|
|
506
|
-
color: #9ca3af;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
.search-input::placeholder {
|
|
510
|
-
color: #bbb;
|
|
556
|
+
background-color: #fff;
|
|
557
|
+
color: var(--text-color, #334155);
|
|
511
558
|
}
|
|
512
559
|
|
|
513
560
|
.search-input:focus {
|
|
@@ -520,34 +567,28 @@ defineExpose({
|
|
|
520
567
|
right: 0.75rem;
|
|
521
568
|
color: #6b7280;
|
|
522
569
|
pointer-events: none;
|
|
570
|
+
display: inline-flex;
|
|
523
571
|
}
|
|
524
572
|
|
|
525
|
-
.search-
|
|
526
|
-
|
|
527
|
-
top: 100%;
|
|
528
|
-
left: 0;
|
|
529
|
-
right: 0;
|
|
530
|
-
background: #fff;
|
|
531
|
-
border: 2px solid #e1e5e9;
|
|
532
|
-
border-top: none;
|
|
533
|
-
border-radius: 0 0 8px 8px;
|
|
534
|
-
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
535
|
-
max-height: 300px;
|
|
536
|
-
overflow-y: auto;
|
|
537
|
-
z-index: 1000;
|
|
573
|
+
.search-icon .is-spinning {
|
|
574
|
+
animation: quick-search-spin 0.8s linear infinite;
|
|
538
575
|
}
|
|
539
576
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
577
|
+
@keyframes quick-search-spin {
|
|
578
|
+
to {
|
|
579
|
+
transform: rotate(360deg);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.search-status {
|
|
584
|
+
margin: 0;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.search-results {
|
|
588
|
+
max-height: min(20rem, 50vh);
|
|
589
|
+
overflow-y: auto;
|
|
590
|
+
border: 1px solid var(--border-color, #e1e5e9);
|
|
591
|
+
border-radius: 0.4em;
|
|
551
592
|
}
|
|
552
593
|
|
|
553
594
|
.search-result-item {
|
|
@@ -555,8 +596,7 @@ defineExpose({
|
|
|
555
596
|
align-items: center;
|
|
556
597
|
padding: 0.75rem 1rem;
|
|
557
598
|
cursor: pointer;
|
|
558
|
-
|
|
559
|
-
border-bottom: 1px solid #f3f4f6;
|
|
599
|
+
border-bottom: 1px solid var(--border-color, #f3f4f6);
|
|
560
600
|
}
|
|
561
601
|
|
|
562
602
|
.search-result-item:last-child {
|
|
@@ -566,6 +606,7 @@ defineExpose({
|
|
|
566
606
|
.search-result-item:hover,
|
|
567
607
|
.search-result-item.active {
|
|
568
608
|
background-color: var(--hover-background-color);
|
|
609
|
+
color: var(--hover-text-color);
|
|
569
610
|
}
|
|
570
611
|
|
|
571
612
|
.result-content {
|
|
@@ -575,7 +616,6 @@ defineExpose({
|
|
|
575
616
|
|
|
576
617
|
.result-title {
|
|
577
618
|
font-weight: 500;
|
|
578
|
-
color: #1f2937;
|
|
579
619
|
margin-bottom: 0.25rem;
|
|
580
620
|
}
|
|
581
621
|
|
|
@@ -596,26 +636,14 @@ defineExpose({
|
|
|
596
636
|
margin-right: 0.75rem;
|
|
597
637
|
color: #6b7280;
|
|
598
638
|
flex-shrink: 0;
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
.no-results {
|
|
602
|
-
position: absolute;
|
|
603
|
-
top: 100%;
|
|
604
|
-
left: 0;
|
|
605
|
-
right: 0;
|
|
606
|
-
background: #fff;
|
|
607
|
-
border: 2px solid #e1e5e9;
|
|
608
|
-
border-top: none;
|
|
609
|
-
border-radius: 0 0 8px 8px;
|
|
610
|
-
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
611
|
-
z-index: 1000;
|
|
639
|
+
display: inline-flex;
|
|
612
640
|
}
|
|
613
641
|
|
|
614
642
|
.no-results-content {
|
|
615
643
|
display: flex;
|
|
616
644
|
flex-direction: column;
|
|
617
645
|
align-items: center;
|
|
618
|
-
padding:
|
|
646
|
+
padding: 1.5rem 1rem;
|
|
619
647
|
color: #6b7280;
|
|
620
648
|
text-align: center;
|
|
621
649
|
}
|
|
@@ -625,26 +653,19 @@ defineExpose({
|
|
|
625
653
|
font-size: 0.875rem;
|
|
626
654
|
}
|
|
627
655
|
|
|
656
|
+
.search-hint {
|
|
657
|
+
margin: 0;
|
|
658
|
+
}
|
|
659
|
+
|
|
628
660
|
mark {
|
|
629
|
-
background-color: #fef3c7;
|
|
630
|
-
color:
|
|
661
|
+
background-color: var(--karma-important, #fef3c7);
|
|
662
|
+
color: inherit;
|
|
631
663
|
padding: 0;
|
|
632
664
|
border-radius: 2px;
|
|
633
665
|
}
|
|
634
666
|
|
|
635
|
-
/* Dark theme */
|
|
636
667
|
html[data-theme="dark"] {
|
|
637
|
-
.quick-search
|
|
638
|
-
background: #111827;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
.quick-search-overlay-title,
|
|
642
|
-
.quick-search-overlay-close {
|
|
643
|
-
color: #f9fafb;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
.quick-search--overlay .search-input {
|
|
647
|
-
background-color: #1f2937;
|
|
668
|
+
.quick-search-dialog {
|
|
648
669
|
color: #f9fafb;
|
|
649
670
|
}
|
|
650
671
|
|
|
@@ -654,62 +675,18 @@ html[data-theme="dark"] {
|
|
|
654
675
|
color: #f9fafb;
|
|
655
676
|
}
|
|
656
677
|
|
|
657
|
-
.search-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
.search-
|
|
663
|
-
color: #9ca3af;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
.search-results {
|
|
667
|
-
background: #1f2937;
|
|
668
|
-
border-color: #374151;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
.search-result-item {
|
|
672
|
-
border-bottom-color: #374151;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
.search-result-item:hover,
|
|
676
|
-
.search-result-item.active {
|
|
677
|
-
background-color: #374151;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
.result-title {
|
|
681
|
-
color: #f9fafb;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
.result-description {
|
|
685
|
-
color: #d1d5db;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
.result-category {
|
|
689
|
-
color: #9ca3af;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
.result-icon {
|
|
678
|
+
.search-icon,
|
|
679
|
+
.result-description,
|
|
680
|
+
.result-category,
|
|
681
|
+
.result-icon,
|
|
682
|
+
.no-results-content,
|
|
683
|
+
.search-status {
|
|
693
684
|
color: #9ca3af;
|
|
694
685
|
}
|
|
695
686
|
|
|
696
|
-
.
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
.no-results-content {
|
|
702
|
-
color: #9ca3af;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
.quick-search--overlay .search-results,
|
|
706
|
-
.quick-search--overlay .no-results {
|
|
707
|
-
border-color: #374151;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
mark {
|
|
711
|
-
background-color: #451a03;
|
|
712
|
-
color: #fbbf24;
|
|
687
|
+
.search-input:focus {
|
|
688
|
+
border-color: #60a5fa;
|
|
689
|
+
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
|
|
713
690
|
}
|
|
714
691
|
}
|
|
715
692
|
</style>
|