From 611af1ac672f9d30d1fc1d99a94338c50ced91e2 Mon Sep 17 00:00:00 2001 From: LOUIS POTEVIN Date: Thu, 2 Jul 2026 13:55:46 +0200 Subject: [PATCH] feat: add Home, Project, and Work Index views with responsive design and SEO enhancements - Implemented HomeView with a timeline layout, featuring sections for hero, profile, projects, stack, and contact. - Created ProjectView to display individual project details, including status, highlights, and tech stack. - Developed WorkIndexView to list all projects with breadcrumb navigation. - Enhanced TypeScript configuration for improved module resolution. --- .gitignore | 15 +- README.md | 140 +- astro.config.mjs | 41 +- package-lock.json | 1963 ++++++++++++----- package.json | 26 +- public/favicon.svg | 16 +- src/components/BrandLinkedin.astro | 22 +- src/components/Eyebrow.astro | 36 +- src/components/ProjectCard.astro | 170 +- src/components/SiteFooter.astro | 150 +- src/components/SiteNav.astro | 197 +- src/components/SpaceBackground.astro | 52 +- src/components/TechIcon.astro | 79 +- src/components/scene/scene.ts | 530 +++-- src/components/scene/voyage.ts | 517 +++++ src/components/scenes/BlueprintScene.astro | 238 ++ src/components/scenes/InfraScene.astro | 329 +++ src/components/scenes/SorterScene.astro | 287 +++ .../showcase/ComponentShowcase.astro | 871 ++++++++ src/data/projects.ts | 333 +-- src/data/site.ts | 59 +- src/env.d.ts | 4 + src/i18n/index.ts | 59 + src/i18n/ui.ts | 315 +++ src/layouts/BaseLayout.astro | 145 +- src/lib/scrub.ts | 34 + src/lib/seo.ts | 97 + src/lib/smooth.ts | 46 + src/pages/404.astro | 49 + src/pages/about.astro | 314 +-- src/pages/contact.astro | 296 +-- src/pages/docs.astro | 5 + src/pages/en/about.astro | 5 + src/pages/en/contact.astro | 5 + src/pages/en/docs.astro | 5 + src/pages/en/index.astro | 5 + src/pages/en/work/[slug].astro | 12 + src/pages/en/work/index.astro | 5 + src/pages/index.astro | 420 +--- src/pages/work/[slug].astro | 301 +-- src/pages/work/index.astro | 61 +- src/styles/_type.scss | 8 +- src/styles/global.scss | 75 +- src/views/AboutView.astro | 215 ++ src/views/ContactView.astro | 350 +++ src/views/DocsView.astro | 1277 +++++++++++ src/views/HomeView.astro | 547 +++++ src/views/ProjectView.astro | 203 ++ src/views/WorkIndexView.astro | 67 + tsconfig.json | 11 +- 50 files changed, 8134 insertions(+), 2873 deletions(-) create mode 100644 src/components/scene/voyage.ts create mode 100644 src/components/scenes/BlueprintScene.astro create mode 100644 src/components/scenes/InfraScene.astro create mode 100644 src/components/scenes/SorterScene.astro create mode 100644 src/components/showcase/ComponentShowcase.astro create mode 100644 src/env.d.ts create mode 100644 src/i18n/index.ts create mode 100644 src/i18n/ui.ts create mode 100644 src/lib/scrub.ts create mode 100644 src/lib/seo.ts create mode 100644 src/lib/smooth.ts create mode 100644 src/pages/404.astro create mode 100644 src/pages/docs.astro create mode 100644 src/pages/en/about.astro create mode 100644 src/pages/en/contact.astro create mode 100644 src/pages/en/docs.astro create mode 100644 src/pages/en/index.astro create mode 100644 src/pages/en/work/[slug].astro create mode 100644 src/pages/en/work/index.astro create mode 100644 src/views/AboutView.astro create mode 100644 src/views/ContactView.astro create mode 100644 src/views/DocsView.astro create mode 100644 src/views/HomeView.astro create mode 100644 src/views/ProjectView.astro create mode 100644 src/views/WorkIndexView.astro diff --git a/.gitignore b/.gitignore index 41cb8f7..de35ccf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,4 @@ -# build +node_modules/ dist/ .astro/ - -# deps -node_modules/ - -# env & logs -.env -.env.* -*.log -npm-debug.log* - -# os / editor .DS_Store -.vscode/ -.idea/ diff --git a/README.md b/README.md index 26967f0..271e220 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,96 @@ -# louis-potevin.dev +# louis-potevin.dev — Portfolio v2 -Professional portfolio of **Louis Potevin** - full-stack developer. -Static multi-page website built with **Astro + TypeScript + Sass**, styled -**exclusively** by the in-house design system **`@unkn0wndo3s/nova-design-system`** -(tokens, components), featuring a 3D space background controlled via scrolling. +Portfolio bilingue (FR/EN) construit avec **Astro 7**, **Nova Design System**, **Three.js** et **Lenis**. +Accueil = un « voyage » : le scroll ne fait jamais défiler une page, il pilote une caméra 3D +et l'animation continue. Chaque projet a sa propre scène sur-mesure. ---- +## Stack -## Getting Started +- [Astro 7](https://astro.build) — SSG, i18n natif, zéro JS par défaut +- [`@unkn0wndo3s/nova-design-system`](https://www.npmjs.com/package/@unkn0wndo3s/nova-design-system) — composants + tokens (source unique du design) +- [Three.js](https://threejs.org) — le moteur du voyage (accueil) +- [Lenis](https://lenis.darkroom.engineering) — smooth scroll ; toutes les animations lisent le scroll depuis Lenis, d'où la sensation d'une seule courbe de mouvement +- Sass — les mixins typographiques NDS sont exposés via un `loadPath` (voir `astro.config.mjs`) + +## Démarrer ```bash -npm install -npm run dev # dev server → http://localhost:4321 -npm run build # static build → ./dist -npm run preview # preview the build +npm install # .npmrc force legacy-peer-deps (requis par @lucide/astro avec Astro 7) +npm run dev # http://localhost:4321 +npm run build # sortie statique dans dist/ +npm run check # astro check (TS + diagnostics) - 0 erreur attendu ``` -> **Installation Note.** `@lucide/astro` has not yet declared its -compatibility with Astro 7 in its `peerDependencies`. The **`.npmrc`** -> file therefore enables `legacy-peer-deps=true` so that `npm install` succeeds. -> Functionality is not affected. - ---- +Node >= 22.12 (contrainte du NDS). ## Structure ``` src/ -├─ data/ -│ ├─ site.ts # profile + links (single source of truth) -│ └─ projects.ts # project content -├─ layouts/ -│ └─ BaseLayout.astro # NDS styles + 3D background + nav + footer + HUD -├─ components/ -│ ├─ scene/scene.ts # Three.js background (stars, nebula, ambient asteroid) -│ ├─ SpaceBackground.astro -│ ├─ ProjectCard.astro # project card (NDS Card component) -│ ├─ SiteNav / SiteFooter / Eyebrow / TechIcon / BrandLinkedin -└─ pages/ - ├─ index.astro # home - ├─ work/index.astro # all projects - ├─ work/[slug].astro # project details (getStaticPaths) - ├─ about.astro - └─ contact.astro +├── data/ site.ts (profil, liens), projects.ts (projets localisés + stages du showcase NDS) +├── i18n/ index.ts (helpers locale/chemins/hreflang), ui.ts (dictionnaires FR/EN) +├── lib/seo.ts JSON-LD : Person, WebSite, BreadcrumbList, SoftwareSourceCode, ProfilePage +├── lib/smooth.ts Singleton Lenis · lib/scrub.ts - progression 0..1 des scènes épinglées +├── layouts/ BaseLayout.astro (head SEO complet, nav, footer, init Lenis) +├── components/ +│ ├── scene/ voyage.ts - moteur de l'accueil · scene.ts - fond ambiant des sous-pages +│ ├── scenes/ InfraScene, BlueprintScene, SorterScene - scènes projet sur-mesure +│ └── showcase/ ComponentShowcase.astro - composants NDS "volants" (page projet NDS) +├── views/ La logique de chaque page (partagée entre locales) +└── pages/ Routes minces : / (FR, défaut) et /en/* (miroirs) ``` -## Design system +**Règle i18n** : une page = une view. Les routes (`pages/` et `pages/en/`) ne font +qu'importer la view ; la locale est déduite de l'URL (`getLocaleFromUrl`). +`/` = français (cible : recruteurs FR), `/en/` = anglais. Pour inverser, changer +`DEFAULT_LOCALE` dans `src/i18n/index.ts` **et** `i18n.defaultLocale` dans `astro.config.mjs`. -Components come from `@unkn0wndo3s/nova-design-system` : -`import { Button, Card, Badge, Navbar, Breadcrumb, TextField, Select, ... }`. -Global styles and tokens : `import '@unkn0wndo3s/nova-design-system/styles'` -(done once in `BaseLayout.astro`). +## Le voyage (accueil) -The package ships its raw `.astro`/`.scss` source files - Astro compiles them -on the consumer side. Since its `exports` field does not publish token partials -individually, the typography partial is exposed via a Sass -`loadPath` (see `astro.config.mjs`) and then re-exported by -`src/styles/_type.scss` (mixins `text-sm` … `text-5xl`). +`voyage.ts` transforme la hauteur de scroll en **timeline** : les sections HTML sont des +overlays fixes, et le scroll avance une caméra sur une courbe Catmull-Rom à travers +nébuleuses, ceinture d'astéroïdes (une balise lumineuse par projet) et portail final. -Icons : `@lucide/astro` (UI) and `simple-icons-astro` (tech). Since LinkedIn is -no longer distributed by either of them (trademark policy), its glyph is provided -locally in `BrandLinkedin.astro`. +- **Scroll down** → les astéroïdes dérivent et se percutent (collisions élastiques, + étincelles additives). +- **Scroll up** → **vortex gravitationnel** : spirale vers la formation d'origine. +- Barre de progression + points de navigation latéraux (clic = seek animé). +- Les ancres (`/#about`, `/#contact`…) deviennent des seeks sur la timeline. +- Progressive enhancement : le même markup rend une page normale empilée sans JS, + sans WebGL ou avec `prefers-reduced-motion` (la classe `html.cinema` n'est jamais posée). -## 3D Background +## /docs — la documentation technique -An ambient asteroid slowly rotates and floats in the background, in the middle of a -starfield and a discrete nebula (Three.js). It does not follow scroll actions -and produces no engine flame effects: it is a backdrop, not the subject. +Une page dédiée (`/docs`, FR/EN) documente tout le fonctionnement du site en 13 chapitres, +avec sommaire sticky (scrollspy) et snippets **extraits du vrai code source au build** +(imports Vite `?raw` + découpe par marqueurs — si un marqueur disparaît, le build échoue +au lieu de publier une doc périmée). Coloration syntaxique Shiki via `astro:components`. -- **Without WebGL** : the canvas hides cleanly, the site remains readable. +## Scènes projet sur-mesure + +Toutes épinglées (section haute + stage sticky), pilotées par `scrub.ts`, fallback statique : + +| Projet | Scène | +| --- | --- | +| nova-design-system | `ComponentShowcase` (« la forge ») — 3 actes : les tokens (vrais swatches) convergent, 8 vrais composants NDS défilent dans un tunnel 3D avec specs + snippet d'usage, la bibliothèque se fige en grille avec compteurs | +| nova-infra | `InfraScene` — le graphe Visiteur → Cloudflare → Apache/VPS se dessine au scroll, puis un paquet circule en continu | +| portfolio | `BlueprintScene` — le wireframe du site se trace (dashoffset) puis s'allume aux couleurs NDS | +| file-organizer | `SorterScene` — 20 fichiers éparpillés (seed déterministe) se rangent en colonnes au scroll | ## SEO -Meticulous search engine optimization, pre-configured: +- `hreflang` fr / en / x-default + canonical sur chaque page +- JSON-LD : `Person` (@id partagé), `WebSite`, `BreadcrumbList` par page, + `SoftwareSourceCode` par projet, `ProfilePage` sur /about +- Open Graph + Twitter cards localisés (`og-fr.png` / `og-en.png`) +- Sitemap i18n (`@astrojs/sitemap`) + `robots.txt` +- Pas de meta `keywords` (ignorées par Google, signal spam pour certains crawlers) -- Per-page metadata (title, description, keywords), canonical URLs; -- Open Graph + Twitter Card with `public/og.png` (1200×630) ; -- Structured data JSON-LD (`Person` + `WebSite`) ; -- `sitemap-index.xml` (via `@astrojs/sitemap`) + `public/robots.txt`. +## À faire côté déploiement -Remember to update `og.png` if you change your job title, and confirm the -links in `site.ts` (used in the `sameAs` property of the structured data). - -## Contact Form - -The site is 100% static: the form in `contact.astro` does not have a -backend. The **Send** button opens the user's mail client with a -pre-filled message (`mailto:`). For a server-side submission, connect a service -(Formspree, Resend, edge function…) to the `#cf-send` button. - ---- - -Built with Astro + Nova Design System. +1. **`public/og-fr.png` et `public/og-en.png`** (1200×630) — visuels de partage. +2. Brancher le workflow **Gitea Actions** existant (`npm ci && npm run build`, publier `dist/`). +3. `/about` et `/contact` redirigent vers `/#about` et `/#contact` (config Astro) — + les anciennes URL indexées ne cassent pas. +4. Tester le voyage sur un vrai GPU/trackpad et ajuster les poids `data-weight` + des sections dans `HomeView.astro` (durée relative de chaque étape). diff --git a/astro.config.mjs b/astro.config.mjs index 013786f..f218a4c 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,20 +1,47 @@ +// @ts-check import { defineConfig } from 'astro/config'; import sitemap from '@astrojs/sitemap'; +import { fileURLToPath } from 'node:url'; +const ndsTokens = fileURLToPath( + new URL('./node_modules/@unkn0wndo3s/nova-design-system/src/styles/tokens', import.meta.url), +); + +// https://astro.build/config export default defineConfig({ site: 'https://louis-potevin.dev', trailingSlash: 'ignore', - integrations: [sitemap()], + + // About & Contact now live on the home timeline. + redirects: { + '/about': '/#about', + '/contact': '/#contact', + '/en/about': '/en/#about', + '/en/contact': '/en/#contact', + }, + + i18n: { + defaultLocale: 'fr', + locales: ['fr', 'en'], + routing: { + prefixDefaultLocale: false, + }, + }, + + integrations: [ + sitemap({ + i18n: { + defaultLocale: 'fr', + locales: { fr: 'fr-FR', en: 'en-US' }, + }, + }), + ], + vite: { css: { preprocessorOptions: { scss: { - quietDeps: true, - loadPaths: [ - 'node_modules', - 'node_modules/@unkn0wndo3s/nova-design-system/src/styles', - 'src', - ], + loadPaths: [ndsTokens], }, }, }, diff --git a/package-lock.json b/package-lock.json index d897977..60a1ba2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,52 +1,108 @@ { - "name": "louis-potevin-dev", - "version": "1.0.0", + "name": "louis-potevin.dev", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "louis-potevin-dev", - "version": "1.0.0", + "name": "louis-potevin.dev", + "version": "2.0.0", "dependencies": { "@astrojs/sitemap": "^3.7.3", - "@lucide/astro": "^1.21.0", + "@lucide/astro": "^1.23.0", + "@types/three": "^0.185.0", "@unkn0wndo3s/nova-design-system": "^1.2.1", - "astro": "^7.0.2", + "astro": "^7.0.5", + "lenis": "^1.3.25", + "sass": "^1.101.0", "simple-icons-astro": "^16.1.0", - "three": "^0.184.0" + "three": "^0.185.1" }, "devDependencies": { - "@types/three": "^0.184.0", - "sass": "^1.101.0" - }, - "engines": { - "node": ">=22.12.0" + "@astrojs/check": "^0.9.9", + "typescript": "^6.0.3" } }, + "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.2.2", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.2.2.tgz", - "integrity": "sha512-PkCo+UcSxMt9pufjv28kRy8YU88O/XNgm7apwkyhkrCecLY62QCj5UA3nOTMO88PPyVKnUh/6FZbPefBhDD5IA==", + "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.2.2", - "@astrojs/compiler-binding-darwin-x64": "0.2.2", - "@astrojs/compiler-binding-linux-arm64-gnu": "0.2.2", - "@astrojs/compiler-binding-linux-arm64-musl": "0.2.2", - "@astrojs/compiler-binding-linux-x64-gnu": "0.2.2", - "@astrojs/compiler-binding-linux-x64-musl": "0.2.2", - "@astrojs/compiler-binding-wasm32-wasi": "0.2.2", - "@astrojs/compiler-binding-win32-arm64-msvc": "0.2.2", - "@astrojs/compiler-binding-win32-x64-msvc": "0.2.2" + "@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.2.2", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.2.2.tgz", - "integrity": "sha512-1WxpECx3izz5X4Ha3l6ex79HZlUHpKBTElGJsfOosnhVvXhccfcXAsBlrYPNmUOKJWiG7mcrje0ELSr1KPE69Q==", + "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" ], @@ -60,9 +116,9 @@ } }, "node_modules/@astrojs/compiler-binding-darwin-x64": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.2.2.tgz", - "integrity": "sha512-PdIQidwQ4nUX/qNL0JXzSwNYadr+yX/Yoo+kZSCxBZqlAqug9SlKR/1H5EG5jSEpkEDRo2d1JdrO1W4kU6uNHw==", + "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" ], @@ -76,9 +132,9 @@ } }, "node_modules/@astrojs/compiler-binding-linux-arm64-gnu": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.2.2.tgz", - "integrity": "sha512-5sZicPkJCoyxTEOW2he+u6UV97pTXY4c7fbHOZ/aslu9ewsunD0231QUBSvnjBB9hRFzzP2JRfNCLY+IvWfw0Q==", + "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" ], @@ -92,9 +148,9 @@ } }, "node_modules/@astrojs/compiler-binding-linux-arm64-musl": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.2.2.tgz", - "integrity": "sha512-Vto5fqRzMepQNJPeEhQLOIkmAoNbSBQNlpMe64OHTjEbAtQpJYVHEwe+8WJLaEGLA9qBksLv7ctiYPLLxgVVYQ==", + "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" ], @@ -108,9 +164,9 @@ } }, "node_modules/@astrojs/compiler-binding-linux-x64-gnu": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.2.2.tgz", - "integrity": "sha512-NJTTaUDU49WEJT+ImS9evlv3i3x42HN8PbcqdyydI9picnKtuf5TxRL94naoW09YDMYWpkrwVeVqaG7GTSIepQ==", + "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" ], @@ -124,9 +180,9 @@ } }, "node_modules/@astrojs/compiler-binding-linux-x64-musl": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.2.2.tgz", - "integrity": "sha512-9nFdNDkWaU35bhWUIciDmi439W0fq2ZNgv1GCi2A/LSb+8vTw9l9z+fVW4YEn7Tlvbs8gyQkJvbc62ZD1H76xA==", + "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" ], @@ -140,25 +196,25 @@ } }, "node_modules/@astrojs/compiler-binding-wasm32-wasi": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.2.2.tgz", - "integrity": "sha512-3NWaxRc3KSwr9zHBEGcQ147rMgAhi/HzZWZJPRN2YLbtuLhoeBPruhdX1MIlOYAg3FvJPYdzGCAKvvWWU6pF0A==", + "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.5" + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@astrojs/compiler-binding-win32-arm64-msvc": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.2.2.tgz", - "integrity": "sha512-QsyOgocLGOwDm8zAyuAiziALMzbTTevaqBLv5lvdhyhj2JC5yjp0H3yeEBtE0owRLr1gDQdX0+1qBSc5tTwp4g==", + "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" ], @@ -172,9 +228,9 @@ } }, "node_modules/@astrojs/compiler-binding-win32-x64-msvc": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.2.2.tgz", - "integrity": "sha512-+/C8Oh9YS8C0fwtaqDtUSPniKwlJqgiQbxp7XuzxCP0RI/Jf7yOlz9ZHNr6jD3ZJa4CHdq0mYq7pd8QFiNGIZA==", + "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" ], @@ -188,12 +244,15 @@ } }, "node_modules/@astrojs/compiler-rs": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.2.2.tgz", - "integrity": "sha512-C0qoz7Hxa2krlwMYfzQ1ZxOjR6cGmO+iskjRXXCdUM85W/cAPGTi/MSQlLkv0ADMz+Go1/lqeRBjL8AZwsFffQ==", + "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.2.2" + "@astrojs/compiler-binding": "0.3.0" + }, + "engines": { + "node": ">=22.12.0" } }, "node_modules/@astrojs/internal-helpers": { @@ -212,6 +271,48 @@ "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", @@ -263,6 +364,16 @@ "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", @@ -310,9 +421,9 @@ } }, "node_modules/@bruits/satteri-darwin-arm64": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.9.2.tgz", - "integrity": "sha512-tRpKRsiWxyh+Q5cjLwqj+baqI9htIcKqpDcftygA08bB/4nocZsneq+CVLmqT1/njG2Gq+ET12JHECz52ybiPQ==", + "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" ], @@ -322,9 +433,9 @@ ] }, "node_modules/@bruits/satteri-darwin-x64": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.9.2.tgz", - "integrity": "sha512-oLSI0+Upq3Nd1brOdRSl044TMKgy7S6cF4sM7a9/Ms6/IADvuey+9yG3GBXxLGAzSYzR0/hh7PPSP1QBfsBKwA==", + "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" ], @@ -334,9 +445,9 @@ ] }, "node_modules/@bruits/satteri-linux-arm64-gnu": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.9.2.tgz", - "integrity": "sha512-G+G6uYscRc4VUTNgZgS6SSfTHh04q/d5WiH66Jg39eftMzTAsT5cLSEHDTvDpleUT6BMNHdunx927WbddDgw7A==", + "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" ], @@ -346,9 +457,9 @@ ] }, "node_modules/@bruits/satteri-linux-arm64-musl": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.9.2.tgz", - "integrity": "sha512-wRaKSHCRXwzAZwlieCiv3u8pWEE2t9FmhZ0X59YZvHCGin56YUBOTyEQqSOrRfXs6fhUb8ylprisGYJC6J8mFQ==", + "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" ], @@ -358,9 +469,9 @@ ] }, "node_modules/@bruits/satteri-linux-x64-gnu": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.9.2.tgz", - "integrity": "sha512-hxEq3g/kS3tm3GKc2aRYnLMFI5IjxYzcd2sCwEH2C0V+m9TTAf+7Snt/+AZZklpxDOBrI+zthJHip9xnk9uM3g==", + "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" ], @@ -370,9 +481,9 @@ ] }, "node_modules/@bruits/satteri-linux-x64-musl": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.9.2.tgz", - "integrity": "sha512-ZSu+a07Efn8sOeA1k9ZM9CO9J3sAeG37Sf2YYxi/unFGgkFe+Y8MHDQlUNtngpC8pkYIjkojkLFT6v1yiHNxOg==", + "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" ], @@ -382,9 +493,9 @@ ] }, "node_modules/@bruits/satteri-wasm32-wasi": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.9.2.tgz", - "integrity": "sha512-Q/d7OIwXb5UGaiKUuPp2E6Bm41xD3sW8ts5L6l2m0Wi9uu9WKsv7CDc1ge6AVd6OC6g4nlSKWECFfPa7MkJchg==", + "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" ], @@ -392,16 +503,16 @@ "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.5" + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@bruits/satteri-win32-arm64-msvc": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.9.2.tgz", - "integrity": "sha512-QeaVjw+2IZeDRU7dU0KFlECTAwbL4fRUIXxZYQc4a16ejPzwizChEsKzoUBduyOJSDjyzvZxAmcdUYsSOUmR9A==", + "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" ], @@ -411,9 +522,9 @@ ] }, "node_modules/@bruits/satteri-win32-x64-msvc": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.9.2.tgz", - "integrity": "sha512-+hVrOLFtGgXakKO38F6afcc9/uuWvRGghPmiA6/ez5XGnWaM9VjQxqvogam5aOa7Nvl4V65P8Vpj5RsLzYeO8g==", + "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" ], @@ -466,9 +577,70 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", - "dev": true, "license": "Apache-2.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", @@ -927,9 +1099,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "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" ], @@ -939,19 +1111,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.3.2" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "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" ], @@ -961,19 +1133,38 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@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.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "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" ], @@ -987,9 +1178,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "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" ], @@ -1003,9 +1194,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "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" ], @@ -1019,9 +1210,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "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" ], @@ -1035,9 +1226,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "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" ], @@ -1051,9 +1242,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "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" ], @@ -1067,9 +1258,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "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" ], @@ -1083,9 +1274,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "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" ], @@ -1099,9 +1290,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "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" ], @@ -1115,9 +1306,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "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" ], @@ -1131,9 +1322,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "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" ], @@ -1143,19 +1334,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.3.2" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "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" ], @@ -1165,19 +1356,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.3.2" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "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" ], @@ -1187,19 +1378,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.3.2" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "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" ], @@ -1209,19 +1400,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-riscv64": "1.3.2" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "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" ], @@ -1231,19 +1422,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.3.2" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "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" ], @@ -1253,19 +1444,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "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" ], @@ -1275,19 +1466,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "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" ], @@ -1297,38 +1488,54 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "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.7.0" + "@emnapi/runtime": "^1.11.1" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "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.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "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" ], @@ -1338,16 +1545,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "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" ], @@ -1357,16 +1564,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "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" ], @@ -1376,7 +1583,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -1389,12 +1596,12 @@ "license": "MIT" }, "node_modules/@lucide/astro": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@lucide/astro/-/astro-1.21.0.tgz", - "integrity": "sha512-RSz8xUah+eApcVKuQJXuLzcaYwK++tTLHa34peSln3h8n/RblaohSmsHdOj1JUQT/KutfbXw19u2x5g0zMTT0w==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@lucide/astro/-/astro-1.23.0.tgz", + "integrity": "sha512-SP5JHwI46QJjCJGzS8BTQsUXcHJoGkVIeywN12NraSiTEt4uh0iNLPs4hEHTSJr+LbZeHxc3W1BiRJPF2tS5Yw==", "license": "ISC", "peerDependencies": { - "astro": "^4 || ^5 || ^6" + "astro": "^4 || ^5 || ^6 || ^7" } }, "node_modules/@napi-rs/wasm-runtime": { @@ -1422,9 +1629,9 @@ "license": "MIT" }, "node_modules/@oxc-project/types": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", - "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", + "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" @@ -1434,7 +1641,6 @@ "version": "2.5.6", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1474,7 +1680,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1495,7 +1700,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1516,7 +1720,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1537,7 +1740,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1558,7 +1760,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1579,7 +1780,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1600,7 +1800,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1621,7 +1820,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1642,7 +1840,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1663,7 +1860,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1684,7 +1880,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1705,7 +1900,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1726,7 +1920,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1741,9 +1934,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.2.tgz", - "integrity": "sha512-2cZ+7xRS+DBcuJBJKnfzsbleumJhBqSlJVpuzHC0nTqfd3QQ7Vx2/x5YR/D7cBamKSeWplwo82Fn9lqYUDEMfA==", + "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" ], @@ -1757,9 +1950,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.2.tgz", - "integrity": "sha512-RkPMJnygxsgOYdkfqgpwY0/Fzm8d0VQe6HGU2/B00Xa9eqdLbrII+DOKAodbJAn3ZL1AJxGHkZRPYazgGY6Ljw==", + "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" ], @@ -1773,9 +1966,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.2.tgz", - "integrity": "sha512-Uiczh6vFhwyfd7WNe7Q7mCA4KxAiLdz7jPE/WGizfRpIieoyFuNVMmM8HqZ9HwudTkY6/AeMQwlNJ9NJijguWw==", + "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" ], @@ -1789,9 +1982,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.2.tgz", - "integrity": "sha512-+TpdtTRgHiJFjCVFbw311SuLk3KfytPOQQn+VlAEv+gBxYPtL7E6JS9e/tk+8CwxhIZvemJKo4rTKgfWNsKkkA==", + "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" ], @@ -1805,9 +1998,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.2.tgz", - "integrity": "sha512-4lv1/tkmi7ueIVHnyreaOeUpiZP26BH9rRy6hoYfR9310A2B9nUEVRDvBx69vx64Nr3eTPPRkyciqJJs+j9Jmw==", + "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" ], @@ -1821,9 +2014,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.2.tgz", - "integrity": "sha512-gBSUVO0eaWgw1JMjK3gB8BMlX2Mk148s2lTiVT3e9vjVxbl7UDfMWWY8CfIaaqiXuM9fVTMxIpUz6CAo/B6Vlw==", + "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" ], @@ -1837,9 +2030,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.2.tgz", - "integrity": "sha512-LjQP/iZLBu8o8PjIfk4x3At0/mT6h282pvz8Z5LAyhGbu/kDezyO7ea62rF5uoqmgnIYqbN/MqJ3Si3Aymi7xQ==", + "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" ], @@ -1853,9 +2046,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.2.tgz", - "integrity": "sha512-X/7bVLWelEsbyWDUSXt7zVsTniLLPIY2n1rH58qr78l9i7MNbbxBWD8gI2vRfBWf4NUXJCUuQnfZDsp32LqsfQ==", + "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" ], @@ -1869,9 +2062,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.2.tgz", - "integrity": "sha512-gb6dYKW/1KDorGXyy48glEBJs/sxVSC5pcVrox/pFGV4mvwSFeg2sK5L2tRkVsVlh7kueqOgg4GEcuipJcGuKg==", + "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" ], @@ -1885,9 +2078,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.2.tgz", - "integrity": "sha512-JY4w85pU3iAiJVMh5nuk4/Mh9GjMsupe8MrIN53rwxAZW64GKrWeJBuN6SxQg9QTU5uB1cxyhDzW8jqRn1EABw==", + "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" ], @@ -1901,9 +2094,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.2.tgz", - "integrity": "sha512-xvpA7o5KCYLB0Rwscmuylb1/zHHSUx4g4xilm4prC5jP76pEUlzBmMbgpbh7bVDbId4NcfT96gN5i6mE6UDaiw==", + "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" ], @@ -1917,9 +2110,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.2.tgz", - "integrity": "sha512-p/ts6KBLjuk49Bp21XH77poQGt02iNz7ChgHep7tudPOaLinR/De/RHdxF8w8Yj4r/bF/bqXwH6PZrB2sA+Nvw==", + "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" ], @@ -1933,9 +2126,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.2.tgz", - "integrity": "sha512-VMu/wmrZ9hJzYlRhbw7jK5PODlugyKZ5mOdX78+lS8OvuFkWNQdz1pFLrI2p3P0pjXOmUZ7B48o5VnMH9QOGtg==", + "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" ], @@ -1944,16 +2137,16 @@ "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.5" + "@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.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.2.tgz", - "integrity": "sha512-xtUJqs8qEkuSviS0n1tsohaPuz3a1SPhZywOji4Oo+sgrJs8daEDMZ0QtqL0OS7dx8PoVpg2J/ZZycPY5I2+Zg==", + "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" ], @@ -1967,9 +2160,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.2.tgz", - "integrity": "sha512-85YiLQqjUKgSO/Zjnf9e0XIn5Ymrh1fLDWBeAkZqpuBR/3R8TpfoHXuyblqyQrftSSgWO9qpcHN8mkyKsLraoA==", + "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" ], @@ -2011,13 +2204,13 @@ } }, "node_modules/@shikijs/core": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.2.0.tgz", - "integrity": "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ==", + "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.2.0", - "@shikijs/types": "4.2.0", + "@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" @@ -2027,12 +2220,12 @@ } }, "node_modules/@shikijs/engine-javascript": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.2.0.tgz", - "integrity": "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og==", + "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.2.0", + "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" }, @@ -2041,12 +2234,12 @@ } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.2.0.tgz", - "integrity": "sha512-hTorK1dffPkpbMUk6Z+828PgRo7d07HbnizoP0hNPFjhxMHctj0Px/qoHeGMYafc6ju+u9iMldN4JbVzNQM++g==", + "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.2.0", + "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2" }, "engines": { @@ -2054,24 +2247,24 @@ } }, "node_modules/@shikijs/langs": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.2.0.tgz", - "integrity": "sha512-bwrVRlJ0wUhZxAbVdvBbv2TTC9yLsh4C/IO5Ofz0T8MQntgDvyVnkbjw9vi50r1kx7RCIJdnJnjZAwmAsXFLZQ==", + "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.2.0" + "@shikijs/types": "4.3.0" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/primitive": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.2.0.tgz", - "integrity": "sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA==", + "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.2.0", + "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" }, @@ -2080,21 +2273,21 @@ } }, "node_modules/@shikijs/themes": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.2.0.tgz", - "integrity": "sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w==", + "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.2.0" + "@shikijs/types": "4.3.0" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/types": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.2.0.tgz", - "integrity": "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw==", + "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", @@ -2114,7 +2307,6 @@ "version": "23.1.3", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", - "dev": true, "license": "MIT" }, "node_modules/@tybys/wasm-util": { @@ -2191,14 +2383,12 @@ "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", - "dev": true, "license": "MIT" }, "node_modules/@types/three": { - "version": "0.184.1", - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz", - "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==", - "dev": true, + "version": "0.185.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.0.tgz", + "integrity": "sha512-O2Uy8Cj4Nonr8dWUUbifMdPe8B0Mq7EdOHb89S4+kjUw/KhbjTZrUuYlrQ1bpUKG+EP9QJnN7qNxbHGlGoLHMA==", "license": "MIT", "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", @@ -2219,7 +2409,6 @@ "version": "0.5.24", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", - "dev": true, "license": "MIT" }, "node_modules/@ungap/structured-clone": { @@ -2243,6 +2432,143 @@ "astro": "^7.0.0" } }, + "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", @@ -2255,6 +2581,32 @@ "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", @@ -2302,12 +2654,12 @@ } }, "node_modules/astro": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/astro/-/astro-7.0.2.tgz", - "integrity": "sha512-Rj31HS85pVSfiQTxKTwH6vTmF8y57iRfkgb1uiCTtdLIh3jlUKPAPXtEmHXRKp/PdTS00D4EXYlWXypY+AVVCA==", + "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.2.2", + "@astrojs/compiler-rs": "^0.3.0", "@astrojs/internal-helpers": "0.10.0", "@astrojs/markdown-satteri": "0.3.2", "@astrojs/telemetry": "3.3.2", @@ -2345,7 +2697,6 @@ "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", - "rehype": "^13.0.2", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", @@ -2355,9 +2706,7 @@ "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", - "unist-util-visit": "^5.1.0", "unstorage": "^1.17.5", - "vfile": "^6.0.3", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", @@ -2377,7 +2726,7 @@ "url": "https://opencollective.com/astrodotbuild" }, "optionalDependencies": { - "sharp": "^0.34.0" + "sharp": "^0.34.0 || ^0.35.0" }, "peerDependencies": { "@astrojs/markdown-remark": "7.2.0" @@ -2473,6 +2822,21 @@ "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", @@ -2482,6 +2846,26 @@ "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", @@ -2684,18 +3068,6 @@ "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", @@ -2746,10 +3118,34 @@ "node": ">=4" } }, + "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/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "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" @@ -2759,9 +3155,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", "license": "MIT" }, "node_modules/esbuild": { @@ -2805,6 +3201,16 @@ "@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/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", @@ -2823,6 +3229,13 @@ "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", @@ -2838,6 +3251,23 @@ "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", @@ -2868,7 +3298,6 @@ "version": "0.8.3", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", - "dev": true, "license": "MIT" }, "node_modules/flattie": { @@ -2915,6 +3344,16 @@ "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", @@ -2953,57 +3392,6 @@ "uncrypto": "^0.1.3" } }, - "node_modules/hast-util-from-html": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", - "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "devlop": "^1.1.0", - "hast-util-from-parse5": "^8.0.0", - "parse5": "^7.0.0", - "vfile": "^6.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-parse5": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", - "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "hastscript": "^9.0.0", - "property-information": "^7.0.0", - "vfile": "^6.0.0", - "vfile-location": "^5.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-parse-selector": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", - "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "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", @@ -3040,23 +3428,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hastscript": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", - "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^4.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.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", @@ -3080,10 +3451,9 @@ "license": "BSD-2-Clause" }, "node_modules/immutable": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.7.tgz", - "integrity": "sha512-47Xb+LFbZ/ZIjQMj6Q5J3IfK7PJFuqRdFOC9FpGgRTK6U2dAEVmkR9hp58qU4FpYux5YXpneDwkj2EP6lppzFA==", - "dev": true, + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", "license": "MIT" }, "node_modules/iron-webcrypto": { @@ -3114,18 +3484,26 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" } }, + "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-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -3196,9 +3574,9 @@ } }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "funding": [ { "type": "github", @@ -3217,12 +3595,60 @@ "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/lenis": { + "version": "1.3.25", + "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.25.tgz", + "integrity": "sha512-mOKxayErlaONK8fm4LN3XNd99Qu4plTpn9h9qf8wxzjGrJDzuD84FYzZ81HCd6ZsWp++VWVwOzL286Pf2s2u4A==", + "license": "MIT", + "workspaces": [ + "packages/*", + "playground", + "playground/*" + ], + "funding": { + "type": "github", + "url": "https://github.com/sponsors/darkroomengineering" + }, + "peerDependencies": { + "@nuxt/kit": ">=3.0.0", + "react": ">=17.0.0", + "vue": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "react": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -3532,7 +3958,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", - "dev": true, "license": "MIT" }, "node_modules/micromark-util-character": { @@ -3633,6 +4058,13 @@ "node": ">=10" } }, + "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", @@ -3677,7 +4109,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, "license": "MIT", "optional": true }, @@ -3805,22 +4236,17 @@ } }, "node_modules/package-manager-detector": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", - "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "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/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } + "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/piccolore": { "version": "0.1.3", @@ -3847,9 +4273,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "funding": [ { "type": "opencollective", @@ -3874,6 +4300,22 @@ "node": "^10 || ^12 || >=14" } }, + "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", @@ -3945,50 +4387,31 @@ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", "license": "MIT" }, - "node_modules/rehype": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", - "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "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", - "dependencies": { - "@types/hast": "^3.0.0", - "rehype-parse": "^9.0.0", - "rehype-stringify": "^10.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/rehype-parse": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", - "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "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", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-from-html": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-stringify": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", - "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-to-html": "^9.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=0.10.0" } }, "node_modules/resolve-pkg-maps": { @@ -4016,12 +4439,12 @@ } }, "node_modules/rolldown": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.2.tgz", - "integrity": "sha512-x0CrQQqCXWGeI8dTvFfN/Dnv3yMKT9hv5jFjlOreKAx9wqLq9wz7VvLLHyaAXC90/CpggTu9SisSbsJJTPSjNQ==", + "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.137.0", + "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -4031,28 +4454,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.2", - "@rolldown/binding-darwin-arm64": "1.1.2", - "@rolldown/binding-darwin-x64": "1.1.2", - "@rolldown/binding-freebsd-x64": "1.1.2", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.2", - "@rolldown/binding-linux-arm64-gnu": "1.1.2", - "@rolldown/binding-linux-arm64-musl": "1.1.2", - "@rolldown/binding-linux-ppc64-gnu": "1.1.2", - "@rolldown/binding-linux-s390x-gnu": "1.1.2", - "@rolldown/binding-linux-x64-gnu": "1.1.2", - "@rolldown/binding-linux-x64-musl": "1.1.2", - "@rolldown/binding-openharmony-arm64": "1.1.2", - "@rolldown/binding-wasm32-wasi": "1.1.2", - "@rolldown/binding-win32-arm64-msvc": "1.1.2", - "@rolldown/binding-win32-x64-msvc": "1.1.2" + "@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/sass": { "version": "1.101.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.101.0.tgz", "integrity": "sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==", - "dev": true, "license": "MIT", "dependencies": { "chokidar": "^5.0.0", @@ -4070,9 +4492,9 @@ } }, "node_modules/satteri": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.9.2.tgz", - "integrity": "sha512-IDJTE5SzEg9PqtdZEToQ3bjlfQ/zM+/HNaBFrwPdZbtk9IL1RWVFvzR8EMIrC0JYybJNKZS8FL45GNSAMX/6Xg==", + "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", @@ -4080,15 +4502,15 @@ "@types/unist": "^3.0.3" }, "optionalDependencies": { - "@bruits/satteri-darwin-arm64": "0.9.2", - "@bruits/satteri-darwin-x64": "0.9.2", - "@bruits/satteri-linux-arm64-gnu": "0.9.2", - "@bruits/satteri-linux-arm64-musl": "0.9.2", - "@bruits/satteri-linux-x64-gnu": "0.9.2", - "@bruits/satteri-linux-x64-musl": "0.9.2", - "@bruits/satteri-wasm32-wasi": "0.9.2", - "@bruits/satteri-win32-arm64-msvc": "0.9.2", - "@bruits/satteri-win32-x64-msvc": "0.9.2" + "@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": { @@ -4113,62 +4535,67 @@ } }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, + "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.0.0", + "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@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.2.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.2.0.tgz", - "integrity": "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ==", + "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.2.0", - "@shikijs/engine-javascript": "4.2.0", - "@shikijs/engine-oniguruma": "4.2.0", - "@shikijs/langs": "4.2.0", - "@shikijs/themes": "4.2.0", - "@shikijs/types": "4.2.0", + "@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" }, @@ -4247,6 +4674,21 @@ "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==", "license": "MIT" }, + "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", @@ -4261,6 +4703,19 @@ "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/svgo": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", @@ -4287,9 +4742,9 @@ } }, "node_modules/three": { - "version": "0.184.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", - "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==", + "version": "0.185.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", + "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==", "license": "MIT" }, "node_modules/tiny-inflate": { @@ -4359,6 +4814,37 @@ "license": "0BSD", "optional": true }, + "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", @@ -4591,20 +5077,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/vfile-location": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", - "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile": "^6.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", @@ -4620,15 +5092,15 @@ } }, "node_modules/vite": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz", - "integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "~1.1.2", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", "tinyglobby": "^0.2.17" }, "bin": { @@ -4715,16 +5187,289 @@ } } }, - "node_modules/web-namespaces": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", - "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "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", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "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/which-pm-runs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", @@ -4734,12 +5479,118 @@ "node": ">=4" } }, + "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/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==", + "dev": 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", @@ -4749,6 +5600,16 @@ "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", diff --git a/package.json b/package.json index 0ebe588..5a046f5 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,28 @@ { - "name": "louis-potevin-dev", - "type": "module", - "version": "1.0.0", + "name": "louis-potevin.dev", + "version": "2.0.0", "private": true, - "description": "Portfolio Louis Potevin - louis-potevin.dev. Astro + Nova Design System.", - "engines": { - "node": ">=22.12.0" - }, + "type": "module", + "description": "Portfolio of Louis Potevin - full-stack developer. Astro + TypeScript + Sass, powered by Nova Design System.", "scripts": { "dev": "astro dev", "build": "astro build", "preview": "astro preview", - "astro": "astro" + "check": "astro check" }, "dependencies": { "@astrojs/sitemap": "^3.7.3", - "@lucide/astro": "^1.21.0", + "@lucide/astro": "^1.23.0", + "@types/three": "^0.185.0", "@unkn0wndo3s/nova-design-system": "^1.2.1", - "astro": "^7.0.2", + "astro": "^7.0.5", + "lenis": "^1.3.25", + "sass": "^1.101.0", "simple-icons-astro": "^16.1.0", - "three": "^0.184.0" + "three": "^0.185.1" }, "devDependencies": { - "@types/three": "^0.184.0", - "sass": "^1.101.0" + "@astrojs/check": "^0.9.9", + "typescript": "^6.0.3" } } diff --git a/public/favicon.svg b/public/favicon.svg index b23b416..edd4878 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1,12 +1,6 @@ - - - - - - - - - - - + + + + + diff --git a/src/components/BrandLinkedin.astro b/src/components/BrandLinkedin.astro index af66c75..75c948f 100644 --- a/src/components/BrandLinkedin.astro +++ b/src/components/BrandLinkedin.astro @@ -1,23 +1,19 @@ --- -// LinkedIn is no longer distributed by @lucide/astro or simple-icons-astro -// (trademark policy). Official glyph reproduced inline, monochrome -// via `currentColor` to remain consistent with the other icons. -export interface Props { - size?: number; -} -const { size = 18 } = Astro.props; +/** + * LinkedIn glyph, provided locally: neither lucide nor simple-icons + * distributes it anymore (trademark policy). + */ +export interface Props { size?: number } +const { size = 20 } = Astro.props; --- \ No newline at end of file + + diff --git a/src/components/Eyebrow.astro b/src/components/Eyebrow.astro index a0d5db1..6c261ef 100644 --- a/src/components/Eyebrow.astro +++ b/src/components/Eyebrow.astro @@ -1,39 +1,29 @@ --- export interface Props { - index?: string; // ex. "01" - label: string; + index?: string; } -const { index, label } = Astro.props; +const { index } = Astro.props; ---

