ketekny-ui-kit 1.0.75 → 1.0.77

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.75",
4
+ "version": "1.0.77",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -3,7 +3,7 @@
3
3
  class="relative flex min-h-screen flex-col bg-[#edf4fb] lg:h-screen lg:flex-row lg:overflow-hidden"
4
4
  >
5
5
  <button
6
- v-show="isSidebarCollapsed"
6
+ v-show="isDesktopViewport && isSidebarCollapsed"
7
7
  type="button"
8
8
  :aria-label="expandLabel"
9
9
  v-tooltip.right="expandLabel"
@@ -16,7 +16,7 @@
16
16
  </button>
17
17
 
18
18
  <button
19
- v-show="!isSidebarCollapsed"
19
+ v-show="isDesktopViewport && !isSidebarCollapsed"
20
20
  type="button"
21
21
  :aria-label="collapseLabel"
22
22
  v-tooltip.left="collapseLabel"
@@ -29,229 +29,91 @@
29
29
  </button>
30
30
 
31
31
  <aside
32
- class="flex h-screen w-full flex-col overflow-y-auto 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:w-[360px] lg:shrink-0 lg:will-change-transform"
33
- :class="
34
- isSidebarCollapsed
35
- ? 'lg:relative lg:z-30 lg:w-[76px]'
36
- : 'lg:relative lg:z-30 lg:translate-x-0'
37
- "
32
+ v-if="isDesktopViewport"
33
+ class="hidden lg:flex lg:shrink-0 transition-all duration-300 ease-out"
34
+ :class="isSidebarCollapsed ? 'lg:w-[76px]' : 'lg:w-[360px]'"
38
35
  >
39
- <div class="relative">
40
- <button
41
- v-if="logoSrc"
42
- type="button"
43
- class="block w-full text-left"
44
- v-tooltip.right.html="
45
- isSidebarCollapsed ? collapsedContactTooltip : logoAlt
46
- "
47
- @click="$emit('logo-click')"
48
- >
49
- <img
50
- :alt="logoAlt"
51
- :src="isSidebarCollapsed ? collapsedLogoSrc : logoSrc"
52
- class="cursor-pointer px-4 py-4 transition-all duration-300"
53
- />
54
- </button>
55
- <div
56
- v-if="title && !isSidebarCollapsed"
57
- class="py-3 text-xl font-bold text-center"
58
- >
59
- {{ title }}
60
- </div>
61
-
62
- <slot name="sidebar-header" />
63
- </div>
36
+ <SidebarPanel
37
+ :title="title"
38
+ :collapsed-logo-src="collapsedLogoSrc"
39
+ :logo-src="logoSrc"
40
+ :logo-alt="logoAlt"
41
+ :show-sponsors="showSponsors"
42
+ :menu-items="normalizedMainMenu"
43
+ :show-contact-info="showContactInfo"
44
+ :account="account"
45
+ :contact-email="contactEmail"
46
+ :collapsed="isSidebarCollapsed"
47
+ @logo-click="handleLogoClick"
48
+ @signin="handleSignin"
49
+ @signup="handleSignup"
50
+ @signout="handleSignout"
51
+ @edit-profile="handleEditProfile"
52
+ >
53
+ <template #sidebar-header>
54
+ <slot name="sidebar-header" />
55
+ </template>
56
+ </SidebarPanel>
57
+ </aside>
64
58
 
65
- <div class="flex-1 pt-4">
66
- <div
67
- v-if="isSidebarCollapsed"
68
- class="mx-1 mt-3 flex flex-col items-center gap-2"
69
- >
70
- <SidebarIconLink
71
- v-for="item in normalizedMainMenu"
72
- :key="item.key"
73
- :icon="item.icon"
74
- :active="item.active"
75
- v-bind="item.bindings"
76
- v-tooltip.right="item.label"
77
- />
78
- </div>
79
- <div v-else-if="mainMenu.length" class="mx-2 mt-3 space-y-3">
80
- <component
81
- v-for="item in normalizedMainMenu"
82
- :key="item.key"
83
- :is="menuItemTag(item)"
84
- v-bind="item.bindings"
85
- class="flex w-full items-start gap-3 rounded-2xl border px-4 py-4 text-left transition-all duration-200"
86
- :class="item.classes"
87
- >
88
- <span
89
- 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"
90
- :class="item.active ? 'bg-slate-950 text-white' : ''"
91
- >
92
- <kIcon :name="item.icon" />
93
- </span>
94
- <span class="min-w-0">
95
- <span class="block font-semibold leading-tight">
96
- {{ item.label }}
97
- </span>
98
- <span
99
- v-if="item.description"
100
- class="mt-1 block text-sm"
101
- :class="item.active ? 'text-slate-700' : 'text-slate-300'"
102
- >
103
- {{ item.description }}
104
- </span>
105
- </span>
106
- </component>
107
- </div>
108
- </div>
59
+ <button
60
+ v-if="!isDesktopViewport && !mobileMenuOpen"
61
+ type="button"
62
+ 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"
63
+ aria-label="Open menu"
64
+ @click="mobileMenuOpen = true"
65
+ >
66
+ <kIcon name="Menu" size="22" class="text-white" />
67
+ </button>
109
68
 
