picocrank 1.3.0 → 1.3.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 +1 -1
- package/router.js +32 -7
- package/vue/components/Header.vue +2 -0
- package/vue/components/QuickSearch.vue +512 -0
- package/vue/components/Sidebar.vue +67 -76
- package/vue/views/Admin.vue +81 -0
- package/vue/views/App.vue +49 -26
- /package/vue/{examples → views}/TableExample.vue +0 -0
- /package/vue/{examples → views}/ViewItem.vue +0 -0
package/package.json
CHANGED
package/router.js
CHANGED
|
@@ -1,23 +1,48 @@
|
|
|
1
1
|
import { createRouter, createWebHistory } from 'vue-router';
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { HomeIcon } from '@hugeicons/core-free-icons';
|
|
4
|
+
import { TableIcon } from '@hugeicons/core-free-icons';
|
|
5
|
+
import { ViewIcon } from '@hugeicons/core-free-icons';
|
|
6
|
+
import { SecurityValidationIcon } from '@hugeicons/core-free-icons';
|
|
4
7
|
|
|
5
8
|
const routes = [
|
|
6
9
|
{
|
|
7
|
-
path: '/',
|
|
8
10
|
name: 'Welcome',
|
|
9
|
-
|
|
11
|
+
path: '/',
|
|
12
|
+
component: () => import('./vue/views/Welcome.vue'),
|
|
13
|
+
meta: {
|
|
14
|
+
title: 'Welcome',
|
|
15
|
+
icon: HomeIcon,
|
|
16
|
+
}
|
|
10
17
|
},
|
|
11
18
|
{
|
|
12
|
-
path: '/table-example',
|
|
13
19
|
name: 'TableExample',
|
|
14
|
-
|
|
20
|
+
path: '/table-example',
|
|
21
|
+
title: 'Table Example',
|
|
22
|
+
component: () => import('./vue/views/TableExample.vue'),
|
|
23
|
+
meta: {
|
|
24
|
+
title: 'Table Example',
|
|
25
|
+
icon: TableIcon,
|
|
26
|
+
}
|
|
15
27
|
},
|
|
16
28
|
{
|
|
17
|
-
path: '/view-item/:id',
|
|
18
29
|
name: 'ViewItem',
|
|
19
|
-
|
|
30
|
+
path: '/view-item/:id',
|
|
31
|
+
component: () => import('./vue/views/ViewItem.vue'),
|
|
20
32
|
props: true,
|
|
33
|
+
meta: {
|
|
34
|
+
title: 'View Item',
|
|
35
|
+
icon: ViewIcon,
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'Admin',
|
|
40
|
+
path: '/admin',
|
|
41
|
+
component: () => import('./vue/views/Admin.vue'),
|
|
42
|
+
meta: {
|
|
43
|
+
title: 'Admin',
|
|
44
|
+
icon: SecurityValidationIcon,
|
|
45
|
+
}
|
|
21
46
|
}
|
|
22
47
|
];
|
|
23
48
|
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="quick-search">
|
|
3
|
+
<div class="search-container">
|
|
4
|
+
<input
|
|
5
|
+
ref="searchInput"
|
|
6
|
+
v-model="searchQuery"
|
|
7
|
+
type="text"
|
|
8
|
+
class="search-input"
|
|
9
|
+
:placeholder="placeholder"
|
|
10
|
+
@input="onSearch"
|
|
11
|
+
@focus="onFocus"
|
|
12
|
+
@blur="onBlur"
|
|
13
|
+
@keydown="onKeydown"
|
|
14
|
+
/>
|
|
15
|
+
<div class="search-icon">
|
|
16
|
+
<HugeiconsIcon :icon="SearchIcon" width="1em" height="1em" />
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div v-if="showResults && filteredItems.length > 0" class="search-results">
|
|
21
|
+
<div
|
|
22
|
+
v-for="(item, index) in filteredItems"
|
|
23
|
+
:key="item.id"
|
|
24
|
+
:class="['search-result-item', { active: selectedIndex === index }]"
|
|
25
|
+
@click="selectItem(item)"
|
|
26
|
+
@mouseenter="selectedIndex = index"
|
|
27
|
+
>
|
|
28
|
+
<div class="result-content">
|
|
29
|
+
<div class="result-title" v-html="highlightText(item.title, searchQuery)"></div>
|
|
30
|
+
<div v-if="item.description" class="result-description" v-html="highlightText(item.description, searchQuery)"></div>
|
|
31
|
+
<div v-if="item.category" class="result-category">{{ item.category }}</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div v-if="item.icon" class="result-icon">
|
|
34
|
+
<HugeiconsIcon :icon="item.icon" width="1.2em" height="1.2em" />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div v-if="showResults && searchQuery && filteredItems.length === 0" class="no-results">
|
|
40
|
+
<div class="no-results-content">
|
|
41
|
+
<HugeiconsIcon :icon="SearchRemoveIcon" width="2em" height="2em" />
|
|
42
|
+
<p>No results found for "{{ searchQuery }}"</p>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<script setup>
|
|
49
|
+
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
|
50
|
+
import { useRouter } from 'vue-router'
|
|
51
|
+
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
52
|
+
import { SearchIcon } from '@hugeicons/core-free-icons'
|
|
53
|
+
import { SearchRemoveIcon } from '@hugeicons/core-free-icons'
|
|
54
|
+
import { ViewIcon } from '@hugeicons/core-free-icons'
|
|
55
|
+
|
|
56
|
+
const props = defineProps({
|
|
57
|
+
placeholder: {
|
|
58
|
+
type: String,
|
|
59
|
+
default: 'Search...'
|
|
60
|
+
},
|
|
61
|
+
items: {
|
|
62
|
+
type: Array,
|
|
63
|
+
default: () => []
|
|
64
|
+
},
|
|
65
|
+
searchFields: {
|
|
66
|
+
type: Array,
|
|
67
|
+
default: () => ['title', 'description']
|
|
68
|
+
},
|
|
69
|
+
maxResults: {
|
|
70
|
+
type: Number,
|
|
71
|
+
default: 10
|
|
72
|
+
},
|
|
73
|
+
debounceMs: {
|
|
74
|
+
type: Number,
|
|
75
|
+
default: 300
|
|
76
|
+
},
|
|
77
|
+
enableGlobalShortcut: {
|
|
78
|
+
type: Boolean,
|
|
79
|
+
default: true
|
|
80
|
+
},
|
|
81
|
+
autoImportRoutes: {
|
|
82
|
+
type: Boolean,
|
|
83
|
+
default: true
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const emit = defineEmits(['select', 'search', 'focus', 'blur'])
|
|
88
|
+
|
|
89
|
+
const router = useRouter()
|
|
90
|
+
const searchInput = ref(null)
|
|
91
|
+
const searchQuery = ref('')
|
|
92
|
+
const showResults = ref(false)
|
|
93
|
+
const selectedIndex = ref(-1)
|
|
94
|
+
const items = ref([...props.items])
|
|
95
|
+
const debounceTimer = ref(null)
|
|
96
|
+
|
|
97
|
+
function importRoutesFromRouter() {
|
|
98
|
+
const routeItems = router.getRoutes()
|
|
99
|
+
.filter(route => route.name) // Exclude unnamed routes
|
|
100
|
+
.map(route => ({
|
|
101
|
+
id: `route-${route.name}`,
|
|
102
|
+
title: route.meta.title || route.name,
|
|
103
|
+
description: `Navigate to ${route.path}`,
|
|
104
|
+
category: 'Navigation',
|
|
105
|
+
path: route.path,
|
|
106
|
+
icon: route.meta.icon || ViewIcon,
|
|
107
|
+
type: 'route'
|
|
108
|
+
}))
|
|
109
|
+
|
|
110
|
+
// Add the items to the search
|
|
111
|
+
routeItems.forEach(item => addItem(item))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const filteredItems = computed(() => {
|
|
115
|
+
if (!searchQuery.value.trim()) {
|
|
116
|
+
return []
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const query = searchQuery.value.toLowerCase()
|
|
120
|
+
const filtered = items.value.filter(item => {
|
|
121
|
+
return props.searchFields.some(field => {
|
|
122
|
+
const value = item[field]
|
|
123
|
+
return value && value.toLowerCase().includes(query)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
return filtered.slice(0, props.maxResults)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
function handleGlobalKeydown(event) {
|
|
131
|
+
// Check if Ctrl+K is pressed
|
|
132
|
+
if (event.ctrlKey && event.key === 'k') {
|
|
133
|
+
event.preventDefault()
|
|
134
|
+
|
|
135
|
+
// Don't trigger if user is typing in an input/textarea
|
|
136
|
+
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
focus()
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function onSearch() {
|
|
145
|
+
clearTimeout(debounceTimer.value)
|
|
146
|
+
debounceTimer.value = setTimeout(() => {
|
|
147
|
+
selectedIndex.value = -1
|
|
148
|
+
showResults.value = true
|
|
149
|
+
emit('search', searchQuery.value)
|
|
150
|
+
}, props.debounceMs)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function onFocus() {
|
|
154
|
+
showResults.value = true
|
|
155
|
+
emit('focus')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function onBlur() {
|
|
159
|
+
// Delay hiding results to allow for click events
|
|
160
|
+
setTimeout(() => {
|
|
161
|
+
showResults.value = false
|
|
162
|
+
selectedIndex.value = -1
|
|
163
|
+
emit('blur')
|
|
164
|
+
}, 150)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function onKeydown(event) {
|
|
168
|
+
if (!showResults.value || filteredItems.value.length === 0) return
|
|
169
|
+
|
|
170
|
+
switch (event.key) {
|
|
171
|
+
case 'ArrowDown':
|
|
172
|
+
event.preventDefault()
|
|
173
|
+
selectedIndex.value = Math.min(selectedIndex.value + 1, filteredItems.value.length - 1)
|
|
174
|
+
break
|
|
175
|
+
case 'ArrowUp':
|
|
176
|
+
event.preventDefault()
|
|
177
|
+
selectedIndex.value = Math.max(selectedIndex.value - 1, -1)
|
|
178
|
+
break
|
|
179
|
+
case 'Enter':
|
|
180
|
+
event.preventDefault()
|
|
181
|
+
if (selectedIndex.value >= 0) {
|
|
182
|
+
selectItem(filteredItems.value[selectedIndex.value])
|
|
183
|
+
}
|
|
184
|
+
break
|
|
185
|
+
case 'Escape':
|
|
186
|
+
showResults.value = false
|
|
187
|
+
selectedIndex.value = -1
|
|
188
|
+
searchInput.value.blur()
|
|
189
|
+
break
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function selectItem(item) {
|
|
194
|
+
switch(item.type) {
|
|
195
|
+
case 'route':
|
|
196
|
+
router.push({ path: item.path })
|
|
197
|
+
break
|
|
198
|
+
case 'callback':
|
|
199
|
+
item.callback()
|
|
200
|
+
break
|
|
201
|
+
default:
|
|
202
|
+
emit('select', item)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
searchQuery.value = ''
|
|
206
|
+
showResults.value = false
|
|
207
|
+
selectedIndex.value = -1
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function highlightText(text, query) {
|
|
211
|
+
if (!query || !text) return text
|
|
212
|
+
|
|
213
|
+
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi')
|
|
214
|
+
return text.replace(regex, '<mark>$1</mark>')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// API Methods
|
|
218
|
+
function addItem(item) {
|
|
219
|
+
const existingIndex = items.value.findIndex(i => i.id === item.id)
|
|
220
|
+
if (existingIndex >= 0) {
|
|
221
|
+
items.value[existingIndex] = { ...item }
|
|
222
|
+
} else {
|
|
223
|
+
items.value.push({ ...item })
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function removeItem(itemId) {
|
|
228
|
+
items.value = items.value.filter(item => item.id !== itemId)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function clearItems() {
|
|
232
|
+
items.value = []
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getItems() {
|
|
236
|
+
return [...items.value]
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function setItems(newItems) {
|
|
240
|
+
items.value = [...newItems]
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function focus() {
|
|
244
|
+
searchInput.value?.focus()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function blur() {
|
|
248
|
+
searchInput.value?.blur()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function clear() {
|
|
252
|
+
searchQuery.value = ''
|
|
253
|
+
showResults.value = false
|
|
254
|
+
selectedIndex.value = -1
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function refreshRoutes() {
|
|
258
|
+
if (props.autoImportRoutes) {
|
|
259
|
+
importRoutesFromRouter()
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Global shortcut management
|
|
264
|
+
onMounted(() => {
|
|
265
|
+
if (props.enableGlobalShortcut) {
|
|
266
|
+
document.addEventListener('keydown', handleGlobalKeydown)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Auto-import routes on mount
|
|
270
|
+
if (props.autoImportRoutes) {
|
|
271
|
+
importRoutesFromRouter()
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
onUnmounted(() => {
|
|
276
|
+
if (props.enableGlobalShortcut) {
|
|
277
|
+
document.removeEventListener('keydown', handleGlobalKeydown)
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
// Watch for external items changes
|
|
282
|
+
watch(() => props.items, (newItems) => {
|
|
283
|
+
items.value = [...newItems]
|
|
284
|
+
}, { deep: true })
|
|
285
|
+
|
|
286
|
+
defineExpose({
|
|
287
|
+
addItem,
|
|
288
|
+
removeItem,
|
|
289
|
+
clearItems,
|
|
290
|
+
getItems,
|
|
291
|
+
setItems,
|
|
292
|
+
focus,
|
|
293
|
+
blur,
|
|
294
|
+
clear,
|
|
295
|
+
refreshRoutes,
|
|
296
|
+
searchQuery,
|
|
297
|
+
filteredItems,
|
|
298
|
+
showResults
|
|
299
|
+
})
|
|
300
|
+
</script>
|
|
301
|
+
|
|
302
|
+
<style scoped>
|
|
303
|
+
.quick-search {
|
|
304
|
+
position: relative;
|
|
305
|
+
width: 100%;
|
|
306
|
+
max-width: 400px;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.search-container {
|
|
310
|
+
position: relative;
|
|
311
|
+
display: flex;
|
|
312
|
+
align-items: center;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.search-input {
|
|
316
|
+
width: 100%;
|
|
317
|
+
padding: 0.4em;
|
|
318
|
+
border: none;
|
|
319
|
+
border-radius: .4em;
|
|
320
|
+
font-size: 1rem;
|
|
321
|
+
outline: none;
|
|
322
|
+
transition: all 0.2s ease;
|
|
323
|
+
background-color: #666;
|
|
324
|
+
color: #fff;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.search-input::placeholder {
|
|
328
|
+
color: #bbb;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.search-input:focus {
|
|
332
|
+
border-color: #3b82f6;
|
|
333
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.search-icon {
|
|
337
|
+
position: absolute;
|
|
338
|
+
right: 0.75rem;
|
|
339
|
+
color: #6b7280;
|
|
340
|
+
pointer-events: none;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.search-results {
|
|
344
|
+
position: absolute;
|
|
345
|
+
top: 100%;
|
|
346
|
+
left: 0;
|
|
347
|
+
right: 0;
|
|
348
|
+
background: #fff;
|
|
349
|
+
border: 2px solid #e1e5e9;
|
|
350
|
+
border-top: none;
|
|
351
|
+
border-radius: 0 0 8px 8px;
|
|
352
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
353
|
+
max-height: 300px;
|
|
354
|
+
overflow-y: auto;
|
|
355
|
+
z-index: 1000;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.search-result-item {
|
|
359
|
+
display: flex;
|
|
360
|
+
align-items: center;
|
|
361
|
+
padding: 0.75rem 1rem;
|
|
362
|
+
cursor: pointer;
|
|
363
|
+
transition: background-color 0.15s ease;
|
|
364
|
+
border-bottom: 1px solid #f3f4f6;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.search-result-item:last-child {
|
|
368
|
+
border-bottom: none;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.search-result-item:hover,
|
|
372
|
+
.search-result-item.active {
|
|
373
|
+
background-color: #f8fafc;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.result-content {
|
|
377
|
+
flex: 1;
|
|
378
|
+
min-width: 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.result-title {
|
|
382
|
+
font-weight: 500;
|
|
383
|
+
color: #1f2937;
|
|
384
|
+
margin-bottom: 0.25rem;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.result-description {
|
|
388
|
+
font-size: 0.875rem;
|
|
389
|
+
color: #6b7280;
|
|
390
|
+
margin-bottom: 0.25rem;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.result-category {
|
|
394
|
+
font-size: 0.75rem;
|
|
395
|
+
color: #9ca3af;
|
|
396
|
+
text-transform: uppercase;
|
|
397
|
+
letter-spacing: 0.05em;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.result-icon {
|
|
401
|
+
margin-left: 0.75rem;
|
|
402
|
+
color: #6b7280;
|
|
403
|
+
flex-shrink: 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.no-results {
|
|
407
|
+
position: absolute;
|
|
408
|
+
top: 100%;
|
|
409
|
+
left: 0;
|
|
410
|
+
right: 0;
|
|
411
|
+
background: #fff;
|
|
412
|
+
border: 2px solid #e1e5e9;
|
|
413
|
+
border-top: none;
|
|
414
|
+
border-radius: 0 0 8px 8px;
|
|
415
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
416
|
+
z-index: 1000;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.no-results-content {
|
|
420
|
+
display: flex;
|
|
421
|
+
flex-direction: column;
|
|
422
|
+
align-items: center;
|
|
423
|
+
padding: 2rem 1rem;
|
|
424
|
+
color: #6b7280;
|
|
425
|
+
text-align: center;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.no-results-content p {
|
|
429
|
+
margin: 0.5rem 0 0 0;
|
|
430
|
+
font-size: 0.875rem;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
mark {
|
|
434
|
+
background-color: #fef3c7;
|
|
435
|
+
color: #92400e;
|
|
436
|
+
padding: 0;
|
|
437
|
+
border-radius: 2px;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/* Dark theme */
|
|
441
|
+
@media (prefers-color-scheme: dark) {
|
|
442
|
+
.search-input {
|
|
443
|
+
background-color: #1f2937;
|
|
444
|
+
border-color: #374151;
|
|
445
|
+
color: #f9fafb;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.search-input:focus {
|
|
449
|
+
border-color: #60a5fa;
|
|
450
|
+
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.search-icon {
|
|
454
|
+
color: #9ca3af;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.search-results {
|
|
458
|
+
background: #1f2937;
|
|
459
|
+
border-color: #374151;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.search-result-item {
|
|
463
|
+
border-bottom-color: #374151;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.search-result-item:hover,
|
|
467
|
+
.search-result-item.active {
|
|
468
|
+
background-color: #374151;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.result-title {
|
|
472
|
+
color: #f9fafb;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.result-description {
|
|
476
|
+
color: #d1d5db;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.result-category {
|
|
480
|
+
color: #9ca3af;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.result-icon {
|
|
484
|
+
color: #9ca3af;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.no-results {
|
|
488
|
+
background: #1f2937;
|
|
489
|
+
border-color: #374151;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.no-results-content {
|
|
493
|
+
color: #9ca3af;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
mark {
|
|
497
|
+
background-color: #451a03;
|
|
498
|
+
color: #fbbf24;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/* Responsive */
|
|
503
|
+
@media (max-width: 640px) {
|
|
504
|
+
.quick-search {
|
|
505
|
+
max-width: 100%;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.search-results {
|
|
509
|
+
max-height: 250px;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
</style>
|
|
@@ -15,17 +15,11 @@
|
|
|
15
15
|
|
|
16
16
|
<nav class="mainnav">
|
|
17
17
|
<ul class="navigation-links">
|
|
18
|
-
<li v-for="link in navigationLinks" :key="link.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
</li>
|
|
24
|
-
</ul>
|
|
25
|
-
|
|
26
|
-
<ul class="supplemental-links">
|
|
27
|
-
<li v-for="link in supplementalLinks" :key="link.id" :title="link.title">
|
|
28
|
-
<router-link :to="link.path" :class="{ active: isActive(link.path) }">
|
|
18
|
+
<li v-for="link in navigationLinks" :key="link.name" :title="link.title">
|
|
19
|
+
<!-- Render separator if link is a separator -->
|
|
20
|
+
<div v-if="link.type === 'separator'" class="separator"></div>
|
|
21
|
+
<!-- Render regular link if not a separator -->
|
|
22
|
+
<router-link v-else :to="link.path" :class="{ active: isActive(link.path) }">
|
|
29
23
|
<HugeiconsIcon :icon="link.icon" />
|
|
30
24
|
<span>{{ link.title }}</span>
|
|
31
25
|
</router-link>
|
|
@@ -36,12 +30,9 @@
|
|
|
36
30
|
</template>
|
|
37
31
|
|
|
38
32
|
<script setup>
|
|
39
|
-
|
|
40
|
-
import { useRoute } from 'vue-router'
|
|
33
|
+
import { ref } from 'vue'
|
|
34
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
41
35
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
42
|
-
import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
|
|
43
|
-
import { LeftToRightListDashIcon } from '@hugeicons/core-free-icons'
|
|
44
|
-
import { Wrench01Icon } from '@hugeicons/core-free-icons'
|
|
45
36
|
import { Pin02Icon } from '@hugeicons/core-free-icons'
|
|
46
37
|
import { PinIcon } from '@hugeicons/core-free-icons'
|
|
47
38
|
|
|
@@ -49,9 +40,54 @@ const isOpen = ref(false)
|
|
|
49
40
|
const isStuck = ref(false)
|
|
50
41
|
const navigationLinks = ref([])
|
|
51
42
|
|
|
52
|
-
const supplementalLinks = ref([])
|
|
53
|
-
|
|
54
43
|
const route = useRoute()
|
|
44
|
+
const router = useRouter()
|
|
45
|
+
|
|
46
|
+
function addRouterLink(link) {
|
|
47
|
+
const route = router.getRoutes().find(r => r.name === link)
|
|
48
|
+
|
|
49
|
+
if (route) {
|
|
50
|
+
const routeLink = {
|
|
51
|
+
name: link,
|
|
52
|
+
title: route.meta.title || route.name,
|
|
53
|
+
path: route.path,
|
|
54
|
+
icon: route.meta.icon || PinIcon,
|
|
55
|
+
type: 'route'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
addNavigationLink(routeLink)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function addNavigationLink(link) {
|
|
63
|
+
const existingIndex = navigationLinks.value.findIndex(l => l.name === link.name)
|
|
64
|
+
if (existingIndex >= 0) {
|
|
65
|
+
navigationLinks.value[existingIndex] = { ...link }
|
|
66
|
+
} else {
|
|
67
|
+
navigationLinks.value.push({ ...link })
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function addSeparator(id) {
|
|
72
|
+
const separator = {
|
|
73
|
+
name: id || `nav-separator-${Date.now()}`,
|
|
74
|
+
type: 'separator',
|
|
75
|
+
title: 'Separator'
|
|
76
|
+
}
|
|
77
|
+
addNavigationLink(separator)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function removeNavigationLink(linkId) {
|
|
81
|
+
navigationLinks.value = navigationLinks.value.filter(link => link.id !== linkId)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function clearNavigationLinks() {
|
|
85
|
+
navigationLinks.value = []
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getNavigationLinks() {
|
|
89
|
+
return [...navigationLinks.value]
|
|
90
|
+
}
|
|
55
91
|
|
|
56
92
|
function toggleStick() {
|
|
57
93
|
isStuck.value = !isStuck.value
|
|
@@ -83,54 +119,9 @@ function isActive(path) {
|
|
|
83
119
|
return route.path === path
|
|
84
120
|
}
|
|
85
121
|
|
|
86
|
-
function addNavigationLink(link) {
|
|
87
|
-
link.icon = DashboardSquare01Icon
|
|
88
|
-
|
|
89
|
-
const existingIndex = navigationLinks.value.findIndex(l => l.id === link.id)
|
|
90
|
-
if (existingIndex >= 0) {
|
|
91
|
-
navigationLinks.value[existingIndex] = { ...link }
|
|
92
|
-
} else {
|
|
93
|
-
navigationLinks.value.push({ ...link })
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function addSupplementalLink(link) {
|
|
98
|
-
const existingIndex = supplementalLinks.value.findIndex(l => l.id === link.id)
|
|
99
|
-
if (existingIndex >= 0) {
|
|
100
|
-
supplementalLinks.value[existingIndex] = { ...link }
|
|
101
|
-
} else {
|
|
102
|
-
supplementalLinks.value.push({ ...link })
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function removeNavigationLink(linkId) {
|
|
107
|
-
navigationLinks.value = navigationLinks.value.filter(link => link.id !== linkId)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function removeSupplementalLink(linkId) {
|
|
111
|
-
supplementalLinks.value = supplementalLinks.value.filter(link => link.id !== linkId)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function clearNavigationLinks() {
|
|
115
|
-
navigationLinks.value = []
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function clearSupplementalLinks() {
|
|
119
|
-
supplementalLinks.value = []
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function getNavigationLinks() {
|
|
123
|
-
return [...navigationLinks.value]
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function getSupplementalLinks() {
|
|
127
|
-
return [...supplementalLinks.value]
|
|
128
|
-
}
|
|
129
|
-
|
|
130
122
|
defineExpose({
|
|
131
123
|
isOpen,
|
|
132
124
|
navigationLinks,
|
|
133
|
-
supplementalLinks,
|
|
134
125
|
stick,
|
|
135
126
|
unstick,
|
|
136
127
|
toggleStick,
|
|
@@ -139,13 +130,11 @@ defineExpose({
|
|
|
139
130
|
close,
|
|
140
131
|
isActive,
|
|
141
132
|
addNavigationLink,
|
|
142
|
-
|
|
133
|
+
addRouterLink,
|
|
134
|
+
addSeparator,
|
|
143
135
|
removeNavigationLink,
|
|
144
|
-
removeSupplementalLink,
|
|
145
136
|
clearNavigationLinks,
|
|
146
|
-
clearSupplementalLinks,
|
|
147
137
|
getNavigationLinks,
|
|
148
|
-
getSupplementalLinks
|
|
149
138
|
})
|
|
150
139
|
</script>
|
|
151
140
|
|
|
@@ -164,8 +153,7 @@ li {
|
|
|
164
153
|
padding: 0;
|
|
165
154
|
}
|
|
166
155
|
|
|
167
|
-
.navigation-links a
|
|
168
|
-
.supplemental-links a {
|
|
156
|
+
.navigation-links a {
|
|
169
157
|
display: flex;
|
|
170
158
|
align-items: center;
|
|
171
159
|
gap: 0.75rem;
|
|
@@ -173,23 +161,26 @@ li {
|
|
|
173
161
|
border-radius: 0;
|
|
174
162
|
}
|
|
175
163
|
|
|
164
|
+
.separator {
|
|
165
|
+
height: 1px;
|
|
166
|
+
background-color: #eee;
|
|
167
|
+
margin: 0.5rem 0.75rem;
|
|
168
|
+
}
|
|
169
|
+
|
|
176
170
|
.icon {
|
|
177
171
|
font-size: 1.2em;
|
|
178
172
|
width: 1.5rem;
|
|
179
173
|
text-align: center;
|
|
180
174
|
}
|
|
181
175
|
|
|
182
|
-
.supplemental-links {
|
|
183
|
-
border-top: 1px solid #eee;
|
|
184
|
-
margin-top: 1rem;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
176
|
@media (prefers-color-scheme: dark) {
|
|
188
|
-
.navigation-links a
|
|
189
|
-
.supplemental-links a {
|
|
177
|
+
.navigation-links a {
|
|
190
178
|
color: #f8f9fa;
|
|
191
179
|
}
|
|
192
180
|
|
|
181
|
+
.separator {
|
|
182
|
+
background-color: #444;
|
|
183
|
+
}
|
|
193
184
|
|
|
194
185
|
.supplemental-links {
|
|
195
186
|
border-top: 1px solid #444;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Section
|
|
3
|
+
title="Admin Panel"
|
|
4
|
+
subtitle="Administrative interface for system management"
|
|
5
|
+
padding
|
|
6
|
+
>
|
|
7
|
+
<div class="admin-content">
|
|
8
|
+
<div class="admin-section">
|
|
9
|
+
<h3>System Overview</h3>
|
|
10
|
+
<p>Welcome to the administrative panel. This is a placeholder for administrative functionality.</p>
|
|
11
|
+
|
|
12
|
+
<div class="stats-grid">
|
|
13
|
+
<div class="stat-card">
|
|
14
|
+
<h4>Total Users</h4>
|
|
15
|
+
<div class="stat-value">1,247</div>
|
|
16
|
+
<div class="stat-change">+12% from last month</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="stat-card">
|
|
20
|
+
<h4>Active Sessions</h4>
|
|
21
|
+
<div class="stat-value">89</div>
|
|
22
|
+
<div class="stat-change">-3% from last hour</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="stat-card">
|
|
26
|
+
<h4>System Load</h4>
|
|
27
|
+
<div class="stat-value">67%</div>
|
|
28
|
+
<div class="stat-change">Normal</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="stat-card">
|
|
32
|
+
<h4>Storage Used</h4>
|
|
33
|
+
<div class="stat-value">2.4 GB</div>
|
|
34
|
+
<div class="stat-change">45% of total</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="admin-section">
|
|
40
|
+
<h3>Recent Activity</h3>
|
|
41
|
+
<div class="activity-list">
|
|
42
|
+
<div class="activity-item">
|
|
43
|
+
<span class="activity-time">2 minutes ago</span>
|
|
44
|
+
<span class="activity-text">User "john.doe" logged in</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="activity-item">
|
|
47
|
+
<span class="activity-time">5 minutes ago</span>
|
|
48
|
+
<span class="activity-text">New user account created</span>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="activity-item">
|
|
51
|
+
<span class="activity-time">12 minutes ago</span>
|
|
52
|
+
<span class="activity-text">System backup completed</span>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="activity-item">
|
|
55
|
+
<span class="activity-time">1 hour ago</span>
|
|
56
|
+
<span class="activity-text">Database maintenance performed</span>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="activity-item">
|
|
59
|
+
<span class="activity-time">2 hours ago</span>
|
|
60
|
+
<span class="activity-text">User "admin" updated settings</span>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="admin-section">
|
|
66
|
+
<h3>Quick Actions</h3>
|
|
67
|
+
<div class="action-buttons">
|
|
68
|
+
<button class="action-btn">Create User</button>
|
|
69
|
+
<button class="action-btn">System Backup</button>
|
|
70
|
+
<button class="action-btn">View Logs</button>
|
|
71
|
+
<button class="action-btn">Settings</button>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</Section>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<script setup>
|
|
79
|
+
// This is a placeholder admin view with dummy content
|
|
80
|
+
// No functionality implemented yet
|
|
81
|
+
</script>
|
package/vue/views/App.vue
CHANGED
|
@@ -1,24 +1,38 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<Header username = "Guest"
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
2
|
+
<Header username = "Guest" @toggleSidebar="toggleSidebar" title = "PicoCrank" >
|
|
3
|
+
<template #toolbar>
|
|
4
|
+
<QuickSearch
|
|
5
|
+
ref="quickSearchRef"
|
|
6
|
+
placeholder="Search items..."
|
|
7
|
+
:search-fields="['title', 'name']"
|
|
8
|
+
:max-results="15"
|
|
9
|
+
/>
|
|
10
|
+
</template>
|
|
11
|
+
</Header>
|
|
12
|
+
|
|
13
|
+
<div id = "layout">
|
|
14
|
+
<Sidebar ref = "sidebar" />
|
|
15
|
+
|
|
16
|
+
<div id = "content">
|
|
17
|
+
<main>
|
|
18
|
+
<router-view />
|
|
19
|
+
</main>
|
|
20
|
+
|
|
21
|
+
<footer>
|
|
22
|
+
<span>PicoCrank</span>
|
|
23
|
+
</footer>
|
|
15
24
|
</div>
|
|
25
|
+
</div>
|
|
16
26
|
</template>
|
|
17
27
|
|
|
18
28
|
<script setup>
|
|
19
|
-
import { ref } from 'vue';
|
|
20
|
-
import {
|
|
29
|
+
import { ref, onMounted } from 'vue';
|
|
30
|
+
import { useRouter } from 'vue-router';
|
|
21
31
|
|
|
32
|
+
import QuickSearch from './../components/QuickSearch.vue'
|
|
33
|
+
|
|
34
|
+
const quickSearchRef = ref(null)
|
|
35
|
+
const router = useRouter();
|
|
22
36
|
const sidebar = ref(null);
|
|
23
37
|
|
|
24
38
|
function toggleSidebar() {
|
|
@@ -28,19 +42,28 @@
|
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
onMounted(() => {
|
|
31
|
-
sidebar.value.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
sidebar.value.
|
|
37
|
-
id: 'table-example',
|
|
38
|
-
title: "Table Example",
|
|
39
|
-
path: "/table-example",
|
|
40
|
-
})
|
|
41
|
-
|
|
45
|
+
sidebar.value.addRouterLink('Welcome')
|
|
46
|
+
sidebar.value.addRouterLink('TableExample')
|
|
47
|
+
sidebar.value.addSeparator('separator-1');
|
|
48
|
+
sidebar.value.addRouterLink('ViewItem', { id: 1 })
|
|
49
|
+
sidebar.value.addSeparator('separator-2');
|
|
50
|
+
sidebar.value.addRouterLink('Admin')
|
|
42
51
|
sidebar.value.open();
|
|
43
52
|
sidebar.value.stick();
|
|
53
|
+
|
|
54
|
+
quickSearchRef.value.addItem({
|
|
55
|
+
id: 'hello-world',
|
|
56
|
+
title: 'Hello World',
|
|
57
|
+
name: 'hello-world',
|
|
58
|
+
description: 'This is a test item',
|
|
59
|
+
category: 'Actions',
|
|
60
|
+
type: 'callback',
|
|
61
|
+
callback: helloWorld
|
|
62
|
+
})
|
|
44
63
|
});
|
|
45
64
|
|
|
65
|
+
function helloWorld() {
|
|
66
|
+
alert('Hello World')
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
</script>
|
|
File without changes
|
|
File without changes
|