ketekny-ui-kit 1.0.84 → 1.0.86

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.86",
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-lg shadow-black/25 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-lg shadow-black/25 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
  >
@@ -80,7 +82,7 @@
80
82
  <transition name="mobile-menu-panel">
81
83
  <aside
82
84
  v-if="!isDesktopViewport && mobileMenuOpen"
83
- class="fixed inset-y-0 left-0 z-[90] flex h-[100dvh] w-[min(90vw,420px)] max-w-full overflow-hidden shadow-2xl"
85
+ class="fixed inset-y-0 left-0 z-[90] flex h-[100dvh] w-[min(90vw,420px)] max-w-full overflow-hidden shadow-[10px_0_30px_rgba(0,0,0,0.2)]"
84
86
  role="dialog"
85
87
  aria-modal="true"
86
88
  >
@@ -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,13 +1,17 @@
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-[10px_0_28px_rgba(0,0,0,0.18)] 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
7
7
  v-if="logoSrc"
8
8
  type="button"
9
9
  class="block w-full text-left"
10
- v-tooltip.right.html="collapsed ? collapsedContactTooltip : logoAlt"
10
+ :title="collapsed ? null : logoAlt"
11
+ @mouseenter="handleCollapsedContactEnter($event)"
12
+ @mouseleave="handleCollapsedContactLeave"
13
+ @focus="handleCollapsedContactEnter($event)"
14
+ @blur="handleCollapsedContactLeave"
11
15
  @click="$emit('logo-click')"
12
16
  >
13
17
  <img
@@ -16,6 +20,7 @@
16
20
  class="px-4 py-4 transition-all duration-300 cursor-pointer"
17
21
  />
18
22
  </button>
23
+
19
24
  <div
20
25
  v-if="title && !collapsed"
21
26
  class="py-3 text-xl font-bold text-center"
@@ -23,81 +28,130 @@
23
28
  {{ title }}
24
29
  </div>
25
30
 
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
31
  <slot name="sidebar-header" />
39
32
  </div>
40
33
 
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>
34
+ <div class="min-h-0 flex-1 overflow-y-auto px-3 pb-3 pt-2">
35
+ <div v-if="showMenuSearch && !collapsed" class="mb-3 px-1">
36
+ <kSearch
37
+ v-model="menuSearchTerm"
38
+ :debounce="0"
39
+ :placeholder="menuSearchPlaceholder"
40
+ input-class="!border-[#242834] !bg-[#1f1f1f] !text-[#eef2f7] !placeholder:text-[#8a92a1] !shadow-none focus:!border-[#3b82f6] focus:!ring-[#3b82f6]/20 dark:!border-[#242834] dark:!bg-[#1f1f1f] dark:!text-[#eef2f7] dark:!placeholder:text-[#8a92a1]"
41
+ icon-class="text-[#8a92a1]"
42
+ />
59
43
  </div>
60
- <div v-else-if="menuItems.length" class="mx-2 mt-3 space-y-3">
61
- <component
62
- :is="menuItemComponent(item)"
44
+
45
+ <div
46
+ v-if="collapsed"
47
+ class="mt-2 flex flex-col items-center gap-1.5"
48
+ >
49
+ <div
63
50
  v-for="item in menuItems"
64
51
  :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)"
52
+ class="relative"
53
+ @mouseenter="handleCollapsedItemEnter(item, $event)"
54
+ @mouseleave="handleCollapsedItemLeave(item)"
69
55
  >
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
- "
56
+ <component
57
+ :is="menuItemComponent(item)"
58
+ v-bind="item.bindings"
59
+ class="relative flex h-11 w-11 items-center justify-center rounded-lg border border-transparent text-[#b3b9c6] transition hover:bg-white/6"
60
+ :class="item.classes"
61
+ :title="collapsedItemTooltip(item)"
62
+ @mouseenter="handleCollapsedItemEnter(item, $event)"
63
+ @click="handleCollapsedItemClick(item, $event)"
79
64
  >
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>
65
+ <kIcon
66
+ :name="item.icon"
67
+ size="18"
68
+ :color="item.active ? '#ffffff' : '#b3b9c6'"
69
+ />
70
+ </component>
71
+ </div>
72
+ </div>
73
+ <div v-else-if="menuItems.length" class="mt-2">
74
+ <TwoColSidebarMenuList
75
+ :items="filteredMenuItems"
76
+ :expanded-state="expandedBranches"
77
+ @item-click="$emit('item-click', $event)"
78
+ @toggle-branch="toggleBranch"
79
+ />
95
80
  </div>