110
- <div
111
- v-if="account"
112
- class="shrink-0 border-b border-white/10 bg-slate-900/80 px-1 pb-4 text-white/80"
113
- >
114
- <div v-if="isSidebarCollapsed" class="flex flex-col gap-3">
115
- <div v-if="account.loggedIn" class="flex flex-col items-center gap-2">
116
- <SidebarIconLink
117
- icon="UserRoundCog"
118
- :on-click="() => $emit('edit-profile')"
119
- :title="'Επεξεργασία προφίλ'"
120
- v-tooltip.right="'Επεξεργασία προφίλ'"
121
- />
122
- <SidebarIconLink
123
- icon="LogOut"
124
- warning
125
- :on-click="() => $emit('signout')"
126
- :title="'Αποσύνδεση'"
127
- v-tooltip.right="'Αποσύνδεση'"
128
- />
129
- </div>
130
- <div v-else class="flex flex-col items-center gap-2">
131
- <SidebarIconLink
132
- icon="LogIn"
133
- :on-click="() => $emit('signin')"
134
- :title="'Sign in'"
135
- v-tooltip.right="'Sign in'"
136
- />
137
- <SidebarIconLink
138
- icon="UserPlus"
139
- :on-click="() => $emit('signup')"
140
- :title="'Sign up'"
141
- v-tooltip.right="'Sign up'"
142
- />
143
- </div>
144
- </div>
145
- <div v-else class="flex flex-col gap-3 px-3">
146
- <div class="min-w-0">
147
- <p
148
- v-if="account.loggedIn"
149
- class="mt-2 truncate text-lg font-semibold text-white"
150
- >
151
- {{ accountDisplayName }}
152
- </p>
153
- <p
154
- v-if="account.loggedIn && displayEmail"
155
- class="truncate text-sm text-slate-300"
156
- >
157
- {{ displayEmail }}
158
- </p>
159
- </div>
160
- <div class="flex flex-wrap justify-between gap-2">
161
- <kButton
162
- v-if="!account.loggedIn"
163
- secondary
164
- icon="LogIn"
165
- label="Σύνδεση"
166
- @click="$emit('signin')"
167
- />
168
- <kButton
169
- v-if="!account.loggedIn"
170
- secondary
171
- icon="UserPlus"
172
- label="Εγγραφή"
173
- @click="$emit('signup')"
174
- />
175
- <kButton
176
- v-if="account.loggedIn"
177
- variant="soft"
178
- icon="UserRoundCog"
179
- label="Επεξεργασία προφίλ"
180
- @click="$emit('edit-profile')"
181
- />
182
- <kButton
183
- v-if="account.loggedIn"
184
- icon="LogOut"
185
- warning
186
- label="Αποσύνδεση"
187
- @click="$emit('signout')"
188
- />
189
- </div>
190
- </div>
191
- </div>
69
+ <transition name="mobile-menu-backdrop">
70
+ <button
71
+ v-if="!isDesktopViewport && mobileMenuOpen"
72
+ type="button"
73
+ class="fixed inset-0 z-[85] bg-black/50 backdrop-blur-sm"
74
+ aria-label="Close menu"
75
+ @click="closeMobileMenu"
76
+ />
77
+ </transition>
192
78
 
193
- <div
194
- v-if="!isSidebarCollapsed"
195
- class="shrink-0 border-t border-white/10 text-white/50"
79
+ <transition name="mobile-menu-panel">
80
+ <aside
81
+ v-if="!isDesktopViewport && mobileMenuOpen"
82
+ class="fixed inset-y-0 left-0 z-[90] flex h-[100dvh] w-[min(90vw,420px)] max-w-full overflow-hidden shadow-2xl"
83
+ role="dialog"
84
+ aria-modal="true"
196
85
  >
