ketekny-ui-kit 1.0.114 → 1.0.115

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.114",
4
+ "version": "1.0.115",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -1,5 +1,11 @@
1
1
  <template>
2
2
  <main class="flex min-h-0 w-full max-w-none flex-1 flex-col overflow-hidden bg-slate-100">
3
+ <div
4
+ v-if="mobileSidebarOpen && !isDesktopViewport"
5
+ class="fixed inset-0 z-20 bg-slate-950/40 backdrop-blur-[1px] lg:hidden"
6
+ @click="toggleMobileSidebar"
7
+ />
8
+
3
9
  <section
4
10
  v-if="hasTopSection"
5
11
  class="shrink-0 bg-white px-4 pb-4 pb-0 pt-8 sm:px-6 lg:px-8"
@@ -44,8 +50,27 @@
44
50
  </div>
45
51
 
46
52
  <div class="flex min-h-0 flex-1 flex-col overflow-hidden bg-slate-100 py-4">
53
+ <div
54
+ class="lg:hidden"
55
+ :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'"
56
+ >
57
+ <kButton
58
+ :label="mobileSidebarOpen ? 'Hide sidebar' : 'Show sidebar'"
59
+ :icon="mobileSidebarOpen ? 'PanelLeftClose' : 'PanelLeftOpen'"
60
+ variant="soft"
61
+ :icon-only="!mobileSidebarOpen"
62
+ :aria-label="mobileSidebarOpen ? null : 'Show sidebar'"
63
+ :class="mobileSidebarOpen ? '' : 'rounded-full shadow-lg shadow-slate-900/15'"
64
+ @click="toggleMobileSidebar"
65
+ />
66
+ </div>
47
67
  <div class="mx-auto flex min-h-0 w-full max-w-none flex-1 gap-2">
48
- <aside class="ml-8 min-h-0 shrink-0 overflow-y-auto pr-4" :style="sidebarStyle">
68
+ <aside
69
+ v-if="showSidebar"
70
+ 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"
71
+ :class="mobileSidebarClass"
72
+ :style="sidebarStyle"
73
+ >
49
74
  <slot name="sidebar" />
50
75
  </aside>
51
76
  <section class="min-h-0 min-w-0 flex-1 overflow-y-auto px-2">
@@ -60,6 +85,9 @@
60
85
  import packageJson from '../../package.json'
61
86
  import kButton from '../ui/kButton.vue'
62
87
 
88
+ const LG_VIEWPORT_QUERY = '(min-width: 1024px)'
89
+ const MAIN_MENU_TOGGLE_EVENT = 'ketekny:two-col-layout-mobile-menu'
90
+
63
91
  export default {
64
92
  name: 'PageTwoColShell',
65
93
  components: { kButton },
@@ -85,6 +113,13 @@ export default {
85
113
  default: '300px',
86
114
  },
87
115
  },