96
81
  </div>
97
82
 
83
+ <Teleport to="body">
84
+ <div
85
+ v-if="collapsedContactFlyoutVisible"
86
+ class="fixed z-[200]"
87
+ :style="collapsedContactFlyoutStyle"
88
+ @mouseenter="handleCollapsedContactEnter()"
89
+ @mouseleave="handleCollapsedContactLeave"
90
+ >
91
+ <div
92
+ class="w-[320px] rounded-2xl border border-[#242834] bg-[#0f1116] p-3 shadow-[0_18px_44px_rgba(0,0,0,0.32)]"
93
+ >
94
+ <div class="mb-2 px-2 text-[11px] font-semibold uppercase tracking-[0.24em] text-[#7f8794]">
95
+ KETEKNY AE
96
+ </div>
97
+ <div class="flex flex-col gap-1 px-2 text-sm leading-6 text-white">
98
+ <div class="text-[#d1d5db]">{{ contactAddressLabel }}</div>
99
+ <a href="tel:2103648337" class="text-[#d1d5db] transition hover:text-white hover:underline">
100
+ 210 3648 337
101
+ </a>
102
+ <a
103
+ :href="`mailto:${contactEmail}`"
104
+ class="text-[#d1d5db] transition hover:text-white hover:underline"
105
+ >
106
+ {{ contactEmail }}
107
+ </a>
108
+ <a
109
+ href="https://ketekny.gr"
110
+ target="_blank"
111
+ rel="noreferrer"
112
+ class="text-[#d1d5db] transition hover:text-white hover:underline"
113
+ >
114
+ https://ketekny.gr
115
+ </a>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <div
121
+ v-if="collapsedFlyoutItem"
122
+ class="fixed z-[200]"
123
+ :style="collapsedFlyoutStyle"
124
+ @mouseenter="handleCollapsedItemEnter(collapsedFlyoutItem)"
125
+ @mouseleave="handleCollapsedItemLeave(collapsedFlyoutItem)"
126
+ >
127
+ <div
128
+ class="w-[320px] rounded-2xl border border-[#242834] bg-[#0f1116] p-3 shadow-[0_18px_44px_rgba(0,0,0,0.32)]"
129
+ >
130
+ <div class="mb-2 px-2">
131
+ <div class="text-sm font-semibold text-white">
132
+ {{ collapsedFlyoutItem.label }}
133
+ </div>
134
+ <div
135
+ v-if="collapsedFlyoutItem.description"
136
+ class="mt-0.5 text-xs text-[#7f8794]"
137
+ >
138
+ {{ collapsedFlyoutItem.description }}
139
+ </div>
140
+ </div>
141
+
142
+ <TwoColSidebarMenuList
143
+ :items="collapsedFlyoutItem.children"
144
+ :expanded-state="expandedBranches"
145
+ @item-click="$emit('item-click', $event)"
146
+ @toggle-branch="toggleBranch"
147
+ />
148
+ </div>
149
+ </div>
150
+ </Teleport>
151
+
98
152
  <div
99
153
  v-if="account"
100
- class="px-1 pb-4 border-b shrink-0 border-white/10 bg-slate-900/80 text-white/90"
154
+ class="shrink-0 border-t border-[#171a21] px-3 pb-4 pt-3 text-white/90"
101
155
  >
102
156
  <div v-if="collapsed" class="flex flex-col gap-3">
103
157
  <div v-if="helpUrl" class="flex flex-col items-center gap-2">
@@ -105,66 +159,98 @@
105
159
  :href="helpUrl"
106
160
  target="_blank"
107
161
  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="'Χρειάζεστε βοήθεια;'"
