refactor[ frontend / components ]: aggiornamento UI globale, ottimizzazione header e gestione errori
Header: Rifatto il componente con menu mobile ed effetti di background. Cleanup: Rimossi i componenti Hero e Services non più necessari. Layout: Migliorata la struttura e lo stile della pagina Contacts. Core: Implementata la gestione globale degli errori in main.js. Styles: Aggiornati gli stili globali e le media queries per una migliore responsività.
This commit is contained in:
@@ -1,20 +1,24 @@
|
|||||||
# Cima Progetti - Sito Aziendale
|
# Cima Progetti - Sito Aziendale
|
||||||
|
|
||||||
Sito web moderno e responsivo sviluppato con **Svelte + Vite** per presentare servizi, metodologia e contatti aziendali.
|
Sito web moderno, performante e accessibile sviluppato con **Svelte 5 + Vite** per presentare servizi, metodologia e contatti aziendali. Completamente responsivo con supporto completo per telefoni e tablet.
|
||||||
|
|
||||||
## ✨ Funzionalità principali
|
## ✨ Funzionalità principali
|
||||||
|
|
||||||
- 🌓 **Tema light/dark** con persistenza locale
|
- 🌓 **Tema light/dark** con persistenza dual-storage (localStorage + cookie backup)
|
||||||
- 📱 **Design completamente responsivo**
|
- 📱 **Design completamente responsivo** con breakpoint intelligenti
|
||||||
- ⬆️ **Navbar collapsibile** con hide/show al scroll
|
- ⬆️ **Header smart** — trasparente all'inizio, sfondo blur opaco dopo scroll
|
||||||
- 🎨 **Sistema di componenti riusabili**
|
- 🍔 **Menu hamburger** con animazione fluida di apertura/chiusura
|
||||||
- 🚀 **Routing client-side** con pagine dedicate e fallback 404
|
- 🎨 **Sistema di componenti riusabili** e theme-aware
|
||||||
- ♿ **Accessibilità** e buone pratiche web
|
- 🚀 **Routing client-side** con History API e fallback 404 per pagine non create
|
||||||
|
- 🔐 **Cookie consent** con personalizzazione categorie
|
||||||
|
- ♿ **Accessibilità** — focus-visible preservato, tap highlight rimosso su touch
|
||||||
|
- ⚡ **Svelte 5 Runes** — reattività state-driven con $state(), $derived()
|
||||||
|
- 🐛 **Global error handling** — trap di SyntaxError visibile su dev server
|
||||||
|
|
||||||
## 🛠️ Requisiti
|
## 🛠️ Requisiti
|
||||||
|
|
||||||
- `Node.js` 18+ (consigliato LTS)
|
- **Node.js** 18+ (consigliato LTS)
|
||||||
- `npm` 9+
|
- **npm** 9+
|
||||||
|
|
||||||
## 🚀 Avvio rapido
|
## 🚀 Avvio rapido
|
||||||
|
|
||||||
@@ -23,76 +27,201 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Applicazione disponibile su `http://localhost:5173`.
|
Applicazione disponibile su `http://localhost:5173` (accesso locale) e `http://192.168.XX.XXX:5173` (rete locale).
|
||||||
|
|
||||||
## 📝 Script disponibili
|
## 📝 Script disponibili
|
||||||
|
|
||||||
- `npm run dev` - avvia il server di sviluppo con hot reload
|
| Script | Descrizione |
|
||||||
- `npm run build` - genera la build di produzione
|
|--------|-------------|
|
||||||
- `npm run preview` - avvia un'anteprima della build
|
| `npm run dev` | Avvia il server di sviluppo con hot reload (esposto su 0.0.0.0:5173) |
|
||||||
|
| `npm run build` | Genera la build di produzione ottimizzata in `dist/` |
|
||||||
|
| `npm run preview` | Avvia preview della build (simula produzione in locale) |
|
||||||
|
|
||||||
## 📁 Struttura del progetto
|
## 📁 Struttura del progetto
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
cimaprogetti/
|
||||||
├── App.svelte # Componente root con routing
|
├── index.html # Template HTML entry point
|
||||||
├── main.js # Punto di ingresso
|
├── package.json # Dependencies e scripts
|
||||||
├── styles/
|
├── vite.config.js # Configurazione Vite (SPA + host 0.0.0.0)
|
||||||
│ ├── global.css # Stili globali e variabili tema
|
├── svelte.config.js # Configurazione Svelte
|
||||||
│ ├── App.css # Layout principale app
|
│
|
||||||
│ └── error-pages.css # Stili pagine errore
|
├── src/
|
||||||
├── components/
|
│ ├── main.js # Entry point con error trap globale
|
||||||
│ ├── Header/ # Navbar con scroll hide/show
|
│ ├── App.svelte # Root component con routing e layout
|
||||||
│ ├── Hero/ # Sezione hero principale
|
│ │
|
||||||
│ ├── Services/ # Griglia servizi
|
│ ├── components/
|
||||||
│ ├── Footer/ # Footer con info aziendali
|
│ │ ├── Header/
|
||||||
│ ├── Button/ # Componente button riusabile
|
│ │ │ ├── Header.svelte # Navbar sticky con tema + scroll detection
|
||||||
│ └── ThemeToggle/ # Toggle tema light/dark
|
│ │ │ └── Header.css # Stili header (blur backdrop, absolute menu)
|
||||||
└── pages/
|
│ │ │
|
||||||
├── Contacts.svelte # Pagina contatti
|
│ │ ├── Button/
|
||||||
├── 403.svelte # Accesso negato
|
│ │ │ ├── Button.svelte # Button/link riusabile (16 prop)
|
||||||
├── 404.svelte # Pagina non trovata
|
│ │ │ └── Button.css # Stili + focus-visible + no tap highlight
|
||||||
├── 500.svelte # Errore server
|
│ │ │
|
||||||
└── ErrorGeneric.svelte # Errore generico
|
│ │ ├── CookiePopUp/
|
||||||
|
│ │ │ ├── CookiePopUp.svelte # Cookie consent con toggle personalizzato
|
||||||
|
│ │ │ ├── CookiePopUp.css # Dialog mobile + desktop
|
||||||
|
│ │ │ └── assets/ # SVG cookie icon
|
||||||
|
│ │ │
|
||||||
|
│ │ ├── Toggle/
|
||||||
|
│ │ │ ├── Toggle.svelte # Switch accessibile
|
||||||
|
│ │ │ └── Toggle.css # Stili toggle (animated knob)
|
||||||
|
│ │ │
|
||||||
|
│ │ ├── ThemeToggle/
|
||||||
|
│ │ │ ├── ThemeToggle.svelte
|
||||||
|
│ │ │ └── ThemeToggle.css
|
||||||
|
│ │ │
|
||||||
|
│ │ ├── Footer/
|
||||||
|
│ │ │ ├── Footer.svelte
|
||||||
|
│ │ │ └── Footer.css
|
||||||
|
│ │ │
|
||||||
|
│ │ └── Hero/, Services/ # Componenti home
|
||||||
|
│ │
|
||||||
|
│ ├── pages/
|
||||||
|
│ │ ├── index.svelte # Home page
|
||||||
|
│ │ ├── Contacts.svelte # Pagina contatti con team profiles
|
||||||
|
│ │ ├── 403.svelte, 404.svelte, 500.svelte, errore.svelte # Error pages
|
||||||
|
│ │
|
||||||
|
│ ├── lib/
|
||||||
|
│ │ ├── theme.js # Theme persistence (localStorage + cookie)
|
||||||
|
│ │ ├── navigation.js # Client-side routing utilities
|
||||||
|
│ │ ├── cookieConsent.js # Cookie storage & consent management
|
||||||
|
│ │ └── errorRouting.js # Path normalization & aliases
|
||||||
|
│ │
|
||||||
|
│ └── styles/
|
||||||
|
│ ├── global.css # CSS variables, typography, resets
|
||||||
|
│ ├── App.css # Layout principale
|
||||||
|
│ ├── contacts.css # Pagina contatti (flex + absolute positioning)
|
||||||
|
│ └── error-pages.css # Pagine errore (clamp typography)
|
||||||
|
│
|
||||||
|
└── public/
|
||||||
|
└── images/
|
||||||
|
├── background/ # Background patterns
|
||||||
|
├── icons/ # Logo, UI icons
|
||||||
|
└── contacts/ # Hero images (light/dark theme)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🌐 Configurazione Network
|
||||||
|
|
||||||
|
Il server Vite è configurato per essere **accessibile dalla rete locale**:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// vite.config.js
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0', // Ascolta su tutte le interfacce
|
||||||
|
port: 5173,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Accesso dal telefono sulla stessa Wi-Fi
|
||||||
|
|
||||||
|
1. Avvia il dev server: `npm run dev`
|
||||||
|
2. Nel terminale vedrai l'indirizzo IP locale (es. `192.168.68.119`)
|
||||||
|
3. Dal telefono apri: `http://192.168.68.119:5173`
|
||||||
|
|
||||||
|
**Nota:** Per produzione, usa `npm run build && npm run preview` per servire la versione compilata su `http://192.168.68.119:4173` (compatibile con browser vecchi).
|
||||||
|
|
||||||
## 🗺️ Routing
|
## 🗺️ Routing
|
||||||
|
|
||||||
Il routing è gestito in `src/App.svelte` leggendo `window.location.pathname` e reagendo ai cambi di History API.
|
Il routing è **client-side only** — nessuna pagina server. Gestito in `App.svelte` leggendo `window.location.pathname` e reagendo ai cambi di History API.
|
||||||
|
|
||||||
| Route | Componente | Descrizione |
|
### Tabella rotte
|
||||||
|-------|-----------|-------------|
|
|
||||||
| `/` | `Hero` + `Services` | Home page |
|
|
||||||
| `/contatti`, `/contacts` | `Contacts` | Pagina contatti |
|
|
||||||
| `/403` | `Forbidden` | Accesso negato |
|
|
||||||
| `/404` | `NotFound` | Pagina non trovata |
|
|
||||||
| `/500` | `ServerError` | Errore server |
|
|
||||||
| `/errore` | `ErrorGeneric` | Errore generico |
|
|
||||||
| qualsiasi altra route | `NotFound` | Fallback 404 per pagine non ancora create |
|
|
||||||
|
|
||||||
Le voci della navbar puntano a route reali come `/servizi`, `/metodo`, `/progetti` e `/chi-siamo`: se una pagina non esiste ancora, il contenuto mostrato è il 404 ma l'URL resta quello richiesto.
|
| Route | Pagina | Comportamento |
|
||||||
|
|-------|--------|---------------|
|
||||||
|
| `/` | `index.svelte` | Home page (Hero + Services) |
|
||||||
|
| `/contatti` o `/contacts` | `Contacts.svelte` | Info contatti + team profiles |
|
||||||
|
| `/servizi`, `/metodo`, `/progetti`, `/chi-siamo` | `404.svelte` | Non create — mostra 404 ma mantiene URL |
|
||||||
|
| `/403`, `/404`, `/500`, `/errore` | Error pages | Pagine errore dedicate |
|
||||||
|
| Qualsiasi altra rotta | `404.svelte` | Fallback automatico |
|
||||||
|
|
||||||
## 🎨 Tema e Stili
|
### Meccanismo di routing
|
||||||
|
|
||||||
### Variabili CSS globali
|
```js
|
||||||
|
// src/App.svelte
|
||||||
|
const getRoute = (path) => {
|
||||||
|
const normalized = resolvePathname(path) // Normalizza trailing slash
|
||||||
|
if (normalized === '/') return 'home'
|
||||||
|
if (normalized === '/contatti' || normalized === '/contacts') return 'contacts'
|
||||||
|
// ...
|
||||||
|
return '404' // Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentRoute = $derived(getRoute(pathname))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Come aggiungere una nuova pagina
|
||||||
|
|
||||||
|
1. **Crea il componente** in `src/pages/MyPage.svelte`
|
||||||
|
2. **Importa in App.svelte**:
|
||||||
|
```svelte
|
||||||
|
import MyPage from "./pages/MyPage.svelte"
|
||||||
|
```
|
||||||
|
3. **Aggiungi la route** in `getRoute()`:
|
||||||
|
```js
|
||||||
|
if (normalized === '/my-page') return 'mypage'
|
||||||
|
```
|
||||||
|
4. **Renderizza nel template**:
|
||||||
|
```svelte
|
||||||
|
{:else if currentRoute === 'mypage'}
|
||||||
|
<MyPage />
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Sistema di Tema
|
||||||
|
|
||||||
|
### Tema Persistente Dual-Storage
|
||||||
|
|
||||||
|
Il tema viene salvato su **localStorage** (primario) e **cookie** (backup) simultaneamente. Questo assicura che il tema sopravviva anche se l'utente cancella parte dello storage del browser.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// src/lib/theme.js
|
||||||
|
|
||||||
|
export const getSavedTheme = () => {
|
||||||
|
// Prova localStorage prima, poi cookie come fallback
|
||||||
|
return localStorage.getItem('theme') || getCookie('theme')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const saveTheme = (theme) => {
|
||||||
|
localStorage.setItem('theme', theme)
|
||||||
|
setCookie('theme', theme) // Backup redundante
|
||||||
|
}
|
||||||
|
|
||||||
|
export const applyTheme = (theme) => {
|
||||||
|
document.documentElement.setAttribute('data-theme', theme)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variabili CSS Globali
|
||||||
|
|
||||||
Tutte le variabili tema sono in `src/styles/global.css`:
|
Tutte le variabili tema sono in `src/styles/global.css`:
|
||||||
|
|
||||||
```css
|
```css
|
||||||
:root {
|
:root {
|
||||||
--primary-color: #1343F0; /* Colore primario */
|
--navbar-height: 140px;
|
||||||
--text-color: #111827; /* Testo light mode */
|
--primary-color: #1343F0; /* Blu Cima */
|
||||||
--surface: #ffffff; /* Sfondo light mode */
|
--text-color: #111827; /* Grigio scuro */
|
||||||
|
--surface: #ffffff; /* Background carta */
|
||||||
--background: rgba(246, 249, 255, 0.5);
|
--background: rgba(246, 249, 255, 0.5);
|
||||||
/* ... altre variabili */
|
--background-opaque: #00000033;
|
||||||
|
--muted-color: #6b7280;
|
||||||
|
--lateral-margin: 12rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme='dark'] {
|
:root[data-theme='dark'] {
|
||||||
--primary-color: #4983F2;
|
--primary-color: #4983F2; /* Blu chiaro */
|
||||||
--text-color: #e6eef8;
|
--text-color: #e6eef8; /* Grigio molto chiaro */
|
||||||
--surface: #000;
|
--surface: #000;
|
||||||
--background: rgba(0, 0, 0, 0.8);
|
--background: rgba(0, 0, 0, 0.8);
|
||||||
/* ... altre variabili */
|
--background-opaque: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile breakpoint */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
:root {
|
||||||
|
--navbar-height: 110px;
|
||||||
|
--lateral-margin: 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -100,126 +229,362 @@ Tutte le variabili tema sono in `src/styles/global.css`:
|
|||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<div style="color: var(--text-color); background: var(--surface);">
|
<div style="color: var(--text-color); background: var(--surface);">
|
||||||
Contenuto che segue il tema
|
Contenuto automaticamente theme-aware
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🧩 Componenti principali
|
## 🧩 Componenti Principali
|
||||||
|
|
||||||
### Button
|
### Header (Navbar Intelligente)
|
||||||
|
|
||||||
Componente button/link riusabile con tema integrato.
|
La navbar è **sticky** e reagisce allo scroll con una transizione elegante:
|
||||||
|
|
||||||
|
- **Trasparente all'inizio** (`background-color: transparent`)
|
||||||
|
- **Sfondo blur dopo 100px di scroll** (`backdrop-filter: blur(8px)`)
|
||||||
|
- **Scompare al scroll verso il basso** (nascosto con `translateY(-100%)`)
|
||||||
|
- **Riappare al scroll verso l'alto** (velocità rilevata)
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<!-- Button base -->
|
<Header />
|
||||||
<Button text="Click me" />
|
```
|
||||||
|
|
||||||
<!-- Button con link -->
|
**Modifiche in Header.svelte:**
|
||||||
<Button text="HOME" href="/" />
|
- Stato `hasBackground` che attiva a `scrollY > 100`
|
||||||
|
- Classe `with-bg` per applicare blur e shadow
|
||||||
|
- Mobile menu hamburger con animazione da basso
|
||||||
|
|
||||||
<!-- Button con bordo interno -->
|
**CSS Features:**
|
||||||
|
- `backdrop-filter: blur(8px)` per effetto frosted glass
|
||||||
|
- `position: fixed` con `transition: transform 0.28s`
|
||||||
|
- Menu mobile `transform: translateY(100%)` → `translateY(0)` con cubic-bezier smoothing
|
||||||
|
|
||||||
|
### Button (Componente Riusabile)
|
||||||
|
|
||||||
|
Componente ultra-flessibile che renderizza come `<a>` se ha `href`, altrimenti `<button>`.
|
||||||
|
|
||||||
|
```svelte
|
||||||
<Button
|
<Button
|
||||||
text="Custom Button"
|
text="PRENOTA UNA CALL"
|
||||||
href="/"
|
href="/contatti"
|
||||||
borderWidth="2px"
|
borderWidth="1.5px"
|
||||||
/>
|
borderColor="var(--text-color)"
|
||||||
|
color="transparent"
|
||||||
<!-- Stili avanzati -->
|
textColor="var(--text-color)"
|
||||||
<Button
|
|
||||||
text="Premium"
|
|
||||||
color="var(--primary-color)"
|
|
||||||
textColor="var(--surface)"
|
|
||||||
padding="12px 32px"
|
padding="12px 32px"
|
||||||
round="12px"
|
round="5px"
|
||||||
borderWidth="1px"
|
margin="1rem 0 0"
|
||||||
|
bold
|
||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Prop disponibili:**
|
**Tutte le prop:**
|
||||||
- `text` - testo del button (default: `'Button'`)
|
|
||||||
- `color` - colore sfondo (default: `var(--primary-color)`)
|
|
||||||
- `textColor` - colore testo (default: `var(--surface)`)
|
|
||||||
- `padding` - padding interno (default: `'8px 24px'`)
|
|
||||||
- `round` - border-radius (default: `'8px'`)
|
|
||||||
- `href` - se presente, renderizza come `<a>` tag
|
|
||||||
- `borderWidth` - spessore bordo interno (default: `'0px'`)
|
|
||||||
|
|
||||||
Se `href` punta a una route interna, il click usa la navigazione client-side e aggiorna il contenuto senza ricaricare la pagina.
|
| Prop | Default | Descrizione |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `text` | `'Button'` | Testo del pulsante |
|
||||||
|
| `color` | `var(--primary-color)` | Background |
|
||||||
|
| `textColor` | `var(--surface)` | Colore testo |
|
||||||
|
| `padding` | `'10px 20px'` | Padding interno |
|
||||||
|
| `round` | `'5px'` | border-radius |
|
||||||
|
| `href` | — | Se presente, renderizza come `<a>` con navigazione client-side |
|
||||||
|
| `borderWidth` | `'0px'` | Spessore border interno (inset) |
|
||||||
|
| `borderColor` | `var(--primary-color)` | Colore border |
|
||||||
|
| `bold` | `true` | Font-weight bold |
|
||||||
|
| `margin` | `'0'` | **NEW** — margini esterni |
|
||||||
|
|
||||||
### Header
|
**Accessibilità:**
|
||||||
|
- Outline su `:focus-visible` (tastiera)
|
||||||
|
- Tap highlight rimosso su touch (`:focus:not(:focus-visible)`)
|
||||||
|
|
||||||
Navbar sticky che si nasconde al scroll verso il basso e riappare al scroll verso l'alto.
|
### CookiePopUp (Cookie Consent Dialog)
|
||||||
|
|
||||||
Le voci della navbar usano route interne. Le pagine non ancora create devono restituire 404.
|
Dialog modale con due view:
|
||||||
|
|
||||||
Modifica il contenuto in `src/components/Header/Header.svelte`.
|
1. **Main** — Descrizione generica + bottoni accetta/rifiuta/personalizza
|
||||||
|
2. **Prefs** — Toggle per tre categorie: Tecnici (always on), Analitici, Profilazione
|
||||||
### ThemeToggle
|
|
||||||
|
|
||||||
Pulsante per switchare tra tema light e dark. Il tema viene salvato in `localStorage` e come backup in cookie, così resta disponibile anche dopo operazioni che puliscono parte dello storage del browser.
|
|
||||||
|
|
||||||
## 📱 Modificare i contenuti
|
|
||||||
|
|
||||||
- **Header/Navigazione:** `src/components/Header/Header.svelte`
|
|
||||||
- **Routing e redirect:** `src/App.svelte`, `src/lib/navigation.js`, `src/lib/errorRouting.js`
|
|
||||||
- **Sezione hero:** `src/components/Hero/Hero.svelte`
|
|
||||||
- **Servizi:** `src/components/Services/Services.svelte`
|
|
||||||
- **Footer:** `src/components/Footer/Footer.svelte`
|
|
||||||
- **Pagine errore:** `src/pages/*.svelte`
|
|
||||||
|
|
||||||
## ➕ Aggiungere nuove pagine
|
|
||||||
|
|
||||||
1. Crea un nuovo file `.svelte` in `src/pages/`
|
|
||||||
2. Aggiungi una route in `src/App.svelte` nello script:
|
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
import MyPage from "./pages/MyPage.svelte"
|
<CookiePopUp />
|
||||||
|
```
|
||||||
|
|
||||||
const getRoute = (path) => {
|
**Varianti layout:**
|
||||||
// ...
|
- **Desktop** (> 900px): Fixed al bottom, full-width
|
||||||
if (normalized === '/my-page') return 'mypage'
|
- **Tablet** (768–900px): Drawer da destra (clip-path animation)
|
||||||
// ...
|
- **Mobile** (< 768px): Full-page dal basso (translateY animation)
|
||||||
|
|
||||||
|
**Accessibilità:**
|
||||||
|
- `role="dialog"` + `aria-modal="true"`
|
||||||
|
- Focus trap — primo elemento focusabile automaticamente
|
||||||
|
- Escape key per chiudere (con reject)
|
||||||
|
- Tap highlight disabilitato
|
||||||
|
|
||||||
|
### Contacts Page (Pagina Contatti)
|
||||||
|
|
||||||
|
Layout two-column con immagine decorativa e team profiles.
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<div class="contact-us">
|
||||||
|
<div class="contact-us-container">
|
||||||
|
<!-- Titolo, testo, email/WhatsApp, button -->
|
||||||
|
</div>
|
||||||
|
<div class="contact-image">
|
||||||
|
<!-- SVG tema-aware (light/dark) -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="behind-cima">
|
||||||
|
<h2>PEOPLE BEHIND CIMA</h2>
|
||||||
|
<div class="profiles-container">
|
||||||
|
<!-- Due profile cards con flex: 1 1 0 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Responsive:**
|
||||||
|
- **Desktop** (> 900px): Due colonne con gap 40px
|
||||||
|
- **Mobile** (< 900px):
|
||||||
|
- `contact-image` diventa `position: absolute` centrata
|
||||||
|
- `contact-us-container` full width
|
||||||
|
- Profili stacked verticalmente
|
||||||
|
|
||||||
|
### Toggle (Accessibile Switch)
|
||||||
|
|
||||||
|
Switch accessibile con label opzionale e stato animato.
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<Toggle
|
||||||
|
bind:checked={analitici}
|
||||||
|
label="Analitici"
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**CSS Variables** (customizzabile):
|
||||||
|
- `--toggle-on-bg` — colore quando ON
|
||||||
|
- `--toggle-off-bg` — colore quando OFF
|
||||||
|
- `--toggle-knob` — colore pallina
|
||||||
|
- `--toggle-width` / `--toggle-height` — dimensioni
|
||||||
|
|
||||||
|
## 📱 Responsive Design
|
||||||
|
|
||||||
|
### Breakpoint Principale
|
||||||
|
|
||||||
|
L'unico breakpoint è **900px** (tablet). Sotto questo valore attivano:
|
||||||
|
|
||||||
|
- Margini orizzontali ridotti: `12rem` → `1.5rem`
|
||||||
|
- Navbar height ridotta: `140px` → `110px`
|
||||||
|
- Menu hamburger (al posto di nav desktop)
|
||||||
|
- Header flexibile trasparente con blur
|
||||||
|
- Contatti layout single-column
|
||||||
|
- Profiles stacked
|
||||||
|
- Error pages con tipografia clamp()
|
||||||
|
|
||||||
|
### Clamp() per Tipografia Fluida
|
||||||
|
|
||||||
|
Errori, heading e font-size critici usano `clamp()` per scalare fluidamente:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Error code number */
|
||||||
|
.error-code {
|
||||||
|
font-size: clamp(2rem, 5vw, 4rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error page title background */
|
||||||
|
.error-text {
|
||||||
|
font-size: clamp(10rem, 20vw, 15rem) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links in contacts */
|
||||||
|
.contact-info-item a {
|
||||||
|
font-size: clamp(1.25rem, 2.5vw, 2rem);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Aggiungi il rendering nel template:
|
## 🔐 Cookie Consent & Privacy
|
||||||
|
|
||||||
```svelte
|
### Gestione Categoria Cookie
|
||||||
{:else if currentRoute === 'mypage'}
|
|
||||||
<MyPage />
|
```js
|
||||||
|
// src/lib/cookieConsent.js
|
||||||
|
|
||||||
|
const consent = {
|
||||||
|
value: 'custom', // 'accepted_all', 'rejected_all', 'custom'
|
||||||
|
preferences: {
|
||||||
|
tecnici: true, // Obbligatorio
|
||||||
|
analitici: false, // Personalizzabile
|
||||||
|
profilazione: false // Personalizzabile
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Se non aggiungi la route in `getRoute`, il path continuerà a mostrare il fallback 404.
|
### Persistenza
|
||||||
|
|
||||||
## 🚀 Build e Deploy
|
- Cookie di consenso salvato in `cookie-consent` (via `js-cookie`)
|
||||||
|
- Configurazione letta al mount di `App.svelte`
|
||||||
|
- Toggle popup non riappare se esiste consenso valido
|
||||||
|
|
||||||
### Build di produzione
|
## 🐛 Error Handling & Debugging
|
||||||
|
|
||||||
|
### Global Error Trap (main.js)
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Cattura SyntaxError e runtime errors prima del mount
|
||||||
|
window.addEventListener('error', (event) => {
|
||||||
|
const msg = `ERROR: ${event.error?.message || event.message}`;
|
||||||
|
console.error(msg, event.error);
|
||||||
|
alert(msg); // Visibile su telefono per debug
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const app = mount(App, { target: document.getElementById('app') })
|
||||||
|
console.log("App mounted successfully!")
|
||||||
|
} catch (e) {
|
||||||
|
alert(`Mount Error: ${e.message}`)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Diagnostica Schermo Bianco
|
||||||
|
|
||||||
|
Se il telefono mostra schermo bianco:
|
||||||
|
|
||||||
|
1. **Apri DevTools** → Console → controlla errori
|
||||||
|
2. **Prova build di produzione**:
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm run preview
|
||||||
|
# Accedi da http://192.168.XX.XXX:4173
|
||||||
|
```
|
||||||
|
3. **Se l'alert appare**, c'è un SyntaxError nei moduli ES
|
||||||
|
4. **Prova altro browser** (Chrome vs Safari su iOS)
|
||||||
|
|
||||||
|
## 🔄 Svelte 5 & Modern JavaScript
|
||||||
|
|
||||||
|
### Svelte 5 Runes
|
||||||
|
|
||||||
|
Il progetto usa **Svelte 5 Runes** per reattività state-driven:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script>
|
||||||
|
let activeNav = $state(window.location.pathname)
|
||||||
|
let isMenuOpen = $state(false)
|
||||||
|
let hasBackground = $state(false)
|
||||||
|
|
||||||
|
let currentRoute = $derived(getRoute(pathname))
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Evitare:**
|
||||||
|
- ~~`on:click`~~ → Usare `onclick={...}`
|
||||||
|
- ~~Stores `writable()`~~ → Usare `$state()`
|
||||||
|
- ~~`reactive`~~ → Usare `$derived()`
|
||||||
|
|
||||||
|
### Rune Disponibili
|
||||||
|
|
||||||
|
- `$state(value)` — Mutabile, reattivo
|
||||||
|
- `$derived(expr)` — Calcolato e cached
|
||||||
|
- `$effect(fn)` — Side effect reattivo
|
||||||
|
- `$props()` — Default props type-safe
|
||||||
|
|
||||||
|
## 🚀 Build & Deploy
|
||||||
|
|
||||||
|
### Build di Produzione
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
Genera i file ottimizzati in `dist/`.
|
Genera file ottimizzati in `dist/`:
|
||||||
|
- `.html` minificato
|
||||||
|
- `.js` bundle (tree-shaken)
|
||||||
|
- `.css` minificato
|
||||||
|
- Source map opzionali
|
||||||
|
|
||||||
### Preview locale
|
### Preview Locale
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run preview
|
npm run preview
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Simula il server di produzione su `http://localhost:4173` — ottimo per testare la build prima di deployare.
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
Il progetto è una **SPA statica** — deployabile su:
|
||||||
|
|
||||||
|
- **Vercel**, **Netlify** (drag & drop `dist/`)
|
||||||
|
- **GitHub Pages** (aggiungere `base: "/repo-name/"` in vite.config.js)
|
||||||
|
- **AWS S3 + CloudFront**
|
||||||
|
- **Qualsiasi server web** (serve `dist/` con fallback 404 → `/index.html`)
|
||||||
|
|
||||||
## 📦 Dipendenze
|
## 📦 Dipendenze
|
||||||
|
|
||||||
- **Svelte** - Framework UI
|
```json
|
||||||
- **Vite** - Build tool e dev server
|
{
|
||||||
- **@sveltejs/vite-plugin-svelte** - Plugin Svelte per Vite
|
"devDependencies": {
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||||
|
"svelte": "^5.55.5",
|
||||||
|
"vite": "^8.0.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 🎓 Tecnologie utilizzate
|
**Zero runtime dependencies** — tutta la logica è vanilla JavaScript.
|
||||||
|
|
||||||
- **Svelte 5** - UI framework reattivo
|
## 🎓 Tecnologie & Approcci
|
||||||
- **Vite** - Build tool moderno e veloce
|
|
||||||
- **CSS3** - Styling con variabili CSS
|
| Aspetto | Soluzione |
|
||||||
- **Vanilla JS** - JavaScript puro
|
|---------|----------|
|
||||||
|
| **Framework UI** | Svelte 5 (Runes + reactivity) |
|
||||||
|
| **Build** | Vite 8 (zero-config, super veloce) |
|
||||||
|
| **CSS** | CSS3 nativo (custom properties + clamp) |
|
||||||
|
| **Routing** | History API (client-side only) |
|
||||||
|
| **Tema** | Data attribute + CSS variables |
|
||||||
|
| **Cookies** | Vanilla JS (no library) |
|
||||||
|
| **Accessibilità** | ARIA roles + focus-visible + keyboard nav |
|
||||||
|
| **Mobile** | Responsive design + touch optimized |
|
||||||
|
|
||||||
|
## 🎯 Convenzioni Codice
|
||||||
|
|
||||||
|
### Naming
|
||||||
|
|
||||||
|
- **Components**: PascalCase (`Header.svelte`, `Button.svelte`)
|
||||||
|
- **Pages**: PascalCase (`Contacts.svelte`, `index.svelte`)
|
||||||
|
- **Styles**: kebab-case (`.contact-us-container`, `.mobile-menu`)
|
||||||
|
- **JS utils**: camelCase (`getSavedTheme`, `navigateTo`)
|
||||||
|
|
||||||
|
### Organizzazione File
|
||||||
|
|
||||||
|
```
|
||||||
|
Component/
|
||||||
|
├── Component.svelte # Logica + template
|
||||||
|
├── Component.css # Stili isolati
|
||||||
|
└── assets/ # SVG, immagini specifiche
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS Classi
|
||||||
|
|
||||||
|
- `.component-name` per root
|
||||||
|
- `.component-name-child` per nested (BEM-like)
|
||||||
|
- Aggiungere media query alla fine del file
|
||||||
|
- Commentare sezioni con `/* ===== SECTION NAME ===== */`
|
||||||
|
|
||||||
|
## 🤝 Contribuire
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
|
||||||
|
1. **Crea branch** per feature: `git checkout -b feature/nome`
|
||||||
|
2. **Test locale**: `npm run dev` + mobile preview
|
||||||
|
3. **Build test**: `npm run build && npm run preview`
|
||||||
|
4. **Commit**: Messaggi chiari (`feat:`, `fix:`, `docs:`)
|
||||||
|
5. **Push** e open PR
|
||||||
|
|
||||||
|
## 🔗 Link Utili
|
||||||
|
|
||||||
|
- [Svelte 5 Docs](https://svelte.dev)
|
||||||
|
- [Vite Docs](https://vitejs.dev)
|
||||||
|
- [Svelte REPL](https://svelte.dev/repl)
|
||||||
|
- [Can I Use](https://caniuse.com) — Compatibilità browser
|
||||||
|
|
||||||
## 📄 Licenza
|
## 📄 Licenza
|
||||||
|
|
||||||
Progetto proprietario © CIMA PROGETTI
|
Progetto proprietario © **CIMA PROGETTI**
|
||||||
@@ -5,6 +5,15 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<title>cimaprogetti</title>
|
<title>cimaprogetti</title>
|
||||||
|
<script>
|
||||||
|
window.onerror = function(message, source, lineno, colno, error) {
|
||||||
|
alert("ERRORE: " + message + "\nRiga: " + lineno + "\nFile: " + source);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<!-- Aggiunto script per console su mobile -->
|
||||||
|
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
|
||||||
|
<script>eruda.init();</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="500" height="657" viewBox="0 0 500 657" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M237.691 383.215C210.065 374.604 33.9083 350.993 24.77 364.701C15.0073 379.342 41.0933 419.975 47.398 432.588" stroke="#4983F2" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M266.485 418.187C280.276 446.048 382.424 631.697 377.575 641.393C296.026 632.097 140.508 637.817 122.48 619.791C111.766 609.077 109.491 586.421 100.88 573.506" stroke="#4983F2" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M147.179 414.075C147.995 411.054 145.059 409.131 144.093 406.875" stroke="#4983F2" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M484.602 120.734C419.985 -69.859 139.011 18.7118 194.691 216.545C224.878 323.792 430.166 374.272 474.772 203.749" stroke="white" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M261.738 184.321C263.054 174.16 263.542 167.971 265.73 158.861" stroke="white" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M320.406 186.233C320.894 177.676 323.036 171.396 325.311 163.131" stroke="white" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M70.0282 463.446C81.7161 465.915 92.559 470.361 101.916 477.845C185.952 545.073 58.7422 568.42 19.6263 490.189C11.989 474.911 13.9534 458.193 23.7422 445.96" stroke="white" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M474.898 405.281C452.184 309.129 310.29 379.438 328.836 453.624C339.851 497.677 379.58 444.374 385.41 426.88" stroke="white" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M384.381 443.768C387.652 488.786 436.11 468.859 453.299 444.796" stroke="white" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="500" height="657" viewBox="0 0 500 657" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M237.691 383.215C210.065 374.604 33.9083 350.993 24.77 364.701C15.0073 379.342 41.0933 419.975 47.398 432.588" stroke="#1343F0" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M266.485 418.187C280.276 446.048 382.424 631.697 377.575 641.393C296.026 632.097 140.508 637.817 122.48 619.791C111.766 609.077 109.491 586.421 100.88 573.506" stroke="#1343F0" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M147.179 414.074C147.995 411.054 145.059 409.131 144.093 406.875" stroke="#1343F0" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M484.602 120.733C419.985 -69.8593 139.011 18.7115 194.691 216.545C224.878 323.792 430.166 374.271 474.772 203.749" stroke="#282828" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M261.738 184.321C263.054 174.16 263.542 167.971 265.73 158.861" stroke="#282828" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M320.406 186.233C320.894 177.676 323.036 171.396 325.311 163.13" stroke="#282828" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M70.0282 463.446C81.7161 465.915 92.559 470.361 101.916 477.845C185.952 545.073 58.7422 568.42 19.6263 490.189C11.989 474.911 13.9534 458.193 23.7422 445.96" stroke="#282828" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M474.898 405.28C452.184 309.128 310.29 379.438 328.836 453.623C339.851 497.677 379.58 444.373 385.41 426.88" stroke="#282828" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M384.381 443.768C387.652 488.786 436.11 468.859 453.299 444.796" stroke="#282828" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
+6
-5
@@ -1,11 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import Hero from "./components/Hero/Hero.svelte";
|
|
||||||
import Services from "./components/Services/Services.svelte";
|
|
||||||
import Footer from "./components/Footer/Footer.svelte";
|
import Footer from "./components/Footer/Footer.svelte";
|
||||||
import Header from "./components/Header/Header.svelte";
|
import Header from "./components/Header/Header.svelte";
|
||||||
import CookiePopUp from "./components/CookiePopUp/CookiePopUp.svelte";
|
import CookiePopUp from "./components/CookiePopUp/CookiePopUp.svelte";
|
||||||
import Contacts from "./pages/Contacts.svelte";
|
import IndexPage from "./pages/index.svelte";
|
||||||
|
import Contacts from "./pages/contacts.svelte";
|
||||||
import Forbidden from "./pages/403.svelte";
|
import Forbidden from "./pages/403.svelte";
|
||||||
import ServerError from "./pages/500.svelte";
|
import ServerError from "./pages/500.svelte";
|
||||||
import ErrorGeneric from "./pages/errore.svelte";
|
import ErrorGeneric from "./pages/errore.svelte";
|
||||||
@@ -41,6 +40,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
console.log("Svelte App.svelte mounted!");
|
||||||
|
console.log("Current pathname:", pathname);
|
||||||
|
console.log("Current route:", currentRoute);
|
||||||
initializeStoredConsent()
|
initializeStoredConsent()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -80,8 +82,7 @@
|
|||||||
<Header />
|
<Header />
|
||||||
<main class:center-content={isCenteredMain}>
|
<main class:center-content={isCenteredMain}>
|
||||||
{#if currentRoute === 'home'}
|
{#if currentRoute === 'home'}
|
||||||
<Hero />
|
<IndexPage />
|
||||||
<Services />
|
|
||||||
{:else if currentRoute === 'contacts'}
|
{:else if currentRoute === 'contacts'}
|
||||||
<Contacts />
|
<Contacts />
|
||||||
{:else if currentRoute === '403'}
|
{:else if currentRoute === '403'}
|
||||||
|
|||||||
@@ -2,21 +2,34 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
width: fit-content;
|
||||||
|
inline-size: fit-content;
|
||||||
background-color: var(--button-color);
|
background-color: var(--button-color);
|
||||||
color: var(--button-text-color);
|
color: var(--button-text-color);
|
||||||
padding: var(--button-padding);
|
padding: var(--button-padding);
|
||||||
|
margin: var(--button-margin, 0);
|
||||||
border-radius: var(--button-radius);
|
border-radius: var(--button-radius);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: bold;
|
font-weight: normal;
|
||||||
transition: transform 0.2s, opacity 0.2s, background-color 0.2s, box-shadow 0.2s;
|
transition: transform 0.2s, opacity 0.2s, background-color 0.2s, box-shadow 0.2s;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: inset 0 0 0 var(--button-border-width) var(--primary-color);
|
box-shadow: inset 0 0 0 var(--button-border-width) var(--button-border-color);
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Occupa tutto lo spazio disponibile nel contenitore padre */
|
||||||
|
.button.full-width {
|
||||||
|
width: 100%;
|
||||||
|
inline-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
opacity: 0.92;
|
opacity: 0.92;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
@@ -30,3 +43,16 @@
|
|||||||
outline: 2px solid var(--primary-color);
|
outline: 2px solid var(--primary-color);
|
||||||
outline-offset: 3px;
|
outline-offset: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Remove touch highlight on buttons and hide focus for touch/pointer events
|
||||||
|
while keeping keyboard focus-visible for accessibility */
|
||||||
|
.button,
|
||||||
|
.button * {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:focus:not(:focus-visible) {
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
@@ -8,8 +8,11 @@
|
|||||||
textColor = '#fff',
|
textColor = '#fff',
|
||||||
round = '5px',
|
round = '5px',
|
||||||
padding = '10px 20px',
|
padding = '10px 20px',
|
||||||
|
margin = '0',
|
||||||
href = null,
|
href = null,
|
||||||
borderWidth = '0px',
|
borderWidth = '0px',
|
||||||
|
borderColor = 'var(--primary-color)',
|
||||||
|
fullWidth = false,
|
||||||
...restProps
|
...restProps
|
||||||
} = $props()
|
} = $props()
|
||||||
|
|
||||||
@@ -25,9 +28,10 @@
|
|||||||
{#if href}
|
{#if href}
|
||||||
<a
|
<a
|
||||||
class="button"
|
class="button"
|
||||||
style={`--button-color: ${color}; --button-text-color: ${textColor}; --button-radius: ${round}; --button-padding: ${padding}; --button-border-width: ${borderWidth};`}
|
class:full-width={fullWidth}
|
||||||
|
style={`--button-color: ${color}; --button-text-color: ${textColor}; --button-radius: ${round}; --button-padding: ${padding}; --button-border-width: ${borderWidth}; --button-border-color: ${borderColor}; --button-margin: ${margin};`}
|
||||||
{href}
|
{href}
|
||||||
on:click={handleClick}
|
onclick={handleClick}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
@@ -35,7 +39,8 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
class="button"
|
class="button"
|
||||||
style={`--button-color: ${color}; --button-text-color: ${textColor}; --button-radius: ${round}; --button-padding: ${padding}; --button-border-width: ${borderWidth};`}
|
class:full-width={fullWidth}
|
||||||
|
style={`--button-color: ${color}; --button-text-color: ${textColor}; --button-radius: ${round}; --button-padding: ${padding}; --button-border-width: ${borderWidth}; --button-border-color: ${borderColor}; --button-margin: ${margin};`}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
.cookie-backdrop {
|
.cookie-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 199;
|
z-index: 199;
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cookie-popup {
|
.cookie-popup {
|
||||||
@@ -11,29 +11,42 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 50px;
|
gap: 50px;
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
bottom: 25px;
|
bottom: 25px;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
width: min(100%, 1100px);
|
width: min(100%, 1100px);
|
||||||
max-width: calc(100% - 25px * 2);
|
max-width: calc(100% - 25px * 2);
|
||||||
padding: 50px;
|
max-height: calc(100vh - 50px);
|
||||||
border-radius: 5px;
|
overflow-y: auto;
|
||||||
background-color: var(--surface);
|
padding: 50px;
|
||||||
color: var(--text-color);
|
border-radius: 5px;
|
||||||
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.18);
|
background-color: var(--surface);
|
||||||
border: 2px solid rgba(127, 127, 127, 0.3);
|
color: var(--text-color);
|
||||||
|
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.18);
|
||||||
|
border: 2px solid rgba(127, 127, 127, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove mobile tap highlight and browser default focus ring inside cookie popup */
|
||||||
|
.cookie-popup,
|
||||||
|
.cookie-popup * {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-popup :focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cookie-popup-content p {
|
.cookie-popup-content p {
|
||||||
margin: 15px 0 15px;
|
margin: 15px 0 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cookie-popup-title {
|
.cookie-popup-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cookie-popup-title h2 {
|
.cookie-popup-title h2 {
|
||||||
@@ -42,44 +55,44 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cookie-icon {
|
.cookie-icon {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-color: var(--text-color);
|
background-color: var(--text-color);
|
||||||
-webkit-mask-image: url('./assets/cookie.svg');
|
-webkit-mask-image: url('./assets/cookie.svg');
|
||||||
-webkit-mask-repeat: no-repeat;
|
-webkit-mask-repeat: no-repeat;
|
||||||
-webkit-mask-position: center;
|
-webkit-mask-position: center;
|
||||||
-webkit-mask-size: contain;
|
-webkit-mask-size: contain;
|
||||||
mask-image: url('./assets/cookie.svg');
|
mask-image: url('./assets/cookie.svg');
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cookie-popup-actions {
|
.cookie-popup-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 50px;
|
gap: 50px;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cookie-link {
|
.cookie-link {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cookie-link-button {
|
.cookie-link-button {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cookie-link:hover {
|
.cookie-link:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cookie-popup-buttons {
|
.cookie-popup-buttons {
|
||||||
@@ -90,14 +103,12 @@
|
|||||||
|
|
||||||
.cookie-popup-buttons .button {
|
.cookie-popup-buttons .button {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
|
||||||
min-width: fit-content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cookie-toggle-item {
|
.cookie-toggle-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -115,36 +126,84 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cookie-toggle-container {
|
.cookie-toggle-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 50px;
|
gap: 0 50px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* toggle item styling moved to Toggle.css */
|
/* ============================================================
|
||||||
|
MEDIA QUERIES
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 1280px) {}
|
||||||
.cookie-popup {
|
|
||||||
bottom: 12px;
|
|
||||||
padding: 16px;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cookie-popup-actions {
|
@media (max-width: 1024px) {
|
||||||
justify-content: stretch;
|
.cookie-popup-buttons {
|
||||||
gap: 12px;
|
flex-wrap: wrap;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cookie-popup-actions .button,
|
.cookie-popup-buttons .button {
|
||||||
.cookie-link {
|
width: 100%;
|
||||||
flex: 1 1 100%;
|
flex: none;
|
||||||
text-align: center;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.cookie-link-button {
|
.cookie-popup-buttons .button {
|
||||||
padding: 10px 0;
|
flex: 1 1 auto;
|
||||||
}
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-link-button {
|
||||||
|
padding: 8px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.cookie-popup {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
transform: none;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 85vh;
|
||||||
|
padding: 20px 16px;
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-popup-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-popup-buttons {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-popup-buttons .button {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {}
|
||||||
|
|
||||||
|
@media (max-width: 360px) {
|
||||||
|
.cookie-popup-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-popup-buttons .button {
|
||||||
|
width: 100%;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
.footer p, .footer a, .footer h4 {
|
.footer p, .footer a, .footer h4 {
|
||||||
font-size: 0.7rem;
|
font-size: 0.9rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@
|
|||||||
color: var(--muted-color);
|
color: var(--muted-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 900px) {
|
||||||
.footer {
|
.footer {
|
||||||
padding: 40px 20px 20px;
|
padding: 40px 20px 20px;
|
||||||
}
|
}
|
||||||
@@ -198,5 +198,6 @@
|
|||||||
.footer-section-info-container {
|
.footer-section-info-container {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,28 +19,28 @@
|
|||||||
<p>Scoprirci anche sui nostri canali social</p>
|
<p>Scoprirci anche sui nostri canali social</p>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a
|
<a
|
||||||
href="https://www.facebook.com/cimaprogetti"
|
href="https://www.facebook.com/profile.php?id=61587060986122"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer">
|
||||||
<span class="social-icon icon-facebook" aria-hidden="true"></span>
|
<span class="social-icon icon-facebook" aria-hidden="true"></span>
|
||||||
<span class="sr-only">Facebook</span>
|
<span class="sr-only">Facebook</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://www.instagram.com/cimaprogetti"
|
href="https://www.instagram.com/cimaprogetti/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer">
|
||||||
<span class="social-icon icon-instagram" aria-hidden="true"></span>
|
<span class="social-icon icon-instagram" aria-hidden="true"></span>
|
||||||
<span class="sr-only">Instagram</span>
|
<span class="sr-only">Instagram</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://www.linkedin.com/company/cimaprogetti"
|
href="https://www.linkedin.com/company/cima-progetti-srls/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer">
|
||||||
<span class="social-icon icon-linkedin" aria-hidden="true"></span>
|
<span class="social-icon icon-linkedin" aria-hidden="true"></span>
|
||||||
<span class="sr-only">LinkedIn</span>
|
<span class="sr-only">LinkedIn</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://www.tiktok.com/@cimaprogetti"
|
href="https://www.tiktok.com/@cima.progetti"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer">
|
||||||
<span class="social-icon icon-tiktok" aria-hidden="true"></span>
|
<span class="social-icon icon-tiktok" aria-hidden="true"></span>
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
/* ============================================================
|
||||||
|
HEADER — struttura base
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -5,7 +9,18 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
transition: transform 0.28s ease;
|
transition: transform 0.3s ease, background-color 1s ease, backdrop-filter 0.3s ease, -webkit-backdrop-filter 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
background-color: transparent;
|
||||||
|
backdrop-filter: blur(0px);
|
||||||
|
-webkit-backdrop-filter: blur(0px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header.with-bg {
|
||||||
|
background-color: var(--background-header);
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
-webkit-backdrop-filter: blur(3px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header.hidden {
|
.header.hidden {
|
||||||
@@ -13,15 +28,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-container {
|
.header-container {
|
||||||
max-width: 1000px;
|
margin: 0 var(--lateral-margin);
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 20px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: var(--navbar-height);
|
height: var(--navbar-height);
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-img{
|
/* ============================================================
|
||||||
|
LOGO
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
width: 98px;
|
width: 98px;
|
||||||
height: 70px;
|
height: 70px;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
@@ -30,7 +48,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo-mask {
|
.logo-mask {
|
||||||
background-color: var(--text-color);
|
background-color: var(--primary-color);
|
||||||
-webkit-mask-image: url('/images/icons/logo-nero.svg');
|
-webkit-mask-image: url('/images/icons/logo-nero.svg');
|
||||||
-webkit-mask-repeat: no-repeat;
|
-webkit-mask-repeat: no-repeat;
|
||||||
-webkit-mask-position: center;
|
-webkit-mask-position: center;
|
||||||
@@ -45,18 +63,16 @@
|
|||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
NAV DESKTOP
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav a {
|
.nav a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
@@ -74,22 +90,203 @@
|
|||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Temporaneo*/
|
/* ============================================================
|
||||||
@media (max-width: 768px) {
|
HEADER ACTIONS (desktop)
|
||||||
.header-container {
|
============================================================ */
|
||||||
flex-direction: column;
|
|
||||||
height: auto;
|
.header-actions {
|
||||||
padding: 15px 20px;
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
HAMBURGER BUTTON — icona SVG, niente span
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.hamburger {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--text-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger:hover {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
MENU MOBILE FULL-PAGE (sale dal basso)
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.mobile-menu {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 99;
|
||||||
|
background-color: var(--surface);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
/* Nascosto: traslato completamente fuori dal basso */
|
||||||
|
transform: translateY(100%);
|
||||||
|
/* Transizione sempre attiva per animare sia l'apertura che la chiusura */
|
||||||
|
transition: transform 0.38s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
/* Solo su mobile */
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu.open {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
NAV INTERNA AL MENU MOBILE
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.mobile-menu-nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0 1.5rem 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-nav a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: clamp(2rem, 8vw, 3.5rem);
|
||||||
|
line-height: 1;
|
||||||
|
padding: 20px 0;
|
||||||
|
border-bottom: 2px solid var(--background-opaque);
|
||||||
|
transition: color 0.2s, padding-left 0.2s;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-nav a:first-child {
|
||||||
|
border-top: 2px solid var(--background-opaque);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-nav a:hover,
|
||||||
|
.mobile-menu-nav a.active {
|
||||||
|
color: var(--primary-color);
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
FOOTER DEL MENU MOBILE (bottone contattaci)
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.mobile-menu-footer {
|
||||||
|
padding: 24px 1.5rem 40px;
|
||||||
|
border-top: 2px solid var(--background-opaque);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-footer .button {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
MEDIA QUERIES
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
/* —— Desktop grande (> 1280px) ————————————————————————————— */
|
||||||
|
@media (min-width: 1280px) {}
|
||||||
|
|
||||||
|
/* —— Desktop medio (1024px – 1279px) ————————————————————— */
|
||||||
|
@media (min-width: 1024px) and (max-width: 1279px) {}
|
||||||
|
|
||||||
|
/* —— Tablet (768px – 1023px): pannello laterale 50% ————————— */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
.mobile-menu {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 50%;
|
||||||
|
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.12);
|
||||||
|
border-left: 2px solid var(--background-opaque);
|
||||||
|
clip-path: inset(0 0 0 100%);
|
||||||
|
transition: clip-path 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
.mobile-menu.open {
|
||||||
gap: 15px;
|
clip-path: inset(0 0 0 0%);
|
||||||
width: 100%;
|
transition: clip-path 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav a {
|
.mobile-menu-nav a {
|
||||||
font-size: 14px;
|
font-size: clamp(1.2rem, 3vw, 1.6rem);
|
||||||
|
padding: 16px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* —— Mobile (< 900px): attiva hamburger + menu full-page ——— */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.header-container {
|
||||||
|
margin: 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nasconde nav e bottone desktop */
|
||||||
|
.nav-desktop,
|
||||||
|
.header-actions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mostra hamburger */
|
||||||
|
.hamburger {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Abilita il menu mobile (di default display:none) */
|
||||||
|
.mobile-menu {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* —— Mobile piccolo (< 480px) ————————————————————————————— */
|
||||||
|
@media (max-width: 480px) {}
|
||||||
|
|
||||||
|
/* —— Mobile minimo (< 360px) —————————————————————————————— */
|
||||||
|
@media (max-width: 360px) {}
|
||||||
|
|
||||||
|
/* Touch / tap highlight & focus handling for mobile interaction
|
||||||
|
- disable native tap highlight which creates colored rectangles
|
||||||
|
- keep keyboard focus-visible outlines intact for accessibility
|
||||||
|
*/
|
||||||
|
.hamburger,
|
||||||
|
.mobile-menu a,
|
||||||
|
.mobile-menu .button,
|
||||||
|
.mobile-menu .button * {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide focus ring for pointer/touch interactions but preserve for keyboard
|
||||||
|
users via :focus-visible (only remove when element is focused but not
|
||||||
|
focus-visible). */
|
||||||
|
.hamburger:focus:not(:focus-visible),
|
||||||
|
.mobile-menu a:focus:not(:focus-visible),
|
||||||
|
.mobile-menu .button:focus:not(:focus-visible) {
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
@@ -6,27 +6,41 @@
|
|||||||
|
|
||||||
let activeNav = $state(typeof window !== 'undefined' ? window.location.pathname : '/')
|
let activeNav = $state(typeof window !== 'undefined' ? window.location.pathname : '/')
|
||||||
let isHidden = $state(false)
|
let isHidden = $state(false)
|
||||||
|
let isMenuOpen = $state(false)
|
||||||
|
let hasBackground = $state(false)
|
||||||
let lastScrollY = 0
|
let lastScrollY = 0
|
||||||
|
|
||||||
const SCROLL_DELTA = 8
|
const SCROLL_DELTA = 8
|
||||||
const TOP_OFFSET = 24
|
const TOP_OFFSET = 24
|
||||||
|
const BG_THRESHOLD = 100
|
||||||
|
|
||||||
function handleNavClick(event, path) {
|
function handleNavClick(event, path) {
|
||||||
if (!isInternalPath(path)) return
|
if (!isInternalPath(path)) return
|
||||||
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return
|
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
isMenuOpen = false
|
||||||
navigateTo(path)
|
navigateTo(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleMenu() {
|
||||||
|
isMenuOpen = !isMenuOpen
|
||||||
|
}
|
||||||
|
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
const currentScrollY = window.scrollY || 0
|
const currentScrollY = window.scrollY || 0
|
||||||
const diff = currentScrollY - lastScrollY
|
const diff = currentScrollY - lastScrollY
|
||||||
|
|
||||||
|
// Aggiungi sfondo quando scrolli oltre il threshold
|
||||||
|
hasBackground = currentScrollY > BG_THRESHOLD
|
||||||
|
|
||||||
if (currentScrollY <= TOP_OFFSET) {
|
if (currentScrollY <= TOP_OFFSET) {
|
||||||
isHidden = false
|
isHidden = false
|
||||||
|
hasBackground = false
|
||||||
} else if (diff > SCROLL_DELTA) {
|
} else if (diff > SCROLL_DELTA) {
|
||||||
isHidden = true
|
isHidden = true
|
||||||
|
hasBackground = false
|
||||||
|
isMenuOpen = false
|
||||||
} else if (diff < -SCROLL_DELTA) {
|
} else if (diff < -SCROLL_DELTA) {
|
||||||
isHidden = false
|
isHidden = false
|
||||||
}
|
}
|
||||||
@@ -50,49 +64,111 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="header" class:hidden={isHidden} id="home">
|
<header class="header" class:hidden={isHidden} class:with-bg={hasBackground} id="home">
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<a href="/" aria-label="CIMA PROGETTI home" on:click={(e) => handleNavClick(e, '/')}>
|
<a href="/" aria-label="CIMA PROGETTI home" onclick={(e) => handleNavClick(e, '/')}>
|
||||||
<span class="logo-img logo-mask" aria-hidden="true"></span>
|
<span class="logo-img logo-mask" aria-hidden="true"></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<nav class="nav">
|
|
||||||
|
<!-- Nav desktop -->
|
||||||
|
<nav class="nav nav-desktop" aria-label="Navigazione principale">
|
||||||
<a
|
<a
|
||||||
href="/servizi"
|
href="/servizi"
|
||||||
class:active={activeNav === '/servizi'}
|
class:active={activeNav === '/servizi'}
|
||||||
on:click={(e) => handleNavClick(e, '/servizi')}
|
onclick={(e) => handleNavClick(e, '/servizi')}
|
||||||
aria-current={activeNav === '/servizi' ? 'page' : undefined}
|
aria-current={activeNav === '/servizi' ? 'page' : undefined}
|
||||||
>
|
>Servizi</a>
|
||||||
Servizi
|
|
||||||
</a>
|
|
||||||
<a
|
<a
|
||||||
href="/metodo"
|
href="/metodo"
|
||||||
class:active={activeNav === '/metodo'}
|
class:active={activeNav === '/metodo'}
|
||||||
on:click={(e) => handleNavClick(e, '/metodo')}
|
onclick={(e) => handleNavClick(e, '/metodo')}
|
||||||
aria-current={activeNav === '/metodo' ? 'page' : undefined}
|
aria-current={activeNav === '/metodo' ? 'page' : undefined}
|
||||||
>
|
>Il metodo</a>
|
||||||
Il metodo
|
|
||||||
</a>
|
|
||||||
<a
|
<a
|
||||||
href="/progetti"
|
href="/progetti"
|
||||||
class:active={activeNav === '/progetti'}
|
class:active={activeNav === '/progetti'}
|
||||||
on:click={(e) => handleNavClick(e, '/progetti')}
|
onclick={(e) => handleNavClick(e, '/progetti')}
|
||||||
aria-current={activeNav === '/progetti' ? 'page' : undefined}
|
aria-current={activeNav === '/progetti' ? 'page' : undefined}
|
||||||
>
|
>Progetti</a>
|
||||||
Progetti
|
|
||||||
</a>
|
|
||||||
<a
|
<a
|
||||||
href="/chi-siamo"
|
href="/chi-siamo"
|
||||||
class:active={activeNav === '/chi-siamo'}
|
class:active={activeNav === '/chi-siamo'}
|
||||||
on:click={(e) => handleNavClick(e, '/chi-siamo')}
|
onclick={(e) => handleNavClick(e, '/chi-siamo')}
|
||||||
aria-current={activeNav === '/chi-siamo' ? 'page' : undefined}
|
aria-current={activeNav === '/chi-siamo' ? 'page' : undefined}
|
||||||
>
|
>Chi Siamo</a>
|
||||||
Chi Siamo
|
|
||||||
</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<Button text="Contattaci" href="/contatti" />
|
<Button text="Contattaci" href="/contatti" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Pulsante hamburger / close (solo mobile) — SVG inline, niente span -->
|
||||||
|
<button
|
||||||
|
class="hamburger"
|
||||||
|
onclick={toggleMenu}
|
||||||
|
aria-label={isMenuOpen ? 'Chiudi menu' : 'Apri menu'}
|
||||||
|
aria-expanded={isMenuOpen}
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{#if isMenuOpen}
|
||||||
|
<!-- Icona X -->
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
<line x1="4" y1="4" x2="20" y2="20" stroke="currentColor" stroke-width="2" stroke-linecap="square"/>
|
||||||
|
<line x1="20" y1="4" x2="4" y2="20" stroke="currentColor" stroke-width="2" stroke-linecap="square"/>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<!-- Icona hamburger -->
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
<line x1="3" y1="6" x2="21" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="square"/>
|
||||||
|
<line x1="3" y1="12" x2="21" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="square"/>
|
||||||
|
<line x1="3" y1="18" x2="21" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="square"/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Menu mobile full-page (si apre dal basso) -->
|
||||||
|
<div
|
||||||
|
class="mobile-menu"
|
||||||
|
class:open={isMenuOpen}
|
||||||
|
id="mobile-menu"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Menu di navigazione"
|
||||||
|
aria-hidden={!isMenuOpen}
|
||||||
|
>
|
||||||
|
<nav class="mobile-menu-nav" aria-label="Navigazione mobile">
|
||||||
|
<a
|
||||||
|
href="/servizi"
|
||||||
|
class:active={activeNav === '/servizi'}
|
||||||
|
onclick={(e) => handleNavClick(e, '/servizi')}
|
||||||
|
aria-current={activeNav === '/servizi' ? 'page' : undefined}
|
||||||
|
>Servizi</a>
|
||||||
|
<a
|
||||||
|
href="/metodo"
|
||||||
|
class:active={activeNav === '/metodo'}
|
||||||
|
onclick={(e) => handleNavClick(e, '/metodo')}
|
||||||
|
aria-current={activeNav === '/metodo' ? 'page' : undefined}
|
||||||
|
>Il metodo</a>
|
||||||
|
<a
|
||||||
|
href="/progetti"
|
||||||
|
class:active={activeNav === '/progetti'}
|
||||||
|
onclick={(e) => handleNavClick(e, '/progetti')}
|
||||||
|
aria-current={activeNav === '/progetti' ? 'page' : undefined}
|
||||||
|
>Progetti</a>
|
||||||
|
<a
|
||||||
|
href="/chi-siamo"
|
||||||
|
class:active={activeNav === '/chi-siamo'}
|
||||||
|
onclick={(e) => handleNavClick(e, '/chi-siamo')}
|
||||||
|
aria-current={activeNav === '/chi-siamo' ? 'page' : undefined}
|
||||||
|
>Chi Siamo</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="mobile-menu-footer">
|
||||||
|
<Button text="Contattaci" href="/contatti" onclick={() => { isMenuOpen = false }} padding="14px 32px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
.hero {
|
|
||||||
background: linear-gradient(135deg, #0066cc 0%, #0052a3 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 120px 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content h1 {
|
|
||||||
font-size: 48px;
|
|
||||||
margin: 0 0 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content p {
|
|
||||||
font-size: 24px;
|
|
||||||
margin: 0 0 40px 0;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-button {
|
|
||||||
background-color: #ff6b35;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 14px 40px;
|
|
||||||
font-size: 18px;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-button:hover {
|
|
||||||
background-color: #e55a24;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.hero {
|
|
||||||
padding: 80px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content h1 {
|
|
||||||
font-size: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content p {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-button {
|
|
||||||
padding: 12px 30px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}.hero {
|
|
||||||
background: linear-gradient(135deg, #0066cc 0%, #0052a3 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 120px 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content h1 {
|
|
||||||
font-size: 48px;
|
|
||||||
margin: 0 0 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content p {
|
|
||||||
font-size: 24px;
|
|
||||||
margin: 0 0 40px 0;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-button {
|
|
||||||
background-color: #ff6b35;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 14px 40px;
|
|
||||||
font-size: 18px;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-button:hover {
|
|
||||||
background-color: #e55a24;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.hero {
|
|
||||||
padding: 80px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content h1 {
|
|
||||||
font-size: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content p {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-button {
|
|
||||||
padding: 12px 30px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<script>
|
|
||||||
import './Hero.css'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section class="hero">
|
|
||||||
<div class="hero-container">
|
|
||||||
<div class="hero-content">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
.services {
|
|
||||||
padding: 80px 20px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services h2 {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 36px;
|
|
||||||
margin-bottom: 50px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
gap: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card {
|
|
||||||
background: #f9f9f9;
|
|
||||||
padding: 30px;
|
|
||||||
border-radius: 8px;
|
|
||||||
text-align: center;
|
|
||||||
transition: transform 0.3s, box-shadow 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card h3 {
|
|
||||||
font-size: 20px;
|
|
||||||
margin: 15px 0;
|
|
||||||
color: #0066cc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card p {
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.services {
|
|
||||||
padding: 60px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services h2 {
|
|
||||||
font-size: 28px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<script>
|
|
||||||
import './Services.css'
|
|
||||||
|
|
||||||
const services = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'Consulenza',
|
|
||||||
description: 'Supporto strategico per la crescita della tua azienda',
|
|
||||||
icon: '💼'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'Sviluppo Web',
|
|
||||||
description: 'Siti e applicazioni web moderne e performanti',
|
|
||||||
icon: '🚀'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: 'Analisi Dati',
|
|
||||||
description: 'Insights approfonditi dai tuoi dati aziendali',
|
|
||||||
icon: '📊'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: 'Cloud Solutions',
|
|
||||||
description: 'Migrazione e gestione dell\'infrastruttura cloud',
|
|
||||||
icon: '☁️'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section class="services">
|
|
||||||
<div class="services-container">
|
|
||||||
<h2>I Nostri Servizi</h2>
|
|
||||||
<div class="services-grid">
|
|
||||||
{#each services as service (service.id)}
|
|
||||||
<div class="service-card">
|
|
||||||
<div class="icon">{service.icon}</div>
|
|
||||||
<h3>{service.title}</h3>
|
|
||||||
<p>{service.description}</p>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
+19
-3
@@ -2,8 +2,24 @@ import './styles/global.css'
|
|||||||
import { mount } from 'svelte'
|
import { mount } from 'svelte'
|
||||||
import App from './App.svelte'
|
import App from './App.svelte'
|
||||||
|
|
||||||
const app = mount(App, {
|
// Global error trap to catch syntax errors before app mount
|
||||||
target: document.getElementById('app'),
|
window.addEventListener('error', (event) => {
|
||||||
})
|
const msg = `ERROR: ${event.error?.message || event.message}`;
|
||||||
|
console.error(msg, event.error);
|
||||||
|
alert(msg);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
console.log("main.js is starting!");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const app = mount(App, {
|
||||||
|
target: document.getElementById('app'),
|
||||||
|
})
|
||||||
|
console.log("App mounted successfully!");
|
||||||
|
} catch (e) {
|
||||||
|
const msg = `Mount Error: ${e.message}`;
|
||||||
|
alert(msg);
|
||||||
|
console.error("Error mounting App:", e);
|
||||||
|
}
|
||||||
|
|
||||||
export default app
|
export default app
|
||||||
|
|||||||
+105
-21
@@ -1,26 +1,110 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import "../styles/contacts.css";
|
||||||
|
import Button from "../components/Button/Button.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="contacts-page">
|
<div class="contacts-page">
|
||||||
<h1>Contatti</h1>
|
<div class="contact-us">
|
||||||
<p>Pagina contatti - contenuto da aggiungere</p>
|
<div class="contact-us-container">
|
||||||
|
<div class="contact-title">
|
||||||
|
<h1>Contatta<span>ci</span></h1>
|
||||||
|
<span class="divider"></span>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Siamo qui per <span style="font-weight: bold;">ascoltarti</span>.
|
||||||
|
</p>
|
||||||
|
<div class="contact-info-container">
|
||||||
|
<div class="contact-info-item">
|
||||||
|
<h4 class="contact-info-type">EMAIL</h4>
|
||||||
|
<a href="mailto:info@cimaprogetti.it"
|
||||||
|
>info@cimaprogetti.it</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="contact-info-item">
|
||||||
|
<h4 class="contact-info-type">WHATSAPP</h4>
|
||||||
|
<a href="https://wa.me/393382451171" target="_blank"
|
||||||
|
>+39 338 245 1171</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
text="PRENOTA UNA CALL"
|
||||||
|
borderWidth="1.5px"
|
||||||
|
borderColor="var(--text-color)"
|
||||||
|
color="transparent"
|
||||||
|
textColor="var(--text-color)"
|
||||||
|
href="https://google.com"
|
||||||
|
margin="1rem 0 0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="contact-image"></div>
|
||||||
|
</div>
|
||||||
|
<div class="behind-cima">
|
||||||
|
<h2>PEOPLE BEHIND CIMA</h2>
|
||||||
|
<div class="profiles-container">
|
||||||
|
<div class="behind-cima-profile">
|
||||||
|
<h1>Nicola Leone CIARDI</h1>
|
||||||
|
<div class="profile-role">
|
||||||
|
<span class="divider"></span>
|
||||||
|
<h4>CO-FOUNDER & CEO</h4>
|
||||||
|
</div>
|
||||||
|
<p>Management and computer science</p>
|
||||||
|
<div class="contact-info-container-personal">
|
||||||
|
<div class="contact-info-item">
|
||||||
|
<h4 class="contact-info-type">EMAIL</h4>
|
||||||
|
<a
|
||||||
|
href="mailto:nicolaleone.ciardi@cimaprogetti.it"
|
||||||
|
class="personal"
|
||||||
|
>nicolaleone.ciardi@cimaprogetti.it</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="contact-info-item">
|
||||||
|
<h4 class="contact-info-type">WHATSAPP</h4>
|
||||||
|
<a
|
||||||
|
href="https://wa.me/393382451178"
|
||||||
|
target="_blank"
|
||||||
|
class="personal">+39 338 245 1178</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
text="Parliamone!"
|
||||||
|
href="https://google.com"
|
||||||
|
margin="2rem 0 0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="behind-cima-profile">
|
||||||
|
<h1>Valentina MADAUDO</h1>
|
||||||
|
<div class="profile-role">
|
||||||
|
<span class="divider"></span>
|
||||||
|
<h4>CO-FOUNDER & CFO</h4>
|
||||||
|
</div>
|
||||||
|
<p>Jr Engineer & economist for sustainable development</p>
|
||||||
|
<div class="contact-info-container-personal">
|
||||||
|
<div class="contact-info-item">
|
||||||
|
<h4 class="contact-info-type">EMAIL</h4>
|
||||||
|
<a
|
||||||
|
href="mailto:valentina.madaudo@cimaprogetti.it"
|
||||||
|
class="personal"
|
||||||
|
>valentina.madaudo@cimaprogetti.it</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="contact-info-item">
|
||||||
|
<h4 class="contact-info-type">WHATSAPP</h4>
|
||||||
|
<a
|
||||||
|
href="https://wa.me/393393580805"
|
||||||
|
target="_blank"
|
||||||
|
class="personal">+39 339 358 0805</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
text="Confrontiamoci!"
|
||||||
|
href="https://google.com"
|
||||||
|
margin="2rem 0 0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
.contacts-page {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 40px 20px;
|
|
||||||
min-height: 60vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<script>
|
||||||
|
import "../styles/global.css";
|
||||||
|
import "../styles/index.css";
|
||||||
|
import Button from "../components/Button/Button.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="index-page">
|
||||||
|
<h1>Home</h1>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
.contacts-page {
|
||||||
|
margin: 0 var(--lateral-margin) ;
|
||||||
|
padding-bottom: var(--lateral-margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-us {
|
||||||
|
display: flex;
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-us-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 30%;
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: 100dvh;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-us-container p {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-title {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-title span {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
h4.contact-info-type {
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info-item a {
|
||||||
|
color: var(--text-color);
|
||||||
|
text-decoration: none;
|
||||||
|
margin-top: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: clamp(1.25rem, 2.5vw, 2rem);
|
||||||
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-image {
|
||||||
|
flex: 0 0 clamp(240px, 24vw, 400px);
|
||||||
|
width: clamp(240px, 24vw, 400px);
|
||||||
|
aspect-ratio: 500 / 657;
|
||||||
|
background-image: url("/images/contacts/hero-lighttheme.svg");
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] .contact-image {
|
||||||
|
background-image: url("/images/contacts/hero-blacktheme.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-button {
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-button:hover {
|
||||||
|
background-color: var(--background-opaque);
|
||||||
|
color: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.behind-cima {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profiles-container {
|
||||||
|
display: flex;
|
||||||
|
gap: clamp(20px, 3vw, 40px);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.behind-cima-profile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-role {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-role span {
|
||||||
|
margin: 0 1rem 0 0;
|
||||||
|
width: 5rem;;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.personal {
|
||||||
|
font-size: clamp(0.9rem, 2vw, 1.25rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.behind-cima-profile p {
|
||||||
|
margin: 0 0 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info-container-personal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.contact-us {
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-us-container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-image {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 80%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
flex: none;
|
||||||
|
width: clamp(120px, 30vw, 400px);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profiles-container {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.behind-cima-profile {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.contact-image {
|
||||||
|
top: 80%;
|
||||||
|
left: 80%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: clamp(120px, 15vw, 180px);
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
-16
@@ -1,16 +1,12 @@
|
|||||||
.error-page {
|
.error-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
min-height: 100dvh;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-page h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-content {
|
.error-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -41,6 +37,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
line-height: 0.9;
|
line-height: 0.9;
|
||||||
|
transform: translateX(1rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-page .title-top {
|
.error-page .title-top {
|
||||||
@@ -52,15 +49,6 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
width: 50%;
|
|
||||||
height: 5px;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message,
|
.error-message,
|
||||||
.error-solution {
|
.error-solution {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -79,12 +67,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.error-code {
|
.error-code {
|
||||||
font-size: 4rem;
|
font-size: clamp(1rem, 5vw, 4rem);
|
||||||
line-height: 0.4;
|
line-height: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-text {
|
.error-text {
|
||||||
font-size: 15rem !important;
|
font-size: clamp(5rem, 20vw, 15rem) !important;
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
line-height: 0.8;
|
line-height: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
MEDIA QUERIES
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.error-content {
|
||||||
|
margin: 0 var(--lateral-margin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {}
|
||||||
|
|
||||||
|
@media (max-width: 360px) {}
|
||||||
+64
-15
@@ -5,7 +5,14 @@
|
|||||||
--muted-color: #6b7280;
|
--muted-color: #6b7280;
|
||||||
--surface: #ffffff;
|
--surface: #ffffff;
|
||||||
--background: rgba(246, 249, 255, 0.5);
|
--background: rgba(246, 249, 255, 0.5);
|
||||||
--background-opaque: rgba(0, 0, 0, 0.2);
|
--background-opaque: #00000033;
|
||||||
|
--background-header: rgba(249, 249, 249, 0.5);
|
||||||
|
--lateral-margin: 12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix mobile overscroll area color (rubber-band / bounce effect) */
|
||||||
|
html {
|
||||||
|
background-color: #f6f9ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme overrides (toggle by setting attribute on <html>) */
|
/* Dark theme overrides (toggle by setting attribute on <html>) */
|
||||||
@@ -16,6 +23,15 @@
|
|||||||
--surface: #000;
|
--surface: #000;
|
||||||
--background: rgba(0, 0, 0, 0.8);
|
--background: rgba(0, 0, 0, 0.8);
|
||||||
--background-opaque: rgba(255, 255, 255, 0.2);
|
--background-opaque: rgba(255, 255, 255, 0.2);
|
||||||
|
--background-header: rgba(51, 51, 51, 0.5);
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
:root {
|
||||||
|
--navbar-height: 110px;
|
||||||
|
--lateral-margin: 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -29,7 +45,7 @@ body {
|
|||||||
font-family: Helvetica, 'Segoe UI';
|
font-family: Helvetica, 'Segoe UI';
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
background-color: var(--surface);
|
background-color: var(--surface);
|
||||||
background-image: url('/images/background/paper.png');
|
background-image: linear-gradient(var(--background), var(--background)), url('/images/background/paper.png');
|
||||||
background-repeat: repeat;
|
background-repeat: repeat;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -37,19 +53,15 @@ body {
|
|||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
#app {
|
#app {
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Background overlay layer that follows theme color */
|
main {
|
||||||
body::before {
|
min-height: 100vh;
|
||||||
content: "";
|
min-height: 100dvh;
|
||||||
position: fixed;
|
display: flow-root;
|
||||||
inset: 0;
|
|
||||||
background-color: var(--background);
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reusable theme asset helpers:
|
/* Reusable theme asset helpers:
|
||||||
@@ -84,6 +96,8 @@ html[data-theme='dark'] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 3rem;
|
||||||
font-family: 'IBM Plex Mono', monospace;
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +121,42 @@ p, a {
|
|||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Usage: add data attribute with SVG path
|
.divider {
|
||||||
Example: <div class="svg-mask" style="--mask-url: url('/path/to/icon.svg'); width: 40px; height: 40px;"></div>
|
display: block;
|
||||||
Then use inline style: -webkit-mask-image: var(--mask-url); mask-image: var(--mask-url);
|
position: relative;
|
||||||
OR use a specific class that sets the mask-image */
|
width: 50%;
|
||||||
|
height: 5px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
MEDIA QUERIES
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
:root {
|
||||||
|
--lateral-margin: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
:root {
|
||||||
|
--lateral-margin: 5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
:root {
|
||||||
|
--lateral-margin: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 360px) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.index-page {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
+2
-9
@@ -3,16 +3,9 @@ import { svelte } from '@sveltejs/vite-plugin-svelte'
|
|||||||
|
|
||||||
export default defineConfig(() => ({
|
export default defineConfig(() => ({
|
||||||
plugins: [svelte()],
|
plugins: [svelte()],
|
||||||
|
appType: 'spa',
|
||||||
server: {
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
port: 5173,
|
port: 5173,
|
||||||
middlewares: [
|
|
||||||
(req, res, next) => {
|
|
||||||
if (req.url === '/' || req.url.match(/\.\w+$/) || req.url.includes('/public/')) {
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
req.url = '/'
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user