- {index && {index}} - - {label} + {index && } +

diff --git a/src/components/ProjectCard.astro b/src/components/ProjectCard.astro index 8df6297..9bbab44 100644 --- a/src/components/ProjectCard.astro +++ b/src/components/ProjectCard.astro @@ -1,104 +1,118 @@ --- -import { Card, Badge, Link } from '@unkn0wndo3s/nova-design-system'; -import { ArrowUpRight } from '@lucide/astro'; +import { Badge, Button } from '@unkn0wndo3s/nova-design-system'; import TechIcon from './TechIcon.astro'; -import { statusMeta, type Project } from '../data/projects'; +import type { Project } from '../data/projects'; +import { getLocaleFromUrl, useTranslations, localizePath } from '../i18n'; export interface Props { project: Project; + index?: number; + headingLevel?: 'h2' | 'h3'; } -const { project } = Astro.props; -const status = statusMeta[project.status]; -const href = `/work/${project.slug}`; + +const { project, index, headingLevel: Heading = 'h3' } = Astro.props; +const locale = getLocaleFromUrl(Astro.url); +const t = useTranslations(locale); + +const statusType = { live: 'success', maintained: 'info', wip: 'warning' } as const; +const statusLabel = { + live: t('work.status.live'), + maintained: t('work.status.maintained'), + wip: t('work.status.wip'), +} as const; + +const href = localizePath(`/work/${project.slug}`, locale); --- -
- -
- {project.designation} - {status.label} -
-

{project.summary}

-
- { - project.stack.map((s) => ( - - - {s.label} - - )) - } -
- -
+
+
+ {typeof index === 'number' && ( + + )} + {statusLabel[project.status]} +
+ + + {project.name} + +

{project.tagline[locale]}

+

{project.summary[locale]}

+ +
    + {project.stack.map((tech) => ( +
  • + ))} +