197
- <div class="flex flex-col gap-2 px-4" v-if="showContactInfo">
198
- <p
199
- class="text-sm font-semibold uppercase tracking-[0.25em] text-white/60"
200
- >
201
- KETEKNY AE
202
- </p>
203
- <div
204
- class="flex flex-wrap items-center gap-x-3 text-sm"
205
- :class="isSidebarCollapsed ? 'justify-center' : ''"
206
- >
207
- <span v-if="isSidebarCollapsed" class="sr-only"
208
- >KETEKNY AE contact info</span
209
- >
210
- <span class="whitespace-nowrap">Βερανζέρου 13, 10677, Αθήνα</span>
211
- <span class="whitespace-nowrap">210 3648 337</span>
212
- <span class="whitespace-nowrap">
213
- <a
214
- :href="`mailto:${contactEmail}`"
215
- class="whitespace-nowrap hover:underline"
216
- >
217
- {{ contactEmail }}
218
- </a>
219
- </span>
220
- <a
221
- href="https://ketekny.gr"
222
- target="_blank"
223
- rel="noreferrer"
224
- class="whitespace-nowrap hover:underline"
86
+ <div class="relative flex h-full w-full flex-col bg-transparent">
87
+ <div class="flex-1 overflow-y-auto bg-transparent">
88
+ <SidebarPanel
89
+ :title="title"
90
+ :collapsed-logo-src="collapsedLogoSrc"
91
+ :logo-src="logoSrc"
92
+ :logo-alt="logoAlt"
93
+ :show-sponsors="showSponsors"
94
+ :menu-items="normalizedMainMenu"
95
+ :show-contact-info="showContactInfo"
96
+ :account="account"
97
+ :contact-email="contactEmail"
98
+ :collapsed="false"
99
+ @logo-click="handleLogoClick"
100
+ @item-click="closeMobileMenu"
101
+ @signin="handleSignin"
102
+ @signup="handleSignup"
103
+ @signout="handleSignout"
104
+ @edit-profile="handleEditProfile"
225
105
  >
226
- https://ketekny.gr
227
- </a>
106
+ <template #sidebar-header>
107
+ <slot name="sidebar-header" />
108
+ </template>
109
+ </SidebarPanel>
228
110
  </div>
229
111
  </div>
230
-
231
- <div
232
- v-if="showSponsors && !isSidebarCollapsed"
233
- class="flex flex-col gap-3 px-4 py-1 bg-[#0d4b87] mt-2"
234
- >
235
- <div>
236
- <img
237
- src="https://s3.ketekny.gr/public/web-apps/sponsors/Greece 2.0_NextGeneration_White_gr.png"
238
- alt="Tameio Anakampsis"
239
- class="mx-auto max-h-14 w-auto object-contain"
240
- />
241
- </div>
242
- <div>
243
- <img
244
- src="https://s3.ketekny.gr/public/web-apps/sponsors/yp_ygeias_dark.png"
245
- alt="Ypourgeio Ygeias"
246
- class="mx-auto max-h-14 w-auto object-contain"
247
- />
248
- </div>
249
- </div>
250
- </div>
251
- </aside>
112
+ </aside>
113
+ </transition>
252
114
 
253
115
  <main
254
- class="min-h-screen min-w-0 flex-1 overflow-y-auto overflow-x-hidden lg:h-full lg:min-h-0"
116
+ class="min-h-0 min-w-0 flex-1 overflow-visible lg:h-full lg:min-h-0 lg:overflow-y-auto lg:overflow-x-hidden"
255
117
  >
256
118
  <div class="w-full">
257
119
  <slot />
@@ -260,68 +122,18 @@
260
122
  </div>
261
123
  </template>
262
124
 
263
- <script>
264
- import { h } from "vue";
265
- import kButton from "../ui/kButton.vue";
125
+ <script>
266
126
  import kIcon from "../ui/kIcon.vue";
