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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocrank",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
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,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
- <div class = "user-info">
19
- <span v-if="username">{{ username }}</span>
20
- </div>
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.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
+ <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
- import { ref, onMounted, getCurrentInstance } from 'vue'
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
- addSupplementalLink,
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" 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';
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.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
-
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