picocrank 1.21.5 → 1.22.1
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 +139 -33
- package/vue/components/QuickSearch.vue +352 -351
- 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/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,42 @@ 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()
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function escapeHtml(value) {
|
|
366
|
+
return String(value)
|
|
367
|
+
.replace(/&/g, '&')
|
|
368
|
+
.replace(/</g, '<')
|
|
369
|
+
.replace(/>/g, '>')
|
|
370
|
+
.replace(/"/g, '"')
|
|
371
|
+
.replace(/'/g, ''')
|
|
288
372
|
}
|
|
289
373
|
|
|
290
374
|
function highlightText(text, query) {
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
return
|
|
375
|
+
if (text == null || text === '') return ''
|
|
376
|
+
|
|
377
|
+
const source = String(text)
|
|
378
|
+
if (!query) return escapeHtml(source)
|
|
379
|
+
|
|
380
|
+
const regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')
|
|
381
|
+
let result = ''
|
|
382
|
+
let lastIndex = 0
|
|
383
|
+
|
|
384
|
+
for (const match of source.matchAll(regex)) {
|
|
385
|
+
const start = match.index
|
|
386
|
+
const matched = match[0]
|
|
387
|
+
result += escapeHtml(source.slice(lastIndex, start))
|
|
388
|
+
result += `<mark>${escapeHtml(matched)}</mark>`
|
|
389
|
+
lastIndex = start + matched.length
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
result += escapeHtml(source.slice(lastIndex))
|
|
393
|
+
return result
|
|
295
394
|
}
|
|
296
395
|
|
|
297
|
-
// API Methods
|
|
298
396
|
function addItem(item) {
|
|
299
397
|
const existingIndex = items.value.findIndex(i => i.id === item.id)
|
|
300
398
|
if (existingIndex >= 0) {
|
|
@@ -320,26 +418,62 @@ function setItems(newItems) {
|
|
|
320
418
|
items.value = [...newItems]
|
|
321
419
|
}
|
|
322
420
|
|
|
323
|
-
function
|
|
324
|
-
|
|
325
|
-
|
|
421
|
+
function setRemoteItems(newItems) {
|
|
422
|
+
remoteItems.value = Array.isArray(newItems) ? [...newItems] : []
|
|
423
|
+
isLoading.value = false
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function open() {
|
|
427
|
+
if (!dialogRef.value) {
|
|
326
428
|
return
|
|
327
429
|
}
|
|
430
|
+
|
|
431
|
+
if (!dialogRef.value.open) {
|
|
432
|
+
dialogRef.value.showModal()
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
isOpen.value = true
|
|
436
|
+
emit('open')
|
|
437
|
+
emit('focus')
|
|
438
|
+
|
|
439
|
+
await nextTick()
|
|
328
440
|
searchInput.value?.focus()
|
|
329
441
|
}
|
|
330
442
|
|
|
331
|
-
function
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
return
|
|
443
|
+
function close() {
|
|
444
|
+
if (dialogRef.value?.open) {
|
|
445
|
+
dialogRef.value.close()
|
|
335
446
|
}
|
|
336
|
-
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function onDialogClose() {
|
|
450
|
+
cancelSearch()
|
|
451
|
+
clearTimeout(debounceTimer.value)
|
|
452
|
+
isOpen.value = false
|
|
453
|
+
selectedIndex.value = -1
|
|
454
|
+
emit('close')
|
|
455
|
+
emit('blur')
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function onDialogBackdropClick(event) {
|
|
459
|
+
if (event.target === dialogRef.value) {
|
|
460
|
+
close()
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function focus() {
|
|
465
|
+
open()
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function blur() {
|
|
469
|
+
close()
|
|
337
470
|
}
|
|
338
471
|
|
|
339
472
|
function clear() {
|
|
340
473
|
searchQuery.value = ''
|
|
341
|
-
showResults.value = false
|
|
342
474
|
selectedIndex.value = -1
|
|
475
|
+
clearRemoteResults()
|
|
476
|
+
cancelSearch()
|
|
343
477
|
}
|
|
344
478
|
|
|
345
479
|
function refreshRoutes() {
|
|
@@ -348,26 +482,11 @@ function refreshRoutes() {
|
|
|
348
482
|
}
|
|
349
483
|
}
|
|
350
484
|
|
|
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
485
|
onMounted(() => {
|
|
366
486
|
if (props.enableGlobalShortcut) {
|
|
367
487
|
document.addEventListener('keydown', handleGlobalKeydown)
|
|
368
488
|
}
|
|
369
|
-
|
|
370
|
-
// Auto-import routes on mount
|
|
489
|
+
|
|
371
490
|
if (props.autoImportRoutes) {
|
|
372
491
|
importRoutesFromRouter()
|
|
373
492
|
}
|
|
@@ -377,12 +496,10 @@ onUnmounted(() => {
|
|
|
377
496
|
if (props.enableGlobalShortcut) {
|
|
378
497
|
document.removeEventListener('keydown', handleGlobalKeydown)
|
|
379
498
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
499
|
+
clearTimeout(debounceTimer.value)
|
|
500
|
+
cancelSearch()
|
|
383
501
|
})
|
|
384
502
|
|
|
385
|
-
// Watch for external items changes
|
|
386
503
|
watch(() => props.items, (newItems) => {
|
|
387
504
|
items.value = [...newItems]
|
|
388
505
|
}, { deep: true })
|
|
@@ -393,89 +510,58 @@ defineExpose({
|
|
|
393
510
|
clearItems,
|
|
394
511
|
getItems,
|
|
395
512
|
setItems,
|
|
513
|
+
setRemoteItems,
|
|
514
|
+
cancelSearch,
|
|
396
515
|
focus,
|
|
397
516
|
blur,
|
|
517
|
+
open,
|
|
518
|
+
close,
|
|
398
519
|
clear,
|
|
399
520
|
refreshRoutes,
|
|
400
|
-
openOverlay,
|
|
401
|
-
closeOverlay,
|
|
402
521
|
searchQuery,
|
|
403
|
-
filteredItems,
|
|
404
|
-
|
|
405
|
-
|
|
522
|
+
filteredItems: displayedItems,
|
|
523
|
+
displayedItems,
|
|
524
|
+
remoteItems,
|
|
525
|
+
isLoading,
|
|
526
|
+
searchError,
|
|
527
|
+
hasAsyncSearch,
|
|
528
|
+
isOpen,
|
|
529
|
+
showResults: isOpen,
|
|
406
530
|
})
|
|
407
531
|
</script>
|
|
408
532
|
|
|
409
533
|
<style scoped>
|
|
410
|
-
.quick-search
|
|
411
|
-
flex-shrink: 0;
|
|
534
|
+
.quick-search {
|
|
412
535
|
display: inline-flex;
|
|
413
536
|
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);
|
|
537
|
+
flex-shrink: 0;
|
|
425
538
|
}
|
|
426
539
|
|
|
427
|
-
.
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
z-index: 2000;
|
|
431
|
-
display: flex;
|
|
432
|
-
align-items: flex-start;
|
|
540
|
+
.search-trigger {
|
|
541
|
+
display: inline-flex;
|
|
542
|
+
align-items: center;
|
|
433
543
|
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
544
|
}
|
|
443
545
|
|
|
444
|
-
.quick-search {
|
|
445
|
-
|
|
446
|
-
width:
|
|
546
|
+
.quick-search-dialog {
|
|
547
|
+
width: min(32rem, calc(100vw - 2rem));
|
|
548
|
+
max-width: 32rem;
|
|
549
|
+
padding: 0;
|
|
550
|
+
border: 1px solid var(--border-color, #d7d7d7);
|
|
551
|
+
background: var(--standout-bg-color, #fff);
|
|
552
|
+
/* Dialog is rendered under <header>, so do not inherit header's white text */
|
|
553
|
+
color: var(--text-color, #334155);
|
|
447
554
|
}
|
|
448
555
|
|
|
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);
|
|
556
|
+
.quick-search-dialog::backdrop {
|
|
557
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
458
558
|
}
|
|
459
559
|
|
|
460
|
-
.
|
|
560
|
+
.dialog-body {
|
|
461
561
|
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;
|
|
562
|
+
flex-direction: column;
|
|
563
|
+
gap: 0.75rem;
|
|
564
|
+
padding: 1rem;
|
|
479
565
|
}
|
|
480
566
|
|
|
481
567
|
.search-container {
|
|
@@ -486,28 +572,13 @@ defineExpose({
|
|
|
486
572
|
|
|
487
573
|
.search-input {
|
|
488
574
|
width: 100%;
|
|
489
|
-
padding: 0.
|
|
490
|
-
border:
|
|
491
|
-
border-radius: .4em;
|
|
575
|
+
padding: 0.75em 2.25em 0.75em 0.75em;
|
|
576
|
+
border: 1px solid var(--border-color, #d7d7d7);
|
|
577
|
+
border-radius: 0.4em;
|
|
492
578
|
font-size: 1rem;
|
|
493
579
|
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;
|
|
580
|
+
background-color: #fff;
|
|
581
|
+
color: var(--text-color, #334155);
|
|
511
582
|
}
|
|
512
583
|
|
|
513
584
|
.search-input:focus {
|
|
@@ -520,34 +591,28 @@ defineExpose({
|
|
|
520
591
|
right: 0.75rem;
|
|
521
592
|
color: #6b7280;
|
|
522
593
|
pointer-events: none;
|
|
594
|
+
display: inline-flex;
|
|
523
595
|
}
|
|
524
596
|
|
|
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;
|
|
597
|
+
.search-icon .is-spinning {
|
|
598
|
+
animation: quick-search-spin 0.8s linear infinite;
|
|
538
599
|
}
|
|
539
600
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
601
|
+
@keyframes quick-search-spin {
|
|
602
|
+
to {
|
|
603
|
+
transform: rotate(360deg);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.search-status {
|
|
608
|
+
margin: 0;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.search-results {
|
|
612
|
+
max-height: min(20rem, 50vh);
|
|
613
|
+
overflow-y: auto;
|
|
614
|
+
border: 1px solid var(--border-color, #e1e5e9);
|
|
615
|
+
border-radius: 0.4em;
|
|
551
616
|
}
|
|
552
617
|
|
|
553
618
|
.search-result-item {
|
|
@@ -555,8 +620,7 @@ defineExpose({
|
|
|
555
620
|
align-items: center;
|
|
556
621
|
padding: 0.75rem 1rem;
|
|
557
622
|
cursor: pointer;
|
|
558
|
-
|
|
559
|
-
border-bottom: 1px solid #f3f4f6;
|
|
623
|
+
border-bottom: 1px solid var(--border-color, #f3f4f6);
|
|
560
624
|
}
|
|
561
625
|
|
|
562
626
|
.search-result-item:last-child {
|
|
@@ -566,6 +630,7 @@ defineExpose({
|
|
|
566
630
|
.search-result-item:hover,
|
|
567
631
|
.search-result-item.active {
|
|
568
632
|
background-color: var(--hover-background-color);
|
|
633
|
+
color: var(--hover-text-color);
|
|
569
634
|
}
|
|
570
635
|
|
|
571
636
|
.result-content {
|
|
@@ -575,7 +640,6 @@ defineExpose({
|
|
|
575
640
|
|
|
576
641
|
.result-title {
|
|
577
642
|
font-weight: 500;
|
|
578
|
-
color: #1f2937;
|
|
579
643
|
margin-bottom: 0.25rem;
|
|
580
644
|
}
|
|
581
645
|
|
|
@@ -596,26 +660,14 @@ defineExpose({
|
|
|
596
660
|
margin-right: 0.75rem;
|
|
597
661
|
color: #6b7280;
|
|
598
662
|
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;
|
|
663
|
+
display: inline-flex;
|
|
612
664
|
}
|
|
613
665
|
|
|
614
666
|
.no-results-content {
|
|
615
667
|
display: flex;
|
|
616
668
|
flex-direction: column;
|
|
617
669
|
align-items: center;
|
|
618
|
-
padding:
|
|
670
|
+
padding: 1.5rem 1rem;
|
|
619
671
|
color: #6b7280;
|
|
620
672
|
text-align: center;
|
|
621
673
|
}
|
|
@@ -625,26 +677,19 @@ defineExpose({
|
|
|
625
677
|
font-size: 0.875rem;
|
|
626
678
|
}
|
|
627
679
|
|
|
680
|
+
.search-hint {
|
|
681
|
+
margin: 0;
|
|
682
|
+
}
|
|
683
|
+
|
|
628
684
|
mark {
|
|
629
|
-
background-color: #fef3c7;
|
|
630
|
-
color:
|
|
685
|
+
background-color: var(--karma-important, #fef3c7);
|
|
686
|
+
color: inherit;
|
|
631
687
|
padding: 0;
|
|
632
688
|
border-radius: 2px;
|
|
633
689
|
}
|
|
634
690
|
|
|
635
|
-
/* Dark theme */
|
|
636
691
|
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;
|
|
692
|
+
.quick-search-dialog {
|
|
648
693
|
color: #f9fafb;
|
|
649
694
|
}
|
|
650
695
|
|
|
@@ -654,62 +699,18 @@ html[data-theme="dark"] {
|
|
|
654
699
|
color: #f9fafb;
|
|
655
700
|
}
|
|
656
701
|
|
|
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 {
|
|
702
|
+
.search-icon,
|
|
703
|
+
.result-description,
|
|
704
|
+
.result-category,
|
|
705
|
+
.result-icon,
|
|
706
|
+
.no-results-content,
|
|
707
|
+
.search-status {
|
|
693
708
|
color: #9ca3af;
|
|
694
709
|
}
|
|
695
710
|
|
|
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;
|
|
711
|
+
.search-input:focus {
|
|
712
|
+
border-color: #60a5fa;
|
|
713
|
+
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
|
|
713
714
|
}
|
|
714
715
|
}
|
|
715
716
|
</style>
|