# 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
+///
+
+declare namespace App {
+ interface Locals {
+ user?: { id: number; username: string };
+ }
+}
+```
+
+`src/pages/index.astro` (placeholder, sostituito nel Task 9):
+```astro
+---
+export const prerender = true;
+---
+
InsanityLab
+InsanityLab — in costruzione
+```
+
+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 -- `), 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": {
+ ".jpg": "hero-1.jpg",
+ ".jpg": "info-performance.jpg",
+ ".jpg": "info-balance.jpg",
+ ".jpg": "info-longevity.jpg",
+ ".jpg": "feature-performance.jpg",
+ ".jpg": "feature-balance.jpg",
+ ".jpg": "feature-longevity.jpg",
+ ".jpg": "quote-bg.jpg",
+ ".jpg": "training-one-to-one.jpg",
+ ".jpg": "training-small-groups.jpg",
+ ".jpg": "training-performance-class.jpg",
+ ".jpg": "training-fit-remote.jpg",
+ ".jpg": "team-donata.jpg",
+ ".jpg": "team-nicola.jpg",
+ ".jpg": "team-eleonora.jpg",
+ ".jpg": "team-antonello.jpg",
+ ".jpg": "method-step-1.jpg",
+ ".jpg": "method-step-2.jpg",
+ ".jpg": "method-step-3.jpg",
+ ".jpg": "method-step-4.jpg",
+ ".png": "partner-edenred.png",
+ ".png": "partner-welbee.png",
+ ".png": "partner-aon.png",
+ ".png": "partner-howden.png"
+ },
+ "public/img": {
+ ".png": "logo-dark.png",
+ ".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();
+
+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: 'corpo
', 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 -- ');
+ 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: ` ` layout con slot; ` ` 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;
+---
+
+
+
+
+
+ {title} — InsanityLab
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+- [ ] **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;
+---
+
+
+
+
+
+```
+
+- [ ] **Step 3: ContactForm.astro**
+
+```astro
+---
+interface Props { variant?: 'full' | 'footer' }
+const { variant = 'full' } = Astro.props;
+---
+
+
+
+```
+
+- [ ] **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/`), colonna CONTACT con ` `. 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();
+---
+
+
+
+
+
PERFORMANCE. BALANCE. LONGEVITY.
+
Promuoviamo il movimento come cura di sé. Abitudini sostenibili per benessere fisico e mentale.
+
{site.phone} {site.address}, {site.cityLine}
+ {site.hours.map((h) =>
{h}
)}
+
+
+ Training
+ {site.footerTraining.map((l) => {l.label} )}
+
+
+ Programmi
+ {site.footerPrograms.map((l) => {l.label} )}
+
+
+
Contact
+
+
+
+
+
+
© {year} Insanitylab , All Rights Reserved
+
Seguici {site.socials.map((s) => {s.label} )}
+
+
+
+
+
+```
+
+- [ ] **Step 5: 404.astro**
+
+```astro
+---
+import Base from '../layouts/Base.astro';
+export const prerender = true;
+---
+
+
+
+
Errore 404
+
Pagina non trovata
+
La pagina che cerchi non esiste o è stata spostata.
+
Torna alla home
+
+
+
+```
+
+- [ ] **Step 6: aggiorna index placeholder per usare Base**
+
+```astro
+---
+import Base from '../layouts/Base.astro';
+export const prerender = true;
+---
+
+
+
+```
+
+- [ ] **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: ` ` riusato dal Task 11; ` ` 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' },
+];
+---
+
+ {slides.map((s, i) => (
+
+
+
+
{s.title.map((line) => {line} )}
+
Scopri
+
+
+
+
+ ))}
+ {slides.length > 1 && (
+ <>
+ ‹
+ ›
+ >
+ )}
+
+
+
+
+
+```
+
+- [ ] **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 },
+];
+---
+
+
+
+```
+
+- [ ] **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.' },
+];
+---
+
+
+ {blocks.map((b) => (
+
+
+
+
{b.title}
+
{b.text}
+
Scopri
+
+
+ ))}
+
+
+
+
+```
+
+- [ ] **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';
+---
+
+
+
“
+
+ PERFORMANCE È SPINGERE OLTRE I PROPRI LIMITI, BALANCE È SAPERLI ASCOLTARE, LONGEVITY È COSTRUIRE NEL TEMPO CIÒ CHE DAVVERO CONTA.
+ Insanitylab
+
+
+
+
+
+```
+
+- [ ] **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'];
+---
+
+ {trainings.map((t, i) => (
+ <>
+
+ {imgOf(t.image) && }
+ >
+ ))}
+
+
+
+```
+(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 = {
+ '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',
+};
+---
+
+
+
+
Programmi
+
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.
+
+
+
+
+
+
+```
+(Se nell'archivio Figma sono presenti le icone originali dei programmi, usale al posto degli SVG inline: mappale nel Task 2 come `icon-.png`.)
+
+- [ ] **Step 7: Agenda.astro (condiviso)**
+
+Sezione scura con tab discipline e griglia settimanale:
+
+```astro
+---
+import { agenda, disciplines, days, slots } from '../data/agenda';
+---
+
+
+
Agenda
+
+ {disciplines.map((d, i) => (
+ {d}
+ ))}
+
+
+
+ {days.map((d) =>
{d}
)}
+ {slots.map((slot) => (
+ <>
+
{slot}
+ {days.map((_, dayIdx) => {
+ const entry = agenda.find((e) => e.day === dayIdx && e.slot === slot);
+ return entry
+ ?
{entry.discipline} {entry.time} {entry.trainer}
+ :
;
+ })}
+ >
+ ))}
+
+
+
+
+
+
+
+```
+
+- [ ] **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;
+---
+
+
+
+
+ {members.map((m) => (
+
+
+ {m.shortName}
+
+ ))}
+
+
+
+
+
+```
+
+`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;
+---
+
+
+
+
Il metodo InsanityLab
+
Metodo Insanitylab')} />
+
+
{methodExtraNote}
+
+ {methodSteps.map((s) => (
+
+
+ {imgOf(s.image) && }
+ {s.n}
+
+
{s.title}
+
{s.text}
+
+ ))}
+
+
+
+
+
+```
+
+`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;
+---
+
+
+ {partners.map((p) => imgOf(p.image) && )}
+
+
+
+
+```
+
+- [ ] **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;
+---
+
+
+
+
+
+
+
+
Training
+
Porta i tuoi allenamenti al livello successivo: trova il percorso più adatto alle tue esigenze.
+
+
+
+
+
+
+
+
+
+```
+
+- [ ] **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: ` ` 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;
+---
+
+
+ {eyebrow &&
{eyebrow}
}
+
{title}
+
+
+
+
+```
+
+- [ ] **Step 2: Timeline.astro (Storylab)**
+
+Blocco 2021-2024 aperto con testo; 2025, 2026 e OGGI come accordion ``:
+
+```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.' },
+];
+---
+
+
+
+
Storylab
+
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.
+
+
+
INSANITYLAB 2021-2024
+
{openText}
+ {stages.map((s) => (
+
+ + {s.label}
+ {s.text}
+
+ ))}
+
+
+
+
+
+```
+
+- [ ] **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;
+---
+
+
+
+
+
+
Mission
+
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.
+
+
+
+
+
+
+
Direzione
+
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.
+
Scopri
+
+
+
Approccio
+
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.
+
Contattaci
+
+
+
+
+
+
+
About
+
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.
+
+
+
+
+
+
+
+
+
Convenzioni
+
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.
+
+
+ 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.
+ Ogni collaborazione parte da una consulenza gratuita preliminare per analizzare esigenze e obiettivi, così da sviluppare soluzioni efficaci e sostenibili.
+ Le convenzioni vengono definite su misura insieme a InsanityLab, in base alle caratteristiche e alle necessità dell’azienda partner.
+
+
Per ricevere maggiori informazioni o attivare una convenzione: {site.email} · {site.phone}
+
+
+
+
+
+
+
Team
+
Il team di InsanityLab riunisce professionisti specializzati in allenamento, salute e benessere, con un approccio multidisciplinare basato su competenza, personalizzazione e attenzione alla persona.
+
+
+ {founders.map((m) => (
+
+ {m.image && imgOf(m.image) && }
+ {m.shortName}
+ {m.role}
+ {m.bio}
+
+ ))}
+
+
Accanto ai fondatori opera un team in continua crescita composto da trainer e professionisti con specializzazioni complementari:
+
+ {staff.map((m) => (
+
+ {m.image && imgOf(m.image) && }
+ {m.shortName}
+ {m.role}
+ {m.bio}
+
+ ))}
+
+
+
+
+
+
+
Metodo
+
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.
+
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.
+
+
+
+
+
+
In-Sanity Studios
+
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.
+
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.
+
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.
+
+
+
+
+
+
Vision
+
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.
+
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à.
+
+
+
+
+
+```
+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), ` `, ` ` (Task 9), ` ` (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;
+---
+
+
+
+
+
+
Training
+
Porta i tuoi allenamenti al livello successivo: programmi completi e percorsi su misura. Trova il training più adatto alle tue esigenze.
+
+
+
+
+
+
+ {trainings.map((t, i) => (
+
+ {imgOf(t.image) &&
}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
Prenota la lezione di prova
+
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.
+
Prenota
+
+
+
+
+
+
+```
+
+- [ ] **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), ` `.
+- 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;
+---
+
+
+
+
+
+
Programmi
+
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.
+
+
+ {programs.map((p) => (
+
+ {p.title}
+ {p.subtitle}
+ {p.excerpt}
+ Per saperne di più →
+
+ ))}
+
+
+
+
+
+
+```
+
+- [ ] **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'];
+---
+
+
+
+
+
+
+
{program.title.toUpperCase()}
+
{program.longDescription}
+
+ {program.features.map((f) => {f} )}
+
+
+ {program.faq.map((item, i) => (
+
+ {item.q}
+ {item.a}
+
+ ))}
+
+
+
+ Categorie
+ {blogCategories.map((c) => {c}
)}
+ Altri programmi
+ {others.map((o) => {o.title}
)}
+
+
+
+
+ {program.hasPricing && (
+
+
+
+
{performanceClassPricing.heading}
+
{performanceClassPricing.intro}
+
+ {performanceClassPricing.groups.map((g) => (
+
+ {g.open ? '+' : '–'} {g.label}
+ {g.plans.length === 0 ? (
+ Contattaci per i dettagli di questo programma. Scrivici →
+ ) : (
+
+ {g.plans.map((p) => (
+
+
{p.monthly}€
+
al mese, semestrale
+
{p.entries} {p.entries === 1 ? 'ingresso settimanale' : 'ingressi settimanali'}
+
{p.note}
+
{p.installments}
+
{p.best}
+
Acquista
+
+ ))}
+
+ )}
+
+ ))}
+
+
+ )}
+
+
+
+```
+
+- [ ] **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), ` ` (Task 7), ` ` (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` (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;
+ 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 {
+ 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}`);
+---
+
+
+
+
+
+
Carica la mappa (Google Maps)
+
La mappa viene caricata da Google solo dopo il tuo consenso.
+
+
+
+
+
+
Indirizzo
+
{site.address} {site.cityLine}
+
+
+
Telefono
+
{site.phone}
+
+
+
+
+
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.
+
+
+
+
+
+
+
+```
+
+- [ ] **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), ` `.
+- 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}` : ''}`;
+};
+---
+
+
+
+
+
+
Blog & Edugo
+
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.
+
+
+ Mostra tutto
+ {CATEGORIES.map((c) => (
+ {c}
+ ))}
+
+ {items.length === 0 ? (
+
Nessun articolo pubblicato{category ? ' in questa categoria' : ''}, torna a trovarci presto.
+ ) : (
+
+ {items.map((post) => (
+
+ {post.cover && }
+ {fmt(post.published_at)}
+
+ {post.excerpt}
+ Per saperne di più →
+
+ ))}
+
+ )}
+ {pages > 1 && (
+
+ {Array.from({ length: pages }, (_, i) => (
+ {i + 1}
+ ))}
+
+ )}
+
+
+
+
+
+```
+
+- [ ] **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`));
+---
+
+
+
+```
+
+- [ ] **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' });
+---
+
+
+
+
+ {post.cover && }
+ {post.published_at && fmt.format(new Date(post.published_at))} · {post.category}
+ {post.title}
+
+
+
+
+
+
+
+
+```
+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.', 'Corpo di prova .
', 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/' }`.
+ - `GET /uploads/` 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 = 'Titolo Testo forte ed enfasi
citazione ';
+ expect(sanitizeHtml(html)).toBe(html);
+ });
+ it('rimuove script e handler', () => {
+ expect(sanitizeHtml('ciao
')).toBe('ciao
');
+ });
+ it('consente immagini locali e https, blocca javascript:', () => {
+ expect(sanitizeHtml(' ')).toContain('/uploads/a.jpg');
+ expect(sanitizeHtml('x ')).toBe('x ');
+ });
+});
+```
+
+- [ ] **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;
+---
+
+
+
+
+
+ {title} — Admin InsanityLab
+
+
+
+
+
+
+
+
+```
+
+`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.';
+ }
+}
+---
+
+ Accesso
+ {error && {error}
}
+
+
+
+ Entra
+
+
+```
+
+`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' });
+---
+
+ Articoli
+ + Nuovo articolo
+
+ Titolo Categoria Stato Aggiornato
+
+ {posts.map((p) => (
+
+ {p.title}
+ {p.category}
+ {p.draft ? 'Bozza' : 'Pubblicato'}
+ {fmt.format(new Date(p.updated_at.replace(' ', 'T') + 'Z'))}
+ Elimina
+
+ ))}
+
+
+ {posts.length === 0 && Nessun articolo: creane uno nuovo.
}
+
+
+
+```
+
+- [ ] **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;
+---
+
+
+
+
+ {CATEGORIES.map((c) => {c} )}
+
+ {post?.excerpt ?? ''}
+
+ Copertina:
+
+ {post?.cover && }
+
+
+ H2
+ H3
+ B
+ I
+ • Elenco
+ 1. Elenco
+ “ Citazione
+ Link
+ Immagine
+
+
+
+ Bozza (non visibile sul sito)
+ Salva
+
+
+
+
+
+```
+
+`src/pages/admin/new.astro`:
+```astro
+---
+import Admin from '../../layouts/Admin.astro';
+import PostForm from '../../components/admin/PostForm.astro';
+export const prerender = false;
+---
+
+ Nuovo articolo
+
+
+
+```
+
+`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');
+---
+
+ Modifica articolo
+
+
+
+```
+
+- [ ] **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) {
+ 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;
+ 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;
+ 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 = { '.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 = { '.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/`; 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
+
+
+
+
+```
+
+- [ ] **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 && 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 @@
+
+
+
+
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 -- ');
+ 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';
+---
+
+
+
Agenda
+
+ {disciplines.map((d, i) => (
+ {d}
+ ))}
+
+
+
+ {days.map((d) =>
{d}
)}
+ {slots.map((slot) => (
+ <>
+
{slot}
+ {days.map((_, dayIdx) => {
+ const entry = agenda.find((e) => e.day === dayIdx && e.slot === slot);
+ return entry
+ ?
{entry.discipline} {entry.time} {entry.trainer}
+ :
;
+ })}
+ >
+ ))}
+
+
+
+
+
+
+
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;
+---
+
+
+ {variant === 'full' && }
+ {variant === 'full' && }
+
+
+
+ Invia messaggio
+
+
+
+
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();
+---
+
+
+
+
+
PERFORMANCE. BALANCE. LONGEVITY.
+
Promuoviamo il movimento come cura di sé. Abitudini sostenibili per benessere fisico e mentale.
+
{site.phone} {site.address}, {site.cityLine}
+ {site.hours.map((h) =>
{h}
)}
+
+
+ Training
+ {site.footerTraining.map((l) => {l.label} )}
+
+
+ Programmi
+ {site.footerPrograms.map((l) => {l.label} )}
+
+
+
Contact
+
+
+
+
+
+
© {year} Insanitylab , All Rights Reserved
+
Seguici {site.socials.map((s) => {s.label} )}
+
+
+
+
+
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;
+---
+
+
+
+
+
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;
+---
+
+
+ {eyebrow &&
{eyebrow}
}
+
{title}
+
+
+
+
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.' },
+];
+---
+
+
+
+
Storylab
+
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.
+
+
+
INSANITYLAB 2021-2024
+
{openText}
+ {stages.map((s) => (
+
+ + {s.label}
+ {s.text}
+
+ ))}
+
+
+
+
+
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;
+---
+
+
+
+
+ {CATEGORIES.map((c) => {c} )}
+
+ {post?.excerpt ?? ''}
+
+ Copertina:
+
+ {post?.cover && }
+
+
+ H2
+ H3
+ B
+ I
+ • Elenco
+ 1. Elenco
+ " Citazione
+ Link
+ Immagine
+
+
+
+ Bozza (non visibile sul sito)
+ Salva
+
+
+
+
+
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`));
+---
+
+
+
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.' },
+];
+---
+
+
+ {blocks.map((b) => (
+
+
+
+
{b.title}
+
{b.text}
+
Scopri
+
+
+ ))}
+
+
+
+
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' },
+];
+---
+
+ {slides.map((s, i) => (
+
+
+
+
{s.title.map((line) => {line} )}
+
Scopri
+
+
+
+
+ ))}
+ {slides.length > 1 && (
+ <>
+ ‹
+ ›
+ >
+ )}
+
+
+
+
+
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 },
+];
+---
+
+
+
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;
+---
+
+
+
+
Il metodo InsanityLab
+
Metodo Insanitylab')} />
+
+
{methodExtraNote}
+
+ {methodSteps.map((s) => (
+
+
+ {imgOf(s.image) && }
+ {s.n}
+
+
{s.title}
+
{s.text}
+
+ ))}
+
+
+
+
+
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;
+---
+
+
+ {partners.map((p) => imgOf(p.image) && )}
+
+
+
+
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 = {
+ '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',
+};
+---
+
+
+
+
Programmi
+
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.
+
+
+
+
+
+
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';
+---
+
+
+
“
+
+ PERFORMANCE È SPINGERE OLTRE I PROPRI LIMITI, BALANCE È SAPERLI ASCOLTARE, LONGEVITY È COSTRUIRE NEL TEMPO CIÒ CHE DAVVERO CONTA.
+ Insanitylab
+
+
+
+
+
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;
+---
+
+
+
+
+ {members.map((m) => (
+
+
+ {m.shortName}
+
+ ))}
+
+
+
+
+
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;
+---
+
+ {trainings.map((t, i) => {
+ const img = imgOf(t.image);
+ const card = (
+
+ );
+ const photo = img ? : null;
+ return isSwapRow(i) ? <>{photo}{card}> : <>{card}{photo}>;
+ })}
+
+
+
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 @@
+///
+
+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;
+---
+
+
+
+
+
+ {title} — Admin InsanityLab
+
+
+
+
+
+
+
+
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;
+---
+
+
+
+
+
+ {title} — InsanityLab
+
+
+
+
+
+
+
+
+
+
+
+
+
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;
+ 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 {
+ 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) {
+ 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();
+
+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;
+---
+
+
+
+
Errore 404
+
Pagina non trovata
+
La pagina che cerchi non esiste o è stata spostata.
+
Torna alla home
+
+
+
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;
+---
+
+
+
+
+
+
+
+
Mission
+
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.
+
+ {imgOf('about-training-group') && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
Direzione
+
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.
+
Scopri
+
+
+
Approccio
+
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.
+
Contattaci
+
+
+
+
+
+
+
+
+
About
+
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.
+
+ {imgOf('about-team-group') && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
Convenzioni
+
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.
+
+
+ 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.
+ Ogni collaborazione parte da una consulenza gratuita preliminare per analizzare esigenze e obiettivi, così da sviluppare soluzioni efficaci e sostenibili.
+ Le convenzioni vengono definite su misura insieme a InsanityLab, in base alle caratteristiche e alle necessità dell'azienda partner.
+
+
Per ricevere maggiori informazioni o attivare una convenzione: {site.email} · {site.phone}
+
+
+
+
+
+
+
Team
+
Il team di InsanityLab riunisce professionisti specializzati in allenamento, salute e benessere, con un approccio multidisciplinare basato su competenza, personalizzazione e attenzione alla persona.
+
+
+ {founders.map((m) => (
+
+ {m.image && imgOf(m.image) && }
+ {m.shortName}
+ {m.role}
+ {m.bio}
+
+ ))}
+
+
Accanto ai fondatori opera un team in continua crescita composto da trainer e professionisti con specializzazioni complementari:
+
+ {staff.map((m) => (
+
+ {m.image && imgOf(m.image) && }
+ {m.shortName}
+ {m.role}
+ {m.bio}
+
+ ))}
+
+
+
+
+
+
+
Metodo
+
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.
+
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.
+
+
+
+
+
+
+
+
In-Sanity Studios
+
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.
+
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.
+
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.
+
+ {imgOf('about-exterior') && (
+
+
+
+ )}
+
+
+
+
+
+
+
Vision
+
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.
+
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à.
+
+
+
+
+
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');
+---
+
+ Modifica articolo
+
+
+
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' });
+---
+
+ Articoli
+ + Nuovo articolo
+
+ Titolo Categoria Stato Aggiornato
+
+ {posts.map((p) => (
+
+ {p.title}
+ {p.category}
+ {p.draft ? 'Bozza' : 'Pubblicato'}
+ {fmt.format(new Date(p.updated_at.replace(' ', 'T') + 'Z'))}
+ Elimina
+
+ ))}
+
+
+ {posts.length === 0 && Nessun articolo: creane uno nuovo.
}
+
+
+
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.';
+ }
+}
+---
+
+ Accesso
+ {error && {error}
}
+
+
+
+ Entra
+
+
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;
+---
+
+ Nuovo articolo
+
+
+
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;
+ 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;
+ 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 = { '.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' });
+---
+
+
+
+
+ {post.cover && }
+ {post.published_at && fmt.format(new Date(post.published_at))} · {post.category}
+ {post.title}
+
+
+
+
+
+
+
+
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}` : ''}`;
+};
+---
+
+
+
+
+
+
Blog & Edugo
+
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.
+
+
+ Mostra tutto
+ {CATEGORIES.map((c) => (
+ {c}
+ ))}
+
+ {items.length === 0 ? (
+
Nessun articolo pubblicato{category ? ' in questa categoria' : ''}, torna a trovarci presto.
+ ) : (
+
+ {items.map((post) => (
+
+ {post.cover && }
+ {fmt(post.published_at)}
+
+ {post.excerpt}
+ Per saperne di più →
+
+ ))}
+
+ )}
+ {pages > 1 && (
+
+ {Array.from({ length: pages }, (_, i) => (
+ {i + 1}
+ ))}
+
+ )}
+
+
+
+
+
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}`);
+---
+
+
+
+
+
+
Carica la mappa (Google Maps)
+
La mappa viene caricata da Google solo dopo il tuo consenso.
+
+
+
+
+
+
Indirizzo
+
{site.address} {site.cityLine}
+
+
+
Telefono
+
{site.phone}
+
+
+
+
+
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.
+
+
+
+
+
+
+
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;
+---
+
+
+
+
+
+
+
+
Training
+
Porta i tuoi allenamenti al livello successivo: trova il percorso più adatto alle tue esigenze.
+
+
+
+
+
+
+
+
+
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'];
+---
+
+
+
+
+
+
+
{program.title.toUpperCase()}
+
{program.longDescription}
+
+ {program.features.map((f) => {f} )}
+
+
+ {program.faq.map((item, i) => (
+
+ {item.q}
+ {item.a}
+
+ ))}
+
+
+
+ Categorie
+ {blogCategories.map((c) => {c}
)}
+ Altri programmi
+ {others.map((o) => {o.title}
)}
+
+
+
+
+ {program.hasPricing && (
+
+
+
+
{performanceClassPricing.heading}
+
{performanceClassPricing.intro}
+
+ {performanceClassPricing.groups.map((g) => (
+
+ {g.open ? '+' : '–'} {g.label}
+ {g.plans.length === 0 ? (
+ Contattaci per i dettagli di questo programma. Scrivici →
+ ) : (
+
+ {g.plans.map((p) => (
+
+
{p.monthly}€
+
al mese, semestrale
+
{p.entries} {p.entries === 1 ? 'ingresso settimanale' : 'ingressi settimanali'}
+
{p.note}
+
{p.installments}
+
{p.best}
+
Acquista
+
+ ))}
+
+ )}
+
+ ))}
+
+
+ )}
+
+
+
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;
+---
+
+
+
+
+
+
Programmi
+
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.
+
+
+ {programs.map((p) => (
+
+ {p.title}
+ {p.subtitle}
+ {p.excerpt}
+ Per saperne di più →
+
+ ))}
+
+
+
+
+
+
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;
+---
+
+
+
+
+
+
Training
+
Porta i tuoi allenamenti al livello successivo: programmi completi e percorsi su misura. Trova il training più adatto alle tue esigenze.
+
+
+
+
+
+
+ {trainings.map((t, i) => (
+
+ {imgOf(t.image) &&
}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
Prenota la lezione di prova
+
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.
+
Prenota
+
+
+
+
+
+
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 = { '.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: 'corpo
', 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 = 'Titolo Testo forte ed enfasi
citazione ';
+ expect(sanitizeHtml(html)).toBe(html);
+ });
+ it('rimuove script e handler', () => {
+ expect(sanitizeHtml('ciao
')).toBe('ciao
');
+ });
+ it('consente immagini locali e https, blocca javascript:', () => {
+ expect(sanitizeHtml(' ')).toContain('/uploads/a.jpg');
+ expect(sanitizeHtml('x ')).toBe('x ');
+ });
+});
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'] },
+});