38 KiB
Executable File
38 KiB
Executable File
📱 RESPONSIVE - Documentazione Completa NetGescon
Sistema Responsive dell'interfaccia universale
📅 Creato: 21/07/2025
🔄 Ultimo aggiornamento: 21/07/2025
🎯 Priorità: ⚡ ALTO
📋 OVERVIEW RESPONSIVE SYSTEM
Il Sistema Responsive NetGescon garantisce ESPERIENZA OTTIMALE su:
- 📱 Mobile Devices (320px - 767px)
- 📟 Tablet Devices (768px - 1023px)
- 💻 Desktop Screens (1024px - 1439px)
- 🖥️ Large Displays (1440px+)
- 📺 Ultra-wide Monitors (2560px+)
🎯 Responsive Philosophy
- Mobile First - Design ottimizzato prima per mobile
- Progressive Enhancement - Funzionalità aggiuntive su schermi più grandi
- Fluid Layouts - Adattamento fluido a qualsiasi dimensione
- Touch Friendly - Interfaccia ottimizzata per touch
📐 BREAKPOINT SYSTEM
🎨 CSS Breakpoints
/* /public/css/responsive/breakpoints.css */
:root {
/* === BREAKPOINT DEFINITIONS === */
--netgescon-breakpoint-xs: 320px; /* Extra Small devices */
--netgescon-breakpoint-sm: 640px; /* Small devices */
--netgescon-breakpoint-md: 768px; /* Medium devices */
--netgescon-breakpoint-lg: 1024px; /* Large devices */
--netgescon-breakpoint-xl: 1280px; /* Extra Large devices */
--netgescon-breakpoint-2xl: 1536px; /* 2X Large devices */
--netgescon-breakpoint-3xl: 1920px; /* 3X Large devices */
}
/* === MEDIA QUERY MIXINS === */
@media (max-width: 639px) {
/* Mobile styles */
.mobile-only { display: block !important; }
.tablet-up { display: none !important; }
.desktop-up { display: none !important; }
}
@media (min-width: 640px) and (max-width: 767px) {
/* Small tablet styles */
.mobile-only { display: none !important; }
.tablet-only { display: block !important; }
}
@media (min-width: 768px) {
/* Tablet and up */
.mobile-only { display: none !important; }
.tablet-up { display: block !important; }
}
@media (min-width: 1024px) {
/* Desktop and up */
.tablet-only { display: none !important; }
.desktop-up { display: block !important; }
}
@media (min-width: 1280px) {
/* Large desktop */
.desktop-only { display: none !important; }
.large-desktop-up { display: block !important; }
}
📱 JavaScript Breakpoints
// /public/js/responsive/breakpoints.js
class ResponsiveManager {
constructor() {
this.breakpoints = {
xs: 320,
sm: 640,
md: 768,
lg: 1024,
xl: 1280,
'2xl': 1536,
'3xl': 1920
};
this.currentBreakpoint = this.getCurrentBreakpoint();
this.orientation = this.getOrientation();
this.init();
}
init() {
// Listen for resize events
window.addEventListener('resize', this.handleResize.bind(this));
// Listen for orientation change
window.addEventListener('orientationchange', this.handleOrientationChange.bind(this));
// Initial setup
this.updateLayout();
}
getCurrentBreakpoint() {
const width = window.innerWidth;
if (width >= this.breakpoints['3xl']) return '3xl';
if (width >= this.breakpoints['2xl']) return '2xl';
if (width >= this.breakpoints.xl) return 'xl';
if (width >= this.breakpoints.lg) return 'lg';
if (width >= this.breakpoints.md) return 'md';
if (width >= this.breakpoints.sm) return 'sm';
return 'xs';
}
getOrientation() {
return window.innerHeight > window.innerWidth ? 'portrait' : 'landscape';
}
handleResize() {
const newBreakpoint = this.getCurrentBreakpoint();
const newOrientation = this.getOrientation();
if (newBreakpoint !== this.currentBreakpoint || newOrientation !== this.orientation) {
this.currentBreakpoint = newBreakpoint;
this.orientation = newOrientation;
this.updateLayout();
// Dispatch custom event
document.dispatchEvent(new CustomEvent('netgescon:breakpoint-change', {
detail: {
breakpoint: this.currentBreakpoint,
orientation: this.orientation,
width: window.innerWidth,
height: window.innerHeight
}
}));
}
}
handleOrientationChange() {
setTimeout(() => {
this.handleResize();
}, 100);
}
updateLayout() {
document.body.className = document.body.className
.replace(/breakpoint-\w+/g, '')
.replace(/orientation-\w+/g, '');
document.body.classList.add(`breakpoint-${this.currentBreakpoint}`);
document.body.classList.add(`orientation-${this.orientation}`);
// Update CSS custom property
document.documentElement.style.setProperty('--current-breakpoint', this.currentBreakpoint);
}
isMobile() {
return ['xs', 'sm'].includes(this.currentBreakpoint);
}
isTablet() {
return this.currentBreakpoint === 'md';
}
isDesktop() {
return ['lg', 'xl', '2xl', '3xl'].includes(this.currentBreakpoint);
}
isTouch() {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
}
}
// Initialize responsive manager
const ResponsiveManager = new ResponsiveManager();
window.ResponsiveManager = ResponsiveManager;
🏗️ LAYOUT SYSTEM
📱 Mobile Layout (320px - 767px)
/* /public/css/responsive/mobile.css */
@media (max-width: 767px) {
/* === LAYOUT STRUCTURE === */
.netgescon-layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* Header mobile */
.netgescon-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
z-index: 1000;
padding: 0 1rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* Sidebar mobile */
.netgescon-sidebar {
position: fixed;
top: 60px;
left: -280px;
width: 280px;
height: calc(100vh - 60px);
background: white;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
transition: left 0.3s ease;
z-index: 999;
overflow-y: auto;
}
.netgescon-sidebar.show {
left: 0;
}
/* Content mobile */
.netgescon-content {
margin-top: 60px;
padding: 1rem;
flex: 1;
min-height: calc(100vh - 60px);
}
/* Footer mobile */
.netgescon-footer {
display: none; /* Hidden on mobile */
}
/* === OVERLAY === */
.sidebar-overlay {
position: fixed;
top: 60px;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 998;
}
.sidebar-overlay.show {
opacity: 1;
visibility: visible;
}
/* === MOBILE NAVIGATION === */
.mobile-nav-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: none;
border: none;
color: var(--netgescon-gray-600);
font-size: 1.25rem;
cursor: pointer;
}
.mobile-nav-toggle:hover {
color: var(--netgescon-primary);
background: var(--netgescon-gray-100);
border-radius: var(--netgescon-border-radius);
}
/* === MOBILE CARDS === */
.netgescon-card {
margin-bottom: 1rem;
padding: 1rem;
}
.stats-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.content-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
/* === MOBILE FORMS === */
.form-row {
flex-direction: column;
}
.form-group {
margin-bottom: 1rem;
}
.btn-group {
flex-direction: column;
gap: 0.5rem;
}
.btn-group .btn {
width: 100%;
justify-content: center;
}
/* === MOBILE TABLES === */
.table-responsive-mobile {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.mobile-table-stack {
display: none;
}
@media (max-width: 576px) {
.table-responsive-mobile table {
display: none;
}
.mobile-table-stack {
display: block;
}
.mobile-table-item {
background: white;
border: 1px solid var(--netgescon-gray-200);
border-radius: var(--netgescon-border-radius);
padding: 1rem;
margin-bottom: 1rem;
}
.mobile-table-item .item-header {
font-weight: var(--netgescon-font-semibold);
color: var(--netgescon-gray-900);
margin-bottom: 0.5rem;
}
.mobile-table-item .item-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.25rem 0;
border-bottom: 1px solid var(--netgescon-gray-100);
}
.mobile-table-item .item-row:last-child {
border-bottom: none;
}
.mobile-table-item .item-label {
font-weight: var(--netgescon-font-medium);
color: var(--netgescon-gray-600);
font-size: var(--netgescon-text-sm);
}
.mobile-table-item .item-value {
color: var(--netgescon-gray-900);
text-align: right;
}
}
}
📟 Tablet Layout (768px - 1023px)
/* /public/css/responsive/tablet.css */
@media (min-width: 768px) and (max-width: 1023px) {
/* === LAYOUT STRUCTURE === */
.netgescon-layout {
display: grid;
grid-template-areas:
"header header"
"sidebar content"
"sidebar footer";
grid-template-columns: 260px 1fr;
grid-template-rows: 70px 1fr auto;
min-height: 100vh;
}
/* Header tablet */
.netgescon-header {
grid-area: header;
position: sticky;
top: 0;
z-index: 100;
}
/* Sidebar tablet */
.netgescon-sidebar {
grid-area: sidebar;
width: 260px;
position: sticky;
top: 70px;
height: calc(100vh - 70px);
overflow-y: auto;
border-right: 1px solid var(--netgescon-gray-200);
}
.netgescon-sidebar.collapsed {
width: 70px;
grid-template-columns: 70px 1fr;
}
/* Content tablet */
.netgescon-content {
grid-area: content;
padding: 1.5rem;
overflow-x: auto;
}
/* Footer tablet */
.netgescon-footer {
grid-area: footer;
border-top: 1px solid var(--netgescon-gray-200);
}
/* === TABLET CARDS === */
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
.content-grid {
grid-template-columns: repeat(2, 1fr);
gap: 2rem;
}
/* === TABLET FORMS === */
.form-row {
display: flex;
gap: 1rem;
}
.form-row .form-group {
flex: 1;
}
.btn-group {
flex-direction: row;
gap: 1rem;
}
/* === TABLET MODALS === */
.modal-dialog {
max-width: 600px;
margin: 2rem auto;
}
.modal-dialog-lg {
max-width: 800px;
}
}
💻 Desktop Layout (1024px+)
/* /public/css/responsive/desktop.css */
@media (min-width: 1024px) {
/* === LAYOUT STRUCTURE === */
.netgescon-layout {
display: grid;
grid-template-areas:
"header header"
"sidebar content"
"sidebar footer";
grid-template-columns: 280px 1fr;
grid-template-rows: 70px 1fr 70px;
min-height: 100vh;
}
/* Header desktop */
.netgescon-header {
grid-area: header;
position: sticky;
top: 0;
z-index: 100;
}
/* Sidebar desktop */
.netgescon-sidebar {
grid-area: sidebar;
width: 280px;
position: sticky;
top: 70px;
height: calc(100vh - 140px);
overflow-y: auto;
border-right: 1px solid var(--netgescon-gray-200);
}
.netgescon-sidebar.collapsed {
width: 70px;
.sidebar-text {
display: none;
}
.sidebar-icon {
margin: 0 auto;
}
}
/* Content desktop */
.netgescon-content {
grid-area: content;
padding: 2rem;
overflow-x: auto;
}
/* Footer desktop */
.netgescon-footer {
grid-area: footer;
height: 70px;
border-top: 1px solid var(--netgescon-gray-200);
}
/* === DESKTOP CARDS === */
.stats-grid {
grid-template-columns: repeat(4, 1fr);
gap: 2rem;
}
.content-grid {
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
/* === DESKTOP FORMS === */
.form-row {
display: flex;
gap: 1.5rem;
}
.form-row .form-group {
flex: 1;
}
.form-row .form-group.col-2 {
flex: 2;
}
.form-row .form-group.col-3 {
flex: 3;
}
/* === DESKTOP MODALS === */
.modal-dialog {
max-width: 800px;
margin: 3rem auto;
}
.modal-dialog-lg {
max-width: 1000px;
}
.modal-dialog-xl {
max-width: 1200px;
}
/* === DESKTOP TOOLTIPS === */
.tooltip {
display: block;
}
/* === DESKTOP HOVER EFFECTS === */
.netgescon-card:hover {
transform: translateY(-2px);
box-shadow: var(--netgescon-shadow-lg);
}
.btn:hover {
transform: translateY(-1px);
box-shadow: var(--netgescon-shadow-md);
}
}
📱 MOBILE ENHANCEMENTS
👆 Touch Interactions
/* /public/css/responsive/touch.css */
/* === TOUCH OPTIMIZATIONS === */
@media (hover: none) and (pointer: coarse) {
/* Touch device specific styles */
/* Larger touch targets */
.btn {
min-height: 44px;
min-width: 44px;
padding: 12px 16px;
}
.form-input {
min-height: 44px;
padding: 12px;
font-size: 16px; /* Prevent zoom on iOS */
}
.form-select {
min-height: 44px;
padding: 12px;
font-size: 16px;
}
/* Touch-friendly navigation */
.nav-link {
padding: 12px 16px;
min-height: 44px;
display: flex;
align-items: center;
}
/* Touch-friendly table rows */
.table tbody tr {
min-height: 48px;
}
.table tbody td {
padding: 12px 8px;
}
/* Remove hover effects on touch */
.netgescon-card:hover {
transform: none;
box-shadow: var(--netgescon-shadow);
}
.btn:hover {
transform: none;
box-shadow: none;
}
/* Touch feedback */
.btn:active {
transform: scale(0.98);
background-color: var(--netgescon-primary-dark);
}
.netgescon-card:active {
transform: scale(0.99);
}
}
📱 Mobile Navigation
// /public/js/responsive/mobile-nav.js
class MobileNavigation {
constructor() {
this.sidebar = document.getElementById('sidebar');
this.overlay = document.getElementById('sidebar-overlay');
this.toggleBtn = document.getElementById('mobile-nav-toggle');
this.isOpen = false;
this.init();
}
init() {
if (!this.sidebar || !this.toggleBtn) return;
// Create overlay if it doesn't exist
if (!this.overlay) {
this.createOverlay();
}
// Bind events
this.toggleBtn.addEventListener('click', this.toggle.bind(this));
this.overlay.addEventListener('click', this.close.bind(this));
// Close on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isOpen) {
this.close();
}
});
// Close on orientation change
window.addEventListener('orientationchange', () => {
setTimeout(() => this.close(), 100);
});
// Close when switching to desktop
window.addEventListener('resize', () => {
if (window.innerWidth >= 768 && this.isOpen) {
this.close();
}
});
}
createOverlay() {
this.overlay = document.createElement('div');
this.overlay.id = 'sidebar-overlay';
this.overlay.className = 'sidebar-overlay';
document.body.appendChild(this.overlay);
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
open() {
this.isOpen = true;
this.sidebar.classList.add('show');
this.overlay.classList.add('show');
document.body.style.overflow = 'hidden';
// Trap focus in sidebar
this.trapFocus();
// Dispatch event
document.dispatchEvent(new CustomEvent('netgescon:mobile-nav:open'));
}
close() {
this.isOpen = false;
this.sidebar.classList.remove('show');
this.overlay.classList.remove('show');
document.body.style.overflow = '';
// Release focus trap
this.releaseFocus();
// Dispatch event
document.dispatchEvent(new CustomEvent('netgescon:mobile-nav:close'));
}
trapFocus() {
const focusableElements = this.sidebar.querySelectorAll(
'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select'
);
if (focusableElements.length === 0) return;
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
this.focusHandler = (e) => {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
};
document.addEventListener('keydown', this.focusHandler);
firstElement.focus();
}
releaseFocus() {
if (this.focusHandler) {
document.removeEventListener('keydown', this.focusHandler);
this.focusHandler = null;
}
}
}
// Initialize mobile navigation
document.addEventListener('DOMContentLoaded', () => {
if (window.innerWidth < 768) {
new MobileNavigation();
}
});
📊 RESPONSIVE TABLES
📱 Mobile Table Stack
// /public/js/responsive/responsive-tables.js
class ResponsiveTables {
constructor() {
this.tables = document.querySelectorAll('.table-responsive-mobile');
this.init();
}
init() {
this.tables.forEach(tableContainer => {
this.enhanceTable(tableContainer);
});
// Re-initialize on breakpoint change
document.addEventListener('netgescon:breakpoint-change', (e) => {
if (e.detail.breakpoint === 'xs') {
this.createMobileViews();
} else if (e.detail.breakpoint !== 'xs') {
this.removeMobileViews();
}
});
}
enhanceTable(container) {
const table = container.querySelector('table');
if (!table) return;
// Create mobile stack version
this.createMobileStack(container, table);
// Add horizontal scroll indicators
this.addScrollIndicators(container);
}
createMobileStack(container, table) {
const mobileStack = document.createElement('div');
mobileStack.className = 'mobile-table-stack';
const headers = Array.from(table.querySelectorAll('thead th')).map(th => th.textContent.trim());
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const cells = row.querySelectorAll('td');
const mobileItem = document.createElement('div');
mobileItem.className = 'mobile-table-item';
// Create header if there's a primary column
const primaryCell = cells[0];
if (primaryCell) {
const itemHeader = document.createElement('div');
itemHeader.className = 'item-header';
itemHeader.textContent = primaryCell.textContent.trim();
mobileItem.appendChild(itemHeader);
}
// Create rows for each cell
cells.forEach((cell, index) => {
if (index === 0) return; // Skip primary cell
const itemRow = document.createElement('div');
itemRow.className = 'item-row';
const label = document.createElement('div');
label.className = 'item-label';
label.textContent = headers[index] || `Campo ${index}`;
const value = document.createElement('div');
value.className = 'item-value';
value.innerHTML = cell.innerHTML;
itemRow.appendChild(label);
itemRow.appendChild(value);
mobileItem.appendChild(itemRow);
});
mobileStack.appendChild(mobileItem);
});
container.appendChild(mobileStack);
}
addScrollIndicators(container) {
const table = container.querySelector('table');
if (!table) return;
const indicator = document.createElement('div');
indicator.className = 'scroll-indicator';
indicator.innerHTML = '<i class="fas fa-arrow-right"></i> Scorri per vedere più colonne';
container.appendChild(indicator);
// Hide indicator when scrolled to end
container.addEventListener('scroll', () => {
const isScrolledToEnd = container.scrollLeft >= (container.scrollWidth - container.clientWidth - 10);
indicator.style.opacity = isScrolledToEnd ? '0' : '1';
});
}
createMobileViews() {
this.tables.forEach(container => {
container.classList.add('mobile-view');
});
}
removeMobileViews() {
this.tables.forEach(container => {
container.classList.remove('mobile-view');
});
}
}
// Initialize responsive tables
document.addEventListener('DOMContentLoaded', () => {
new ResponsiveTables();
});
🎨 RESPONSIVE COMPONENTS
📱 Responsive Cards
<!-- resources/views/components/responsive-card.blade.php -->
@props([
'title' => '',
'mobileTitle' => null,
'content' => '',
'actions' => [],
'mobileLayout' => 'stack'
])
<div class="netgescon-card responsive-card" data-mobile-layout="{{ $mobileLayout }}">
<!-- Desktop/Tablet Header -->
<div class="card-header tablet-up">
<h3 class="card-title">{{ $title }}</h3>
@if(count($actions) > 0)
<div class="card-actions">
@foreach($actions as $action)
<a href="{{ $action['url'] }}" class="btn btn-{{ $action['variant'] ?? 'primary' }} btn-sm">
@if(isset($action['icon']))
<i class="{{ $action['icon'] }} mr-2"></i>
@endif
{{ $action['text'] }}
</a>
@endforeach
</div>
@endif
</div>
<!-- Mobile Header -->
<div class="card-header mobile-only">
<h4 class="card-title-mobile">{{ $mobileTitle ?? $title }}</h4>
@if(count($actions) > 0)
<div class="dropdown">
<button class="btn btn-ghost btn-sm dropdown-toggle" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu">
@foreach($actions as $action)
<li>
<a class="dropdown-item" href="{{ $action['url'] }}">
@if(isset($action['icon']))
<i class="{{ $action['icon'] }} mr-2"></i>
@endif
{{ $action['text'] }}
</a>
</li>
@endforeach
</ul>
</div>
@endif
</div>
<!-- Content -->
<div class="card-content">
{!! $content !!}
</div>
</div>
📱 Responsive Form
<!-- resources/views/components/responsive-form.blade.php -->
@props([
'action' => '',
'method' => 'POST',
'columns' => 1,
'mobileColumns' => 1
])
<form action="{{ $action }}" method="{{ $method }}"
class="netgescon-form responsive-form"
data-columns="{{ $columns }}"
data-mobile-columns="{{ $mobileColumns }}">
@csrf
@if($method !== 'GET' && $method !== 'POST')
@method($method)
@endif
<div class="form-grid
lg:grid-cols-{{ $columns }}
grid-cols-{{ $mobileColumns }}">
{{ $slot }}
</div>
<!-- Form Actions -->
<div class="form-actions">
{{ $actions ?? '' }}
</div>
</form>
<style>
.form-grid {
display: grid;
gap: 1rem;
}
@media (min-width: 768px) {
.form-grid {
gap: 1.5rem;
}
}
.form-actions {
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--netgescon-gray-200);
display: flex;
gap: 1rem;
justify-content: flex-end;
}
@media (max-width: 767px) {
.form-actions {
flex-direction: column;
}
.form-actions .btn {
width: 100%;
justify-content: center;
}
}
</style>
🧪 TESTING RESPONSIVO
✅ Device Testing Checklist
// /public/js/testing/responsive-tests.js
class ResponsiveTests {
constructor() {
this.testResults = new Map();
this.deviceSizes = {
'iPhone SE': { width: 375, height: 667 },
'iPhone 12': { width: 390, height: 844 },
'iPad': { width: 768, height: 1024 },
'iPad Pro': { width: 1024, height: 1366 },
'Desktop HD': { width: 1920, height: 1080 },
'Desktop 4K': { width: 3840, height: 2160 }
};
this.runTests();
}
runTests() {
console.log('🧪 Running Responsive Tests...');
Object.entries(this.deviceSizes).forEach(([device, size]) => {
this.testDevice(device, size);
});
this.generateReport();
}
testDevice(deviceName, size) {
console.log(`📱 Testing ${deviceName} (${size.width}x${size.height})`);
// Simulate device size
this.setViewportSize(size.width, size.height);
const tests = [
() => this.testLayout(),
() => this.testNavigation(),
() => this.testForms(),
() => this.testTables(),
() => this.testCards(),
() => this.testButtons(),
() => this.testImages(),
() => this.testText()
];
const results = tests.map(test => {
try {
return test();
} catch (error) {
return { passed: false, error: error.message };
}
});
this.testResults.set(deviceName, results);
}
setViewportSize(width, height) {
// This would work in a testing environment
// In browser, we can only simulate with CSS
document.documentElement.style.setProperty('--test-width', `${width}px`);
document.documentElement.style.setProperty('--test-height', `${height}px`);
}
testLayout() {
const layout = document.querySelector('.netgescon-layout');
const sidebar = document.querySelector('.netgescon-sidebar');
const content = document.querySelector('.netgescon-content');
const tests = [
layout !== null,
sidebar !== null,
content !== null,
!this.hasHorizontalScrollbar(document.body),
this.isContentVisible(content)
];
return {
name: 'Layout',
passed: tests.every(Boolean),
details: {
hasLayout: tests[0],
hasSidebar: tests[1],
hasContent: tests[2],
noHorizontalScroll: tests[3],
contentVisible: tests[4]
}
};
}
testNavigation() {
const navItems = document.querySelectorAll('.nav-link');
const mobileToggle = document.querySelector('.mobile-nav-toggle');
const tests = [
navItems.length > 0,
window.innerWidth < 768 ? mobileToggle !== null : true,
this.areElementsTouchFriendly(navItems)
];
return {
name: 'Navigation',
passed: tests.every(Boolean),
details: {
hasNavItems: tests[0],
hasMobileToggle: tests[1],
touchFriendly: tests[2]
}
};
}
testForms() {
const inputs = document.querySelectorAll('.form-input');
const buttons = document.querySelectorAll('.btn');
const tests = [
this.areElementsTouchFriendly(inputs),
this.areElementsTouchFriendly(buttons),
this.hasProperFontSize(inputs)
];
return {
name: 'Forms',
passed: tests.every(Boolean),
details: {
inputsTouchFriendly: tests[0],
buttonsTouchFriendly: tests[1],
properFontSize: tests[2]
}
};
}
testTables() {
const tables = document.querySelectorAll('.table-responsive-mobile');
if (tables.length === 0) {
return { name: 'Tables', passed: true, details: { noTables: true } };
}
const tests = [
!this.hasHorizontalScrollbar(tables[0]),
window.innerWidth < 576 ? this.hasMobileTableVersion(tables[0]) : true
];
return {
name: 'Tables',
passed: tests.every(Boolean),
details: {
noHorizontalScroll: tests[0],
hasMobileVersion: tests[1]
}
};
}
testCards() {
const cards = document.querySelectorAll('.netgescon-card');
const tests = [
cards.length > 0,
this.areCardsResponsive(cards)
];
return {
name: 'Cards',
passed: tests.every(Boolean),
details: {
hasCards: tests[0],
responsive: tests[1]
}
};
}
testButtons() {
const buttons = document.querySelectorAll('.btn');
const tests = [
this.areElementsTouchFriendly(buttons),
this.haveProperSpacing(buttons)
];
return {
name: 'Buttons',
passed: tests.every(Boolean),
details: {
touchFriendly: tests[0],
properSpacing: tests[1]
}
};
}
testImages() {
const images = document.querySelectorAll('img');
const tests = [
this.areImagesResponsive(images),
this.haveAltText(images)
];
return {
name: 'Images',
passed: tests.every(Boolean),
details: {
responsive: tests[0],
hasAltText: tests[1]
}
};
}
testText() {
const textElements = document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, span');
const tests = [
this.hasReadableSize(textElements),
this.hasProperLineHeight(textElements)
];
return {
name: 'Text',
passed: tests.every(Boolean),
details: {
readableSize: tests[0],
properLineHeight: tests[1]
}
};
}
// Helper methods
hasHorizontalScrollbar(element) {
return element.scrollWidth > element.clientWidth;
}
isContentVisible(element) {
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
areElementsTouchFriendly(elements) {
return Array.from(elements).every(el => {
const rect = el.getBoundingClientRect();
return rect.height >= 44 && rect.width >= 44;
});
}
hasProperFontSize(elements) {
return Array.from(elements).every(el => {
const style = getComputedStyle(el);
const fontSize = parseFloat(style.fontSize);
return fontSize >= 16; // Prevent zoom on iOS
});
}
hasMobileTableVersion(tableContainer) {
return tableContainer.querySelector('.mobile-table-stack') !== null;
}
areCardsResponsive(cards) {
return Array.from(cards).every(card => {
const rect = card.getBoundingClientRect();
return rect.width <= window.innerWidth;
});
}
haveProperSpacing(elements) {
// Check if buttons have adequate spacing between them
const buttons = Array.from(elements);
for (let i = 0; i < buttons.length - 1; i++) {
const current = buttons[i].getBoundingClientRect();
const next = buttons[i + 1].getBoundingClientRect();
const spacing = Math.abs(next.left - current.right);
if (spacing < 8 && spacing > 0) return false;
}
return true;
}
areImagesResponsive(images) {
return Array.from(images).every(img => {
const style = getComputedStyle(img);
return style.maxWidth === '100%' || style.width === '100%';
});
}
haveAltText(images) {
return Array.from(images).every(img => img.hasAttribute('alt'));
}
hasReadableSize(elements) {
return Array.from(elements).every(el => {
const style = getComputedStyle(el);
const fontSize = parseFloat(style.fontSize);
return fontSize >= 14;
});
}
hasProperLineHeight(elements) {
return Array.from(elements).every(el => {
const style = getComputedStyle(el);
const lineHeight = parseFloat(style.lineHeight);
const fontSize = parseFloat(style.fontSize);
return lineHeight / fontSize >= 1.2;
});
}
generateReport() {
console.log('\n📊 Responsive Test Report');
console.log('=========================');
this.testResults.forEach((results, device) => {
console.log(`\n📱 ${device}:`);
results.forEach(test => {
const status = test.passed ? '✅' : '❌';
console.log(` ${status} ${test.name}`);
if (!test.passed && test.details) {
Object.entries(test.details).forEach(([key, value]) => {
if (!value) {
console.log(` ⚠️ ${key}: failed`);
}
});
}
});
});
// Overall summary
const totalTests = Array.from(this.testResults.values()).flat().length;
const passedTests = Array.from(this.testResults.values()).flat().filter(t => t.passed).length;
const passRate = Math.round((passedTests / totalTests) * 100);
console.log(`\n📈 Overall Pass Rate: ${passRate}% (${passedTests}/${totalTests})`);
}
}
// Run tests in development
if (NetGesconConfig.get('app.debug')) {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
new ResponsiveTests();
}, 2000);
});
}
📚 FILES CORRELATI
📱 Responsive CSS Files
/public/css/responsive/(directory)/public/css/responsive/breakpoints.css/public/css/responsive/mobile.css/public/css/responsive/tablet.css/public/css/responsive/desktop.css/public/css/responsive/touch.css
⚡ Responsive JavaScript Files
/public/js/responsive/(directory)/public/js/responsive/breakpoints.js/public/js/responsive/mobile-nav.js/public/js/responsive/responsive-tables.js/public/js/testing/responsive-tests.js
🧩 Responsive Components
/resources/views/components/responsive-card.blade.php/resources/views/components/responsive-form.blade.php/resources/views/components/responsive-table.blade.php
🧪 TESTING CHECKLIST
✅ Mobile Testing (320px - 767px)
- Navigation funzionale
- Touch targets ≥ 44px
- No horizontal scroll
- Font size ≥ 16px
- Tables responsive o stack
- Forms easy to use
- Content leggibile
✅ Tablet Testing (768px - 1023px)
- Layout a 2 colonne
- Sidebar collassabile
- Cards in griglia 2x2
- Forms a 2 colonne
- Navigation persistente
✅ Desktop Testing (1024px+)
- Layout a 3 colonne
- Sidebar fissa
- Cards in griglia 4x1
- Hover effects
- Tooltips funzionali
🎯 NEXT STEPS
🔮 Responsive Future
- Container Queries - Layout basato su container size
- Advanced Grid - CSS Grid subgrid support
- Dynamic Viewports - Support per viewport dinamici
- Foldable Devices - Support per schermi pieghevoli
- AR/VR Integration - Responsive per dispositivi immersivi
📝 Documento mantenuto da: GitHub Copilot AI Assistant
🔗 Componente: Responsive System NetGescon Interface v2.1.0