ketekny-ui-kit 1.0.101 → 1.0.103
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
|
Binary file
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import {
|
|
3
3
|
Search,
|
|
4
|
-
Bell,
|
|
5
4
|
Building2,
|
|
6
5
|
CircleUser,
|
|
7
6
|
CircleHelp,
|
|
@@ -10,14 +9,19 @@ import {
|
|
|
10
9
|
PanelLeftClose,
|
|
11
10
|
PanelLeftOpen,
|
|
12
11
|
UserRound,
|
|
12
|
+
Ellipsis,
|
|
13
13
|
} from '@lucide/vue'
|
|
14
14
|
import SidebarMenuItem from './SidebarMenuItem.vue'
|
|
15
|
+
import kMenu from '../../ui/kMenu.vue'
|
|
16
|
+
import demoAvatarUrl from '../assets/placeholder-user.jpg'
|
|
17
|
+
|
|
18
|
+
const FAVORITES_STORAGE_KEY = 'ketekny-ui-sidebar-favorites'
|
|
19
|
+
const MENU_TAB_STORAGE_KEY = 'ketekny-ui-sidebar-menu-tab'
|
|
15
20
|
|
|
16
21
|
export default {
|
|
17
22
|
name: 'MultilevelSidebar',
|
|
18
23
|
components: {
|
|
19
24
|
Search,
|
|
20
|
-
Bell,
|
|
21
25
|
Building2,
|
|
22
26
|
CircleUser,
|
|
23
27
|
CircleHelp,
|
|
@@ -26,7 +30,9 @@ export default {
|
|
|
26
30
|
PanelLeftClose,
|
|
27
31
|
PanelLeftOpen,
|
|
28
32
|
UserRound,
|
|
33
|
+
Ellipsis,
|
|
29
34
|
SidebarMenuItem,
|
|
35
|
+
kMenu,
|
|
30
36
|
},
|
|
31
37
|
props: {
|
|
32
38
|
title: {
|
|
@@ -69,10 +75,13 @@ export default {
|
|
|
69
75
|
emits: ['select', 'logo-click', 'signin', 'signup', 'signout', 'edit-profile', 'item-click'],
|
|
70
76
|
data() {
|
|
71
77
|
return {
|
|
78
|
+
demoAvatarUrl,
|
|
72
79
|
activeId: '',
|
|
80
|
+
activeMenuTab: 'all',
|
|
73
81
|
query: '',
|
|
74
82
|
collapsed: false,
|
|
75
83
|
openIds: new Set(['projects']),
|
|
84
|
+
favoriteIds: new Set(),
|
|
76
85
|
footerLinks: [
|
|
77
86
|
{ id: 'my-profile', label: 'Το προφίλ μου', icon: 'UserRound' },
|
|
78
87
|
{ id: 'help', label: 'Βοήθεια', icon: 'CircleHelp' },
|
|
@@ -103,6 +112,44 @@ export default {
|
|
|
103
112
|
if (!q) return this.mainMenu
|
|
104
113
|
return this.filterTree(this.mainMenu, q)
|
|
105
114
|
},
|
|
115
|
+
favoriteItems() {
|
|
116
|
+
const favorites = []
|
|
117
|
+
const collect = (items) => {
|
|
118
|
+
for (const item of items) {
|
|
119
|
+
if (item?.children?.length) {
|
|
120
|
+
collect(item.children)
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
if (this.favoriteIds.has(item.id)) favorites.push(item)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
collect(this.mainMenu)
|
|
127
|
+
return favorites
|
|
128
|
+
},
|
|
129
|
+
hasFavorites() {
|
|
130
|
+
return this.favoriteItems.length > 0
|
|
131
|
+
},
|
|
132
|
+
showMenuTabs() {
|
|
133
|
+
return !this.collapsed && this.hasFavorites
|
|
134
|
+
},
|
|
135
|
+
displayedMenuItems() {
|
|
136
|
+
if (this.activeMenuTab === 'favorites') return this.favoriteItems
|
|
137
|
+
return this.filteredMenu
|
|
138
|
+
},
|
|
139
|
+
collapsedMenuItems() {
|
|
140
|
+
if (!this.collapsed) return this.displayedMenuItems
|
|
141
|
+
if (!this.hasFavorites) return this.mainMenu
|
|
142
|
+
return [
|
|
143
|
+
{
|
|
144
|
+
id: 'favorites-shortcut',
|
|
145
|
+
label: 'Αγαπημένα',
|
|
146
|
+
icon: 'Star',
|
|
147
|
+
syntheticFavoritesShortcut: true,
|
|
148
|
+
children: this.favoriteItems,
|
|
149
|
+
},
|
|
150
|
+
...this.mainMenu,
|
|
151
|
+
]
|
|
152
|
+
},
|
|
106
153
|
effectiveOpenIds() {
|
|
107
154
|
const q = this.query.trim().toLowerCase()
|
|
108
155
|
if (!q) return this.openIds
|
|
@@ -121,10 +168,31 @@ export default {
|
|
|
121
168
|
expandedFooterLinks() {
|
|
122
169
|
return this.footerLinks.filter(link => link.id !== 'my-profile')
|
|
123
170
|
},
|
|
171
|
+
accountMenuLinks() {
|
|
172
|
+
return [
|
|
173
|
+
{
|
|
174
|
+
label: 'Το προφίλ μου',
|
|
175
|
+
icon: 'UserRound',
|
|
176
|
+
action: () => this.handleFooterSelect('my-profile'),
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
label: 'Βοήθεια',
|
|
180
|
+
icon: 'CircleHelp',
|
|
181
|
+
action: () => this.handleFooterSelect('help'),
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
label: 'Αποσύνδεση',
|
|
185
|
+
icon: 'LogOut',
|
|
186
|
+
tone: 'danger',
|
|
187
|
+
action: () => this.handleFooterSelect('sign-out'),
|
|
188
|
+
},
|
|
189
|
+
]
|
|
190
|
+
},
|
|
124
191
|
},
|
|
125
192
|
watch: {
|
|
126
193
|
mainMenu: {
|
|
127
194
|
handler() {
|
|
195
|
+
this.syncFavoritesFromMenu()
|
|
128
196
|
this.syncActiveFromMenu()
|
|
129
197
|
},
|
|
130
198
|
deep: true,
|
|
@@ -133,8 +201,36 @@ export default {
|
|
|
133
201
|
$route() {
|
|
134
202
|
this.syncActiveFromMenu()
|
|
135
203
|
},
|
|
204
|
+
favoriteIds: {
|
|
205
|
+
handler(value) {
|
|
206
|
+
this.persistFavorites(value)
|
|
207
|
+
if (!value.size && this.activeMenuTab === 'favorites') this.activeMenuTab = 'all'
|
|
208
|
+
},
|
|
209
|
+
deep: false,
|
|
210
|
+
},
|
|
211
|
+
activeMenuTab(value) {
|
|
212
|
+
this.persistActiveMenuTab(value)
|
|
213
|
+
},
|
|
136
214
|
},
|
|
137
215
|
methods: {
|
|
216
|
+
syncFavoritesFromMenu() {
|
|
217
|
+
const storedFavorites = this.readStoredFavorites()
|
|
218
|
+
const storedTab = this.readStoredActiveMenuTab()
|
|
219
|
+
const discovered = []
|
|
220
|
+
const collect = (items) => {
|
|
221
|
+
for (const item of items) {
|
|
222
|
+
if (item?.favorite && !item?.children?.length) discovered.push(item.id)
|
|
223
|
+
if (item?.children?.length) collect(item.children)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
collect(this.mainMenu)
|
|
227
|
+
this.favoriteIds = new Set([...discovered, ...storedFavorites])
|
|
228
|
+
if (storedTab === 'favorites' && (this.favoriteIds.size > 0 || discovered.length > 0)) {
|
|
229
|
+
this.activeMenuTab = 'favorites'
|
|
230
|
+
} else {
|
|
231
|
+
this.activeMenuTab = 'all'
|
|
232
|
+
}
|
|
233
|
+
},
|
|
138
234
|
syncActiveFromMenu() {
|
|
139
235
|
const routePath = this.$route?.path
|
|
140
236
|
const routeFullPath = this.$route?.fullPath
|
|
@@ -175,8 +271,11 @@ export default {
|
|
|
175
271
|
this.collapsed = !this.collapsed
|
|
176
272
|
},
|
|
177
273
|
toggle(id) {
|
|
178
|
-
if (this.openIds.has(id))
|
|
179
|
-
|
|
274
|
+
if (this.openIds.has(id)) {
|
|
275
|
+
this.openIds = new Set()
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
this.openIds = new Set([id])
|
|
180
279
|
},
|
|
181
280
|
select(itemOrId) {
|
|
182
281
|
const id = typeof itemOrId === 'string' ? itemOrId : itemOrId?.id
|
|
@@ -239,6 +338,57 @@ export default {
|
|
|
239
338
|
clearQuery() {
|
|
240
339
|
this.query = ''
|
|
241
340
|
},
|
|
341
|
+
toggleFavorite(item) {
|
|
342
|
+
const next = new Set(this.favoriteIds)
|
|
343
|
+
if (next.has(item.id)) next.delete(item.id)
|
|
344
|
+
else next.add(item.id)
|
|
345
|
+
this.favoriteIds = next
|
|
346
|
+
if (next.size && this.activeMenuTab !== 'favorites' && this.activeMenuTab !== 'all') {
|
|
347
|
+
this.activeMenuTab = 'all'
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
readStoredFavorites() {
|
|
351
|
+
if (typeof window === 'undefined') return []
|
|
352
|
+
try {
|
|
353
|
+
const raw = window.localStorage.getItem(FAVORITES_STORAGE_KEY)
|
|
354
|
+
if (!raw) return []
|
|
355
|
+
const parsed = JSON.parse(raw)
|
|
356
|
+
return Array.isArray(parsed) ? parsed.filter(value => typeof value === 'string') : []
|
|
357
|
+
} catch {
|
|
358
|
+
return []
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
persistFavorites(favorites) {
|
|
362
|
+
if (typeof window === 'undefined') return
|
|
363
|
+
try {
|
|
364
|
+
window.localStorage.setItem(
|
|
365
|
+
FAVORITES_STORAGE_KEY,
|
|
366
|
+
JSON.stringify(Array.from(favorites))
|
|
367
|
+
)
|
|
368
|
+
} catch {
|
|
369
|
+
// Ignore storage failures silently.
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
readStoredActiveMenuTab() {
|
|
373
|
+
if (typeof window === 'undefined') return 'all'
|
|
374
|
+
try {
|
|
375
|
+
const raw = window.localStorage.getItem(MENU_TAB_STORAGE_KEY)
|
|
376
|
+
return raw === 'favorites' ? 'favorites' : 'all'
|
|
377
|
+
} catch {
|
|
378
|
+
return 'all'
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
persistActiveMenuTab(value) {
|
|
382
|
+
if (typeof window === 'undefined') return
|
|
383
|
+
try {
|
|
384
|
+
window.localStorage.setItem(
|
|
385
|
+
MENU_TAB_STORAGE_KEY,
|
|
386
|
+
value === 'favorites' ? 'favorites' : 'all'
|
|
387
|
+
)
|
|
388
|
+
} catch {
|
|
389
|
+
// Ignore storage failures silently.
|
|
390
|
+
}
|
|
391
|
+
},
|
|
242
392
|
},
|
|
243
393
|
}
|
|
244
394
|
</script>
|
|
@@ -302,79 +452,90 @@ export default {
|
|
|
302
452
|
</div>
|
|
303
453
|
</div>
|
|
304
454
|
|
|
305
|
-
<div v-if="!collapsed" class="px-3 pb-2">
|
|
306
|
-
<div class="relative">
|
|
307
|
-
<Search
|
|
308
|
-
class="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-sidebar-muted"
|
|
309
|
-
:stroke-width="2"
|
|
310
|
-
/>
|
|
311
|
-
<input
|
|
312
|
-
v-model="query"
|
|
313
|
-
type="text"
|
|
314
|
-
placeholder="Αναζήτηση μενού..."
|
|
315
|
-
aria-label="Αναζήτηση μενού"
|
|
316
|
-
class="w-full border rounded-md border-sidebar-border/35 bg-sidebar-accent/40 py-2 pl-8 pr-9 text-sidebar-foreground placeholder:text-sidebar-muted focus:border-white/15 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
317
|
-
/>
|
|
318
|
-
<button
|
|
319
|
-
v-if="query"
|
|
320
|
-
type="button"
|
|
321
|
-
class="absolute right-2 top-1/2 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md text-sidebar-muted transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground"
|
|
322
|
-
aria-label="Καθαρισμός αναζήτησης"
|
|
323
|
-
@click="clearQuery"
|
|
324
|
-
>
|
|
325
|
-
<X class="size-3.5" :stroke-width="2" />
|
|
326
|
-
</button>
|
|
327
|
-
</div>
|
|
328
|
-
</div>
|
|
329
|
-
|
|
330
455
|
<nav
|
|
331
456
|
class="flex-1 px-3 py-2"
|
|
332
457
|
:class="collapsed ? 'overflow-visible' : 'overflow-y-auto'"
|
|
333
458
|
aria-label="Main navigation"
|
|
334
459
|
>
|
|
460
|
+
<div v-if="showMenuTabs" class="mb-3 px-1">
|
|
461
|
+
<div class="inline-flex w-full rounded-lg bg-sidebar-accent/35 p-1">
|
|
462
|
+
<button
|
|
463
|
+
type="button"
|
|
464
|
+
class="flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors"
|
|
465
|
+
:class="activeMenuTab === 'all'
|
|
466
|
+
? 'bg-sidebar text-sidebar-foreground shadow-sm'
|
|
467
|
+
: 'text-sidebar-muted hover:text-sidebar-foreground'"
|
|
468
|
+
@click="activeMenuTab = 'all'"
|
|
469
|
+
>
|
|
470
|
+
Όλα
|
|
471
|
+
</button>
|
|
472
|
+
<button
|
|
473
|
+
type="button"
|
|
474
|
+
class="flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors"
|
|
475
|
+
:class="activeMenuTab === 'favorites'
|
|
476
|
+
? 'bg-sidebar text-sidebar-foreground shadow-sm'
|
|
477
|
+
: 'text-sidebar-muted hover:text-sidebar-foreground'"
|
|
478
|
+
@click="activeMenuTab = 'favorites'"
|
|
479
|
+
>
|
|
480
|
+
Αγαπημένα
|
|
481
|
+
</button>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
|
|
485
|
+
<div v-if="!collapsed && (!showMenuTabs || activeMenuTab === 'all')" class="px-1 pb-3">
|
|
486
|
+
<div class="relative">
|
|
487
|
+
<Search
|
|
488
|
+
class="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-sidebar-muted"
|
|
489
|
+
:stroke-width="2"
|
|
490
|
+
/>
|
|
491
|
+
<input
|
|
492
|
+
v-model="query"
|
|
493
|
+
type="text"
|
|
494
|
+
placeholder="Αναζήτηση μενού..."
|
|
495
|
+
aria-label="Αναζήτηση μενού"
|
|
496
|
+
class="w-full border rounded-md border-sidebar-border/35 bg-sidebar-accent/40 py-2 pl-8 pr-9 text-sidebar-foreground placeholder:text-sidebar-muted focus:border-white/15 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
497
|
+
/>
|
|
498
|
+
<button
|
|
499
|
+
v-if="query"
|
|
500
|
+
type="button"
|
|
501
|
+
class="absolute right-2 top-1/2 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md text-sidebar-muted transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground"
|
|
502
|
+
aria-label="Καθαρισμός αναζήτησης"
|
|
503
|
+
@click="clearQuery"
|
|
504
|
+
>
|
|
505
|
+
<X class="size-3.5" :stroke-width="2" />
|
|
506
|
+
</button>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
|
|
335
510
|
<ul class="flex flex-col gap-0.5">
|
|
336
511
|
<SidebarMenuItem
|
|
337
|
-
v-for="item in
|
|
512
|
+
v-for="item in collapsed ? collapsedMenuItems : displayedMenuItems"
|
|
338
513
|
:key="item.id"
|
|
339
514
|
:item="item"
|
|
340
515
|
:level="0"
|
|
341
516
|
:active-id="activeId"
|
|
342
517
|
:open-ids="effectiveOpenIds"
|
|
343
518
|
:collapsed="collapsed"
|
|
519
|
+
:favorites="favoriteIds"
|
|
344
520
|
@select="handleMenuSelect"
|
|
345
521
|
@toggle="toggle"
|
|
522
|
+
@toggle-favorite="toggleFavorite"
|
|
346
523
|
/>
|
|
347
524
|
</ul>
|
|
348
525
|
<p
|
|
349
|
-
v-if="!collapsed &&
|
|
526
|
+
v-if="!collapsed && displayedMenuItems.length === 0"
|
|
350
527
|
class="px-3 py-6 text-center text-sidebar-muted"
|
|
351
528
|
>
|
|
352
529
|
Δεν βρέθηκαν αποτελέσματα.
|
|
353
530
|
</p>
|
|
354
531
|
</nav>
|
|
355
532
|
|
|
356
|
-
<div class="
|
|
357
|
-
<div
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
@click="handleFooterSelect('my-profile')"
|
|
363
|
-
>
|
|
364
|
-
<CircleUser class="sidebar-footer-item-icon size-4 shrink-0" :stroke-width="1.9" />
|
|
365
|
-
<span class="min-w-0 flex-1">
|
|
366
|
-
<span class="block truncate">Το προφίλ μου</span>
|
|
367
|
-
<span class="block truncate text-xs font-normal text-sidebar-muted">{{ accountEmail }}</span>
|
|
368
|
-
</span>
|
|
369
|
-
</button>
|
|
370
|
-
<button
|
|
371
|
-
type="button"
|
|
372
|
-
class="rounded-md p-1.5 text-sidebar-muted hover:bg-sidebar-accent hover:text-sidebar-foreground"
|
|
373
|
-
aria-label="Notifications"
|
|
374
|
-
>
|
|
375
|
-
<Bell class="size-4" :stroke-width="2" />
|
|
376
|
-
</button>
|
|
377
|
-
</div>
|
|
533
|
+
<div class="mt-auto border-t border-sidebar-border px-3 pb-3 pt-4">
|
|
534
|
+
<div
|
|
535
|
+
:class="collapsed
|
|
536
|
+
? 'flex flex-col items-center gap-1'
|
|
537
|
+
: ''"
|
|
538
|
+
>
|
|
378
539
|
<template v-if="collapsed">
|
|
379
540
|
<button
|
|
380
541
|
v-for="link in footerLinks"
|
|
@@ -408,35 +569,34 @@ export default {
|
|
|
408
569
|
</button>
|
|
409
570
|
</template>
|
|
410
571
|
<template v-else>
|
|
411
|
-
<
|
|
412
|
-
|
|
413
|
-
:
|
|
414
|
-
|
|
415
|
-
class="flex items-center w-full rounded-md transition-colors duration-150"
|
|
416
|
-
:class="[
|
|
417
|
-
'gap-2.5 px-2 py-2 text-left',
|
|
418
|
-
activeId === link.id
|
|
419
|
-
? 'bg-brand text-brand-foreground font-medium'
|
|
420
|
-
: link.id === 'sign-out'
|
|
421
|
-
? 'text-orange-300 hover:bg-orange-500/10 hover:text-orange-200'
|
|
422
|
-
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground',
|
|
423
|
-
]"
|
|
424
|
-
@click="handleFooterSelect(link.id)"
|
|
572
|
+
<kMenu
|
|
573
|
+
class="block w-full"
|
|
574
|
+
:links="accountMenuLinks"
|
|
575
|
+
aria-label="Account options"
|
|
425
576
|
>
|
|
426
|
-
<
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
577
|
+
<template #trigger>
|
|
578
|
+
<div class="flex w-full items-center gap-2 rounded-lg px-1 py-1 text-sidebar-foreground/90 transition-colors duration-150">
|
|
579
|
+
<div class="sidebar-footer-item flex min-h-[44px] flex-1 items-center gap-2.5 rounded-lg bg-transparent px-1.5 py-1.5 text-left font-medium">
|
|
580
|
+
<img
|
|
581
|
+
:src="demoAvatarUrl"
|
|
582
|
+
alt="Account avatar"
|
|
583
|
+
class="size-8 shrink-0 rounded-md object-cover"
|
|
584
|
+
/>
|
|
585
|
+
<span class="min-w-0 flex-1">
|
|
586
|
+
<span class="block truncate text-sm font-semibold">{{ accountDisplayName || 'Το προφίλ μου' }}</span>
|
|
587
|
+
<span class="block truncate text-xs font-normal text-sidebar-muted">{{ accountEmail }}</span>
|
|
588
|
+
</span>
|
|
589
|
+
</div>
|
|
590
|
+
|
|
591
|
+
<span
|
|
592
|
+
class="inline-flex size-9 items-center justify-center rounded-lg text-sidebar-muted"
|
|
593
|
+
aria-hidden="true"
|
|
594
|
+
>
|
|
595
|
+
<Ellipsis class="size-4" :stroke-width="2" />
|
|
596
|
+
</span>
|
|
597
|
+
</div>
|
|
598
|
+
</template>
|
|
599
|
+
</kMenu>
|
|
440
600
|
</template>
|
|
441
601
|
</div>
|
|
442
602
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { ChevronRight, ExternalLink } from '@lucide/vue'
|
|
2
|
+
import { ChevronRight, ExternalLink, Star } from '@lucide/vue'
|
|
3
3
|
import * as icons from '@lucide/vue'
|
|
4
4
|
|
|
5
5
|
export default {
|
|
@@ -7,6 +7,7 @@ export default {
|
|
|
7
7
|
components: {
|
|
8
8
|
ChevronRight,
|
|
9
9
|
ExternalLink,
|
|
10
|
+
Star,
|
|
10
11
|
},
|
|
11
12
|
props: {
|
|
12
13
|
item: { type: Object, required: true },
|
|
@@ -14,8 +15,9 @@ export default {
|
|
|
14
15
|
activeId: { type: String, default: '' },
|
|
15
16
|
openIds: { type: Object, required: true },
|
|
16
17
|
collapsed: { type: Boolean, default: false },
|
|
18
|
+
favorites: { type: Object, default: null },
|
|
17
19
|
},
|
|
18
|
-
emits: ['select', 'toggle'],
|
|
20
|
+
emits: ['select', 'toggle', 'toggle-favorite'],
|
|
19
21
|
computed: {
|
|
20
22
|
hasChildren() {
|
|
21
23
|
return Array.isArray(this.item.children) && this.item.children.length > 0
|
|
@@ -27,6 +29,7 @@ export default {
|
|
|
27
29
|
return this.item.icon ? icons[this.item.icon] : null
|
|
28
30
|
},
|
|
29
31
|
isActiveTrail() {
|
|
32
|
+
if (this.item?.syntheticFavoritesShortcut) return false
|
|
30
33
|
return this.hasChildren && this.containsActive(this.item)
|
|
31
34
|
},
|
|
32
35
|
isActiveLeaf() {
|
|
@@ -41,6 +44,9 @@ export default {
|
|
|
41
44
|
isExternalLink() {
|
|
42
45
|
return !this.hasChildren && Boolean(this.item?.link?.href ?? this.item?.href)
|
|
43
46
|
},
|
|
47
|
+
isFavorite() {
|
|
48
|
+
return !this.hasChildren && this.favorites?.has?.(this.item.id)
|
|
49
|
+
},
|
|
44
50
|
indentStyle() {
|
|
45
51
|
return {
|
|
46
52
|
paddingLeft: this.level === 0 ? '0.75rem' : `${this.level * 0.875 + 0.75}rem`,
|
|
@@ -60,6 +66,12 @@ export default {
|
|
|
60
66
|
this.$emit('select', this.item)
|
|
61
67
|
}
|
|
62
68
|
},
|
|
69
|
+
toggleFavorite(event) {
|
|
70
|
+
event.preventDefault()
|
|
71
|
+
event.stopPropagation()
|
|
72
|
+
if (this.hasChildren) return
|
|
73
|
+
this.$emit('toggle-favorite', this.item)
|
|
74
|
+
},
|
|
63
75
|
onEnter(el) {
|
|
64
76
|
el.style.height = '0'
|
|
65
77
|
el.offsetHeight
|
|
@@ -131,6 +143,21 @@ export default {
|
|
|
131
143
|
:stroke-width="2"
|
|
132
144
|
/>
|
|
133
145
|
|
|
146
|
+
<button
|
|
147
|
+
v-if="!hasChildren"
|
|
148
|
+
type="button"
|
|
149
|
+
class="inline-flex size-7 shrink-0 items-center justify-center rounded-md text-sidebar-muted opacity-0 transition-all duration-150 group-hover:opacity-100 hover:bg-sidebar-accent hover:text-sidebar-foreground focus-visible:opacity-100 focus-visible:outline-none"
|
|
150
|
+
:class="isFavorite ? 'opacity-100 text-amber-400 hover:text-amber-300' : ''"
|
|
151
|
+
:aria-label="isFavorite ? 'Remove from favorites' : 'Add to favorites'"
|
|
152
|
+
@click="toggleFavorite"
|
|
153
|
+
>
|
|
154
|
+
<Star
|
|
155
|
+
class="size-3.5"
|
|
156
|
+
:class="isFavorite ? 'fill-current' : ''"
|
|
157
|
+
:stroke-width="2"
|
|
158
|
+
/>
|
|
159
|
+
</button>
|
|
160
|
+
|
|
134
161
|
<ChevronRight
|
|
135
162
|
v-if="hasChildren"
|
|
136
163
|
class="size-4 shrink-0 text-sidebar-muted transition-transform duration-200"
|
|
@@ -165,8 +192,10 @@ export default {
|
|
|
165
192
|
:active-id="activeId"
|
|
166
193
|
:open-ids="openIds"
|
|
167
194
|
:collapsed="collapsed"
|
|
195
|
+
:favorites="favorites"
|
|
168
196
|
@select="$emit('select', $event)"
|
|
169
197
|
@toggle="$emit('toggle', $event)"
|
|
198
|
+
@toggle-favorite="$emit('toggle-favorite', $event)"
|
|
170
199
|
/>
|
|
171
200
|
</ul>
|
|
172
201
|
</Transition>
|
|
@@ -209,8 +238,10 @@ export default {
|
|
|
209
238
|
:active-id="activeId"
|
|
210
239
|
:open-ids="openIds"
|
|
211
240
|
:collapsed="false"
|
|
241
|
+
:favorites="favorites"
|
|
212
242
|
@select="$emit('select', $event)"
|
|
213
243
|
@toggle="$emit('toggle', $event)"
|
|
244
|
+
@toggle-favorite="$emit('toggle-favorite', $event)"
|
|
214
245
|
/>
|
|
215
246
|
</ul>
|
|
216
247
|
</div>
|
package/src/ui/kMenu.vue
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
<div
|
|
25
25
|
v-if="isOpen"
|
|
26
26
|
ref="menuPanelRef"
|
|
27
|
-
class="z-[9999] overflow-
|
|
27
|
+
class="z-[9999] overflow-visible border rounded-lg shadow-lg bg-white/95 backdrop-blur-sm border-primary/20 ring-1 ring-primary/10 dark:bg-slate-800/95 dark:border-slate-700 dark:ring-slate-700"
|
|
28
28
|
:style="panelStyle"
|
|
29
29
|
role="menu">
|
|
30
30
|
<span
|
|
@@ -36,30 +36,28 @@
|
|
|
36
36
|
: 'top-[-7px] border-l border-t border-primary/20',
|
|
37
37
|
]"
|
|
38
38
|
:style="{ left: `${chevronOffset}px` }"></span>
|
|
39
|
-
<
|
|
40
|
-
<
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
link
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
</ul>
|
|
62
|
-
</slot>
|
|
39
|
+
<div class="max-h-[70vh] overflow-y-auto overflow-x-hidden rounded-lg">
|
|
40
|
+
<slot name="items" :close-menu="closeMenu" :close="closeMenu" :select="handleLinkClick">
|
|
41
|
+
<ul class="p-2">
|
|
42
|
+
<li v-for="(link, index) in normalizedLinks" :key="link.key || `${link.label}-${index}`">
|
|
43
|
+
<component
|
|
44
|
+
:is="resolveLinkTag(link)"
|
|
45
|
+
class="flex items-center w-full gap-2 px-3 py-2 min-h-11 text-base rounded-md"
|
|
46
|
+
:class="itemClass(link)"
|
|
47
|
+
:to="isRouterLink(link) ? link.to : undefined"
|
|
48
|
+
:href="!isRouterLink(link) && link.href ? link.href : undefined"
|
|
49
|
+
:target="!isRouterLink(link) && link.target ? link.target : undefined"
|
|
50
|
+
:rel="!isRouterLink(link) ? resolvedRel(link) : undefined"
|
|
51
|
+
:disabled="resolveLinkTag(link) === 'button' ? !!link.disabled : undefined"
|
|
52
|
+
role="menuitem"
|
|
53
|
+
@click="handleLinkClick(link, $event)">
|
|
54
|
+
<kIcon v-if="link.icon" :name="link.icon" :size="16" class="shrink-0 self-center" :class="iconClass(link)" />
|
|
55
|
+
<span class="leading-none">{{ link.label }}</span>
|
|
56
|
+
</component>
|
|
57
|
+
</li>
|
|
58
|
+
</ul>
|
|
59
|
+
</slot>
|
|
60
|
+
</div>
|
|
63
61
|
</div>
|
|
64
62
|
</Teleport>
|
|
65
63
|
</div>
|
|
@@ -126,6 +124,17 @@ export default {
|
|
|
126
124
|
if (!link?.target || link.target !== "_blank") return link?.rel || undefined;
|
|
127
125
|
return link?.rel || "noopener noreferrer";
|
|
128
126
|
},
|
|
127
|
+
itemClass(link) {
|
|
128
|
+
if (link?.disabled) return "cursor-not-allowed opacity-50 text-slate-700 dark:text-slate-200";
|
|
129
|
+
if (link?.tone === "danger") {
|
|
130
|
+
return "cursor-pointer text-orange-600 hover:bg-orange-500/10 hover:text-orange-700 active:bg-orange-500/15 dark:text-orange-300 dark:hover:bg-orange-500/10 dark:hover:text-orange-200";
|
|
131
|
+
}
|
|
132
|
+
return "cursor-pointer text-slate-700 hover:bg-primary/10 hover:text-primary active:bg-primary/15 dark:text-slate-200";
|
|
133
|
+
},
|
|
134
|
+
iconClass(link) {
|
|
135
|
+
if (link?.tone === "danger") return "text-orange-500 dark:text-orange-300";
|
|
136
|
+
return "text-primary/80";
|
|
137
|
+
},
|
|
129
138
|
handleLinkClick(link, event) {
|
|
130
139
|
if (link?.disabled) {
|
|
131
140
|
event.preventDefault();
|