162
+ aria-label="Βοήθεια"
163
+ title="Βοήθεια"
164
+ 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"
165
+ v-tooltip.right="'Βοήθεια'"
112
166
  >
113
- <kIcon name="HelpCircle" size="20" color="#ffffff" />
167
+ <kIcon name="HelpCircle" size="18" color="#b3b9c6" />
114
168
  </a>
115
169
  </div>
116
170
  <div v-if="account.loggedIn" class="flex flex-col items-center gap-2">
117
171
  <button
118
172
  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"
173
+ 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
174
  :title="editProfileLabel"
121
175
  v-tooltip.right="editProfileLabel"
122
176
  @click="$emit('edit-profile')"
123
177
  >
124
- <kIcon name="UserRoundCog" size="20" color="#ffffff" />
178
+ <kIcon name="UserRoundCog" size="18" color="#b3b9c6" />
125
179
  </button>
126
180
  <button
127
181
  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]"
182
+ 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
183
  :title="signOutLabel"
130
184
  v-tooltip.right="signOutLabel"
131
185
  @click="$emit('signout')"
132
186
  >
133
- <kIcon name="LogOut" size="20" color="#ffffff" />
187
+ <kIcon name="LogOut" size="18" color="#b3b9c6" />
134
188
  </button>
135
189
  </div>
136
190
  <div v-else class="flex flex-col items-center gap-2">
137
191
  <button
138
192
  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"
193
+ 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
194
  :title="signInLabel"
141
195
  v-tooltip.right="signInLabel"
142
196
  @click="$emit('signin')"
143
197
  >
144
- <kIcon name="LogIn" size="20" color="#ffffff" />
198
+ <kIcon name="LogIn" size="18" color="#b3b9c6" />
145
199
  </button>
146
200
  <button
147
201
  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"
202
+ 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
203
  :title="signUpLabel"
150
204
  v-tooltip.right="signUpLabel"
151
205
  @click="$emit('signup')"
152
206
  >
153
- <kIcon name="UserPlus" size="20" color="#ffffff" />
207
+ <kIcon name="UserPlus" size="18" color="#b3b9c6" />
154
208
  </button>
155
209
  </div>
156
210
  </div>
157
- <div v-else class="flex flex-col gap-3 px-3">
158
- <div class="min-w-0">
211
+ <div v-else class="flex flex-col px-1">
212
+ <div
213
+ v-if="helpUrl || account.loggedIn"
214
+ >
215
+ <a
216
+ v-if="helpUrl"
217
+ :href="helpUrl"
218
+ target="_blank"
219
+ 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
+ >
222
+ <kIcon name="HelpCircle" size="16" color="#d7dbe3" />
223
+ <span>Βοήθεια και υποστήριξη</span>
224
+ </a>
225
+ <button
226
+ v-if="account.loggedIn"
227
+ 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
+ @click="$emit('edit-profile')"
230
+ >
231
+ <kIcon name="UserRoundCog" size="16" color="#d7dbe3" />
232
+ <span>Το προφίλ μου</span>
233
+ </button>
234
+ <button
235
+ v-if="account.loggedIn"
236
+ 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
+ @click="$emit('signout')"
239
+ >
240
+ <kIcon name="LogOut" size="16" color="#f59e0b" />
241
+ <span>Αποσύνδεση</span>
242
+ </button>
243
+ </div>
244
+ <div v-if="!account.loggedIn" class="min-w-0">
159
245
  <p
160
246
  v-if="account.loggedIn"
161
- class="mt-2 text-lg font-semibold text-white truncate"
247
+ class="mt-1 truncate text-sm font-semibold text-white"
162
248
  >
163
249
  {{ accountDisplayName }}
164
250
  </p>
165
251
  <p
166
252
  v-if="account.loggedIn && displayEmail"
167
- class="text-sm truncate text-slate-300"
253
+ class="truncate text-xs text-[#7c8392]"
168
254
  >
169
255
  {{ displayEmail }}
170
256
  </p>
@@ -184,33 +270,19 @@
184
270
  :label="signUpLabel"
185
271
  @click="$emit('signup')"
186
272
  />
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
273
  </div>
202
274
  </div>
203
275
  </div>
204
276
 
205
277
  <div
