ketekny-ui-kit 1.0.63 → 1.0.65

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/index.js CHANGED
@@ -34,6 +34,7 @@ import kAppHeader from './src/layout/kAppHeader.vue'
34
34
  import kAppFooter from './src/layout/kAppFooter.vue'
35
35
  import kAppMain from './src/layout/kAppMain.vue'
36
36
  import kHero from './src/layout/kHero.vue'
37
+ import twoColLayout from './src/layout/twoColLayout.vue'
37
38
 
38
39
  // Toast/Confirm/Alert Plugins
39
40
  import toastPlugin from './src/plugins/toastPlugin.js'
@@ -57,7 +58,7 @@ export {
57
58
  kDialog, kDrawer,
58
59
 
59
60
  // Layout Components
60
- kAppHeader, kAppFooter, kAppMain, kHero,
61
+ kAppHeader, kAppFooter, kAppMain, kHero, twoColLayout,
61
62
 
62
63
  // Plugins
63
64
  toastPlugin, confirmPlugin, alertPlugin, tooltipPlugin, inputDialogPlugin,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ketekny-ui-kit",
3
3
  "type": "module",
4
- "version": "1.0.63",
4
+ "version": "1.0.65",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -0,0 +1,393 @@
1
+ <template>
2
+ <div
3
+ class="relative flex min-h-screen flex-col bg-[#edf4fb] lg:h-screen lg:flex-row lg:overflow-hidden"
4
+ >
5
+ <button
6
+ v-show="isSidebarCollapsed"
7
+ type="button"
8
+ :aria-label="expandLabel"
9
+ v-tooltip.right="expandLabel"
10
+ class="fixed left-0 top-4 z-[70] hidden h-10 w-10 items-center justify-center rounded-r-full border border-white/15 bg-[#121b2e] text-white shadow-2xl shadow-black/50 transition-all duration-300 hover:bg-[#1a2640] lg:flex"
11
+ @click="toggleSidebar"
12
+ >
13
+ <slot name="collapsed-toggle-icon">
14
+ <kIcon :name="expandIcon" size="16" class="text-white" />
15
+ </slot>
16
+ </button>
17
+
18
+ <aside
19
+ 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:border-r lg:border-white/10 lg:will-change-transform"
20
+ :class="
21
+ isSidebarCollapsed
22
+ ? 'lg:relative lg:z-30 lg:w-[76px]'
23
+ : 'lg:relative lg:z-30 lg:translate-x-0'
24
+ "
25
+ >
26
+ <div class="relative border-b border-white/10">
27
+ <button
28
+ v-if="logoSrc || title"
29
+ type="button"
30
+ class="block w-full text-left"
31
+ v-tooltip.right.html="isSidebarCollapsed ? collapsedContactTooltip : logoAlt"
32
+ @click="$emit('logo-click')"
33
+ >
34
+ <img
35
+ v-if="logoSrc"
36
+ :alt="logoAlt"
37
+ :src="isSidebarCollapsed ? collapsedLogoSrc : logoSrc"
38
+ class="cursor-pointer px-4 py-4 transition-all duration-300"
39
+ />
40
+ <div v-if="title && !isSidebarCollapsed" class="py-3 mt-3 text-lg text-center bg-white/10">
41
+ {{ title }}
42
+ </div>
43
+ </button>
44
+
45
+ <slot name="sidebar-header" />
46
+
47
+ <button
48
+ v-show="!isSidebarCollapsed"
49
+ type="button"
50
+ :aria-label="collapseLabel"
51
+ v-tooltip.left="collapseLabel"
52
+ class="absolute right-2 top-2 z-[70] hidden h-10 w-10 items-center justify-center rounded-full border border-white/15 bg-[#121b2e] text-white shadow-2xl shadow-black/50 transition hover:bg-[#1a2640] lg:flex"
53
+ @click.stop="toggleSidebar"
54
+ >
55
+ <slot name="toggle-icon">
56
+ <kIcon
57
+ :name="collapseIcon"
58
+ size="16"
59
+ class="text-white"
60
+ />
61
+ </slot>
62
+ </button>
63
+ </div>
64
+
65
+ <div class="flex-1 pt-4">
66
+ <div v-if="isSidebarCollapsed" class="mx-1 mt-3 flex flex-col items-center gap-2">
67
+ <SidebarIconLink
68
+ v-for="item in normalizedMainMenu"
69
+ :key="item.key"
70
+ :icon="item.icon"
71
+ :active="item.active"
72
+ :to="item.to"
73
+ :href="item.href"
74
+ :title="item.title"
75
+ :is-external="item.isExternal"
76
+ :target="item.target"
77
+ :rel="item.rel"
78
+ :on-click="item.onClick"
79
+ v-tooltip.right="item.label"
80
+ />
81
+ </div>
82
+ <div v-else-if="mainMenu.length" class="mx-2 mt-3 space-y-3">
83
+ <component
84
+ v-for="item in normalizedMainMenu"
85
+ :key="item.key"
86
+ :is="menuItemTag(item)"
87
+ v-bind="item.bindings"
88
+ class="flex w-full items-start gap-3 rounded-2xl border px-4 py-4 text-left transition-all duration-200"
89
+ :class="item.classes"
90
+ >
91
+ <span
92
+ 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"
93
+ :class="item.active ? 'bg-slate-950 text-white' : ''"
94
+ >
95
+ <kIcon :name="item.icon" />
96
+ </span>
97
+ <span class="min-w-0">
98
+ <span class="block font-semibold leading-tight">
99
+ {{ item.label }}
100
+ </span>
101
+ <span
102
+ v-if="item.description"
103
+ class="mt-1 block text-sm"
104
+ :class="item.active ? 'text-slate-700' : 'text-slate-300'"
105
+ >
106
+ {{ item.description }}
107
+ </span>
108
+ </span>
109
+ </component>
110
+ </div>
111
+ </div>
112
+
113
+ <div v-if="account " class="shrink-0 border-t border-white/10 bg-slate-900/80 px-1 py-4 text-white/80">
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 icon="UserRoundCog" :on-click="() => $emit('edit-profile')" :title="'Επεξεργασία προφίλ'" v-tooltip.right="'Επεξεργασία προφίλ'" />
117
+ <SidebarIconLink icon="LogOut" warning :on-click="() => $emit('signout')" :title="'Αποσύνδεση'" v-tooltip.right="'Αποσύνδεση'" />
118
+ </div>
119
+ <div v-else class="flex flex-col items-center gap-2">
120
+ <SidebarIconLink icon="LogIn" :on-click="() => $emit('signin')" :title="'Sign in'" v-tooltip.right="'Sign in'" />
121
+ <SidebarIconLink icon="UserPlus" :on-click="() => $emit('signup')" :title="'Sign up'" v-tooltip.right="'Sign up'" />
122
+ </div>
123
+ </div>
124
+ <div v-else class="flex flex-col gap-3 px-3">
125
+ <div class="min-w-0">
126
+ <p v-if="account.loggedIn" class="mt-2 truncate text-lg font-semibold text-white">
127
+ {{ accountDisplayName }}
128
+ </p>
129
+ <p v-if="account.loggedIn && account.email" class="truncate text-sm text-slate-300">
130
+ {{ account.email }}
131
+ </p>
132
+ </div>
133
+ <div class="flex flex-wrap gap-2">
134
+ <kButton
135
+ v-if="!account.loggedIn"
136
+ secondary
137
+ icon="LogIn"
138
+ label="Sign in"
139
+ @click="$emit('signin')"
140
+ />
141
+ <kButton
142
+ v-if="!account.loggedIn"
143
+ secondary
144
+ icon="UserPlus"
145
+ label="Sign up"
146
+ @click="$emit('signup')"
147
+ />
148
+ <kButton
149
+ v-if="account.loggedIn"
150
+ secondary
151
+ icon="UserRoundCog"
152
+ label="Επεξεργασία προφίλ"
153
+ @click="$emit('edit-profile')"
154
+ />
155
+ <kButton
156
+ v-if="account.loggedIn"
157
+ icon="LogOut"
158
+ warning
159
+ label="Αποσύνδεση"
160
+ @click="$emit('signout')"
161
+ />
162
+ </div>
163
+ </div>
164
+ </div>
165
+
166
+ <div
167
+ v-if="showContactInfo && !isSidebarCollapsed"
168
+ class="shrink-0 border-t border-white/10 p-4 text-white/50"
169
+ >
170
+ <div class="flex flex-col gap-2">
171
+ <p v-if="!isSidebarCollapsed" class="text-sm font-semibold uppercase tracking-[0.25em] text-white/60">
172
+ KETEKNY AE
173
+ </p>
174
+ <div class="flex flex-wrap items-center gap-x-3 text-sm leading-relaxed" :class="isSidebarCollapsed ? 'justify-center' : ''">
175
+ <span v-if="isSidebarCollapsed" class="sr-only">KETEKNY AE contact info</span>
176
+ <span class="whitespace-nowrap">Βερανζέρου 13, 10677, Αθήνα</span>
177
+ <span class="whitespace-nowrap">210 3648 337</span>
178
+ <span class="whitespace-nowrap">
179
+ <a href="mailto:grammateia@ketekny.gr" class="whitespace-nowrap hover:underline">
180
+ grammateia@ketekny.gr
181
+ </a>
182
+ </span>
183
+ <a
184
+ href="https://ketekny.gr"
185
+ target="_blank"
186
+ rel="noreferrer"
187
+ class="whitespace-nowrap hover:underline"
188
+ >
189
+ https://ketekny.gr
190
+ </a>
191
+ </div>
192
+ </div>
193
+ </div>
194
+
195
+ </aside>
196
+
197
+ <main class="min-h-screen min-w-0 flex-1 overflow-y-auto overflow-x-hidden lg:h-full lg:min-h-0">
198
+ <div class="w-full">
199
+ <slot />
200
+ </div>
201
+ </main>
202
+ </div>
203
+ </template>
204
+
205
+ <script>
206
+ import kButton from '../ui/kButton.vue'
207
+ import kIcon from '../ui/kIcon.vue'
208
+ import { RouterLink } from 'vue-router'
209
+
210
+ const SidebarIconLink = {
211
+ props: {
212
+ icon: { type: String, required: true },
213
+ active: { type: Boolean, default: false },
214
+ to: { type: [String, Object], default: null },
215
+ href: { type: String, default: null },
216
+ title: { type: String, default: '' },
217
+ isExternal: { type: Boolean, default: false },
218
+ target: { type: String, default: '_blank' },
219
+ rel: { type: String, default: 'noreferrer' },
220
+ onClick: { type: Function, default: null },
221
+ warning: { type: Boolean, default: false },
222
+ tooltip: { type: String, default: '' },
223
+ },
224
+ methods: {
225
+ handleClick(event) {
226
+ if (this.onClick) this.onClick(event)
227
+ },
228
+ },
229
+ template: `
230
+ <component
231
+ :is="to ? RouterLink : isExternal ? 'a' : 'button'"
232
+ :to="to"
233
+ :href="href"
234
+ :target="isExternal ? target : null"
235
+ :rel="isExternal ? rel : null"
236
+ :type="to || isExternal ? null : 'button'"
237
+ :title="title"
238
+ class="group relative flex h-14 w-14 items-center justify-center rounded-xl border border-white/15 transition"
239
+ :class="active ? 'bg-slate-950 text-white' : warning ? 'bg-[#d97706] text-white hover:bg-[#b45309]' : 'bg-white/5 text-white hover:border-cyan-400/40 hover:bg-white/10'"
240
+ @click="handleClick"
241
+ >
242
+ <kIcon :name="icon" size="20" class="text-current" />
243
+ </component>
244
+ `,
245
+ components: { RouterLink, kIcon },
246
+ }
247
+
248
+ const DEFAULT_STORAGE_KEY = 'two-col-layout-sidebar-collapsed'
249
+
250
+ export default {
251
+ components: {
252
+ kButton,
253
+ kIcon,
254
+ RouterLink,
255
+ SidebarIconLink,
256
+ },
257
+ emits: ['logo-click', 'signin', 'signup', 'signout', 'edit-profile'],
258
+ props: {
259
+ title: { type: String, default: '' },
260
+ collapsedLogoSrc: {
261
+ type: String,
262
+ default: 'https://s3.ketekny.gr/public/web-apps/logos/logo-icon-dark.png',
263
+ },
264
+ logoSrc: { type: String, default: '' },
265
+ logoAlt: { type: String, default: 'Logo' },
266
+ collapseLabel: { type: String, default: 'Collapse sidebar' },
267
+ expandLabel: { type: String, default: 'Expand sidebar' },
268
+ collapseIcon: { type: String, default: 'ArrowLeftToLine' },
269
+ expandIcon: { type: String, default: 'ArrowRightToLine' },
270
+ storageKey: { type: String, default: DEFAULT_STORAGE_KEY },
271
+ defaultCollapsed: { type: Boolean, default: false },
272
+ mainMenu: { type: Array, default: () => [] },
273
+ showContactInfo: { type: Boolean, default: false },
274
+ account: { type: Object, default: null },
275
+ },
276
+ data() {
277
+ return {
278
+ isSidebarCollapsed: false,
279
+ mdCollapseQuery: null,
280
+ }
281
+ },
282
+ created() {
283
+ this.restoreSidebarState()
284
+ },
285
+ mounted() {
286
+ this.mdCollapseQuery = window.matchMedia('(min-width: 768px) and (max-width: 1023px)')
287
+ this.syncSidebarForViewport()
288
+ this.mdCollapseQuery.addEventListener('change', this.syncSidebarForViewport)
289
+ },
290
+ beforeUnmount() {
291
+ this.mdCollapseQuery?.removeEventListener('change', this.syncSidebarForViewport)
292
+ },
293
+ methods: {
294
+ restoreSidebarState() {
295
+ if (typeof window === 'undefined') {
296
+ this.isSidebarCollapsed = this.defaultCollapsed
297
+ return
298
+ }
299
+
300
+ const stored = window.localStorage.getItem(this.storageKey)
301
+ if (stored == null) {
302
+ this.isSidebarCollapsed = this.defaultCollapsed
303
+ return
304
+ }
305
+
306
+ this.isSidebarCollapsed = stored === 'true'
307
+ },
308
+ syncSidebarForViewport() {
309
+ if (typeof window === 'undefined') return
310
+
311
+ const isMd = window.matchMedia('(min-width: 768px) and (max-width: 1023px)').matches
312
+ if (isMd) {
313
+ this.isSidebarCollapsed = true
314
+ return
315
+ }
316
+
317
+ this.restoreSidebarState()
318
+ },
319
+ persistSidebarState() {
320
+ if (typeof window === 'undefined') return
321
+
322
+ window.localStorage.setItem(this.storageKey, this.isSidebarCollapsed ? 'true' : 'false')
323
+ },
324
+ toggleSidebar() {
325
+ this.isSidebarCollapsed = !this.isSidebarCollapsed
326
+ this.persistSidebarState()
327
+ },
328
+ menuItemTag(item) {
329
+ if (item?.isLink) return 'RouterLink'
330
+ if (item?.isExternal) return 'a'
331
+ return 'button'
332
+ },
333
+ },
334
+ computed: {
335
+ normalizedMainMenu() {
336
+ return this.mainMenu.map((item, index) => {
337
+ const isExternal = Boolean(item?.href)
338
+ const isLink = Boolean(item?.to)
339
+ const active = Boolean(item?.active)
340
+
341
+ return {
342
+ key: item?.key || item?.label || index,
343
+ label: item?.label || '',
344
+ description: item?.description || '',
345
+ icon: item?.icon || 'Dot',
346
+ active,
347
+ isExternal,
348
+ isLink,
349
+ bindings: isLink
350
+ ? {
351
+ to: item.to,
352
+ title: item.title || item.label,
353
+ 'aria-current': active ? 'page' : null,
354
+ }
355
+ : isExternal
356
+ ? {
357
+ href: item.href,
358
+ target: item.target || '_blank',
359
+ rel: item.rel || 'noreferrer',
360
+ title: item.title || item.label,
361
+ }
362
+ : {
363
+ type: 'button',
364
+ title: item.title || item.label,
365
+ },
366
+ classes: active
367
+ ? 'border-white/70 bg-white/90 text-slate-950 shadow-lg shadow-black/20'
368
+ : 'border-white/10 bg-white/5 text-white hover:border-cyan-400/40 hover:bg-white/10',
369
+ }
370
+ })
371
+ },
372
+ accountDisplayName() {
373
+ if (!this.account) return ''
374
+ return [this.account.firstName, this.account.lastName].filter(Boolean).join(' ').trim() || 'Account'
375
+ },
376
+ collapsedContactTooltip() {
377
+ return `
378
+ <div style="min-width: 220px;">
379
+ <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;">
380
+ KETEKNY AE
381
+ </div>
382
+ <div style="display: flex; flex-direction: column; gap: 4px; font-size: 13px; line-height: 1.4; color: rgba(255,255,255,0.9);">
383
+ <div>Βερανζέρου 13, 10677, Αθήνα</div>
384
+ <div>210 3648 337</div>
385
+ <div>grammateia@ketekny.gr</div>
386
+ <div>https://ketekny.gr</div>
387
+ </div>
388
+ </div>
389
+ `
390
+ },
391
+ },
392
+ }
393
+ </script>