b913a2bf90
Refs gitea:Adriano/AI-OS#310 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
14288 lines
582 KiB
Diff
14288 lines
582 KiB
Diff
# Review package: 063972d..HEAD
|
||
|
||
## Commits
|
||
29d633c chore: robots, favicon e smoke test
|
||
f0ad162 feat: area admin con editor tiptap, api crud e upload immagini
|
||
f3fb2fe feat: blog pubblico con filtri, paginazione e articolo
|
||
e984010 feat: pagina contatti e api email con validazione testata
|
||
be4d933 feat: pagine programmi con dettaglio e pricing performance class
|
||
ed35491 feat: pagina training
|
||
c4cb05e fix: immagine sezione about a risoluzione adeguata
|
||
ba89568 feat: pagina about
|
||
45e5214 feat: homepage completa da design Figma
|
||
2fdd1b9 feat: layout base con header, footer e 404
|
||
ac75720 fix: apostrofi tipografici nei dati statici
|
||
50ce224 feat: dati statici contenuti da design Figma
|
||
6caba6f feat: autenticazione a sessioni, middleware admin e script create-user
|
||
d3b5e67 feat: layer sqlite e repository posts con test
|
||
42d603b feat: slugify, rate limit ed env helper con test
|
||
c9a9ff9 feat: design tokens e css globale
|
||
4445c42 fix: loghi insanitylab estratti dal pdf figma al posto dei loghi template
|
||
810e5d8 fix: limite altezza in optimize-assets, pesi immagini e report dimensioni
|
||
551957e fix: pipeline asset riproducibile con script di ottimizzazione
|
||
d8ef59e feat: asset immagini estratti da Figma e mappati
|
||
5f7ed1f chore: scaffold progetto Astro 5 con adapter node
|
||
9780e51 Aggiunge piano implementazione sito
|
||
|
||
## Files changed
|
||
.env.example | 8 +
|
||
.gitignore | 8 +
|
||
.superpowers/sdd/task-2-report.md | 247 +
|
||
README.md | 137 +
|
||
astro.config.mjs | 10 +
|
||
data/.gitkeep | 0
|
||
.../plans/2026-07-01-insanitylab-website.md | 3513 ++++++++++
|
||
package-lock.json | 6875 ++++++++++++++++++++
|
||
package.json | 38 +
|
||
public/favicon.svg | 4 +
|
||
public/img/logo-dark.png | Bin 0 -> 26648 bytes
|
||
public/img/logo-light.png | Bin 0 -> 36949 bytes
|
||
public/robots.txt | 4 +
|
||
scripts/apply-asset-map.mjs | 42 +
|
||
scripts/asset-map.json | 37 +
|
||
scripts/create-user.mjs | 23 +
|
||
scripts/extract-fig-images.sh | 17 +
|
||
scripts/extract-logos.sh | 53 +
|
||
scripts/optimize-assets.sh | 40 +
|
||
scripts/smoke.sh | 17 +
|
||
src/assets/img/about-exterior.jpg | Bin 0 -> 190714 bytes
|
||
src/assets/img/about-team-group.jpg | Bin 0 -> 25553 bytes
|
||
src/assets/img/about-trainer-1.jpg | Bin 0 -> 148343 bytes
|
||
src/assets/img/about-trainer-2.jpg | Bin 0 -> 135795 bytes
|
||
src/assets/img/about-trainer-3.jpg | Bin 0 -> 118975 bytes
|
||
src/assets/img/about-trainer-4.jpg | Bin 0 -> 162617 bytes
|
||
src/assets/img/about-training-group.jpg | Bin 0 -> 143401 bytes
|
||
src/assets/img/feature-balance.jpg | Bin 0 -> 101290 bytes
|
||
src/assets/img/feature-longevity.jpg | Bin 0 -> 25258 bytes
|
||
src/assets/img/feature-performance.jpg | Bin 0 -> 79834 bytes
|
||
src/assets/img/hero-1.jpg | Bin 0 -> 97196 bytes
|
||
src/assets/img/info-balance.jpg | Bin 0 -> 136394 bytes
|
||
src/assets/img/info-longevity.jpg | Bin 0 -> 252166 bytes
|
||
src/assets/img/info-performance.jpg | Bin 0 -> 260295 bytes
|
||
src/assets/img/method-step-1.jpg | Bin 0 -> 105410 bytes
|
||
src/assets/img/method-step-2.jpg | Bin 0 -> 175977 bytes
|
||
src/assets/img/method-step-3.jpg | Bin 0 -> 11290 bytes
|
||
src/assets/img/method-step-4.jpg | Bin 0 -> 30346 bytes
|
||
src/assets/img/partner-aon.png | Bin 0 -> 8049 bytes
|
||
src/assets/img/partner-edenred.png | Bin 0 -> 6875 bytes
|
||
src/assets/img/partner-howden.png | Bin 0 -> 3756 bytes
|
||
src/assets/img/partner-welbee.png | Bin 0 -> 12423 bytes
|
||
src/assets/img/quote-bg.jpg | Bin 0 -> 469103 bytes
|
||
src/assets/img/team-antonello.jpg | Bin 0 -> 149301 bytes
|
||
src/assets/img/team-donata.jpg | Bin 0 -> 138907 bytes
|
||
src/assets/img/team-eleonora.jpg | Bin 0 -> 145996 bytes
|
||
src/assets/img/team-nicola.jpg | Bin 0 -> 171336 bytes
|
||
src/assets/img/training-fit-remote.jpg | Bin 0 -> 38375 bytes
|
||
src/assets/img/training-one-to-one.jpg | Bin 0 -> 38837 bytes
|
||
src/assets/img/training-performance-class.jpg | Bin 0 -> 242256 bytes
|
||
src/assets/img/training-small-groups.jpg | Bin 0 -> 142807 bytes
|
||
src/components/Agenda.astro | 54 +
|
||
src/components/ContactForm.astro | 42 +
|
||
src/components/Footer.astro | 49 +
|
||
src/components/Header.astro | 45 +
|
||
src/components/PageHero.astro | 15 +
|
||
src/components/about/Timeline.astro | 34 +
|
||
src/components/admin/PostForm.astro | 136 +
|
||
src/components/blog/Sidebar.astro | 26 +
|
||
src/components/home/FeatureBlocks.astro | 35 +
|
||
src/components/home/HeroSlider.astro | 54 +
|
||
src/components/home/InfoColumns.astro | 26 +
|
||
src/components/home/MethodSteps.astro | 39 +
|
||
src/components/home/PartnersStrip.astro | 18 +
|
||
src/components/home/ProgramsGrid.astro | 38 +
|
||
src/components/home/QuoteBanner.astro | 22 +
|
||
src/components/home/TeamSection.astro | 34 +
|
||
src/components/home/TrainingCards.astro | 42 +
|
||
src/data/agenda.ts | 36 +
|
||
src/data/method.ts | 9 +
|
||
src/data/partners.ts | 6 +
|
||
src/data/pricing.ts | 17 +
|
||
src/data/programs.ts | 104 +
|
||
src/data/site.ts | 34 +
|
||
src/data/team.ts | 24 +
|
||
src/data/trainings.ts | 6 +
|
||
src/env.d.ts | 7 +
|
||
src/layouts/Admin.astro | 36 +
|
||
src/layouts/Base.astro | 33 +
|
||
src/lib/auth.ts | 48 +
|
||
src/lib/blog-const.ts | 6 +
|
||
src/lib/contact.ts | 31 +
|
||
src/lib/db.ts | 47 +
|
||
src/lib/env.ts | 5 +
|
||
src/lib/mailer.ts | 25 +
|
||
src/lib/post-body.ts | 21 +
|
||
src/lib/posts.ts | 74 +
|
||
src/lib/rate-limit.ts | 17 +
|
||
src/lib/sanitize.ts | 11 +
|
||
src/lib/slug.ts | 8 +
|
||
src/middleware.ts | 24 +
|
||
src/pages/404.astro | 14 +
|
||
src/pages/about.astro | 166 +
|
||
src/pages/admin/edit/[id].astro | 14 +
|
||
src/pages/admin/index.astro | 37 +
|
||
src/pages/admin/login.astro | 34 +
|
||
src/pages/admin/logout.ts | 11 +
|
||
src/pages/admin/new.astro | 10 +
|
||
src/pages/api/admin/posts/[id].ts | 29 +
|
||
src/pages/api/admin/posts/index.ts | 22 +
|
||
src/pages/api/admin/upload.ts | 26 +
|
||
src/pages/api/contact.ts | 26 +
|
||
src/pages/blog/[slug].astro | 33 +
|
||
src/pages/blog/index.astro | 81 +
|
||
src/pages/contact.astro | 55 +
|
||
src/pages/index.astro | 32 +
|
||
src/pages/programs/[slug].astro | 98 +
|
||
src/pages/programs/index.astro | 38 +
|
||
src/pages/training.astro | 57 +
|
||
src/pages/uploads/[...path].ts | 20 +
|
||
src/styles/global.css | 54 +
|
||
tests/auth.test.ts | 45 +
|
||
tests/contact.test.ts | 33 +
|
||
tests/posts.test.ts | 74 +
|
||
tests/rate-limit.test.ts | 23 +
|
||
tests/sanitize.test.ts | 16 +
|
||
tests/slug.test.ts | 20 +
|
||
tsconfig.json | 5 +
|
||
uploads/.gitkeep | 0
|
||
vitest.config.ts | 5 +
|
||
120 files changed, 13489 insertions(+)
|
||
|
||
## Diff
|
||
diff --git a/.env.example b/.env.example
|
||
new file mode 100644
|
||
index 0000000..7b2033f
|
||
--- /dev/null
|
||
+++ b/.env.example
|
||
@@ -0,0 +1,8 @@
|
||
+SMTP_HOST=smtp.example.com
|
||
+SMTP_PORT=587
|
||
+SMTP_USER=user
|
||
+SMTP_PASS=pass
|
||
+CONTACT_TO=info@insanitylab.it
|
||
+CONTACT_FROM=sito@insanitylab.it
|
||
+DB_PATH=data/insanitylab.db
|
||
+UPLOADS_DIR=uploads
|
||
diff --git a/.gitignore b/.gitignore
|
||
new file mode 100644
|
||
index 0000000..68a080b
|
||
--- /dev/null
|
||
+++ b/.gitignore
|
||
@@ -0,0 +1,8 @@
|
||
+node_modules/
|
||
+dist/
|
||
+.astro/
|
||
+.env
|
||
+data/*.db*
|
||
+uploads/*
|
||
+!uploads/.gitkeep
|
||
+assets-src/
|
||
diff --git a/.superpowers/sdd/task-2-report.md b/.superpowers/sdd/task-2-report.md
|
||
new file mode 100644
|
||
index 0000000..6318927
|
||
--- /dev/null
|
||
+++ b/.superpowers/sdd/task-2-report.md
|
||
@@ -0,0 +1,247 @@
|
||
+# Task 2 Report — Estrazione e mappatura asset immagine da Figma
|
||
+
|
||
+**Data:** 2026-07-01
|
||
+**Fonte:** `../FigmaDoc/Web Insanity.fig` (60 immagini uniche + ~66 duplicati = 126 estratte)
|
||
+**Destinazione:** `src/assets/img/` (31 file) + `public/img/` (2 loghi)
|
||
+
|
||
+---
|
||
+
|
||
+## Mappature completate
|
||
+
|
||
+### Hero
|
||
+| File destinazione | Hash sorgente | Descrizione |
|
||
+|---|---|---|
|
||
+| `hero-1.jpg` | `8b71f158...` | Uomo con palla medica (medicine ball) che la tiene verso donna in posizione crunch, palestra scura — "uomo con palla medica + donna" ✓ |
|
||
+
|
||
+### Info sezioni (PERFORMANCE / BALANCE / LONGEVITY)
|
||
+| File destinazione | Hash sorgente | Descrizione |
|
||
+|---|---|---|
|
||
+| `info-performance.jpg` | `0f803439...` | Uomo muscoloso in planche su sfondo grigio neutro |
|
||
+| `info-balance.jpg` | `432d397a...` | Due donne sedute schiena a schiena in abiti sportivi beige/neutri |
|
||
+| `info-longevity.jpg` | `d7fcf0aa...` | Donna anziana in posizione push-up — longevità attiva |
|
||
+
|
||
+### Feature sezioni (immagini più grandi per ciascun pilastro)
|
||
+| File destinazione | Hash sorgente | Descrizione |
|
||
+|---|---|---|
|
||
+| `feature-performance.jpg` | `6d602573...` | Uomo con chalk sulle mani — preparazione sollevamento pesi, performance atletica |
|
||
+| `feature-balance.jpg` | `16e611de...` | Gruppo yoga all'aperto al tramonto — equilibrio e benessere |
|
||
+| `feature-longevity.jpg` | `98f1f8d2...` | Donna anziana in piedi con bottiglia d'acqua, outfit sportivo beige — longevità sana |
|
||
+
|
||
+### Quote background
|
||
+| File destinazione | Hash sorgente | Descrizione |
|
||
+|---|---|---|
|
||
+| `quote-bg.jpg` | `28e8157d...` | Scritta "PERFORMANCE. BALANCE. LONGEVITY." su muro di intonaco — elemento visivo identitario |
|
||
+
|
||
+### Training programs
|
||
+| File destinazione | Hash sorgente | Descrizione |
|
||
+|---|---|---|
|
||
+| `training-one-to-one.jpg` | `85808a0c...` | Trainer e cliente a colloquio in palestra, tablet in mano — consulenza personalizzata |
|
||
+| `training-small-groups.jpg` | `bb632b8d...` | Due donne con manubri leggeri in posizione atletica — small group training |
|
||
+| `training-performance-class.jpg` | `91aa82b5...` | Grande gruppo in sala fitness/danza con palle fitball — performance class |
|
||
+| `training-fit-remote.jpg` | `86c1363b...` | Donna a casa sul tappetino con laptop — allenamento remoto online |
|
||
+
|
||
+### Team Trainer (homepage — 4 ritratti luce dorata)
|
||
+| File destinazione | Hash sorgente | Persona identificata |
|
||
+|---|---|---|
|
||
+| `team-donata.jpg` | `0b6cf694...` | Donna capelli lunghi scuri dritti, luce dorada laterale — Donata ✓ |
|
||
+| `team-nicola.jpg` | `1cc33c41...` | Uomo barba folta, turtleneck scuro, luce dorata — Nicola ✓ |
|
||
+| `team-eleonora.jpg` | `a75ca105...` | Donna capelli biondi/chiari, stessa luce dorata — Eleonora ✓ |
|
||
+| `team-antonello.jpg` | `e3c84f99...` | Uomo occhiali tondi, capelli rasatissimi, luce dorata — Antonello ✓ |
|
||
+
|
||
+### Il Metodo (4 passi circolari)
|
||
+| File destinazione | Hash sorgente | Step identificato |
|
||
+|---|---|---|
|
||
+| `method-step-1.jpg` | `88cfcd5f...` | Calibro plicometro su addome — valutazione composizione corporea |
|
||
+| `method-step-2.jpg` | `d21671bb...` | Donna in cucina con verdure fresche e metro — pianificazione nutrizionale |
|
||
+| `method-step-3.jpg` | `a3e84d7d...` | Silhouette uomo in palestra in controluce — sessione allenamento |
|
||
+| `method-step-4.jpg` | `e50b6f78...` | Scarpe su tapis roulant — monitoraggio continuo e progressione |
|
||
+
|
||
+### Partner loghi
|
||
+| File destinazione | Hash sorgente | Logo identificato |
|
||
+|---|---|---|
|
||
+| `partner-edenred.png` | `bee22f37...` | "Ticket Welfare Edenred" — logo rosso/blu su sfondo trasparente ✓ |
|
||
+| `partner-welbee.png` | `cf079250...` | "welbee" — logo verde su sfondo bianco ✓ |
|
||
+| `partner-aon.png` | `014a7708...` | "Aon" — logo rosso su sfondo bianco ✓ |
|
||
+| `partner-howden.png` | `872c3679...` | "HOWDEN" — logo blu su sfondo bianco ✓ |
|
||
+
|
||
+### Loghi InsanityLab (public/img/)
|
||
+| File destinazione | Sorgente | Note |
|
||
+|---|---|---|
|
||
+| `logo-dark.png` | `../Dati/logo-dark.png` | **Fallback** — logo non presente nel file Figma |
|
||
+| `logo-light.png` | `../Dati/logo-light.png` | **Fallback** — logo non presente nel file Figma |
|
||
+
|
||
+### About page (bonus — non richiesti dal brief)
|
||
+| File destinazione | Hash sorgente | Descrizione |
|
||
+|---|---|---|
|
||
+| `about-exterior.jpg` | `8ffc5f7a...` | Facciata esterna palestra/studio InsanityLab, sera |
|
||
+| `about-team-group.jpg` | `32ecd00a...` | Foto di gruppo team InsanityLab in bianco e nero |
|
||
+| `about-training-group.jpg` | `d4608b10...` | Gruppo che salta con kettlebell in sala beige |
|
||
+| `about-trainer-1.jpg` | `dafcc193...` | Trainer donna capelli scuri ondulati, luce dorata |
|
||
+| `about-trainer-2.jpg` | `9c50f1d3...` | Trainer donna capelli scuri più corti, matura, luce dorata |
|
||
+| `about-trainer-3.jpg` | `c51a49b9...` | Trainer donna capelli neri lisci, sorriso, sfondo chiaro |
|
||
+| `about-trainer-4.jpg` | `3341a11b...` | Trainer uomo giovane senza barba, turtleneck scuro |
|
||
+
|
||
+---
|
||
+
|
||
+## Immagini attese ma non trovate nel Figma
|
||
+
|
||
+| Nome atteso | Motivo mancanza | Azione intrapresa |
|
||
+|---|---|---|
|
||
+| `logo-dark.png` | Assente in `Web Insanity.fig` | Fallback da `../Dati/logo-dark.png` ✓ |
|
||
+| `logo-light.png` | Assente in `Web Insanity.fig` | Fallback da `../Dati/logo-light.png` ✓ |
|
||
+
|
||
+Tutti e 4 i loghi partner sono stati trovati nel Figma. Nessuna altra immagine richiesta dal brief risulta mancante.
|
||
+
|
||
+---
|
||
+
|
||
+## Dimensioni finali
|
||
+
|
||
+```
|
||
+src/assets/img/ 3.7 MB (31 file JPG + 4 PNG partner loghi)
|
||
+public/img/ ~3.4 KB (2 PNG logo: logo-dark.png 1.7 KB, logo-light.png 1.7 KB)
|
||
+```
|
||
+
|
||
+Tutte le immagini sono ≤ 1400 px larghezza e ≤ 1600 px altezza. Il file più pesante è quote-bg.jpg a 460 KB. Nessun file supera 500 KB.
|
||
+
|
||
+*Aggiornato dopo fix round 2 — i numeri precedenti (3.4 MB / nessun file >400 KB) erano errati: la pipeline originale non vincolava l'altezza, lasciando immagini portrait a 2-3 MB ciascuna.*
|
||
+
|
||
+---
|
||
+
|
||
+## Note tecnica — PNG → JPG
|
||
+
|
||
+Le foto trainer e training erano estratte dal Figma come PNG lossless (6–8 MB ciascuna). Convertite a JPG qualità 85 con resize a 1200 px max → risparmio ~98% per file. I loghi partner restano PNG per preservare sfondo trasparente.
|
||
+
|
||
+Il file `asset-map.json` referenzia gli hash originali (estensione .png/.jpg come estratti). I file di destinazione finali hanno tutti estensione .jpg (eccetto loghi partner .png), il che diverge dai nomi originali nel map ma i file in `src/assets/img/` riflettono il formato convertito.
|
||
+
|
||
+---
|
||
+
|
||
+## Conteggio
|
||
+
|
||
+- **Mappate e presenti:** 33 (31 in src/assets/img + 2 in public/img)
|
||
+- **Mancanti nel Figma:** 2 (logo-dark, logo-light) → risolti con fallback
|
||
+- **Immagini estratte totali:** 126 (inclusi ~66 duplicati)
|
||
+- **Lasciate in assets-src/ (gitignored):** 126 file sorgente
|
||
+
|
||
+---
|
||
+
|
||
+## Fix round 1
|
||
+
|
||
+**Data:** 2026-07-02
|
||
+
|
||
+### Cosa cambiato
|
||
+
|
||
+1. **`scripts/asset-map.json`** — rimossa sezione `"public/img"` (chiavi non-hash, rompeva pipeline dopo `extract-fig-images.sh`). Aggiunta sezione top-level `"fallbacks"` con struttura `{ "public/img": { "../Dati/logo-dark.png": "logo-dark.png", "../Dati/logo-light.png": "logo-light.png" } }`. Le destinazioni in `src/assets/img` mantengono l'estensione REALE del sorgente estratto da Figma (`.png` per PNG Figma, `.jpg` per JPEG già presenti in assets-src).
|
||
+
|
||
+2. **`scripts/apply-asset-map.mjs`** — aggiunto commento d'intestazione sull'ordine pipeline (extract → apply → optimize). Aggiunto loop `fallbacks`: copia da path relativo alla root del repo invece che da `assets-src/`, senza exit 1 per file assenti nel .fig.
|
||
+
|
||
+3. **`scripts/optimize-assets.sh`** (nuovo) — bash, `set -euo pipefail`. Converte tutti i PNG non-partner in `src/assets/img/` → JPG qualità 85 con resize `1400x>`, poi ridimensiona JPG già esistenti se larghezza > 1400px. Idempotente (nullglob su `.png`).
|
||
+
|
||
+### Output verifica riproducibilità
|
||
+
|
||
+```
|
||
+node scripts/apply-asset-map.mjs → "Copiate 33 immagini"
|
||
+./scripts/optimize-assets.sh → 16 PNG→JPG, 5 JPG ridimensionati (hero-1, info-balance, info-longevity, method-step-1, method-step-2)
|
||
+git status --short → elenco file identico al pre-fix (31 img + 4 PNG partner)
|
||
+ 18 file binari rigenerati (ricompressione diversa da conversione manuale originale)
|
||
+ → ricommittati: repo = output pipeline ✓
|
||
+```
|
||
+
|
||
+### Esito punto 4 — bassa risoluzione
|
||
+
|
||
+- **`method-step-3.jpg`** (184×276): sorgente in `assets-src/a3e84d7daf0b25c750ef694f4b9fc10bf0f2f1a8.jpg` è già 184×276 — stessa risoluzione del file committato. Nessuna versione ad alta risoluzione disponibile in `assets-src/`. **Richiede sorgente migliore dal cliente.**
|
||
+- **`about-team-group.jpg`** (270×480): sorgente in `assets-src/32ecd00a20321e58c700fbaf739e53ac3bf03876.png` è già 270×480. Nessuna versione ad alta risoluzione disponibile. **Richiede sorgente migliore dal cliente.**
|
||
+
|
||
+---
|
||
+
|
||
+## Fix round 2
|
||
+
|
||
+**Data:** 2026-07-02
|
||
+
|
||
+### Numeri finali
|
||
+
|
||
+```
|
||
+src/assets/img/ 3.7 MB totale (31 JPG + 4 PNG partner)
|
||
+```
|
||
+
|
||
+Top-5 per peso:
|
||
+| File | Dimensioni | Peso |
|
||
+|---|---|---|
|
||
+| quote-bg.jpg | 1087×1600 | 460 KB |
|
||
+| info-performance.jpg | 1200×1500 | 256 KB |
|
||
+| info-longevity.jpg | 1095×1600 | 248 KB |
|
||
+| training-performance-class.jpg | 1400×1400 | 240 KB |
|
||
+| about-exterior.jpg | 1400×781 | 188 KB |
|
||
+
|
||
+Nessun file supera 500 KB. Precedente info-longevity.jpg era 2.7 MB, info-balance.jpg 2.0 MB.
|
||
+
|
||
+### Cosa cambiato
|
||
+
|
||
+1. **`scripts/optimize-assets.sh`** — vincolo resize cambiato da `'1400x>'` (solo larghezza) a `'1400x1600>'` (larghezza E altezza). Qualità JPG da 85 a 83. Tutti i JPG già esistenti vengono ri-codificati a q83 con nuovo vincolo (loop separato, skip file appena convertiti per evitare doppia codifica). Risultato: portrait da 2-3 MB → 248-460 KB.
|
||
+
|
||
+2. **`scripts/asset-map.json`** — aggiunta chiave `"_nota"` in testa per documentare la conversione PNG→JPG operata da optimize-assets.sh (JSON non supporta commenti, chiave dedicata con prefisso `_`).
|
||
+
|
||
+3. **`scripts/apply-asset-map.mjs`** — aggiunto `destDir.startsWith('_')` nella condizione di skip, così le chiavi meta `_*` del JSON vengono ignorate nella pipeline.
|
||
+
|
||
+### Esito idempotenza
|
||
+
|
||
+Terza esecuzione `apply-asset-map.mjs + optimize-assets.sh`: md5sum su tutti i .jpg identico prima e dopo → **IDEMPOTENTE** ✓
|
||
+
|
||
+### Esito verifica loghi
|
||
+
|
||
+`public/img/logo-dark.png` (130×19, 1725 B) e `logo-light.png` (135×19, 1701 B) sono **identici** ai file sorgente in `../Dati/logo-dark.png` e `../Dati/logo-light.png` (stessa dimensione byte-level). Non sono placeholder: sono i loghi reali InsanityLab copiati dalla sezione `fallbacks` di asset-map.json tramite apply-asset-map.mjs. Le dimensioni in pixel ridotte (130×19 px) sono caratteristiche del PNG sorgente in Dati/, non di un errore di pipeline.
|
||
+
|
||
+---
|
||
+
|
||
+## Fix round 3
|
||
+
|
||
+**Data:** 2026-07-02
|
||
+
|
||
+### Problema chiuso
|
||
+
|
||
+`public/img/logo-dark.png` e `logo-light.png` contenevano i loghi del template "PROWESS++" copiati da `../Dati/` (sorgente errato). Sostituiti con i loghi reali "insanitylab" estratti vettorialmente dal PDF Figma.
|
||
+
|
||
+### Metodo
|
||
+
|
||
+Sorgente: `../FigmaDoc/Homepage.pdf` (1334×10460 pt, 1 pagina).
|
||
+
|
||
+Render esplorativi a 72–150 dpi per individuare le zone logo:
|
||
+- **Logo dark** (header, fondo bianco): riga nav in alto, x≈25pt, y≈10pt
|
||
+- **Logo light** (footer, fondo scuro `srgb(58,41,41)`): sezione footer, y≈8490pt
|
||
+
|
||
+Estrazione finale con `pdftoppm -r 300`, poi ImageMagick:
|
||
+- Dark: `-fuzz 8% -transparent white -trim -resize 600x -strip`
|
||
+- Light: `-crop 850x660+130+60` (esclude tagline e colonne destra), `-fuzz 12% -transparent 'srgb(58,41,41)' -trim -resize 600x -strip`
|
||
+
|
||
+`-strip` su entrambi garantisce idempotenza md5sum tra esecuzioni successive.
|
||
+
|
||
+### Coordinate pdftoppm a 300 dpi (pixel)
|
||
+
|
||
+| Logo | -x | -y | -W | -H | crop interno |
|
||
+|---|---|---|---|---|---|
|
||
+| dark | 104 | 41 | 895 | 250 | — |
|
||
+| light | 0 | 35375 | 1800 | 833 | `850x660+130+60` |
|
||
+
|
||
+### Dimensioni finali
|
||
+
|
||
+| File | Dimensioni | Tipo |
|
||
+|---|---|---|
|
||
+| `public/img/logo-dark.png` | **600×108 px** | TrueColorAlpha (PNG trasparente) |
|
||
+| `public/img/logo-light.png` | **600×342 px** | TrueColorAlpha (PNG trasparente) |
|
||
+
|
||
+### Aggiornamenti pipeline
|
||
+
|
||
+- Creato `scripts/extract-logos.sh` (bash, `set -euo pipefail`, coordinate fisse, idempotente)
|
||
+- `scripts/asset-map.json`: rimossa sezione `fallbacks` (loghi ora da extract-logos.sh, non da `../Dati`)
|
||
+- `scripts/apply-asset-map.mjs`: commento pipeline aggiornato a step 4
|
||
+- Ordine pipeline: `extract-fig-images.sh → apply-asset-map.mjs → optimize-assets.sh → extract-logos.sh`
|
||
+
|
||
+### Verifica visiva
|
||
+
|
||
+- `logo-dark.png`: icona onda/battito cardiaco beige + scritta "insanitylab" scura, layout orizzontale, sfondo trasparente. **Nessun residuo PROWESS++.**
|
||
+- `logo-light.png`: icona onda/battito cardiaco beige sopra, scritta "insanitylab" beige sotto, layout stacked, sfondo trasparente. **Nessun residuo PROWESS++.**
|
||
+
|
||
+### Idempotenza
|
||
+
|
||
+```
|
||
+md5sum run1 = md5sum run2 → IDEMPOTENTE ✓
|
||
+```
|
||
diff --git a/README.md b/README.md
|
||
new file mode 100644
|
||
index 0000000..118dd1b
|
||
--- /dev/null
|
||
+++ b/README.md
|
||
@@ -0,0 +1,137 @@
|
||
+# InsanityLab Website
|
||
+
|
||
+Sito vetrina per InsanityLab con blog integrato e form di contatti.
|
||
+
|
||
+## Descrizione
|
||
+
|
||
+Il progetto realizza un sito web per InsanityLab, combinando una vetrina aziendale con un sistema di blog gestito tramite SQLite e un form di contatti funzionante. L'architettura è costruita su **Astro 5** con adapter **Node.js** per deployment su server, garantendo performance ottimali e SEO-friendly pages.
|
||
+
|
||
+## Stack Tecnologico
|
||
+
|
||
+- **Framework**: Astro 5
|
||
+- **Runtime**: Node.js (adapter standalone)
|
||
+- **Database**: SQLite (better-sqlite3)
|
||
+- **Autenticazione**: bcryptjs
|
||
+- **Email**: Nodemailer
|
||
+- **Markup Editor**: TipTap (rich text)
|
||
+- **Font System**: @fontsource (Montserrat, Open Sans)
|
||
+- **Sitemap**: @astrojs/sitemap
|
||
+- **Testing**: Vitest
|
||
+- **Linguaggio**: TypeScript
|
||
+
|
||
+## Installazione
|
||
+
|
||
+### Prerequisiti
|
||
+
|
||
+- Node.js 18+ e npm
|
||
+
|
||
+### Setup
|
||
+
|
||
+```bash
|
||
+# Installa dipendenze
|
||
+npm install
|
||
+
|
||
+# Copia il template di configurazione env
|
||
+cp .env.example .env
|
||
+
|
||
+# Crea il primo utente amministratore (dopo aver configurato il DB)
|
||
+npm run create-user -- admin tuapassword
|
||
+```
|
||
+
|
||
+Modifica `.env` con le credenziali SMTP, percorsi database e upload directory secondo il tuo ambiente.
|
||
+
|
||
+## Comandi Disponibili
|
||
+
|
||
+| Comando | Descrizione |
|
||
+|---------|-------------|
|
||
+| `npm run dev` | Avvia il server di sviluppo (localhost:3000) |
|
||
+| `npm run build` | Compila il progetto per production |
|
||
+| `npm run preview` | Visualizza l'anteprima della build (locale) |
|
||
+| `npm run start` | Avvia il server production (richiede `npm run build` preliminare) |
|
||
+| `npm run test` | Esegui i test con Vitest |
|
||
+| `npm run create-user` | Crea un nuovo utente (admin o redattore) |
|
||
+
|
||
+## Struttura Progetto
|
||
+
|
||
+```
|
||
+insanitylab-website/
|
||
+├── src/
|
||
+│ ├── pages/ # Pagine Astro (routing automatico)
|
||
+│ ├── components/ # Componenti Astro riutilizzabili
|
||
+│ ├── layouts/ # Layout master
|
||
+│ ├── utils/ # Funzioni di utilità
|
||
+│ ├── env.d.ts # Type definitions globali
|
||
+│ └── ...
|
||
+├── data/ # Database SQLite e dati persistenti
|
||
+│ └── insanitylab.db # (generato al primo avvio)
|
||
+├── uploads/ # Directory per upload immagini e file
|
||
+├── dist/ # Output build (server)
|
||
+├── tests/ # Test suite
|
||
+├── astro.config.mjs # Configurazione Astro
|
||
+├── tsconfig.json # Configurazione TypeScript
|
||
+├── vitest.config.ts # Configurazione test framework
|
||
+├── .env.example # Template variabili di ambiente
|
||
+├── .env # Variabili di ambiente (non versionato)
|
||
+├── package.json # Dipendenze e script npm
|
||
+└── README.md # Questo file
|
||
+```
|
||
+
|
||
+## Configurazione Ambiente
|
||
+
|
||
+Il file `.env` richiede le seguenti variabili:
|
||
+
|
||
+- `SMTP_HOST`: Host SMTP per invio email
|
||
+- `SMTP_PORT`: Porta SMTP (solitamente 587)
|
||
+- `SMTP_USER`: Utente SMTP
|
||
+- `SMTP_PASS`: Password SMTP
|
||
+- `CONTACT_TO`: Email di destinazione contatti
|
||
+- `CONTACT_FROM`: Email mittente
|
||
+- `DB_PATH`: Percorso database SQLite
|
||
+- `UPLOADS_DIR`: Cartella upload file
|
||
+
|
||
+## Development Workflow
|
||
+
|
||
+1. **Avvia il server locale**: `npm run dev`
|
||
+2. **Modifica file** in `src/` (hot reload automatico)
|
||
+3. **Test**: `npm run test`
|
||
+4. **Build**: `npm run build` (verifica errori di build)
|
||
+5. **Commit**: Con prefisso `feat:` (feature) o `chore:` (manutenzione)
|
||
+
|
||
+## Deployment
|
||
+
|
||
+```bash
|
||
+# Build per production
|
||
+npm run build
|
||
+
|
||
+# Opzionalmente, preview locale
|
||
+npm run preview
|
||
+
|
||
+# In production, il server parte con:
|
||
+npm run start
|
||
+
|
||
+# Variabili ambiente in production vanno impostate sull'hosting
|
||
+# (es. Hostinger VPS, Vercel, etc.)
|
||
+```
|
||
+
|
||
+## Riferimenti Tecnici
|
||
+
|
||
+Per dettagli su architettura, schema database, componenti e specifiche vedi:
|
||
+- `/docs/superpowers/specs/2026-07-01-insanitylab-website-design.md`
|
||
+- `/docs/superpowers/plans/2026-07-01-insanitylab-website.md`
|
||
+
|
||
+## Problemi Comuni
|
||
+
|
||
+### `better-sqlite3` fallisce su npm install
|
||
+
|
||
+Se `better-sqlite3` non compila, assicurati di avere build tools installati:
|
||
+- **Linux**: `sudo apt-get install build-essential python3`
|
||
+- **macOS**: Xcode Command Line Tools (`xcode-select --install`)
|
||
+- **Windows**: Visual Studio Build Tools o MinGW
|
||
+
|
||
+### Database locked
|
||
+
|
||
+Se il database è locato durante i test, verifica che non ci siano processi in background che accedono a `data/insanitylab.db`.
|
||
+
|
||
+## Licenza
|
||
+
|
||
+Proprietario InsanityLab.
|
||
diff --git a/astro.config.mjs b/astro.config.mjs
|
||
new file mode 100644
|
||
index 0000000..3c44492
|
||
--- /dev/null
|
||
+++ b/astro.config.mjs
|
||
@@ -0,0 +1,10 @@
|
||
+import { defineConfig } from 'astro/config';
|
||
+import node from '@astrojs/node';
|
||
+import sitemap from '@astrojs/sitemap';
|
||
+
|
||
+export default defineConfig({
|
||
+ site: 'https://insanitylab.tielogic.xyz',
|
||
+ output: 'server',
|
||
+ adapter: node({ mode: 'standalone' }),
|
||
+ integrations: [sitemap()],
|
||
+});
|
||
diff --git a/data/.gitkeep b/data/.gitkeep
|
||
new file mode 100644
|
||
index 0000000..e69de29
|
||
diff --git a/docs/superpowers/plans/2026-07-01-insanitylab-website.md b/docs/superpowers/plans/2026-07-01-insanitylab-website.md
|
||
new file mode 100644
|
||
index 0000000..808bfa1
|
||
--- /dev/null
|
||
+++ b/docs/superpowers/plans/2026-07-01-insanitylab-website.md
|
||
@@ -0,0 +1,3513 @@
|
||
+# Sito InsanityLab — Piano di implementazione
|
||
+
|
||
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
+
|
||
+**Goal:** Sito vetrina InsanityLab con blog gestibile dal cliente (login + editor TipTap) e form contatti via email, pronto per porting Docker su VPS in fase 2.
|
||
+
|
||
+**Architecture:** Astro 5 con output `server` (adapter Node standalone): pagine vetrina prerenderizzate, blog/admin server-rendered da SQLite. Un'unica app Node, JS client vanilla minimo.
|
||
+
|
||
+**Tech Stack:** Astro 5, @astrojs/node, better-sqlite3, bcryptjs, nodemailer, sanitize-html, TipTap, @fontsource (Montserrat, Open Sans), vitest.
|
||
+
|
||
+**Spec:** `docs/superpowers/specs/2026-07-01-insanitylab-website-design.md`
|
||
+
|
||
+## Global Constraints
|
||
+
|
||
+- Directory progetto: `/home/adriano/Documenti/Clienti/InsanityLab/insanitylab-website` (repo git già inizializzato, branch `main`).
|
||
+- Route pubbliche in inglese: `/`, `/about`, `/training`, `/programs`, `/programs/[slug]`, `/blog`, `/blog/[slug]`, `/contact`. Admin: `/admin`, `/admin/login`, `/admin/new`, `/admin/edit/[id]`. API: `POST /api/contact`, `/api/admin/posts*`, `POST /api/admin/upload`.
|
||
+- Slug programmi: `personal-training`, `performance-class`, `coaching`, `pilates-flow`, `rehab`, `nutrition`.
|
||
+- Categorie blog fisse: `Articoli tecnici & scientifici`, `Contenuti educativi`, `LinkedIn / Insight`, `Eventi & Presidi`.
|
||
+- Palette: accento tan `#b5a48b`, marrone scuro `#3a2d26`, fondo alternativo `#f5f3f0` (verificare campionando i PDF, Task 3).
|
||
+- Testi UI in italiano. Niente Google Fonts CDN (font self-hosted). Niente framework CSS o JS client.
|
||
+- DB: `data/insanitylab.db` (gitignored). Upload: `uploads/` a root progetto (gitignored), serviti da route dinamica `/uploads/[...path]`.
|
||
+- Variabili d'ambiente lette con fallback `process.env.X ?? import.meta.env.X` (dev: Vite carica `.env`; prod: `node --env-file=.env`).
|
||
+- Sito di riferimento design: PDF in `../FigmaDoc/`. Template analizzato: `../Dati/cassetta-me-analisi.md`.
|
||
+- Dati di contatto reali: tel `351 7535977`, `info@insanitylab.it`, Via Leandro Alberti 76, 40139 Bologna, Lun–Ven 6.00–21.00, Sab 8.00–19.00, Dom chiuso.
|
||
+- Commit frequenti, messaggi brevi in italiano, prefissi `feat:`/`fix:`/`chore:`/`test:`.
|
||
+
|
||
+---
|
||
+
|
||
+### Task 1: Scaffold progetto Astro
|
||
+
|
||
+**Files:**
|
||
+- Create: `package.json`, `astro.config.mjs`, `tsconfig.json`, `vitest.config.ts`, `.gitignore`, `.env.example`, `README.md`, `src/pages/index.astro` (placeholder), `src/env.d.ts`, `data/.gitkeep`, `uploads/.gitkeep`
|
||
+
|
||
+**Interfaces:**
|
||
+- Produces: progetto buildabile; script npm `dev`, `build`, `preview`, `start`, `test`, `create-user`.
|
||
+
|
||
+- [ ] **Step 1: package.json e installazione dipendenze**
|
||
+
|
||
+Crea `package.json`:
|
||
+
|
||
+```json
|
||
+{
|
||
+ "name": "insanitylab-website",
|
||
+ "version": "0.1.0",
|
||
+ "private": true,
|
||
+ "type": "module",
|
||
+ "scripts": {
|
||
+ "dev": "astro dev",
|
||
+ "build": "astro build",
|
||
+ "preview": "astro preview",
|
||
+ "start": "node --env-file=.env ./dist/server/entry.mjs",
|
||
+ "test": "vitest run",
|
||
+ "create-user": "node scripts/create-user.mjs"
|
||
+ }
|
||
+}
|
||
+```
|
||
+
|
||
+Run:
|
||
+```bash
|
||
+cd /home/adriano/Documenti/Clienti/InsanityLab/insanitylab-website
|
||
+npm install astro @astrojs/node @astrojs/sitemap better-sqlite3 bcryptjs nodemailer sanitize-html @fontsource/montserrat @fontsource/open-sans @tiptap/core @tiptap/starter-kit @tiptap/extension-image @tiptap/extension-link
|
||
+npm install -D vitest typescript @types/better-sqlite3 @types/bcryptjs @types/nodemailer @types/sanitize-html
|
||
+```
|
||
+Expected: install senza errori (better-sqlite3 compila nativo).
|
||
+
|
||
+- [ ] **Step 2: file di configurazione**
|
||
+
|
||
+`astro.config.mjs`:
|
||
+```js
|
||
+import { defineConfig } from 'astro/config';
|
||
+import node from '@astrojs/node';
|
||
+import sitemap from '@astrojs/sitemap';
|
||
+
|
||
+export default defineConfig({
|
||
+ site: 'https://insanitylab.tielogic.xyz',
|
||
+ output: 'server',
|
||
+ adapter: node({ mode: 'standalone' }),
|
||
+ integrations: [sitemap()],
|
||
+});
|
||
+```
|
||
+
|
||
+`tsconfig.json`:
|
||
+```json
|
||
+{
|
||
+ "extends": "astro/tsconfigs/strict",
|
||
+ "include": [".astro/types.d.ts", "src/**/*", "tests/**/*"],
|
||
+ "exclude": ["dist"]
|
||
+}
|
||
+```
|
||
+
|
||
+`vitest.config.ts`:
|
||
+```ts
|
||
+import { defineConfig } from 'vitest/config';
|
||
+
|
||
+export default defineConfig({
|
||
+ test: { include: ['tests/**/*.test.ts'] },
|
||
+});
|
||
+```
|
||
+
|
||
+`.gitignore`:
|
||
+```
|
||
+node_modules/
|
||
+dist/
|
||
+.astro/
|
||
+.env
|
||
+data/*.db*
|
||
+uploads/*
|
||
+!uploads/.gitkeep
|
||
+assets-src/
|
||
+```
|
||
+
|
||
+`.env.example`:
|
||
+```
|
||
+SMTP_HOST=smtp.example.com
|
||
+SMTP_PORT=587
|
||
+SMTP_USER=user
|
||
+SMTP_PASS=pass
|
||
+CONTACT_TO=info@insanitylab.it
|
||
+CONTACT_FROM=sito@insanitylab.it
|
||
+DB_PATH=data/insanitylab.db
|
||
+UPLOADS_DIR=uploads
|
||
+```
|
||
+
|
||
+`src/env.d.ts`:
|
||
+```ts
|
||
+/// <reference types="astro/client" />
|
||
+
|
||
+declare namespace App {
|
||
+ interface Locals {
|
||
+ user?: { id: number; username: string };
|
||
+ }
|
||
+}
|
||
+```
|
||
+
|
||
+`src/pages/index.astro` (placeholder, sostituito nel Task 9):
|
||
+```astro
|
||
+---
|
||
+export const prerender = true;
|
||
+---
|
||
+<html lang="it"><head><title>InsanityLab</title></head>
|
||
+<body><h1>InsanityLab — in costruzione</h1></body></html>
|
||
+```
|
||
+
|
||
+Crea `data/.gitkeep` e `uploads/.gitkeep` vuoti. Copia `.env.example` in `.env`.
|
||
+
|
||
+- [ ] **Step 3: README.md**
|
||
+
|
||
+Prosa italiana completa: scopo (sito vetrina InsanityLab con blog e form contatti), setup (`npm install`, `cp .env.example .env`, `npm run create-user -- <user> <pass>`), comandi (`dev`, `build`, `start`, `test`), stack, struttura cartelle, riferimento alla spec in `docs/superpowers/specs/`.
|
||
+
|
||
+- [ ] **Step 4: verifica build**
|
||
+
|
||
+Run: `npm run build`
|
||
+Expected: build completata, cartella `dist/` creata.
|
||
+
|
||
+- [ ] **Step 5: commit**
|
||
+
|
||
+```bash
|
||
+git add -A && git commit -m "chore: scaffold progetto Astro 5 con adapter node"
|
||
+```
|
||
+
|
||
+---
|
||
+
|
||
+### Task 2: Estrazione asset da Figma
|
||
+
|
||
+**Files:**
|
||
+- Create: `scripts/extract-fig-images.sh`, `scripts/asset-map.json`, `scripts/apply-asset-map.mjs`, `src/assets/img/*` (immagini rinominate), `public/img/logo-*.png`
|
||
+
|
||
+**Interfaces:**
|
||
+- Produces: immagini in `src/assets/img/` con nomi semantici (`hero-1.png`, `info-performance.jpg`, `team-donata.jpg`, `program-one-to-one.jpg`, `partner-edenred.png`, ecc.); loghi in `public/img/`.
|
||
+
|
||
+- [ ] **Step 1: script estrazione**
|
||
+
|
||
+`scripts/extract-fig-images.sh`:
|
||
+```bash
|
||
+#!/usr/bin/env bash
|
||
+set -euo pipefail
|
||
+cd "$(dirname "$0")/.."
|
||
+FIG="../FigmaDoc/Web Insanity.fig"
|
||
+OUT="assets-src"
|
||
+rm -rf "$OUT" && mkdir -p "$OUT"
|
||
+unzip -o -j -q "$FIG" 'images/*' -d "$OUT"
|
||
+cd "$OUT"
|
||
+for f in *; do
|
||
+ case "$(file --brief --mime-type "$f")" in
|
||
+ image/png) mv "$f" "$f.png" ;;
|
||
+ image/jpeg) mv "$f" "$f.jpg" ;;
|
||
+ image/webp) mv "$f" "$f.webp" ;;
|
||
+ *) rm -f "$f" ;;
|
||
+ esac
|
||
+done
|
||
+echo "Estratte: $(ls | wc -l) immagini in $OUT/"
|
||
+```
|
||
+
|
||
+Run: `chmod +x scripts/extract-fig-images.sh && ./scripts/extract-fig-images.sh`
|
||
+Expected: decine di immagini con estensione in `assets-src/`.
|
||
+
|
||
+- [ ] **Step 2: identificazione visiva e mappatura**
|
||
+
|
||
+Visualizza le immagini estratte (tool Read su ogni file, oppure contact sheet con `montage assets-src/*.{png,jpg} -tile 6x -geometry 200x200+4+4 -label '%f' assets-src/sheet_%d.png`). Compila `scripts/asset-map.json` associando ogni hash al nome di destinazione. Formato:
|
||
+
|
||
+```json
|
||
+{
|
||
+ "src/assets/img": {
|
||
+ "<hash>.jpg": "hero-1.jpg",
|
||
+ "<hash>.jpg": "info-performance.jpg",
|
||
+ "<hash>.jpg": "info-balance.jpg",
|
||
+ "<hash>.jpg": "info-longevity.jpg",
|
||
+ "<hash>.jpg": "feature-performance.jpg",
|
||
+ "<hash>.jpg": "feature-balance.jpg",
|
||
+ "<hash>.jpg": "feature-longevity.jpg",
|
||
+ "<hash>.jpg": "quote-bg.jpg",
|
||
+ "<hash>.jpg": "training-one-to-one.jpg",
|
||
+ "<hash>.jpg": "training-small-groups.jpg",
|
||
+ "<hash>.jpg": "training-performance-class.jpg",
|
||
+ "<hash>.jpg": "training-fit-remote.jpg",
|
||
+ "<hash>.jpg": "team-donata.jpg",
|
||
+ "<hash>.jpg": "team-nicola.jpg",
|
||
+ "<hash>.jpg": "team-eleonora.jpg",
|
||
+ "<hash>.jpg": "team-antonello.jpg",
|
||
+ "<hash>.jpg": "method-step-1.jpg",
|
||
+ "<hash>.jpg": "method-step-2.jpg",
|
||
+ "<hash>.jpg": "method-step-3.jpg",
|
||
+ "<hash>.jpg": "method-step-4.jpg",
|
||
+ "<hash>.png": "partner-edenred.png",
|
||
+ "<hash>.png": "partner-welbee.png",
|
||
+ "<hash>.png": "partner-aon.png",
|
||
+ "<hash>.png": "partner-howden.png"
|
||
+ },
|
||
+ "public/img": {
|
||
+ "<hash>.png": "logo-dark.png",
|
||
+ "<hash>.png": "logo-light.png"
|
||
+ }
|
||
+}
|
||
+```
|
||
+
|
||
+Nomi aggiuntivi liberi per immagini di about/training trovate nell'archivio (`about-*.jpg`, ecc.). Le immagini non usate restano in `assets-src/` (gitignored). Se un'immagine attesa manca (es. loghi), fallback: `../Dati/logo-dark.png`, `../Dati/logo-light.png` e altri file in `../Dati/`.
|
||
+
|
||
+- [ ] **Step 3: script applicazione mappa**
|
||
+
|
||
+`scripts/apply-asset-map.mjs`:
|
||
+```js
|
||
+import { copyFileSync, mkdirSync, existsSync } from 'node:fs';
|
||
+import { readFileSync } from 'node:fs';
|
||
+import { join, dirname } from 'node:path';
|
||
+import { fileURLToPath } from 'node:url';
|
||
+
|
||
+const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||
+const map = JSON.parse(readFileSync(join(root, 'scripts/asset-map.json'), 'utf8'));
|
||
+let n = 0, missing = 0;
|
||
+for (const [destDir, entries] of Object.entries(map)) {
|
||
+ mkdirSync(join(root, destDir), { recursive: true });
|
||
+ for (const [src, dest] of Object.entries(entries)) {
|
||
+ const from = join(root, 'assets-src', src);
|
||
+ if (!existsSync(from)) { console.error(`MANCANTE: ${src}`); missing++; continue; }
|
||
+ copyFileSync(from, join(root, destDir, dest));
|
||
+ n++;
|
||
+ }
|
||
+}
|
||
+console.log(`Copiate ${n} immagini${missing ? `, ${missing} mancanti` : ''}`);
|
||
+process.exit(missing ? 1 : 0);
|
||
+```
|
||
+
|
||
+Run: `node scripts/apply-asset-map.mjs`
|
||
+Expected: `Copiate N immagini`, exit 0.
|
||
+
|
||
+- [ ] **Step 4: ottimizza pesi**
|
||
+
|
||
+Le immagini Figma possono superare 5 MB. Ridimensiona quelle sopra 2000 px di larghezza:
|
||
+```bash
|
||
+cd src/assets/img
|
||
+for f in *.jpg *.png; do
|
||
+ w=$(identify -format '%w' "$f")
|
||
+ [ "$w" -gt 2000 ] && mogrify -resize 2000x "$f" && echo "ridotta: $f"
|
||
+done
|
||
+```
|
||
+Expected: nessuna immagine > 2000 px; `du -sh src/assets/img` sotto ~15 MB.
|
||
+
|
||
+- [ ] **Step 5: commit**
|
||
+
|
||
+```bash
|
||
+git add -A && git commit -m "feat: asset immagini estratti da Figma e mappati"
|
||
+```
|
||
+
|
||
+---
|
||
+
|
||
+### Task 3: Design tokens e CSS globale
|
||
+
|
||
+**Files:**
|
||
+- Create: `src/styles/global.css`
|
||
+
|
||
+**Interfaces:**
|
||
+- Produces: variabili CSS `--c-accent`, `--c-accent-dark`, `--c-dark`, `--c-text`, `--c-heading`, `--c-bg-alt`; classi utility `.container`, `.btn`, `.btn--dark`, `.btn--light`, `.section`, `.section--dark`, `.section-heading`, `.eyebrow`; font family var `--font-heading`, `--font-body`.
|
||
+
|
||
+- [ ] **Step 1: campiona colori esatti dai PDF**
|
||
+
|
||
+```bash
|
||
+S=/tmp/ilab-sample && mkdir -p $S
|
||
+pdftoppm -png -r 30 "../FigmaDoc/Homepage.pdf" $S/h
|
||
+# bottone SCOPRI hero (~x55,y200 alla risoluzione 30dpi), sezione agenda scura, sfondo chiaro
|
||
+convert $S/h-1.png -format "%[pixel:p{60,205}] %[pixel:p{50,1500}] %[pixel:p{300,1360}]" info:
|
||
+```
|
||
+Annota i valori RGB reali e usali negli step successivi al posto dei default indicati (attesi: tan ~`#b5a48b`, marrone ~`#3a2d26`, chiaro ~`#f5f3f0`).
|
||
+
|
||
+- [ ] **Step 2: global.css**
|
||
+
|
||
+```css
|
||
+/* === Design tokens (valori campionati da Figma) === */
|
||
+:root {
|
||
+ --c-accent: #b5a48b;
|
||
+ --c-accent-dark: #9c8b70;
|
||
+ --c-dark: #3a2d26;
|
||
+ --c-dark-2: #4a3a30;
|
||
+ --c-heading: #2b2b2b;
|
||
+ --c-text: #4b4b4b;
|
||
+ --c-text-light: #8a8a8a;
|
||
+ --c-bg: #ffffff;
|
||
+ --c-bg-alt: #f5f3f0;
|
||
+ --font-heading: 'Montserrat', sans-serif;
|
||
+ --font-body: 'Open Sans', sans-serif;
|
||
+ --header-h: 70px;
|
||
+}
|
||
+
|
||
+/* === Reset === */
|
||
+*, *::before, *::after { box-sizing: border-box; }
|
||
+body { margin: 0; font-family: var(--font-body); color: var(--c-text); font-size: 16px; line-height: 1.7; background: var(--c-bg); }
|
||
+img { max-width: 100%; height: auto; display: block; }
|
||
+h1, h2, h3, h4 { font-family: var(--font-heading); color: var(--c-heading); font-weight: 500; letter-spacing: .04em; line-height: 1.2; margin: 0 0 .5em; }
|
||
+h1 { font-size: clamp(2.2rem, 5vw, 3.8rem); }
|
||
+h2 { font-size: clamp(1.8rem, 4vw, 2.6rem); }
|
||
+h3 { font-size: 1.3rem; }
|
||
+a { color: inherit; }
|
||
+p { margin: 0 0 1em; }
|
||
+
|
||
+/* === Utility === */
|
||
+.container { max-width: 1200px; margin-inline: auto; padding-inline: 20px; }
|
||
+.section { padding-block: 90px; }
|
||
+.section--alt { background: var(--c-bg-alt); }
|
||
+.section--dark { background: var(--c-dark); color: #cfc6bd; }
|
||
+.section--dark h2, .section--dark h3 { color: #fff; }
|
||
+.eyebrow { font-family: var(--font-heading); text-transform: uppercase; letter-spacing: .25em; font-size: .75rem; color: var(--c-accent-dark); }
|
||
+.section-heading { text-align: center; max-width: 640px; margin: 0 auto 56px; }
|
||
+.section-heading h2 { text-transform: uppercase; letter-spacing: .12em; }
|
||
+
|
||
+.btn { display: inline-flex; align-items: center; gap: 14px; font-family: var(--font-heading); font-size: .72rem; font-weight: 600; letter-spacing: .2em; text-transform: uppercase; text-decoration: none; border: 0; cursor: pointer; padding: 16px 26px; background: var(--c-accent); color: #fff; transition: background .2s; }
|
||
+.btn:hover { background: var(--c-accent-dark); }
|
||
+.btn::after { content: '→'; font-size: .9rem; }
|
||
+.btn--dark { background: var(--c-dark); }
|
||
+.btn--dark:hover { background: var(--c-dark-2); }
|
||
+.btn--light { background: #fff; color: var(--c-heading); }
|
||
+
|
||
+/* === Form === */
|
||
+.field { width: 100%; border: 1px solid #ddd; background: #fff; padding: 14px 16px; font-family: var(--font-body); font-size: .95rem; color: var(--c-text); margin-bottom: 14px; }
|
||
+.field:focus { outline: 2px solid var(--c-accent); outline-offset: -1px; }
|
||
+textarea.field { min-height: 120px; resize: vertical; }
|
||
+.form-msg { font-size: .9rem; margin-top: 8px; }
|
||
+.form-msg--ok { color: #3c7a3c; }
|
||
+.form-msg--err { color: #b03030; }
|
||
+
|
||
+/* honeypot */
|
||
+.hp { position: absolute; left: -9999px; opacity: 0; }
|
||
+```
|
||
+
|
||
+- [ ] **Step 2b: aggiorna i valori con quelli campionati allo Step 1.**
|
||
+
|
||
+- [ ] **Step 3: commit**
|
||
+
|
||
+```bash
|
||
+git add src/styles/global.css && git commit -m "feat: design tokens e css globale"
|
||
+```
|
||
+
|
||
+---
|
||
+
|
||
+### Task 4: Lib di base — slugify e rate limit (TDD)
|
||
+
|
||
+**Files:**
|
||
+- Create: `src/lib/slug.ts`, `src/lib/rate-limit.ts`, `src/lib/env.ts`
|
||
+- Test: `tests/slug.test.ts`, `tests/rate-limit.test.ts`
|
||
+
|
||
+**Interfaces:**
|
||
+- Produces: `slugify(input: string): string`; `rateLimit(key: string, max: number, windowMs: number): boolean` (true = consentito); `getEnv(name: string, fallback?: string): string`.
|
||
+
|
||
+- [ ] **Step 1: test slugify (falliranno)**
|
||
+
|
||
+`tests/slug.test.ts`:
|
||
+```ts
|
||
+import { describe, it, expect } from 'vitest';
|
||
+import { slugify } from '../src/lib/slug';
|
||
+
|
||
+describe('slugify', () => {
|
||
+ it('minuscole e trattini', () => {
|
||
+ expect(slugify('Il Metodo InsanityLab')).toBe('il-metodo-insanitylab');
|
||
+ });
|
||
+ it('rimuove accenti', () => {
|
||
+ expect(slugify('Attività fisica è già qui')).toBe('attivita-fisica-e-gia-qui');
|
||
+ });
|
||
+ it('comprime simboli e spazi multipli', () => {
|
||
+ expect(slugify('Blog & Edugo -- news!!')).toBe('blog-edugo-news');
|
||
+ });
|
||
+ it('niente trattini ai bordi', () => {
|
||
+ expect(slugify(' ciao ')).toBe('ciao');
|
||
+ });
|
||
+ it('stringa vuota resta vuota', () => {
|
||
+ expect(slugify('')).toBe('');
|
||
+ });
|
||
+});
|
||
+```
|
||
+
|
||
+- [ ] **Step 2: verifica che falliscano**
|
||
+
|
||
+Run: `npx vitest run tests/slug.test.ts`
|
||
+Expected: FAIL (`slugify` non esiste).
|
||
+
|
||
+- [ ] **Step 3: implementa slugify**
|
||
+
|
||
+`src/lib/slug.ts`:
|
||
+```ts
|
||
+export function slugify(input: string): string {
|
||
+ return input
|
||
+ .toLowerCase()
|
||
+ .normalize('NFD')
|
||
+ .replace(/[\u0300-\u036f]/g, '')
|
||
+ .replace(/[^a-z0-9]+/g, '-')
|
||
+ .replace(/^-+|-+$/g, '');
|
||
+}
|
||
+```
|
||
+
|
||
+- [ ] **Step 4: test rate limit (falliranno)**
|
||
+
|
||
+`tests/rate-limit.test.ts`:
|
||
+```ts
|
||
+import { describe, it, expect, vi, afterEach } from 'vitest';
|
||
+import { rateLimit, _resetBuckets } from '../src/lib/rate-limit';
|
||
+
|
||
+afterEach(() => { _resetBuckets(); vi.useRealTimers(); });
|
||
+
|
||
+describe('rateLimit', () => {
|
||
+ it('consente fino a max richieste', () => {
|
||
+ expect(rateLimit('a', 2, 1000)).toBe(true);
|
||
+ expect(rateLimit('a', 2, 1000)).toBe(true);
|
||
+ expect(rateLimit('a', 2, 1000)).toBe(false);
|
||
+ });
|
||
+ it('chiavi indipendenti', () => {
|
||
+ expect(rateLimit('a', 1, 1000)).toBe(true);
|
||
+ expect(rateLimit('b', 1, 1000)).toBe(true);
|
||
+ });
|
||
+ it('la finestra scade', () => {
|
||
+ vi.useFakeTimers();
|
||
+ expect(rateLimit('a', 1, 1000)).toBe(true);
|
||
+ expect(rateLimit('a', 1, 1000)).toBe(false);
|
||
+ vi.advanceTimersByTime(1100);
|
||
+ expect(rateLimit('a', 1, 1000)).toBe(true);
|
||
+ });
|
||
+});
|
||
+```
|
||
+
|
||
+- [ ] **Step 5: implementa rate-limit ed env**
|
||
+
|
||
+`src/lib/rate-limit.ts`:
|
||
+```ts
|
||
+const buckets = new Map<string, number[]>();
|
||
+
|
||
+export function rateLimit(key: string, max: number, windowMs: number): boolean {
|
||
+ const now = Date.now();
|
||
+ const recent = (buckets.get(key) ?? []).filter((t) => now - t < windowMs);
|
||
+ if (recent.length >= max) {
|
||
+ buckets.set(key, recent);
|
||
+ return false;
|
||
+ }
|
||
+ recent.push(now);
|
||
+ buckets.set(key, recent);
|
||
+ return true;
|
||
+}
|
||
+
|
||
+export function _resetBuckets(): void {
|
||
+ buckets.clear();
|
||
+}
|
||
+```
|
||
+
|
||
+`src/lib/env.ts`:
|
||
+```ts
|
||
+export function getEnv(name: string, fallback?: string): string {
|
||
+ const v = process.env[name] ?? (import.meta.env ? import.meta.env[name] : undefined) ?? fallback;
|
||
+ if (v === undefined) throw new Error(`Variabile d'ambiente mancante: ${name}`);
|
||
+ return v;
|
||
+}
|
||
+```
|
||
+
|
||
+- [ ] **Step 6: verifica test verdi**
|
||
+
|
||
+Run: `npm test`
|
||
+Expected: tutti PASS.
|
||
+
|
||
+- [ ] **Step 7: commit**
|
||
+
|
||
+```bash
|
||
+git add src/lib tests && git commit -m "feat: slugify, rate limit ed env helper con test"
|
||
+```
|
||
+
|
||
+---
|
||
+
|
||
+### Task 5: Layer database — schema e repository posts (TDD)
|
||
+
|
||
+**Files:**
|
||
+- Create: `src/lib/db.ts`, `src/lib/posts.ts`
|
||
+- Test: `tests/posts.test.ts`
|
||
+
|
||
+**Interfaces:**
|
||
+- Consumes: `slugify` da Task 4 (nei consumer, non qui).
|
||
+- Produces:
|
||
+ - `createDb(path?: string): Database.Database` — apre/crea DB, applica schema idempotente.
|
||
+ - `getDb(): Database.Database` — singleton su `DB_PATH`.
|
||
+ - Tipo `Post { id: number; title: string; slug: string; category: string; excerpt: string; body_html: string; cover: string | null; draft: number; published_at: string | null; created_at: string; updated_at: string }`.
|
||
+ - `PostInput { title: string; slug: string; category: string; excerpt: string; body_html: string; cover?: string | null; draft: boolean }`.
|
||
+ - `createPost(db, input: PostInput): number` (ritorna id; se `draft=false` imposta `published_at`).
|
||
+ - `updatePost(db, id: number, input: PostInput): void` (imposta `published_at` alla prima pubblicazione, aggiorna `updated_at`).
|
||
+ - `deletePost(db, id: number): void`
|
||
+ - `getPostById(db, id: number): Post | undefined`
|
||
+ - `getPublishedBySlug(db, slug: string): Post | undefined`
|
||
+ - `listPublished(db, opts?: { category?: string; page?: number; perPage?: number }): { items: Post[]; total: number }` (ordinati per `published_at` desc)
|
||
+ - `listAllPosts(db): Post[]` (anche bozze, per admin)
|
||
+ - `listRecent(db, limit?: number): Post[]`
|
||
+ - `listArchiveMonths(db): { month: string; count: number }[]` (formato `YYYY-MM`)
|
||
+
|
||
+- [ ] **Step 1: implementa db.ts**
|
||
+
|
||
+`src/lib/db.ts`:
|
||
+```ts
|
||
+import Database from 'better-sqlite3';
|
||
+import { mkdirSync } from 'node:fs';
|
||
+import { dirname } from 'node:path';
|
||
+
|
||
+const SCHEMA = `
|
||
+CREATE TABLE IF NOT EXISTS posts (
|
||
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
+ title TEXT NOT NULL,
|
||
+ slug TEXT NOT NULL UNIQUE,
|
||
+ category TEXT NOT NULL,
|
||
+ excerpt TEXT NOT NULL DEFAULT '',
|
||
+ body_html TEXT NOT NULL DEFAULT '',
|
||
+ cover TEXT,
|
||
+ draft INTEGER NOT NULL DEFAULT 1,
|
||
+ published_at TEXT,
|
||
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
+);
|
||
+CREATE TABLE IF NOT EXISTS users (
|
||
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
+ username TEXT NOT NULL UNIQUE,
|
||
+ password_hash TEXT NOT NULL
|
||
+);
|
||
+CREATE TABLE IF NOT EXISTS sessions (
|
||
+ token TEXT PRIMARY KEY,
|
||
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
+ expires_at TEXT NOT NULL
|
||
+);
|
||
+CREATE INDEX IF NOT EXISTS idx_posts_pub ON posts (draft, published_at DESC);
|
||
+`;
|
||
+
|
||
+export function createDb(path?: string): Database.Database {
|
||
+ const p = path ?? process.env.DB_PATH ?? 'data/insanitylab.db';
|
||
+ if (p !== ':memory:') mkdirSync(dirname(p), { recursive: true });
|
||
+ const db = new Database(p);
|
||
+ db.pragma('journal_mode = WAL');
|
||
+ db.pragma('foreign_keys = ON');
|
||
+ db.exec(SCHEMA);
|
||
+ return db;
|
||
+}
|
||
+
|
||
+let singleton: Database.Database | null = null;
|
||
+
|
||
+export function getDb(): Database.Database {
|
||
+ if (!singleton) singleton = createDb();
|
||
+ return singleton;
|
||
+}
|
||
+```
|
||
+
|
||
+- [ ] **Step 2: test posts (falliranno)**
|
||
+
|
||
+`tests/posts.test.ts`:
|
||
+```ts
|
||
+import { describe, it, expect, beforeEach } from 'vitest';
|
||
+import type Database from 'better-sqlite3';
|
||
+import { createDb } from '../src/lib/db';
|
||
+import {
|
||
+ createPost, updatePost, deletePost, getPostById, getPublishedBySlug,
|
||
+ listPublished, listAllPosts, listRecent, listArchiveMonths, type PostInput,
|
||
+} from '../src/lib/posts';
|
||
+
|
||
+let db: Database.Database;
|
||
+beforeEach(() => { db = createDb(':memory:'); });
|
||
+
|
||
+const base: PostInput = {
|
||
+ title: 'Titolo', slug: 'titolo', category: 'Contenuti educativi',
|
||
+ excerpt: 'estratto', body_html: '<p>corpo</p>', cover: null, draft: false,
|
||
+};
|
||
+
|
||
+describe('posts repository', () => {
|
||
+ it('crea e rilegge un post pubblicato', () => {
|
||
+ const id = createPost(db, base);
|
||
+ const post = getPostById(db, id)!;
|
||
+ expect(post.title).toBe('Titolo');
|
||
+ expect(post.draft).toBe(0);
|
||
+ expect(post.published_at).not.toBeNull();
|
||
+ });
|
||
+
|
||
+ it('bozza non ha published_at e non appare nelle liste pubbliche', () => {
|
||
+ const id = createPost(db, { ...base, slug: 'bozza', draft: true });
|
||
+ expect(getPostById(db, id)!.published_at).toBeNull();
|
||
+ expect(getPublishedBySlug(db, 'bozza')).toBeUndefined();
|
||
+ expect(listPublished(db).total).toBe(0);
|
||
+ expect(listAllPosts(db)).toHaveLength(1);
|
||
+ });
|
||
+
|
||
+ it('slug duplicato lancia', () => {
|
||
+ createPost(db, base);
|
||
+ expect(() => createPost(db, base)).toThrow();
|
||
+ });
|
||
+
|
||
+ it('update imposta published_at alla prima pubblicazione e lo conserva', () => {
|
||
+ const id = createPost(db, { ...base, draft: true });
|
||
+ updatePost(db, id, { ...base, draft: false });
|
||
+ const first = getPostById(db, id)!.published_at;
|
||
+ expect(first).not.toBeNull();
|
||
+ updatePost(db, id, { ...base, title: 'Nuovo', draft: false });
|
||
+ expect(getPostById(db, id)!.published_at).toBe(first);
|
||
+ expect(getPostById(db, id)!.title).toBe('Nuovo');
|
||
+ });
|
||
+
|
||
+ it('listPublished filtra per categoria e pagina', () => {
|
||
+ for (let i = 0; i < 12; i++) {
|
||
+ createPost(db, { ...base, slug: `post-${i}`, category: i % 2 ? 'Contenuti educativi' : 'Eventi & Presidi' });
|
||
+ }
|
||
+ const all = listPublished(db, { page: 1, perPage: 9 });
|
||
+ expect(all.total).toBe(12);
|
||
+ expect(all.items).toHaveLength(9);
|
||
+ const edu = listPublished(db, { category: 'Contenuti educativi' });
|
||
+ expect(edu.total).toBe(6);
|
||
+ });
|
||
+
|
||
+ it('delete rimuove', () => {
|
||
+ const id = createPost(db, base);
|
||
+ deletePost(db, id);
|
||
+ expect(getPostById(db, id)).toBeUndefined();
|
||
+ });
|
||
+
|
||
+ it('listRecent e archivio mesi', () => {
|
||
+ createPost(db, base);
|
||
+ expect(listRecent(db, 4)).toHaveLength(1);
|
||
+ const months = listArchiveMonths(db);
|
||
+ expect(months).toHaveLength(1);
|
||
+ expect(months[0].count).toBe(1);
|
||
+ expect(months[0].month).toMatch(/^\d{4}-\d{2}$/);
|
||
+ });
|
||
+});
|
||
+```
|
||
+
|
||
+- [ ] **Step 3: verifica che falliscano**
|
||
+
|
||
+Run: `npx vitest run tests/posts.test.ts`
|
||
+Expected: FAIL (modulo `posts` mancante).
|
||
+
|
||
+- [ ] **Step 4: implementa posts.ts**
|
||
+
|
||
+`src/lib/posts.ts`:
|
||
+```ts
|
||
+import type Database from 'better-sqlite3';
|
||
+
|
||
+export interface Post {
|
||
+ id: number; title: string; slug: string; category: string; excerpt: string;
|
||
+ body_html: string; cover: string | null; draft: number;
|
||
+ published_at: string | null; created_at: string; updated_at: string;
|
||
+}
|
||
+
|
||
+export interface PostInput {
|
||
+ title: string; slug: string; category: string; excerpt: string;
|
||
+ body_html: string; cover?: string | null; draft: boolean;
|
||
+}
|
||
+
|
||
+export function createPost(db: Database.Database, input: PostInput): number {
|
||
+ const res = db.prepare(
|
||
+ `INSERT INTO posts (title, slug, category, excerpt, body_html, cover, draft, published_at)
|
||
+ VALUES (@title, @slug, @category, @excerpt, @body_html, @cover, @draft,
|
||
+ CASE WHEN @draft = 0 THEN datetime('now') ELSE NULL END)`
|
||
+ ).run({ ...input, cover: input.cover ?? null, draft: input.draft ? 1 : 0 });
|
||
+ return Number(res.lastInsertRowid);
|
||
+}
|
||
+
|
||
+export function updatePost(db: Database.Database, id: number, input: PostInput): void {
|
||
+ db.prepare(
|
||
+ `UPDATE posts SET title = @title, slug = @slug, category = @category,
|
||
+ excerpt = @excerpt, body_html = @body_html, cover = @cover, draft = @draft,
|
||
+ published_at = CASE WHEN @draft = 0 AND published_at IS NULL THEN datetime('now') ELSE published_at END,
|
||
+ updated_at = datetime('now')
|
||
+ WHERE id = @id`
|
||
+ ).run({ ...input, id, cover: input.cover ?? null, draft: input.draft ? 1 : 0 });
|
||
+}
|
||
+
|
||
+export function deletePost(db: Database.Database, id: number): void {
|
||
+ db.prepare('DELETE FROM posts WHERE id = ?').run(id);
|
||
+}
|
||
+
|
||
+export function getPostById(db: Database.Database, id: number): Post | undefined {
|
||
+ return db.prepare('SELECT * FROM posts WHERE id = ?').get(id) as Post | undefined;
|
||
+}
|
||
+
|
||
+export function getPublishedBySlug(db: Database.Database, slug: string): Post | undefined {
|
||
+ return db.prepare('SELECT * FROM posts WHERE slug = ? AND draft = 0').get(slug) as Post | undefined;
|
||
+}
|
||
+
|
||
+export function listPublished(
|
||
+ db: Database.Database,
|
||
+ opts: { category?: string; page?: number; perPage?: number } = {},
|
||
+): { items: Post[]; total: number } {
|
||
+ const { category, page = 1, perPage = 9 } = opts;
|
||
+ const where = category ? 'WHERE draft = 0 AND category = @category' : 'WHERE draft = 0';
|
||
+ const total = (db.prepare(`SELECT COUNT(*) AS n FROM posts ${where}`)
|
||
+ .get({ category }) as { n: number }).n;
|
||
+ const items = db.prepare(
|
||
+ `SELECT * FROM posts ${where} ORDER BY published_at DESC LIMIT @limit OFFSET @offset`
|
||
+ ).all({ category, limit: perPage, offset: (page - 1) * perPage }) as Post[];
|
||
+ return { items, total };
|
||
+}
|
||
+
|
||
+export function listAllPosts(db: Database.Database): Post[] {
|
||
+ return db.prepare('SELECT * FROM posts ORDER BY updated_at DESC').all() as Post[];
|
||
+}
|
||
+
|
||
+export function listRecent(db: Database.Database, limit = 4): Post[] {
|
||
+ return db.prepare(
|
||
+ 'SELECT * FROM posts WHERE draft = 0 ORDER BY published_at DESC LIMIT ?'
|
||
+ ).all(limit) as Post[];
|
||
+}
|
||
+
|
||
+export function listArchiveMonths(db: Database.Database): { month: string; count: number }[] {
|
||
+ return db.prepare(
|
||
+ `SELECT strftime('%Y-%m', published_at) AS month, COUNT(*) AS count
|
||
+ FROM posts WHERE draft = 0 GROUP BY month ORDER BY month DESC`
|
||
+ ).all() as { month: string; count: number }[];
|
||
+}
|
||
+```
|
||
+
|
||
+- [ ] **Step 5: verifica test verdi**
|
||
+
|
||
+Run: `npm test`
|
||
+Expected: tutti PASS.
|
||
+
|
||
+- [ ] **Step 6: commit**
|
||
+
|
||
+```bash
|
||
+git add src/lib/db.ts src/lib/posts.ts tests/posts.test.ts && git commit -m "feat: layer sqlite e repository posts con test"
|
||
+```
|
||
+
|
||
+---
|
||
+
|
||
+### Task 6: Autenticazione, middleware e script create-user (TDD)
|
||
+
|
||
+**Files:**
|
||
+- Create: `src/lib/auth.ts`, `src/middleware.ts`, `scripts/create-user.mjs`
|
||
+- Test: `tests/auth.test.ts`
|
||
+
|
||
+**Interfaces:**
|
||
+- Consumes: `createDb`/`getDb` (Task 5), `rateLimit` (Task 4).
|
||
+- Produces:
|
||
+ - `hashPassword(plain: string): string`, `verifyPassword(plain: string, hash: string): boolean`
|
||
+ - `createUser(db, username: string, password: string): number`
|
||
+ - `login(db, username: string, password: string): string | null` — token sessione o null.
|
||
+ - `getSessionUser(db, token: string): { id: number; username: string } | null` — null se scaduta (e la elimina).
|
||
+ - `logout(db, token: string): void`
|
||
+ - Costante `SESSION_COOKIE = 'session'`, durata 7 giorni.
|
||
+ - Middleware: protegge `/admin*` (tranne `/admin/login`) e `/api/admin*`; popola `Astro.locals.user`.
|
||
+
|
||
+- [ ] **Step 1: test auth (falliranno)**
|
||
+
|
||
+`tests/auth.test.ts`:
|
||
+```ts
|
||
+import { describe, it, expect, beforeEach } from 'vitest';
|
||
+import type Database from 'better-sqlite3';
|
||
+import { createDb } from '../src/lib/db';
|
||
+import { hashPassword, verifyPassword, createUser, login, getSessionUser, logout } from '../src/lib/auth';
|
||
+
|
||
+let db: Database.Database;
|
||
+beforeEach(() => { db = createDb(':memory:'); });
|
||
+
|
||
+describe('auth', () => {
|
||
+ it('hash e verifica password', () => {
|
||
+ const h = hashPassword('segreta123');
|
||
+ expect(h).not.toBe('segreta123');
|
||
+ expect(verifyPassword('segreta123', h)).toBe(true);
|
||
+ expect(verifyPassword('sbagliata', h)).toBe(false);
|
||
+ });
|
||
+
|
||
+ it('login corretto crea sessione recuperabile', () => {
|
||
+ createUser(db, 'adriano', 'segreta123');
|
||
+ const token = login(db, 'adriano', 'segreta123');
|
||
+ expect(token).toBeTruthy();
|
||
+ const user = getSessionUser(db, token!);
|
||
+ expect(user).toMatchObject({ username: 'adriano' });
|
||
+ });
|
||
+
|
||
+ it('login errato ritorna null', () => {
|
||
+ createUser(db, 'adriano', 'segreta123');
|
||
+ expect(login(db, 'adriano', 'sbagliata')).toBeNull();
|
||
+ expect(login(db, 'inesistente', 'x')).toBeNull();
|
||
+ });
|
||
+
|
||
+ it('sessione scaduta viene rifiutata ed eliminata', () => {
|
||
+ createUser(db, 'adriano', 'segreta123');
|
||
+ const token = login(db, 'adriano', 'segreta123')!;
|
||
+ db.prepare("UPDATE sessions SET expires_at = datetime('now', '-1 day')").run();
|
||
+ expect(getSessionUser(db, token)).toBeNull();
|
||
+ expect(db.prepare('SELECT COUNT(*) AS n FROM sessions').get()).toMatchObject({ n: 0 });
|
||
+ });
|
||
+
|
||
+ it('logout elimina la sessione', () => {
|
||
+ createUser(db, 'adriano', 'segreta123');
|
||
+ const token = login(db, 'adriano', 'segreta123')!;
|
||
+ logout(db, token);
|
||
+ expect(getSessionUser(db, token)).toBeNull();
|
||
+ });
|
||
+});
|
||
+```
|
||
+
|
||
+- [ ] **Step 2: verifica che falliscano**
|
||
+
|
||
+Run: `npx vitest run tests/auth.test.ts`
|
||
+Expected: FAIL (modulo `auth` mancante).
|
||
+
|
||
+- [ ] **Step 3: implementa auth.ts**
|
||
+
|
||
+`src/lib/auth.ts`:
|
||
+```ts
|
||
+import type Database from 'better-sqlite3';
|
||
+import bcrypt from 'bcryptjs';
|
||
+import { randomBytes } from 'node:crypto';
|
||
+
|
||
+export const SESSION_COOKIE = 'session';
|
||
+const SESSION_DAYS = 7;
|
||
+
|
||
+export function hashPassword(plain: string): string {
|
||
+ return bcrypt.hashSync(plain, 12);
|
||
+}
|
||
+
|
||
+export function verifyPassword(plain: string, hash: string): boolean {
|
||
+ return bcrypt.compareSync(plain, hash);
|
||
+}
|
||
+
|
||
+export function createUser(db: Database.Database, username: string, password: string): number {
|
||
+ const res = db.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)')
|
||
+ .run(username, hashPassword(password));
|
||
+ return Number(res.lastInsertRowid);
|
||
+}
|
||
+
|
||
+export function login(db: Database.Database, username: string, password: string): string | null {
|
||
+ const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username) as
|
||
+ | { id: number; password_hash: string } | undefined;
|
||
+ if (!user || !verifyPassword(password, user.password_hash)) return null;
|
||
+ const token = randomBytes(32).toString('hex');
|
||
+ db.prepare(
|
||
+ `INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, datetime('now', '+${SESSION_DAYS} days'))`
|
||
+ ).run(token, user.id);
|
||
+ return token;
|
||
+}
|
||
+
|
||
+export function getSessionUser(db: Database.Database, token: string): { id: number; username: string } | null {
|
||
+ const row = db.prepare(
|
||
+ `SELECT s.token, s.expires_at, u.id, u.username
|
||
+ FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.token = ?`
|
||
+ ).get(token) as { token: string; expires_at: string; id: number; username: string } | undefined;
|
||
+ if (!row) return null;
|
||
+ if (row.expires_at <= new Date().toISOString().slice(0, 19).replace('T', ' ')) {
|
||
+ db.prepare('DELETE FROM sessions WHERE token = ?').run(token);
|
||
+ return null;
|
||
+ }
|
||
+ return { id: row.id, username: row.username };
|
||
+}
|
||
+
|
||
+export function logout(db: Database.Database, token: string): void {
|
||
+ db.prepare('DELETE FROM sessions WHERE token = ?').run(token);
|
||
+}
|
||
+```
|
||
+
|
||
+- [ ] **Step 4: verifica test verdi**
|
||
+
|
||
+Run: `npm test`
|
||
+Expected: tutti PASS.
|
||
+
|
||
+- [ ] **Step 5: middleware**
|
||
+
|
||
+`src/middleware.ts`:
|
||
+```ts
|
||
+import { defineMiddleware } from 'astro:middleware';
|
||
+import { getDb } from './lib/db';
|
||
+import { getSessionUser, SESSION_COOKIE } from './lib/auth';
|
||
+
|
||
+export const onRequest = defineMiddleware((context, next) => {
|
||
+ const { pathname } = context.url;
|
||
+ const isProtected =
|
||
+ (pathname.startsWith('/admin') && pathname !== '/admin/login') ||
|
||
+ pathname.startsWith('/api/admin');
|
||
+ if (!isProtected) return next();
|
||
+
|
||
+ const token = context.cookies.get(SESSION_COOKIE)?.value;
|
||
+ const user = token ? getSessionUser(getDb(), token) : null;
|
||
+ if (!user) {
|
||
+ if (pathname.startsWith('/api/')) {
|
||
+ return new Response(JSON.stringify({ error: 'Non autorizzato' }), {
|
||
+ status: 401, headers: { 'Content-Type': 'application/json' },
|
||
+ });
|
||
+ }
|
||
+ return context.redirect('/admin/login');
|
||
+ }
|
||
+ context.locals.user = user;
|
||
+ return next();
|
||
+});
|
||
+```
|
||
+
|
||
+- [ ] **Step 6: script create-user**
|
||
+
|
||
+`scripts/create-user.mjs`:
|
||
+```js
|
||
+import Database from 'better-sqlite3';
|
||
+import bcrypt from 'bcryptjs';
|
||
+import { mkdirSync } from 'node:fs';
|
||
+import { dirname } from 'node:path';
|
||
+
|
||
+const [, , username, password] = process.argv;
|
||
+if (!username || !password) {
|
||
+ console.error('Uso: npm run create-user -- <username> <password>');
|
||
+ process.exit(1);
|
||
+}
|
||
+const path = process.env.DB_PATH ?? 'data/insanitylab.db';
|
||
+mkdirSync(dirname(path), { recursive: true });
|
||
+const db = new Database(path);
|
||
+db.exec(`CREATE TABLE IF NOT EXISTS users (
|
||
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
+ username TEXT NOT NULL UNIQUE,
|
||
+ password_hash TEXT NOT NULL
|
||
+);`);
|
||
+db.prepare(
|
||
+ `INSERT INTO users (username, password_hash) VALUES (?, ?)
|
||
+ ON CONFLICT(username) DO UPDATE SET password_hash = excluded.password_hash`
|
||
+).run(username, bcrypt.hashSync(password, 12));
|
||
+console.log(`Utente "${username}" creato/aggiornato in ${path}`);
|
||
+```
|
||
+
|
||
+Run: `npm run create-user -- admin prova1234 && sqlite3 data/insanitylab.db 'SELECT username FROM users;'`
|
||
+Expected: `admin`.
|
||
+
|
||
+- [ ] **Step 7: commit**
|
||
+
|
||
+```bash
|
||
+git add src/lib/auth.ts src/middleware.ts scripts/create-user.mjs tests/auth.test.ts
|
||
+git commit -m "feat: autenticazione a sessioni, middleware admin e script create-user"
|
||
+```
|
||
+
|
||
+---
|
||
+
|
||
+### Task 7: Layout base, Header, Footer, 404
|
||
+
|
||
+**Files:**
|
||
+- Create: `src/layouts/Base.astro`, `src/components/Header.astro`, `src/components/Footer.astro`, `src/components/ContactForm.astro`, `src/pages/404.astro`
|
||
+- Modify: `src/pages/index.astro` (usa Base)
|
||
+- Depends: `src/data/site.ts` (Task 8 — eseguire Task 8 prima o creare qui il file e ampliarlo lì; ordine consigliato: Task 8 poi Task 7 se si preferisce, sono indipendenti dagli altri)
|
||
+
|
||
+**Interfaces:**
|
||
+- Consumes: `site` da `src/data/site.ts` (Task 8): `{ name, phone, email, address, cityLine, hours: string[], socials: { label, url }[], navigation: { label, href }[] }`.
|
||
+- Produces: `<Base title description>` layout con slot; `<ContactForm variant="full" | "footer" />` che POSTa a `/api/contact` via fetch.
|
||
+
|
||
+- [ ] **Step 1: Base.astro**
|
||
+
|
||
+```astro
|
||
+---
|
||
+import '@fontsource/montserrat/400.css';
|
||
+import '@fontsource/montserrat/500.css';
|
||
+import '@fontsource/montserrat/600.css';
|
||
+import '@fontsource/montserrat/700.css';
|
||
+import '@fontsource/open-sans/400.css';
|
||
+import '@fontsource/open-sans/600.css';
|
||
+import '../styles/global.css';
|
||
+import Header from '../components/Header.astro';
|
||
+import Footer from '../components/Footer.astro';
|
||
+
|
||
+interface Props { title: string; description?: string }
|
||
+const { title, description = 'InsanityLab — Performance. Balance. Longevity. Allenamento personalizzato, piccoli gruppi e benessere a Bologna.' } = Astro.props;
|
||
+---
|
||
+<!doctype html>
|
||
+<html lang="it">
|
||
+ <head>
|
||
+ <meta charset="utf-8" />
|
||
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
+ <title>{title} — InsanityLab</title>
|
||
+ <meta name="description" content={description} />
|
||
+ <meta property="og:title" content={`${title} — InsanityLab`} />
|
||
+ <meta property="og:description" content={description} />
|
||
+ <meta property="og:type" content="website" />
|
||
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||
+ <link rel="sitemap" href="/sitemap-index.xml" />
|
||
+ </head>
|
||
+ <body>
|
||
+ <Header />
|
||
+ <main><slot /></main>
|
||
+ <Footer />
|
||
+ </body>
|
||
+</html>
|
||
+```
|
||
+
|
||
+- [ ] **Step 2: Header.astro**
|
||
+
|
||
+Nav da `site.navigation`: Insanitylab→`/about`, Training→`/training`, Programmi→`/programs`, Blog & Edugo→`/blog`, Contatti→`/contact`. Logo `/img/logo-dark.png` a sinistra, link `/`.
|
||
+
|
||
+```astro
|
||
+---
|
||
+import { site } from '../data/site';
|
||
+const path = Astro.url.pathname;
|
||
+---
|
||
+<header class="hdr" id="site-header">
|
||
+ <div class="container hdr__in">
|
||
+ <a href="/" class="hdr__logo"><img src="/img/logo-dark.png" alt="InsanityLab" width="150" /></a>
|
||
+ <nav class="hdr__nav" id="site-nav" aria-label="principale">
|
||
+ {site.navigation.map((item) => (
|
||
+ <a href={item.href} class:list={['hdr__link', { 'is-active': path === item.href || (item.href !== '/' && path.startsWith(item.href)) }]}>{item.label}</a>
|
||
+ ))}
|
||
+ </nav>
|
||
+ <button class="hdr__burger" id="nav-toggle" aria-label="Apri menu" aria-expanded="false">
|
||
+ <span></span><span></span><span></span>
|
||
+ </button>
|
||
+ </div>
|
||
+</header>
|
||
+
|
||
+<style>
|
||
+ .hdr { position: sticky; top: 0; z-index: 100; background: #fff; transition: box-shadow .2s; }
|
||
+ .hdr.is-scrolled { box-shadow: 0 2px 14px rgba(0,0,0,.08); }
|
||
+ .hdr__in { display: flex; align-items: center; justify-content: space-between; height: var(--header-h); }
|
||
+ .hdr__nav { display: flex; gap: 34px; }
|
||
+ .hdr__link { font-family: var(--font-heading); font-size: .72rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; text-decoration: none; color: var(--c-heading); }
|
||
+ .hdr__link:hover, .hdr__link.is-active { color: var(--c-accent-dark); }
|
||
+ .hdr__burger { display: none; background: none; border: 0; cursor: pointer; padding: 8px; }
|
||
+ .hdr__burger span { display: block; width: 22px; height: 2px; background: var(--c-heading); margin: 5px 0; }
|
||
+ @media (max-width: 991px) {
|
||
+ .hdr__nav { display: none; position: absolute; top: var(--header-h); left: 0; right: 0; background: #fff; flex-direction: column; gap: 0; padding: 10px 20px 20px; box-shadow: 0 10px 20px rgba(0,0,0,.08); }
|
||
+ .hdr__nav.is-open { display: flex; }
|
||
+ .hdr__link { padding: 12px 0; }
|
||
+ .hdr__burger { display: block; }
|
||
+ }
|
||
+</style>
|
||
+
|
||
+<script>
|
||
+ const header = document.getElementById('site-header')!;
|
||
+ const toggle = document.getElementById('nav-toggle')!;
|
||
+ const nav = document.getElementById('site-nav')!;
|
||
+ addEventListener('scroll', () => header.classList.toggle('is-scrolled', scrollY > 10), { passive: true });
|
||
+ toggle.addEventListener('click', () => {
|
||
+ const open = nav.classList.toggle('is-open');
|
||
+ toggle.setAttribute('aria-expanded', String(open));
|
||
+ });
|
||
+</script>
|
||
+```
|
||
+
|
||
+- [ ] **Step 3: ContactForm.astro**
|
||
+
|
||
+```astro
|
||
+---
|
||
+interface Props { variant?: 'full' | 'footer' }
|
||
+const { variant = 'full' } = Astro.props;
|
||
+---
|
||
+<form class:list={['cform', `cform--${variant}`]} method="post" action="/api/contact" novalidate>
|
||
+ <input class="field" name="firstName" placeholder="Nome" required maxlength="100" />
|
||
+ {variant === 'full' && <input class="field" name="lastName" placeholder="Cognome" maxlength="100" />}
|
||
+ {variant === 'full' && <input class="field" name="phone" placeholder="Telefono" maxlength="40" />}
|
||
+ <input class="field" type="email" name="email" placeholder="E-mail" required maxlength="200" />
|
||
+ <textarea class="field" name="message" placeholder="Messaggio" required maxlength="5000"></textarea>
|
||
+ <input class="hp" type="text" name="website" tabindex="-1" autocomplete="off" />
|
||
+ <button class="btn" type="submit">Invia messaggio</button>
|
||
+ <p class="form-msg" hidden></p>
|
||
+</form>
|
||
+
|
||
+<script>
|
||
+ document.querySelectorAll<HTMLFormElement>('.cform').forEach((form) => {
|
||
+ form.addEventListener('submit', async (e) => {
|
||
+ e.preventDefault();
|
||
+ const msg = form.querySelector<HTMLParagraphElement>('.form-msg')!;
|
||
+ const btn = form.querySelector<HTMLButtonElement>('button')!;
|
||
+ btn.disabled = true;
|
||
+ msg.hidden = true;
|
||
+ try {
|
||
+ const res = await fetch('/api/contact', {
|
||
+ method: 'POST',
|
||
+ headers: { 'Content-Type': 'application/json' },
|
||
+ body: JSON.stringify(Object.fromEntries(new FormData(form))),
|
||
+ });
|
||
+ const data = await res.json();
|
||
+ msg.textContent = res.ok ? 'Messaggio inviato, ti ricontatteremo al più presto.' : (data.error ?? 'Errore di invio, riprova più tardi.');
|
||
+ msg.className = `form-msg ${res.ok ? 'form-msg--ok' : 'form-msg--err'}`;
|
||
+ if (res.ok) form.reset();
|
||
+ } catch {
|
||
+ msg.textContent = 'Errore di rete, riprova più tardi.';
|
||
+ msg.className = 'form-msg form-msg--err';
|
||
+ }
|
||
+ msg.hidden = false;
|
||
+ btn.disabled = false;
|
||
+ });
|
||
+ });
|
||
+</script>
|
||
+```
|
||
+
|
||
+- [ ] **Step 4: Footer.astro**
|
||
+
|
||
+Quattro colonne come da Figma (Homepage.pdf, fondo `--c-dark`): brand (logo light, tagline "PERFORMANCE. BALANCE. LONGEVITY.", testo breve, telefono, indirizzo, orari), colonna TRAINING (One to one, Small groups, Performance class, Fit remote → `/training`), colonna PROGRAMMI (le 6 voci → `/programs/<slug>`), colonna CONTACT con `<ContactForm variant="footer" />`. Barra finale: `© {new Date().getFullYear()} Insanitylab, All Rights Reserved` + "Seguici" con link social da `site.socials`.
|
||
+
|
||
+```astro
|
||
+---
|
||
+import { site } from '../data/site';
|
||
+import ContactForm from './ContactForm.astro';
|
||
+const year = new Date().getFullYear();
|
||
+---
|
||
+<footer class="ftr">
|
||
+ <div class="container ftr__grid">
|
||
+ <div>
|
||
+ <img src="/img/logo-light.png" alt="InsanityLab" width="140" loading="lazy" />
|
||
+ <p class="ftr__tag">PERFORMANCE. BALANCE. LONGEVITY.</p>
|
||
+ <p>Promuoviamo il movimento come cura di sé. Abitudini sostenibili per benessere fisico e mentale.</p>
|
||
+ <p>{site.phone}<br />{site.address}, {site.cityLine}</p>
|
||
+ {site.hours.map((h) => <p class="ftr__hours">{h}</p>)}
|
||
+ </div>
|
||
+ <nav aria-label="training">
|
||
+ <h4>Training</h4>
|
||
+ {site.footerTraining.map((l) => <a href={l.href}>{l.label}</a>)}
|
||
+ </nav>
|
||
+ <nav aria-label="programmi">
|
||
+ <h4>Programmi</h4>
|
||
+ {site.footerPrograms.map((l) => <a href={l.href}>{l.label}</a>)}
|
||
+ </nav>
|
||
+ <div>
|
||
+ <h4>Contact</h4>
|
||
+ <ContactForm variant="footer" />
|
||
+ </div>
|
||
+ </div>
|
||
+ <div class="ftr__bar">
|
||
+ <div class="container ftr__bar-in">
|
||
+ <p>© {year} <strong>Insanitylab</strong>, All Rights Reserved</p>
|
||
+ <p>Seguici {site.socials.map((s) => <a href={s.url} rel="noopener" target="_blank">{s.label}</a>)}</p>
|
||
+ </div>
|
||
+ </div>
|
||
+</footer>
|
||
+
|
||
+<style>
|
||
+ .ftr { background: var(--c-dark); color: #b9aea4; font-size: .88rem; }
|
||
+ .ftr a { color: #b9aea4; text-decoration: none; display: block; padding: 4px 0; }
|
||
+ .ftr a:hover { color: #fff; }
|
||
+ .ftr h4 { color: #fff; text-transform: uppercase; letter-spacing: .18em; font-size: .78rem; margin-bottom: 18px; }
|
||
+ .ftr__grid { display: grid; grid-template-columns: 1.3fr 1fr 1fr 1.3fr; gap: 40px; padding-block: 70px; }
|
||
+ .ftr__tag { font-family: var(--font-heading); font-size: .7rem; letter-spacing: .2em; color: #fff; }
|
||
+ .ftr__hours { margin: 0; }
|
||
+ .ftr__bar { border-top: 1px solid rgba(255,255,255,.08); padding-block: 18px; }
|
||
+ .ftr__bar-in { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px; }
|
||
+ .ftr__bar-in a { display: inline; margin-left: 12px; }
|
||
+ @media (max-width: 991px) { .ftr__grid { grid-template-columns: 1fr 1fr; } }
|
||
+ @media (max-width: 600px) { .ftr__grid { grid-template-columns: 1fr; } }
|
||
+</style>
|
||
+```
|
||
+
|
||
+- [ ] **Step 5: 404.astro**
|
||
+
|
||
+```astro
|
||
+---
|
||
+import Base from '../layouts/Base.astro';
|
||
+export const prerender = true;
|
||
+---
|
||
+<Base title="Pagina non trovata">
|
||
+ <section class="section" style="text-align:center; padding-block:140px;">
|
||
+ <div class="container">
|
||
+ <p class="eyebrow">Errore 404</p>
|
||
+ <h1>Pagina non trovata</h1>
|
||
+ <p>La pagina che cerchi non esiste o è stata spostata.</p>
|
||
+ <a class="btn" href="/">Torna alla home</a>
|
||
+ </div>
|
||
+ </section>
|
||
+</Base>
|
||
+```
|
||
+
|
||
+- [ ] **Step 6: aggiorna index placeholder per usare Base**
|
||
+
|
||
+```astro
|
||
+---
|
||
+import Base from '../layouts/Base.astro';
|
||
+export const prerender = true;
|
||
+---
|
||
+<Base title="Performance. Balance. Longevity.">
|
||
+ <section class="section"><div class="container"><h1>Homepage in costruzione</h1></div></section>
|
||
+</Base>
|
||
+```
|
||
+
|
||
+- [ ] **Step 7: verifica visiva e build**
|
||
+
|
||
+Run: `npm run dev` e apri `http://localhost:4321/` — header sticky con nav, footer scuro 4 colonne, pagina 404 su URL inesistente. Poi `npm run build`.
|
||
+Expected: nessun errore; nav mobile funzionante sotto 992px.
|
||
+
|
||
+- [ ] **Step 8: commit**
|
||
+
|
||
+```bash
|
||
+git add -A && git commit -m "feat: layout base con header, footer e 404"
|
||
+```
|
||
+
|
||
+---
|
||
+
|
||
+### Task 8: Dati statici dei contenuti
|
||
+
|
||
+**Files:**
|
||
+- Create: `src/data/site.ts`, `src/data/programs.ts`, `src/data/trainings.ts`, `src/data/agenda.ts`, `src/data/team.ts`, `src/data/method.ts`, `src/data/pricing.ts`, `src/data/partners.ts`
|
||
+
|
||
+**Interfaces:**
|
||
+- Produces: export nominati usati dalle pagine. Testi reali dai PDF Figma (riportati sotto). Nessun lorem ipsum salvo dove il Figma stesso è placeholder (agenda, che riusa i nomi del template).
|
||
+
|
||
+- [ ] **Step 1: site.ts**
|
||
+
|
||
+```ts
|
||
+export const site = {
|
||
+ name: 'InsanityLab',
|
||
+ phone: '351 7535977',
|
||
+ email: 'info@insanitylab.it',
|
||
+ address: 'Via Leandro Alberti, 76',
|
||
+ cityLine: '40139 Bologna',
|
||
+ hours: ['Lun – Ven 6.00 – 21.00', 'Sab 8.00 – 19.00 · Dom chiuso'],
|
||
+ socials: [
|
||
+ { label: 'Instagram', url: 'https://www.instagram.com/insanitylab' },
|
||
+ { label: 'Facebook', url: 'https://www.facebook.com/insanitylab' },
|
||
+ { label: 'LinkedIn', url: 'https://www.linkedin.com/company/insanitylab' },
|
||
+ ],
|
||
+ navigation: [
|
||
+ { label: 'Insanitylab', href: '/about' },
|
||
+ { label: 'Training', href: '/training' },
|
||
+ { label: 'Programmi', href: '/programs' },
|
||
+ { label: 'Blog & Edugo', href: '/blog' },
|
||
+ { label: 'Contatti', href: '/contact' },
|
||
+ ],
|
||
+ footerTraining: [
|
||
+ { label: 'One to one', href: '/training#one-to-one' },
|
||
+ { label: 'Small groups', href: '/training#small-groups' },
|
||
+ { label: 'Performance class', href: '/training#performance-class' },
|
||
+ { label: 'Fit remote', href: '/training#fit-remote' },
|
||
+ ],
|
||
+ footerPrograms: [
|
||
+ { label: 'Personal Training', href: '/programs/personal-training' },
|
||
+ { label: 'Performance Class', href: '/programs/performance-class' },
|
||
+ { label: 'Coaching', href: '/programs/coaching' },
|
||
+ { label: 'Rehab', href: '/programs/rehab' },
|
||
+ { label: 'Pilates Flow', href: '/programs/pilates-flow' },
|
||
+ { label: 'Nutrizione', href: '/programs/nutrition' },
|
||
+ ],
|
||
+};
|
||
+```
|
||
+
|
||
+- [ ] **Step 2: programs.ts**
|
||
+
|
||
+Excerpt = testi reali di Programmi.pdf. `longDescription` e `faq`: per `performance-class` dai testi di Performance class.pdf; per gli altri derivati dall'excerpt e da Training.pdf (il cliente li integrerà — testo comunque sensato, niente lorem).
|
||
+
|
||
+```ts
|
||
+export interface Program {
|
||
+ slug: string;
|
||
+ title: string;
|
||
+ subtitle: string;
|
||
+ excerpt: string;
|
||
+ longDescription: string;
|
||
+ features: string[];
|
||
+ faq: { q: string; a: string }[];
|
||
+ hasPricing: boolean;
|
||
+}
|
||
+
|
||
+export const programs: Program[] = [
|
||
+ {
|
||
+ slug: 'personal-training',
|
||
+ title: 'Personal Training',
|
||
+ subtitle: 'One to One / Small Group',
|
||
+ excerpt: 'Il personal training InsanityLab è un percorso one-to-one su misura, seguito da un professionista dedicato, per raggiungere in modo sicuro ed efficace obiettivi di forma fisica, performance e benessere.',
|
||
+ longDescription: 'Allenati con un trainer dedicato che costruisce un percorso su misura per i tuoi obiettivi. Ogni sessione one to one è pensata per migliorare performance, tecnica e risultati, con supporto costante, attenzione totale e allenamenti personalizzati. Disponibile anche in formula small group (massimo 3 persone) per unire attenzione personalizzata, motivazione e condivisione.',
|
||
+ features: ['Trainer dedicato', 'Percorso su misura', 'Valutazione iniziale e monitoraggio', 'Formula one to one o small group (max 3)'],
|
||
+ faq: [
|
||
+ { q: 'Come si svolge la prima seduta?', a: 'Con una consulenza conoscitiva iniziale: raccogliamo l’anamnesi, definiamo gli obiettivi e impostiamo il percorso più adatto.' },
|
||
+ { q: 'Con quale frequenza ci si allena?', a: 'La frequenza viene definita insieme al trainer in base a obiettivi, disponibilità e livello di partenza.' },
|
||
+ { q: 'È adatto a chi parte da zero?', a: 'Sì: il percorso è costruito sul tuo livello, con progressioni graduali e supervisione costante.' },
|
||
+ ],
|
||
+ hasPricing: false,
|
||
+ },
|
||
+ {
|
||
+ slug: 'performance-class',
|
||
+ title: 'Performance Class',
|
||
+ subtitle: 'Gruppo max 8 persone',
|
||
+ excerpt: 'La Performance Class è un allenamento in piccoli gruppi, basato su una programmazione progressiva che combina forza e condizionamento per ottenere risultati concreti e costanti nel tempo.',
|
||
+ longDescription: 'La Performance Class è un allenamento in piccoli gruppi con programmazione progressiva e scientifica, pensato per migliorare forza, composizione corporea e performance attraverso sessioni strutturate che combinano forza, ipertrofia e condizionamento metabolico. Non si improvvisa: ogni sessione ha un posto preciso all’interno di un piano più ampio.',
|
||
+ features: ['Allenamento ibrido', 'Forza, ipertrofia, condizionamento metabolico e lavoro funzionale', 'Sessioni da 50 minuti', 'Fasi di bassa, media e alta intensità', 'Programmazione mensile'],
|
||
+ faq: [
|
||
+ { q: 'Che cosa si intende per Performance Class?', a: 'Un allenamento in piccoli gruppi (massimo 8 persone) guidato da trainer certificati, con programmazione progressiva mensile: ogni sessione ha un posto preciso all’interno di un piano più ampio.' },
|
||
+ { q: 'Che tipo di allenamento viene svolto?', a: 'Un allenamento ibrido che alterna forza, ipertrofia, condizionamento metabolico e lavoro funzionale. Le sessioni durano 50 minuti e prevedono fasi di bassa, media e alta intensità, calibrate sulla programmazione mensile.' },
|
||
+ { q: 'A chi è consigliato questo servizio?', a: 'A chi vuole allenarsi con qualità e continuità in un contesto di gruppo motivante, mantenendo una programmazione seria e misurabile.' },
|
||
+ { q: 'Qual è la differenza rispetto a una class tradizionale?', a: 'La programmazione: non è una lezione isolata ma parte di un piano mensile progressivo, con carichi e intensità calibrati.' },
|
||
+ { q: 'Che tipo di risultati o miglioramenti si ottengono?', a: 'Miglioramento di forza, composizione corporea e performance, con risultati concreti e costanti nel tempo.' },
|
||
+ { q: 'Qual è l’intensità del lavoro?', a: 'Alternata: fasi di bassa, media e alta intensità all’interno della stessa sessione, adattate al livello dei partecipanti.' },
|
||
+ ],
|
||
+ hasPricing: true,
|
||
+ },
|
||
+ {
|
||
+ slug: 'coaching',
|
||
+ title: 'Coaching',
|
||
+ subtitle: 'Programma di allenamento e monitoraggio',
|
||
+ excerpt: 'Il coaching InsanityLab è un percorso personalizzato che, con programma su misura e supporto costante, guida la persona verso i propri obiettivi sviluppando autonomia e continuità nell’allenamento.',
|
||
+ longDescription: 'Con il coaching hai un programma di allenamento su misura e un monitoraggio costante, ovunque ti alleni: in palestra, a casa o in viaggio. Il percorso sviluppa autonomia e continuità, adattandosi al tuo stile di vita (formula Fit Remote).',
|
||
+ features: ['Programma su misura', 'Supporto e monitoraggio costanti', 'Ovunque ti alleni (Fit Remote)', 'Sviluppo di autonomia'],
|
||
+ faq: [
|
||
+ { q: 'Come funziona il monitoraggio?', a: 'Il trainer segue i tuoi progressi con check periodici e adatta il programma in base ai risultati.' },
|
||
+ { q: 'Serve attrezzatura?', a: 'Il programma viene costruito sull’attrezzatura che hai a disposizione, in palestra o a casa.' },
|
||
+ { q: 'È adatto se viaggio spesso?', a: 'Sì: la formula Fit Remote è pensata per chi si allena in luoghi diversi.' },
|
||
+ ],
|
||
+ hasPricing: false,
|
||
+ },
|
||
+ {
|
||
+ slug: 'pilates-flow',
|
||
+ title: 'Pilates Flow',
|
||
+ subtitle: 'Movimento e controllo',
|
||
+ excerpt: 'Il Pilates Flow migliora postura, core e consapevolezza corporea con un movimento fluido, mentre l’osteopatia supporta recupero, prevenzione e benessere globale integrandosi con l’allenamento.',
|
||
+ longDescription: 'Il Pilates Flow unisce movimento fluido e controllo: migliora postura, core e consapevolezza corporea. Integrato con l’osteopatia, supporta recupero, prevenzione e benessere globale all’interno del percorso di allenamento.',
|
||
+ features: ['Postura e core', 'Movimento fluido e controllo', 'Integrazione con osteopatia', 'Benessere globale'],
|
||
+ faq: [
|
||
+ { q: 'A chi è adatto il Pilates Flow?', a: 'A chiunque voglia migliorare postura, mobilità e controllo del movimento, a ogni livello.' },
|
||
+ { q: 'Serve esperienza precedente?', a: 'No: le progressioni sono graduali e seguite da professionisti qualificati.' },
|
||
+ { q: 'Come si integra con l’osteopatia?', a: 'Il lavoro osteopatico supporta recupero e prevenzione, in sinergia con le sessioni di movimento.' },
|
||
+ ],
|
||
+ hasPricing: false,
|
||
+ },
|
||
+ {
|
||
+ slug: 'rehab',
|
||
+ title: 'Rehab',
|
||
+ subtitle: 'Recupero post-infortunio, patologie muscolo-scheletriche',
|
||
+ excerpt: 'Il Rehab InsanityLab è un percorso supervisionato che accompagna dal recupero riabilitativo al ritorno all’attività fisica in modo sicuro, progressivo e personalizzato.',
|
||
+ longDescription: 'Il Rehab accompagna dal recupero post-infortunio o da patologie muscolo-scheletriche fino al ritorno all’attività fisica. Percorso supervisionato da chinesiologi e osteopati, sicuro, progressivo e personalizzato.',
|
||
+ features: ['Recupero post-infortunio', 'Patologie muscolo-scheletriche', 'Supervisione di chinesiologi e osteopati', 'Progressione sicura e personalizzata'],
|
||
+ faq: [
|
||
+ { q: 'Chi segue il percorso Rehab?', a: 'Professionisti specializzati in rieducazione funzionale ed esercizio adattato, in sinergia con l’area osteopatica.' },
|
||
+ { q: 'Serve una diagnosi medica?', a: 'Se disponibile è utile portarla alla prima consulenza; il percorso si integra con le indicazioni dei sanitari.' },
|
||
+ { q: 'Quanto dura il percorso?', a: 'Dipende dalla condizione di partenza: la progressione viene definita e aggiornata durante il percorso.' },
|
||
+ ],
|
||
+ hasPricing: false,
|
||
+ },
|
||
+ {
|
||
+ slug: 'nutrition',
|
||
+ title: 'Nutrizione',
|
||
+ subtitle: 'Alimentazione e stile di vita',
|
||
+ excerpt: 'La nutrizione in InsanityLab è un servizio personalizzato che integra alimentazione e allenamento per migliorare energia, performance e benessere con piani su misura e sostenibili.',
|
||
+ longDescription: 'Il servizio di nutrizione integra alimentazione e allenamento: piani su misura e sostenibili per migliorare energia, performance e benessere, costruiti sul tuo stile di vita.',
|
||
+ features: ['Piani alimentari su misura', 'Integrazione con l’allenamento', 'Sostenibilità nel tempo', 'Focus su energia e performance'],
|
||
+ faq: [
|
||
+ { q: 'Come inizia il percorso di nutrizione?', a: 'Con una valutazione iniziale di abitudini, obiettivi e allenamento, da cui nasce il piano personalizzato.' },
|
||
+ { q: 'È integrato con il training?', a: 'Sì: alimentazione e allenamento vengono programmati in modo coerente.' },
|
||
+ { q: 'Sono previsti controlli periodici?', a: 'Sì, con aggiornamento del piano in base ai risultati.' },
|
||
+ ],
|
||
+ hasPricing: false,
|
||
+ },
|
||
+];
|
||
+
|
||
+export function getProgram(slug: string): Program | undefined {
|
||
+ return programs.find((p) => p.slug === slug);
|
||
+}
|
||
+```
|
||
+
|
||
+- [ ] **Step 3: trainings.ts (4 card Training)**
|
||
+
|
||
+```ts
|
||
+export const trainings = [
|
||
+ { id: 'one-to-one', title: 'One to One', caption: 'Allenamento con un trainer dedicato', image: 'training-one-to-one', description: 'Allenati con un trainer dedicato che costruisce un percorso su misura per i tuoi obiettivi. Ogni sessione one to one è pensata per migliorare performance, tecnica e risultati. Supporto costante, attenzione totale e allenamenti personalizzati per dare il massimo.', cta: { label: 'Contattaci', href: '/contact' } },
|
||
+ { id: 'small-groups', title: 'Small Groups', caption: 'Mini gruppi (max 3 persone) con trainer dedicato', image: 'training-small-groups', description: 'Allenati in mini gruppi da massimo 3 persone con un trainer dedicato. Un’esperienza small group che unisce attenzione personalizzata, motivazione e condivisione. Programmi mirati e supporto costante per raggiungere i tuoi obiettivi insieme.', cta: { label: 'Contattaci', href: '/contact' } },
|
||
+ { id: 'performance-class', title: 'Performance Class', caption: 'Classi fino a 8 persone con trainer certificato', image: 'training-performance-class', description: 'Allenati in classi fino a 8 persone guidate da trainer certificati. Sessioni dinamiche ad alta intensità pensate per migliorare forza, resistenza e performance. Energia, motivazione e allenamenti strutturati per dare il massimo in ogni workout.', cta: { label: 'Scopri', href: '/programs/performance-class' } },
|
||
+ { id: 'fit-remote', title: 'Fit Remote', caption: 'Piano di allenamento su misura, ovunque ti alleni.', image: 'training-fit-remote', description: 'Con Fit Remote hai un piano di allenamento su misura, ovunque ti alleni. Programmi personalizzati creati sui tuoi obiettivi, da seguire in palestra, a casa o in viaggio. Supporto costante e allenamenti pensati per adattarsi al tuo stile di vita.', cta: { label: 'Contattaci', href: '/contact' } },
|
||
+] as const;
|
||
+```
|
||
+
|
||
+- [ ] **Step 4: agenda.ts (orario placeholder dal template, come nel Figma)**
|
||
+
|
||
+```ts
|
||
+export const disciplines = ['Events', 'Crossfit', 'Cardio', 'Box', 'Meditation', 'Yoga Classes', 'Body Balance'] as const;
|
||
+export const days = ['Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato', 'Domenica'] as const;
|
||
+export const slots = ['10.00', '12.00', '16.00', '18.00', '20.00'] as const;
|
||
+
|
||
+export interface AgendaEntry { day: number; slot: string; discipline: string; time: string; trainer: string }
|
||
+
|
||
+// Placeholder dal template Prowess (come nel Figma); il cliente fornirà l'orario reale.
|
||
+export const agenda: AgendaEntry[] = [
|
||
+ { day: 0, slot: '10.00', discipline: 'Crossfit', time: '10.00 - 11.00', trainer: 'Amanda Snow' },
|
||
+ { day: 0, slot: '12.00', discipline: 'Box', time: '12.00 - 13.00', trainer: 'Matthew Dunn' },
|
||
+ { day: 0, slot: '18.00', discipline: 'Cardio', time: '18.00 - 19.00', trainer: 'Randy Nelson' },
|
||
+ { day: 0, slot: '20.00', discipline: 'Crossfit', time: '20.00 - 21.00', trainer: 'Amanda Snow' },
|
||
+ { day: 1, slot: '10.00', discipline: 'Cardio', time: '10.00 - 11.00', trainer: 'Randy Nelson' },
|
||
+ { day: 1, slot: '12.00', discipline: 'Crossfit', time: '12.00 - 13.00', trainer: 'Amanda Snow' },
|
||
+ { day: 1, slot: '16.00', discipline: 'Body Balance', time: '16.00 - 17.00', trainer: 'Barbara Holland' },
|
||
+ { day: 1, slot: '20.00', discipline: 'Box', time: '20.00 - 21.00', trainer: 'Matthew Dunn' },
|
||
+ { day: 2, slot: '10.00', discipline: 'Crossfit', time: '10.00 - 11.00', trainer: 'Amanda Snow' },
|
||
+ { day: 2, slot: '12.00', discipline: 'Meditation', time: '12.00 - 13.00', trainer: 'Nicole Fuller' },
|
||
+ { day: 2, slot: '16.00', discipline: 'Box', time: '16.00 - 17.00', trainer: 'Matthew Dunn' },
|
||
+ { day: 2, slot: '20.00', discipline: 'Body Balance', time: '20.00 - 21.00', trainer: 'Barbara Holland' },
|
||
+ { day: 3, slot: '10.00', discipline: 'Body Balance', time: '10.00 - 11.00', trainer: 'Barbara Holland' },
|
||
+ { day: 3, slot: '12.00', discipline: 'Box', time: '12.00 - 13.00', trainer: 'Matthew Dunn' },
|
||
+ { day: 3, slot: '16.00', discipline: 'Crossfit', time: '16.00 - 17.00', trainer: 'Amanda Snow' },
|
||
+ { day: 3, slot: '20.00', discipline: 'Body Balance', time: '20.00 - 21.00', trainer: 'Randy Nelson' },
|
||
+ { day: 4, slot: '12.00', discipline: 'Crossfit', time: '12.00 - 13.00', trainer: 'Amanda Snow' },
|
||
+ { day: 4, slot: '16.00', discipline: 'Body Balance', time: '16.00 - 17.00', trainer: 'Barbara Holland' },
|
||
+ { day: 4, slot: '18.00', discipline: 'Cardio', time: '18.00 - 19.00', trainer: 'Randy Nelson' },
|
||
+ { day: 4, slot: '20.00', discipline: 'Yoga Classes', time: '20.00 - 21.00', trainer: 'Amanda Snow' },
|
||
+ { day: 5, slot: '10.00', discipline: 'Cardio', time: '10.00 - 11.00', trainer: 'Randy Nelson' },
|
||
+ { day: 5, slot: '12.00', discipline: 'Body Balance', time: '12.00 - 13.00', trainer: 'Barbara Holland' },
|
||
+ { day: 5, slot: '18.00', discipline: 'Box', time: '18.00 - 19.00', trainer: 'Matthew Dunn' },
|
||
+ { day: 6, slot: '12.00', discipline: 'Crossfit', time: '12.00 - 13.00', trainer: 'Amanda Snow' },
|
||
+ { day: 6, slot: '16.00', discipline: 'Cardio', time: '16.00 - 17.00', trainer: 'Randy Nelson' },
|
||
+ { day: 6, slot: '18.00', discipline: 'Box', time: '18.00 - 19.00', trainer: 'Matthew Dunn' },
|
||
+ { day: 6, slot: '20.00', discipline: 'Body Balance', time: '20.00 - 21.00', trainer: 'Barbara Holland' },
|
||
+];
|
||
+```
|
||
+
|
||
+- [ ] **Step 5: team.ts**
|
||
+
|
||
+```ts
|
||
+export interface Member { name: string; shortName: string; role: string; bio: string; image?: string; founder: boolean }
|
||
+
|
||
+export const team: Member[] = [
|
||
+ { name: 'Donata Dal Pastro', shortName: 'Donata', role: 'Chinesiologa e Osteopata', founder: true, image: 'team-donata',
|
||
+ bio: 'Donata Dal Pastro è chinesiologa e osteopata specializzata in rieducazione funzionale, riatletizzazione sportiva ed esercizio adattato. Supervisiona l’area dedicata a prevenzione, recupero e benessere, con competenze specifiche anche in pediatria e gravidanza.' },
|
||
+ { name: 'Nicola Antonelli', shortName: 'Nicola', role: 'Chinesiologo e Osteopata', founder: true, image: 'team-nicola',
|
||
+ bio: 'Nicola Antonelli è chinesiologo e osteopata specializzato in performance sportiva, functional training e bodybuilding. Coordina lo sviluppo tecnico dei percorsi dedicati alla performance e all’allenamento avanzato.' },
|
||
+ { name: 'Eva Cassetta', shortName: 'Eva', role: 'Business Manager – Balance & Strategy', founder: true,
|
||
+ bio: 'Eva Cassetta ricopre il ruolo di Business Manager – Balance & Strategy e si occupa della gestione amministrativa, economica e finanziaria del progetto, garantendone solidità, organizzazione e sostenibilità nel tempo.' },
|
||
+ { name: 'Alessandro Lazzari', shortName: 'Alessandro', role: 'Trainer – Functional & Movement', founder: false,
|
||
+ bio: 'Trainer – Functional & Movement. Laureando in Scienze Motorie, specializzato in running e atletica leggera.' },
|
||
+ { name: 'Eleonora Cassani', shortName: 'Eleonora', role: 'Trainer – Planning Manager', founder: false, image: 'team-eleonora',
|
||
+ bio: 'Trainer – Planning Manager. Chinesiologa specializzata in attività preventiva e adattata, con focus su allenamento in gravidanza e post-gravidanza.' },
|
||
+ { name: "Antonello D'Alessandro", shortName: 'Antonello', role: 'Trainer – Exercise & Health', founder: false, image: 'team-antonello',
|
||
+ bio: 'Trainer – Exercise & Health. Chinesiologo specializzato in attività preventiva e adattata, Dottore in Osteopatia.' },
|
||
+ { name: 'Alice Roda', shortName: 'Alice', role: 'Trainer – Functional & Movement', founder: false,
|
||
+ bio: 'Trainer – Functional & Movement. Laureanda in Scienze Motorie e Preparatore Atletico. Scienza del movimento e massima cura della persona: un approccio al functional training empatico e accogliente, dove l’ascolto umano guida e supporta ogni tuo progresso.' },
|
||
+ { name: 'Davide Bisagni', shortName: 'Davide', role: 'Trainer – Functional & Movement', founder: false,
|
||
+ bio: 'Trainer – Functional & Movement. Chinesiologo e allenamento funzionale al servizio del corpo: massima cura del gesto atletico e attenzione mirata al recupero per prestazioni sostenibili e durature.' },
|
||
+ { name: 'Giulia Barbàra', shortName: 'Giulia', role: 'Trainer – Pilates, Yoga & Mindfulness', founder: false,
|
||
+ bio: 'Trainer – Pilates, Yoga & Mindfulness. Laureata in Psicologia Clinica e Riabilitazione, insegnante di Pilates, Yoga e Mindfulness, con approccio orientato al benessere e all’equilibrio mente-corpo.' },
|
||
+];
|
||
+
|
||
+export const homeTeam = ['Donata', 'Nicola', 'Eleonora', 'Antonello'];
|
||
+```
|
||
+
|
||
+- [ ] **Step 6: method.ts, pricing.ts, partners.ts**
|
||
+
|
||
+`src/data/method.ts`:
|
||
+```ts
|
||
+export const methodIntro = 'Il Metodo Insanitylab si basa su un percorso strutturato in fasi, dove le prime due rappresentano il focus standard del lavoro: valutazione iniziale e avvio dell’allenamento personalizzato. A queste possono essere aggiunti servizi e approfondimenti extra, in base alle esigenze e agli obiettivi della persona.';
|
||
+export const methodExtraNote = 'Disponibili su richiesta, non incluse nel percorso standard.';
|
||
+
|
||
+export const methodSteps = [
|
||
+ { n: 1, title: 'Prima consulenza conoscitiva iniziale', text: 'Dove raccogliamo un’anamnesi iniziale per comprendere il cliente, i suoi obiettivi e guidarlo verso il percorso più adatto.', extra: false, image: 'method-step-1' },
|
||
+ { n: 2, title: 'Prima seduta operativa', text: 'In cui si effettua la profilazione iniziale e la valutazione sul campo per definire il punto di partenza e impostare un percorso personalizzato.', extra: false, image: 'method-step-2' },
|
||
+ { n: 3, title: 'Plicometria', text: 'Test che misura il grasso sottocutaneo per stimare la massa grassa, disponibile su richiesta o come servizio aggiuntivo.', extra: true, image: 'method-step-3' },
|
||
+ { n: 4, title: 'Analisi antropometriche', text: 'Le analisi antropometriche sono misurazioni del corpo utili a valutare la composizione fisica e lo stato di forma.', extra: true, image: 'method-step-4' },
|
||
+];
|
||
+```
|
||
+
|
||
+`src/data/pricing.ts` (prezzi da Performance class.pdf):
|
||
+```ts
|
||
+export const performanceClassPricing = {
|
||
+ heading: 'Acquista la tua performance class',
|
||
+ intro: 'Questi sono i programmi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi un’esperienza di allenamento completa e mirata.',
|
||
+ groups: [
|
||
+ {
|
||
+ label: 'Programmi performance class semestrale',
|
||
+ open: true,
|
||
+ plans: [
|
||
+ { monthly: 130, entries: 1, note: 'tot: 780€ · da 23€ lezione singola', installments: '2 rate da 390€ – inizio 60', best: 'best price 720€' },
|
||
+ { monthly: 234, entries: 2, note: 'tot: 1.400€ · da 23€ lezione singola', installments: '2 rate da 700€ – inizio 60', best: 'best price 1.250€' },
|
||
+ { monthly: 300, entries: 3, note: 'tot: 1.800€ · da 23€ lezione singola', installments: '2 rate da 900€ – inizio 60', best: 'best price 1.650€' },
|
||
+ ],
|
||
+ },
|
||
+ { label: 'Programmi performance class trimestrale', open: false, plans: [] },
|
||
+ { label: 'Programmi performance class mensile', open: false, plans: [] },
|
||
+ ],
|
||
+};
|
||
+```
|
||
+(I gruppi senza piani mostrano "Contattaci per i dettagli" — nel Figma sono accordion chiusi senza contenuto visibile.)
|
||
+
|
||
+`src/data/partners.ts`:
|
||
+```ts
|
||
+export const partners = [
|
||
+ { name: 'Ticket Welfare Edenred', image: 'partner-edenred' },
|
||
+ { name: 'Welbee', image: 'partner-welbee' },
|
||
+ { name: 'AON', image: 'partner-aon' },
|
||
+ { name: 'Howden', image: 'partner-howden' },
|
||
+];
|
||
+```
|
||
+
|
||
+- [ ] **Step 7: verifica typecheck e commit**
|
||
+
|
||
+Run: `npx astro check 2>&1 | tail -5` (o `npx tsc --noEmit`)
|
||
+Expected: nessun errore sui file dati.
|
||
+
|
||
+```bash
|
||
+git add src/data && git commit -m "feat: dati statici contenuti da design Figma"
|
||
+```
|
||
+
|
||
+---
|
||
+
|
||
+### Task 9: Homepage
|
||
+
|
||
+**Files:**
|
||
+- Create: `src/components/home/HeroSlider.astro`, `src/components/home/InfoColumns.astro`, `src/components/home/FeatureBlocks.astro`, `src/components/home/QuoteBanner.astro`, `src/components/home/TrainingCards.astro`, `src/components/home/ProgramsGrid.astro`, `src/components/Agenda.astro`, `src/components/home/TeamSection.astro`, `src/components/home/MethodSteps.astro`, `src/components/home/PartnersStrip.astro`
|
||
+- Modify: `src/pages/index.astro`
|
||
+
|
||
+**Interfaces:**
|
||
+- Consumes: dati Task 8, immagini Task 2 (`src/assets/img/*` via `astro:assets`), utility CSS Task 3.
|
||
+- Produces: `<Agenda />` riusato dal Task 11; `<TrainingCards />` riusato dal Task 11.
|
||
+
|
||
+Riferimento visivo: `../FigmaDoc/Homepage.pdf`. Per confronto genera le fette: `pdftoppm -png -r 50 ../FigmaDoc/Homepage.pdf /tmp/ilab-home && convert /tmp/ilab-home-1.png -crop x1800 +repage /tmp/ilab-home-slice_%d.png`.
|
||
+
|
||
+- [ ] **Step 1: HeroSlider.astro**
|
||
+
|
||
+Hero fullscreen: titolo `PERFORMANCE.` / `BALANCE.` / `LONGEVITY.` su righe, bottone SCOPRI → `/programs`, immagine hero a destra, pattern di linee orizzontali a sinistra, frecce navigazione. Il Figma mostra una sola slide compiuta: implementare struttura a slide con array (1 slide ora, estendibile).
|
||
+
|
||
+```astro
|
||
+---
|
||
+import { Image } from 'astro:assets';
|
||
+import hero1 from '../../assets/img/hero-1.jpg';
|
||
+const slides = [
|
||
+ { title: ['PERFORMANCE.', 'BALANCE.', 'LONGEVITY.'], image: hero1, alt: 'Atleti InsanityLab' },
|
||
+];
|
||
+---
|
||
+<section class="hero" aria-label="presentazione">
|
||
+ {slides.map((s, i) => (
|
||
+ <div class:list={['hero__slide', { 'is-active': i === 0 }]}>
|
||
+ <div class="container hero__in">
|
||
+ <div class="hero__text">
|
||
+ <h1>{s.title.map((line) => <span class="hero__line">{line}</span>)}</h1>
|
||
+ <a class="btn" href="/programs">Scopri</a>
|
||
+ </div>
|
||
+ <Image src={s.image} alt={s.alt} class="hero__img" widths={[600, 1000, 1400]} sizes="(max-width: 991px) 100vw, 60vw" loading="eager" />
|
||
+ </div>
|
||
+ </div>
|
||
+ ))}
|
||
+ {slides.length > 1 && (
|
||
+ <>
|
||
+ <button class="hero__arrow hero__arrow--prev" aria-label="precedente">‹</button>
|
||
+ <button class="hero__arrow hero__arrow--next" aria-label="successiva">›</button>
|
||
+ </>
|
||
+ )}
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .hero { position: relative; background:
|
||
+ repeating-linear-gradient(to bottom, transparent 0 10px, rgba(0,0,0,.05) 10px 11px) left top / 40% 60% no-repeat, #fff; }
|
||
+ .hero__slide { display: none; }
|
||
+ .hero__slide.is-active { display: block; }
|
||
+ .hero__in { display: grid; grid-template-columns: 1fr 1.2fr; align-items: center; min-height: calc(100vh - var(--header-h)); gap: 30px; }
|
||
+ .hero__line { display: block; }
|
||
+ .hero__text h1 { font-weight: 500; }
|
||
+ .hero__img { object-fit: cover; height: 100%; max-height: calc(100vh - var(--header-h)); }
|
||
+ .hero__arrow { position: absolute; top: 50%; transform: translateY(-50%); background: none; border: 0; font-size: 3rem; color: var(--c-heading); cursor: pointer; }
|
||
+ .hero__arrow--prev { left: 12px; } .hero__arrow--next { right: 12px; }
|
||
+ @media (max-width: 991px) { .hero__in { grid-template-columns: 1fr; padding-block: 40px; min-height: 0; } }
|
||
+</style>
|
||
+
|
||
+<script>
|
||
+ const slides = document.querySelectorAll('.hero__slide');
|
||
+ if (slides.length > 1) {
|
||
+ let cur = 0;
|
||
+ const show = (i: number) => {
|
||
+ slides[cur].classList.remove('is-active');
|
||
+ cur = (i + slides.length) % slides.length;
|
||
+ slides[cur].classList.add('is-active');
|
||
+ };
|
||
+ document.querySelector('.hero__arrow--prev')?.addEventListener('click', () => show(cur - 1));
|
||
+ document.querySelector('.hero__arrow--next')?.addEventListener('click', () => show(cur + 1));
|
||
+ }
|
||
+</script>
|
||
+```
|
||
+
|
||
+- [ ] **Step 2: InfoColumns.astro**
|
||
+
|
||
+Tre colonne full-width con foto di sfondo e overlay scuro, titoli `PERFORMANCE.` `BALANCE.` `LONGEVITY.`; ogni colonna linka `/programs`.
|
||
+
|
||
+```astro
|
||
+---
|
||
+import infoPerformance from '../../assets/img/info-performance.jpg';
|
||
+import infoBalance from '../../assets/img/info-balance.jpg';
|
||
+import infoLongevity from '../../assets/img/info-longevity.jpg';
|
||
+const cols = [
|
||
+ { title: 'PERFORMANCE.', image: infoPerformance },
|
||
+ { title: 'BALANCE.', image: infoBalance },
|
||
+ { title: 'LONGEVITY.', image: infoLongevity },
|
||
+];
|
||
+---
|
||
+<section class="info" aria-label="pilastri">
|
||
+ {cols.map((c) => (
|
||
+ <a class="info__col" href="/programs" style={`background-image:url(${c.image.src})`}>
|
||
+ <span class="info__title">{c.title}</span>
|
||
+ </a>
|
||
+ ))}
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .info { display: grid; grid-template-columns: repeat(3, 1fr); }
|
||
+ .info__col { position: relative; min-height: 320px; background-size: cover; background-position: center; display: flex; align-items: center; justify-content: center; text-decoration: none; }
|
||
+ .info__col::before { content: ''; position: absolute; inset: 0; background: rgba(30,24,20,.35); transition: background .2s; }
|
||
+ .info__col:hover::before { background: rgba(30,24,20,.15); }
|
||
+ .info__title { position: relative; color: #fff; font-family: var(--font-heading); font-weight: 600; letter-spacing: .1em; font-size: 1.1rem; }
|
||
+ @media (max-width: 767px) { .info { grid-template-columns: 1fr; } }
|
||
+</style>
|
||
+```
|
||
+
|
||
+- [ ] **Step 3: FeatureBlocks.astro**
|
||
+
|
||
+Tre blocchi alternati testo/immagine (testi reali da Homepage.pdf):
|
||
+
|
||
+```astro
|
||
+---
|
||
+import { Image } from 'astro:assets';
|
||
+import featPerformance from '../../assets/img/feature-performance.jpg';
|
||
+import featBalance from '../../assets/img/feature-balance.jpg';
|
||
+import featLongevity from '../../assets/img/feature-longevity.jpg';
|
||
+const blocks = [
|
||
+ { title: 'PERFORMANCE', image: featPerformance, reverse: false,
|
||
+ text: 'Allenamenti progettati su misura per adattarsi ai tuoi obiettivi e al tuo livello. Programmi mirati che sviluppano forza, resistenza e funzionalità in modo equilibrato. Un metodo efficace per migliorare le performance e ottenere risultati concreti e duraturi.' },
|
||
+ { title: 'BALANCE', image: featBalance, reverse: true,
|
||
+ text: 'L’equilibrio tra corpo e mente è la base di ogni performance duratura. Attraverso movimento, recupero e gestione dello stress, costruiamo stabilità e controllo. Un approccio integrato che sostiene benessere, energia e continuità nel tempo.' },
|
||
+ { title: 'LONGEVITY', image: featLongevity, reverse: false,
|
||
+ text: 'Benessere completo tra movimento, nutrizione e stile di vita. Un percorso personalizzato che unisce prevenzione, recupero ed equilibrio. Investi nella tua salute e qualità della vita nel tempo.' },
|
||
+];
|
||
+---
|
||
+<section class="section">
|
||
+ <div class="container">
|
||
+ {blocks.map((b) => (
|
||
+ <div class:list={['feat', { 'feat--reverse': b.reverse }]}>
|
||
+ <Image src={b.image} alt={b.title} class="feat__img" widths={[500, 900]} sizes="(max-width: 767px) 100vw, 45vw" />
|
||
+ <div class="feat__text">
|
||
+ <h2>{b.title}</h2>
|
||
+ <p>{b.text}</p>
|
||
+ <a class="btn" href="/programs">Scopri</a>
|
||
+ </div>
|
||
+ </div>
|
||
+ ))}
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .feat { display: grid; grid-template-columns: 1fr 1fr; gap: 60px; align-items: center; padding-block: 40px; }
|
||
+ .feat--reverse .feat__img { order: 2; }
|
||
+ .feat__text h2 { text-transform: uppercase; letter-spacing: .1em; }
|
||
+ @media (max-width: 767px) { .feat { grid-template-columns: 1fr; gap: 24px; } .feat--reverse .feat__img { order: 0; } }
|
||
+</style>
|
||
+```
|
||
+
|
||
+- [ ] **Step 4: QuoteBanner.astro**
|
||
+
|
||
+Banner scuro con foto di sfondo e citazione (nel Figma non è marquee ma banner statico con virgolette):
|
||
+
|
||
+```astro
|
||
+---
|
||
+import quoteBg from '../../assets/img/quote-bg.jpg';
|
||
+---
|
||
+<section class="quote" style={`background-image:url(${quoteBg.src})`}>
|
||
+ <div class="container quote__in">
|
||
+ <p class="quote__mark">“</p>
|
||
+ <blockquote>
|
||
+ <p><strong>PERFORMANCE</strong> È SPINGERE OLTRE I PROPRI LIMITI, <strong>BALANCE</strong> È SAPERLI ASCOLTARE, <strong>LONGEVITY</strong> È COSTRUIRE NEL TEMPO CIÒ CHE DAVVERO CONTA.</p>
|
||
+ <cite>Insanitylab</cite>
|
||
+ </blockquote>
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .quote { position: relative; background-size: cover; background-position: center; text-align: center; }
|
||
+ .quote::before { content: ''; position: absolute; inset: 0; background: rgba(46,32,24,.82); }
|
||
+ .quote__in { position: relative; padding-block: 110px; max-width: 820px; }
|
||
+ .quote__mark { color: #fff; font-size: 4rem; font-family: var(--font-heading); margin: 0; line-height: 1; }
|
||
+ .quote blockquote { margin: 0; }
|
||
+ .quote blockquote p { color: #fff; font-family: var(--font-heading); font-size: clamp(1.1rem, 2.4vw, 1.6rem); letter-spacing: .06em; line-height: 1.6; }
|
||
+ .quote cite { display: block; margin-top: 24px; color: var(--c-accent); font-style: normal; font-family: var(--font-heading); font-size: .75rem; letter-spacing: .3em; text-transform: uppercase; }
|
||
+</style>
|
||
+```
|
||
+
|
||
+- [ ] **Step 5: TrainingCards.astro**
|
||
+
|
||
+Griglia 2×4 in alternanza card testo (scura/tan) e card foto, dati da `trainings.ts` (riusato in `/training`):
|
||
+
|
||
+```astro
|
||
+---
|
||
+import { Image } from 'astro:assets';
|
||
+import { trainings } from '../../data/trainings';
|
||
+const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/training-*.{jpg,png}', { eager: true });
|
||
+const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1].default;
|
||
+const tones = ['dark', 'tan', 'dark', 'tan'];
|
||
+---
|
||
+<section class="tcards">
|
||
+ {trainings.map((t, i) => (
|
||
+ <>
|
||
+ <div class:list={['tcards__cell', `tcards__cell--${tones[i]}`, { 'tcards__cell--swap': i % 2 === 1 }]}>
|
||
+ <h3>{t.title}</h3>
|
||
+ <p>{t.caption}</p>
|
||
+ <a class="tcards__link" href={t.cta.href}>Scopri →</a>
|
||
+ </div>
|
||
+ {imgOf(t.image) && <Image src={imgOf(t.image)!} alt={t.title} class="tcards__img" widths={[400, 700]} sizes="(max-width: 767px) 50vw, 25vw" />}
|
||
+ </>
|
||
+ ))}
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .tcards { display: grid; grid-template-columns: repeat(4, 1fr); }
|
||
+ .tcards__cell { padding: 48px 36px; display: flex; flex-direction: column; justify-content: center; }
|
||
+ .tcards__cell--dark { background: var(--c-dark); }
|
||
+ .tcards__cell--dark h3, .tcards__cell--dark p { color: #efe9e3; }
|
||
+ .tcards__cell--tan { background: var(--c-accent); }
|
||
+ .tcards__cell--tan h3, .tcards__cell--tan p { color: #fff; }
|
||
+ .tcards__cell h3 { text-transform: uppercase; letter-spacing: .12em; }
|
||
+ .tcards__cell p { font-size: .88rem; }
|
||
+ .tcards__link { color: #fff; font-family: var(--font-heading); font-size: .7rem; letter-spacing: .2em; text-transform: uppercase; text-decoration: none; margin-top: 18px; }
|
||
+ .tcards__img { width: 100%; height: 100%; object-fit: cover; aspect-ratio: 1; }
|
||
+ /* alterna ordine nella riga 2: foto prima della card */
|
||
+ .tcards__cell--swap { order: 1; }
|
||
+ @media (max-width: 767px) { .tcards { grid-template-columns: repeat(2, 1fr); } .tcards__cell--swap { order: 0; } }
|
||
+</style>
|
||
+```
|
||
+(Verifica l'alternanza card/foto contro il PDF: riga 1 = card, foto, card, foto; riga 2 = foto, card, foto, card. Correggi con `order` finché combacia.)
|
||
+
|
||
+- [ ] **Step 6: ProgramsGrid.astro**
|
||
+
|
||
+Intestazione "PROGRAMMI" + griglia 3×2 di voci con icona linea, titolo e sottotitolo, link alla pagina dettaglio:
|
||
+
|
||
+```astro
|
||
+---
|
||
+import { programs } from '../../data/programs';
|
||
+const icons: Record<string, string> = {
|
||
+ 'personal-training': 'M4 12h3m10 0h3M9 8v8m6-8v8M7 10v4m10-4v4',
|
||
+ 'performance-class': 'M13 3l-2 7h4l-6 11 2-8H7l4-10z',
|
||
+ 'coaching': 'M12 3a4 4 0 110 8 4 4 0 010-8zm-7 18a7 7 0 0114 0',
|
||
+ 'pilates-flow': 'M12 4a2 2 0 110 4 2 2 0 010-4zM6 20c2-6 10-6 12 0M12 8v6',
|
||
+ 'rehab': 'M12 21C7 17 3 13 3 9a5 5 0 019-3 5 5 0 019 3c0 4-4 8-9 12z',
|
||
+ 'nutrition': 'M12 3c4 0 7 3 7 8s-3 10-7 10-7-5-7-10 3-8 7-8zm0-1v4',
|
||
+};
|
||
+---
|
||
+<section class="section">
|
||
+ <div class="container">
|
||
+ <div class="section-heading">
|
||
+ <h2>Programmi</h2>
|
||
+ <p>Questi sono i programmi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi un’esperienza di allenamento completa e mirata.</p>
|
||
+ </div>
|
||
+ <div class="pgrid">
|
||
+ {programs.map((p) => (
|
||
+ <a class="pgrid__item" href={`/programs/${p.slug}`}>
|
||
+ <svg viewBox="0 0 24 24" fill="none" stroke="var(--c-accent-dark)" stroke-width="1.4" width="52" height="52" aria-hidden="true"><path d={icons[p.slug]} /></svg>
|
||
+ <h3>{p.title}</h3>
|
||
+ <p>{p.subtitle}</p>
|
||
+ </a>
|
||
+ ))}
|
||
+ </div>
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .pgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 50px 40px; text-align: center; }
|
||
+ .pgrid__item { text-decoration: none; }
|
||
+ .pgrid__item svg { margin-inline: auto; }
|
||
+ .pgrid__item h3 { text-transform: uppercase; letter-spacing: .12em; font-size: .95rem; margin: 18px 0 6px; }
|
||
+ .pgrid__item p { font-size: .85rem; color: var(--c-text-light); margin: 0; }
|
||
+ @media (max-width: 767px) { .pgrid { grid-template-columns: 1fr 1fr; } }
|
||
+ @media (max-width: 480px) { .pgrid { grid-template-columns: 1fr; } }
|
||
+</style>
|
||
+```
|
||
+(Se nell'archivio Figma sono presenti le icone originali dei programmi, usale al posto degli SVG inline: mappale nel Task 2 come `icon-<slug>.png`.)
|
||
+
|
||
+- [ ] **Step 7: Agenda.astro (condiviso)**
|
||
+
|
||
+Sezione scura con tab discipline e griglia settimanale:
|
||
+
|
||
+```astro
|
||
+---
|
||
+import { agenda, disciplines, days, slots } from '../data/agenda';
|
||
+---
|
||
+<section class="section section--dark agenda">
|
||
+ <div class="container">
|
||
+ <div class="section-heading"><h2>Agenda</h2></div>
|
||
+ <div class="agenda__tabs" role="tablist">
|
||
+ {disciplines.map((d, i) => (
|
||
+ <button class:list={['agenda__tab', { 'is-active': i === 0 }]} data-filter={d} role="tab" aria-selected={i === 0 ? 'true' : 'false'}>{d}</button>
|
||
+ ))}
|
||
+ </div>
|
||
+ <div class="agenda__grid">
|
||
+ <div class="agenda__corner"></div>
|
||
+ {days.map((d) => <div class="agenda__day">{d}</div>)}
|
||
+ {slots.map((slot) => (
|
||
+ <>
|
||
+ <div class="agenda__slot">{slot}</div>
|
||
+ {days.map((_, dayIdx) => {
|
||
+ const entry = agenda.find((e) => e.day === dayIdx && e.slot === slot);
|
||
+ return entry
|
||
+ ? <div class="agenda__cell" data-discipline={entry.discipline}><strong>{entry.discipline}</strong><span>{entry.time}</span><em>{entry.trainer}</em></div>
|
||
+ : <div class="agenda__cell agenda__cell--empty"></div>;
|
||
+ })}
|
||
+ </>
|
||
+ ))}
|
||
+ </div>
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .agenda__tabs { display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; margin-bottom: 40px; }
|
||
+ .agenda__tab { background: transparent; color: #b9aea4; border: 0; cursor: pointer; font-family: var(--font-heading); font-size: .68rem; letter-spacing: .18em; text-transform: uppercase; padding: 10px 18px; }
|
||
+ .agenda__tab.is-active { background: var(--c-accent); color: #fff; }
|
||
+ .agenda__grid { display: grid; grid-template-columns: 70px repeat(7, 1fr); gap: 2px; }
|
||
+ .agenda__day, .agenda__slot { font-family: var(--font-heading); font-size: .66rem; letter-spacing: .14em; text-transform: uppercase; color: #b9aea4; display: flex; align-items: center; justify-content: center; padding: 10px 4px; }
|
||
+ .agenda__cell { background: var(--c-accent); color: #fff; padding: 14px 8px; text-align: center; font-size: .72rem; display: flex; flex-direction: column; gap: 2px; }
|
||
+ .agenda__cell strong { font-family: var(--font-heading); font-size: .7rem; letter-spacing: .1em; text-transform: uppercase; }
|
||
+ .agenda__cell em { font-style: normal; opacity: .85; }
|
||
+ .agenda__cell--empty { background: transparent; }
|
||
+ .agenda__cell.is-dim { opacity: .15; }
|
||
+ @media (max-width: 991px) { .agenda__grid { grid-template-columns: 60px repeat(7, minmax(90px, 1fr)); overflow-x: auto; } }
|
||
+</style>
|
||
+
|
||
+<script>
|
||
+ document.querySelectorAll<HTMLElement>('.agenda').forEach((root) => {
|
||
+ const tabs = root.querySelectorAll<HTMLButtonElement>('.agenda__tab');
|
||
+ const cells = root.querySelectorAll<HTMLElement>('.agenda__cell[data-discipline]');
|
||
+ tabs.forEach((tab) => tab.addEventListener('click', () => {
|
||
+ tabs.forEach((t) => { t.classList.toggle('is-active', t === tab); t.setAttribute('aria-selected', String(t === tab)); });
|
||
+ const f = tab.dataset.filter!;
|
||
+ cells.forEach((c) => c.classList.toggle('is-dim', f !== 'Events' && c.dataset.discipline !== f));
|
||
+ }));
|
||
+ });
|
||
+</script>
|
||
+```
|
||
+
|
||
+- [ ] **Step 8: TeamSection.astro, MethodSteps.astro, PartnersStrip.astro**
|
||
+
|
||
+`TeamSection.astro` — titolo "TEAM TRAINER" a sinistra, bottone "Scopri team" → `/about`, 4 ritratti con nome verticale sul bordo:
|
||
+```astro
|
||
+---
|
||
+import { Image } from 'astro:assets';
|
||
+import { team, homeTeam } from '../../data/team';
|
||
+const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/team-*.{jpg,png}', { eager: true });
|
||
+const members = homeTeam.map((short) => team.find((m) => m.shortName === short)!).filter((m) => m.image);
|
||
+const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1].default;
|
||
+---
|
||
+<section class="section">
|
||
+ <div class="container">
|
||
+ <div class="tteam__head">
|
||
+ <h2>Team<br />Trainer</h2>
|
||
+ <a class="btn" href="/about">Scopri team</a>
|
||
+ </div>
|
||
+ <div class="tteam__grid">
|
||
+ {members.map((m) => (
|
||
+ <figure class="tteam__card">
|
||
+ <Image src={imgOf(m.image!)!} alt={m.name} widths={[300, 500]} sizes="(max-width: 767px) 50vw, 25vw" />
|
||
+ <figcaption>{m.shortName}</figcaption>
|
||
+ </figure>
|
||
+ ))}
|
||
+ </div>
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .tteam__head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 40px; }
|
||
+ .tteam__head h2 { text-transform: uppercase; letter-spacing: .1em; margin: 0; }
|
||
+ .tteam__grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }
|
||
+ .tteam__card { position: relative; margin: 0; }
|
||
+ .tteam__card img { aspect-ratio: 3/4; object-fit: cover; width: 100%; }
|
||
+ .tteam__card figcaption { position: absolute; right: 10px; bottom: 14px; background: #fff; padding: 10px 6px; writing-mode: vertical-rl; font-family: var(--font-heading); font-size: .62rem; letter-spacing: .3em; text-transform: uppercase; color: var(--c-accent-dark); }
|
||
+ @media (max-width: 767px) { .tteam__grid { grid-template-columns: 1fr 1fr; } }
|
||
+</style>
|
||
+```
|
||
+
|
||
+`MethodSteps.astro` — intestazione "IL METODO INSANITYLAB" + 4 cerchi numerati con linea tratteggiata; nota extra sopra gli step 3–4:
|
||
+```astro
|
||
+---
|
||
+import { Image } from 'astro:assets';
|
||
+import { methodIntro, methodExtraNote, methodSteps } from '../../data/method';
|
||
+const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/method-*.{jpg,png}', { eager: true });
|
||
+const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1].default;
|
||
+---
|
||
+<section class="section section--alt">
|
||
+ <div class="container">
|
||
+ <div class="section-heading">
|
||
+ <h2>Il metodo<br />InsanityLab</h2>
|
||
+ <p set:html={methodIntro.replace('Metodo Insanitylab', '<strong>Metodo Insanitylab</strong>')} />
|
||
+ </div>
|
||
+ <p class="method__note">{methodExtraNote}</p>
|
||
+ <div class="method__row">
|
||
+ {methodSteps.map((s) => (
|
||
+ <div class:list={['method__step', { 'method__step--extra': s.extra }]}>
|
||
+ <div class="method__circle">
|
||
+ {imgOf(s.image) && <Image src={imgOf(s.image)!} alt="" widths={[220]} sizes="220px" />}
|
||
+ <span>{s.n}</span>
|
||
+ </div>
|
||
+ <h3>{s.title}</h3>
|
||
+ <p>{s.text}</p>
|
||
+ </div>
|
||
+ ))}
|
||
+ </div>
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .method__note { text-align: right; font-size: .8rem; color: var(--c-text-light); max-width: 260px; margin-left: auto; }
|
||
+ .method__row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 30px; text-align: center; position: relative; }
|
||
+ .method__circle { position: relative; width: 150px; height: 150px; margin: 0 auto 20px; border: 1px dashed var(--c-accent-dark); border-radius: 50%; padding: 8px; }
|
||
+ .method__circle img { border-radius: 50%; width: 100%; height: 100%; object-fit: cover; }
|
||
+ .method__circle span { position: absolute; top: -6px; left: -6px; font-family: var(--font-heading); font-size: 2.4rem; color: var(--c-heading); background: var(--c-bg-alt); padding: 0 8px; }
|
||
+ .method__step h3 { font-size: .85rem; text-transform: uppercase; letter-spacing: .14em; }
|
||
+ .method__step p { font-size: .85rem; color: var(--c-text-light); }
|
||
+ @media (max-width: 767px) { .method__row { grid-template-columns: 1fr 1fr; } }
|
||
+</style>
|
||
+```
|
||
+
|
||
+`PartnersStrip.astro` — striscia tan con i 4 loghi:
|
||
+```astro
|
||
+---
|
||
+import { Image } from 'astro:assets';
|
||
+import { partners } from '../../data/partners';
|
||
+const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/partner-*.{jpg,png}', { eager: true });
|
||
+const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1].default;
|
||
+---
|
||
+<section class="pstrip" aria-label="partner">
|
||
+ <div class="container pstrip__in">
|
||
+ {partners.map((p) => imgOf(p.image) && <Image src={imgOf(p.image)!} alt={p.name} height={44} />)}
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .pstrip { background: var(--c-accent); padding-block: 40px; }
|
||
+ .pstrip__in { display: flex; align-items: center; justify-content: space-around; gap: 30px; flex-wrap: wrap; }
|
||
+ .pstrip img { height: 44px; width: auto; opacity: .9; }
|
||
+</style>
|
||
+```
|
||
+
|
||
+- [ ] **Step 9: componi index.astro**
|
||
+
|
||
+```astro
|
||
+---
|
||
+import Base from '../layouts/Base.astro';
|
||
+import HeroSlider from '../components/home/HeroSlider.astro';
|
||
+import InfoColumns from '../components/home/InfoColumns.astro';
|
||
+import FeatureBlocks from '../components/home/FeatureBlocks.astro';
|
||
+import QuoteBanner from '../components/home/QuoteBanner.astro';
|
||
+import TrainingCards from '../components/home/TrainingCards.astro';
|
||
+import ProgramsGrid from '../components/home/ProgramsGrid.astro';
|
||
+import Agenda from '../components/Agenda.astro';
|
||
+import TeamSection from '../components/home/TeamSection.astro';
|
||
+import MethodSteps from '../components/home/MethodSteps.astro';
|
||
+import PartnersStrip from '../components/home/PartnersStrip.astro';
|
||
+export const prerender = true;
|
||
+---
|
||
+<Base title="Performance. Balance. Longevity.">
|
||
+ <HeroSlider />
|
||
+ <InfoColumns />
|
||
+ <FeatureBlocks />
|
||
+ <QuoteBanner />
|
||
+ <section class="section" style="padding-bottom:0">
|
||
+ <div class="section-heading">
|
||
+ <h2>Training</h2>
|
||
+ <p>Porta i tuoi allenamenti al livello successivo: trova il percorso più adatto alle tue esigenze.</p>
|
||
+ </div>
|
||
+ </section>
|
||
+ <TrainingCards />
|
||
+ <ProgramsGrid />
|
||
+ <Agenda />
|
||
+ <TeamSection />
|
||
+ <MethodSteps />
|
||
+ <PartnersStrip />
|
||
+</Base>
|
||
+```
|
||
+
|
||
+- [ ] **Step 10: confronto visivo con Figma**
|
||
+
|
||
+Run: `npm run dev`, apri `http://localhost:4321/` e confronta sezione per sezione con `/tmp/ilab-home-slice_*.png`. Correggi spaziature, dimensioni titoli, colori finché la corrispondenza è fedele (non pixel-perfect: stessa gerarchia, palette, proporzioni). Verifica mobile a 375px.
|
||
+
|
||
+- [ ] **Step 11: build e commit**
|
||
+
|
||
+Run: `npm run build`
|
||
+Expected: build ok.
|
||
+
|
||
+```bash
|
||
+git add -A && git commit -m "feat: homepage completa da design Figma"
|
||
+```
|
||
+
|
||
+---
|
||
+
|
||
+### Task 10: Pagina About (`/about`)
|
||
+
|
||
+**Files:**
|
||
+- Create: `src/pages/about.astro`, `src/components/about/Timeline.astro`, `src/components/PageHero.astro`
|
||
+
|
||
+**Interfaces:**
|
||
+- Consumes: `team` (Task 8), immagini `about-*`/`team-*` (Task 2), `site`.
|
||
+- Produces: `<PageHero title />` riusato da training/programs/blog/contact (banner titolo pagina su fondo chiaro con eyebrow).
|
||
+
|
||
+Riferimento: `../FigmaDoc/Insanitylab.pdf` (generare fette come per la homepage). Sezioni nell'ordine: hero titolo "Insanitylab", Mission, Direzione (+ bottone Scopri), Approccio (+ bottone Contattaci), About, Storylab (timeline), Convenzioni, Team (3 fondatori + 6 trainer), Metodo (testo), In-Sanity Studios, Vision.
|
||
+
|
||
+- [ ] **Step 1: PageHero.astro**
|
||
+
|
||
+```astro
|
||
+---
|
||
+interface Props { title: string; eyebrow?: string }
|
||
+const { title, eyebrow } = Astro.props;
|
||
+---
|
||
+<section class="phero">
|
||
+ <div class="container">
|
||
+ {eyebrow && <p class="eyebrow">{eyebrow}</p>}
|
||
+ <h1>{title}</h1>
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .phero { background: var(--c-bg-alt); padding-block: 70px; }
|
||
+ .phero h1 { text-transform: lowercase; font-weight: 500; margin: 0; }
|
||
+</style>
|
||
+```
|
||
+
|
||
+- [ ] **Step 2: Timeline.astro (Storylab)**
|
||
+
|
||
+Blocco 2021-2024 aperto con testo; 2025, 2026 e OGGI come accordion `<details>`:
|
||
+
|
||
+```astro
|
||
+---
|
||
+const openText = `Il percorso di crescita di InsanityLab si è sviluppato attraverso tappe precise e coerenti con la visione iniziale. Nato nel 2021 con un piccolo team e grandi ambizioni, il progetto ha conosciuto una forte evoluzione nel 2024, anno in cui il servizio si è ampliato attraverso nuovi format come gli small group e una sempre maggiore specializzazione nella profilazione fisica del cliente, grazie a sistemi avanzati di valutazione e monitoraggio. Parallelamente, anche l’area dedicata alla riabilitazione e all’esercizio preventivo si è consolidata, ponendo le basi per un importante salto di qualità.`;
|
||
+const stages = [
|
||
+ { label: 'INSANITYLAB 2025', text: 'Contenuto in arrivo: il cliente fornirà il testo di questa tappa.' },
|
||
+ { label: 'INSANITYLAB 2026', text: 'Contenuto in arrivo: il cliente fornirà il testo di questa tappa.' },
|
||
+ { label: 'INSANITYLAB OGGI', text: 'Contenuto in arrivo: il cliente fornirà il testo di questa tappa.' },
|
||
+];
|
||
+---
|
||
+<section class="section">
|
||
+ <div class="container">
|
||
+ <div class="section-heading">
|
||
+ <h2>Storylab</h2>
|
||
+ <p>Scopri la nostra storia: come nasce InsanityLab, perché abbiamo deciso di creare qualcosa di diverso e chi sono i visionari che hanno trasformato questa idea in un nuovo modo di vivere benessere, movimento e salute.</p>
|
||
+ </div>
|
||
+ <div class="tl">
|
||
+ <h3 class="tl__label">INSANITYLAB 2021-2024</h3>
|
||
+ <p>{openText}</p>
|
||
+ {stages.map((s) => (
|
||
+ <details class="tl__stage">
|
||
+ <summary>+ {s.label}</summary>
|
||
+ <p>{s.text}</p>
|
||
+ </details>
|
||
+ ))}
|
||
+ </div>
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .tl { max-width: 720px; margin-inline: auto; }
|
||
+ .tl__label { text-transform: uppercase; letter-spacing: .16em; font-size: .9rem; }
|
||
+ .tl__stage { border-top: 1px solid #e4ded6; padding-block: 14px; }
|
||
+ .tl__stage summary { cursor: pointer; font-family: var(--font-heading); font-size: .82rem; letter-spacing: .16em; text-transform: uppercase; color: var(--c-heading); list-style: none; }
|
||
+ .tl__stage summary::-webkit-details-marker { display: none; }
|
||
+</style>
|
||
+```
|
||
+
|
||
+- [ ] **Step 3: about.astro**
|
||
+
|
||
+Testi reali da Insanitylab.pdf (già estratti — riportati integralmente qui sotto). Struttura: alternanza di sezioni testo/immagine come nel PDF.
|
||
+
|
||
+```astro
|
||
+---
|
||
+import Base from '../layouts/Base.astro';
|
||
+import PageHero from '../components/PageHero.astro';
|
||
+import Timeline from '../components/about/Timeline.astro';
|
||
+import { Image } from 'astro:assets';
|
||
+import { team } from '../data/team';
|
||
+import { site } from '../data/site';
|
||
+const images = import.meta.glob<{ default: ImageMetadata }>('../assets/img/*.{jpg,png}', { eager: true });
|
||
+const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1]?.default;
|
||
+const founders = team.filter((m) => m.founder);
|
||
+const staff = team.filter((m) => !m.founder);
|
||
+export const prerender = true;
|
||
+---
|
||
+<Base title="Insanitylab" description="Mission, storia, team e metodo di InsanityLab: un laboratorio esclusivo dove movimento, prevenzione e performance convivono in un approccio personalizzato.">
|
||
+ <PageHero title="Insanitylab" />
|
||
+
|
||
+ <section class="section">
|
||
+ <div class="container section-heading">
|
||
+ <h2>Mission</h2>
|
||
+ <p>In InsanityLab crediamo che il movimento non debba essere vissuto come un obbligo o un sacrificio, ma come una forma autentica di cura di sé. La nostra missione è accompagnare ogni persona in un percorso di consapevolezza, aiutandola a riscoprire il benessere fisico e mentale attraverso un approccio sostenibile, concreto e duraturo.</p>
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ <section class="section section--alt">
|
||
+ <div class="container about-2col">
|
||
+ <div>
|
||
+ <h2>Direzione</h2>
|
||
+ <p>Non ci limitiamo ad allenare il corpo: creiamo percorsi personalizzati che uniscono movimento, equilibrio e benessere, attraverso il supporto di professionisti qualificati, programmi su misura e un approccio centrato sulla persona e su risultati duraturi.</p>
|
||
+ <a class="btn" href="/programs">Scopri</a>
|
||
+ </div>
|
||
+ <div>
|
||
+ <h2>Approccio</h2>
|
||
+ <p>InsanityLab si distingue per un approccio serio e personalizzato al benessere, basato su competenze specializzate, costanza e cura quotidiana, rivolto a chiunque voglia migliorare il proprio stile di vita in modo consapevole e duraturo.</p>
|
||
+ <a class="btn btn--dark" href="/contact">Contattaci</a>
|
||
+ </div>
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ <section class="section">
|
||
+ <div class="container section-heading">
|
||
+ <h2>About</h2>
|
||
+ <p>InsanityLab nasce con l’obiettivo di unire fitness e salute in un unico percorso dedicato al benessere globale, superando il concetto tradizionale di palestra. Un laboratorio esclusivo, guidato da osteopati e chinesiologi e professionisti specializzati, dove movimento, prevenzione e performance convivono all’interno di un approccio personalizzato e consapevole.</p>
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ <Timeline />
|
||
+
|
||
+ <section class="section section--dark">
|
||
+ <div class="container">
|
||
+ <div class="section-heading">
|
||
+ <h2>Convenzioni</h2>
|
||
+ <p>InsanityLab collabora con aziende, enti e organizzazioni che desiderano investire concretamente nel benessere delle proprie persone, attraverso programmi dedicati alla salute, al movimento e alla qualità della vita. Le convenzioni nascono con l’obiettivo di portare il valore del benessere anche all’interno del contesto professionale, creando percorsi che favoriscano equilibrio fisico, prevenzione e performance quotidiana.</p>
|
||
+ </div>
|
||
+ <ul class="conv">
|
||
+ <li>Le realtà convenzionate possono accedere a programmi di welfare aziendale personalizzati, con percorsi individuali, small group training e classi di gruppo seguiti da professionisti qualificati.</li>
|
||
+ <li>Ogni collaborazione parte da una consulenza gratuita preliminare per analizzare esigenze e obiettivi, così da sviluppare soluzioni efficaci e sostenibili.</li>
|
||
+ <li>Le convenzioni vengono definite su misura insieme a InsanityLab, in base alle caratteristiche e alle necessità dell’azienda partner.</li>
|
||
+ </ul>
|
||
+ <p class="conv__cta">Per ricevere maggiori informazioni o attivare una convenzione: {site.email} · {site.phone}</p>
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ <section class="section">
|
||
+ <div class="container">
|
||
+ <div class="section-heading">
|
||
+ <h2>Team</h2>
|
||
+ <p>Il team di InsanityLab riunisce professionisti specializzati in allenamento, salute e benessere, con un approccio multidisciplinare basato su competenza, personalizzazione e attenzione alla persona.</p>
|
||
+ </div>
|
||
+ <div class="tgrid tgrid--founders">
|
||
+ {founders.map((m) => (
|
||
+ <article class="tcard">
|
||
+ {m.image && imgOf(m.image) && <Image src={imgOf(m.image)!} alt={m.name} widths={[300, 500]} sizes="(max-width:767px) 100vw, 33vw" />}
|
||
+ <h3>{m.shortName}</h3>
|
||
+ <p class="tcard__role">{m.role}</p>
|
||
+ <p>{m.bio}</p>
|
||
+ </article>
|
||
+ ))}
|
||
+ </div>
|
||
+ <p class="section-heading" style="margin-top:60px">Accanto ai fondatori opera un team in continua crescita composto da trainer e professionisti con specializzazioni complementari:</p>
|
||
+ <div class="tgrid">
|
||
+ {staff.map((m) => (
|
||
+ <article class="tcard">
|
||
+ {m.image && imgOf(m.image) && <Image src={imgOf(m.image)!} alt={m.name} widths={[300, 500]} sizes="(max-width:767px) 100vw, 33vw" />}
|
||
+ <h3>{m.shortName}</h3>
|
||
+ <p class="tcard__role">{m.role}</p>
|
||
+ <p>{m.bio}</p>
|
||
+ </article>
|
||
+ ))}
|
||
+ </div>
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ <section class="section section--alt">
|
||
+ <div class="container section-heading">
|
||
+ <h2>Metodo</h2>
|
||
+ <p>Il team InsanityLab lavora con un approccio empatico e personalizzato, seguendo ogni persona in un percorso continuo e su misura. Unisce competenze multidisciplinari in allenamento, prevenzione, riabilitazione e performance, supportate da formazione costante. Il metodo si basa su sinergia, condivisione e qualità, per generare risultati concreti e duraturi.</p>
|
||
+ <p>In poche parole, il team di InsanityLab è composto da professionisti che credono profondamente in ciò che fanno: un gruppo unito da valori condivisi, formato per eccellere e guidato dalla convinzione che il movimento sia uno strumento di benessere, equilibrio e qualità della vita.</p>
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ <section class="section">
|
||
+ <div class="container section-heading">
|
||
+ <h2>In-Sanity Studios</h2>
|
||
+ <p>IN-SANITY STUDIOS è il nuovo spazio professionale nato dall’evoluzione di InsanityLab, dedicato ai professionisti della salute, del benessere e della performance. Più che un semplice insieme di studi professionali, rappresenta un ecosistema multidisciplinare pensato per favorire collaborazione, qualità del servizio e integrazione tra competenze diverse.</p>
|
||
+ <p>Il progetto nasce con l’obiettivo di offrire a liberi professionisti uno spazio elegante, riservato e già attrezzato, in cui poter svolgere la propria attività all’interno di un contesto consolidato e altamente qualificato, senza dover affrontare i costi e le complessità di una struttura indipendente.</p>
|
||
+ <p>IN-SANITY STUDIOS si rivolge a figure specializzate nel settore salute e wellness, come fisioterapisti, osteopati, psicologi, nutrizionisti, massaggiatori e altri professionisti dedicati alla cura della persona. Ogni studio è progettato per garantire comfort, professionalità e continuità operativa, all’interno di un ambiente coerente con la filosofia di InsanityLab: mettere il benessere globale della persona al centro del percorso.</p>
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ <section class="section section--alt">
|
||
+ <div class="container section-heading">
|
||
+ <h2>Vision</h2>
|
||
+ <p>InsanityLab nasce con l’obiettivo di aiutare le persone a ritrovare energia ed equilibrio attraverso il movimento, promuovendo un approccio autentico, scientifico e umano al benessere. La sua visione va oltre l’estetica e la performance: allenarsi significa prendersi cura di sé, costruendo abitudini sane, sostenibili e orientate alla longevità, alla prevenzione e alla qualità della vita.</p>
|
||
+ <p>L’obiettivo è diventare un punto di riferimento capace di lasciare un impatto reale nella comunità, trasformando il movimento in uno strumento di salute, consapevolezza e identità.</p>
|
||
+ </div>
|
||
+ </section>
|
||
+</Base>
|
||
+
|
||
+<style>
|
||
+ .about-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 60px; }
|
||
+ .conv { max-width: 760px; margin: 0 auto 30px; display: grid; gap: 18px; padding-left: 20px; }
|
||
+ .conv__cta { text-align: center; font-family: var(--font-heading); letter-spacing: .06em; }
|
||
+ .tgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 40px; }
|
||
+ .tcard img { aspect-ratio: 3/4; object-fit: cover; margin-bottom: 16px; }
|
||
+ .tcard h3 { text-transform: lowercase; letter-spacing: .2em; }
|
||
+ .tcard__role { font-size: .8rem; text-transform: uppercase; letter-spacing: .12em; color: var(--c-accent-dark); }
|
||
+ .tcard p { font-size: .9rem; }
|
||
+ @media (max-width: 767px) { .about-2col, .tgrid { grid-template-columns: 1fr; } }
|
||
+</style>
|
||
+```
|
||
+Nota: se nell'archivio Figma trovi le immagini delle sezioni about (mission, direzione, in-sanity studios), aggiungi accanto ai testi con lo stesso pattern `Image` — confronta col PDF.
|
||
+
|
||
+- [ ] **Step 2b: i tre accordion Storylab (2025, 2026, OGGI) hanno testo placeholder esplicito "Contenuto in arrivo" — segnala all'operatore nel riepilogo finale del task che servono i testi dal cliente.**
|
||
+
|
||
+- [ ] **Step 4: verifica e commit**
|
||
+
|
||
+Run: `npm run dev`, confronta con le fette di `Insanitylab.pdf`; poi `npm run build`.
|
||
+
|
||
+```bash
|
||
+git add -A && git commit -m "feat: pagina about"
|
||
+```
|
||
+
|
||
+---
|
||
+
|
||
+### Task 11: Pagina Training (`/training`)
|
||
+
|
||
+**Files:**
|
||
+- Create: `src/pages/training.astro`
|
||
+
|
||
+**Interfaces:**
|
||
+- Consumes: `trainings` (Task 8), `<TrainingCards />`, `<Agenda />` (Task 9), `<PageHero />` (Task 10).
|
||
+
|
||
+Riferimento: `../FigmaDoc/Training.pdf`. Struttura: hero "training", intestazione Training, griglia 4 card (riuso `TrainingCards`), 4 blocchi alternati testo/immagine (one to one, small groups, performance class, fit remote) con ancore `id`, Agenda, CTA "Prenota la lezione di prova".
|
||
+
|
||
+- [ ] **Step 1: training.astro**
|
||
+
|
||
+```astro
|
||
+---
|
||
+import Base from '../layouts/Base.astro';
|
||
+import PageHero from '../components/PageHero.astro';
|
||
+import TrainingCards from '../components/home/TrainingCards.astro';
|
||
+import Agenda from '../components/Agenda.astro';
|
||
+import { Image } from 'astro:assets';
|
||
+import { trainings } from '../data/trainings';
|
||
+const images = import.meta.glob<{ default: ImageMetadata }>('../assets/img/training-*.{jpg,png}', { eager: true });
|
||
+const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1]?.default;
|
||
+export const prerender = true;
|
||
+---
|
||
+<Base title="Training" description="One to One, Small Groups, Performance Class e Fit Remote: trova il training InsanityLab più adatto a te.">
|
||
+ <PageHero title="training" />
|
||
+
|
||
+ <section class="section" style="padding-bottom:0">
|
||
+ <div class="section-heading">
|
||
+ <h2>Training</h2>
|
||
+ <p>Porta i tuoi allenamenti al livello successivo: programmi completi e percorsi su misura. Trova il training più adatto alle tue esigenze.</p>
|
||
+ </div>
|
||
+ </section>
|
||
+ <TrainingCards />
|
||
+
|
||
+ <section class="section">
|
||
+ <div class="container">
|
||
+ {trainings.map((t, i) => (
|
||
+ <div class:list={['tblock', { 'tblock--reverse': i % 2 === 1 }]} id={t.id}>
|
||
+ {imgOf(t.image) && <Image src={imgOf(t.image)!} alt={t.title} widths={[500, 900]} sizes="(max-width:767px) 100vw, 45vw" />}
|
||
+ <div>
|
||
+ <h2>{t.title.toLowerCase()}</h2>
|
||
+ <p>{t.description}</p>
|
||
+ <a class="btn" href={t.cta.href}>{t.cta.label}</a>
|
||
+ </div>
|
||
+ </div>
|
||
+ ))}
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ <Agenda />
|
||
+
|
||
+ <section class="section section--alt">
|
||
+ <div class="container trial">
|
||
+ <div>
|
||
+ <h2>Prenota la lezione di prova</h2>
|
||
+ <p>La lezione di prova è l’occasione perfetta per conoscere il metodo, vivere l’esperienza in prima persona e capire il percorso più adatto a te. Inizia oggi il tuo percorso: prenota la tua lezione di prova.</p>
|
||
+ <a class="btn btn--dark" href="/contact">Prenota</a>
|
||
+ </div>
|
||
+ </div>
|
||
+ </section>
|
||
+</Base>
|
||
+
|
||
+<style>
|
||
+ .tblock { display: grid; grid-template-columns: 1fr 1fr; gap: 60px; align-items: center; padding-block: 40px; scroll-margin-top: calc(var(--header-h) + 20px); }
|
||
+ .tblock--reverse img { order: 2; }
|
||
+ .tblock h2 { letter-spacing: .1em; }
|
||
+ .trial { max-width: 720px; text-align: center; }
|
||
+ @media (max-width: 767px) { .tblock { grid-template-columns: 1fr; gap: 24px; } .tblock--reverse img { order: 0; } }
|
||
+</style>
|
||
+```
|
||
+
|
||
+- [ ] **Step 2: verifica e commit**
|
||
+
|
||
+Run: `npm run dev`, confronta con `Training.pdf`; ancore `#one-to-one` ecc. funzionanti dal footer; `npm run build`.
|
||
+
|
||
+```bash
|
||
+git add -A && git commit -m "feat: pagina training"
|
||
+```
|
||
+
|
||
+---
|
||
+
|
||
+### Task 12: Pagine Programmi (`/programs`, `/programs/[slug]`)
|
||
+
|
||
+**Files:**
|
||
+- Create: `src/pages/programs/index.astro`, `src/pages/programs/[slug].astro`
|
||
+
|
||
+**Interfaces:**
|
||
+- Consumes: `programs`, `getProgram` (Task 8), `performanceClassPricing` (Task 8), `<PageHero />`.
|
||
+- Produces: pagine prerenderizzate via `getStaticPaths`.
|
||
+
|
||
+Riferimento: `../FigmaDoc/Programmi.pdf` e `../FigmaDoc/Performance class.pdf`.
|
||
+
|
||
+- [ ] **Step 1: programs/index.astro**
|
||
+
|
||
+```astro
|
||
+---
|
||
+import Base from '../../layouts/Base.astro';
|
||
+import PageHero from '../../components/PageHero.astro';
|
||
+import { programs } from '../../data/programs';
|
||
+export const prerender = true;
|
||
+---
|
||
+<Base title="Programmi" description="I programmi InsanityLab: Personal Training, Performance Class, Coaching, Pilates Flow, Rehab e Nutrizione.">
|
||
+ <PageHero title="programmi" />
|
||
+ <section class="section">
|
||
+ <div class="container">
|
||
+ <div class="section-heading">
|
||
+ <h2>Programmi</h2>
|
||
+ <p>Questi sono i programmi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi un’esperienza di allenamento completa e mirata.</p>
|
||
+ </div>
|
||
+ <div class="plist">
|
||
+ {programs.map((p) => (
|
||
+ <article class="plist__card">
|
||
+ <h3>{p.title}</h3>
|
||
+ <p class="plist__sub">{p.subtitle}</p>
|
||
+ <p>{p.excerpt}</p>
|
||
+ <a class="plist__more" href={`/programs/${p.slug}`}>Per saperne di più →</a>
|
||
+ </article>
|
||
+ ))}
|
||
+ </div>
|
||
+ </div>
|
||
+ </section>
|
||
+</Base>
|
||
+
|
||
+<style>
|
||
+ .plist { display: grid; grid-template-columns: repeat(3, 1fr); gap: 50px 40px; }
|
||
+ .plist__card h3 { letter-spacing: .08em; }
|
||
+ .plist__sub { font-size: .8rem; text-transform: uppercase; letter-spacing: .12em; color: var(--c-accent-dark); }
|
||
+ .plist__card p { font-size: .92rem; }
|
||
+ .plist__more { font-family: var(--font-heading); font-size: .72rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; text-decoration: none; color: var(--c-heading); }
|
||
+ .plist__more:hover { color: var(--c-accent-dark); }
|
||
+ @media (max-width: 991px) { .plist { grid-template-columns: 1fr 1fr; } }
|
||
+ @media (max-width: 600px) { .plist { grid-template-columns: 1fr; } }
|
||
+</style>
|
||
+```
|
||
+
|
||
+- [ ] **Step 2: programs/[slug].astro**
|
||
+
|
||
+Layout da Performance class.pdf: FAQ accordion in alto, blocco descrizione con feature list, sidebar (categorie blog + altri programmi), pricing solo se `hasPricing`.
|
||
+
|
||
+```astro
|
||
+---
|
||
+import Base from '../../layouts/Base.astro';
|
||
+import PageHero from '../../components/PageHero.astro';
|
||
+import { programs } from '../../data/programs';
|
||
+import { performanceClassPricing } from '../../data/pricing';
|
||
+export const prerender = true;
|
||
+
|
||
+export function getStaticPaths() {
|
||
+ return programs.map((p) => ({ params: { slug: p.slug }, props: { program: p } }));
|
||
+}
|
||
+const { program } = Astro.props;
|
||
+const others = programs.filter((p) => p.slug !== program.slug);
|
||
+const blogCategories = ['Fitness', 'Salute', 'Stile di vita', 'Nutrizione'];
|
||
+---
|
||
+<Base title={program.title} description={program.excerpt}>
|
||
+ <PageHero title={program.title.toUpperCase()} />
|
||
+
|
||
+ <section class="section">
|
||
+ <div class="container prog">
|
||
+ <div>
|
||
+ <h2>{program.title.toUpperCase()}</h2>
|
||
+ <p>{program.longDescription}</p>
|
||
+ <ul class="prog__features">
|
||
+ {program.features.map((f) => <li>{f}</li>)}
|
||
+ </ul>
|
||
+ <div class="prog__faq">
|
||
+ {program.faq.map((item, i) => (
|
||
+ <details open={i === 0}>
|
||
+ <summary>{item.q}</summary>
|
||
+ <p>{item.a}</p>
|
||
+ </details>
|
||
+ ))}
|
||
+ </div>
|
||
+ </div>
|
||
+ <aside class="prog__side">
|
||
+ <h3>Categorie</h3>
|
||
+ {blogCategories.map((c) => <p><a href="/blog">{c}</a></p>)}
|
||
+ <h3>Altri programmi</h3>
|
||
+ {others.map((o) => <p><a href={`/programs/${o.slug}`}>{o.title}</a></p>)}
|
||
+ </aside>
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ {program.hasPricing && (
|
||
+ <section class="section section--alt">
|
||
+ <div class="container">
|
||
+ <div class="section-heading">
|
||
+ <h2>{performanceClassPricing.heading}</h2>
|
||
+ <p>{performanceClassPricing.intro}</p>
|
||
+ </div>
|
||
+ {performanceClassPricing.groups.map((g) => (
|
||
+ <details class="price__group" open={g.open}>
|
||
+ <summary>{g.open ? '+' : '–'} {g.label}</summary>
|
||
+ {g.plans.length === 0 ? (
|
||
+ <p class="price__empty">Contattaci per i dettagli di questo programma. <a href="/contact">Scrivici →</a></p>
|
||
+ ) : (
|
||
+ <div class="price__row">
|
||
+ {g.plans.map((p) => (
|
||
+ <div class="price__card">
|
||
+ <p class="price__amount">{p.monthly}€</p>
|
||
+ <p class="price__per">al mese, semestrale</p>
|
||
+ <p class="price__entries">{p.entries} {p.entries === 1 ? 'ingresso settimanale' : 'ingressi settimanali'}</p>
|
||
+ <p>{p.note}</p>
|
||
+ <p>{p.installments}</p>
|
||
+ <p class="price__best">{p.best}</p>
|
||
+ <a class="btn" href="/contact">Acquista</a>
|
||
+ </div>
|
||
+ ))}
|
||
+ </div>
|
||
+ )}
|
||
+ </details>
|
||
+ ))}
|
||
+ </div>
|
||
+ </section>
|
||
+ )}
|
||
+</Base>
|
||
+
|
||
+<style>
|
||
+ .prog { display: grid; grid-template-columns: 2.2fr 1fr; gap: 60px; }
|
||
+ .prog__features { padding-left: 20px; display: grid; gap: 8px; margin-bottom: 30px; }
|
||
+ .prog__features li::marker { color: var(--c-accent); }
|
||
+ .prog__faq details { border-top: 1px solid #e4ded6; padding-block: 14px; }
|
||
+ .prog__faq summary { cursor: pointer; font-family: var(--font-heading); font-size: .85rem; letter-spacing: .08em; color: var(--c-heading); }
|
||
+ .prog__side h3 { font-size: .85rem; text-transform: uppercase; letter-spacing: .16em; margin-top: 30px; }
|
||
+ .prog__side a { text-decoration: none; color: var(--c-text); }
|
||
+ .prog__side a:hover { color: var(--c-accent-dark); }
|
||
+ .price__group { border-top: 1px solid #ddd5ca; padding-block: 18px; }
|
||
+ .price__group summary { cursor: pointer; font-family: var(--font-heading); letter-spacing: .12em; text-transform: uppercase; font-size: .85rem; color: var(--c-heading); }
|
||
+ .price__row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 30px; margin-top: 30px; }
|
||
+ .price__card { background: #fff; padding: 36px 28px; text-align: center; }
|
||
+ .price__amount { font-family: var(--font-heading); font-size: 2.6rem; color: var(--c-heading); margin: 0; }
|
||
+ .price__per { color: var(--c-text-light); font-size: .8rem; }
|
||
+ .price__entries { font-family: var(--font-heading); text-transform: uppercase; letter-spacing: .1em; font-size: .82rem; color: var(--c-heading); }
|
||
+ .price__card p { margin-bottom: 6px; font-size: .85rem; }
|
||
+ .price__best { font-weight: 600; color: var(--c-accent-dark); }
|
||
+ .price__card .btn { margin-top: 16px; }
|
||
+ @media (max-width: 991px) { .prog { grid-template-columns: 1fr; } .price__row { grid-template-columns: 1fr; } }
|
||
+</style>
|
||
+```
|
||
+
|
||
+- [ ] **Step 3: verifica e commit**
|
||
+
|
||
+Run: `npm run dev`; controlla `/programs`, `/programs/performance-class` (pricing visibile, 3 card, accordion trimestrale/mensile chiusi), `/programs/rehab` (senza pricing). Confronta con i PDF. `npm run build` — devono comparire 6 pagine `programs/*` prerenderizzate.
|
||
+
|
||
+```bash
|
||
+git add -A && git commit -m "feat: pagine programmi con dettaglio e pricing performance class"
|
||
+```
|
||
+
|
||
+---
|
||
+
|
||
+### Task 13: Pagina Contatti e API email (TDD sulla validazione)
|
||
+
|
||
+**Files:**
|
||
+- Create: `src/lib/contact.ts`, `src/lib/mailer.ts`, `src/pages/api/contact.ts`, `src/pages/contact.astro`
|
||
+- Test: `tests/contact.test.ts`
|
||
+
|
||
+**Interfaces:**
|
||
+- Consumes: `rateLimit` (Task 4), `getEnv` (Task 4), `<ContactForm variant="full" />` (Task 7), `<PageHero />` (Task 10), `site` (Task 8).
|
||
+- Produces:
|
||
+ - `validateContact(data: unknown): { ok: true; value: ContactData } | { ok: false; error: string }` con `ContactData { firstName: string; lastName: string; phone: string; email: string; message: string; website: string }` (campi assenti → stringa vuota).
|
||
+ - `sendContactEmail(data: ContactData): Promise<void>` (nodemailer).
|
||
+ - `POST /api/contact` → 200 `{ ok: true }` | 400 `{ error }` | 429 `{ error }` | 502 `{ error }`.
|
||
+
|
||
+- [ ] **Step 1: test validazione (falliranno)**
|
||
+
|
||
+`tests/contact.test.ts`:
|
||
+```ts
|
||
+import { describe, it, expect } from 'vitest';
|
||
+import { validateContact } from '../src/lib/contact';
|
||
+
|
||
+describe('validateContact', () => {
|
||
+ const good = { firstName: 'Mario', email: 'mario@example.com', message: 'Ciao, vorrei informazioni.' };
|
||
+
|
||
+ it('accetta payload valido e normalizza i campi opzionali', () => {
|
||
+ const r = validateContact(good);
|
||
+ expect(r.ok).toBe(true);
|
||
+ if (r.ok) {
|
||
+ expect(r.value.lastName).toBe('');
|
||
+ expect(r.value.email).toBe('mario@example.com');
|
||
+ }
|
||
+ });
|
||
+
|
||
+ it('rifiuta email non valida', () => {
|
||
+ expect(validateContact({ ...good, email: 'non-email' }).ok).toBe(false);
|
||
+ });
|
||
+
|
||
+ it('rifiuta campi obbligatori mancanti o vuoti', () => {
|
||
+ expect(validateContact({ ...good, firstName: ' ' }).ok).toBe(false);
|
||
+ expect(validateContact({ ...good, message: '' }).ok).toBe(false);
|
||
+ expect(validateContact(null).ok).toBe(false);
|
||
+ });
|
||
+
|
||
+ it('rifiuta honeypot compilato', () => {
|
||
+ expect(validateContact({ ...good, website: 'spam.com' }).ok).toBe(false);
|
||
+ });
|
||
+
|
||
+ it('rifiuta messaggi oltre 5000 caratteri', () => {
|
||
+ expect(validateContact({ ...good, message: 'x'.repeat(5001) }).ok).toBe(false);
|
||
+ });
|
||
+});
|
||
+```
|
||
+
|
||
+- [ ] **Step 2: verifica che falliscano**
|
||
+
|
||
+Run: `npx vitest run tests/contact.test.ts` — Expected: FAIL.
|
||
+
|
||
+- [ ] **Step 3: implementa contact.ts**
|
||
+
|
||
+`src/lib/contact.ts`:
|
||
+```ts
|
||
+export interface ContactData {
|
||
+ firstName: string; lastName: string; phone: string;
|
||
+ email: string; message: string; website: string;
|
||
+}
|
||
+
|
||
+const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
+
|
||
+function str(v: unknown, max: number): string {
|
||
+ return typeof v === 'string' ? v.trim().slice(0, max + 1) : '';
|
||
+}
|
||
+
|
||
+export function validateContact(data: unknown):
|
||
+ | { ok: true; value: ContactData }
|
||
+ | { ok: false; error: string } {
|
||
+ if (typeof data !== 'object' || data === null) return { ok: false, error: 'Dati non validi.' };
|
||
+ const d = data as Record<string, unknown>;
|
||
+ const value: ContactData = {
|
||
+ firstName: str(d.firstName, 100),
|
||
+ lastName: str(d.lastName, 100),
|
||
+ phone: str(d.phone, 40),
|
||
+ email: str(d.email, 200),
|
||
+ message: str(d.message, 5000),
|
||
+ website: str(d.website, 200),
|
||
+ };
|
||
+ if (value.website) return { ok: false, error: 'Richiesta non valida.' };
|
||
+ if (!value.firstName) return { ok: false, error: 'Il nome è obbligatorio.' };
|
||
+ if (!EMAIL_RE.test(value.email)) return { ok: false, error: 'Inserisci una e-mail valida.' };
|
||
+ if (!value.message) return { ok: false, error: 'Il messaggio è obbligatorio.' };
|
||
+ if (value.message.length > 5000) return { ok: false, error: 'Messaggio troppo lungo.' };
|
||
+ return { ok: true, value };
|
||
+}
|
||
+```
|
||
+Nota: `str()` taglia a `max + 1` così un messaggio oltre il limite non viene accettato silenziosamente ma rifiutato dal check di lunghezza.
|
||
+
|
||
+- [ ] **Step 4: verifica test verdi**
|
||
+
|
||
+Run: `npm test` — Expected: PASS.
|
||
+
|
||
+- [ ] **Step 5: mailer.ts e api/contact.ts**
|
||
+
|
||
+`src/lib/mailer.ts`:
|
||
+```ts
|
||
+import nodemailer from 'nodemailer';
|
||
+import { getEnv } from './env';
|
||
+import type { ContactData } from './contact';
|
||
+
|
||
+export async function sendContactEmail(data: ContactData): Promise<void> {
|
||
+ const transporter = nodemailer.createTransport({
|
||
+ host: getEnv('SMTP_HOST'),
|
||
+ port: Number(getEnv('SMTP_PORT', '587')),
|
||
+ secure: Number(getEnv('SMTP_PORT', '587')) === 465,
|
||
+ auth: { user: getEnv('SMTP_USER'), pass: getEnv('SMTP_PASS') },
|
||
+ });
|
||
+ await transporter.sendMail({
|
||
+ from: getEnv('CONTACT_FROM'),
|
||
+ to: getEnv('CONTACT_TO', 'info@insanitylab.it'),
|
||
+ replyTo: data.email,
|
||
+ subject: `Nuovo contatto dal sito: ${data.firstName} ${data.lastName}`.trim(),
|
||
+ text: [
|
||
+ `Nome: ${data.firstName} ${data.lastName}`.trim(),
|
||
+ data.phone && `Telefono: ${data.phone}`,
|
||
+ `E-mail: ${data.email}`,
|
||
+ '',
|
||
+ data.message,
|
||
+ ].filter(Boolean).join('\n'),
|
||
+ });
|
||
+}
|
||
+```
|
||
+
|
||
+`src/pages/api/contact.ts`:
|
||
+```ts
|
||
+import type { APIRoute } from 'astro';
|
||
+import { validateContact } from '../../lib/contact';
|
||
+import { sendContactEmail } from '../../lib/mailer';
|
||
+import { rateLimit } from '../../lib/rate-limit';
|
||
+
|
||
+export const prerender = false;
|
||
+
|
||
+const json = (status: number, body: object) =>
|
||
+ new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
||
+
|
||
+export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||
+ if (!rateLimit(`contact:${clientAddress}`, 5, 60 * 60 * 1000)) {
|
||
+ return json(429, { error: 'Troppe richieste, riprova più tardi.' });
|
||
+ }
|
||
+ let data: unknown;
|
||
+ try { data = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); }
|
||
+ const result = validateContact(data);
|
||
+ if (!result.ok) return json(400, { error: result.error });
|
||
+ try {
|
||
+ await sendContactEmail(result.value);
|
||
+ } catch (err) {
|
||
+ console.error('Invio email fallito:', err);
|
||
+ return json(502, { error: 'Invio non riuscito, riprova più tardi o scrivici direttamente.' });
|
||
+ }
|
||
+ return json(200, { ok: true });
|
||
+};
|
||
+```
|
||
+
|
||
+- [ ] **Step 6: contact.astro**
|
||
+
|
||
+Layout da Contatti.pdf: mappa a sinistra (click-to-load), form a destra, sotto indirizzo/telefono/email + invito.
|
||
+
|
||
+```astro
|
||
+---
|
||
+import Base from '../layouts/Base.astro';
|
||
+import PageHero from '../components/PageHero.astro';
|
||
+import ContactForm from '../components/ContactForm.astro';
|
||
+import { site } from '../data/site';
|
||
+export const prerender = true;
|
||
+const mapsQuery = encodeURIComponent(`${site.address}, ${site.cityLine}`);
|
||
+---
|
||
+<Base title="Contatti" description="Contatta InsanityLab: Via Leandro Alberti 76, Bologna. Compila il form o chiamaci per una consulenza iniziale.">
|
||
+ <PageHero title="Contattaci" />
|
||
+ <section class="section">
|
||
+ <div class="container cgrid">
|
||
+ <div class="cmap" id="map-box">
|
||
+ <button class="btn btn--dark" id="map-load">Carica la mappa (Google Maps)</button>
|
||
+ <p class="cmap__note">La mappa viene caricata da Google solo dopo il tuo consenso.</p>
|
||
+ </div>
|
||
+ <ContactForm variant="full" />
|
||
+ </div>
|
||
+ <div class="container cinfo">
|
||
+ <div>
|
||
+ <h3>Indirizzo</h3>
|
||
+ <p>{site.address} {site.cityLine}</p>
|
||
+ </div>
|
||
+ <div>
|
||
+ <h3>Telefono</h3>
|
||
+ <p>{site.phone}</p>
|
||
+ </div>
|
||
+ <div>
|
||
+ <h3>E-mail</h3>
|
||
+ <p><a href={`mailto:${site.email}`}>{site.email}</a></p>
|
||
+ </div>
|
||
+ </div>
|
||
+ <div class="container">
|
||
+ <p class="cinvite">Per informazioni o per richiedere una consulenza iniziale, compila il form. Un team specializzato ti accoglierà e ti ricontatterà al più presto, oppure lasciaci il tuo contatto.</p>
|
||
+ </div>
|
||
+ </section>
|
||
+</Base>
|
||
+
|
||
+<style>
|
||
+ .cgrid { display: grid; grid-template-columns: 1.2fr 1fr; gap: 50px; margin-bottom: 50px; }
|
||
+ .cmap { background: var(--c-bg-alt); min-height: 420px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; }
|
||
+ .cmap iframe { width: 100%; height: 100%; border: 0; min-height: 420px; }
|
||
+ .cmap__note { font-size: .8rem; color: var(--c-text-light); }
|
||
+ .cinfo { display: grid; grid-template-columns: repeat(3, 1fr); gap: 30px; margin-bottom: 30px; }
|
||
+ .cinfo h3 { font-size: .85rem; text-transform: uppercase; letter-spacing: .16em; }
|
||
+ .cinvite { max-width: 640px; }
|
||
+ @media (max-width: 991px) { .cgrid, .cinfo { grid-template-columns: 1fr; } }
|
||
+</style>
|
||
+
|
||
+<script define:vars={{ mapsQuery }}>
|
||
+ document.getElementById('map-load')?.addEventListener('click', () => {
|
||
+ const box = document.getElementById('map-box');
|
||
+ box.innerHTML = `<iframe src="https://www.google.com/maps?q=${mapsQuery}&output=embed" loading="lazy" title="Mappa InsanityLab" referrerpolicy="no-referrer-when-downgrade"></iframe>`;
|
||
+ });
|
||
+</script>
|
||
+```
|
||
+
|
||
+- [ ] **Step 7: verifica manuale API**
|
||
+
|
||
+Run (con dev server attivo):
|
||
+```bash
|
||
+curl -s -X POST localhost:4321/api/contact -H 'Content-Type: application/json' -d '{"firstName":"Test","email":"t@t.it","message":"ciao"}'
|
||
+```
|
||
+Expected: `{"error":"Invio non riuscito..."}` (502, SMTP finto in `.env`) — la catena valida→invia funziona. Con honeypot: `-d '{"firstName":"T","email":"t@t.it","message":"x","website":"spam"}'` → 400.
|
||
+
|
||
+- [ ] **Step 8: commit**
|
||
+
|
||
+```bash
|
||
+git add -A && git commit -m "feat: pagina contatti e api email con validazione testata"
|
||
+```
|
||
+
|
||
+---
|
||
+
|
||
+### Task 14: Blog pubblico (`/blog`, `/blog/[slug]`)
|
||
+
|
||
+**Files:**
|
||
+- Create: `src/pages/blog/index.astro`, `src/pages/blog/[slug].astro`, `src/lib/blog-const.ts`, `src/components/blog/Sidebar.astro`
|
||
+
|
||
+**Interfaces:**
|
||
+- Consumes: `getDb`, `listPublished`, `getPublishedBySlug`, `listRecent`, `listArchiveMonths` (Task 5), `<PageHero />`.
|
||
+- Produces: `CATEGORIES: string[]` in `blog-const.ts` (usato anche dall'admin, Task 15).
|
||
+
|
||
+- [ ] **Step 1: blog-const.ts**
|
||
+
|
||
+```ts
|
||
+export const CATEGORIES = [
|
||
+ 'Articoli tecnici & scientifici',
|
||
+ 'Contenuti educativi',
|
||
+ 'LinkedIn / Insight',
|
||
+ 'Eventi & Presidi',
|
||
+] as const;
|
||
+```
|
||
+
|
||
+- [ ] **Step 2: blog/index.astro (SSR)**
|
||
+
|
||
+```astro
|
||
+---
|
||
+import Base from '../../layouts/Base.astro';
|
||
+import PageHero from '../../components/PageHero.astro';
|
||
+import { getDb } from '../../lib/db';
|
||
+import { listPublished } from '../../lib/posts';
|
||
+import { CATEGORIES } from '../../lib/blog-const';
|
||
+export const prerender = false;
|
||
+
|
||
+const PER_PAGE = 9;
|
||
+const url = Astro.url;
|
||
+const category = url.searchParams.get('categoria') ?? undefined;
|
||
+const page = Math.max(1, Number(url.searchParams.get('pagina') ?? '1') || 1);
|
||
+const { items, total } = listPublished(getDb(), {
|
||
+ category: category && (CATEGORIES as readonly string[]).includes(category) ? category : undefined,
|
||
+ page, perPage: PER_PAGE,
|
||
+});
|
||
+const pages = Math.max(1, Math.ceil(total / PER_PAGE));
|
||
+const fmt = (d: string | null) => d ? new Intl.DateTimeFormat('it-IT', { dateStyle: 'long' }).format(new Date(d)) : '';
|
||
+const linkFor = (cat?: string, p = 1) => {
|
||
+ const q = new URLSearchParams();
|
||
+ if (cat) q.set('categoria', cat);
|
||
+ if (p > 1) q.set('pagina', String(p));
|
||
+ const s = q.toString();
|
||
+ return `/blog${s ? `?${s}` : ''}`;
|
||
+};
|
||
+---
|
||
+<Base title="Blog & Edugo" description="Edugo è lo spazio di InsanityLab dedicato alla conoscenza: articoli, guide e approfondimenti su allenamento, nutrizione e salute.">
|
||
+ <PageHero title="Blog & Edugo" />
|
||
+ <section class="section">
|
||
+ <div class="container">
|
||
+ <div class="section-heading">
|
||
+ <h2>Blog & Edugo</h2>
|
||
+ <p>Edugo è lo spazio di InsanityLab dedicato alla conoscenza: articoli, guide e approfondimenti su allenamento, nutrizione e salute per capire il corpo e migliorare il proprio benessere. Perché la conoscenza è il primo passo del cambiamento.</p>
|
||
+ </div>
|
||
+ <nav class="bfilters" aria-label="categorie">
|
||
+ <a class:list={['bfilters__item', { 'is-active': !category }]} href={linkFor()}>Mostra tutto</a>
|
||
+ {CATEGORIES.map((c) => (
|
||
+ <a class:list={['bfilters__item', { 'is-active': category === c }]} href={linkFor(c)}>{c}</a>
|
||
+ ))}
|
||
+ </nav>
|
||
+ {items.length === 0 ? (
|
||
+ <p style="text-align:center">Nessun articolo pubblicato{category ? ' in questa categoria' : ''}, torna a trovarci presto.</p>
|
||
+ ) : (
|
||
+ <div class="bgrid">
|
||
+ {items.map((post) => (
|
||
+ <article class="bcard">
|
||
+ {post.cover && <a href={`/blog/${post.slug}`}><img src={post.cover} alt="" loading="lazy" /></a>}
|
||
+ <p class="bcard__date">{fmt(post.published_at)}</p>
|
||
+ <h3><a href={`/blog/${post.slug}`}>{post.title}</a></h3>
|
||
+ <p>{post.excerpt}</p>
|
||
+ <a class="bcard__more" href={`/blog/${post.slug}`}>Per saperne di più →</a>
|
||
+ </article>
|
||
+ ))}
|
||
+ </div>
|
||
+ )}
|
||
+ {pages > 1 && (
|
||
+ <nav class="bpages" aria-label="paginazione">
|
||
+ {Array.from({ length: pages }, (_, i) => (
|
||
+ <a class:list={['bpages__n', { 'is-active': page === i + 1 }]} href={linkFor(category, i + 1)}>{i + 1}</a>
|
||
+ ))}
|
||
+ </nav>
|
||
+ )}
|
||
+ </div>
|
||
+ </section>
|
||
+</Base>
|
||
+
|
||
+<style>
|
||
+ .bfilters { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; margin-bottom: 50px; }
|
||
+ .bfilters__item { font-family: var(--font-heading); font-size: .7rem; letter-spacing: .16em; text-transform: uppercase; text-decoration: none; color: var(--c-text); padding: 10px 16px; }
|
||
+ .bfilters__item.is-active { background: var(--c-accent); color: #fff; }
|
||
+ .bgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 50px 40px; }
|
||
+ .bcard img { aspect-ratio: 4/3; object-fit: cover; width: 100%; margin-bottom: 14px; }
|
||
+ .bcard__date { font-size: .78rem; color: var(--c-text-light); margin-bottom: 4px; }
|
||
+ .bcard h3 a { text-decoration: none; }
|
||
+ .bcard__more { font-family: var(--font-heading); font-size: .7rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; text-decoration: none; color: var(--c-heading); }
|
||
+ .bpages { display: flex; gap: 8px; justify-content: center; margin-top: 50px; }
|
||
+ .bpages__n { font-family: var(--font-heading); text-decoration: none; padding: 8px 14px; color: var(--c-text); }
|
||
+ .bpages__n.is-active { background: var(--c-dark); color: #fff; }
|
||
+ @media (max-width: 991px) { .bgrid { grid-template-columns: 1fr 1fr; } }
|
||
+ @media (max-width: 600px) { .bgrid { grid-template-columns: 1fr; } }
|
||
+</style>
|
||
+```
|
||
+
|
||
+- [ ] **Step 3: Sidebar.astro**
|
||
+
|
||
+```astro
|
||
+---
|
||
+import { getDb } from '../../lib/db';
|
||
+import { listRecent, listArchiveMonths } from '../../lib/posts';
|
||
+import { CATEGORIES } from '../../lib/blog-const';
|
||
+const db = getDb();
|
||
+const recent = listRecent(db, 4);
|
||
+const months = listArchiveMonths(db);
|
||
+const monthLabel = (m: string) => new Intl.DateTimeFormat('it-IT', { month: 'long', year: 'numeric' }).format(new Date(`${m}-01`));
|
||
+---
|
||
+<aside class="bside">
|
||
+ <h3>Categorie</h3>
|
||
+ {CATEGORIES.map((c) => <p><a href={`/blog?categoria=${encodeURIComponent(c)}`}>{c}</a></p>)}
|
||
+ <h3>Ultimi articoli</h3>
|
||
+ {recent.map((p) => (
|
||
+ <p><a href={`/blog/${p.slug}`}>{p.title}</a></p>
|
||
+ ))}
|
||
+ <h3>Archivio</h3>
|
||
+ {months.map((m) => <p class="bside__month">{monthLabel(m.month)} ({m.count})</p>)}
|
||
+</aside>
|
||
+
|
||
+<style>
|
||
+ .bside h3 { font-size: .85rem; text-transform: uppercase; letter-spacing: .16em; margin-top: 30px; }
|
||
+ .bside a { text-decoration: none; color: var(--c-text); }
|
||
+ .bside a:hover { color: var(--c-accent-dark); }
|
||
+ .bside__month { text-transform: capitalize; }
|
||
+</style>
|
||
+```
|
||
+
|
||
+- [ ] **Step 4: blog/[slug].astro (SSR)**
|
||
+
|
||
+```astro
|
||
+---
|
||
+import Base from '../../layouts/Base.astro';
|
||
+import Sidebar from '../../components/blog/Sidebar.astro';
|
||
+import { getDb } from '../../lib/db';
|
||
+import { getPublishedBySlug } from '../../lib/posts';
|
||
+export const prerender = false;
|
||
+
|
||
+const post = getPublishedBySlug(getDb(), Astro.params.slug!);
|
||
+if (!post) return Astro.redirect('/404');
|
||
+const fmt = new Intl.DateTimeFormat('it-IT', { dateStyle: 'long' });
|
||
+---
|
||
+<Base title={post.title} description={post.excerpt}>
|
||
+ <section class="section">
|
||
+ <div class="container art">
|
||
+ <article>
|
||
+ {post.cover && <img class="art__cover" src={post.cover} alt="" />}
|
||
+ <p class="art__meta">{post.published_at && fmt.format(new Date(post.published_at))} · {post.category}</p>
|
||
+ <h1>{post.title}</h1>
|
||
+ <div class="art__body" set:html={post.body_html} />
|
||
+ </article>
|
||
+ <Sidebar />
|
||
+ </div>
|
||
+ </section>
|
||
+</Base>
|
||
+
|
||
+<style>
|
||
+ .art { display: grid; grid-template-columns: 2.4fr 1fr; gap: 60px; }
|
||
+ .art__cover { width: 100%; margin-bottom: 24px; }
|
||
+ .art__meta { font-size: .8rem; color: var(--c-text-light); }
|
||
+ .art__body :global(img) { margin-block: 20px; }
|
||
+ .art__body :global(blockquote) { border-left: 3px solid var(--c-accent); margin: 24px 0; padding: 6px 0 6px 20px; font-style: italic; color: var(--c-heading); }
|
||
+ @media (max-width: 991px) { .art { grid-template-columns: 1fr; } }
|
||
+</style>
|
||
+```
|
||
+Nota: `body_html` è già sanitizzato al salvataggio (Task 15); `set:html` è accettabile solo per questo campo.
|
||
+
|
||
+- [ ] **Step 5: verifica con dati di prova**
|
||
+
|
||
+```bash
|
||
+sqlite3 data/insanitylab.db "INSERT INTO posts (title, slug, category, excerpt, body_html, draft, published_at) VALUES ('Articolo di prova', 'articolo-di-prova', 'Contenuti educativi', 'Un estratto di prova.', '<p>Corpo <strong>di prova</strong>.</p>', 0, datetime('now'));"
|
||
+curl -s localhost:4321/blog | grep -o 'Articolo di prova' | head -1
|
||
+curl -s localhost:4321/blog/articolo-di-prova | grep -o 'Corpo' | head -1
|
||
+curl -s -o /dev/null -w '%{http_code}' localhost:4321/blog/inesistente
|
||
+```
|
||
+Expected: `Articolo di prova`, `Corpo`, redirect/404 per lo slug inesistente. Verifica filtri categoria e paginazione con più insert se serve.
|
||
+
|
||
+- [ ] **Step 6: commit**
|
||
+
|
||
+```bash
|
||
+git add -A && git commit -m "feat: blog pubblico con filtri, paginazione e articolo"
|
||
+```
|
||
+
|
||
+---
|
||
+
|
||
+### Task 15: Area admin — login, lista, editor TipTap, API CRUD e upload (TDD su sanitizzazione)
|
||
+
|
||
+**Files:**
|
||
+- Create: `src/lib/sanitize.ts`, `src/lib/post-body.ts`, `src/layouts/Admin.astro`, `src/pages/admin/login.astro`, `src/pages/admin/logout.ts`, `src/pages/admin/index.astro`, `src/pages/admin/new.astro`, `src/pages/admin/edit/[id].astro`, `src/components/admin/PostForm.astro`, `src/pages/api/admin/posts/index.ts`, `src/pages/api/admin/posts/[id].ts`, `src/pages/api/admin/upload.ts`, `src/pages/uploads/[...path].ts`
|
||
+- Test: `tests/sanitize.test.ts`
|
||
+
|
||
+**Interfaces:**
|
||
+- Consumes: auth/middleware (Task 6), posts (Task 5), `slugify` (Task 4), `CATEGORIES` (Task 14), `rateLimit` (Task 4), `getEnv` (Task 4).
|
||
+- Produces:
|
||
+ - `sanitizeHtml(html: string): string` in `src/lib/sanitize.ts`.
|
||
+ - `POST /api/admin/posts` body `{ title, slug?, category, excerpt, body_html, cover?, draft }` → 201 `{ id, slug }`.
|
||
+ - `PUT /api/admin/posts/[id]` stesso body → 200; `DELETE /api/admin/posts/[id]` → 200.
|
||
+ - `POST /api/admin/upload` (multipart, campo `file`) → 200 `{ url: '/uploads/<nome>' }`.
|
||
+ - `GET /uploads/<file>` serve da `UPLOADS_DIR`.
|
||
+
|
||
+- [ ] **Step 1: test sanitizzazione (falliranno)**
|
||
+
|
||
+`tests/sanitize.test.ts`:
|
||
+```ts
|
||
+import { describe, it, expect } from 'vitest';
|
||
+import { sanitizeHtml } from '../src/lib/sanitize';
|
||
+
|
||
+describe('sanitizeHtml', () => {
|
||
+ it('mantiene la formattazione consentita', () => {
|
||
+ const html = '<h2>Titolo</h2><p>Testo <strong>forte</strong> ed <em>enfasi</em></p><ul><li>voce</li></ul><blockquote>citazione</blockquote>';
|
||
+ expect(sanitizeHtml(html)).toBe(html);
|
||
+ });
|
||
+ it('rimuove script e handler', () => {
|
||
+ expect(sanitizeHtml('<p onclick="x()">ciao</p><script>alert(1)</script>')).toBe('<p>ciao</p>');
|
||
+ });
|
||
+ it('consente immagini locali e https, blocca javascript:', () => {
|
||
+ expect(sanitizeHtml('<img src="/uploads/a.jpg">')).toContain('/uploads/a.jpg');
|
||
+ expect(sanitizeHtml('<a href="javascript:alert(1)">x</a>')).toBe('<a>x</a>');
|
||
+ });
|
||
+});
|
||
+```
|
||
+
|
||
+- [ ] **Step 2: verifica che falliscano, poi implementa sanitize.ts**
|
||
+
|
||
+Run: `npx vitest run tests/sanitize.test.ts` — FAIL. Poi:
|
||
+
|
||
+`src/lib/sanitize.ts`:
|
||
+```ts
|
||
+import sanitize from 'sanitize-html';
|
||
+
|
||
+export function sanitizeHtml(html: string): string {
|
||
+ return sanitize(html, {
|
||
+ allowedTags: ['h2', 'h3', 'p', 'strong', 'em', 'u', 's', 'ul', 'ol', 'li', 'a', 'img', 'blockquote', 'br', 'hr'],
|
||
+ allowedAttributes: { a: ['href', 'rel', 'target'], img: ['src', 'alt'] },
|
||
+ allowedSchemes: ['https', 'http', 'mailto'],
|
||
+ allowedSchemesAppliedToAttributes: ['href', 'src'],
|
||
+ allowProtocolRelative: false,
|
||
+ });
|
||
+}
|
||
+```
|
||
+
|
||
+Run: `npm test` — PASS. (Se `sanitize-html` mantiene `src` relativi di default lo verifichi col test; in caso contrario aggiungi `allowedSchemesByTag` o normalizza i percorsi.)
|
||
+
|
||
+- [ ] **Step 3: layout Admin e login**
|
||
+
|
||
+`src/layouts/Admin.astro`:
|
||
+```astro
|
||
+---
|
||
+interface Props { title: string }
|
||
+const { title } = Astro.props;
|
||
+const user = Astro.locals.user;
|
||
+---
|
||
+<!doctype html>
|
||
+<html lang="it">
|
||
+ <head>
|
||
+ <meta charset="utf-8" />
|
||
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
+ <title>{title} — Admin InsanityLab</title>
|
||
+ <meta name="robots" content="noindex" />
|
||
+ <style is:global>
|
||
+ body { font-family: system-ui, sans-serif; margin: 0; background: #f4f2ef; color: #333; }
|
||
+ .abar { background: #3a2d26; color: #fff; padding: 12px 24px; display: flex; justify-content: space-between; align-items: center; }
|
||
+ .abar a { color: #d9cfc4; text-decoration: none; margin-left: 16px; }
|
||
+ .awrap { max-width: 960px; margin: 30px auto; padding: 0 20px; }
|
||
+ .abtn { background: #b5a48b; color: #fff; border: 0; padding: 10px 18px; cursor: pointer; font-size: .9rem; text-decoration: none; display: inline-block; }
|
||
+ .abtn--danger { background: #a33; }
|
||
+ .afield { width: 100%; padding: 10px; margin-bottom: 14px; border: 1px solid #ccc; box-sizing: border-box; font: inherit; }
|
||
+ table { width: 100%; border-collapse: collapse; background: #fff; }
|
||
+ th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid #eee; font-size: .92rem; }
|
||
+ .pill { font-size: .75rem; padding: 2px 10px; border-radius: 10px; background: #e5decf; }
|
||
+ .pill--draft { background: #f3d9a4; }
|
||
+ </style>
|
||
+ </head>
|
||
+ <body>
|
||
+ <div class="abar">
|
||
+ <strong>InsanityLab · Admin</strong>
|
||
+ <span>
|
||
+ {user && <>Ciao, {user.username} <a href="/admin">Articoli</a> <a href="/admin/new">Nuovo</a> <a href="/admin/logout">Esci</a> <a href="/blog" target="_blank">Vedi sito ↗</a></>}
|
||
+ </span>
|
||
+ </div>
|
||
+ <div class="awrap"><slot /></div>
|
||
+ </body>
|
||
+</html>
|
||
+```
|
||
+
|
||
+`src/pages/admin/login.astro` (POST gestito nel frontmatter):
|
||
+```astro
|
||
+---
|
||
+import Admin from '../../layouts/Admin.astro';
|
||
+import { getDb } from '../../lib/db';
|
||
+import { login, SESSION_COOKIE } from '../../lib/auth';
|
||
+import { rateLimit } from '../../lib/rate-limit';
|
||
+export const prerender = false;
|
||
+
|
||
+let error = '';
|
||
+if (Astro.request.method === 'POST') {
|
||
+ if (!rateLimit(`login:${Astro.clientAddress}`, 5, 15 * 60 * 1000)) {
|
||
+ error = 'Troppi tentativi: riprova tra 15 minuti.';
|
||
+ } else {
|
||
+ const form = await Astro.request.formData();
|
||
+ const token = login(getDb(), String(form.get('username') ?? ''), String(form.get('password') ?? ''));
|
||
+ if (token) {
|
||
+ Astro.cookies.set(SESSION_COOKIE, token, {
|
||
+ httpOnly: true, sameSite: 'lax', path: '/',
|
||
+ secure: import.meta.env.PROD, maxAge: 7 * 24 * 3600,
|
||
+ });
|
||
+ return Astro.redirect('/admin');
|
||
+ }
|
||
+ error = 'Credenziali non valide.';
|
||
+ }
|
||
+}
|
||
+---
|
||
+<Admin title="Login">
|
||
+ <h1>Accesso</h1>
|
||
+ {error && <p style="color:#a33">{error}</p>}
|
||
+ <form method="post" style="max-width:360px">
|
||
+ <input class="afield" name="username" placeholder="Utente" required autocomplete="username" />
|
||
+ <input class="afield" type="password" name="password" placeholder="Password" required autocomplete="current-password" />
|
||
+ <button class="abtn" type="submit">Entra</button>
|
||
+ </form>
|
||
+</Admin>
|
||
+```
|
||
+
|
||
+`src/pages/admin/logout.ts`:
|
||
+```ts
|
||
+import type { APIRoute } from 'astro';
|
||
+import { getDb } from '../../lib/db';
|
||
+import { logout, SESSION_COOKIE } from '../../lib/auth';
|
||
+export const prerender = false;
|
||
+
|
||
+export const GET: APIRoute = ({ cookies, redirect }) => {
|
||
+ const token = cookies.get(SESSION_COOKIE)?.value;
|
||
+ if (token) logout(getDb(), token);
|
||
+ cookies.delete(SESSION_COOKIE, { path: '/' });
|
||
+ return redirect('/admin/login');
|
||
+};
|
||
+```
|
||
+
|
||
+- [ ] **Step 4: lista articoli**
|
||
+
|
||
+`src/pages/admin/index.astro`:
|
||
+```astro
|
||
+---
|
||
+import Admin from '../../layouts/Admin.astro';
|
||
+import { getDb } from '../../lib/db';
|
||
+import { listAllPosts } from '../../lib/posts';
|
||
+export const prerender = false;
|
||
+const posts = listAllPosts(getDb());
|
||
+const fmt = new Intl.DateTimeFormat('it-IT', { dateStyle: 'short', timeStyle: 'short' });
|
||
+---
|
||
+<Admin title="Articoli">
|
||
+ <h1>Articoli</h1>
|
||
+ <p><a class="abtn" href="/admin/new">+ Nuovo articolo</a></p>
|
||
+ <table>
|
||
+ <thead><tr><th>Titolo</th><th>Categoria</th><th>Stato</th><th>Aggiornato</th><th></th></tr></thead>
|
||
+ <tbody>
|
||
+ {posts.map((p) => (
|
||
+ <tr>
|
||
+ <td><a href={`/admin/edit/${p.id}`}>{p.title}</a></td>
|
||
+ <td>{p.category}</td>
|
||
+ <td><span class:list={['pill', { 'pill--draft': p.draft }]}>{p.draft ? 'Bozza' : 'Pubblicato'}</span></td>
|
||
+ <td>{fmt.format(new Date(p.updated_at.replace(' ', 'T') + 'Z'))}</td>
|
||
+ <td><button class="abtn abtn--danger" data-del={p.id}>Elimina</button></td>
|
||
+ </tr>
|
||
+ ))}
|
||
+ </tbody>
|
||
+ </table>
|
||
+ {posts.length === 0 && <p>Nessun articolo: creane uno nuovo.</p>}
|
||
+</Admin>
|
||
+
|
||
+<script>
|
||
+ document.querySelectorAll<HTMLButtonElement>('[data-del]').forEach((b) =>
|
||
+ b.addEventListener('click', async () => {
|
||
+ if (!confirm('Eliminare definitivamente questo articolo?')) return;
|
||
+ const res = await fetch(`/api/admin/posts/${b.dataset.del}`, { method: 'DELETE' });
|
||
+ if (res.ok) location.reload();
|
||
+ else alert('Eliminazione non riuscita.');
|
||
+ }));
|
||
+</script>
|
||
+```
|
||
+
|
||
+- [ ] **Step 5: PostForm.astro con TipTap**
|
||
+
|
||
+```astro
|
||
+---
|
||
+import { CATEGORIES } from '../../lib/blog-const';
|
||
+import type { Post } from '../../lib/posts';
|
||
+interface Props { post?: Post }
|
||
+const { post } = Astro.props;
|
||
+---
|
||
+<form id="post-form" data-id={post?.id ?? ''}>
|
||
+ <input class="afield" name="title" placeholder="Titolo" required value={post?.title ?? ''} />
|
||
+ <input class="afield" name="slug" placeholder="Slug (vuoto = generato dal titolo)" value={post?.slug ?? ''} />
|
||
+ <select class="afield" name="category">
|
||
+ {CATEGORIES.map((c) => <option value={c} selected={post?.category === c}>{c}</option>)}
|
||
+ </select>
|
||
+ <textarea class="afield" name="excerpt" placeholder="Estratto (anteprima in lista blog)" maxlength="300">{post?.excerpt ?? ''}</textarea>
|
||
+
|
||
+ <label>Copertina: <input type="file" id="cover-file" accept="image/*" /></label>
|
||
+ <input type="hidden" name="cover" value={post?.cover ?? ''} />
|
||
+ <p id="cover-preview">{post?.cover && <img src={post.cover} style="max-width:200px" />}</p>
|
||
+
|
||
+ <div class="ed-toolbar" id="ed-toolbar">
|
||
+ <button type="button" data-cmd="h2">H2</button>
|
||
+ <button type="button" data-cmd="h3">H3</button>
|
||
+ <button type="button" data-cmd="bold"><b>B</b></button>
|
||
+ <button type="button" data-cmd="italic"><i>I</i></button>
|
||
+ <button type="button" data-cmd="bullet">• Elenco</button>
|
||
+ <button type="button" data-cmd="ordered">1. Elenco</button>
|
||
+ <button type="button" data-cmd="quote">“ Citazione</button>
|
||
+ <button type="button" data-cmd="link">Link</button>
|
||
+ <button type="button" data-cmd="image">Immagine</button>
|
||
+ </div>
|
||
+ <div id="editor" class="ed-body"></div>
|
||
+
|
||
+ <label><input type="checkbox" name="draft" checked={post ? post.draft === 1 : true} /> Bozza (non visibile sul sito)</label>
|
||
+ <p><button class="abtn" type="submit">Salva</button> <span id="save-msg"></span></p>
|
||
+</form>
|
||
+
|
||
+<style>
|
||
+ .ed-toolbar { display: flex; gap: 6px; flex-wrap: wrap; background: #fff; border: 1px solid #ccc; border-bottom: 0; padding: 8px; }
|
||
+ .ed-toolbar button { background: #eee; border: 1px solid #ccc; padding: 4px 10px; cursor: pointer; }
|
||
+ .ed-toolbar button.is-active { background: #b5a48b; color: #fff; }
|
||
+ .ed-body { background: #fff; border: 1px solid #ccc; min-height: 340px; padding: 12px 16px; margin-bottom: 14px; }
|
||
+ .ed-body :global(.ProseMirror) { outline: none; min-height: 320px; }
|
||
+</style>
|
||
+
|
||
+<script>
|
||
+ import { Editor } from '@tiptap/core';
|
||
+ import StarterKit from '@tiptap/starter-kit';
|
||
+ import Image from '@tiptap/extension-image';
|
||
+ import Link from '@tiptap/extension-link';
|
||
+
|
||
+ const form = document.getElementById('post-form') as HTMLFormElement;
|
||
+ const initial = (document.getElementById('initial-body') as HTMLScriptElement | null)?.textContent ?? '';
|
||
+
|
||
+ const editor = new Editor({
|
||
+ element: document.getElementById('editor')!,
|
||
+ extensions: [
|
||
+ StarterKit.configure({ heading: { levels: [2, 3] } }),
|
||
+ Image,
|
||
+ Link.configure({ openOnClick: false }),
|
||
+ ],
|
||
+ content: initial,
|
||
+ });
|
||
+
|
||
+ async function uploadFile(file: File): Promise<string | null> {
|
||
+ const fd = new FormData();
|
||
+ fd.append('file', file);
|
||
+ const res = await fetch('/api/admin/upload', { method: 'POST', body: fd });
|
||
+ if (!res.ok) { alert('Upload non riuscito.'); return null; }
|
||
+ return (await res.json()).url;
|
||
+ }
|
||
+
|
||
+ document.getElementById('ed-toolbar')!.addEventListener('click', async (e) => {
|
||
+ const btn = (e.target as HTMLElement).closest('button');
|
||
+ if (!btn) return;
|
||
+ const c = editor.chain().focus();
|
||
+ switch (btn.dataset.cmd) {
|
||
+ case 'h2': c.toggleHeading({ level: 2 }).run(); break;
|
||
+ case 'h3': c.toggleHeading({ level: 3 }).run(); break;
|
||
+ case 'bold': c.toggleBold().run(); break;
|
||
+ case 'italic': c.toggleItalic().run(); break;
|
||
+ case 'bullet': c.toggleBulletList().run(); break;
|
||
+ case 'ordered': c.toggleOrderedList().run(); break;
|
||
+ case 'quote': c.toggleBlockquote().run(); break;
|
||
+ case 'link': {
|
||
+ const url = prompt('URL del link:');
|
||
+ if (url) c.setLink({ href: url }).run();
|
||
+ break;
|
||
+ }
|
||
+ case 'image': {
|
||
+ const input = document.createElement('input');
|
||
+ input.type = 'file'; input.accept = 'image/*';
|
||
+ input.onchange = async () => {
|
||
+ const url = input.files?.[0] && await uploadFile(input.files[0]);
|
||
+ if (url) editor.chain().focus().setImage({ src: url }).run();
|
||
+ };
|
||
+ input.click();
|
||
+ break;
|
||
+ }
|
||
+ }
|
||
+ });
|
||
+
|
||
+ document.getElementById('cover-file')!.addEventListener('change', async (e) => {
|
||
+ const file = (e.target as HTMLInputElement).files?.[0];
|
||
+ if (!file) return;
|
||
+ const url = await uploadFile(file);
|
||
+ if (url) {
|
||
+ (form.elements.namedItem('cover') as HTMLInputElement).value = url;
|
||
+ document.getElementById('cover-preview')!.innerHTML = `<img src="${url}" style="max-width:200px">`;
|
||
+ }
|
||
+ });
|
||
+
|
||
+ form.addEventListener('submit', async (e) => {
|
||
+ e.preventDefault();
|
||
+ const id = form.dataset.id;
|
||
+ const body = {
|
||
+ title: (form.elements.namedItem('title') as HTMLInputElement).value,
|
||
+ slug: (form.elements.namedItem('slug') as HTMLInputElement).value,
|
||
+ category: (form.elements.namedItem('category') as HTMLSelectElement).value,
|
||
+ excerpt: (form.elements.namedItem('excerpt') as HTMLTextAreaElement).value,
|
||
+ cover: (form.elements.namedItem('cover') as HTMLInputElement).value || null,
|
||
+ draft: (form.elements.namedItem('draft') as HTMLInputElement).checked,
|
||
+ body_html: editor.getHTML(),
|
||
+ };
|
||
+ const res = await fetch(id ? `/api/admin/posts/${id}` : '/api/admin/posts', {
|
||
+ method: id ? 'PUT' : 'POST',
|
||
+ headers: { 'Content-Type': 'application/json' },
|
||
+ body: JSON.stringify(body),
|
||
+ });
|
||
+ const msg = document.getElementById('save-msg')!;
|
||
+ if (res.ok) {
|
||
+ msg.textContent = 'Salvato ✓';
|
||
+ if (!id) location.href = '/admin';
|
||
+ } else {
|
||
+ msg.textContent = (await res.json()).error ?? 'Errore di salvataggio';
|
||
+ }
|
||
+ });
|
||
+</script>
|
||
+```
|
||
+
|
||
+`src/pages/admin/new.astro`:
|
||
+```astro
|
||
+---
|
||
+import Admin from '../../layouts/Admin.astro';
|
||
+import PostForm from '../../components/admin/PostForm.astro';
|
||
+export const prerender = false;
|
||
+---
|
||
+<Admin title="Nuovo articolo">
|
||
+ <h1>Nuovo articolo</h1>
|
||
+ <script id="initial-body" type="text/plain"></script>
|
||
+ <PostForm />
|
||
+</Admin>
|
||
+```
|
||
+
|
||
+`src/pages/admin/edit/[id].astro`:
|
||
+```astro
|
||
+---
|
||
+import Admin from '../../../layouts/Admin.astro';
|
||
+import PostForm from '../../../components/admin/PostForm.astro';
|
||
+import { getDb } from '../../../lib/db';
|
||
+import { getPostById } from '../../../lib/posts';
|
||
+export const prerender = false;
|
||
+const post = getPostById(getDb(), Number(Astro.params.id));
|
||
+if (!post) return Astro.redirect('/admin');
|
||
+---
|
||
+<Admin title={`Modifica: ${post.title}`}>
|
||
+ <h1>Modifica articolo</h1>
|
||
+ <script id="initial-body" type="text/plain" set:html={post.body_html}></script>
|
||
+ <PostForm post={post} />
|
||
+</Admin>
|
||
+```
|
||
+
|
||
+- [ ] **Step 6: API CRUD**
|
||
+
|
||
+`src/lib/post-body.ts` (parsing condiviso tra POST e PUT):
|
||
+```ts
|
||
+import { slugify } from './slug';
|
||
+import { sanitizeHtml } from './sanitize';
|
||
+import { CATEGORIES } from './blog-const';
|
||
+
|
||
+export function parsePostBody(data: Record<string, unknown>) {
|
||
+ const title = String(data.title ?? '').trim();
|
||
+ if (!title) return { error: 'Il titolo è obbligatorio.' } as const;
|
||
+ const category = String(data.category ?? '');
|
||
+ if (!(CATEGORIES as readonly string[]).includes(category)) return { error: 'Categoria non valida.' } as const;
|
||
+ const slug = slugify(String(data.slug ?? '') || title);
|
||
+ if (!slug) return { error: 'Slug non valido.' } as const;
|
||
+ return {
|
||
+ value: {
|
||
+ title, slug, category,
|
||
+ excerpt: String(data.excerpt ?? '').trim().slice(0, 300),
|
||
+ body_html: sanitizeHtml(String(data.body_html ?? '')),
|
||
+ cover: data.cover ? String(data.cover) : null,
|
||
+ draft: Boolean(data.draft),
|
||
+ },
|
||
+ } as const;
|
||
+}
|
||
+```
|
||
+
|
||
+`src/pages/api/admin/posts/index.ts`:
|
||
+```ts
|
||
+import type { APIRoute } from 'astro';
|
||
+import { getDb } from '../../../../lib/db';
|
||
+import { createPost } from '../../../../lib/posts';
|
||
+import { parsePostBody } from '../../../../lib/post-body';
|
||
+export const prerender = false;
|
||
+
|
||
+const json = (status: number, body: object) =>
|
||
+ new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
||
+
|
||
+export const POST: APIRoute = async ({ request }) => {
|
||
+ let data: Record<string, unknown>;
|
||
+ try { data = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); }
|
||
+ const parsed = parsePostBody(data);
|
||
+ if ('error' in parsed) return json(400, { error: parsed.error });
|
||
+ try {
|
||
+ const id = createPost(getDb(), parsed.value);
|
||
+ return json(201, { id, slug: parsed.value.slug });
|
||
+ } catch (err: any) {
|
||
+ if (String(err?.message).includes('UNIQUE')) return json(400, { error: 'Slug già esistente.' });
|
||
+ throw err;
|
||
+ }
|
||
+};
|
||
+```
|
||
+
|
||
+`src/pages/api/admin/posts/[id].ts`:
|
||
+```ts
|
||
+import type { APIRoute } from 'astro';
|
||
+import { getDb } from '../../../../lib/db';
|
||
+import { getPostById, updatePost, deletePost } from '../../../../lib/posts';
|
||
+import { parsePostBody } from '../../../../lib/post-body';
|
||
+export const prerender = false;
|
||
+
|
||
+const json = (status: number, body: object) =>
|
||
+ new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
||
+
|
||
+export const PUT: APIRoute = async ({ params, request }) => {
|
||
+ const id = Number(params.id);
|
||
+ if (!getPostById(getDb(), id)) return json(404, { error: 'Articolo non trovato.' });
|
||
+ let data: Record<string, unknown>;
|
||
+ try { data = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); }
|
||
+ const parsed = parsePostBody(data);
|
||
+ if ('error' in parsed) return json(400, { error: parsed.error });
|
||
+ try {
|
||
+ updatePost(getDb(), id, parsed.value);
|
||
+ return json(200, { ok: true });
|
||
+ } catch (err: any) {
|
||
+ if (String(err?.message).includes('UNIQUE')) return json(400, { error: 'Slug già esistente.' });
|
||
+ throw err;
|
||
+ }
|
||
+};
|
||
+
|
||
+export const DELETE: APIRoute = ({ params }) => {
|
||
+ deletePost(getDb(), Number(params.id));
|
||
+ return json(200, { ok: true });
|
||
+};
|
||
+```
|
||
+
|
||
+- [ ] **Step 7: upload e route file**
|
||
+
|
||
+`src/pages/api/admin/upload.ts`:
|
||
+```ts
|
||
+import type { APIRoute } from 'astro';
|
||
+import { writeFile, mkdir } from 'node:fs/promises';
|
||
+import { randomBytes } from 'node:crypto';
|
||
+import { join, extname } from 'node:path';
|
||
+import { getEnv } from '../../../lib/env';
|
||
+export const prerender = false;
|
||
+
|
||
+const ALLOWED: Record<string, string> = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp', '.gif': 'image/gif' };
|
||
+const MAX_BYTES = 5 * 1024 * 1024;
|
||
+
|
||
+const json = (status: number, body: object) =>
|
||
+ new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
||
+
|
||
+export const POST: APIRoute = async ({ request }) => {
|
||
+ const form = await request.formData().catch(() => null);
|
||
+ const file = form?.get('file');
|
||
+ if (!(file instanceof File)) return json(400, { error: 'Nessun file.' });
|
||
+ const ext = extname(file.name).toLowerCase();
|
||
+ if (!ALLOWED[ext]) return json(400, { error: 'Formato non consentito (jpg, png, webp, gif).' });
|
||
+ if (file.size > MAX_BYTES) return json(400, { error: 'File troppo grande (max 5 MB).' });
|
||
+ const dir = getEnv('UPLOADS_DIR', 'uploads');
|
||
+ await mkdir(dir, { recursive: true });
|
||
+ const name = `${Date.now()}-${randomBytes(4).toString('hex')}${ext}`;
|
||
+ await writeFile(join(dir, name), Buffer.from(await file.arrayBuffer()));
|
||
+ return json(200, { url: `/uploads/${name}` });
|
||
+};
|
||
+```
|
||
+
|
||
+`src/pages/uploads/[...path].ts`:
|
||
+```ts
|
||
+import type { APIRoute } from 'astro';
|
||
+import { readFile } from 'node:fs/promises';
|
||
+import { join, normalize, extname } from 'node:path';
|
||
+import { getEnv } from '../../lib/env';
|
||
+export const prerender = false;
|
||
+
|
||
+const MIME: Record<string, string> = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp', '.gif': 'image/gif' };
|
||
+
|
||
+export const GET: APIRoute = async ({ params }) => {
|
||
+ const rel = normalize(params.path ?? '').replace(/^(\.\.[/\\])+/, '');
|
||
+ if (!rel || rel.includes('..')) return new Response('Not found', { status: 404 });
|
||
+ const mime = MIME[extname(rel).toLowerCase()];
|
||
+ if (!mime) return new Response('Not found', { status: 404 });
|
||
+ try {
|
||
+ const buf = await readFile(join(getEnv('UPLOADS_DIR', 'uploads'), rel));
|
||
+ return new Response(buf, { headers: { 'Content-Type': mime, 'Cache-Control': 'public, max-age=31536000, immutable' } });
|
||
+ } catch {
|
||
+ return new Response('Not found', { status: 404 });
|
||
+ }
|
||
+};
|
||
+```
|
||
+
|
||
+- [ ] **Step 8: verifica end-to-end manuale**
|
||
+
|
||
+Con dev server attivo e utente creato (`npm run create-user -- admin prova1234`):
|
||
+1. `curl -s -o /dev/null -w '%{http_code}' localhost:4321/admin` → `302` (redirect a login da non autenticati).
|
||
+2. `curl -s -X POST localhost:4321/api/admin/posts -H 'Content-Type: application/json' -d '{}'` → `401`.
|
||
+3. Browser: login su `/admin/login`, crea articolo con immagine nel corpo, salva come pubblicato, verifica su `/blog` e `/blog/<slug>`; modifica; elimina (con conferma).
|
||
+Expected: flusso completo senza errori console.
|
||
+
|
||
+- [ ] **Step 9: commit**
|
||
+
|
||
+```bash
|
||
+git add -A && git commit -m "feat: area admin con editor tiptap, api crud e upload immagini"
|
||
+```
|
||
+
|
||
+---
|
||
+
|
||
+### Task 16: SEO, favicon, robots e smoke test finale
|
||
+
|
||
+**Files:**
|
||
+- Create: `public/robots.txt`, `public/favicon.svg`, `scripts/smoke.sh`
|
||
+
|
||
+**Interfaces:**
|
||
+- Consumes: tutto il sito.
|
||
+
|
||
+- [ ] **Step 1: robots e favicon**
|
||
+
|
||
+`public/robots.txt`:
|
||
+```
|
||
+User-agent: *
|
||
+Disallow: /admin
|
||
+Disallow: /api
|
||
+Sitemap: https://insanitylab.tielogic.xyz/sitemap-index.xml
|
||
+```
|
||
+
|
||
+`public/favicon.svg` — battito/onda del logo InsanityLab su fondo scuro (ricava la forma dal logo estratto; in mancanza):
|
||
+```svg
|
||
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||
+ <rect width="32" height="32" rx="6" fill="#3a2d26"/>
|
||
+ <path d="M4 18h5l3-8 4 12 3-6h9" fill="none" stroke="#b5a48b" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||
+</svg>
|
||
+```
|
||
+
|
||
+- [ ] **Step 2: smoke test**
|
||
+
|
||
+`scripts/smoke.sh`:
|
||
+```bash
|
||
+#!/usr/bin/env bash
|
||
+set -euo pipefail
|
||
+cd "$(dirname "$0")/.."
|
||
+npm run build
|
||
+PORT=4399 node --env-file=.env ./dist/server/entry.mjs & SRV=$!
|
||
+trap 'kill $SRV' EXIT
|
||
+sleep 2
|
||
+fail=0
|
||
+for path in / /about /training /programs /programs/performance-class /programs/rehab /blog /contact /admin/login /robots.txt; do
|
||
+ code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:4399$path")
|
||
+ echo "$code $path"
|
||
+ [ "$code" = "200" ] || fail=1
|
||
+done
|
||
+code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:4399/admin)
|
||
+echo "$code /admin (atteso 302)"
|
||
+[ "$code" = "302" ] || fail=1
|
||
+exit $fail
|
||
+```
|
||
+
|
||
+Run: `chmod +x scripts/smoke.sh && ./scripts/smoke.sh`
|
||
+Expected: tutti `200`, `/admin` `302`, exit 0. (Il server standalone usa `PORT`; se la porta è occupata cambiala.)
|
||
+
|
||
+- [ ] **Step 3: controllo qualità finale**
|
||
+
|
||
+- `npm test` → tutti verdi.
|
||
+- `npx astro check` → nessun errore.
|
||
+- Lighthouse rapido (facoltativo): `npx lighthouse http://localhost:4399/ --quiet --chrome-flags='--headless'` — performance e SEO ≥ 90 attesi su pagine statiche.
|
||
+
|
||
+- [ ] **Step 4: commit**
|
||
+
|
||
+```bash
|
||
+git add -A && git commit -m "chore: robots, favicon e smoke test"
|
||
+```
|
||
+
|
||
+---
|
||
+
|
||
+### Task 17: Remote GitLab e push
|
||
+
|
||
+**Files:**
|
||
+- Nessun file di progetto; solo configurazione git.
|
||
+
|
||
+- [ ] **Step 1: verifica accesso GitLab**
|
||
+
|
||
+Run: `command -v glab && glab auth status`
|
||
+- Se `glab` autenticato: `glab repo create insanitylab-website --private --source . --remote origin --push`.
|
||
+- Se `glab` assente o non autenticato: chiedi all'operatore l'URL del repo GitLab (o le credenziali/namespace) e usa `git remote add origin <url> && git push -u origin main`.
|
||
+
|
||
+- [ ] **Step 2: verifica push**
|
||
+
|
||
+Run: `git remote -v && git log origin/main --oneline | head -3`
|
||
+Expected: remote configurato, commit presenti sul remote.
|
||
+
|
||
+---
|
||
+
|
||
+## Ordine di esecuzione e note
|
||
+
|
||
+1 → 2 → 3 → 4 → 5 → 6 → 8 → 7 → 9 → 10 → 11 → 12 → 13 → 14 → 15 → 16 → 17
|
||
+(Il Task 8 precede il 7 perché Header/Footer consumano `site.ts`; i task 9–15 dipendono da 7+8.)
|
||
+
|
||
+**Punti aperti da segnalare all'operatore a fine esecuzione:**
|
||
+- Testi Storylab 2025/2026/OGGI mancanti (placeholder «Contenuto in arrivo»).
|
||
+- Orario agenda = placeholder dal template (nomi trainer finti).
|
||
+- URL social reali da confermare in `src/data/site.ts`.
|
||
+- Credenziali SMTP reali da mettere in `.env` (ora finte).
|
||
+- Testi estesi/FAQ dei programmi diversi da Performance Class: bozze ragionevoli da far validare al cliente.
|
||
+
|
||
+
|
||
+
|
||
+
|
||
+
|
||
diff --git a/package-lock.json b/package-lock.json
|
||
new file mode 100644
|
||
index 0000000..a60ca66
|
||
--- /dev/null
|
||
+++ b/package-lock.json
|
||
@@ -0,0 +1,6875 @@
|
||
+{
|
||
+ "name": "insanitylab-website",
|
||
+ "version": "0.1.0",
|
||
+ "lockfileVersion": 3,
|
||
+ "requires": true,
|
||
+ "packages": {
|
||
+ "": {
|
||
+ "name": "insanitylab-website",
|
||
+ "version": "0.1.0",
|
||
+ "dependencies": {
|
||
+ "@astrojs/node": "^11.0.1",
|
||
+ "@astrojs/sitemap": "^3.7.3",
|
||
+ "@fontsource/montserrat": "^5.2.8",
|
||
+ "@fontsource/open-sans": "^5.2.7",
|
||
+ "@tiptap/core": "^3.27.1",
|
||
+ "@tiptap/extension-image": "^3.27.1",
|
||
+ "@tiptap/extension-link": "^3.27.1",
|
||
+ "@tiptap/starter-kit": "^3.27.1",
|
||
+ "astro": "^7.0.5",
|
||
+ "bcryptjs": "^3.0.3",
|
||
+ "better-sqlite3": "^12.11.1",
|
||
+ "nodemailer": "^9.0.3",
|
||
+ "sanitize-html": "^2.17.5"
|
||
+ },
|
||
+ "devDependencies": {
|
||
+ "@astrojs/check": "^0.9.9",
|
||
+ "@types/bcryptjs": "^2.4.6",
|
||
+ "@types/better-sqlite3": "^7.6.13",
|
||
+ "@types/nodemailer": "^8.0.1",
|
||
+ "@types/sanitize-html": "^2.16.1",
|
||
+ "typescript": "^6.0.3",
|
||
+ "vitest": "^4.1.9"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/check": {
|
||
+ "version": "0.9.9",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.9.tgz",
|
||
+ "integrity": "sha512-A5UW8uIuErLWEoRQvzgXpO1gTjUFtK8r7nU2Z7GewAMxUb7bPvpk11qaKKgxqXlHJWlAvaaxy+Xg28A6bmQ1Tg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@astrojs/language-server": "^2.16.7",
|
||
+ "chokidar": "^4.0.3",
|
||
+ "kleur": "^4.1.5",
|
||
+ "yargs": "^17.7.2"
|
||
+ },
|
||
+ "bin": {
|
||
+ "astro-check": "bin/astro-check.js"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "typescript": "^5.0.0 || ^6.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/check/node_modules/chokidar": {
|
||
+ "version": "4.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "readdirp": "^4.0.1"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">= 14.16.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://paulmillr.com/funding/"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/check/node_modules/readdirp": {
|
||
+ "version": "4.1.2",
|
||
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">= 14.18.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "individual",
|
||
+ "url": "https://paulmillr.com/funding/"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/compiler": {
|
||
+ "version": "2.13.1",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.13.1.tgz",
|
||
+ "integrity": "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/@astrojs/compiler-binding": {
|
||
+ "version": "0.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.3.0.tgz",
|
||
+ "integrity": "sha512-zlsOT5COD9hRwplJCgQhS21unxON5AKirf0vgt1ijXwuseYIaZdm2ZOpF8fsz+DY9EyXx+I/ukxtg7uoBep68A==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "@astrojs/compiler-binding-darwin-arm64": "0.3.0",
|
||
+ "@astrojs/compiler-binding-darwin-x64": "0.3.0",
|
||
+ "@astrojs/compiler-binding-linux-arm64-gnu": "0.3.0",
|
||
+ "@astrojs/compiler-binding-linux-arm64-musl": "0.3.0",
|
||
+ "@astrojs/compiler-binding-linux-x64-gnu": "0.3.0",
|
||
+ "@astrojs/compiler-binding-linux-x64-musl": "0.3.0",
|
||
+ "@astrojs/compiler-binding-wasm32-wasi": "0.3.0",
|
||
+ "@astrojs/compiler-binding-win32-arm64-msvc": "0.3.0",
|
||
+ "@astrojs/compiler-binding-win32-x64-msvc": "0.3.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/compiler-binding-darwin-arm64": {
|
||
+ "version": "0.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.3.0.tgz",
|
||
+ "integrity": "sha512-3n0uu+uJpnCq8b4JFi3uGDsIisAvHctxSmH+cIO9Gbei1H1Y1QXaYboXyiWJugUmprr3OEYP7+LdodzpVFzLMQ==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "darwin"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/compiler-binding-darwin-x64": {
|
||
+ "version": "0.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.3.0.tgz",
|
||
+ "integrity": "sha512-scxNGKjOBydMo1QR4LtK0FMgh7ubQomJDv953nz2msQFkPKke/0FpPv/cQM0T/kuZdReZQFU8Oz3iOrP/6WHEg==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "darwin"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/compiler-binding-linux-arm64-gnu": {
|
||
+ "version": "0.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.3.0.tgz",
|
||
+ "integrity": "sha512-NZrWLolVUANmrnl0zrFK/Sx5Sock1gEUT49ALfMTTCA5Ya2ec/BoJXMIg4KgE+wZcrdXJ8e+WyEhM7YLk/FJkA==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/compiler-binding-linux-arm64-musl": {
|
||
+ "version": "0.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.3.0.tgz",
|
||
+ "integrity": "sha512-PjwRmKgMFDsFhg82g0poXlIY8Qn3fMA3hXjaR0coJWJzTJsRH9ATU0j2ocigjtU1h3vL/yR7yLUxGj/lTCq73g==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/compiler-binding-linux-x64-gnu": {
|
||
+ "version": "0.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.3.0.tgz",
|
||
+ "integrity": "sha512-Dr69VJYlnSfyL8gzELW6S4mE41P7TDPn1IKjwMnjdZ7+dxgJI50oMLFSk1LVe26bHmWB3ktuh8fDVK1THI9e9A==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/compiler-binding-linux-x64-musl": {
|
||
+ "version": "0.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.3.0.tgz",
|
||
+ "integrity": "sha512-AEt+bRw8PfImCcyRH1lpXVB8CdmQ1K/wPo5u99iec4/U/XdNvQZ715YVuNzIJpbJXelgQeZ5H2+Ea7XwRyWY5g==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/compiler-binding-wasm32-wasi": {
|
||
+ "version": "0.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.3.0.tgz",
|
||
+ "integrity": "sha512-U80tA1j8V6LjhiTZzVCtG4E8hrNVVNXDGV5fCgJ94q8FU9CPH+XwdDDhLzBybfWhKfyItXmQiZNRPTiPCYTpVg==",
|
||
+ "cpu": [
|
||
+ "wasm32"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "dependencies": {
|
||
+ "@napi-rs/wasm-runtime": "^1.1.6"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=14.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/compiler-binding-win32-arm64-msvc": {
|
||
+ "version": "0.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.3.0.tgz",
|
||
+ "integrity": "sha512-CpY1RII2r1XMpOUVD1VR/F2wtuRsiOCkFULS10Khyj8/DFZMtxVuUCAWGw+CW2Ka0h6eP3Xc1CA+glFlvXMPxA==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "win32"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/compiler-binding-win32-x64-msvc": {
|
||
+ "version": "0.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.3.0.tgz",
|
||
+ "integrity": "sha512-qmFbs769oeeGrRebAnCW7aBk8m71vf85W/dX/jddfx5Z06/w0wf7TZCfJPOX1Fld2t+4N+iXzfGEJG+zJQ+bzg==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "win32"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/compiler-rs": {
|
||
+ "version": "0.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.3.0.tgz",
|
||
+ "integrity": "sha512-J2qEVHtIDjEM9TxwmwuebOGmZNwhKu/dR7P7qBpnJKGmBBX0vdweQ/4cEXhj8fBbWVUB5V12xWChri3CgKNULQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@astrojs/compiler-binding": "0.3.0"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/internal-helpers": {
|
||
+ "version": "0.10.0",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.0.tgz",
|
||
+ "integrity": "sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/hast": "^3.0.4",
|
||
+ "@types/mdast": "^4.0.4",
|
||
+ "js-yaml": "^4.1.1",
|
||
+ "picomatch": "^4.0.4",
|
||
+ "retext-smartypants": "^6.2.0",
|
||
+ "shiki": "^4.0.2",
|
||
+ "smol-toml": "^1.6.0",
|
||
+ "unified": "^11.0.5"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/language-server": {
|
||
+ "version": "2.16.10",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.16.10.tgz",
|
||
+ "integrity": "sha512-87VQ/5GSdHlRnUA+hGuerYyIGAj+9RbZmATyuKLEUePinUXhQ5YkRnRrHhOD9sSi5JOErLjrLkHnfZFEvGrV8w==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@astrojs/compiler": "^2.13.1",
|
||
+ "@astrojs/yaml2ts": "^0.2.4",
|
||
+ "@jridgewell/sourcemap-codec": "^1.5.5",
|
||
+ "@volar/kit": "~2.4.28",
|
||
+ "@volar/language-core": "~2.4.28",
|
||
+ "@volar/language-server": "~2.4.28",
|
||
+ "@volar/language-service": "~2.4.28",
|
||
+ "muggle-string": "^0.4.1",
|
||
+ "tinyglobby": "^0.2.16",
|
||
+ "volar-service-css": "0.0.70",
|
||
+ "volar-service-emmet": "0.0.70",
|
||
+ "volar-service-html": "0.0.70",
|
||
+ "volar-service-prettier": "0.0.70",
|
||
+ "volar-service-typescript": "0.0.70",
|
||
+ "volar-service-typescript-twoslash-queries": "0.0.70",
|
||
+ "volar-service-yaml": "0.0.70",
|
||
+ "vscode-html-languageservice": "^5.6.2",
|
||
+ "vscode-uri": "^3.1.0"
|
||
+ },
|
||
+ "bin": {
|
||
+ "astro-ls": "bin/nodeServer.js"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "prettier": "^3.0.0",
|
||
+ "prettier-plugin-astro": ">=0.11.0"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "prettier": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "prettier-plugin-astro": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/markdown-satteri": {
|
||
+ "version": "0.3.2",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.3.2.tgz",
|
||
+ "integrity": "sha512-feXuUPy41gVfeM7EHT1ciUim8ozGr+YHXab9uUBc1Hk8y60DQosO8ldL+AoPXnCAoGj1OChwHfvXmmJ6XVnY9A==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@astrojs/internal-helpers": "0.10.0",
|
||
+ "@astrojs/prism": "4.0.2",
|
||
+ "github-slugger": "^2.0.0",
|
||
+ "satteri": "^0.9.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/node": {
|
||
+ "version": "11.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/node/-/node-11.0.1.tgz",
|
||
+ "integrity": "sha512-92s+sCSxe40xmiJfHH1Dti4uawI40/48yaL2GmVk+EbiaoztECmsIL5wBEB5xwMLI/9VBy8lZMqmfAd5u4OxhQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@astrojs/internal-helpers": "0.10.0",
|
||
+ "send": "^1.2.1",
|
||
+ "server-destroy": "^1.0.1"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "astro": "^7.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/prism": {
|
||
+ "version": "4.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.2.tgz",
|
||
+ "integrity": "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "prismjs": "^1.30.0"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/sitemap": {
|
||
+ "version": "3.7.3",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.3.tgz",
|
||
+ "integrity": "sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "sitemap": "^9.0.0",
|
||
+ "stream-replace-string": "^2.0.0",
|
||
+ "zod": "^4.3.6"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/telemetry": {
|
||
+ "version": "3.3.2",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.2.tgz",
|
||
+ "integrity": "sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "ci-info": "^4.4.0",
|
||
+ "dset": "^3.1.4",
|
||
+ "is-docker": "^4.0.0",
|
||
+ "is-wsl": "^3.1.1",
|
||
+ "which-pm-runs": "^1.1.0"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": "18.20.8 || ^20.3.0 || >=22.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@astrojs/yaml2ts": {
|
||
+ "version": "0.2.4",
|
||
+ "resolved": "https://registry.npmjs.org/@astrojs/yaml2ts/-/yaml2ts-0.2.4.tgz",
|
||
+ "integrity": "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "yaml": "^2.8.3"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@babel/helper-string-parser": {
|
||
+ "version": "7.29.7",
|
||
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=6.9.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@babel/helper-validator-identifier": {
|
||
+ "version": "7.29.7",
|
||
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=6.9.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@babel/parser": {
|
||
+ "version": "7.29.7",
|
||
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@babel/types": "^7.29.7"
|
||
+ },
|
||
+ "bin": {
|
||
+ "parser": "bin/babel-parser.js"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=6.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@babel/types": {
|
||
+ "version": "7.29.7",
|
||
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@babel/helper-string-parser": "^7.29.7",
|
||
+ "@babel/helper-validator-identifier": "^7.29.7"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=6.9.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@bruits/satteri-darwin-arm64": {
|
||
+ "version": "0.9.4",
|
||
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.9.4.tgz",
|
||
+ "integrity": "sha512-W3MSUkr2mZRR8Stoe+lqNAyzQzRuFMU8WffV9IvFSxTok0LGWR0ZZQPLELU4QTRiUbhL2Y4VUP9vV7pj8rHjgg==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "darwin"
|
||
+ ]
|
||
+ },
|
||
+ "node_modules/@bruits/satteri-darwin-x64": {
|
||
+ "version": "0.9.4",
|
||
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.9.4.tgz",
|
||
+ "integrity": "sha512-DXOuuaE1lsv7mpk2mOvGrzqoEWEvOIZEO/fXVa7zfM23Iob+CBjBkRAMwpHA4pmZ3j6Gj7WJzPKw0kQ7w741AQ==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "darwin"
|
||
+ ]
|
||
+ },
|
||
+ "node_modules/@bruits/satteri-linux-arm64-gnu": {
|
||
+ "version": "0.9.4",
|
||
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.9.4.tgz",
|
||
+ "integrity": "sha512-gJxU9rGGoqIznSEgEzpjxkry24jeHuMpoo1tCIAhHYh7WaD3j5F8zt3jmHxEaN1Uwa+K5+wFgIR2uIGOnMzEmw==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ]
|
||
+ },
|
||
+ "node_modules/@bruits/satteri-linux-arm64-musl": {
|
||
+ "version": "0.9.4",
|
||
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.9.4.tgz",
|
||
+ "integrity": "sha512-Wjzu9hmmAbfmDkBfPI1VdZygJtYz9uYZQnkEyrXi6S2JFi+2pXQ1A5irj38bqm0IZmWcTbk0cVG4NZnPdtVNJA==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ]
|
||
+ },
|
||
+ "node_modules/@bruits/satteri-linux-x64-gnu": {
|
||
+ "version": "0.9.4",
|
||
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.9.4.tgz",
|
||
+ "integrity": "sha512-MR1Q+wMx65FQlbSV7cRqWW87Knp0zkoaIV55Dt+xZl028wJABXEPEEmG3670SLq7lVZvcGIDwCgSg2kCYxvRwA==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ]
|
||
+ },
|
||
+ "node_modules/@bruits/satteri-linux-x64-musl": {
|
||
+ "version": "0.9.4",
|
||
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.9.4.tgz",
|
||
+ "integrity": "sha512-T4gxhXve3zyNAZesrXAd/rDZOGRkbfFIUFld4TGsw6BsjoIteCcDji6IMqeXyaWEVSykY2X8Eid2hr6aXGYAaw==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ]
|
||
+ },
|
||
+ "node_modules/@bruits/satteri-wasm32-wasi": {
|
||
+ "version": "0.9.4",
|
||
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.9.4.tgz",
|
||
+ "integrity": "sha512-/CEG8LUlpaBEnhFnYVn0UnlHFLs51UhrkJBUPDUXLzkadzAcnR88iRA/nOl7Zwhjb4WhfBV4p3P5qeOJMtH0iA==",
|
||
+ "cpu": [
|
||
+ "wasm32"
|
||
+ ],
|
||
+ "optional": true,
|
||
+ "dependencies": {
|
||
+ "@emnapi/core": "1.11.1",
|
||
+ "@emnapi/runtime": "1.11.1",
|
||
+ "@napi-rs/wasm-runtime": "^1.1.6"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=14.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@bruits/satteri-win32-arm64-msvc": {
|
||
+ "version": "0.9.4",
|
||
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.9.4.tgz",
|
||
+ "integrity": "sha512-E1ZPQbgCtFKiU7pFYVndynvY7ne4coeVDUgnVThErSFlJ2ceQCBZrfRTD1lzrIDy63Bbqo+g/cZY9duw+JYjIw==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "win32"
|
||
+ ]
|
||
+ },
|
||
+ "node_modules/@bruits/satteri-win32-x64-msvc": {
|
||
+ "version": "0.9.4",
|
||
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.9.4.tgz",
|
||
+ "integrity": "sha512-5I7SiarsNdAUuhJb50CXJPTwr/ECVrBoU+fymoLjChK5fW//+srhY4lstcNTzgFRtQSYfVtm4OQZz16CVMeTeA==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "win32"
|
||
+ ]
|
||
+ },
|
||
+ "node_modules/@capsizecss/unpack": {
|
||
+ "version": "4.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.1.tgz",
|
||
+ "integrity": "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "fontkitten": "^1.0.3"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@clack/core": {
|
||
+ "version": "1.4.2",
|
||
+ "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.2.tgz",
|
||
+ "integrity": "sha512-0Ty/1Gfm+Kb07sXcuESjyKfwEhSy4Ns1AgeEisHb/bDY5fWme0tTeTkU14T1Gmcs17YIjB/teiDe4uaCghbYqQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "fast-wrap-ansi": "^0.2.0",
|
||
+ "sisteransi": "^1.0.5"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">= 20.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@clack/prompts": {
|
||
+ "version": "1.6.0",
|
||
+ "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.6.0.tgz",
|
||
+ "integrity": "sha512-EYlRokl8szrP9Z25qT5aepMdBjzBvHF9ZEhzIiUBc9guz/T31EqRgvD0QSgZcpE93xiwrr+OkB4nz0BZyF6fSA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@clack/core": "1.4.2",
|
||
+ "fast-string-width": "^3.0.2",
|
||
+ "fast-wrap-ansi": "^0.2.0",
|
||
+ "sisteransi": "^1.0.5"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">= 20.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@emmetio/abbreviation": {
|
||
+ "version": "2.3.3",
|
||
+ "resolved": "https://registry.npmjs.org/@emmetio/abbreviation/-/abbreviation-2.3.3.tgz",
|
||
+ "integrity": "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@emmetio/scanner": "^1.0.4"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@emmetio/css-abbreviation": {
|
||
+ "version": "2.1.8",
|
||
+ "resolved": "https://registry.npmjs.org/@emmetio/css-abbreviation/-/css-abbreviation-2.1.8.tgz",
|
||
+ "integrity": "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@emmetio/scanner": "^1.0.4"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@emmetio/css-parser": {
|
||
+ "version": "0.4.1",
|
||
+ "resolved": "https://registry.npmjs.org/@emmetio/css-parser/-/css-parser-0.4.1.tgz",
|
||
+ "integrity": "sha512-2bC6m0MV/voF4CTZiAbG5MWKbq5EBmDPKu9Sb7s7nVcEzNQlrZP6mFFFlIaISM8X6514H9shWMme1fCm8cWAfQ==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@emmetio/stream-reader": "^2.2.0",
|
||
+ "@emmetio/stream-reader-utils": "^0.1.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@emmetio/html-matcher": {
|
||
+ "version": "1.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@emmetio/html-matcher/-/html-matcher-1.3.0.tgz",
|
||
+ "integrity": "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==",
|
||
+ "dev": true,
|
||
+ "license": "ISC",
|
||
+ "dependencies": {
|
||
+ "@emmetio/scanner": "^1.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@emmetio/scanner": {
|
||
+ "version": "1.0.4",
|
||
+ "resolved": "https://registry.npmjs.org/@emmetio/scanner/-/scanner-1.0.4.tgz",
|
||
+ "integrity": "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/@emmetio/stream-reader": {
|
||
+ "version": "2.2.0",
|
||
+ "resolved": "https://registry.npmjs.org/@emmetio/stream-reader/-/stream-reader-2.2.0.tgz",
|
||
+ "integrity": "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/@emmetio/stream-reader-utils": {
|
||
+ "version": "0.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/@emmetio/stream-reader-utils/-/stream-reader-utils-0.1.0.tgz",
|
||
+ "integrity": "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/@emnapi/core": {
|
||
+ "version": "1.11.1",
|
||
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
|
||
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "dependencies": {
|
||
+ "@emnapi/wasi-threads": "1.2.2",
|
||
+ "tslib": "^2.4.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@emnapi/runtime": {
|
||
+ "version": "1.11.1",
|
||
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
||
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "dependencies": {
|
||
+ "tslib": "^2.4.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@emnapi/wasi-threads": {
|
||
+ "version": "1.2.2",
|
||
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "dependencies": {
|
||
+ "tslib": "^2.4.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/aix-ppc64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
||
+ "cpu": [
|
||
+ "ppc64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "aix"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/android-arm": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||
+ "cpu": [
|
||
+ "arm"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "android"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/android-arm64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "android"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/android-x64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "android"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/darwin-arm64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "darwin"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/darwin-x64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "darwin"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/freebsd-arm64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
||
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "freebsd"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/freebsd-x64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
||
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "freebsd"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/linux-arm": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||
+ "cpu": [
|
||
+ "arm"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/linux-arm64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/linux-ia32": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||
+ "cpu": [
|
||
+ "ia32"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/linux-loong64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
||
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
||
+ "cpu": [
|
||
+ "loong64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/linux-mips64el": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||
+ "cpu": [
|
||
+ "mips64el"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/linux-ppc64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||
+ "cpu": [
|
||
+ "ppc64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/linux-riscv64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||
+ "cpu": [
|
||
+ "riscv64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/linux-s390x": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||
+ "cpu": [
|
||
+ "s390x"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/linux-x64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/netbsd-arm64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "netbsd"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/netbsd-x64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "netbsd"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/openbsd-arm64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "openbsd"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/openbsd-x64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
||
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "openbsd"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/openharmony-arm64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "openharmony"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/sunos-x64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "sunos"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/win32-arm64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "win32"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/win32-ia32": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||
+ "cpu": [
|
||
+ "ia32"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "win32"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@esbuild/win32-x64": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
||
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "win32"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@fontsource/montserrat": {
|
||
+ "version": "5.2.8",
|
||
+ "resolved": "https://registry.npmjs.org/@fontsource/montserrat/-/montserrat-5.2.8.tgz",
|
||
+ "integrity": "sha512-xTjLxSbSfCycDB0pwmNsfNvdfWPaDaRQ2LC6yt/ZI7SdvXG52zHnzNYC/09mzuAuWNJyShkteutfCoDgym56hQ==",
|
||
+ "license": "OFL-1.1",
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/ayuhito"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@fontsource/open-sans": {
|
||
+ "version": "5.2.7",
|
||
+ "resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.2.7.tgz",
|
||
+ "integrity": "sha512-8yfgDYjE5O0vmTPdrcjV35y4yMnctsokmi9gN49Gcsr0sjzkMkR97AnKDe6OqZh2SFkYlR28fxOvi21bYEgMSw==",
|
||
+ "license": "OFL-1.1",
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/ayuhito"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/colour": {
|
||
+ "version": "1.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-darwin-arm64": {
|
||
+ "version": "0.35.3",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz",
|
||
+ "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "Apache-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "darwin"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=20.9.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "@img/sharp-libvips-darwin-arm64": "1.3.2"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-darwin-x64": {
|
||
+ "version": "0.35.3",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz",
|
||
+ "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "Apache-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "darwin"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=20.9.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "@img/sharp-libvips-darwin-x64": "1.3.2"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-freebsd-wasm32": {
|
||
+ "version": "0.35.3",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz",
|
||
+ "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==",
|
||
+ "license": "Apache-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "freebsd"
|
||
+ ],
|
||
+ "dependencies": {
|
||
+ "@img/sharp-wasm32": "0.35.3"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20.9.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
|
||
+ "version": "1.3.2",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz",
|
||
+ "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "LGPL-3.0-or-later",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "darwin"
|
||
+ ],
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-libvips-darwin-x64": {
|
||
+ "version": "1.3.2",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz",
|
||
+ "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "LGPL-3.0-or-later",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "darwin"
|
||
+ ],
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-libvips-linux-arm": {
|
||
+ "version": "1.3.2",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz",
|
||
+ "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==",
|
||
+ "cpu": [
|
||
+ "arm"
|
||
+ ],
|
||
+ "license": "LGPL-3.0-or-later",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-libvips-linux-arm64": {
|
||
+ "version": "1.3.2",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz",
|
||
+ "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "LGPL-3.0-or-later",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
|
||
+ "version": "1.3.2",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz",
|
||
+ "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==",
|
||
+ "cpu": [
|
||
+ "ppc64"
|
||
+ ],
|
||
+ "license": "LGPL-3.0-or-later",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
|
||
+ "version": "1.3.2",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz",
|
||
+ "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==",
|
||
+ "cpu": [
|
||
+ "riscv64"
|
||
+ ],
|
||
+ "license": "LGPL-3.0-or-later",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-libvips-linux-s390x": {
|
||
+ "version": "1.3.2",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz",
|
||
+ "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==",
|
||
+ "cpu": [
|
||
+ "s390x"
|
||
+ ],
|
||
+ "license": "LGPL-3.0-or-later",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-libvips-linux-x64": {
|
||
+ "version": "1.3.2",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz",
|
||
+ "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "LGPL-3.0-or-later",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||
+ "version": "1.3.2",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz",
|
||
+ "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "LGPL-3.0-or-later",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||
+ "version": "1.3.2",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz",
|
||
+ "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "LGPL-3.0-or-later",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-linux-arm": {
|
||
+ "version": "0.35.3",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz",
|
||
+ "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==",
|
||
+ "cpu": [
|
||
+ "arm"
|
||
+ ],
|
||
+ "license": "Apache-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=20.9.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "@img/sharp-libvips-linux-arm": "1.3.2"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-linux-arm64": {
|
||
+ "version": "0.35.3",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz",
|
||
+ "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "Apache-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=20.9.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "@img/sharp-libvips-linux-arm64": "1.3.2"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-linux-ppc64": {
|
||
+ "version": "0.35.3",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz",
|
||
+ "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==",
|
||
+ "cpu": [
|
||
+ "ppc64"
|
||
+ ],
|
||
+ "license": "Apache-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=20.9.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "@img/sharp-libvips-linux-ppc64": "1.3.2"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-linux-riscv64": {
|
||
+ "version": "0.35.3",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz",
|
||
+ "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==",
|
||
+ "cpu": [
|
||
+ "riscv64"
|
||
+ ],
|
||
+ "license": "Apache-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=20.9.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "@img/sharp-libvips-linux-riscv64": "1.3.2"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-linux-s390x": {
|
||
+ "version": "0.35.3",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz",
|
||
+ "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==",
|
||
+ "cpu": [
|
||
+ "s390x"
|
||
+ ],
|
||
+ "license": "Apache-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=20.9.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "@img/sharp-libvips-linux-s390x": "1.3.2"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-linux-x64": {
|
||
+ "version": "0.35.3",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz",
|
||
+ "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "Apache-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=20.9.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "@img/sharp-libvips-linux-x64": "1.3.2"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-linuxmusl-arm64": {
|
||
+ "version": "0.35.3",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz",
|
||
+ "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "Apache-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=20.9.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.2"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-linuxmusl-x64": {
|
||
+ "version": "0.35.3",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz",
|
||
+ "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "Apache-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=20.9.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.2"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-wasm32": {
|
||
+ "version": "0.35.3",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz",
|
||
+ "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==",
|
||
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||
+ "optional": true,
|
||
+ "dependencies": {
|
||
+ "@emnapi/runtime": "^1.11.1"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20.9.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-webcontainers-wasm32": {
|
||
+ "version": "0.35.3",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz",
|
||
+ "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==",
|
||
+ "cpu": [
|
||
+ "wasm32"
|
||
+ ],
|
||
+ "license": "Apache-2.0",
|
||
+ "optional": true,
|
||
+ "dependencies": {
|
||
+ "@img/sharp-wasm32": "0.35.3"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20.9.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-win32-arm64": {
|
||
+ "version": "0.35.3",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz",
|
||
+ "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "win32"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=20.9.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-win32-ia32": {
|
||
+ "version": "0.35.3",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz",
|
||
+ "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==",
|
||
+ "cpu": [
|
||
+ "ia32"
|
||
+ ],
|
||
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "win32"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.9.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@img/sharp-win32-x64": {
|
||
+ "version": "0.35.3",
|
||
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz",
|
||
+ "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "win32"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">=20.9.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@jridgewell/sourcemap-codec": {
|
||
+ "version": "1.5.5",
|
||
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/@napi-rs/wasm-runtime": {
|
||
+ "version": "1.1.6",
|
||
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
|
||
+ "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "dependencies": {
|
||
+ "@tybys/wasm-util": "^0.10.3"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/Brooooooklyn"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@emnapi/core": "^1.7.1",
|
||
+ "@emnapi/runtime": "^1.7.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@oslojs/encoding": {
|
||
+ "version": "1.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz",
|
||
+ "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/@oxc-project/types": {
|
||
+ "version": "0.138.0",
|
||
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz",
|
||
+ "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/Boshen"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@rolldown/binding-android-arm64": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz",
|
||
+ "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "android"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@rolldown/binding-darwin-arm64": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz",
|
||
+ "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "darwin"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@rolldown/binding-darwin-x64": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz",
|
||
+ "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "darwin"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@rolldown/binding-freebsd-x64": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz",
|
||
+ "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "freebsd"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz",
|
||
+ "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==",
|
||
+ "cpu": [
|
||
+ "arm"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz",
|
||
+ "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz",
|
||
+ "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz",
|
||
+ "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==",
|
||
+ "cpu": [
|
||
+ "ppc64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz",
|
||
+ "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==",
|
||
+ "cpu": [
|
||
+ "s390x"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz",
|
||
+ "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@rolldown/binding-linux-x64-musl": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz",
|
||
+ "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@rolldown/binding-openharmony-arm64": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz",
|
||
+ "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "openharmony"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@rolldown/binding-wasm32-wasi": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz",
|
||
+ "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==",
|
||
+ "cpu": [
|
||
+ "wasm32"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "dependencies": {
|
||
+ "@emnapi/core": "1.11.1",
|
||
+ "@emnapi/runtime": "1.11.1",
|
||
+ "@napi-rs/wasm-runtime": "^1.1.6"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz",
|
||
+ "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "win32"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz",
|
||
+ "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "win32"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@rolldown/pluginutils": {
|
||
+ "version": "1.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/@rollup/pluginutils": {
|
||
+ "version": "5.4.0",
|
||
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz",
|
||
+ "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/estree": "^1.0.0",
|
||
+ "estree-walker": "^2.0.2",
|
||
+ "picomatch": "^4.0.2"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=14.0.0"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "rollup": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@shikijs/core": {
|
||
+ "version": "4.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.3.0.tgz",
|
||
+ "integrity": "sha512-EooU3i9F6IAE8kEu+AnGf9DFZWkQBZ+hJn3tLVbsH+61mtQiva5biai66fAA6nvFPXkLgvrh7BrR7YcJU83xQQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@shikijs/primitive": "4.3.0",
|
||
+ "@shikijs/types": "4.3.0",
|
||
+ "@shikijs/vscode-textmate": "^10.0.2",
|
||
+ "@types/hast": "^3.0.4",
|
||
+ "hast-util-to-html": "^9.0.5"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@shikijs/engine-javascript": {
|
||
+ "version": "4.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.3.0.tgz",
|
||
+ "integrity": "sha512-hTv/KiFf2tpiqlACPiztGGurEARWIutB8YUhcrA1pUC7VzzwKO+g5crUocrLztrZ5ro5Z4hbXg7bYclETn3gSQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@shikijs/types": "4.3.0",
|
||
+ "@shikijs/vscode-textmate": "^10.0.2",
|
||
+ "oniguruma-to-es": "^4.3.6"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@shikijs/engine-oniguruma": {
|
||
+ "version": "4.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.3.0.tgz",
|
||
+ "integrity": "sha512-1vMdN3gHfnKfLYwecUI2ITJI4RhHt96xEaJumVn7Heb0IlJ8WQMIH0Voak+2j22BpSNKdnOfB/pCTPnPm2gq7A==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@shikijs/types": "4.3.0",
|
||
+ "@shikijs/vscode-textmate": "^10.0.2"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@shikijs/langs": {
|
||
+ "version": "4.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.3.0.tgz",
|
||
+ "integrity": "sha512-rnlqFbBRSys9bT4gl/5rw9RnS0W/I84ZldXPkO7cvlEMoV85TyF/aU01N7/NbSR776RNLjrJKjfFUXJR6wN1Cg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@shikijs/types": "4.3.0"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@shikijs/primitive": {
|
||
+ "version": "4.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.3.0.tgz",
|
||
+ "integrity": "sha512-CPkz64PTa5diRW1ggzMZH9VM/du4RNChYgVtgqrFcgruvIybmCvySv8GkiHSczUHXYuuR8TdKEwFx+UnZMpgdg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@shikijs/types": "4.3.0",
|
||
+ "@shikijs/vscode-textmate": "^10.0.2",
|
||
+ "@types/hast": "^3.0.4"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@shikijs/themes": {
|
||
+ "version": "4.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.3.0.tgz",
|
||
+ "integrity": "sha512-Avgt05YiT+Y3prjIc9lmQxhJzHBcCfR6cjiFW4OyaMBbt2A6trX5rfjUzx+Vj/mE9qpArYjatnqo9XPjQNW/AQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@shikijs/types": "4.3.0"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@shikijs/types": {
|
||
+ "version": "4.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.3.0.tgz",
|
||
+ "integrity": "sha512-oc8b9U2SYvofKZk8e/737nIX0qwf6eV2vHFATeObAu7r+mUVpLs8Re0BmVkIjAWAYgkmG/CzLNo7rzuBzRu/wQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@shikijs/vscode-textmate": "^10.0.2",
|
||
+ "@types/hast": "^3.0.4"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@shikijs/vscode-textmate": {
|
||
+ "version": "10.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
|
||
+ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/@standard-schema/spec": {
|
||
+ "version": "1.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/@tiptap/core": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.27.1.tgz",
|
||
+ "integrity": "sha512-rV6Qn4wmC6BxfF+4mu6bqGWj9vA4oXXhsrpXaJL2uhjxeHAGofjwcHof2X84VYzeyXgdlsGmqKie4TAppVXZUQ==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/pm": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-blockquote": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.27.1.tgz",
|
||
+ "integrity": "sha512-VMF7xJx6qEGiX6DTKNiL31NLqypOcd/4sNjFSe8rb41PwejBJh/nOqVIbBvWkiT6NMGFLxMhj7zJ8/zPo1hXeg==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/core": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-bold": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.27.1.tgz",
|
||
+ "integrity": "sha512-TlC5bsS+pqETTrlz4CZz9RO/cKBYtELGIxwtKeivUn3eNfnOxQbbu4WDsiwIfzRFyd0OMnKl6BPM2KnYEehoEQ==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/core": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-bullet-list": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.27.1.tgz",
|
||
+ "integrity": "sha512-faCUHnRP47o9Zh9VZZX6EX/569udw9Vopm2PgEKPWuKLE2qaS5WBuUVU0iItdJmKUqaWiOZkpoW4jvnDmj0dfg==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/extension-list": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-code": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.27.1.tgz",
|
||
+ "integrity": "sha512-epOUpFfEmBzjvnqvjv2qHX7NAuLo5dlOGV690lWu+sAYMjibuJBeVvAiKPyFCfRCCTUxdbDB3jbaOA1yEcEJ7w==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/core": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-code-block": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.27.1.tgz",
|
||
+ "integrity": "sha512-pHlzmZx2OlHfyQ0yRlT5UL4mGokz947DthZuYefN1OleVqOkHpWBG+2JQwqoNq6bmzMne92zbH32rhcJUEYSjA==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/core": "3.27.1",
|
||
+ "@tiptap/pm": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-document": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.27.1.tgz",
|
||
+ "integrity": "sha512-8FbBTkfnRP4iVaoj+2h3iWa+H0eGDD3yTyVCwrmue/sQTkqUNUoSuAZa3GDG4Sd41xdPwTJxl9nUWGgM1qDCnw==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/core": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-dropcursor": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.27.1.tgz",
|
||
+ "integrity": "sha512-blFf9x9RG0Qr7P3FoAH/033ffa+mMLZn34trVs8Vi0Ppk6FmJAg5HpYFOtmYoeREdNDJ5rHJKV7SoACbOHgskQ==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/extensions": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-gapcursor": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.27.1.tgz",
|
||
+ "integrity": "sha512-QoezN0wdvXIwLQ4ee2ccWDaX3RG0lzgQpIMpMz55oPDhpUVax1+19ApsS53LkcktpS4EbnPL4xO4DaJk0Vp7PQ==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/extensions": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-hard-break": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.27.1.tgz",
|
||
+ "integrity": "sha512-iv/m9hzl6jfSj9Q8UEjAxONvCoUDaP7M9SRCPx3PaLNxA230TTD6RE0Ye4zFJ8ze7ZVoJJMAqg9Qpq1iYg2JOQ==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/core": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-heading": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.27.1.tgz",
|
||
+ "integrity": "sha512-SrC4l1kEIyv9ZXFaI/8LQqU2MyMmjczw7XXsWUQOTN4YXv0JyVgMNR3cI/wz0d2xsTfBdZ1N85Tdng+Ga1t0Sg==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/core": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-horizontal-rule": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.27.1.tgz",
|
||
+ "integrity": "sha512-QlKE7qn5qMnIGVGhXQlvYedvLtNJ9z0dmit5w8vPb8tKzW4Spk6M7N2kruprrDA8GBwHfeR5wmF+njfUm34qxg==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/core": "3.27.1",
|
||
+ "@tiptap/pm": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-image": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.27.1.tgz",
|
||
+ "integrity": "sha512-+JTahgQT+NxiGjduaB3qJVyhU/wh4m3pVkht1Earioku2bm/apj5Lb8rSowa/NJYP3B+oQgV/V4YLw5dtDgBoA==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/core": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-italic": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.27.1.tgz",
|
||
+ "integrity": "sha512-jGGeyn9uRUnNjSTHpbqhiGsp6KaYTSbV09jDXPJI9cDwfV9hpugLvpaCZd0BMBbhU1B1W6kOfX0BE15qX/HQfA==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/core": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-link": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.27.1.tgz",
|
||
+ "integrity": "sha512-/2jBfsxBZUDGJmpZifqRQPz7f1E5qpS1BckTZ39TADzUJX+feKy7RJ3DtQ02+8y6SSMzvP9loGVjrk6zEMTk4g==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "linkifyjs": "^4.3.3"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/core": "3.27.1",
|
||
+ "@tiptap/pm": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-list": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.27.1.tgz",
|
||
+ "integrity": "sha512-c2Upru7lj0/ZV/Ibww6cNz6sUS8m6Dp/9uygFhYcZOd3X8M0xBIEk42c6m6SQehkPziVA8QOgNJz7sMqsbz1OQ==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/core": "3.27.1",
|
||
+ "@tiptap/pm": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-list-item": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.27.1.tgz",
|
||
+ "integrity": "sha512-zwRl01ETfCkWUvtvK5fw9bXtAajMPkvlkE3Cq6JvH3LF7XXJwDtNj5Tj7exacMpCaSZmlNc43vFb2rAYnrnwMA==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/extension-list": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-list-keymap": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.27.1.tgz",
|
||
+ "integrity": "sha512-OIMZNlzPSO8WRd4ic73Fxckzl4N1tesjjLL2XApaNA/uMpO0LoF6WSRPAWv+Z24Wp92ARRJAnRP7iZoI5+Jxig==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/extension-list": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-ordered-list": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.27.1.tgz",
|
||
+ "integrity": "sha512-GYrKqD//9nHJ2r80uXqbDMzRnFpGzbaEQRTSGaO/SH7DvXWFMow8evkOdjQ7PCQO07jNjJo75+A85Jwu3Ov3AA==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/extension-list": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-paragraph": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.27.1.tgz",
|
||
+ "integrity": "sha512-7K7eo1gruOgAsnbK+GCV23AUVUI0cL1bTig8HaPneoFMVbig7vddk8jNLKBWO8TXVbG7TuHdnDN4F98vdtwh5Q==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/core": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-strike": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.27.1.tgz",
|
||
+ "integrity": "sha512-Y3DW1jlSlCNCyMGHP3+3qBNNPS83wuFz4RTYGjZtvRRTCRh7apZme9XRWMq1rN5mJ2Cr7fKocA2/5Bs13KgN6Q==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/core": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-text": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.27.1.tgz",
|
||
+ "integrity": "sha512-6ZwaZwSrDh+KFFv6V1J79oO37yPs7y1bFxvk1/9Ih2rn3Xr5AWz+eMS+n8RpH3djBVVAQpdIAeYQgcn+VCSsTg==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/core": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extension-underline": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.27.1.tgz",
|
||
+ "integrity": "sha512-N889J4nXN/TPfVt8uF9N1A0SY82E90zwc1y26lqOcw6KWNLmQrlhMh/9OD4ikLDbekmFpOBq/UicpHf/6S8hbQ==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/core": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/extensions": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.27.1.tgz",
|
||
+ "integrity": "sha512-1Tdx9faw8k0/83V6X+xCDVhV8yElGt95JxeW3YMkKQJI56QdlPz0xOdJPlMiSGJKinPyVier+x9LJD/YZUZIaw==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@tiptap/core": "3.27.1",
|
||
+ "@tiptap/pm": "3.27.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/pm": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.27.1.tgz",
|
||
+ "integrity": "sha512-Ffjx+vimmBU7zH/KrpXzJid3+pziCe/VL2aexSTP63cyQwKQ65LkFkCKaIsSpFdQQuakVZBGWjCA5RoBV852pw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "prosemirror-changeset": "^2.3.0",
|
||
+ "prosemirror-commands": "^1.6.2",
|
||
+ "prosemirror-dropcursor": "^1.8.1",
|
||
+ "prosemirror-gapcursor": "^1.3.2",
|
||
+ "prosemirror-history": "^1.4.1",
|
||
+ "prosemirror-inputrules": "^1.4.0",
|
||
+ "prosemirror-keymap": "^1.2.3",
|
||
+ "prosemirror-model": "^1.25.7",
|
||
+ "prosemirror-schema-list": "^1.5.0",
|
||
+ "prosemirror-state": "^1.4.4",
|
||
+ "prosemirror-tables": "^1.8.0",
|
||
+ "prosemirror-transform": "^1.12.0",
|
||
+ "prosemirror-view": "^1.41.8"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tiptap/starter-kit": {
|
||
+ "version": "3.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.27.1.tgz",
|
||
+ "integrity": "sha512-vfxRsqW8rCc0k4pzo0ilU3wobVi2wqVj88VZI2SlgZlNnUAkrDGDIAph7CTa9k9fshV+O1ivpEgPC5yC046jow==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@tiptap/core": "^3.27.1",
|
||
+ "@tiptap/extension-blockquote": "^3.27.1",
|
||
+ "@tiptap/extension-bold": "^3.27.1",
|
||
+ "@tiptap/extension-bullet-list": "^3.27.1",
|
||
+ "@tiptap/extension-code": "^3.27.1",
|
||
+ "@tiptap/extension-code-block": "^3.27.1",
|
||
+ "@tiptap/extension-document": "^3.27.1",
|
||
+ "@tiptap/extension-dropcursor": "^3.27.1",
|
||
+ "@tiptap/extension-gapcursor": "^3.27.1",
|
||
+ "@tiptap/extension-hard-break": "^3.27.1",
|
||
+ "@tiptap/extension-heading": "^3.27.1",
|
||
+ "@tiptap/extension-horizontal-rule": "^3.27.1",
|
||
+ "@tiptap/extension-italic": "^3.27.1",
|
||
+ "@tiptap/extension-link": "^3.27.1",
|
||
+ "@tiptap/extension-list": "^3.27.1",
|
||
+ "@tiptap/extension-list-item": "^3.27.1",
|
||
+ "@tiptap/extension-list-keymap": "^3.27.1",
|
||
+ "@tiptap/extension-ordered-list": "^3.27.1",
|
||
+ "@tiptap/extension-paragraph": "^3.27.1",
|
||
+ "@tiptap/extension-strike": "^3.27.1",
|
||
+ "@tiptap/extension-text": "^3.27.1",
|
||
+ "@tiptap/extension-underline": "^3.27.1",
|
||
+ "@tiptap/extensions": "^3.27.1",
|
||
+ "@tiptap/pm": "^3.27.1"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ueberdosis"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@tybys/wasm-util": {
|
||
+ "version": "0.10.3",
|
||
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
|
||
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "dependencies": {
|
||
+ "tslib": "^2.4.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@types/bcryptjs": {
|
||
+ "version": "2.4.6",
|
||
+ "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
||
+ "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/@types/better-sqlite3": {
|
||
+ "version": "7.6.13",
|
||
+ "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
||
+ "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/node": "*"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@types/chai": {
|
||
+ "version": "5.2.3",
|
||
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/deep-eql": "*",
|
||
+ "assertion-error": "^2.0.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@types/deep-eql": {
|
||
+ "version": "4.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/@types/estree": {
|
||
+ "version": "1.0.9",
|
||
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/@types/estree-jsx": {
|
||
+ "version": "1.0.5",
|
||
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
|
||
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/estree": "*"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@types/hast": {
|
||
+ "version": "3.0.4",
|
||
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/unist": "*"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@types/mdast": {
|
||
+ "version": "4.0.4",
|
||
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
||
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/unist": "*"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@types/nlcst": {
|
||
+ "version": "2.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz",
|
||
+ "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/unist": "*"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@types/node": {
|
||
+ "version": "24.13.2",
|
||
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
|
||
+ "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "undici-types": "~7.18.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@types/nodemailer": {
|
||
+ "version": "8.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.1.tgz",
|
||
+ "integrity": "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/node": "*"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@types/sanitize-html": {
|
||
+ "version": "2.16.1",
|
||
+ "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.16.1.tgz",
|
||
+ "integrity": "sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "htmlparser2": "^10.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@types/sax": {
|
||
+ "version": "1.2.7",
|
||
+ "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz",
|
||
+ "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/node": "*"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@types/unist": {
|
||
+ "version": "3.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/@ungap/structured-clone": {
|
||
+ "version": "1.3.2",
|
||
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz",
|
||
+ "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==",
|
||
+ "license": "ISC"
|
||
+ },
|
||
+ "node_modules/@vitest/expect": {
|
||
+ "version": "4.1.9",
|
||
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
|
||
+ "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@standard-schema/spec": "^1.1.0",
|
||
+ "@types/chai": "^5.2.2",
|
||
+ "@vitest/spy": "4.1.9",
|
||
+ "@vitest/utils": "4.1.9",
|
||
+ "chai": "^6.2.2",
|
||
+ "tinyrainbow": "^3.1.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/vitest"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@vitest/mocker": {
|
||
+ "version": "4.1.9",
|
||
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz",
|
||
+ "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@vitest/spy": "4.1.9",
|
||
+ "estree-walker": "^3.0.3",
|
||
+ "magic-string": "^0.30.21"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/vitest"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "msw": "^2.4.9",
|
||
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "msw": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "vite": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@vitest/mocker/node_modules/estree-walker": {
|
||
+ "version": "3.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/estree": "^1.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@vitest/pretty-format": {
|
||
+ "version": "4.1.9",
|
||
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz",
|
||
+ "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "tinyrainbow": "^3.1.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/vitest"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@vitest/runner": {
|
||
+ "version": "4.1.9",
|
||
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz",
|
||
+ "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@vitest/utils": "4.1.9",
|
||
+ "pathe": "^2.0.3"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/vitest"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@vitest/snapshot": {
|
||
+ "version": "4.1.9",
|
||
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz",
|
||
+ "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@vitest/pretty-format": "4.1.9",
|
||
+ "@vitest/utils": "4.1.9",
|
||
+ "magic-string": "^0.30.21",
|
||
+ "pathe": "^2.0.3"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/vitest"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@vitest/spy": {
|
||
+ "version": "4.1.9",
|
||
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz",
|
||
+ "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/vitest"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@vitest/utils": {
|
||
+ "version": "4.1.9",
|
||
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz",
|
||
+ "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@vitest/pretty-format": "4.1.9",
|
||
+ "convert-source-map": "^2.0.0",
|
||
+ "tinyrainbow": "^3.1.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/vitest"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@volar/kit": {
|
||
+ "version": "2.4.28",
|
||
+ "resolved": "https://registry.npmjs.org/@volar/kit/-/kit-2.4.28.tgz",
|
||
+ "integrity": "sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@volar/language-service": "2.4.28",
|
||
+ "@volar/typescript": "2.4.28",
|
||
+ "typesafe-path": "^0.2.2",
|
||
+ "vscode-languageserver-textdocument": "^1.0.11",
|
||
+ "vscode-uri": "^3.0.8"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "typescript": "*"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@volar/language-core": {
|
||
+ "version": "2.4.28",
|
||
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
|
||
+ "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@volar/source-map": "2.4.28"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@volar/language-server": {
|
||
+ "version": "2.4.28",
|
||
+ "resolved": "https://registry.npmjs.org/@volar/language-server/-/language-server-2.4.28.tgz",
|
||
+ "integrity": "sha512-NqcLnE5gERKuS4PUFwlhMxf6vqYo7hXtbMFbViXcbVkbZ905AIVWhnSo0ZNBC2V127H1/2zP7RvVOVnyITFfBw==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@volar/language-core": "2.4.28",
|
||
+ "@volar/language-service": "2.4.28",
|
||
+ "@volar/typescript": "2.4.28",
|
||
+ "path-browserify": "^1.0.1",
|
||
+ "request-light": "^0.7.0",
|
||
+ "vscode-languageserver": "^9.0.1",
|
||
+ "vscode-languageserver-protocol": "^3.17.5",
|
||
+ "vscode-languageserver-textdocument": "^1.0.11",
|
||
+ "vscode-uri": "^3.0.8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@volar/language-service": {
|
||
+ "version": "2.4.28",
|
||
+ "resolved": "https://registry.npmjs.org/@volar/language-service/-/language-service-2.4.28.tgz",
|
||
+ "integrity": "sha512-Rh/wYCZJrI5vCwMk9xyw/Z+MsWxlJY1rmMZPsxUoJKfzIRjS/NF1NmnuEcrMbEVGja00aVpCsInJfixQTMdvLw==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@volar/language-core": "2.4.28",
|
||
+ "vscode-languageserver-protocol": "^3.17.5",
|
||
+ "vscode-languageserver-textdocument": "^1.0.11",
|
||
+ "vscode-uri": "^3.0.8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@volar/source-map": {
|
||
+ "version": "2.4.28",
|
||
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
|
||
+ "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/@volar/typescript": {
|
||
+ "version": "2.4.28",
|
||
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
|
||
+ "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@volar/language-core": "2.4.28",
|
||
+ "path-browserify": "^1.0.1",
|
||
+ "vscode-uri": "^3.0.8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@vscode/emmet-helper": {
|
||
+ "version": "2.11.0",
|
||
+ "resolved": "https://registry.npmjs.org/@vscode/emmet-helper/-/emmet-helper-2.11.0.tgz",
|
||
+ "integrity": "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "emmet": "^2.4.3",
|
||
+ "jsonc-parser": "^2.3.0",
|
||
+ "vscode-languageserver-textdocument": "^1.0.1",
|
||
+ "vscode-languageserver-types": "^3.15.1",
|
||
+ "vscode-uri": "^3.0.8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/@vscode/emmet-helper/node_modules/jsonc-parser": {
|
||
+ "version": "2.3.1",
|
||
+ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz",
|
||
+ "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/@vscode/l10n": {
|
||
+ "version": "0.0.18",
|
||
+ "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz",
|
||
+ "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/ajv": {
|
||
+ "version": "8.20.0",
|
||
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "fast-deep-equal": "^3.1.3",
|
||
+ "fast-uri": "^3.0.1",
|
||
+ "json-schema-traverse": "^1.0.0",
|
||
+ "require-from-string": "^2.0.2"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/epoberezkin"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/ajv-draft-04": {
|
||
+ "version": "1.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
|
||
+ "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "peerDependencies": {
|
||
+ "ajv": "^8.5.0"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "ajv": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/am-i-vibing": {
|
||
+ "version": "0.4.0",
|
||
+ "resolved": "https://registry.npmjs.org/am-i-vibing/-/am-i-vibing-0.4.0.tgz",
|
||
+ "integrity": "sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "process-ancestry": "^0.1.0"
|
||
+ },
|
||
+ "bin": {
|
||
+ "am-i-vibing": "dist/cli.mjs"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/ansi-regex": {
|
||
+ "version": "5.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/ansi-styles": {
|
||
+ "version": "4.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "color-convert": "^2.0.1"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=8"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/anymatch": {
|
||
+ "version": "3.1.3",
|
||
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||
+ "license": "ISC",
|
||
+ "dependencies": {
|
||
+ "normalize-path": "^3.0.0",
|
||
+ "picomatch": "^2.0.4"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">= 8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/anymatch/node_modules/picomatch": {
|
||
+ "version": "2.3.2",
|
||
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=8.6"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/jonschlinkert"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/arg": {
|
||
+ "version": "5.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/argparse": {
|
||
+ "version": "2.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||
+ "license": "Python-2.0"
|
||
+ },
|
||
+ "node_modules/aria-query": {
|
||
+ "version": "5.3.2",
|
||
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||
+ "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
||
+ "license": "Apache-2.0",
|
||
+ "engines": {
|
||
+ "node": ">= 0.4"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/assertion-error": {
|
||
+ "version": "2.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=12"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/astro": {
|
||
+ "version": "7.0.5",
|
||
+ "resolved": "https://registry.npmjs.org/astro/-/astro-7.0.5.tgz",
|
||
+ "integrity": "sha512-KR/zBBgU6I+F5vDoTsuTbXbBmI565CToDOgPr0pPiL2Hgrvx4CV8Cg2wtT6VqqyOS7BIfBkdagKD+40SojGl7w==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@astrojs/compiler-rs": "^0.3.0",
|
||
+ "@astrojs/internal-helpers": "0.10.0",
|
||
+ "@astrojs/markdown-satteri": "0.3.2",
|
||
+ "@astrojs/telemetry": "3.3.2",
|
||
+ "@capsizecss/unpack": "^4.0.0",
|
||
+ "@clack/prompts": "^1.1.0",
|
||
+ "@oslojs/encoding": "^1.1.0",
|
||
+ "@rollup/pluginutils": "^5.3.0",
|
||
+ "am-i-vibing": "^0.4.0",
|
||
+ "aria-query": "^5.3.2",
|
||
+ "axobject-query": "^4.1.0",
|
||
+ "ci-info": "^4.4.0",
|
||
+ "clsx": "^2.1.1",
|
||
+ "common-ancestor-path": "^2.0.0",
|
||
+ "cookie": "^1.1.1",
|
||
+ "devalue": "^5.8.1",
|
||
+ "diff": "^8.0.3",
|
||
+ "dset": "^3.1.4",
|
||
+ "es-module-lexer": "^2.0.0",
|
||
+ "esbuild": "^0.28.0",
|
||
+ "flattie": "^1.1.1",
|
||
+ "fontace": "~0.4.1",
|
||
+ "get-tsconfig": "5.0.0-beta.4",
|
||
+ "github-slugger": "^2.0.0",
|
||
+ "html-escaper": "3.0.3",
|
||
+ "http-cache-semantics": "^4.2.0",
|
||
+ "js-yaml": "^4.1.1",
|
||
+ "jsonc-parser": "^3.3.1",
|
||
+ "magic-string": "^0.30.21",
|
||
+ "magicast": "^0.5.2",
|
||
+ "mrmime": "^2.0.1",
|
||
+ "neotraverse": "^0.6.18",
|
||
+ "obug": "^2.1.1",
|
||
+ "p-limit": "^7.3.0",
|
||
+ "p-queue": "^9.1.0",
|
||
+ "package-manager-detector": "^1.6.0",
|
||
+ "piccolore": "^0.1.3",
|
||
+ "picomatch": "^4.0.4",
|
||
+ "semver": "^7.7.4",
|
||
+ "shiki": "^4.0.2",
|
||
+ "smol-toml": "^1.6.0",
|
||
+ "svgo": "^4.0.1",
|
||
+ "tinyclip": "^0.1.12",
|
||
+ "tinyexec": "^1.0.4",
|
||
+ "tinyglobby": "^0.2.15",
|
||
+ "ultrahtml": "^1.6.0",
|
||
+ "unifont": "~0.7.4",
|
||
+ "unstorage": "^1.17.5",
|
||
+ "vite": "^8.0.13",
|
||
+ "vitefu": "^1.1.2",
|
||
+ "xxhash-wasm": "^1.1.0",
|
||
+ "yargs-parser": "^22.0.0",
|
||
+ "zod": "^4.3.6"
|
||
+ },
|
||
+ "bin": {
|
||
+ "astro": "bin/astro.mjs"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=22.12.0",
|
||
+ "npm": ">=9.6.5",
|
||
+ "pnpm": ">=7.1.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/astrodotbuild"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "sharp": "^0.34.0 || ^0.35.0"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@astrojs/markdown-remark": "7.2.0"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "@astrojs/markdown-remark": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/axobject-query": {
|
||
+ "version": "4.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||
+ "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
||
+ "license": "Apache-2.0",
|
||
+ "engines": {
|
||
+ "node": ">= 0.4"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/bail": {
|
||
+ "version": "2.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
||
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/wooorm"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/base64-js": {
|
||
+ "version": "1.5.1",
|
||
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||
+ "funding": [
|
||
+ {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/feross"
|
||
+ },
|
||
+ {
|
||
+ "type": "patreon",
|
||
+ "url": "https://www.patreon.com/feross"
|
||
+ },
|
||
+ {
|
||
+ "type": "consulting",
|
||
+ "url": "https://feross.org/support"
|
||
+ }
|
||
+ ],
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/bcryptjs": {
|
||
+ "version": "3.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||
+ "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||
+ "license": "BSD-3-Clause",
|
||
+ "bin": {
|
||
+ "bcrypt": "bin/bcrypt"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/better-sqlite3": {
|
||
+ "version": "12.11.1",
|
||
+ "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz",
|
||
+ "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==",
|
||
+ "hasInstallScript": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "bindings": "^1.5.0",
|
||
+ "prebuild-install": "^7.1.1"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/bindings": {
|
||
+ "version": "1.5.0",
|
||
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "file-uri-to-path": "1.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/bl": {
|
||
+ "version": "4.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "buffer": "^5.5.0",
|
||
+ "inherits": "^2.0.4",
|
||
+ "readable-stream": "^3.4.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/boolbase": {
|
||
+ "version": "1.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||
+ "license": "ISC"
|
||
+ },
|
||
+ "node_modules/buffer": {
|
||
+ "version": "5.7.1",
|
||
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||
+ "funding": [
|
||
+ {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/feross"
|
||
+ },
|
||
+ {
|
||
+ "type": "patreon",
|
||
+ "url": "https://www.patreon.com/feross"
|
||
+ },
|
||
+ {
|
||
+ "type": "consulting",
|
||
+ "url": "https://feross.org/support"
|
||
+ }
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "base64-js": "^1.3.1",
|
||
+ "ieee754": "^1.1.13"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/ccount": {
|
||
+ "version": "2.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
||
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/wooorm"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/chai": {
|
||
+ "version": "6.2.2",
|
||
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/character-entities-html4": {
|
||
+ "version": "2.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
|
||
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/wooorm"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/character-entities-legacy": {
|
||
+ "version": "3.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
|
||
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/wooorm"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/chokidar": {
|
||
+ "version": "5.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||
+ "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "readdirp": "^5.0.0"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">= 20.19.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://paulmillr.com/funding/"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/chownr": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||
+ "license": "ISC"
|
||
+ },
|
||
+ "node_modules/ci-info": {
|
||
+ "version": "4.4.0",
|
||
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz",
|
||
+ "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==",
|
||
+ "funding": [
|
||
+ {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/sibiraj-s"
|
||
+ }
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/cliui": {
|
||
+ "version": "8.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||
+ "dev": true,
|
||
+ "license": "ISC",
|
||
+ "dependencies": {
|
||
+ "string-width": "^4.2.0",
|
||
+ "strip-ansi": "^6.0.1",
|
||
+ "wrap-ansi": "^7.0.0"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=12"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/clsx": {
|
||
+ "version": "2.1.1",
|
||
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=6"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/color-convert": {
|
||
+ "version": "2.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "color-name": "~1.1.4"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=7.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/color-name": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/comma-separated-tokens": {
|
||
+ "version": "2.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/wooorm"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/commander": {
|
||
+ "version": "11.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
|
||
+ "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=16"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/common-ancestor-path": {
|
||
+ "version": "2.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz",
|
||
+ "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==",
|
||
+ "license": "BlueOak-1.0.0",
|
||
+ "engines": {
|
||
+ "node": ">= 18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/convert-source-map": {
|
||
+ "version": "2.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/cookie": {
|
||
+ "version": "1.1.1",
|
||
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/express"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/cookie-es": {
|
||
+ "version": "1.2.3",
|
||
+ "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz",
|
||
+ "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/crossws": {
|
||
+ "version": "0.3.5",
|
||
+ "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz",
|
||
+ "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "uncrypto": "^0.1.3"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/css-select": {
|
||
+ "version": "5.2.2",
|
||
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||
+ "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||
+ "license": "BSD-2-Clause",
|
||
+ "dependencies": {
|
||
+ "boolbase": "^1.0.0",
|
||
+ "css-what": "^6.1.0",
|
||
+ "domhandler": "^5.0.2",
|
||
+ "domutils": "^3.0.1",
|
||
+ "nth-check": "^2.0.1"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/fb55"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/css-tree": {
|
||
+ "version": "3.2.1",
|
||
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "mdn-data": "2.27.1",
|
||
+ "source-map-js": "^1.2.1"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/css-what": {
|
||
+ "version": "6.2.2",
|
||
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||
+ "license": "BSD-2-Clause",
|
||
+ "engines": {
|
||
+ "node": ">= 6"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/fb55"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/csso": {
|
||
+ "version": "5.0.5",
|
||
+ "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
|
||
+ "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "css-tree": "~2.2.0"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
|
||
+ "npm": ">=7.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/csso/node_modules/css-tree": {
|
||
+ "version": "2.2.1",
|
||
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
|
||
+ "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "mdn-data": "2.0.28",
|
||
+ "source-map-js": "^1.0.1"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
|
||
+ "npm": ">=7.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/csso/node_modules/mdn-data": {
|
||
+ "version": "2.0.28",
|
||
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
|
||
+ "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
|
||
+ "license": "CC0-1.0"
|
||
+ },
|
||
+ "node_modules/dayjs": {
|
||
+ "version": "1.11.21",
|
||
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
|
||
+ "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/debug": {
|
||
+ "version": "4.4.3",
|
||
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "ms": "^2.1.3"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=6.0"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "supports-color": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/decompress-response": {
|
||
+ "version": "6.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "mimic-response": "^3.1.0"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=10"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/sindresorhus"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/deep-extend": {
|
||
+ "version": "0.6.0",
|
||
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=4.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/deepmerge": {
|
||
+ "version": "4.3.1",
|
||
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=0.10.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/defu": {
|
||
+ "version": "6.1.7",
|
||
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
|
||
+ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/depd": {
|
||
+ "version": "2.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">= 0.8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/dequal": {
|
||
+ "version": "2.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=6"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/destr": {
|
||
+ "version": "2.0.5",
|
||
+ "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
||
+ "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/detect-libc": {
|
||
+ "version": "2.1.2",
|
||
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||
+ "license": "Apache-2.0",
|
||
+ "engines": {
|
||
+ "node": ">=8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/devalue": {
|
||
+ "version": "5.8.1",
|
||
+ "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz",
|
||
+ "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/devlop": {
|
||
+ "version": "1.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
|
||
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "dequal": "^2.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/wooorm"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/diff": {
|
||
+ "version": "8.0.4",
|
||
+ "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz",
|
||
+ "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==",
|
||
+ "license": "BSD-3-Clause",
|
||
+ "engines": {
|
||
+ "node": ">=0.3.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/dom-serializer": {
|
||
+ "version": "2.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "domelementtype": "^2.3.0",
|
||
+ "domhandler": "^5.0.2",
|
||
+ "entities": "^4.2.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/dom-serializer/node_modules/entities": {
|
||
+ "version": "4.5.0",
|
||
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||
+ "license": "BSD-2-Clause",
|
||
+ "engines": {
|
||
+ "node": ">=0.12"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/fb55/entities?sponsor=1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/domelementtype": {
|
||
+ "version": "2.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||
+ "funding": [
|
||
+ {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/fb55"
|
||
+ }
|
||
+ ],
|
||
+ "license": "BSD-2-Clause"
|
||
+ },
|
||
+ "node_modules/domhandler": {
|
||
+ "version": "5.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||
+ "license": "BSD-2-Clause",
|
||
+ "dependencies": {
|
||
+ "domelementtype": "^2.3.0"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">= 4"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/domutils": {
|
||
+ "version": "3.2.2",
|
||
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||
+ "license": "BSD-2-Clause",
|
||
+ "dependencies": {
|
||
+ "dom-serializer": "^2.0.0",
|
||
+ "domelementtype": "^2.3.0",
|
||
+ "domhandler": "^5.0.3"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/fb55/domutils?sponsor=1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/dset": {
|
||
+ "version": "3.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz",
|
||
+ "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=4"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/ee-first": {
|
||
+ "version": "1.1.1",
|
||
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/emmet": {
|
||
+ "version": "2.4.11",
|
||
+ "resolved": "https://registry.npmjs.org/emmet/-/emmet-2.4.11.tgz",
|
||
+ "integrity": "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "workspaces": [
|
||
+ "./packages/scanner",
|
||
+ "./packages/abbreviation",
|
||
+ "./packages/css-abbreviation",
|
||
+ "./"
|
||
+ ],
|
||
+ "dependencies": {
|
||
+ "@emmetio/abbreviation": "^2.3.3",
|
||
+ "@emmetio/css-abbreviation": "^2.1.8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/emoji-regex": {
|
||
+ "version": "8.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/encodeurl": {
|
||
+ "version": "2.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">= 0.8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/end-of-stream": {
|
||
+ "version": "1.4.5",
|
||
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "once": "^1.4.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/entities": {
|
||
+ "version": "7.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||
+ "license": "BSD-2-Clause",
|
||
+ "engines": {
|
||
+ "node": ">=0.12"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/fb55/entities?sponsor=1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/es-module-lexer": {
|
||
+ "version": "2.2.0",
|
||
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.2.0.tgz",
|
||
+ "integrity": "sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/esbuild": {
|
||
+ "version": "0.28.1",
|
||
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||
+ "hasInstallScript": true,
|
||
+ "license": "MIT",
|
||
+ "bin": {
|
||
+ "esbuild": "bin/esbuild"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "@esbuild/aix-ppc64": "0.28.1",
|
||
+ "@esbuild/android-arm": "0.28.1",
|
||
+ "@esbuild/android-arm64": "0.28.1",
|
||
+ "@esbuild/android-x64": "0.28.1",
|
||
+ "@esbuild/darwin-arm64": "0.28.1",
|
||
+ "@esbuild/darwin-x64": "0.28.1",
|
||
+ "@esbuild/freebsd-arm64": "0.28.1",
|
||
+ "@esbuild/freebsd-x64": "0.28.1",
|
||
+ "@esbuild/linux-arm": "0.28.1",
|
||
+ "@esbuild/linux-arm64": "0.28.1",
|
||
+ "@esbuild/linux-ia32": "0.28.1",
|
||
+ "@esbuild/linux-loong64": "0.28.1",
|
||
+ "@esbuild/linux-mips64el": "0.28.1",
|
||
+ "@esbuild/linux-ppc64": "0.28.1",
|
||
+ "@esbuild/linux-riscv64": "0.28.1",
|
||
+ "@esbuild/linux-s390x": "0.28.1",
|
||
+ "@esbuild/linux-x64": "0.28.1",
|
||
+ "@esbuild/netbsd-arm64": "0.28.1",
|
||
+ "@esbuild/netbsd-x64": "0.28.1",
|
||
+ "@esbuild/openbsd-arm64": "0.28.1",
|
||
+ "@esbuild/openbsd-x64": "0.28.1",
|
||
+ "@esbuild/openharmony-arm64": "0.28.1",
|
||
+ "@esbuild/sunos-x64": "0.28.1",
|
||
+ "@esbuild/win32-arm64": "0.28.1",
|
||
+ "@esbuild/win32-ia32": "0.28.1",
|
||
+ "@esbuild/win32-x64": "0.28.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/escalade": {
|
||
+ "version": "3.2.0",
|
||
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=6"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/escape-html": {
|
||
+ "version": "1.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/escape-string-regexp": {
|
||
+ "version": "4.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=10"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/sindresorhus"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/estree-walker": {
|
||
+ "version": "2.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/etag": {
|
||
+ "version": "1.8.1",
|
||
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">= 0.6"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/eventemitter3": {
|
||
+ "version": "5.0.4",
|
||
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/expand-template": {
|
||
+ "version": "2.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||
+ "license": "(MIT OR WTFPL)",
|
||
+ "engines": {
|
||
+ "node": ">=6"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/expect-type": {
|
||
+ "version": "1.4.0",
|
||
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
|
||
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
|
||
+ "dev": true,
|
||
+ "license": "Apache-2.0",
|
||
+ "engines": {
|
||
+ "node": ">=12.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/extend": {
|
||
+ "version": "3.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/fast-deep-equal": {
|
||
+ "version": "3.1.3",
|
||
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/fast-string-truncated-width": {
|
||
+ "version": "3.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz",
|
||
+ "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/fast-string-width": {
|
||
+ "version": "3.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz",
|
||
+ "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "fast-string-truncated-width": "^3.0.2"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/fast-uri": {
|
||
+ "version": "3.1.3",
|
||
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
|
||
+ "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
|
||
+ "dev": true,
|
||
+ "funding": [
|
||
+ {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/fastify"
|
||
+ },
|
||
+ {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/fastify"
|
||
+ }
|
||
+ ],
|
||
+ "license": "BSD-3-Clause"
|
||
+ },
|
||
+ "node_modules/fast-wrap-ansi": {
|
||
+ "version": "0.2.2",
|
||
+ "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz",
|
||
+ "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "fast-string-width": "^3.0.2"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/fdir": {
|
||
+ "version": "6.5.0",
|
||
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=12.0.0"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "picomatch": "^3 || ^4"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "picomatch": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/file-uri-to-path": {
|
||
+ "version": "1.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/flattie": {
|
||
+ "version": "1.1.1",
|
||
+ "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz",
|
||
+ "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/fontace": {
|
||
+ "version": "0.4.1",
|
||
+ "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.1.tgz",
|
||
+ "integrity": "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "fontkitten": "^1.0.2"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/fontkitten": {
|
||
+ "version": "1.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.3.tgz",
|
||
+ "integrity": "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "tiny-inflate": "^1.0.3"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/fresh": {
|
||
+ "version": "2.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">= 0.8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/fs-constants": {
|
||
+ "version": "1.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/fsevents": {
|
||
+ "version": "2.3.3",
|
||
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||
+ "hasInstallScript": true,
|
||
+ "license": "MIT",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "darwin"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/get-caller-file": {
|
||
+ "version": "2.0.5",
|
||
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||
+ "dev": true,
|
||
+ "license": "ISC",
|
||
+ "engines": {
|
||
+ "node": "6.* || 8.* || >= 10.*"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/get-tsconfig": {
|
||
+ "version": "5.0.0-beta.4",
|
||
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.4.tgz",
|
||
+ "integrity": "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "resolve-pkg-maps": "^1.0.0"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20.20.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/github-from-package": {
|
||
+ "version": "0.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/github-slugger": {
|
||
+ "version": "2.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
|
||
+ "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==",
|
||
+ "license": "ISC"
|
||
+ },
|
||
+ "node_modules/h3": {
|
||
+ "version": "1.15.11",
|
||
+ "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz",
|
||
+ "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "cookie-es": "^1.2.3",
|
||
+ "crossws": "^0.3.5",
|
||
+ "defu": "^6.1.6",
|
||
+ "destr": "^2.0.5",
|
||
+ "iron-webcrypto": "^1.2.1",
|
||
+ "node-mock-http": "^1.0.4",
|
||
+ "radix3": "^1.1.2",
|
||
+ "ufo": "^1.6.3",
|
||
+ "uncrypto": "^0.1.3"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/hast-util-to-html": {
|
||
+ "version": "9.0.5",
|
||
+ "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
|
||
+ "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/hast": "^3.0.0",
|
||
+ "@types/unist": "^3.0.0",
|
||
+ "ccount": "^2.0.0",
|
||
+ "comma-separated-tokens": "^2.0.0",
|
||
+ "hast-util-whitespace": "^3.0.0",
|
||
+ "html-void-elements": "^3.0.0",
|
||
+ "mdast-util-to-hast": "^13.0.0",
|
||
+ "property-information": "^7.0.0",
|
||
+ "space-separated-tokens": "^2.0.0",
|
||
+ "stringify-entities": "^4.0.0",
|
||
+ "zwitch": "^2.0.4"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/hast-util-whitespace": {
|
||
+ "version": "3.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
|
||
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/hast": "^3.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/html-escaper": {
|
||
+ "version": "3.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
|
||
+ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/html-void-elements": {
|
||
+ "version": "3.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
||
+ "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/wooorm"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/htmlparser2": {
|
||
+ "version": "10.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
|
||
+ "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
|
||
+ "funding": [
|
||
+ "https://github.com/fb55/htmlparser2?sponsor=1",
|
||
+ {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/fb55"
|
||
+ }
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "domelementtype": "^2.3.0",
|
||
+ "domhandler": "^5.0.3",
|
||
+ "domutils": "^3.2.2",
|
||
+ "entities": "^7.0.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/http-cache-semantics": {
|
||
+ "version": "4.2.0",
|
||
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||
+ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
|
||
+ "license": "BSD-2-Clause"
|
||
+ },
|
||
+ "node_modules/http-errors": {
|
||
+ "version": "2.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "depd": "~2.0.0",
|
||
+ "inherits": "~2.0.4",
|
||
+ "setprototypeof": "~1.2.0",
|
||
+ "statuses": "~2.0.2",
|
||
+ "toidentifier": "~1.0.1"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">= 0.8"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/express"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/ieee754": {
|
||
+ "version": "1.2.1",
|
||
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||
+ "funding": [
|
||
+ {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/feross"
|
||
+ },
|
||
+ {
|
||
+ "type": "patreon",
|
||
+ "url": "https://www.patreon.com/feross"
|
||
+ },
|
||
+ {
|
||
+ "type": "consulting",
|
||
+ "url": "https://feross.org/support"
|
||
+ }
|
||
+ ],
|
||
+ "license": "BSD-3-Clause"
|
||
+ },
|
||
+ "node_modules/inherits": {
|
||
+ "version": "2.0.4",
|
||
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||
+ "license": "ISC"
|
||
+ },
|
||
+ "node_modules/ini": {
|
||
+ "version": "1.3.8",
|
||
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||
+ "license": "ISC"
|
||
+ },
|
||
+ "node_modules/iron-webcrypto": {
|
||
+ "version": "1.2.1",
|
||
+ "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
|
||
+ "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/brc-dd"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/is-docker": {
|
||
+ "version": "4.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-4.0.0.tgz",
|
||
+ "integrity": "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==",
|
||
+ "license": "MIT",
|
||
+ "bin": {
|
||
+ "is-docker": "cli.js"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/sindresorhus"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/is-fullwidth-code-point": {
|
||
+ "version": "3.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/is-inside-container": {
|
||
+ "version": "1.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
|
||
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "is-docker": "^3.0.0"
|
||
+ },
|
||
+ "bin": {
|
||
+ "is-inside-container": "cli.js"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=14.16"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/sindresorhus"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/is-inside-container/node_modules/is-docker": {
|
||
+ "version": "3.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
|
||
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
|
||
+ "license": "MIT",
|
||
+ "bin": {
|
||
+ "is-docker": "cli.js"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/sindresorhus"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/is-plain-obj": {
|
||
+ "version": "4.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
||
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=12"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/sindresorhus"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/is-plain-object": {
|
||
+ "version": "5.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=0.10.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/is-wsl": {
|
||
+ "version": "3.1.1",
|
||
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
|
||
+ "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "is-inside-container": "^1.0.0"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=16"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/sindresorhus"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/js-yaml": {
|
||
+ "version": "4.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||
+ "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||
+ "funding": [
|
||
+ {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/puzrin"
|
||
+ },
|
||
+ {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/nodeca"
|
||
+ }
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "argparse": "^2.0.1"
|
||
+ },
|
||
+ "bin": {
|
||
+ "js-yaml": "bin/js-yaml.js"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/json-schema-traverse": {
|
||
+ "version": "1.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/jsonc-parser": {
|
||
+ "version": "3.3.1",
|
||
+ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
|
||
+ "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/kleur": {
|
||
+ "version": "4.1.5",
|
||
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=6"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/launder": {
|
||
+ "version": "1.7.1",
|
||
+ "resolved": "https://registry.npmjs.org/launder/-/launder-1.7.1.tgz",
|
||
+ "integrity": "sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "dayjs": "^1.11.7"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/lightningcss": {
|
||
+ "version": "1.32.0",
|
||
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||
+ "license": "MPL-2.0",
|
||
+ "dependencies": {
|
||
+ "detect-libc": "^2.0.3"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">= 12.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/parcel"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "lightningcss-android-arm64": "1.32.0",
|
||
+ "lightningcss-darwin-arm64": "1.32.0",
|
||
+ "lightningcss-darwin-x64": "1.32.0",
|
||
+ "lightningcss-freebsd-x64": "1.32.0",
|
||
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||
+ "lightningcss-linux-arm64-gnu": "1.32.0",
|
||
+ "lightningcss-linux-arm64-musl": "1.32.0",
|
||
+ "lightningcss-linux-x64-gnu": "1.32.0",
|
||
+ "lightningcss-linux-x64-musl": "1.32.0",
|
||
+ "lightningcss-win32-arm64-msvc": "1.32.0",
|
||
+ "lightningcss-win32-x64-msvc": "1.32.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/lightningcss-android-arm64": {
|
||
+ "version": "1.32.0",
|
||
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
||
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MPL-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "android"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">= 12.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/parcel"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/lightningcss-darwin-arm64": {
|
||
+ "version": "1.32.0",
|
||
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
||
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MPL-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "darwin"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">= 12.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/parcel"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/lightningcss-darwin-x64": {
|
||
+ "version": "1.32.0",
|
||
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
||
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MPL-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "darwin"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">= 12.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/parcel"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/lightningcss-freebsd-x64": {
|
||
+ "version": "1.32.0",
|
||
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
||
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MPL-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "freebsd"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">= 12.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/parcel"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
|
||
+ "version": "1.32.0",
|
||
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
||
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
||
+ "cpu": [
|
||
+ "arm"
|
||
+ ],
|
||
+ "license": "MPL-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">= 12.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/parcel"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/lightningcss-linux-arm64-gnu": {
|
||
+ "version": "1.32.0",
|
||
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
||
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MPL-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">= 12.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/parcel"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/lightningcss-linux-arm64-musl": {
|
||
+ "version": "1.32.0",
|
||
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
||
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MPL-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">= 12.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/parcel"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/lightningcss-linux-x64-gnu": {
|
||
+ "version": "1.32.0",
|
||
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MPL-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">= 12.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/parcel"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/lightningcss-linux-x64-musl": {
|
||
+ "version": "1.32.0",
|
||
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MPL-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "linux"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">= 12.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/parcel"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/lightningcss-win32-arm64-msvc": {
|
||
+ "version": "1.32.0",
|
||
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
||
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
||
+ "cpu": [
|
||
+ "arm64"
|
||
+ ],
|
||
+ "license": "MPL-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "win32"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">= 12.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/parcel"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/lightningcss-win32-x64-msvc": {
|
||
+ "version": "1.32.0",
|
||
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||
+ "cpu": [
|
||
+ "x64"
|
||
+ ],
|
||
+ "license": "MPL-2.0",
|
||
+ "optional": true,
|
||
+ "os": [
|
||
+ "win32"
|
||
+ ],
|
||
+ "engines": {
|
||
+ "node": ">= 12.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/parcel"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/linkifyjs": {
|
||
+ "version": "4.3.3",
|
||
+ "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz",
|
||
+ "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/lru-cache": {
|
||
+ "version": "11.5.1",
|
||
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
|
||
+ "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
|
||
+ "license": "BlueOak-1.0.0",
|
||
+ "engines": {
|
||
+ "node": "20 || >=22"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/magic-string": {
|
||
+ "version": "0.30.21",
|
||
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@jridgewell/sourcemap-codec": "^1.5.5"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/magicast": {
|
||
+ "version": "0.5.3",
|
||
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz",
|
||
+ "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@babel/parser": "^7.29.3",
|
||
+ "@babel/types": "^7.29.0",
|
||
+ "source-map-js": "^1.2.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/mdast-util-to-hast": {
|
||
+ "version": "13.2.1",
|
||
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
|
||
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/hast": "^3.0.0",
|
||
+ "@types/mdast": "^4.0.0",
|
||
+ "@ungap/structured-clone": "^1.0.0",
|
||
+ "devlop": "^1.0.0",
|
||
+ "micromark-util-sanitize-uri": "^2.0.0",
|
||
+ "trim-lines": "^3.0.0",
|
||
+ "unist-util-position": "^5.0.0",
|
||
+ "unist-util-visit": "^5.0.0",
|
||
+ "vfile": "^6.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/mdn-data": {
|
||
+ "version": "2.27.1",
|
||
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
|
||
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
||
+ "license": "CC0-1.0"
|
||
+ },
|
||
+ "node_modules/micromark-util-character": {
|
||
+ "version": "2.1.1",
|
||
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
|
||
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
|
||
+ "funding": [
|
||
+ {
|
||
+ "type": "GitHub Sponsors",
|
||
+ "url": "https://github.com/sponsors/unifiedjs"
|
||
+ },
|
||
+ {
|
||
+ "type": "OpenCollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "micromark-util-symbol": "^2.0.0",
|
||
+ "micromark-util-types": "^2.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/micromark-util-encode": {
|
||
+ "version": "2.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
|
||
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
|
||
+ "funding": [
|
||
+ {
|
||
+ "type": "GitHub Sponsors",
|
||
+ "url": "https://github.com/sponsors/unifiedjs"
|
||
+ },
|
||
+ {
|
||
+ "type": "OpenCollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ ],
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/micromark-util-sanitize-uri": {
|
||
+ "version": "2.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
|
||
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
|
||
+ "funding": [
|
||
+ {
|
||
+ "type": "GitHub Sponsors",
|
||
+ "url": "https://github.com/sponsors/unifiedjs"
|
||
+ },
|
||
+ {
|
||
+ "type": "OpenCollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "micromark-util-character": "^2.0.0",
|
||
+ "micromark-util-encode": "^2.0.0",
|
||
+ "micromark-util-symbol": "^2.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/micromark-util-symbol": {
|
||
+ "version": "2.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
|
||
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
|
||
+ "funding": [
|
||
+ {
|
||
+ "type": "GitHub Sponsors",
|
||
+ "url": "https://github.com/sponsors/unifiedjs"
|
||
+ },
|
||
+ {
|
||
+ "type": "OpenCollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ ],
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/micromark-util-types": {
|
||
+ "version": "2.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
|
||
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
|
||
+ "funding": [
|
||
+ {
|
||
+ "type": "GitHub Sponsors",
|
||
+ "url": "https://github.com/sponsors/unifiedjs"
|
||
+ },
|
||
+ {
|
||
+ "type": "OpenCollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ ],
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/mime-db": {
|
||
+ "version": "1.54.0",
|
||
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">= 0.6"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/mime-types": {
|
||
+ "version": "3.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "mime-db": "^1.54.0"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/express"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/mimic-response": {
|
||
+ "version": "3.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=10"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/sindresorhus"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/minimist": {
|
||
+ "version": "1.2.8",
|
||
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/ljharb"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/mkdirp-classic": {
|
||
+ "version": "0.5.3",
|
||
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/mrmime": {
|
||
+ "version": "2.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||
+ "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=10"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/ms": {
|
||
+ "version": "2.1.3",
|
||
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/muggle-string": {
|
||
+ "version": "0.4.1",
|
||
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
|
||
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/nanoid": {
|
||
+ "version": "3.3.15",
|
||
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
||
+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
|
||
+ "funding": [
|
||
+ {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ai"
|
||
+ }
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "bin": {
|
||
+ "nanoid": "bin/nanoid.cjs"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/napi-build-utils": {
|
||
+ "version": "2.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/neotraverse": {
|
||
+ "version": "0.6.18",
|
||
+ "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz",
|
||
+ "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">= 10"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/nlcst-to-string": {
|
||
+ "version": "4.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz",
|
||
+ "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/nlcst": "^2.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/node-abi": {
|
||
+ "version": "3.93.0",
|
||
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.93.0.tgz",
|
||
+ "integrity": "sha512-Cu6yUpX5Iavugm8BeX7c0wgU9CvOqfd1yM6A1d2q2ZMjym7GjpASv2GdRcTq3Fx+Sb5OgBkEEpw4VnAbY6Y5RA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "semver": "^7.3.5"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=10"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/node-fetch-native": {
|
||
+ "version": "1.6.7",
|
||
+ "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
||
+ "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/node-mock-http": {
|
||
+ "version": "1.0.4",
|
||
+ "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz",
|
||
+ "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/nodemailer": {
|
||
+ "version": "9.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.3.tgz",
|
||
+ "integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==",
|
||
+ "license": "MIT-0",
|
||
+ "engines": {
|
||
+ "node": ">=6.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/normalize-path": {
|
||
+ "version": "3.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=0.10.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/nth-check": {
|
||
+ "version": "2.1.1",
|
||
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||
+ "license": "BSD-2-Clause",
|
||
+ "dependencies": {
|
||
+ "boolbase": "^1.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/obug": {
|
||
+ "version": "2.1.3",
|
||
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
|
||
+ "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
|
||
+ "funding": [
|
||
+ "https://github.com/sponsors/sxzz",
|
||
+ "https://opencollective.com/debug"
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=12.20.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/ofetch": {
|
||
+ "version": "1.5.1",
|
||
+ "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz",
|
||
+ "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "destr": "^2.0.5",
|
||
+ "node-fetch-native": "^1.6.7",
|
||
+ "ufo": "^1.6.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/ohash": {
|
||
+ "version": "2.0.11",
|
||
+ "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
|
||
+ "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/on-finished": {
|
||
+ "version": "2.4.1",
|
||
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "ee-first": "1.1.1"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">= 0.8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/once": {
|
||
+ "version": "1.4.0",
|
||
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||
+ "license": "ISC",
|
||
+ "dependencies": {
|
||
+ "wrappy": "1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/oniguruma-parser": {
|
||
+ "version": "0.12.2",
|
||
+ "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz",
|
||
+ "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/oniguruma-to-es": {
|
||
+ "version": "4.3.6",
|
||
+ "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz",
|
||
+ "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "oniguruma-parser": "^0.12.2",
|
||
+ "regex": "^6.1.0",
|
||
+ "regex-recursion": "^6.0.2"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/orderedmap": {
|
||
+ "version": "2.1.1",
|
||
+ "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||
+ "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/p-limit": {
|
||
+ "version": "7.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz",
|
||
+ "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "yocto-queue": "^1.2.1"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/sindresorhus"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/p-queue": {
|
||
+ "version": "9.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz",
|
||
+ "integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "eventemitter3": "^5.0.4",
|
||
+ "p-timeout": "^7.0.0"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/sindresorhus"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/p-timeout": {
|
||
+ "version": "7.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz",
|
||
+ "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=20"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/sindresorhus"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/package-manager-detector": {
|
||
+ "version": "1.7.0",
|
||
+ "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.7.0.tgz",
|
||
+ "integrity": "sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/parse-srcset": {
|
||
+ "version": "1.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||
+ "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/path-browserify": {
|
||
+ "version": "1.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/pathe": {
|
||
+ "version": "2.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/piccolore": {
|
||
+ "version": "0.1.3",
|
||
+ "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz",
|
||
+ "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==",
|
||
+ "license": "ISC"
|
||
+ },
|
||
+ "node_modules/picocolors": {
|
||
+ "version": "1.1.1",
|
||
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||
+ "license": "ISC"
|
||
+ },
|
||
+ "node_modules/picomatch": {
|
||
+ "version": "4.0.4",
|
||
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=12"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/jonschlinkert"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/postcss": {
|
||
+ "version": "8.5.16",
|
||
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||
+ "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
|
||
+ "funding": [
|
||
+ {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/postcss/"
|
||
+ },
|
||
+ {
|
||
+ "type": "tidelift",
|
||
+ "url": "https://tidelift.com/funding/github/npm/postcss"
|
||
+ },
|
||
+ {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/ai"
|
||
+ }
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "nanoid": "^3.3.12",
|
||
+ "picocolors": "^1.1.1",
|
||
+ "source-map-js": "^1.2.1"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": "^10 || ^12 || >=14"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/prebuild-install": {
|
||
+ "version": "7.1.3",
|
||
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||
+ "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "detect-libc": "^2.0.0",
|
||
+ "expand-template": "^2.0.3",
|
||
+ "github-from-package": "0.0.0",
|
||
+ "minimist": "^1.2.3",
|
||
+ "mkdirp-classic": "^0.5.3",
|
||
+ "napi-build-utils": "^2.0.0",
|
||
+ "node-abi": "^3.3.0",
|
||
+ "pump": "^3.0.0",
|
||
+ "rc": "^1.2.7",
|
||
+ "simple-get": "^4.0.0",
|
||
+ "tar-fs": "^2.0.0",
|
||
+ "tunnel-agent": "^0.6.0"
|
||
+ },
|
||
+ "bin": {
|
||
+ "prebuild-install": "bin.js"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=10"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/prettier": {
|
||
+ "version": "3.9.4",
|
||
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz",
|
||
+ "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "bin": {
|
||
+ "prettier": "bin/prettier.cjs"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=14"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/prettier/prettier?sponsor=1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/prismjs": {
|
||
+ "version": "1.30.0",
|
||
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||
+ "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=6"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/process-ancestry": {
|
||
+ "version": "0.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/process-ancestry/-/process-ancestry-0.1.0.tgz",
|
||
+ "integrity": "sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=18.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/property-information": {
|
||
+ "version": "7.2.0",
|
||
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
|
||
+ "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/wooorm"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/prosemirror-changeset": {
|
||
+ "version": "2.4.1",
|
||
+ "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz",
|
||
+ "integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "prosemirror-transform": "^1.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/prosemirror-commands": {
|
||
+ "version": "1.7.1",
|
||
+ "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
|
||
+ "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "prosemirror-model": "^1.0.0",
|
||
+ "prosemirror-state": "^1.0.0",
|
||
+ "prosemirror-transform": "^1.10.2"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/prosemirror-dropcursor": {
|
||
+ "version": "1.8.2",
|
||
+ "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
|
||
+ "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "prosemirror-state": "^1.0.0",
|
||
+ "prosemirror-transform": "^1.1.0",
|
||
+ "prosemirror-view": "^1.1.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/prosemirror-gapcursor": {
|
||
+ "version": "1.4.1",
|
||
+ "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
|
||
+ "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "prosemirror-keymap": "^1.0.0",
|
||
+ "prosemirror-model": "^1.0.0",
|
||
+ "prosemirror-state": "^1.0.0",
|
||
+ "prosemirror-view": "^1.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/prosemirror-history": {
|
||
+ "version": "1.5.0",
|
||
+ "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
|
||
+ "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "prosemirror-state": "^1.2.2",
|
||
+ "prosemirror-transform": "^1.0.0",
|
||
+ "prosemirror-view": "^1.31.0",
|
||
+ "rope-sequence": "^1.3.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/prosemirror-inputrules": {
|
||
+ "version": "1.5.1",
|
||
+ "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
|
||
+ "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "prosemirror-state": "^1.0.0",
|
||
+ "prosemirror-transform": "^1.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/prosemirror-keymap": {
|
||
+ "version": "1.2.3",
|
||
+ "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
|
||
+ "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "prosemirror-state": "^1.0.0",
|
||
+ "w3c-keyname": "^2.2.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/prosemirror-model": {
|
||
+ "version": "1.25.9",
|
||
+ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.9.tgz",
|
||
+ "integrity": "sha512-pRTklkDDMMRopyoAcrr9wV/8g/RYgrLHBuJAb5hlEuYZRdm5yqmPjWId83fpBwPpSFqEdja0H7Dfd7z1X/npcA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "orderedmap": "^2.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/prosemirror-schema-list": {
|
||
+ "version": "1.5.1",
|
||
+ "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
|
||
+ "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "prosemirror-model": "^1.0.0",
|
||
+ "prosemirror-state": "^1.0.0",
|
||
+ "prosemirror-transform": "^1.7.3"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/prosemirror-state": {
|
||
+ "version": "1.4.4",
|
||
+ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||
+ "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "prosemirror-model": "^1.0.0",
|
||
+ "prosemirror-transform": "^1.0.0",
|
||
+ "prosemirror-view": "^1.27.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/prosemirror-tables": {
|
||
+ "version": "1.8.5",
|
||
+ "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
|
||
+ "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "prosemirror-keymap": "^1.2.3",
|
||
+ "prosemirror-model": "^1.25.4",
|
||
+ "prosemirror-state": "^1.4.4",
|
||
+ "prosemirror-transform": "^1.10.5",
|
||
+ "prosemirror-view": "^1.41.4"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/prosemirror-transform": {
|
||
+ "version": "1.12.0",
|
||
+ "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz",
|
||
+ "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "prosemirror-model": "^1.21.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/prosemirror-view": {
|
||
+ "version": "1.42.0",
|
||
+ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.0.tgz",
|
||
+ "integrity": "sha512-N54DF3OXNWDuP81G1kbfCys8ZzIjuL1VnvJ2mk5STSu/fNxWIcX/EutQLA3s9KR/2wVhgDi4hzBB/1fINVxk0A==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "prosemirror-model": "^1.25.8",
|
||
+ "prosemirror-state": "^1.0.0",
|
||
+ "prosemirror-transform": "^1.1.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/pump": {
|
||
+ "version": "3.0.4",
|
||
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||
+ "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "end-of-stream": "^1.1.0",
|
||
+ "once": "^1.3.1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/radix3": {
|
||
+ "version": "1.1.2",
|
||
+ "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz",
|
||
+ "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/range-parser": {
|
||
+ "version": "1.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
|
||
+ "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">= 0.6"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/express"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/rc": {
|
||
+ "version": "1.2.8",
|
||
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||
+ "dependencies": {
|
||
+ "deep-extend": "^0.6.0",
|
||
+ "ini": "~1.3.0",
|
||
+ "minimist": "^1.2.0",
|
||
+ "strip-json-comments": "~2.0.1"
|
||
+ },
|
||
+ "bin": {
|
||
+ "rc": "cli.js"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/readable-stream": {
|
||
+ "version": "3.6.2",
|
||
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "inherits": "^2.0.3",
|
||
+ "string_decoder": "^1.1.1",
|
||
+ "util-deprecate": "^1.0.1"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">= 6"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/readdirp": {
|
||
+ "version": "5.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||
+ "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">= 20.19.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "individual",
|
||
+ "url": "https://paulmillr.com/funding/"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/regex": {
|
||
+ "version": "6.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
|
||
+ "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "regex-utilities": "^2.3.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/regex-recursion": {
|
||
+ "version": "6.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
|
||
+ "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "regex-utilities": "^2.3.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/regex-utilities": {
|
||
+ "version": "2.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
|
||
+ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/request-light": {
|
||
+ "version": "0.7.0",
|
||
+ "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.7.0.tgz",
|
||
+ "integrity": "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/require-directory": {
|
||
+ "version": "2.1.1",
|
||
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=0.10.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/require-from-string": {
|
||
+ "version": "2.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=0.10.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/resolve-pkg-maps": {
|
||
+ "version": "1.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/retext-smartypants": {
|
||
+ "version": "6.2.0",
|
||
+ "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz",
|
||
+ "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/nlcst": "^2.0.0",
|
||
+ "nlcst-to-string": "^4.0.0",
|
||
+ "unist-util-visit": "^5.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/rolldown": {
|
||
+ "version": "1.1.4",
|
||
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz",
|
||
+ "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@oxc-project/types": "=0.138.0",
|
||
+ "@rolldown/pluginutils": "^1.0.0"
|
||
+ },
|
||
+ "bin": {
|
||
+ "rolldown": "bin/cli.mjs"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "@rolldown/binding-android-arm64": "1.1.4",
|
||
+ "@rolldown/binding-darwin-arm64": "1.1.4",
|
||
+ "@rolldown/binding-darwin-x64": "1.1.4",
|
||
+ "@rolldown/binding-freebsd-x64": "1.1.4",
|
||
+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.4",
|
||
+ "@rolldown/binding-linux-arm64-gnu": "1.1.4",
|
||
+ "@rolldown/binding-linux-arm64-musl": "1.1.4",
|
||
+ "@rolldown/binding-linux-ppc64-gnu": "1.1.4",
|
||
+ "@rolldown/binding-linux-s390x-gnu": "1.1.4",
|
||
+ "@rolldown/binding-linux-x64-gnu": "1.1.4",
|
||
+ "@rolldown/binding-linux-x64-musl": "1.1.4",
|
||
+ "@rolldown/binding-openharmony-arm64": "1.1.4",
|
||
+ "@rolldown/binding-wasm32-wasi": "1.1.4",
|
||
+ "@rolldown/binding-win32-arm64-msvc": "1.1.4",
|
||
+ "@rolldown/binding-win32-x64-msvc": "1.1.4"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/rope-sequence": {
|
||
+ "version": "1.3.4",
|
||
+ "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
||
+ "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/safe-buffer": {
|
||
+ "version": "5.2.1",
|
||
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||
+ "funding": [
|
||
+ {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/feross"
|
||
+ },
|
||
+ {
|
||
+ "type": "patreon",
|
||
+ "url": "https://www.patreon.com/feross"
|
||
+ },
|
||
+ {
|
||
+ "type": "consulting",
|
||
+ "url": "https://feross.org/support"
|
||
+ }
|
||
+ ],
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/sanitize-html": {
|
||
+ "version": "2.17.5",
|
||
+ "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.5.tgz",
|
||
+ "integrity": "sha512-ZmU1joGRrvoyctKIiuwUxqR6moLoU2Wk+2bMccN6f7UwhAmwYDvWziqPxRDDN2Qip62NqnIrVrT9akbL6Wretg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "deepmerge": "^4.2.2",
|
||
+ "escape-string-regexp": "^4.0.0",
|
||
+ "htmlparser2": "^10.1.0",
|
||
+ "is-plain-object": "^5.0.0",
|
||
+ "launder": "^1.7.1",
|
||
+ "parse-srcset": "^1.0.2",
|
||
+ "postcss": "^8.3.11"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/satteri": {
|
||
+ "version": "0.9.4",
|
||
+ "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.9.4.tgz",
|
||
+ "integrity": "sha512-BKob126Tay84diOZsnVNH/Q/c+3njPJTCad3w5zLKa6j8bVjxskPNHDtxrMwYK4bN/RlqUSdMnPwKY4k65EMOQ==",
|
||
+ "dependencies": {
|
||
+ "@types/estree-jsx": "^1.0.5",
|
||
+ "@types/hast": "^3.0.4",
|
||
+ "@types/mdast": "^4.0.4",
|
||
+ "@types/unist": "^3.0.3"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "@bruits/satteri-darwin-arm64": "0.9.4",
|
||
+ "@bruits/satteri-darwin-x64": "0.9.4",
|
||
+ "@bruits/satteri-linux-arm64-gnu": "0.9.4",
|
||
+ "@bruits/satteri-linux-arm64-musl": "0.9.4",
|
||
+ "@bruits/satteri-linux-x64-gnu": "0.9.4",
|
||
+ "@bruits/satteri-linux-x64-musl": "0.9.4",
|
||
+ "@bruits/satteri-wasm32-wasi": "0.9.4",
|
||
+ "@bruits/satteri-win32-arm64-msvc": "0.9.4",
|
||
+ "@bruits/satteri-win32-x64-msvc": "0.9.4"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/sax": {
|
||
+ "version": "1.6.0",
|
||
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
|
||
+ "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
|
||
+ "license": "BlueOak-1.0.0",
|
||
+ "engines": {
|
||
+ "node": ">=11.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/semver": {
|
||
+ "version": "7.8.5",
|
||
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||
+ "license": "ISC",
|
||
+ "bin": {
|
||
+ "semver": "bin/semver.js"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=10"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/send": {
|
||
+ "version": "1.2.1",
|
||
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "debug": "^4.4.3",
|
||
+ "encodeurl": "^2.0.0",
|
||
+ "escape-html": "^1.0.3",
|
||
+ "etag": "^1.8.1",
|
||
+ "fresh": "^2.0.0",
|
||
+ "http-errors": "^2.0.1",
|
||
+ "mime-types": "^3.0.2",
|
||
+ "ms": "^2.1.3",
|
||
+ "on-finished": "^2.4.1",
|
||
+ "range-parser": "^1.2.1",
|
||
+ "statuses": "^2.0.2"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">= 18"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/express"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/server-destroy": {
|
||
+ "version": "1.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz",
|
||
+ "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==",
|
||
+ "license": "ISC"
|
||
+ },
|
||
+ "node_modules/setprototypeof": {
|
||
+ "version": "1.2.0",
|
||
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||
+ "license": "ISC"
|
||
+ },
|
||
+ "node_modules/sharp": {
|
||
+ "version": "0.35.3",
|
||
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz",
|
||
+ "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==",
|
||
+ "license": "Apache-2.0",
|
||
+ "optional": true,
|
||
+ "dependencies": {
|
||
+ "@img/colour": "^1.1.0",
|
||
+ "detect-libc": "^2.1.2",
|
||
+ "semver": "^7.8.5"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20.9.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/libvips"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "@img/sharp-darwin-arm64": "0.35.3",
|
||
+ "@img/sharp-darwin-x64": "0.35.3",
|
||
+ "@img/sharp-freebsd-wasm32": "0.35.3",
|
||
+ "@img/sharp-libvips-darwin-arm64": "1.3.2",
|
||
+ "@img/sharp-libvips-darwin-x64": "1.3.2",
|
||
+ "@img/sharp-libvips-linux-arm": "1.3.2",
|
||
+ "@img/sharp-libvips-linux-arm64": "1.3.2",
|
||
+ "@img/sharp-libvips-linux-ppc64": "1.3.2",
|
||
+ "@img/sharp-libvips-linux-riscv64": "1.3.2",
|
||
+ "@img/sharp-libvips-linux-s390x": "1.3.2",
|
||
+ "@img/sharp-libvips-linux-x64": "1.3.2",
|
||
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.2",
|
||
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.2",
|
||
+ "@img/sharp-linux-arm": "0.35.3",
|
||
+ "@img/sharp-linux-arm64": "0.35.3",
|
||
+ "@img/sharp-linux-ppc64": "0.35.3",
|
||
+ "@img/sharp-linux-riscv64": "0.35.3",
|
||
+ "@img/sharp-linux-s390x": "0.35.3",
|
||
+ "@img/sharp-linux-x64": "0.35.3",
|
||
+ "@img/sharp-linuxmusl-arm64": "0.35.3",
|
||
+ "@img/sharp-linuxmusl-x64": "0.35.3",
|
||
+ "@img/sharp-webcontainers-wasm32": "0.35.3",
|
||
+ "@img/sharp-win32-arm64": "0.35.3",
|
||
+ "@img/sharp-win32-ia32": "0.35.3",
|
||
+ "@img/sharp-win32-x64": "0.35.3"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "@types/node": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/shiki": {
|
||
+ "version": "4.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.3.0.tgz",
|
||
+ "integrity": "sha512-NKKjWzR6LIGL3sXBrWDw9sDS9cxx42/DkysaNqJEeOWE8Kix5gpak0bc00OfDVEO4oyXSyz8+aRaqKoBD1yo7A==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@shikijs/core": "4.3.0",
|
||
+ "@shikijs/engine-javascript": "4.3.0",
|
||
+ "@shikijs/engine-oniguruma": "4.3.0",
|
||
+ "@shikijs/langs": "4.3.0",
|
||
+ "@shikijs/themes": "4.3.0",
|
||
+ "@shikijs/types": "4.3.0",
|
||
+ "@shikijs/vscode-textmate": "^10.0.2",
|
||
+ "@types/hast": "^3.0.4"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/siginfo": {
|
||
+ "version": "2.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||
+ "dev": true,
|
||
+ "license": "ISC"
|
||
+ },
|
||
+ "node_modules/simple-concat": {
|
||
+ "version": "1.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||
+ "funding": [
|
||
+ {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/feross"
|
||
+ },
|
||
+ {
|
||
+ "type": "patreon",
|
||
+ "url": "https://www.patreon.com/feross"
|
||
+ },
|
||
+ {
|
||
+ "type": "consulting",
|
||
+ "url": "https://feross.org/support"
|
||
+ }
|
||
+ ],
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/simple-get": {
|
||
+ "version": "4.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||
+ "funding": [
|
||
+ {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/feross"
|
||
+ },
|
||
+ {
|
||
+ "type": "patreon",
|
||
+ "url": "https://www.patreon.com/feross"
|
||
+ },
|
||
+ {
|
||
+ "type": "consulting",
|
||
+ "url": "https://feross.org/support"
|
||
+ }
|
||
+ ],
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "decompress-response": "^6.0.0",
|
||
+ "once": "^1.3.1",
|
||
+ "simple-concat": "^1.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/sisteransi": {
|
||
+ "version": "1.0.5",
|
||
+ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/sitemap": {
|
||
+ "version": "9.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-9.0.1.tgz",
|
||
+ "integrity": "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/node": "^24.9.2",
|
||
+ "@types/sax": "^1.2.1",
|
||
+ "arg": "^5.0.0",
|
||
+ "sax": "^1.4.1"
|
||
+ },
|
||
+ "bin": {
|
||
+ "sitemap": "dist/esm/cli.js"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=20.19.5",
|
||
+ "npm": ">=10.8.2"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/smol-toml": {
|
||
+ "version": "1.7.0",
|
||
+ "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz",
|
||
+ "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==",
|
||
+ "license": "BSD-3-Clause",
|
||
+ "engines": {
|
||
+ "node": ">= 18"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/cyyynthia"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/source-map-js": {
|
||
+ "version": "1.2.1",
|
||
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||
+ "license": "BSD-3-Clause",
|
||
+ "engines": {
|
||
+ "node": ">=0.10.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/space-separated-tokens": {
|
||
+ "version": "2.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
|
||
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/wooorm"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/stackback": {
|
||
+ "version": "0.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/statuses": {
|
||
+ "version": "2.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">= 0.8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/std-env": {
|
||
+ "version": "4.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
|
||
+ "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/stream-replace-string": {
|
||
+ "version": "2.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz",
|
||
+ "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/string_decoder": {
|
||
+ "version": "1.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "safe-buffer": "~5.2.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/string-width": {
|
||
+ "version": "4.2.3",
|
||
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "emoji-regex": "^8.0.0",
|
||
+ "is-fullwidth-code-point": "^3.0.0",
|
||
+ "strip-ansi": "^6.0.1"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/stringify-entities": {
|
||
+ "version": "4.0.4",
|
||
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
||
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "character-entities-html4": "^2.0.0",
|
||
+ "character-entities-legacy": "^3.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/wooorm"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/strip-ansi": {
|
||
+ "version": "6.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "ansi-regex": "^5.0.1"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/strip-json-comments": {
|
||
+ "version": "2.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=0.10.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/svgo": {
|
||
+ "version": "4.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz",
|
||
+ "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "commander": "^11.1.0",
|
||
+ "css-select": "^5.1.0",
|
||
+ "css-tree": "^3.0.1",
|
||
+ "css-what": "^6.1.0",
|
||
+ "csso": "^5.0.5",
|
||
+ "picocolors": "^1.1.1",
|
||
+ "sax": "^1.5.0"
|
||
+ },
|
||
+ "bin": {
|
||
+ "svgo": "bin/svgo.js"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=16"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/svgo"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/tar-fs": {
|
||
+ "version": "2.1.5",
|
||
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
|
||
+ "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "chownr": "^1.1.1",
|
||
+ "mkdirp-classic": "^0.5.2",
|
||
+ "pump": "^3.0.0",
|
||
+ "tar-stream": "^2.1.4"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/tar-stream": {
|
||
+ "version": "2.2.0",
|
||
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "bl": "^4.0.3",
|
||
+ "end-of-stream": "^1.4.1",
|
||
+ "fs-constants": "^1.0.0",
|
||
+ "inherits": "^2.0.3",
|
||
+ "readable-stream": "^3.1.1"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=6"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/tiny-inflate": {
|
||
+ "version": "1.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||
+ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/tinybench": {
|
||
+ "version": "2.9.0",
|
||
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/tinyclip": {
|
||
+ "version": "0.1.15",
|
||
+ "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.15.tgz",
|
||
+ "integrity": "sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": "^16.14.0 || >= 17.3.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/tinyexec": {
|
||
+ "version": "1.2.4",
|
||
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
|
||
+ "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=18"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/tinyglobby": {
|
||
+ "version": "0.2.17",
|
||
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "fdir": "^6.5.0",
|
||
+ "picomatch": "^4.0.4"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=12.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/SuperchupuDev"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/tinyrainbow": {
|
||
+ "version": "3.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
|
||
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=14.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/toidentifier": {
|
||
+ "version": "1.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=0.6"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/trim-lines": {
|
||
+ "version": "3.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
||
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/wooorm"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/trough": {
|
||
+ "version": "2.2.0",
|
||
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
|
||
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/wooorm"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/tslib": {
|
||
+ "version": "2.8.1",
|
||
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||
+ "license": "0BSD",
|
||
+ "optional": true
|
||
+ },
|
||
+ "node_modules/tunnel-agent": {
|
||
+ "version": "0.6.0",
|
||
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||
+ "license": "Apache-2.0",
|
||
+ "dependencies": {
|
||
+ "safe-buffer": "^5.0.1"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": "*"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/typesafe-path": {
|
||
+ "version": "0.2.2",
|
||
+ "resolved": "https://registry.npmjs.org/typesafe-path/-/typesafe-path-0.2.2.tgz",
|
||
+ "integrity": "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/typescript": {
|
||
+ "version": "6.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||
+ "dev": true,
|
||
+ "license": "Apache-2.0",
|
||
+ "bin": {
|
||
+ "tsc": "bin/tsc",
|
||
+ "tsserver": "bin/tsserver"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=14.17"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/typescript-auto-import-cache": {
|
||
+ "version": "0.3.6",
|
||
+ "resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.6.tgz",
|
||
+ "integrity": "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "semver": "^7.3.8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/ufo": {
|
||
+ "version": "1.6.4",
|
||
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
|
||
+ "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/ultrahtml": {
|
||
+ "version": "1.6.0",
|
||
+ "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz",
|
||
+ "integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/uncrypto": {
|
||
+ "version": "0.1.3",
|
||
+ "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz",
|
||
+ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/undici-types": {
|
||
+ "version": "7.18.2",
|
||
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/unified": {
|
||
+ "version": "11.0.5",
|
||
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
|
||
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/unist": "^3.0.0",
|
||
+ "bail": "^2.0.0",
|
||
+ "devlop": "^1.0.0",
|
||
+ "extend": "^3.0.0",
|
||
+ "is-plain-obj": "^4.0.0",
|
||
+ "trough": "^2.0.0",
|
||
+ "vfile": "^6.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/unifont": {
|
||
+ "version": "0.7.4",
|
||
+ "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.4.tgz",
|
||
+ "integrity": "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "css-tree": "^3.1.0",
|
||
+ "ofetch": "^1.5.1",
|
||
+ "ohash": "^2.0.11"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/unist-util-is": {
|
||
+ "version": "6.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
|
||
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/unist": "^3.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/unist-util-position": {
|
||
+ "version": "5.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
|
||
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/unist": "^3.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/unist-util-stringify-position": {
|
||
+ "version": "4.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
|
||
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/unist": "^3.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/unist-util-visit": {
|
||
+ "version": "5.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
|
||
+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/unist": "^3.0.0",
|
||
+ "unist-util-is": "^6.0.0",
|
||
+ "unist-util-visit-parents": "^6.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/unist-util-visit-parents": {
|
||
+ "version": "6.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
|
||
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/unist": "^3.0.0",
|
||
+ "unist-util-is": "^6.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/unstorage": {
|
||
+ "version": "1.17.5",
|
||
+ "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz",
|
||
+ "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "anymatch": "^3.1.3",
|
||
+ "chokidar": "^5.0.0",
|
||
+ "destr": "^2.0.5",
|
||
+ "h3": "^1.15.10",
|
||
+ "lru-cache": "^11.2.7",
|
||
+ "node-fetch-native": "^1.6.7",
|
||
+ "ofetch": "^1.5.1",
|
||
+ "ufo": "^1.6.3"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@azure/app-configuration": "^1.8.0",
|
||
+ "@azure/cosmos": "^4.2.0",
|
||
+ "@azure/data-tables": "^13.3.0",
|
||
+ "@azure/identity": "^4.6.0",
|
||
+ "@azure/keyvault-secrets": "^4.9.0",
|
||
+ "@azure/storage-blob": "^12.26.0",
|
||
+ "@capacitor/preferences": "^6 || ^7 || ^8",
|
||
+ "@deno/kv": ">=0.9.0",
|
||
+ "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0",
|
||
+ "@planetscale/database": "^1.19.0",
|
||
+ "@upstash/redis": "^1.34.3",
|
||
+ "@vercel/blob": ">=0.27.1",
|
||
+ "@vercel/functions": "^2.2.12 || ^3.0.0",
|
||
+ "@vercel/kv": "^1 || ^2 || ^3",
|
||
+ "aws4fetch": "^1.0.20",
|
||
+ "db0": ">=0.2.1",
|
||
+ "idb-keyval": "^6.2.1",
|
||
+ "ioredis": "^5.4.2",
|
||
+ "uploadthing": "^7.4.4"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "@azure/app-configuration": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@azure/cosmos": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@azure/data-tables": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@azure/identity": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@azure/keyvault-secrets": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@azure/storage-blob": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@capacitor/preferences": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@deno/kv": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@netlify/blobs": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@planetscale/database": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@upstash/redis": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@vercel/blob": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@vercel/functions": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@vercel/kv": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "aws4fetch": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "db0": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "idb-keyval": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "ioredis": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "uploadthing": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/util-deprecate": {
|
||
+ "version": "1.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/vfile": {
|
||
+ "version": "6.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
||
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/unist": "^3.0.0",
|
||
+ "vfile-message": "^4.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/vfile-message": {
|
||
+ "version": "4.0.3",
|
||
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
|
||
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@types/unist": "^3.0.0",
|
||
+ "unist-util-stringify-position": "^4.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "type": "opencollective",
|
||
+ "url": "https://opencollective.com/unified"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/vite": {
|
||
+ "version": "8.1.2",
|
||
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.2.tgz",
|
||
+ "integrity": "sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ==",
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "lightningcss": "^1.32.0",
|
||
+ "picomatch": "^4.0.4",
|
||
+ "postcss": "^8.5.16",
|
||
+ "rolldown": "~1.1.3",
|
||
+ "tinyglobby": "^0.2.17"
|
||
+ },
|
||
+ "bin": {
|
||
+ "vite": "bin/vite.js"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || >=22.12.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/vitejs/vite?sponsor=1"
|
||
+ },
|
||
+ "optionalDependencies": {
|
||
+ "fsevents": "~2.3.3"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@types/node": "^20.19.0 || >=22.12.0",
|
||
+ "@vitejs/devtools": "^0.3.0",
|
||
+ "esbuild": "^0.27.0 || ^0.28.0",
|
||
+ "jiti": ">=1.21.0",
|
||
+ "less": "^4.0.0",
|
||
+ "sass": "^1.70.0",
|
||
+ "sass-embedded": "^1.70.0",
|
||
+ "stylus": ">=0.54.8",
|
||
+ "sugarss": "^5.0.0",
|
||
+ "terser": "^5.16.0",
|
||
+ "tsx": "^4.8.1",
|
||
+ "yaml": "^2.4.2"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "@types/node": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@vitejs/devtools": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "esbuild": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "jiti": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "less": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "sass": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "sass-embedded": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "stylus": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "sugarss": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "terser": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "tsx": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "yaml": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/vitefu": {
|
||
+ "version": "1.1.3",
|
||
+ "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz",
|
||
+ "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==",
|
||
+ "license": "MIT",
|
||
+ "workspaces": [
|
||
+ "tests/deps/*",
|
||
+ "tests/projects/*",
|
||
+ "tests/projects/workspace/packages/*"
|
||
+ ],
|
||
+ "peerDependencies": {
|
||
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "vite": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/vitest": {
|
||
+ "version": "4.1.9",
|
||
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz",
|
||
+ "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@vitest/expect": "4.1.9",
|
||
+ "@vitest/mocker": "4.1.9",
|
||
+ "@vitest/pretty-format": "4.1.9",
|
||
+ "@vitest/runner": "4.1.9",
|
||
+ "@vitest/snapshot": "4.1.9",
|
||
+ "@vitest/spy": "4.1.9",
|
||
+ "@vitest/utils": "4.1.9",
|
||
+ "es-module-lexer": "^2.0.0",
|
||
+ "expect-type": "^1.3.0",
|
||
+ "magic-string": "^0.30.21",
|
||
+ "obug": "^2.1.1",
|
||
+ "pathe": "^2.0.3",
|
||
+ "picomatch": "^4.0.3",
|
||
+ "std-env": "^4.0.0-rc.1",
|
||
+ "tinybench": "^2.9.0",
|
||
+ "tinyexec": "^1.0.2",
|
||
+ "tinyglobby": "^0.2.15",
|
||
+ "tinyrainbow": "^3.1.0",
|
||
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
||
+ "why-is-node-running": "^2.3.0"
|
||
+ },
|
||
+ "bin": {
|
||
+ "vitest": "vitest.mjs"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://opencollective.com/vitest"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@edge-runtime/vm": "*",
|
||
+ "@opentelemetry/api": "^1.9.0",
|
||
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||
+ "@vitest/browser-playwright": "4.1.9",
|
||
+ "@vitest/browser-preview": "4.1.9",
|
||
+ "@vitest/browser-webdriverio": "4.1.9",
|
||
+ "@vitest/coverage-istanbul": "4.1.9",
|
||
+ "@vitest/coverage-v8": "4.1.9",
|
||
+ "@vitest/ui": "4.1.9",
|
||
+ "happy-dom": "*",
|
||
+ "jsdom": "*",
|
||
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "@edge-runtime/vm": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@opentelemetry/api": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@types/node": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@vitest/browser-playwright": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@vitest/browser-preview": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@vitest/browser-webdriverio": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@vitest/coverage-istanbul": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@vitest/coverage-v8": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "@vitest/ui": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "happy-dom": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "jsdom": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "vite": {
|
||
+ "optional": false
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/volar-service-css": {
|
||
+ "version": "0.0.70",
|
||
+ "resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.70.tgz",
|
||
+ "integrity": "sha512-K1qyOvBpE3rzdAv3e4/6Rv5yizrYPy5R/ne3IWCAzLBuMO4qBMV3kSqWzj6KUVe6S0AnN6wxF7cRkiaKfYMYJw==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "vscode-css-languageservice": "^6.3.0",
|
||
+ "vscode-languageserver-textdocument": "^1.0.11",
|
||
+ "vscode-uri": "^3.0.8"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@volar/language-service": "~2.4.0"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "@volar/language-service": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/volar-service-emmet": {
|
||
+ "version": "0.0.70",
|
||
+ "resolved": "https://registry.npmjs.org/volar-service-emmet/-/volar-service-emmet-0.0.70.tgz",
|
||
+ "integrity": "sha512-xi5bC4m/VyE3zy/n2CXspKeDZs3qA41tHLTw275/7dNWM/RqE2z3BnDICQybHIVp/6G1iOQj5c1qXMgQC08TNg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@emmetio/css-parser": "^0.4.1",
|
||
+ "@emmetio/html-matcher": "^1.3.0",
|
||
+ "@vscode/emmet-helper": "^2.9.3",
|
||
+ "vscode-uri": "^3.0.8"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@volar/language-service": "~2.4.0"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "@volar/language-service": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/volar-service-html": {
|
||
+ "version": "0.0.70",
|
||
+ "resolved": "https://registry.npmjs.org/volar-service-html/-/volar-service-html-0.0.70.tgz",
|
||
+ "integrity": "sha512-eR6vCgMdmYAo4n+gcT7DSyBQbwB8S3HZZvSagTf0sxNaD4WppMCFfpqWnkrlGStPKMZvMiejRRVmqsX9dYcTvQ==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "vscode-html-languageservice": "^5.3.0",
|
||
+ "vscode-languageserver-textdocument": "^1.0.11",
|
||
+ "vscode-uri": "^3.0.8"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@volar/language-service": "~2.4.0"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "@volar/language-service": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/volar-service-prettier": {
|
||
+ "version": "0.0.70",
|
||
+ "resolved": "https://registry.npmjs.org/volar-service-prettier/-/volar-service-prettier-0.0.70.tgz",
|
||
+ "integrity": "sha512-Z6BCFSpGVCd8BPAsZ785Kce1BGlWd5ODqmqZGVuB14MJvrR4+CYz6cDy4F+igmE1gMifqfvMhdgT8Aud4M5ngg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "vscode-uri": "^3.0.8"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@volar/language-service": "~2.4.0",
|
||
+ "prettier": "^2.2 || ^3.0"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "@volar/language-service": {
|
||
+ "optional": true
|
||
+ },
|
||
+ "prettier": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/volar-service-typescript": {
|
||
+ "version": "0.0.70",
|
||
+ "resolved": "https://registry.npmjs.org/volar-service-typescript/-/volar-service-typescript-0.0.70.tgz",
|
||
+ "integrity": "sha512-l46Bx4cokkUedTd74ojO5H/zqHZJ8SUuyZ0IB8JN4jfRqUM3bQFBHoOwlZCyZmOeO0A3RQNkMnFclxO4c++gsg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "path-browserify": "^1.0.1",
|
||
+ "semver": "^7.6.2",
|
||
+ "typescript-auto-import-cache": "^0.3.5",
|
||
+ "vscode-languageserver-textdocument": "^1.0.11",
|
||
+ "vscode-nls": "^5.2.0",
|
||
+ "vscode-uri": "^3.0.8"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@volar/language-service": "~2.4.0"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "@volar/language-service": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/volar-service-typescript-twoslash-queries": {
|
||
+ "version": "0.0.70",
|
||
+ "resolved": "https://registry.npmjs.org/volar-service-typescript-twoslash-queries/-/volar-service-typescript-twoslash-queries-0.0.70.tgz",
|
||
+ "integrity": "sha512-IdD13Z9N2Bu8EM6CM0fDV1E69olEYGHDU25X51YXmq8Y0CmJ2LNj6gOiBJgpS5JGUqFzECVhMNBW7R0sPdRTMQ==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "vscode-uri": "^3.0.8"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@volar/language-service": "~2.4.0"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "@volar/language-service": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/volar-service-yaml": {
|
||
+ "version": "0.0.70",
|
||
+ "resolved": "https://registry.npmjs.org/volar-service-yaml/-/volar-service-yaml-0.0.70.tgz",
|
||
+ "integrity": "sha512-0c8bXDBeoATF9F6iPIlOuYTuZAC4c+yi0siQo920u7eiBJk8oQmUmg9cDUbR4+Gl++bvGP4plj3fErbJuPqdcQ==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "vscode-uri": "^3.0.8",
|
||
+ "yaml-language-server": "~1.20.0"
|
||
+ },
|
||
+ "peerDependencies": {
|
||
+ "@volar/language-service": "~2.4.0"
|
||
+ },
|
||
+ "peerDependenciesMeta": {
|
||
+ "@volar/language-service": {
|
||
+ "optional": true
|
||
+ }
|
||
+ }
|
||
+ },
|
||
+ "node_modules/vscode-css-languageservice": {
|
||
+ "version": "6.3.10",
|
||
+ "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.10.tgz",
|
||
+ "integrity": "sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@vscode/l10n": "^0.0.18",
|
||
+ "vscode-languageserver-textdocument": "^1.0.12",
|
||
+ "vscode-languageserver-types": "3.17.5",
|
||
+ "vscode-uri": "^3.1.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/vscode-css-languageservice/node_modules/vscode-languageserver-types": {
|
||
+ "version": "3.17.5",
|
||
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
|
||
+ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/vscode-html-languageservice": {
|
||
+ "version": "5.6.2",
|
||
+ "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.6.2.tgz",
|
||
+ "integrity": "sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@vscode/l10n": "^0.0.18",
|
||
+ "vscode-languageserver-textdocument": "^1.0.12",
|
||
+ "vscode-languageserver-types": "^3.17.5",
|
||
+ "vscode-uri": "^3.1.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/vscode-json-languageservice": {
|
||
+ "version": "4.1.8",
|
||
+ "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-4.1.8.tgz",
|
||
+ "integrity": "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "jsonc-parser": "^3.0.0",
|
||
+ "vscode-languageserver-textdocument": "^1.0.1",
|
||
+ "vscode-languageserver-types": "^3.16.0",
|
||
+ "vscode-nls": "^5.0.0",
|
||
+ "vscode-uri": "^3.0.2"
|
||
+ },
|
||
+ "engines": {
|
||
+ "npm": ">=7.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/vscode-jsonrpc": {
|
||
+ "version": "9.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.1.tgz",
|
||
+ "integrity": "sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=14.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/vscode-languageserver": {
|
||
+ "version": "9.0.1",
|
||
+ "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz",
|
||
+ "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "vscode-languageserver-protocol": "3.17.5"
|
||
+ },
|
||
+ "bin": {
|
||
+ "installServerIntoExtension": "bin/installServerIntoExtension"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/vscode-languageserver-protocol": {
|
||
+ "version": "3.18.2",
|
||
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.2.tgz",
|
||
+ "integrity": "sha512-XRyDbT0Pp3sSNti3JmxVEUMySWCSi1hhM+/KUlCy1hV1zmrqpM1OwO12EAki8blhmLuIMpaJrYbo0OzGVfK2Qg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "vscode-jsonrpc": "9.0.1",
|
||
+ "vscode-languageserver-types": "3.18.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/vscode-languageserver-textdocument": {
|
||
+ "version": "1.0.12",
|
||
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
|
||
+ "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/vscode-languageserver-types": {
|
||
+ "version": "3.18.0",
|
||
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.0.tgz",
|
||
+ "integrity": "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/vscode-languageserver/node_modules/vscode-jsonrpc": {
|
||
+ "version": "8.2.0",
|
||
+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
||
+ "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=14.0.0"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/vscode-languageserver/node_modules/vscode-languageserver-protocol": {
|
||
+ "version": "3.17.5",
|
||
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
|
||
+ "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "vscode-jsonrpc": "8.2.0",
|
||
+ "vscode-languageserver-types": "3.17.5"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/vscode-languageserver/node_modules/vscode-languageserver-types": {
|
||
+ "version": "3.17.5",
|
||
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
|
||
+ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/vscode-nls": {
|
||
+ "version": "5.2.0",
|
||
+ "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz",
|
||
+ "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/vscode-uri": {
|
||
+ "version": "3.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
||
+ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/w3c-keyname": {
|
||
+ "version": "2.2.8",
|
||
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/which-pm-runs": {
|
||
+ "version": "1.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz",
|
||
+ "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=4"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/why-is-node-running": {
|
||
+ "version": "2.3.0",
|
||
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "siginfo": "^2.0.0",
|
||
+ "stackback": "0.0.2"
|
||
+ },
|
||
+ "bin": {
|
||
+ "why-is-node-running": "cli.js"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=8"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/wrap-ansi": {
|
||
+ "version": "7.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "ansi-styles": "^4.0.0",
|
||
+ "string-width": "^4.1.0",
|
||
+ "strip-ansi": "^6.0.0"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=10"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/wrappy": {
|
||
+ "version": "1.0.2",
|
||
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||
+ "license": "ISC"
|
||
+ },
|
||
+ "node_modules/xxhash-wasm": {
|
||
+ "version": "1.1.0",
|
||
+ "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz",
|
||
+ "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==",
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/y18n": {
|
||
+ "version": "5.0.8",
|
||
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||
+ "dev": true,
|
||
+ "license": "ISC",
|
||
+ "engines": {
|
||
+ "node": ">=10"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/yaml": {
|
||
+ "version": "2.9.0",
|
||
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||
+ "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
|
||
+ "devOptional": true,
|
||
+ "license": "ISC",
|
||
+ "bin": {
|
||
+ "yaml": "bin.mjs"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">= 14.6"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/eemeli"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/yaml-language-server": {
|
||
+ "version": "1.20.0",
|
||
+ "resolved": "https://registry.npmjs.org/yaml-language-server/-/yaml-language-server-1.20.0.tgz",
|
||
+ "integrity": "sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "@vscode/l10n": "^0.0.18",
|
||
+ "ajv": "^8.17.1",
|
||
+ "ajv-draft-04": "^1.0.0",
|
||
+ "prettier": "^3.5.0",
|
||
+ "request-light": "^0.5.7",
|
||
+ "vscode-json-languageservice": "4.1.8",
|
||
+ "vscode-languageserver": "^9.0.0",
|
||
+ "vscode-languageserver-textdocument": "^1.0.1",
|
||
+ "vscode-languageserver-types": "^3.16.0",
|
||
+ "vscode-uri": "^3.0.2",
|
||
+ "yaml": "2.7.1"
|
||
+ },
|
||
+ "bin": {
|
||
+ "yaml-language-server": "bin/yaml-language-server"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/yaml-language-server/node_modules/request-light": {
|
||
+ "version": "0.5.8",
|
||
+ "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.5.8.tgz",
|
||
+ "integrity": "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==",
|
||
+ "dev": true,
|
||
+ "license": "MIT"
|
||
+ },
|
||
+ "node_modules/yaml-language-server/node_modules/yaml": {
|
||
+ "version": "2.7.1",
|
||
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz",
|
||
+ "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==",
|
||
+ "dev": true,
|
||
+ "license": "ISC",
|
||
+ "bin": {
|
||
+ "yaml": "bin.mjs"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">= 14"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/yargs": {
|
||
+ "version": "17.7.3",
|
||
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz",
|
||
+ "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
|
||
+ "dev": true,
|
||
+ "license": "MIT",
|
||
+ "dependencies": {
|
||
+ "cliui": "^8.0.1",
|
||
+ "escalade": "^3.1.1",
|
||
+ "get-caller-file": "^2.0.5",
|
||
+ "require-directory": "^2.1.1",
|
||
+ "string-width": "^4.2.3",
|
||
+ "y18n": "^5.0.5",
|
||
+ "yargs-parser": "^21.1.1"
|
||
+ },
|
||
+ "engines": {
|
||
+ "node": ">=12"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/yargs-parser": {
|
||
+ "version": "22.0.0",
|
||
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
|
||
+ "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
|
||
+ "license": "ISC",
|
||
+ "engines": {
|
||
+ "node": "^20.19.0 || ^22.12.0 || >=23"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/yargs/node_modules/yargs-parser": {
|
||
+ "version": "21.1.1",
|
||
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||
+ "dev": true,
|
||
+ "license": "ISC",
|
||
+ "engines": {
|
||
+ "node": ">=12"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/yocto-queue": {
|
||
+ "version": "1.2.2",
|
||
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
|
||
+ "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==",
|
||
+ "license": "MIT",
|
||
+ "engines": {
|
||
+ "node": ">=12.20"
|
||
+ },
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/sindresorhus"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/zod": {
|
||
+ "version": "4.4.3",
|
||
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "url": "https://github.com/sponsors/colinhacks"
|
||
+ }
|
||
+ },
|
||
+ "node_modules/zwitch": {
|
||
+ "version": "2.0.4",
|
||
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
|
||
+ "license": "MIT",
|
||
+ "funding": {
|
||
+ "type": "github",
|
||
+ "url": "https://github.com/sponsors/wooorm"
|
||
+ }
|
||
+ }
|
||
+ }
|
||
+}
|
||
diff --git a/package.json b/package.json
|
||
new file mode 100644
|
||
index 0000000..57ad834
|
||
--- /dev/null
|
||
+++ b/package.json
|
||
@@ -0,0 +1,38 @@
|
||
+{
|
||
+ "name": "insanitylab-website",
|
||
+ "version": "0.1.0",
|
||
+ "private": true,
|
||
+ "type": "module",
|
||
+ "scripts": {
|
||
+ "dev": "astro dev",
|
||
+ "build": "astro build",
|
||
+ "preview": "astro preview",
|
||
+ "start": "node --env-file=.env ./dist/server/entry.mjs",
|
||
+ "test": "vitest run",
|
||
+ "create-user": "node scripts/create-user.mjs"
|
||
+ },
|
||
+ "dependencies": {
|
||
+ "@astrojs/node": "^11.0.1",
|
||
+ "@astrojs/sitemap": "^3.7.3",
|
||
+ "@fontsource/montserrat": "^5.2.8",
|
||
+ "@fontsource/open-sans": "^5.2.7",
|
||
+ "@tiptap/core": "^3.27.1",
|
||
+ "@tiptap/extension-image": "^3.27.1",
|
||
+ "@tiptap/extension-link": "^3.27.1",
|
||
+ "@tiptap/starter-kit": "^3.27.1",
|
||
+ "astro": "^7.0.5",
|
||
+ "bcryptjs": "^3.0.3",
|
||
+ "better-sqlite3": "^12.11.1",
|
||
+ "nodemailer": "^9.0.3",
|
||
+ "sanitize-html": "^2.17.5"
|
||
+ },
|
||
+ "devDependencies": {
|
||
+ "@astrojs/check": "^0.9.9",
|
||
+ "@types/bcryptjs": "^2.4.6",
|
||
+ "@types/better-sqlite3": "^7.6.13",
|
||
+ "@types/nodemailer": "^8.0.1",
|
||
+ "@types/sanitize-html": "^2.16.1",
|
||
+ "typescript": "^6.0.3",
|
||
+ "vitest": "^4.1.9"
|
||
+ }
|
||
+}
|
||
diff --git a/public/favicon.svg b/public/favicon.svg
|
||
new file mode 100644
|
||
index 0000000..c6e9cb0
|
||
--- /dev/null
|
||
+++ b/public/favicon.svg
|
||
@@ -0,0 +1,4 @@
|
||
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||
+ <rect width="32" height="32" rx="6" fill="#3a2929"/>
|
||
+ <path d="M4 18h5l3-8 4 12 3-6h9" fill="none" stroke="#b8a98c" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||
+</svg>
|
||
diff --git a/public/img/logo-dark.png b/public/img/logo-dark.png
|
||
new file mode 100644
|
||
index 0000000..5535d27
|
||
Binary files /dev/null and b/public/img/logo-dark.png differ
|
||
diff --git a/public/img/logo-light.png b/public/img/logo-light.png
|
||
new file mode 100644
|
||
index 0000000..7d5354c
|
||
Binary files /dev/null and b/public/img/logo-light.png differ
|
||
diff --git a/public/robots.txt b/public/robots.txt
|
||
new file mode 100644
|
||
index 0000000..66a96dc
|
||
--- /dev/null
|
||
+++ b/public/robots.txt
|
||
@@ -0,0 +1,4 @@
|
||
+User-agent: *
|
||
+Disallow: /admin
|
||
+Disallow: /api
|
||
+Sitemap: https://insanitylab.tielogic.xyz/sitemap-index.xml
|
||
diff --git a/scripts/apply-asset-map.mjs b/scripts/apply-asset-map.mjs
|
||
new file mode 100644
|
||
index 0000000..931b99c
|
||
--- /dev/null
|
||
+++ b/scripts/apply-asset-map.mjs
|
||
@@ -0,0 +1,42 @@
|
||
+// Pipeline asset: extract-fig-images.sh → apply-asset-map.mjs → optimize-assets.sh → extract-logos.sh
|
||
+// 1. extract-fig-images.sh: estrae hash-named images da .fig → assets-src/
|
||
+// 2. apply-asset-map.mjs: copia da assets-src/ → destinazioni (src/assets/img, public/img)
|
||
+// 3. optimize-assets.sh: converte PNG fotografici → JPG q85, ridimensiona ≤1400px
|
||
+// 4. extract-logos.sh: estrae loghi insanitylab da FigmaDoc/Homepage.pdf → public/img/logo-{dark,light}.png
|
||
+
|
||
+import { copyFileSync, mkdirSync, existsSync } from 'node:fs';
|
||
+import { readFileSync } from 'node:fs';
|
||
+import { join, dirname } from 'node:path';
|
||
+import { fileURLToPath } from 'node:url';
|
||
+
|
||
+const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||
+const map = JSON.parse(readFileSync(join(root, 'scripts/asset-map.json'), 'utf8'));
|
||
+let n = 0, missing = 0;
|
||
+
|
||
+// Sezione principale: copia da assets-src/
|
||
+for (const [destDir, entries] of Object.entries(map)) {
|
||
+ if (destDir.startsWith('_') || destDir === 'fallbacks') continue;
|
||
+ mkdirSync(join(root, destDir), { recursive: true });
|
||
+ for (const [src, dest] of Object.entries(entries)) {
|
||
+ const from = join(root, 'assets-src', src);
|
||
+ if (!existsSync(from)) { console.error(`MANCANTE: ${src}`); missing++; continue; }
|
||
+ copyFileSync(from, join(root, destDir, dest));
|
||
+ n++;
|
||
+ }
|
||
+}
|
||
+
|
||
+// Sezione fallbacks: copia da path relativo alla root del repo (es. ../Dati/)
|
||
+if (map.fallbacks) {
|
||
+ for (const [destDir, entries] of Object.entries(map.fallbacks)) {
|
||
+ mkdirSync(join(root, destDir), { recursive: true });
|
||
+ for (const [srcRelPath, dest] of Object.entries(entries)) {
|
||
+ const from = join(root, srcRelPath);
|
||
+ if (!existsSync(from)) { console.error(`MANCANTE (fallback): ${srcRelPath}`); missing++; continue; }
|
||
+ copyFileSync(from, join(root, destDir, dest));
|
||
+ n++;
|
||
+ }
|
||
+ }
|
||
+}
|
||
+
|
||
+console.log(`Copiate ${n} immagini${missing ? `, ${missing} mancanti` : ''}`);
|
||
+process.exit(missing ? 1 : 0);
|
||
diff --git a/scripts/asset-map.json b/scripts/asset-map.json
|
||
new file mode 100644
|
||
index 0000000..3155034
|
||
--- /dev/null
|
||
+++ b/scripts/asset-map.json
|
||
@@ -0,0 +1,37 @@
|
||
+{
|
||
+ "_nota": "Le destinazioni .png fotografiche vengono convertite in .jpg da optimize-assets.sh; il nome finale su disco ha estensione .jpg",
|
||
+ "src/assets/img": {
|
||
+ "8b71f158c30a0a23cc5113bfe1cd9a6c4f19e4c4.jpg": "hero-1.jpg",
|
||
+ "0f8034394657985cc4cf35eb8441ae93a7ce5302.jpg": "info-performance.jpg",
|
||
+ "432d397a342b4e8312a3a13a4fea836b3e867a64.jpg": "info-balance.jpg",
|
||
+ "d7fcf0aa827ec50f660145c0d319049b9153fefb.jpg": "info-longevity.jpg",
|
||
+ "6d6025730c3d0b73c0052fa67b1bb111c8462a08.jpg": "feature-performance.jpg",
|
||
+ "16e611de84f51bf5d01486a5e74ba0f8249850be.jpg": "feature-balance.jpg",
|
||
+ "98f1f8d257dfeffbaf35507d033e5bb51e08895e.png": "feature-longevity.png",
|
||
+ "28e8157dc88a42cffdbcd028e4cb02a6eb3de337.png": "quote-bg.png",
|
||
+ "85808a0c9aeb461c70c059b013eab47faa4fc4f1.png": "training-one-to-one.png",
|
||
+ "bb632b8da5e9e4d4b05207f04a8883163caac53a.png": "training-small-groups.png",
|
||
+ "91aa82b5dedee8ad56c6db249c9947c7d6d9c89a.png": "training-performance-class.png",
|
||
+ "86c1363b22a8fb3bb7b2bf044e52856d474d927e.jpg": "training-fit-remote.jpg",
|
||
+ "0b6cf69438af21bbca934b0b27995ac4fa967451.png": "team-donata.png",
|
||
+ "1cc33c41368d47c30d949e991c5a442ff51d1a91.png": "team-nicola.png",
|
||
+ "a75ca105e362e3c3bc5cfe0bfc7c39e69645660d.png": "team-eleonora.png",
|
||
+ "e3c84f9989df8b160d185bd40b774e460a4f6896.png": "team-antonello.png",
|
||
+ "88cfcd5fd321b5ca6e4833e3980b792a95090545.jpg": "method-step-1.jpg",
|
||
+ "d21671bb08f4f2eadde09f91a46b568788a209d7.jpg": "method-step-2.jpg",
|
||
+ "a3e84d7daf0b25c750ef694f4b9fc10bf0f2f1a8.jpg": "method-step-3.jpg",
|
||
+ "e50b6f7818b3742ddba0f4ad4a162a8aafc505e0.jpg": "method-step-4.jpg",
|
||
+ "bee22f37bba6f18d3defcd47762375e1c8ddd0f5.png": "partner-edenred.png",
|
||
+ "cf079250ea1db7f0055b76af21e04b1ed1870770.png": "partner-welbee.png",
|
||
+ "872c3679df24bd0d3fdca7c4cb9a24d1836309a0.png": "partner-howden.png",
|
||
+ "014a7708ad61b12c5765ad4a60539d2fa325f9aa.png": "partner-aon.png",
|
||
+ "8ffc5f7a2428b0d0eabd09f676ab98d3e5644f0d.png": "about-exterior.png",
|
||
+ "32ecd00a20321e58c700fbaf739e53ac3bf03876.png": "about-team-group.png",
|
||
+ "d4608b102eb67ac067e6585250b3c37963db1d34.png": "about-training-group.png",
|
||
+ "dafcc19362e3a76dd20e9a656499a8068a3a9fca.png": "about-trainer-1.png",
|
||
+ "9c50f1d3b3a0477e80e03c0849d3d4687610b461.png": "about-trainer-2.png",
|
||
+ "c51a49b9be7f1b1a31de6b2f5261c8c5b5fdab52.png": "about-trainer-3.png",
|
||
+ "3341a11b7153349a23c75b1fa2a9a62d48953330.png": "about-trainer-4.png"
|
||
+ },
|
||
+ "_loghi": "logo-dark.png e logo-light.png estratti da extract-logos.sh (FigmaDoc/Homepage.pdf), non da ../Dati"
|
||
+}
|
||
diff --git a/scripts/create-user.mjs b/scripts/create-user.mjs
|
||
new file mode 100644
|
||
index 0000000..5bd0e67
|
||
--- /dev/null
|
||
+++ b/scripts/create-user.mjs
|
||
@@ -0,0 +1,23 @@
|
||
+import Database from 'better-sqlite3';
|
||
+import bcrypt from 'bcryptjs';
|
||
+import { mkdirSync } from 'node:fs';
|
||
+import { dirname } from 'node:path';
|
||
+
|
||
+const [, , username, password] = process.argv;
|
||
+if (!username || !password) {
|
||
+ console.error('Uso: npm run create-user -- <username> <password>');
|
||
+ process.exit(1);
|
||
+}
|
||
+const path = process.env.DB_PATH ?? 'data/insanitylab.db';
|
||
+mkdirSync(dirname(path), { recursive: true });
|
||
+const db = new Database(path);
|
||
+db.exec(`CREATE TABLE IF NOT EXISTS users (
|
||
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
+ username TEXT NOT NULL UNIQUE,
|
||
+ password_hash TEXT NOT NULL
|
||
+);`);
|
||
+db.prepare(
|
||
+ `INSERT INTO users (username, password_hash) VALUES (?, ?)
|
||
+ ON CONFLICT(username) DO UPDATE SET password_hash = excluded.password_hash`
|
||
+).run(username, bcrypt.hashSync(password, 12));
|
||
+console.log(`Utente "${username}" creato/aggiornato in ${path}`);
|
||
diff --git a/scripts/extract-fig-images.sh b/scripts/extract-fig-images.sh
|
||
new file mode 100755
|
||
index 0000000..ef3bdf1
|
||
--- /dev/null
|
||
+++ b/scripts/extract-fig-images.sh
|
||
@@ -0,0 +1,17 @@
|
||
+#!/usr/bin/env bash
|
||
+set -euo pipefail
|
||
+cd "$(dirname "$0")/.."
|
||
+FIG="../FigmaDoc/Web Insanity.fig"
|
||
+OUT="assets-src"
|
||
+rm -rf "$OUT" && mkdir -p "$OUT"
|
||
+unzip -o -j -q "$FIG" 'images/*' -d "$OUT"
|
||
+cd "$OUT"
|
||
+for f in *; do
|
||
+ case "$(file --brief --mime-type "$f")" in
|
||
+ image/png) mv "$f" "$f.png" ;;
|
||
+ image/jpeg) mv "$f" "$f.jpg" ;;
|
||
+ image/webp) mv "$f" "$f.webp" ;;
|
||
+ *) rm -f "$f" ;;
|
||
+ esac
|
||
+done
|
||
+echo "Estratte: $(ls | wc -l) immagini in $OUT/"
|
||
diff --git a/scripts/extract-logos.sh b/scripts/extract-logos.sh
|
||
new file mode 100755
|
||
index 0000000..a84b5bc
|
||
--- /dev/null
|
||
+++ b/scripts/extract-logos.sh
|
||
@@ -0,0 +1,53 @@
|
||
+#!/usr/bin/env bash
|
||
+# extract-logos.sh — estrae loghi insanitylab dal PDF Figma Homepage
|
||
+# Pipeline: extract-fig-images.sh → apply-asset-map.mjs → optimize-assets.sh → extract-logos.sh
|
||
+# Idempotente: stessi file ad ogni esecuzione, coordinate fisse.
|
||
+set -euo pipefail
|
||
+
|
||
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
+ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||
+PDF="$ROOT/../FigmaDoc/Homepage.pdf"
|
||
+OUT="$ROOT/public/img"
|
||
+WORK="$(mktemp -d)"
|
||
+trap 'rm -rf "$WORK"' EXIT
|
||
+
|
||
+if [[ ! -f "$PDF" ]]; then
|
||
+ echo "ERRORE: PDF non trovato: $PDF" >&2
|
||
+ exit 1
|
||
+fi
|
||
+
|
||
+mkdir -p "$OUT"
|
||
+
|
||
+echo "Estrazione loghi da $PDF..."
|
||
+
|
||
+# ── Logo DARK (header, su fondo bianco) ─────────────────────────────────────
|
||
+# Zona: x=104, y=41, W=895, H=250 px a 300dpi (pt: x≈25, y≈10, W≈215, H≈60)
|
||
+pdftoppm -png -r 300 -f 1 -l 1 \
|
||
+ -x 104 -y 41 -W 895 -H 250 \
|
||
+ "$PDF" "$WORK/hdr"
|
||
+
|
||
+convert "$WORK/hdr-1.png" \
|
||
+ -fuzz 8% -transparent white \
|
||
+ -trim +repage \
|
||
+ -resize 600x \
|
||
+ -strip \
|
||
+ "$OUT/logo-dark.png"
|
||
+
|
||
+# ── Logo LIGHT (footer, su fondo scuro #3A2929) ─────────────────────────────
|
||
+# Zona: x=0, y=35375, W=1800, H=833 px a 300dpi (pt: x≈0, y≈8490, W≈432, H≈200)
|
||
+# poi crop interno: x=130, y=60, W=850, H=660 per includere solo icona+testo
|
||
+pdftoppm -png -r 300 -f 1 -l 1 \
|
||
+ -x 0 -y 35375 -W 1800 -H 833 \
|
||
+ "$PDF" "$WORK/ftr"
|
||
+
|
||
+convert "$WORK/ftr-1.png" \
|
||
+ -crop 850x660+130+60 +repage \
|
||
+ -fuzz 12% -transparent 'srgb(58,41,41)' \
|
||
+ -trim +repage \
|
||
+ -resize 600x \
|
||
+ -strip \
|
||
+ "$OUT/logo-light.png"
|
||
+
|
||
+echo "Logo dark: $OUT/logo-dark.png $(identify -format '%wx%h' "$OUT/logo-dark.png")"
|
||
+echo "Logo light: $OUT/logo-light.png $(identify -format '%wx%h' "$OUT/logo-light.png")"
|
||
+echo "Done."
|
||
diff --git a/scripts/optimize-assets.sh b/scripts/optimize-assets.sh
|
||
new file mode 100755
|
||
index 0000000..01bb004
|
||
--- /dev/null
|
||
+++ b/scripts/optimize-assets.sh
|
||
@@ -0,0 +1,40 @@
|
||
+#!/usr/bin/env bash
|
||
+# Passo 3 della pipeline asset (dopo apply-asset-map.mjs).
|
||
+# Converte PNG fotografici in src/assets/img/ → JPG q83 e ridimensiona a ≤1400x1600px.
|
||
+# I PNG partner-* (loghi con trasparenza) restano PNG.
|
||
+# Tutti i JPG esistenti vengono ri-codificati a q83 con vincolo 1400x1600>
|
||
+# (larghezza ≤1400 px E altezza ≤1600 px, aspect ratio preservato).
|
||
+# Idempotente se abbinato a apply-asset-map.mjs (che ripristina i sorgenti originali).
|
||
+set -euo pipefail
|
||
+
|
||
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
+ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||
+IMG_DIR="$ROOT/src/assets/img"
|
||
+
|
||
+cd "$IMG_DIR"
|
||
+
|
||
+# --- PNG fotografici → JPG q83 + resize ≤1400x1600 ---
|
||
+shopt -s nullglob
|
||
+declare -A just_converted
|
||
+converted=0
|
||
+for png in *.png; do
|
||
+ [[ "$png" == partner-* ]] && continue
|
||
+ jpg="${png%.png}.jpg"
|
||
+ convert "$png" -quality 83 -resize '1400x1600>' "$jpg"
|
||
+ rm "$png"
|
||
+ echo " PNG→JPG: $png → $jpg"
|
||
+ just_converted["$jpg"]=1
|
||
+ (( converted++ )) || true
|
||
+done
|
||
+
|
||
+# --- Ri-codifica JPG esistenti a q83 + vincolo 1400x1600 ---
|
||
+# Skip file appena convertiti (già a q83), elabora tutti gli altri.
|
||
+reencoded=0
|
||
+for jpg in *.jpg; do
|
||
+ [[ -n "${just_converted[$jpg]+x}" ]] && continue
|
||
+ mogrify -quality 83 -resize '1400x1600>' "$jpg"
|
||
+ echo " Ricodificato: $jpg"
|
||
+ (( reencoded++ )) || true
|
||
+done
|
||
+
|
||
+echo "Ottimizzazione completata: ${converted} PNG→JPG, ${reencoded} JPG ricodificati (q83, ≤1400x1600)."
|
||
diff --git a/scripts/smoke.sh b/scripts/smoke.sh
|
||
new file mode 100755
|
||
index 0000000..67e9a56
|
||
--- /dev/null
|
||
+++ b/scripts/smoke.sh
|
||
@@ -0,0 +1,17 @@
|
||
+#!/usr/bin/env bash
|
||
+set -euo pipefail
|
||
+cd "$(dirname "$0")/.."
|
||
+npm run build
|
||
+PORT=4399 node --env-file=.env ./dist/server/entry.mjs & SRV=$!
|
||
+trap 'kill $SRV' EXIT
|
||
+sleep 2
|
||
+fail=0
|
||
+for path in / /about /training /programs /programs/performance-class /programs/rehab /blog /contact /admin/login /robots.txt; do
|
||
+ code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:4399$path")
|
||
+ echo "$code $path"
|
||
+ [ "$code" = "200" ] || fail=1
|
||
+done
|
||
+code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:4399/admin)
|
||
+echo "$code /admin (atteso 302)"
|
||
+[ "$code" = "302" ] || fail=1
|
||
+exit $fail
|
||
diff --git a/src/assets/img/about-exterior.jpg b/src/assets/img/about-exterior.jpg
|
||
new file mode 100644
|
||
index 0000000..99a44e7
|
||
Binary files /dev/null and b/src/assets/img/about-exterior.jpg differ
|
||
diff --git a/src/assets/img/about-team-group.jpg b/src/assets/img/about-team-group.jpg
|
||
new file mode 100644
|
||
index 0000000..a37d5ad
|
||
Binary files /dev/null and b/src/assets/img/about-team-group.jpg differ
|
||
diff --git a/src/assets/img/about-trainer-1.jpg b/src/assets/img/about-trainer-1.jpg
|
||
new file mode 100644
|
||
index 0000000..b958d89
|
||
Binary files /dev/null and b/src/assets/img/about-trainer-1.jpg differ
|
||
diff --git a/src/assets/img/about-trainer-2.jpg b/src/assets/img/about-trainer-2.jpg
|
||
new file mode 100644
|
||
index 0000000..76cfa77
|
||
Binary files /dev/null and b/src/assets/img/about-trainer-2.jpg differ
|
||
diff --git a/src/assets/img/about-trainer-3.jpg b/src/assets/img/about-trainer-3.jpg
|
||
new file mode 100644
|
||
index 0000000..e17f529
|
||
Binary files /dev/null and b/src/assets/img/about-trainer-3.jpg differ
|
||
diff --git a/src/assets/img/about-trainer-4.jpg b/src/assets/img/about-trainer-4.jpg
|
||
new file mode 100644
|
||
index 0000000..54c1470
|
||
Binary files /dev/null and b/src/assets/img/about-trainer-4.jpg differ
|
||
diff --git a/src/assets/img/about-training-group.jpg b/src/assets/img/about-training-group.jpg
|
||
new file mode 100644
|
||
index 0000000..6c795a3
|
||
Binary files /dev/null and b/src/assets/img/about-training-group.jpg differ
|
||
diff --git a/src/assets/img/feature-balance.jpg b/src/assets/img/feature-balance.jpg
|
||
new file mode 100644
|
||
index 0000000..416d835
|
||
Binary files /dev/null and b/src/assets/img/feature-balance.jpg differ
|
||
diff --git a/src/assets/img/feature-longevity.jpg b/src/assets/img/feature-longevity.jpg
|
||
new file mode 100644
|
||
index 0000000..06ec041
|
||
Binary files /dev/null and b/src/assets/img/feature-longevity.jpg differ
|
||
diff --git a/src/assets/img/feature-performance.jpg b/src/assets/img/feature-performance.jpg
|
||
new file mode 100644
|
||
index 0000000..f7209e7
|
||
Binary files /dev/null and b/src/assets/img/feature-performance.jpg differ
|
||
diff --git a/src/assets/img/hero-1.jpg b/src/assets/img/hero-1.jpg
|
||
new file mode 100644
|
||
index 0000000..edaa600
|
||
Binary files /dev/null and b/src/assets/img/hero-1.jpg differ
|
||
diff --git a/src/assets/img/info-balance.jpg b/src/assets/img/info-balance.jpg
|
||
new file mode 100644
|
||
index 0000000..57ffdac
|
||
Binary files /dev/null and b/src/assets/img/info-balance.jpg differ
|
||
diff --git a/src/assets/img/info-longevity.jpg b/src/assets/img/info-longevity.jpg
|
||
new file mode 100644
|
||
index 0000000..e7b194d
|
||
Binary files /dev/null and b/src/assets/img/info-longevity.jpg differ
|
||
diff --git a/src/assets/img/info-performance.jpg b/src/assets/img/info-performance.jpg
|
||
new file mode 100644
|
||
index 0000000..bdc360c
|
||
Binary files /dev/null and b/src/assets/img/info-performance.jpg differ
|
||
diff --git a/src/assets/img/method-step-1.jpg b/src/assets/img/method-step-1.jpg
|
||
new file mode 100644
|
||
index 0000000..3c3748d
|
||
Binary files /dev/null and b/src/assets/img/method-step-1.jpg differ
|
||
diff --git a/src/assets/img/method-step-2.jpg b/src/assets/img/method-step-2.jpg
|
||
new file mode 100644
|
||
index 0000000..81dfb8a
|
||
Binary files /dev/null and b/src/assets/img/method-step-2.jpg differ
|
||
diff --git a/src/assets/img/method-step-3.jpg b/src/assets/img/method-step-3.jpg
|
||
new file mode 100644
|
||
index 0000000..d8e7b96
|
||
Binary files /dev/null and b/src/assets/img/method-step-3.jpg differ
|
||
diff --git a/src/assets/img/method-step-4.jpg b/src/assets/img/method-step-4.jpg
|
||
new file mode 100644
|
||
index 0000000..ec6fc69
|
||
Binary files /dev/null and b/src/assets/img/method-step-4.jpg differ
|
||
diff --git a/src/assets/img/partner-aon.png b/src/assets/img/partner-aon.png
|
||
new file mode 100644
|
||
index 0000000..a9e8c2c
|
||
Binary files /dev/null and b/src/assets/img/partner-aon.png differ
|
||
diff --git a/src/assets/img/partner-edenred.png b/src/assets/img/partner-edenred.png
|
||
new file mode 100644
|
||
index 0000000..c409f48
|
||
Binary files /dev/null and b/src/assets/img/partner-edenred.png differ
|
||
diff --git a/src/assets/img/partner-howden.png b/src/assets/img/partner-howden.png
|
||
new file mode 100644
|
||
index 0000000..3771321
|
||
Binary files /dev/null and b/src/assets/img/partner-howden.png differ
|
||
diff --git a/src/assets/img/partner-welbee.png b/src/assets/img/partner-welbee.png
|
||
new file mode 100644
|
||
index 0000000..b1c188b
|
||
Binary files /dev/null and b/src/assets/img/partner-welbee.png differ
|
||
diff --git a/src/assets/img/quote-bg.jpg b/src/assets/img/quote-bg.jpg
|
||
new file mode 100644
|
||
index 0000000..e5ab31d
|
||
Binary files /dev/null and b/src/assets/img/quote-bg.jpg differ
|
||
diff --git a/src/assets/img/team-antonello.jpg b/src/assets/img/team-antonello.jpg
|
||
new file mode 100644
|
||
index 0000000..0c8a08c
|
||
Binary files /dev/null and b/src/assets/img/team-antonello.jpg differ
|
||
diff --git a/src/assets/img/team-donata.jpg b/src/assets/img/team-donata.jpg
|
||
new file mode 100644
|
||
index 0000000..71ddb49
|
||
Binary files /dev/null and b/src/assets/img/team-donata.jpg differ
|
||
diff --git a/src/assets/img/team-eleonora.jpg b/src/assets/img/team-eleonora.jpg
|
||
new file mode 100644
|
||
index 0000000..003469f
|
||
Binary files /dev/null and b/src/assets/img/team-eleonora.jpg differ
|
||
diff --git a/src/assets/img/team-nicola.jpg b/src/assets/img/team-nicola.jpg
|
||
new file mode 100644
|
||
index 0000000..2e0befd
|
||
Binary files /dev/null and b/src/assets/img/team-nicola.jpg differ
|
||
diff --git a/src/assets/img/training-fit-remote.jpg b/src/assets/img/training-fit-remote.jpg
|
||
new file mode 100644
|
||
index 0000000..c7602b4
|
||
Binary files /dev/null and b/src/assets/img/training-fit-remote.jpg differ
|
||
diff --git a/src/assets/img/training-one-to-one.jpg b/src/assets/img/training-one-to-one.jpg
|
||
new file mode 100644
|
||
index 0000000..6a81d1f
|
||
Binary files /dev/null and b/src/assets/img/training-one-to-one.jpg differ
|
||
diff --git a/src/assets/img/training-performance-class.jpg b/src/assets/img/training-performance-class.jpg
|
||
new file mode 100644
|
||
index 0000000..434a95f
|
||
Binary files /dev/null and b/src/assets/img/training-performance-class.jpg differ
|
||
diff --git a/src/assets/img/training-small-groups.jpg b/src/assets/img/training-small-groups.jpg
|
||
new file mode 100644
|
||
index 0000000..885e2b7
|
||
Binary files /dev/null and b/src/assets/img/training-small-groups.jpg differ
|
||
diff --git a/src/components/Agenda.astro b/src/components/Agenda.astro
|
||
new file mode 100644
|
||
index 0000000..116b0c0
|
||
--- /dev/null
|
||
+++ b/src/components/Agenda.astro
|
||
@@ -0,0 +1,54 @@
|
||
+---
|
||
+import { agenda, disciplines, days, slots } from '../data/agenda';
|
||
+---
|
||
+<section class="section section--dark agenda">
|
||
+ <div class="container">
|
||
+ <div class="section-heading"><h2>Agenda</h2></div>
|
||
+ <div class="agenda__tabs" role="tablist">
|
||
+ {disciplines.map((d, i) => (
|
||
+ <button class:list={['agenda__tab', { 'is-active': i === 0 }]} data-filter={d} role="tab" aria-selected={i === 0 ? 'true' : 'false'}>{d}</button>
|
||
+ ))}
|
||
+ </div>
|
||
+ <div class="agenda__grid">
|
||
+ <div class="agenda__corner"></div>
|
||
+ {days.map((d) => <div class="agenda__day">{d}</div>)}
|
||
+ {slots.map((slot) => (
|
||
+ <>
|
||
+ <div class="agenda__slot">{slot}</div>
|
||
+ {days.map((_, dayIdx) => {
|
||
+ const entry = agenda.find((e) => e.day === dayIdx && e.slot === slot);
|
||
+ return entry
|
||
+ ? <div class="agenda__cell" data-discipline={entry.discipline}><strong>{entry.discipline}</strong><span>{entry.time}</span><em>{entry.trainer}</em></div>
|
||
+ : <div class="agenda__cell agenda__cell--empty"></div>;
|
||
+ })}
|
||
+ </>
|
||
+ ))}
|
||
+ </div>
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .agenda__tabs { display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; margin-bottom: 40px; }
|
||
+ .agenda__tab { background: transparent; color: #b9aea4; border: 0; cursor: pointer; font-family: var(--font-heading); font-size: .68rem; letter-spacing: .18em; text-transform: uppercase; padding: 10px 18px; }
|
||
+ .agenda__tab.is-active { background: var(--c-accent); color: #fff; }
|
||
+ .agenda__grid { display: grid; grid-template-columns: 70px repeat(7, 1fr); gap: 2px; }
|
||
+ .agenda__day, .agenda__slot { font-family: var(--font-heading); font-size: .66rem; letter-spacing: .14em; text-transform: uppercase; color: #b9aea4; display: flex; align-items: center; justify-content: center; padding: 10px 4px; }
|
||
+ .agenda__cell { background: var(--c-accent); color: #fff; padding: 14px 8px; text-align: center; font-size: .72rem; display: flex; flex-direction: column; gap: 2px; }
|
||
+ .agenda__cell strong { font-family: var(--font-heading); font-size: .7rem; letter-spacing: .1em; text-transform: uppercase; }
|
||
+ .agenda__cell em { font-style: normal; opacity: .85; }
|
||
+ .agenda__cell--empty { background: transparent; }
|
||
+ .agenda__cell.is-dim { opacity: .15; }
|
||
+ @media (max-width: 991px) { .agenda__grid { grid-template-columns: 60px repeat(7, minmax(90px, 1fr)); overflow-x: auto; } }
|
||
+</style>
|
||
+
|
||
+<script>
|
||
+ document.querySelectorAll<HTMLElement>('.agenda').forEach((root) => {
|
||
+ const tabs = root.querySelectorAll<HTMLButtonElement>('.agenda__tab');
|
||
+ const cells = root.querySelectorAll<HTMLElement>('.agenda__cell[data-discipline]');
|
||
+ tabs.forEach((tab) => tab.addEventListener('click', () => {
|
||
+ tabs.forEach((t) => { t.classList.toggle('is-active', t === tab); t.setAttribute('aria-selected', String(t === tab)); });
|
||
+ const f = tab.dataset.filter!;
|
||
+ cells.forEach((c) => c.classList.toggle('is-dim', f !== 'Events' && c.dataset.discipline !== f));
|
||
+ }));
|
||
+ });
|
||
+</script>
|
||
diff --git a/src/components/ContactForm.astro b/src/components/ContactForm.astro
|
||
new file mode 100644
|
||
index 0000000..fec0282
|
||
--- /dev/null
|
||
+++ b/src/components/ContactForm.astro
|
||
@@ -0,0 +1,42 @@
|
||
+---
|
||
+interface Props { variant?: 'full' | 'footer' }
|
||
+const { variant = 'full' } = Astro.props;
|
||
+---
|
||
+<form class:list={['cform', `cform--${variant}`]} method="post" action="/api/contact" novalidate>
|
||
+ <input class="field" name="firstName" placeholder="Nome" required maxlength="100" />
|
||
+ {variant === 'full' && <input class="field" name="lastName" placeholder="Cognome" maxlength="100" />}
|
||
+ {variant === 'full' && <input class="field" name="phone" placeholder="Telefono" maxlength="40" />}
|
||
+ <input class="field" type="email" name="email" placeholder="E-mail" required maxlength="200" />
|
||
+ <textarea class="field" name="message" placeholder="Messaggio" required maxlength="5000"></textarea>
|
||
+ <input class="hp" type="text" name="website" tabindex="-1" autocomplete="off" />
|
||
+ <button class="btn" type="submit">Invia messaggio</button>
|
||
+ <p class="form-msg" hidden></p>
|
||
+</form>
|
||
+
|
||
+<script>
|
||
+ document.querySelectorAll<HTMLFormElement>('.cform').forEach((form) => {
|
||
+ form.addEventListener('submit', async (e) => {
|
||
+ e.preventDefault();
|
||
+ const msg = form.querySelector<HTMLParagraphElement>('.form-msg')!;
|
||
+ const btn = form.querySelector<HTMLButtonElement>('button')!;
|
||
+ btn.disabled = true;
|
||
+ msg.hidden = true;
|
||
+ try {
|
||
+ const res = await fetch('/api/contact', {
|
||
+ method: 'POST',
|
||
+ headers: { 'Content-Type': 'application/json' },
|
||
+ body: JSON.stringify(Object.fromEntries(new FormData(form))),
|
||
+ });
|
||
+ const data = await res.json();
|
||
+ msg.textContent = res.ok ? 'Messaggio inviato, ti ricontatteremo al più presto.' : (data.error ?? 'Errore di invio, riprova più tardi.');
|
||
+ msg.className = `form-msg ${res.ok ? 'form-msg--ok' : 'form-msg--err'}`;
|
||
+ if (res.ok) form.reset();
|
||
+ } catch {
|
||
+ msg.textContent = 'Errore di rete, riprova più tardi.';
|
||
+ msg.className = 'form-msg form-msg--err';
|
||
+ }
|
||
+ msg.hidden = false;
|
||
+ btn.disabled = false;
|
||
+ });
|
||
+ });
|
||
+</script>
|
||
diff --git a/src/components/Footer.astro b/src/components/Footer.astro
|
||
new file mode 100644
|
||
index 0000000..1546466
|
||
--- /dev/null
|
||
+++ b/src/components/Footer.astro
|
||
@@ -0,0 +1,49 @@
|
||
+---
|
||
+import { site } from '../data/site';
|
||
+import ContactForm from './ContactForm.astro';
|
||
+const year = new Date().getFullYear();
|
||
+---
|
||
+<footer class="ftr">
|
||
+ <div class="container ftr__grid">
|
||
+ <div>
|
||
+ <img src="/img/logo-light.png" alt="InsanityLab" width="140" loading="lazy" />
|
||
+ <p class="ftr__tag">PERFORMANCE. BALANCE. LONGEVITY.</p>
|
||
+ <p>Promuoviamo il movimento come cura di sé. Abitudini sostenibili per benessere fisico e mentale.</p>
|
||
+ <p>{site.phone}<br />{site.address}, {site.cityLine}</p>
|
||
+ {site.hours.map((h) => <p class="ftr__hours">{h}</p>)}
|
||
+ </div>
|
||
+ <nav aria-label="training">
|
||
+ <h4>Training</h4>
|
||
+ {site.footerTraining.map((l) => <a href={l.href}>{l.label}</a>)}
|
||
+ </nav>
|
||
+ <nav aria-label="programmi">
|
||
+ <h4>Programmi</h4>
|
||
+ {site.footerPrograms.map((l) => <a href={l.href}>{l.label}</a>)}
|
||
+ </nav>
|
||
+ <div>
|
||
+ <h4>Contact</h4>
|
||
+ <ContactForm variant="footer" />
|
||
+ </div>
|
||
+ </div>
|
||
+ <div class="ftr__bar">
|
||
+ <div class="container ftr__bar-in">
|
||
+ <p>© {year} <strong>Insanitylab</strong>, All Rights Reserved</p>
|
||
+ <p>Seguici {site.socials.map((s) => <a href={s.url} rel="noopener" target="_blank">{s.label}</a>)}</p>
|
||
+ </div>
|
||
+ </div>
|
||
+</footer>
|
||
+
|
||
+<style>
|
||
+ .ftr { background: var(--c-dark); color: #b9aea4; font-size: .88rem; }
|
||
+ .ftr a { color: #b9aea4; text-decoration: none; display: block; padding: 4px 0; }
|
||
+ .ftr a:hover { color: #fff; }
|
||
+ .ftr h4 { color: #fff; text-transform: uppercase; letter-spacing: .18em; font-size: .78rem; margin-bottom: 18px; }
|
||
+ .ftr__grid { display: grid; grid-template-columns: 1.3fr 1fr 1fr 1.3fr; gap: 40px; padding-block: 70px; }
|
||
+ .ftr__tag { font-family: var(--font-heading); font-size: .7rem; letter-spacing: .2em; color: #fff; }
|
||
+ .ftr__hours { margin: 0; }
|
||
+ .ftr__bar { border-top: 1px solid rgba(255,255,255,.08); padding-block: 18px; }
|
||
+ .ftr__bar-in { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px; }
|
||
+ .ftr__bar-in a { display: inline; margin-left: 12px; }
|
||
+ @media (max-width: 991px) { .ftr__grid { grid-template-columns: 1fr 1fr; } }
|
||
+ @media (max-width: 600px) { .ftr__grid { grid-template-columns: 1fr; } }
|
||
+</style>
|
||
diff --git a/src/components/Header.astro b/src/components/Header.astro
|
||
new file mode 100644
|
||
index 0000000..7c62429
|
||
--- /dev/null
|
||
+++ b/src/components/Header.astro
|
||
@@ -0,0 +1,45 @@
|
||
+---
|
||
+import { site } from '../data/site';
|
||
+const path = Astro.url.pathname;
|
||
+---
|
||
+<header class="hdr" id="site-header">
|
||
+ <div class="container hdr__in">
|
||
+ <a href="/" class="hdr__logo"><img src="/img/logo-dark.png" alt="InsanityLab" width="150" /></a>
|
||
+ <nav class="hdr__nav" id="site-nav" aria-label="principale">
|
||
+ {site.navigation.map((item) => (
|
||
+ <a href={item.href} class:list={['hdr__link', { 'is-active': path === item.href || (item.href !== '/' && path.startsWith(item.href)) }]}>{item.label}</a>
|
||
+ ))}
|
||
+ </nav>
|
||
+ <button class="hdr__burger" id="nav-toggle" aria-label="Apri menu" aria-expanded="false">
|
||
+ <span></span><span></span><span></span>
|
||
+ </button>
|
||
+ </div>
|
||
+</header>
|
||
+
|
||
+<style>
|
||
+ .hdr { position: sticky; top: 0; z-index: 100; background: #fff; transition: box-shadow .2s; }
|
||
+ .hdr.is-scrolled { box-shadow: 0 2px 14px rgba(0,0,0,.08); }
|
||
+ .hdr__in { display: flex; align-items: center; justify-content: space-between; height: var(--header-h); }
|
||
+ .hdr__nav { display: flex; gap: 34px; }
|
||
+ .hdr__link { font-family: var(--font-heading); font-size: .72rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; text-decoration: none; color: var(--c-heading); }
|
||
+ .hdr__link:hover, .hdr__link.is-active { color: var(--c-accent-dark); }
|
||
+ .hdr__burger { display: none; background: none; border: 0; cursor: pointer; padding: 8px; }
|
||
+ .hdr__burger span { display: block; width: 22px; height: 2px; background: var(--c-heading); margin: 5px 0; }
|
||
+ @media (max-width: 991px) {
|
||
+ .hdr__nav { display: none; position: absolute; top: var(--header-h); left: 0; right: 0; background: #fff; flex-direction: column; gap: 0; padding: 10px 20px 20px; box-shadow: 0 10px 20px rgba(0,0,0,.08); }
|
||
+ .hdr__nav.is-open { display: flex; }
|
||
+ .hdr__link { padding: 12px 0; }
|
||
+ .hdr__burger { display: block; }
|
||
+ }
|
||
+</style>
|
||
+
|
||
+<script>
|
||
+ const header = document.getElementById('site-header')!;
|
||
+ const toggle = document.getElementById('nav-toggle')!;
|
||
+ const nav = document.getElementById('site-nav')!;
|
||
+ addEventListener('scroll', () => header.classList.toggle('is-scrolled', scrollY > 10), { passive: true });
|
||
+ toggle.addEventListener('click', () => {
|
||
+ const open = nav.classList.toggle('is-open');
|
||
+ toggle.setAttribute('aria-expanded', String(open));
|
||
+ });
|
||
+</script>
|
||
diff --git a/src/components/PageHero.astro b/src/components/PageHero.astro
|
||
new file mode 100644
|
||
index 0000000..7a4529f
|
||
--- /dev/null
|
||
+++ b/src/components/PageHero.astro
|
||
@@ -0,0 +1,15 @@
|
||
+---
|
||
+interface Props { title: string; eyebrow?: string }
|
||
+const { title, eyebrow } = Astro.props;
|
||
+---
|
||
+<section class="phero">
|
||
+ <div class="container">
|
||
+ {eyebrow && <p class="eyebrow">{eyebrow}</p>}
|
||
+ <h1>{title}</h1>
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .phero { background: var(--c-bg-alt); padding-block: 70px; }
|
||
+ .phero h1 { text-transform: lowercase; font-weight: 500; margin: 0; }
|
||
+</style>
|
||
diff --git a/src/components/about/Timeline.astro b/src/components/about/Timeline.astro
|
||
new file mode 100644
|
||
index 0000000..2eb91dc
|
||
--- /dev/null
|
||
+++ b/src/components/about/Timeline.astro
|
||
@@ -0,0 +1,34 @@
|
||
+---
|
||
+const openText = `Il percorso di crescita di InsanityLab si è sviluppato attraverso tappe precise e coerenti con la visione iniziale. Nato nel 2021 con un piccolo team e grandi ambizioni, il progetto ha conosciuto una forte evoluzione nel 2024, anno in cui il servizio si è ampliato attraverso nuovi format come gli small group e una sempre maggiore specializzazione nella profilazione fisica del cliente, grazie a sistemi avanzati di valutazione e monitoraggio. Parallelamente, anche l'area dedicata alla riabilitazione e all'esercizio preventivo si è consolidata, ponendo le basi per un importante salto di qualità.`;
|
||
+const stages = [
|
||
+ { label: 'INSANITYLAB 2025', text: 'Contenuto in arrivo: il cliente fornirà il testo di questa tappa.' },
|
||
+ { label: 'INSANITYLAB 2026', text: 'Contenuto in arrivo: il cliente fornirà il testo di questa tappa.' },
|
||
+ { label: 'INSANITYLAB OGGI', text: 'Contenuto in arrivo: il cliente fornirà il testo di questa tappa.' },
|
||
+];
|
||
+---
|
||
+<section class="section">
|
||
+ <div class="container">
|
||
+ <div class="section-heading">
|
||
+ <h2>Storylab</h2>
|
||
+ <p>Scopri la nostra storia: come nasce InsanityLab, perché abbiamo deciso di creare qualcosa di diverso e chi sono i visionari che hanno trasformato questa idea in un nuovo modo di vivere benessere, movimento e salute.</p>
|
||
+ </div>
|
||
+ <div class="tl">
|
||
+ <h3 class="tl__label">INSANITYLAB 2021-2024</h3>
|
||
+ <p>{openText}</p>
|
||
+ {stages.map((s) => (
|
||
+ <details class="tl__stage">
|
||
+ <summary>+ {s.label}</summary>
|
||
+ <p>{s.text}</p>
|
||
+ </details>
|
||
+ ))}
|
||
+ </div>
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .tl { max-width: 720px; margin-inline: auto; }
|
||
+ .tl__label { text-transform: uppercase; letter-spacing: .16em; font-size: .9rem; }
|
||
+ .tl__stage { border-top: 1px solid #e4ded6; padding-block: 14px; }
|
||
+ .tl__stage summary { cursor: pointer; font-family: var(--font-heading); font-size: .82rem; letter-spacing: .16em; text-transform: uppercase; color: var(--c-heading); list-style: none; }
|
||
+ .tl__stage summary::-webkit-details-marker { display: none; }
|
||
+</style>
|
||
diff --git a/src/components/admin/PostForm.astro b/src/components/admin/PostForm.astro
|
||
new file mode 100644
|
||
index 0000000..4ffdf2a
|
||
--- /dev/null
|
||
+++ b/src/components/admin/PostForm.astro
|
||
@@ -0,0 +1,136 @@
|
||
+---
|
||
+import { CATEGORIES } from '../../lib/blog-const';
|
||
+import type { Post } from '../../lib/posts';
|
||
+interface Props { post?: Post }
|
||
+const { post } = Astro.props;
|
||
+---
|
||
+<form id="post-form" data-id={post?.id ?? ''}>
|
||
+ <input class="afield" name="title" placeholder="Titolo" required value={post?.title ?? ''} />
|
||
+ <input class="afield" name="slug" placeholder="Slug (vuoto = generato dal titolo)" value={post?.slug ?? ''} />
|
||
+ <select class="afield" name="category">
|
||
+ {CATEGORIES.map((c) => <option value={c} selected={post?.category === c}>{c}</option>)}
|
||
+ </select>
|
||
+ <textarea class="afield" name="excerpt" placeholder="Estratto (anteprima in lista blog)" maxlength="300">{post?.excerpt ?? ''}</textarea>
|
||
+
|
||
+ <label>Copertina: <input type="file" id="cover-file" accept="image/*" /></label>
|
||
+ <input type="hidden" name="cover" value={post?.cover ?? ''} />
|
||
+ <p id="cover-preview">{post?.cover && <img src={post.cover} style="max-width:200px" />}</p>
|
||
+
|
||
+ <div class="ed-toolbar" id="ed-toolbar">
|
||
+ <button type="button" data-cmd="h2">H2</button>
|
||
+ <button type="button" data-cmd="h3">H3</button>
|
||
+ <button type="button" data-cmd="bold"><b>B</b></button>
|
||
+ <button type="button" data-cmd="italic"><i>I</i></button>
|
||
+ <button type="button" data-cmd="bullet">• Elenco</button>
|
||
+ <button type="button" data-cmd="ordered">1. Elenco</button>
|
||
+ <button type="button" data-cmd="quote">" Citazione</button>
|
||
+ <button type="button" data-cmd="link">Link</button>
|
||
+ <button type="button" data-cmd="image">Immagine</button>
|
||
+ </div>
|
||
+ <div id="editor" class="ed-body"></div>
|
||
+
|
||
+ <label><input type="checkbox" name="draft" checked={post ? post.draft === 1 : true} /> Bozza (non visibile sul sito)</label>
|
||
+ <p><button class="abtn" type="submit">Salva</button> <span id="save-msg"></span></p>
|
||
+</form>
|
||
+
|
||
+<style>
|
||
+ .ed-toolbar { display: flex; gap: 6px; flex-wrap: wrap; background: #fff; border: 1px solid #ccc; border-bottom: 0; padding: 8px; }
|
||
+ .ed-toolbar button { background: #eee; border: 1px solid #ccc; padding: 4px 10px; cursor: pointer; }
|
||
+ .ed-toolbar button.is-active { background: #b5a48b; color: #fff; }
|
||
+ .ed-body { background: #fff; border: 1px solid #ccc; min-height: 340px; padding: 12px 16px; margin-bottom: 14px; }
|
||
+ .ed-body :global(.ProseMirror) { outline: none; min-height: 320px; }
|
||
+</style>
|
||
+
|
||
+<script>
|
||
+ import { Editor } from '@tiptap/core';
|
||
+ import StarterKit from '@tiptap/starter-kit';
|
||
+ import Image from '@tiptap/extension-image';
|
||
+ import Link from '@tiptap/extension-link';
|
||
+
|
||
+ const form = document.getElementById('post-form') as HTMLFormElement;
|
||
+ const initial = (document.getElementById('initial-body') as HTMLScriptElement | null)?.textContent ?? '';
|
||
+
|
||
+ const editor = new Editor({
|
||
+ element: document.getElementById('editor')!,
|
||
+ extensions: [
|
||
+ StarterKit.configure({ heading: { levels: [2, 3] } }),
|
||
+ Image,
|
||
+ Link.configure({ openOnClick: false }),
|
||
+ ],
|
||
+ content: initial,
|
||
+ });
|
||
+
|
||
+ async function uploadFile(file: File): Promise<string | null> {
|
||
+ const fd = new FormData();
|
||
+ fd.append('file', file);
|
||
+ const res = await fetch('/api/admin/upload', { method: 'POST', body: fd });
|
||
+ if (!res.ok) { alert('Upload non riuscito.'); return null; }
|
||
+ return (await res.json()).url;
|
||
+ }
|
||
+
|
||
+ document.getElementById('ed-toolbar')!.addEventListener('click', async (e) => {
|
||
+ const btn = (e.target as HTMLElement).closest('button');
|
||
+ if (!btn) return;
|
||
+ const c = editor.chain().focus();
|
||
+ switch (btn.dataset.cmd) {
|
||
+ case 'h2': c.toggleHeading({ level: 2 }).run(); break;
|
||
+ case 'h3': c.toggleHeading({ level: 3 }).run(); break;
|
||
+ case 'bold': c.toggleBold().run(); break;
|
||
+ case 'italic': c.toggleItalic().run(); break;
|
||
+ case 'bullet': c.toggleBulletList().run(); break;
|
||
+ case 'ordered': c.toggleOrderedList().run(); break;
|
||
+ case 'quote': c.toggleBlockquote().run(); break;
|
||
+ case 'link': {
|
||
+ const url = prompt('URL del link:');
|
||
+ if (url) c.setLink({ href: url }).run();
|
||
+ break;
|
||
+ }
|
||
+ case 'image': {
|
||
+ const input = document.createElement('input');
|
||
+ input.type = 'file'; input.accept = 'image/*';
|
||
+ input.onchange = async () => {
|
||
+ const url = input.files?.[0] && await uploadFile(input.files[0]);
|
||
+ if (url) editor.chain().focus().setImage({ src: url }).run();
|
||
+ };
|
||
+ input.click();
|
||
+ break;
|
||
+ }
|
||
+ }
|
||
+ });
|
||
+
|
||
+ document.getElementById('cover-file')!.addEventListener('change', async (e) => {
|
||
+ const file = (e.target as HTMLInputElement).files?.[0];
|
||
+ if (!file) return;
|
||
+ const url = await uploadFile(file);
|
||
+ if (url) {
|
||
+ (form.elements.namedItem('cover') as HTMLInputElement).value = url;
|
||
+ document.getElementById('cover-preview')!.innerHTML = `<img src="${url}" style="max-width:200px">`;
|
||
+ }
|
||
+ });
|
||
+
|
||
+ form.addEventListener('submit', async (e) => {
|
||
+ e.preventDefault();
|
||
+ const id = form.dataset.id;
|
||
+ const body = {
|
||
+ title: (form.elements.namedItem('title') as HTMLInputElement).value,
|
||
+ slug: (form.elements.namedItem('slug') as HTMLInputElement).value,
|
||
+ category: (form.elements.namedItem('category') as HTMLSelectElement).value,
|
||
+ excerpt: (form.elements.namedItem('excerpt') as HTMLTextAreaElement).value,
|
||
+ cover: (form.elements.namedItem('cover') as HTMLInputElement).value || null,
|
||
+ draft: (form.elements.namedItem('draft') as HTMLInputElement).checked,
|
||
+ body_html: editor.getHTML(),
|
||
+ };
|
||
+ const res = await fetch(id ? `/api/admin/posts/${id}` : '/api/admin/posts', {
|
||
+ method: id ? 'PUT' : 'POST',
|
||
+ headers: { 'Content-Type': 'application/json' },
|
||
+ body: JSON.stringify(body),
|
||
+ });
|
||
+ const msg = document.getElementById('save-msg')!;
|
||
+ if (res.ok) {
|
||
+ msg.textContent = 'Salvato ✓';
|
||
+ if (!id) location.href = '/admin';
|
||
+ } else {
|
||
+ msg.textContent = (await res.json()).error ?? 'Errore di salvataggio';
|
||
+ }
|
||
+ });
|
||
+</script>
|
||
diff --git a/src/components/blog/Sidebar.astro b/src/components/blog/Sidebar.astro
|
||
new file mode 100644
|
||
index 0000000..a89dcc5
|
||
--- /dev/null
|
||
+++ b/src/components/blog/Sidebar.astro
|
||
@@ -0,0 +1,26 @@
|
||
+---
|
||
+import { getDb } from '../../lib/db';
|
||
+import { listRecent, listArchiveMonths } from '../../lib/posts';
|
||
+import { CATEGORIES } from '../../lib/blog-const';
|
||
+const db = getDb();
|
||
+const recent = listRecent(db, 4);
|
||
+const months = listArchiveMonths(db);
|
||
+const monthLabel = (m: string) => new Intl.DateTimeFormat('it-IT', { month: 'long', year: 'numeric' }).format(new Date(`${m}-01`));
|
||
+---
|
||
+<aside class="bside">
|
||
+ <h3>Categorie</h3>
|
||
+ {CATEGORIES.map((c) => <p><a href={`/blog?categoria=${encodeURIComponent(c)}`}>{c}</a></p>)}
|
||
+ <h3>Ultimi articoli</h3>
|
||
+ {recent.map((p) => (
|
||
+ <p><a href={`/blog/${p.slug}`}>{p.title}</a></p>
|
||
+ ))}
|
||
+ <h3>Archivio</h3>
|
||
+ {months.map((m) => <p class="bside__month">{monthLabel(m.month)} ({m.count})</p>)}
|
||
+</aside>
|
||
+
|
||
+<style>
|
||
+ .bside h3 { font-size: .85rem; text-transform: uppercase; letter-spacing: .16em; margin-top: 30px; }
|
||
+ .bside a { text-decoration: none; color: var(--c-text); }
|
||
+ .bside a:hover { color: var(--c-accent-dark); }
|
||
+ .bside__month { text-transform: capitalize; }
|
||
+</style>
|
||
diff --git a/src/components/home/FeatureBlocks.astro b/src/components/home/FeatureBlocks.astro
|
||
new file mode 100644
|
||
index 0000000..914f7fd
|
||
--- /dev/null
|
||
+++ b/src/components/home/FeatureBlocks.astro
|
||
@@ -0,0 +1,35 @@
|
||
+---
|
||
+import { Image } from 'astro:assets';
|
||
+import featPerformance from '../../assets/img/feature-performance.jpg';
|
||
+import featBalance from '../../assets/img/feature-balance.jpg';
|
||
+import featLongevity from '../../assets/img/feature-longevity.jpg';
|
||
+const blocks = [
|
||
+ { title: 'PERFORMANCE', image: featPerformance, reverse: false,
|
||
+ text: 'Allenamenti progettati su misura per adattarsi ai tuoi obiettivi e al tuo livello. Programmi mirati che sviluppano forza, resistenza e funzionalità in modo equilibrato. Un metodo efficace per migliorare le performance e ottenere risultati concreti e duraturi.' },
|
||
+ { title: 'BALANCE', image: featBalance, reverse: true,
|
||
+ text: 'L’equilibrio tra corpo e mente è la base di ogni performance duratura. Attraverso movimento, recupero e gestione dello stress, costruiamo stabilità e controllo. Un approccio integrato che sostiene benessere, energia e continuità nel tempo.' },
|
||
+ { title: 'LONGEVITY', image: featLongevity, reverse: false,
|
||
+ text: 'Benessere completo tra movimento, nutrizione e stile di vita. Un percorso personalizzato che unisce prevenzione, recupero ed equilibrio. Investi nella tua salute e qualità della vita nel tempo.' },
|
||
+];
|
||
+---
|
||
+<section class="section">
|
||
+ <div class="container">
|
||
+ {blocks.map((b) => (
|
||
+ <div class:list={['feat', { 'feat--reverse': b.reverse }]}>
|
||
+ <Image src={b.image} alt={b.title} class="feat__img" widths={[500, 900]} sizes="(max-width: 767px) 100vw, 45vw" />
|
||
+ <div class="feat__text">
|
||
+ <h2>{b.title}</h2>
|
||
+ <p>{b.text}</p>
|
||
+ <a class="btn" href="/programs">Scopri</a>
|
||
+ </div>
|
||
+ </div>
|
||
+ ))}
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .feat { display: grid; grid-template-columns: 1fr 1fr; gap: 60px; align-items: center; padding-block: 40px; }
|
||
+ .feat--reverse .feat__img { order: 2; }
|
||
+ .feat__text h2 { text-transform: uppercase; letter-spacing: .1em; }
|
||
+ @media (max-width: 767px) { .feat { grid-template-columns: 1fr; gap: 24px; } .feat--reverse .feat__img { order: 0; } }
|
||
+</style>
|
||
diff --git a/src/components/home/HeroSlider.astro b/src/components/home/HeroSlider.astro
|
||
new file mode 100644
|
||
index 0000000..75f3e9d
|
||
--- /dev/null
|
||
+++ b/src/components/home/HeroSlider.astro
|
||
@@ -0,0 +1,54 @@
|
||
+---
|
||
+import { Image } from 'astro:assets';
|
||
+import hero1 from '../../assets/img/hero-1.jpg';
|
||
+const slides = [
|
||
+ { title: ['PERFORMANCE.', 'BALANCE.', 'LONGEVITY.'], image: hero1, alt: 'Atleti InsanityLab' },
|
||
+];
|
||
+---
|
||
+<section class="hero" aria-label="presentazione">
|
||
+ {slides.map((s, i) => (
|
||
+ <div class:list={['hero__slide', { 'is-active': i === 0 }]}>
|
||
+ <div class="container hero__in">
|
||
+ <div class="hero__text">
|
||
+ <h1>{s.title.map((line) => <span class="hero__line">{line}</span>)}</h1>
|
||
+ <a class="btn" href="/programs">Scopri</a>
|
||
+ </div>
|
||
+ <Image src={s.image} alt={s.alt} class="hero__img" widths={[600, 1000, 1400]} sizes="(max-width: 991px) 100vw, 60vw" loading="eager" />
|
||
+ </div>
|
||
+ </div>
|
||
+ ))}
|
||
+ {slides.length > 1 && (
|
||
+ <>
|
||
+ <button class="hero__arrow hero__arrow--prev" aria-label="precedente">‹</button>
|
||
+ <button class="hero__arrow hero__arrow--next" aria-label="successiva">›</button>
|
||
+ </>
|
||
+ )}
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .hero { position: relative; background:
|
||
+ repeating-linear-gradient(to bottom, transparent 0 10px, rgba(0,0,0,.05) 10px 11px) left top / 40% 60% no-repeat, #fff; }
|
||
+ .hero__slide { display: none; }
|
||
+ .hero__slide.is-active { display: block; }
|
||
+ .hero__in { display: grid; grid-template-columns: 1fr 1.2fr; align-items: center; min-height: calc(100vh - var(--header-h)); gap: 30px; }
|
||
+ .hero__line { display: block; }
|
||
+ .hero__text h1 { font-weight: 500; }
|
||
+ .hero__img { object-fit: cover; height: 100%; max-height: calc(100vh - var(--header-h)); width: 100%; }
|
||
+ .hero__arrow { position: absolute; top: 50%; transform: translateY(-50%); background: none; border: 0; font-size: 3rem; color: var(--c-heading); cursor: pointer; }
|
||
+ .hero__arrow--prev { left: 12px; } .hero__arrow--next { right: 12px; }
|
||
+ @media (max-width: 991px) { .hero__in { grid-template-columns: 1fr; padding-block: 40px; min-height: 0; } }
|
||
+</style>
|
||
+
|
||
+<script>
|
||
+ const slides = document.querySelectorAll('.hero__slide');
|
||
+ if (slides.length > 1) {
|
||
+ let cur = 0;
|
||
+ const show = (i: number) => {
|
||
+ slides[cur].classList.remove('is-active');
|
||
+ cur = (i + slides.length) % slides.length;
|
||
+ slides[cur].classList.add('is-active');
|
||
+ };
|
||
+ document.querySelector('.hero__arrow--prev')?.addEventListener('click', () => show(cur - 1));
|
||
+ document.querySelector('.hero__arrow--next')?.addEventListener('click', () => show(cur + 1));
|
||
+ }
|
||
+</script>
|
||
diff --git a/src/components/home/InfoColumns.astro b/src/components/home/InfoColumns.astro
|
||
new file mode 100644
|
||
index 0000000..763bc2b
|
||
--- /dev/null
|
||
+++ b/src/components/home/InfoColumns.astro
|
||
@@ -0,0 +1,26 @@
|
||
+---
|
||
+import infoPerformance from '../../assets/img/info-performance.jpg';
|
||
+import infoBalance from '../../assets/img/info-balance.jpg';
|
||
+import infoLongevity from '../../assets/img/info-longevity.jpg';
|
||
+const cols = [
|
||
+ { title: 'PERFORMANCE.', image: infoPerformance },
|
||
+ { title: 'BALANCE.', image: infoBalance },
|
||
+ { title: 'LONGEVITY.', image: infoLongevity },
|
||
+];
|
||
+---
|
||
+<section class="info" aria-label="pilastri">
|
||
+ {cols.map((c) => (
|
||
+ <a class="info__col" href="/programs" style={`background-image:url(${c.image.src})`}>
|
||
+ <span class="info__title">{c.title}</span>
|
||
+ </a>
|
||
+ ))}
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .info { display: grid; grid-template-columns: repeat(3, 1fr); }
|
||
+ .info__col { position: relative; min-height: 320px; background-size: cover; background-position: center; display: flex; align-items: center; justify-content: center; text-decoration: none; }
|
||
+ .info__col::before { content: ''; position: absolute; inset: 0; background: rgba(30,24,20,.35); transition: background .2s; }
|
||
+ .info__col:hover::before { background: rgba(30,24,20,.15); }
|
||
+ .info__title { position: relative; color: #fff; font-family: var(--font-heading); font-weight: 600; letter-spacing: .1em; font-size: 1.1rem; }
|
||
+ @media (max-width: 767px) { .info { grid-template-columns: 1fr; } }
|
||
+</style>
|
||
diff --git a/src/components/home/MethodSteps.astro b/src/components/home/MethodSteps.astro
|
||
new file mode 100644
|
||
index 0000000..dfbbd1a
|
||
--- /dev/null
|
||
+++ b/src/components/home/MethodSteps.astro
|
||
@@ -0,0 +1,39 @@
|
||
+---
|
||
+import { Image } from 'astro:assets';
|
||
+import type { ImageMetadata } from 'astro';
|
||
+import { methodIntro, methodExtraNote, methodSteps } from '../../data/method';
|
||
+const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/method-*.{jpg,png}', { eager: true });
|
||
+const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1].default;
|
||
+---
|
||
+<section class="section section--alt">
|
||
+ <div class="container">
|
||
+ <div class="section-heading">
|
||
+ <h2>Il metodo<br />InsanityLab</h2>
|
||
+ <p set:html={methodIntro.replace('Metodo Insanitylab', '<strong>Metodo Insanitylab</strong>')} />
|
||
+ </div>
|
||
+ <p class="method__note">{methodExtraNote}</p>
|
||
+ <div class="method__row">
|
||
+ {methodSteps.map((s) => (
|
||
+ <div class:list={['method__step', { 'method__step--extra': s.extra }]}>
|
||
+ <div class="method__circle">
|
||
+ {imgOf(s.image) && <Image src={imgOf(s.image)!} alt="" widths={[220]} sizes="220px" />}
|
||
+ <span>{s.n}</span>
|
||
+ </div>
|
||
+ <h3>{s.title}</h3>
|
||
+ <p>{s.text}</p>
|
||
+ </div>
|
||
+ ))}
|
||
+ </div>
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .method__note { text-align: right; font-size: .8rem; color: var(--c-text-light); max-width: 260px; margin-left: auto; }
|
||
+ .method__row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 30px; text-align: center; position: relative; }
|
||
+ .method__circle { position: relative; width: 150px; height: 150px; margin: 0 auto 20px; border: 1px dashed var(--c-accent-dark); border-radius: 50%; padding: 8px; }
|
||
+ .method__circle img { border-radius: 50%; width: 100%; height: 100%; object-fit: cover; }
|
||
+ .method__circle span { position: absolute; top: -6px; left: -6px; font-family: var(--font-heading); font-size: 2.4rem; color: var(--c-heading); background: var(--c-bg-alt); padding: 0 8px; }
|
||
+ .method__step h3 { font-size: .85rem; text-transform: uppercase; letter-spacing: .14em; }
|
||
+ .method__step p { font-size: .85rem; color: var(--c-text-light); }
|
||
+ @media (max-width: 767px) { .method__row { grid-template-columns: 1fr 1fr; } }
|
||
+</style>
|
||
diff --git a/src/components/home/PartnersStrip.astro b/src/components/home/PartnersStrip.astro
|
||
new file mode 100644
|
||
index 0000000..f5e8bf6
|
||
--- /dev/null
|
||
+++ b/src/components/home/PartnersStrip.astro
|
||
@@ -0,0 +1,18 @@
|
||
+---
|
||
+import { Image } from 'astro:assets';
|
||
+import type { ImageMetadata } from 'astro';
|
||
+import { partners } from '../../data/partners';
|
||
+const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/partner-*.{jpg,png}', { eager: true });
|
||
+const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1].default;
|
||
+---
|
||
+<section class="pstrip" aria-label="partner">
|
||
+ <div class="container pstrip__in">
|
||
+ {partners.map((p) => imgOf(p.image) && <Image src={imgOf(p.image)!} alt={p.name} height={44} />)}
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .pstrip { background: var(--c-accent); padding-block: 40px; }
|
||
+ .pstrip__in { display: flex; align-items: center; justify-content: space-around; gap: 30px; flex-wrap: wrap; }
|
||
+ .pstrip img { height: 44px; width: auto; opacity: .9; }
|
||
+</style>
|
||
diff --git a/src/components/home/ProgramsGrid.astro b/src/components/home/ProgramsGrid.astro
|
||
new file mode 100644
|
||
index 0000000..d4c1c98
|
||
--- /dev/null
|
||
+++ b/src/components/home/ProgramsGrid.astro
|
||
@@ -0,0 +1,38 @@
|
||
+---
|
||
+import { programs } from '../../data/programs';
|
||
+const icons: Record<string, string> = {
|
||
+ 'personal-training': 'M4 12h3m10 0h3M9 8v8m6-8v8M7 10v4m10-4v4',
|
||
+ 'performance-class': 'M13 3l-2 7h4l-6 11 2-8H7l4-10z',
|
||
+ 'coaching': 'M12 3a4 4 0 110 8 4 4 0 010-8zm-7 18a7 7 0 0114 0',
|
||
+ 'pilates-flow': 'M12 4a2 2 0 110 4 2 2 0 010-4zM6 20c2-6 10-6 12 0M12 8v6',
|
||
+ 'rehab': 'M12 21C7 17 3 13 3 9a5 5 0 019-3 5 5 0 019 3c0 4-4 8-9 12z',
|
||
+ 'nutrition': 'M12 3c4 0 7 3 7 8s-3 10-7 10-7-5-7-10 3-8 7-8zm0-1v4',
|
||
+};
|
||
+---
|
||
+<section class="section">
|
||
+ <div class="container">
|
||
+ <div class="section-heading">
|
||
+ <h2>Programmi</h2>
|
||
+ <p>Questi sono i programmi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi un’esperienza di allenamento completa e mirata.</p>
|
||
+ </div>
|
||
+ <div class="pgrid">
|
||
+ {programs.map((p) => (
|
||
+ <a class="pgrid__item" href={`/programs/${p.slug}`}>
|
||
+ <svg viewBox="0 0 24 24" fill="none" stroke="var(--c-accent-dark)" stroke-width="1.4" width="52" height="52" aria-hidden="true"><path d={icons[p.slug]} /></svg>
|
||
+ <h3>{p.title}</h3>
|
||
+ <p>{p.subtitle}</p>
|
||
+ </a>
|
||
+ ))}
|
||
+ </div>
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .pgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 50px 40px; text-align: center; }
|
||
+ .pgrid__item { text-decoration: none; }
|
||
+ .pgrid__item svg { margin-inline: auto; }
|
||
+ .pgrid__item h3 { text-transform: uppercase; letter-spacing: .12em; font-size: .95rem; margin: 18px 0 6px; }
|
||
+ .pgrid__item p { font-size: .85rem; color: var(--c-text-light); margin: 0; }
|
||
+ @media (max-width: 767px) { .pgrid { grid-template-columns: 1fr 1fr; } }
|
||
+ @media (max-width: 480px) { .pgrid { grid-template-columns: 1fr; } }
|
||
+</style>
|
||
diff --git a/src/components/home/QuoteBanner.astro b/src/components/home/QuoteBanner.astro
|
||
new file mode 100644
|
||
index 0000000..09a6e06
|
||
--- /dev/null
|
||
+++ b/src/components/home/QuoteBanner.astro
|
||
@@ -0,0 +1,22 @@
|
||
+---
|
||
+import quoteBg from '../../assets/img/quote-bg.jpg';
|
||
+---
|
||
+<section class="quote" style={`background-image:url(${quoteBg.src})`}>
|
||
+ <div class="container quote__in">
|
||
+ <p class="quote__mark">“</p>
|
||
+ <blockquote>
|
||
+ <p><strong>PERFORMANCE</strong> È SPINGERE OLTRE I PROPRI LIMITI, <strong>BALANCE</strong> È SAPERLI ASCOLTARE, <strong>LONGEVITY</strong> È COSTRUIRE NEL TEMPO CIÒ CHE DAVVERO CONTA.</p>
|
||
+ <cite>Insanitylab</cite>
|
||
+ </blockquote>
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .quote { position: relative; background-size: cover; background-position: center; text-align: center; }
|
||
+ .quote::before { content: ''; position: absolute; inset: 0; background: rgba(46,32,24,.82); }
|
||
+ .quote__in { position: relative; padding-block: 110px; max-width: 820px; }
|
||
+ .quote__mark { color: #fff; font-size: 4rem; font-family: var(--font-heading); margin: 0; line-height: 1; }
|
||
+ .quote blockquote { margin: 0; }
|
||
+ .quote blockquote p { color: #fff; font-family: var(--font-heading); font-size: clamp(1.1rem, 2.4vw, 1.6rem); letter-spacing: .06em; line-height: 1.6; }
|
||
+ .quote cite { display: block; margin-top: 24px; color: var(--c-accent); font-style: normal; font-family: var(--font-heading); font-size: .75rem; letter-spacing: .3em; text-transform: uppercase; }
|
||
+</style>
|
||
diff --git a/src/components/home/TeamSection.astro b/src/components/home/TeamSection.astro
|
||
new file mode 100644
|
||
index 0000000..65cb0b7
|
||
--- /dev/null
|
||
+++ b/src/components/home/TeamSection.astro
|
||
@@ -0,0 +1,34 @@
|
||
+---
|
||
+import { Image } from 'astro:assets';
|
||
+import type { ImageMetadata } from 'astro';
|
||
+import { team, homeTeam } from '../../data/team';
|
||
+const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/team-*.{jpg,png}', { eager: true });
|
||
+const members = homeTeam.map((short) => team.find((m) => m.shortName === short)!).filter((m) => m.image);
|
||
+const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1].default;
|
||
+---
|
||
+<section class="section">
|
||
+ <div class="container">
|
||
+ <div class="tteam__head">
|
||
+ <h2>Team<br />Trainer</h2>
|
||
+ <a class="btn" href="/about">Scopri team</a>
|
||
+ </div>
|
||
+ <div class="tteam__grid">
|
||
+ {members.map((m) => (
|
||
+ <figure class="tteam__card">
|
||
+ <Image src={imgOf(m.image!)!} alt={m.name} widths={[300, 500]} sizes="(max-width: 767px) 50vw, 25vw" />
|
||
+ <figcaption>{m.shortName}</figcaption>
|
||
+ </figure>
|
||
+ ))}
|
||
+ </div>
|
||
+ </div>
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .tteam__head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 40px; }
|
||
+ .tteam__head h2 { text-transform: uppercase; letter-spacing: .1em; margin: 0; }
|
||
+ .tteam__grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }
|
||
+ .tteam__card { position: relative; margin: 0; }
|
||
+ .tteam__card img { aspect-ratio: 3/4; object-fit: cover; width: 100%; }
|
||
+ .tteam__card figcaption { position: absolute; right: 10px; bottom: 14px; background: #fff; padding: 10px 6px; writing-mode: vertical-rl; font-family: var(--font-heading); font-size: .62rem; letter-spacing: .3em; text-transform: uppercase; color: var(--c-accent-dark); }
|
||
+ @media (max-width: 767px) { .tteam__grid { grid-template-columns: 1fr 1fr; } }
|
||
+</style>
|
||
diff --git a/src/components/home/TrainingCards.astro b/src/components/home/TrainingCards.astro
|
||
new file mode 100644
|
||
index 0000000..93b2fbe
|
||
--- /dev/null
|
||
+++ b/src/components/home/TrainingCards.astro
|
||
@@ -0,0 +1,42 @@
|
||
+---
|
||
+import { Image } from 'astro:assets';
|
||
+import type { ImageMetadata } from 'astro';
|
||
+import { trainings } from '../../data/trainings';
|
||
+const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/training-*.{jpg,png}', { eager: true });
|
||
+const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1].default;
|
||
+const tones = ['taupe', 'tan', 'dark', 'charcoal'];
|
||
+// riga 1 (training 0-1): card, foto | riga 2 (training 2-3): foto, card
|
||
+const isSwapRow = (i: number) => Math.floor(i / 2) % 2 === 1;
|
||
+---
|
||
+<section class="tcards">
|
||
+ {trainings.map((t, i) => {
|
||
+ const img = imgOf(t.image);
|
||
+ const card = (
|
||
+ <div class:list={['tcards__cell', `tcards__cell--${tones[i]}`]}>
|
||
+ <h3>{t.title}</h3>
|
||
+ <p>{t.caption}</p>
|
||
+ <a class="tcards__link" href={t.cta.href}>Scopri →</a>
|
||
+ </div>
|
||
+ );
|
||
+ const photo = img ? <Image src={img} alt={t.title} class="tcards__img" widths={[400, 700]} sizes="(max-width: 767px) 50vw, 25vw" /> : null;
|
||
+ return isSwapRow(i) ? <>{photo}{card}</> : <>{card}{photo}</>;
|
||
+ })}
|
||
+</section>
|
||
+
|
||
+<style>
|
||
+ .tcards { display: grid; grid-template-columns: repeat(4, 1fr); }
|
||
+ .tcards__cell { padding: 48px 36px; display: flex; flex-direction: column; justify-content: center; aspect-ratio: 1; }
|
||
+ .tcards__cell--taupe { background: #6b5d5b; }
|
||
+ .tcards__cell--dark { background: var(--c-dark); }
|
||
+ .tcards__cell--charcoal { background: #27252a; }
|
||
+ .tcards__cell--taupe h3, .tcards__cell--taupe p,
|
||
+ .tcards__cell--dark h3, .tcards__cell--dark p,
|
||
+ .tcards__cell--charcoal h3, .tcards__cell--charcoal p { color: #efe9e3; }
|
||
+ .tcards__cell--tan { background: var(--c-accent); }
|
||
+ .tcards__cell--tan h3, .tcards__cell--tan p { color: #fff; }
|
||
+ .tcards__cell h3 { text-transform: uppercase; letter-spacing: .12em; }
|
||
+ .tcards__cell p { font-size: .88rem; }
|
||
+ .tcards__link { color: #fff; font-family: var(--font-heading); font-size: .7rem; letter-spacing: .2em; text-transform: uppercase; text-decoration: none; margin-top: 18px; }
|
||
+ .tcards__img { width: 100%; height: 100%; object-fit: cover; aspect-ratio: 1; }
|
||
+ @media (max-width: 767px) { .tcards { grid-template-columns: repeat(2, 1fr); } }
|
||
+</style>
|
||
diff --git a/src/data/agenda.ts b/src/data/agenda.ts
|
||
new file mode 100644
|
||
index 0000000..3942c7e
|
||
--- /dev/null
|
||
+++ b/src/data/agenda.ts
|
||
@@ -0,0 +1,36 @@
|
||
+export const disciplines = ["Events", "Crossfit", "Cardio", "Box", "Meditation", "Yoga Classes", "Body Balance"] as const;
|
||
+export const days = ["Lunedì", "Martedì", "Mercoledì", "Giovedì", "Venerdì", "Sabato", "Domenica"] as const;
|
||
+export const slots = ["10.00", "12.00", "16.00", "18.00", "20.00"] as const;
|
||
+
|
||
+export interface AgendaEntry { day: number; slot: string; discipline: string; time: string; trainer: string }
|
||
+
|
||
+// Placeholder dal template Prowess (come nel Figma); il cliente fornirà l’orario reale.
|
||
+export const agenda: AgendaEntry[] = [
|
||
+ { day: 0, slot: "10.00", discipline: "Crossfit", time: "10.00 - 11.00", trainer: "Amanda Snow" },
|
||
+ { day: 0, slot: "12.00", discipline: "Box", time: "12.00 - 13.00", trainer: "Matthew Dunn" },
|
||
+ { day: 0, slot: "18.00", discipline: "Cardio", time: "18.00 - 19.00", trainer: "Randy Nelson" },
|
||
+ { day: 0, slot: "20.00", discipline: "Crossfit", time: "20.00 - 21.00", trainer: "Amanda Snow" },
|
||
+ { day: 1, slot: "10.00", discipline: "Cardio", time: "10.00 - 11.00", trainer: "Randy Nelson" },
|
||
+ { day: 1, slot: "12.00", discipline: "Crossfit", time: "12.00 - 13.00", trainer: "Amanda Snow" },
|
||
+ { day: 1, slot: "16.00", discipline: "Body Balance", time: "16.00 - 17.00", trainer: "Barbara Holland" },
|
||
+ { day: 1, slot: "20.00", discipline: "Box", time: "20.00 - 21.00", trainer: "Matthew Dunn" },
|
||
+ { day: 2, slot: "10.00", discipline: "Crossfit", time: "10.00 - 11.00", trainer: "Amanda Snow" },
|
||
+ { day: 2, slot: "12.00", discipline: "Meditation", time: "12.00 - 13.00", trainer: "Nicole Fuller" },
|
||
+ { day: 2, slot: "16.00", discipline: "Box", time: "16.00 - 17.00", trainer: "Matthew Dunn" },
|
||
+ { day: 2, slot: "20.00", discipline: "Body Balance", time: "20.00 - 21.00", trainer: "Barbara Holland" },
|
||
+ { day: 3, slot: "10.00", discipline: "Body Balance", time: "10.00 - 11.00", trainer: "Barbara Holland" },
|
||
+ { day: 3, slot: "12.00", discipline: "Box", time: "12.00 - 13.00", trainer: "Matthew Dunn" },
|
||
+ { day: 3, slot: "16.00", discipline: "Crossfit", time: "16.00 - 17.00", trainer: "Amanda Snow" },
|
||
+ { day: 3, slot: "20.00", discipline: "Body Balance", time: "20.00 - 21.00", trainer: "Randy Nelson" },
|
||
+ { day: 4, slot: "12.00", discipline: "Crossfit", time: "12.00 - 13.00", trainer: "Amanda Snow" },
|
||
+ { day: 4, slot: "16.00", discipline: "Body Balance", time: "16.00 - 17.00", trainer: "Barbara Holland" },
|
||
+ { day: 4, slot: "18.00", discipline: "Cardio", time: "18.00 - 19.00", trainer: "Randy Nelson" },
|
||
+ { day: 4, slot: "20.00", discipline: "Yoga Classes", time: "20.00 - 21.00", trainer: "Amanda Snow" },
|
||
+ { day: 5, slot: "10.00", discipline: "Cardio", time: "10.00 - 11.00", trainer: "Randy Nelson" },
|
||
+ { day: 5, slot: "12.00", discipline: "Body Balance", time: "12.00 - 13.00", trainer: "Barbara Holland" },
|
||
+ { day: 5, slot: "18.00", discipline: "Box", time: "18.00 - 19.00", trainer: "Matthew Dunn" },
|
||
+ { day: 6, slot: "12.00", discipline: "Crossfit", time: "12.00 - 13.00", trainer: "Amanda Snow" },
|
||
+ { day: 6, slot: "16.00", discipline: "Cardio", time: "16.00 - 17.00", trainer: "Randy Nelson" },
|
||
+ { day: 6, slot: "18.00", discipline: "Box", time: "18.00 - 19.00", trainer: "Matthew Dunn" },
|
||
+ { day: 6, slot: "20.00", discipline: "Body Balance", time: "20.00 - 21.00", trainer: "Barbara Holland" },
|
||
+];
|
||
diff --git a/src/data/method.ts b/src/data/method.ts
|
||
new file mode 100644
|
||
index 0000000..26d609b
|
||
--- /dev/null
|
||
+++ b/src/data/method.ts
|
||
@@ -0,0 +1,9 @@
|
||
+export const methodIntro = "Il Metodo Insanitylab si basa su un percorso strutturato in fasi, dove le prime due rappresentano il focus standard del lavoro: valutazione iniziale e avvio dell’allenamento personalizzato. A queste possono essere aggiunti servizi e approfondimenti extra, in base alle esigenze e agli obiettivi della persona.";
|
||
+export const methodExtraNote = "Disponibili su richiesta, non incluse nel percorso standard.";
|
||
+
|
||
+export const methodSteps = [
|
||
+ { n: 1, title: "Prima consulenza conoscitiva iniziale", text: "Dove raccogliamo un’anamnesi iniziale per comprendere il cliente, i suoi obiettivi e guidarlo verso il percorso più adatto.", extra: false, image: "method-step-1" },
|
||
+ { n: 2, title: "Prima seduta operativa", text: "In cui si effettua la profilazione iniziale e la valutazione sul campo per definire il punto di partenza e impostare un percorso personalizzato.", extra: false, image: "method-step-2" },
|
||
+ { n: 3, title: "Plicometria", text: "Test che misura il grasso sottocutaneo per stimare la massa grassa, disponibile su richiesta o come servizio aggiuntivo.", extra: true, image: "method-step-3" },
|
||
+ { n: 4, title: "Analisi antropometriche", text: "Le analisi antropometriche sono misurazioni del corpo utili a valutare la composizione fisica e lo stato di forma.", extra: true, image: "method-step-4" },
|
||
+];
|
||
diff --git a/src/data/partners.ts b/src/data/partners.ts
|
||
new file mode 100644
|
||
index 0000000..be9cba6
|
||
--- /dev/null
|
||
+++ b/src/data/partners.ts
|
||
@@ -0,0 +1,6 @@
|
||
+export const partners = [
|
||
+ { name: "Ticket Welfare Edenred", image: "partner-edenred" },
|
||
+ { name: "Welbee", image: "partner-welbee" },
|
||
+ { name: "AON", image: "partner-aon" },
|
||
+ { name: "Howden", image: "partner-howden" },
|
||
+];
|
||
diff --git a/src/data/pricing.ts b/src/data/pricing.ts
|
||
new file mode 100644
|
||
index 0000000..6673960
|
||
--- /dev/null
|
||
+++ b/src/data/pricing.ts
|
||
@@ -0,0 +1,17 @@
|
||
+export const performanceClassPricing = {
|
||
+ heading: "Acquista la tua performance class",
|
||
+ intro: "Questi sono i programmi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi un’esperienza di allenamento completa e mirata.",
|
||
+ groups: [
|
||
+ {
|
||
+ label: "Programmi performance class semestrale",
|
||
+ open: true,
|
||
+ plans: [
|
||
+ { monthly: 130, entries: 1, note: "tot: 780€ · da 23€ lezione singola", installments: "2 rate da 390€ – inizio 60", best: "best price 720€" },
|
||
+ { monthly: 234, entries: 2, note: "tot: 1.400€ · da 23€ lezione singola", installments: "2 rate da 700€ – inizio 60", best: "best price 1.250€" },
|
||
+ { monthly: 300, entries: 3, note: "tot: 1.800€ · da 23€ lezione singola", installments: "2 rate da 900€ – inizio 60", best: "best price 1.650€" },
|
||
+ ],
|
||
+ },
|
||
+ { label: "Programmi performance class trimestrale", open: false, plans: [] },
|
||
+ { label: "Programmi performance class mensile", open: false, plans: [] },
|
||
+ ],
|
||
+};
|
||
diff --git a/src/data/programs.ts b/src/data/programs.ts
|
||
new file mode 100644
|
||
index 0000000..089eec9
|
||
--- /dev/null
|
||
+++ b/src/data/programs.ts
|
||
@@ -0,0 +1,104 @@
|
||
+export interface Program {
|
||
+ slug: string;
|
||
+ title: string;
|
||
+ subtitle: string;
|
||
+ excerpt: string;
|
||
+ longDescription: string;
|
||
+ features: string[];
|
||
+ faq: { q: string; a: string }[];
|
||
+ hasPricing: boolean;
|
||
+}
|
||
+
|
||
+export const programs: Program[] = [
|
||
+ {
|
||
+ slug: "personal-training",
|
||
+ title: "Personal Training",
|
||
+ subtitle: "One to One / Small Group",
|
||
+ excerpt: "Il personal training InsanityLab è un percorso one-to-one su misura, seguito da un professionista dedicato, per raggiungere in modo sicuro ed efficace obiettivi di forma fisica, performance e benessere.",
|
||
+ longDescription: "Allenati con un trainer dedicato che costruisce un percorso su misura per i tuoi obiettivi. Ogni sessione one to one è pensata per migliorare performance, tecnica e risultati, con supporto costante, attenzione totale e allenamenti personalizzati. Disponibile anche in formula small group (massimo 3 persone) per unire attenzione personalizzata, motivazione e condivisione.",
|
||
+ features: ["Trainer dedicato", "Percorso su misura", "Valutazione iniziale e monitoraggio", "Formula one to one o small group (max 3)"],
|
||
+ faq: [
|
||
+ { q: "Come si svolge la prima seduta?", a: "Con una consulenza conoscitiva iniziale: raccogliamo l’anamnesi, definiamo gli obiettivi e impostiamo il percorso più adatto." },
|
||
+ { q: "Con quale frequenza ci si allena?", a: "La frequenza viene definita insieme al trainer in base a obiettivi, disponibilità e livello di partenza." },
|
||
+ { q: "È adatto a chi parte da zero?", a: "Sì: il percorso è costruito sul tuo livello, con progressioni graduali e supervisione costante." },
|
||
+ ],
|
||
+ hasPricing: false,
|
||
+ },
|
||
+ {
|
||
+ slug: "performance-class",
|
||
+ title: "Performance Class",
|
||
+ subtitle: "Gruppo max 8 persone",
|
||
+ excerpt: "La Performance Class è un allenamento in piccoli gruppi, basato su una programmazione progressiva che combina forza e condizionamento per ottenere risultati concreti e costanti nel tempo.",
|
||
+ longDescription: "La Performance Class è un allenamento in piccoli gruppi con programmazione progressiva e scientifica, pensato per migliorare forza, composizione corporea e performance attraverso sessioni strutturate che combinano forza, ipertrofia e condizionamento metabolico. Non si improvvisa: ogni sessione ha un posto preciso all’interno di un piano più ampio.",
|
||
+ features: ["Allenamento ibrido", "Forza, ipertrofia, condizionamento metabolico e lavoro funzionale", "Sessioni da 50 minuti", "Fasi di bassa, media e alta intensità", "Programmazione mensile"],
|
||
+ faq: [
|
||
+ { q: "Che cosa si intende per Performance Class?", a: "Un allenamento in piccoli gruppi (massimo 8 persone) guidato da trainer certificati, con programmazione progressiva mensile: ogni sessione ha un posto preciso all’interno di un piano più ampio." },
|
||
+ { q: "Che tipo di allenamento viene svolto?", a: "Un allenamento ibrido che alterna forza, ipertrofia, condizionamento metabolico e lavoro funzionale. Le sessioni durano 50 minuti e prevedono fasi di bassa, media e alta intensità, calibrate sulla programmazione mensile." },
|
||
+ { q: "A chi è consigliato questo servizio?", a: "A chi vuole allenarsi con qualità e continuità in un contesto di gruppo motivante, mantenendo una programmazione seria e misurabile." },
|
||
+ { q: "Qual è la differenza rispetto a una class tradizionale?", a: "La programmazione: non è una lezione isolata ma parte di un piano mensile progressivo, con carichi e intensità calibrati." },
|
||
+ { q: "Che tipo di risultati o miglioramenti si ottengono?", a: "Miglioramento di forza, composizione corporea e performance, con risultati concreti e costanti nel tempo." },
|
||
+ { q: "Qual è l’intensità del lavoro?", a: "Alternata: fasi di bassa, media e alta intensità all’interno della stessa sessione, adattate al livello dei partecipanti." },
|
||
+ ],
|
||
+ hasPricing: true,
|
||
+ },
|
||
+ {
|
||
+ slug: "coaching",
|
||
+ title: "Coaching",
|
||
+ subtitle: "Programma di allenamento e monitoraggio",
|
||
+ excerpt: "Il coaching InsanityLab è un percorso personalizzato che, con programma su misura e supporto costante, guida la persona verso i propri obiettivi sviluppando autonomia e continuità nell’allenamento.",
|
||
+ longDescription: "Con il coaching hai un programma di allenamento su misura e un monitoraggio costante, ovunque ti alleni: in palestra, a casa o in viaggio. Il percorso sviluppa autonomia e continuità, adattandosi al tuo stile di vita (formula Fit Remote).",
|
||
+ features: ["Programma su misura", "Supporto e monitoraggio costanti", "Ovunque ti alleni (Fit Remote)", "Sviluppo di autonomia"],
|
||
+ faq: [
|
||
+ { q: "Come funziona il monitoraggio?", a: "Il trainer segue i tuoi progressi con check periodici e adatta il programma in base ai risultati." },
|
||
+ { q: "Serve attrezzatura?", a: "Il programma viene costruito sull’attrezzatura che hai a disposizione, in palestra o a casa." },
|
||
+ { q: "È adatto se viaggio spesso?", a: "Sì: la formula Fit Remote è pensata per chi si allena in luoghi diversi." },
|
||
+ ],
|
||
+ hasPricing: false,
|
||
+ },
|
||
+ {
|
||
+ slug: "pilates-flow",
|
||
+ title: "Pilates Flow",
|
||
+ subtitle: "Movimento e controllo",
|
||
+ excerpt: "Il Pilates Flow migliora postura, core e consapevolezza corporea con un movimento fluido, mentre l’osteopatia supporta recupero, prevenzione e benessere globale integrandosi con l’allenamento.",
|
||
+ longDescription: "Il Pilates Flow unisce movimento fluido e controllo: migliora postura, core e consapevolezza corporea. Integrato con l’osteopatia, supporta recupero, prevenzione e benessere globale all’interno del percorso di allenamento.",
|
||
+ features: ["Postura e core", "Movimento fluido e controllo", "Integrazione con osteopatia", "Benessere globale"],
|
||
+ faq: [
|
||
+ { q: "A chi è adatto il Pilates Flow?", a: "A chiunque voglia migliorare postura, mobilità e controllo del movimento, a ogni livello." },
|
||
+ { q: "Serve esperienza precedente?", a: "No: le progressioni sono graduali e seguite da professionisti qualificati." },
|
||
+ { q: "Come si integra con l’osteopatia?", a: "Il lavoro osteopatico supporta recupero e prevenzione, in sinergia con le sessioni di movimento." },
|
||
+ ],
|
||
+ hasPricing: false,
|
||
+ },
|
||
+ {
|
||
+ slug: "rehab",
|
||
+ title: "Rehab",
|
||
+ subtitle: "Recupero post-infortunio, patologie muscolo-scheletriche",
|
||
+ excerpt: "Il Rehab InsanityLab è un percorso supervisionato che accompagna dal recupero riabilitativo al ritorno all’attività fisica in modo sicuro, progressivo e personalizzato.",
|
||
+ longDescription: "Il Rehab accompagna dal recupero post-infortunio o da patologie muscolo-scheletriche fino al ritorno all’attività fisica. Percorso supervisionato da chinesiologi e osteopati, sicuro, progressivo e personalizzato.",
|
||
+ features: ["Recupero post-infortunio", "Patologie muscolo-scheletriche", "Supervisione di chinesiologi e osteopati", "Progressione sicura e personalizzata"],
|
||
+ faq: [
|
||
+ { q: "Chi segue il percorso Rehab?", a: "Professionisti specializzati in rieducazione funzionale ed esercizio adattato, in sinergia con l’area osteopatica." },
|
||
+ { q: "Serve una diagnosi medica?", a: "Se disponibile è utile portarla alla prima consulenza; il percorso si integra con le indicazioni dei sanitari." },
|
||
+ { q: "Quanto dura il percorso?", a: "Dipende dalla condizione di partenza: la progressione viene definita e aggiornata durante il percorso." },
|
||
+ ],
|
||
+ hasPricing: false,
|
||
+ },
|
||
+ {
|
||
+ slug: "nutrition",
|
||
+ title: "Nutrizione",
|
||
+ subtitle: "Alimentazione e stile di vita",
|
||
+ excerpt: "La nutrizione in InsanityLab è un servizio personalizzato che integra alimentazione e allenamento per migliorare energia, performance e benessere con piani su misura e sostenibili.",
|
||
+ longDescription: "Il servizio di nutrizione integra alimentazione e allenamento: piani su misura e sostenibili per migliorare energia, performance e benessere, costruiti sul tuo stile di vita.",
|
||
+ features: ["Piani alimentari su misura", "Integrazione con l’allenamento", "Sostenibilità nel tempo", "Focus su energia e performance"],
|
||
+ faq: [
|
||
+ { q: "Come inizia il percorso di nutrizione?", a: "Con una valutazione iniziale di abitudini, obiettivi e allenamento, da cui nasce il piano personalizzato." },
|
||
+ { q: "È integrato con il training?", a: "Sì: alimentazione e allenamento vengono programmati in modo coerente." },
|
||
+ { q: "Sono previsti controlli periodici?", a: "Sì, con aggiornamento del piano in base ai risultati." },
|
||
+ ],
|
||
+ hasPricing: false,
|
||
+ },
|
||
+];
|
||
+
|
||
+export function getProgram(slug: string): Program | undefined {
|
||
+ return programs.find((p) => p.slug === slug);
|
||
+}
|
||
diff --git a/src/data/site.ts b/src/data/site.ts
|
||
new file mode 100644
|
||
index 0000000..5b1c6ee
|
||
--- /dev/null
|
||
+++ b/src/data/site.ts
|
||
@@ -0,0 +1,34 @@
|
||
+export const site = {
|
||
+ name: "InsanityLab",
|
||
+ phone: "351 7535977",
|
||
+ email: "info@insanitylab.it",
|
||
+ address: "Via Leandro Alberti, 76",
|
||
+ cityLine: "40139 Bologna",
|
||
+ hours: ["Lun – Ven 6.00 – 21.00", "Sab 8.00 – 19.00 · Dom chiuso"],
|
||
+ socials: [
|
||
+ { label: "Instagram", url: "https://www.instagram.com/insanitylab" },
|
||
+ { label: "Facebook", url: "https://www.facebook.com/insanitylab" },
|
||
+ { label: "LinkedIn", url: "https://www.linkedin.com/company/insanitylab" },
|
||
+ ],
|
||
+ navigation: [
|
||
+ { label: "Insanitylab", href: "/about" },
|
||
+ { label: "Training", href: "/training" },
|
||
+ { label: "Programmi", href: "/programs" },
|
||
+ { label: "Blog & Edugo", href: "/blog" },
|
||
+ { label: "Contatti", href: "/contact" },
|
||
+ ],
|
||
+ footerTraining: [
|
||
+ { label: "One to one", href: "/training#one-to-one" },
|
||
+ { label: "Small groups", href: "/training#small-groups" },
|
||
+ { label: "Performance class", href: "/training#performance-class" },
|
||
+ { label: "Fit remote", href: "/training#fit-remote" },
|
||
+ ],
|
||
+ footerPrograms: [
|
||
+ { label: "Personal Training", href: "/programs/personal-training" },
|
||
+ { label: "Performance Class", href: "/programs/performance-class" },
|
||
+ { label: "Coaching", href: "/programs/coaching" },
|
||
+ { label: "Rehab", href: "/programs/rehab" },
|
||
+ { label: "Pilates Flow", href: "/programs/pilates-flow" },
|
||
+ { label: "Nutrizione", href: "/programs/nutrition" },
|
||
+ ],
|
||
+};
|
||
diff --git a/src/data/team.ts b/src/data/team.ts
|
||
new file mode 100644
|
||
index 0000000..9287197
|
||
--- /dev/null
|
||
+++ b/src/data/team.ts
|
||
@@ -0,0 +1,24 @@
|
||
+export interface Member { name: string; shortName: string; role: string; bio: string; image?: string; founder: boolean }
|
||
+
|
||
+export const team: Member[] = [
|
||
+ { name: "Donata Dal Pastro", shortName: "Donata", role: "Chinesiologa e Osteopata", founder: true, image: "team-donata",
|
||
+ bio: "Donata Dal Pastro è chinesiologa e osteopata specializzata in rieducazione funzionale, riatletizzazione sportiva ed esercizio adattato. Supervisiona l’area dedicata a prevenzione, recupero e benessere, con competenze specifiche anche in pediatria e gravidanza." },
|
||
+ { name: "Nicola Antonelli", shortName: "Nicola", role: "Chinesiologo e Osteopata", founder: true, image: "team-nicola",
|
||
+ bio: "Nicola Antonelli è chinesiologo e osteopata specializzato in performance sportiva, functional training e bodybuilding. Coordina lo sviluppo tecnico dei percorsi dedicati alla performance e all’allenamento avanzato." },
|
||
+ { name: "Eva Cassetta", shortName: "Eva", role: "Business Manager – Balance & Strategy", founder: true,
|
||
+ bio: "Eva Cassetta ricopre il ruolo di Business Manager – Balance & Strategy e si occupa della gestione amministrativa, economica e finanziaria del progetto, garantendone solidità, organizzazione e sostenibilità nel tempo." },
|
||
+ { name: "Alessandro Lazzari", shortName: "Alessandro", role: "Trainer – Functional & Movement", founder: false,
|
||
+ bio: "Trainer – Functional & Movement. Laureando in Scienze Motorie, specializzato in running e atletica leggera." },
|
||
+ { name: "Eleonora Cassani", shortName: "Eleonora", role: "Trainer – Planning Manager", founder: false, image: "team-eleonora",
|
||
+ bio: "Trainer – Planning Manager. Chinesiologa specializzata in attività preventiva e adattata, con focus su allenamento in gravidanza e post-gravidanza." },
|
||
+ { name: "Antonello D’Alessandro", shortName: "Antonello", role: "Trainer – Exercise & Health", founder: false, image: "team-antonello",
|
||
+ bio: "Trainer – Exercise & Health. Chinesiologo specializzato in attività preventiva e adattata, Dottore in Osteopatia." },
|
||
+ { name: "Alice Roda", shortName: "Alice", role: "Trainer – Functional & Movement", founder: false,
|
||
+ bio: "Trainer – Functional & Movement. Laureanda in Scienze Motorie e Preparatore Atletico. Scienza del movimento e massima cura della persona: un approccio al functional training empatico e accogliente, dove l’ascolto umano guida e supporta ogni tuo progresso." },
|
||
+ { name: "Davide Bisagni", shortName: "Davide", role: "Trainer – Functional & Movement", founder: false,
|
||
+ bio: "Trainer – Functional & Movement. Chinesiologo e allenamento funzionale al servizio del corpo: massima cura del gesto atletico e attenzione mirata al recupero per prestazioni sostenibili e durature." },
|
||
+ { name: "Giulia Barbàra", shortName: "Giulia", role: "Trainer – Pilates, Yoga & Mindfulness", founder: false,
|
||
+ bio: "Trainer – Pilates, Yoga & Mindfulness. Laureata in Psicologia Clinica e Riabilitazione, insegnante di Pilates, Yoga e Mindfulness, con approccio orientato al benessere e all’equilibrio mente-corpo." },
|
||
+];
|
||
+
|
||
+export const homeTeam = ["Donata", "Nicola", "Eleonora", "Antonello"];
|
||
diff --git a/src/data/trainings.ts b/src/data/trainings.ts
|
||
new file mode 100644
|
||
index 0000000..f87e364
|
||
--- /dev/null
|
||
+++ b/src/data/trainings.ts
|
||
@@ -0,0 +1,6 @@
|
||
+export const trainings = [
|
||
+ { id: "one-to-one", title: "One to One", caption: "Allenamento con un trainer dedicato", image: "training-one-to-one", description: "Allenati con un trainer dedicato che costruisce un percorso su misura per i tuoi obiettivi. Ogni sessione one to one è pensata per migliorare performance, tecnica e risultati. Supporto costante, attenzione totale e allenamenti personalizzati per dare il massimo.", cta: { label: "Contattaci", href: "/contact" } },
|
||
+ { id: "small-groups", title: "Small Groups", caption: "Mini gruppi (max 3 persone) con trainer dedicato", image: "training-small-groups", description: "Allenati in mini gruppi da massimo 3 persone con un trainer dedicato. Un’esperienza small group che unisce attenzione personalizzata, motivazione e condivisione. Programmi mirati e supporto costante per raggiungere i tuoi obiettivi insieme.", cta: { label: "Contattaci", href: "/contact" } },
|
||
+ { id: "performance-class", title: "Performance Class", caption: "Classi fino a 8 persone con trainer certificato", image: "training-performance-class", description: "Allenati in classi fino a 8 persone guidate da trainer certificati. Sessioni dinamiche ad alta intensità pensate per migliorare forza, resistenza e performance. Energia, motivazione e allenamenti strutturati per dare il massimo in ogni workout.", cta: { label: "Scopri", href: "/programs/performance-class" } },
|
||
+ { id: "fit-remote", title: "Fit Remote", caption: "Piano di allenamento su misura, ovunque ti alleni.", image: "training-fit-remote", description: "Con Fit Remote hai un piano di allenamento su misura, ovunque ti alleni. Programmi personalizzati creati sui tuoi obiettivi, da seguire in palestra, a casa o in viaggio. Supporto costante e allenamenti pensati per adattarsi al tuo stile di vita.", cta: { label: "Contattaci", href: "/contact" } },
|
||
+] as const;
|
||
diff --git a/src/env.d.ts b/src/env.d.ts
|
||
new file mode 100644
|
||
index 0000000..81f5f4a
|
||
--- /dev/null
|
||
+++ b/src/env.d.ts
|
||
@@ -0,0 +1,7 @@
|
||
+/// <reference types="astro/client" />
|
||
+
|
||
+declare namespace App {
|
||
+ interface Locals {
|
||
+ user?: { id: number; username: string };
|
||
+ }
|
||
+}
|
||
diff --git a/src/layouts/Admin.astro b/src/layouts/Admin.astro
|
||
new file mode 100644
|
||
index 0000000..0cb4bd0
|
||
--- /dev/null
|
||
+++ b/src/layouts/Admin.astro
|
||
@@ -0,0 +1,36 @@
|
||
+---
|
||
+interface Props { title: string }
|
||
+const { title } = Astro.props;
|
||
+const user = Astro.locals.user;
|
||
+---
|
||
+<!doctype html>
|
||
+<html lang="it">
|
||
+ <head>
|
||
+ <meta charset="utf-8" />
|
||
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
+ <title>{title} — Admin InsanityLab</title>
|
||
+ <meta name="robots" content="noindex" />
|
||
+ <style is:global>
|
||
+ body { font-family: system-ui, sans-serif; margin: 0; background: #f4f2ef; color: #333; }
|
||
+ .abar { background: #3a2d26; color: #fff; padding: 12px 24px; display: flex; justify-content: space-between; align-items: center; }
|
||
+ .abar a { color: #d9cfc4; text-decoration: none; margin-left: 16px; }
|
||
+ .awrap { max-width: 960px; margin: 30px auto; padding: 0 20px; }
|
||
+ .abtn { background: #b5a48b; color: #fff; border: 0; padding: 10px 18px; cursor: pointer; font-size: .9rem; text-decoration: none; display: inline-block; }
|
||
+ .abtn--danger { background: #a33; }
|
||
+ .afield { width: 100%; padding: 10px; margin-bottom: 14px; border: 1px solid #ccc; box-sizing: border-box; font: inherit; }
|
||
+ table { width: 100%; border-collapse: collapse; background: #fff; }
|
||
+ th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid #eee; font-size: .92rem; }
|
||
+ .pill { font-size: .75rem; padding: 2px 10px; border-radius: 10px; background: #e5decf; }
|
||
+ .pill--draft { background: #f3d9a4; }
|
||
+ </style>
|
||
+ </head>
|
||
+ <body>
|
||
+ <div class="abar">
|
||
+ <strong>InsanityLab · Admin</strong>
|
||
+ <span>
|
||
+ {user && <>{`Ciao, ${user.username}`} <a href="/admin">Articoli</a> <a href="/admin/new">Nuovo</a> <a href="/admin/logout">Esci</a> <a href="/blog" target="_blank">Vedi sito ↗</a></>}
|
||
+ </span>
|
||
+ </div>
|
||
+ <div class="awrap"><slot /></div>
|
||
+ </body>
|
||
+</html>
|
||
diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro
|
||
new file mode 100644
|
||
index 0000000..4b19758
|
||
--- /dev/null
|
||
+++ b/src/layouts/Base.astro
|
||
@@ -0,0 +1,33 @@
|
||
+---
|
||
+import '@fontsource/montserrat/400.css';
|
||
+import '@fontsource/montserrat/500.css';
|
||
+import '@fontsource/montserrat/600.css';
|
||
+import '@fontsource/montserrat/700.css';
|
||
+import '@fontsource/open-sans/400.css';
|
||
+import '@fontsource/open-sans/600.css';
|
||
+import '../styles/global.css';
|
||
+import Header from '../components/Header.astro';
|
||
+import Footer from '../components/Footer.astro';
|
||
+
|
||
+interface Props { title: string; description?: string }
|
||
+const { title, description = 'InsanityLab — Performance. Balance. Longevity. Allenamento personalizzato, piccoli gruppi e benessere a Bologna.' } = Astro.props;
|
||
+---
|
||
+<!doctype html>
|
||
+<html lang="it">
|
||
+ <head>
|
||
+ <meta charset="utf-8" />
|
||
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
+ <title>{title} — InsanityLab</title>
|
||
+ <meta name="description" content={description} />
|
||
+ <meta property="og:title" content={`${title} — InsanityLab`} />
|
||
+ <meta property="og:description" content={description} />
|
||
+ <meta property="og:type" content="website" />
|
||
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||
+ <link rel="sitemap" href="/sitemap-index.xml" />
|
||
+ </head>
|
||
+ <body>
|
||
+ <Header />
|
||
+ <main><slot /></main>
|
||
+ <Footer />
|
||
+ </body>
|
||
+</html>
|
||
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
|
||
new file mode 100644
|
||
index 0000000..1946702
|
||
--- /dev/null
|
||
+++ b/src/lib/auth.ts
|
||
@@ -0,0 +1,48 @@
|
||
+import type Database from 'better-sqlite3';
|
||
+import bcrypt from 'bcryptjs';
|
||
+import { randomBytes } from 'node:crypto';
|
||
+
|
||
+export const SESSION_COOKIE = 'session';
|
||
+const SESSION_DAYS = 7;
|
||
+
|
||
+export function hashPassword(plain: string): string {
|
||
+ return bcrypt.hashSync(plain, 12);
|
||
+}
|
||
+
|
||
+export function verifyPassword(plain: string, hash: string): boolean {
|
||
+ return bcrypt.compareSync(plain, hash);
|
||
+}
|
||
+
|
||
+export function createUser(db: Database.Database, username: string, password: string): number {
|
||
+ const res = db.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)')
|
||
+ .run(username, hashPassword(password));
|
||
+ return Number(res.lastInsertRowid);
|
||
+}
|
||
+
|
||
+export function login(db: Database.Database, username: string, password: string): string | null {
|
||
+ const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username) as
|
||
+ | { id: number; password_hash: string } | undefined;
|
||
+ if (!user || !verifyPassword(password, user.password_hash)) return null;
|
||
+ const token = randomBytes(32).toString('hex');
|
||
+ db.prepare(
|
||
+ `INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, datetime('now', '+${SESSION_DAYS} days'))`
|
||
+ ).run(token, user.id);
|
||
+ return token;
|
||
+}
|
||
+
|
||
+export function getSessionUser(db: Database.Database, token: string): { id: number; username: string } | null {
|
||
+ const row = db.prepare(
|
||
+ `SELECT s.token, s.expires_at, u.id, u.username
|
||
+ FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.token = ?`
|
||
+ ).get(token) as { token: string; expires_at: string; id: number; username: string } | undefined;
|
||
+ if (!row) return null;
|
||
+ if (row.expires_at <= new Date().toISOString().slice(0, 19).replace('T', ' ')) {
|
||
+ db.prepare('DELETE FROM sessions WHERE token = ?').run(token);
|
||
+ return null;
|
||
+ }
|
||
+ return { id: row.id, username: row.username };
|
||
+}
|
||
+
|
||
+export function logout(db: Database.Database, token: string): void {
|
||
+ db.prepare('DELETE FROM sessions WHERE token = ?').run(token);
|
||
+}
|
||
diff --git a/src/lib/blog-const.ts b/src/lib/blog-const.ts
|
||
new file mode 100644
|
||
index 0000000..d1b7436
|
||
--- /dev/null
|
||
+++ b/src/lib/blog-const.ts
|
||
@@ -0,0 +1,6 @@
|
||
+export const CATEGORIES = [
|
||
+ 'Articoli tecnici & scientifici',
|
||
+ 'Contenuti educativi',
|
||
+ 'LinkedIn / Insight',
|
||
+ 'Eventi & Presidi',
|
||
+] as const;
|
||
diff --git a/src/lib/contact.ts b/src/lib/contact.ts
|
||
new file mode 100644
|
||
index 0000000..96f9a32
|
||
--- /dev/null
|
||
+++ b/src/lib/contact.ts
|
||
@@ -0,0 +1,31 @@
|
||
+export interface ContactData {
|
||
+ firstName: string; lastName: string; phone: string;
|
||
+ email: string; message: string; website: string;
|
||
+}
|
||
+
|
||
+const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
+
|
||
+function str(v: unknown, max: number): string {
|
||
+ return typeof v === 'string' ? v.trim().slice(0, max + 1) : '';
|
||
+}
|
||
+
|
||
+export function validateContact(data: unknown):
|
||
+ | { ok: true; value: ContactData }
|
||
+ | { ok: false; error: string } {
|
||
+ if (typeof data !== 'object' || data === null) return { ok: false, error: 'Dati non validi.' };
|
||
+ const d = data as Record<string, unknown>;
|
||
+ const value: ContactData = {
|
||
+ firstName: str(d.firstName, 100),
|
||
+ lastName: str(d.lastName, 100),
|
||
+ phone: str(d.phone, 40),
|
||
+ email: str(d.email, 200),
|
||
+ message: str(d.message, 5000),
|
||
+ website: str(d.website, 200),
|
||
+ };
|
||
+ if (value.website) return { ok: false, error: 'Richiesta non valida.' };
|
||
+ if (!value.firstName) return { ok: false, error: 'Il nome è obbligatorio.' };
|
||
+ if (!EMAIL_RE.test(value.email)) return { ok: false, error: 'Inserisci una e-mail valida.' };
|
||
+ if (!value.message) return { ok: false, error: 'Il messaggio è obbligatorio.' };
|
||
+ if (value.message.length > 5000) return { ok: false, error: 'Messaggio troppo lungo.' };
|
||
+ return { ok: true, value };
|
||
+}
|
||
diff --git a/src/lib/db.ts b/src/lib/db.ts
|
||
new file mode 100644
|
||
index 0000000..537d0aa
|
||
--- /dev/null
|
||
+++ b/src/lib/db.ts
|
||
@@ -0,0 +1,47 @@
|
||
+import Database from 'better-sqlite3';
|
||
+import { mkdirSync } from 'node:fs';
|
||
+import { dirname } from 'node:path';
|
||
+
|
||
+const SCHEMA = `
|
||
+CREATE TABLE IF NOT EXISTS posts (
|
||
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
+ title TEXT NOT NULL,
|
||
+ slug TEXT NOT NULL UNIQUE,
|
||
+ category TEXT NOT NULL,
|
||
+ excerpt TEXT NOT NULL DEFAULT '',
|
||
+ body_html TEXT NOT NULL DEFAULT '',
|
||
+ cover TEXT,
|
||
+ draft INTEGER NOT NULL DEFAULT 1,
|
||
+ published_at TEXT,
|
||
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
+);
|
||
+CREATE TABLE IF NOT EXISTS users (
|
||
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
+ username TEXT NOT NULL UNIQUE,
|
||
+ password_hash TEXT NOT NULL
|
||
+);
|
||
+CREATE TABLE IF NOT EXISTS sessions (
|
||
+ token TEXT PRIMARY KEY,
|
||
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
+ expires_at TEXT NOT NULL
|
||
+);
|
||
+CREATE INDEX IF NOT EXISTS idx_posts_pub ON posts (draft, published_at DESC);
|
||
+`;
|
||
+
|
||
+export function createDb(path?: string): Database.Database {
|
||
+ const p = path ?? process.env.DB_PATH ?? 'data/insanitylab.db';
|
||
+ if (p !== ':memory:') mkdirSync(dirname(p), { recursive: true });
|
||
+ const db = new Database(p);
|
||
+ db.pragma('journal_mode = WAL');
|
||
+ db.pragma('foreign_keys = ON');
|
||
+ db.exec(SCHEMA);
|
||
+ return db;
|
||
+}
|
||
+
|
||
+let singleton: Database.Database | null = null;
|
||
+
|
||
+export function getDb(): Database.Database {
|
||
+ if (!singleton) singleton = createDb();
|
||
+ return singleton;
|
||
+}
|
||
diff --git a/src/lib/env.ts b/src/lib/env.ts
|
||
new file mode 100644
|
||
index 0000000..fe0ba40
|
||
--- /dev/null
|
||
+++ b/src/lib/env.ts
|
||
@@ -0,0 +1,5 @@
|
||
+export function getEnv(name: string, fallback?: string): string {
|
||
+ const v = process.env[name] ?? (import.meta.env ? import.meta.env[name] : undefined) ?? fallback;
|
||
+ if (v === undefined) throw new Error(`Variabile d'ambiente mancante: ${name}`);
|
||
+ return v;
|
||
+}
|
||
diff --git a/src/lib/mailer.ts b/src/lib/mailer.ts
|
||
new file mode 100644
|
||
index 0000000..a2b8b99
|
||
--- /dev/null
|
||
+++ b/src/lib/mailer.ts
|
||
@@ -0,0 +1,25 @@
|
||
+import nodemailer from 'nodemailer';
|
||
+import { getEnv } from './env';
|
||
+import type { ContactData } from './contact';
|
||
+
|
||
+export async function sendContactEmail(data: ContactData): Promise<void> {
|
||
+ const transporter = nodemailer.createTransport({
|
||
+ host: getEnv('SMTP_HOST'),
|
||
+ port: Number(getEnv('SMTP_PORT', '587')),
|
||
+ secure: Number(getEnv('SMTP_PORT', '587')) === 465,
|
||
+ auth: { user: getEnv('SMTP_USER'), pass: getEnv('SMTP_PASS') },
|
||
+ });
|
||
+ await transporter.sendMail({
|
||
+ from: getEnv('CONTACT_FROM'),
|
||
+ to: getEnv('CONTACT_TO', 'info@insanitylab.it'),
|
||
+ replyTo: data.email,
|
||
+ subject: `Nuovo contatto dal sito: ${data.firstName} ${data.lastName}`.trim(),
|
||
+ text: [
|
||
+ `Nome: ${data.firstName} ${data.lastName}`.trim(),
|
||
+ data.phone && `Telefono: ${data.phone}`,
|
||
+ `E-mail: ${data.email}`,
|
||
+ '',
|
||
+ data.message,
|
||
+ ].filter(Boolean).join('\n'),
|
||
+ });
|
||
+}
|
||
diff --git a/src/lib/post-body.ts b/src/lib/post-body.ts
|
||
new file mode 100644
|
||
index 0000000..7d34ee4
|
||
--- /dev/null
|
||
+++ b/src/lib/post-body.ts
|
||
@@ -0,0 +1,21 @@
|
||
+import { slugify } from './slug';
|
||
+import { sanitizeHtml } from './sanitize';
|
||
+import { CATEGORIES } from './blog-const';
|
||
+
|
||
+export function parsePostBody(data: Record<string, unknown>) {
|
||
+ const title = String(data.title ?? '').trim();
|
||
+ if (!title) return { error: 'Il titolo è obbligatorio.' } as const;
|
||
+ const category = String(data.category ?? '');
|
||
+ if (!(CATEGORIES as readonly string[]).includes(category)) return { error: 'Categoria non valida.' } as const;
|
||
+ const slug = slugify(String(data.slug ?? '') || title);
|
||
+ if (!slug) return { error: 'Slug non valido.' } as const;
|
||
+ return {
|
||
+ value: {
|
||
+ title, slug, category,
|
||
+ excerpt: String(data.excerpt ?? '').trim().slice(0, 300),
|
||
+ body_html: sanitizeHtml(String(data.body_html ?? '')),
|
||
+ cover: data.cover ? String(data.cover) : null,
|
||
+ draft: Boolean(data.draft),
|
||
+ },
|
||
+ } as const;
|
||
+}
|
||
diff --git a/src/lib/posts.ts b/src/lib/posts.ts
|
||
new file mode 100644
|
||
index 0000000..dad9159
|
||
--- /dev/null
|
||
+++ b/src/lib/posts.ts
|
||
@@ -0,0 +1,74 @@
|
||
+import type Database from 'better-sqlite3';
|
||
+
|
||
+export interface Post {
|
||
+ id: number; title: string; slug: string; category: string; excerpt: string;
|
||
+ body_html: string; cover: string | null; draft: number;
|
||
+ published_at: string | null; created_at: string; updated_at: string;
|
||
+}
|
||
+
|
||
+export interface PostInput {
|
||
+ title: string; slug: string; category: string; excerpt: string;
|
||
+ body_html: string; cover?: string | null; draft: boolean;
|
||
+}
|
||
+
|
||
+export function createPost(db: Database.Database, input: PostInput): number {
|
||
+ const res = db.prepare(
|
||
+ `INSERT INTO posts (title, slug, category, excerpt, body_html, cover, draft, published_at)
|
||
+ VALUES (@title, @slug, @category, @excerpt, @body_html, @cover, @draft,
|
||
+ CASE WHEN @draft = 0 THEN datetime('now') ELSE NULL END)`
|
||
+ ).run({ ...input, cover: input.cover ?? null, draft: input.draft ? 1 : 0 });
|
||
+ return Number(res.lastInsertRowid);
|
||
+}
|
||
+
|
||
+export function updatePost(db: Database.Database, id: number, input: PostInput): void {
|
||
+ db.prepare(
|
||
+ `UPDATE posts SET title = @title, slug = @slug, category = @category,
|
||
+ excerpt = @excerpt, body_html = @body_html, cover = @cover, draft = @draft,
|
||
+ published_at = CASE WHEN @draft = 0 AND published_at IS NULL THEN datetime('now') ELSE published_at END,
|
||
+ updated_at = datetime('now')
|
||
+ WHERE id = @id`
|
||
+ ).run({ ...input, id, cover: input.cover ?? null, draft: input.draft ? 1 : 0 });
|
||
+}
|
||
+
|
||
+export function deletePost(db: Database.Database, id: number): void {
|
||
+ db.prepare('DELETE FROM posts WHERE id = ?').run(id);
|
||
+}
|
||
+
|
||
+export function getPostById(db: Database.Database, id: number): Post | undefined {
|
||
+ return db.prepare('SELECT * FROM posts WHERE id = ?').get(id) as Post | undefined;
|
||
+}
|
||
+
|
||
+export function getPublishedBySlug(db: Database.Database, slug: string): Post | undefined {
|
||
+ return db.prepare('SELECT * FROM posts WHERE slug = ? AND draft = 0').get(slug) as Post | undefined;
|
||
+}
|
||
+
|
||
+export function listPublished(
|
||
+ db: Database.Database,
|
||
+ opts: { category?: string; page?: number; perPage?: number } = {},
|
||
+): { items: Post[]; total: number } {
|
||
+ const { category, page = 1, perPage = 9 } = opts;
|
||
+ const where = category ? 'WHERE draft = 0 AND category = @category' : 'WHERE draft = 0';
|
||
+ const total = (db.prepare(`SELECT COUNT(*) AS n FROM posts ${where}`)
|
||
+ .get({ category }) as { n: number }).n;
|
||
+ const items = db.prepare(
|
||
+ `SELECT * FROM posts ${where} ORDER BY published_at DESC LIMIT @limit OFFSET @offset`
|
||
+ ).all({ category, limit: perPage, offset: (page - 1) * perPage }) as Post[];
|
||
+ return { items, total };
|
||
+}
|
||
+
|
||
+export function listAllPosts(db: Database.Database): Post[] {
|
||
+ return db.prepare('SELECT * FROM posts ORDER BY updated_at DESC').all() as Post[];
|
||
+}
|
||
+
|
||
+export function listRecent(db: Database.Database, limit = 4): Post[] {
|
||
+ return db.prepare(
|
||
+ 'SELECT * FROM posts WHERE draft = 0 ORDER BY published_at DESC LIMIT ?'
|
||
+ ).all(limit) as Post[];
|
||
+}
|
||
+
|
||
+export function listArchiveMonths(db: Database.Database): { month: string; count: number }[] {
|
||
+ return db.prepare(
|
||
+ `SELECT strftime('%Y-%m', published_at) AS month, COUNT(*) AS count
|
||
+ FROM posts WHERE draft = 0 GROUP BY month ORDER BY month DESC`
|
||
+ ).all() as { month: string; count: number }[];
|
||
+}
|
||
diff --git a/src/lib/rate-limit.ts b/src/lib/rate-limit.ts
|
||
new file mode 100644
|
||
index 0000000..9f8905c
|
||
--- /dev/null
|
||
+++ b/src/lib/rate-limit.ts
|
||
@@ -0,0 +1,17 @@
|
||
+const buckets = new Map<string, number[]>();
|
||
+
|
||
+export function rateLimit(key: string, max: number, windowMs: number): boolean {
|
||
+ const now = Date.now();
|
||
+ const recent = (buckets.get(key) ?? []).filter((t) => now - t < windowMs);
|
||
+ if (recent.length >= max) {
|
||
+ buckets.set(key, recent);
|
||
+ return false;
|
||
+ }
|
||
+ recent.push(now);
|
||
+ buckets.set(key, recent);
|
||
+ return true;
|
||
+}
|
||
+
|
||
+export function _resetBuckets(): void {
|
||
+ buckets.clear();
|
||
+}
|
||
diff --git a/src/lib/sanitize.ts b/src/lib/sanitize.ts
|
||
new file mode 100644
|
||
index 0000000..b72e440
|
||
--- /dev/null
|
||
+++ b/src/lib/sanitize.ts
|
||
@@ -0,0 +1,11 @@
|
||
+import sanitize from 'sanitize-html';
|
||
+
|
||
+export function sanitizeHtml(html: string): string {
|
||
+ return sanitize(html, {
|
||
+ allowedTags: ['h2', 'h3', 'p', 'strong', 'em', 'u', 's', 'ul', 'ol', 'li', 'a', 'img', 'blockquote', 'br', 'hr'],
|
||
+ allowedAttributes: { a: ['href', 'rel', 'target'], img: ['src', 'alt'] },
|
||
+ allowedSchemes: ['https', 'http', 'mailto'],
|
||
+ allowedSchemesAppliedToAttributes: ['href', 'src'],
|
||
+ allowProtocolRelative: false,
|
||
+ });
|
||
+}
|
||
diff --git a/src/lib/slug.ts b/src/lib/slug.ts
|
||
new file mode 100644
|
||
index 0000000..5bdd460
|
||
--- /dev/null
|
||
+++ b/src/lib/slug.ts
|
||
@@ -0,0 +1,8 @@
|
||
+export function slugify(input: string): string {
|
||
+ return input
|
||
+ .toLowerCase()
|
||
+ .normalize('NFD')
|
||
+ .replace(/[̀-ͯ]/g, '')
|
||
+ .replace(/[^a-z0-9]+/g, '-')
|
||
+ .replace(/^-+|-+$/g, '');
|
||
+}
|
||
diff --git a/src/middleware.ts b/src/middleware.ts
|
||
new file mode 100644
|
||
index 0000000..fa8dcfd
|
||
--- /dev/null
|
||
+++ b/src/middleware.ts
|
||
@@ -0,0 +1,24 @@
|
||
+import { defineMiddleware } from 'astro:middleware';
|
||
+import { getDb } from './lib/db';
|
||
+import { getSessionUser, SESSION_COOKIE } from './lib/auth';
|
||
+
|
||
+export const onRequest = defineMiddleware((context, next) => {
|
||
+ const { pathname } = context.url;
|
||
+ const isProtected =
|
||
+ (pathname.startsWith('/admin') && pathname !== '/admin/login') ||
|
||
+ pathname.startsWith('/api/admin');
|
||
+ if (!isProtected) return next();
|
||
+
|
||
+ const token = context.cookies.get(SESSION_COOKIE)?.value;
|
||
+ const user = token ? getSessionUser(getDb(), token) : null;
|
||
+ if (!user) {
|
||
+ if (pathname.startsWith('/api/')) {
|
||
+ return new Response(JSON.stringify({ error: 'Non autorizzato' }), {
|
||
+ status: 401, headers: { 'Content-Type': 'application/json' },
|
||
+ });
|
||
+ }
|
||
+ return context.redirect('/admin/login');
|
||
+ }
|
||
+ context.locals.user = user;
|
||
+ return next();
|
||
+});
|
||
diff --git a/src/pages/404.astro b/src/pages/404.astro
|
||
new file mode 100644
|
||
index 0000000..9aa6d78
|
||
--- /dev/null
|
||
+++ b/src/pages/404.astro
|
||
@@ -0,0 +1,14 @@
|
||
+---
|
||
+import Base from '../layouts/Base.astro';
|
||
+export const prerender = true;
|
||
+---
|
||
+<Base title="Pagina non trovata">
|
||
+ <section class="section" style="text-align:center; padding-block:140px;">
|
||
+ <div class="container">
|
||
+ <p class="eyebrow">Errore 404</p>
|
||
+ <h1>Pagina non trovata</h1>
|
||
+ <p>La pagina che cerchi non esiste o è stata spostata.</p>
|
||
+ <a class="btn" href="/">Torna alla home</a>
|
||
+ </div>
|
||
+ </section>
|
||
+</Base>
|
||
diff --git a/src/pages/about.astro b/src/pages/about.astro
|
||
new file mode 100644
|
||
index 0000000..1cfd76f
|
||
--- /dev/null
|
||
+++ b/src/pages/about.astro
|
||
@@ -0,0 +1,166 @@
|
||
+---
|
||
+import Base from '../layouts/Base.astro';
|
||
+import PageHero from '../components/PageHero.astro';
|
||
+import Timeline from '../components/about/Timeline.astro';
|
||
+import { Image } from 'astro:assets';
|
||
+import { team } from '../data/team';
|
||
+import { site } from '../data/site';
|
||
+const images = import.meta.glob<{ default: ImageMetadata }>('../assets/img/*.{jpg,png}', { eager: true });
|
||
+const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1]?.default;
|
||
+const founders = team.filter((m) => m.founder);
|
||
+const staff = team.filter((m) => !m.founder);
|
||
+export const prerender = true;
|
||
+---
|
||
+<Base title="Insanitylab" description="Mission, storia, team e metodo di InsanityLab: un laboratorio esclusivo dove movimento, prevenzione e performance convivono in un approccio personalizzato.">
|
||
+ <PageHero title="Insanitylab" />
|
||
+
|
||
+ <section class="section">
|
||
+ <div class="container">
|
||
+ <div class="about-img-row">
|
||
+ <div class="about-img-row__text section-heading">
|
||
+ <h2>Mission</h2>
|
||
+ <p>In InsanityLab crediamo che il movimento non debba essere vissuto come un obbligo o un sacrificio, ma come una forma autentica di cura di sé. La nostra missione è accompagnare ogni persona in un percorso di consapevolezza, aiutandola a riscoprire il benessere fisico e mentale attraverso un approccio sostenibile, concreto e duraturo.</p>
|
||
+ </div>
|
||
+ {imgOf('about-training-group') && (
|
||
+ <div class="about-img-row__img">
|
||
+ <Image src={imgOf('about-training-group')!} alt="Training group InsanityLab" widths={[600, 900]} sizes="(max-width:767px) 100vw, 50vw" />
|
||
+ </div>
|
||
+ )}
|
||
+ </div>
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ <section class="section section--alt">
|
||
+ <div class="container about-2col">
|
||
+ <div>
|
||
+ <h2>Direzione</h2>
|
||
+ <p>Non ci limitiamo ad allenare il corpo: creiamo percorsi personalizzati che uniscono movimento, equilibrio e benessere, attraverso il supporto di professionisti qualificati, programmi su misura e un approccio centrato sulla persona e su risultati duraturi.</p>
|
||
+ <a class="btn" href="/programs">Scopri</a>
|
||
+ </div>
|
||
+ <div>
|
||
+ <h2>Approccio</h2>
|
||
+ <p>InsanityLab si distingue per un approccio serio e personalizzato al benessere, basato su competenze specializzate, costanza e cura quotidiana, rivolto a chiunque voglia migliorare il proprio stile di vita in modo consapevole e duraturo.</p>
|
||
+ <a class="btn btn--dark" href="/contact">Contattaci</a>
|
||
+ </div>
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ <section class="section">
|
||
+ <div class="container">
|
||
+ <div class="about-img-row about-img-row--reverse">
|
||
+ <div class="about-img-row__text section-heading">
|
||
+ <h2>About</h2>
|
||
+ <p>InsanityLab nasce con l'obiettivo di unire fitness e salute in un unico percorso dedicato al benessere globale, superando il concetto tradizionale di palestra. Un laboratorio esclusivo, guidato da osteopati e chinesiologi e professionisti specializzati, dove movimento, prevenzione e performance convivono all'interno di un approccio personalizzato e consapevole.</p>
|
||
+ </div>
|
||
+ {imgOf('about-team-group') && (
|
||
+ <div class="about-img-row__img about-img-row__img--native">
|
||
+ <Image src={imgOf('about-team-group')!} alt="Team InsanityLab" widths={[270]} sizes="270px" />
|
||
+ </div>
|
||
+ )}
|
||
+ </div>
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ <Timeline />
|
||
+
|
||
+ <section class="section section--dark">
|
||
+ <div class="container">
|
||
+ <div class="section-heading">
|
||
+ <h2>Convenzioni</h2>
|
||
+ <p>InsanityLab collabora con aziende, enti e organizzazioni che desiderano investire concretamente nel benessere delle proprie persone, attraverso programmi dedicati alla salute, al movimento e alla qualità della vita. Le convenzioni nascono con l'obiettivo di portare il valore del benessere anche all'interno del contesto professionale, creando percorsi che favoriscano equilibrio fisico, prevenzione e performance quotidiana.</p>
|
||
+ </div>
|
||
+ <ul class="conv">
|
||
+ <li>Le realtà convenzionate possono accedere a programmi di welfare aziendale personalizzati, con percorsi individuali, small group training e classi di gruppo seguiti da professionisti qualificati.</li>
|
||
+ <li>Ogni collaborazione parte da una consulenza gratuita preliminare per analizzare esigenze e obiettivi, così da sviluppare soluzioni efficaci e sostenibili.</li>
|
||
+ <li>Le convenzioni vengono definite su misura insieme a InsanityLab, in base alle caratteristiche e alle necessità dell'azienda partner.</li>
|
||
+ </ul>
|
||
+ <p class="conv__cta">Per ricevere maggiori informazioni o attivare una convenzione: {site.email} · {site.phone}</p>
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ <section class="section">
|
||
+ <div class="container">
|
||
+ <div class="section-heading">
|
||
+ <h2>Team</h2>
|
||
+ <p>Il team di InsanityLab riunisce professionisti specializzati in allenamento, salute e benessere, con un approccio multidisciplinare basato su competenza, personalizzazione e attenzione alla persona.</p>
|
||
+ </div>
|
||
+ <div class="tgrid tgrid--founders">
|
||
+ {founders.map((m) => (
|
||
+ <article class="tcard">
|
||
+ {m.image && imgOf(m.image) && <Image src={imgOf(m.image)!} alt={m.name} widths={[300, 500]} sizes="(max-width:767px) 100vw, 33vw" />}
|
||
+ <h3>{m.shortName}</h3>
|
||
+ <p class="tcard__role">{m.role}</p>
|
||
+ <p>{m.bio}</p>
|
||
+ </article>
|
||
+ ))}
|
||
+ </div>
|
||
+ <p class="section-heading" style="margin-top:60px">Accanto ai fondatori opera un team in continua crescita composto da trainer e professionisti con specializzazioni complementari:</p>
|
||
+ <div class="tgrid">
|
||
+ {staff.map((m) => (
|
||
+ <article class="tcard">
|
||
+ {m.image && imgOf(m.image) && <Image src={imgOf(m.image)!} alt={m.name} widths={[300, 500]} sizes="(max-width:767px) 100vw, 33vw" />}
|
||
+ <h3>{m.shortName}</h3>
|
||
+ <p class="tcard__role">{m.role}</p>
|
||
+ <p>{m.bio}</p>
|
||
+ </article>
|
||
+ ))}
|
||
+ </div>
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ <section class="section section--alt">
|
||
+ <div class="container section-heading">
|
||
+ <h2>Metodo</h2>
|
||
+ <p>Il team InsanityLab lavora con un approccio empatico e personalizzato, seguendo ogni persona in un percorso continuo e su misura. Unisce competenze multidisciplinari in allenamento, prevenzione, riabilitazione e performance, supportate da formazione costante. Il metodo si basa su sinergia, condivisione e qualità, per generare risultati concreti e duraturi.</p>
|
||
+ <p>In poche parole, il team di InsanityLab è composto da professionisti che credono profondamente in ciò che fanno: un gruppo unito da valori condivisi, formato per eccellere e guidato dalla convinzione che il movimento sia uno strumento di benessere, equilibrio e qualità della vita.</p>
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ <section class="section">
|
||
+ <div class="container">
|
||
+ <div class="about-img-row">
|
||
+ <div class="about-img-row__text section-heading">
|
||
+ <h2>In-Sanity Studios</h2>
|
||
+ <p>IN-SANITY STUDIOS è il nuovo spazio professionale nato dall'evoluzione di InsanityLab, dedicato ai professionisti della salute, del benessere e della performance. Più che un semplice insieme di studi professionali, rappresenta un ecosistema multidisciplinare pensato per favorire collaborazione, qualità del servizio e integrazione tra competenze diverse.</p>
|
||
+ <p>Il progetto nasce con l'obiettivo di offrire a liberi professionisti uno spazio elegante, riservato e già attrezzato, in cui poter svolgere la propria attività all'interno di un contesto consolidato e altamente qualificato, senza dover affrontare i costi e le complessità di una struttura indipendente.</p>
|
||
+ <p>IN-SANITY STUDIOS si rivolge a figure specializzate nel settore salute e wellness, come fisioterapisti, osteopati, psicologi, nutrizionisti, massaggiatori e altri professionisti dedicati alla cura della persona. Ogni studio è progettato per garantire comfort, professionalità e continuità operativa, all'interno di un ambiente coerente con la filosofia di InsanityLab: mettere il benessere globale della persona al centro del percorso.</p>
|
||
+ </div>
|
||
+ {imgOf('about-exterior') && (
|
||
+ <div class="about-img-row__img">
|
||
+ <Image src={imgOf('about-exterior')!} alt="In-Sanity Studios" widths={[600, 900]} sizes="(max-width:767px) 100vw, 50vw" />
|
||
+ </div>
|
||
+ )}
|
||
+ </div>
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ <section class="section section--alt">
|
||
+ <div class="container section-heading">
|
||
+ <h2>Vision</h2>
|
||
+ <p>InsanityLab nasce con l'obiettivo di aiutare le persone a ritrovare energia ed equilibrio attraverso il movimento, promuovendo un approccio autentico, scientifico e umano al benessere. La sua visione va oltre l'estetica e la performance: allenarsi significa prendersi cura di sé, costruendo abitudini sane, sostenibili e orientate alla longevità, alla prevenzione e alla qualità della vita.</p>
|
||
+ <p>L'obiettivo è diventare un punto di riferimento capace di lasciare un impatto reale nella comunità, trasformando il movimento in uno strumento di salute, consapevolezza e identità.</p>
|
||
+ </div>
|
||
+ </section>
|
||
+</Base>
|
||
+
|
||
+<style>
|
||
+ .about-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 60px; }
|
||
+ .about-img-row { display: grid; grid-template-columns: 1fr 1fr; gap: 60px; align-items: center; }
|
||
+ .about-img-row--reverse { direction: rtl; }
|
||
+ .about-img-row--reverse > * { direction: ltr; }
|
||
+ .about-img-row__img img { width: 100%; height: auto; display: block; }
|
||
+ .about-img-row__img--native { max-width: 320px; margin: 0 auto; }
|
||
+ .conv { max-width: 760px; margin: 0 auto 30px; display: grid; gap: 18px; padding-left: 20px; }
|
||
+ .conv__cta { text-align: center; font-family: var(--font-heading); letter-spacing: .06em; }
|
||
+ .tgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 40px; }
|
||
+ .tcard img { aspect-ratio: 3/4; object-fit: cover; margin-bottom: 16px; }
|
||
+ .tcard h3 { text-transform: lowercase; letter-spacing: .2em; }
|
||
+ .tcard__role { font-size: .8rem; text-transform: uppercase; letter-spacing: .12em; color: var(--c-accent-dark); }
|
||
+ .tcard p { font-size: .9rem; }
|
||
+ @media (max-width: 767px) {
|
||
+ .about-2col,
|
||
+ .tgrid,
|
||
+ .about-img-row { grid-template-columns: 1fr; }
|
||
+ .about-img-row--reverse { direction: ltr; }
|
||
+ }
|
||
+</style>
|
||
diff --git a/src/pages/admin/edit/[id].astro b/src/pages/admin/edit/[id].astro
|
||
new file mode 100644
|
||
index 0000000..7789e84
|
||
--- /dev/null
|
||
+++ b/src/pages/admin/edit/[id].astro
|
||
@@ -0,0 +1,14 @@
|
||
+---
|
||
+import Admin from '../../../layouts/Admin.astro';
|
||
+import PostForm from '../../../components/admin/PostForm.astro';
|
||
+import { getDb } from '../../../lib/db';
|
||
+import { getPostById } from '../../../lib/posts';
|
||
+export const prerender = false;
|
||
+const post = getPostById(getDb(), Number(Astro.params.id));
|
||
+if (!post) return Astro.redirect('/admin');
|
||
+---
|
||
+<Admin title={`Modifica: ${post.title}`}>
|
||
+ <h1>Modifica articolo</h1>
|
||
+ <script id="initial-body" type="text/plain" set:html={post.body_html}></script>
|
||
+ <PostForm post={post} />
|
||
+</Admin>
|
||
diff --git a/src/pages/admin/index.astro b/src/pages/admin/index.astro
|
||
new file mode 100644
|
||
index 0000000..a8e0cd3
|
||
--- /dev/null
|
||
+++ b/src/pages/admin/index.astro
|
||
@@ -0,0 +1,37 @@
|
||
+---
|
||
+import Admin from '../../layouts/Admin.astro';
|
||
+import { getDb } from '../../lib/db';
|
||
+import { listAllPosts } from '../../lib/posts';
|
||
+export const prerender = false;
|
||
+const posts = listAllPosts(getDb());
|
||
+const fmt = new Intl.DateTimeFormat('it-IT', { dateStyle: 'short', timeStyle: 'short' });
|
||
+---
|
||
+<Admin title="Articoli">
|
||
+ <h1>Articoli</h1>
|
||
+ <p><a class="abtn" href="/admin/new">+ Nuovo articolo</a></p>
|
||
+ <table>
|
||
+ <thead><tr><th>Titolo</th><th>Categoria</th><th>Stato</th><th>Aggiornato</th><th></th></tr></thead>
|
||
+ <tbody>
|
||
+ {posts.map((p) => (
|
||
+ <tr>
|
||
+ <td><a href={`/admin/edit/${p.id}`}>{p.title}</a></td>
|
||
+ <td>{p.category}</td>
|
||
+ <td><span class:list={['pill', { 'pill--draft': p.draft }]}>{p.draft ? 'Bozza' : 'Pubblicato'}</span></td>
|
||
+ <td>{fmt.format(new Date(p.updated_at.replace(' ', 'T') + 'Z'))}</td>
|
||
+ <td><button class="abtn abtn--danger" data-del={p.id}>Elimina</button></td>
|
||
+ </tr>
|
||
+ ))}
|
||
+ </tbody>
|
||
+ </table>
|
||
+ {posts.length === 0 && <p>Nessun articolo: creane uno nuovo.</p>}
|
||
+</Admin>
|
||
+
|
||
+<script>
|
||
+ document.querySelectorAll<HTMLButtonElement>('[data-del]').forEach((b) =>
|
||
+ b.addEventListener('click', async () => {
|
||
+ if (!confirm('Eliminare definitivamente questo articolo?')) return;
|
||
+ const res = await fetch(`/api/admin/posts/${b.dataset.del}`, { method: 'DELETE' });
|
||
+ if (res.ok) location.reload();
|
||
+ else alert('Eliminazione non riuscita.');
|
||
+ }));
|
||
+</script>
|
||
diff --git a/src/pages/admin/login.astro b/src/pages/admin/login.astro
|
||
new file mode 100644
|
||
index 0000000..2f39dd8
|
||
--- /dev/null
|
||
+++ b/src/pages/admin/login.astro
|
||
@@ -0,0 +1,34 @@
|
||
+---
|
||
+import Admin from '../../layouts/Admin.astro';
|
||
+import { getDb } from '../../lib/db';
|
||
+import { login, SESSION_COOKIE } from '../../lib/auth';
|
||
+import { rateLimit } from '../../lib/rate-limit';
|
||
+export const prerender = false;
|
||
+
|
||
+let error = '';
|
||
+if (Astro.request.method === 'POST') {
|
||
+ if (!rateLimit(`login:${Astro.clientAddress}`, 5, 15 * 60 * 1000)) {
|
||
+ error = 'Troppi tentativi: riprova tra 15 minuti.';
|
||
+ } else {
|
||
+ const form = await Astro.request.formData();
|
||
+ const token = login(getDb(), String(form.get('username') ?? ''), String(form.get('password') ?? ''));
|
||
+ if (token) {
|
||
+ Astro.cookies.set(SESSION_COOKIE, token, {
|
||
+ httpOnly: true, sameSite: 'lax', path: '/',
|
||
+ secure: import.meta.env.PROD, maxAge: 7 * 24 * 3600,
|
||
+ });
|
||
+ return Astro.redirect('/admin');
|
||
+ }
|
||
+ error = 'Credenziali non valide.';
|
||
+ }
|
||
+}
|
||
+---
|
||
+<Admin title="Login">
|
||
+ <h1>Accesso</h1>
|
||
+ {error && <p style="color:#a33">{error}</p>}
|
||
+ <form method="post" style="max-width:360px">
|
||
+ <input class="afield" name="username" placeholder="Utente" required autocomplete="username" />
|
||
+ <input class="afield" type="password" name="password" placeholder="Password" required autocomplete="current-password" />
|
||
+ <button class="abtn" type="submit">Entra</button>
|
||
+ </form>
|
||
+</Admin>
|
||
diff --git a/src/pages/admin/logout.ts b/src/pages/admin/logout.ts
|
||
new file mode 100644
|
||
index 0000000..883170b
|
||
--- /dev/null
|
||
+++ b/src/pages/admin/logout.ts
|
||
@@ -0,0 +1,11 @@
|
||
+import type { APIRoute } from 'astro';
|
||
+import { getDb } from '../../lib/db';
|
||
+import { logout, SESSION_COOKIE } from '../../lib/auth';
|
||
+export const prerender = false;
|
||
+
|
||
+export const GET: APIRoute = ({ cookies, redirect }) => {
|
||
+ const token = cookies.get(SESSION_COOKIE)?.value;
|
||
+ if (token) logout(getDb(), token);
|
||
+ cookies.delete(SESSION_COOKIE, { path: '/' });
|
||
+ return redirect('/admin/login');
|
||
+};
|
||
diff --git a/src/pages/admin/new.astro b/src/pages/admin/new.astro
|
||
new file mode 100644
|
||
index 0000000..08163ea
|
||
--- /dev/null
|
||
+++ b/src/pages/admin/new.astro
|
||
@@ -0,0 +1,10 @@
|
||
+---
|
||
+import Admin from '../../layouts/Admin.astro';
|
||
+import PostForm from '../../components/admin/PostForm.astro';
|
||
+export const prerender = false;
|
||
+---
|
||
+<Admin title="Nuovo articolo">
|
||
+ <h1>Nuovo articolo</h1>
|
||
+ <script id="initial-body" type="text/plain"></script>
|
||
+ <PostForm />
|
||
+</Admin>
|
||
diff --git a/src/pages/api/admin/posts/[id].ts b/src/pages/api/admin/posts/[id].ts
|
||
new file mode 100644
|
||
index 0000000..c7e8a33
|
||
--- /dev/null
|
||
+++ b/src/pages/api/admin/posts/[id].ts
|
||
@@ -0,0 +1,29 @@
|
||
+import type { APIRoute } from 'astro';
|
||
+import { getDb } from '../../../../lib/db';
|
||
+import { getPostById, updatePost, deletePost } from '../../../../lib/posts';
|
||
+import { parsePostBody } from '../../../../lib/post-body';
|
||
+export const prerender = false;
|
||
+
|
||
+const json = (status: number, body: object) =>
|
||
+ new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
||
+
|
||
+export const PUT: APIRoute = async ({ params, request }) => {
|
||
+ const id = Number(params.id);
|
||
+ if (!getPostById(getDb(), id)) return json(404, { error: 'Articolo non trovato.' });
|
||
+ let data: Record<string, unknown>;
|
||
+ try { data = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); }
|
||
+ const parsed = parsePostBody(data);
|
||
+ if ('error' in parsed) return json(400, { error: parsed.error });
|
||
+ try {
|
||
+ updatePost(getDb(), id, parsed.value);
|
||
+ return json(200, { ok: true });
|
||
+ } catch (err: any) {
|
||
+ if (String(err?.message).includes('UNIQUE')) return json(400, { error: 'Slug già esistente.' });
|
||
+ throw err;
|
||
+ }
|
||
+};
|
||
+
|
||
+export const DELETE: APIRoute = ({ params }) => {
|
||
+ deletePost(getDb(), Number(params.id));
|
||
+ return json(200, { ok: true });
|
||
+};
|
||
diff --git a/src/pages/api/admin/posts/index.ts b/src/pages/api/admin/posts/index.ts
|
||
new file mode 100644
|
||
index 0000000..16305d7
|
||
--- /dev/null
|
||
+++ b/src/pages/api/admin/posts/index.ts
|
||
@@ -0,0 +1,22 @@
|
||
+import type { APIRoute } from 'astro';
|
||
+import { getDb } from '../../../../lib/db';
|
||
+import { createPost } from '../../../../lib/posts';
|
||
+import { parsePostBody } from '../../../../lib/post-body';
|
||
+export const prerender = false;
|
||
+
|
||
+const json = (status: number, body: object) =>
|
||
+ new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
||
+
|
||
+export const POST: APIRoute = async ({ request }) => {
|
||
+ let data: Record<string, unknown>;
|
||
+ try { data = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); }
|
||
+ const parsed = parsePostBody(data);
|
||
+ if ('error' in parsed) return json(400, { error: parsed.error });
|
||
+ try {
|
||
+ const id = createPost(getDb(), parsed.value);
|
||
+ return json(201, { id, slug: parsed.value.slug });
|
||
+ } catch (err: any) {
|
||
+ if (String(err?.message).includes('UNIQUE')) return json(400, { error: 'Slug già esistente.' });
|
||
+ throw err;
|
||
+ }
|
||
+};
|
||
diff --git a/src/pages/api/admin/upload.ts b/src/pages/api/admin/upload.ts
|
||
new file mode 100644
|
||
index 0000000..e14487d
|
||
--- /dev/null
|
||
+++ b/src/pages/api/admin/upload.ts
|
||
@@ -0,0 +1,26 @@
|
||
+import type { APIRoute } from 'astro';
|
||
+import { writeFile, mkdir } from 'node:fs/promises';
|
||
+import { randomBytes } from 'node:crypto';
|
||
+import { join, extname } from 'node:path';
|
||
+import { getEnv } from '../../../lib/env';
|
||
+export const prerender = false;
|
||
+
|
||
+const ALLOWED: Record<string, string> = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp', '.gif': 'image/gif' };
|
||
+const MAX_BYTES = 5 * 1024 * 1024;
|
||
+
|
||
+const json = (status: number, body: object) =>
|
||
+ new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
||
+
|
||
+export const POST: APIRoute = async ({ request }) => {
|
||
+ const form = await request.formData().catch(() => null);
|
||
+ const file = form?.get('file');
|
||
+ if (!(file instanceof File)) return json(400, { error: 'Nessun file.' });
|
||
+ const ext = extname(file.name).toLowerCase();
|
||
+ if (!ALLOWED[ext]) return json(400, { error: 'Formato non consentito (jpg, png, webp, gif).' });
|
||
+ if (file.size > MAX_BYTES) return json(400, { error: 'File troppo grande (max 5 MB).' });
|
||
+ const dir = getEnv('UPLOADS_DIR', 'uploads');
|
||
+ await mkdir(dir, { recursive: true });
|
||
+ const name = `${Date.now()}-${randomBytes(4).toString('hex')}${ext}`;
|
||
+ await writeFile(join(dir, name), Buffer.from(await file.arrayBuffer()));
|
||
+ return json(200, { url: `/uploads/${name}` });
|
||
+};
|
||
diff --git a/src/pages/api/contact.ts b/src/pages/api/contact.ts
|
||
new file mode 100644
|
||
index 0000000..74aba2f
|
||
--- /dev/null
|
||
+++ b/src/pages/api/contact.ts
|
||
@@ -0,0 +1,26 @@
|
||
+import type { APIRoute } from 'astro';
|
||
+import { validateContact } from '../../lib/contact';
|
||
+import { sendContactEmail } from '../../lib/mailer';
|
||
+import { rateLimit } from '../../lib/rate-limit';
|
||
+
|
||
+export const prerender = false;
|
||
+
|
||
+const json = (status: number, body: object) =>
|
||
+ new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
||
+
|
||
+export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||
+ if (!rateLimit(`contact:${clientAddress}`, 5, 60 * 60 * 1000)) {
|
||
+ return json(429, { error: 'Troppe richieste, riprova più tardi.' });
|
||
+ }
|
||
+ let data: unknown;
|
||
+ try { data = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); }
|
||
+ const result = validateContact(data);
|
||
+ if (!result.ok) return json(400, { error: result.error });
|
||
+ try {
|
||
+ await sendContactEmail(result.value);
|
||
+ } catch (err) {
|
||
+ console.error('Invio email fallito:', err);
|
||
+ return json(502, { error: 'Invio non riuscito, riprova più tardi o scrivici direttamente.' });
|
||
+ }
|
||
+ return json(200, { ok: true });
|
||
+};
|
||
diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[slug].astro
|
||
new file mode 100644
|
||
index 0000000..fdf30f1
|
||
--- /dev/null
|
||
+++ b/src/pages/blog/[slug].astro
|
||
@@ -0,0 +1,33 @@
|
||
+---
|
||
+import Base from '../../layouts/Base.astro';
|
||
+import Sidebar from '../../components/blog/Sidebar.astro';
|
||
+import { getDb } from '../../lib/db';
|
||
+import { getPublishedBySlug } from '../../lib/posts';
|
||
+export const prerender = false;
|
||
+
|
||
+const post = getPublishedBySlug(getDb(), Astro.params.slug!);
|
||
+if (!post) return Astro.redirect('/404');
|
||
+const fmt = new Intl.DateTimeFormat('it-IT', { dateStyle: 'long' });
|
||
+---
|
||
+<Base title={post.title} description={post.excerpt}>
|
||
+ <section class="section">
|
||
+ <div class="container art">
|
||
+ <article>
|
||
+ {post.cover && <img class="art__cover" src={post.cover} alt="" />}
|
||
+ <p class="art__meta">{post.published_at && fmt.format(new Date(post.published_at))} · {post.category}</p>
|
||
+ <h1>{post.title}</h1>
|
||
+ <div class="art__body" set:html={post.body_html} />
|
||
+ </article>
|
||
+ <Sidebar />
|
||
+ </div>
|
||
+ </section>
|
||
+</Base>
|
||
+
|
||
+<style>
|
||
+ .art { display: grid; grid-template-columns: 2.4fr 1fr; gap: 60px; }
|
||
+ .art__cover { width: 100%; margin-bottom: 24px; }
|
||
+ .art__meta { font-size: .8rem; color: var(--c-text-light); }
|
||
+ .art__body :global(img) { margin-block: 20px; }
|
||
+ .art__body :global(blockquote) { border-left: 3px solid var(--c-accent); margin: 24px 0; padding: 6px 0 6px 20px; font-style: italic; color: var(--c-heading); }
|
||
+ @media (max-width: 991px) { .art { grid-template-columns: 1fr; } }
|
||
+</style>
|
||
diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro
|
||
new file mode 100644
|
||
index 0000000..dcd8a05
|
||
--- /dev/null
|
||
+++ b/src/pages/blog/index.astro
|
||
@@ -0,0 +1,81 @@
|
||
+---
|
||
+import Base from '../../layouts/Base.astro';
|
||
+import PageHero from '../../components/PageHero.astro';
|
||
+import { getDb } from '../../lib/db';
|
||
+import { listPublished } from '../../lib/posts';
|
||
+import { CATEGORIES } from '../../lib/blog-const';
|
||
+export const prerender = false;
|
||
+
|
||
+const PER_PAGE = 9;
|
||
+const url = Astro.url;
|
||
+const category = url.searchParams.get('categoria') ?? undefined;
|
||
+const page = Math.max(1, Number(url.searchParams.get('pagina') ?? '1') || 1);
|
||
+const { items, total } = listPublished(getDb(), {
|
||
+ category: category && (CATEGORIES as readonly string[]).includes(category) ? category : undefined,
|
||
+ page, perPage: PER_PAGE,
|
||
+});
|
||
+const pages = Math.max(1, Math.ceil(total / PER_PAGE));
|
||
+const fmt = (d: string | null) => d ? new Intl.DateTimeFormat('it-IT', { dateStyle: 'long' }).format(new Date(d)) : '';
|
||
+const linkFor = (cat?: string, p = 1) => {
|
||
+ const q = new URLSearchParams();
|
||
+ if (cat) q.set('categoria', cat);
|
||
+ if (p > 1) q.set('pagina', String(p));
|
||
+ const s = q.toString();
|
||
+ return `/blog${s ? `?${s}` : ''}`;
|
||
+};
|
||
+---
|
||
+<Base title="Blog & Edugo" description="Edugo è lo spazio di InsanityLab dedicato alla conoscenza: articoli, guide e approfondimenti su allenamento, nutrizione e salute.">
|
||
+ <PageHero title="Blog & Edugo" />
|
||
+ <section class="section">
|
||
+ <div class="container">
|
||
+ <div class="section-heading">
|
||
+ <h2>Blog & Edugo</h2>
|
||
+ <p>Edugo è lo spazio di InsanityLab dedicato alla conoscenza: articoli, guide e approfondimenti su allenamento, nutrizione e salute per capire il corpo e migliorare il proprio benessere. Perché la conoscenza è il primo passo del cambiamento.</p>
|
||
+ </div>
|
||
+ <nav class="bfilters" aria-label="categorie">
|
||
+ <a class:list={['bfilters__item', { 'is-active': !category }]} href={linkFor()}>Mostra tutto</a>
|
||
+ {CATEGORIES.map((c) => (
|
||
+ <a class:list={['bfilters__item', { 'is-active': category === c }]} href={linkFor(c)}>{c}</a>
|
||
+ ))}
|
||
+ </nav>
|
||
+ {items.length === 0 ? (
|
||
+ <p style="text-align:center">Nessun articolo pubblicato{category ? ' in questa categoria' : ''}, torna a trovarci presto.</p>
|
||
+ ) : (
|
||
+ <div class="bgrid">
|
||
+ {items.map((post) => (
|
||
+ <article class="bcard">
|
||
+ {post.cover && <a href={`/blog/${post.slug}`}><img src={post.cover} alt="" loading="lazy" /></a>}
|
||
+ <p class="bcard__date">{fmt(post.published_at)}</p>
|
||
+ <h3><a href={`/blog/${post.slug}`}>{post.title}</a></h3>
|
||
+ <p>{post.excerpt}</p>
|
||
+ <a class="bcard__more" href={`/blog/${post.slug}`}>Per saperne di più →</a>
|
||
+ </article>
|
||
+ ))}
|
||
+ </div>
|
||
+ )}
|
||
+ {pages > 1 && (
|
||
+ <nav class="bpages" aria-label="paginazione">
|
||
+ {Array.from({ length: pages }, (_, i) => (
|
||
+ <a class:list={['bpages__n', { 'is-active': page === i + 1 }]} href={linkFor(category, i + 1)}>{i + 1}</a>
|
||
+ ))}
|
||
+ </nav>
|
||
+ )}
|
||
+ </div>
|
||
+ </section>
|
||
+</Base>
|
||
+
|
||
+<style>
|
||
+ .bfilters { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; margin-bottom: 50px; }
|
||
+ .bfilters__item { font-family: var(--font-heading); font-size: .7rem; letter-spacing: .16em; text-transform: uppercase; text-decoration: none; color: var(--c-text); padding: 10px 16px; }
|
||
+ .bfilters__item.is-active { background: var(--c-accent); color: #fff; }
|
||
+ .bgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 50px 40px; }
|
||
+ .bcard img { aspect-ratio: 4/3; object-fit: cover; width: 100%; margin-bottom: 14px; }
|
||
+ .bcard__date { font-size: .78rem; color: var(--c-text-light); margin-bottom: 4px; }
|
||
+ .bcard h3 a { text-decoration: none; }
|
||
+ .bcard__more { font-family: var(--font-heading); font-size: .7rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; text-decoration: none; color: var(--c-heading); }
|
||
+ .bpages { display: flex; gap: 8px; justify-content: center; margin-top: 50px; }
|
||
+ .bpages__n { font-family: var(--font-heading); text-decoration: none; padding: 8px 14px; color: var(--c-text); }
|
||
+ .bpages__n.is-active { background: var(--c-dark); color: #fff; }
|
||
+ @media (max-width: 991px) { .bgrid { grid-template-columns: 1fr 1fr; } }
|
||
+ @media (max-width: 600px) { .bgrid { grid-template-columns: 1fr; } }
|
||
+</style>
|
||
diff --git a/src/pages/contact.astro b/src/pages/contact.astro
|
||
new file mode 100644
|
||
index 0000000..f3e35dc
|
||
--- /dev/null
|
||
+++ b/src/pages/contact.astro
|
||
@@ -0,0 +1,55 @@
|
||
+---
|
||
+import Base from '../layouts/Base.astro';
|
||
+import PageHero from '../components/PageHero.astro';
|
||
+import ContactForm from '../components/ContactForm.astro';
|
||
+import { site } from '../data/site';
|
||
+export const prerender = true;
|
||
+const mapsQuery = encodeURIComponent(`${site.address}, ${site.cityLine}`);
|
||
+---
|
||
+<Base title="Contatti" description="Contatta InsanityLab: Via Leandro Alberti 76, Bologna. Compila il form o chiamaci per una consulenza iniziale.">
|
||
+ <PageHero title="Contattaci" />
|
||
+ <section class="section">
|
||
+ <div class="container cgrid">
|
||
+ <div class="cmap" id="map-box">
|
||
+ <button class="btn btn--dark" id="map-load">Carica la mappa (Google Maps)</button>
|
||
+ <p class="cmap__note">La mappa viene caricata da Google solo dopo il tuo consenso.</p>
|
||
+ </div>
|
||
+ <ContactForm variant="full" />
|
||
+ </div>
|
||
+ <div class="container cinfo">
|
||
+ <div>
|
||
+ <h3>Indirizzo</h3>
|
||
+ <p>{site.address} {site.cityLine}</p>
|
||
+ </div>
|
||
+ <div>
|
||
+ <h3>Telefono</h3>
|
||
+ <p>{site.phone}</p>
|
||
+ </div>
|
||
+ <div>
|
||
+ <h3>E-mail</h3>
|
||
+ <p><a href={`mailto:${site.email}`}>{site.email}</a></p>
|
||
+ </div>
|
||
+ </div>
|
||
+ <div class="container">
|
||
+ <p class="cinvite">Per informazioni o per richiedere una consulenza iniziale, compila il form. Un team specializzato ti accoglierà e ti ricontatterà al più presto, oppure lasciaci il tuo contatto.</p>
|
||
+ </div>
|
||
+ </section>
|
||
+</Base>
|
||
+
|
||
+<style>
|
||
+ .cgrid { display: grid; grid-template-columns: 1.2fr 1fr; gap: 50px; margin-bottom: 50px; }
|
||
+ .cmap { background: var(--c-bg-alt); min-height: 420px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; }
|
||
+ .cmap iframe { width: 100%; height: 100%; border: 0; min-height: 420px; }
|
||
+ .cmap__note { font-size: .8rem; color: var(--c-text-light); }
|
||
+ .cinfo { display: grid; grid-template-columns: repeat(3, 1fr); gap: 30px; margin-bottom: 30px; }
|
||
+ .cinfo h3 { font-size: .85rem; text-transform: uppercase; letter-spacing: .16em; }
|
||
+ .cinvite { max-width: 640px; }
|
||
+ @media (max-width: 991px) { .cgrid, .cinfo { grid-template-columns: 1fr; } }
|
||
+</style>
|
||
+
|
||
+<script define:vars={{ mapsQuery }}>
|
||
+ document.getElementById('map-load')?.addEventListener('click', () => {
|
||
+ const box = document.getElementById('map-box');
|
||
+ box.innerHTML = `<iframe src="https://www.google.com/maps?q=${mapsQuery}&output=embed" loading="lazy" title="Mappa InsanityLab" referrerpolicy="no-referrer-when-downgrade"></iframe>`;
|
||
+ });
|
||
+</script>
|
||
diff --git a/src/pages/index.astro b/src/pages/index.astro
|
||
new file mode 100644
|
||
index 0000000..665a928
|
||
--- /dev/null
|
||
+++ b/src/pages/index.astro
|
||
@@ -0,0 +1,32 @@
|
||
+---
|
||
+import Base from '../layouts/Base.astro';
|
||
+import HeroSlider from '../components/home/HeroSlider.astro';
|
||
+import InfoColumns from '../components/home/InfoColumns.astro';
|
||
+import FeatureBlocks from '../components/home/FeatureBlocks.astro';
|
||
+import QuoteBanner from '../components/home/QuoteBanner.astro';
|
||
+import TrainingCards from '../components/home/TrainingCards.astro';
|
||
+import ProgramsGrid from '../components/home/ProgramsGrid.astro';
|
||
+import Agenda from '../components/Agenda.astro';
|
||
+import TeamSection from '../components/home/TeamSection.astro';
|
||
+import MethodSteps from '../components/home/MethodSteps.astro';
|
||
+import PartnersStrip from '../components/home/PartnersStrip.astro';
|
||
+export const prerender = true;
|
||
+---
|
||
+<Base title="Performance. Balance. Longevity.">
|
||
+ <HeroSlider />
|
||
+ <InfoColumns />
|
||
+ <FeatureBlocks />
|
||
+ <QuoteBanner />
|
||
+ <section class="section" style="padding-bottom:0">
|
||
+ <div class="section-heading">
|
||
+ <h2>Training</h2>
|
||
+ <p>Porta i tuoi allenamenti al livello successivo: trova il percorso più adatto alle tue esigenze.</p>
|
||
+ </div>
|
||
+ </section>
|
||
+ <TrainingCards />
|
||
+ <ProgramsGrid />
|
||
+ <Agenda />
|
||
+ <TeamSection />
|
||
+ <MethodSteps />
|
||
+ <PartnersStrip />
|
||
+</Base>
|
||
diff --git a/src/pages/programs/[slug].astro b/src/pages/programs/[slug].astro
|
||
new file mode 100644
|
||
index 0000000..6f0d178
|
||
--- /dev/null
|
||
+++ b/src/pages/programs/[slug].astro
|
||
@@ -0,0 +1,98 @@
|
||
+---
|
||
+import Base from '../../layouts/Base.astro';
|
||
+import PageHero from '../../components/PageHero.astro';
|
||
+import { programs } from '../../data/programs';
|
||
+import { performanceClassPricing } from '../../data/pricing';
|
||
+export const prerender = true;
|
||
+
|
||
+export function getStaticPaths() {
|
||
+ return programs.map((p) => ({ params: { slug: p.slug }, props: { program: p } }));
|
||
+}
|
||
+const { program } = Astro.props;
|
||
+const others = programs.filter((p) => p.slug !== program.slug);
|
||
+const blogCategories = ['Fitness', 'Salute', 'Stile di vita', 'Nutrizione'];
|
||
+---
|
||
+<Base title={program.title} description={program.excerpt}>
|
||
+ <PageHero title={program.title.toUpperCase()} />
|
||
+
|
||
+ <section class="section">
|
||
+ <div class="container prog">
|
||
+ <div>
|
||
+ <h2>{program.title.toUpperCase()}</h2>
|
||
+ <p>{program.longDescription}</p>
|
||
+ <ul class="prog__features">
|
||
+ {program.features.map((f) => <li>{f}</li>)}
|
||
+ </ul>
|
||
+ <div class="prog__faq">
|
||
+ {program.faq.map((item, i) => (
|
||
+ <details open={i === 0}>
|
||
+ <summary>{item.q}</summary>
|
||
+ <p>{item.a}</p>
|
||
+ </details>
|
||
+ ))}
|
||
+ </div>
|
||
+ </div>
|
||
+ <aside class="prog__side">
|
||
+ <h3>Categorie</h3>
|
||
+ {blogCategories.map((c) => <p><a href="/blog">{c}</a></p>)}
|
||
+ <h3>Altri programmi</h3>
|
||
+ {others.map((o) => <p><a href={`/programs/${o.slug}`}>{o.title}</a></p>)}
|
||
+ </aside>
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ {program.hasPricing && (
|
||
+ <section class="section section--alt">
|
||
+ <div class="container">
|
||
+ <div class="section-heading">
|
||
+ <h2>{performanceClassPricing.heading}</h2>
|
||
+ <p>{performanceClassPricing.intro}</p>
|
||
+ </div>
|
||
+ {performanceClassPricing.groups.map((g) => (
|
||
+ <details class="price__group" open={g.open}>
|
||
+ <summary>{g.open ? '+' : '–'} {g.label}</summary>
|
||
+ {g.plans.length === 0 ? (
|
||
+ <p class="price__empty">Contattaci per i dettagli di questo programma. <a href="/contact">Scrivici →</a></p>
|
||
+ ) : (
|
||
+ <div class="price__row">
|
||
+ {g.plans.map((p) => (
|
||
+ <div class="price__card">
|
||
+ <p class="price__amount">{p.monthly}€</p>
|
||
+ <p class="price__per">al mese, semestrale</p>
|
||
+ <p class="price__entries">{p.entries} {p.entries === 1 ? 'ingresso settimanale' : 'ingressi settimanali'}</p>
|
||
+ <p>{p.note}</p>
|
||
+ <p>{p.installments}</p>
|
||
+ <p class="price__best">{p.best}</p>
|
||
+ <a class="btn" href="/contact">Acquista</a>
|
||
+ </div>
|
||
+ ))}
|
||
+ </div>
|
||
+ )}
|
||
+ </details>
|
||
+ ))}
|
||
+ </div>
|
||
+ </section>
|
||
+ )}
|
||
+</Base>
|
||
+
|
||
+<style>
|
||
+ .prog { display: grid; grid-template-columns: 2.2fr 1fr; gap: 60px; }
|
||
+ .prog__features { padding-left: 20px; display: grid; gap: 8px; margin-bottom: 30px; }
|
||
+ .prog__features li::marker { color: var(--c-accent); }
|
||
+ .prog__faq details { border-top: 1px solid #e4ded6; padding-block: 14px; }
|
||
+ .prog__faq summary { cursor: pointer; font-family: var(--font-heading); font-size: .85rem; letter-spacing: .08em; color: var(--c-heading); }
|
||
+ .prog__side h3 { font-size: .85rem; text-transform: uppercase; letter-spacing: .16em; margin-top: 30px; }
|
||
+ .prog__side a { text-decoration: none; color: var(--c-text); }
|
||
+ .prog__side a:hover { color: var(--c-accent-dark); }
|
||
+ .price__group { border-top: 1px solid #ddd5ca; padding-block: 18px; }
|
||
+ .price__group summary { cursor: pointer; font-family: var(--font-heading); letter-spacing: .12em; text-transform: uppercase; font-size: .85rem; color: var(--c-heading); }
|
||
+ .price__row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 30px; margin-top: 30px; }
|
||
+ .price__card { background: #fff; padding: 36px 28px; text-align: center; }
|
||
+ .price__amount { font-family: var(--font-heading); font-size: 2.6rem; color: var(--c-heading); margin: 0; }
|
||
+ .price__per { color: var(--c-text-light); font-size: .8rem; }
|
||
+ .price__entries { font-family: var(--font-heading); text-transform: uppercase; letter-spacing: .1em; font-size: .82rem; color: var(--c-heading); }
|
||
+ .price__card p { margin-bottom: 6px; font-size: .85rem; }
|
||
+ .price__best { font-weight: 600; color: var(--c-accent-dark); }
|
||
+ .price__card .btn { margin-top: 16px; }
|
||
+ @media (max-width: 991px) { .prog { grid-template-columns: 1fr; } .price__row { grid-template-columns: 1fr; } }
|
||
+</style>
|
||
diff --git a/src/pages/programs/index.astro b/src/pages/programs/index.astro
|
||
new file mode 100644
|
||
index 0000000..d2446df
|
||
--- /dev/null
|
||
+++ b/src/pages/programs/index.astro
|
||
@@ -0,0 +1,38 @@
|
||
+---
|
||
+import Base from '../../layouts/Base.astro';
|
||
+import PageHero from '../../components/PageHero.astro';
|
||
+import { programs } from '../../data/programs';
|
||
+export const prerender = true;
|
||
+---
|
||
+<Base title="Programmi" description="I programmi InsanityLab: Personal Training, Performance Class, Coaching, Pilates Flow, Rehab e Nutrizione.">
|
||
+ <PageHero title="programmi" />
|
||
+ <section class="section">
|
||
+ <div class="container">
|
||
+ <div class="section-heading">
|
||
+ <h2>Programmi</h2>
|
||
+ <p>Questi sono i programmi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi un'esperienza di allenamento completa e mirata.</p>
|
||
+ </div>
|
||
+ <div class="plist">
|
||
+ {programs.map((p) => (
|
||
+ <article class="plist__card">
|
||
+ <h3>{p.title}</h3>
|
||
+ <p class="plist__sub">{p.subtitle}</p>
|
||
+ <p>{p.excerpt}</p>
|
||
+ <a class="plist__more" href={`/programs/${p.slug}`}>Per saperne di più →</a>
|
||
+ </article>
|
||
+ ))}
|
||
+ </div>
|
||
+ </div>
|
||
+ </section>
|
||
+</Base>
|
||
+
|
||
+<style>
|
||
+ .plist { display: grid; grid-template-columns: repeat(3, 1fr); gap: 50px 40px; }
|
||
+ .plist__card h3 { letter-spacing: .08em; }
|
||
+ .plist__sub { font-size: .8rem; text-transform: uppercase; letter-spacing: .12em; color: var(--c-accent-dark); }
|
||
+ .plist__card p { font-size: .92rem; }
|
||
+ .plist__more { font-family: var(--font-heading); font-size: .72rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; text-decoration: none; color: var(--c-heading); }
|
||
+ .plist__more:hover { color: var(--c-accent-dark); }
|
||
+ @media (max-width: 991px) { .plist { grid-template-columns: 1fr 1fr; } }
|
||
+ @media (max-width: 600px) { .plist { grid-template-columns: 1fr; } }
|
||
+</style>
|
||
diff --git a/src/pages/training.astro b/src/pages/training.astro
|
||
new file mode 100644
|
||
index 0000000..747fffb
|
||
--- /dev/null
|
||
+++ b/src/pages/training.astro
|
||
@@ -0,0 +1,57 @@
|
||
+---
|
||
+import Base from '../layouts/Base.astro';
|
||
+import PageHero from '../components/PageHero.astro';
|
||
+import TrainingCards from '../components/home/TrainingCards.astro';
|
||
+import Agenda from '../components/Agenda.astro';
|
||
+import { Image } from 'astro:assets';
|
||
+import { trainings } from '../data/trainings';
|
||
+const images = import.meta.glob<{ default: ImageMetadata }>('../assets/img/training-*.{jpg,png}', { eager: true });
|
||
+const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1]?.default;
|
||
+export const prerender = true;
|
||
+---
|
||
+<Base title="Training" description="One to One, Small Groups, Performance Class e Fit Remote: trova il training InsanityLab più adatto a te.">
|
||
+ <PageHero title="training" />
|
||
+
|
||
+ <section class="section" style="padding-bottom:0">
|
||
+ <div class="section-heading">
|
||
+ <h2>Training</h2>
|
||
+ <p>Porta i tuoi allenamenti al livello successivo: programmi completi e percorsi su misura. Trova il training più adatto alle tue esigenze.</p>
|
||
+ </div>
|
||
+ </section>
|
||
+ <TrainingCards />
|
||
+
|
||
+ <section class="section">
|
||
+ <div class="container">
|
||
+ {trainings.map((t, i) => (
|
||
+ <div class:list={['tblock', { 'tblock--reverse': i % 2 === 1 }]} id={t.id}>
|
||
+ {imgOf(t.image) && <Image src={imgOf(t.image)!} alt={t.title} widths={[500, 900]} sizes="(max-width:767px) 100vw, 45vw" />}
|
||
+ <div>
|
||
+ <h2>{t.title.toLowerCase()}</h2>
|
||
+ <p>{t.description}</p>
|
||
+ <a class="btn" href={t.cta.href}>{t.cta.label}</a>
|
||
+ </div>
|
||
+ </div>
|
||
+ ))}
|
||
+ </div>
|
||
+ </section>
|
||
+
|
||
+ <Agenda />
|
||
+
|
||
+ <section class="section section--alt">
|
||
+ <div class="container trial">
|
||
+ <div>
|
||
+ <h2>Prenota la lezione di prova</h2>
|
||
+ <p>La lezione di prova è l'occasione perfetta per conoscere il metodo, vivere l'esperienza in prima persona e capire il percorso più adatto a te. Inizia oggi il tuo percorso: prenota la tua lezione di prova.</p>
|
||
+ <a class="btn btn--dark" href="/contact">Prenota</a>
|
||
+ </div>
|
||
+ </div>
|
||
+ </section>
|
||
+</Base>
|
||
+
|
||
+<style>
|
||
+ .tblock { display: grid; grid-template-columns: 1fr 1fr; gap: 60px; align-items: center; padding-block: 40px; scroll-margin-top: calc(var(--header-h) + 20px); }
|
||
+ .tblock--reverse img { order: 2; }
|
||
+ .tblock h2 { letter-spacing: .1em; }
|
||
+ .trial { max-width: 720px; text-align: center; }
|
||
+ @media (max-width: 767px) { .tblock { grid-template-columns: 1fr; gap: 24px; } .tblock--reverse img { order: 0; } }
|
||
+</style>
|
||
diff --git a/src/pages/uploads/[...path].ts b/src/pages/uploads/[...path].ts
|
||
new file mode 100644
|
||
index 0000000..1444dfa
|
||
--- /dev/null
|
||
+++ b/src/pages/uploads/[...path].ts
|
||
@@ -0,0 +1,20 @@
|
||
+import type { APIRoute } from 'astro';
|
||
+import { readFile } from 'node:fs/promises';
|
||
+import { join, normalize, extname } from 'node:path';
|
||
+import { getEnv } from '../../lib/env';
|
||
+export const prerender = false;
|
||
+
|
||
+const MIME: Record<string, string> = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp', '.gif': 'image/gif' };
|
||
+
|
||
+export const GET: APIRoute = async ({ params }) => {
|
||
+ const rel = normalize(params.path ?? '').replace(/^(\.\.[/\\])+/, '');
|
||
+ if (!rel || rel.includes('..')) return new Response('Not found', { status: 404 });
|
||
+ const mime = MIME[extname(rel).toLowerCase()];
|
||
+ if (!mime) return new Response('Not found', { status: 404 });
|
||
+ try {
|
||
+ const buf = await readFile(join(getEnv('UPLOADS_DIR', 'uploads'), rel));
|
||
+ return new Response(buf, { headers: { 'Content-Type': mime, 'Cache-Control': 'public, max-age=31536000, immutable' } });
|
||
+ } catch {
|
||
+ return new Response('Not found', { status: 404 });
|
||
+ }
|
||
+};
|
||
diff --git a/src/styles/global.css b/src/styles/global.css
|
||
new file mode 100644
|
||
index 0000000..e071d40
|
||
--- /dev/null
|
||
+++ b/src/styles/global.css
|
||
@@ -0,0 +1,54 @@
|
||
+/* === Design tokens (valori campionati da Figma) === */
|
||
+:root {
|
||
+ --c-accent: #b8a98c;
|
||
+ --c-accent-dark: #9c8b70;
|
||
+ --c-dark: #3a2929;
|
||
+ --c-dark-2: #4a3a30;
|
||
+ --c-heading: #2b2b2b;
|
||
+ --c-text: #4b4b4b;
|
||
+ --c-text-light: #8a8a8a;
|
||
+ --c-bg: #ffffff;
|
||
+ --c-bg-alt: #f5f3f0;
|
||
+ --font-heading: 'Montserrat', sans-serif;
|
||
+ --font-body: 'Open Sans', sans-serif;
|
||
+ --header-h: 70px;
|
||
+}
|
||
+
|
||
+/* === Reset === */
|
||
+*, *::before, *::after { box-sizing: border-box; }
|
||
+body { margin: 0; font-family: var(--font-body); color: var(--c-text); font-size: 16px; line-height: 1.7; background: var(--c-bg); }
|
||
+img { max-width: 100%; height: auto; display: block; }
|
||
+h1, h2, h3, h4 { font-family: var(--font-heading); color: var(--c-heading); font-weight: 500; letter-spacing: .04em; line-height: 1.2; margin: 0 0 .5em; }
|
||
+h1 { font-size: clamp(2.2rem, 5vw, 3.8rem); }
|
||
+h2 { font-size: clamp(1.8rem, 4vw, 2.6rem); }
|
||
+h3 { font-size: 1.3rem; }
|
||
+a { color: inherit; }
|
||
+p { margin: 0 0 1em; }
|
||
+
|
||
+/* === Utility === */
|
||
+.container { max-width: 1200px; margin-inline: auto; padding-inline: 20px; }
|
||
+.section { padding-block: 90px; }
|
||
+.section--alt { background: var(--c-bg-alt); }
|
||
+.section--dark { background: var(--c-dark); color: #cfc6bd; }
|
||
+.section--dark h2, .section--dark h3 { color: #fff; }
|
||
+.eyebrow { font-family: var(--font-heading); text-transform: uppercase; letter-spacing: .25em; font-size: .75rem; color: var(--c-accent-dark); }
|
||
+.section-heading { text-align: center; max-width: 640px; margin: 0 auto 56px; }
|
||
+.section-heading h2 { text-transform: uppercase; letter-spacing: .12em; }
|
||
+
|
||
+.btn { display: inline-flex; align-items: center; gap: 14px; font-family: var(--font-heading); font-size: .72rem; font-weight: 600; letter-spacing: .2em; text-transform: uppercase; text-decoration: none; border: 0; cursor: pointer; padding: 16px 26px; background: var(--c-accent); color: #fff; transition: background .2s; }
|
||
+.btn:hover { background: var(--c-accent-dark); }
|
||
+.btn::after { content: '→'; font-size: .9rem; }
|
||
+.btn--dark { background: var(--c-dark); }
|
||
+.btn--dark:hover { background: var(--c-dark-2); }
|
||
+.btn--light { background: #fff; color: var(--c-heading); }
|
||
+
|
||
+/* === Form === */
|
||
+.field { width: 100%; border: 1px solid #ddd; background: #fff; padding: 14px 16px; font-family: var(--font-body); font-size: .95rem; color: var(--c-text); margin-bottom: 14px; }
|
||
+.field:focus { outline: 2px solid var(--c-accent); outline-offset: -1px; }
|
||
+textarea.field { min-height: 120px; resize: vertical; }
|
||
+.form-msg { font-size: .9rem; margin-top: 8px; }
|
||
+.form-msg--ok { color: #3c7a3c; }
|
||
+.form-msg--err { color: #b03030; }
|
||
+
|
||
+/* honeypot */
|
||
+.hp { position: absolute; left: -9999px; opacity: 0; }
|
||
diff --git a/tests/auth.test.ts b/tests/auth.test.ts
|
||
new file mode 100644
|
||
index 0000000..5eb2459
|
||
--- /dev/null
|
||
+++ b/tests/auth.test.ts
|
||
@@ -0,0 +1,45 @@
|
||
+import { describe, it, expect, beforeEach } from 'vitest';
|
||
+import type Database from 'better-sqlite3';
|
||
+import { createDb } from '../src/lib/db';
|
||
+import { hashPassword, verifyPassword, createUser, login, getSessionUser, logout } from '../src/lib/auth';
|
||
+
|
||
+let db: Database.Database;
|
||
+beforeEach(() => { db = createDb(':memory:'); });
|
||
+
|
||
+describe('auth', () => {
|
||
+ it('hash e verifica password', () => {
|
||
+ const h = hashPassword('segreta123');
|
||
+ expect(h).not.toBe('segreta123');
|
||
+ expect(verifyPassword('segreta123', h)).toBe(true);
|
||
+ expect(verifyPassword('sbagliata', h)).toBe(false);
|
||
+ });
|
||
+
|
||
+ it('login corretto crea sessione recuperabile', () => {
|
||
+ createUser(db, 'adriano', 'segreta123');
|
||
+ const token = login(db, 'adriano', 'segreta123');
|
||
+ expect(token).toBeTruthy();
|
||
+ const user = getSessionUser(db, token!);
|
||
+ expect(user).toMatchObject({ username: 'adriano' });
|
||
+ });
|
||
+
|
||
+ it('login errato ritorna null', () => {
|
||
+ createUser(db, 'adriano', 'segreta123');
|
||
+ expect(login(db, 'adriano', 'sbagliata')).toBeNull();
|
||
+ expect(login(db, 'inesistente', 'x')).toBeNull();
|
||
+ });
|
||
+
|
||
+ it('sessione scaduta viene rifiutata ed eliminata', () => {
|
||
+ createUser(db, 'adriano', 'segreta123');
|
||
+ const token = login(db, 'adriano', 'segreta123')!;
|
||
+ db.prepare("UPDATE sessions SET expires_at = datetime('now', '-1 day')").run();
|
||
+ expect(getSessionUser(db, token)).toBeNull();
|
||
+ expect(db.prepare('SELECT COUNT(*) AS n FROM sessions').get()).toMatchObject({ n: 0 });
|
||
+ });
|
||
+
|
||
+ it('logout elimina la sessione', () => {
|
||
+ createUser(db, 'adriano', 'segreta123');
|
||
+ const token = login(db, 'adriano', 'segreta123')!;
|
||
+ logout(db, token);
|
||
+ expect(getSessionUser(db, token)).toBeNull();
|
||
+ });
|
||
+});
|
||
diff --git a/tests/contact.test.ts b/tests/contact.test.ts
|
||
new file mode 100644
|
||
index 0000000..9fdbf67
|
||
--- /dev/null
|
||
+++ b/tests/contact.test.ts
|
||
@@ -0,0 +1,33 @@
|
||
+import { describe, it, expect } from 'vitest';
|
||
+import { validateContact } from '../src/lib/contact';
|
||
+
|
||
+describe('validateContact', () => {
|
||
+ const good = { firstName: 'Mario', email: 'mario@example.com', message: 'Ciao, vorrei informazioni.' };
|
||
+
|
||
+ it('accetta payload valido e normalizza i campi opzionali', () => {
|
||
+ const r = validateContact(good);
|
||
+ expect(r.ok).toBe(true);
|
||
+ if (r.ok) {
|
||
+ expect(r.value.lastName).toBe('');
|
||
+ expect(r.value.email).toBe('mario@example.com');
|
||
+ }
|
||
+ });
|
||
+
|
||
+ it('rifiuta email non valida', () => {
|
||
+ expect(validateContact({ ...good, email: 'non-email' }).ok).toBe(false);
|
||
+ });
|
||
+
|
||
+ it('rifiuta campi obbligatori mancanti o vuoti', () => {
|
||
+ expect(validateContact({ ...good, firstName: ' ' }).ok).toBe(false);
|
||
+ expect(validateContact({ ...good, message: '' }).ok).toBe(false);
|
||
+ expect(validateContact(null).ok).toBe(false);
|
||
+ });
|
||
+
|
||
+ it('rifiuta honeypot compilato', () => {
|
||
+ expect(validateContact({ ...good, website: 'spam.com' }).ok).toBe(false);
|
||
+ });
|
||
+
|
||
+ it('rifiuta messaggi oltre 5000 caratteri', () => {
|
||
+ expect(validateContact({ ...good, message: 'x'.repeat(5001) }).ok).toBe(false);
|
||
+ });
|
||
+});
|
||
diff --git a/tests/posts.test.ts b/tests/posts.test.ts
|
||
new file mode 100644
|
||
index 0000000..ad373fa
|
||
--- /dev/null
|
||
+++ b/tests/posts.test.ts
|
||
@@ -0,0 +1,74 @@
|
||
+import { describe, it, expect, beforeEach } from 'vitest';
|
||
+import type Database from 'better-sqlite3';
|
||
+import { createDb } from '../src/lib/db';
|
||
+import {
|
||
+ createPost, updatePost, deletePost, getPostById, getPublishedBySlug,
|
||
+ listPublished, listAllPosts, listRecent, listArchiveMonths, type PostInput,
|
||
+} from '../src/lib/posts';
|
||
+
|
||
+let db: Database.Database;
|
||
+beforeEach(() => { db = createDb(':memory:'); });
|
||
+
|
||
+const base: PostInput = {
|
||
+ title: 'Titolo', slug: 'titolo', category: 'Contenuti educativi',
|
||
+ excerpt: 'estratto', body_html: '<p>corpo</p>', cover: null, draft: false,
|
||
+};
|
||
+
|
||
+describe('posts repository', () => {
|
||
+ it('crea e rilegge un post pubblicato', () => {
|
||
+ const id = createPost(db, base);
|
||
+ const post = getPostById(db, id)!;
|
||
+ expect(post.title).toBe('Titolo');
|
||
+ expect(post.draft).toBe(0);
|
||
+ expect(post.published_at).not.toBeNull();
|
||
+ });
|
||
+
|
||
+ it('bozza non ha published_at e non appare nelle liste pubbliche', () => {
|
||
+ const id = createPost(db, { ...base, slug: 'bozza', draft: true });
|
||
+ expect(getPostById(db, id)!.published_at).toBeNull();
|
||
+ expect(getPublishedBySlug(db, 'bozza')).toBeUndefined();
|
||
+ expect(listPublished(db).total).toBe(0);
|
||
+ expect(listAllPosts(db)).toHaveLength(1);
|
||
+ });
|
||
+
|
||
+ it('slug duplicato lancia', () => {
|
||
+ createPost(db, base);
|
||
+ expect(() => createPost(db, base)).toThrow();
|
||
+ });
|
||
+
|
||
+ it('update imposta published_at alla prima pubblicazione e lo conserva', () => {
|
||
+ const id = createPost(db, { ...base, draft: true });
|
||
+ updatePost(db, id, { ...base, draft: false });
|
||
+ const first = getPostById(db, id)!.published_at;
|
||
+ expect(first).not.toBeNull();
|
||
+ updatePost(db, id, { ...base, title: 'Nuovo', draft: false });
|
||
+ expect(getPostById(db, id)!.published_at).toBe(first);
|
||
+ expect(getPostById(db, id)!.title).toBe('Nuovo');
|
||
+ });
|
||
+
|
||
+ it('listPublished filtra per categoria e pagina', () => {
|
||
+ for (let i = 0; i < 12; i++) {
|
||
+ createPost(db, { ...base, slug: `post-${i}`, category: i % 2 ? 'Contenuti educativi' : 'Eventi & Presidi' });
|
||
+ }
|
||
+ const all = listPublished(db, { page: 1, perPage: 9 });
|
||
+ expect(all.total).toBe(12);
|
||
+ expect(all.items).toHaveLength(9);
|
||
+ const edu = listPublished(db, { category: 'Contenuti educativi' });
|
||
+ expect(edu.total).toBe(6);
|
||
+ });
|
||
+
|
||
+ it('delete rimuove', () => {
|
||
+ const id = createPost(db, base);
|
||
+ deletePost(db, id);
|
||
+ expect(getPostById(db, id)).toBeUndefined();
|
||
+ });
|
||
+
|
||
+ it('listRecent e archivio mesi', () => {
|
||
+ createPost(db, base);
|
||
+ expect(listRecent(db, 4)).toHaveLength(1);
|
||
+ const months = listArchiveMonths(db);
|
||
+ expect(months).toHaveLength(1);
|
||
+ expect(months[0].count).toBe(1);
|
||
+ expect(months[0].month).toMatch(/^\d{4}-\d{2}$/);
|
||
+ });
|
||
+});
|
||
diff --git a/tests/rate-limit.test.ts b/tests/rate-limit.test.ts
|
||
new file mode 100644
|
||
index 0000000..930c2dc
|
||
--- /dev/null
|
||
+++ b/tests/rate-limit.test.ts
|
||
@@ -0,0 +1,23 @@
|
||
+import { describe, it, expect, vi, afterEach } from 'vitest';
|
||
+import { rateLimit, _resetBuckets } from '../src/lib/rate-limit';
|
||
+
|
||
+afterEach(() => { _resetBuckets(); vi.useRealTimers(); });
|
||
+
|
||
+describe('rateLimit', () => {
|
||
+ it('consente fino a max richieste', () => {
|
||
+ expect(rateLimit('a', 2, 1000)).toBe(true);
|
||
+ expect(rateLimit('a', 2, 1000)).toBe(true);
|
||
+ expect(rateLimit('a', 2, 1000)).toBe(false);
|
||
+ });
|
||
+ it('chiavi indipendenti', () => {
|
||
+ expect(rateLimit('a', 1, 1000)).toBe(true);
|
||
+ expect(rateLimit('b', 1, 1000)).toBe(true);
|
||
+ });
|
||
+ it('la finestra scade', () => {
|
||
+ vi.useFakeTimers();
|
||
+ expect(rateLimit('a', 1, 1000)).toBe(true);
|
||
+ expect(rateLimit('a', 1, 1000)).toBe(false);
|
||
+ vi.advanceTimersByTime(1100);
|
||
+ expect(rateLimit('a', 1, 1000)).toBe(true);
|
||
+ });
|
||
+});
|
||
diff --git a/tests/sanitize.test.ts b/tests/sanitize.test.ts
|
||
new file mode 100644
|
||
index 0000000..0830958
|
||
--- /dev/null
|
||
+++ b/tests/sanitize.test.ts
|
||
@@ -0,0 +1,16 @@
|
||
+import { describe, it, expect } from 'vitest';
|
||
+import { sanitizeHtml } from '../src/lib/sanitize';
|
||
+
|
||
+describe('sanitizeHtml', () => {
|
||
+ it('mantiene la formattazione consentita', () => {
|
||
+ const html = '<h2>Titolo</h2><p>Testo <strong>forte</strong> ed <em>enfasi</em></p><ul><li>voce</li></ul><blockquote>citazione</blockquote>';
|
||
+ expect(sanitizeHtml(html)).toBe(html);
|
||
+ });
|
||
+ it('rimuove script e handler', () => {
|
||
+ expect(sanitizeHtml('<p onclick="x()">ciao</p><script>alert(1)</script>')).toBe('<p>ciao</p>');
|
||
+ });
|
||
+ it('consente immagini locali e https, blocca javascript:', () => {
|
||
+ expect(sanitizeHtml('<img src="/uploads/a.jpg">')).toContain('/uploads/a.jpg');
|
||
+ expect(sanitizeHtml('<a href="javascript:alert(1)">x</a>')).toBe('<a>x</a>');
|
||
+ });
|
||
+});
|
||
diff --git a/tests/slug.test.ts b/tests/slug.test.ts
|
||
new file mode 100644
|
||
index 0000000..5ca5bed
|
||
--- /dev/null
|
||
+++ b/tests/slug.test.ts
|
||
@@ -0,0 +1,20 @@
|
||
+import { describe, it, expect } from 'vitest';
|
||
+import { slugify } from '../src/lib/slug';
|
||
+
|
||
+describe('slugify', () => {
|
||
+ it('minuscole e trattini', () => {
|
||
+ expect(slugify('Il Metodo InsanityLab')).toBe('il-metodo-insanitylab');
|
||
+ });
|
||
+ it('rimuove accenti', () => {
|
||
+ expect(slugify('Attività fisica è già qui')).toBe('attivita-fisica-e-gia-qui');
|
||
+ });
|
||
+ it('comprime simboli e spazi multipli', () => {
|
||
+ expect(slugify('Blog & Edugo -- news!!')).toBe('blog-edugo-news');
|
||
+ });
|
||
+ it('niente trattini ai bordi', () => {
|
||
+ expect(slugify(' ciao ')).toBe('ciao');
|
||
+ });
|
||
+ it('stringa vuota resta vuota', () => {
|
||
+ expect(slugify('')).toBe('');
|
||
+ });
|
||
+});
|
||
diff --git a/tsconfig.json b/tsconfig.json
|
||
new file mode 100644
|
||
index 0000000..fe31721
|
||
--- /dev/null
|
||
+++ b/tsconfig.json
|
||
@@ -0,0 +1,5 @@
|
||
+{
|
||
+ "extends": "astro/tsconfigs/strict",
|
||
+ "include": [".astro/types.d.ts", "src/**/*", "tests/**/*"],
|
||
+ "exclude": ["dist"]
|
||
+}
|
||
diff --git a/uploads/.gitkeep b/uploads/.gitkeep
|
||
new file mode 100644
|
||
index 0000000..e69de29
|
||
diff --git a/vitest.config.ts b/vitest.config.ts
|
||
new file mode 100644
|
||
index 0000000..aefccb5
|
||
--- /dev/null
|
||
+++ b/vitest.config.ts
|
||
@@ -0,0 +1,5 @@
|
||
+import { defineConfig } from 'vitest/config';
|
||
+
|
||
+export default defineConfig({
|
||
+ test: { include: ['tests/**/*.test.ts'] },
|
||
+});
|