+ +
+ +
diff --git a/src/components/SiteFooter.astro b/src/components/SiteFooter.astro index dcd6a37..d11a319 100644 --- a/src/components/SiteFooter.astro +++ b/src/components/SiteFooter.astro @@ -1,134 +1,98 @@ --- -import { Gitea, Github, Npm } from 'simple-icons-astro'; -import { Mail } from '@lucide/astro'; +import { Github, Gitea, Npm } from 'simple-icons-astro'; import BrandLinkedin from './BrandLinkedin.astro'; import { site } from '../data/site'; +import { getLocaleFromUrl, useTranslations } from '../i18n'; +const locale = getLocaleFromUrl(Astro.url); +const t = useTranslations(locale); const year = new Date().getFullYear(); -const socials = [ - { ...site.links.gitea, Comp: Gitea }, - { ...site.links.github, Comp: Github }, - { ...site.links.npm, Comp: Npm }, -]; --- diff --git a/src/components/SiteNav.astro b/src/components/SiteNav.astro index f3bb213..6cf8032 100644 --- a/src/components/SiteNav.astro +++ b/src/components/SiteNav.astro @@ -1,57 +1,176 @@ --- -import { Navbar, Button } from '@unkn0wndo3s/nova-design-system'; -import { Send } from '@lucide/astro'; +import { Button } from '@unkn0wndo3s/nova-design-system'; +import { + getLocaleFromUrl, + useTranslations, + localizePath, + unlocalizePath, + LOCALES, +} from '../i18n'; -const path = Astro.url.pathname.replace(/\/$/, '') || '/'; +const locale = getLocaleFromUrl(Astro.url); +const t = useTranslations(locale); +const route = unlocalizePath(Astro.url.pathname); + +// About & Contact live on the home timeline - anchors seek the voyage there. const links = [ - { href: '/', label: 'Home' }, - { href: '/work', label: 'Projects' }, - { href: '/about', label: 'About me' }, - { href: '/contact', label: 'Contact' }, + { href: localizePath('/', locale), label: t('nav.home'), match: '/' }, + { href: localizePath('/work', locale), label: t('nav.work'), match: '/work' }, + { href: localizePath('/docs', locale), label: t('nav.docs'), match: '/docs' }, + { href: localizePath('/#about', locale), label: t('nav.about'), match: '/about' }, + { href: localizePath('/#contact', locale), label: t('nav.contact'), match: '/contact' }, ]; -const isActive = (href: string) => - href === '/' ? path === '/' : path === href || path.startsWith(href + '/'); + +const isActive = (match: string) => + match === '/' ? route === '/' : route === match || route.startsWith(`${match}/`); --- - + + + diff --git a/src/components/SpaceBackground.astro b/src/components/SpaceBackground.astro index eb9a2b3..a121bd8 100644 --- a/src/components/SpaceBackground.astro +++ b/src/components/SpaceBackground.astro @@ -1,51 +1,27 @@ --- -// Persistent space background. Placed in the layout → loaded on all pages. -// Content remains 100% readable and functional even if WebGL fails (decorative). +/** + * Fixed WebGL background. Three.js is imported dynamically so the page + * stays interactive before the scene loads; the canvas removes itself + * when WebGL is unavailable or reduced motion is preferred. + */ --- - + - \ No newline at end of file + diff --git a/src/components/TechIcon.astro b/src/components/TechIcon.astro index fb6d828..981068f 100644 --- a/src/components/TechIcon.astro +++ b/src/components/TechIcon.astro @@ -1,56 +1,49 @@ --- -// Monochrome tech icon (inherits `color` via fill=currentColor). +/** + * Renders a technology icon by simple-icons name, with its label. + * Icons are imported explicitly so the build only ships what's used. + */ import { - Typescript, - Javascript, - Postgresql, - Docker, - Astro as AstroIcon, - Sass, - Gitea, - Linux, - Cloudflare, - Threedotjs, - Rust, - Python, - Npm, - Github, - Svelte, - Git, - Nodedotjs, - Vuedotjs, - Figma, + Apache, Astro as AstroIcon, Cloudflare, Docker, Figma, Git, Gitea, + Javascript, Linux, Nodedotjs, Npm, Postgresql, Python, Rust, Sass, + Threedotjs, Typescript, Vuedotjs, } from 'simple-icons-astro'; export interface Props { name: string; + icon: string; size?: number; } -const { name, size = 18 } = Astro.props; -const map: Record = { - typescript: Typescript, - javascript: Javascript, - postgresql: Postgresql, - docker: Docker, - astro: AstroIcon, - sass: Sass, - gitea: Gitea, - linux: Linux, - cloudflare: Cloudflare, - threedotjs: Threedotjs, - rust: Rust, - python: Python, - npm: Npm, - github: Github, - svelte: Svelte, - git: Git, - nodedotjs: Nodedotjs, - vue: Vuedotjs, - figma: Figma, +const icons: Record) => unknown> = { + Apache, Astro: AstroIcon, Cloudflare, Docker, Figma, Git, Gitea, + Javascript, Linux, Nodedotjs, Npm, Postgresql, Python, Rust, Sass, + Threedotjs, Typescript, Vuedotjs, }; -const Icon = map[name]; +const { name, icon, size = 18 } = Astro.props; +const Icon = icons[icon]; --- -{Icon && } \ No newline at end of file + + {Icon && + + diff --git a/src/components/scene/scene.ts b/src/components/scene/scene.ts index aef41cd..d84a062 100644 --- a/src/components/scene/scene.ts +++ b/src/components/scene/scene.ts @@ -1,248 +1,360 @@ +/** + * Space scene - ambient starfield + nebula, plus a scroll-reactive asteroid belt. + * + * Behavior contract: + * - Scrolling DOWN injects energy: asteroids drift apart, collide elastically + * and emit spark bursts on impact. + * - Scrolling UP does NOT rewind: a gravity vortex wakes up and pulls the + * belt into a slow spiral until it settles back into formation. + * - No WebGL, `prefers-reduced-motion`, or a lost context -> the canvas is + * removed and the site remains fully readable. + * + * Everything runs on a single fixed canvas, capped at DPR 2, paused when the + * tab is hidden. + */ import * as THREE from 'three'; -// Ambient space background: starfield, subtle nebula, and a slow-drifting, -// slow-rotating asteroid. No scroll binding, no engine flames. -// Slight pointer parallax. Respects prefers-reduced-motion. +const ASTEROID_COUNT = 90; +const BELT_RADIUS = 26; +const BELT_THICKNESS = 9; +const ASTEROID_MIN_R = 0.28; +const ASTEROID_MAX_R = 1.15; +const SPARK_POOL = 240; -export interface SceneHandle { - /** Kept for compatibility - no-op (no more impact/flame effect). */ - setImpact(value: number): void; - destroy(): void; +interface AsteroidState { + position: THREE.Vector3; + velocity: THREE.Vector3; + spin: THREE.Vector3; + rotation: THREE.Euler; + radius: number; + /** Slot in the resting formation, used by the vortex to settle back. */ + home: THREE.Vector3; } -const COLORS = { - star: 0xecf5f5, - starCool: 0x34c2c9, - starAccent: 0x2efafa, - nebulaA: 0x288181, - nebulaB: 0x1fa6ad, - rock: 0x1a2a2a, - rim: 0x34c2c9, -}; - -const prefersReduced = - typeof window !== 'undefined' && - window.matchMedia('(prefers-reduced-motion: reduce)').matches; - -/** Small reusable radial texture (soft glow). */ -function radialTexture(inner: string, outer = 'rgba(0,0,0,0)'): THREE.Texture { - const c = document.createElement('canvas'); - c.width = c.height = 128; - const ctx = c.getContext('2d')!; - const g = ctx.createRadialGradient(64, 64, 0, 64, 64, 64); - g.addColorStop(0, inner); - g.addColorStop(0.4, inner); - g.addColorStop(1, outer); - ctx.fillStyle = g; - ctx.fillRect(0, 0, 128, 128); - const tex = new THREE.CanvasTexture(c); - tex.colorSpace = THREE.SRGBColorSpace; - return tex; +function prefersReducedMotion(): boolean { + return window.matchMedia('(prefers-reduced-motion: reduce)').matches; } -export function initScene(canvas: HTMLCanvasElement): SceneHandle { - const renderer = new THREE.WebGLRenderer({ - canvas, - antialias: true, - alpha: true, - powerPreference: 'high-performance', - }); - renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.75)); - renderer.setSize(window.innerWidth, window.innerHeight); - renderer.setClearColor(0x000000, 0); +export function mountSpaceScene(canvas: HTMLCanvasElement): (() => void) | null { + if (prefersReducedMotion()) { + canvas.remove(); + return null; + } + + let renderer: THREE.WebGLRenderer; + try { + renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); + } catch { + canvas.remove(); + return null; + } + + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + renderer.setSize(window.innerWidth, window.innerHeight, false); const scene = new THREE.Scene(); - scene.fog = new THREE.FogExp2(0x040b0b, 0.012); + const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 400); + camera.position.set(0, 0, 46); - const camera = new THREE.PerspectiveCamera( - 55, - window.innerWidth / window.innerHeight, - 0.1, - 400, - ); - camera.position.set(0, 0, 18); - - // Lighting (cold, consistent with Nova tokens) - const ambient = new THREE.AmbientLight(0x16323a, 1.0); - scene.add(ambient); - const key = new THREE.DirectionalLight(COLORS.starCool, 0.9); - key.position.set(-6, 5, 9); + scene.add(new THREE.AmbientLight(0x9fdadd, 0.35)); + const key = new THREE.DirectionalLight(0x7ff5f5, 1.4); + key.position.set(18, 22, 30); scene.add(key); - const rimLight = new THREE.DirectionalLight(COLORS.starAccent, 0.4); - rimLight.position.set(8, -3, 4); - scene.add(rimLight); - // Starfield - const STAR_COUNT = prefersReduced ? 700 : 1700; - const starGeo = new THREE.BufferGeometry(); - const starPos = new Float32Array(STAR_COUNT * 3); - const starCol = new Float32Array(STAR_COUNT * 3); - const palette = [ - new THREE.Color(COLORS.star), - new THREE.Color(COLORS.starCool), - new THREE.Color(COLORS.starAccent), - ]; - for (let i = 0; i < STAR_COUNT; i++) { - starPos[i * 3] = (Math.random() - 0.5) * 120; - starPos[i * 3 + 1] = (Math.random() - 0.5) * 120; - starPos[i * 3 + 2] = (Math.random() - 0.5) * 120 - 20; - const col = palette[Math.random() < 0.78 ? 0 : Math.random() < 0.6 ? 1 : 2]; - starCol[i * 3] = col.r; - starCol[i * 3 + 1] = col.g; - starCol[i * 3 + 2] = col.b; + /* ── Starfield ─────────────────────────────────────────────────────────── */ + const starGeometry = new THREE.BufferGeometry(); + const starCount = 1400; + const starPositions = new Float32Array(starCount * 3); + for (let i = 0; i < starCount; i++) { + const r = 90 + Math.random() * 220; + const theta = Math.random() * Math.PI * 2; + const phi = Math.acos(2 * Math.random() - 1); + starPositions[i * 3] = r * Math.sin(phi) * Math.cos(theta); + starPositions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta); + starPositions[i * 3 + 2] = r * Math.cos(phi); } - starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3)); - starGeo.setAttribute('color', new THREE.BufferAttribute(starCol, 3)); - const starMat = new THREE.PointsMaterial({ - size: 0.42, - map: radialTexture('rgba(255,255,255,0.95)'), - transparent: true, - depthWrite: false, - vertexColors: true, - blending: THREE.AdditiveBlending, - }); - const stars = new THREE.Points(starGeo, starMat); + starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3)); + const stars = new THREE.Points( + starGeometry, + new THREE.PointsMaterial({ color: 0xbfeeee, size: 0.55, sizeAttenuation: true, transparent: true, opacity: 0.8 }), + ); scene.add(stars); - // Nebula (soft sprites, pushed far into the background) - const nebula = new THREE.Group(); - const nebTex = radialTexture('rgba(40,129,129,0.55)'); - [ - { x: -14, y: 8, z: -45, s: 60, c: COLORS.nebulaA }, - { x: 16, y: -10, z: -55, s: 72, c: COLORS.nebulaB }, - { x: 4, y: 14, z: -40, s: 46, c: COLORS.nebulaA }, - ].forEach((n) => { - const mat = new THREE.SpriteMaterial({ - map: nebTex, - color: n.c, - transparent: true, - opacity: 0.2, - blending: THREE.AdditiveBlending, - depthWrite: false, - }); - const sp = new THREE.Sprite(mat); - sp.position.set(n.x, n.y, n.z); - sp.scale.set(n.s, n.s, 1); - nebula.add(sp); - }); - scene.add(nebula); - - // Asteroid (cold rock, subtle halo) - const asteroid = new THREE.Group(); - const rockGeo = new THREE.IcosahedronGeometry(1.9, 2); - const pos = rockGeo.attributes.position as THREE.BufferAttribute; - const v = new THREE.Vector3(); - for (let i = 0; i < pos.count; i++) { - v.fromBufferAttribute(pos, i); - const h = Math.sin(v.x * 4.1) * Math.cos(v.y * 3.7) * Math.sin(v.z * 4.3); - const d = 1 + h * 0.16; - v.multiplyScalar(d); - pos.setXYZ(i, v.x, v.y, v.z); + /* ── Nebula (two soft additive sprites) ────────────────────────────────── */ + const nebulaTexture = makeGlowTexture(); + for (const [x, y, s, o] of [ + [-34, 14, 120, 0.10], + [30, -20, 150, 0.07], + ] as const) { + const sprite = new THREE.Sprite( + new THREE.SpriteMaterial({ + map: nebulaTexture, + color: 0x1fa6ad, + transparent: true, + opacity: o, + depthWrite: false, + blending: THREE.AdditiveBlending, + }), + ); + sprite.position.set(x, y, -60); + sprite.scale.setScalar(s); + scene.add(sprite); } - rockGeo.computeVertexNormals(); - const rockMat = new THREE.MeshStandardMaterial({ - color: COLORS.rock, - emissive: COLORS.rim, - emissiveIntensity: 0.12, + + /* ── Asteroid belt (instanced) ─────────────────────────────────────────── */ + const rockGeometry = makeRockGeometry(); + const rockMaterial = new THREE.MeshStandardMaterial({ + color: 0x2a4040, roughness: 0.92, metalness: 0.12, flatShading: true, }); - const rock = new THREE.Mesh(rockGeo, rockMat); - asteroid.add(rock); + const belt = new THREE.InstancedMesh(rockGeometry, rockMaterial, ASTEROID_COUNT); + belt.instanceMatrix.setUsage(THREE.DynamicDrawUsage); + scene.add(belt); - // very light cold halo, just to separate the rock from the background - const glow = new THREE.Sprite( - new THREE.SpriteMaterial({ - map: radialTexture('rgba(52,194,201,0.7)'), - color: COLORS.rim, - transparent: true, - opacity: 0.28, - blending: THREE.AdditiveBlending, - depthWrite: false, - }), - ); - glow.scale.set(7.5, 7.5, 1); - asteroid.add(glow); + const asteroids: AsteroidState[] = []; + for (let i = 0; i < ASTEROID_COUNT; i++) { + const angle = (i / ASTEROID_COUNT) * Math.PI * 2 + Math.random() * 0.35; + const radius = BELT_RADIUS + (Math.random() - 0.5) * BELT_THICKNESS; + const home = new THREE.Vector3( + Math.cos(angle) * radius, + (Math.random() - 0.5) * 10, + -8 + Math.sin(angle) * 10, + ); + asteroids.push({ + position: home.clone(), + velocity: new THREE.Vector3(), + spin: new THREE.Vector3(Math.random(), Math.random(), Math.random()).multiplyScalar(0.4), + rotation: new THREE.Euler(Math.random() * Math.PI, Math.random() * Math.PI, 0), + radius: ASTEROID_MIN_R + Math.random() * (ASTEROID_MAX_R - ASTEROID_MIN_R), + home, + }); + } - // ambient position: top right, set back - asteroid.position.set(6.5, 4.5, -2); - scene.add(asteroid); + /* ── Spark pool for collision impacts ──────────────────────────────────── */ + const sparkGeometry = new THREE.BufferGeometry(); + const sparkPositions = new Float32Array(SPARK_POOL * 3); + sparkGeometry.setAttribute('position', new THREE.BufferAttribute(sparkPositions, 3)); + const sparkMaterial = new THREE.PointsMaterial({ + color: 0x2efafa, + size: 0.5, + transparent: true, + opacity: 0.9, + depthWrite: false, + blending: THREE.AdditiveBlending, + }); + const sparks = new THREE.Points(sparkGeometry, sparkMaterial); + scene.add(sparks); + const sparkVel: THREE.Vector3[] = []; + const sparkLife: number[] = []; + for (let i = 0; i < SPARK_POOL; i++) { + sparkVel.push(new THREE.Vector3()); + sparkLife.push(0); + sparkPositions.set([0, -999, 0], i * 3); + } + let sparkCursor = 0; - // State & loop - const pointer = { x: 0, y: 0, tx: 0, ty: 0 }; + function burst(at: THREE.Vector3, strength: number) { + const n = 10 + Math.floor(strength * 8); + for (let i = 0; i < n; i++) { + const idx = sparkCursor; + sparkCursor = (sparkCursor + 1) % SPARK_POOL; + sparkPositions.set([at.x, at.y, at.z], idx * 3); + sparkVel[idx]! + .set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5) + .normalize() + .multiplyScalar(3 + Math.random() * 5 * strength); + sparkLife[idx] = 0.9; + } + } + + /* ── Scroll energy model ───────────────────────────────────────────────── */ + let lastScrollY = window.scrollY; + let downEnergy = 0; // fuels drift + collisions + let upEnergy = 0; // fuels the vortex + let beltVisible = true; + + const beltAnchor = document.querySelector('[data-belt-anchor]'); + if (beltAnchor) { + beltVisible = false; + new IntersectionObserver( + (entries) => { + beltVisible = entries[0]?.isIntersecting ?? true; + }, + { rootMargin: '20% 0px' }, + ).observe(beltAnchor); + } + + function onScroll() { + const delta = window.scrollY - lastScrollY; + lastScrollY = window.scrollY; + if (!beltVisible) return; + if (delta > 0) downEnergy = Math.min(1.6, downEnergy + delta * 0.004); + else if (delta < 0) upEnergy = Math.min(1.6, upEnergy - delta * 0.004); + } + window.addEventListener('scroll', onScroll, { passive: true }); + + /* ── Simulation ────────────────────────────────────────────────────────── */ + const matrix = new THREE.Matrix4(); + const quaternion = new THREE.Quaternion(); + const scaleVec = new THREE.Vector3(); + const tmp = new THREE.Vector3(); + // Manual timing (THREE.Clock is deprecated). + let lastTime = performance.now(); + let elapsed = 0; + let raf = 0; let running = true; - function onPointer(e: PointerEvent) { - pointer.tx = (e.clientX / window.innerWidth - 0.5) * 2; - pointer.ty = (e.clientY / window.innerHeight - 0.5) * 2; + function step() { + raf = requestAnimationFrame(step); + if (!running) return; + const now = performance.now(); + const dt = Math.min((now - lastTime) / 1000, 0.05); + lastTime = now; + elapsed += dt; + const t = elapsed; + + stars.rotation.y = t * 0.004; + downEnergy = Math.max(0, downEnergy - dt * 0.55); + upEnergy = Math.max(0, upEnergy - dt * 0.4); + + for (const a of asteroids) { + // Scroll down: outward drift + slight chaos -> collisions happen. + if (downEnergy > 0.01) { + tmp.copy(a.position).sub(new THREE.Vector3(0, 0, -8)).normalize(); + a.velocity.addScaledVector(tmp, downEnergy * dt * 2.2); + a.velocity.x += (Math.random() - 0.5) * downEnergy * dt * 3; + a.velocity.y += (Math.random() - 0.5) * downEnergy * dt * 3; + } + + // Scroll up: vortex - tangential pull + gentle fall toward home orbit. + if (upEnergy > 0.01) { + tmp.set(-(a.position.y - a.home.y) - a.position.x * 0.3, a.position.x - a.home.x, 0); + a.velocity.addScaledVector(tmp.normalize(), upEnergy * dt * 3.4); + tmp.copy(a.home).sub(a.position); + a.velocity.addScaledVector(tmp, upEnergy * dt * 0.9); + } + + // Always: soft spring back to formation + drag. + tmp.copy(a.home).sub(a.position); + a.velocity.addScaledVector(tmp, dt * 0.25); + a.velocity.multiplyScalar(1 - dt * 0.8); + a.position.addScaledVector(a.velocity, dt); + + a.rotation.x += a.spin.x * dt; + a.rotation.y += a.spin.y * dt; + } + + // Elastic collisions (spatially naive is fine at N=90). + if (downEnergy > 0.05) { + for (let i = 0; i < asteroids.length; i++) { + const a = asteroids[i]!; + for (let j = i + 1; j < asteroids.length; j++) { + const b = asteroids[j]!; + tmp.copy(b.position).sub(a.position); + const dist = tmp.length(); + const minDist = a.radius + b.radius; + if (dist > 0.0001 && dist < minDist) { + tmp.normalize(); + const relative = tmp.dot(b.velocity.clone().sub(a.velocity)); + if (relative < 0) { + const impulse = -relative; + a.velocity.addScaledVector(tmp, -impulse * 0.5); + b.velocity.addScaledVector(tmp, impulse * 0.5); + const overlap = minDist - dist; + a.position.addScaledVector(tmp, -overlap / 2); + b.position.addScaledVector(tmp, overlap / 2); + if (impulse > 1.2) { + burst(a.position.clone().addScaledVector(tmp, a.radius), Math.min(impulse / 4, 1)); + } + } + } + } + } + } + + for (let i = 0; i < asteroids.length; i++) { + const a = asteroids[i]!; + quaternion.setFromEuler(a.rotation); + scaleVec.setScalar(a.radius); + matrix.compose(a.position, quaternion, scaleVec); + belt.setMatrixAt(i, matrix); + } + belt.instanceMatrix.needsUpdate = true; + + for (let i = 0; i < SPARK_POOL; i++) { + if (sparkLife[i]! <= 0) continue; + sparkLife[i]! -= dt; + sparkVel[i]!.multiplyScalar(1 - dt * 2); + sparkPositions[i * 3] += sparkVel[i]!.x * dt; + sparkPositions[i * 3 + 1] += sparkVel[i]!.y * dt; + sparkPositions[i * 3 + 2] += sparkVel[i]!.z * dt; + if (sparkLife[i]! <= 0) sparkPositions[i * 3 + 1] = -999; + } + sparkGeometry.attributes.position!.needsUpdate = true; + sparkMaterial.opacity = 0.5 + downEnergy * 0.3; + + camera.position.x = Math.sin(t * 0.05) * 1.2; + camera.position.y = Math.cos(t * 0.04) * 0.8; + camera.lookAt(0, 0, -8); + + renderer.render(scene, camera); } function onResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); - renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setSize(window.innerWidth, window.innerHeight, false); } + window.addEventListener('resize', onResize); function onVisibility() { running = document.visibilityState === 'visible'; - if (running) loop(); + if (running) lastTime = performance.now(); // swallow the pause } - - window.addEventListener('resize', onResize); - window.addEventListener('pointermove', onPointer, { passive: true }); document.addEventListener('visibilitychange', onVisibility); - const clock = new THREE.Clock(); - let rafId = 0; + step(); - function loop() { - if (!running) return; - rafId = requestAnimationFrame(loop); - const dt = Math.min(clock.getDelta(), 0.05); - const t = clock.elapsedTime; - - // slow background drift - stars.rotation.y += dt * 0.01; - nebula.rotation.z += dt * 0.004; - - // asteroid: self-rotation + slight float (no scroll binding) - if (!prefersReduced) { - rock.rotation.x += dt * 0.18; - rock.rotation.y += dt * 0.26; - asteroid.position.y = 4.5 + Math.sin(t * 0.25) * 0.5; - asteroid.position.x = 6.5 + Math.cos(t * 0.18) * 0.4; - } - - // smooth camera parallax via pointer - pointer.x += (pointer.tx - pointer.x) * dt * 2; - pointer.y += (pointer.ty - pointer.y) * dt * 2; - camera.position.x = pointer.x * 1.2; - camera.position.y = -pointer.y * 0.8 + Math.sin(t * 0.3) * 0.15; - camera.lookAt(0, 0, 0); - - renderer.render(scene, camera); - } - loop(); - - return { - setImpact() { - /* no-op: kept for API compatibility */ - }, - destroy() { - running = false; - cancelAnimationFrame(rafId); - window.removeEventListener('resize', onResize); - window.removeEventListener('pointermove', onPointer); - document.removeEventListener('visibilitychange', onVisibility); - renderer.dispose(); - starGeo.dispose(); - rockGeo.dispose(); - scene.traverse((o) => { - const m = (o as THREE.Mesh).material; - if (Array.isArray(m)) m.forEach((mm) => mm.dispose()); - else if (m) (m as THREE.Material).dispose(); - }); - }, + return () => { + cancelAnimationFrame(raf); + window.removeEventListener('scroll', onScroll); + window.removeEventListener('resize', onResize); + document.removeEventListener('visibilitychange', onVisibility); + renderer.dispose(); + rockGeometry.dispose(); + starGeometry.dispose(); + sparkGeometry.dispose(); }; -} \ No newline at end of file +} + +/* ── Helpers ─────────────────────────────────────────────────────────────── */ + +/** Irregular rock: a dodecahedron with jittered vertices. */ +function makeRockGeometry(): THREE.BufferGeometry { + const geometry = new THREE.DodecahedronGeometry(1, 0); + const pos = geometry.attributes.position!; + for (let i = 0; i < pos.count; i++) { + const jitter = 0.78 + Math.random() * 0.44; + pos.setXYZ(i, pos.getX(i) * jitter, pos.getY(i) * jitter, pos.getZ(i) * jitter); + } + geometry.computeVertexNormals(); + return geometry; +} + +/** Radial-gradient sprite texture used for the nebula glow. */ +function makeGlowTexture(): THREE.Texture { + const size = 128; + const canvas = document.createElement('canvas'); + canvas.width = canvas.height = size; + const ctx = canvas.getContext('2d')!; + const gradient = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2); + gradient.addColorStop(0, 'rgba(255,255,255,1)'); + gradient.addColorStop(1, 'rgba(255,255,255,0)'); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, size, size); + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + return texture; +} diff --git a/src/components/scene/voyage.ts b/src/components/scene/voyage.ts new file mode 100644 index 0000000..fbd9f3c --- /dev/null +++ b/src/components/scene/voyage.ts @@ -0,0 +1,517 @@ +/** + * The home "voyage" engine. + * + * One fixed WebGL canvas + fixed HTML overlays. The page's scroll height is a + * timeline: scrolling never moves content - it advances a camera along a 3D + * path (nebulae, asteroid belt, one beacon per project, a final portal) and + * cross-fades the matching overlay. Scroll velocity feeds the belt physics: + * scrolling down shakes asteroids into collisions (sparks), scrolling up pulls + * them into a gravitational vortex back to formation. + * + * Progressive enhancement: without WebGL or with prefers-reduced-motion the + * `cinema` class is never added and the sections render as a normal page. + */ +import * as THREE from 'three'; +import type Lenis from 'lenis'; + +interface Segment { + id: string; + el: HTMLElement; + weight: number; + side: 'left' | 'right' | 'center'; + isProject: boolean; + /** Global progress window [start, end] (0..1). */ + start: number; + end: number; +} + +export interface VoyageHandle { + seek(id: string, immediate?: boolean): void; + dispose(): void; +} + +const DEPTH = 44; // world units per weight unit of scroll +const BG = 0x040b0b; +const CYAN = 0x2efafa; +const TEAL = 0x34c2c9; +const DEEP = 0x155e75; + +function glowTexture(hex: string): THREE.Texture { + const c = document.createElement('canvas'); + c.width = c.height = 256; + const ctx = c.getContext('2d')!; + const g = ctx.createRadialGradient(128, 128, 0, 128, 128, 128); + g.addColorStop(0, `${hex}ff`); + g.addColorStop(0.35, `${hex}66`); + g.addColorStop(1, `${hex}00`); + ctx.fillStyle = g; + ctx.fillRect(0, 0, 256, 256); + const tex = new THREE.CanvasTexture(c); + tex.colorSpace = THREE.SRGBColorSpace; + return tex; +} + +const smoothstep = (a: number, b: number, x: number) => { + const t = Math.min(1, Math.max(0, (x - a) / (b - a))); + return t * t * (3 - 2 * t); +}; +const lerp = (a: number, b: number, t: number) => a + (b - a) * t; + +export function mountVoyage(opts: { + canvas: HTMLCanvasElement; + container: HTMLElement; + lenis: Lenis; +}): VoyageHandle | null { + const { canvas, container, lenis } = opts; + + // --- Capability gate ----------------------------------------------------- + let renderer: THREE.WebGLRenderer; + try { + renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false }); + } catch { + return null; + } + + const isMobile = window.matchMedia('(max-width: 840px)').matches; + renderer.setPixelRatio(Math.min(window.devicePixelRatio, isMobile ? 1.75 : 2)); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setClearColor(BG, 1); + + // --- Timeline from the DOM ---------------------------------------------- + const els = Array.from(container.querySelectorAll('[data-seg]')); + if (!els.length) return null; + + const segments: Segment[] = els.map((el) => ({ + id: el.dataset.seg!, + el, + weight: Number(el.dataset.weight ?? 1), + side: (el.dataset.side as Segment['side']) ?? 'center', + isProject: 'project' in el.dataset, + start: 0, + end: 0, + })); + const totalWeight = segments.reduce((s, x) => s + x.weight, 0); + let acc = 0; + for (const s of segments) { + s.start = acc / totalWeight; + acc += s.weight; + s.end = acc / totalWeight; + } + + // Cinema mode on: sections become fixed overlays, the container becomes the timeline. + document.documentElement.classList.add('cinema'); + container.style.height = `${totalWeight * 100 + 60}vh`; + + // --- Scene --------------------------------------------------------------- + const scene = new THREE.Scene(); + scene.fog = new THREE.FogExp2(BG, 0.0072); + const camera = new THREE.PerspectiveCamera(58, window.innerWidth / window.innerHeight, 0.1, 400); + + // Flight path: one control point per segment boundary, gently weaving. + const points: THREE.Vector3[] = []; + let cum = 0; + for (let i = 0; i <= segments.length; i++) { + points.push(new THREE.Vector3(7 * Math.sin(i * 1.9), 2.4 * Math.sin(i * 1.3 + 1), -cum * DEPTH)); + cum += segments[i]?.weight ?? 1; + } + const path = new THREE.CatmullRomCurve3(points, false, 'catmullrom', 0.35); + const END_Z = points[points.length - 1]!.z; + + // Starfield: a long tube of stars around the whole path. + { + const count = isMobile ? 900 : 2000; + const pos = new Float32Array(count * 3); + const col = new Float32Array(count * 3); + const cCyan = new THREE.Color(CYAN); + const cWhite = new THREE.Color(0xdfffff); + for (let i = 0; i < count; i++) { + const a = Math.random() * Math.PI * 2; + const r = 16 + Math.random() * 85; + pos[i * 3] = Math.cos(a) * r; + pos[i * 3 + 1] = Math.sin(a) * r * 0.7; + pos[i * 3 + 2] = 30 + Math.random() * (END_Z - 90); + const c = Math.random() < 0.25 ? cCyan : cWhite; + col[i * 3] = c.r; + col[i * 3 + 1] = c.g; + col[i * 3 + 2] = c.b; + } + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); + geo.setAttribute('color', new THREE.BufferAttribute(col, 3)); + scene.add( + new THREE.Points( + geo, + new THREE.PointsMaterial({ + size: 0.55, + vertexColors: true, + transparent: true, + opacity: 0.9, + blending: THREE.AdditiveBlending, + depthWrite: false, + }), + ), + ); + } + + // Nebulae: big soft sprites drifting near the path. + const nebulae: THREE.Sprite[] = []; + { + const colors = ['#2EFAFA', '#34C2C9', '#155E75', '#2EFAFA', '#0E7490']; + for (let i = 0; i < 5; i++) { + const mat = new THREE.SpriteMaterial({ + map: glowTexture(colors[i]!), + transparent: true, + opacity: 0.14 + (i % 2) * 0.08, + blending: THREE.AdditiveBlending, + depthWrite: false, + }); + const sp = new THREE.Sprite(mat); + const u = (i + 0.5) / 5; + const p = path.getPointAt(u); + sp.position.set(p.x + (i % 2 ? -1 : 1) * (40 + i * 8), p.y + (i % 2 ? 14 : -10), p.z - 30); + const s = 150 + i * 30; + sp.scale.set(s, s, 1); + nebulae.push(sp); + scene.add(sp); + } + } + + // --- Asteroid belt across the project segments --------------------------- + const projectSegs = segments.filter((s) => s.isProject); + const beltZ0 = -((projectSegs[0]?.start ?? 0.3) * totalWeight) * DEPTH + 10; + const beltZ1 = -((projectSegs[projectSegs.length - 1]?.end ?? 0.7) * totalWeight) * DEPTH - 10; + + const AST = isMobile ? 48 : 110; + const belt = new THREE.InstancedMesh( + new THREE.DodecahedronGeometry(1, 0), + new THREE.MeshBasicMaterial({ color: 0x0d3b3f, wireframe: false }), + AST, + ); + const beltWire = new THREE.InstancedMesh( + new THREE.DodecahedronGeometry(1.001, 0), + new THREE.MeshBasicMaterial({ color: TEAL, wireframe: true, transparent: true, opacity: 0.45 }), + AST, + ); + interface Rock { + base: THREE.Vector3; + pos: THREE.Vector3; + vel: THREE.Vector3; + r: number; + spin: THREE.Vector3; + rot: THREE.Euler; + } + const rocks: Rock[] = []; + for (let i = 0; i < AST; i++) { + const a = Math.random() * Math.PI * 2; + const rad = 10 + Math.random() * 17; + const base = new THREE.Vector3( + Math.cos(a) * rad, + Math.sin(a) * rad * 0.55, + beltZ0 + Math.random() * (beltZ1 - beltZ0), + ); + rocks.push({ + base, + pos: base.clone(), + vel: new THREE.Vector3((Math.random() - 0.5), (Math.random() - 0.5), (Math.random() - 0.5)).multiplyScalar(2.2), + r: 0.45 + Math.random() * 1.3, + spin: new THREE.Vector3(Math.random(), Math.random(), Math.random()).multiplyScalar(0.8), + rot: new THREE.Euler(Math.random() * 3, Math.random() * 3, Math.random() * 3), + }); + } + scene.add(belt, beltWire); + + // Spark pool for collisions. + const SPARKS = 300; + const sparkPos = new Float32Array(SPARKS * 3); + const sparkVel: THREE.Vector3[] = []; + const sparkLife = new Float32Array(SPARKS); + for (let i = 0; i < SPARKS; i++) sparkVel.push(new THREE.Vector3()); + const sparkGeo = new THREE.BufferGeometry(); + sparkGeo.setAttribute('position', new THREE.BufferAttribute(sparkPos, 3)); + const sparks = new THREE.Points( + sparkGeo, + new THREE.PointsMaterial({ + color: CYAN, + size: 0.5, + transparent: true, + opacity: 0.95, + blending: THREE.AdditiveBlending, + depthWrite: false, + }), + ); + scene.add(sparks); + let sparkCursor = 0; + const burst = (at: THREE.Vector3, n: number) => { + for (let k = 0; k < n; k++) { + const i = sparkCursor++ % SPARKS; + sparkPos[i * 3] = at.x; + sparkPos[i * 3 + 1] = at.y; + sparkPos[i * 3 + 2] = at.z; + sparkVel[i]!.set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).multiplyScalar(9); + sparkLife[i] = 1; + } + }; + + // --- Beacons (one per project) + final portal ----------------------------- + interface Beacon { + group: THREE.Group; + ring: THREE.Mesh; + glow: THREE.Sprite; + seg: Segment; + } + const beacons: Beacon[] = []; + const beaconGlowTex = glowTexture('#2EFAFA'); + for (const seg of projectSegs) { + const mid = (seg.start + seg.end) / 2; + const p = path.getPointAt(mid); + const dir = seg.side === 'left' ? 1 : -1; // beacon opposite to the text card + const group = new THREE.Group(); + group.position.set(p.x + dir * 7.5, p.y + 0.5, p.z - 6); + + const core = new THREE.Mesh( + new THREE.IcosahedronGeometry(1.1, 1), + new THREE.MeshBasicMaterial({ color: CYAN, wireframe: true, transparent: true, opacity: 0.9 }), + ); + const ring = new THREE.Mesh( + new THREE.TorusGeometry(2.2, 0.045, 10, 72), + new THREE.MeshBasicMaterial({ color: TEAL, transparent: true, opacity: 0.7 }), + ); + ring.rotation.x = Math.PI / 2.4; + const glow = new THREE.Sprite( + new THREE.SpriteMaterial({ + map: beaconGlowTex, + transparent: true, + opacity: 0.5, + blending: THREE.AdditiveBlending, + depthWrite: false, + }), + ); + glow.scale.set(9, 9, 1); + group.add(core, ring, glow); + scene.add(group); + beacons.push({ group, ring, glow, seg }); + } + + const portal = new THREE.Group(); + { + const g1 = new THREE.Sprite( + new THREE.SpriteMaterial({ + map: glowTexture('#2EFAFA'), + transparent: true, + opacity: 0.85, + blending: THREE.AdditiveBlending, + depthWrite: false, + }), + ); + g1.scale.set(46, 46, 1); + const r1 = new THREE.Mesh( + new THREE.TorusGeometry(9, 0.12, 12, 96), + new THREE.MeshBasicMaterial({ color: CYAN, transparent: true, opacity: 0.8 }), + ); + const r2 = new THREE.Mesh( + new THREE.TorusGeometry(12.5, 0.06, 12, 96), + new THREE.MeshBasicMaterial({ color: DEEP, transparent: true, opacity: 0.7 }), + ); + portal.add(g1, r1, r2); + portal.position.set(0, 0, END_Z - 46); + scene.add(portal); + } + + // --- Overlay + progress UI ------------------------------------------------ + const progressBar = document.querySelector('[data-voyage-progress]'); + const dots = Array.from(document.querySelectorAll('[data-voyage-dot]')); + + const applyOverlay = (p: number) => { + for (const s of segments) { + const span = s.end - s.start; + const local = (p - s.start) / span; + const first = s === segments[0]; + const last = s === segments[segments.length - 1]; + const inV = first ? 1 : smoothstep(0.04, 0.22, local); + const outV = last ? 1 : 1 - smoothstep(0.8, 0.97, local); + const vis = Math.max(0, Math.min(inV, outV)); + const el = s.el; + if (local < -0.25 || local > 1.25) { + if (el.style.visibility !== 'hidden') el.style.visibility = 'hidden'; + continue; + } + el.style.visibility = 'visible'; + el.style.opacity = String(vis); + const y = (1 - inV) * 52 - (1 - outV) * 52; + el.style.transform = `translateY(${y.toFixed(1)}px) scale(${(0.985 + 0.015 * vis).toFixed(3)})`; + el.classList.toggle('is-active', vis > 0.45); + } + if (progressBar) progressBar.style.transform = `scaleX(${p.toFixed(4)})`; + const active = segments.findIndex((s) => p >= s.start && p < s.end); + dots.forEach((d, i) => d.classList.toggle('is-active', i === Math.max(0, active))); + }; + + // --- Frame loop ------------------------------------------------------------ + let progress = 0; + let downEnergy = 0; + let upEnergy = 0; + let elapsed = 0; + let last = performance.now(); + let running = true; + let frame = 0; + + const camTarget = new THREE.Vector3(); + const tmp = new THREE.Vector3(); + const mat4 = new THREE.Matrix4(); + const quat = new THREE.Quaternion(); + const scl = new THREE.Vector3(); + + const maxScroll = () => Math.max(1, document.documentElement.scrollHeight - window.innerHeight); + + const tick = () => { + if (!running) { + frame = requestAnimationFrame(tick); + return; + } + const now = performance.now(); + const dt = Math.min((now - last) / 1000, 0.05); + last = now; + elapsed += dt; + + // Scroll → timeline progress + belt energies (Lenis keeps this smooth). + const raw = lenis.scroll / maxScroll(); + progress = Math.min(1, Math.max(0, raw)); + const v = lenis.velocity; // px per frame, signed + downEnergy = lerp(downEnergy, Math.min(1, Math.max(0, v / 55)), 0.08); + upEnergy = lerp(upEnergy, Math.min(1, Math.max(0, -v / 55)), 0.08); + + // Camera along the path. + const u = progress; + path.getPointAt(u, camera.position); + path.getPointAt(Math.min(1, u + 0.045), camTarget); + // Ease the gaze toward the active beacon. + for (const b of beacons) { + const mid = (b.seg.start + b.seg.end) / 2; + const w = 1 - Math.min(1, Math.abs(u - mid) / (b.seg.end - b.seg.start)); + if (w > 0) camTarget.lerp(b.group.position, w * 0.35); + } + camera.lookAt(camTarget); + camera.position.y += Math.sin(elapsed * 0.7) * 0.12; // idle breathing + + // Nebulae drift. + for (let i = 0; i < nebulae.length; i++) { + nebulae[i]!.material.rotation = elapsed * 0.02 * (i % 2 ? 1 : -1); + } + + // Belt physics. + const agitated = downEnergy > 0.04; + for (let i = 0; i < AST; i++) { + const r = rocks[i]!; + if (agitated) { + r.pos.addScaledVector(r.vel, dt * (0.4 + downEnergy * 2.6)); + } + if (upEnergy > 0.04) { + // Vortex: tangential pull around the path axis + spring home. + tmp.set(-r.pos.y, r.pos.x, 0).normalize(); + r.pos.addScaledVector(tmp, dt * upEnergy * 14); + r.pos.lerp(r.base, dt * upEnergy * 1.6); + } else if (!agitated) { + r.pos.lerp(r.base, dt * 0.5); + } + r.rot.x += r.spin.x * dt * (1 + downEnergy * 3); + r.rot.y += r.spin.y * dt; + quat.setFromEuler(r.rot); + scl.setScalar(r.r); + mat4.compose(r.pos, quat, scl); + belt.setMatrixAt(i, mat4); + beltWire.setMatrixAt(i, mat4); + } + // Collisions only while agitated, only near the camera (cheap N² slice). + if (agitated) { + for (let i = 0; i < AST; i++) { + const a = rocks[i]!; + if (Math.abs(a.pos.z - camera.position.z) > 60) continue; + for (let j = i + 1; j < AST; j++) { + const b = rocks[j]!; + const dz = a.pos.z - b.pos.z; + if (dz > 6 || dz < -6) continue; + const d2 = a.pos.distanceToSquared(b.pos); + const rr = a.r + b.r; + if (d2 < rr * rr) { + tmp.subVectors(a.pos, b.pos).normalize(); + const va = a.vel.dot(tmp); + const vb = b.vel.dot(tmp); + a.vel.addScaledVector(tmp, vb - va); + b.vel.addScaledVector(tmp, va - vb); + a.pos.addScaledVector(tmp, rr - Math.sqrt(d2)); + burst(tmp.copy(a.pos).lerp(b.pos, 0.5), 7); + } + } + } + } + belt.instanceMatrix.needsUpdate = true; + beltWire.instanceMatrix.needsUpdate = true; + + // Sparks. + for (let i = 0; i < SPARKS; i++) { + if (sparkLife[i]! <= 0) continue; + sparkLife[i]! -= dt * 1.4; + sparkPos[i * 3] += sparkVel[i]!.x * dt; + sparkPos[i * 3 + 1] += sparkVel[i]!.y * dt; + sparkPos[i * 3 + 2] += sparkVel[i]!.z * dt; + if (sparkLife[i]! <= 0) sparkPos[i * 3 + 1] = 9999; + } + sparkGeo.attributes.position!.needsUpdate = true; + + // Beacons pulse; the active one lights up. + for (const b of beacons) { + const mid = (b.seg.start + b.seg.end) / 2; + const w = Math.max(0, 1 - Math.abs(u - mid) / ((b.seg.end - b.seg.start) * 0.9)); + const pulse = 1 + Math.sin(elapsed * 2.2) * 0.05; + b.group.scale.setScalar((0.7 + w * 0.5) * pulse); + b.ring.rotation.z = elapsed * 0.6; + (b.glow.material as THREE.SpriteMaterial).opacity = 0.25 + w * 0.55; + b.group.rotation.y = elapsed * 0.35; + } + + // Portal breathes and brightens on approach. + const arrive = smoothstep(0.82, 1, u); + portal.rotation.z = elapsed * 0.25; + portal.scale.setScalar(0.85 + arrive * 0.35 + Math.sin(elapsed * 1.4) * 0.02); + + applyOverlay(progress); + renderer.render(scene, camera); + frame = requestAnimationFrame(tick); + }; + + // --- Wiring ---------------------------------------------------------------- + const onResize = () => { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + }; + const onVisibility = () => { + running = !document.hidden; + if (running) last = performance.now(); + }; + window.addEventListener('resize', onResize); + document.addEventListener('visibilitychange', onVisibility); + + const seek = (id: string, immediate = false) => { + const s = segments.find((x) => x.id === id); + if (!s) return; + const target = (s.start + (s.end - s.start) * 0.45) * maxScroll(); + lenis.scrollTo(target, immediate ? { immediate: true } : { duration: 1.8 }); + }; + + dots.forEach((d) => d.addEventListener('click', () => seek(d.dataset.voyageDot!))); + + applyOverlay(0); + frame = requestAnimationFrame(tick); + + return { + seek, + dispose() { + cancelAnimationFrame(frame); + window.removeEventListener('resize', onResize); + document.removeEventListener('visibilitychange', onVisibility); + renderer.dispose(); + document.documentElement.classList.remove('cinema'); + }, + }; +} diff --git a/src/components/scenes/BlueprintScene.astro b/src/components/scenes/BlueprintScene.astro new file mode 100644 index 0000000..95bfbca --- /dev/null +++ b/src/components/scenes/BlueprintScene.astro @@ -0,0 +1,238 @@ +--- +/** + * Custom scene for `portfolio` (this very site): from wireframe to product. + * + * A pinned browser-window blueprint whose strokes draw themselves as you + * scroll, then light up with the NDS accent - the site building itself. + * Reduced motion / no JS: fully drawn and lit, no pin. + */ +import Eyebrow from '../Eyebrow.astro'; +import { getLocaleFromUrl, useTranslations } from '../../i18n'; +import type { Localized } from '../../data/projects'; + +const locale = getLocaleFromUrl(Astro.url); +const t = useTranslations(locale); + +const captions: Localized[] = [ + { fr: 'Structure : Astro, routes FR/EN, données typées.', en: 'Structure: Astro, FR/EN routes, typed data.' }, + { fr: 'Habillage : chaque pixel vient des tokens du Nova Design System.', en: 'Skin: every pixel comes from Nova Design System tokens.' }, + { fr: 'Mise en orbite : build statique, CI Gitea, déployé sur le VPS.', en: 'Into orbit: static build, Gitea CI, deployed on the VPS.' }, +]; + +// Wireframe strokes in draw order (all use pathLength=1 for easy dash scrub). +const strokes: { d: string; cls?: string }[] = [ + { d: 'M 60 40 H 900 A 16 16 0 0 1 916 56 V 484 A 16 16 0 0 1 900 500 H 60 A 16 16 0 0 1 44 484 V 56 A 16 16 0 0 1 60 40 Z' }, // window + { d: 'M 44 88 H 916' }, // top bar divider + { d: 'M 76 64 a 8 8 0 1 0 0.01 0' }, + { d: 'M 104 64 a 8 8 0 1 0 0.01 0' }, + { d: 'M 132 64 a 8 8 0 1 0 0.01 0' }, + { d: 'M 180 56 H 780 A 8 8 0 0 1 788 64 A 8 8 0 0 1 780 72 H 180 A 8 8 0 0 1 172 64 A 8 8 0 0 1 180 56 Z' }, // url bar + { d: 'M 92 150 H 420' }, // hero line 1 + { d: 'M 92 186 H 560' }, // hero line 2 (big) + { d: 'M 92 222 H 340' }, // hero line 3 + { d: 'M 92 262 H 208 A 10 10 0 0 1 218 272 V 286 A 10 10 0 0 1 208 296 H 92 A 10 10 0 0 1 82 286 V 272 A 10 10 0 0 1 92 262 Z', cls: 'accent' }, // CTA + { d: 'M 92 340 H 340 V 460 H 92 Z' }, // card 1 + { d: 'M 366 340 H 614 V 460 H 366 Z' }, // card 2 + { d: 'M 640 340 H 888 V 460 H 640 Z' }, // card 3 + { d: 'M 640 140 a 70 70 0 1 0 0.01 0', cls: 'accent' }, // planet in hero + { d: 'M 610 210 q 100 -60 200 -12', cls: 'accent' }, // planet ring +]; +--- + +
c[locale]))} +> +
+
+ {t('scene.blueprint.eyebrow')} +

