ketekny-ui-kit 1.0.126 → 1.0.128

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
@@ -205,7 +205,7 @@ Components:
205
205
  - `kSelectButton`, `kTags`, `kSearch`, `kArrayList`, `kList`, `kTextArea`
206
206
  - `kDialog`, `kDrawer`
207
207
  - `kAppHeader`, `kAppFooter`, `kAppMain`, `kHero`
208
- - `kSingleColPage`, `kTwoColPage`, `twoColLayout`, `legacyTwoColLayout`
208
+ - `kSingleColPage`, `kMultiColPage`, `kTwoColPage` (backwards-compatible alias), `twoColLayout`, `legacyTwoColLayout`
209
209
 
210
210
  Other exports:
211
211
 
@@ -242,4 +242,3 @@ ISC
242
242
 
243
243
  - Live demo/docs: https://ui.ketekny.gr
244
244
  - npm: https://www.npmjs.com/package/ketekny-ui-kit
245
-
package/index.js CHANGED
@@ -40,6 +40,7 @@ import kAppFooter from './src/layout/kAppFooter.vue'
40
40
  import kAppMain from './src/layout/kAppMain.vue'
41
41
  import kHero from './src/layout/kHero.vue'
42
42
  import kSingleColPage from './src/layout/kSingleColPage.vue'
43
+ import kMultiColPage from './src/layout/kMultiColPage.vue'
43
44
  import kTwoColPage from './src/layout/kTwoColPage.vue'
44
45
  import twoColLayout from './src/twoColLayout/components/TwoColLayout.vue'
45
46
  import legacyTwoColLayout from './src/layout/twoColLayout.vue'
