generated from corrad-software/corrad-af-2024
408 lines
11 KiB
Vue
408 lines
11 KiB
Vue
<script setup>
|
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
|
import { useDesignSystem } from '~/composables/useDesignSystem';
|
|
|
|
const props = defineProps({
|
|
// Layout type
|
|
layout: {
|
|
type: String,
|
|
default: 'default',
|
|
validator: (value) => ['default', 'sidebar', 'dashboard', 'fullscreen', 'split'].includes(value)
|
|
},
|
|
|
|
// Responsive behavior
|
|
adaptive: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
|
|
// Mobile behavior
|
|
mobileCollapse: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
|
|
// Padding/spacing
|
|
padding: {
|
|
type: String,
|
|
default: 'md',
|
|
validator: (value) => ['none', 'sm', 'md', 'lg', 'xl'].includes(value)
|
|
},
|
|
|
|
// Maximum width
|
|
maxWidth: {
|
|
type: String,
|
|
default: 'full',
|
|
validator: (value) => ['sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl', '7xl', 'full'].includes(value)
|
|
},
|
|
|
|
// Center content
|
|
centered: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
// Mobile navigation
|
|
showMobileNav: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(['breakpoint-change', 'mobile-nav-toggle']);
|
|
|
|
// Design system
|
|
const { tokens, utils, layouts } = useDesignSystem();
|
|
|
|
// Responsive state
|
|
const currentBreakpoint = ref('lg');
|
|
const isMobile = ref(false);
|
|
const isTablet = ref(false);
|
|
const isDesktop = ref(true);
|
|
const mobileNavOpen = ref(false);
|
|
|
|
// Breakpoints
|
|
const breakpoints = {
|
|
sm: 640,
|
|
md: 768,
|
|
lg: 1024,
|
|
xl: 1280,
|
|
'2xl': 1536
|
|
};
|
|
|
|
// Update responsive state
|
|
const updateBreakpoint = () => {
|
|
const width = window.innerWidth;
|
|
let newBreakpoint = 'sm';
|
|
|
|
if (width >= breakpoints['2xl']) newBreakpoint = '2xl';
|
|
else if (width >= breakpoints.xl) newBreakpoint = 'xl';
|
|
else if (width >= breakpoints.lg) newBreakpoint = 'lg';
|
|
else if (width >= breakpoints.md) newBreakpoint = 'md';
|
|
else newBreakpoint = 'sm';
|
|
|
|
if (newBreakpoint !== currentBreakpoint.value) {
|
|
currentBreakpoint.value = newBreakpoint;
|
|
emit('breakpoint-change', newBreakpoint);
|
|
}
|
|
|
|
isMobile.value = width < breakpoints.md;
|
|
isTablet.value = width >= breakpoints.md && width < breakpoints.lg;
|
|
isDesktop.value = width >= breakpoints.lg;
|
|
};
|
|
|
|
// Computed classes
|
|
const containerClasses = computed(() => {
|
|
const classes = ['responsive-container'];
|
|
|
|
// Layout classes
|
|
switch (props.layout) {
|
|
case 'sidebar':
|
|
classes.push(isMobile.value ? 'layout-mobile' : 'layout-sidebar');
|
|
break;
|
|
case 'dashboard':
|
|
classes.push('layout-dashboard');
|
|
break;
|
|
case 'fullscreen':
|
|
classes.push('layout-fullscreen');
|
|
break;
|
|
case 'split':
|
|
classes.push(isMobile.value ? 'layout-mobile' : 'layout-split');
|
|
break;
|
|
default:
|
|
classes.push('layout-default');
|
|
}
|
|
|
|
// Responsive classes
|
|
classes.push(`breakpoint-${currentBreakpoint.value}`);
|
|
|
|
if (isMobile.value) classes.push('is-mobile');
|
|
if (isTablet.value) classes.push('is-tablet');
|
|
if (isDesktop.value) classes.push('is-desktop');
|
|
|
|
// Padding classes
|
|
const paddingMap = {
|
|
none: '',
|
|
sm: 'p-2 md:p-4',
|
|
md: 'p-4 md:p-6',
|
|
lg: 'p-6 md:p-8',
|
|
xl: 'p-8 md:p-12'
|
|
};
|
|
if (paddingMap[props.padding]) {
|
|
classes.push(paddingMap[props.padding]);
|
|
}
|
|
|
|
// Max width classes
|
|
if (props.maxWidth !== 'full') {
|
|
classes.push(`max-w-${props.maxWidth}`);
|
|
}
|
|
|
|
// Centering
|
|
if (props.centered) {
|
|
classes.push('mx-auto');
|
|
}
|
|
|
|
return classes.join(' ');
|
|
});
|
|
|
|
// Mobile navigation toggle
|
|
const toggleMobileNav = () => {
|
|
mobileNavOpen.value = !mobileNavOpen.value;
|
|
emit('mobile-nav-toggle', mobileNavOpen.value);
|
|
};
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
updateBreakpoint();
|
|
window.addEventListener('resize', updateBreakpoint);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('resize', updateBreakpoint);
|
|
});
|
|
|
|
// Expose reactive state
|
|
defineExpose({
|
|
currentBreakpoint,
|
|
isMobile,
|
|
isTablet,
|
|
isDesktop,
|
|
mobileNavOpen,
|
|
toggleMobileNav
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div :class="containerClasses">
|
|
<!-- Mobile Navigation Header -->
|
|
<header
|
|
v-if="isMobile && showMobileNav"
|
|
class="mobile-nav-header sticky top-0 z-40 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-600 p-4"
|
|
>
|
|
<div class="flex items-center justify-between">
|
|
<slot name="mobile-title">
|
|
<h1 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
|
DMS Mobile
|
|
</h1>
|
|
</slot>
|
|
|
|
<button
|
|
@click="toggleMobileNav"
|
|
class="p-2 rounded-md text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
aria-label="Toggle navigation"
|
|
>
|
|
<Icon
|
|
:name="mobileNavOpen ? 'mdi:close' : 'mdi:menu'"
|
|
class="w-6 h-6"
|
|
/>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Mobile Navigation Drawer -->
|
|
<Transition
|
|
enter-active-class="transition-transform duration-300 ease-out"
|
|
enter-from-class="transform -translate-x-full"
|
|
enter-to-class="transform translate-x-0"
|
|
leave-active-class="transition-transform duration-300 ease-in"
|
|
leave-from-class="transform translate-x-0"
|
|
leave-to-class="transform -translate-x-full"
|
|
>
|
|
<aside
|
|
v-if="isMobile && mobileNavOpen"
|
|
class="fixed inset-y-0 left-0 z-50 w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-600 shadow-lg"
|
|
>
|
|
<div class="h-full flex flex-col">
|
|
<div class="flex-1 overflow-y-auto p-4">
|
|
<slot name="mobile-nav">
|
|
<!-- Default mobile navigation content -->
|
|
<nav class="space-y-2">
|
|
<a href="#" class="block p-2 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
|
Dashboard
|
|
</a>
|
|
<a href="#" class="block p-2 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
|
Documents
|
|
</a>
|
|
<a href="#" class="block p-2 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
|
Settings
|
|
</a>
|
|
</nav>
|
|
</slot>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</Transition>
|
|
|
|
<!-- Mobile Navigation Overlay -->
|
|
<div
|
|
v-if="isMobile && mobileNavOpen"
|
|
class="fixed inset-0 z-40 bg-black/50"
|
|
@click="toggleMobileNav"
|
|
></div>
|
|
|
|
<!-- Main Content -->
|
|
<main
|
|
class="main-content"
|
|
:class="{
|
|
'ml-0': isMobile || !mobileNavOpen,
|
|
'transition-all duration-300': props.adaptive
|
|
}"
|
|
>
|
|
<!-- Desktop Sidebar -->
|
|
<aside
|
|
v-if="layout === 'sidebar' && !isMobile"
|
|
class="sidebar-desktop fixed left-0 top-0 z-30 h-full w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-600"
|
|
>
|
|
<slot name="sidebar">
|
|
<!-- Default sidebar content -->
|
|
<div class="p-6">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
Navigation
|
|
</h2>
|
|
<nav class="space-y-2">
|
|
<a href="#" class="block p-2 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
|
Dashboard
|
|
</a>
|
|
<a href="#" class="block p-2 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
|
Documents
|
|
</a>
|
|
</nav>
|
|
</div>
|
|
</slot>
|
|
</aside>
|
|
|
|
<!-- Content Area -->
|
|
<div
|
|
class="content-area"
|
|
:class="{
|
|
'ml-64': layout === 'sidebar' && !isMobile,
|
|
'grid grid-cols-1 lg:grid-cols-2 gap-6': layout === 'split' && !isMobile,
|
|
'min-h-screen': layout === 'fullscreen'
|
|
}"
|
|
>
|
|
<!-- Split Layout - Left Panel -->
|
|
<section v-if="layout === 'split' && !isMobile" class="split-left">
|
|
<slot name="split-left">
|
|
<div class="h-full bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
Left Panel
|
|
</h3>
|
|
<p class="text-gray-600 dark:text-gray-400">
|
|
Left panel content goes here.
|
|
</p>
|
|
</div>
|
|
</slot>
|
|
</section>
|
|
|
|
<!-- Main Content / Split Layout - Right Panel -->
|
|
<section
|
|
class="main-section"
|
|
:class="{
|
|
'split-right': layout === 'split' && !isMobile
|
|
}"
|
|
>
|
|
<slot name="default">
|
|
<!-- Default content -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
|
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
Main Content
|
|
</h2>
|
|
<p class="text-gray-600 dark:text-gray-400">
|
|
Your main content goes here. This container adapts to different screen sizes.
|
|
</p>
|
|
</div>
|
|
</slot>
|
|
</section>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Responsive Debug Info (Development only) -->
|
|
<div
|
|
v-if="process.env.NODE_ENV === 'development'"
|
|
class="fixed bottom-4 right-4 z-50 bg-black/80 text-white text-xs p-2 rounded-md font-mono"
|
|
>
|
|
<div>Breakpoint: {{ currentBreakpoint }}</div>
|
|
<div>Width: {{ isMobile ? 'Mobile' : isTablet ? 'Tablet' : 'Desktop' }}</div>
|
|
<div>Layout: {{ layout }}</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.responsive-container {
|
|
@apply min-h-screen;
|
|
}
|
|
|
|
/* Layout-specific styles */
|
|
.layout-sidebar {
|
|
@apply flex;
|
|
}
|
|
|
|
.layout-dashboard {
|
|
@apply grid grid-cols-1 lg:grid-cols-12 gap-6;
|
|
}
|
|
|
|
.layout-fullscreen {
|
|
@apply h-screen overflow-hidden;
|
|
}
|
|
|
|
.layout-split {
|
|
@apply grid grid-cols-1 lg:grid-cols-2 gap-6;
|
|
}
|
|
|
|
.layout-mobile {
|
|
@apply flex flex-col;
|
|
}
|
|
|
|
/* Mobile-specific adjustments */
|
|
.is-mobile .sidebar-desktop {
|
|
@apply hidden;
|
|
}
|
|
|
|
.is-mobile .content-area {
|
|
@apply ml-0;
|
|
}
|
|
|
|
/* Smooth transitions for adaptive layouts */
|
|
.main-content {
|
|
transition: margin-left 0.3s ease;
|
|
}
|
|
|
|
/* Enhanced mobile navigation */
|
|
.mobile-nav-header {
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
|
|
/* Custom scrollbar for mobile nav */
|
|
.mobile-nav-drawer::-webkit-scrollbar {
|
|
width: 4px;
|
|
}
|
|
|
|
.mobile-nav-drawer::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.mobile-nav-drawer::-webkit-scrollbar-thumb {
|
|
background-color: rgba(156, 163, 175, 0.5);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* Responsive typography */
|
|
@media (max-width: 640px) {
|
|
h1 { @apply text-lg; }
|
|
h2 { @apply text-base; }
|
|
h3 { @apply text-sm; }
|
|
}
|
|
|
|
@media (min-width: 641px) and (max-width: 1024px) {
|
|
h1 { @apply text-xl; }
|
|
h2 { @apply text-lg; }
|
|
h3 { @apply text-base; }
|
|
}
|
|
|
|
@media (min-width: 1025px) {
|
|
h1 { @apply text-2xl; }
|
|
h2 { @apply text-xl; }
|
|
h3 { @apply text-lg; }
|
|
}
|
|
</style> |