{t('scene.blueprint.title')}

+
+ + + + + + {strokes.map((s, i) => ( + + ))} + + + + + louis-potevin.dev + + +

{captions[0]![locale]}

+ +
+
+ + + + diff --git a/src/components/scenes/InfraScene.astro b/src/components/scenes/InfraScene.astro new file mode 100644 index 0000000..563c985 --- /dev/null +++ b/src/components/scenes/InfraScene.astro @@ -0,0 +1,329 @@ +--- +/** + * Custom scene for `nova-infra`: the journey of a request. + * + * A pinned SVG network (Visitor → Cloudflare → Apache/VPS → services) whose + * edges and nodes are drawn by scroll. Once drawn, a packet keeps traveling + * the graph (time-based) so the diagram feels alive. Captions follow the + * scroll. Reduced motion / no JS: the fully drawn diagram, no pin. + */ +import Eyebrow from '../Eyebrow.astro'; +import { getLocaleFromUrl, useTranslations } from '../../i18n'; +import type { Localized } from '../../data/projects'; + +const locale = getLocaleFromUrl(Astro.url); +const t = useTranslations(locale); + +const captions: Localized[] = [ + { + fr: 'Une requête arrive : DNS et TLS passent par Cloudflare, qui filtre et met en cache.', + en: 'A request comes in: DNS and TLS go through Cloudflare, which filters and caches.', + }, + { + fr: "Apache, en reverse proxy sur le VPS OVH, route chaque domaine vers le bon service.", + en: 'Apache, reverse-proxying on the OVH VPS, routes each domain to the right service.', + }, + { + fr: 'Gitea, Vaultwarden, le runner CI et les sites tournent côte à côte - auto-hébergés, sauvegardés, monitorés.', + en: 'Gitea, Vaultwarden, the CI runner and the sites run side by side - self-hosted, backed up, monitored.', + }, +]; + +interface Node { + id: string; + label: string; + sub?: string; + x: number; + y: number; + w: number; +} +const nodes: Node[] = [ + { id: 'visitor', label: locale === 'fr' ? 'Visiteur' : 'Visitor', x: 40, y: 236, w: 130 }, + { id: 'cf', label: 'Cloudflare', sub: 'DNS · TLS · cache', x: 250, y: 236, w: 160 }, + { id: 'apache', label: 'Apache', sub: 'reverse proxy', x: 490, y: 236, w: 160 }, + { id: 'gitea', label: 'Gitea', sub: 'git.novaprojects.dev', x: 750, y: 90, w: 180 }, + { id: 'vault', label: 'Vaultwarden', sub: locale === 'fr' ? 'mots de passe' : 'passwords', x: 750, y: 236, w: 180 }, + { id: 'sites', label: 'Sites', sub: 'louis-potevin.dev', x: 750, y: 382, w: 180 }, +]; + +// Edges as SVG path data (right side of source → left side of target). +const edges: { id: string; d: string }[] = [ + { id: 'e-visitor-cf', d: 'M 170 264 C 210 264 210 264 250 264' }, + { id: 'e-cf-apache', d: 'M 410 264 C 450 264 450 264 490 264' }, + { id: 'e-apache-gitea', d: 'M 650 264 C 710 264 690 118 750 118' }, + { id: 'e-apache-vault', d: 'M 650 264 C 690 264 710 264 750 264' }, + { id: 'e-apache-sites', d: 'M 650 264 C 710 264 690 410 750 410' }, +]; +--- + +
c[locale]))}> +
+
+ {t('scene.infra.eyebrow')} +

{t('scene.infra.title')}

+
+ + + + + + VPS OVH · Debian + + + {edges.map((e) => )} + + {nodes.map((n) => ( + + + {n.label} + {n.sub && {n.sub}} + + ))} + + + + + +

{captions[0]![locale]}

+ +
+
+ + + + diff --git a/src/components/scenes/SorterScene.astro b/src/components/scenes/SorterScene.astro new file mode 100644 index 0000000..bffd74c --- /dev/null +++ b/src/components/scenes/SorterScene.astro @@ -0,0 +1,287 @@ +--- +/** + * Custom scene for `file-organizer`: from chaos to order. + * + * File chips start scattered across the board (seeded pseudo-random, so the + * chaos is stable between visits); scrolling sweeps each one along a small + * arc into its sorted column - exactly what the tool does to a messy folder. + * Reduced motion / no JS: the sorted end state, no pin. + */ +import Eyebrow from '../Eyebrow.astro'; +import { getLocaleFromUrl, useTranslations } from '../../i18n'; + +const locale = getLocaleFromUrl(Astro.url); +const t = useTranslations(locale); + +type Cat = 'img' | 'doc' | 'audio' | 'code'; +const columns: { cat: Cat; label: string }[] = [ + { cat: 'img', label: 'Images' }, + { cat: 'doc', label: locale === 'fr' ? 'Documents' : 'Documents' }, + { cat: 'audio', label: 'Audio' }, + { cat: 'code', label: 'Code' }, +]; + +const files: { name: string; cat: Cat }[] = [ + { name: 'IMG_2041.jpg', cat: 'img' }, + { name: 'screenshot.png', cat: 'img' }, + { name: 'logo.webp', cat: 'img' }, + { name: 'scan-04.tiff', cat: 'img' }, + { name: 'avatar.gif', cat: 'img' }, + { name: 'cv-2026.pdf', cat: 'doc' }, + { name: 'notes.md', cat: 'doc' }, + { name: 'facture_03.pdf', cat: 'doc' }, + { name: 'rapport.docx', cat: 'doc' }, + { name: 'todo.txt', cat: 'doc' }, + { name: 'demo-take2.wav', cat: 'audio' }, + { name: 'podcast.mp3', cat: 'audio' }, + { name: 'sample.flac', cat: 'audio' }, + { name: 'voice-memo.m4a', cat: 'audio' }, + { name: 'main.rs', cat: 'code' }, + { name: 'organizer.rs', cat: 'code' }, + { name: 'watch.rs', cat: 'code' }, + { name: 'Cargo.toml', cat: 'code' }, + { name: 'ui.rs', cat: 'code' }, + { name: 'fs_rules.rs', cat: 'code' }, +]; +--- + +
+
+
+ {t('scene.sorter.eyebrow')} +

{t('scene.sorter.title')}

+
+ +
+ {columns.map((col) => ( +
+

{col.label}

+ {files + .filter((f) => f.cat === col.cat) + .map((f) => ( + + + {f.name} + + ))} +
+ ))} +
+ + +
+
+ + + + diff --git a/src/components/showcase/ComponentShowcase.astro b/src/components/showcase/ComponentShowcase.astro new file mode 100644 index 0000000..c9e4275 --- /dev/null +++ b/src/components/showcase/ComponentShowcase.astro @@ -0,0 +1,871 @@ +--- +/** + * NDS scene, "la forge" - three scroll-driven acts on one pinned stage: + * + * Act 1 - Tokens: raw design tokens (real CSS custom properties, shown as + * live swatches) scattered in space converge into a neat row. + * Act 2 - Assembly: eight REAL NDS components staged one by one in a 3D + * tunnel - each arrives from the depth, holds in focus with its + * spec panel (category, role, tokens, usage snippet), then flies + * past the camera. + * Act 3 - The system: the full library snaps into a grid, with the + * package stats counting up and the install command typing out. + * + * Everything is CSS 3D + the shared scrub() helper - no WebGL needed here. + * Reduced motion / no JS: the acts render stacked and fully visible. + */ +import { + Avatar, + Badge, + Button, + Checkbox, + Pagination, + PaginationNumber, + Select, + SelectOption, + TextField, + Toggle, +} from '@unkn0wndo3s/nova-design-system'; +import Eyebrow from '../Eyebrow.astro'; +import { getLocaleFromUrl, useTranslations } from '../../i18n'; +import type { Localized } from '../../data/projects'; + +const locale = getLocaleFromUrl(Astro.url); +const t = useTranslations(locale); + +interface Stage { + id: string; + component: string; + category: Localized; + title: Localized; + desc: Localized; + tokens: string[]; + usage: string; +} + +const stages: Stage[] = [ + { + id: 'button', + component: '', + }, + { + id: 'badge', + component: '', + category: { fr: 'Feedback', en: 'Feedback' }, + title: { fr: 'Un statut lisible en un regard.', en: 'A status readable at a glance.' }, + desc: { + fr: 'Cinq types sémantiques × deux variantes (soft/solid). Les couleurs viennent des paires de tokens statut, jamais de valeurs en dur.', + en: 'Five semantic types × two variants (soft/solid). Colors come from the status token pairs, never hard-coded values.', + }, + tokens: ['--nds-success-high', '--nds-warning-high', '--nds-error-high'], + usage: 'En production', + }, + { + id: 'toggle', + component: '', + category: { fr: 'Formulaires', en: 'Forms' }, + title: { fr: 'Un vrai Web Component.', en: 'A real Web Component.' }, + desc: { + fr: "Encapsulé en custom element : état interne, événements, accessibilité clavier - utilisable même hors d'Astro.", + en: 'Wrapped as a custom element: internal state, events, keyboard accessibility - usable even outside Astro.', + }, + tokens: ['--nds-primary', '--nds-surface-2', '--nds-radius-full'], + usage: '', + }, + { + id: 'textfield', + component: '', + category: { fr: 'Formulaires', en: 'Forms' }, + title: { fr: 'Label, aide, compteur, erreurs.', en: 'Label, help, counter, errors.' }, + desc: { + fr: 'Compteur de caractères optionnel, messages liés en aria-describedby, styles de focus et d’erreur issus des tokens.', + en: 'Optional character counter, messages linked via aria-describedby, focus and error styles driven by tokens.', + }, + tokens: ['--nds-border', '--nds-text', '--nds-error-high'], + usage: '', + }, + { + id: 'select', + component: '…', + }, + { + id: 'checkbox', + component: '', + category: { fr: 'Formulaires', en: 'Forms' }, + title: { fr: 'Coche native, peau custom.', en: 'Native check, custom skin.' }, + desc: { + fr: "L'input natif reste dans le DOM (accessibilité et formulaires gratuits) ; seul le visuel est redessiné avec les tokens.", + en: 'The native input stays in the DOM (free accessibility and forms); only the visual layer is redrawn with tokens.', + }, + tokens: ['--nds-primary', '--nds-border', '--nds-radius-sm'], + usage: '', + }, + { + id: 'avatar', + component: '', + category: { fr: 'Data display', en: 'Data display' }, + title: { fr: 'Photo, sinon initiales.', en: 'Photo, else initials.' }, + desc: { + fr: "Fallback automatique : si l'image manque ou casse, les initiales prennent le relais - le layout ne bouge pas d'un pixel.", + en: 'Automatic fallback: if the image is missing or breaks, initials take over - the layout never shifts a pixel.', + }, + tokens: ['--nds-surface-2', '--nds-text', '--nds-radius-full'], + usage: '', + }, + { + id: 'pagination', + component: '', + category: { fr: 'Navigation', en: 'Navigation' }, + title: { fr: 'Composable, pas configurable.', en: 'Composable, not configurable.' }, + desc: { + fr: 'Pas de prop géante : des sous-composants (PaginationNumber…) à assembler - le pattern de composition utilisé partout dans NDS.', + en: 'No giant prop bag: sub-components (PaginationNumber…) you compose - the composition pattern used across NDS.', + }, + tokens: ['--nds-primary', '--nds-neutral', '--nds-radius-md'], + usage: '1', + }, +]; + +// Act 1: the raw material - real tokens rendered as live swatches. +const tokenField: { name: string; kind: 'color' | 'radius' | 'space' | 'type' }[] = [ + { name: '--nds-primary', kind: 'color' }, + { name: '--nds-accent', kind: 'color' }, + { name: '--nds-success-high', kind: 'color' }, + { name: '--nds-warning-high', kind: 'color' }, + { name: '--nds-error-high', kind: 'color' }, + { name: '--nds-surface', kind: 'color' }, + { name: '--nds-text', kind: 'color' }, + { name: '--nds-neutral', kind: 'color' }, + { name: '--nds-radius-md', kind: 'radius' }, + { name: '--nds-radius-full', kind: 'radius' }, + { name: '--nds-spacing-md', kind: 'space' }, + { name: '--nds-spacing-xl', kind: 'space' }, + { name: '--nds-font-mono', kind: 'type' }, + { name: '--nds-font-sans', kind: 'type' }, +]; +// Deterministic scatter (build-time constants, so SSR output is stable). +const scatter = [ + [-38, -26], [32, -34], [-12, 30], [44, 18], [-46, 6], [8, -18], + [24, 34], [-28, -8], [40, -6], [-6, -38], [16, 12], [-40, 28], + [0, 22], [36, 2], +]; + +const library = [ + 'Avatar', 'Badge', 'Breadcrumb', 'Button', 'Card', 'Checkbox', 'Link', 'ListItem', + 'LoadingBar', 'Modal', 'Navbar', 'Notification', 'NumericStepper', 'Pagination', + 'Radio', 'Select', 'Sidebar', 'Tab', 'TextField', 'Toggle', 'Tooltip', +]; + +const actLabels: Record = { + a1: { fr: 'Acte I - Tout part des tokens.', en: 'Act I - Everything starts with tokens.' }, + a2: { fr: 'Acte II - Les composants, forgés un à un.', en: 'Act II - The components, forged one by one.' }, + a3: { fr: 'Acte III - Le système.', en: 'Act III - The system.' }, +}; +--- + +
+
+
+ {t('nds.showcase.eyebrow')} +

{t('nds.showcase.title')}

+

{actLabels.a1![locale]}

