ketekny-ui-kit 1.0.90 → 1.0.91
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
package/src/style/style.css
CHANGED
|
@@ -1,48 +1,17 @@
|
|
|
1
|
-
@import url(
|
|
1
|
+
@import url('https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;500;700&display=swap');
|
|
2
2
|
|
|
3
3
|
@tailwind base;
|
|
4
4
|
@tailwind components;
|
|
5
5
|
@tailwind utilities;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
h1, h2, h3, h4{
|
|
13
|
-
color: theme('colors.primary');
|
|
14
|
-
font-weight: bold;
|
|
15
|
-
line-height: 1.2;
|
|
16
|
-
}
|
|
17
|
-
h1{
|
|
18
|
-
font-size: clamp(1.625rem, 4vw + 0.75rem, 2.5rem);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
h2{
|
|
22
|
-
font-size: clamp(1.375rem, 3vw + 0.7rem, 2rem);
|
|
23
|
-
}
|
|
24
|
-
h3{
|
|
25
|
-
font-size: clamp(1.125rem, 2vw + 0.6rem, 1.5rem);
|
|
7
|
+
html,
|
|
8
|
+
body,
|
|
9
|
+
#app {
|
|
10
|
+
height: 100%;
|
|
26
11
|
}
|
|
27
|
-
h4{
|
|
28
|
-
font-size: clamp(1rem, 1.5vw + 0.55rem, 1.25rem);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.group{
|
|
32
|
-
border: 1px dotted #ccc;
|
|
33
|
-
padding: 10px;
|
|
34
|
-
border-radius: 5px;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.inputLabel{
|
|
38
|
-
font-weight: bold;
|
|
39
|
-
color: theme('colors.primary');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
.menuList{
|
|
43
|
-
@apply p-2;
|
|
44
|
-
li{
|
|
45
|
-
@apply p-2 rounded cursor-pointer ;
|
|
46
12
|
|
|
47
|
-
|
|
13
|
+
body {
|
|
14
|
+
margin: 0;
|
|
15
|
+
font-family: 'Ubuntu', ui-sans-serif, system-ui, sans-serif;
|
|
16
|
+
-webkit-font-smoothing: antialiased;
|
|
48
17
|
}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import {
|
|
3
|
+
Search,
|
|
4
|
+
Bell,
|
|
5
|
+
Building2,
|
|
6
|
+
CircleUser,
|
|
7
|
+
CircleHelp,
|
|
8
|
+
X,
|
|
9
|
+
LogOut,
|
|
10
|
+
PanelLeftClose,
|
|
11
|
+
PanelLeftOpen,
|
|
12
|
+
UserRound,
|
|
13
|
+
} from '@lucide/vue'
|
|
14
|
+
import SidebarMenuItem from './SidebarMenuItem.vue'
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
name: 'MultilevelSidebar',
|
|
18
|
+
components: {
|
|
19
|
+
Search,
|
|
20
|
+
Bell,
|
|
21
|
+
Building2,
|
|
22
|
+
CircleUser,
|
|
23
|
+
CircleHelp,
|
|
24
|
+
X,
|
|
25
|
+
LogOut,
|
|
26
|
+
PanelLeftClose,
|
|
27
|
+
PanelLeftOpen,
|
|
28
|
+
UserRound,
|
|
29
|
+
SidebarMenuItem,
|
|
30
|
+
},
|
|
31
|
+
props: {
|
|
32
|
+
title: {
|
|
33
|
+
type: String,
|
|
34
|
+
default: "",
|
|
35
|
+
},
|
|
36
|
+
width: {
|
|
37
|
+
type: String,
|
|
38
|
+
default: "360px",
|
|
39
|
+
},
|
|
40
|
+
collapsedWidth: {
|
|
41
|
+
type: String,
|
|
42
|
+
default: "76px",
|
|
43
|
+
},
|
|
44
|
+
mainMenu: {
|
|
45
|
+
type: Array,
|
|
46
|
+
default: () => [],
|
|
47
|
+
},
|
|
48
|
+
account: {
|
|
49
|
+
type: Object,
|
|
50
|
+
default: null,
|
|
51
|
+
},
|
|
52
|
+
contactEmail: {
|
|
53
|
+
type: String,
|
|
54
|
+
default: "grammateia@ketekny.gr",
|
|
55
|
+
},
|
|
56
|
+
helpUrl: {
|
|
57
|
+
type: String,
|
|
58
|
+
default: "https://help.ketekny.gr",
|
|
59
|
+
},
|
|
60
|
+
mobileOpen: {
|
|
61
|
+
type: Boolean,
|
|
62
|
+
default: false,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
emits: ['select', 'logo-click', 'signin', 'signup', 'signout', 'edit-profile', 'item-click'],
|
|
66
|
+
data() {
|
|
67
|
+
return {
|
|
68
|
+
activeId: 'dashboard',
|
|
69
|
+
query: '',
|
|
70
|
+
collapsed: false,
|
|
71
|
+
openIds: new Set(['projects']),
|
|
72
|
+
footerLinks: [
|
|
73
|
+
{ id: 'my-profile', label: 'Το προφίλ μου', icon: 'UserRound' },
|
|
74
|
+
{ id: 'help', label: 'Βοήθεια', icon: 'CircleHelp' },
|
|
75
|
+
{ id: 'sign-out', label: 'Αποσύνδεση', icon: 'LogOut' },
|
|
76
|
+
],
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
computed: {
|
|
80
|
+
sidebarWidthStyle() {
|
|
81
|
+
return {
|
|
82
|
+
width: this.collapsed ? this.collapsedWidth : this.width,
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
collapsedTitle() {
|
|
86
|
+
if (!this.title) return ''
|
|
87
|
+
const words = this.title.trim().split(/\s+/).filter(Boolean)
|
|
88
|
+
return words.slice(0, 3).map(word => word.charAt(0)).join('').toUpperCase()
|
|
89
|
+
},
|
|
90
|
+
accountDisplayName() {
|
|
91
|
+
if (!this.account) return 'Ava Mitchell'
|
|
92
|
+
return [this.account.firstName, this.account.lastName].filter(Boolean).join(' ') || 'Account'
|
|
93
|
+
},
|
|
94
|
+
accountEmail() {
|
|
95
|
+
return this.account?.email || 'ava@nimbus.io'
|
|
96
|
+
},
|
|
97
|
+
filteredMenu() {
|
|
98
|
+
const q = this.query.trim().toLowerCase()
|
|
99
|
+
if (!q) return this.mainMenu
|
|
100
|
+
return this.filterTree(this.mainMenu, q)
|
|
101
|
+
},
|
|
102
|
+
effectiveOpenIds() {
|
|
103
|
+
const q = this.query.trim().toLowerCase()
|
|
104
|
+
if (!q) return this.openIds
|
|
105
|
+
const all = new Set()
|
|
106
|
+
const collect = (nodes) => {
|
|
107
|
+
for (const node of nodes) {
|
|
108
|
+
if (node.children) {
|
|
109
|
+
all.add(node.id)
|
|
110
|
+
collect(node.children)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
collect(this.filteredMenu)
|
|
115
|
+
return all
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
methods: {
|
|
119
|
+
toggleCollapsed() {
|
|
120
|
+
this.collapsed = !this.collapsed
|
|
121
|
+
},
|
|
122
|
+
toggle(id) {
|
|
123
|
+
if (this.openIds.has(id)) this.openIds.delete(id)
|
|
124
|
+
else this.openIds.add(id)
|
|
125
|
+
},
|
|
126
|
+
select(itemOrId) {
|
|
127
|
+
const id = typeof itemOrId === 'string' ? itemOrId : itemOrId?.id
|
|
128
|
+
if (!id) return
|
|
129
|
+
this.activeId = id
|
|
130
|
+
this.$emit('select', id)
|
|
131
|
+
},
|
|
132
|
+
navigateItem(item) {
|
|
133
|
+
const to = item?.link?.to ?? item?.to
|
|
134
|
+
const href = item?.link?.href ?? item?.href
|
|
135
|
+
|
|
136
|
+
if (to && this.$router) {
|
|
137
|
+
this.$router.push(to).catch(() => {})
|
|
138
|
+
return true
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (href) {
|
|
142
|
+
window.open(href, '_blank', 'noopener,noreferrer')
|
|
143
|
+
return true
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return false
|
|
147
|
+
},
|
|
148
|
+
filterTree(nodes, q) {
|
|
149
|
+
const result = []
|
|
150
|
+
for (const node of nodes) {
|
|
151
|
+
const matches = node.label.toLowerCase().includes(q)
|
|
152
|
+
const matchedChildren = node.children ? this.filterTree(node.children, q) : []
|
|
153
|
+
if (matches) result.push(node)
|
|
154
|
+
else if (matchedChildren.length) result.push({ ...node, children: matchedChildren })
|
|
155
|
+
}
|
|
156
|
+
return result
|
|
157
|
+
},
|
|
158
|
+
handleFooterSelect(id) {
|
|
159
|
+
if (id === 'help') {
|
|
160
|
+
window.open(this.helpUrl || 'https://help.ketekny.gr', '_blank', 'noopener,noreferrer')
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
if (id === 'my-profile') {
|
|
164
|
+
this.$emit('edit-profile')
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
if (id === 'sign-out') {
|
|
168
|
+
this.$emit('signout')
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
this.select(id)
|
|
172
|
+
},
|
|
173
|
+
handleLogoClick() {
|
|
174
|
+
this.$emit('logo-click')
|
|
175
|
+
},
|
|
176
|
+
handleMenuSelect(item) {
|
|
177
|
+
const isExternalLink = Boolean(item?.link?.href ?? item?.href)
|
|
178
|
+
if (!isExternalLink) {
|
|
179
|
+
this.select(item)
|
|
180
|
+
}
|
|
181
|
+
this.$emit('item-click', item)
|
|
182
|
+
this.navigateItem(item)
|
|
183
|
+
},
|
|
184
|
+
clearQuery() {
|
|
185
|
+
this.query = ''
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
</script>
|
|
190
|
+
|
|
191
|
+
<template>
|
|
192
|
+
<aside
|
|
193
|
+
class="fixed inset-y-0 left-0 z-40 flex h-full shrink-0 flex-col border-r border-sidebar-border bg-sidebar text-sidebar-foreground transition-[width,transform] duration-200 ease-out lg:static lg:translate-x-0"
|
|
194
|
+
:style="sidebarWidthStyle"
|
|
195
|
+
:class="[
|
|
196
|
+
mobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0',
|
|
197
|
+
]"
|
|
198
|
+
>
|
|
199
|
+
<div class="flex items-center px-4 py-4" :class="collapsed ? 'justify-center' : 'gap-2.5'">
|
|
200
|
+
<img
|
|
201
|
+
:src="
|
|
202
|
+
collapsed
|
|
203
|
+
? 'https://s3.ketekny.gr/public/web-apps/logos/logo-icon-dark.png'
|
|
204
|
+
: 'https://support.ketekny.gr/logo-dark.png'
|
|
205
|
+
"
|
|
206
|
+
alt="Ketekny logo"
|
|
207
|
+
class="object-contain"
|
|
208
|
+
:class="collapsed ? 'size-9 shrink-0 rounded-lg' : 'h-10 min-w-0 flex-1'"
|
|
209
|
+
@click="handleLogoClick"
|
|
210
|
+
/>
|
|
211
|
+
<button
|
|
212
|
+
v-if="!collapsed"
|
|
213
|
+
type="button"
|
|
214
|
+
class="ml-auto hidden rounded-md p-1.5 text-sidebar-muted hover:bg-sidebar-accent hover:text-sidebar-foreground lg:inline-flex"
|
|
215
|
+
aria-label="Collapse sidebar"
|
|
216
|
+
@click="toggleCollapsed"
|
|
217
|
+
>
|
|
218
|
+
<PanelLeftClose class="size-4" :stroke-width="2" />
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div v-if="title && !collapsed" class="px-4 pb-2">
|
|
223
|
+
<p class="text-xl font-bold text-center text-sidebar-foreground">
|
|
224
|
+
{{ title }}
|
|
225
|
+
</p>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<div v-else-if="title && collapsed" class="px-3 pb-2">
|
|
229
|
+
<div
|
|
230
|
+
v-tooltip.right="title"
|
|
231
|
+
class="flex h-10 w-10 items-center justify-center rounded-md border border-sidebar-border/30 bg-sidebar-accent/30 text-[10px] font-bold tracking-[0.18em] text-sidebar-muted"
|
|
232
|
+
>
|
|
233
|
+
{{ collapsedTitle }}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<div v-if="collapsed" class="justify-center hidden px-3 pb-1 lg:flex">
|
|
238
|
+
<button
|
|
239
|
+
type="button"
|
|
240
|
+
v-tooltip.right="'Ανάπτυξη πλαϊνού μενού'"
|
|
241
|
+
class="flex h-10 w-10 items-center justify-center rounded-md text-sidebar-muted hover:bg-sidebar-accent hover:text-sidebar-foreground"
|
|
242
|
+
aria-label="Ανάπτυξη πλαϊνού μενού"
|
|
243
|
+
@click="toggleCollapsed"
|
|
244
|
+
>
|
|
245
|
+
<PanelLeftOpen class="size-4" :stroke-width="2" />
|
|
246
|
+
</button>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<div v-if="!collapsed" class="px-3 pb-2">
|
|
250
|
+
<div class="relative">
|
|
251
|
+
<Search
|
|
252
|
+
class="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-sidebar-muted"
|
|
253
|
+
:stroke-width="2"
|
|
254
|
+
/>
|
|
255
|
+
<input
|
|
256
|
+
v-model="query"
|
|
257
|
+
type="text"
|
|
258
|
+
placeholder="Αναζήτηση μενού..."
|
|
259
|
+
aria-label="Αναζήτηση μενού"
|
|
260
|
+
class="w-full py-2 pl-8 pr-9 text-sm border rounded-md border-sidebar-border/35 bg-sidebar-accent/40 text-sidebar-foreground placeholder:text-sidebar-muted focus:border-white/15 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
261
|
+
/>
|
|
262
|
+
<button
|
|
263
|
+
v-if="query"
|
|
264
|
+
type="button"
|
|
265
|
+
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"
|
|
266
|
+
aria-label="Καθαρισμός αναζήτησης"
|
|
267
|
+
@click="clearQuery"
|
|
268
|
+
>
|
|
269
|
+
<X class="size-3.5" :stroke-width="2" />
|
|
270
|
+
</button>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<nav
|
|
275
|
+
class="flex-1 px-3 py-2"
|
|
276
|
+
:class="collapsed ? 'overflow-visible' : 'overflow-y-auto'"
|
|
277
|
+
aria-label="Main navigation"
|
|
278
|
+
>
|
|
279
|
+
<ul class="flex flex-col gap-0.5">
|
|
280
|
+
<SidebarMenuItem
|
|
281
|
+
v-for="item in filteredMenu"
|
|
282
|
+
:key="item.id"
|
|
283
|
+
:item="item"
|
|
284
|
+
:level="0"
|
|
285
|
+
:active-id="activeId"
|
|
286
|
+
:open-ids="effectiveOpenIds"
|
|
287
|
+
:collapsed="collapsed"
|
|
288
|
+
@select="handleMenuSelect"
|
|
289
|
+
@toggle="toggle"
|
|
290
|
+
/>
|
|
291
|
+
</ul>
|
|
292
|
+
<p
|
|
293
|
+
v-if="!collapsed && filteredMenu.length === 0"
|
|
294
|
+
class="px-3 py-6 text-sm text-center text-sidebar-muted"
|
|
295
|
+
>
|
|
296
|
+
Δεν βρέθηκαν αποτελέσματα.
|
|
297
|
+
</p>
|
|
298
|
+
</nav>
|
|
299
|
+
|
|
300
|
+
<div class="p-3 mt-auto border-t border-sidebar-border">
|
|
301
|
+
<div
|
|
302
|
+
class="flex items-center py-2 rounded-md hover:bg-sidebar-accent"
|
|
303
|
+
:class="collapsed ? 'justify-center px-0' : 'gap-2.5 px-2'"
|
|
304
|
+
>
|
|
305
|
+
<CircleUser class="size-8 shrink-0 text-sidebar-muted" :stroke-width="1.75" />
|
|
306
|
+
<div v-if="!collapsed" class="flex-1 min-w-0">
|
|
307
|
+
<p class="text-sm font-medium truncate text-sidebar-foreground">{{ accountDisplayName }}</p>
|
|
308
|
+
<p class="text-xs truncate text-sidebar-muted">{{ accountEmail }}</p>
|
|
309
|
+
</div>
|
|
310
|
+
<button
|
|
311
|
+
v-if="!collapsed"
|
|
312
|
+
type="button"
|
|
313
|
+
class="rounded-md p-1.5 text-sidebar-muted hover:bg-sidebar-accent hover:text-sidebar-foreground"
|
|
314
|
+
aria-label="Notifications"
|
|
315
|
+
>
|
|
316
|
+
<Bell class="size-4" :stroke-width="2" />
|
|
317
|
+
</button>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<div
|
|
321
|
+
class="pt-2 mt-2 border-t border-sidebar-border/70"
|
|
322
|
+
:class="collapsed ? 'flex flex-col items-center gap-1' : 'space-y-1'"
|
|
323
|
+
>
|
|
324
|
+
<button
|
|
325
|
+
v-for="link in footerLinks"
|
|
326
|
+
:key="link.id"
|
|
327
|
+
v-if="collapsed"
|
|
328
|
+
type="button"
|
|
329
|
+
v-tooltip.right="link.label"
|
|
330
|
+
class="flex items-center w-full text-sm transition-colors duration-150 rounded-md"
|
|
331
|
+
:class="[
|
|
332
|
+
'justify-center px-0 py-2',
|
|
333
|
+
activeId === link.id
|
|
334
|
+
? 'bg-brand text-brand-foreground font-medium'
|
|
335
|
+
: link.id === 'sign-out'
|
|
336
|
+
? 'text-orange-300 hover:bg-orange-500/10 hover:text-orange-200'
|
|
337
|
+
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground',
|
|
338
|
+
]"
|
|
339
|
+
:aria-label="collapsed ? link.label : undefined"
|
|
340
|
+
@click="handleFooterSelect(link.id)"
|
|
341
|
+
>
|
|
342
|
+
<component
|
|
343
|
+
:is="link.icon"
|
|
344
|
+
class="size-4 shrink-0"
|
|
345
|
+
:class="
|
|
346
|
+
activeId === link.id
|
|
347
|
+
? 'text-brand-foreground'
|
|
348
|
+
: link.id === 'sign-out'
|
|
349
|
+
? 'text-orange-400'
|
|
350
|
+
: 'text-sidebar-muted'
|
|
351
|
+
"
|
|
352
|
+
:stroke-width="2"
|
|
353
|
+
/>
|
|
354
|
+
</button>
|
|
355
|
+
<button
|
|
356
|
+
v-for="link in footerLinks"
|
|
357
|
+
:key="`${link.id}-expanded`"
|
|
358
|
+
v-else
|
|
359
|
+
type="button"
|
|
360
|
+
class="flex items-center w-full text-sm transition-colors duration-150 rounded-md"
|
|
361
|
+
:class="[
|
|
362
|
+
'gap-2.5 px-2 py-2 text-left',
|
|
363
|
+
activeId === link.id
|
|
364
|
+
? 'bg-brand text-brand-foreground font-medium'
|
|
365
|
+
: link.id === 'sign-out'
|
|
366
|
+
? 'text-orange-300 hover:bg-orange-500/10 hover:text-orange-200'
|
|
367
|
+
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground',
|
|
368
|
+
]"
|
|
369
|
+
@click="handleFooterSelect(link.id)"
|
|
370
|
+
>
|
|
371
|
+
<component
|
|
372
|
+
:is="link.icon"
|
|
373
|
+
class="size-4 shrink-0"
|
|
374
|
+
:class="
|
|
375
|
+
activeId === link.id
|
|
376
|
+
? 'text-brand-foreground'
|
|
377
|
+
: link.id === 'sign-out'
|
|
378
|
+
? 'text-orange-400'
|
|
379
|
+
: 'text-sidebar-muted'
|
|
380
|
+
"
|
|
381
|
+
:stroke-width="2"
|
|
382
|
+
/>
|
|
383
|
+
<span class="truncate">{{ link.label }}</span>
|
|
384
|
+
</button>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
<div
|
|
388
|
+
v-if="!collapsed"
|
|
389
|
+
class="mt-3 border-t border-sidebar-border/70 px-2 pt-2 text-[11px] leading-snug text-sidebar-foreground/90"
|
|
390
|
+
>
|
|
391
|
+
<div class="flex items-center gap-1.5 text-sidebar-foreground">
|
|
392
|
+
<Building2 class="size-3 shrink-0 text-sidebar-muted" :stroke-width="2" />
|
|
393
|
+
<p class="font-semibold uppercase tracking-[0.28em]">KETEKNY AE</p>
|
|
394
|
+
</div>
|
|
395
|
+
<div class="mt-1.5 pl-[18px]">
|
|
396
|
+
<p class="truncate ">Βερανζέρου 13, 10677, Αθήνα
|
|
397
|
+
<span> </span>
|
|
398
|
+
<a href="tel:+302103648337" class="transition-colors hover:text-sidebar-foreground">
|
|
399
|
+
210 3648 337
|
|
400
|
+
</a></p>
|
|
401
|
+
|
|
402
|
+
<div class="flex flex-wrap items-center gap-x-3">
|
|
403
|
+
<a
|
|
404
|
+
:href="`mailto:${contactEmail}`"
|
|
405
|
+
class="transition-colors hover:text-sidebar-foreground"
|
|
406
|
+
>
|
|
407
|
+
{{ contactEmail }}
|
|
408
|
+
</a>
|
|
409
|
+
<a
|
|
410
|
+
:href="helpUrl || 'https://help.ketekny.gr'"
|
|
411
|
+
target="_blank"
|
|
412
|
+
rel="noreferrer"
|
|
413
|
+
class="truncate transition-colors hover:text-sidebar-foreground"
|
|
414
|
+
>
|
|
415
|
+
{{ helpUrl || 'https://help.ketekny.gr' }}
|
|
416
|
+
</a>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
</aside>
|
|
422
|
+
</template>
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { ChevronRight, ExternalLink } from '@lucide/vue'
|
|
3
|
+
import * as icons from '@lucide/vue'
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
name: 'SidebarMenuItem',
|
|
7
|
+
components: {
|
|
8
|
+
ChevronRight,
|
|
9
|
+
ExternalLink,
|
|
10
|
+
},
|
|
11
|
+
props: {
|
|
12
|
+
item: { type: Object, required: true },
|
|
13
|
+
level: { type: Number, default: 0 },
|
|
14
|
+
activeId: { type: String, default: '' },
|
|
15
|
+
openIds: { type: Object, required: true },
|
|
16
|
+
collapsed: { type: Boolean, default: false },
|
|
17
|
+
},
|
|
18
|
+
emits: ['select', 'toggle'],
|
|
19
|
+
computed: {
|
|
20
|
+
hasChildren() {
|
|
21
|
+
return Array.isArray(this.item.children) && this.item.children.length > 0
|
|
22
|
+
},
|
|
23
|
+
isOpen() {
|
|
24
|
+
return this.openIds.has(this.item.id)
|
|
25
|
+
},
|
|
26
|
+
IconComponent() {
|
|
27
|
+
return this.item.icon ? icons[this.item.icon] : null
|
|
28
|
+
},
|
|
29
|
+
isActiveTrail() {
|
|
30
|
+
return this.hasChildren && this.containsActive(this.item)
|
|
31
|
+
},
|
|
32
|
+
isActiveLeaf() {
|
|
33
|
+
return !this.hasChildren && this.item.id === this.activeId
|
|
34
|
+
},
|
|
35
|
+
isActive() {
|
|
36
|
+
return this.isActiveLeaf || this.isActiveTrail
|
|
37
|
+
},
|
|
38
|
+
isCollapsedTop() {
|
|
39
|
+
return this.collapsed && this.level === 0
|
|
40
|
+
},
|
|
41
|
+
isExternalLink() {
|
|
42
|
+
return !this.hasChildren && Boolean(this.item?.link?.href ?? this.item?.href)
|
|
43
|
+
},
|
|
44
|
+
indentStyle() {
|
|
45
|
+
return {
|
|
46
|
+
paddingLeft: this.level === 0 ? '0.75rem' : `${this.level * 0.875 + 0.75}rem`,
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
methods: {
|
|
51
|
+
containsActive(node) {
|
|
52
|
+
if (node.id === this.activeId) return true
|
|
53
|
+
if (!node.children) return false
|
|
54
|
+
return node.children.some((child) => this.containsActive(child))
|
|
55
|
+
},
|
|
56
|
+
handleClick() {
|
|
57
|
+
if (this.hasChildren && !this.isCollapsedTop) {
|
|
58
|
+
this.$emit('toggle', this.item.id)
|
|
59
|
+
} else if (!this.hasChildren) {
|
|
60
|
+
this.$emit('select', this.item)
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
onEnter(el) {
|
|
64
|
+
el.style.height = '0'
|
|
65
|
+
el.offsetHeight
|
|
66
|
+
el.style.height = `${el.scrollHeight}px`
|
|
67
|
+
},
|
|
68
|
+
onAfterEnter(el) {
|
|
69
|
+
el.style.height = 'auto'
|
|
70
|
+
},
|
|
71
|
+
onLeave(el) {
|
|
72
|
+
el.style.height = `${el.scrollHeight}px`
|
|
73
|
+
el.offsetHeight
|
|
74
|
+
el.style.height = '0'
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<template>
|
|
81
|
+
<li :class="isCollapsedTop ? 'group/fly relative' : ''">
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
:style="isCollapsedTop ? undefined : indentStyle"
|
|
85
|
+
class="group relative flex w-full items-center rounded-md text-left text-sm transition-colors duration-150"
|
|
86
|
+
:class="[
|
|
87
|
+
isCollapsedTop ? 'h-10 justify-center px-0' : 'gap-2.5 py-2 pr-2',
|
|
88
|
+
isActiveLeaf || (isCollapsedTop && isActive)
|
|
89
|
+
? 'bg-brand text-brand-foreground font-medium'
|
|
90
|
+
: isActiveTrail
|
|
91
|
+
? 'text-sidebar-foreground'
|
|
92
|
+
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground',
|
|
93
|
+
]"
|
|
94
|
+
:aria-expanded="hasChildren && !isCollapsedTop ? isOpen : undefined"
|
|
95
|
+
:aria-label="isCollapsedTop ? item.label : undefined"
|
|
96
|
+
@click="handleClick"
|
|
97
|
+
>
|
|
98
|
+
<span
|
|
99
|
+
v-if="isActiveLeaf && !isCollapsedTop"
|
|
100
|
+
class="absolute left-0 top-1/2 h-5 w-1 -translate-y-1/2 rounded-r bg-brand-foreground/80"
|
|
101
|
+
aria-hidden="true"
|
|
102
|
+
/>
|
|
103
|
+
|
|
104
|
+
<component
|
|
105
|
+
:is="IconComponent"
|
|
106
|
+
v-if="IconComponent"
|
|
107
|
+
class="size-4 shrink-0"
|
|
108
|
+
:class="[
|
|
109
|
+
isActiveLeaf || (isCollapsedTop && isActive)
|
|
110
|
+
? 'text-brand-foreground'
|
|
111
|
+
: 'text-sidebar-muted group-hover:text-sidebar-foreground',
|
|
112
|
+
]"
|
|
113
|
+
:stroke-width="2"
|
|
114
|
+
/>
|
|
115
|
+
|
|
116
|
+
<template v-if="!isCollapsedTop">
|
|
117
|
+
<span class="flex-1 truncate">{{ item.label }}</span>
|
|
118
|
+
|
|
119
|
+
<span
|
|
120
|
+
v-if="item.badge != null"
|
|
121
|
+
class="ml-auto rounded-full px-1.5 py-0.5 text-xs font-medium tabular-nums"
|
|
122
|
+
:class="isActiveLeaf ? 'bg-brand-foreground/20 text-brand-foreground' : 'bg-sidebar-accent text-sidebar-muted'"
|
|
123
|
+
>
|
|
124
|
+
{{ item.badge }}
|
|
125
|
+
</span>
|
|
126
|
+
|
|
127
|
+
<ExternalLink
|
|
128
|
+
v-if="isExternalLink"
|
|
129
|
+
class="size-3.5 shrink-0"
|
|
130
|
+
:class="isActiveLeaf ? 'text-brand-foreground' : 'text-sidebar-muted'"
|
|
131
|
+
:stroke-width="2"
|
|
132
|
+
/>
|
|
133
|
+
|
|
134
|
+
<ChevronRight
|
|
135
|
+
v-if="hasChildren"
|
|
136
|
+
class="size-4 shrink-0 text-sidebar-muted transition-transform duration-200"
|
|
137
|
+
:class="isOpen ? 'rotate-90' : ''"
|
|
138
|
+
:stroke-width="2"
|
|
139
|
+
/>
|
|
140
|
+
</template>
|
|
141
|
+
|
|
142
|
+
<span
|
|
143
|
+
v-if="isCollapsedTop && hasChildren"
|
|
144
|
+
class="absolute right-1.5 top-1.5 size-1.5 rounded-full"
|
|
145
|
+
:class="isActive ? 'bg-brand-foreground/80' : 'bg-sidebar-muted/60'"
|
|
146
|
+
aria-hidden="true"
|
|
147
|
+
/>
|
|
148
|
+
</button>
|
|
149
|
+
|
|
150
|
+
<Transition
|
|
151
|
+
v-if="!isCollapsedTop"
|
|
152
|
+
@enter="onEnter"
|
|
153
|
+
@after-enter="onAfterEnter"
|
|
154
|
+
@leave="onLeave"
|
|
155
|
+
>
|
|
156
|
+
<ul
|
|
157
|
+
v-if="hasChildren && isOpen"
|
|
158
|
+
class="overflow-hidden border-l border-sidebar-border/25 ml-4 pl-2 transition-[height] duration-200 ease-out"
|
|
159
|
+
>
|
|
160
|
+
<SidebarMenuItem
|
|
161
|
+
v-for="child in item.children"
|
|
162
|
+
:key="child.id"
|
|
163
|
+
:item="child"
|
|
164
|
+
:level="level + 1"
|
|
165
|
+
:active-id="activeId"
|
|
166
|
+
:open-ids="openIds"
|
|
167
|
+
@select="$emit('select', $event)"
|
|
168
|
+
@toggle="$emit('toggle', $event)"
|
|
169
|
+
/>
|
|
170
|
+
</ul>
|
|
171
|
+
</Transition>
|
|
172
|
+
|
|
173
|
+
<div
|
|
174
|
+
v-if="isCollapsedTop"
|
|
175
|
+
class="invisible absolute left-full top-0 z-50 pl-2 opacity-0 transition-opacity duration-150 group-hover/fly:visible group-hover/fly:opacity-100"
|
|
176
|
+
>
|
|
177
|
+
<div
|
|
178
|
+
class="min-w-56 rounded-lg border border-sidebar-border bg-sidebar p-2 shadow-xl shadow-black/40"
|
|
179
|
+
role="menu"
|
|
180
|
+
:aria-label="item.label"
|
|
181
|
+
>
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
class="mb-1 flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm font-semibold"
|
|
185
|
+
:class="isActive ? 'text-brand' : 'text-sidebar-foreground hover:bg-sidebar-accent'"
|
|
186
|
+
@click="!hasChildren && $emit('select', item)"
|
|
187
|
+
>
|
|
188
|
+
<component
|
|
189
|
+
:is="IconComponent"
|
|
190
|
+
v-if="IconComponent"
|
|
191
|
+
class="size-4 shrink-0"
|
|
192
|
+
:stroke-width="2"
|
|
193
|
+
/>
|
|
194
|
+
<span class="truncate">{{ item.label }}</span>
|
|
195
|
+
<ExternalLink
|
|
196
|
+
v-if="isExternalLink"
|
|
197
|
+
class="ml-auto size-3.5 shrink-0 text-sidebar-muted"
|
|
198
|
+
:stroke-width="2"
|
|
199
|
+
/>
|
|
200
|
+
</button>
|
|
201
|
+
|
|
202
|
+
<ul v-if="hasChildren" class="flex flex-col gap-0.5">
|
|
203
|
+
<SidebarMenuItem
|
|
204
|
+
v-for="child in item.children"
|
|
205
|
+
:key="child.id"
|
|
206
|
+
:item="child"
|
|
207
|
+
:level="0"
|
|
208
|
+
:active-id="activeId"
|
|
209
|
+
:open-ids="openIds"
|
|
210
|
+
@select="$emit('select', $event)"
|
|
211
|
+
@toggle="$emit('toggle', $event)"
|
|
212
|
+
/>
|
|
213
|
+
</ul>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</li>
|
|
217
|
+
</template>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex h-screen bg-canvas text-canvas-foreground">
|
|
3
|
+
<div v-if="mobileMenuOpen" class="fixed inset-0 z-30 bg-slate-950/50 backdrop-blur-[1px] lg:hidden" @click="closeMenu" />
|
|
4
|
+
|
|
5
|
+
<MultilevelSidebar
|
|
6
|
+
:title="title"
|
|
7
|
+
:width="width"
|
|
8
|
+
:collapsed-width="collapsedWidth"
|
|
9
|
+
:main-menu="mainMenu"
|
|
10
|
+
:mobile-open="mobileMenuOpen"
|
|
11
|
+
:account="account"
|
|
12
|
+
:contact-email="contactEmail"
|
|
13
|
+
:help-url="helpUrl"
|
|
14
|
+
@logo-click="handleLogoClick"
|
|
15
|
+
@signin="handleSignin"
|
|
16
|
+
@signup="handleSignup"
|
|
17
|
+
@signout="handleSignout"
|
|
18
|
+
@edit-profile="handleEditProfile"
|
|
19
|
+
@item-click="handleItemClick"
|
|
20
|
+
@select="handleSelect"
|
|
21
|
+
/>
|
|
22
|
+
|
|
23
|
+
<main class="flex flex-col flex-1 overflow-y-auto">
|
|
24
|
+
<slot />
|
|
25
|
+
</main>
|
|
26
|
+
|
|
27
|
+
<button
|
|
28
|
+
v-if="showBurger && !mobileMenuOpen"
|
|
29
|
+
type="button"
|
|
30
|
+
class="fixed z-50 inline-flex items-center justify-center text-white transition-colors rounded-full shadow-lg bottom-5 left-5 size-14 bg-sidebar shadow-black/25 hover:bg-sidebar-accent lg:hidden"
|
|
31
|
+
aria-label="Open menu"
|
|
32
|
+
@click="toggleMenu">
|
|
33
|
+
<Menu class="size-6" :stroke-width="2.25" />
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script>
|
|
39
|
+
import { Menu } from "@lucide/vue";
|
|
40
|
+
import MultilevelSidebar from "./MultilevelSidebar.vue";
|
|
41
|
+
|
|
42
|
+
export default {
|
|
43
|
+
name: "TwoColLayout",
|
|
44
|
+
components: {
|
|
45
|
+
Menu,
|
|
46
|
+
MultilevelSidebar,
|
|
47
|
+
},
|
|
48
|
+
props: {
|
|
49
|
+
title: {
|
|
50
|
+
type: String,
|
|
51
|
+
default: "",
|
|
52
|
+
},
|
|
53
|
+
width: {
|
|
54
|
+
type: String,
|
|
55
|
+
default: "360px",
|
|
56
|
+
},
|
|
57
|
+
collapsedWidth: {
|
|
58
|
+
type: String,
|
|
59
|
+
default: "76px",
|
|
60
|
+
},
|
|
61
|
+
mainMenu: {
|
|
62
|
+
type: Array,
|
|
63
|
+
default: () => [],
|
|
64
|
+
},
|
|
65
|
+
account: {
|
|
66
|
+
type: Object,
|
|
67
|
+
default: null,
|
|
68
|
+
},
|
|
69
|
+
contactEmail: {
|
|
70
|
+
type: String,
|
|
71
|
+
default: "grammateia@ketekny.gr",
|
|
72
|
+
},
|
|
73
|
+
helpUrl: {
|
|
74
|
+
type: String,
|
|
75
|
+
default: "https://help.ketekny.gr",
|
|
76
|
+
},
|
|
77
|
+
showBurger: {
|
|
78
|
+
type: Boolean,
|
|
79
|
+
default: true,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
emits: ["select-menu", "logo-click", "signin", "signup", "signout", "edit-profile", "item-click"],
|
|
83
|
+
data() {
|
|
84
|
+
return {
|
|
85
|
+
mobileMenuOpen: false,
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
methods: {
|
|
89
|
+
toggleMenu() {
|
|
90
|
+
this.mobileMenuOpen = !this.mobileMenuOpen;
|
|
91
|
+
},
|
|
92
|
+
closeMenu() {
|
|
93
|
+
this.mobileMenuOpen = false;
|
|
94
|
+
},
|
|
95
|
+
handleSelect(id) {
|
|
96
|
+
this.closeMenu();
|
|
97
|
+
this.$emit("select-menu", id);
|
|
98
|
+
},
|
|
99
|
+
handleLogoClick() {
|
|
100
|
+
this.closeMenu();
|
|
101
|
+
this.$emit("logo-click");
|
|
102
|
+
},
|
|
103
|
+
handleSignin() {
|
|
104
|
+
this.closeMenu();
|
|
105
|
+
this.$emit("signin");
|
|
106
|
+
},
|
|
107
|
+
handleSignup() {
|
|
108
|
+
this.closeMenu();
|
|
109
|
+
this.$emit("signup");
|
|
110
|
+
},
|
|
111
|
+
handleSignout() {
|
|
112
|
+
this.closeMenu();
|
|
113
|
+
this.$emit("signout");
|
|
114
|
+
},
|
|
115
|
+
handleEditProfile() {
|
|
116
|
+
this.closeMenu();
|
|
117
|
+
this.$emit("edit-profile");
|
|
118
|
+
},
|
|
119
|
+
handleItemClick(item) {
|
|
120
|
+
this.closeMenu();
|
|
121
|
+
this.$emit("item-click", item);
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
</script>
|
package/tailwind.config.js
CHANGED
|
@@ -6,6 +6,10 @@ export default {
|
|
|
6
6
|
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
|
7
7
|
"./preview/**/*.{vue,js,ts}",
|
|
8
8
|
],
|
|
9
|
+
fontFamily: {
|
|
10
|
+
sans: ['Ubuntu', 'ui-sans-serif', 'system-ui', 'sans-serif'],
|
|
11
|
+
mono: ['Geist Mono', 'ui-monospace', 'monospace'],
|
|
12
|
+
},
|
|
9
13
|
safelist: [
|
|
10
14
|
// Runtime-generated semantic classes used by alert/message/toast components.
|
|
11
15
|
{ pattern: /(bg|text|border)-semantic-(success|warning|error|info)-(bg|border|text|disabled|button)/ },
|
|
@@ -18,7 +22,28 @@ export default {
|
|
|
18
22
|
],
|
|
19
23
|
theme: {
|
|
20
24
|
extend: {
|
|
25
|
+
|
|
21
26
|
colors: {
|
|
27
|
+
sidebar: {
|
|
28
|
+
DEFAULT: 'hsl(222 18% 11%)',
|
|
29
|
+
foreground: 'hsl(220 14% 80%)',
|
|
30
|
+
muted: 'hsl(220 10% 55%)',
|
|
31
|
+
accent: 'hsl(222 16% 17%)',
|
|
32
|
+
border: 'hsl(220 14% 100% / 0.06)',
|
|
33
|
+
},
|
|
34
|
+
brand: {
|
|
35
|
+
DEFAULT: 'hsl(2 78% 55%)',
|
|
36
|
+
foreground: 'hsl(0 0% 100%)',
|
|
37
|
+
},
|
|
38
|
+
canvas: {
|
|
39
|
+
DEFAULT: 'hsl(220 20% 97%)',
|
|
40
|
+
foreground: 'hsl(222 30% 12%)',
|
|
41
|
+
muted: 'hsl(220 10% 45%)',
|
|
42
|
+
card: 'hsl(0 0% 100%)',
|
|
43
|
+
border: 'hsl(220 14% 90%)',
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
|
|
22
47
|
default: "#1B2A33",
|
|
23
48
|
header: "#256D96",
|
|
24
49
|
background: "#FEFCF6",
|