267
- import { RouterLink } from "vue-router";
268
-
269
- const SidebarIconLink = {
270
- props: {
271
- icon: { type: String, required: true },
272
- active: { type: Boolean, default: false },
273
- to: { type: [String, Object], default: null },
274
- href: { type: String, default: null },
275
- title: { type: String, default: "" },
276
- isExternal: { type: Boolean, default: false },
277
- target: { type: String, default: "_blank" },
278
- rel: { type: String, default: "noreferrer" },
279
- onClick: { type: Function, default: null },
280
- warning: { type: Boolean, default: false },
281
- tooltip: { type: String, default: "" },
282
- },
283
- methods: {
284
- handleClick(event) {
285
- if (this.onClick) this.onClick(event);
286
- },
287
- },
288
- render() {
289
- const isLink = Boolean(this.to);
290
- const tag = isLink ? RouterLink : this.isExternal ? "a" : "button";
291
-
292
- return h(
293
- tag,
294
- {
295
- to: isLink ? this.to : undefined,
296
- href: this.href,
297
- target: this.isExternal ? this.target : null,
298
- rel: this.isExternal ? this.rel : null,
299
- type: isLink || this.isExternal ? null : "button",
300
- title: this.title,
301
- class: [
302
- "group relative flex h-14 w-14 items-center justify-center rounded-xl border border-white/15 transition",
303
- this.active
304
- ? "bg-slate-950 text-white"
305
- : this.warning
306
- ? "bg-[#d97706] text-white hover:bg-[#b45309]"
307
- : "bg-white/5 text-white hover:border-cyan-400/40 hover:bg-white/10",
308
- isLink ? "cursor-pointer" : "",
309
- ],
310
- onClick: this.handleClick,
311
- },
312
- [h(kIcon, { name: this.icon, size: "20", color: "#ffffff" })]
313
- );
314
- },
315
- };
127
+ import SidebarPanel from "./twoColSidebarPanel.vue";
316
128
 
317
129
  const DEFAULT_STORAGE_KEY = "two-col-layout-sidebar-collapsed";
130
+ const TAILWIND_MD_QUERY = "(min-width: 768px) and (max-width: 1023px)";
131
+ const TAILWIND_LG_QUERY = "(min-width: 1024px)";
318
132
 