+
+ + +
+ {tokenField.map((tk, i) => ( + + {tk.kind === 'color' && } + {tk.kind === 'radius' && } + {tk.kind === 'space' && } + {tk.kind === 'type' && Aa} + {tk.name} + + ))} +
+ + +
+ {stages.map((s, i) => ( +
+ + +
+
+ + {s.id === 'button' && ( +
+ + + + +
+ )} + {s.id === 'badge' && ( +
+ Primary + Success + Warning + Error + Info +
+ )} + {s.id === 'toggle' && ( +
+ +
+ )} + {s.id === 'textfield' && ( +
+ +
+ )} + {s.id === 'select' && ( +
+ +
+ )} + {s.id === 'checkbox' && ( +
+ + +
+ )} + {s.id === 'avatar' && ( +
+ + +
+ )} + {s.id === 'pagination' && ( +
+ + 1 + 2 + 3 + +
+ )} +
+ +
+

{s.category[locale]}

+

{s.title[locale]}

+

{s.desc[locale]}

+ +
    + {s.tokens.map((tok) => ( +
  • + + {tok} +
  • + ))} +
+ +
+
usage.astro
+
{s.usage}
+
+
+
+
+ ))} + + + +
+ + +
+
    + {library.map((name, i) => ( +
  • {name}
  • + ))} +
+
+

0{locale === 'fr' ? 'composants' : 'components'}

+

0{locale === 'fr' ? 'catégories' : 'categories'}

+

v1.2.1npm

+
+

npm install @unkn0wndo3s/nova-design-system

