picocrank 1.3.0 → 1.4.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 +1 -1
- package/router.js +32 -7
- package/vue/components/Header.vue +7 -3
- package/vue/components/QuickSearch.vue +512 -0
- package/vue/components/Sidebar.vue +85 -76
- package/vue/views/Admin.vue +81 -0
- package/vue/views/App.vue +52 -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
|
|
|
@@ -12,12 +12,16 @@
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
14
|
<Breadcrumbs v-if="breadcrumbs" />
|
|
15
|
+
|
|
16
|
+
<slot name="toolbar" />
|
|
15
17
|
|
|
16
18
|
<div class = "fg1"></div>
|
|
17
19
|
|
|
18
|
-
<
|
|
19
|
-
<
|
|
20
|
-
|
|
20
|
+
<slot name = "user-info">
|
|
21
|
+
<div class = "user-info">
|
|
22
|
+
<span v-if="username">{{ username }}</span>
|
|
23
|
+
</div>
|
|
24
|
+
</slot>
|
|
21
25
|
</header>
|
|
22
26
|
</template>
|
|
23
27
|
|
|
@@ -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,16 @@
|
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
<div v-else-if="link.type === 'callback'">
|
|
22
|
+
<a href="#" @click.prevent="link.callback()">
|
|
23
|
+
<HugeiconsIcon :icon="link.icon" />
|
|
24
|
+
<span>{{ link.title }}</span>
|
|
25
|
+
</a>
|
|
26
|
+
</div>
|
|
27
|
+
<router-link v-else :to="link.path" :class="{ active: isActive(link.path) }">
|
|
29
28
|
<HugeiconsIcon :icon="link.icon" />
|
|
30
29
|
<span>{{ link.title }}</span>
|
|
31
30
|
</router-link>
|
|
@@ -36,12 +35,9 @@
|
|
|
36
35
|
</template>
|
|
37
36
|
|
|
38
37
|
<script setup>
|
|
39
|
-
|
|
40
|
-
import { useRoute } from 'vue-router'
|
|
38
|
+
import { ref } from 'vue'
|
|
39
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
41
40
|
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
41
|
import { Pin02Icon } from '@hugeicons/core-free-icons'
|
|
46
42
|
import { PinIcon } from '@hugeicons/core-free-icons'
|
|
47
43
|
|
|
@@ -49,9 +45,66 @@ const isOpen = ref(false)
|
|
|
49
45
|
const isStuck = ref(false)
|
|
50
46
|
const navigationLinks = ref([])
|
|
51
47
|
|
|
52
|
-
const supplementalLinks = ref([])
|
|
53
|
-
|
|
54
48
|
const route = useRoute()
|
|
49
|
+
const router = useRouter()
|
|
50
|
+
|
|
51
|
+
function addRouterLink(link) {
|
|
52
|
+
const route = router.getRoutes().find(r => r.name === link)
|
|
53
|
+
|
|
54
|
+
if (route) {
|
|
55
|
+
const routeLink = {
|
|
56
|
+
name: link,
|
|
57
|
+
title: route.meta.title || route.name,
|
|
58
|
+
path: route.path,
|
|
59
|
+
icon: route.meta.icon || PinIcon,
|
|
60
|
+
type: 'route'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
addNavigationLink(routeLink)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function addNavigationLink(link) {
|
|
68
|
+
const existingIndex = navigationLinks.value.findIndex(l => l.name === link.name)
|
|
69
|
+
if (existingIndex >= 0) {
|
|
70
|
+
navigationLinks.value[existingIndex] = { ...link }
|
|
71
|
+
} else {
|
|
72
|
+
navigationLinks.value.push({ ...link })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function addCallback(title, callback, options = {}) {
|
|
77
|
+
const callbackLink = {
|
|
78
|
+
name: options.name || title.toLowerCase().replace(/\s+/g, '-'),
|
|
79
|
+
type: 'callback',
|
|
80
|
+
icon: options.icon || PinIcon,
|
|
81
|
+
callback: callback || (() => {}),
|
|
82
|
+
title: title
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
addNavigationLink(callbackLink)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function addSeparator(id) {
|
|
89
|
+
const separator = {
|
|
90
|
+
name: id || `nav-separator-${Date.now()}`,
|
|
91
|
+
type: 'separator',
|
|
92
|
+
title: 'Separator'
|
|
93
|
+
}
|
|
94
|
+
addNavigationLink(separator)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function removeNavigationLink(linkId) {
|
|
98
|
+
navigationLinks.value = navigationLinks.value.filter(link => link.id !== linkId)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function clearNavigationLinks() {
|
|
102
|
+
navigationLinks.value = []
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getNavigationLinks() {
|
|
106
|
+
return [...navigationLinks.value]
|
|
107
|
+
}
|
|
55
108
|
|
|
56
109
|
function toggleStick() {
|
|
57
110
|
isStuck.value = !isStuck.value
|
|
@@ -83,54 +136,9 @@ function isActive(path) {
|
|
|
83
136
|
return route.path === path
|
|
84
137
|
}
|
|
85
138
|
|
|
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
139
|
defineExpose({
|
|
131
140
|
isOpen,
|
|
132
141
|
navigationLinks,
|
|
133
|
-
supplementalLinks,
|
|
134
142
|
stick,
|
|
135
143
|
unstick,
|
|
136
144
|
toggleStick,
|
|
@@ -139,13 +147,12 @@ defineExpose({
|
|
|
139
147
|
close,
|
|
140
148
|
isActive,
|
|
141
149
|
addNavigationLink,
|
|
142
|
-
|
|
150
|
+
addRouterLink,
|
|
151
|
+
addCallback,
|
|
152
|
+
addSeparator,
|
|
143
153
|
removeNavigationLink,
|
|
144
|
-
removeSupplementalLink,
|
|
145
154
|
clearNavigationLinks,
|
|
146
|
-
clearSupplementalLinks,
|
|
147
155
|
getNavigationLinks,
|
|
148
|
-
getSupplementalLinks
|
|
149
156
|
})
|
|
150
157
|
</script>
|
|
151
158
|
|
|
@@ -164,8 +171,7 @@ li {
|
|
|
164
171
|
padding: 0;
|
|
165
172
|
}
|
|
166
173
|
|
|
167
|
-
.navigation-links a
|
|
168
|
-
.supplemental-links a {
|
|
174
|
+
.navigation-links a {
|
|
169
175
|
display: flex;
|
|
170
176
|
align-items: center;
|
|
171
177
|
gap: 0.75rem;
|
|
@@ -173,23 +179,26 @@ li {
|
|
|
173
179
|
border-radius: 0;
|
|
174
180
|
}
|
|
175
181
|
|
|
182
|
+
.separator {
|
|
183
|
+
height: 1px;
|
|
184
|
+
background-color: #eee;
|
|
185
|
+
margin: 0.5rem 0.75rem;
|
|
186
|
+
}
|
|
187
|
+
|
|
176
188
|
.icon {
|
|
177
189
|
font-size: 1.2em;
|
|
178
190
|
width: 1.5rem;
|
|
179
191
|
text-align: center;
|
|
180
192
|
}
|
|
181
193
|
|
|
182
|
-
.supplemental-links {
|
|
183
|
-
border-top: 1px solid #eee;
|
|
184
|
-
margin-top: 1rem;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
194
|
@media (prefers-color-scheme: dark) {
|
|
188
|
-
.navigation-links a
|
|
189
|
-
.supplemental-links a {
|
|
195
|
+
.navigation-links a {
|
|
190
196
|
color: #f8f9fa;
|
|
191
197
|
}
|
|
192
198
|
|
|
199
|
+
.separator {
|
|
200
|
+
background-color: #444;
|
|
201
|
+
}
|
|
193
202
|
|
|
194
203
|
.supplemental-links {
|
|
195
204
|
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,39 @@
|
|
|
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';
|
|
31
|
+
import { Pin02Icon } from '@hugeicons/core-free-icons'
|
|
21
32
|
|
|
33
|
+
import QuickSearch from './../components/QuickSearch.vue'
|
|
34
|
+
|
|
35
|
+
const quickSearchRef = ref(null)
|
|
36
|
+
const router = useRouter();
|
|
22
37
|
const sidebar = ref(null);
|
|
23
38
|
|
|
24
39
|
function toggleSidebar() {
|
|
@@ -28,19 +43,30 @@
|
|
|
28
43
|
}
|
|
29
44
|
|
|
30
45
|
onMounted(() => {
|
|
31
|
-
sidebar.value.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
sidebar.value.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
path: "/table-example",
|
|
40
|
-
})
|
|
41
|
-
|
|
46
|
+
sidebar.value.addRouterLink('Welcome')
|
|
47
|
+
sidebar.value.addRouterLink('TableExample')
|
|
48
|
+
sidebar.value.addSeparator('separator-1');
|
|
49
|
+
sidebar.value.addRouterLink('ViewItem', { id: 1 })
|
|
50
|
+
sidebar.value.addSeparator('separator-2');
|
|
51
|
+
sidebar.value.addCallback('Hello World', helloWorld, { icon: Pin02Icon })
|
|
52
|
+
sidebar.value.addSeparator('separator-3');
|
|
53
|
+
sidebar.value.addRouterLink('Admin')
|
|
42
54
|
sidebar.value.open();
|
|
43
55
|
sidebar.value.stick();
|
|
56
|
+
|
|
57
|
+
quickSearchRef.value.addItem({
|
|
58
|
+
id: 'hello-world',
|
|
59
|
+
title: 'Hello World',
|
|
60
|
+
name: 'hello-world',
|
|
61
|
+
description: 'This is a test item',
|
|
62
|
+
category: 'Actions',
|
|
63
|
+
type: 'callback',
|
|
64
|
+
callback: helloWorld
|
|
65
|
+
})
|
|
44
66
|
});
|
|
45
67
|
|
|
68
|
+
function helloWorld() {
|
|
69
|
+
alert('Hello World')
|
|
70
|
+
}
|
|
71
|
+
|
|
46
72
|
</script>
|
|
File without changes
|
|
File without changes
|