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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocrank",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "A set of zero dependency Vue components built on the Femtocrank global CSS theme.",
5
5
  "main": "vite.config.js",
6
6
  "scripts": {
package/router.js CHANGED
@@ -1,23 +1,48 @@
1
1
  import { createRouter, createWebHistory } from 'vue-router';
2
2
 
3
- import Welcome from './vue/views/Welcome.vue';
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
- component: Welcome,
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
- component: () => import('./vue/examples/TableExample.vue'),
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
- component: () => import('./vue/examples/ViewItem.vue'),
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,6 +12,8 @@
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
 
@@ -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.id" :title="link.title">
19
- <router-link :to="link.path" :class="{ active: isActive(link.path) }">
20
- <HugeiconsIcon :icon="link.icon" />
21
- <span>{{ link.title }}</span>
22
- </router-link>
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
- import { ref, onMounted, getCurrentInstance } from 'vue'
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
- addSupplementalLink,
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" breadcrumbs @toggleSidebar="toggleSidebar" title = "PicoCrank" />
3
- <div id = "layout">
4
- <Sidebar ref = "sidebar" />
5
-
6
- <div id = "content">
7
- <main>
8
- <router-view />
9
- </main>
10
-
11
- <footer>
12
- <span>PicoCrank</span>
13
- </footer>
14
- </div>
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 { onMounted } from 'vue';
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.addNavigationLink({
32
- id: 'welcome',
33
- title: "Welcome",
34
- path: "/",
35
- })
36
- sidebar.value.addNavigationLink({
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