ketekny-ui-kit 1.0.84 → 1.0.85

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.84",
4
+ "version": "1.0.85",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -1,13 +1,13 @@
1
1
  <template>
2
2
  <div
3
- class="relative flex min-h-screen flex-col bg-[#edf4fb] lg:h-screen lg:flex-row lg:overflow-hidden"
3
+ class="relative flex min-h-screen flex-col bg-[#edf4fb] lg:h-screen lg:flex-row lg:overflow-visible"
4
4
  >
5
5
  <button
6
6
  v-show="isDesktopViewport && isSidebarCollapsed"
7
7
  type="button"
8
8
  :aria-label="expandLabel"
9
9
  v-tooltip.right="expandLabel"
10
- class="fixed top-0 z-[70] hidden h-10 w-10 items-center justify-center rounded-r-full border-white/15 bg-[#121b2e] text-white shadow-2xl shadow-black/50 transition-all duration-300 hover:bg-[#1a2640] lg:left-[42px] lg:translate-x-full lg:flex"
10
+ class="fixed top-0 z-[70] hidden h-10 w-10 items-center justify-center rounded-r-full border-white/15 bg-[#0b0c0f] text-white shadow-2xl shadow-black/50 transition-all duration-300 hover:bg-[#14161b] lg:left-[42px] lg:translate-x-full lg:flex"
11
11
  @click="toggleSidebar"
12
12
  >
13
13
  <slot name="collapsed-toggle-icon">
@@ -20,7 +20,7 @@
20
20
  type="button"
21
21
  :aria-label="collapseLabel"
22
22
  v-tooltip.left="collapseLabel"
23
- class="fixed top-0 z-[70] hidden h-10 w-10 items-center justify-center rounded-r-full border-white/15 bg-[#121b2e] text-white shadow-2xl shadow-black/50 transition-all duration-300 hover:bg-[#1a2640] lg:left-[326px] lg:translate-x-full lg:flex"
23
+ class="fixed top-0 z-[70] hidden h-10 w-10 items-center justify-center rounded-r-full border-white/15 bg-[#0b0c0f] text-white shadow-2xl shadow-black/50 transition-all duration-300 hover:bg-[#14161b] lg:left-[326px] lg:translate-x-full lg:flex"
24
24
  @click.stop="toggleSidebar"
25
25
  >
26
26
  <slot name="toggle-icon">
@@ -45,6 +45,8 @@
45
45
  :contact-email="contactEmail"
46
46
  :help-url="helpUrl"
47
47
  :collapsed="isSidebarCollapsed"
48
+ :show-menu-search="showMenuSearch"
49
+ :menu-search-placeholder="menuSearchPlaceholder"
48
50
  @logo-click="handleLogoClick"
49
51
  @signin="handleSignin"
50
52
  @signup="handleSignup"
@@ -60,7 +62,7 @@
60
62
  <button
61
63
  v-if="!isDesktopViewport && !mobileMenuOpen"
62
64
  type="button"
63
- class="fixed bottom-4 left-4 z-[80] flex h-14 w-14 items-center justify-center rounded-full bg-[#121b2e] text-white shadow-2xl shadow-black/40 transition-transform duration-200 hover:scale-105 hover:bg-[#1a2640] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
65
+ class="fixed bottom-4 left-4 z-[80] flex h-14 w-14 items-center justify-center rounded-full bg-[#0b0c0f] text-white shadow-2xl shadow-black/40 transition-transform duration-200 hover:scale-105 hover:bg-[#14161b] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
64
66
  aria-label="Open menu"
65
67
  @click="mobileMenuOpen = true"
66
68
  >
@@ -98,6 +100,8 @@
98
100
  :contact-email="contactEmail"
99
101
  :help-url="helpUrl"
100
102
  :collapsed="false"
103
+ :show-menu-search="showMenuSearch"
104
+ :menu-search-placeholder="menuSearchPlaceholder"
101
105
  @logo-click="handleLogoClick"
102
106
  @item-click="closeMobileMenu"
103
107
  @signin="handleSignin"
@@ -158,6 +162,11 @@ export default {
158
162
  account: { type: Object, default: null },
159
163
  contactEmail: { type: String, default: "grammateia@ketekny.gr" },
160
164
  helpUrl: { type: String, default: "" },
165
+ showMenuSearch: { type: Boolean, default: false },
166
+ menuSearchPlaceholder: {
167
+ type: String,
168
+ default: "Αναζήτηση...",
169
+ },
161
170
  },
