picocrank 1.2.7 → 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/Section.vue +10 -2
- package/vue/components/Sidebar.vue +67 -76
- package/vue/views/Admin.vue +81 -0
- package/vue/views/App.vue +49 -26
- package/vue/views/Welcome.vue +40 -1
- /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>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section class = "with-header-and-content">
|
|
2
|
+
<section :class = "'with-header-and-content ' + classes" :id = "id">
|
|
3
3
|
<div class = "section-header flex-row">
|
|
4
4
|
<div class = "fg1">
|
|
5
|
-
|
|
5
|
+
<h2 :title = "subtitle">{{ title }}</h2>
|
|
6
6
|
</div>
|
|
7
7
|
<div role = "toolbar">
|
|
8
8
|
<slot name="toolbar" />
|
|
@@ -29,5 +29,13 @@ const props = defineProps({
|
|
|
29
29
|
type: String,
|
|
30
30
|
default: "",
|
|
31
31
|
},
|
|
32
|
+
id: {
|
|
33
|
+
type: String,
|
|
34
|
+
default: "",
|
|
35
|
+
},
|
|
36
|
+
classes: {
|
|
37
|
+
type: String,
|
|
38
|
+
default: "",
|
|
39
|
+
},
|
|
32
40
|
});
|
|
33
41
|
</script>
|
|
@@ -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>
|
package/vue/views/Welcome.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<Section title = "Welcome">
|
|
2
|
+
<Section title = "Welcome" classes = "">
|
|
3
3
|
<p>
|
|
4
4
|
Welcome to Femtocrank, a web-based tool for analyzing and visualizing the performance of your code.
|
|
5
5
|
To get started, you can either upload a file or paste your code directly into the editor.
|
|
@@ -9,6 +9,45 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
</Section>
|
|
11
11
|
|
|
12
|
+
<Section title = "Lorum" id = "lorem">
|
|
13
|
+
<p>
|
|
14
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
|
15
|
+
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
|
16
|
+
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
|
|
17
|
+
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
|
18
|
+
</p>
|
|
19
|
+
</Section>
|
|
20
|
+
|
|
21
|
+
<Section title = "Shakespeare" id = "shakespeare">
|
|
22
|
+
<p>
|
|
23
|
+
To be, or not to be, that is the question:
|
|
24
|
+
Whether 'tis nobler in the mind to suffer
|
|
25
|
+
The slings and arrows of outrageous fortune,
|
|
26
|
+
Or to take arms against a sea of troubles
|
|
27
|
+
And by opposing end them. To die: to sleep;
|
|
28
|
+
No more; and by a sleep to say we end
|
|
29
|
+
The heart-ache and the thousand natural shocks
|
|
30
|
+
That flesh is heir to, 'tis a consummation
|
|
31
|
+
Devoutly to be wish'd. To die, to sleep;
|
|
32
|
+
To sleep: perchance to dream: ay, there's the rub;
|
|
33
|
+
For in that sleep of death what dreams may come
|
|
34
|
+
When we have shuffled off this mortal coil,
|
|
35
|
+
Must give us pause: there's the respect
|
|
36
|
+
That makes calamity of so long life;
|
|
37
|
+
For who would bear the whips and scorns of time,
|
|
38
|
+
The oppressor's wrong
|
|
39
|
+
</p>
|
|
40
|
+
</Section>
|
|
41
|
+
|
|
42
|
+
<Section title = "More Lorem" id = "more-lorem">
|
|
43
|
+
<p>
|
|
44
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
|
45
|
+
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
|
46
|
+
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
|
|
47
|
+
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
|
48
|
+
</p>
|
|
49
|
+
</Section>
|
|
50
|
+
|
|
12
51
|
</template>
|
|
13
52
|
|
|
14
53
|
<script setup>
|
|
File without changes
|
|
File without changes
|