@@ -66,7 +67,7 @@ export {
66
67
  kDialog, kDrawer,
67
68
 
68
69
  // Layout Components
69
- kAppHeader, kAppFooter, kAppMain, kHero, kSingleColPage, kTwoColPage, twoColLayout, legacyTwoColLayout,
70
+ kAppHeader, kAppFooter, kAppMain, kHero, kSingleColPage, kMultiColPage, kTwoColPage, twoColLayout, legacyTwoColLayout,
70
71
 
71
72
  // Plugins
72
73
  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.126",
4
+ "version": "1.0.128",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -0,0 +1,5 @@
1
+ <script>
2
+ import kMultiColPage from './kTwoColPage.vue'
3
+
4
+ export default kMultiColPage
5
+ </script>
@@ -10,7 +10,7 @@
10
10
  v-if="showMobileOverlay"
11
11
  class="fixed inset-0 z-20 bg-slate-950/40 backdrop-blur-[1px] lg:hidden"
12
12
  :style="mobileOverlayStyle"
13
- @click="toggleMobileSidebar"
13
+ @click="closeMobileSidebar"
14
14
  />
15
15
 
16
16
  <section
@@ -59,31 +59,73 @@
59
59
  <div class="flex min-h-0 flex-1 flex-col overflow-hidden bg-slate-100 py-4">
60
60
  <div
61
61
  class="lg:hidden"
62
- :class="mobileSidebarOpen ? 'mx-auto flex w-full max-w-none px-4 pb-3 sm:px-6' : 'fixed left-0 top-1/2 z-20 -translate-y-1/2'"
62
+ :class="mobileSidebarOpen ? 'mx-auto flex w-full max-w-none items-center justify-between gap-3 px-4 pb-3 sm:px-6' : 'pointer-events-none fixed inset-x-0 top-1/2 z-20 flex -translate-y-1/2'"
63
63
  >
64
- <kButton
65
- :label="mobileSidebarOpen ? 'Hide sidebar' : 'Show sidebar'"
66
- :icon="mobileSidebarOpen ? 'PanelLeftClose' : 'PanelLeftOpen'"
67
- variant="soft"
68
- size="large"
69
- :icon-only="!mobileSidebarOpen"
70
- :aria-label="mobileSidebarOpen ? null : 'Show sidebar'"
71
- :class="mobileSidebarOpen ? '' : 'rounded-full text-slate-900 shadow-lg shadow-slate-900/15'"
72
- @click="toggleMobileSidebar"
73
- />
64
+ <button
65
+ v-if="hasLeftSidebar"
66
+ type="button"
67
+ :aria-label="mobileSidebarButtonAriaLabel('left')"
68
+ class="mobile-sidebar-trigger pointer-events-auto"
69
+ :class="mobileSidebarButtonClass('left')"
70
+ @click="toggleMobileSidebar('left')"
71
+ >
72
+ <kIcon
73
+ :name="mobileSidebarIconName('left')"
74
+ size="18"
75
+ />
76
+ <span v-if="mobileSidebarButtonShowsLabel('left')" class="mobile-sidebar-trigger__label">
77
+ {{ mobileSidebarButtonLabel('left') }}
78
+ </span>
79
+ </button>
80
+ <button
81
+ v-if="hasRightSidebar"
82
+ type="button"
83
+ :aria-label="mobileSidebarButtonAriaLabel('right')"
84
+ class="mobile-sidebar-trigger pointer-events-auto"
85
+ :class="mobileSidebarButtonClass('right')"
86
+ @click="toggleMobileSidebar('right')"
87
+ >
88
+ <kIcon
89
+ :name="mobileSidebarIconName('right')"
90
+ size="18"
91
+ />
92
+ <span v-if="mobileSidebarButtonShowsLabel('right')" class="mobile-sidebar-trigger__label">
93
+ {{ mobileSidebarButtonLabel('right') }}
94
+ </span>
95
+ </button>
74
96
  </div>
75
97
  <div class="mx-auto flex min-h-0 w-full max-w-none flex-1 gap-2">
76
98
  <aside
77
- v-if="showSidebar"
99
+ v-if="showLeftSidebar"
78
100
  class="min-h-0 shrink-0 overflow-y-auto bg-slate-100 px-4 sm:px-6 lg:ml-8 lg:pr-4 lg:pl-0"
79
- :class="mobileSidebarClass"
80
- :style="mobileSidebarStyle"
101
+ :class="mobileSidebarClass('left')"
102
+ :style="mobileSidebarStyle('left')"
81
103
  >
82
- <slot name="sidebar" />
104
+ <div v-if="effectiveSidebarLeftTitle" class="pb-3">
105
+ <p class="text-xs font-semibold uppercase tracking-[0.22em] text-slate-500">
106
+ {{ effectiveSidebarLeftTitle }}
107
+ </p>
108
+ </div>
109
+ <slot name="sidebar-left" v-bind="sidebarSlotProps('left')">
110
+ <slot name="sidebar" v-bind="sidebarSlotProps('left')" />
111
+ </slot>
83
112
  </aside>
84
113
  <section class="min-h-0 min-w-0 flex-1 overflow-y-auto px-2">
85
114
  <slot name="content" />
86
115
  </section>
116
+ <aside
117
+ v-if="showRightSidebar"
118
+ class="min-h-0 shrink-0 overflow-y-auto bg-slate-100 px-4 sm:px-6 lg:mr-8 lg:pl-4 lg:pr-0"
119
+ :class="mobileSidebarClass('right')"
120
+ :style="mobileSidebarStyle('right')"
121
+ >
122
+ <div v-if="sidebarRightTitle" class="pb-3">
123
+ <p class="text-xs font-semibold uppercase tracking-[0.22em] text-slate-500">
124
+ {{ sidebarRightTitle }}
125
+ </p>
126
+ </div>
127
+ <slot name="sidebar-right" v-bind="sidebarSlotProps('right')" />
128
+ </aside>
87
129
  </div>
88
130
  </div>
89
131
  </main>
@@ -92,6 +134,7 @@
92
134
  <script>
93
135
  import packageJson from '../../package.json'
94
136
  import kButton from '../ui/kButton.vue'
137
+ import kIcon from '../ui/kIcon.vue'
95
138
 
96
139
  const LG_VIEWPORT_QUERY = '(min-width: 1024px)'
97
140
  const MAIN_MENU_TOGGLE_EVENT = 'ketekny:two-col-layout-mobile-menu'
@@ -100,8 +143,8 @@ const MOBILE_SWIPE_OPEN_DISTANCE = 52
100
143
  const MOBILE_SWIPE_VERTICAL_TOLERANCE = 48
101
144
 
102
145
  export default {
103
- name: 'PageTwoColShell',
104
- components: { kButton },
146
+ name: 'MultiColPage',
147
+ components: { kButton, kIcon },
105
148
  props: {
106
149
  appTitle: {
107
150
  type: String,
@@ -123,13 +166,35 @@ export default {
123
166
  type: String,
124
167
  default: '300px',
125
168
  },
169
+ sidebarLeftWidth: {
170
+ type: String,
171
+ default: '',
172
+ },
173
+ sidebarTitle: {
174
+ type: String,
175
+ default: '',
176
+ },
177
+ sidebarLeftTitle: {
178
+ type: String,
179
+ default: '',
180
+ },
181
+ sidebarRightWidth: {
182
+ type: String,
183
+ default: '',
184
+ },
185
+ sidebarRightTitle: {
186
+ type: String,
187
+ default: '',
188
+ },
126
189
  },
127
190
  data() {
128
191
  return {
129
192
  isDesktopViewport: true,
130
193
  mobileSidebarOpen: false,
194
+ mobileSidebarSide: 'left',
131
195
  viewportQuery: null,
132
196
  touchGesture: {
197
+ side: 'left',
133
198
  startX: 0,
134
199
  startY: 0,
135
200
  currentX: 0,
@@ -151,33 +216,39 @@ export default {
151
216
  if (!target) return ''
152
217
  return target.replace(/^https?:\/\//i, '').replace(/\/$/, '')
153
218
  },
154
- sidebarStyle() {
219
+ hasLeftSidebar() {
220
+ return Boolean(this.$slots['sidebar-left'] || this.$slots.sidebar)
221
+ },
222
+ hasRightSidebar() {
223
+ return Boolean(this.$slots['sidebar-right'])
224
+ },
225
+ effectiveSidebarLeftWidth() {
226
+ return this.sidebarLeftWidth || this.sidebarWidth || '300px'
227
+ },
228
+ effectiveSidebarLeftTitle() {
229
+ return this.sidebarLeftTitle || this.sidebarTitle
230
+ },
231
+ effectiveSidebarRightWidth() {
232
+ return this.sidebarRightWidth || this.effectiveSidebarLeftWidth
233
+ },
234
+ sidebarLeftStyle() {
155
235
  return {
156
- minWidth: this.sidebarWidth,
157
- maxWidth: this.sidebarWidth,
236
+ minWidth: this.effectiveSidebarLeftWidth,
237
+ maxWidth: this.effectiveSidebarLeftWidth,
158
238
  }
159
239
  },
160
- mobileSidebarClass() {
161
- if (this.isDesktopViewport) return ''
162
- return 'fixed left-0 top-0 z-20 h-full max-w-[85vw] border-r border-slate-200 bg-slate-100 py-4 shadow-xl shadow-slate-900/20 transition-transform duration-200 ease-out'
240
+ sidebarRightStyle() {
241
+ return {
242
+ minWidth: this.effectiveSidebarRightWidth,
243
+ maxWidth: this.effectiveSidebarRightWidth,
244
+ }
163
245
  },
164
246
  dragOffset() {
165
247
  if (!this.touchGesture.trackingEdgeSwipe) return 0
166
- return Math.max(0, this.touchGesture.currentX - this.touchGesture.startX)
167
- },
168
- mobileSidebarStyle() {
169
- const baseStyle = this.sidebarStyle
170
- if (this.isDesktopViewport || this.mobileSidebarOpen || !this.touchGesture.trackingEdgeSwipe) {
171
- return baseStyle
172
- }
173
- const widthValue = Number.parseFloat(this.sidebarWidth)
174
- const effectiveWidth = Number.isFinite(widthValue) ? widthValue : 300
175
- const clampedOffset = Math.min(this.dragOffset, effectiveWidth)
176
- return {
177
- ...baseStyle,
178
- transform: `translateX(${clampedOffset - effectiveWidth}px)`,
179
- transition: 'none',
248
+ if (this.touchGesture.side === 'right') {
249
+ return Math.max(0, this.touchGesture.startX - this.touchGesture.currentX)
180
250
  }
251
+ return Math.max(0, this.touchGesture.currentX - this.touchGesture.startX)
181
252
  },
182
253
  showMobileOverlay() {
183
254
  return !this.isDesktopViewport && (this.mobileSidebarOpen || this.touchGesture.trackingEdgeSwipe)
@@ -189,8 +260,13 @@ export default {
189
260
  opacity: String(progress),
190
261
  }
191
262
  },
192
- showSidebar() {
193
- return this.isDesktopViewport || this.mobileSidebarOpen || this.touchGesture.trackingEdgeSwipe
263
+ showLeftSidebar() {
264
+ if (!this.hasLeftSidebar) return false
265
+ return this.showSidebarForSide('left')
266
+ },
267
+ showRightSidebar() {
268
+ if (!this.hasRightSidebar) return false
269
+ return this.showSidebarForSide('right')
194
270
  },
195
271
  },
196
272
  mounted() {
@@ -206,10 +282,101 @@ export default {
206
282
  window.removeEventListener(MAIN_MENU_TOGGLE_EVENT, this.handleMainMenuToggle)
207
283
  },
208
284
  methods: {
285
+ sidebarSlotProps(side) {
286
+ return {
287
+ side,
288
+ title: side === 'right' ? this.sidebarRightTitle : this.effectiveSidebarLeftTitle,
289
+ isDesktopViewport: this.isDesktopViewport,
290
+ mobileSidebarOpen: this.mobileSidebarOpen,
291
+ mobileSidebarSide: this.mobileSidebarSide,
292
+ isMobileSidebarActive: this.mobileSidebarOpen && this.mobileSidebarSide === side,
293
+ toggleMobileSidebar: () => this.toggleMobileSidebar(side),
294
+ closeMobileSidebar: this.closeMobileSidebar,
295
+ }
296
+ },
297
+ showSidebarForSide(side) {
298
+ if (this.isDesktopViewport) return true
299
+ return (
300
+ (this.mobileSidebarOpen && this.mobileSidebarSide === side) ||
301
+ (this.touchGesture.trackingEdgeSwipe && this.touchGesture.side === side)
302
+ )
303
+ },
304
+ mobileSidebarClass(side) {
305
+ if (this.isDesktopViewport) return ''
306
+ const horizontalClass = side === 'right'
307
+ ? 'fixed right-0 top-0 border-l border-slate-200'
308
+ : 'fixed left-0 top-0 border-r border-slate-200'
309
+ return `${horizontalClass} z-20 h-full max-w-[85vw] bg-slate-100 py-4 shadow-xl shadow-slate-900/20 transition-transform duration-200 ease-out`
310
+ },
311
+ mobileSidebarStyle(side) {
312
+ const baseStyle = side === 'right' ? this.sidebarRightStyle : this.sidebarLeftStyle
313
+ if (
314
+ this.isDesktopViewport ||
315
+ this.mobileSidebarOpen ||
316
+ !this.touchGesture.trackingEdgeSwipe ||
317
+ this.touchGesture.side !== side
318
+ ) {
319
+ return baseStyle
320
+ }
321
+ const widthValue = Number.parseFloat(
322
+ side === 'right' ? this.effectiveSidebarRightWidth : this.effectiveSidebarLeftWidth
323
+ )
324
+ const effectiveWidth = Number.isFinite(widthValue) ? widthValue : 300
325
+ const clampedOffset = Math.min(this.dragOffset, effectiveWidth)
326
+ const hiddenOffset = effectiveWidth - clampedOffset
327
+ const direction = side === 'right' ? hiddenOffset : clampedOffset - effectiveWidth
328
+ return {
329
+ ...baseStyle,
330
+ transform: `translateX(${direction}px)`,
331
+ transition: 'none',
332
+ }
333
+ },
334
+ mobileSidebarButtonClass(side) {
335
+ if (this.mobileSidebarOpen) {
336
+ return side === 'right' ? 'ml-auto' : ''
337
+ }
338
+ const title = this.sidebarTitleForSide(side)
339
+ const edgeClass = side === 'right'
340
+ ? 'ml-auto rounded-l-xl rounded-r-none border-r-0'
341
+ : 'rounded-l-none rounded-r-xl border-l-0'
342
+ if (title) {
343
+ return `${edgeClass} mobile-sidebar-trigger--titled min-h-0 min-w-0 px-2 py-3`
344
+ }
345
+ return side === 'right'
346
+ ? 'ml-auto rounded-full'
347
+ : 'rounded-full'
348
+ },
349
+ sidebarTitleForSide(side) {
350
+ return side === 'right' ? this.sidebarRightTitle : this.effectiveSidebarLeftTitle
351
+ },
352
+ mobileSidebarButtonLabel(side) {
353
+ if (this.mobileSidebarOpen && this.mobileSidebarSide === side) {
354
+ return 'Hide sidebar'
355
+ }
356
+ return this.sidebarTitleForSide(side) || 'Show sidebar'
357
+ },
358
+ mobileSidebarButtonAriaLabel(side) {
359
+ if (this.mobileSidebarOpen && this.mobileSidebarSide === side) return null
360
+ return this.sidebarTitleForSide(side) || 'Show sidebar'
361
+ },
362
+ mobileSidebarButtonShowsLabel(side) {
363
+ if (this.mobileSidebarOpen && this.mobileSidebarSide === side) return true
364
+ return Boolean(this.sidebarTitleForSide(side))
365
+ },
366
+ mobileSidebarIconName(side) {
367
+ if (side === 'right') {
368
+ return this.mobileSidebarOpen && this.mobileSidebarSide === side
369
+ ? 'PanelRightClose'
370
+ : 'PanelRightOpen'
371
+ }
372
+ return this.mobileSidebarOpen && this.mobileSidebarSide === side
373
+ ? 'PanelLeftClose'
374
+ : 'PanelLeftOpen'
375
+ },
209
376
  handleMainMenuToggle(event) {
210
377
  if (this.isDesktopViewport) return
211
378
  if (event?.detail?.open) {
212
- this.mobileSidebarOpen = false
379
+ this.closeMobileSidebar()
213
380
  }
214
381
  },
215
382
  handleViewportChange(event) {
@@ -218,13 +385,22 @@ export default {
218
385
  },
219
386
  syncSidebarForViewport() {
220
387
  if (!this.isDesktopViewport) {
221
- this.mobileSidebarOpen = false
222
- this.resetTouchState()
388
+ this.closeMobileSidebar()
223
389
  }
224
390
  },
225
- toggleMobileSidebar() {
391
+ toggleMobileSidebar(side = 'left') {
226
392
  if (this.isDesktopViewport) return
227
- this.mobileSidebarOpen = !this.mobileSidebarOpen
393
+ if ((side === 'left' && !this.hasLeftSidebar) || (side === 'right' && !this.hasRightSidebar)) return
394
+ if (this.mobileSidebarOpen && this.mobileSidebarSide === side) {
395
+ this.mobileSidebarOpen = false
396
+ } else {
397
+ this.mobileSidebarOpen = true
398
+ this.mobileSidebarSide = side
399
+ }
400
+ this.resetTouchState()
401
+ },
402
+ closeMobileSidebar() {
403
+ this.mobileSidebarOpen = false
228
404
  this.resetTouchState()
229
405
  },
230
406
  handleTouchStart(event) {
@@ -234,11 +410,19 @@ export default {
234
410
  }
235
411
  const touch = event.touches?.[0]
236
412
  if (!touch) return
413
+ const viewportWidth = typeof window === 'undefined' ? 0 : window.innerWidth
414
+ const isLeftEdge = this.hasLeftSidebar && touch.clientX <= MOBILE_EDGE_SWIPE_THRESHOLD
415
+ const isRightEdge = this.hasRightSidebar && viewportWidth - touch.clientX <= MOBILE_EDGE_SWIPE_THRESHOLD
416
+ if (!isLeftEdge && !isRightEdge) {
417
+ this.resetTouchState()
418
+ return
419
+ }
237
420
  this.touchGesture = {
421
+ side: isRightEdge ? 'right' : 'left',
238
422
  startX: touch.clientX,
239
423
  startY: touch.clientY,
240
424
  currentX: touch.clientX,
241
- trackingEdgeSwipe: touch.clientX <= MOBILE_EDGE_SWIPE_THRESHOLD,
425
+ trackingEdgeSwipe: true,
242
426
  }
243
427
  },
244
428
  handleTouchMove(event) {
@@ -255,14 +439,18 @@ export default {
255
439
  this.resetTouchState()
256
440
  return
257
441
  }
258
- const deltaX = this.touchGesture.currentX - this.touchGesture.startX
442
+ const deltaX = this.touchGesture.side === 'right'
443
+ ? this.touchGesture.startX - this.touchGesture.currentX
444
+ : this.touchGesture.currentX - this.touchGesture.startX
259
445
  if (deltaX >= MOBILE_SWIPE_OPEN_DISTANCE) {
260
446
  this.mobileSidebarOpen = true
447
+ this.mobileSidebarSide = this.touchGesture.side
261
448
  }
262
449
  this.resetTouchState()
263
450
  },
264
451
  resetTouchState() {
265
452
  this.touchGesture = {
453
+ side: 'left',
266
454
  startX: 0,
267
455
  startY: 0,
268
456
  currentX: 0,
@@ -277,3 +465,56 @@ export default {
277
465
  },
278
466
  }
279
467
  </script>
468
+
469
+ <style scoped>
470
+ .mobile-sidebar-trigger {
471
+ display: inline-flex;
472
+ align-items: center;
473
+ justify-content: center;
474
+ gap: 0.5rem;
475
+ min-height: 3.5rem;
476
+ padding: 0.875rem;
477
+ border: 1px solid rgb(203 213 225);
478
+ background: rgb(239 246 255);
479
+ color: rgb(15 23 42);
480
+ box-shadow: 0 10px 25px -15px rgba(15, 23, 42, 0.45);
481
+ }
482
+
483
+ .mobile-sidebar-trigger--titled {
484
+ flex-direction: column;
485
+ width: 3rem;
486
+ min-height: 8.5rem;
487
+ padding: 0.75rem 0.35rem;
488
+ }
489
+
490
+ .mobile-sidebar-trigger:focus-visible {
491
+ outline: 2px solid rgb(37 99 235);
492
+ outline-offset: 2px;
493
+ }
494
+
495
+ .mobile-sidebar-trigger__label {
496
+ display: inline-block;
497
+ font-size: 0.625rem;
498
+ font-weight: 700;
499
+ letter-spacing: 0.08em;
500
+ text-transform: uppercase;
501
+ white-space: nowrap;
502
+ }
503
+
504
+ .mobile-sidebar-trigger--titled .mobile-sidebar-trigger__label {
505
+ writing-mode: vertical-rl;
506
+ text-orientation: mixed;
507
+ line-height: 1;
508
+ }
509
+
510
+ .mobile-sidebar-trigger--titled :deep(svg) {
511
+ width: 0.9rem;
512
+ height: 0.9rem;
513
+ flex-shrink: 0;
514
+ display: block;
515
+ }
516
+
517
+ .mobile-sidebar-trigger--titled.rounded-l-none .mobile-sidebar-trigger__label {
518
+ transform: rotate(180deg);
519
+ }
520
+ </style>
@@ -639,6 +639,9 @@ export default {
639
639
  >
640
640
  Δεν βρέθηκαν αποτελέσματα.
641
641
  </p>
642
+ <div v-if="!collapsed && $slots['sidebar-panel']" class="px-1 pt-3">
643
+ <slot name="sidebar-panel" />
644
+ </div>
642
645
  </nav>
643
646
 
644
647
  <div class="mt-auto border-t border-[var(--sidebar-border)] px-3 pb-3 pt-4">
@@ -26,6 +26,9 @@
26
26
  <template v-if="$slots['sidebar-header']" #sidebar-header>
27
27
  <slot name="sidebar-header" />
28
28
  </template>
29
+ <template v-if="$slots['sidebar-panel']" #sidebar-panel>
30
+ <slot name="sidebar-panel" />
31
+ </template>
29
32
  </MultilevelSidebar>
30
33
 
31
34
  <main class="flex min-h-0 flex-1 flex-col overflow-hidden">