206
278
  v-if="!collapsed"
207
- class="border-t shrink-0 border-white/10 text-white/70"
279
+ class="shrink-0 border-t border-[#171a21] p-3 text-[#8c93a1]"
208
280
  >
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">
281
+ <div v-if="showContactInfo" class="flex flex-col gap-2 px-1">
282
+ <p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#666d79]">
211
283
  KETEKNY AE
212
284
  </p>
213
- <div class="flex flex-wrap items-center text-sm gap-x-3">
285
+ <div class="flex flex-wrap items-center gap-x-3 text-xs">
214
286
  <span class="whitespace-nowrap">{{ contactAddressLabel }}</span>
215
287
  <span class="whitespace-nowrap">210 3648 337</span>
216
288
  <span class="whitespace-nowrap">
@@ -232,10 +304,7 @@
232
304
  </div>
233
305
  </div>
234
306
 
235
- <div
236
- v-if="showSponsors"
237
- class="mt-2 flex flex-col gap-3 bg-[#0d4b87] px-4 py-1"
238
- >
307
+ <div v-if="showSponsors" class="mt-2 -mx-3 flex flex-col gap-3 bg-[#0d4b87] px-4 py-1">
239
308
  <div>
240
309
  <img
241
310
  src="https://s3.ketekny.gr/public/web-apps/sponsors/Greece 2.0_NextGeneration_White_gr.png"
@@ -259,6 +328,8 @@
259
328
  import { RouterLink } from "vue-router";
260
329
  import kButton from "../ui/kButton.vue";
261
330
  import kIcon from "../ui/kIcon.vue";
331
+ import kSearch from "../ui/kSearch.vue";
332
+ import TwoColSidebarMenuList from "./twoColSidebarMenuList.vue";
262
333
 