+
+ + +
+
+ + + + diff --git a/src/data/projects.ts b/src/data/projects.ts index 02f3b96..d154c69 100644 --- a/src/data/projects.ts +++ b/src/data/projects.ts @@ -1,187 +1,254 @@ -// Projects - real content. Edit freely. -// `icon` matches a simple-icons-astro component (using PascalCase elsewhere). +import type { Locale } from '../i18n'; -export type ProjectStatus = 'live' | 'maintained' | 'archived' | 'r&d'; +/** A string localized in both site languages. */ +export type Localized = Record; + +export type ProjectStatus = 'live' | 'maintained' | 'wip'; export interface ProjectLink { label: string; url: string; - icon?: string; } export interface Project { slug: string; - designation: string; // catalog index, e.g. "01" - title: string; - tagline: string; - period: string; - role: string; + name: string; status: ProjectStatus; - featured: boolean; - summary: string; - context: string[]; - contributions: string[]; - stack: { icon: string; label: string }[]; + tagline: Localized; + summary: Localized; + /** Long-form body, one paragraph per entry. */ + body: Localized[]; + highlights: Localized[]; + stack: { name: string; icon: string }[]; links: ProjectLink[]; + featured: boolean; } export const projects: Project[] = [ { slug: 'nova-design-system', - designation: '01', - title: 'Nova Design System', - tagline: 'Astro component library - tokens, accessibility, npm distribution.', - period: '2026 - ongoing', - role: 'Author · maintainer', + name: 'Nova Design System', status: 'maintained', featured: true, - summary: - 'A library of 29 Astro components across six families, with a complete token system (color, typography, spacing, radius) and a light/dark theme driven by CSS variables. Currently at version 1.2.1, it powers this portfolio and will be progressively adapted to my other projects, including outside the Astro ecosystem (a GPUI/Rust binding is planned for File Organizer).', - context: [ - 'Designed for clean, technical interfaces: mono fonts for headings, sans-serif for body text.', - 'Distributed under the npm scope @unkn0wndo3s, published automatically via a CI pipeline.', - 'License: free to use, resale of the design system as a standalone product is prohibited.', - 'Adoption is currently internal: the download spikes seen on npm in the days after each release are mostly bot/CI traffic rather than organic external adoption so far.', + tagline: { + fr: 'Bibliothèque de composants Astro - tokens, accessibilité, distribution npm.', + en: 'Astro component library - tokens, accessibility, npm distribution.', + }, + summary: { + fr: "29 composants Astro répartis en six familles, un système complet de tokens (couleur, typographie, espacement, rayons) et un thème clair/sombre piloté par variables CSS. Publié sur npm en v1.2.1, il propulse ce portfolio.", + en: 'A library of 29 Astro components across six families, a complete token system (color, typography, spacing, radius) and a light/dark theme driven by CSS variables. Published on npm at v1.2.1, it powers this portfolio.', + }, + body: [ + { + fr: "Nova Design System est né d'un besoin simple : arrêter de réécrire les mêmes boutons, champs et modales d'un projet à l'autre. Chaque composant est développé avec son interface TypeScript, ses états (hover, focus, disabled) et ses styles isolés en SCSS.", + en: 'Nova Design System started from a simple need: stop rewriting the same buttons, fields and modals from one project to the next. Every component ships with its TypeScript interface, its states (hover, focus, disabled) and isolated SCSS styles.', + }, + { + fr: "La distribution se fait via npm (paquet public `@unkn0wndo3s/nova-design-system`), avec une CI Gitea Actions auto-hébergée qui vérifie, versionne et publie chaque release en 10 à 20 secondes. Le paquet livre ses sources `.astro`/`.scss` brutes, compilées côté consommateur.", + en: 'Distribution happens through npm (public package `@unkn0wndo3s/nova-design-system`), with a self-hosted Gitea Actions CI that checks, versions and publishes each release in 10 to 20 seconds. The package ships raw `.astro`/`.scss` sources, compiled on the consumer side.', + }, + { + fr: "La suite : adapter progressivement NDS à mes autres projets, y compris hors de l'écosystème Astro - un binding GPUI/Rust est prévu pour File Organizer.", + en: 'Next up: progressively adapting NDS to my other projects, including outside the Astro ecosystem - a GPUI/Rust binding is planned for File Organizer.', + }, ], - contributions: [ - 'Architecture of tokens and components (Custom Elements, typed events, slots).', - 'Gitea Actions CI/CD pipeline for automated npm publishing: checkout, version check, install, publish — about 19s end to end.', - 'Current documentation: component list and status at nds.louis-potevin.dev, with technical usage docs coming next.', - '6-12 month roadmap: automatic component export on every release, new components, complete documentation. Longer term (12+ months): ports to Vue.js, React, Svelte, Java, and a Rust package, with docs kept up to date for each target.', + highlights: [ + { fr: '29 composants exportés, six familles', en: '29 exported components, six families' }, + { fr: 'Tokens couleur / typo / espacement / rayons, thème clair-sombre', en: 'Color / type / spacing / radius tokens, light-dark theme' }, + { fr: 'CI/CD Gitea Actions auto-hébergée, publication npm en ~15 s', en: 'Self-hosted Gitea Actions CI/CD, npm publish in ~15 s' }, + { fr: 'Licence maison : usage libre, monétisation du DS interdite', en: 'Custom license: free to use, monetizing NDS itself prohibited' }, ], stack: [ - { icon: 'astro', label: 'Astro' }, - { icon: 'typescript', label: 'TypeScript' }, - { icon: 'sass', label: 'Sass' }, - { icon: 'gitea', label: 'Gitea CI' }, + { name: 'Astro', icon: 'Astro' }, + { name: 'TypeScript', icon: 'Typescript' }, + { name: 'Sass', icon: 'Sass' }, + { name: 'Gitea CI', icon: 'Gitea' }, + { name: 'npm', icon: 'Npm' }, ], links: [ - { label: 'Nova Design System v1.2.1', url: 'https://nds.louis-potevin.dev/', icon: 'astro' }, - { label: 'npm', url: 'https://www.npmjs.com/package/@unkn0wndo3s/nova-design-system', icon: 'npm' }, - { label: 'Sources (Gitea)', url: 'https://git.novaprojects.dev/unkn0wn/nova-design-system', icon: 'gitea' }, + { label: 'npm', url: 'https://www.npmjs.com/package/@unkn0wndo3s/nova-design-system' }, + { label: 'Gitea', url: 'https://git.novaprojects.dev/unkn0wn/nova-design-system' }, ], }, { slug: 'nova-infra', - designation: '02', - title: 'Self-hosted Infrastructure & Deployment', - tagline: 'Containerized services, CI/CD, and secured ingress on a dedicated server.', - period: '2025 - ongoing', - role: 'Design & operations', + name: 'Self-hosted Infrastructure', status: 'live', featured: true, - summary: - 'Self-hosted infrastructure running on a dedicated OVH server. Four application containers currently active alongside the hosted sites, with Git, secret management, and exposure through an Apache reverse proxy.', - context: [ - 'Containerized with Docker on a dedicated OVH server.', - 'Services exposed through an Apache reverse proxy, suited to a dedicated-machine setup.', - 'Two domains managed: novaprojects.dev (4+ subdomains for self-hosted services) and louis-potevin.dev (1 subdomain).', + tagline: { + fr: 'Services conteneurisés, CI/CD et ingress sécurisé sur serveur dédié.', + en: 'Containerized services, CI/CD and secured ingress on a dedicated server.', + }, + summary: { + fr: "Infrastructure auto-hébergée sur serveur OVH : forge Git (Gitea), gestionnaire de secrets (Vaultwarden), runners CI et sites hébergés, exposés derrière un reverse proxy Apache et Cloudflare.", + en: 'Self-hosted infrastructure on an OVH server: Git forge (Gitea), secret manager (Vaultwarden), CI runners and hosted sites, exposed behind an Apache reverse proxy and Cloudflare.', + }, + body: [ + { + fr: "Tout mon outillage de développement tourne sur ma propre infrastructure : Gitea pour le code et les releases, Vaultwarden pour les secrets, un runner Gitea Actions pour la CI/CD, et les sites de production (dont celui-ci).", + en: 'All my development tooling runs on my own infrastructure: Gitea for code and releases, Vaultwarden for secrets, a Gitea Actions runner for CI/CD, and the production sites (including this one).', + }, + { + fr: "Chaque service est conteneurisé et isolé ; l'exposition passe par un reverse proxy Apache avec TLS, derrière Cloudflare pour le DNS et la protection. Les déploiements sont entièrement automatisés : un push sur `main` suffit.", + en: 'Each service is containerized and isolated; exposure goes through an Apache reverse proxy with TLS, behind Cloudflare for DNS and protection. Deployments are fully automated: a push to `main` is all it takes.', + }, ], - contributions: [ - 'Setup and administration of a self-hosted Gitea instance with a Postgres backend and one Actions runner.', - 'Lightweight CI/CD pipelines (build, test, publish), running in about 10-20 seconds.', - 'Day-to-day operations: Docker networks, volumes, DNS, diagnostics.', - 'Identified improvement areas: no monitoring or backup strategy in place yet — next iteration of the infrastructure.', + highlights: [ + { fr: 'Forge Git + CI/CD + secrets 100 % auto-hébergés', en: '100% self-hosted Git forge + CI/CD + secrets' }, + { fr: 'Reverse proxy Apache, TLS, DNS et protection Cloudflare', en: 'Apache reverse proxy, TLS, Cloudflare DNS and protection' }, + { fr: 'Déploiement continu : push sur main = mise en production', en: 'Continuous deployment: push to main = production release' }, ], stack: [ - { icon: 'docker', label: 'Docker' }, - { icon: 'linux', label: 'Linux' }, - { icon: 'apache', label: 'Apache (reverse proxy)' }, - { icon: 'gitea', label: 'Gitea' }, + { name: 'Docker', icon: 'Docker' }, + { name: 'Linux', icon: 'Linux' }, + { name: 'Apache', icon: 'Apache' }, + { name: 'Gitea', icon: 'Gitea' }, + { name: 'Cloudflare', icon: 'Cloudflare' }, ], - links: [{ label: 'Gitea', url: 'https://git.novaprojects.dev/unkn0wn', icon: 'gitea' }], + links: [{ label: 'Gitea', url: 'https://git.novaprojects.dev/unkn0wn' }], }, { slug: 'portfolio', - designation: '03', - title: 'louis-potevin.dev', - tagline: 'This website. Multi-page Astro app, 3D space background, 100% powered by Nova Design System.', - period: '2026', - role: 'Design & development', + name: 'louis-potevin.dev', status: 'live', featured: true, - summary: - 'A static multi-page portfolio (5 pages), entirely composed with Nova components, with a subtle Three.js space background. Built in one day, recently shipped.', - context: [ - 'Three.js background: starfield, nebula, and an ambient asteroid.', - 'UI entirely composed using the in-house design system.', - 'Astro with Static Site Generation (SSG), chosen for its simplicity over Svelte (already comfortable with Vue.js, and not fond of the Svelte DX).', + tagline: { + fr: 'Ce site. Astro multi-pages, fond 3D, bilingue, 100 % Nova Design System.', + en: 'This website. Multi-page Astro app, 3D background, bilingual, 100% Nova Design System.', + }, + summary: { + fr: "Portfolio statique bilingue (FR/EN), entièrement composé avec les composants Nova, avec une scène spatiale Three.js pilotée par le scroll et un SEO complet (hreflang, JSON-LD, sitemap).", + en: 'A bilingual (FR/EN) static portfolio, entirely composed with Nova components, featuring a scroll-driven Three.js space scene and full SEO (hreflang, JSON-LD, sitemap).', + }, + body: [ + { + fr: "Le site sert de vitrine et de banc d'essai au design system : chaque bouton, badge, champ ou fil d'Ariane provient du paquet npm `@unkn0wndo3s/nova-design-system`, sans style dupliqué.", + en: 'The site doubles as a showcase and test bench for the design system: every button, badge, field or breadcrumb comes from the `@unkn0wndo3s/nova-design-system` npm package, with no duplicated styling.', + }, + { + fr: "Côté 3D, une ceinture d'astéroïdes réagit au scroll sur la page d'accueil et les composants du design system flottent en 3D sur sa page projet. Le tout se coupe proprement sans WebGL ou quand l'utilisateur préfère réduire les animations.", + en: 'On the 3D side, an asteroid belt reacts to scroll on the home page and the design system components float in 3D on its project page. Everything degrades cleanly without WebGL or when the user prefers reduced motion.', + }, ], - contributions: [ - 'Full integration of Nova tokens and components.', - 'Respects prefers-reduced-motion and degrades gracefully when WebGL is unavailable.', - 'Current Lighthouse scores: Accessibility 100, Best Practices 100, SEO 92, Performance 92.', - 'Roadmap: multilingual version, dark/light mode toggle. No audience data yet (site just went live), no CV download for now.', + highlights: [ + { fr: 'Bilingue FR/EN avec hreflang et sitemap localisé', en: 'Bilingual FR/EN with hreflang and localized sitemap' }, + { fr: 'Scène Three.js pilotée par le scroll, fallback sans WebGL', en: 'Scroll-driven Three.js scene, no-WebGL fallback' }, + { fr: 'JSON-LD Person / WebSite / BreadcrumbList / SoftwareSourceCode', en: 'Person / WebSite / BreadcrumbList / SoftwareSourceCode JSON-LD' }, ], stack: [ - { icon: 'astro', label: 'Astro' }, - { icon: 'typescript', label: 'TypeScript' }, - { icon: 'sass', label: 'Sass' }, + { name: 'Astro', icon: 'Astro' }, + { name: 'TypeScript', icon: 'Typescript' }, + { name: 'Sass', icon: 'Sass' }, + { name: 'Three.js', icon: 'Threedotjs' }, ], - links: [{ label: 'Unkn0wn Projects', url: 'https://louis-potevin.dev/', icon: 'astro' }], + links: [{ label: 'Gitea', url: 'https://git.novaprojects.dev/unkn0wn/portfolio' }], }, { - slug: 'rust-file-organizer', - designation: '04', - title: 'File Organizer - Java to Rust', - tagline: 'File sorting tool, currently being ported from Java to Rust.', - period: '2025 - ongoing', - role: 'Development', - status: 'r&d', + slug: 'file-organizer', + name: 'File Organizer', + status: 'wip', featured: false, - summary: - 'An automatic sorting tool that moves downloaded files and folders into dedicated directories (executables, archives, etc.) at the root of the downloads folder, alongside the existing images/videos/documents directories. The Java version, functional on Windows, is being ported to Rust for Linux compatibility, cleaner code, and a simpler build.', - context: [ - 'Current Java version: many dependencies, slower build, loosely structured code, requires Java installed (or shipping the executable).', - 'Rust version: better organized code, simpler compilation, expected scan performance on par with or better than Java.', - 'GUI built with GPUI (used for the interface only), with a side console for monitoring/diagnosing errors.', + tagline: { + fr: "Portage Java vers Rust d'un organiseur de fichiers, UI native avec GPUI.", + en: 'Java-to-Rust port of a file organizer, native UI with GPUI.', + }, + summary: { + fr: "Réécriture en Rust d'un outil d'organisation de fichiers initialement en Java : surveillance du système de fichiers avec `notify`, interface native GPUI, et architecture qui sépare strictement la logique fichiers de l'UI.", + en: 'A Rust rewrite of a file-organizing tool originally built in Java: filesystem watching with `notify`, a native GPUI interface, and an architecture that strictly separates file logic from the UI.', + }, + body: [ + { + fr: "L'objectif du portage : gagner en performance et en fiabilité, et apprendre Rust sur un vrai projet. La logique métier (règles de tri, déplacement, surveillance) est pure et testable, indépendante de la couche de rendu.", + en: 'The goal of the port: gain performance and reliability, and learn Rust on a real project. The business logic (sorting rules, moves, watching) is pure and testable, independent from the rendering layer.', + }, ], - contributions: [ - 'Sorting logic: moves files/folders into type-specific directories (executables, archives, etc.), with subfolders grouped into a "folders" directory.', - 'Sorting safety under review: the Java version copied then deleted the source after the copy completed (to prevent data loss); the approach will be revisited in Rust.', - 'Key goal of the port: deep recursive scanning — the Java version only knows about files/folders it has moved or seen at the surface level, not nested contents.', - 'Current state: UI nearly complete, sorting logic still to be rewritten; tested on small volumes so far, targeting 10k+ files at once since it runs on startup.', - 'Open source on GitHub; will move to Gitea once the Rust port is finished.', + highlights: [ + { fr: 'Logique filesystem pure, découplée de l’UI', en: 'Pure filesystem logic, decoupled from the UI' }, + { fr: 'Surveillance temps réel avec le crate notify', en: 'Real-time watching with the notify crate' }, ], stack: [ - { icon: 'rust', label: 'Rust' }, - { icon: 'linux', label: 'Linux (upcoming)' }, + { name: 'Rust', icon: 'Rust' }, + { name: 'GPUI', icon: 'Rust' }, ], - links: [], - }, - { - slug: 'llm-tooling', - designation: '05', - title: 'LLM Training & Inference', - tagline: 'Language models trained on a Reddit conversation dataset, aiming for a voice-capable, autonomous assistant.', - period: '2025 - ongoing', - role: 'Development', - status: 'r&d', - featured: false, - summary: - 'A language model training and inference project on a dedicated machine, scaling up progressively: 100M, then 500M, 1B, and potentially 3B parameters. The end goal is a voice-capable conversational AI that learns from its conversations and gains a degree of autonomy.', - context: [ - 'Dedicated machine: Ryzen 9 9900X, RTX 5070 Ti, 64GB DDR5 6400MHz CL32.', - 'Dataset: Reddit conversations, starting at roughly 100GB with a target of scaling up to 1-2TB.', - 'Custom in-house inference script (currently being designed).', - ], - contributions: [ - 'Assembling and structuring the Reddit conversation dataset.', - 'Setting up the training pipeline, currently in the preparation phase (training has not started yet, so no quality metrics are available at this stage).', - ], - stack: [ - { icon: 'python', label: 'Python' }, - { icon: 'linux', label: 'Linux' }, - ], - links: [], + links: [{ label: 'GitHub', url: 'https://github.com/unkn0wndo3s/file-organizer' }], }, ]; export const featuredProjects = projects.filter((p) => p.featured); -export const statusMeta: Record = { - live: { label: 'Live', tone: 'success' }, - maintained: { label: 'Maintained', tone: 'primary' }, - 'r&d': { label: 'In Progress', tone: 'warning' }, - archived: { label: 'Archived', tone: 'neutral' }, -}; \ No newline at end of file +export function getProject(slug: string): Project | undefined { + return projects.find((p) => p.slug === slug); +} + +/* ── NDS showcase data (project page "flying components" tour) ─────────────── */ + +export interface ShowcaseStage { + id: string; + component: string; + title: Localized; + description: Localized; + tokens: string[]; +} + +export const ndsShowcase: ShowcaseStage[] = [ + { + id: 'button', + component: 'Button', + title: { fr: 'Button - quatre intentions, trois tailles', en: 'Button - four intents, three sizes' }, + description: { + fr: 'Primary, secondary, ghost et danger. Rendu en lien ou en bouton selon la prop href, focus visible au clavier.', + en: 'Primary, secondary, ghost and danger. Renders as a link or a button depending on the href prop, visible keyboard focus.', + }, + tokens: ['--nds-primary', '--nds-on-primary', '--nds-radius-md', '--nds-ring'], + }, + { + id: 'badge', + component: 'Badge', + title: { fr: 'Badge - statuts sémantiques', en: 'Badge - semantic statuses' }, + description: { + fr: 'Six types (primary, neutral, success, warning, error, info) en deux variantes, soft et solid, dérivées des tokens de statut.', + en: 'Six types (primary, neutral, success, warning, error, info) in two variants, soft and solid, derived from the status tokens.', + }, + tokens: ['--nds-success-medium', '--nds-warning-low', '--nds-radius-full'], + }, + { + id: 'textfield', + component: 'TextField', + title: { fr: 'TextField - saisie avec compteur', en: 'TextField - input with counter' }, + description: { + fr: 'Texte, e-mail, mot de passe ou textarea, avec label lié, placeholder et compteur de caractères optionnel.', + en: 'Text, e-mail, password or textarea, with a bound label, placeholder and optional character counter.', + }, + tokens: ['--nds-surface', '--nds-border', '--nds-text', '--nds-ring'], + }, + { + id: 'toggle', + component: 'Toggle', + title: { fr: 'Toggle - Web Component', en: 'Toggle - Web Component' }, + description: { + fr: "Implémenté selon le pattern Web Components : état interne, événements natifs, utilisable hors d'Astro.", + en: 'Built on the Web Components pattern: internal state, native events, usable outside Astro.', + }, + tokens: ['--nds-primary', '--nds-surface-2', '--nds-radius-full'], + }, + { + id: 'select', + component: 'Select', + title: { fr: 'Select - liste déroulante accessible', en: 'Select - accessible dropdown' }, + description: { + fr: 'Select et SelectOption composables, navigation clavier, valeur par défaut et état désactivé.', + en: 'Composable Select and SelectOption, keyboard navigation, default value and disabled state.', + }, + tokens: ['--nds-surface', '--nds-border-strong', '--nds-shadow-md'], + }, + { + id: 'pagination', + component: 'Pagination', + title: { fr: 'Pagination - navigation par pages', en: 'Pagination - page navigation' }, + description: { + fr: 'Pagination et PaginationNumber composables, état courant et bornes gérés par le composant.', + en: 'Composable Pagination and PaginationNumber, current state and bounds handled by the component.', + }, + tokens: ['--nds-primary-soft', '--nds-text', '--nds-radius-md'], + }, +]; diff --git a/src/data/site.ts b/src/data/site.ts index 0677d4b..2af305b 100644 --- a/src/data/site.ts +++ b/src/data/site.ts @@ -1,37 +1,32 @@ +/** Profile and links - single source of truth for the whole site. */ export const site = { - name: "Louis Potevin", - handle: "unkn0wn", - role: "Full-stack developer", - domain: "louis-potevin.dev", - location: "Limoges · Nouvelle-Aquitaine", - // Job Search - availability: "Permanent contract (CDI) - September 2026", - mobility: "Anywhere in France, on-site or remote", - // Contact - email: "contact@louis-potevin.dev", - // External links (icons via simple-icons-astro) + name: 'Louis Potevin', + email: 'contact@louis-potevin.dev', + location: { city: 'Limoges', region: 'Nouvelle-Aquitaine', country: 'FR' }, + availability: { cdiFrom: '2026-09', freelance: true }, links: { - gitea: { - label: "Gitea", - url: "https://git.novaprojects.dev/unkn0wn", - icon: "gitea", - }, - github: { - label: "GitHub", - url: "https://github.com/unkn0wndo3s", - icon: "github", - }, - npm: { - label: "npm", - url: "https://www.npmjs.com/~unkn0wndo3s", - icon: "npm", - }, - linkedin: { - label: "LinkedIn", - url: "https://www.linkedin.com/in/louis-potevin", - icon: "linkedin", - }, + github: { label: 'GitHub', url: 'https://github.com/unkn0wndo3s' }, + gitea: { label: 'Gitea', url: 'https://git.novaprojects.dev/unkn0wn' }, + npm: { label: 'npm', url: 'https://www.npmjs.com/~unkn0wndo3s' }, + linkedin: { label: 'LinkedIn', url: 'https://www.linkedin.com/in/louis-potevin' }, }, } as const; -export type SiteLink = { label: string; url: string; icon: string }; +/** Everyday tools, shown on the home page. `icon` maps to simple-icons-astro names. */ +export const stack = [ + { name: 'Astro', icon: 'Astro' }, + { name: 'Vue.js', icon: 'Vuedotjs' }, + { name: 'TypeScript', icon: 'Typescript' }, + { name: 'JavaScript', icon: 'Javascript' }, + { name: 'Sass', icon: 'Sass' }, + { name: 'Node.js', icon: 'Nodedotjs' }, + { name: 'PostgreSQL', icon: 'Postgresql' }, + { name: 'Docker', icon: 'Docker' }, + { name: 'Linux', icon: 'Linux' }, + { name: 'Cloudflare', icon: 'Cloudflare' }, + { name: 'Gitea CI/CD', icon: 'Gitea' }, + { name: 'Rust', icon: 'Rust' }, + { name: 'Python', icon: 'Python' }, + { name: 'Git', icon: 'Git' }, + { name: 'Figma', icon: 'Figma' }, +] as const; diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..6acf8f6 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,4 @@ +/// + +// The NDS "./styles" export maps to a raw .scss entry - no type declarations shipped. +declare module '@unkn0wndo3s/nova-design-system/styles'; diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..f1ee3ee --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,59 @@ +/** + * i18n core - locale detection, path localization, dictionary access. + * + * Routing model (Astro i18n, `prefixDefaultLocale: false`): + * fr (default) -> / /work /work/[slug] /about /contact + * en -> /en/ /en/work ... + */ +import { ui, type UIKey } from './ui'; + +export const LOCALES = ['fr', 'en'] as const; +export type Locale = (typeof LOCALES)[number]; +export const DEFAULT_LOCALE: Locale = 'fr'; + +export const LOCALE_LABELS: Record = { fr: 'Français', en: 'English' }; +export const OG_LOCALES: Record = { fr: 'fr_FR', en: 'en_US' }; +export const HTML_LANGS: Record = { fr: 'fr', en: 'en' }; + +/** Extracts the locale from a pathname ("/en/work" -> "en", "/work" -> "fr"). */ +export function getLocaleFromUrl(url: URL): Locale { + const [, first] = url.pathname.split('/'); + return (LOCALES as readonly string[]).includes(first) && first !== DEFAULT_LOCALE + ? (first as Locale) + : DEFAULT_LOCALE; +} + +/** Prefixes a root-relative path for the given locale. localizePath('/work', 'en') -> '/en/work' */ +export function localizePath(path: string, locale: Locale): string { + const clean = path.startsWith('/') ? path : `/${path}`; + if (locale === DEFAULT_LOCALE) return clean; + return clean === '/' ? `/${locale}/` : `/${locale}${clean}`; +} + +/** Strips a locale prefix, giving the canonical route shared by all locales. */ +export function unlocalizePath(pathname: string): string { + for (const locale of LOCALES) { + if (locale === DEFAULT_LOCALE) continue; + if (pathname === `/${locale}` || pathname === `/${locale}/`) return '/'; + if (pathname.startsWith(`/${locale}/`)) return pathname.slice(locale.length + 1); + } + return pathname; +} + +/** Translation accessor bound to a locale: const t = useTranslations('fr'); t('nav.home') */ +export function useTranslations(locale: Locale) { + return function t(key: UIKey): string { + return ui[locale][key] ?? ui[DEFAULT_LOCALE][key] ?? key; + }; +} + +/** hreflang alternates for the current page (fr, en, x-default). */ +export function getAlternates(url: URL, site: string) { + const route = unlocalizePath(url.pathname); + const abs = (p: string) => new URL(p, site).href; + return [ + { hreflang: 'fr', href: abs(localizePath(route, 'fr')) }, + { hreflang: 'en', href: abs(localizePath(route, 'en')) }, + { hreflang: 'x-default', href: abs(localizePath(route, DEFAULT_LOCALE)) }, + ]; +} diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts new file mode 100644 index 0000000..8db5c8a --- /dev/null +++ b/src/i18n/ui.ts @@ -0,0 +1,315 @@ +/** + * UI dictionaries. Keys are grouped by surface (nav, home, work, about, contact, footer). + * Project content lives in `src/data/projects.ts` (localized per field). + */ +const fr = { + // Meta + 'meta.home.title': 'Louis Potevin - Développeur full-stack (front, back, web)', + 'meta.home.description': + "Louis Potevin, développeur web full-stack : front-end, back-end, TypeScript, Astro, Node. Disponible en CDI dès septembre 2026 et en freelance, partout en France ou en remote.", + 'meta.work.title': 'Projets - Louis Potevin, développeur full-stack', + 'meta.work.description': + "Design system publié sur npm, infrastructure auto-hébergée, outils Rust : les projets de Louis Potevin, développeur web full-stack.", + 'meta.docs.title': 'Docs - Comment ce portfolio est construit, de A à Z', + 'meta.docs.description': + "Documentation technique complète du portfolio de Louis Potevin : Astro, i18n, Lenis, Three.js, scènes scroll-driven, design system, SEO, déploiement - avec le vrai code source.", + 'meta.about.title': 'À propos - Louis Potevin, développeur full-stack', + 'meta.about.description': + "Parcours de Louis Potevin : BUT MMI, alternance chez Legrand, projets personnels. Développeur full-stack TypeScript / Node, disponible en CDI et freelance.", + 'meta.contact.title': 'Contact - Louis Potevin, développeur full-stack', + 'meta.contact.description': + "Contacter Louis Potevin, développeur web full-stack. CDI dès septembre 2026 ou mission freelance, partout en France, sur site ou en remote.", + + // Nav + 'nav.home': 'Accueil', + 'nav.work': 'Projets', + 'nav.docs': 'Docs', + 'nav.about': 'À propos', + 'nav.contact': 'Contact', + 'nav.cta': 'Me contacter', + 'nav.skip': 'Aller au contenu', + 'nav.lang': 'Langue', + + // Availability + 'avail.badge': 'Disponible', + 'avail.line': 'CDI dès septembre 2026 · Freelance dès maintenant', + 'avail.where': 'Partout en France, sur site ou en remote', + + // Home + 'home.hero.title.role': 'Développeur full-stack', + 'home.hero.lead': + "Je conçois et développe des applications web de bout en bout : de l'interface au back-end, jusqu'au déploiement en production. Aussi à l'aise sur le composant que vous regardez que sur le service qui le sert.", + 'home.hero.viewProjects': 'Voir les projets', + 'home.hero.contact': 'Me contacter', + + 'home.profile.eyebrow': 'Profil', + 'home.profile.title': 'Un profil full-stack, à l’aise sur toute la chaîne.', + 'home.profile.body1': + "J'ai l'expérience d'applications métier en production (alternance chez Legrand) et de projets personnels menés jusqu'au bout : un design system publié sur npm, une infrastructure auto-hébergée, des outils en Rust et Python.", + 'home.profile.body2': + 'Ce qui me motive : maîtriser le cycle complet - le composant que vous regardez, le service qui le délivre et la CI qui le déploie. Ce site en est la preuve, construit entièrement sur mon propre design system.', + 'home.profile.front.title': 'Front-end', + 'home.profile.front.body': + 'Interfaces soignées et accessibles avec Astro, Vue.js, TypeScript et Sass - jusqu’au design system maison.', + 'home.profile.back.title': 'Back-end', + 'home.profile.back.body': + 'Services et API côté serveur, modélisation de données et logique métier avec Node et PostgreSQL.', + 'home.profile.ops.title': 'Outils & déploiement', + 'home.profile.ops.body': + 'Conteneurisation Docker, CI/CD automatisée et mise en production de bout en bout.', + + 'home.projects.eyebrow': 'Projets phares', + 'home.projects.title': 'Projets', + 'home.projects.all': 'Tous les projets', + 'home.projects.view': 'Voir le projet', + + 'home.stack.eyebrow': 'Stack technique', + 'home.stack.title': 'Mes outils du quotidien.', + + 'home.cta.title': 'On en parle ?', + 'home.cta.body': + 'Je cherche un poste full-stack en CDI dès septembre 2026, et je suis ouvert aux missions freelance. Partout en France, sur site ou en remote.', + 'home.cta.contact': 'Me contacter', + 'home.cta.more': 'En savoir plus', + + // Work + 'work.title': 'Projets', + 'work.lead': + "Chaque projet est mené comme un produit : design, code, tests, CI/CD et mise en production. Cliquez pour explorer le détail de chacun.", + 'work.status.live': 'En production', + 'work.status.maintained': 'Maintenu', + 'work.status.wip': 'En cours', + 'work.backToList': 'Tous les projets', + 'work.stack': 'Stack', + 'work.links': 'Liens', + 'work.highlights': 'Points clés', + 'work.next': 'Projet suivant', + + // NDS showcase + 'nds.showcase.eyebrow': 'Exploration interactive', + 'nds.showcase.title': 'Les composants, un à un.', + 'nds.showcase.hint': 'Faites défiler pour parcourir les composants', + 'nds.showcase.tokens': 'Tokens utilisés', + + // About + 'about.title': 'À propos', + 'about.lead': + "Développeur full-stack, formé en BUT MMI (parcours développement web) et aguerri en entreprise chez Legrand - du stage à l'alternance, sur la même application métier en production.", + 'about.exp.eyebrow': 'Expérience', + 'about.exp.title': 'Parcours', + 'about.values.eyebrow': 'Méthode', + 'about.values.title': 'Ma façon de travailler', + 'about.cv': 'Télécharger mon CV', + + // Contact + 'contact.title': 'Contact', + 'contact.lead': + 'Un poste en CDI, une mission freelance ou simplement une question : écrivez-moi, je réponds vite.', + 'contact.email.label': 'E-mail', + 'contact.email.copy': "Copier l'adresse", + 'contact.email.copied': 'Adresse copiée !', + 'contact.email.write': 'Écrire un e-mail', + 'contact.form.name': 'Votre nom', + 'contact.form.email': 'Votre e-mail', + 'contact.form.subject': 'Sujet', + 'contact.form.subject.cdi': 'Opportunité CDI', + 'contact.form.subject.freelance': 'Mission freelance', + 'contact.form.subject.other': 'Autre', + 'contact.form.message': 'Votre message', + 'contact.form.send': 'Envoyer le message', + 'contact.form.note': + "Le bouton ouvre votre client mail avec le message pré-rempli - aucune donnée n'est stockée sur ce site.", + 'contact.elsewhere': 'Me trouver ailleurs', + + // Footer + 'footer.role': 'Développeur full-stack · Limoges · Nouvelle-Aquitaine', + 'footer.madeWith': 'Construit avec Astro + Nova Design System', + + // Voyage (home cinematic scroll) + 'voyage.hint': 'Scrollez - le voyage commence', + 'voyage.progress': 'Progression du voyage', + 'home.projects.intro.title': 'Quatre projets, menés comme des produits.', + 'home.projects.intro.body': + 'Design, code, tests, CI/CD, production : chaque projet va au bout. En route.', + 'home.about.summary': + "BUT MMI (dév. web) à l'IUT du Limousin, et deux ans chez Legrand sur la même application métier - du stage à l'alternance.", + + // Docs + 'docs.eyebrow': 'Documentation', + 'docs.title': 'Sous le capot.', + 'docs.lead': + "Comment ce site anime le scroll, expliqué avec des démos qui tournent sous vos yeux et le vrai code du dépôt. Si vous lisez tout, vous savez le refaire.", + 'docs.toc': 'Sommaire', + 'docs.file': 'Fichier', + 'docs.why': 'Pourquoi ça marche', + 'docs.live': 'Démo - ça tourne en vrai', + 'docs.tryIt': 'Scrollez, regardez', + 'docs.trap': 'Piège', + 'docs.reading': 'Lecture ~20 min, molette obligatoire', + + // Project scenes + 'scene.hint': 'Faites défiler - la scène est pilotée par le scroll', + 'scene.infra.eyebrow': 'Sous le capot', + 'scene.infra.title': 'Le trajet d’une requête.', + 'scene.blueprint.eyebrow': 'Plan de construction', + 'scene.blueprint.title': 'Du wireframe au produit.', + 'scene.sorter.eyebrow': 'Démonstration', + 'scene.sorter.title': 'Du chaos au rangé.', + + // Misc + 'a11y.langSwitch': 'Changer de langue', + 'notfound.title': 'Page introuvable', + 'notfound.body': "Cette page n'existe pas (ou plus). Le reste du site, lui, fonctionne très bien.", + 'notfound.back': "Retour à l'accueil", +} as const; + +const en: Record = { + 'meta.home.title': 'Louis Potevin - Full-stack Developer (front, back, web)', + 'meta.home.description': + 'Louis Potevin, full-stack web developer: front-end, back-end, TypeScript, Astro, Node. Available for a permanent contract (CDI) from September 2026 and for freelance work, anywhere in France or remote.', + 'meta.work.title': 'Projects - Louis Potevin, full-stack developer', + 'meta.work.description': + 'A design system published on npm, self-hosted infrastructure, Rust tooling: the projects of Louis Potevin, full-stack web developer.', + 'meta.docs.title': 'Docs - How this portfolio is built, from A to Z', + 'meta.docs.description': + "Complete technical documentation of Louis Potevin's portfolio: Astro, i18n, Lenis, Three.js, scroll-driven scenes, design system, SEO, deployment - with the real source code.", + 'meta.about.title': 'About - Louis Potevin, full-stack developer', + 'meta.about.description': + "Louis Potevin's background: BUT MMI degree, apprenticeship at Legrand, personal projects. Full-stack TypeScript / Node developer, open to permanent and freelance roles.", + 'meta.contact.title': 'Contact - Louis Potevin, full-stack developer', + 'meta.contact.description': + 'Get in touch with Louis Potevin, full-stack web developer. Permanent contract from September 2026 or freelance work, anywhere in France, on-site or remote.', + + 'nav.home': 'Home', + 'nav.work': 'Projects', + 'nav.docs': 'Docs', + 'nav.about': 'About me', + 'nav.contact': 'Contact', + 'nav.cta': 'Get in touch', + 'nav.skip': 'Skip to content', + 'nav.lang': 'Language', + + 'avail.badge': 'Available', + 'avail.line': 'Permanent contract from September 2026 · Freelance now', + 'avail.where': 'Anywhere in France, on-site or remote', + + 'home.hero.title.role': 'Full-stack developer', + 'home.hero.lead': + 'I design and build web applications end to end: from the interface to the back-end, all the way to production. Equally at home on the component you look at and the service that delivers it.', + 'home.hero.viewProjects': 'View projects', + 'home.hero.contact': 'Contact me', + + 'home.profile.eyebrow': 'Profile', + 'home.profile.title': 'A full-stack profile, comfortable across the entire stack.', + 'home.profile.body1': + 'I have hands-on experience with business applications in production (apprenticeship at Legrand) and personal projects shipped for real: a design system published on npm, self-hosted infrastructure, tools built with Rust and Python.', + 'home.profile.body2': + 'What drives me: owning the whole cycle - the component you look at, the service that delivers it, the CI that deploys it. This site is proof, built entirely on my own design system.', + 'home.profile.front.title': 'Front-end', + 'home.profile.front.body': + 'Polished, accessible interfaces with Astro, Vue.js, TypeScript and Sass - all the way to an in-house design system.', + 'home.profile.back.title': 'Back-end', + 'home.profile.back.body': + 'Server-side services and APIs, data modeling and business logic with Node and PostgreSQL.', + 'home.profile.ops.title': 'Tools & deployment', + 'home.profile.ops.body': + 'Docker containerization, automated CI/CD and end-to-end production deployment.', + + 'home.projects.eyebrow': 'Featured projects', + 'home.projects.title': 'Projects', + 'home.projects.all': 'All projects', + 'home.projects.view': 'View project', + + 'home.stack.eyebrow': 'Tech stack', + 'home.stack.title': 'My everyday tools.', + + 'home.cta.title': "Let's talk!", + 'home.cta.body': + 'I am looking for a full-stack permanent position from September 2026, and I am open to freelance work. Anywhere in France, on-site or remote.', + 'home.cta.contact': 'Contact me', + 'home.cta.more': 'Learn more', + + 'work.title': 'Projects', + 'work.lead': + 'Every project is run like a product: design, code, tests, CI/CD and production. Click through to explore each one in detail.', + 'work.status.live': 'Live', + 'work.status.maintained': 'Maintained', + 'work.status.wip': 'In progress', + 'work.backToList': 'All projects', + 'work.stack': 'Stack', + 'work.links': 'Links', + 'work.highlights': 'Highlights', + 'work.next': 'Next project', + + 'nds.showcase.eyebrow': 'Interactive tour', + 'nds.showcase.title': 'The components, one by one.', + 'nds.showcase.hint': 'Scroll to browse the components', + 'nds.showcase.tokens': 'Tokens in use', + + 'about.title': 'About me', + 'about.lead': + 'Full-stack developer, trained through a BUT MMI degree (web development track) and battle-tested at Legrand - from internship to apprenticeship, on the same business application in production.', + 'about.exp.eyebrow': 'Experience', + 'about.exp.title': 'Background', + 'about.values.eyebrow': 'Method', + 'about.values.title': 'How I work', + 'about.cv': 'Download my resume', + + 'contact.title': 'Contact', + 'contact.lead': + 'A permanent role, a freelance mission or just a question: write to me, I reply fast.', + 'contact.email.label': 'E-mail', + 'contact.email.copy': 'Copy address', + 'contact.email.copied': 'Address copied!', + 'contact.email.write': 'Write an e-mail', + 'contact.form.name': 'Your name', + 'contact.form.email': 'Your e-mail', + 'contact.form.subject': 'Subject', + 'contact.form.subject.cdi': 'Permanent position (CDI)', + 'contact.form.subject.freelance': 'Freelance mission', + 'contact.form.subject.other': 'Other', + 'contact.form.message': 'Your message', + 'contact.form.send': 'Send message', + 'contact.form.note': + 'The button opens your mail client with the message pre-filled - no data is stored on this site.', + 'contact.elsewhere': 'Find me elsewhere', + + 'footer.role': 'Full-stack developer · Limoges · Nouvelle-Aquitaine, France', + 'footer.madeWith': 'Built with Astro + Nova Design System', + + 'voyage.hint': 'Scroll - the journey begins', + 'voyage.progress': 'Journey progress', + 'home.projects.intro.title': 'Four projects, run like products.', + 'home.projects.intro.body': + 'Design, code, tests, CI/CD, production: every project ships for real. Off we go.', + 'home.about.summary': + 'BUT MMI degree (web dev) at IUT du Limousin, and two years at Legrand on the same business application - from internship to apprenticeship.', + + 'docs.eyebrow': 'Documentation', + 'docs.title': 'Under the hood.', + 'docs.lead': + 'How this site animates scrolling, explained with demos running before your eyes and the real code from the repo. Read it all and you can rebuild it.', + 'docs.toc': 'Contents', + 'docs.file': 'File', + 'docs.why': 'Why it works', + 'docs.live': 'Demo - running for real', + 'docs.tryIt': 'Scroll and watch', + 'docs.trap': 'Gotcha', + 'docs.reading': '~20 min read, scroll wheel required', + + 'scene.hint': 'Keep scrolling - the scene is driven by scroll', + 'scene.infra.eyebrow': 'Under the hood', + 'scene.infra.title': 'The journey of a request.', + 'scene.blueprint.eyebrow': 'Blueprint', + 'scene.blueprint.title': 'From wireframe to product.', + 'scene.sorter.eyebrow': 'Live demo', + 'scene.sorter.title': 'From chaos to order.', + + 'a11y.langSwitch': 'Switch language', + 'notfound.title': 'Page not found', + 'notfound.body': "This page doesn't exist (anymore). The rest of the site works just fine.", + 'notfound.back': 'Back to home', +}; + +export type UIKey = keyof typeof fr; +export const ui = { fr, en } as const; diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 6358a27..b36ed8d 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -6,122 +6,71 @@ import SpaceBackground from '../components/SpaceBackground.astro'; import SiteNav from '../components/SiteNav.astro'; import SiteFooter from '../components/SiteFooter.astro'; import { site } from '../data/site'; +import { personLd, webSiteLd } from '../lib/seo'; +import { + getLocaleFromUrl, + getAlternates, + useTranslations, + HTML_LANGS, + OG_LOCALES, + LOCALES, +} from '../i18n'; export interface Props { - title?: string; - description?: string; - /** Page-specific keywords (appended to the global keywords list). */ - keywords?: string[]; + title: string; + description: string; + /** Extra JSON-LD blocks for this page (breadcrumbs, project, profile...). */ + jsonLd?: object[]; + /** og:type - "website" for index pages, "article" for project pages. */ + ogType?: 'website' | 'article'; + /** Ambient space background canvas (off on pages that mount their own scene). */ + background?: boolean; + /** Full-bleed main (no max-width shell) for cinematic pages. */ + fullBleed?: boolean; } -const { - title = 'Louis Potevin - Full-stack Developer (front, back, web)', - description = "Louis Potevin, full-stack web developer: front-end, back-end, TypeScript, Astro, Node. Available for a permanent contract (CDI) starting September 2026, anywhere in France or remote.", - keywords = [], -} = Astro.props; +const SITE_URL = 'https://louis-potevin.dev'; +const { title, description, jsonLd = [], ogType = 'website', background = true, fullBleed = false } = Astro.props; -const baseKeywords = [ - // French Keywords - 'Louis Potevin', - 'développeur web', - 'developpeur web', - 'dev web', - 'développeur full-stack', - 'developpeur full stack', - 'dev front', - 'développeur front-end', - 'dev back', - 'développeur back-end', - 'développeur TypeScript', - 'développeur Astro', - 'développeur JavaScript', - 'développeur Node.js', - 'développeur freelance France', - 'portfolio développeur', - 'web developer', - 'full-stack developer', - 'full stack developer', - 'front-end developer', - 'front end dev', - 'back-end developer', - 'back end dev', - 'TypeScript developer', - 'Astro developer', - 'JavaScript developer', - 'Node.js developer', - 'software engineer France', - 'developer portfolio', -]; -const allKeywords = [...baseKeywords, ...keywords].join(', '); +const locale = getLocaleFromUrl(Astro.url); +const t = useTranslations(locale); +const canonical = new URL(Astro.url.pathname, Astro.site ?? SITE_URL).href; +const ogImage = new URL(`/og-${locale}.png`, Astro.site ?? SITE_URL).href; +const alternates = getAlternates(Astro.url, (Astro.site ?? new URL(SITE_URL)).href); +const otherLocales = LOCALES.filter((l) => l !== locale); -const canonical = new URL(Astro.url.pathname, Astro.site ?? 'https://louis-potevin.dev').href; -const ogImage = new URL('/og.png', Astro.site ?? 'https://louis-potevin.dev').href; - -// Structured data: Person profile (helps Google link your name ↔ profession ↔ social links). -const personLd = { - '@context': 'https://schema.org', - '@type': 'Person', - name: site.name, - jobTitle: 'Full-stack Developer / Développeur full-stack', - description, - url: canonical, - email: `mailto:${site.email}`, - address: { '@type': 'PostalAddress', addressCountry: 'FR', addressLocality: 'Limoges' }, - knowsAbout: [ - 'Web development', - 'Développement web', - 'Front-end', - 'Back-end', - 'TypeScript', - 'JavaScript', - 'Astro', - 'Node.js', - 'Sass', - 'Docker', - 'CI/CD', - ], - sameAs: [ - site.links.github.url, - site.links.gitea.url, - site.links.npm.url, - site.links.linkedin.url, - ], -}; - -const siteLd = { - '@context': 'https://schema.org', - '@type': 'WebSite', - name: `${site.name} - Portfolio`, - url: Astro.site?.href ?? 'https://louis-potevin.dev', - inLanguage: ['en-US', 'fr-FR'], -}; +const ldBlocks = [personLd(locale), webSiteLd(), ...jsonLd]; --- - + + + + {title} - + {alternates.map((a) => )} - + - - + + {otherLocales.map((l) => )} + @@ -130,23 +79,29 @@ const siteLd = { - + - \ No newline at end of file + diff --git a/src/lib/scrub.ts b/src/lib/scrub.ts new file mode 100644 index 0000000..cc5bb2f --- /dev/null +++ b/src/lib/scrub.ts @@ -0,0 +1,34 @@ +/** + * Scroll-scrub helper for pinned scenes. + * + * A scene = a tall section with a sticky 100vh stage inside. This maps the + * section's traversal to a 0..1 progress each frame (cheap: only while the + * section is near the viewport). Works natively with Lenis since Lenis drives + * real window scrolling. + */ +export function scrub(section: HTMLElement, cb: (p: number) => void): () => void { + let raf = 0; + let lastP = -1; + const loop = () => { + const rect = section.getBoundingClientRect(); + const vh = window.innerHeight; + if (rect.bottom > -vh && rect.top < vh * 2) { + const total = rect.height - vh; + const p = total > 0 ? Math.min(1, Math.max(0, -rect.top / total)) : 1; + if (p !== lastP) { + lastP = p; + cb(p); + } + } + raf = requestAnimationFrame(loop); + }; + raf = requestAnimationFrame(loop); + return () => cancelAnimationFrame(raf); +} + +export const clamp01 = (x: number) => Math.min(1, Math.max(0, x)); +/** Progress remapped to a [a,b] window with smoothstep easing. */ +export const window01 = (p: number, a: number, b: number) => { + const t = clamp01((p - a) / (b - a)); + return t * t * (3 - 2 * t); +}; diff --git a/src/lib/seo.ts b/src/lib/seo.ts new file mode 100644 index 0000000..b1d9be0 --- /dev/null +++ b/src/lib/seo.ts @@ -0,0 +1,97 @@ +/** + * JSON-LD builders. Each returns a plain object serialized by the layout. + * Kept framework-agnostic so it can be unit-tested without Astro. + */ +import { site } from '../data/site'; +import type { Project } from '../data/projects'; +import type { Locale } from '../i18n'; + +const SITE_URL = 'https://louis-potevin.dev'; + +const PERSON_ID = `${SITE_URL}/#person`; + +export function personLd(locale: Locale) { + return { + '@context': 'https://schema.org', + '@type': 'Person', + '@id': PERSON_ID, + name: site.name, + jobTitle: locale === 'fr' ? 'Développeur full-stack' : 'Full-stack Developer', + url: SITE_URL, + email: `mailto:${site.email}`, + address: { + '@type': 'PostalAddress', + addressLocality: site.location.city, + addressRegion: site.location.region, + addressCountry: site.location.country, + }, + knowsAbout: [ + 'Web development', + 'TypeScript', + 'JavaScript', + 'Astro', + 'Vue.js', + 'Node.js', + 'Sass', + 'PostgreSQL', + 'Docker', + 'CI/CD', + 'Rust', + ], + knowsLanguage: ['fr', 'en'], + sameAs: Object.values(site.links).map((l) => l.url), + }; +} + +export function webSiteLd() { + return { + '@context': 'https://schema.org', + '@type': 'WebSite', + '@id': `${SITE_URL}/#website`, + name: `${site.name} - Portfolio`, + url: SITE_URL, + inLanguage: ['fr-FR', 'en-US'], + publisher: { '@id': PERSON_ID }, + }; +} + +export interface Crumb { + name: string; + url: string; +} + +export function breadcrumbLd(crumbs: Crumb[]) { + return { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: crumbs.map((c, i) => ({ + '@type': 'ListItem', + position: i + 1, + name: c.name, + item: new URL(c.url, SITE_URL).href, + })), + }; +} + +export function projectLd(project: Project, locale: Locale, pageUrl: string) { + return { + '@context': 'https://schema.org', + '@type': 'SoftwareSourceCode', + name: project.name, + description: project.summary[locale], + url: pageUrl, + codeRepository: project.links[0]?.url, + programmingLanguage: project.stack.map((s) => s.name), + author: { '@id': PERSON_ID }, + inLanguage: locale === 'fr' ? 'fr-FR' : 'en-US', + }; +} + +export function profilePageLd(pageUrl: string) { + return { + '@context': 'https://schema.org', + '@type': 'ProfilePage', + url: pageUrl, + mainEntity: { '@id': PERSON_ID }, + }; +} diff --git a/src/lib/smooth.ts b/src/lib/smooth.ts new file mode 100644 index 0000000..e613c55 --- /dev/null +++ b/src/lib/smooth.ts @@ -0,0 +1,46 @@ +/** + * Smooth scroll (Lenis), shared as a singleton. + * + * Everything animated on the site reads scroll from Lenis so motion stays + * on a single easing curve - that is what makes scrolling feel like one + * continuous animation instead of a page moving. + */ +import Lenis from 'lenis'; + +let lenis: Lenis | null = null; +let rafId = 0; + +export function prefersReducedMotion(): boolean { + return window.matchMedia('(prefers-reduced-motion: reduce)').matches; +} + +/** Idempotent. Returns null when the user prefers reduced motion. */ +export function ensureSmooth(): Lenis | null { + if (prefersReducedMotion()) return null; + if (lenis) return lenis; + + lenis = new Lenis({ + lerp: 0.09, + wheelMultiplier: 0.95, + // Touch keeps native scrolling; the scenes still read positions each frame. + }); + + const raf = (time: number) => { + lenis!.raf(time); + rafId = requestAnimationFrame(raf); + }; + rafId = requestAnimationFrame(raf); + + return lenis; +} + +export function getSmooth(): Lenis | null { + return lenis; +} + +export function destroySmooth(): void { + if (rafId) cancelAnimationFrame(rafId); + lenis?.destroy(); + lenis = null; + rafId = 0; +} diff --git a/src/pages/404.astro b/src/pages/404.astro new file mode 100644 index 0000000..264e64c --- /dev/null +++ b/src/pages/404.astro @@ -0,0 +1,49 @@ +--- +import BaseLayout from '../layouts/BaseLayout.astro'; +import { Button } from '@unkn0wndo3s/nova-design-system'; +import { getLocaleFromUrl, useTranslations, localizePath } from '../i18n'; + +const locale = getLocaleFromUrl(Astro.url); +const t = useTranslations(locale); +--- + + +
+ +