162
171
  data() {
163
172
  return {
@@ -281,17 +290,34 @@ export default {
281
290
  },
282
291
  computed: {
283
292
  normalizedMainMenu() {
284
- return this.mainMenu.map((item, index) => {
293
+ const normalizeItem = (item, index, parentKey = "menu") => {
285
294
  const isExternal = Boolean(item?.href);
286
295
  const isLink = Boolean(item?.to);
296
+ const children = Array.isArray(item?.children)
297
+ ? item.children.map((child, childIndex) =>
298
+ normalizeItem(
299
+ child,
300
+ childIndex,
301
+ item?.key || item?.label || `${parentKey}-${index}`
302
+ )
303
+ )
304
+ : [];
305
+ const hasActiveChild = children.some(
306
+ child => child.active || child.hasActiveChild
307
+ );
287
308
  const active = Boolean(item?.active);
309
+ const key = item?.key || item?.label || `${parentKey}-${index}`;
288
310
 
289
311
  return {
290
- key: item?.key || item?.label || index,
312
+ key,
291
313
  label: item?.label || "",
292
314
  description: item?.description || "",
293
315
  icon: item?.icon || "Dot",
294
316
  active,
317
+ hasActiveChild,
318
+ hasChildren: children.length > 0,
319
+ children,
320
+ defaultExpanded: Boolean(item?.defaultExpanded),
295
321
  isExternal,
296
322
  isLink,
297
323
  bindings: isLink
@@ -311,11 +337,11 @@ export default {
311
337
  type: "button",
312
338
  title: item.title || item.label,
313
339
  },
314
- classes: active
315
- ? "border-white/70 bg-white/90 text-slate-950 shadow-lg shadow-black/20"
316
- : "border-white/10 bg-white/5 text-white hover:border-cyan-400/40 hover:bg-white/10",
340
+ classes: "",
317
341
  };
318
- });
342
+ };
343
+
344
+ return this.mainMenu.map((item, index) => normalizeItem(item, index));
319
345
  },
320
346
  },
321
347
  };
@@ -0,0 +1,112 @@
1
+ <template>
2
+ <div class="space-y-1.5">
3
+ <div v-for="item in items" :key="item.key" class="space-y-1.5">
4
+ <div
5
+ class="flex items-stretch"
6
+ :class="item.hasChildren ? 'gap-2' : 'gap-0'"
7
+ >
8
+ <component
9
+ :is="menuItemComponent(item)"
10
+ v-bind="item.bindings"
11
+ class="flex min-w-0 flex-1 appearance-none items-center gap-3 border-0 bg-transparent px-2 py-1.5 text-left outline-none transition-all duration-150"
12
+ :class="itemRowClasses(item)"
13
+ @click="handleItemClick(item, $event)"
14
+ >
15
+ <span
16
+ class="flex h-5 w-5 shrink-0 items-center justify-center text-[#eef2f7] transition-colors duration-150"
17
+ >
18
+ <kIcon :name="item.icon" size="18" color="#eef2f7" />
19
+ </span>
20
+ <span class="min-w-0 flex-1">
21
+ <span class="block truncate text-[15px] font-medium leading-5">
22
+ {{ item.label }}
23
+ </span>
24
+ <span
25
+ v-if="item.description"
26
+ class="mt-1 block truncate text-[12px] leading-4"
27
+ :class="item.active ? 'text-[#9fa6b5]' : 'text-[#6f7786]'"
28
+ >
29
+ {{ item.description }}
30
+ </span>
31
+ </span>
32
+ </component>
33
+
34
+ <button
35
+ v-if="item.hasChildren"
36
+ type="button"
37
+ class="flex w-8 shrink-0 appearance-none items-center justify-center border-0 bg-transparent text-[#eef2f7] outline-none transition hover:bg-white/6 hover:text-white"
38
+ :aria-expanded="isExpanded(item)"
39
+ :aria-label="`${isExpanded(item) ? 'Collapse' : 'Expand'} ${item.label}`"
40
+ @click.stop="toggle(item.key)"
41
+ >
42
+ <kIcon
43
+ name="ChevronDown"
44
+ size="15"
45
+ color="#eef2f7"
46
+ class="transition-transform duration-200"
47
+ :class="isExpanded(item) ? 'rotate-180' : ''"
48
+ />
49
+ </button>
50
+ </div>
51
+
52
+ <div
53
+ v-if="item.hasChildren && isExpanded(item)"
54
+ class="ml-2 border-l border-[#242834] pl-4"
55
+ >
56
+ <TwoColSidebarMenuList
57
+ :items="item.children"
58
+ :expanded-state="expandedState"
59
+ @item-click="$emit('item-click', $event)"
60
+ @toggle-branch="$emit('toggle-branch', $event)"
61
+ />
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </template>
66
+
67
+ <script>
68
+ import { RouterLink } from "vue-router";
69
+ import kIcon from "../ui/kIcon.vue";
70
+
71
+ export default {
72
+ name: "TwoColSidebarMenuList",
73
+ components: {
74
+ RouterLink,
75
+ kIcon,
76
+ },
77
+ props: {
78
+ items: { type: Array, default: () => [] },
79
+ expandedState: { type: Object, required: true },
80
+ },
81
+ emits: ["item-click", "toggle-branch"],
82
+ methods: {
83
+ menuItemComponent(item) {
84
+ if (item?.isLink) return RouterLink;
85
+ if (item?.isExternal) return "a";
86
+ return "button";
87
+ },
88
+ handleItemClick(item, event) {
89
+ event?.stopPropagation?.();
90
+
91
+ if (item?.hasChildren) {
92
+ event?.preventDefault?.();
93
+ this.toggle(item.key);
94
+ return;
95
+ }
96
+
97
+ this.$emit("item-click", item);
98
+ },
99
+ isExpanded(item) {
100
+ return Boolean(this.expandedState[item.key]);
101
+ },
102
+ itemRowClasses(item) {
103
+ return item.active
104
+ ? "relative rounded-xl bg-[#242424] text-white before:absolute before:bottom-2 before:left-0 before:top-2 before:w-[3px] before:rounded-r-full before:bg-[#2491ff]"
105
+ : "rounded-xl text-[#e5e7eb] hover:bg-[#242424] hover:text-white";
106
+ },
107
+ toggle(key) {
108
+ this.$emit("toggle-branch", key);
109
+ },
110
+ },
111
+ };
112
+ </script>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div
3
- class="flex h-full w-full flex-col overflow-x-hidden bg-[radial-gradient(circle_at_0_0,#ffffff0f,#0000_34%),linear-gradient(#101a2d_0%,#0a1120_52%,#070d19_100%)] text-white shadow-2xl transition-all duration-300 ease-out lg:relative lg:z-30 lg:will-change-transform"
3
+ class="flex h-full w-full flex-col overflow-visible border-r border-[#171a21] bg-[#0b0c0f] text-white shadow-[20px_0_50px_rgba(0,0,0,0.35)] transition-all duration-300 ease-out lg:relative lg:z-30 lg:will-change-transform"
4
4
  >
5
5
  <div class="relative">
6
6
  <button
@@ -16,6 +16,7 @@
16
16
  class="px-4 py-4 transition-all duration-300 cursor-pointer"
17
17
  />
18
18
  </button>
19
+
19
20
  <div
20
21
  v-if="title && !collapsed"
21
22
  class="py-3 text-xl font-bold text-center"
@@ -23,81 +24,94 @@
23
24
  {{ title }}
24
25
  </div>
25
26
 
26
- <div v-if="helpUrl && !collapsed" class="px-4 pb-2">
27
- <a
28
- :href="helpUrl"
29
- target="_blank"
30
- rel="noreferrer"
31
- class="inline-flex items-center gap-2 rounded-md px-2 py-1.5 text-sm font-semibold text-white/85 transition hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
32
- >
33
- <kIcon name="HelpCircle" size="16" color="#ffffff" />
34
- <span>Χρειάζεστε βοήθεια;</span>
35
- </a>
36
- </div>
37
-
38
27
  <slot name="sidebar-header" />
39
28
  </div>
40
29
 
41
- <div class="flex-1 pt-1">
42
- <div v-if="collapsed" class="flex flex-col items-center gap-2 mx-1 mt-3">
43
- <component
44
- :is="menuItemComponent(item)"
45
- v-for="item in menuItems"
46
- :key="item.key"
47
- v-bind="item.bindings"
48
- class="relative flex items-center justify-center transition border group h-14 w-14 rounded-xl border-white/15"
49
- :class="item.classes"
50
- v-tooltip.right="item.label"
51
- @click="$emit('item-click', item)"
52
- >
53
- <kIcon
54
- :name="item.icon"
55
- size="20"
56
- :color="item.active ? '#0f172a' : '#ffffff'"
57
- />
58
- </component>
30
+ <div class="min-h-0 flex-1 overflow-y-auto px-3 pb-3 pt-2">
31
+ <div v-if="showMenuSearch && !collapsed" class="mb-3 px-1">
32
+ <kSearch
33
+ v-model="menuSearchTerm"
34
+ :debounce="0"
35
+ :placeholder="menuSearchPlaceholder"
36
+ input-class="border-[#242834] bg-[#1f1f1f] text-[#eef2f7] placeholder:text-[#8a92a1] shadow-none focus:border-[#3b82f6] focus:ring-[#3b82f6]/20"
37
+ icon-class="text-[#8a92a1]"
38
+ />
59
39
  </div>
60
- <div v-else-if="menuItems.length" class="mx-2 mt-3 space-y-3">
61
- <component
62
- :is="menuItemComponent(item)"
40
+
41
+ <div
42
+ v-if="collapsed"
43
+ class="mt-2 flex flex-col items-center gap-1.5"
44
+ >
45
+ <div
63
46
  v-for="item in menuItems"
64
47
  :key="item.key"
65
- v-bind="item.bindings"
66
- class="flex items-start w-full gap-3 px-4 py-4 text-left transition-all duration-200 border rounded-2xl"
67
- :class="item.classes"
68
- @click="$emit('item-click', item)"
48
+ class="relative"
49
+ @mouseenter="handleCollapsedItemEnter(item, $event)"
50
+ @mouseleave="handleCollapsedItemLeave(item)"
69
51
  >
70
- <span
71
- class="mt-0.5 flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-white/10 text-cyan-100 transition-colors duration-200"
72
- :class="
73
- item.active
74
- ? 'bg-slate-950 text-white'
75
- : item.label === 'Settings'
76
- ? 'bg-sky-500/20 text-sky-50 ring-1 ring-sky-400/25'
77
- : ''
78
- "
52
+ <component
53
+ :is="menuItemComponent(item)"
54
+ v-bind="item.bindings"
55
+ class="relative flex h-11 w-11 items-center justify-center rounded-lg border border-transparent text-[#b3b9c6] transition hover:bg-white/6"
56
+ :class="item.classes"
57
+ :title="collapsedItemTooltip(item)"
58
+ @mouseenter="handleCollapsedItemEnter(item, $event)"
59
+ @click="handleCollapsedItemClick(item, $event)"
79
60
  >
80
- <kIcon :name="item.icon" />
81
- </span>
82
- <span class="min-w-0">
83
- <span class="block font-semibold leading-tight">
84
- {{ item.label }}
85
- </span>
86
- <span
87
- v-if="item.description"
88
- class="block mt-1 text-sm"
89
- :class="item.active ? 'text-slate-700' : 'text-slate-200'"
90
- >
91
- {{ item.description }}
92
- </span>
93
- </span>
94
- </component>
61
+ <kIcon
62
+ :name="item.icon"
63
+ size="18"
64
+ :color="item.active ? '#ffffff' : '#b3b9c6'"
65
+ />
66
+ </component>
67
+ </div>
68
+ </div>
69
+ <div v-else-if="menuItems.length" class="mt-2">
70
+ <TwoColSidebarMenuList
71
+ :items="filteredMenuItems"
72
+ :expanded-state="expandedBranches"
73
+ @item-click="$emit('item-click', $event)"
74
+ @toggle-branch="toggleBranch"
75
+ />
95
76
  </div>
96
77
  </div>
97
78
 
79
+ <Teleport to="body">
80
+ <div
81
+ v-if="collapsedFlyoutItem"
82
+ class="fixed z-[200]"
83
+ :style="collapsedFlyoutStyle"
84
+ @mouseenter="handleCollapsedItemEnter(collapsedFlyoutItem)"
85
+ @mouseleave="handleCollapsedItemLeave(collapsedFlyoutItem)"
86
+ >
87
+ <div
88
+ class="w-[320px] rounded-2xl border border-[#242834] bg-[#0f1116] p-3 shadow-[0_24px_60px_rgba(0,0,0,0.45)]"
89
+ >
90
+ <div class="mb-2 px-2">
91
+ <div class="text-sm font-semibold text-white">
92
+ {{ collapsedFlyoutItem.label }}
93
+ </div>
94
+ <div
95
+ v-if="collapsedFlyoutItem.description"
96
+ class="mt-0.5 text-xs text-[#7f8794]"
97
+ >
98
+ {{ collapsedFlyoutItem.description }}
99
+ </div>
100
+ </div>
101
+
102
+ <TwoColSidebarMenuList
103
+ :items="collapsedFlyoutItem.children"
104
+ :expanded-state="expandedBranches"
105
+ @item-click="$emit('item-click', $event)"
106
+ @toggle-branch="toggleBranch"
107
+ />
108
+ </div>
109
+ </div>
110
+ </Teleport>
111
+
98
112
  <div
99
113
  v-if="account"
100
- class="px-1 pb-4 border-b shrink-0 border-white/10 bg-slate-900/80 text-white/90"
114
+ class="shrink-0 border-t border-[#171a21] px-3 pb-4 pt-3 text-white/90"
101
115
  >
102
116
  <div v-if="collapsed" class="flex flex-col gap-3">
103
117
  <div v-if="helpUrl" class="flex flex-col items-center gap-2">
@@ -105,66 +119,98 @@
105
119
  :href="helpUrl"
106
120
  target="_blank"
107
121
  rel="noreferrer"
108
- aria-label="Χρειάζεστε βοήθεια;"
109
- title="Χρειάζεστε βοήθεια;"
110
- class="relative flex items-center justify-center text-white transition border group h-14 w-14 rounded-xl border-white/15 bg-white/5 hover:border-cyan-400/40 hover:bg-white/10"
111
- v-tooltip.right="'Χρειάζεστε βοήθεια;'"
122
+ aria-label="Βοήθεια"
123
+ title="Βοήθεια"
124
+ class="relative flex h-11 w-11 items-center justify-center rounded-lg border border-transparent text-[#b3b9c6] transition hover:bg-white/6 hover:text-white"
125
+ v-tooltip.right="'Βοήθεια'"
112
126
  >
113
- <kIcon name="HelpCircle" size="20" color="#ffffff" />
127
+ <kIcon name="HelpCircle" size="18" color="#b3b9c6" />
114
128
  </a>
115
129
  </div>
116
130
  <div v-if="account.loggedIn" class="flex flex-col items-center gap-2">
117
131
  <button
118
132
  type="button"
119
- class="relative flex items-center justify-center text-white transition border group h-14 w-14 rounded-xl border-white/15 bg-white/5 hover:border-cyan-400/40 hover:bg-white/10"
133
+ class="relative flex h-11 w-11 items-center justify-center rounded-lg border border-transparent text-[#b3b9c6] transition hover:bg-white/6 hover:text-white"
120
134
  :title="editProfileLabel"
121
135
  v-tooltip.right="editProfileLabel"
122
136
  @click="$emit('edit-profile')"
123
137
  >
124
- <kIcon name="UserRoundCog" size="20" color="#ffffff" />
138
+ <kIcon name="UserRoundCog" size="18" color="#b3b9c6" />
125
139
  </button>
126
140
  <button
127
141
  type="button"
128
- class="group relative flex h-14 w-14 items-center justify-center rounded-xl border border-white/15 bg-[#d97706] text-white transition hover:bg-[#b45309]"
142
+ class="group relative flex h-11 w-11 items-center justify-center rounded-lg border border-transparent text-[#b3b9c6] transition hover:bg-white/6 hover:text-white"
129
143
  :title="signOutLabel"
130
144
  v-tooltip.right="signOutLabel"
131
145
  @click="$emit('signout')"
132
146
  >
133
- <kIcon name="LogOut" size="20" color="#ffffff" />
147
+ <kIcon name="LogOut" size="18" color="#b3b9c6" />
134
148
  </button>
135
149
  </div>
136
150
  <div v-else class="flex flex-col items-center gap-2">
137
151
  <button
138
152
  type="button"
139
- class="relative flex items-center justify-center text-white transition border group h-14 w-14 rounded-xl border-white/15 bg-white/5 hover:border-cyan-400/40 hover:bg-white/10"
153
+ class="relative flex h-11 w-11 items-center justify-center rounded-lg border border-transparent text-[#b3b9c6] transition hover:bg-white/6 hover:text-white"
140
154
  :title="signInLabel"
141
155
  v-tooltip.right="signInLabel"
142
156
  @click="$emit('signin')"
143
157
  >
144
- <kIcon name="LogIn" size="20" color="#ffffff" />
158
+ <kIcon name="LogIn" size="18" color="#b3b9c6" />
145
159
  </button>
146
160
  <button
147
161
  type="button"
148
- class="relative flex items-center justify-center text-white transition border group h-14 w-14 rounded-xl border-white/15 bg-white/5 hover:border-cyan-400/40 hover:bg-white/10"
162
+ class="relative flex h-11 w-11 items-center justify-center rounded-lg border border-transparent text-[#b3b9c6] transition hover:bg-white/6 hover:text-white"
149
163
  :title="signUpLabel"
150
164
  v-tooltip.right="signUpLabel"
151
165
  @click="$emit('signup')"
152
166
  >
153
- <kIcon name="UserPlus" size="20" color="#ffffff" />
167
+ <kIcon name="UserPlus" size="18" color="#b3b9c6" />
154
168
  </button>
155
169
  </div>
156
170
  </div>
157
- <div v-else class="flex flex-col gap-3 px-3">
158
- <div class="min-w-0">
171
+ <div v-else class="flex flex-col px-1">
172
+ <div
173
+ v-if="helpUrl || account.loggedIn"
174
+ >
175
+ <a
176
+ v-if="helpUrl"
177
+ :href="helpUrl"
178
+ target="_blank"
179
+ rel="noreferrer"
180
+ 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"
181
+ >
182
+ <kIcon name="HelpCircle" size="16" color="#d7dbe3" />
183
+ <span>Βοήθεια και υποστήριξη</span>
184
+ </a>
185
+ <button
186
+ v-if="account.loggedIn"
187
+ type="button"
188
+ 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"
189
+ @click="$emit('edit-profile')"
190
+ >
191
+ <kIcon name="UserRoundCog" size="16" color="#d7dbe3" />
192
+ <span>Το προφίλ μου</span>
193
+ </button>
194
+ <button
195
+ v-if="account.loggedIn"
196
+ type="button"
197
+ 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"
198
+ @click="$emit('signout')"
199
+ >
200
+ <kIcon name="LogOut" size="16" color="#f59e0b" />
201
+ <span>Αποσύνδεση</span>
202
+ </button>
203
+ </div>
204
+ <div v-if="!account.loggedIn" class="min-w-0">
159
205
  <p
160
206
  v-if="account.loggedIn"
161
- class="mt-2 text-lg font-semibold text-white truncate"
207
+ class="mt-1 truncate text-sm font-semibold text-white"
162
208
  >
163
209
  {{ accountDisplayName }}
164
210
  </p>
165
211
  <p
166
212
  v-if="account.loggedIn && displayEmail"
167
- class="text-sm truncate text-slate-300"
213
+ class="truncate text-xs text-[#7c8392]"
168
214
  >
169
215
  {{ displayEmail }}
170
216
  </p>
@@ -184,33 +230,19 @@
184
230
  :label="signUpLabel"
185
231
  @click="$emit('signup')"
186
232
  />
187
- <kButton
188
- v-if="account.loggedIn"
189
- variant="soft"
190
- icon="UserRoundCog"
191
- :label="editProfileLabel"
192
- @click="$emit('edit-profile')"
193
- />
194
- <kButton
195
- v-if="account.loggedIn"
196
- icon="LogOut"
197
- warning
198
- :label="signOutLabel"
199
- @click="$emit('signout')"
200
- />
201
233
  </div>
202
234
  </div>
203
235
  </div>
204
236
 
205
237
  <div
206
238
  v-if="!collapsed"
207
- class="border-t shrink-0 border-white/10 text-white/70"
239
+ class="shrink-0 border-t border-[#171a21] p-3 text-[#8c93a1]"
208
240
  >
209
- <div v-if="showContactInfo" class="flex flex-col gap-2 px-4">
210
- <p class="text-sm font-semibold uppercase tracking-[0.25em] text-white/80">
241
+ <div v-if="showContactInfo" class="flex flex-col gap-2 px-1">
242
+ <p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#666d79]">
211
243
  KETEKNY AE
212
244
  </p>
213
- <div class="flex flex-wrap items-center text-sm gap-x-3">
245
+ <div class="flex flex-wrap items-center gap-x-3 text-xs">
214
246
  <span class="whitespace-nowrap">{{ contactAddressLabel }}</span>
215
247
  <span class="whitespace-nowrap">210 3648 337</span>
216
248
  <span class="whitespace-nowrap">
@@ -232,10 +264,7 @@
232
264
  </div>
233
265
  </div>
234
266
 
235
- <div
236
- v-if="showSponsors"
237
- class="mt-2 flex flex-col gap-3 bg-[#0d4b87] px-4 py-1"
238
- >
267
+ <div v-if="showSponsors" class="mt-2 -mx-3 flex flex-col gap-3 bg-[#0d4b87] px-4 py-1">
239
268
  <div>
240
269
  <img
241
270
  src="https://s3.ketekny.gr/public/web-apps/sponsors/Greece 2.0_NextGeneration_White_gr.png"
@@ -259,6 +288,8 @@
259
288
  import { RouterLink } from "vue-router";
260
289
  import kButton from "../ui/kButton.vue";
261
290
  import kIcon from "../ui/kIcon.vue";
291
+ import kSearch from "../ui/kSearch.vue";
292
+ import TwoColSidebarMenuList from "./twoColSidebarMenuList.vue";
262
293
 
263
294
  const LABELS = {
264
295
  editProfile: "Το προφίλ μου",
@@ -274,7 +305,9 @@ export default {
274
305
  components: {
275
306
  kButton,
276
307
  kIcon,
308
+ kSearch,
277
309
  RouterLink,
310
+ TwoColSidebarMenuList,
278
311
  },
279
312
  emits: [
280
313
  "logo-click",
@@ -299,8 +332,64 @@ export default {
299
332
  contactEmail: { type: String, default: "grammateia@ketekny.gr" },
300
333
  helpUrl: { type: String, default: "" },
301
334
  collapsed: { type: Boolean, default: false },
335
+ showMenuSearch: { type: Boolean, default: false },
336
+ menuSearchPlaceholder: {
337
+ type: String,
338
+ default: "Αναζήτηση...",
339
+ },
340
+ },
341
+ data() {
342
+ return {
343
+ expandedBranches: {},
344
+ collapsedFlyoutKey: null,
345
+ collapsedFlyoutPosition: null,
346
+ collapsedFlyoutCloseTimer: null,
347
+ menuSearchTerm: "",
348
+ };
302
349
  },
303
350
  computed: {
351
+ filteredMenuItems() {
352
+ if (!this.showMenuSearch) return this.menuItems;
353
+
354
+ const query = this.menuSearchTerm.trim().toLowerCase();
355
+ if (!query) return this.menuItems;
356
+
357
+ const filterItems = items =>
358
+ items.reduce((acc, item) => {
359
+ const filteredChildren = item.children?.length
360
+ ? filterItems(item.children)
361
+ : [];
362
+ const matchesSelf =
363
+ item.label?.toLowerCase().includes(query) ||
364
+ item.description?.toLowerCase().includes(query);
365
+
366
+ if (!matchesSelf && filteredChildren.length === 0) {
367
+ return acc;
368
+ }
369
+
370
+ acc.push({
371
+ ...item,
372
+ children: filteredChildren,
373
+ hasChildren: filteredChildren.length > 0,
374
+ });
375
+ return acc;
376
+ }, []);
377
+
378
+ return filterItems(this.menuItems);
379
+ },
380
+ collapsedFlyoutItem() {
381
+ if (!this.collapsedFlyoutKey) return null;
382
+
383
+ return this.findMenuItemByKey(this.filteredMenuItems, this.collapsedFlyoutKey);
384
+ },
385
+ collapsedFlyoutStyle() {
386
+ if (!this.collapsedFlyoutPosition) return {};
387
+
388
+ return {
389
+ left: `${this.collapsedFlyoutPosition.left}px`,
390
+ top: `${this.collapsedFlyoutPosition.top}px`,
391
+ };
392
+ },
304
393
  accountDisplayName() {
305
394
  if (!this.account) return "";
306
395
 
@@ -345,12 +434,129 @@ export default {
345
434
  `;
346
435
  },
347
436
  },
437
+ watch: {
438
+ menuItems: {
439
+ immediate: true,
440
+ handler(items) {
441
+ this.expandedBranches = this.buildExpandedState(items, {
442
+ ...this.expandedBranches,
443
+ });
444
+
445
+ if (
446
+ this.collapsedFlyoutKey &&
447
+ !this.findMenuItemByKey(items, this.collapsedFlyoutKey)
448
+ ) {
449
+ this.collapsedFlyoutKey = null;
450
+ this.collapsedFlyoutPosition = null;
451
+ }
452
+ },
453
+ },
454
+ menuSearchTerm() {
455
+ this.expandedBranches = this.buildExpandedState(this.filteredMenuItems, {
456
+ ...this.expandedBranches,
457
+ });
458
+ },
459
+ },
460
+ beforeUnmount() {
461
+ this.clearCollapsedFlyoutCloseTimer();
462
+ },
348
463
  methods: {
349
464
  menuItemComponent(item) {
350
465
  if (item?.isLink) return RouterLink;
351
466
  if (item?.isExternal) return "a";
352
467
  return "button";
353
468
  },
469
+ collapsedItemTooltip(item) {
470
+ return item?.hasChildren ? null : item.label;
471
+ },
472
+ buildExpandedState(items, state = {}) {
473
+ items.forEach(item => {
474
+ if (!item?.hasChildren) return;
475
+
476
+ const hasActiveDescendant = this.branchHasActiveChild(item.children);
477
+ if (!(item.key in state)) {
478
+ state[item.key] = Boolean(hasActiveDescendant);
479
+ } else if (hasActiveDescendant) {
480
+ state[item.key] = true;
481
+ }
482
+ this.buildExpandedState(item.children, state);
483
+ });
484
+
485
+ return state;
486
+ },
487
+ branchHasActiveChild(children = []) {
488
+ return children.some(
489
+ child => child.active || this.branchHasActiveChild(child.children || [])
490
+ );
491
+ },
492
+ toggleBranch(key) {
493
+ this.expandedBranches = {
494
+ ...this.expandedBranches,
495
+ [key]: !this.expandedBranches[key],
496
+ };
497
+ },
498
+ findMenuItemByKey(items, key) {
499
+ for (const item of items) {
500
+ if (item.key === key) return item;
501
+ if (item.children?.length) {
502
+ const found = this.findMenuItemByKey(item.children, key);
503
+ if (found) return found;
504
+ }
505
+ }
506
+
507
+ return null;
508
+ },
509
+ updateCollapsedFlyoutPosition(event) {
510
+ const trigger = event?.currentTarget;
511
+ if (!trigger || typeof window === "undefined") return;
512
+
513
+ const rect = trigger.getBoundingClientRect();
514
+ this.collapsedFlyoutPosition = {
515
+ left: rect.right + 12,
516
+ top: Math.max(12, rect.top),
517
+ };
518
+ },
519
+ clearCollapsedFlyoutCloseTimer() {
520
+ if (this.collapsedFlyoutCloseTimer) {
521
+ window.clearTimeout(this.collapsedFlyoutCloseTimer);
522
+ this.collapsedFlyoutCloseTimer = null;
523
+ }
524
+ },
525
+ handleCollapsedItemEnter(item, event) {
526
+ if (!item?.hasChildren) return;
527
+
528
+ this.clearCollapsedFlyoutCloseTimer();
529
+ this.collapsedFlyoutKey = item.key;
530
+ if (event) {
531
+ this.updateCollapsedFlyoutPosition(event);
532
+ }
533
+ },
534
+ handleCollapsedItemLeave(item) {
535
+ if (this.collapsedFlyoutKey !== item?.key) return;
536
+
537
+ this.clearCollapsedFlyoutCloseTimer();
538
+ this.collapsedFlyoutCloseTimer = window.setTimeout(() => {
539
+ if (this.collapsedFlyoutKey !== item?.key) return;
540
+
541
+ this.collapsedFlyoutKey = null;
542
+ this.collapsedFlyoutPosition = null;
543
+ this.collapsedFlyoutCloseTimer = null;
544
+ }, 180);
545
+ },
546
+ showCollapsedFlyout(item) {
547
+ return this.collapsedFlyoutKey === item?.key && item?.hasChildren;
548
+ },
549
+ handleCollapsedItemClick(item, event) {
550
+ if (item?.hasChildren) {
551
+ event?.preventDefault?.();
552
+ this.clearCollapsedFlyoutCloseTimer();
553
+ this.collapsedFlyoutKey = item.key;
554
+ this.updateCollapsedFlyoutPosition(event);
555
+ return;
556
+ }
557
+
558
+ this.$emit("item-click", item);
559
+ },
354
560
  },
355
561
  };
356
562
  </script>
@@ -8,12 +8,15 @@
8
8
  type="text"
9
9
  v-model="localValue"
10
10
  @input="onInput"
11
- :class="[defaultStyle, disabled ? disabledStyle : '']"
11
+ :class="[defaultStyle, inputClass, disabled ? disabledStyle : '']"
12
12
  :placeholder="placeholder"
13
13
  :aria-label="!label ? placeholder : null"
14
14
  :disabled="disabled"
15
15
  />
16
- <Search class="absolute w-4 h-4 text-primary/70 -translate-y-1/2 pointer-events-none right-3 top-1/2" />
16
+ <Search
17
+ class="absolute w-4 h-4 -translate-y-1/2 pointer-events-none right-3 top-1/2"
18
+ :class="iconClass"
19
+ />
17
20
  </div>
18
21
  </template>
19
22
 
@@ -30,6 +33,8 @@ export default {
30
33
  placeholder: { type: String, default: "Αναζήτηση..." },
31
34
  label: { type: String, default: "" },
32
35
  disabled: { type: Boolean, default: false },
36
+ inputClass: { type: String, default: "" },
37
+ iconClass: { type: String, default: "text-primary/70" },
33
38
  },
34
39
  data() {
35
40
  return {