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:
2026-05-06 23:05:17 +02:00
parent 575e2b4779
commit e3098d3004
25 changed files with 1426 additions and 565 deletions
+493 -128
View File
@@ -1,20 +1,24 @@
# 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
- 🌓 **Tema light/dark** con persistenza locale
- 📱 **Design completamente responsivo**
- ⬆️ **Navbar collapsibile** con hide/show al scroll
- 🎨 **Sistema di componenti riusabili**
- 🚀 **Routing client-side** con pagine dedicate e fallback 404
- **Accessibilità** e buone pratiche web
- 🌓 **Tema light/dark** con persistenza dual-storage (localStorage + cookie backup)
- 📱 **Design completamente responsivo** con breakpoint intelligenti
- ⬆️ **Header smart** — trasparente all'inizio, sfondo blur opaco dopo scroll
- 🍔 **Menu hamburger** con animazione fluida di apertura/chiusura
- 🎨 **Sistema di componenti riusabili** e theme-aware
- 🚀 **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
- `Node.js` 18+ (consigliato LTS)
- `npm` 9+
- **Node.js** 18+ (consigliato LTS)
- **npm** 9+
## 🚀 Avvio rapido
@@ -23,76 +27,201 @@ npm install
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
- `npm run dev` - avvia il server di sviluppo con hot reload
- `npm run build` - genera la build di produzione
- `npm run preview` - avvia un'anteprima della build
| Script | Descrizione |
|--------|-------------|
| `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
```
src/
├── App.svelte # Componente root con routing
├── main.js # Punto di ingresso
├── styles/
│ ├── global.css # Stili globali e variabili tema
├── App.css # Layout principale app
│ └── error-pages.css # Stili pagine errore
├── components/
│ ├── Header/ # Navbar con scroll hide/show
├── Hero/ # Sezione hero principale
│ ├── Services/ # Griglia servizi
│ ├── Footer/ # Footer con info aziendali
├── Button/ # Componente button riusabile
└── ThemeToggle/ # Toggle tema light/dark
└── pages/
├── Contacts.svelte # Pagina contatti
├── 403.svelte # Accesso negato
├── 404.svelte # Pagina non trovata
├── 500.svelte # Errore server
└── ErrorGeneric.svelte # Errore generico
cimaprogetti/
├── index.html # Template HTML entry point
├── package.json # Dependencies e scripts
├── vite.config.js # Configurazione Vite (SPA + host 0.0.0.0)
├── svelte.config.js # Configurazione Svelte
├── src/
│ ├── main.js # Entry point con error trap globale
│ ├── App.svelte # Root component con routing e layout
│ ├── components/
│ ├── Header/
├── Header.svelte # Navbar sticky con tema + scroll detection
│ │ └── Header.css # Stili header (blur backdrop, absolute menu)
│ │ │
│ │ ├── Button/
│ │ │ ├── Button.svelte # Button/link riusabile (16 prop)
│ │ └── Button.css # Stili + focus-visible + no tap highlight
│ │ │
│ │ ├── 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
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 |
|-------|-----------|-------------|
| `/` | `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 |
### Tabella rotte
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`:
```css
:root {
--primary-color: #1343F0; /* Colore primario */
--text-color: #111827; /* Testo light mode */
--surface: #ffffff; /* Sfondo light mode */
--navbar-height: 140px;
--primary-color: #1343F0; /* Blu Cima */
--text-color: #111827; /* Grigio scuro */
--surface: #ffffff; /* Background carta */
--background: rgba(246, 249, 255, 0.5);
/* ... altre variabili */
--background-opaque: #00000033;
--muted-color: #6b7280;
--lateral-margin: 12rem;
}
:root[data-theme='dark'] {
--primary-color: #4983F2;
--text-color: #e6eef8;
--primary-color: #4983F2; /* Blu chiaro */
--text-color: #e6eef8; /* Grigio molto chiaro */
--surface: #000;
--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
<div style="color: var(--text-color); background: var(--surface);">
Contenuto che segue il tema
Contenuto automaticamente theme-aware
</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
<!-- Button base -->
<Button text="Click me" />
<Header />
```
<!-- Button con link -->
<Button text="HOME" href="/" />
**Modifiche in Header.svelte:**
- 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
text="Custom Button"
href="/"
borderWidth="2px"
/>
<!-- Stili avanzati -->
<Button
text="Premium"
color="var(--primary-color)"
textColor="var(--surface)"
text="PRENOTA UNA CALL"
href="/contatti"
borderWidth="1.5px"
borderColor="var(--text-color)"
color="transparent"
textColor="var(--text-color)"
padding="12px 32px"
round="12px"
borderWidth="1px"
round="5px"
margin="1rem 0 0"
bold
/>
```
**Prop disponibili:**
- `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'`)
**Tutte le prop:**
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`.
### 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:
1. **Main** — Descrizione generica + bottoni accetta/rifiuta/personalizza
2. **Prefs** — Toggle per tre categorie: Tecnici (always on), Analitici, Profilazione
```svelte
import MyPage from "./pages/MyPage.svelte"
<CookiePopUp />
```
const getRoute = (path) => {
// ...
if (normalized === '/my-page') return 'mypage'
// ...
**Varianti layout:**
- **Desktop** (> 900px): Fixed al bottom, full-width
- **Tablet** (768900px): 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
{:else if currentRoute === 'mypage'}
<MyPage />
### Gestione Categoria Cookie
```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
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
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
- **Svelte** - Framework UI
- **Vite** - Build tool e dev server
- **@sveltejs/vite-plugin-svelte** - Plugin Svelte per Vite
```json
{
"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
- **Vite** - Build tool moderno e veloce
- **CSS3** - Styling con variabili CSS
- **Vanilla JS** - JavaScript puro
## 🎓 Tecnologie & Approcci
| Aspetto | Soluzione |
|---------|----------|
| **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
Progetto proprietario © CIMA PROGETTI
Progetto proprietario © **CIMA PROGETTI**
+9
View File
@@ -5,6 +5,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<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>
<body>
<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
View File
@@ -1,11 +1,10 @@
<script>
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 Header from "./components/Header/Header.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 ServerError from "./pages/500.svelte";
import ErrorGeneric from "./pages/errore.svelte";
@@ -41,6 +40,9 @@
}
onMount(() => {
console.log("Svelte App.svelte mounted!");
console.log("Current pathname:", pathname);
console.log("Current route:", currentRoute);
initializeStoredConsent()
try {
@@ -80,8 +82,7 @@
<Header />
<main class:center-content={isCenteredMain}>
{#if currentRoute === 'home'}
<Hero />
<Services />
<IndexPage />
{:else if currentRoute === 'contacts'}
<Contacts />
{:else if currentRoute === '403'}
+28 -2
View File
@@ -2,21 +2,34 @@
display: inline-flex;
align-items: center;
justify-content: center;
width: fit-content;
inline-size: fit-content;
background-color: var(--button-color);
color: var(--button-text-color);
padding: var(--button-padding);
margin: var(--button-margin, 0);
border-radius: var(--button-radius);
font: inherit;
cursor: pointer;
font-weight: bold;
font-weight: normal;
transition: transform 0.2s, opacity 0.2s, background-color 0.2s, box-shadow 0.2s;
text-decoration: 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;
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 {
opacity: 0.92;
transform: translateY(-1px);
@@ -30,3 +43,16 @@
outline: 2px solid var(--primary-color);
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 -3
View File
@@ -8,8 +8,11 @@
textColor = '#fff',
round = '5px',
padding = '10px 20px',
margin = '0',
href = null,
borderWidth = '0px',
borderColor = 'var(--primary-color)',
fullWidth = false,
...restProps
} = $props()
@@ -25,9 +28,10 @@
{#if href}
<a
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}
on:click={handleClick}
onclick={handleClick}
{...restProps}
>
{text}
@@ -35,7 +39,8 @@
{:else}
<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}
>
{text}
+134 -75
View File
@@ -1,9 +1,9 @@
.cookie-backdrop {
position: fixed;
inset: 0;
z-index: 199;
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(2px);
position: fixed;
inset: 0;
z-index: 199;
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(2px);
}
.cookie-popup {
@@ -11,29 +11,42 @@
justify-content: space-between;
align-items: center;
gap: 50px;
z-index: 200;
position: fixed;
left: 50%;
bottom: 25px;
transform: translateX(-50%);
width: min(100%, 1100px);
max-width: calc(100% - 25px * 2);
padding: 50px;
border-radius: 5px;
background-color: var(--surface);
color: var(--text-color);
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.18);
border: 2px solid rgba(127, 127, 127, 0.3);
z-index: 200;
position: fixed;
left: 50%;
bottom: 25px;
transform: translateX(-50%);
width: min(100%, 1100px);
max-width: calc(100% - 25px * 2);
max-height: calc(100vh - 50px);
overflow-y: auto;
padding: 50px;
border-radius: 5px;
background-color: var(--surface);
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 {
margin: 15px 0 15px;
margin: 15px 0 15px;
}
.cookie-popup-title {
display: flex;
align-items: center;
gap: 20px;
display: flex;
align-items: center;
gap: 20px;
}
.cookie-popup-title h2 {
@@ -42,44 +55,44 @@
}
.cookie-icon {
width: 30px;
height: 30px;
display: inline-block;
background-color: var(--text-color);
-webkit-mask-image: url('./assets/cookie.svg');
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
-webkit-mask-size: contain;
mask-image: url('./assets/cookie.svg');
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
transition: background-color 0.2s ease;
width: 30px;
height: 30px;
display: inline-block;
background-color: var(--text-color);
-webkit-mask-image: url('./assets/cookie.svg');
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
-webkit-mask-size: contain;
mask-image: url('./assets/cookie.svg');
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
transition: background-color 0.2s ease;
}
.cookie-popup-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 50px;
flex-wrap: nowrap;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 50px;
flex-wrap: nowrap;
}
.cookie-link {
color: var(--primary-color);
text-decoration: none;
color: var(--primary-color);
text-decoration: none;
cursor: pointer;
}
.cookie-link-button {
background: transparent;
border: 0;
padding: 0;
font: inherit;
background: transparent;
border: 0;
padding: 0;
font: inherit;
}
.cookie-link:hover {
text-decoration: underline;
text-decoration: underline;
}
.cookie-popup-buttons {
@@ -90,14 +103,12 @@
.cookie-popup-buttons .button {
white-space: nowrap;
flex-shrink: 0;
min-width: fit-content;
}
.cookie-toggle-item {
cursor: pointer;
display: flex;
align-items: center;
align-items: center;
padding: 8px 0;
margin: 0;
}
@@ -115,36 +126,84 @@
}
.cookie-toggle-container {
display: flex;
flex-wrap: wrap;
gap: 50px;
display: flex;
flex-wrap: wrap;
gap: 0 50px;
margin-bottom: 12px;
}
/* toggle item styling moved to Toggle.css */
/* ============================================================
MEDIA QUERIES
============================================================ */
@media (max-width: 640px) {
.cookie-popup {
bottom: 12px;
padding: 16px;
flex-direction: column;
align-items: stretch;
gap: 16px;
}
@media (max-width: 1280px) {}
.cookie-popup-actions {
justify-content: stretch;
gap: 12px;
flex-direction: column;
}
@media (max-width: 1024px) {
.cookie-popup-buttons {
flex-wrap: wrap;
flex-direction: column;
}
.cookie-popup-actions .button,
.cookie-link {
flex: 1 1 100%;
text-align: center;
}
.cookie-popup-buttons .button {
width: 100%;
flex: none;
}
.cookie-link-button {
padding: 10px 0;
}
.cookie-popup-buttons .button {
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;
}
}
+3 -2
View File
@@ -1,5 +1,5 @@
.footer p, .footer a, .footer h4 {
font-size: 0.7rem;
font-size: 0.9rem;
margin: 0;
}
@@ -176,7 +176,7 @@
color: var(--muted-color);
}
@media (max-width: 768px) {
@media (max-width: 900px) {
.footer {
padding: 40px 20px 20px;
}
@@ -198,5 +198,6 @@
.footer-section-info-container {
flex-direction: column;
gap: 24px;
margin-right: 0;
}
}
+4 -4
View File
@@ -19,28 +19,28 @@
<p>Scoprirci anche sui nostri canali social</p>
<div class="footer-links">
<a
href="https://www.facebook.com/cimaprogetti"
href="https://www.facebook.com/profile.php?id=61587060986122"
target="_blank"
rel="noopener noreferrer">
<span class="social-icon icon-facebook" aria-hidden="true"></span>
<span class="sr-only">Facebook</span>
</a>
<a
href="https://www.instagram.com/cimaprogetti"
href="https://www.instagram.com/cimaprogetti/"
target="_blank"
rel="noopener noreferrer">
<span class="social-icon icon-instagram" aria-hidden="true"></span>
<span class="sr-only">Instagram</span>
</a>
<a
href="https://www.linkedin.com/company/cimaprogetti"
href="https://www.linkedin.com/company/cima-progetti-srls/"
target="_blank"
rel="noopener noreferrer">
<span class="social-icon icon-linkedin" aria-hidden="true"></span>
<span class="sr-only">LinkedIn</span>
</a>
<a
href="https://www.tiktok.com/@cimaprogetti"
href="https://www.tiktok.com/@cima.progetti"
target="_blank"
rel="noopener noreferrer">
<span class="social-icon icon-tiktok" aria-hidden="true"></span>
+222 -25
View File
@@ -1,3 +1,7 @@
/* ============================================================
HEADER — struttura base
============================================================ */
.header {
position: fixed;
top: 0;
@@ -5,7 +9,18 @@
width: 100%;
z-index: 100;
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 {
@@ -13,15 +28,18 @@
}
.header-container {
max-width: 1000px;
margin: 0 auto;
padding: 0 20px;
margin: 0 var(--lateral-margin);
display: flex;
align-items: center;
height: var(--navbar-height);
justify-content: space-between;
}
.logo-img{
/* ============================================================
LOGO
============================================================ */
.logo-img {
width: 98px;
height: 70px;
margin-right: auto;
@@ -30,7 +48,7 @@
}
.logo-mask {
background-color: var(--text-color);
background-color: var(--primary-color);
-webkit-mask-image: url('/images/icons/logo-nero.svg');
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
@@ -45,18 +63,16 @@
transform: translateY(-1px);
}
/* ============================================================
NAV DESKTOP
============================================================ */
.nav {
display: flex;
gap: 30px;
margin: 0 auto;
}
.header-actions {
display: flex;
gap: 12px;
align-items: center;
}
.nav a {
text-decoration: none;
color: var(--text-color);
@@ -74,22 +90,203 @@
color: var(--primary-color);
}
/*Temporaneo*/
@media (max-width: 768px) {
.header-container {
flex-direction: column;
height: auto;
padding: 15px 20px;
/* ============================================================
HEADER ACTIONS (desktop)
============================================================ */
.header-actions {
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 {
gap: 15px;
width: 100%;
justify-content: center;
flex-wrap: wrap;
.mobile-menu.open {
clip-path: inset(0 0 0 0%);
transition: clip-path 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.nav a {
font-size: 14px;
.mobile-menu-nav a {
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;
}
+95 -19
View File
@@ -6,27 +6,41 @@
let activeNav = $state(typeof window !== 'undefined' ? window.location.pathname : '/')
let isHidden = $state(false)
let isMenuOpen = $state(false)
let hasBackground = $state(false)
let lastScrollY = 0
const SCROLL_DELTA = 8
const TOP_OFFSET = 24
const BG_THRESHOLD = 100
function handleNavClick(event, path) {
if (!isInternalPath(path)) return
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return
event.preventDefault()
isMenuOpen = false
navigateTo(path)
}
function toggleMenu() {
isMenuOpen = !isMenuOpen
}
function handleScroll() {
const currentScrollY = window.scrollY || 0
const diff = currentScrollY - lastScrollY
// Aggiungi sfondo quando scrolli oltre il threshold
hasBackground = currentScrollY > BG_THRESHOLD
if (currentScrollY <= TOP_OFFSET) {
isHidden = false
hasBackground = false
} else if (diff > SCROLL_DELTA) {
isHidden = true
hasBackground = false
isMenuOpen = false
} else if (diff < -SCROLL_DELTA) {
isHidden = false
}
@@ -50,49 +64,111 @@
})
</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="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>
</a>
</div>
<nav class="nav">
<!-- Nav desktop -->
<nav class="nav nav-desktop" aria-label="Navigazione principale">
<a
href="/servizi"
class:active={activeNav === '/servizi'}
on:click={(e) => handleNavClick(e, '/servizi')}
onclick={(e) => handleNavClick(e, '/servizi')}
aria-current={activeNav === '/servizi' ? 'page' : undefined}
>
Servizi
</a>
>Servizi</a>
<a
href="/metodo"
class:active={activeNav === '/metodo'}
on:click={(e) => handleNavClick(e, '/metodo')}
onclick={(e) => handleNavClick(e, '/metodo')}
aria-current={activeNav === '/metodo' ? 'page' : undefined}
>
Il metodo
</a>
>Il metodo</a>
<a
href="/progetti"
class:active={activeNav === '/progetti'}
on:click={(e) => handleNavClick(e, '/progetti')}
onclick={(e) => handleNavClick(e, '/progetti')}
aria-current={activeNav === '/progetti' ? 'page' : undefined}
>
Progetti
</a>
>Progetti</a>
<a
href="/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}
>
Chi Siamo
</a>
>Chi Siamo</a>
</nav>
<div class="header-actions">
<Button text="Contattaci" href="/contatti" />
</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>
</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>
-111
View File
@@ -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;
}
}
-11
View File
@@ -1,11 +0,0 @@
<script>
import './Hero.css'
</script>
<section class="hero">
<div class="hero-container">
<div class="hero-content">
</div>
</div>
</section>
-68
View File
@@ -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;
}
}
-45
View File
@@ -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
View File
@@ -2,8 +2,24 @@ import './styles/global.css'
import { mount } from 'svelte'
import App from './App.svelte'
const app = mount(App, {
target: document.getElementById('app'),
})
// Global error trap to catch syntax errors before app mount
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
+105 -21
View File
@@ -1,26 +1,110 @@
<script>
import "../styles/contacts.css";
import Button from "../components/Button/Button.svelte";
</script>
<div class="contacts-page">
<h1>Contatti</h1>
<p>Pagina contatti - contenuto da aggiungere</p>
<div class="contact-us">
<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>
<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>
+9
View File
@@ -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>
View File
+169
View File
@@ -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
View File
@@ -1,16 +1,12 @@
.error-page {
min-height: 100vh;
min-height: 100dvh;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.error-page h1 {
margin: 0;
font-size: 3rem;
}
.error-content {
display: flex;
flex-direction: column;
@@ -41,6 +37,7 @@
flex-direction: column;
align-items: flex-start;
line-height: 0.9;
transform: translateX(1rem);
}
.error-page .title-top {
@@ -52,15 +49,6 @@
position: relative;
}
.divider {
display: block;
position: relative;
width: 50%;
height: 5px;
background-color: var(--primary-color);
margin-top: 8px;
}
.error-message,
.error-solution {
text-align: center;
@@ -79,12 +67,32 @@
}
.error-code {
font-size: 4rem;
font-size: clamp(1rem, 5vw, 4rem);
line-height: 0.4;
}
.error-text {
font-size: 15rem !important;
font-size: clamp(5rem, 20vw, 15rem) !important;
opacity: 0.2;
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
View File
@@ -5,7 +5,14 @@
--muted-color: #6b7280;
--surface: #ffffff;
--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>) */
@@ -16,6 +23,15 @@
--surface: #000;
--background: rgba(0, 0, 0, 0.8);
--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';
color: var(--text-color);
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;
position: relative;
}
@@ -37,19 +53,15 @@ body {
html,
body,
#app {
height: 100%;
position: relative;
z-index: 1;
}
/* Background overlay layer that follows theme color */
body::before {
content: "";
position: fixed;
inset: 0;
background-color: var(--background);
pointer-events: none;
z-index: 0;
main {
min-height: 100vh;
min-height: 100dvh;
display: flow-root;
}
/* Reusable theme asset helpers:
@@ -84,6 +96,8 @@ html[data-theme='dark'] {
}
h1 {
margin: 0;
font-size: 3rem;
font-family: 'IBM Plex Mono', monospace;
}
@@ -107,7 +121,42 @@ p, a {
transition: background-color 0.2s ease;
}
/* Usage: add data attribute with SVG path
Example: <div class="svg-mask" style="--mask-url: url('/path/to/icon.svg'); width: 40px; height: 40px;"></div>
Then use inline style: -webkit-mask-image: var(--mask-url); mask-image: var(--mask-url);
OR use a specific class that sets the mask-image */
.divider {
display: block;
position: relative;
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) {
}
+7
View File
@@ -0,0 +1,7 @@
.index-page {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
text-decoration: underline;
}
+2 -9
View File
@@ -3,16 +3,9 @@ import { svelte } from '@sveltejs/vite-plugin-svelte'
export default defineConfig(() => ({
plugins: [svelte()],
appType: 'spa',
server: {
host: '0.0.0.0',
port: 5173,
middlewares: [
(req, res, next) => {
if (req.url === '/' || req.url.match(/\.\w+$/) || req.url.includes('/public/')) {
return next()
}
req.url = '/'
next()
}
]
}
}))