{t('notfound.title')}

+

{t('notfound.body')}

+ +
+
+ + diff --git a/src/pages/about.astro b/src/pages/about.astro index 33e428c..2385ece 100644 --- a/src/pages/about.astro +++ b/src/pages/about.astro @@ -1,315 +1,5 @@ --- -import BaseLayout from '../layouts/BaseLayout.astro'; -import Eyebrow from '../components/Eyebrow.astro'; -import TechIcon from '../components/TechIcon.astro'; -import BrandLinkedin from '../components/BrandLinkedin.astro'; -import { - Breadcrumb, - BreadcrumbItem, - Badge, - Button, - Link, - ListItem, - ListItemTitle, - ListItemSubtitle, -} from '@unkn0wndo3s/nova-design-system'; -import { Send, Mail } from '@lucide/astro'; -import { site } from '../data/site'; - -const title = 'About - Louis Potevin, full-stack web developer / développeur full-stack'; -const description = - "Louis Potevin, full-stack web developer with a BUT MMI degree (DWDI track). 18 months of professional experience at Legrand France. Available for a permanent contract (CDI), anywhere in France or remote."; - -const parcours = [ - { - year: '2024 - 2026', - title: 'Professional Experience · Legrand France', - body: "18 months total: 12 weeks of internship, 1 month on a fixed-term contract (CDD), followed by 1 year of work-study apprenticeship. Web development on a production-ready internal corporate application.", - }, - { - year: '2024 - 2026', - title: 'BUT MMI - DWDI Track · IUT du Limousin', - body: "Multimedia and Internet Job Degrees (Métiers du Multimédia et de l'Internet), specializing in Web Development and Interactive Devices (DWDI). Degree completed in 2026.", - }, - { - year: '2026 - ongoing', - title: 'Personal Projects', - body: "Designing an open-source design system published on npm, managing a self-hosted server infrastructure, and building system tools with Rust and Python.", - }, -]; - -const stack = [ - { icon: 'astro', label: 'Astro' }, - { icon: 'typescript', label: 'TypeScript' }, - { icon: 'javascript', label: 'JavaScript' }, - { icon: 'sass', label: 'Sass' }, - { icon: 'postgresql', label: 'PostgreSQL' }, - { icon: 'docker', label: 'Docker' }, - { icon: 'linux', label: 'Linux' }, - { icon: 'cloudflare', label: 'Cloudflare' }, - { icon: 'gitea', label: 'Gitea' }, - { icon: 'rust', label: 'Rust' }, - { icon: 'python', label: 'Python' }, - { icon: 'git', label: 'Git' }, -]; +import AboutView from '../views/AboutView.astro'; --- - -
- - Home - About - - -

About me

-

- Full-stack developer based in {site.location}. I design complete web applications, - from front-end interfaces to back-end architecture, and manage their deployment into production. -

-
- -
-
-
- -

- I graduated with a BUT MMI degree, specializing in Web Development and Interactive - Devices (DWDI) at the IUT du Limousin. I solidified my practical engineering skills - in the industry: spending 18 months at Legrand France split between a 12-week internship, - a 1-month contract, and a full year as a software apprentice, working on a business-critical - production application. -

-

- Joining Legrand as an apprentice without knowing anyone prior, I quickly integrated - into the team and built strong working relationships across departments. I am equally - comfortable collaborating in agile team structures as I am working independently, and I excel - at adapting swiftly to new technical environments. -

-

- Outside of my professional employment, I build and maintain my own digital infrastructure: - a modular design system published on npm, a private self-hosted suite of services, and custom - automation tools written in Rust and Python. This is my preferred way of keeping up with industry - standards and thoroughly exploring engineering topics end-to-end. -

-
- -
- -
    - { - parcours.map((p) => ( -
  1. - {p.year} -
    - - {p.title} - {p.body} - -
    -
  2. - )) - } -
-
- -
- -
    - { - stack.map((s) => ( -
  • - - {s.label} -
  • - )) - } -
-
-
- - -
-
- - \ No newline at end of file + diff --git a/src/pages/contact.astro b/src/pages/contact.astro index 4da1340..631033e 100644 --- a/src/pages/contact.astro +++ b/src/pages/contact.astro @@ -1,297 +1,5 @@ --- -import BaseLayout from '../layouts/BaseLayout.astro'; -import Eyebrow from '../components/Eyebrow.astro'; -import TechIcon from '../components/TechIcon.astro'; -import { - Breadcrumb, - BreadcrumbItem, - TextField, - Select, - SelectOption, - Button, - Badge, -} from '@unkn0wndo3s/nova-design-system'; -import { Send, Mail } from '@lucide/astro'; -import BrandLinkedin from '../components/BrandLinkedin.astro'; -import { site } from '../data/site'; - -const title = 'Contact - Louis Potevin'; -const description = - 'Contact Louis Potevin, full-stack web developer. Available for permanent positions (CDI) starting September 2026, anywhere in France or remote.'; - -const directLinks = [ - { kind: 'mail', label: site.email, url: `mailto:${site.email}` }, - { kind: 'linkedin', label: 'LinkedIn', url: site.links.linkedin.url }, - { kind: 'gitea', label: 'Gitea', url: site.links.gitea.url }, - { kind: 'github', label: 'GitHub', url: site.links.github.url }, -]; +import ContactView from '../views/ContactView.astro'; --- - -
- - Home - Contact - - -

Get in touch

-

- An opening, a project, or just a question? Email is the most direct - channel, but the form works just as well. -

-
- Available - {site.availability} · {site.mobility} -
-
- -
- - - - - \ No newline at end of file + diff --git a/src/pages/docs.astro b/src/pages/docs.astro new file mode 100644 index 0000000..6b5d663 --- /dev/null +++ b/src/pages/docs.astro @@ -0,0 +1,5 @@ +--- +import DocsView from '../views/DocsView.astro'; +--- + + diff --git a/src/pages/en/about.astro b/src/pages/en/about.astro new file mode 100644 index 0000000..59256b3 --- /dev/null +++ b/src/pages/en/about.astro @@ -0,0 +1,5 @@ +--- +import AboutView from '../../views/AboutView.astro'; +--- + + diff --git a/src/pages/en/contact.astro b/src/pages/en/contact.astro new file mode 100644 index 0000000..a22243c --- /dev/null +++ b/src/pages/en/contact.astro @@ -0,0 +1,5 @@ +--- +import ContactView from '../../views/ContactView.astro'; +--- + + diff --git a/src/pages/en/docs.astro b/src/pages/en/docs.astro new file mode 100644 index 0000000..d170c14 --- /dev/null +++ b/src/pages/en/docs.astro @@ -0,0 +1,5 @@ +--- +import DocsView from '../../views/DocsView.astro'; +--- + + diff --git a/src/pages/en/index.astro b/src/pages/en/index.astro new file mode 100644 index 0000000..88e4073 --- /dev/null +++ b/src/pages/en/index.astro @@ -0,0 +1,5 @@ +--- +import HomeView from '../../views/HomeView.astro'; +--- + + diff --git a/src/pages/en/work/[slug].astro b/src/pages/en/work/[slug].astro new file mode 100644 index 0000000..8aa27b7 --- /dev/null +++ b/src/pages/en/work/[slug].astro @@ -0,0 +1,12 @@ +--- +import ProjectView from '../../../views/ProjectView.astro'; +import { projects } from '../../../data/projects'; + +export function getStaticPaths() { + return projects.map((project) => ({ params: { slug: project.slug }, props: { project } })); +} + +const { project } = Astro.props; +--- + + diff --git a/src/pages/en/work/index.astro b/src/pages/en/work/index.astro new file mode 100644 index 0000000..8f20d9c --- /dev/null +++ b/src/pages/en/work/index.astro @@ -0,0 +1,5 @@ +--- +import WorkIndexView from '../../../views/WorkIndexView.astro'; +--- + + diff --git a/src/pages/index.astro b/src/pages/index.astro index ce00f41..062f13f 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,421 +1,5 @@ --- -import { Button, Badge } from '@unkn0wndo3s/nova-design-system'; -import { ArrowUpRight, Send } from '@lucide/astro'; - -import BaseLayout from '../layouts/BaseLayout.astro'; -import Eyebrow from '../components/Eyebrow.astro'; -import ProjectCard from '../components/ProjectCard.astro'; -import TechIcon from '../components/TechIcon.astro'; - -import { site } from '../data/site'; -import { featuredProjects } from '../data/projects'; - -const capabilities = [ - { - label: 'Front-end', - body: 'Sleek and accessible interfaces built with Astro, TypeScript, and Sass - all the way to an in-house design system.', - }, - { - label: 'Back-end', - body: 'Server-side services and APIs, data modeling, and business logic using Node and PostgreSQL.', - }, - { - label: 'Tools & deployment', - body: 'Docker containerization, automated CI/CD, and end-to-end production deployment.', - }, -]; - -const instruments = [ - { icon: 'astro', label: 'Astro' }, - { icon: 'vue', label: 'Vue.js'}, - { icon: 'typescript', label: 'TypeScript' }, - { icon: 'javascript', label: 'JavaScript' }, - { icon: 'sass', label: 'Sass' }, - { icon: 'postgresql', label: 'PostgreSQL' }, - { icon: 'docker', label: 'Docker' }, - { icon: 'linux', label: 'Linux' }, - { icon: 'cloudflare', label: 'Cloudflare' }, - { icon: 'gitea', label: 'Gitea CI/CD' }, - { icon: 'rust', label: 'Rust' }, - { icon: 'python', label: 'Python' }, - { icon: 'git', label: 'Git' }, - { icon: 'nodedotjs', label: 'Node.js' }, - { icon: 'figma', label: 'Figma' }, -]; +import HomeView from '../views/HomeView.astro'; --- - - {/* HERO */} -
-
- - Available · {site.availability} -
- -

- Louis Potevin - Full-stack developer -

- -

- I design and develop web applications from end to end - from front-end - to back-end and production deployment. I am equally comfortable working - on the user interface as I am with the services and infrastructure running them. -

- -
- - -
- -
- Available - {site.availability} - - {site.mobility} -
-
- - {/* PROFILE / CAPABILITIES */} -
- -
-

- A full-stack profile, comfortable across the entire stack. -

-
-

- I have hands-on experience running business applications in production while - simultaneously developing my own side projects: a design system published - on npm, a self-hosted infrastructure, and tools built with Rust and Python. -

-

- What drives me: mastering the entire cycle - the component you look at, - the service that delivers it, and the CI that deploys it. This website is - a prime example, built entirely on top of my own design system. -

-
-
- -
    - { - capabilities.map((c) => ( -
  • - {c.label} -

    {c.body}

    -
  • - )) - } -
-
- - {/* FEATURED PROJECTS */} -
- -
-

Projects

- - All projects - -
-
- {featuredProjects.map((p) => )} -
-
- - {/* STACK */} -
- -

- My everyday tools. -

-
    - { - instruments.map((t) => ( -
  • - - - - {t.label} -
  • - )) - } -
-
- - {/* CONTACT CTA */} -
-

Available - {site.availability}

-

Let's talk!

-

- I am looking for a full-stack permanent position (CDI) starting September 2026. - {' '}{site.mobility}. -

-
- - -
-
-
- - \ No newline at end of file + diff --git a/src/pages/work/[slug].astro b/src/pages/work/[slug].astro index 56cce18..8fd5f49 100644 --- a/src/pages/work/[slug].astro +++ b/src/pages/work/[slug].astro @@ -1,305 +1,12 @@ --- -import BaseLayout from '../../layouts/BaseLayout.astro'; -import Eyebrow from '../../components/Eyebrow.astro'; -import TechIcon from '../../components/TechIcon.astro'; -import { - Breadcrumb, - BreadcrumbItem, - Badge, - Button, - Link, -} from '@unkn0wndo3s/nova-design-system'; -import { ArrowUpRight, ArrowLeft } from '@lucide/astro'; -import { projects, statusMeta, type Project } from '../../data/projects'; +import ProjectView from '../../views/ProjectView.astro'; +import { projects } from '../../data/projects'; export function getStaticPaths() { - return projects.map((project) => ({ - params: { slug: project.slug }, - props: { project }, - })); + return projects.map((project) => ({ params: { slug: project.slug }, props: { project } })); } -interface Props { - project: Project; -} const { project } = Astro.props; -const status = statusMeta[project.status]; -const title = `${project.title} - Louis Potevin`; --- - -
-
- - Home - Projects - - {project.designation} - - - -
- {project.designation} - {status.label} -
- -

{project.title}

-

{project.tagline}

- -
-
-
Role
-
{project.role}
-
-
-
Timeline
-
{project.period}
-
-
-
- -
-
-

{project.summary}

- -
- -
    - {project.context.map((c) =>
  • {c}
  • )} -
-
- -
- -
    - {project.contributions.map((c) =>
  • {c}
  • )} -
-
-
- - -
- -
- - -
-
-
- - \ No newline at end of file + diff --git a/src/pages/work/index.astro b/src/pages/work/index.astro index 5d04acf..e95f213 100644 --- a/src/pages/work/index.astro +++ b/src/pages/work/index.astro @@ -1,62 +1,5 @@ --- -import BaseLayout from '../../layouts/BaseLayout.astro'; -import Eyebrow from '../../components/Eyebrow.astro'; -import ProjectCard from '../../components/ProjectCard.astro'; -import { Breadcrumb, BreadcrumbItem } from '@unkn0wndo3s/nova-design-system'; -import { projects } from '../../data/projects'; - -const title = 'Projects - Louis Potevin, full-stack web developer / développeur full-stack'; -const description = - 'Projects by Louis Potevin: Astro design system, self-hosted infrastructure, systems tools in Rust, and LLM explorations. Full-stack web developer.'; +import WorkIndexView from '../../views/WorkIndexView.astro'; --- - -
- - Home - Projects - - - -

Projects

-

- A showcase of everything I build - from business software running in production to - the infrastructure supporting it, alongside my personal tools. -

-
- -
- {projects.map((p) => )} -
-
- - \ No newline at end of file + diff --git a/src/styles/_type.scss b/src/styles/_type.scss index 643b86c..3577835 100644 --- a/src/styles/_type.scss +++ b/src/styles/_type.scss @@ -1,5 +1,3 @@ -// Re-exports the official typography mixins from Nova DS. -// The partial is resolved via a loadPath pointing to the package's -// style folder (its `exports` field prevents direct access to the subpath). -// Usage in a component: @use "styles/type" as *; then @include text-3xl; -@forward "tokens/typography"; +// Re-exports NDS typography mixins (text-sm ... text-5xl) + font vars. +// The tokens directory is exposed through a Sass loadPath (see astro.config.mjs). +@forward 'typography'; diff --git a/src/styles/global.scss b/src/styles/global.scss index b471d99..06f4d5b 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -1,44 +1,73 @@ -@use 'styles/type' as *; +@use './type' as type; html { scroll-behavior: smooth; -} - -@media (prefers-reduced-motion: reduce) { - html { - scroll-behavior: auto; - } + background: var(--nds-background); } body { margin: 0; - min-height: 100vh; + min-height: 100dvh; color: var(--nds-text); font-family: var(--nds-font-sans); -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; - background-color: var(--nds-background); + overflow-x: clip; } ::selection { - background: color-mix(in srgb, var(--nds-primary) 35%, transparent); - color: var(--nds-text); + background: var(--nds-primary); + color: var(--nds-on-primary); } -* { - scrollbar-width: thin; - scrollbar-color: var(--nds-border-strong) transparent; +h1, h2, h3 { + font-family: var(--nds-font-mono); + text-wrap: balance; + margin: 0; } -a { - color: inherit; -} +p { margin: 0; } -img { - max-width: 100%; - display: block; -} +img, svg, canvas { max-width: 100%; height: auto; } :focus-visible { - outline: none; + outline: 2px solid var(--nds-primary); + outline-offset: 2px; +} + +.section { + padding-block: clamp(64px, 10vh, 128px); +} + +.eyebrow-row { + display: flex; + align-items: baseline; + gap: var(--nds-spacing-md); + margin-bottom: var(--nds-spacing-lg); +} + +.section-title { + @include type.text-4xl; + font-size: clamp(1.6rem, 3.4vw, 2.4rem); +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* ---- Lenis smooth scroll (recommended base styles) ---- */ +html.lenis, html.lenis body { height: auto; } +.lenis.lenis-smooth { scroll-behavior: auto !important; } +.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; } +.lenis.lenis-stopped { overflow: hidden; } + +/* ---- Cinema mode (home voyage): the footer slides over the last overlay ---- */ +html.cinema footer { + position: relative; + z-index: 30; + background: var(--nds-background); } diff --git a/src/views/AboutView.astro b/src/views/AboutView.astro new file mode 100644 index 0000000..032e5c1 --- /dev/null +++ b/src/views/AboutView.astro @@ -0,0 +1,215 @@ +--- +import BaseLayout from '../layouts/BaseLayout.astro'; +import Eyebrow from '../components/Eyebrow.astro'; +import { Badge, Button } from '@unkn0wndo3s/nova-design-system'; +import { breadcrumbLd, profilePageLd } from '../lib/seo'; +import { getLocaleFromUrl, useTranslations, localizePath } from '../i18n'; +import type { Localized } from '../data/projects'; + +const locale = getLocaleFromUrl(Astro.url); +const t = useTranslations(locale); + +interface Step { + period: Localized; + role: Localized; + place: string; + detail: Localized; +} + +const steps: Step[] = [ + { + period: { fr: 'Sept. 2025 - Août 2026', en: 'Sept. 2025 - Aug. 2026' }, + role: { fr: 'Développeur full-stack en alternance', en: 'Full-stack developer (apprenticeship)' }, + place: 'Legrand France, Limoges', + detail: { + fr: "Développement du DAN, application web interne pour les ateliers de production : refonte du module de gestion des utilisateurs (Vue.js/TypeScript, export CSV) et du module de statistiques avec composants graphiques sur mesure. Back-end Node.js/Express.", + en: 'Development of DAN, an internal web app for production workshops: rework of the user management module (Vue.js/TypeScript, CSV export) and of the statistics module with custom chart components. Node.js/Express back-end.', + }, + }, + { + period: { fr: 'Avril - Août 2025', en: 'April - Aug. 2025' }, + role: { fr: "Stage puis job d'été", en: 'Internship, then summer job' }, + place: 'Legrand France, Limoges', + detail: { + fr: "Premiers travaux sur le DAN : montée en compétence sur la base de code, premières fonctionnalités livrées en production. L'expérience s'est prolongée en alternance sur la même application.", + en: 'First work on DAN: ramping up on the codebase, first features shipped to production. The experience continued as an apprenticeship on the same application.', + }, + }, + { + period: { fr: '2023 - 2026', en: '2023 - 2026' }, + role: { fr: 'BUT MMI, parcours Développement Web (DWDI)', en: 'BUT MMI degree, Web Development track (DWDI)' }, + place: 'IUT du Limousin', + detail: { + fr: "Formation en développement web et dispositifs interactifs : front-end, back-end, UX, gestion de projet. Diplôme obtenu en 2026.", + en: 'Training in web development and interactive systems: front-end, back-end, UX, project management. Graduating in 2026.', + }, + }, +]; + +interface Value { + title: Localized; + body: Localized; +} + +const values: Value[] = [ + { + title: { fr: 'Livrer, pas prototyper', en: 'Ship, not prototype' }, + body: { + fr: 'Un projet est terminé quand il tourne en production avec sa CI. Mon design system est sur npm, mon infra héberge ce site : tout ce que je montre est réellement en service.', + en: 'A project is done when it runs in production with its CI. My design system is on npm, my infrastructure hosts this site: everything I show is actually in service.', + }, + }, + { + title: { fr: 'Du composant au serveur', en: 'From component to server' }, + body: { + fr: "Je conçois l'interface, l'API qui la sert et la machine qui l'héberge. Cette vision complète évite les frictions entre front, back et ops.", + en: 'I design the interface, the API that serves it and the machine that hosts it. That end-to-end view removes friction between front, back and ops.', + }, + }, + { + title: { fr: 'Réutilisable par défaut', en: 'Reusable by default' }, + body: { + fr: "Tokens, composants, pipelines : je factorise ce qui se répète. C'est ce qui m'a mené à construire Nova Design System plutôt que de copier-coller des styles.", + en: 'Tokens, components, pipelines: I factor out what repeats. That mindset is what led me to build Nova Design System instead of copy-pasting styles.', + }, + }, +]; + +const crumbs = [ + { name: t('nav.home'), url: localizePath('/', locale) }, + { name: t('nav.about'), url: localizePath('/about', locale) }, +]; +const pageUrl = new URL(Astro.url.pathname, Astro.site ?? 'https://louis-potevin.dev').href; +const jsonLd = [breadcrumbLd(crumbs), profilePageLd(pageUrl)]; +--- + + +
+

{t('about.title')}

+

{t('about.lead')}

+

+ {t('avail.badge')} + {t('avail.line')} · {t('avail.where')} +

+
+ +
+
{t('about.exp.eyebrow')}
+

{t('about.exp.title')}

+
    + {steps.map((step) => ( +
  1. +

    {step.period[locale]}

    +

    {step.role[locale]}

    +

    {step.place}

    +

    {step.detail[locale]}

    +
  2. + ))} +
+
+ +
+
{t('about.values.eyebrow')}
+

{t('about.values.title')}

+
    + {values.map((value) => ( +
  • +

    {value.title[locale]}

    +

    {value.body[locale]}

    +
  • + ))} +
+
+ +
+
+
+ + diff --git a/src/views/ContactView.astro b/src/views/ContactView.astro new file mode 100644 index 0000000..a3dee55 --- /dev/null +++ b/src/views/ContactView.astro @@ -0,0 +1,350 @@ +--- +/** + * Contact page - static-friendly by design. + * + * The "form" is a mailto composer: it pre-fills the visitor's mail client and + * stores nothing server-side. To switch to a real backend later, point the + * submit handler at a Formspree/own endpoint and keep the same fields. + */ +import BaseLayout from '../layouts/BaseLayout.astro'; +import Eyebrow from '../components/Eyebrow.astro'; +import BrandLinkedin from '../components/BrandLinkedin.astro'; +import { Mail, Copy, Send } from '@lucide/astro'; +import { Github, Gitea, Npm } from 'simple-icons-astro'; +import { + Breadcrumb, + BreadcrumbItem, + Button, + TextField, + Select, + SelectOption, +} from '@unkn0wndo3s/nova-design-system'; +import { site } from '../data/site'; +import { breadcrumbLd } from '../lib/seo'; +import { getLocaleFromUrl, useTranslations, localizePath } from '../i18n'; + +const locale = getLocaleFromUrl(Astro.url); +const t = useTranslations(locale); + +const crumbs = [ + { name: t('nav.home'), url: localizePath('/', locale) }, + { name: t('nav.contact'), url: localizePath('/contact', locale) }, +]; + +const subjects = [ + { value: 'cdi', label: t('contact.form.subject.cdi') }, + { value: 'freelance', label: t('contact.form.subject.freelance') }, + { value: 'other', label: t('contact.form.subject.other') }, +]; + +const elsewhere = [ + { ...site.links.github, icon: 'github' }, + { ...site.links.gitea, icon: 'gitea' }, + { ...site.links.npm, icon: 'npm' }, + { ...site.links.linkedin, icon: 'linkedin' }, +]; +--- + + + + +
+ {t('avail.badge')} +

{t('contact.title')}

+

{t('contact.lead')}

+
+ +
+
+

{t('contact.form.send')}

+ +
+
+ + +
+ +
+ {t('contact.form.subject')} + +
+ + + +
+ +

{t('contact.form.note')}

+
+
+
+ + +
+
+ + + + diff --git a/src/views/DocsView.astro b/src/views/DocsView.astro new file mode 100644 index 0000000..20ed127 --- /dev/null +++ b/src/views/DocsView.astro @@ -0,0 +1,1277 @@ +--- +/** + * /docs - "Sous le capot", version animée. + * + * The doc eats its own dog food: every concept ships with a live demo built + * on the exact mechanism it explains (scrub for the pinned ones, Lenis + * velocity for the physics one). Snippets are still pulled from the real + * source files at build time (`?raw` + markers), so they can't go stale. + * + * A center-band IntersectionObserver keeps ONE chapter in focus while the + * others dim - big scroll margins between chapters make the focus obvious. + */ +import BaseLayout from '../layouts/BaseLayout.astro'; +import Eyebrow from '../components/Eyebrow.astro'; +import { Badge } from '@unkn0wndo3s/nova-design-system'; +import { Code } from 'astro:components'; +import { breadcrumbLd } from '../lib/seo'; +import { getLocaleFromUrl, useTranslations, localizePath } from '../i18n'; + +import configRaw from '../../astro.config.mjs?raw'; +import smoothRaw from '../lib/smooth.ts?raw'; +import scrubRaw from '../lib/scrub.ts?raw'; +import voyageRaw from '../components/scene/voyage.ts?raw'; +import homeRaw from '../views/HomeView.astro?raw'; +import infraRaw from '../components/scenes/InfraScene.astro?raw'; +import sorterRaw from '../components/scenes/SorterScene.astro?raw'; +import enHomeRaw from '../pages/en/index.astro?raw'; + +const locale = getLocaleFromUrl(Astro.url); +const t = useTranslations(locale); +const L = (fr: string, en: string) => (locale === 'fr' ? fr : en); + +/** Slice a source between two unique markers - fails the build if missing. */ +function slice(src: string, from: string, to: string, label: string): string { + const a = src.indexOf(from); + if (a === -1) throw new Error(`docs: marker "${from}" not found (${label})`); + const b = src.indexOf(to, a + from.length); + if (b === -1) throw new Error(`docs: marker "${to}" not found (${label})`); + return src.slice(a, b).replace(/\s+$/, ''); +} + +const snips = { + smooth: smoothRaw.trimEnd(), + scrub: scrubRaw.trimEnd(), + segments: slice(voyageRaw, ' // --- Timeline from the DOM', ' // --- Scene ---', 'segments'), + camera: slice(voyageRaw, ' // Flight path:', ' // Starfield', 'camera path'), + camTick: slice(voyageRaw, ' // Camera along the path.', ' // Nebulae drift.', 'camera tick'), + overlay: slice(voyageRaw, ' const applyOverlay', ' // --- Frame loop', 'overlay'), + cinemaCss: slice(homeRaw, ' /* Cinema: fixed overlays', ' /* ------------------------------- Cards', 'cinema css'), + energy: slice(voyageRaw, ' // Scroll → timeline progress', ' // Camera along the path.', 'energy'), + collisions: slice(voyageRaw, ' // Collisions only while agitated', ' belt.instanceMatrix', 'collisions'), + seek: slice(voyageRaw, ' const seek = (id: string', ' dots.forEach', 'seek'), + gate: slice(voyageRaw, ' // --- Capability gate', ' // --- Timeline from the DOM', 'gate'), + infraDash: slice(infraRaw, ' .edge {', ' .node {', 'infra dash'), + sorterMeasure: slice(sorterRaw, ' const measure = () => {', ' let lastP = 0;', 'sorter measure'), + sorterRng: slice(sorterRaw, ' /** Deterministic PRNG', ' function init()', 'sorter rng'), + configI18n: slice(configRaw, ' i18n: {', ' integrations', 'config i18n'), + configVite: slice(configRaw, ' vite: {', '});', 'vite'), + enRoute: enHomeRaw.trimEnd(), +}; + +const chapters = [ + { id: 'une-valeur', num: '01', title: L('Une seule valeur pilote tout', 'One value drives everything') }, + { id: 'lenis', num: '02', title: L('Le lissage', 'The smoothing') }, + { id: 'epingler', num: '03', title: L('Épingler une scène', 'Pinning a scene') }, + { id: 'camera', num: '04', title: L('Une caméra sur des rails', 'A camera on rails') }, + { id: 'overlays', num: '05', title: L('Des sections qui ne défilent pas', 'Sections that never scroll') }, + { id: 'velocite', num: '06', title: L('Le scroll a une vitesse', 'Scroll has a velocity') }, + { id: 'dessiner', num: '07', title: L('Dessiner au scroll', 'Drawing with scroll') }, + { id: 'ranger', num: '08', title: L('Ranger des fichiers', 'Sorting files') }, + { id: 'reste', num: '09', title: L('Le reste, en bref', 'The rest, briefly') }, + { id: 'refaire', num: '10', title: L('Refaites-le', 'Rebuild it') }, +]; + +const crumbs = [ + { name: t('nav.home'), url: localizePath('/', locale) }, + { name: 'Docs', url: localizePath('/docs', locale) }, +]; +const THEME = 'github-dark-default'; + +// Mini sorter demo data (chapter 08) +const miniFiles = [ + { name: 'photo.jpg', cat: 'img' }, + { name: 'logo.png', cat: 'img' }, + { name: 'scan.webp', cat: 'img' }, + { name: 'main.rs', cat: 'code' }, + { name: 'lib.rs', cat: 'code' }, + { name: 'Cargo.toml', cat: 'code' }, +]; +--- + + +
+ {t('docs.eyebrow')} +

