ketekny-ui-kit 1.0.89 → 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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ketekny-ui-kit",
3
3
  "type": "module",
4
- "version": "1.0.89",
4
+ "version": "1.0.91",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -317,6 +317,7 @@ export default {
317
317
  key,
318
318
  label: item?.label || "",
319
319
  description: item?.description || "",
320
+ badge: item?.badge ?? null,
320
321
  icon: item?.icon || "Dot",
321
322
  active,
322
323
  hasActiveChild,
@@ -12,23 +12,29 @@
12
12
  <component
13
13
  :is="menuItemComponent(item)"
14
14
  v-bind="item.bindings"
15
- class="sidebar-menu-item flex min-w-0 flex-1 appearance-none items-center gap-3 rounded-r-xl border-l-2 border-transparent bg-transparent px-3 py-2 text-left outline-none transition-all duration-150"
15
+ class="sidebar-menu-item relative flex min-h-[38px] min-w-0 flex-1 items-center gap-2.5 rounded-md px-3 py-2 text-left outline-none transition-colors duration-150"
16
16
  :class="itemRowClasses(item)"
17
17
  @click="handleItemClick(item, $event)"
18
18
  >
19
19
  <span
20
- class="flex h-5 w-5 shrink-0 items-center justify-center transition-colors duration-150"
21
- :class="item.active ? 'text-[#eef2f7]' : 'text-gray-500'"
20
+ v-if="item.active"
21
+ class="absolute left-0 top-1/2 h-5 w-1 -translate-y-1/2 rounded-r bg-[#2491ff]"
22
+ aria-hidden="true"
23
+ />
24
+ <span
25
+ class="flex h-4 w-4 shrink-0 items-center justify-center transition-colors duration-150"
26
+ :class="iconWrapClasses(item)"
22
27
  >
23
28
  <kIcon
24
29
  v-if="showFullIcon(item)"
25
30
  :name="item.icon"
26
- size="18"
27
- :color="item.active ? '#eef2f7' : '#9ca3af'"
31
+ size="16"
32
+ color="currentColor"
33
+ class="sidebar-menu-item-icon"
28
34
  />
29
35
  <span
30
36
  v-else
31
- class="h-1.5 w-1.5 rounded-full bg-current opacity-80"
37
+ class="h-1.5 w-1.5 rounded-full bg-current"
32
38
  />
33
39
  </span>
34
40
  <span class="min-w-0 flex-1">
@@ -37,18 +43,25 @@
37
43
  </span>
38
44
  <span
39
45
  v-if="item.description"
40
- class="mt-1 block truncate"
46
+ class="block truncate"
41
47
  :class="[itemDescriptionClasses, item.active ? 'text-[#9fa6b5]' : 'sidebar-menu-item-description text-[#6f7786]']"
42
48
  >
43
49
  {{ item.description }}
44
50
  </span>
45
51
  </span>
52
+ <span
53
+ v-if="item.badge != null"
54
+ class="ml-auto rounded-full px-1.5 py-0.5 text-[11px] font-medium leading-none tabular-nums transition-colors duration-150"
55
+ :class="badgeClasses(item)"
56
+ >
57
+ {{ item.badge }}
58
+ </span>
46
59
  </component>
47
60
 
48
61
  <button
49
62
  v-if="item.hasChildren"
50
63
  type="button"
51
- class="flex w-8 shrink-0 appearance-none items-center justify-center border-0 bg-transparent text-[#6f7786] outline-none transition hover:bg-white/6 hover:text-[#cbd5e1]"
64
+ class="flex w-8 shrink-0 appearance-none items-center justify-center border-0 bg-transparent text-[#6f7786] outline-none transition-colors duration-150 hover:text-[#e5e7eb]"
52
65
  :aria-expanded="isExpanded(item)"
53
66
  :aria-label="`${isExpanded(item) ? 'Collapse' : 'Expand'} ${item.label}`"
54
67
  @click.stop="toggle(item.key)"
@@ -65,7 +78,7 @@
65
78
 
66
79
  <div
67
80
  v-if="item.hasChildren && isExpanded(item)"
68
- class="ml-2 pl-4"
81
+ class="pl-3"
69
82
  >
70
83
  <TwoColSidebarMenuList
71
84
  :items="item.children"
@@ -130,12 +143,28 @@ export default {
130
143
  showFullIcon(item) {
131
144
  return this.level === 0 && Boolean(item?.icon);
132
145
  },
146
+ iconWrapClasses(item) {
147
+ if (item.active) return "text-[#eef2f7]";
148
+ if (item.hasActiveChild) return "text-[#cfd6e4]";
149
+ return "text-[#7b8494] group-hover:text-[#eef2f7]";
150
+ },
151
+ badgeClasses(item) {
152
+ if (item.active) {
153
+ return "bg-white/12 text-[#eef2f7]";
154
+ }
155
+
156
+ return "sidebar-menu-item-badge bg-white/8 text-[#8e97a8]";
157
+ },
133
158
  itemRowClasses(item) {
134
159
  if (item.active) {
135
- return "sidebar-menu-item--active text-white border-l-[#2491ff] bg-[#141922]";
160
+ return "sidebar-menu-item--active bg-white/10 text-[#eef2f7]";
161
+ }
162
+
163
+ if (item.hasActiveChild) {
164
+ return "text-[#eef2f7]";
136
165
  }
137
166
 
138
- return "text-[#e5e7eb] hover:text-white";
167
+ return "text-[#c7cfdb] hover:bg-white/6 hover:text-[#eef2f7]";
139
168
  },
140
169
  toggle(key) {
141
170
  this.$emit("toggle-branch", key);
@@ -145,11 +174,37 @@ export default {
145
174
  </script>
146
175
 
147
176
  <style scoped>
177
+ .sidebar-menu-item-icon,
178
+ .sidebar-menu-item-description {
179
+ transition: color 150ms ease-out;
180
+ }
181
+
148
182
  .sidebar-menu-item--active {
149
- box-shadow: inset 0 0 0 1px rgba(36, 145, 255, 0.1);
183
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04);
184
+ }
185
+
186
+ .sidebar-menu-item-icon {
187
+ color: currentColor !important;
188
+ }
189
+
190
+ .sidebar-menu-item--active .sidebar-menu-item-icon {
191
+ color: #eef2f7 !important;
192
+ }
193
+
194
+ .sidebar-menu-item:hover .sidebar-menu-item-icon {
195
+ color: #eef2f7 !important;
196
+ }
197
+
198
+ .sidebar-menu-item:hover {
199
+ color: #eef2f7;
200
+ }
201
+
202
+ .sidebar-menu-item:hover .sidebar-menu-item-badge {
203
+ color: #eef2f7;
150
204
  }
151
205
 
206
+ .sidebar-menu-item--active .sidebar-menu-item-description,
152
207
  .sidebar-menu-item:hover .sidebar-menu-item-description {
153
- color: #ffffff;
208
+ color: #eef2f7;
154
209
  }
155
210
  </style>
@@ -211,33 +211,34 @@
211
211
  <div v-else class="flex flex-col px-1">
212
212
  <div
213
213
  v-if="helpUrl || account.loggedIn"
214
+ class="flex flex-col gap-0.5"
214
215
  >
215
216
  <a
216
217
  v-if="helpUrl"
217
218
  :href="helpUrl"
218
219
  target="_blank"
219
220
  rel="noreferrer"
220
- class="flex w-full items-center gap-3 rounded-xl px-2 py-2 text-lg font-medium text-[#d7dbe3] transition hover:bg-[#242424] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
221
+ class="sidebar-footer-item flex min-h-[38px] w-full items-center gap-2.5 rounded-md bg-transparent px-3 py-2 text-left text-[15px] font-medium text-[#c7cfdb] outline-none transition-colors duration-150 hover:bg-white/6 hover:text-[#eef2f7] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
221
222
  >
222
- <kIcon name="HelpCircle" size="16" color="#d7dbe3" />
223
+ <kIcon name="HelpCircle" size="16" color="currentColor" class="sidebar-footer-item-icon" />
223
224
  <span>Βοήθεια και υποστήριξη</span>
224
225
  </a>
225
226
  <button
226
227
  v-if="account.loggedIn"
227
228
  type="button"
228
- class="flex w-full items-center gap-3 rounded-xl px-2 py-2 text-left text-lg font-medium text-[#d7dbe3] transition hover:bg-[#242424] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
229
+ class="sidebar-footer-item flex min-h-[38px] w-full items-center gap-2.5 rounded-md bg-transparent px-3 py-2 text-left text-[15px] font-medium text-[#c7cfdb] outline-none transition-colors duration-150 hover:bg-white/6 hover:text-[#eef2f7] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
229
230
  @click="$emit('edit-profile')"
230
231
  >
231
- <kIcon name="UserRoundCog" size="16" color="#d7dbe3" />
232
+ <kIcon name="UserRoundCog" size="16" color="currentColor" class="sidebar-footer-item-icon" />
232
233
  <span>Το προφίλ μου</span>
233
234
  </button>
234
235
  <button
235
236
  v-if="account.loggedIn"
236
237
  type="button"
237
- class="flex w-full items-center gap-3 rounded-xl px-2 py-2 text-left text-lg font-medium text-[#f59e0b] transition hover:bg-[#3a1f12] hover:text-[#fbbf24] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#f59e0b]/40"
238
+ class="sidebar-footer-item sidebar-footer-item--warning flex min-h-[38px] w-full items-center gap-2.5 rounded-md bg-transparent px-3 py-2 text-left text-[15px] font-medium text-[#f59e0b] outline-none transition-colors duration-150 hover:bg-[#3a2410] hover:text-[#fbbf24] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#f59e0b]/40"
238
239
  @click="$emit('signout')"
239
240
  >
240
- <kIcon name="LogOut" size="16" color="#f59e0b" />
241
+ <kIcon name="LogOut" size="16" color="currentColor" class="sidebar-footer-item-icon" />
241
242
  <span>Αποσύνδεση</span>
242
243
  </button>
243
244
  </div>
@@ -631,3 +632,22 @@ export default {
631
632
  },
632
633
  };
633
634
  </script>
635
+
636
+ <style scoped>
637
+ .sidebar-footer-item-icon {
638
+ color: #9ca3af !important;
639
+ transition: color 200ms ease-out;
640
+ }
641
+
642
+ .sidebar-footer-item:hover .sidebar-footer-item-icon {
643
+ color: #ffffff !important;
644
+ }
645
+
646
+ .sidebar-footer-item--warning .sidebar-footer-item-icon {
647
+ color: #f59e0b !important;
648
+ }
649
+
650
+ .sidebar-footer-item--warning:hover .sidebar-footer-item-icon {
651
+ color: #fbbf24 !important;
652
+ }
653
+ </style>
@@ -1,48 +1,17 @@
1
- @import url("https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&display=swap");
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
- body {
8
- font-family: "Ubuntu", Helvetica, Arial, sans-serif;
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>&nbsp;</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>
@@ -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",