116
+ data() {
117
+ return {
118
+ isDesktopViewport: true,
119
+ mobileSidebarOpen: false,
120
+ viewportQuery: null,
121
+ }
122
+ },
88
123
  computed: {
89
124
  hasTopSection() {
90
125
  return Boolean(
@@ -105,8 +140,46 @@ export default {
105
140
  maxWidth: this.sidebarWidth,
106
141
  }
107
142
  },
143
+ mobileSidebarClass() {
144
+ if (this.isDesktopViewport) return ''
145
+ 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'
146
+ },
147
+ showSidebar() {
148
+ return this.isDesktopViewport || this.mobileSidebarOpen
149
+ },
150
+ },
151
+ mounted() {
152
+ if (typeof window === 'undefined') return
153
+ this.viewportQuery = window.matchMedia(LG_VIEWPORT_QUERY)
154
+ this.isDesktopViewport = this.viewportQuery.matches
155
+ this.syncSidebarForViewport()
156
+ this.viewportQuery.addEventListener('change', this.handleViewportChange)
157
+ window.addEventListener(MAIN_MENU_TOGGLE_EVENT, this.handleMainMenuToggle)
158
+ },
159
+ beforeUnmount() {
160
+ this.viewportQuery?.removeEventListener?.('change', this.handleViewportChange)
161
+ window.removeEventListener(MAIN_MENU_TOGGLE_EVENT, this.handleMainMenuToggle)
108
162
  },
109
163
  methods: {
164
+ handleMainMenuToggle(event) {
165
+ if (this.isDesktopViewport) return
166
+ if (event?.detail?.open) {
167
+ this.mobileSidebarOpen = false
168
+ }
169
+ },
170
+ handleViewportChange(event) {
171
+ this.isDesktopViewport = event.matches
172
+ this.syncSidebarForViewport()
173
+ },
174
+ syncSidebarForViewport() {
175
+ if (!this.isDesktopViewport) {
176
+ this.mobileSidebarOpen = false
177
+ }
178
+ },
179
+ toggleMobileSidebar() {
180
+ if (this.isDesktopViewport) return
181
+ this.mobileSidebarOpen = !this.mobileSidebarOpen
182
+ },
110
183
  openExternalUrl() {
111
184
  const target = String(this.externalUrl || '').trim()
112
185
  if (!target) return
@@ -18,6 +18,7 @@ import demoAvatarUrl from '../assets/placeholder-user.jpg'
18
18
  const FAVORITES_STORAGE_KEY = 'ketekny-ui-sidebar-favorites'
19
19
  const MENU_TAB_STORAGE_KEY = 'ketekny-ui-sidebar-menu-tab'
20
20
  const LOGO_BASE_URL = 'https://s3.ketekny.gr/public/web-apps/logos'
21
+ const LG_VIEWPORT_QUERY = '(min-width: 1024px)'
21
22
 
22
23
  export default {
23
24
  name: 'MultilevelSidebar',
@@ -86,6 +87,8 @@ export default {
86
87
  activeMenuTab: 'all',
87
88
  query: '',
88
89
  collapsed: false,
90
+ isDesktopViewport: true,
91
+ viewportQuery: null,
89
92
  openIds: new Set(['projects']),
90
93
  favoriteIds: new Set(),
91
94
  footerLinks: [
@@ -232,8 +235,30 @@ export default {
232
235
  activeMenuTab(value) {
233
236
  this.persistActiveMenuTab(value)
234
237
  },
238
+ mobileOpen(isOpen) {
239
+ if (isOpen) this.syncCollapsedForViewport()
240
+ },
241
+ },
242
+ mounted() {
243
+ if (typeof window === 'undefined') return
244
+ this.viewportQuery = window.matchMedia(LG_VIEWPORT_QUERY)
245
+ this.isDesktopViewport = this.viewportQuery.matches
246
+ this.syncCollapsedForViewport()
247
+ this.viewportQuery.addEventListener('change', this.handleViewportChange)
248
+ },
249
+ beforeUnmount() {
250
+ this.viewportQuery?.removeEventListener?.('change', this.handleViewportChange)
235
251
  },
236
252
  methods: {
253
+ handleViewportChange(event) {
254
+ this.isDesktopViewport = event.matches
255
+ this.syncCollapsedForViewport()
256
+ },
257
+ syncCollapsedForViewport() {
258
+ if (!this.isDesktopViewport) {
259
+ this.collapsed = false
260
+ }
261
+ },
237
262
  findMenuItemById(items, id) {
238
263
  for (const item of items) {
239
264
  if (item.id === id) return item
@@ -307,6 +332,10 @@ export default {
307
332
  }
308
333
  },
309
334
  toggleCollapsed() {
335
+ if (!this.isDesktopViewport) {
336
+ this.collapsed = false
337
+ return
338
+ }
310
339
  this.collapsed = !this.collapsed
311
340
  },
312
341
  toggle(id) {
@@ -39,9 +39,9 @@
39
39
  </main>
40
40
 
41
41
  <button
42
- v-if="!mobileMenuOpen"
42
+ v-if="!mobileMenuOpen && showMobileMenuButton"
43
43
  type="button"
44
- class="fixed bottom-5 left-5 z-50 inline-flex size-14 items-center justify-center rounded-full transition-colors lg:hidden"
44
+ class="fixed bottom-0 left-0 z-50 inline-flex size-14 items-center justify-center rounded-tr-2xl transition-colors lg:hidden"
45
45
  :class="theme === 'light'
46
46
  ? 'border border-slate-200 bg-white text-slate-700 shadow-lg shadow-slate-900/10 hover:bg-slate-100'
47
47
  : 'bg-sidebar text-white shadow-lg shadow-black/25 hover:bg-sidebar-accent'"
@@ -56,6 +56,9 @@
56
56
  import { Menu } from "@lucide/vue";
57
57
  import MultilevelSidebar from "./MultilevelSidebar.vue";
58
58
 
59
+ const LG_VIEWPORT_QUERY = "(min-width: 1024px)";
60
+ const MAIN_MENU_TOGGLE_EVENT = "ketekny:two-col-layout-mobile-menu";
61
+
59
62
  export default {
60
63
  name: "TwoColLayout",
61
64
  components: {
@@ -105,14 +108,66 @@ export default {
105
108
  data() {
106
109
  return {
107
110
  mobileMenuOpen: false,
111
+ desktopViewportQuery: null,
112
+ showMobileMenuButton: true,
113
+ mobileScrollHideTimer: null,
108
114
  };
109
115
  },
116
+ mounted() {
117
+ if (typeof window === "undefined") return;
118
+ this.desktopViewportQuery = window.matchMedia(LG_VIEWPORT_QUERY);
119
+ this.syncMenuForViewport(this.desktopViewportQuery.matches);
120
+ this.desktopViewportQuery.addEventListener("change", this.handleViewportChange);
121
+ window.addEventListener("scroll", this.handleWindowScroll, { passive: true });
122
+ },
123
+ beforeUnmount() {
124
+ this.desktopViewportQuery?.removeEventListener?.("change", this.handleViewportChange);
125
+ window.removeEventListener("scroll", this.handleWindowScroll);
126
+ if (this.mobileScrollHideTimer) {
127
+ window.clearTimeout(this.mobileScrollHideTimer);
128
+ this.mobileScrollHideTimer = null;
129
+ }
130
+ },
110
131
  methods: {
132
+ handleWindowScroll() {
133
+ if (typeof window === "undefined") return;
134
+ if (this.mobileMenuOpen || this.desktopViewportQuery?.matches) return;
135
+ this.showMobileMenuButton = false;
136
+ if (this.mobileScrollHideTimer) {
137
+ window.clearTimeout(this.mobileScrollHideTimer);
138
+ }
139
+ this.mobileScrollHideTimer = window.setTimeout(() => {
140
+ this.showMobileMenuButton = true;
141
+ this.mobileScrollHideTimer = null;
142
+ }, 180);
143
+ },
144
+ emitMobileMenuState() {
145
+ if (typeof window === "undefined") return;
146
+ window.dispatchEvent(
147
+ new CustomEvent(MAIN_MENU_TOGGLE_EVENT, {
148
+ detail: { open: this.mobileMenuOpen },
149
+ })
150
+ );
151
+ },
152
+ handleViewportChange(event) {
153
+ this.syncMenuForViewport(event.matches);
154
+ },
155
+ syncMenuForViewport(isDesktopViewport) {
156
+ if (!isDesktopViewport) {
157
+ this.mobileMenuOpen = false;
158
+ this.emitMobileMenuState();
159
+ this.showMobileMenuButton = true;
160
+ }
161
+ },
111
162
  toggleMenu() {
112
163
  this.mobileMenuOpen = !this.mobileMenuOpen;
164
+ this.showMobileMenuButton = !this.mobileMenuOpen;
165
+ this.emitMobileMenuState();
113
166
  },
114
167
  closeMenu() {
115
168
  this.mobileMenuOpen = false;
169
+ this.showMobileMenuButton = true;
170
+ this.emitMobileMenuState();
116
171
  },
117
172
  handleSelect(id) {
118
173
  this.closeMenu();