{t('docs.title')}

+

{t('docs.lead')}

+

+ Three.js + Lenis + Astro 7 + NDS v1.2.1 + {t('docs.reading')} +

+
+ +
+ + +
+ +
+
01

{chapters[0]!.title}

+

{L( + "Quand j'ai décidé de refaire ce portfolio, je suis parti d'une frustration : sur la plupart des sites, on sent le scroll. La page glisse, le contenu monte, et l'illusion se casse. Ce que je voulais, c'est que scroller ne fasse jamais défiler quoi que ce soit - juste avancer une animation.", + "When I decided to rebuild this portfolio, I started from a frustration: on most sites, you can feel the scroll. The page slides, the content moves up, and the illusion breaks. What I wanted was for scrolling to never scroll anything - just move an animation forward.", + )}

+

{L( + "La solution tient en une phrase : je convertis la position de scroll en un nombre entre 0 et 1, et ce nombre pilote absolument tout. La caméra 3D, l'apparition des sections, la barre de progression, la physique des astéroïdes. Une seule source de vérité, lue une fois par frame. Le voilà, ce nombre, en direct :", + "The whole solution fits in one sentence: I convert the scroll position into a number between 0 and 1, and that number drives absolutely everything. The 3D camera, the sections fading in, the progress bar, the asteroid physics. One source of truth, read once per frame. Here it is, live:", + )}

+ +
+
{t('docs.live')}{L('la progression de CETTE page', 'THIS page’s progress')}
+
+

0.000

+
+

{L('progress = scroll ÷ (hauteur totale − hauteur de fenêtre)', 'progress = scroll ÷ (total height − viewport height)')}

+
+
+ + +
+ + +
+
02

{chapters[1]!.title}

+

{L( + "Un scroll natif avance par crans - chaque coup de molette est un saut de ~100px. Si la caméra suit ça au pixel près, elle saute aussi, et adieu le cinéma. C'est là que Lenis entre en jeu : il intercepte la molette et fait glisser la vraie position de scroll vers la cible avec une interpolation.", + "Native scrolling moves in steps - every wheel notch is a ~100px jump. If the camera follows that pixel-for-pixel, it jumps too, and there goes the cinematic feel. That's where Lenis comes in: it intercepts the wheel and eases the real scroll position toward the target with an interpolation.", + )}

+

{L( + "Le principe de l'interpolation (lerp), c'est trois mots de code : à chaque frame, on parcourt 9% du chemin restant vers la cible. Regardez la différence - le point du haut suit la valeur brute, celui du bas la version lissée :", + "The interpolation (lerp) principle is three words of code: each frame, travel 9% of the remaining distance to the target. Watch the difference - the top dot follows the raw value, the bottom one the smoothed version:", + )}

+ +
+
+
{t('docs.live')}{t('docs.tryIt')}
+
+
{L('brut', 'raw')}
+
lerp 0.09
+

pos += (target − pos) × 0.09 — {L('chaque frame', 'every frame')}

+
+
+
+ +

{L( + "Le bonus qui change tout : comme Lenis lisse la position de scroll elle-même, tout ce qui la lit hérite de la même inertie. Je n'ai pas dix easings à synchroniser - j'en ai un. Le fichier complet, singleton compris (il est partagé par toutes les pages) :", + "The bonus that changes everything: since Lenis smooths the scroll position itself, anything that reads it inherits the same inertia. I don't have ten easings to keep in sync - I have one. The complete file, singleton included (it's shared by every page):", + )}

+

src/lib/smooth.ts

+ +
+

{t('docs.trap')}

+

{L( + "Enlevez scroll-behavior: smooth de votre CSS quand Lenis est actif, sinon les deux lissages s'empilent et le scroll devient un bateau ivre.", + 'Remove scroll-behavior: smooth from your CSS when Lenis is active, otherwise the two smoothings stack and scrolling turns into a drunken boat.', + )}

+
+
+ + +
+
03

{chapters[2]!.title}

+

{L( + "Confession : j'ai commencé avec GSAP ScrollTrigger, comme tout le monde. Je l'ai désinstallé au bout d'une heure. Pas parce qu'il est mauvais - parce que je n'avais besoin que d'une chose : savoir où j'en suis dans la traversée d'une section. Et ça, le navigateur le donne gratuitement.", + "Confession: I started with GSAP ScrollTrigger, like everyone. I uninstalled it an hour later. Not because it's bad - because I only needed one thing: knowing how far I am through a section. And the browser gives that away for free.", + )}

+

{L( + "La recette : une section très haute (disons 280vh), et dedans, un étage en position: sticky de 100vh. Pendant qu'on traverse la section, l'étage reste collé à l'écran. La progression de la traversée, c'est juste de la géométrie sur getBoundingClientRect(). Cette démo est elle-même épinglée - vous êtes en train de la traverser :", + "The recipe: a very tall section (say 280vh) with a 100vh position: sticky stage inside. While you traverse the section, the stage stays glued to the screen. The traversal progress is just geometry on getBoundingClientRect(). This demo is itself pinned - you are traversing it right now:", + )}

+ +
+
+
{t('docs.live')}{L('cette boîte est sticky, la section derrière fait 220vh', 'this box is sticky, the section behind is 220vh tall')}
+
+ +

p = 0.00

+ +
+
+
+ +

{L('Le helper complet fait 30 lignes, et les quatre scènes projet tournent dessus :', 'The complete helper is 30 lines, and all four project scenes run on it:')}

+

src/lib/scrub.ts

+ +
+

{t('docs.why')}

+

{L( + "getBoundingClientRect() reste juste avec Lenis parce que Lenis fait défiler la vraie fenêtre, pas un wrapper en transform. Beaucoup de libs de smooth scroll cassent cette propriété - c'est LE critère qui m'a fait choisir Lenis.", + "getBoundingClientRect() stays accurate with Lenis because Lenis scrolls the real window, not a transformed wrapper. Many smooth-scroll libs break that property - it's THE criterion that made me pick Lenis.", + )}

+
+
+ + +
+
04

{chapters[3]!.title}

+

{L( + "Sur l'accueil, le progress ne remplit pas une jauge : il place une caméra sur une courbe. Je pose un point de contrôle par section, en zigzag doux, et une Catmull-Rom fait passer une courbe lisse par tous. À chaque frame : position = courbe(p), regard = courbe(p + 0.045) - viser un peu devant soi suffit à donner une trajectoire naturelle, comme des phares dans un virage.", + "On the home page, progress doesn't fill a gauge: it places a camera on a curve. I drop one control point per section, gently zigzagging, and a Catmull-Rom threads a smooth curve through them all. Each frame: position = curve(p), gaze = curve(p + 0.045) - aiming slightly ahead of yourself is enough for a natural trajectory, like headlights in a bend.", + )}

+ +
+
+
{t('docs.live')}{L('la même mécanique, vue de dessus', 'the same mechanic, seen from above')}
+
+ + + + + + + + + +

{L('point plein = caméra · petit point = cible du regard, à p + 0.045 · les balises s’allument à l’approche', 'solid dot = camera · small dot = gaze target, at p + 0.045 · beacons light up on approach')}

+
+
+
+ +

{L( + "Détail que j'aime bien : la timeline n'est pas codée en dur, elle est lue dans le DOM. Chaque section porte data-seg et un data-weight (sa durée relative) - pour rallonger l'étape « projets », je change un attribut HTML, pas le moteur :", + "A detail I'm fond of: the timeline isn't hard-coded, it's read from the DOM. Each section carries data-seg and a data-weight (its relative duration) - to make the “projects” leg longer, I change an HTML attribute, not the engine:", + )}

+

src/components/scene/voyage.ts

+ + + +
+ + +
+
05

{chapters[4]!.title}

+

{L( + "Reste le texte. Si les sections restaient dans le flux, on les verrait défiler - retour à la case départ. Alors en mode cinéma elles passent toutes en position: fixed, empilées au même endroit, et c'est le progress qui décide laquelle existe : chacune a une fenêtre [début, fin] sur la timeline, avec un fondu d'entrée et de sortie en smoothstep.", + "That leaves the text. If the sections stayed in the flow, you'd see them scroll by - back to square one. So in cinema mode they all become position: fixed, stacked in the same spot, and progress decides which one exists: each has a [start, end] window on the timeline, with smoothstep fade-in and fade-out.", + )}

+ +
+
+
{t('docs.live')}{L('trois fenêtres, un seul écran', 'three windows, one screen')}
+
+
+

{L('Section A', 'Section A')}

{L('fenêtre 0.00 → 0.40', 'window 0.00 → 0.40')}

+

{L('Section B', 'Section B')}

{L('fenêtre 0.30 → 0.70', 'window 0.30 → 0.70')}

+

{L('Section C', 'Section C')}

{L('fenêtre 0.60 → 1.00', 'window 0.60 → 1.00')}

+
+ +
+
+
+ +

{L( + "Pas de transitions CSS, pas de lib d'animation : opacité et transform sont recalculés chaque frame depuis p. Et le point crucial pour l'accessibilité et le SEO : ce CSS n'existe qu'en mode cinéma. Sans JS, sans WebGL ou avec prefers-reduced-motion, les mêmes sections s'empilent en page normale.", + "No CSS transitions, no animation lib: opacity and transform are recomputed every frame from p. And the crucial bit for accessibility and SEO: that CSS only exists in cinema mode. Without JS, without WebGL or with prefers-reduced-motion, the same sections stack into a normal page.", + )}

+ +

src/views/HomeView.astro (style)

+ +
+ + +
+
06

{chapters[5]!.title}

+

{L( + "La position ne dit pas tout : Lenis expose aussi la vélocité, signée. Je la lisse en deux « énergies » - une pour la descente, une pour la remontée - et je les branche sur la physique de la ceinture d'astéroïdes. Descendre agite les rochers jusqu'à la collision (rebond élastique + étincelles) ; remonter les aspire dans un vortex qui les ramène en formation.", + "Position isn't the whole story: Lenis also exposes the velocity, signed. I smooth it into two “energies” - one for scrolling down, one for up - and wire them into the asteroid belt physics. Scrolling down agitates the rocks into collisions (elastic bounce + sparks); scrolling up sucks them into a vortex that brings them back into formation.", + )}

+ +
+
{t('docs.live')}{L('scrollez vite vers le bas, puis remontez - n’importe où sur la page', 'scroll down fast, then back up - anywhere on the page')}
+
+ +

v = 0 px/frame → {L('énergie descente', 'down energy')} 0.00 · {L('énergie remontée', 'up energy')} 0.00

+
+
+ +

{L('Le vrai code du moteur - le lissage des énergies, puis la boucle de collisions (limitée aux rochers proches de la caméra, sinon le O(n²) pique) :', 'The real engine code - the energy smoothing, then the collision loop (limited to rocks near the camera, otherwise the O(n²) bites):')}

+

src/components/scene/voyage.ts

+ + +
+ + +
+
07

{chapters[6]!.title}

+

{L( + "Mon attribut SVG préféré : pathLength=\"1\". Il déclare que le chemin mesure 1, peu importe sa vraie longueur. Du coup stroke-dasharray: 1 + un stroke-dashoffset qui va de 1 à 0, et n'importe quel tracé se dessine - pas de getTotalLength(), pas de calcul, le même CSS pour tous les chemins. C'est ce qui anime le schéma réseau de nova-infra et le blueprint du portfolio.", + 'My favorite SVG attribute: pathLength="1". It declares the path is 1 unit long, whatever its real length. So stroke-dasharray: 1 + a stroke-dashoffset going from 1 to 0, and any path draws itself - no getTotalLength(), no math, the same CSS for every path. That is what animates the nova-infra network diagram and the portfolio blueprint.', + )}

+ +
+
+
{t('docs.live')}{t('docs.tryIt')}
+
+ +

stroke-dashoffset: 1.00

+
+
+
+ +

src/components/scenes/InfraScene.astro (style)

+ +
+ + +
+
08

{chapters[7]!.title}

+

{L( + "La scène de file-organizer, c'est un FLIP à l'envers. Je laisse d'abord la grille CSS ranger les puces (l'état FINAL), je mesure ces positions, je fige la hauteur du plateau, et seulement là je les passe en absolu sur des positions éparpillées. Le scroll interpole ensuite chaque puce vers sa place. Le chaos vient d'un PRNG seedé : même bazar à chaque visite, ce qui est plus honnête qu'un Math.random() qui changerait dans votre dos.", + "The file-organizer scene is FLIP in reverse. I first let the CSS grid lay the chips out (the FINAL state), measure those positions, freeze the board height, and only then absolutize them onto scattered positions. Scroll then interpolates each chip to its slot. The chaos comes from a seeded PRNG: same mess on every visit, which is more honest than a Math.random() shifting behind your back.", + )}

+ +
+
+
{t('docs.live')}{t('docs.tryIt')}
+
+
+

Images

{miniFiles.filter((f) => f.cat === 'img').map((f) => {f.name})}
+

Code

{miniFiles.filter((f) => f.cat === 'code').map((f) => {f.name})}
+
+
+
+
+ +

{L( + "Petit aveu : mon premier bug ici, c'était le resize - je re-mesurais les cibles sans libérer la hauteur figée du plateau. Les puces visaient des positions d'un layout qui n'existait plus. D'où le board.style.height = '' en première ligne de measure() :", + "Small confession: my first bug here was resize - I re-measured the targets without releasing the board's frozen height. The chips were aiming at positions from a layout that no longer existed. Hence the board.style.height = '' on the first line of measure():", + )}

+

src/components/scenes/SorterScene.astro

+ + +
+ + +
+
09

{chapters[8]!.title}

+

{L( + "Le reste du site est volontairement classique, et c'est ce qui permet à l'animation d'exister. Trois portes s'ouvrent avant le mode cinéma - reduced-motion, création du renderer WebGL, et seulement ensuite la classe html.cinema. Si une seule reste fermée, vous avez une page statique complète, avec exactement le même contenu. Côté budget : DPR plafonné à 2, moitié moins de particules sur mobile, boucle en pause quand l'onglet est caché, et Three.js (528 Ko quand même) en import dynamique - il ne se télécharge que si le voyage va vraiment démarrer.", + "The rest of the site is deliberately boring, and that's what lets the animation exist. Three gates open before cinema mode - reduced-motion, WebGL renderer creation, and only then the html.cinema class. If a single one stays shut, you get a complete static page with exactly the same content. Budget-wise: DPR capped at 2, half the particles on mobile, loop paused when the tab is hidden, and Three.js (a hefty 528 KB) behind a dynamic import - it only downloads if the voyage is actually going to start.", + )}

+

src/components/scene/voyage.ts

+ +

{L( + "Bilingue : le français vit à la racine, l'anglais sous /en/, et chaque route anglaise fait quatre lignes parce que toute la logique est dans des vues partagées. Les dictionnaires sont typés - une clé oubliée en anglais, et le build refuse.", + "Bilingual: French lives at the root, English under /en/, and every English route is four lines because all the logic sits in shared views. Dictionaries are typed - forget a key in English and the build refuses.", + )}

+

astro.config.mjs

+ +

src/pages/en/index.astro

+ +

{L( + "Le design system fournit les tokens (var(--nds-…)) et les mixins typo Sass, branchés via un loadPath - le site n'écrit jamais une couleur en dur, y compris dans les scènes 3D. Déploiement : astro build sort un dist/ statique, un runner Gitea Actions sur mon VPS OVH le build et le publie derrière Apache et Cloudflare.", + "The design system provides the tokens (var(--nds-…)) and the Sass type mixins, wired through a loadPath - the site never hard-codes a color, 3D scenes included. Deployment: astro build outputs a static dist/, a Gitea Actions runner on my OVH VPS builds and publishes it behind Apache and Cloudflare.", + )}

+

astro.config.mjs

+ +
+ + +
+
10

{chapters[9]!.title}

+

{L( + "Si je devais tout refaire demain, voilà l'ordre - c'est presque celui que j'ai suivi, en enlevant les détours :", + "If I had to redo it all tomorrow, here's the order - it's almost the one I followed, minus the detours:", + )}

+
    +
  1. {L("La page statique d'abord. Toutes les sections, empilées, finies, belles. C'est votre fallback, votre SEO, et votre filet de sécurité pour la suite.", 'The static page first. All sections, stacked, finished, good-looking. That is your fallback, your SEO, and your safety net for what follows.')}
  2. +
  3. {L('Lenis en singleton (chapitre 02). Copiez smooth.ts, il est fait pour ça.', 'Lenis as a singleton (chapter 02). Copy smooth.ts, that is what it is for.')}
  4. +
  5. {L('Le nombre magique : affichez le progress de la page dans un coin (chapitre 01). Tant que ce nombre ne bouge pas proprement, rien ne bougera proprement.', 'The magic number: display the page progress in a corner (chapter 01). Until that number moves cleanly, nothing will.')}
  6. +
  7. {L("La caméra (chapitre 04) : les points de contrôle, la Catmull-Rom, position = courbe(p). Regardez-la voler dans le vide avant d'ajouter quoi que ce soit.", 'The camera (chapter 04): control points, the Catmull-Rom, position = curve(p). Watch it fly through the void before adding anything.')}
  8. +
  9. {L('Les overlays (chapitre 05) : passez les sections en fixed derrière une classe html.cinema, jamais par défaut.', 'The overlays (chapter 05): switch the sections to fixed behind an html.cinema class, never by default.')}
  10. +
  11. {L("La matière : étoiles, nébuleuses, ceinture. Puis la vélocité (chapitre 06) - c'est elle qui rend le scroll vivant.", 'The matter: stars, nebulae, belt. Then the velocity (chapter 06) - that is what makes the scroll feel alive.')}
  12. +
  13. {L('scrub.ts (chapitre 03) et une scène épinglée par projet. Le fallback statique se code EN MÊME TEMPS que la scène, pas après.', 'scrub.ts (chapter 03) and one pinned scene per project. The static fallback is written AT THE SAME TIME as the scene, not after.')}
  14. +
  15. {L('SEO, i18n, CI (chapitre 09). Ennuyeux, indispensable.', 'SEO, i18n, CI (chapter 09). Boring, indispensable.')}
  16. +
  17. {L("Testez sur un vrai trackpad, un vrai téléphone, et ajustez les data-weight. Les miens ont bougé cinq fois.", 'Test on a real trackpad, a real phone, and tune the data-weight values. Mine changed five times.')}
  18. +
+

{L( + "Et si un truc reste flou : tout le code est sur mon Gitea, et chaque snippet de cette page en est extrait au build - ce que vous lisez ici est forcément à jour.", + "And if anything is still unclear: all the code is on my Gitea, and every snippet on this page is extracted from it at build time - what you read here is up to date by construction.", + )}

+
+
+
+
+ + + + diff --git a/src/views/HomeView.astro b/src/views/HomeView.astro new file mode 100644 index 0000000..94011c4 --- /dev/null +++ b/src/views/HomeView.astro @@ -0,0 +1,547 @@ +--- +/** + * Home = the voyage. One long timeline: hero → profile → asteroid belt with a + * beacon per project → stack constellation → contact portal. + * + * Two rendering modes from the same markup: + * - default (no JS, no WebGL, reduced motion): normal stacked sections + * - `html.cinema` (set by voyage.ts): sections become fixed overlays, + * the wrapper's height becomes the scroll timeline. + */ +import BaseLayout from '../layouts/BaseLayout.astro'; +import Eyebrow from '../components/Eyebrow.astro'; +import TechIcon from '../components/TechIcon.astro'; +import BrandLinkedin from '../components/BrandLinkedin.astro'; +import { Copy, ChevronDown } from '@lucide/astro'; +import { Github, Gitea, Npm } from 'simple-icons-astro'; +import { Badge, Button } from '@unkn0wndo3s/nova-design-system'; +import { site, stack } from '../data/site'; +import { featuredProjects, projects, type Project } from '../data/projects'; +import { breadcrumbLd, profilePageLd } from '../lib/seo'; +import { getLocaleFromUrl, useTranslations, localizePath } from '../i18n'; + +const locale = getLocaleFromUrl(Astro.url); +const t = useTranslations(locale); + +const voyageProjects: Project[] = [...featuredProjects, ...projects.filter((p) => !p.featured)]; +const statusType = { live: 'success', maintained: 'info', wip: 'warning' } as const; +const statusLabel = { + live: t('work.status.live'), + maintained: t('work.status.maintained'), + wip: t('work.status.wip'), +} as const; + +const pillars = [ + { title: t('home.profile.front.title'), body: t('home.profile.front.body') }, + { title: t('home.profile.back.title'), body: t('home.profile.back.body') }, + { title: t('home.profile.ops.title'), body: t('home.profile.ops.body') }, +]; + +const dots = ['hero', 'about', 'projects', ...voyageProjects.map((p) => `p-${p.slug}`), 'stack', 'contact']; +--- + + + + +
+ +
+
+

{t('avail.badge')} {t('avail.line')}

+

+ {site.name} + {t('home.hero.title.role')} +

+

{t('home.hero.lead')}

+
+ + +
+ +
+
+ + +
+
+ {t('home.profile.eyebrow')} +

{t('home.profile.title')}

+

{t('home.profile.body1')}

+

{t('home.about.summary')}

+
    + {pillars.map((p) => ( +
  • {p.title}{p.body}
  • + ))} +
+
+
+ + +
+
+ {t('home.projects.eyebrow')} +

{t('home.projects.intro.title')}

+

{t('home.projects.intro.body')}

+
+ +
+
+
+ + + {voyageProjects.map((p, i) => ( +
+
+ +
+ {statusLabel[p.status]} +
+

{p.name}

+

{p.tagline[locale]}

+
    + {p.highlights.slice(0, 2).map((h) =>
  • {h[locale]}
  • )} +
+
+ +
+
+
+ ))} + + +
+
+ {t('home.stack.eyebrow')} +

{t('home.stack.title')}

+
    + {stack.map((s) =>
  • )} +
+
+
+ + +
+
+ {t('contact.title')} +

{t('home.cta.title')}

+

{t('home.cta.body')}

+
+ + +
+

{site.email}

+ +
+
+
+ + + + +
+ + + + diff --git a/src/views/ProjectView.astro b/src/views/ProjectView.astro new file mode 100644 index 0000000..f88ea68 --- /dev/null +++ b/src/views/ProjectView.astro @@ -0,0 +1,203 @@ +--- +import BaseLayout from '../layouts/BaseLayout.astro'; +import TechIcon from '../components/TechIcon.astro'; +import ComponentShowcase from '../components/showcase/ComponentShowcase.astro'; +import InfraScene from '../components/scenes/InfraScene.astro'; +import BlueprintScene from '../components/scenes/BlueprintScene.astro'; +import SorterScene from '../components/scenes/SorterScene.astro'; +import { Badge, Button, Breadcrumb, BreadcrumbItem, Link } from '@unkn0wndo3s/nova-design-system'; +import { projects, type Project } from '../data/projects'; +import { breadcrumbLd, projectLd } from '../lib/seo'; +import { getLocaleFromUrl, useTranslations, localizePath } from '../i18n'; + +export interface Props { + project: Project; +} + +const { project } = Astro.props; +const locale = getLocaleFromUrl(Astro.url); +const t = useTranslations(locale); + +const statusType = { live: 'success', maintained: 'info', wip: 'warning' } as const; +const statusLabel = { + live: t('work.status.live'), + maintained: t('work.status.maintained'), + wip: t('work.status.wip'), +} as const; + +const crumbs = [ + { name: t('nav.home'), url: localizePath('/', locale) }, + { name: t('nav.work'), url: localizePath('/work', locale) }, + { name: project.name, url: localizePath(`/work/${project.slug}`, locale) }, +]; + +const pageUrl = new URL(Astro.url.pathname, Astro.site ?? 'https://louis-potevin.dev').href; +const jsonLd = [breadcrumbLd(crumbs), projectLd(project, locale, pageUrl)]; + +const title = `${project.name} - ${t('meta.work.title')}`; +const next = projects[(projects.findIndex((p) => p.slug === project.slug) + 1) % projects.length]!; +--- + + + + +
+
+ {statusLabel[project.status]} +

{project.name}

+

{project.tagline[locale]}

+
+ + {project.slug === 'nova-infra' && } + {project.slug === 'portfolio' && } + {project.slug === 'file-organizer' && } + +
+
+ {project.body.map((paragraph) =>

{paragraph[locale]}

)} + +

{t('work.highlights')}

+
    + {project.highlights.map((h) =>
  • {h[locale]}
  • )} +
+
+ + +
+ + {project.slug === 'nova-design-system' && } + +
+ + +
+
+
+ + diff --git a/src/views/WorkIndexView.astro b/src/views/WorkIndexView.astro new file mode 100644 index 0000000..7d11fcf --- /dev/null +++ b/src/views/WorkIndexView.astro @@ -0,0 +1,67 @@ +--- +import BaseLayout from '../layouts/BaseLayout.astro'; +import ProjectCard from '../components/ProjectCard.astro'; +import { Breadcrumb, BreadcrumbItem } from '@unkn0wndo3s/nova-design-system'; +import { projects } from '../data/projects'; +import { breadcrumbLd } from '../lib/seo'; +import { getLocaleFromUrl, useTranslations, localizePath } from '../i18n'; + +const locale = getLocaleFromUrl(Astro.url); +const t = useTranslations(locale); + +const crumbs = [ + { name: t('nav.home'), url: localizePath('/', locale) }, + { name: t('nav.work'), url: localizePath('/work', locale) }, +]; +--- + + + + +
+

{t('work.title')}

+

{t('work.lead')}

+
+ +
+ {projects.map((project, i) => ( + + ))} +
+
+ + diff --git a/tsconfig.json b/tsconfig.json index c85195b..d58a40a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,11 @@ { "extends": "astro/tsconfigs/strict", - "include": [".astro/types.d.ts", "**/*"], - "exclude": ["dist"], "compilerOptions": { - "types": ["three"] - } + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*", ".astro/types.d.ts"], + "exclude": ["dist"] }