263
334
  const LABELS = {
264
335
  editProfile: "Το προφίλ μου",
@@ -274,7 +345,9 @@ export default {
274
345
  components: {
275
346
  kButton,
276
347
  kIcon,
348
+ kSearch,
277
349
  RouterLink,
350
+ TwoColSidebarMenuList,
278
351
  },
279
352
  emits: [
280
353
  "logo-click",
@@ -299,8 +372,75 @@ export default {
299
372
  contactEmail: { type: String, default: "grammateia@ketekny.gr" },
300
373
  helpUrl: { type: String, default: "" },
301
374
  collapsed: { type: Boolean, default: false },
375
+ showMenuSearch: { type: Boolean, default: false },
376
+ menuSearchPlaceholder: {
377
+ type: String,
378
+ default: "Αναζήτηση...",
379
+ },
380
+ },
381
+ data() {
382
+ return {
383
+ expandedBranches: {},
384
+ collapsedFlyoutKey: null,
385
+ collapsedFlyoutPosition: null,
386
+ collapsedFlyoutCloseTimer: null,
387
+ collapsedContactFlyoutPosition: null,
388
+ collapsedContactFlyoutVisible: false,
389
+ collapsedContactFlyoutCloseTimer: null,
390
+ menuSearchTerm: "",
391
+ };
302
392
  },
303
393
  computed: {
394
+ filteredMenuItems() {
395
+ if (!this.showMenuSearch) return this.menuItems;
396
+
397
+ const query = this.menuSearchTerm.trim().toLowerCase();
398
+ if (!query) return this.menuItems;
399
+
400
+ const filterItems = items =>
401
+ items.reduce((acc, item) => {
402
+ const filteredChildren = item.children?.length
403
+ ? filterItems(item.children)
404
+ : [];
405
+ const matchesSelf =
406
+ item.label?.toLowerCase().includes(query) ||
407
+ item.description?.toLowerCase().includes(query);
408
+
409
+ if (!matchesSelf && filteredChildren.length === 0) {
410
+ return acc;
411
+ }
412
+
413
+ acc.push({
414
+ ...item,
415
+ children: filteredChildren,
416
+ hasChildren: filteredChildren.length > 0,
417
+ });
418
+ return acc;
419
+ }, []);
420
+
421
+ return filterItems(this.menuItems);
422
+ },
423
+ collapsedFlyoutItem() {
424
+ if (!this.collapsedFlyoutKey) return null;
425
+
426
+ return this.findMenuItemByKey(this.filteredMenuItems, this.collapsedFlyoutKey);
427
+ },
428
+ collapsedFlyoutStyle() {
429
+ if (!this.collapsedFlyoutPosition) return {};
430
+
431
+ return {
432
+ left: `${this.collapsedFlyoutPosition.left}px`,
433
+ top: `${this.collapsedFlyoutPosition.top}px`,
434
+ };
435
+ },
436
+ collapsedContactFlyoutStyle() {
437
+ if (!this.collapsedContactFlyoutPosition) return {};
438
+
439
+ return {
440
+ left: `${this.collapsedContactFlyoutPosition.left}px`,
441
+ top: `${this.collapsedContactFlyoutPosition.top}px`,
442
+ };
443
+ },
304
444
  accountDisplayName() {
305
445
  if (!this.account) return "";
306
446
 
@@ -329,21 +469,33 @@ export default {
329
469
  contactAddressLabel() {
330
470
  return LABELS.contactAddress;
331
471
  },
332
- collapsedContactTooltip() {
333
- return `
334
- <div style="min-width: 220px;">
335
- <div style="font-weight: 700; text-transform: uppercase; letter-spacing: 0.25em; font-size: 11px; color: rgba(255,255,255,0.65); margin-bottom: 8px;">
336
- KETEKNY AE
337
- </div>
338
- <div style="display: flex; flex-direction: column; gap: 4px; font-size: 13px; line-height: 1.4; color: rgba(255,255,255,0.9);">
339
- <div>${LABELS.contactAddress}</div>
340
- <div>210 3648 337</div>
341
- <div>${this.contactEmail}</div>
342
- <div>https://ketekny.gr</div>
343
- </div>
344
- </div>
345
- `;
472
+ },
473
+ watch: {
474
+ menuItems: {
475
+ immediate: true,
476
+ handler(items) {
477
+ this.expandedBranches = this.buildExpandedState(items, {
478
+ ...this.expandedBranches,
479
+ });
480
+
481
+ if (
482
+ this.collapsedFlyoutKey &&
483
+ !this.findMenuItemByKey(items, this.collapsedFlyoutKey)
484
+ ) {
485
+ this.collapsedFlyoutKey = null;
486
+ this.collapsedFlyoutPosition = null;
487
+ }
488
+ },
346
489
  },
490
+ menuSearchTerm() {
491
+ this.expandedBranches = this.buildExpandedState(this.filteredMenuItems, {
492
+ ...this.expandedBranches,
493
+ });
494
+ },
495
+ },
496
+ beforeUnmount() {
497
+ this.clearCollapsedFlyoutCloseTimer();
498
+ this.clearCollapsedContactFlyoutCloseTimer();
347
499
  },
348
500
  methods: {
349
501
  menuItemComponent(item) {
@@ -351,6 +503,131 @@ export default {
351
503
  if (item?.isExternal) return "a";
352
504
  return "button";
353
505
  },
506
+ collapsedItemTooltip(item) {
507
+ return item?.hasChildren ? null : item.label;
508
+ },
509
+ buildExpandedState(items, state = {}) {
510
+ items.forEach(item => {
511
+ if (!item?.hasChildren) return;
512
+
513
+ const hasActiveDescendant = this.branchHasActiveChild(item.children);
514
+ if (!(item.key in state)) {
515
+ state[item.key] = Boolean(hasActiveDescendant);
516
+ } else if (hasActiveDescendant) {
517
+ state[item.key] = true;
518
+ }
519
+ this.buildExpandedState(item.children, state);
520
+ });
521
+
522
+ return state;
523
+ },
524
+ branchHasActiveChild(children = []) {
525
+ return children.some(
526
+ child => child.active || this.branchHasActiveChild(child.children || [])
527
+ );
528
+ },
529
+ toggleBranch(key) {
530
+ this.expandedBranches = {
531
+ ...this.expandedBranches,
532
+ [key]: !this.expandedBranches[key],
533
+ };
534
+ },
535
+ findMenuItemByKey(items, key) {
536
+ for (const item of items) {
537
+ if (item.key === key) return item;
538
+ if (item.children?.length) {
539
+ const found = this.findMenuItemByKey(item.children, key);
540
+ if (found) return found;
541
+ }
542
+ }
543
+
544
+ return null;
545
+ },
546
+ updateCollapsedFlyoutPosition(event) {
547
+ const trigger = event?.currentTarget;
548
+ if (!trigger || typeof window === "undefined") return;
549
+
550
+ const rect = trigger.getBoundingClientRect();
551
+ this.collapsedFlyoutPosition = {
552
+ left: rect.right + 12,
553
+ top: Math.max(12, rect.top),
554
+ };
555
+ },
556
+ updateCollapsedContactFlyoutPosition(event) {
557
+ const trigger = event?.currentTarget;
558
+ if (!trigger || typeof window === "undefined") return;
559
+
560
+ const rect = trigger.getBoundingClientRect();
561
+ this.collapsedContactFlyoutPosition = {
562
+ left: rect.right + 12,
563
+ top: Math.max(12, rect.top + 8),
564
+ };
565
+ },
566
+ clearCollapsedFlyoutCloseTimer() {
567
+ if (this.collapsedFlyoutCloseTimer) {
568
+ window.clearTimeout(this.collapsedFlyoutCloseTimer);
569
+ this.collapsedFlyoutCloseTimer = null;
570
+ }
571
+ },
572
+ clearCollapsedContactFlyoutCloseTimer() {
573
+ if (this.collapsedContactFlyoutCloseTimer) {
574
+ window.clearTimeout(this.collapsedContactFlyoutCloseTimer);
575
+ this.collapsedContactFlyoutCloseTimer = null;
576
+ }
577
+ },
578
+ handleCollapsedContactEnter(event) {
579
+ if (!this.collapsed || !this.showContactInfo) return;
580
+
581
+ this.clearCollapsedContactFlyoutCloseTimer();
582
+ this.collapsedContactFlyoutVisible = true;
583
+ if (event) {
584
+ this.updateCollapsedContactFlyoutPosition(event);
585
+ }
586
+ },
587
+ handleCollapsedContactLeave() {
588
+ if (!this.collapsedContactFlyoutVisible) return;
589
+
590
+ this.clearCollapsedContactFlyoutCloseTimer();
591
+ this.collapsedContactFlyoutCloseTimer = window.setTimeout(() => {
592
+ this.collapsedContactFlyoutVisible = false;
593
+ this.collapsedContactFlyoutCloseTimer = null;
594
+ }, 180);
595
+ },
596
+ handleCollapsedItemEnter(item, event) {
597
+ if (!item?.hasChildren) return;
598
+
599
+ this.clearCollapsedFlyoutCloseTimer();
600
+ this.collapsedFlyoutKey = item.key;
601
+ if (event) {
602
+ this.updateCollapsedFlyoutPosition(event);
603
+ }
604
+ },
605
+ handleCollapsedItemLeave(item) {
606
+ if (this.collapsedFlyoutKey !== item?.key) return;
607
+
608
+ this.clearCollapsedFlyoutCloseTimer();
609
+ this.collapsedFlyoutCloseTimer = window.setTimeout(() => {
610
+ if (this.collapsedFlyoutKey !== item?.key) return;
611
+
612
+ this.collapsedFlyoutKey = null;
613
+ this.collapsedFlyoutPosition = null;
614
+ this.collapsedFlyoutCloseTimer = null;
615
+ }, 180);
616
+ },
617
+ showCollapsedFlyout(item) {
618
+ return this.collapsedFlyoutKey === item?.key && item?.hasChildren;
619
+ },
620
+ handleCollapsedItemClick(item, event) {
621
+ if (item?.hasChildren) {
622
+ event?.preventDefault?.();
623
+ this.clearCollapsedFlyoutCloseTimer();
624
+ this.collapsedFlyoutKey = item.key;
625
+ this.updateCollapsedFlyoutPosition(event);
626
+ return;
627
+ }
628
+
629
+ this.$emit("item-click", item);
630
+ },
354
631
  },
355
632
  };
356
633
  </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 {