319
133
  export default {
320
134
  components: {
321
- kButton,
322
135
  kIcon,
323
- RouterLink,
324
- SidebarIconLink,
136
+ SidebarPanel,
325
137
  },
326
138
  emits: ["logo-click", "signin", "signup", "signout", "edit-profile"],
327
139
  props: {
@@ -346,56 +158,110 @@ export default {
346
158
  },
347
159
  data() {
348
160
  return {
349
- isSidebarCollapsed: false,
161
+ isSidebarCollapsed: null,
350
162
  mdCollapseQuery: null,
163
+ lgViewportQuery: null,
164
+ isDesktopViewport: false,
165
+ mobileMenuOpen: false,
351
166
  };
352
167
  },
353
168
  created() {
354
- this.restoreSidebarState();
169
+ this.isSidebarCollapsed = this.getInitialSidebarState();
170
+
171
+ if (typeof window !== "undefined") {
172
+ this.isDesktopViewport = window.matchMedia(TAILWIND_LG_QUERY).matches;
173
+ }
355
174
  },
356
175
  mounted() {
357
- this.mdCollapseQuery = window.matchMedia(
358
- "(min-width: 768px) and (max-width: 1023px)"
359
- );
176
+ this.mdCollapseQuery = window.matchMedia(TAILWIND_MD_QUERY);
177
+ this.lgViewportQuery = window.matchMedia(TAILWIND_LG_QUERY);
178
+ this.isDesktopViewport = this.lgViewportQuery.matches;
179
+
360
180
  this.syncSidebarForViewport();
361
- this.mdCollapseQuery.addEventListener(
362
- "change",
363
- this.syncSidebarForViewport
364
- );
181
+ this.mdCollapseQuery.addEventListener("change", this.syncSidebarForViewport);
182
+ this.lgViewportQuery.addEventListener("change", this.syncDesktopViewport);
365
183
  },
366
184
  beforeUnmount() {
367
185
  this.mdCollapseQuery?.removeEventListener(
368
186
  "change",
369
187
  this.syncSidebarForViewport
370
188
  );
189
+ this.lgViewportQuery?.removeEventListener(
190
+ "change",
191
+ this.syncDesktopViewport
192
+ );
193
+ if (typeof document !== "undefined") {
194
+ document.body.style.overflow = "";
195
+ }
196
+ },
197
+ watch: {
198
+ mobileMenuOpen(isOpen) {
199
+ if (typeof document === "undefined") return;
200
+
201
+ document.body.style.overflow = isOpen ? "hidden" : "";
202
+ },
371
203
  },
372
204
  methods: {
373
- restoreSidebarState() {
205
+ isMdViewport() {
206
+ if (typeof window === "undefined") return false;
207
+
208
+ return window.matchMedia(TAILWIND_MD_QUERY).matches;
209
+ },
210
+ getInitialSidebarState() {
374
211
  if (typeof window === "undefined") {
375
- this.isSidebarCollapsed = this.defaultCollapsed;
376
- return;
212
+ return this.defaultCollapsed;
213
+ }
214
+
215
+ if (this.isMdViewport()) {
216
+ return true;
377
217
  }
378
218
 
379
219
  const stored = window.localStorage.getItem(this.storageKey);
380
220
  if (stored == null) {
381
- this.isSidebarCollapsed = this.defaultCollapsed;
382
- return;
221
+ return this.defaultCollapsed;
383
222
  }
384
223
 
385
- this.isSidebarCollapsed = stored === "true";
224
+ return stored === "true";
386
225
  },
387
226
  syncSidebarForViewport() {
388
227
  if (typeof window === "undefined") return;
389
228
 
390
- const isMd = window.matchMedia(
391
- "(min-width: 768px) and (max-width: 1023px)"
392
- ).matches;
393
- if (isMd) {
229
+ if (this.isMdViewport()) {
394
230
  this.isSidebarCollapsed = true;
395
231
  return;
396
232
  }
397
233
 
398
- this.restoreSidebarState();
234
+ this.isSidebarCollapsed = this.getInitialSidebarState();
235
+ },
236
+ syncDesktopViewport(event) {
237
+ this.isDesktopViewport = event.matches;
238
+
239
+ if (event.matches) {
240
+ this.mobileMenuOpen = false;
241
+ }
242
+ },
243
+ closeMobileMenu() {
244
+ this.mobileMenuOpen = false;
245
+ },
246
+ handleLogoClick() {
247
+ this.closeMobileMenu();
248
+ this.$emit("logo-click");
249
+ },
250
+ handleSignin() {
251
+ this.closeMobileMenu();
252
+ this.$emit("signin");
253
+ },
254
+ handleSignup() {
255
+ this.closeMobileMenu();
256
+ this.$emit("signup");
257
+ },
258
+ handleSignout() {
259
+ this.closeMobileMenu();
260
+ this.$emit("signout");
261
+ },
262
+ handleEditProfile() {
263
+ this.closeMobileMenu();
264
+ this.$emit("edit-profile");
399
265
  },
400
266
  persistSidebarState() {
401
267
  if (typeof window === "undefined") return;
@@ -409,11 +275,6 @@ export default {
409
275
  this.isSidebarCollapsed = !this.isSidebarCollapsed;
410
276
  this.persistSidebarState();
411
277
  },
412
- menuItemTag(item) {
413
- if (item?.to) return RouterLink;
414
- if (item?.href) return "a";
415
- return "button";
416
- },
417
278
  },
418
279
  computed: {
419
280
  normalizedMainMenu() {
@@ -453,56 +314,25 @@ export default {
453
314
  };
454
315
  });
455
316
  },
456
- menuItemBindings(item) {
457
- if (item?.to) {
458
- return {
459
- to: item.to,
460
- title: item.title || item.label,
461
- "aria-current": item.active ? "page" : null,
462
- };
463
- }
464
-
465
- if (item?.href) {
466
- return {
467
- href: item.href,
468
- target: item.target || "_blank",
469
- rel: item.rel || "noreferrer",
470
- title: item.title || item.label,
471
- };
472
- }
473
-
474
- return {
475
- type: "button",
476
- title: item?.title || item?.label,
477
- };
478
- },
479
- accountDisplayName() {
480
- if (!this.account) return "";
481
- return (
482
- [this.account.firstName, this.account.lastName]
483
- .filter(Boolean)
484
- .join(" ")
485
- .trim() || "Account"
486
- );
487
- },
488
- displayEmail() {
489
- return this.account?.email || "";
490
- },
491
- collapsedContactTooltip() {
492
- return `
493
- <div style="min-width: 220px;">
494
- <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;">
495
- KETEKNY AE
496
- </div>
497
- <div style="display: flex; flex-direction: column; gap: 4px; font-size: 13px; line-height: 1.4; color: rgba(255,255,255,0.9);">
498
- <div>Βερανζέρου 13, 10677, Αθήνα</div>
499
- <div>210 3648 337</div>
500
- <div>${this.contactEmail}</div>
501
- <div>https://ketekny.gr</div>
502
- </div>
503
- </div>
504
- `;
505
- },
506
317
  },
507
318
  };
508
319
  </script>
320
+
321
+ <style scoped>
322
+ .mobile-menu-panel-enter-active,
323
+ .mobile-menu-panel-leave-active,
324
+ .mobile-menu-backdrop-enter-active,
325
+ .mobile-menu-backdrop-leave-active {
326
+ transition: opacity 0.25s ease, transform 0.25s ease;
327
+ }
328
+
329
+ .mobile-menu-panel-enter-from,
330
+ .mobile-menu-panel-leave-to {
331
+ transform: translateX(-100%);
332
+ }
333
+
334
+ .mobile-menu-backdrop-enter-from,
335
+ .mobile-menu-backdrop-leave-to {
336
+ opacity: 0;
337
+ }
338
+ </style>
@@ -0,0 +1,329 @@
1
+ <template>
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"
4
+ >
5
+ <div class="relative">
6
+ <button
7
+ v-if="logoSrc"
8
+ type="button"
9
+ class="block w-full text-left"
10
+ v-tooltip.right.html="collapsed ? collapsedContactTooltip : logoAlt"
11
+ @click="$emit('logo-click')"
12
+ >
13
+ <img
14
+ :alt="logoAlt"
15
+ :src="collapsed ? collapsedLogoSrc : logoSrc"
16
+ class="px-4 py-4 transition-all duration-300 cursor-pointer"
17
+ />
18
+ </button>
19
+ <div
20
+ v-if="title && !collapsed"
21
+ class="py-3 text-xl font-bold text-center"
22
+ >
23
+ {{ title }}
24
+ </div>
25
+
26
+ <slot name="sidebar-header" />
27
+ </div>
28
+
29
+ <div class="flex-1 pt-4">
30
+ <div v-if="collapsed" class="flex flex-col items-center gap-2 mx-1 mt-3">
31
+ <component
32
+ :is="menuItemComponent(item)"
33
+ v-for="item in menuItems"
34
+ :key="item.key"
35
+ v-bind="item.bindings"
36
+ class="relative flex items-center justify-center transition border group h-14 w-14 rounded-xl border-white/15"
37
+ :class="item.classes"
38
+ v-tooltip.right="item.label"
39
+ @click="$emit('item-click', item)"
40
+ >
41
+ <kIcon
42
+ :name="item.icon"
43
+ size="20"
44
+ :color="item.active ? '#0f172a' : '#ffffff'"
45
+ />
46
+ </component>
47
+ </div>
48
+ <div v-else-if="menuItems.length" class="mx-2 mt-3 space-y-3">
49
+ <component
50
+ :is="menuItemComponent(item)"
51
+ v-for="item in menuItems"
52
+ :key="item.key"
53
+ v-bind="item.bindings"
54
+ class="flex items-start w-full gap-3 px-4 py-4 text-left transition-all duration-200 border rounded-2xl"
55
+ :class="item.classes"
56
+ @click="$emit('item-click', item)"
57
+ >
58
+ <span
59
+ 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"
60
+ :class="
61
+ item.active
62
+ ? 'bg-slate-950 text-white'
63
+ : item.label === 'Settings'
64
+ ? 'bg-sky-500/20 text-sky-50 ring-1 ring-sky-400/25'
65
+ : ''
66
+ "
67
+ >
68
+ <kIcon :name="item.icon" />
69
+ </span>
70
+ <span class="min-w-0">
71
+ <span class="block font-semibold leading-tight">
72
+ {{ item.label }}
73
+ </span>
74
+ <span
75
+ v-if="item.description"
76
+ class="block mt-1 text-sm"
77
+ :class="item.active ? 'text-slate-700' : 'text-slate-300'"
78
+ >
79
+ {{ item.description }}
80
+ </span>
81
+ </span>
82
+ </component>
83
+ </div>
84
+ </div>
85
+
86
+ <div
87
+ v-if="account"
88
+ class="px-1 pb-4 border-b shrink-0 border-white/10 bg-slate-900/80 text-white/80"
89
+ >
90
+ <div v-if="collapsed" class="flex flex-col gap-3">
91
+ <div v-if="account.loggedIn" class="flex flex-col items-center gap-2">
92
+ <button
93
+ type="button"
94
+ 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"
95
+ :title="editProfileLabel"
96
+ v-tooltip.right="editProfileLabel"
97
+ @click="$emit('edit-profile')"
98
+ >
99
+ <kIcon name="UserRoundCog" size="20" color="#ffffff" />
100
+ </button>
101
+ <button
102
+ type="button"
103
+ 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]"
104
+ :title="signOutLabel"
105
+ v-tooltip.right="signOutLabel"
106
+ @click="$emit('signout')"
107
+ >
108
+ <kIcon name="LogOut" size="20" color="#ffffff" />
109
+ </button>
110
+ </div>
111
+ <div v-else class="flex flex-col items-center gap-2">
112
+ <button
113
+ type="button"
114
+ 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"
115
+ :title="signInLabel"
116
+ v-tooltip.right="signInLabel"
117
+ @click="$emit('signin')"
118
+ >
119
+ <kIcon name="LogIn" size="20" color="#ffffff" />
120
+ </button>
121
+ <button
122
+ type="button"
123
+ 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"
124
+ :title="signUpLabel"
125
+ v-tooltip.right="signUpLabel"
126
+ @click="$emit('signup')"
127
+ >
128
+ <kIcon name="UserPlus" size="20" color="#ffffff" />
129
+ </button>
130
+ </div>
131
+ </div>
132
+ <div v-else class="flex flex-col gap-3 px-3">
133
+ <div class="min-w-0">
134
+ <p
135
+ v-if="account.loggedIn"
136
+ class="mt-2 text-lg font-semibold text-white truncate"
137
+ >
138
+ {{ accountDisplayName }}
139
+ </p>
140
+ <p
141
+ v-if="account.loggedIn && displayEmail"
142
+ class="text-sm truncate text-slate-300"
143
+ >
144
+ {{ displayEmail }}
145
+ </p>
146
+ </div>
147
+ <div class="flex flex-wrap justify-between gap-2">
148
+ <kButton
149
+ v-if="!account.loggedIn"
150
+ secondary
151
+ icon="LogIn"
152
+ :label="signInLabel"
153
+ @click="$emit('signin')"
154
+ />
155
+ <kButton
156
+ v-if="!account.loggedIn"
157
+ secondary
158
+ icon="UserPlus"
159
+ :label="signUpLabel"
160
+ @click="$emit('signup')"
161
+ />
162
+ <kButton
163
+ v-if="account.loggedIn"
164
+ variant="soft"
165
+ icon="UserRoundCog"
166
+ :label="editProfileLabel"
167
+ @click="$emit('edit-profile')"
168
+ />
169
+ <kButton
170
+ v-if="account.loggedIn"
171
+ icon="LogOut"
172
+ warning
173
+ :label="signOutLabel"
174
+ @click="$emit('signout')"
175
+ />
176
+ </div>
177
+ </div>
178
+ </div>
179
+
180
+ <div
181
+ v-if="!collapsed"
182
+ class="border-t shrink-0 border-white/10 text-white/50"
183
+ >
184
+ <div v-if="showContactInfo" class="flex flex-col gap-2 px-4">
185
+ <p class="text-sm font-semibold uppercase tracking-[0.25em] text-white/60">
186
+ KETEKNY AE
187
+ </p>
188
+ <div class="flex flex-wrap items-center text-sm gap-x-3">
189
+ <span class="whitespace-nowrap">{{ contactAddressLabel }}</span>
190
+ <span class="whitespace-nowrap">210 3648 337</span>
191
+ <span class="whitespace-nowrap">
192
+ <a
193
+ :href="`mailto:${contactEmail}`"
194
+ class="whitespace-nowrap hover:underline"
195
+ >
196
+ {{ contactEmail }}
197
+ </a>
198
+ </span>
199
+ <a
200
+ href="https://ketekny.gr"
201
+ target="_blank"
202
+ rel="noreferrer"
203
+ class="whitespace-nowrap hover:underline"
204
+ >
205
+ https://ketekny.gr
206
+ </a>
207
+ </div>
208
+ </div>
209
+
210
+ <div
211
+ v-if="showSponsors"
212
+ class="mt-2 flex flex-col gap-3 bg-[#0d4b87] px-4 py-1"
213
+ >
214
+ <div>
215
+ <img
216
+ src="https://s3.ketekny.gr/public/web-apps/sponsors/Greece 2.0_NextGeneration_White_gr.png"
217
+ alt="Tameio Anakampsis"
218
+ class="object-contain w-auto mx-auto max-h-14"
219
+ />
220
+ </div>
221
+ <div>
222
+ <img
223
+ src="https://s3.ketekny.gr/public/web-apps/sponsors/yp_ygeias_dark.png"
224
+ alt="Ypourgeio Ygeias"
225
+ class="object-contain w-auto mx-auto max-h-14"
226
+ />
227
+ </div>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ </template>
232
+
233
+ <script>
234
+ import { RouterLink } from "vue-router";
235
+ import kButton from "../ui/kButton.vue";
236
+ import kIcon from "../ui/kIcon.vue";
237
+
238
+ const LABELS = {
239
+ editProfile: "Το προφίλ μου",
240
+ signOut: "Αποσύνδεση",
241
+ signIn: "Σύνδεση",
242
+ signUp: "Εγγραφή",
243
+ contactAddress: "Βερανζέου 13, 10677, Αθήνα",
244
+ };
245
+
246
+ export default {
247
+ name: "TwoColSidebarPanel",
248
+ components: {
249
+ kButton,
250
+ kIcon,
251
+ RouterLink,
252
+ },
253
+ emits: [
254
+ "logo-click",
255
+ "item-click",
256
+ "signin",
257
+ "signup",
258
+ "signout",
259
+ "edit-profile",
260
+ ],
261
+ props: {
262
+ title: { type: String, default: "" },
263
+ collapsedLogoSrc: {
264
+ type: String,
265
+ default: "https://s3.ketekny.gr/public/web-apps/logos/logo-icon-dark.png",
266
+ },
267
+ logoSrc: { type: String, default: "" },
268
+ logoAlt: { type: String, default: "Logo" },
269
+ showSponsors: { type: Boolean, default: false },
270
+ menuItems: { type: Array, default: () => [] },
271
+ showContactInfo: { type: Boolean, default: false },
272
+ account: { type: Object, default: null },
273
+ contactEmail: { type: String, default: "grammateia@ketekny.gr" },
274
+ collapsed: { type: Boolean, default: false },
275
+ },
276
+ computed: {
277
+ accountDisplayName() {
278
+ if (!this.account) return "";
279
+
280
+ return (
281
+ [this.account.firstName, this.account.lastName]
282
+ .filter(Boolean)
283
+ .join(" ")
284
+ .trim() || "Account"
285
+ );
286
+ },
287
+ displayEmail() {
288
+ return this.account?.email || "";
289
+ },
290
+ editProfileLabel() {
291
+ return LABELS.editProfile;
292
+ },
293
+ signOutLabel() {
294
+ return LABELS.signOut;
295
+ },
296
+ signInLabel() {
297
+ return LABELS.signIn;
298
+ },
299
+ signUpLabel() {
300
+ return LABELS.signUp;
301
+ },
302
+ contactAddressLabel() {
303
+ return LABELS.contactAddress;
304
+ },
305
+ collapsedContactTooltip() {
306
+ return `
307
+ <div style="min-width: 220px;">
308
+ <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;">
309
+ KETEKNY AE
310
+ </div>
311
+ <div style="display: flex; flex-direction: column; gap: 4px; font-size: 13px; line-height: 1.4; color: rgba(255,255,255,0.9);">
312
+ <div>${LABELS.contactAddress}</div>
313
+ <div>210 3648 337</div>
314
+ <div>${this.contactEmail}</div>
315
+ <div>https://ketekny.gr</div>
316
+ </div>
317
+ </div>
318
+ `;
319
+ },
320
+ },
321
+ methods: {
322
+ menuItemComponent(item) {
323
+ if (item?.isLink) return RouterLink;
324
+ if (item?.isExternal) return "a";
325
+ return "button";
326
+ },
327
+ },
328
+ };
329
+ </script>
@@ -1,7 +1,13 @@
1
+ @import url("https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&display=swap");
2
+
1
3
  @tailwind base;
2
4
  @tailwind components;
3
5
  @tailwind utilities;
4
6
 
7
+ body {
8
+ font-family: "Ubuntu", Helvetica, Arial, sans-serif;
9
+ }
10
+
5
11
 
6
12
  h1, h2, h3, h4{
7
13
  color: theme('colors.primary');