ketekny-ui-kit 1.0.76 → 1.0.78

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # ketekny-ui-kit
1
+ # ketekny-ui-kit
2
2
 
3
3
  Vue 3 UI component library with Tailwind CSS styling and utility plugins.
4
4
 
@@ -45,6 +45,7 @@ import {
45
45
  kSelect,
46
46
  kDateSelector,
47
47
  kToggle,
48
+ kCheckbox,
48
49
  toastPlugin,
49
50
  confirmPlugin,
50
51
  alertPlugin,
@@ -59,6 +60,7 @@ app.component("kInput", kInput);
59
60
  app.component("kSelect", kSelect);
60
61
  app.component("kDateSelector", kDateSelector);
61
62
  app.component("kToggle", kToggle);
63
+ app.component("kCheckbox", kCheckbox);
62
64
 
63
65
  app.use(toastPlugin);
64
66
  app.use(confirmPlugin);
@@ -159,7 +161,7 @@ Components:
159
161
  - `kMessage`, `kCode`, `kToolbar`, `kTable`, `kTabs`, `kChip`
160
162
  - `kSpinner`, `kDatatable`, `kIcon`, `kMenu`, `kSkeleton`
161
163
  - `kProgressBar`, `kPagination`, `kTree`, `kButton`, `kSelect`, `kUploader`
162
- - `kToggle`, `kInput`, `kDateSelector`, `kEditor`
164
+ - `kToggle`, `kCheckbox`, `kInput`, `kDateSelector`, `kEditor`
163
165
  - `kSelectButton`, `kTags`, `kSearch`, `kArrayList`, `kList`, `kTextArea`
164
166
  - `kDialog`, `kDrawer`
165
167
  - `kAppHeader`, `kAppFooter`, `kAppMain`, `kHero`
@@ -199,3 +201,5 @@ ISC
199
201
 
200
202
  - Live demo/docs: https://ui.ketekny.gr
201
203
  - npm: https://www.npmjs.com/package/ketekny-ui-kit
204
+
205
+
package/index.js CHANGED
@@ -1,6 +1,7 @@
1
- // UI Components
1
+ // UI Components
2
2
  import kMessage from './src/ui/kMessage.vue'
3
3
  import kButton from './src/ui/kButton.vue'
4
+ import kCheckbox from './src/ui/kCheckbox.vue'
4
5
  import kChip from './src/ui/kChip.vue'
5
6
  import kCode from './src/ui/kCode.vue'
6
7
  import kDialog from './src/ui/kDialog.vue'
@@ -52,7 +53,7 @@ export {
52
53
  kMessage, kCode, kToolbar, kTable, kTabs, kChip, kSpinner, kDatatable, kIcon, kMenu, kSkeleton, kProgressBar, kPagination, kTree,
53
54
 
54
55
  // Form Components
55
- kButton, kSelect, kUploader, kToggle, kInput, kDateSelector, kEditor, kSelectButton, kTags, kSearch, kArrayList, kList, kTextArea,
56
+ kButton, kCheckbox, kSelect, kUploader, kToggle, kInput, kDateSelector, kEditor, kSelectButton, kTags, kSearch, kArrayList, kList, kTextArea,
56
57
 
57
58
  // Dialogs
58
59
  kDialog, kDrawer,
@@ -66,3 +67,4 @@ export {
66
67
  // Tailwind Config
67
68
  tailwindPreset
68
69
  }
70
+
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ketekny-ui-kit",
3
3
  "type": "module",
4
- "version": "1.0.76",
4
+ "version": "1.0.78",
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,228 +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
- <RouterLink
81
- v-for="item in normalizedMainMenu"
82
- :key="item.key"
83
- v-bind="item.bindings"
84
- class="flex w-full items-start gap-3 rounded-2xl border px-4 py-4 text-left transition-all duration-200"
85
- :class="item.classes"
86
- >
87
- <span
88
- 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"
89
- :class="item.active ? 'bg-slate-950 text-white' : ''"
90
- >
91
- <kIcon :name="item.icon" />
92
- </span>
93
- <span class="min-w-0">
94
- <span class="block font-semibold leading-tight">
95
- {{ item.label }}
96
- </span>
97
- <span
98
- v-if="item.description"
99
- class="mt-1 block text-sm"
100
- :class="item.active ? 'text-slate-700' : 'text-slate-300'"
101
- >
102
- {{ item.description }}
103
- </span>
104
- </span>
105
- </RouterLink>
106
- </div>
107
- </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>
108
68
 
109
- <div
110
- v-if="account"
111
- class="shrink-0 border-b border-white/10 bg-slate-900/80 px-1 pb-4 text-white/80"
112
- >
113
- <div v-if="isSidebarCollapsed" class="flex flex-col gap-3">
114
- <div v-if="account.loggedIn" class="flex flex-col items-center gap-2">
115
- <SidebarIconLink
116
- icon="UserRoundCog"
117
- :on-click="() => $emit('edit-profile')"
118
- :title="'Επεξεργασία προφίλ'"
119
- v-tooltip.right="'Επεξεργασία προφίλ'"
120
- />
121
- <SidebarIconLink
122
- icon="LogOut"
123
- warning
124
- :on-click="() => $emit('signout')"
125
- :title="'Αποσύνδεση'"
126
- v-tooltip.right="'Αποσύνδεση'"
127
- />
128
- </div>
129
- <div v-else class="flex flex-col items-center gap-2">
130
- <SidebarIconLink
131
- icon="LogIn"
132
- :on-click="() => $emit('signin')"
133
- :title="'Sign in'"
134
- v-tooltip.right="'Sign in'"
135
- />
136
- <SidebarIconLink
137
- icon="UserPlus"
138
- :on-click="() => $emit('signup')"
139
- :title="'Sign up'"
140
- v-tooltip.right="'Sign up'"
141
- />
142
- </div>
143
- </div>
144
- <div v-else class="flex flex-col gap-3 px-3">
145
- <div class="min-w-0">
146
- <p
147
- v-if="account.loggedIn"
148
- class="mt-2 truncate text-lg font-semibold text-white"
149
- >
150
- {{ accountDisplayName }}
151
- </p>
152
- <p
153
- v-if="account.loggedIn && displayEmail"
154
- class="truncate text-sm text-slate-300"
155
- >
156
- {{ displayEmail }}
157
- </p>
158
- </div>
159
- <div class="flex flex-wrap justify-between gap-2">
160
- <kButton
161
- v-if="!account.loggedIn"
162
- secondary
163
- icon="LogIn"
164
- label="Σύνδεση"
165
- @click="$emit('signin')"
166
- />
167
- <kButton
168
- v-if="!account.loggedIn"
169
- secondary
170
- icon="UserPlus"
171
- label="Εγγραφή"
172
- @click="$emit('signup')"
173
- />
174
- <kButton
175
- v-if="account.loggedIn"
176
- variant="soft"
177
- icon="UserRoundCog"
178
- label="Επεξεργασία προφίλ"
179
- @click="$emit('edit-profile')"
180
- />
181
- <kButton
182
- v-if="account.loggedIn"
183
- icon="LogOut"
184
- warning
185
- label="Αποσύνδεση"
186
- @click="$emit('signout')"
187
- />
188
- </div>
189
- </div>
190
- </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>
191
78
 
192
- <div
193
- v-if="!isSidebarCollapsed"
194
- 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"
195
85
  >
196
- <div class="flex flex-col gap-2 px-4" v-if="showContactInfo">
197
- <p
198
- class="text-sm font-semibold uppercase tracking-[0.25em] text-white/60"
199
- >
200
- KETEKNY AE
201
- </p>
202
- <div
203
- class="flex flex-wrap items-center gap-x-3 text-sm"
204
- :class="isSidebarCollapsed ? 'justify-center' : ''"
205
- >
206
- <span v-if="isSidebarCollapsed" class="sr-only"
207
- >KETEKNY AE contact info</span
208
- >
209
- <span class="whitespace-nowrap">Βερανζέρου 13, 10677, Αθήνα</span>
210
- <span class="whitespace-nowrap">210 3648 337</span>
211
- <span class="whitespace-nowrap">
212
- <a
213
- :href="`mailto:${contactEmail}`"
214
- class="whitespace-nowrap hover:underline"
215
- >
216
- {{ contactEmail }}
217
- </a>
218
- </span>
219
- <a
220
- href="https://ketekny.gr"
221
- target="_blank"
222
- rel="noreferrer"
223
- 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"
224
105
  >
225
- https://ketekny.gr
226
- </a>
106
+ <template #sidebar-header>
107
+ <slot name="sidebar-header" />
108
+ </template>
109
+ </SidebarPanel>
227
110
  </div>
228
111
  </div>
229
-
230
- <div
231
- v-if="showSponsors && !isSidebarCollapsed"
232
- class="flex flex-col gap-3 px-4 py-1 bg-[#0d4b87] mt-2"
233
- >
234
- <div>
235
- <img
236
- src="https://s3.ketekny.gr/public/web-apps/sponsors/Greece 2.0_NextGeneration_White_gr.png"
237
- alt="Tameio Anakampsis"
238
- class="mx-auto max-h-14 w-auto object-contain"
239
- />
240
- </div>
241
- <div>
242
- <img
243
- src="https://s3.ketekny.gr/public/web-apps/sponsors/yp_ygeias_dark.png"
244
- alt="Ypourgeio Ygeias"
245
- class="mx-auto max-h-14 w-auto object-contain"
246
- />
247
- </div>
248
- </div>
249
- </div>
250
- </aside>
112
+ </aside>
113
+ </transition>
251
114
 
252
115
  <main
253
- 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"
254
117
  >
255
118
  <div class="w-full">
256
119
  <slot />
@@ -259,68 +122,18 @@
259
122
  </div>
260
123
  </template>
261
124
 
262
- <script>
263
- import { h } from "vue";
264
- import kButton from "../ui/kButton.vue";
125
+ <script>
265
126
  import kIcon from "../ui/kIcon.vue";
266
- import { RouterLink } from "vue-router";
267
-
268
- const SidebarIconLink = {
269
- props: {
270
- icon: { type: String, required: true },
271
- active: { type: Boolean, default: false },
272
- to: { type: [String, Object], default: null },
273
- href: { type: String, default: null },
274
- title: { type: String, default: "" },
275
- isExternal: { type: Boolean, default: false },
276
- target: { type: String, default: "_blank" },
277
- rel: { type: String, default: "noreferrer" },
278
- onClick: { type: Function, default: null },
279
- warning: { type: Boolean, default: false },
280
- tooltip: { type: String, default: "" },
281
- },
282
- methods: {
283
- handleClick(event) {
284
- if (this.onClick) this.onClick(event);
285
- },
286
- },
287
- render() {
288
- const isLink = Boolean(this.to);
289
- const tag = isLink ? RouterLink : this.isExternal ? "a" : "button";
290
-
291
- return h(
292
- tag,
293
- {
294
- to: isLink ? this.to : undefined,
295
- href: this.href,
296
- target: this.isExternal ? this.target : null,
297
- rel: this.isExternal ? this.rel : null,
298
- type: isLink || this.isExternal ? null : "button",
299
- title: this.title,
300
- class: [
301
- "group relative flex h-14 w-14 items-center justify-center rounded-xl border border-white/15 transition",
302
- this.active
303
- ? "bg-slate-950 text-white"
304
- : this.warning
305
- ? "bg-[#d97706] text-white hover:bg-[#b45309]"
306
- : "bg-white/5 text-white hover:border-cyan-400/40 hover:bg-white/10",
307
- isLink ? "cursor-pointer" : "",
308
- ],
309
- onClick: this.handleClick,
310
- },
311
- [h(kIcon, { name: this.icon, size: "20", color: "#ffffff" })]
312
- );
313
- },
314
- };
127
+ import SidebarPanel from "./twoColSidebarPanel.vue";
315
128
 
316
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)";
317
132
 
318
133
  export default {
319
134
  components: {
320
- kButton,
321
135
  kIcon,
322
- RouterLink,
323
- SidebarIconLink,
136
+ SidebarPanel,
324
137
  },
325
138
  emits: ["logo-click", "signin", "signup", "signout", "edit-profile"],
326
139
  props: {
@@ -345,56 +158,110 @@ export default {
345
158
  },
346
159
  data() {
347
160
  return {
348
- isSidebarCollapsed: false,
161
+ isSidebarCollapsed: null,
349
162
  mdCollapseQuery: null,
163
+ lgViewportQuery: null,
164
+ isDesktopViewport: false,
165
+ mobileMenuOpen: false,
350
166
  };
351
167
  },
352
168
  created() {
353
- this.restoreSidebarState();
169
+ this.isSidebarCollapsed = this.getInitialSidebarState();
170
+
171
+ if (typeof window !== "undefined") {
172
+ this.isDesktopViewport = window.matchMedia(TAILWIND_LG_QUERY).matches;
173
+ }
354
174
  },
355
175
  mounted() {
356
- this.mdCollapseQuery = window.matchMedia(
357
- "(min-width: 768px) and (max-width: 1023px)"
358
- );
176
+ this.mdCollapseQuery = window.matchMedia(TAILWIND_MD_QUERY);
177
+ this.lgViewportQuery = window.matchMedia(TAILWIND_LG_QUERY);
178
+ this.isDesktopViewport = this.lgViewportQuery.matches;
179
+
359
180
  this.syncSidebarForViewport();
360
- this.mdCollapseQuery.addEventListener(
361
- "change",
362
- this.syncSidebarForViewport
363
- );
181
+ this.mdCollapseQuery.addEventListener("change", this.syncSidebarForViewport);
182
+ this.lgViewportQuery.addEventListener("change", this.syncDesktopViewport);
364
183
  },
365
184
  beforeUnmount() {
366
185
  this.mdCollapseQuery?.removeEventListener(
367
186
  "change",
368
187
  this.syncSidebarForViewport
369
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
+ },
370
203
  },
371
204
  methods: {
372
- restoreSidebarState() {
205
+ isMdViewport() {
206
+ if (typeof window === "undefined") return false;
207
+
208
+ return window.matchMedia(TAILWIND_MD_QUERY).matches;
209
+ },
210
+ getInitialSidebarState() {
373
211
  if (typeof window === "undefined") {
374
- this.isSidebarCollapsed = this.defaultCollapsed;
375
- return;
212
+ return this.defaultCollapsed;
213
+ }
214
+
215
+ if (this.isMdViewport()) {
216
+ return true;
376
217
  }
377
218
 
378
219
  const stored = window.localStorage.getItem(this.storageKey);
379
220
  if (stored == null) {
380
- this.isSidebarCollapsed = this.defaultCollapsed;
381
- return;
221
+ return this.defaultCollapsed;
382
222
  }
383
223
 
384
- this.isSidebarCollapsed = stored === "true";
224
+ return stored === "true";
385
225
  },
386
226
  syncSidebarForViewport() {
387
227
  if (typeof window === "undefined") return;
388
228
 
389
- const isMd = window.matchMedia(
390
- "(min-width: 768px) and (max-width: 1023px)"
391
- ).matches;
392
- if (isMd) {
229
+ if (this.isMdViewport()) {
393
230
  this.isSidebarCollapsed = true;
394
231
  return;
395
232
  }
396
233
 
397
- 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");
398
265
  },
399
266
  persistSidebarState() {
400
267
  if (typeof window === "undefined") return;
@@ -408,11 +275,6 @@ export default {
408
275
  this.isSidebarCollapsed = !this.isSidebarCollapsed;
409
276
  this.persistSidebarState();
410
277
  },
411
- menuItemTag(item) {
412
- if (item?.to) return RouterLink;
413
- if (item?.href) return "a";
414
- return "button";
415
- },
416
278
  },
417
279
  computed: {
418
280
  normalizedMainMenu() {
@@ -452,56 +314,25 @@ export default {
452
314
  };
453
315
  });
454
316
  },
455
- menuItemBindings(item) {
456
- if (item?.to) {
457
- return {
458
- to: item.to,
459
- title: item.title || item.label,
460
- "aria-current": item.active ? "page" : null,
461
- };
462
- }
463
-
464
- if (item?.href) {
465
- return {
466
- href: item.href,
467
- target: item.target || "_blank",
468
- rel: item.rel || "noreferrer",
469
- title: item.title || item.label,
470
- };
471
- }
472
-
473
- return {
474
- type: "button",
475
- title: item?.title || item?.label,
476
- };
477
- },
478
- accountDisplayName() {
479
- if (!this.account) return "";
480
- return (
481
- [this.account.firstName, this.account.lastName]
482
- .filter(Boolean)
483
- .join(" ")
484
- .trim() || "Account"
485
- );
486
- },
487
- displayEmail() {
488
- return this.account?.email || "";
489
- },
490
- collapsedContactTooltip() {
491
- return `
492
- <div style="min-width: 220px;">
493
- <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;">
494
- KETEKNY AE
495
- </div>
496
- <div style="display: flex; flex-direction: column; gap: 4px; font-size: 13px; line-height: 1.4; color: rgba(255,255,255,0.9);">
497
- <div>Βερανζέρου 13, 10677, Αθήνα</div>
498
- <div>210 3648 337</div>
499
- <div>${this.contactEmail}</div>
500
- <div>https://ketekny.gr</div>
501
- </div>
502
- </div>
503
- `;
504
- },
505
317
  },
506
318
  };
507
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');
@@ -0,0 +1,143 @@
1
+ <template>
2
+ <button
3
+ type="button"
4
+ class="admin-checkbox flex w-full items-center gap-2.5 rounded-2xl px-3 py-2 transition"
5
+ :class="[themeTextClass, isActive ? activeTextClass : inactiveTextClass, combinedContainerClass]"
6
+ :title="hoverCaption"
7
+ :aria-label="hoverCaption || label || 'Checkbox'"
8
+ @click="toggle"
9
+ >
10
+ <span
11
+ class="inline-flex shrink-0 items-center justify-center rounded-[6px] transition-colors"
12
+ :style="{ width: `${iconBoxSize}px`, height: `${iconBoxSize}px` }"
13
+ :class="isActive ? activeClass : inactiveClass"
14
+ >
15
+ <Check :size="iconSize" />
16
+ </span>
17
+ <span class="min-w-0 flex-1 text-left">
18
+ <span
19
+ class="block text-sm font-semibold leading-5"
20
+ :class="[truncateLabel ? 'truncate' : 'break-words', labelClass]"
21
+ >
22
+ {{ label }}
23
+ </span>
24
+ <span v-if="description" class="mt-0.5 block text-[11px] leading-4" :class="descriptionTextClass">
25
+ {{ description }}
26
+ </span>
27
+ </span>
28
+ </button>
29
+ </template>
30
+
31
+ <script>
32
+ import { Check } from '@lucide/vue'
33
+
34
+ export default {
35
+ name: 'kCheckbox',
36
+ components: { Check },
37
+ props: {
38
+ containerClass: {
39
+ type: [String, Array, Object],
40
+ default: '',
41
+ },
42
+ labelClass: {
43
+ type: [String, Array, Object],
44
+ default: '',
45
+ },
46
+ theme: {
47
+ type: String,
48
+ default: 'light',
49
+ validator: (value) => ['light', 'dark'].includes(String(value || '').trim().toLowerCase()),
50
+ },
51
+ hoverClass: {
52
+ type: [String, Array, Object],
53
+ default: '',
54
+ },
55
+ descriptionClass: {
56
+ type: [String, Array, Object],
57
+ default: '',
58
+ },
59
+ activeTextClass: {
60
+ type: [String, Array, Object],
61
+ default: '',
62
+ },
63
+ inactiveTextClass: {
64
+ type: [String, Array, Object],
65
+ default: '',
66
+ },
67
+ iconSize: {
68
+ type: Number,
69
+ default: 14,
70
+ },
71
+ iconBoxSize: {
72
+ type: Number,
73
+ default: 24,
74
+ },
75
+ truncateLabel: {
76
+ type: Boolean,
77
+ default: false,
78
+ },
79
+ modelValue: {
80
+ type: Boolean,
81
+ default: undefined,
82
+ },
83
+ label: {
84
+ type: String,
85
+ default: '',
86
+ },
87
+ caption: {
88
+ type: String,
89
+ default: '',
90
+ },
91
+ description: {
92
+ type: String,
93
+ default: '',
94
+ },
95
+ },
96
+ emits: ['click', 'update:modelValue'],
97
+ computed: {
98
+ isActive() {
99
+ return this.modelValue === true
100
+ },
101
+ hoverCaption() {
102
+ const explicitCaption = String(this.caption || '').trim()
103
+ if (explicitCaption) return explicitCaption
104
+ if (this.truncateLabel) return String(this.label || '').trim()
105
+ return ''
106
+ },
107
+ themeTextClass() {
108
+ return this.theme === 'dark' ? 'text-slate-100 hover:text-white' : 'text-slate-700 hover:text-slate-900'
109
+ },
110
+ themeHoverClass() {
111
+ return this.theme === 'dark' ? 'hover:bg-white/5' : 'hover:bg-slate-50'
112
+ },
113
+ themeDescriptionClass() {
114
+ return this.theme === 'dark' ? 'text-slate-300' : 'text-slate-500'
115
+ },
116
+ themeActiveClass() {
117
+ return this.theme === 'dark' ? 'bg-cyan-400 text-cyan-950' : 'bg-sky-600 text-white'
118
+ },
119
+ themeInactiveClass() {
120
+ return this.theme === 'dark' ? 'bg-white/10 text-transparent' : 'bg-slate-200 text-transparent'
121
+ },
122
+ combinedContainerClass() {
123
+ return [this.themeHoverClass, this.hoverClass, this.containerClass]
124
+ },
125
+ activeClass() {
126
+ return this.themeActiveClass
127
+ },
128
+ inactiveClass() {
129
+ return this.themeInactiveClass
130
+ },
131
+ descriptionTextClass() {
132
+ return this.descriptionClass || this.themeDescriptionClass
133
+ },
134
+ },
135
+ methods: {
136
+ toggle() {
137
+ const nextValue = !this.isActive
138
+ this.$emit('update:modelValue', nextValue)
139
+ this.$emit('click', nextValue)
140
+ },
141
+ },
142
+ }
143
+ </script>