1464 lines
38 KiB
Markdown
Executable File
1464 lines
38 KiB
Markdown
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**
|
|
```css
|
|
/* /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**
|
|
```javascript
|
|
// /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)**
|
|
```css
|
|
/* /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)**
|
|
```css
|
|
/* /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+)**
|
|
```css
|
|
/* /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**
|
|
```css
|
|
/* /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**
|
|
```javascript
|
|
// /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**
|
|
```javascript
|
|
// /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**
|
|
```blade
|
|
<!-- 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**
|
|
```blade
|
|
<!-- 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**
|
|
```javascript
|
|
// /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*
|