netgescon-day0/docs/00-MASTER-DOCS-UNIFICATI/03-MANUALI-OPERATIVI/08-RESPONSIVE.md

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