Skip to content

Commit 6750ea7

Browse files
chore: wip
1 parent 7ad4426 commit 6750ea7

25 files changed

+4295
-0
lines changed
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
<script setup lang="ts">
2+
/**
3+
* Dashboard Layout Component
4+
* A unified layout wrapper that combines the modern sidebar and navbar
5+
* for consistent dashboard page structure.
6+
*/
7+
import { ref, computed, provide, onMounted, onUnmounted } from 'vue'
8+
import { useLocalStorage, useDark, useMediaQuery } from '@vueuse/core'
9+
import SidebarModern from './SidebarModern.vue'
10+
import NavbarModern from './NavbarModern.vue'
11+
12+
interface Props {
13+
// Page-level settings
14+
pageTitle?: string
15+
pageDescription?: string
16+
// Layout options
17+
showSidebar?: boolean
18+
showNavbar?: boolean
19+
showWindowControls?: boolean
20+
// Content options
21+
fullWidth?: boolean
22+
noPadding?: boolean
23+
// Background customization (for window controls color extraction)
24+
backgroundImage?: string
25+
}
26+
27+
const props = withDefaults(defineProps<Props>(), {
28+
showSidebar: true,
29+
showNavbar: true,
30+
showWindowControls: false,
31+
fullWidth: false,
32+
noPadding: false,
33+
})
34+
35+
const emit = defineEmits<{
36+
(e: 'minimize'): void
37+
(e: 'maximize'): void
38+
(e: 'close'): void
39+
}>()
40+
41+
// Sidebar state
42+
const isSidebarCollapsed = useLocalStorage('sidebar-collapsed-modern', false)
43+
const isMobileMenuOpen = ref(false)
44+
const isDark = useDark()
45+
46+
// Responsive detection
47+
const isMobile = useMediaQuery('(max-width: 1023px)')
48+
const isDesktop = computed(() => !isMobile.value)
49+
50+
// Sidebar width calculation
51+
const sidebarWidth = computed(() => {
52+
if (isMobile.value) return 0
53+
return isSidebarCollapsed.value ? 64 : 256
54+
})
55+
56+
// Provide layout context to child components
57+
provide('isDark', isDark)
58+
provide('isSidebarCollapsed', isSidebarCollapsed)
59+
provide('isMobile', isMobile)
60+
61+
// Mobile menu handling
62+
function openMobileMenu() {
63+
isMobileMenuOpen.value = true
64+
}
65+
66+
function closeMobileMenu() {
67+
isMobileMenuOpen.value = false
68+
}
69+
70+
// Handle escape key for mobile menu
71+
function handleKeydown(event: KeyboardEvent) {
72+
if (event.key === 'Escape' && isMobileMenuOpen.value) {
73+
closeMobileMenu()
74+
}
75+
}
76+
77+
onMounted(() => {
78+
document.addEventListener('keydown', handleKeydown)
79+
})
80+
81+
onUnmounted(() => {
82+
document.removeEventListener('keydown', handleKeydown)
83+
})
84+
85+
// Window control handlers
86+
function handleMinimize() {
87+
emit('minimize')
88+
}
89+
90+
function handleMaximize() {
91+
emit('maximize')
92+
}
93+
94+
function handleClose() {
95+
emit('close')
96+
}
97+
</script>
98+
99+
<template>
100+
<div class="min-h-screen bg-neutral-50 dark:bg-neutral-950">
101+
<!-- Mobile sidebar overlay -->
102+
<Transition
103+
enter-active-class="transition-opacity duration-300 ease-out"
104+
enter-from-class="opacity-0"
105+
enter-to-class="opacity-100"
106+
leave-active-class="transition-opacity duration-200 ease-in"
107+
leave-from-class="opacity-100"
108+
leave-to-class="opacity-0"
109+
>
110+
<div
111+
v-if="isMobileMenuOpen && isMobile"
112+
class="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm lg:hidden"
113+
@click="closeMobileMenu"
114+
/>
115+
</Transition>
116+
117+
<!-- Mobile sidebar -->
118+
<Transition
119+
enter-active-class="transition-transform duration-300 ease-out"
120+
enter-from-class="-translate-x-full"
121+
enter-to-class="translate-x-0"
122+
leave-active-class="transition-transform duration-200 ease-in"
123+
leave-from-class="translate-x-0"
124+
leave-to-class="-translate-x-full"
125+
>
126+
<div
127+
v-if="isMobileMenuOpen && isMobile && showSidebar"
128+
class="fixed inset-y-0 left-0 z-50 w-64 lg:hidden"
129+
>
130+
<SidebarModern @close="closeMobileMenu" />
131+
</div>
132+
</Transition>
133+
134+
<!-- Desktop sidebar -->
135+
<SidebarModern v-if="showSidebar && isDesktop" />
136+
137+
<!-- Main content area -->
138+
<div
139+
:class="[
140+
'flex flex-col min-h-screen transition-all duration-300',
141+
showSidebar && isDesktop ? 'lg:pl-64' : '',
142+
isSidebarCollapsed && isDesktop ? '!lg:pl-16' : '',
143+
]"
144+
:style="{ paddingLeft: showSidebar && isDesktop ? `${sidebarWidth}px` : '0' }"
145+
>
146+
<!-- Navbar -->
147+
<NavbarModern
148+
v-if="showNavbar"
149+
:show-window-controls="showWindowControls"
150+
@minimize="handleMinimize"
151+
@maximize="handleMaximize"
152+
@close="handleClose"
153+
@open-mobile-menu="openMobileMenu"
154+
/>
155+
156+
<!-- Page header slot -->
157+
<div v-if="$slots.header" class="border-b border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
158+
<div
159+
:class="[
160+
'px-4 lg:px-6 py-4',
161+
fullWidth ? '' : 'max-w-7xl mx-auto',
162+
]"
163+
>
164+
<slot name="header">
165+
<!-- Default page header -->
166+
<div v-if="pageTitle" class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
167+
<div>
168+
<h1 class="text-2xl font-semibold text-neutral-900 dark:text-white">
169+
{{ pageTitle }}
170+
</h1>
171+
<p v-if="pageDescription" class="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
172+
{{ pageDescription }}
173+
</p>
174+
</div>
175+
<slot name="header-actions" />
176+
</div>
177+
</slot>
178+
</div>
179+
</div>
180+
181+
<!-- Main content -->
182+
<main
183+
:class="[
184+
'flex-1',
185+
noPadding ? '' : 'px-4 lg:px-6 py-6',
186+
]"
187+
>
188+
<div
189+
:class="[
190+
fullWidth ? '' : 'max-w-7xl mx-auto',
191+
]"
192+
>
193+
<slot />
194+
</div>
195+
</main>
196+
197+
<!-- Footer slot -->
198+
<footer v-if="$slots.footer" class="border-t border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
199+
<div
200+
:class="[
201+
'px-4 lg:px-6 py-4',
202+
fullWidth ? '' : 'max-w-7xl mx-auto',
203+
]"
204+
>
205+
<slot name="footer" />
206+
</div>
207+
</footer>
208+
</div>
209+
210+
<!-- Global overlays/modals slot -->
211+
<slot name="overlays" />
212+
</div>
213+
</template>

0 commit comments

Comments
 (0)