From d6efb6736fb583a17ec1dd545ad6c4c73cf0e138 Mon Sep 17 00:00:00 2001 From: sucupira Date: Thu, 23 Jul 2026 00:49:02 -0400 Subject: [PATCH] =?UTF-8?q?R=C3=A9=C3=A9criture=20SvelteKit=20statique=20(?= =?UTF-8?q?Svelte=205=20runes=20+=20TS,=20adapter-static)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Données PeerTube (GADE), Castopod (KUTE) et Mastodon (BOKANTE) bakées au build via +page.server.ts (throttle + retry 429 + déduplication) - Charte OKI complète : tokens, thème sombre par défaut (clair opt-in, anti-FOUC), Archivo/Inter self-hébergées, flag-bar, sprite SVG (zéro emoji en interface, zéro Font Awesome/CDN) - i18n FR/EN par routage [[locale]], bundles JSON, hreflang, %lang% serveur - Pages : accueil, /video/[uuid] (embed, téléchargements, partage, commentaires, JSON-LD VideoObject), /categories/[id], /recherche (index JSON baké, recherche client), /direct (live + annonce multi-fuseaux), /dons, /mentions-legales, /offline + 404 kréyòl - Motion gwoka : KineticText (scrub view()), ScrollProgressBar, FlagChip, reveal syncopé, View Transitions, gate prefers-reduced-motion unique - PWA : manifest + Workbox generateSW (fallback /offline/), registerSW statique - Sécurité : CSP par page en (SHA-256 des inline via scripts/postbuild-csp.mjs) + headers globaux (_headers Cloudflare et .htaccess o2switch), redirections des anciennes URLs PHP - Doc : docs/DEPLOIEMENT-SVELTEKIT.md --- .gitignore | 41 +- .npmrc | 1 + README.adoc | 2 + ajax/load-more-videos.php | 128 - browserconfig.xml | 9 - categories.php | 172 - conf/.htaccess.sample | 57 - conf/nginx.conf.sample | 96 - countdown.php | 187 - css/castopod-podcasts.css | 279 - css/categories.css | 157 - css/countdown.css | 350 - css/donations.css | 491 -- css/funkwhale-tracks.css | 273 - css/grid-layout.css | 117 - css/mastodon-timeline.min.css | 1 - css/search.css | 206 - css/styles.css | 3414 --------- css/video-page.css | 1327 ---- css/wordpress-posts.css | 204 - direct.php | 278 - doc2sveltekit-transition/AGENTS.md | 23 + .../charte-oki-design-system.md | 213 + .../playbook-oki-sveltekit.md | 494 ++ .../prompt-kimi-cli-transformation-oki.md | 79 + .../recette-sveltekit-15-sites-awwwards.md | 908 +++ .../recette-sveltekit-playbook-agent.md | 211 + .../svelte-5_code_writer.md | 61 + .../svelte_core_bestpractices.md | 172 + docs/DEPLOIEMENT-SVELTEKIT.md | 52 + dons.php.sample | 295 - img/play-icon.svg | 6 - includes/config.default.php | 286 - includes/config.local.php.sample | 437 -- includes/config.php | 1012 --- includes/featured-videos.php | 77 - includes/footer.php | 99 - includes/header.php | 84 - includes/hero-section.php | 291 - includes/lib/markdown.php | 94 - includes/mobile-menu.php | 124 - includes/pwa-init.php | 82 - includes/recent-videos.php | 29 - includes/security.php | 327 - includes/sidebar.php | 91 - includes/simple-cache.php | 187 - includes/structured-data.php | 320 - includes/wordpress.php | 202 - index.php | 747 -- js/audio-player.js | 147 - js/categories.js | 97 - js/countdown.js | 97 - js/main.js | 529 -- js/mastodon-config.php | 23 - js/mastodon-timeline.umd.js | 9 - js/pleroma-adapter.js | 133 - js/search.js | 12 - mentions-legales.php.sample | 247 - package-lock.json | 6567 +++++++++++++++++ package.json | 29 + recherche.php | 265 - robots.txt.sample | 25 - scripts/postbuild-csp.mjs | 44 + site.webmanifest.sample | 43 - sitemap.xml.sample | 70 - src/app.d.ts | 13 + src/app.html | 23 + src/hooks.server.ts | 9 + src/lib/components/AudioPlayer.svelte | 317 + src/lib/components/CountdownClock.svelte | 80 + src/lib/components/Footer.svelte | 235 + src/lib/components/HeroSection.svelte | 109 + src/lib/components/Icon.svelte | 10 + src/lib/components/MastodonTimeline.svelte | 107 + src/lib/components/Nav.svelte | 219 + src/lib/components/Seo.svelte | 63 + src/lib/components/ShortsCarousel.svelte | 99 + src/lib/components/Sidebar.svelte | 169 + src/lib/components/VideoCard.svelte | 147 + src/lib/components/motion/FlagChip.svelte | 51 + src/lib/components/motion/KineticText.svelte | 67 + .../motion/ScrollProgressBar.svelte | 70 + src/lib/config.ts | 186 + src/lib/i18n/en.json | 164 + src/lib/i18n/fr.json | 164 + src/lib/i18n/index.ts | 75 + src/lib/motion/reveal.ts | 27 + src/lib/motion/tokens.ts | 11 + src/lib/server/castopod.ts | 84 + src/lib/server/http.ts | 108 + src/lib/server/mastodon.ts | 57 + src/lib/server/peertube.ts | 235 + src/lib/styles/base.css | 285 + src/lib/styles/oki-tokens.css | 72 + src/lib/types.ts | 82 + src/params/locale.ts | 3 + src/routes/+error.svelte | 61 + src/routes/+layout.ts | 2 + src/routes/[[locale=locale]]/+layout.svelte | 175 + src/routes/[[locale=locale]]/+layout.ts | 7 + src/routes/[[locale=locale]]/+page.server.ts | 43 + src/routes/[[locale=locale]]/+page.svelte | 202 + .../categories/[id]/+page.server.ts | 22 + .../categories/[id]/+page.svelte | 62 + .../[[locale=locale]]/direct/+page.server.ts | 36 + .../[[locale=locale]]/direct/+page.svelte | 187 + .../[[locale=locale]]/dons/+page.svelte | 206 + src/routes/[[locale=locale]]/dons/+page.ts | 3 + .../mentions-legales/+page.svelte | 107 + .../mentions-legales/+page.ts | 3 + .../[[locale=locale]]/offline/+page.svelte | 51 + src/routes/[[locale=locale]]/offline/+page.ts | 3 + .../[[locale=locale]]/recherche/+page.svelte | 146 + .../[[locale=locale]]/recherche/+page.ts | 3 + .../video/[uuid]/+page.server.ts | 41 + .../video/[uuid]/+page.svelte | 436 ++ src/routes/search-index.json/+server.ts | 12 + src/routes/sitemap.xml/+server.ts | 39 + static/.htaccess | 44 + static/404.html | 113 + static/_headers | 16 + static/fonts/archivo-latin-600-normal.woff2 | Bin 0 -> 13820 bytes static/fonts/archivo-latin-700-normal.woff2 | Bin 0 -> 14508 bytes static/fonts/archivo-latin-800-normal.woff2 | Bin 0 -> 14416 bytes static/fonts/archivo-latin-900-normal.woff2 | Bin 0 -> 13548 bytes static/fonts/fonts.css | 71 + static/fonts/inter-latin-400-normal.woff2 | Bin 0 -> 23664 bytes static/fonts/inter-latin-500-normal.woff2 | Bin 0 -> 24272 bytes static/fonts/inter-latin-600-normal.woff2 | Bin 0 -> 24452 bytes static/fonts/inter-latin-700-normal.woff2 | Bin 0 -> 24356 bytes static/fonts/inter-latin-800-normal.woff2 | Bin 0 -> 24400 bytes static/icons.svg | 147 + .../images}/android-chrome-192x192.png | Bin .../images}/android-chrome-512x512.png | Bin {img => static/images}/apple-touch-icon.png | Bin {img => static/images}/favicon-16x16.png | Bin {img => static/images}/favicon-32x32.png | Bin {img => static/images}/favicon.ico | Bin {img => static/images}/logo.png | Bin static/manifest.webmanifest | 31 + static/registerSW.js | 5 + static/robots.txt | 4 + static/theme.js | 10 + sw.js | 221 - tsconfig.json | 20 + video.php | 672 -- vite.config.ts | 40 + 147 files changed, 14567 insertions(+), 15145 deletions(-) create mode 100644 .npmrc delete mode 100644 ajax/load-more-videos.php delete mode 100644 browserconfig.xml delete mode 100644 categories.php delete mode 100644 conf/.htaccess.sample delete mode 100644 conf/nginx.conf.sample delete mode 100644 countdown.php delete mode 100644 css/castopod-podcasts.css delete mode 100644 css/categories.css delete mode 100644 css/countdown.css delete mode 100644 css/donations.css delete mode 100644 css/funkwhale-tracks.css delete mode 100644 css/grid-layout.css delete mode 100644 css/mastodon-timeline.min.css delete mode 100644 css/search.css delete mode 100644 css/styles.css delete mode 100644 css/video-page.css delete mode 100644 css/wordpress-posts.css delete mode 100644 direct.php create mode 100644 doc2sveltekit-transition/AGENTS.md create mode 100644 doc2sveltekit-transition/charte-oki-design-system.md create mode 100644 doc2sveltekit-transition/playbook-oki-sveltekit.md create mode 100644 doc2sveltekit-transition/prompt-kimi-cli-transformation-oki.md create mode 100644 doc2sveltekit-transition/recette-sveltekit-15-sites-awwwards.md create mode 100644 doc2sveltekit-transition/recette-sveltekit-playbook-agent.md create mode 100644 doc2sveltekit-transition/svelte-5_code_writer.md create mode 100644 doc2sveltekit-transition/svelte_core_bestpractices.md create mode 100644 docs/DEPLOIEMENT-SVELTEKIT.md delete mode 100644 dons.php.sample delete mode 100644 img/play-icon.svg delete mode 100644 includes/config.default.php delete mode 100644 includes/config.local.php.sample delete mode 100644 includes/config.php delete mode 100644 includes/featured-videos.php delete mode 100644 includes/footer.php delete mode 100644 includes/header.php delete mode 100644 includes/hero-section.php delete mode 100644 includes/lib/markdown.php delete mode 100644 includes/mobile-menu.php delete mode 100644 includes/pwa-init.php delete mode 100644 includes/recent-videos.php delete mode 100644 includes/security.php delete mode 100644 includes/sidebar.php delete mode 100644 includes/simple-cache.php delete mode 100644 includes/structured-data.php delete mode 100644 includes/wordpress.php delete mode 100644 index.php delete mode 100644 js/audio-player.js delete mode 100644 js/categories.js delete mode 100644 js/countdown.js delete mode 100644 js/main.js delete mode 100644 js/mastodon-config.php delete mode 100644 js/mastodon-timeline.umd.js delete mode 100644 js/pleroma-adapter.js delete mode 100644 js/search.js delete mode 100644 mentions-legales.php.sample create mode 100644 package-lock.json create mode 100644 package.json delete mode 100644 recherche.php delete mode 100644 robots.txt.sample create mode 100644 scripts/postbuild-csp.mjs delete mode 100644 site.webmanifest.sample delete mode 100644 sitemap.xml.sample create mode 100644 src/app.d.ts create mode 100644 src/app.html create mode 100644 src/hooks.server.ts create mode 100644 src/lib/components/AudioPlayer.svelte create mode 100644 src/lib/components/CountdownClock.svelte create mode 100644 src/lib/components/Footer.svelte create mode 100644 src/lib/components/HeroSection.svelte create mode 100644 src/lib/components/Icon.svelte create mode 100644 src/lib/components/MastodonTimeline.svelte create mode 100644 src/lib/components/Nav.svelte create mode 100644 src/lib/components/Seo.svelte create mode 100644 src/lib/components/ShortsCarousel.svelte create mode 100644 src/lib/components/Sidebar.svelte create mode 100644 src/lib/components/VideoCard.svelte create mode 100644 src/lib/components/motion/FlagChip.svelte create mode 100644 src/lib/components/motion/KineticText.svelte create mode 100644 src/lib/components/motion/ScrollProgressBar.svelte create mode 100644 src/lib/config.ts create mode 100644 src/lib/i18n/en.json create mode 100644 src/lib/i18n/fr.json create mode 100644 src/lib/i18n/index.ts create mode 100644 src/lib/motion/reveal.ts create mode 100644 src/lib/motion/tokens.ts create mode 100644 src/lib/server/castopod.ts create mode 100644 src/lib/server/http.ts create mode 100644 src/lib/server/mastodon.ts create mode 100644 src/lib/server/peertube.ts create mode 100644 src/lib/styles/base.css create mode 100644 src/lib/styles/oki-tokens.css create mode 100644 src/lib/types.ts create mode 100644 src/params/locale.ts create mode 100644 src/routes/+error.svelte create mode 100644 src/routes/+layout.ts create mode 100644 src/routes/[[locale=locale]]/+layout.svelte create mode 100644 src/routes/[[locale=locale]]/+layout.ts create mode 100644 src/routes/[[locale=locale]]/+page.server.ts create mode 100644 src/routes/[[locale=locale]]/+page.svelte create mode 100644 src/routes/[[locale=locale]]/categories/[id]/+page.server.ts create mode 100644 src/routes/[[locale=locale]]/categories/[id]/+page.svelte create mode 100644 src/routes/[[locale=locale]]/direct/+page.server.ts create mode 100644 src/routes/[[locale=locale]]/direct/+page.svelte create mode 100644 src/routes/[[locale=locale]]/dons/+page.svelte create mode 100644 src/routes/[[locale=locale]]/dons/+page.ts create mode 100644 src/routes/[[locale=locale]]/mentions-legales/+page.svelte create mode 100644 src/routes/[[locale=locale]]/mentions-legales/+page.ts create mode 100644 src/routes/[[locale=locale]]/offline/+page.svelte create mode 100644 src/routes/[[locale=locale]]/offline/+page.ts create mode 100644 src/routes/[[locale=locale]]/recherche/+page.svelte create mode 100644 src/routes/[[locale=locale]]/recherche/+page.ts create mode 100644 src/routes/[[locale=locale]]/video/[uuid]/+page.server.ts create mode 100644 src/routes/[[locale=locale]]/video/[uuid]/+page.svelte create mode 100644 src/routes/search-index.json/+server.ts create mode 100644 src/routes/sitemap.xml/+server.ts create mode 100644 static/.htaccess create mode 100644 static/404.html create mode 100644 static/_headers create mode 100644 static/fonts/archivo-latin-600-normal.woff2 create mode 100644 static/fonts/archivo-latin-700-normal.woff2 create mode 100644 static/fonts/archivo-latin-800-normal.woff2 create mode 100644 static/fonts/archivo-latin-900-normal.woff2 create mode 100644 static/fonts/fonts.css create mode 100644 static/fonts/inter-latin-400-normal.woff2 create mode 100644 static/fonts/inter-latin-500-normal.woff2 create mode 100644 static/fonts/inter-latin-600-normal.woff2 create mode 100644 static/fonts/inter-latin-700-normal.woff2 create mode 100644 static/fonts/inter-latin-800-normal.woff2 create mode 100644 static/icons.svg rename {img => static/images}/android-chrome-192x192.png (100%) rename {img => static/images}/android-chrome-512x512.png (100%) rename {img => static/images}/apple-touch-icon.png (100%) rename {img => static/images}/favicon-16x16.png (100%) rename {img => static/images}/favicon-32x32.png (100%) rename {img => static/images}/favicon.ico (100%) rename {img => static/images}/logo.png (100%) create mode 100644 static/manifest.webmanifest create mode 100644 static/registerSW.js create mode 100644 static/robots.txt create mode 100644 static/theme.js delete mode 100644 sw.js create mode 100644 tsconfig.json delete mode 100644 video.php create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore index 0d9b960..99b869b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,31 @@ -# Configuration locale -includes/config.local.php +node_modules -# Fichiers personnalisables pour chaque installation -.htaccess -sitemap.xml -robots.txt -site.webmanifest -mentions-legales.php -dons.php +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* # Fichiers de l'IDE/éditeur .vscode/ .idea/ *.sublime-* -.DS_Store # Fichiers temporaires *.log @@ -22,9 +34,6 @@ dons.php tmp/ temp/ -# Fichiers de cache -cache/ - # Artefacts de documentation générés (docs/generate-readme-pdf.sh) README.html README.pdf @@ -32,9 +41,3 @@ README.pdf # Dossier pour les images d'annonces (tout ignorer sauf .gitkeep) uploads/* !uploads/.gitkeep - -# Fichiers de dépendances (si nécessaire) -# vendor/ -# node_modules/ - -img/movement_presentation.png diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/README.adoc b/README.adoc index ff2dbc9..f813cb7 100644 --- a/README.adoc +++ b/README.adoc @@ -3,6 +3,8 @@ :toc-title: Sommaire / Contents :toclevels: 3 +NOTE: Ce document décrit la version historique PHP (branche `main`). La branche `svelte` contient la réécriture **SvelteKit statique** (données bakées au build, charte OKI, FR/EN, PWA) — voir `docs/DEPLOIEMENT-SVELTEKIT.md` pour le build et le déploiement. + [[top]] 🌍 *Hub multimédia du podcast ANNU KUTE CED* + 🌍 *Multimedia hub for the ANNU KUTE CED podcast* diff --git a/ajax/load-more-videos.php b/ajax/load-more-videos.php deleted file mode 100644 index 921e144..0000000 --- a/ajax/load-more-videos.php +++ /dev/null @@ -1,128 +0,0 @@ - 'Accès non autorisé']); - exit; -} - -// Vérifier l'origine de la requête -if (!validateAjaxOrigin()) { - http_response_code(403); - echo json_encode(['error' => 'Origine non autorisée']); - exit; -} - -// Vérifier le token CSRF -if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { - http_response_code(403); - echo json_encode(['error' => 'Token CSRF invalide']); - exit; -} - -// Récupérer les paramètres -$type = isset($_GET['type']) ? $_GET['type'] : ''; -$page = isset($_GET['page']) ? intval($_GET['page']) : 1; -$categoryId = isset($_GET['category']) ? intval($_GET['category']) : 0; - -// Vérifier que le type est valide -if (!in_array($type, ['recent', 'trending', 'independence', 'category'])) { - http_response_code(400); // Requête incorrecte - echo json_encode(['error' => 'Type de vidéos non valide']); - exit; -} - -// Vérifier que la catégorie est fournie si le type est 'category' -if ($type === 'category' && $categoryId <= 0) { - http_response_code(400); // Requête incorrecte - echo json_encode(['error' => 'ID de catégorie manquant ou invalide']); - exit; -} - -// Récupérer les vidéos en fonction du type -$videos = []; -$offset = $page * LOAD_MORE_COUNT; - -switch ($type) { - case 'recent': - // Récupérer les vidéos récentes - $data = callPeerTubeApi('videos', [ - 'sort' => '-publishedAt', - 'count' => LOAD_MORE_COUNT, - 'start' => $offset, - 'isLocal' => true - ]); - $videos = formatVideosData($data['data'] ?? []); - break; - - case 'trending': - // Récupérer les vidéos tendances - $data = callPeerTubeApi('videos', [ - 'sort' => '-views', - 'count' => LOAD_MORE_COUNT, - 'start' => $offset, - 'isLocal' => true - ]); - $videos = formatVideosData($data['data'] ?? []); - break; - - case 'independence': - // Récupérer les vidéos sur l'indépendance - $data = callPeerTubeApi('videos', [ - 'tagsOneOf' => TAG_INDEPENDENCE, - 'count' => LOAD_MORE_COUNT, - 'start' => $offset, - 'isLocal' => true - ]); - $videos = formatVideosData($data['data'] ?? []); - break; - - case 'category': - // Récupérer les vidéos de la catégorie - $data = callPeerTubeApi('videos', [ - 'categoryOneOf' => $categoryId, - 'count' => LOAD_MORE_COUNT, - 'start' => $offset, - 'sort' => '-publishedAt', // Les plus récentes d'abord - 'isLocal' => true - ]); - $videos = formatVideosData($data['data'] ?? []); - break; -} - -// Préparer la réponse HTML -$html = ''; - -foreach ($videos as $video) { - $html .= '
'; - $html .= '
'; - $html .= ' ' . htmlspecialchars($video['title']) . ''; - $html .= '
'; - $html .= ' '; - $html .= '
'; - $html .= '
' . formatDuration($video['duration']) . '
'; - $html .= '
'; - $html .= '
'; - $html .= '

' . htmlspecialchars($video['title']) . '

'; - $html .= '
' . htmlspecialchars($video['channel']) . '
'; - $html .= ' '; - $html .= '
'; - $html .= '
'; -} - -// Retourner la réponse -echo json_encode([ - 'success' => true, - 'html' => $html, - 'page' => $page, - 'hasMore' => count($videos) >= LOAD_MORE_COUNT -]); -?> \ No newline at end of file diff --git a/browserconfig.xml b/browserconfig.xml deleted file mode 100644 index 1a9e2af..0000000 --- a/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - #FF0000 - - - \ No newline at end of file diff --git a/categories.php b/categories.php deleted file mode 100644 index f75b2bf..0000000 --- a/categories.php +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - - - <?php echo htmlspecialchars($categoryName); ?> - <?php echo SITE_NAME; ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'Accueil', 'url' => getBaseUrl()], - ['name' => 'Catégories', 'url' => getBaseUrl() . '/#categories'], - ['name' => $categoryName, 'url' => $categoryUrl] - ]; - $breadcrumbJsonLd = generateBreadcrumbJsonLd($breadcrumbs); - outputJsonLd($breadcrumbJsonLd); - ?> - - - - - - - - -
- - - -
-
- -

Catégorie :

-
- - -
- -

Aucune vidéo dans cette catégorie

-

Il n'y a pas encore de vidéos dans la catégorie "".

- Retour à l'accueil -
- -
- -
-
- <?php echo htmlspecialchars($video['title']); ?> -
- -
- -
-
-

-
- -
- -
- - <?php echo htmlspecialchars($video['channel']); ?> - - -
- -
-
- -
- - = CATEGORY_VIDEOS_COUNT): ?> - - - -
-
- - - - - - - - diff --git a/conf/.htaccess.sample b/conf/.htaccess.sample deleted file mode 100644 index 303524b..0000000 --- a/conf/.htaccess.sample +++ /dev/null @@ -1,57 +0,0 @@ -RewriteEngine On - -# ====================== -# SÉCURITÉ -# ====================== - -# Bloquer l'accès aux fichiers de configuration - - Require all denied - - -# Bloquer les fichiers PHP dans includes/ - - Require all denied - - -# Protéger les répertoires sensibles -RewriteRule ^(includes|cache|docs|conf)/ - [F,L] - -# Bloquer les fichiers samples et backups - - Require all denied - - -# Empêcher l'exploration des répertoires -Options -Indexes - -# Bloquer l'accès aux fichiers cachés - - Require all denied - - -# ====================== -# RÉÉCRITURE D'URL -# ====================== - -# Masquer l'extension .php -RewriteCond %{REQUEST_FILENAME} !-d -RewriteCond %{REQUEST_FILENAME} !-f -RewriteRule ^([^\.]+)$ $1.php [NC,L] - -# Rediriger les URLs avec .php vers les URLs sans extension -RewriteCond %{THE_REQUEST} /([^.]+)\.php [NC] -RewriteRule ^ /%1 [NC,L,R=301] - -# Pour accéder à page.php via /page -RewriteCond %{REQUEST_FILENAME}.php -f -RewriteRule ^([^/]+)$ $1.php [L] - -# ====================== -# HTTPS -# ====================== - -# Force HTTPS -RewriteCond %{HTTP:X-Forwarded-Proto} !https -RewriteCond %{HTTPS} !on -RewriteRule ^(.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] diff --git a/conf/nginx.conf.sample b/conf/nginx.conf.sample deleted file mode 100644 index 3cddbfd..0000000 --- a/conf/nginx.conf.sample +++ /dev/null @@ -1,96 +0,0 @@ -server { - listen 80; - server_name votre-domaine.com; - return 301 https://$server_name$request_uri; -} - -server { - listen 443 ssl http2; - server_name votre-domaine.com; - root /path/to/your/site; - index index.php index.html; - - # SSL Configuration (adaptez selon votre certificat) - ssl_certificate /path/to/your/certificate.crt; - ssl_certificate_key /path/to/your/private.key; - - # ====================== - # SÉCURITÉ - # ====================== - - # Bloquer l'accès aux fichiers de configuration - location ~* \.(php|inc|conf|config|local)$ { - deny all; - return 404; - } - - # Protéger les répertoires sensibles - location ~ ^/(includes|cache|docs|conf)/ { - deny all; - return 404; - } - - # Bloquer les fichiers samples et backups - location ~* \.(sample|bak|backup|log|tmp)$ { - deny all; - return 404; - } - - # Bloquer l'accès aux fichiers cachés - location ~ /\. { - deny all; - return 404; - } - - # Empêcher l'exploration des répertoires - autoindex off; - - # ====================== - # RÉÉCRITURE D'URL - # ====================== - - # Masquer l'extension .php et redirection - location / { - try_files $uri $uri/ @rewrite; - } - - location @rewrite { - rewrite ^/([^.]+)$ /$1.php last; - } - - # Rediriger les URLs avec .php vers les URLs sans extension - location ~ ^/(.+)\.php$ { - return 301 /$1; - } - - # Traitement des fichiers PHP - location ~ \.php$ { - include fastcgi_params; - fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adaptez selon votre version PHP - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param SCRIPT_NAME $fastcgi_script_name; - } - - # ====================== - # OPTIMISATIONS - # ====================== - - # Cache des fichiers statiques - location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - access_log off; - } - - # Compression gzip - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; - - # Headers de sécurité - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; -} \ No newline at end of file diff --git a/countdown.php b/countdown.php deleted file mode 100644 index aae4781..0000000 --- a/countdown.php +++ /dev/null @@ -1,187 +0,0 @@ - - - - - - - - <?php echo SITE_NAME; ?> - Ouverture prochaine - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - -

- - -

Plateforme Multimédia - Ouverture prochaine

- - -
-
- 00 - Jours -
-
- 00 - Heures -
-
- 00 - Minutes -
-
- 00 - Secondes -
-
- - -
-

La plateforme ouvrira ses portes le getTimestamp()); - ?> (heure de La Réunion).

- - -
- 'America/Martinique', - 'Guyane' => 'America/Cayenne', - 'France' => 'Europe/Paris', - 'Ma\'ohi Nui' => 'Pacific/Tahiti', - 'Kanaky' => 'Pacific/Noumea' - ]; - - foreach($timezones as $name => $timezone): - $targetDateLocal = new DateTime(COUNTDOWN_TARGET_DATE, new DateTimeZone(DEFAULT_TIMEZONE)); - $targetDateLocal->setTimezone(new DateTimeZone($timezone)); - - // Vérifier si c'est un jour différent - $reunionDate = new DateTime(COUNTDOWN_TARGET_DATE); - $dayDiff = $targetDateLocal->format('j') - $reunionDate->format('j'); - - $dayIndicator = ''; - if ($dayDiff > 0) { - $dayIndicator = ' +1j'; - } elseif ($dayDiff < 0) { - $dayIndicator = ' -1j'; - } - ?> -
- - - format('H:i'); ?> - - -
- -
- -

Restez connectés et suivez-nous sur nos réseaux sociaux !

-
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - - - \ No newline at end of file diff --git a/css/castopod-podcasts.css b/css/castopod-podcasts.css deleted file mode 100644 index af3c48c..0000000 --- a/css/castopod-podcasts.css +++ /dev/null @@ -1,279 +0,0 @@ -/* Styles pour la liste compacte de podcasts Castopod */ - -.castopod-container { - background: var(--card-bg); - border-radius: 12px; - border: 1px solid var(--border-color); - box-shadow: var(--card-shadow); - overflow: hidden; - height: 400px; - display: flex; - flex-direction: column; -} - -.castopod-section { - width: 100%; - min-width: 0; - margin-bottom: 1em; -} - -.castopod-header { - background: var(--primary-red); - color: white; - padding: 12px 16px; - font-size: 1rem; - font-weight: bold; - display: flex; - align-items: center; - gap: 8px; -} - -.castopod-header a { - color: white; - text-decoration: none; - transition: opacity 0.2s ease; -} - -.castopod-header a:hover { - opacity: 0.8; - text-decoration: underline; -} - -.castopod-episodes-list { - flex: 1; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: var(--primary-red) transparent; -} - -.castopod-episodes-list::-webkit-scrollbar { - width: 6px; -} - -.castopod-episodes-list::-webkit-scrollbar-track { - background: transparent; -} - -.castopod-episodes-list::-webkit-scrollbar-thumb { - background-color: var(--primary-red); - border-radius: 3px; -} - -.castopod-episode-item { - display: flex; - padding: 8px 12px; - border-bottom: 1px solid var(--border-color); - color: inherit; - transition: background-color 0.2s ease; - gap: 8px; - position: relative; -} - -.castopod-episode-item:hover { - background: var(--hover-bg); -} - -.castopod-episode-item:last-child { - border-bottom: none; -} - -.castopod-episode-item.playing { - background: var(--hover-bg); -} - -.castopod-episode-item.playing .castopod-episode-title { - color: var(--primary-red); -} - -.castopod-episode-thumb { - width: 60px; - height: 60px; - border-radius: 8px; - overflow: hidden; - flex-shrink: 0; - background: var(--tag-bg); - display: flex; - align-items: center; - justify-content: center; - color: var(--text-secondary); - position: relative; - border: none; - padding: 0; - cursor: pointer; - transition: transform 0.2s ease; -} - -.castopod-episode-thumb:hover { - transform: scale(1.05); -} - -.castopod-episode-thumb img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.castopod-play-icon, -.castopod-pause-icon { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 1.5rem; - color: rgba(255, 255, 255, 0.9); - text-shadow: 0 0 10px rgba(0, 0, 0, 0.8); - opacity: 0; - transition: opacity 0.2s ease; - pointer-events: none; -} - -.castopod-episode-item:hover .castopod-play-icon { - opacity: 1; -} - -.castopod-episode-item.playing .castopod-play-icon { - opacity: 0; -} - -.castopod-episode-item.playing .castopod-pause-icon { - opacity: 1; -} - -.castopod-episode-item.playing:hover .castopod-pause-icon { - opacity: 1; -} - -.castopod-episode-info { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - justify-content: center; -} - -.castopod-episode-title { - font-size: 0.8rem; - font-weight: 600; - color: var(--text-color); - margin: 0 0 4px 0; - line-height: 1.2; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.castopod-podcast-badge { - font-size: 0.65rem; - color: var(--primary-red); - font-weight: 600; - margin: 0 0 4px 0; - display: flex; - align-items: center; - gap: 4px; -} - -.castopod-podcast-badge i { - font-size: 0.6rem; -} - -.castopod-episode-meta { - font-size: 0.65rem; - color: var(--text-secondary); - display: flex; - gap: 6px; - align-items: center; - flex-wrap: wrap; -} - -.castopod-episode-meta i { - font-size: 0.6rem; -} - -.castopod-no-episodes { - text-align: center; - color: var(--text-secondary); - padding: 20px; - font-style: italic; - font-size: 0.9rem; -} - -/* Lien externe vers Castopod */ -.castopod-external-link { - display: flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - flex-shrink: 0; - color: var(--text-secondary); - text-decoration: none; - border-radius: 50%; - transition: all 0.2s ease; - margin-left: auto; -} - -.castopod-external-link:hover { - background: var(--tag-bg); - color: var(--primary-red); - transform: scale(1.1); -} - -.castopod-external-link i { - font-size: 0.85rem; -} - -/* Masquer l'élément audio */ -.castopod-episode-item audio { - display: none; -} - -/* Ajustement du layout selon les contenus présents */ -/* Ordre dans le HTML : 1. Castopod, 2. Mastodon, 3. WordPress */ - - -/* Responsive Design */ -@media (max-width: 1024px) { - /* Mobile/Tablette: layout vertical */ - .timeline-container:has(.castopod-section) { - grid-template-columns: 1fr !important; - } - - .castopod-section { - width: 100%; - } - - .castopod-container { - height: auto; - max-height: 400px; - } -} - -@media (max-width: 768px) { - .castopod-episode-thumb { - width: 65px; - height: 65px; - } - - .castopod-episode-item { - padding: 10px 12px; - gap: 10px; - } - - .castopod-episode-title { - font-size: 0.85rem; - } - - .castopod-episode-meta { - font-size: 0.65rem; - } - - .castopod-header { - padding: 10px 12px; - font-size: 0.9rem; - } - - .castopod-play-icon { - font-size: 1.3rem; - } -} diff --git a/css/categories.css b/css/categories.css deleted file mode 100644 index 4721217..0000000 --- a/css/categories.css +++ /dev/null @@ -1,157 +0,0 @@ -/* Styles pour la page des catégories */ - -/* Page d'accueil des catégories */ -.categories-showcase { - margin: 30px 0; -} - -.categories-showcase h1 { - font-size: 2rem; - margin-bottom: 10px; - color: #333; - text-align: center; -} - -.section-description { - text-align: center; - color: #666; - margin-bottom: 30px; -} - -.categories-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 20px; -} - -.category-card-large { - position: relative; - height: 200px; - border-radius: 8px; - overflow: hidden; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); - transition: transform 0.3s; -} - -.category-card-large:hover { - transform: translateY(-5px); -} - -.category-card-large img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.category-card-large .category-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient(to top, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.3)); - display: flex; - flex-direction: column; - justify-content: flex-end; - padding: 20px; -} - -.category-name-large { - color: white; - font-size: 1.5rem; - font-weight: 600; - margin-bottom: 5px; -} - -.category-description-short { - color: rgba(255, 255, 255, 0.8); - font-size: 0.9rem; -} - -/* Page d'une catégorie spécifique */ -.category-header { - margin-bottom: 30px; -} - -.category-banner { - height: 250px; - background-size: cover; - background-position: center; - position: relative; - border-radius: 8px; - overflow: hidden; - margin-bottom: 20px; -} - -.category-banner .category-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient(to top, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.3)); - display: flex; - align-items: center; - justify-content: center; -} - -.category-banner h1 { - color: white; - font-size: 2.5rem; - text-align: center; - text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); -} - -.category-description { - text-align: center; - max-width: 800px; - margin: 0 auto; - color: #555; - font-size: 1.1rem; - line-height: 1.6; -} - -.category-videos { - margin-bottom: 40px; -} - -.no-videos { - text-align: center; - padding: 40px 0; - color: #666; -} - -/* Media Queries */ -@media (min-width: 768px) { - .categories-grid { - grid-template-columns: repeat(3, 1fr); - } -} - -@media (min-width: 992px) { - .categories-grid { - grid-template-columns: repeat(4, 1fr); - } -} - -@media (max-width: 576px) { - .categories-grid { - grid-template-columns: 1fr; - } - - .category-card-large { - height: 150px; - } - - .category-banner { - height: 180px; - } - - .category-banner h1 { - font-size: 1.8rem; - } - - .category-description { - font-size: 0.95rem; - } -} \ No newline at end of file diff --git a/css/countdown.css b/css/countdown.css deleted file mode 100644 index a5098a5..0000000 --- a/css/countdown.css +++ /dev/null @@ -1,350 +0,0 @@ -/* Styles pour la page de compte à rebours */ -body { - margin: 0; - padding: 0; - overflow-x: hidden; -} - -.countdown-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - min-height: 100vh; - background: linear-gradient(135deg, #000000 0%, #1a1a1a 50%, #000000 100%); - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: center; - z-index: 9999; - color: white; - text-align: center; - padding: 20px; - box-sizing: border-box; -} - -.countdown-container { - max-width: 800px; - width: 100%; - margin: auto; - padding: 40px 0; -} - -.countdown-logo { - width: 150px; - height: auto; - margin-bottom: 30px; - filter: drop-shadow(0 0 20px rgba(255, 0, 0, 0.3)); -} - -.countdown-title { - font-size: 3rem; - font-weight: bold; - margin-bottom: 10px; - color: #FF0000; - text-shadow: 0 0 30px rgba(255, 0, 0, 0.5); -} - -.countdown-subtitle { - font-size: 1.5rem; - margin-bottom: 20px; - color: #ffffff; - opacity: 0.9; -} - -.countdown-timer { - display: flex; - justify-content: center; - gap: 30px; - margin-bottom: 40px; - flex-wrap: wrap; -} - -.countdown-unit { - text-align: center; - background: rgba(255, 0, 0, 0.1); - border: 2px solid #FF0000; - border-radius: 15px; - padding: 20px; - min-width: 120px; - backdrop-filter: blur(10px); - transition: transform 0.3s ease, box-shadow 0.3s ease; -} - -.countdown-unit:hover { - transform: translateY(-5px); - box-shadow: 0 10px 30px rgba(255, 0, 0, 0.3); -} - -.countdown-number { - font-size: 3rem; - font-weight: bold; - color: #FF0000; - display: block; - text-shadow: 0 0 20px rgba(255, 0, 0, 0.6); -} - -.countdown-label { - font-size: 1rem; - color: #ffffff; - text-transform: uppercase; - letter-spacing: 1px; - margin-top: 10px; -} - -.countdown-message { - font-size: 1.2rem; - margin-bottom: 30px; - color: #ffffff; - opacity: 0.8; - line-height: 1.6; -} - -.countdown-other-timezones { - margin: 25px 0; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 12px; - max-width: 800px; - margin-left: auto; - margin-right: auto; -} - -.timezone-item { - background: linear-gradient(135deg, rgba(255, 0, 0, 0.15) 0%, rgba(255, 0, 0, 0.05) 100%); - padding: 12px 16px; - border-radius: 12px; - font-size: 0.95rem; - color: #ffffff; - backdrop-filter: blur(15px); - border: 1px solid rgba(255, 0, 0, 0.3); - text-align: center; - transition: all 0.3s ease; - position: relative; - overflow: hidden; -} - -.timezone-item::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); - transition: left 0.5s ease; -} - -.timezone-item:hover { - transform: translateY(-2px); - box-shadow: 0 8px 25px rgba(255, 0, 0, 0.2); - border-color: rgba(255, 0, 0, 0.5); -} - -.timezone-item:hover::before { - left: 100%; -} - -.timezone-name { - display: block; - font-weight: bold; - color: #FF0000; - margin-bottom: 4px; - font-size: 0.8rem; - text-transform: uppercase; - letter-spacing: 0.3px; - line-height: 1.1; - word-wrap: break-word; - overflow-wrap: break-word; -} - -.timezone-time { - display: block; - font-size: 1.1rem; - font-weight: bold; - text-shadow: 0 0 10px rgba(255, 255, 255, 0.3); -} - -.day-indicator { - font-size: 0.75rem; - font-weight: bold; - padding: 2px 6px; - border-radius: 10px; - margin-left: 5px; - text-shadow: none; -} - -.day-indicator.next-day { - background: rgba(0, 255, 0, 0.2); - color: #00ff88; - border: 1px solid rgba(0, 255, 0, 0.4); -} - -.day-indicator.prev-day { - background: rgba(255, 165, 0, 0.2); - color: #ffaa00; - border: 1px solid rgba(255, 165, 0, 0.4); -} - -.countdown-social { - display: flex; - justify-content: center; - gap: 20px; - margin-top: 40px; -} - -.countdown-social a { - color: #ffffff; - font-size: 2rem; - transition: all 0.3s ease; -} - -.countdown-social a:hover { - color: #FF0000; - transform: scale(1.2); - box-shadow: 0 0 20px rgba(255, 0, 0, 0.4); -} - -/* Animation de pulsation pour le titre */ -@keyframes pulse { - 0%, 100% { - text-shadow: 0 0 30px rgba(255, 0, 0, 0.5); - } - 50% { - text-shadow: 0 0 50px rgba(255, 0, 0, 0.8); - } -} - -.countdown-title { - animation: pulse 2s infinite; -} - -/* Responsive */ -@media (max-width: 768px) { - .countdown-container { - padding: 20px 0; - } - - .countdown-title { - font-size: 2rem; - } - - .countdown-subtitle { - font-size: 1.2rem; - } - - .countdown-timer { - gap: 15px; - } - - .countdown-unit { - min-width: 80px; - padding: 15px 10px; - } - - .countdown-number { - font-size: 2rem; - } - - .countdown-label { - font-size: 0.8rem; - } - - .countdown-logo { - width: 100px; - } - - .countdown-other-timezones { - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: 10px; - } - - .timezone-item { - padding: 10px 12px; - } - - .timezone-name { - font-size: 0.75rem; - } - - .timezone-time { - font-size: 1rem; - } - - .day-indicator { - font-size: 0.65rem; - padding: 1px 4px; - } - - .countdown-social { - gap: 15px; - } - - .countdown-social a { - font-size: 1.5rem; - } -} - -@media (max-width: 480px) { - .countdown-timer { - gap: 10px; - } - - .countdown-unit { - min-width: 70px; - padding: 10px 5px; - } - - .countdown-number { - font-size: 1.5rem; - } - - .countdown-label { - font-size: 0.7rem; - } - - .countdown-other-timezones { - grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); - gap: 8px; - } - - .timezone-item { - padding: 8px 10px; - } - - .timezone-name { - font-size: 0.7rem; - } - - .timezone-time { - font-size: 0.9rem; - } - - .day-indicator { - font-size: 0.6rem; - padding: 1px 3px; - margin-left: 3px; - } -} - -/* Animation d'entrée */ -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(30px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.countdown-container > * { - animation: fadeInUp 0.8s ease forwards; -} - -.countdown-container > *:nth-child(1) { animation-delay: 0.1s; } -.countdown-container > *:nth-child(2) { animation-delay: 0.2s; } -.countdown-container > *:nth-child(3) { animation-delay: 0.3s; } -.countdown-container > *:nth-child(4) { animation-delay: 0.4s; } -.countdown-container > *:nth-child(5) { animation-delay: 0.5s; } \ No newline at end of file diff --git a/css/donations.css b/css/donations.css deleted file mode 100644 index 17988db..0000000 --- a/css/donations.css +++ /dev/null @@ -1,491 +0,0 @@ -/* Styles pour la page de dons */ - -/* Ajustements pour la page de dons */ -.main-content .container { - max-width: none; - padding: 0; -} - -/* Hero section */ -.donation-hero { - background: linear-gradient(135deg, var(--primary-red), #cc0000); - color: white; - padding: 60px 0; - text-align: center; - margin-bottom: 40px; -} - -.donation-hero-content h1 { - font-size: 2.5rem; - margin-bottom: 16px; - font-weight: bold; -} - -.donation-hero-content h1 i { - color: #ffdddd; - margin-right: 12px; -} - -.hero-subtitle { - font-size: 1.2rem; - opacity: 0.9; - max-width: 600px; - margin: 0 auto; -} - -/* Section principale */ -.donation-main { - margin-bottom: 60px; - max-width: 1200px; - margin-left: auto; - margin-right: auto; - padding: 0 20px; -} - -.donation-content { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 40px; - align-items: start; -} - -/* Message de don */ -.donation-message h2 { - color: var(--primary-red); - margin-bottom: 20px; - font-size: 1.8rem; -} - -.donation-message p { - margin-bottom: 16px; - line-height: 1.6; - color: var(--text-color); -} - -.donation-message ul { - list-style: none; - padding: 0; - margin: 20px 0; -} - -.donation-message li { - padding: 8px 0; - display: flex; - align-items: center; - color: var(--text-color); -} - -.donation-message li i { - color: var(--primary-red); - margin-right: 12px; - width: 20px; - text-align: center; -} - -/* Interface de don */ -.donation-interface { - background: var(--card-bg); - border-radius: 12px; - padding: 30px; - box-shadow: var(--card-shadow); - border: 1px solid var(--border-color); -} - -.donation-interface h3 { - color: var(--primary-red); - margin-bottom: 24px; - font-size: 1.5rem; - text-align: center; -} - -/* Boutons de montants */ -.donation-amounts { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 12px; - margin-bottom: 30px; -} - -.donation-btn { - background: white; - border: 2px solid var(--border-color); - border-radius: 8px; - padding: 16px 20px; - font-size: 1.2rem; - font-weight: bold; - color: var(--text-color); - cursor: pointer; - transition: all 0.3s ease; - text-align: center; -} - -.donation-btn:hover { - border-color: var(--primary-red); - background: #fff5f5; - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(255, 0, 0, 0.15); -} - -.donation-btn.active { - background: var(--primary-red); - color: white; - border-color: var(--primary-red); - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(255, 0, 0, 0.3); -} - -/* Montant personnalisé */ -.custom-amount { - margin-bottom: 24px; -} - -.custom-amount label { - display: block; - margin-bottom: 12px; - font-weight: 600; - color: var(--text-color); -} - -.custom-amount-input { - display: flex; - align-items: center; - gap: 8px; -} - -.custom-amount-input input { - flex: 1; - padding: 12px 16px; - border: 2px solid var(--border-color); - border-radius: 8px; - font-size: 1.1rem; - transition: border-color 0.3s ease; -} - -.custom-amount-input input:focus { - outline: none; - border-color: var(--primary-red); - box-shadow: 0 0 0 3px rgba(255, 0, 0, 0.1); -} - -.custom-amount-input .currency { - font-weight: bold; - color: var(--text-secondary); - font-size: 1.1rem; -} - -.btn-custom-donate { - background: var(--primary-red); - color: white; - border: none; - border-radius: 8px; - padding: 12px 24px; - font-weight: bold; - cursor: pointer; - transition: all 0.3s ease; - font-size: 1rem; -} - -.btn-custom-donate:hover { - background: #cc0000; - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(255, 0, 0, 0.3); -} - -/* Sécurité */ -.donation-security { - text-align: center; - padding: 20px; - background: #f8f9fa; - border-radius: 8px; - border: 1px solid #e9ecef; -} - -.donation-security p { - margin: 0; - color: var(--text-secondary); - font-size: 0.9rem; -} - -.donation-security i { - color: #28a745; - margin-right: 8px; -} - -/* Section d'informations */ -.donation-info { - margin-top: 60px; - max-width: 1200px; - margin-left: auto; - margin-right: auto; - padding: 0 20px; -} - -.info-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 30px; -} - -.info-card { - text-align: center; - padding: 30px 20px; - background: var(--card-bg); - border-radius: 12px; - border: 1px solid var(--border-color); - transition: transform 0.3s ease, box-shadow 0.3s ease; -} - -.info-card:hover { - transform: translateY(-4px); - box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); -} - -.info-card i { - font-size: 2.5rem; - color: var(--primary-red); - margin-bottom: 16px; -} - -.info-card h4 { - color: var(--text-color); - margin-bottom: 12px; - font-size: 1.3rem; -} - -.info-card p { - color: var(--text-secondary); - line-height: 1.5; - margin: 0; -} - -/* Responsive Design */ -@media (max-width: 1024px) { - .donation-content { - grid-template-columns: 1fr; - gap: 30px; - } - - .info-grid { - grid-template-columns: 1fr; - gap: 20px; - } -} - -@media (max-width: 768px) { - .donation-hero { - padding: 40px 0; - } - - .donation-hero-content h1 { - font-size: 2rem; - } - - .hero-subtitle { - font-size: 1.1rem; - } - - .donation-interface { - padding: 20px; - } - - .donation-amounts { - grid-template-columns: 1fr; - } - - .custom-amount-input { - flex-direction: column; - align-items: stretch; - } - - .custom-amount-input .currency { - order: -1; - text-align: center; - margin-bottom: 8px; - } -} - -@media (max-width: 480px) { - .donation-hero-content h1 { - font-size: 1.6rem; - } - - .donation-btn { - padding: 14px 16px; - font-size: 1.1rem; - } -} - -/* Plateformes de don externes */ -.donation-platforms { - background: var(--card-bg); - border-radius: 12px; - padding: 30px; - box-shadow: var(--card-shadow); - border: 1px solid var(--border-color); - margin-bottom: 30px; -} - -.donation-platforms h3 { - color: var(--primary-red); - margin-bottom: 24px; - font-size: 1.5rem; - text-align: center; -} - -.platforms-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 16px; -} - -.platform-btn { - display: flex; - align-items: center; - gap: 16px; - padding: 20px; - background: var(--card-bg); - border: 2px solid var(--border-color); - border-radius: 12px; - text-decoration: none; - color: var(--text-color); - transition: all 0.3s ease; -} - -.platform-btn:hover { - transform: translateY(-4px); - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); -} - -.platform-btn.liberapay { - border-color: #f6c915; -} - -.platform-btn.liberapay:hover { - background: rgba(246, 201, 21, 0.1); - border-color: #f6c915; -} - -.platform-btn.kofi { - border-color: #13C3FF; -} - -.platform-btn.kofi:hover { - background: rgba(19, 195, 255, 0.1); - border-color: #13C3FF; -} - -.platform-icon { - width: 60px; - height: 60px; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; -} - -.platform-icon svg { - width: 100%; - height: 100%; -} - -.platform-info { - display: flex; - flex-direction: column; - gap: 4px; -} - -.platform-name { - font-size: 1.2rem; - font-weight: bold; - color: var(--text-color); -} - -.platform-desc { - font-size: 0.9rem; - color: var(--text-secondary); -} - -/* Onglets don ponctuel / mensuel */ -.donation-tabs { - display: flex; - gap: 8px; - margin-bottom: 24px; - background: var(--main-bg); - padding: 4px; - border-radius: 8px; -} - -.tab-btn { - flex: 1; - padding: 12px 16px; - background: transparent; - border: none; - border-radius: 6px; - font-weight: 600; - color: var(--text-secondary); - cursor: pointer; - transition: all 0.3s ease; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; -} - -.tab-btn:hover { - background: var(--hover-bg); - color: var(--text-color); -} - -.tab-btn.active { - background: var(--primary-red); - color: white; -} - -.tab-btn i { - font-size: 0.9rem; -} - -.tab-content { - display: none; -} - -.tab-content.active { - display: block; -} - -.donation-btn-custom { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; -} - -/* Dark theme adjustments */ -[data-theme="dark"] .donation-security { - background: var(--card-bg); - border-color: var(--border-color); -} - -[data-theme="dark"] .donation-btn { - background: var(--card-bg); - border-color: var(--border-color); -} - -[data-theme="dark"] .donation-btn:hover { - background: rgba(255, 0, 0, 0.1); -} - -[data-theme="dark"] .platform-btn { - background: var(--main-bg); -} - -/* Responsive pour les plateformes */ -@media (max-width: 768px) { - .platforms-grid { - grid-template-columns: 1fr; - } - - .donation-tabs { - flex-direction: column; - } -} \ No newline at end of file diff --git a/css/funkwhale-tracks.css b/css/funkwhale-tracks.css deleted file mode 100644 index e110685..0000000 --- a/css/funkwhale-tracks.css +++ /dev/null @@ -1,273 +0,0 @@ -/* Styles pour la liste compacte de morceaux Funkwhale */ - -.funkwhale-container { - background: var(--card-bg); - border-radius: 12px; - border: 1px solid var(--border-color); - box-shadow: var(--card-shadow); - overflow: hidden; - height: 400px; - display: flex; - flex-direction: column; -} - -.funkwhale-section { - width: 100%; - min-width: 0; -} - -.funkwhale-header { - background: var(--primary-red); - color: white; - padding: 12px 16px; - font-size: 1rem; - font-weight: bold; - display: flex; - align-items: center; - gap: 8px; -} - -.funkwhale-header a { - color: white; - text-decoration: none; - transition: opacity 0.2s ease; -} - -.funkwhale-header a:hover { - opacity: 0.8; - text-decoration: underline; -} - -.funkwhale-tracks-list { - flex: 1; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: var(--primary-red) transparent; -} - -.funkwhale-tracks-list::-webkit-scrollbar { - width: 6px; -} - -.funkwhale-tracks-list::-webkit-scrollbar-track { - background: transparent; -} - -.funkwhale-tracks-list::-webkit-scrollbar-thumb { - background-color: var(--primary-red); - border-radius: 3px; -} - -.funkwhale-track-item { - display: flex; - padding: 8px 12px; - border-bottom: 1px solid var(--border-color); - color: inherit; - transition: background-color 0.2s ease; - gap: 8px; - position: relative; -} - -.funkwhale-track-item:hover { - background: var(--hover-bg); -} - -.funkwhale-track-item:last-child { - border-bottom: none; -} - -.funkwhale-track-item.playing { - background: var(--hover-bg); -} - -.funkwhale-track-item.playing .funkwhale-track-title { - color: var(--primary-red); -} - -.funkwhale-track-thumb { - width: 60px; - height: 60px; - border-radius: 8px; - overflow: hidden; - flex-shrink: 0; - background: var(--tag-bg); - display: flex; - align-items: center; - justify-content: center; - color: var(--text-secondary); - position: relative; - border: none; - padding: 0; - cursor: pointer; - transition: transform 0.2s ease; -} - -.funkwhale-track-thumb:hover { - transform: scale(1.05); -} - -.funkwhale-track-thumb img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.funkwhale-play-icon, -.funkwhale-pause-icon { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 1.5rem; - color: rgba(255, 255, 255, 0.9); - text-shadow: 0 0 10px rgba(0, 0, 0, 0.8); - opacity: 0; - transition: opacity 0.2s ease; - pointer-events: none; -} - -.funkwhale-track-item:hover .funkwhale-play-icon { - opacity: 1; -} - -.funkwhale-track-item.playing .funkwhale-play-icon { - opacity: 0; -} - -.funkwhale-track-item.playing .funkwhale-pause-icon { - opacity: 1; -} - -.funkwhale-track-item.playing:hover .funkwhale-pause-icon { - opacity: 1; -} - -.funkwhale-track-info { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - justify-content: center; -} - -.funkwhale-track-title { - font-size: 0.8rem; - font-weight: 600; - color: var(--text-color); - margin: 0 0 4px 0; - line-height: 1.2; - display: -webkit-box; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.funkwhale-track-artist { - font-size: 0.7rem; - color: var(--primary-red); - font-weight: 600; - margin: 0 0 4px 0; - display: flex; - align-items: center; - gap: 4px; -} - -.funkwhale-track-artist i { - font-size: 0.65rem; -} - -.funkwhale-track-meta { - font-size: 0.65rem; - color: var(--text-secondary); - display: flex; - gap: 6px; - align-items: center; - flex-wrap: wrap; -} - -.funkwhale-track-meta i { - font-size: 0.6rem; -} - -.funkwhale-no-tracks { - text-align: center; - color: var(--text-secondary); - padding: 20px; - font-style: italic; - font-size: 0.9rem; -} - -/* Lien externe vers Funkwhale */ -.funkwhale-external-link { - display: flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - flex-shrink: 0; - color: var(--text-secondary); - text-decoration: none; - border-radius: 50%; - transition: all 0.2s ease; - margin-left: auto; -} - -.funkwhale-external-link:hover { - background: var(--tag-bg); - color: var(--primary-red); - transform: scale(1.1); -} - -.funkwhale-external-link i { - font-size: 0.85rem; -} - -/* Masquer l'élément audio */ -.funkwhale-track-item audio { - display: none; -} - -/* Responsive Design */ -@media (max-width: 1024px) { - .funkwhale-section { - width: 100%; - } - - .funkwhale-container { - height: auto; - max-height: 400px; - } -} - -@media (max-width: 768px) { - .funkwhale-track-thumb { - width: 65px; - height: 65px; - } - - .funkwhale-track-item { - padding: 10px 12px; - gap: 10px; - } - - .funkwhale-track-title { - font-size: 0.85rem; - } - - .funkwhale-track-artist { - font-size: 0.7rem; - } - - .funkwhale-track-meta { - font-size: 0.65rem; - } - - .funkwhale-header { - padding: 10px 12px; - font-size: 0.9rem; - } - - .funkwhale-play-icon { - font-size: 1.3rem; - } -} diff --git a/css/grid-layout.css b/css/grid-layout.css deleted file mode 100644 index 732bcfc..0000000 --- a/css/grid-layout.css +++ /dev/null @@ -1,117 +0,0 @@ -/* Layout grid pour Hero, Castopod, Funkwhale, Mastodon et WordPress */ -/* Ce fichier surcharge les anciens styles pour le nouveau layout responsive */ - -/* Force le wrapper parent à être un simple bloc, pas une grille */ -.hero-mastodon-wrapper { - display: block !important; - grid-template-columns: none !important; - width: 100%; - padding: 0 20px; - margin-bottom: 30px; -} - -/* Le vrai container grid qui contient tous les éléments */ -.timeline-wordpress-container { - display: grid !important; - gap: 20px; - margin: 0; - width: 100%; -} - -/* Supprime toutes les limitations de largeur sur les anciens containers */ -.timeline-container { - max-width: none !important; - width: 100% !important; - justify-self: auto !important; - grid-template-columns: none !important; - display: block !important; -} - -/* S'assurer que les sections prennent toute la largeur disponible */ -.castopod-section, -.funkwhale-section, -.mastodon-section, -.wordpress-section { - width: 100% !important; - max-width: none !important; - min-width: 0 !important; -} - -/* Desktop : 2 éléments par ligne */ -@media (min-width: 1200px) { - .timeline-wordpress-container { - grid-template-columns: repeat(2, 1fr) !important; - } - - /* Hero prend toute la largeur si présent */ - .hero-video-container, - .hero-live-container, - .hero-playlist-container { - grid-column: 1 / -1 !important; - } - - /* Centrer Mastodon quand il est seul (HERO_TYPE='none', pas de Castopod ni Funkwhale) */ - .timeline-wordpress-container:not(:has(.castopod-section)):not(:has(.funkwhale-section)) { - grid-template-columns: 1fr !important; - justify-items: center !important; - } - - .timeline-wordpress-container:not(:has(.castopod-section)):not(:has(.funkwhale-section)) .mastodon-section { - max-width: 800px !important; - width: 100% !important; - } - - /* Centrer le dernier élément quand il est seul sur sa ligne (nombre impair d'éléments) */ - /* Cas : Castopod + Funkwhale + Mastodon (3 éléments) */ - /* Mastodon sera le 3e élément (impair) et doit être centré */ - .timeline-wordpress-container:has(.castopod-section):has(.funkwhale-section) .mastodon-section:nth-child(3) { - grid-column: 1 / -1 !important; - max-width: 800px !important; - width: 800px !important; - margin: 0 auto !important; - justify-self: center !important; - } -} - -/* Tablette et Mobile : 1 élément par ligne */ -@media (max-width: 1199px) { - .timeline-wordpress-container { - grid-template-columns: 1fr !important; - } - - .hero-mastodon-wrapper { - padding: 0 15px; - } - - /* En mobile, tous les éléments prennent toute la largeur */ - .castopod-section, - .funkwhale-section, - .mastodon-section, - .wordpress-section { - width: 100% !important; - max-width: none !important; - } -} - -@media (max-width: 768px) { - .hero-mastodon-wrapper { - padding: 0 10px; - } -} - -/* Hauteurs fixes pour les containers scrollables */ -.castopod-container, -.funkwhale-container, -.mt-container, -.wordpress-container { - height: 400px; -} - -@media (max-width: 768px) { - .castopod-container, - .funkwhale-container, - .mt-container, - .wordpress-container { - height: 350px; - } -} diff --git a/css/mastodon-timeline.min.css b/css/mastodon-timeline.min.css deleted file mode 100644 index 3007ca3..0000000 --- a/css/mastodon-timeline.min.css +++ /dev/null @@ -1 +0,0 @@ -.mt-container,.mt-container[data-theme=light],.mt-dialog,.mt-dialog[data-theme=light]{--mt-txt-max-lines:none;--mt-preview-max-lines:none;--mt-color-bg:#fff;--mt-color-bg-hover:#e9eef1;--mt-color-line-gray:#c0cdd9;--mt-color-contrast-gray:#606984;--mt-color-content-txt:#000;--mt-color-hashtag:#d7e1e9;--mt-color-link:#3a3bff;--mt-color-error-txt:#8b0000;--mt-color-btn-bg:#6364ff;--mt-color-btn-bg-hover:#563acc;--mt-color-btn-txt:#fff;--mt-color-backdrop:#00000090;--mt-color-placeholder:#60698425}.mt-container[data-theme=dark],.mt-dialog[data-theme=dark]{--mt-color-bg:#181821;--mt-color-bg-hover:#21232c;--mt-color-line-gray:#393f4f;--mt-color-contrast-gray:#606984;--mt-color-content-txt:#fff;--mt-color-hashtag:#292c38;--mt-color-link:#8c8dff;--mt-color-error-txt:#fe6c6c}.mt-container button,.mt-dialog button{font:inherit}.mt-container a,.mt-container button,.mt-dialog button{cursor:pointer}.mt-container{display:flex;flex-direction:column;height:100%;overflow-y:auto;position:relative;background-color:var(--mt-color-bg);scrollbar-color:var(--mt-color-contrast-gray) var(--mt-color-bg);scrollbar-width:auto;container:mt-container/inline-size}.mt-container::-webkit-scrollbar{width:.25rem;height:.25rem}.mt-container::-webkit-scrollbar-thumb{background-color:var(--mt-color-contrast-gray);border:none;border-radius:3rem}.mt-container::-webkit-scrollbar-thumb:active,.mt-container::-webkit-scrollbar-thumb:hover{background-color:var(--mt-color-contrast-gray)}.mt-container::-webkit-scrollbar-track{background-color:var(--mt-color-bg);border:none;border-radius:0}.mt-container::-webkit-scrollbar-corner,.mt-container::-webkit-scrollbar-track:active,.mt-container::-webkit-scrollbar-track:hover{background-color:var(--mt-color-bg)}.mt-container a{text-decoration:none;color:var(--mt-color-link)}.mt-container a:not(.mt-post-preview):hover{text-decoration:underline}.mt-body{padding:1rem;white-space:pre-wrap;word-wrap:break-word;margin-bottom:1rem}.mt-body .invisible{font-size:0;line-height:0;display:inline-block;width:0;height:0;position:absolute}.mt-post{margin:.25rem;padding:1rem .5rem;position:relative;min-height:3.75rem;background-color:transparent;border-bottom:1px solid var(--mt-color-line-gray)}.mt-post:focus,.mt-post:hover{cursor:pointer;background-color:var(--mt-color-bg-hover)}.mt-container:not(:has(.mt-footer)) .mt-post:last-child{border-bottom:none}.mt-post p:last-child{margin-bottom:0}.mt-post-avatar{margin-right:.75rem}.mt-post-avatar-standard{width:2.25rem;height:2.25rem}.mt-post-avatar-boosted{width:3rem;height:3rem;position:relative}.mt-post-avatar-image-big img,.mt-post-avatar-image-small img{aspect-ratio:1/1;border-radius:.25rem;overflow:hidden}.mt-post-avatar-image-big img{width:2.25rem;height:2.25rem}.mt-post-avatar-image-small img{width:1.5rem;height:1.5rem;top:1.5rem;left:1.5rem;position:absolute}.mt-post-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.mt-post-header-user{overflow:hidden;padding-right:.75rem}.mt-post-header-user .mt-custom-emoji{height:1rem;min-width:1rem;width:auto}.mt-container .mt-post-header-user>a{color:var(--mt-color-content-txt);overflow-wrap:anywhere}.mt-container .mt-post-header-user>a:hover{text-decoration:none}.mt-post-header-user-name{font-weight:600}.mt-container .mt-post-header-user:hover .mt-post-header-user-name{text-decoration:underline}.mt-post-header-user-account{display:block;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:var(--mt-color-contrast-gray)}.mt-post-header-date{display:flex;font-size:.75rem;text-align:right;margin-left:auto}.mt-post-header-date .mt-post-pinned{width:1.25rem;margin-top:-.25rem;fill:var(--mt-color-contrast-gray)}.mt-container .mt-post-header-date>a{white-space:nowrap;color:var(--mt-color-contrast-gray)!important}.mt-post-txt{margin-bottom:1rem;color:var(--mt-color-content-txt)}.mt-post-txt .spoiler-txt-hidden{display:none}.mt-post-txt.truncate{display:-webkit-box;overflow:hidden;-webkit-line-clamp:var(--mt-txt-max-lines);-webkit-box-orient:vertical}.mt-post-txt:not(.truncate) .ellipsis::after{content:"..."}.mt-post-txt blockquote{border-left:.25rem solid var(--mt-color-line-gray);margin-left:0;padding-left:.5rem}.mt-post-txt .mt-custom-emoji{height:1.5rem;min-width:1.5rem;margin-bottom:-.25rem;width:auto}.mt-post-txt .mention.hashtag{display:inline-block;font-size:.8rem;border-radius:.25rem;background-color:var(--mt-color-hashtag);padding:.25rem .5rem;margin:0 .25rem .25rem 0}.mt-post-txt .mention.hashtag:only-child{margin:0}.mt-post-poll{margin-bottom:1rem;color:var(--mt-color-content-txt)}.mt-post-poll ul{list-style:none;padding:0;margin:0}.mt-post-poll ul li{font-size:.9rem;margin-bottom:.5rem}.mt-post-poll.mt-post-poll-expired ul li{color:var(--mt-color-contrast-gray)}.mt-post-poll ul li:not(:last-child){margin-bottom:.25rem}.mt-post-poll ul li:before{content:"◯";padding-right:.5rem}.mt-post-poll.mt-post-poll-expired ul li:before{content:"";padding-right:0}.mt-post-media-wrapper{display:grid;grid-template-columns:repeat(2,1fr);gap:.5rem;margin-bottom:1rem}.mt-post-media{position:relative;overflow:hidden;width:100%}.mt-post-media-wrapper:has(> :last-child:nth-child(odd)) .mt-post-media:not(:only-child):first-child{grid-column:1/3}.mt-post-media:only-child{grid-column:1/3}.mt-post-media-spoiler>.mt-btn-play,.mt-post-media-spoiler>audio,.mt-post-media-spoiler>img,.mt-post-media-spoiler>video{filter:blur(2rem);pointer-events:none}.mt-post-media,.mt-post-media-spoiler>audio,.mt-post-media-spoiler>img,.mt-post-media>img,.mt-post-media>video{border-radius:.5rem}.mt-post-media>audio{width:100%;position:relative;z-index:1}.mt-post-media>img,.mt-post-media>video{width:100%;height:100%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);object-fit:cover;text-align:center;color:var(--mt-color-content-txt);background-color:var(--mt-color-placeholder)}body:has(dialog.mt-dialog[open]){overflow:hidden}.mt-dialog{display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;height:100%;border:none;color:var(--mt-color-content-txt);background-color:transparent;padding:0;margin:1rem;overflow:hidden}.mt-dialog::backdrop{background-color:var(--mt-color-backdrop);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}.mt-carousel-header{position:absolute;top:0;right:0;z-index:2}.mt-carousel-body{width:100%;height:100%}.mt-carousel-scroll{display:flex;width:100%;height:100%;margin:0;padding:0;list-style:none;overflow-x:auto;overflow-y:hidden;scroll-snap-type:x mandatory;scroll-behavior:smooth;scrollbar-width:none}.mt-carousel-scroll::-webkit-scrollbar{display:none;-webkit-appearance:none}.mt-carousel-item{scroll-snap-align:center;width:100%;height:100%}.mt-carousel-media-wrapper{width:calc(100vw - 2.5rem);height:100%;display:flex;align-items:center;justify-content:center;text-align:center}.mt-carousel-media{width:100%;height:100%;object-fit:contain;padding:2rem}.mt-carousel-next,.mt-carousel-prev{position:absolute;background-color:transparent;border:none;padding:.5rem;z-index:2}.mt-carousel-prev{left:0;padding-left:0}.mt-carousel-next{right:0;padding-right:0}.mt-post-preview{min-height:4rem;display:flex;flex-direction:row;border:1px solid var(--mt-color-line-gray);border-radius:.5rem;color:var(--mt-color-link);font-size:.8rem;margin:1rem 0;overflow:hidden}.mt-post-preview-image{width:40%;align-self:stretch}.mt-post-preview-image img{display:block;width:100%;height:100%;object-fit:cover;color:var(--mt-color-content-txt)}.mt-post-preview-noImage{width:40%;font-size:1.5rem;align-self:center;text-align:center}.mt-post-preview-content{width:60%;display:flex;align-self:center;flex-direction:column;padding:.5rem 1rem;gap:.5rem}.mt-post-preview-content:has(.mt-post-preview-description.truncate){align-self:unset}.mt-post-preview-description{display:block;color:var(--mt-color-contrast-gray)}.mt-post-preview-description.truncate{display:-webkit-box;overflow:hidden;-webkit-line-clamp:var(--mt-preview-max-lines);-webkit-box-orient:vertical}.mt-post-preview-description:not(.truncate) .ellipsis::after{content:"..."}.mt-post-preview-title{font-weight:600}.mt-post-counter-bar{display:flex;min-width:6rem;max-width:40rem;justify-content:space-between;color:var(--mt-color-contrast-gray)}.mt-post-counter-bar-favorites,.mt-post-counter-bar-reblog,.mt-post-counter-bar-replies{display:flex;font-size:.75rem;gap:.25rem;align-items:center;opacity:.6;cursor:default}.mt-post-counter-bar-favorites>svg,.mt-post-counter-bar-reblog>svg,.mt-post-counter-bar-replies>svg{width:1rem;fill:var(--mt-color-contrast-gray)}.mt-btn-play{display:flex;position:absolute;width:3rem;height:3rem;top:calc(50% - 1.5rem);left:calc(50% - 1.5rem);justify-content:center;align-items:center;background-color:transparent;border:none;cursor:pointer}.mt-btn-play>svg{width:2.5rem;height:2.5rem;fill:var(--mt-color-bg);stroke:var(--mt-color-content-txt);stroke-width:1px}.mt-post-media.mt-loading-spinner .mt-btn-play{display:none}.mt-container .mt-btn-dark,.mt-dialog .mt-btn-dark{display:flex;border-radius:.25rem;background-color:var(--mt-color-line-gray);border:0;color:var(--mt-color-content-txt);font-weight:600;font-size:.75rem;text-align:center;padding:.25rem .5rem;line-height:1.25rem;vertical-align:top}.mt-dialog .mt-btn-dark{margin-left:auto}.mt-container .mt-btn-violet,.mt-container a.mt-btn-violet,.mt-dialog .mt-btn-violet,.mt-dialog a.mt-btn-violet{display:flex;align-items:center;gap:.5rem;border-radius:.25rem;border:.5rem;padding:.5rem .75rem;font-size:1rem;font-weight:600;text-align:center;background-color:var(--mt-color-btn-bg);color:var(--mt-color-btn-txt)}.mt-container .mt-btn-violet:hover,.mt-container a.mt-btn-violet:hover,.mt-dialog .mt-btn-violet:hover,.mt-dialog a.mt-btn-violet:hover{background-color:var(--mt-color-btn-bg-hover);text-decoration:none}.mt-post-txt .mt-btn-spoiler-txt{display:inline-block;vertical-align:middle}.mt-post-media.mt-loading-spinner>.mt-btn-spoiler-media{display:none}.mt-post-media>.mt-btn-spoiler-media-show{position:absolute;top:50%;left:50%;z-index:2;transform:translate(-50%,-50%)}.mt-post-media.mt-post-media-spoiler>.mt-btn-spoiler-media-hide,.mt-post-media:not([data-media-width-hd])>.mt-btn-spoiler-media-hide{display:none}.mt-post-media:not(.mt-post-media-spoiler)>.mt-btn-spoiler-media-show{display:none}.mt-post-media>.mt-btn-spoiler-media-hide{position:absolute;top:.5rem;left:.5rem;z-index:2}.mt-post-media>.mt-btn-spoiler-media-hide>svg{fill:var(--mt-color-content-txt);pointer-events:none}.mt-error{position:absolute;left:50%;transform:translateX(-50%);display:flex;flex-direction:column;height:calc(100% - 3.5rem);width:calc(100% - 4.5rem);justify-content:center;align-items:center;color:var(--mt-color-error-txt);padding:.75rem;text-align:center}.mt-error-icon{font-size:2rem;margin-bottom:1rem}.mt-error-message{width:100%;padding:1rem 0}.mt-error-message hr{color:var(--mt-color-line-gray)}.mt-body>.mt-loading-spinner{position:absolute;width:3rem;height:3rem;margin:auto;top:calc(50% - 1.5rem);right:calc(50% - 1.5rem)}.mt-loading-spinner{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'%3E%3Cg%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 64 64' to='360 64 64' dur='1000ms' repeatCount='indefinite'/%3E%3Cpath d='M64 6.69a57.3 57.3 0 1 1 0 114.61A57.3 57.3 0 0 1 6.69 64' fill='none' stroke='%23404040' stroke-width='12'/%3E%3C/g%3E%3C/svg%3E");background-repeat:no-repeat;background-position:center center;background-color:transparent;background-size:min(2.5rem,calc(100% - .5rem))}.mt-footer{display:flex;flex-flow:wrap;margin:auto auto 2rem auto;padding:0 1rem;gap:1.5rem;align-items:center;justify-content:center}.visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}@supports (container-type:inline-size){@container mt-container (max-width:20rem){.mt-body{padding:0 .5rem}.mt-container .mt-post-header-date>a{white-space:normal}.mt-post-media-wrapper .mt-post-media{grid-column:1/3}}}@supports not (container-type:inline-size){@media screen and (max-width:clamp(20rem,40rem,60rem)){.mt-body{padding:0 .5rem}.mt-container .mt-post-header-date>a{white-space:normal}.mt-post-media-wrapper .mt-post-media{grid-column:1/3}}} \ No newline at end of file diff --git a/css/search.css b/css/search.css deleted file mode 100644 index 186e45d..0000000 --- a/css/search.css +++ /dev/null @@ -1,206 +0,0 @@ -/* Styles pour la page de recherche */ - -.search-page { - margin: 30px 0; -} - -.search-header { - margin-bottom: 30px; -} - -.search-header h1 { - font-size: 1.8rem; - margin-bottom: 15px; - color: #333; -} - -.search-container { - max-width: 600px; - margin-bottom: 20px; -} - -.search-container form { - display: flex; - border: 1px solid #ddd; - border-radius: 4px; - overflow: hidden; -} - -.search-container input { - flex: 1; - padding: 12px 15px; - border: none; - font-size: 16px; -} - -.search-container button { - background-color: #0d6efd; - color: white; - border: none; - padding: 0 20px; - cursor: pointer; - font-size: 18px; -} - -.search-results-count { - margin-bottom: 20px; - color: #666; - font-size: 14px; -} - -.search-results { - display: flex; - flex-direction: column; - gap: 20px; -} - -.search-result-item { - display: flex; - background-color: white; - border-radius: 8px; - overflow: hidden; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - cursor: pointer; - transition: transform 0.2s; -} - -.search-result-item:hover { - transform: translateY(-3px); -} - -.search-result-thumbnail { - width: 246px; - height: 138px; - position: relative; - flex-shrink: 0; -} - -.search-result-thumbnail img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.video-duration { - position: absolute; - bottom: 8px; - right: 8px; - background-color: rgba(0, 0, 0, 0.7); - color: white; - padding: 2px 6px; - border-radius: 4px; - font-size: 12px; -} - -.search-result-info { - padding: 15px; - flex: 1; -} - -.search-result-title { - font-size: 18px; - font-weight: 600; - margin-bottom: 8px; - color: #333; -} - -.search-result-metadata { - display: flex; - font-size: 13px; - color: #666; - margin-bottom: 8px; -} - -.search-result-views { - margin-right: 15px; -} - -.search-result-channel { - font-size: 14px; - color: #0d6efd; - margin-bottom: 8px; -} - -.search-result-description { - font-size: 14px; - color: #555; - margin-bottom: 10px; - line-height: 1.4; -} - -.search-result-tags { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.tag { - font-size: 12px; - color: var(--primary-red); -} - -.no-results, .search-instructions { - text-align: center; - padding: 60px 0; - color: #666; -} - -.no-results i, .search-instructions i { - color: #ccc; - margin-bottom: 20px; -} - -.no-results h2, .search-instructions h2 { - font-size: 20px; - margin-bottom: 10px; - color: #333; -} - -.no-results p, .search-instructions p { - font-size: 16px; - max-width: 500px; - margin: 0 auto; -} - -/* Media Queries */ -@media (max-width: 768px) { - .search-result-item { - flex-direction: column; - } - - .search-result-thumbnail { - width: 100%; - height: 0; - padding-top: 56.25%; /* 16:9 aspect ratio */ - } - - .search-result-thumbnail img { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } - - .search-header h1 { - font-size: 1.5rem; - } -} - -@media (max-width: 576px) { - .search-container input { - padding: 10px; - } - - .search-container button { - padding: 0 15px; - } - - .search-result-title { - font-size: 16px; - } - - .search-result-description { - font-size: 13px; - } -} \ No newline at end of file diff --git a/css/styles.css b/css/styles.css deleted file mode 100644 index 52209ab..0000000 --- a/css/styles.css +++ /dev/null @@ -1,3414 +0,0 @@ -/* Styles généraux */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -/* Accessibilité - Classes utilitaires */ -.sr-only { - position: absolute !important; - width: 1px !important; - height: 1px !important; - padding: 0 !important; - margin: -1px !important; - overflow: hidden !important; - clip: rect(0, 0, 0, 0) !important; - white-space: nowrap !important; - border: 0 !important; -} - -:root { - --primary-red: #FF0000; - --primary-green: #008000; - --primary-black: #000000; - --sidebar-bg: #f5f5f5; - --footer-bg: #f0f0f0; - --main-bg: #ffffff; - --text-color: #000000; - --text-secondary: #666666; - --tag-bg: #f8f8f8; - --tag-border: #e0e0e0; - --search-bg: #e0e0e0; - --card-bg: #ffffff; - --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - --divider-color: #e5e5e5; - --border-color: #e0e0e0; - --hover-bg: rgba(0, 0, 0, 0.05); -} - -/* Dark theme */ -[data-theme="dark"] { - --primary-red: #FF4444; - --primary-green: #00AA00; - --primary-black: #ffffff; - --sidebar-bg: #121212; - --footer-bg: #1a1a1a; - --main-bg: #1a1a1a; - --text-color: #ffffff; - --text-secondary: #b3b3b3; - --tag-bg: #2d2d2d; - --tag-border: #404040; - --search-bg: #333333; - --card-bg: #1e1e1e; - --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); - --divider-color: #404040; - --border-color: #404040; - --hover-bg: rgba(255, 255, 255, 0.1); -} - -body { - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - line-height: 1.6; - color: var(--text-color); - background-color: var(--main-bg); - display: flex; - flex-direction: column; - overflow-x: hidden; -} - -.container { - width: 100%; - max-width: 1200px; - margin: 0 auto; - padding: 0 15px; -} - -a { - text-decoration: none; - color: var(--text-color); -} - -/* Amélioration du focus pour l'accessibilité */ -a:focus, -button:focus, -input:focus, -textarea:focus, -select:focus { - outline: 2px solid var(--primary-red); - outline-offset: 2px; - border-radius: 2px; -} - -/* Support pour les préférences d'animation réduite */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} - -/* Support pour le contraste élevé */ -@media (prefers-contrast: high) { - :root { - --text-color: #000000; - --primary-red: #CC0000; - --card-bg: #FFFFFF; - --sidebar-bg: #F0F0F0; - --tag-bg: #E8E8E8; - --search-bg: #D0D0D0; - } - - [data-theme="dark"] { - --text-color: #ffffff; - --primary-red: #FF6666; - --card-bg: #000000; - --sidebar-bg: #111111; - --main-bg: #000000; - } -} - -img { - max-width: 100%; - height: auto; -} - -/* Layout */ -.sidebar { - width: 250px; - height: 100vh; - position: fixed; - top: 0; - left: 0; - background-color: var(--sidebar-bg); - overflow-y: auto; - padding: 15px 0; - z-index: 100; -} - -.main-content { - margin-left: 250px; - width: calc(100% - 250px); - padding: 0 20px 20px 20px; - position: relative; - box-sizing: border-box; - overflow-x: hidden; -} - -/* Logo */ -.logo { - display: block; - padding: 10px 15px; - margin-bottom: 20px; -} - -.logo img { - height: 100px; -} - -/* Amélioration de la visibilité des logos en mode dark */ -[data-theme="dark"] .logo img, -[data-theme="dark"] .section-logo img, -[data-theme="dark"] .footer-logo img { - filter: - drop-shadow(0 0 1px rgba(255, 255, 255, 0.8)) - drop-shadow(0 0 2px rgba(255, 255, 255, 0.6)) - drop-shadow(0 0 3px rgba(255, 255, 255, 0.4)); -} - -/* Navigation */ -.sidebar-nav { - margin-bottom: 30px; -} - -.nav-item { - display: flex; - align-items: center; - padding: 12px 15px; - font-size: 18px; - font-weight: 500; - transition: background-color 0.2s; - position: relative; - overflow: hidden; -} - -.nav-item:hover { - background-color: var(--hover-bg); -} - -/* PWA Install Button */ -.install-pwa-button { - background: var(--primary-red); - color: white; - border: none; - padding: 8px 12px; - border-radius: 6px; - font-size: 16px; - cursor: pointer; - transition: background-color 0.3s ease; - margin-right: 10px; - flex-shrink: 0; - white-space: nowrap; -} - -.install-pwa-button:hover { - background: #cc0000; -} - -.install-pwa-button:active { - transform: scale(0.95); -} - -/* PWA Styles */ -@media (display-mode: standalone) { - body { - padding-top: env(safe-area-inset-top); - } - - .header { - padding-top: env(safe-area-inset-top); - } -} - -/* Offline indicator */ -.offline-indicator { - position: fixed; - top: 0; - left: 0; - right: 0; - background: #ff4444; - color: white; - text-align: center; - padding: 10px; - z-index: 1000; - display: none; -} - -.offline-indicator.show { - display: block; -} - -/* Enhanced mobile experience */ -@media (max-width: 768px) { - .install-pwa-button { - padding: 6px 10px; - font-size: 14px; - } -} - -/* Styles pour la page active dans la sidebar */ -.nav-item.active { - background-color: rgba(255, 0, 0, 0.08); - font-weight: 600; - color: var(--primary-red); - box-shadow: inset 3px 0 0 var(--primary-red); -} - -.nav-item.active i { - color: var(--primary-red); -} - -.nav-item i, .nav-item img { - margin-right: 15px; - width: 24px; - text-align: center; -} - -.nav-divider { - height: 1px; - background-color: var(--divider-color); - margin: 15px 0; -} - -.category-title { - font-size: 20px; - font-weight: 600; - padding: 10px 15px; - margin-top: 20px; -} - -.tag-item { - position: relative; - display: flex; - align-items: center; - padding: 10px 15px 10px 25px; - color: var(--text-color); - text-decoration: none; - transition: all 0.2s ease; - font-size: 14px; -} - -.tag-item:hover { - background-color: rgba(0, 0, 0, 0.05); - color: var(--primary-red); - transform: translateX(5px); -} - -/* Style pour un tag actif */ -.tag-item.active { - background-color: rgba(255, 0, 0, 0.08); - color: var(--primary-red); - font-weight: 600; - box-shadow: inset 3px 0 0 var(--primary-red); -} - -.tag-icon { - color: var(--primary-red); - font-size: 0.9em; - margin-right: 10px; - transition: transform 0.2s ease; -} - -.tag-item:hover .tag-icon, -.tag-item.active .tag-icon { - transform: scale(1.5); -} - -/* Media queries pour le style actif sur les différentes tailles d'écran */ -@media (max-width: 1200px) and (min-width: 1001px) { - .nav-item.active { - background-color: rgba(255, 0, 0, 0.15); - } - - .nav-item.active::after { - content: attr(data-title); - position: absolute; - left: 70px; - top: 50%; - transform: translateY(-50%); - background: var(--primary-red); - color: white; - padding: 5px 10px; - border-radius: 4px; - white-space: nowrap; - box-shadow: 0 1px 5px rgba(0,0,0,0.2); - z-index: 100; - } -} - -/* Style pour le menu mobile */ -.mobile-menu .nav-item.active { - background-color: rgba(255, 0, 0, 0.08); - color: var(--primary-red); - font-weight: 600; - box-shadow: inset 3px 0 0 var(--primary-red); -} - -.mobile-menu .tag-item.active { - background-color: rgba(255, 0, 0, 0.08); - color: var(--primary-red); - font-weight: 600; - box-shadow: inset 3px 0 0 var(--primary-red); -} - -/* Header */ -.header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px 20px 25px 20px; - background-color: var(--main-bg); - position: sticky; - top: 0; - z-index: 50; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); - width: 100%; - max-width: calc(100vw - 250px); - margin-bottom: 20px; - margin-left: -20px; - padding-left: 40px; - padding-right: 40px; - box-sizing: border-box; -} - -.search-container { - flex: 1; - max-width: 600px; - margin: 0 auto; -} - -.search-container form { - display: flex; - position: relative; -} - -.search-container input { - width: 100%; - padding: 12px 45px 12px 15px; - border: 1px solid #e0e0e0; - border-radius: 25px; - background-color: #f8f8f8; - font-size: 16px; - transition: all 0.3s ease; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); - color: var(--text-color); -} - -.search-container input:focus { - outline: none; - border-color: var(--primary-red); - background-color: white; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); -} - -.search-container input::placeholder { - color: #9e9e9e; - transition: opacity 0.3s ease; -} - -.search-container input:focus::placeholder { - opacity: 0.7; -} - -.search-container button { - position: absolute; - right: 5px; - top: 50%; - transform: translateY(-50%); - background-color: var(--primary-red); - color: white; - border: none; - border-radius: 50%; - width: 35px; - height: 35px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 16px; - transition: all 0.3s ease; -} - -.search-container button:hover { - background-color: #d40000; - transform: translateY(-50%) scale(1.05); -} - -.social-icons, .action-icons { - display: flex; - align-items: center; - flex-shrink: 0; -} - -.icon-button { - background: none; - border: none; - font-size: 24px; - margin-left: 15px; - cursor: pointer; -} - -.icon-button i { - color: var(--text-color); -} - -/* Theme toggle button */ -#theme-toggle { - transition: transform 0.3s ease, background-color 0.3s ease; -} - -#theme-toggle:hover { - background-color: var(--hover-bg); - border-radius: 50%; - transform: scale(1.1); -} - -#theme-toggle:active { - transform: scale(0.95); -} - -/* Theme transition for smooth switching */ -* { - transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; -} - -/* Dark theme specific adjustments */ -[data-theme="dark"] .mt-container { - --mt-color-bg: var(--card-bg); - --mt-color-content-txt: var(--text-color); -} - -/* Override couleurs Mastodon - Theme rouge */ -.mt-container, .mt-container[data-theme=light], .mt-dialog, .mt-dialog[data-theme=light] { - --mt-color-btn-bg: var(--primary-red) !important; - --mt-color-btn-bg-hover: #cc0000 !important; - --mt-color-link: var(--primary-red) !important; -} - -.mt-container[data-theme=dark], .mt-dialog[data-theme=dark] { - --mt-color-btn-bg: var(--primary-red) !important; - --mt-color-btn-bg-hover: #ff6666 !important; - --mt-color-link: #ff6666 !important; -} - -/* Icône Mastodon en rouge */ -.mt-title i.fa-mastodon { - color: var(--primary-red) !important; -} - -/* Bouton de don dans le header */ -.donation-link { - background: transparent !important; - color: var(--primary-red) !important; - position: relative; - overflow: hidden; - border: none !important; - box-shadow: none !important; -} - -.donation-link:hover { - background: rgba(255, 0, 0, 0.1) !important; - transform: scale(1.2); -} - -.donation-link i { - color: var(--primary-red) !important; -} - -/* Animation pulse pour attirer l'attention */ -.donation-link::before { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 0; - height: 0; - background: rgba(255, 255, 255, 0.3); - border-radius: 50%; - transform: translate(-50%, -50%); - animation: pulse-heart 2s infinite; -} - -@keyframes pulse-heart { - 0% { - width: 0; - height: 0; - opacity: 1; - } - 100% { - width: 40px; - height: 40px; - opacity: 0; - } -} - -/* Lien de don dans la sidebar */ -.donation-nav-link i { - color: var(--primary-red) !important; -} - -.donation-nav-link:hover i { - color: #cc0000 !important; -} - -.donation-nav-link.active i { - color: var(--primary-red) !important; -} - -[data-theme="dark"] img { - opacity: 0.9; -} - -[data-theme="dark"] .video-thumbnail img { - opacity: 0.85; -} - -[data-theme="dark"] .video-thumbnail:hover img { - opacity: 1; -} - -/* Améliorer le contraste des icônes en mode sombre */ -[data-theme="dark"] .fa-play-circle { - color: rgba(255, 255, 255, 0.9); - text-shadow: 0 0 10px rgba(0, 0, 0, 0.5); -} - -/* Priorité plus élevée pour les couleurs des réseaux sociaux */ -.icon-button i.icon-facebook, -.icon-button i.icon-youtube, -.icon-button i.icon-instagram, -.icon-button i.icon-tiktok, -.icon-button i.icon-twitter, -.icon-button i.icon-mastodon { - color: inherit; -} - -/* Hero Section */ -.hero-mastodon-wrapper { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; - margin-bottom: 30px; - padding-top: 5px; - align-items: start; -} - -.timeline-container { - max-width: 800px; - width: 100%; - justify-self: center; -} - - -/* Responsive breakpoint pour écrans entre 1025px et 1699px */ -@media (max-width: 1699px) and (min-width: 1025px) { - .hero-mastodon-wrapper { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; - } - - #mt-container { - grid-column: 1 / 2; - } -} - -.hero { - position: relative; - width: 100%; - height: 400px; - background-color: #888; - overflow: hidden; - border-radius: 8px; -} - -/* Styles pour le lecteur vidéo dans le hero */ -.hero-video-container { - position: relative; - width: 100%; - height: 100%; - overflow: hidden; -} - -.hero-video-container iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - border: none; -} - -.hero-video-info { - position: absolute; - bottom: 70px; - left: 0; - width: 100%; - background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); - color: #fff; - padding: 15px; - z-index: 5; - pointer-events: none; -} - -.hero-video-info h2 { - font-size: 16px; - margin-bottom: 5px; - font-weight: 600; - text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7); - opacity: 0.9; -} - -.hero-channel-info { - display: flex; - align-items: center; - opacity: 0.8; -} - -.hero-channel-info .channel-avatar, -.hero-channel-info .channel-avatar-placeholder { - width: 24px; - height: 24px; - border-radius: 50%; - margin-right: 8px; -} - -.hero-channel-info .channel-name { - font-size: 14px; - font-weight: 500; -} - -.live-badge { - position: absolute; - top: 10px; - left: 10px; - background-color: var(--primary-red); - color: white; - border-radius: 4px; - padding: 5px 10px; - font-size: 14px; - font-weight: bold; - z-index: 10; - display: flex; - align-items: center; - gap: 5px; -} - -.live-badge i { - font-size: 10px; - animation: pulse 1.5s infinite; -} - -@keyframes pulse { - 0% { opacity: 1; } - 50% { opacity: 0.5; } - 100% { opacity: 1; } -} - -/* Message quand aucun direct n'est disponible */ -.hero-no-live { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - background-color: rgba(0, 0, 0, 0.7); - color: white; - text-align: center; - padding: 20px; -} - -.hero-no-live i { - font-size: 50px; - margin-bottom: 20px; - opacity: 0.8; -} - -.hero-no-live h2 { - font-size: 24px; - font-weight: 600; - margin-bottom: 15px; -} - -.hero-no-live p { - font-size: 16px; - max-width: 80%; - opacity: 0.9; -} - -/* ========================================= */ -/* Styles pour la section PLAYLIST */ -/* ========================================= */ - -.hero-playlist { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - background: linear-gradient(135deg, var(--sidebar-bg) 0%, var(--main-bg) 100%); -} - -.hero-playlist-header { - padding: 15px 20px; - background: rgba(0, 0, 0, 0.3); - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - display: flex; - align-items: center; - gap: 15px; -} - -.hero-playlist-header i { - font-size: 24px; - color: var(--primary-red); -} - -.hero-playlist-info h2 { - font-size: 18px; - font-weight: 600; - margin: 0 0 5px 0; - color: var(--text-primary); -} - -.hero-playlist-info p { - font-size: 14px; - margin: 0; - color: var(--text-secondary); - opacity: 0.9; -} - -.hero-playlist-container { - flex: 1; - position: relative; - overflow: hidden; -} - -.hero-playlist-container iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - border: none; -} - -.hero-playlist-footer { - padding: 12px 20px; - background: rgba(0, 0, 0, 0.3); - border-top: 1px solid rgba(255, 255, 255, 0.1); - display: flex; - justify-content: center; -} - -.hero-playlist-link { - color: var(--primary-red); - text-decoration: none; - font-size: 14px; - font-weight: 500; - display: inline-flex; - align-items: center; - gap: 8px; - padding: 8px 16px; - border-radius: 4px; - transition: background-color 0.2s ease; -} - -.hero-playlist-link:hover { - background-color: rgba(255, 255, 255, 0.1); -} - -.hero-playlist-link i { - font-size: 12px; -} - -/* Responsive pour les playlists */ -@media (max-width: 768px) { - .hero-playlist-header { - padding: 12px 15px; - } - - .hero-playlist-info h2 { - font-size: 16px; - } - - .hero-playlist-info p { - font-size: 13px; - } - - .hero-playlist-footer { - padding: 10px 15px; - } -} - -/* Annonce du prochain live dans le hero (page d'accueil) - Split gauche/droite */ -.hero-next-live { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - flex-direction: row; - overflow: hidden; - /* Arrière-plan flou - sera défini en ligne avec l'URL de l'image */ - background-size: cover; - background-position: center; - background-repeat: no-repeat; -} - -/* Overlay d'assombrissement pour améliorer la lisibilité */ -.hero-next-live::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient( - to right, - rgba(0, 0, 0, 0.3) 0%, - rgba(0, 0, 0, 0.6) 50%, - rgba(0, 0, 0, 0.85) 100% - ); - z-index: 1; -} - -/* Effet de flou sur l'arrière-plan */ -.hero-next-live::after { - content: ''; - position: absolute; - top: -10px; - left: -10px; - right: -10px; - bottom: -10px; - background: inherit; - filter: blur(20px); - z-index: 0; -} - -.hero-next-live-image-container { - flex: 0 0 50%; - display: flex; - align-items: center; - justify-content: center; - padding: 15px; - position: relative; - z-index: 2; -} - -.hero-next-live-image { - width: 100%; - height: 100%; - object-fit: contain; - object-position: center; - filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.6)); - position: relative; - z-index: 2; -} - -.hero-next-live-content { - flex: 0 0 50%; - padding: 25px 20px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; - background: rgba(0, 0, 0, 0.65); - backdrop-filter: blur(10px); - color: white; - position: relative; - z-index: 2; -} - -.hero-next-live-content i.fa-calendar-alt { - font-size: 35px; - margin-bottom: 10px; - color: var(--primary-red); -} - -.hero-next-live-content h2 { - font-size: 19px; - font-weight: 700; - margin-bottom: 12px; - color: white; - line-height: 1.4; - max-width: 90%; -} - -.hero-next-live-content p { - font-size: 14px; - margin: 0 auto 12px; - color: rgba(255, 255, 255, 0.95); - line-height: 1.4; - max-width: 95%; -} - -.hero-next-live-date { - font-size: 13px !important; - font-weight: 600; - color: white !important; - background: var(--primary-red); - padding: 8px 12px; - border-radius: 8px; - display: inline-block; - margin-bottom: 10px !important; - box-shadow: 0 4px 12px rgba(255, 0, 0, 0.3); - max-width: 90%; - line-height: 1.4; -} - -.hero-next-live-date i { - margin-right: 6px; -} - -.hero-next-live-datetime { - width: 100%; -} - -.hero-next-live-timezones { - display: flex; - flex-wrap: wrap; - gap: 6px; - justify-content: center; - margin-top: 10px; - padding: 10px 12px; - background-color: rgba(255, 255, 255, 0.15); - border-radius: 8px; - max-width: 95%; - margin-left: auto; - margin-right: auto; -} - -.hero-timezone-item { - font-size: 11px; - color: rgba(255, 255, 255, 0.95); - white-space: nowrap; - line-height: 1.5; - background-color: rgba(0, 0, 0, 0.3); - padding: 4px 8px; - border-radius: 5px; - border: 1px solid rgba(255, 255, 255, 0.15); - flex: 0 1 auto; - min-width: fit-content; -} - -.hero-timezone-item strong { - color: white; - font-weight: 700; -} - -#mt-container { - width: 100%; - height: 400px; - border-radius: 8px; - display: flex; - flex-direction: column; - margin-bottom: 1em; -} - -.mt-header { - padding: 12px 15px; - background: linear-gradient(45deg, #000000, #333333); - border-top-left-radius: 8px; - border-top-right-radius: 8px; -} - -.mt-title { - color: white; - font-size: 20px; - font-weight: 600; - display: flex; - align-items: center; - justify-content: center; - gap: 10px; - text-align: center; -} - -.mt-title i { - color: var(--primary-red); -} - -.mt-body { - height: calc(100% - 50px); - overflow-y: auto; -} - -.hero-content { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - color: white; -} - -.hero-logo { - font-size: 50px; - font-weight: 700; - margin-bottom: 20px; - text-transform: uppercase; -} - -.hero-logo .re { - color: var(--primary-red); -} - -.play-button { - width: 80px; - height: 80px; - border-radius: 50%; - background-color: rgba(0, 0, 0, 0.7); - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; -} - -.play-button i { - color: white; - font-size: 36px; -} - -/* Tags Section */ -.tags-section { - margin: 30px 0; - overflow-x: auto; - white-space: nowrap; - padding: 10px 0 20px; - text-align: center; - scrollbar-width: thin; - scrollbar-color: rgba(0, 0, 0, 0.2) transparent; - position: relative; -} - -.tags-section::-webkit-scrollbar { - height: 6px; -} - -.tags-section::-webkit-scrollbar-thumb { - background-color: rgba(0, 0, 0, 0.2); - border-radius: 10px; -} - -.tags-section::-webkit-scrollbar-track { - background-color: transparent; -} - -.tag { - display: inline-flex; - align-items: center; - padding: 8px 16px; - margin: 0 8px 12px; - border: none; - border-radius: 50px; - font-size: 15px; - font-weight: 600; - background: var(--tag-bg); - cursor: pointer; - transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94); - text-decoration: none; - color: var(--text-color); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); - position: relative; - overflow: hidden; - letter-spacing: 0.3px; -} - -.tag::before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient(135deg, var(--primary-red), #ff4d4d); - opacity: 0; - z-index: -1; - transition: opacity 0.3s ease; -} - -.tag:hover { - color: white; - transform: translateY(-3px); - box-shadow: 0 5px 12px rgba(255, 0, 0, 0.2); -} - -.tag:hover::before { - opacity: 1; -} - -.tag:active { - transform: translateY(-1px); - box-shadow: 0 2px 6px rgba(255, 0, 0, 0.3); -} - -/* Section Headers */ -.section-header { - display: flex; - justify-content: flex-start; - align-items: center; - margin-bottom: 20px; - flex-wrap: nowrap; -} - -.section-logo { - margin-right: 15px; - flex-shrink: 0; - min-width: 60px; -} - -.section-logo img { - height: 80px; - width: auto; - min-width: 80px; - object-fit: contain; -} - -.section-title { - font-size: 24px; - font-weight: 600; - word-break: break-word; - overflow-wrap: break-word; - hyphens: auto; -} - -/* Video Sections */ -.video-section { - margin-bottom: 40px; - position: relative; - padding-top: 5px; -} - -.video-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 20px; -} - -.video-card { - background-color: var(--card-bg); - border-radius: 8px; - overflow: hidden; - box-shadow: var(--card-shadow); - transition: transform 0.3s; - cursor: pointer; - height: 100%; - display: flex; - flex-direction: column; -} - -.video-card:hover { - transform: translateY(-5px); -} - -.video-thumbnail { - position: relative; - padding-top: 56.25%; /* 16:9 aspect ratio */ - overflow: hidden; -} - -.video-thumbnail img { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - object-fit: cover; -} - -.video-play-icon { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 50px; - height: 50px; - opacity: 1; - background-image: url('../img/play-icon.svg'); - background-size: contain; - background-repeat: no-repeat; - background-position: center; -} - -/* Cacher l'icône Font Awesome originale */ -.video-play-icon i { - display: none; -} - -.video-info { - padding: 15px; - flex-grow: 1; - display: flex; - flex-direction: column; -} - -.video-title { - font-size: 16px; - font-weight: 600; - margin-bottom: 8px; - line-height: 1.4; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - flex-grow: 1; -} - -.video-channel { - font-size: 14px; - color: var(--text-secondary); - margin-bottom: 8px; - display: flex; - align-items: center; - overflow: hidden; -} - -.channel-avatar { - width: 24px; - height: 24px; - border-radius: 50%; - margin-right: 8px; - object-fit: cover; - flex-shrink: 0; - border: 1px solid #e5e5e5; -} - -.channel-avatar-placeholder { - width: 24px; - height: 24px; - border-radius: 50%; - margin-right: 8px; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - background-color: #e5e5e5; - color: var(--text-secondary); - font-size: 16px; -} - -.channel-name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - line-height: 1.4; - display: flex; - align-items: center; - color: inherit; -} - -.video-metadata { - display: flex; - align-items: center; - justify-content: space-between; - font-size: 12px; - color: var(--text-secondary); - margin-top: auto; -} - -.video-metadata i { - margin-right: 4px; - font-size: 13px; - color: var(--primary-red); -} - -.video-views, .video-date { - display: flex; - align-items: center; - font-size: 12px; -} - -.video-duration { - position: absolute; - bottom: 10px; - right: 10px; - background-color: rgba(0, 0, 0, 0.7); - color: white; - padding: 3px 6px; - border-radius: 4px; - font-size: 12px; - font-weight: 500; -} - -.video-tag { - display: inline-block; - padding: 3px 8px; - background-color: #f0f0f0; - border-radius: 4px; - font-size: 12px; - margin-right: 5px; -} - -/* Carousel */ -.carousel { - position: relative; - overflow: hidden; - margin: 15px 0 30px; - padding: 10px 0; -} - -.carousel-container { - display: flex; - transition: transform 0.5s ease; - padding-bottom: 10px; - cursor: grab; - user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - touch-action: pan-y pinch-zoom; /* Permettre le scroll vertical et le zoom */ -} - -.carousel-container:active { - cursor: grabbing; -} - -.carousel-item { - flex: 0 0 auto; - width: 220px; - margin-right: 20px; -} - -/* Réduire l'espacement en mobile */ -@media (max-width: 768px) { - .carousel-item { - margin-right: 10px; - } -} - -.carousel-controls { - display: flex; - justify-content: center; - margin-top: 15px; -} - -.carousel-dot { - width: 10px; - height: 10px; - border-radius: 50%; - background-color: #ccc; - margin: 0 5px; - cursor: pointer; - transition: all 0.2s ease; -} - -.carousel-dot.active { - background-color: var(--primary-red); - transform: scale(1.2); -} - -/* Shorts specific styling */ -.short-card { - width: 180px; - position: relative; -} - -.short-thumbnail { - aspect-ratio: 9/16; - height: 320px; - border-radius: 12px; - overflow: hidden; - position: relative; - cursor: pointer; - transition: transform 0.3s ease; -} - -.short-thumbnail:hover { - transform: scale(1.05); -} - -.short-thumbnail img { - width: 100%; - height: 100%; - object-fit: cover; - object-position: center; - transition: filter 0.3s ease; -} - -.short-thumbnail:hover img { - filter: blur(2px); -} - -/* Overlay hover pour les shorts */ -.short-thumbnail::before { - content: ''; - position: absolute; - top: 100%; - left: 0; - right: 0; - bottom: 0; - background: rgba(64, 64, 64, 0.9); - transition: top 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); - z-index: 2; -} - -.short-thumbnail:hover::before { - top: 0; -} - -/* Logo overlay */ -.short-thumbnail::after { - content: ''; - position: absolute; - top: 20px; - left: 50%; - transform: translateX(-50%) translateY(20px); - width: 80px; - height: 80px; - background-image: url('../img/logo.png'); - background-size: contain; - background-repeat: no-repeat; - background-position: center; - opacity: 0; - transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.1s; - z-index: 3; -} - -.short-thumbnail:hover::after { - opacity: 1; - transform: translateX(-50%) translateY(0); -} - -/* Titre sur l'overlay pour les shorts */ -.short-card .short-title-overlay { - position: absolute; - bottom: 0; - left: 0; - right: 0; - color: white; - font-weight: 600; - font-size: 14px; - line-height: 1.3; - text-shadow: 0 2px 4px rgba(0,0,0,0.8); - z-index: 4; - background: linear-gradient(transparent, rgba(0,0,0,0.9)); - padding: 30px 12px 12px; - border-radius: 0 0 12px 12px; - text-align: center; - opacity: 0; - transform: translateY(20px); - transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.2s; -} - -.short-card:hover .short-title-overlay { - opacity: 1; - transform: translateY(0); -} - -/* Icône play sur l'overlay pour les shorts */ -.short-card .short-play-overlay { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - opacity: 0; - z-index: 5; - width: 60px; - height: 60px; - background-image: url('../img/play-icon.svg'); - background-size: contain; - background-repeat: no-repeat; - background-position: center; - transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.15s; - pointer-events: none; - filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3)); -} - -/* Cacher l'icône Font Awesome pour les shorts */ -.short-card .short-play-overlay i { - display: none; -} - -.short-card:hover .short-play-overlay { - opacity: 1; - transform: translate(-50%, -50%) scale(1.1); -} - -/* Ajustements pour mobile */ -@media (max-width: 768px) { - .short-card { - width: 150px; - } - - .short-thumbnail { - height: 267px; /* Maintient le ratio 9:16 */ - } -} - -@media (max-width: 480px) { - .short-card { - width: 130px; - } - - .short-thumbnail { - height: 231px; /* Maintient le ratio 9:16 */ - } -} - -/* View More Button */ -.view-more { - display: block; - width: max-content; - margin: 25px auto 5px; - padding: 12px 30px; - background-color: var(--primary-red); - color: white; - border: none; - border-radius: 4px; - font-size: 16px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s; - text-transform: uppercase; - letter-spacing: 1px; -} - -.view-more:hover { - background-color: #cc0000; - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0,0,0,0.1); -} - -.view-more:disabled { - background-color: #999; - cursor: not-allowed; - transform: none; - box-shadow: none; -} - -/* Info Section */ -.info-section { - margin: 40px 0; - padding: 30px; - background-color: var(--card-bg); - border-radius: 8px; - box-shadow: var(--card-shadow); -} - -.info-description { - margin-top: 0; - margin-bottom: 20px; - font-size: 1.1rem; - line-height: 1.6; - text-align: justify; -} - -.info-text { - font-size: 16px; - line-height: 1.6; - margin-bottom: 20px; -} - -.info-text a { - color: var(--primary-red); - font-weight: bold; - text-decoration: underline; - transition: opacity 0.2s; -} - -.info-text a:hover { - opacity: 0.8; -} - -/* Styles pour les licences Creative Commons */ -.cc-licenses { - display: flex; - flex-wrap: wrap; - gap: 20px; - margin: 30px 0; -} - -.cc-license { - flex: 1 0 300px; - background-color: #f8f8f8; - border-radius: 8px; - padding: 20px; - box-shadow: 0 2px 5px rgba(0,0,0,0.1); -} - -.cc-logo { - height: 31px; - margin-bottom: 15px; -} - -.cc-license h4 { - font-size: 16px; - font-weight: 600; - margin-bottom: 10px; - color: #333; -} - -.cc-license p { - font-size: 14px; - line-height: 1.5; - color: #555; -} - -.info-image { - max-width: 100%; - height: auto; - border-radius: 8px; -} - -/* Styles for movement figure and caption */ -.movement-figure { - margin: 0; - padding: 0; - text-align: center; -} - -.info-image { - max-width: 60%; - height: auto; - border-radius: 6px; - margin-bottom: 15px; - margin-left: auto; - margin-right: auto; - display: block; -} - -.movement-caption { - font-size: 0.9rem; - line-height: 1.5; - color: var(--text-color-secondary); - text-align: justify; - margin-top: 10px; -} - -/* Style pour le titre centré */ -.section-title.centered { - text-align: center; - margin: 0 0 20px; - position: relative; - padding-bottom: 15px; -} - -.section-title.centered::after { - content: ''; - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); - width: 60px; - height: 3px; - background-color: var(--primary-red); -} - -/* Media queries pour le responsive */ -@media (max-width: 576px) { - .movement-caption { - font-size: 12px; - } - - .info-image { - max-width: 80%; - } - - .header { - padding-left: 25px; - padding-right: 25px; - } - - .search-container { - max-width: 70%; - } - - .video-grid { - grid-template-columns: 1fr; - gap: 15px; - } - - .section-title { - font-size: 20px; - } - - /* Ajustement du header de section pour mobile */ - .section-header { - flex-wrap: nowrap; - align-items: center; - } - - .section-logo { - margin-right: 10px; - margin-bottom: 0; - } - - .section-logo img { - height: 50px; - min-width: 50px; - } - - /* Pour les cas très extrêmes où le texte est excessivement long */ - @media (max-width: 360px) { - .search-page .section-header, - .category-page .section-header { - flex-direction: column; - align-items: flex-start; - } - - .search-page .section-logo, - .category-page .section-logo { - margin-bottom: 10px; - } - } - - .hero-logo { - font-size: 36px; - } - - .play-button { - width: 60px; - height: 60px; - } - - .play-button i { - font-size: 24px; - } - - .video-title { - font-size: 16px; - } - - .cc-licenses { - gap: 15px; - } - - .cc-license { - flex: 1 0 100%; - padding: 15px; - } - - .cc-license h4 { - font-size: 15px; - } - - .cc-license p { - font-size: 13px; - } - - .info-section { - padding: 20px 15px; - } - - .footer-social { - gap: 10px; - } - - .footer-social a { - width: 35px; - height: 35px; - min-width: 35px; - min-height: 35px; - font-size: 18px; - margin: 0 5px; - } - - .mobile-social-icons { - gap: 10px; - } - - .mobile-social-icons a { - width: 35px; - height: 35px; - min-width: 35px; - min-height: 35px; - font-size: 18px; - } -} - -/* Footer */ -.footer { - margin-top: 40px; - margin-left: 250px; - width: calc(100% - 250px); - padding: 50px 30px 30px; - background-color: var(--footer-bg); - border-top: 1px solid var(--border-color); - box-sizing: border-box; - align-self: stretch; - color: var(--text-color); -} - -.footer-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 40px; - padding-bottom: 30px; - border-bottom: 1px solid var(--border-color); - max-width: 1200px; - margin-left: auto; - margin-right: auto; -} - -.footer-logo { - margin: 0; - cursor: pointer; -} - -.footer-logo img { - height: 90px; - transition: transform 0.3s; - cursor: pointer; -} - -.footer-logo img:hover { - transform: scale(1.05); -} - -.footer-contact-info { - text-align: right; -} - -.footer-contact { - font-size: 18px; - font-weight: 600; - margin-bottom: 10px; - color: var(--text-color); -} - -.footer-email { - font-size: 16px; -} - -.footer-email a { - color: var(--text-secondary); - text-decoration: none; - transition: color 0.2s; -} - -.footer-email a:hover { - color: var(--primary-red); -} - -.footer-columns { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - text-align: left; - margin-bottom: 40px; - max-width: 1200px; - margin-left: auto; - margin-right: auto; -} - -.footer-column { - flex: 1; - min-width: 200px; - padding: 0 20px; - margin-bottom: 30px; -} - -.footer-title { - font-size: 18px; - font-weight: 700; - margin-bottom: 20px; - color: var(--text-color); - position: relative; - padding-bottom: 10px; -} - -.footer-title::after { - content: ''; - position: absolute; - bottom: 0; - left: 0; - width: 40px; - height: 3px; - background-color: var(--primary-red); -} - -.footer-links { - list-style-type: none; - padding: 0; - margin: 0; -} - -.footer-links li { - margin-bottom: 12px; -} - -.footer-links a { - color: var(--text-secondary); - text-decoration: none; - font-size: 15px; - transition: all 0.2s; - display: inline-block; - position: relative; -} - -.footer-links a:hover { - color: var(--primary-red); - transform: translateX(5px); -} - -/* Style pour le lien actif dans le footer */ -.footer-links a.active { - color: var(--primary-red); - font-weight: 600; - position: relative; -} - -.footer-links a.active::after { - content: ''; - position: absolute; - bottom: -3px; - left: 0; - width: 100%; - height: 2px; - background: linear-gradient(90deg, - transparent 0%, - var(--primary-red) 20%, - var(--primary-red) 80%, - transparent 100%); - opacity: 0.8; - transform-origin: center; - transition: transform 0.3s, opacity 0.3s; -} - -.footer-links a.active:hover::after { - opacity: 1; - transform: scaleX(1.1); -} - -.footer-links a i { - margin-right: 8px; -} - -.footer-copyright { - padding-top: 25px; - border-top: 1px solid var(--border-color); - font-size: 14px; - color: var(--text-secondary); - max-width: 1200px; - margin: 0 auto; - text-align: center; -} - -.footer-copyright a { - color: var(--primary-red); - font-weight: bold; - text-decoration: underline; - transition: opacity 0.2s; -} - -.footer-copyright a:hover { - opacity: 0.8; -} - -.footer-social { - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: center; - margin-bottom: 25px; - gap: 20px; - max-width: 500px; - margin-left: auto; - margin-right: auto; -} - -.footer-social a { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border-radius: 50%; - background-color: #fff; - box-shadow: 0 2px 5px rgba(0,0,0,0.1); - color: var(--text-color); - font-size: 20px; - transition: all 0.3s; - min-width: 40px; - min-height: 40px; - aspect-ratio: 1/1; -} - -.footer-social a:hover { - transform: translateY(-5px); - box-shadow: 0 5px 15px rgba(0,0,0,0.1); -} - -@media (max-width: 1000px) { - .footer { - margin-left: 0; - width: 100%; - padding: 40px 20px 20px; - } - - .footer-header { - flex-direction: column; - text-align: center; - margin-bottom: 30px; - padding-bottom: 20px; - } - - .footer-logo { - margin-bottom: 20px; - } - - .footer-contact-info { - text-align: center; - } - - .footer-columns { - flex-direction: column; - align-items: center; - } - - .footer-column { - text-align: center; - margin-bottom: 30px; - width: 100%; - max-width: 400px; - } - - .footer-title::after { - left: 50%; - transform: translateX(-50%); - } - - .footer-links a:hover { - transform: none; - } -} - -/* Dark Mode Toggle */ -.dark-mode-toggle { - background: none; - border: none; - font-size: 20px; - cursor: pointer; - color: var(--text-color); - margin-left: 15px; -} - -/* Mobile Menu */ -.mobile-menu-toggle { - display: none; - background: none; - border: none; - font-size: 24px; - cursor: pointer; - position: relative; - z-index: 200; - margin-left: 15px; - color: var(--text-color); -} - -.mobile-menu { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: var(--main-bg); - z-index: 150; - padding: 60px 20px 20px; - transform: translateX(-100%); - transition: transform 0.3s; - overflow-y: auto; - max-height: 100vh; -} - -.mobile-menu.active { - transform: translateX(0); -} - -.mobile-menu-close { - position: absolute; - top: 15px; - right: 15px; - background: none; - border: none; - font-size: 24px; - cursor: pointer; - color: var(--text-color); -} - -/* Amélioration de l'espacement dans le menu mobile */ -.mobile-menu .search-container { - margin-bottom: 25px; - padding: 0 5px; -} - -.mobile-menu .search-container form { - margin-bottom: 10px; -} - -.mobile-menu nav { - margin-top: 15px; -} - -.mobile-menu .nav-item { - margin-bottom: 2px; -} - -.mobile-menu .mobile-section-title { - margin-top: 20px; - margin-bottom: 15px; -} - -/* Media Queries */ -@media (max-width: 1200px) and (min-width: 1001px) { - .sidebar { - width: 70px; - } - - .main-content, .footer { - margin-left: 70px; - width: calc(100% - 70px); - } - - /* Ajuster le header pour le mode sidebar compact */ - .header { - width: 100%; - max-width: calc(100vw - 70px); - margin-left: -20px; - padding-left: 40px; - padding-right: 40px; - box-sizing: border-box; - } - - .nav-item span { - display: none; - } - - /* Masquer complètement la section Hashtags */ - .category-title, - .tag-item { - display: none; - } - - .nav-item { - justify-content: center; - padding: 15px 0; - position: relative; - } - - .nav-item i { - margin-right: 0; - font-size: 20px; - } - - /* Tooltip for compact mode */ - .nav-item:hover::after { - content: attr(data-title); - position: absolute; - left: 70px; - top: 50%; - transform: translateY(-50%); - background: var(--sidebar-bg); - color: var(--text-color); - padding: 5px 10px; - border-radius: 4px; - white-space: nowrap; - box-shadow: 0 1px 5px rgba(0,0,0,0.2); - z-index: 100; - } - - .logo { - text-align: center; - padding: 10px 0; - } - - .logo img { - height: 50px; - } -} - -@media (max-width: 1000px) { - .sidebar { - display: none; - } - - .main-content, .footer { - margin-left: 0; - width: 100%; - } - - /* Ajuster le header pour l'affichage mobile */ - .header { - width: 100vw; - max-width: 100vw; - margin-left: -20px; - padding-left: 20px; - padding-right: 20px; - box-sizing: border-box; - position: relative; - left: 0; - } - - .mobile-menu-toggle { - display: block; - } - - .search-container { - max-width: 60%; - } - - .social-icons { - display: none; - } - - .action-icons { - display: flex; - align-items: center; - } - - .hero-mastodon-wrapper { - grid-template-columns: 1fr; - } - - .hero, #mt-container { - width: 100%; - height: 300px; - } - - /* Augmenter la hauteur du hero quand il y a une annonce */ - .hero:has(.hero-next-live) { - height: 600px; - } - - /* Augmenter aussi la hauteur du hero pour "no-live" pour cohérence */ - .hero:has(.hero-no-live) { - height: 300px; - } - - .hero-mastodon-wrapper #mt-container { - height: 600px; - } - - /* Pour le nouveau layout timeline-wordpress */ - .timeline-container #mt-container { - height: 400px; - } - - /* Réduire la taille des boutons du footer Mastodon en mobile */ - .mt-footer .mt-btn-violet { - font-size: 12px !important; - padding: 5px 10px !important; - } - - .hero-logo { - font-size: 40px; - } - - .footer-columns { - flex-direction: column; - } - - .footer-column { - text-align: center; - margin-bottom: 30px; - } -} - -@media (max-width: 992px) { - .video-grid { - grid-template-columns: repeat(2, 1fr); - gap: 15px; - } - - .video-title { - font-size: 15px; - } -} - -@media (max-width: 400px) { - .cc-license { - padding: 12px; - } - - .cc-logo { - height: 25px; - } - - .cc-license h4 { - font-size: 14px; - } - - .footer-social { - gap: 10px; - max-width: 250px; - } - - .footer-social a { - width: 32px; - height: 32px; - min-width: 32px; - min-height: 32px; - font-size: 16px; - margin: 0 3px; - } - - .mobile-social-icons { - gap: 5px; - flex-wrap: wrap; - } - - .mobile-social-icons a { - width: 32px; - height: 32px; - min-width: 32px; - min-height: 32px; - font-size: 16px; - } -} - -/* Styles pour le menu dropdown des réseaux sociaux */ -.more-social-container { - position: relative; -} - -.more-social-dropdown { - position: absolute; - top: 100%; - right: 0; - width: 180px; - background-color: var(--main-bg); - border-radius: 4px; - box-shadow: 0 2px 10px rgba(0,0,0,0.2); - padding: 10px 0; - display: none; - z-index: 100; -} - -.more-social-container:hover .more-social-dropdown, -.more-social-toggle:focus + .more-social-dropdown, -.more-social-dropdown:hover { - display: block; -} - -.more-social-item { - display: flex; - align-items: center; - padding: 10px 15px; - font-size: 14px; - transition: background-color 0.2s; -} - -.more-social-item:hover { - background-color: rgba(0,0,0,0.05); -} - -.more-social-item i { - margin-right: 10px; - font-size: 18px; - width: 20px; - text-align: center; - color: inherit; -} - -@media (max-width: 1000px) { - .more-social-dropdown { - right: -15px; - } -} - -/* Styles pour les icônes des réseaux sociaux avec leurs couleurs officielles */ -i.icon-facebook, -.fab.fa-facebook.icon-facebook { - color: #1877F2 !important; /* Bleu Facebook */ -} - -i.icon-youtube, -.fab.fa-youtube.icon-youtube { - color: #FF0000 !important; /* Rouge YouTube */ -} - -i.icon-instagram, -.fab.fa-instagram.icon-instagram { - /* Imiter le dégradé Instagram avec une couleur principale */ - color: #E1306C !important; /* Rose/magenta Instagram */ -} - -i.icon-tiktok, -.fab.fa-tiktok.icon-tiktok { - color: #000000 !important; /* Noir TikTok */ -} - -[data-theme="dark"] .header i.icon-tiktok, -[data-theme="dark"] .header .fab.fa-tiktok.icon-tiktok { - color: #ffffff !important; /* Blanc TikTok en mode sombre - header seulement */ -} - -i.icon-tiktok-accent { - color: #EE1D52 !important; /* Rouge TikTok */ -} - -i.icon-x, -.fab.fa-x-twitter.icon-x, -.fab.fa-x.icon-x { - color: #000000 !important; /* Noir X (anciennement Twitter) */ -} - -[data-theme="dark"] .header i.icon-x, -[data-theme="dark"] .header .fab.fa-x-twitter.icon-x, -[data-theme="dark"] .header .fab.fa-x.icon-x { - color: #ffffff !important; /* Blanc X en mode sombre - header seulement */ -} - -i.icon-mastodon, -.fab.fa-mastodon.icon-mastodon { - color: #563ACC !important; /* Violet Mastodon */ -} - -/* Maintenir la couleur par défaut pour les icônes dans le footer */ - -/* Ajustements pour le menu déroulant des réseaux sociaux */ -.more-social-item i { - margin-right: 10px; - font-size: 18px; - width: 20px; - text-align: center; - /* Supprimer la couleur rouge par défaut pour utiliser les classes spécifiques */ - color: inherit; -} - -/* Styles pour le menu mobile des réseaux sociaux */ -.mobile-social-icons { - display: flex; - justify-content: center; - gap: 20px; - margin: 15px 0; - padding: 10px 15px; -} - -.mobile-social-icons a { - font-size: 24px; - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border-radius: 50%; - background-color: #f5f5f5; - transition: transform 0.2s, background-color 0.2s; - min-width: 40px; - min-height: 40px; - aspect-ratio: 1/1; -} - -.mobile-social-icons a:hover { - transform: scale(1.1); - background-color: #e8e8e8; -} - -@media (max-width: 1000px) { - .mobile-menu-section .social-icons { - display: flex; - } -} - -.mobile-section-title { - font-size: 18px; - font-weight: 600; - margin: 20px 0 10px; - padding: 0; - color: var(--text-color); - text-align: center; -} - -/* Styles pour la page de direct */ -.live-page { - width: 100%; - margin-bottom: 40px; - padding-top: 5px; -} - -.live-container { - position: relative; - width: 100%; - background-color: var(--card-bg); - border-radius: 8px; - overflow: hidden; - box-shadow: var(--card-shadow); - margin-top: 20px; -} - -.live-player { - position: relative; - width: 100%; - padding-top: 56.25%; /* Ratio 16:9 */ -} - -.live-player iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - border: none; -} - -.live-info { - padding: 20px; - border-top: 1px solid var(--divider-color); -} - -.live-title { - font-size: 24px; - font-weight: 600; - margin-bottom: 15px; -} - -.live-channel-info { - display: flex; - align-items: center; - margin-bottom: 20px; -} - -.live-channel-info .channel-avatar, -.live-channel-info .channel-avatar-placeholder { - width: 40px; - height: 40px; - border-radius: 50%; - margin-right: 15px; -} - -.live-channel-info .channel-name { - font-size: 18px; - font-weight: 500; -} - -.live-description { - font-size: 16px; - line-height: 1.6; - color: var(--text-color); -} - -.live-description p { - margin-bottom: 15px; -} - -.live-badge.large { - position: absolute; - top: 20px; - left: 20px; - background-color: var(--primary-red); - color: white; - border-radius: 4px; - padding: 8px 15px; - font-size: 16px; - font-weight: bold; - z-index: 10; - display: flex; - align-items: center; - gap: 8px; -} - -.live-badge.large i { - font-size: 12px; - animation: pulse 1.5s infinite; -} - -.no-live-message { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - background-color: var(--card-bg); - color: var(--text-color); - text-align: center; - padding: 60px 20px; - border-radius: 8px; -} - -.no-live-message i { - font-size: 60px; - margin-bottom: 20px; - opacity: 0.7; - color: var(--primary-red); -} - -.no-live-message h2 { - font-size: 28px; - font-weight: 600; - margin-bottom: 15px; -} - -.no-live-message p { - font-size: 18px; - max-width: 600px; - margin: 0 auto 30px; -} - -/* Annonce du prochain live - Split gauche/droite */ -.next-live-announcement { - display: flex; - flex-direction: row; - border-radius: 8px; - overflow: hidden; - min-height: 500px; - position: relative; - /* Arrière-plan flou - sera défini en ligne avec l'URL de l'image */ - background-size: cover; - background-position: center; - background-repeat: no-repeat; -} - -/* Overlay d'assombrissement pour améliorer la lisibilité */ -.next-live-announcement::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient( - to right, - rgba(0, 0, 0, 0.3) 0%, - rgba(0, 0, 0, 0.6) 50%, - rgba(0, 0, 0, 0.85) 100% - ); - z-index: 1; -} - -/* Effet de flou sur l'arrière-plan */ -.next-live-announcement::after { - content: ''; - position: absolute; - top: -10px; - left: -10px; - right: -10px; - bottom: -10px; - background: inherit; - filter: blur(20px); - z-index: 0; - border-radius: 8px; -} - -.next-live-image-container { - flex: 0 0 50%; - display: flex; - align-items: center; - justify-content: center; - padding: 20px; - position: relative; - z-index: 2; -} - -.next-live-image { - width: 100%; - height: 100%; - object-fit: contain; - object-position: center; - max-height: 600px; - filter: drop-shadow(0 10px 30px rgba(0, 0, 0, 0.7)); - position: relative; - z-index: 2; -} - -.next-live-content { - flex: 0 0 50%; - padding: 50px 40px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; - background: rgba(0, 0, 0, 0.7); - backdrop-filter: blur(15px); - position: relative; - z-index: 2; -} - -.next-live-content i.fa-calendar-alt { - font-size: 60px; - margin-bottom: 20px; - color: var(--primary-red); -} - -.next-live-content h2 { - font-size: 32px; - font-weight: 700; - margin-bottom: 20px; - color: white; - line-height: 1.3; - text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); -} - -.next-live-content p { - font-size: 20px; - margin: 0 auto 25px; - color: rgba(255, 255, 255, 0.95); - line-height: 1.5; - max-width: 100%; - text-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); -} - -.next-live-date { - font-size: 22px !important; - font-weight: 600; - color: white !important; - background: var(--primary-red); - padding: 12px 24px; - border-radius: 8px; - display: inline-block; - margin-bottom: 30px !important; - box-shadow: 0 4px 12px rgba(255, 0, 0, 0.3); -} - -.next-live-date i { - margin-right: 8px; -} - -.utc-offset { - font-size: 16px; - opacity: 0.85; - font-weight: 500; -} - -.next-live-datetime { - width: 100%; -} - -.next-live-timezones { - display: flex; - flex-wrap: wrap; - gap: 10px; - justify-content: center; - margin-top: 15px; - margin-bottom: 20px; - padding: 16px 20px; - background-color: rgba(0, 0, 0, 0.4); - border-radius: 8px; -} - -.timezone-item { - font-size: 16px; - color: rgba(255, 255, 255, 0.95); - white-space: nowrap; - line-height: 1.5; - background-color: rgba(0, 0, 0, 0.3); - padding: 6px 12px; - border-radius: 6px; - border: 1px solid rgba(255, 255, 255, 0.1); - flex: 0 1 auto; - min-width: fit-content; -} - -.timezone-item strong { - color: white; - font-weight: 700; -} - -.day-shift { - font-size: 12px; - color: var(--primary-red); - font-weight: 700; - margin-left: 4px; -} - -.btn-primary { - display: inline-block; - padding: 12px 25px; - background-color: var(--primary-red); - color: white; - border-radius: 4px; - font-size: 16px; - font-weight: 500; - transition: all 0.3s; - cursor: pointer; - text-decoration: none; -} - -.btn-primary:hover { - background-color: #cc0000; - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0,0,0,0.1); -} - -/* Responsive tablette/desktop étroit - 769px à 1024px */ -@media (max-width: 1024px) and (min-width: 769px) { - .hero-mastodon-wrapper { - display: block; - } - - .timeline-container { - width: 100%; - max-width: 100%; - } - - .hero { - width: 100%; - margin-bottom: 20px; - } - - #mt-container { - flex: 1; - } - - .wordpress-section { - flex: 1; - } -} - -/* Responsive pour la page de direct */ -@media (max-width: 768px) { - .live-title { - font-size: 20px; - } - - .live-channel-info .channel-avatar, - .live-channel-info .channel-avatar-placeholder { - width: 32px; - height: 32px; - } - - .live-channel-info .channel-name { - font-size: 16px; - } - - .live-badge.large { - top: 15px; - left: 15px; - padding: 6px 12px; - font-size: 14px; - } - - .no-live-message { - padding: 40px 15px; - } - - .no-live-message i { - font-size: 50px; - } - - .no-live-message h2 { - font-size: 24px; - } - - .no-live-message p { - font-size: 16px; - } - - /* Responsive mobile - Stack vertical pour l'annonce */ - .next-live-announcement { - flex-direction: column; - min-height: auto; - } - - .next-live-image-container { - flex: 0 0 auto; - width: 100%; - max-height: 400px; - padding: 15px; - } - - .next-live-image { - max-height: 350px; - } - - .next-live-content { - flex: 0 0 auto; - width: 100%; - padding: 30px 20px; - overflow-y: auto; - max-height: 600px; - } - - .next-live-content i.fa-calendar-alt { - font-size: 45px; - } - - .next-live-content h2 { - font-size: 24px; - } - - .next-live-content p { - font-size: 16px; - } - - .next-live-date { - font-size: 17px !important; - padding: 10px 18px; - } - - .next-live-timezones { - gap: 8px; - padding: 14px 16px; - margin-top: 12px; - margin-bottom: 16px; - } - - .timezone-item { - font-size: 14px; - padding: 5px 10px; - } - - /* Hero next live responsive */ - .hero-next-live { - flex-direction: column; - } - - .hero-next-live-image-container { - flex: 0 0 300px; - width: 100%; - padding: 15px; - max-height: 300px; - } - - .hero-next-live-image { - max-height: 270px; - } - - .hero-next-live-content { - flex: 1; - width: 100%; - padding: 5px 15px 25px 15px; - overflow-y: auto; - display: flex; - flex-direction: column; - justify-content: flex-start; - } - - .hero-next-live-content i.fa-calendar-alt { - font-size: 35px; - margin-bottom: 10px; - margin-top: 0; - } - - .hero-next-live-content h2 { - font-size: 20px; - margin-bottom: 8px; - line-height: 1.2; - } - - .hero-next-live-content p { - font-size: 13px; - margin-bottom: 10px; - line-height: 1.4; - } - - .hero-next-live-date { - font-size: 13px !important; - padding: 6px 12px; - margin-bottom: 10px !important; - } - - .hero-next-live-timezones { - gap: 5px; - padding: 8px 10px; - margin-top: 8px; - } - - .hero-timezone-item { - font-size: 10px; - padding: 3px 6px; - } - - .utc-offset { - font-size: 11px; - } -} - -/* Media query pour très petits écrans (iPhone 4S, etc.) */ -@media (max-width: 360px) { - /* Réduction de l'icône calendar pour .next-live-announcement sur direct.php */ - .next-live-content i.fa-calendar-alt { - font-size: 35px; - margin-bottom: 8px; - } - - .next-live-content h2 { - font-size: 20px; - margin-bottom: 12px; - } - - .next-live-content p { - font-size: 14px; - margin-bottom: 15px; - } - - .next-live-date { - font-size: 14px !important; - padding: 8px 14px; - margin-bottom: 12px !important; - } - - .next-live-timezones { - gap: 6px; - padding: 10px 12px; - margin-top: 10px; - } - - .timezone-item { - font-size: 11px; - padding: 4px 8px; - } -} - -/* Catégories */ -.category-page { - width: 100%; - margin-bottom: 40px; - padding-top: 5px; -} - -.category-videos { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 25px; - margin-top: 20px; -} - -.no-videos-message { - text-align: center; - padding: 50px 20px; - background-color: var(--card-bg); - border-radius: 8px; - box-shadow: var(--card-shadow); - margin-top: 30px; -} - -.no-videos-message i { - font-size: 50px; - color: var(--primary-red); - margin-bottom: 20px; - opacity: 0.7; -} - -.no-videos-message h3 { - font-size: 24px; - margin-bottom: 15px; -} - -.no-videos-message p { - font-size: 16px; - color: var(--text-secondary); - margin-bottom: 25px; -} - -.btn-primary { - display: inline-block; - padding: 10px 20px; - background-color: var(--primary-red); - color: #fff; - border-radius: 4px; - text-decoration: none; - font-weight: 500; - transition: background-color 0.3s, transform 0.3s; -} - -.btn-primary:hover { - background-color: #d40000; - transform: translateY(-3px); -} - -@media (max-width: 992px) { - .category-videos { - grid-template-columns: repeat(2, 1fr); - } -} - -@media (max-width: 576px) { - .category-videos { - grid-template-columns: 1fr; - } -} - -/* Page de recherche */ -.search-page { - margin-bottom: 40px; - padding-top: 5px; -} - -.search-form-container { - margin: 20px 0 30px; -} - -.search-form { - display: flex; - max-width: 600px; - margin: 0 auto; -} - -.search-form input[type="text"] { - flex: 1; - padding: 12px 15px; - font-size: 16px; - border: 2px solid #ddd; - border-right: none; - border-radius: 4px 0 0 4px; - width: 100%; -} - -.search-form button { - background-color: var(--primary-red); - color: white; - border: none; - padding: 0 20px; - cursor: pointer; - border-radius: 0 4px 4px 0; - font-size: 16px; - transition: background-color 0.3s; -} - -.search-form button:hover { - background-color: #d40000; -} - -.search-results-count { - text-align: center; - margin-bottom: 20px; - padding: 10px; - background-color: #f9f9f9; - border-radius: 4px; -} - -.search-instructions { - text-align: center; - margin: 40px auto; - max-width: 500px; - padding: 30px; - background-color: #f9f9f9; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); -} - -.search-instructions i { - font-size: 48px; - color: var(--primary-red); - margin-bottom: 15px; - opacity: 0.7; -} - -.search-instructions h3 { - font-size: 22px; - margin-bottom: 15px; - color: #333; -} - -.search-instructions p { - color: var(--text-secondary); - line-height: 1.5; - margin-bottom: 10px; -} - -.search-instructions .mt-2 { - margin-top: 20px; -} - -.search-instructions strong { - color: var(--primary-red); - font-weight: 600; -} - -/* Pagination */ -.pagination { - display: flex; - justify-content: center; - align-items: center; - margin-top: 30px; - margin-bottom: 20px; -} - -.page-numbers { - display: flex; - align-items: center; -} - -.page-number { - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - margin: 0 5px; - border-radius: 4px; - background-color: #f5f5f5; - text-decoration: none; - color: #333; - font-weight: 500; - transition: all 0.2s; -} - -.page-number:hover { - background-color: #e0e0e0; -} - -.page-number.current { - background-color: var(--primary-red); - color: white; - cursor: default; -} - -.page-link { - display: flex; - align-items: center; - padding: 8px 15px; - border-radius: 4px; - background-color: #f5f5f5; - text-decoration: none; - color: #333; - font-weight: 500; - margin: 0 10px; - transition: all 0.2s; -} - -.page-link:hover { - background-color: #e0e0e0; -} - -.page-link.prev i { - margin-right: 8px; -} - -.page-link.next i { - margin-left: 8px; -} - -.page-dots { - margin: 0 5px; - color: var(--text-secondary); -} - -/* Styles pour les hashtags dans le menu mobile */ -.mobile-menu .nav-item .fas.fa-hashtag { - color: var(--primary-red); - font-size: 0.9em; - transition: transform 0.2s ease; -} - -.mobile-menu .nav-item:hover .fas.fa-hashtag { - transform: scale(1.2); -} - -.mobile-menu .nav-item:hover { - color: var(--primary-red); - background-color: rgba(255, 0, 0, 0.05); -} - -/* Styles pour le conteneur flex des sections Informations et Tendances Hashtags */ -.info-tags-container { - display: flex; - flex-direction: column; - gap: 30px; - margin-bottom: 40px; - margin-top: 60px; - position: relative; -} - -/* Conteneur pour la section des tags */ -.tags-section-container { - background-color: var(--card-bg); - border-radius: 8px; - padding: 25px 20px; - box-shadow: var(--card-shadow); - display: flex; - flex-direction: column; -} - -/* Section info */ -.info-section { - background-color: var(--card-bg); - border-radius: 8px; - padding: 25px 20px; - box-shadow: var(--card-shadow); -} - -.info-description { - margin-top: 0; - margin-bottom: 20px; - font-size: 1.1rem; - line-height: 1.6; - text-align: justify; -} - -/* Media Queries pour grands écrans */ -@media (min-width: 992px) { - .info-tags-container { - flex-direction: row; - align-items: flex-start; - margin-top: 30px; - } - - .info-section, - .tags-section-container { - display: flex; - flex-direction: column; - margin-top: 0; - margin-bottom: 0; - } - - .info-section { - flex: 3; - margin-right: 30px; - } - - .tags-section-container { - flex: 2; - align-self: stretch; - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - } - - /* Quand la section info n'est pas présente, la section tags prend toute la largeur */ - .info-tags-container:not(:has(.info-section)) .tags-section-container { - flex: 1; - max-width: 800px; - margin: 0 auto; - } - - .tags-section { - flex-grow: 1; - display: flex; - flex-wrap: wrap; - justify-content: center; - align-content: center; - } -} - -/* Séparateur de section */ -.section-divider { - height: 1px; - background-image: linear-gradient(to right, transparent, rgba(0, 0, 0, 0.1) 50%, transparent); - border: none; - margin: 40px 0; - position: relative; - overflow: visible; -} - -.section-divider::before { - content: ''; - display: block; - width: 40px; - height: 5px; - background-color: var(--primary-red); - position: absolute; - top: -2px; - left: 50%; - transform: translateX(-50%); - border-radius: 2px; -} - -/* Version responsive du séparateur */ -@media (max-width: 768px) { - .section-divider { - margin: 30px 0; - } - - .section-divider::before { - width: 30px; - height: 4px; - } -} - - \ No newline at end of file diff --git a/css/video-page.css b/css/video-page.css deleted file mode 100644 index 04d67e8..0000000 --- a/css/video-page.css +++ /dev/null @@ -1,1327 +0,0 @@ -:root { - --primary-red: #FF0000; -} - -/* Styles pour la page vidéo */ -.video-page { - display: grid; - grid-template-columns: 1fr 350px; - grid-template-rows: auto auto; - gap: 30px; - margin-top: 20px; -} - -.video-player-container { - grid-column: 1 / -1; - grid-row: 1; - max-width: 900px; - margin: 0 auto; - width: 100%; -} - -.video-player { - position: relative; - padding-bottom: 56.25%; /* Ratio 16:9 */ - height: 0; - overflow: hidden; - margin-bottom: 1.5rem; - border-radius: 8px; -} - -.video-player iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - border-radius: 8px; -} - -.video-content { - grid-column: 1; - grid-row: 2; - display: flex; - flex-direction: column; - gap: 20px; -} - -.video-primary-info { - border-bottom: 1px solid #ddd; - padding-bottom: 15px; - margin-bottom: 15px; -} - -.video-title { - font-size: 1.5rem; - font-weight: 700; - line-height: 1.3; - margin-bottom: 1rem; - color: var(--text-color); -} - -.video-info { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - margin-bottom: 1.5rem; - padding-bottom: 1.5rem; - border-bottom: 1px solid #eee; -} - -.video-metadata { - display: flex; - flex-direction: row; - align-items: center; - gap: 1rem; - margin-bottom: 0; - flex-wrap: nowrap; -} - -.video-views, .video-date { - font-size: 0.875rem; - color: var(--text-secondary); - display: flex; - align-items: center; - gap: 5px; - height: 36px; - white-space: nowrap; -} - -.video-views i, .video-date i { - font-size: 0.9375rem; - color: var(--primary-red); - display: flex; - align-items: center; - justify-content: center; - width: 20px; -} - -.video-actions { - display: flex; - flex-direction: row; - gap: 1.5rem; - align-items: center; - flex-wrap: nowrap; -} - -.action-button { - display: flex; - align-items: center; - justify-content: center; - background: none; - border: none; - cursor: pointer; - gap: 0.5rem; - font-size: 0.875rem; - padding: 0.5rem 0.75rem; - border-radius: 4px; - transition: background-color 0.2s; - height: 36px; - min-width: 80px; - color: var(--text-color); -} - -.action-button:hover { - background-color: var(--card-bg); -} - -.download-button { - text-decoration: none; - color: inherit; -} - -.disabled-button { - opacity: 0.6; - cursor: not-allowed; - position: relative; -} - -.disabled-button:hover { - background-color: inherit; -} - -.disabled-button::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 4px; - border: 1px dashed var(--text-secondary); - pointer-events: none; -} - -.action-button i { - font-size: 1.25rem; - color: var(--text-secondary); -} - -.action-button span { - display: inline-flex; - align-items: center; - height: 100%; -} - -.video-secondary-info { - margin-bottom: 30px; -} - -.channel-info { - display: flex; - align-items: center; - gap: 1rem; - margin-bottom: 1.5rem; - padding-bottom: 1.5rem; - border-bottom: 1px solid #eee; -} - -.channel-details { - display: flex; - flex-direction: column; - justify-content: center; -} - -.channel-avatar { - width: 48px; - height: 48px; - border-radius: 50%; - overflow: hidden; - flex-shrink: 0; -} - -.channel-avatar img { - width: 48px; - height: 48px; - border-radius: 50%; - object-fit: cover; -} - -.channel-avatar-placeholder { - width: 48px; - height: 48px; - border-radius: 50%; - background-color: #f5f5f5; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -.channel-avatar-placeholder i { - font-size: 2rem; - color: #999; -} - -.channel-name { - font-size: 1rem; - font-weight: 600; - margin: 0; - line-height: 1.4; -} - -.channel-avatar-link { - transition: transform 0.2s ease-in-out; - display: block; -} - -.channel-avatar-link:hover { - transform: scale(1.05); -} - -.channel-name-link { - text-decoration: none; - color: inherit; - position: relative; - display: inline-block; -} - -.channel-name-link:hover .channel-name { - color: var(--primary-red); -} - -.channel-name-link:after { - content: ''; - position: absolute; - width: 100%; - height: 2px; - bottom: -3px; - left: 0; - background-color: var(--primary-red); - transform: scaleX(0); - transition: transform 0.3s; - transform-origin: bottom left; -} - -.channel-name-link:hover:after { - transform: scaleX(1); -} - -.subscribe-button { - background-color: #0d6efd; - color: white; - border: none; - padding: 8px 15px; - border-radius: 4px; - font-size: 14px; - cursor: pointer; - transition: background-color 0.3s; -} - -.subscribe-button:hover { - background-color: #0b5ed7; -} - -.video-description { - font-size: 0.9375rem; - line-height: 1.6; - margin-bottom: 1.5rem; - color: var(--text-color); -} - -/* Styles pour les éléments Markdown dans la description */ -.video-description p { - margin-bottom: 1rem; -} - -.video-description ol, -.video-description ul { - margin-bottom: 1rem; - padding-left: 1.5rem; -} - -.video-description li { - margin-bottom: 0.5rem; -} - -.video-description a { - color: var(--primary-red); - text-decoration: none; - transition: color 0.2s; -} - -.video-description a:hover { - text-decoration: underline; -} - -.video-description blockquote { - border-left: 4px solid var(--border-color); - padding-left: 1rem; - margin-left: 0; - margin-right: 0; - margin-bottom: 1rem; - color: var(--text-secondary); - font-style: italic; -} - -.video-description code { - background-color: var(--card-bg); - padding: 0.2rem 0.4rem; - border-radius: 3px; - font-family: monospace; - font-size: 0.875rem; - color: var(--text-color); -} - -.video-description pre { - background-color: var(--card-bg); - padding: 1rem; - border-radius: 4px; - overflow-x: auto; - margin-bottom: 1rem; - color: var(--text-color); -} - -.video-description pre code { - background-color: transparent; - padding: 0; - border-radius: 0; - display: block; - white-space: pre; -} - -.video-description hr { - border: 0; - height: 1px; - background-color: var(--border-color); - margin: 2rem 0; -} - -.video-description .markdown-image { - max-width: 100%; - height: auto; - border-radius: 4px; - margin: 1rem 0; -} - -.truncated-description, .full-description { - position: relative; - padding-bottom: 40px; -} - -.truncated-description { - position: relative; -} - -.truncated-description::after { - content: ''; - position: absolute; - bottom: 40px; - left: 0; - right: 0; - height: 60px; - background: linear-gradient(transparent, var(--main-bg)); - pointer-events: none; -} - -.show-more-btn, .show-less-btn { - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - background-color: #f5f5f5; - border: 1px solid #e0e0e0; - color: var(--primary-red); - font-size: 0.9rem; - font-weight: 600; - cursor: pointer; - padding: 8px 16px; - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); - transition: all 0.2s; - border-radius: 20px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - width: auto; - min-width: 120px; -} - -.show-more-btn:hover, .show-less-btn:hover { - background-color: #f0f0f0; - box-shadow: 0 3px 6px rgba(0,0,0,0.15); - transform: translateX(-50%) translateY(-2px); -} - -/* Licence de la vidéo */ -.video-licence { - background-color: var(--card-bg); - border-radius: 8px; - padding: 1rem; - margin-bottom: 1.5rem; - border: 1px solid var(--border-color); -} - -.licence-title { - font-size: 0.9375rem; - font-weight: 600; - margin: 0 0 0.5rem 0; - color: var(--text-color); -} - -.licence-info { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.licence-link { - display: flex; - align-items: center; - gap: 0.75rem; - text-decoration: none; - color: inherit; - transition: opacity 0.2s; -} - -.licence-link:hover { - opacity: 0.8; -} - -.licence-logo { - height: 31px; - width: auto; -} - -.licence-label { - font-size: 0.875rem; - color: var(--text-secondary); -} - -.video-tags { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - margin-bottom: 1rem; -} - -.tag { - background-color: var(--tag-bg); - border-radius: 16px; - padding: 0.4rem 0.75rem; - font-size: 0.8125rem; - color: var(--text-color); - text-decoration: none; - transition: all 0.2s ease; - border: 1px solid var(--border-color); - display: inline-flex; - align-items: center; -} - -.tag:hover { - background-color: #e0e0e0; - color: white; - transform: translateY(-2px); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -/* Commentaires */ -.comments-section { - grid-column: 1; - background-color: var(--card-bg); - border-radius: 10px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - margin-bottom: 2rem; - padding: 1.5rem; -} - -.comments-section .section-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1.5rem; - padding-bottom: 1rem; - border-bottom: 1px solid var(--border-color); -} - -.section-title-wrapper { - display: flex; - align-items: center; -} - -.section-title { - font-size: 1.25rem; - font-weight: 600; - margin: 0; - color: var(--text-color); - line-height: 1; -} - -.view-on-peertube { - display: flex; - align-items: center; - gap: 0.5rem; - color: var(--primary-red); - text-decoration: none; - font-size: 0.875rem; - padding: 0.5rem 0.75rem; - border-radius: 4px; - transition: background-color 0.2s; - line-height: 1; - margin-left: 1rem; -} - -.view-on-peertube:hover { - background-color: var(--card-bg); -} - -.view-on-peertube i { - font-size: 0.875rem; -} - -.no-comments { - text-align: center; - padding: 2rem; - color: var(--text-secondary); - display: flex; - flex-direction: column; - align-items: center; - gap: 1rem; -} - -.no-comments i { - font-size: 2.5rem; - color: var(--border-color); -} - -.no-comments p { - margin: 0; - font-size: 0.9375rem; - max-width: 500px; - line-height: 1.5; -} - -.no-comments p:last-child { - color: var(--text-secondary); - font-size: 0.875rem; -} - -.comment-form { - display: flex; - gap: 1rem; - margin-bottom: 2rem; -} - -.comment-avatar img { - width: 40px; - height: 40px; - border-radius: 50%; - object-fit: cover; -} - -.comment-input { - flex: 1; -} - -.comment-input textarea { - width: 100%; - padding: 0.75rem; - border: 1px solid var(--border-color); - border-radius: 4px; - resize: none; - height: 80px; - margin-bottom: 0.75rem; - font-family: inherit; - background-color: var(--main-bg); - color: var(--text-color); -} - -.comment-buttons { - display: flex; - justify-content: flex-end; - gap: 0.75rem; -} - -.comment-cancel { - padding: 0.5rem 0.75rem; - background: none; - border: none; - cursor: pointer; - font-weight: 500; - color: var(--text-secondary); -} - -.comment-submit { - padding: 0.5rem 1.25rem; - background-color: #e62117; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-weight: 500; - transition: background-color 0.2s; -} - -.comment-submit:hover { - background-color: #cc1e12; -} - -.comments-list { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.comment { - display: flex; - gap: 1rem; - padding-bottom: 1.5rem; - border-bottom: 1px solid var(--border-color); -} - -.comment:last-of-type { - border-bottom: none; - margin-bottom: 1.5rem; -} - -.comment-avatar { - width: 40px; - height: 40px; - border-radius: 50%; - overflow: hidden; - flex-shrink: 0; -} - -.comment-avatar img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.comment-content { - flex: 1; -} - -.comment-header { - display: flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 0.5rem; -} - -.comment-author { - font-weight: 600; - color: var(--text-color); -} - -.comment-date { - color: var(--text-secondary); - font-size: 0.875rem; -} - -.comment-text { - color: var(--text-color); - line-height: 1.5; - white-space: pre-line; - margin-bottom: 0.75rem; -} - -.comment-replies-toggle { - margin-top: 0.5rem; -} - -.show-replies { - display: inline-flex; - align-items: center; - gap: 0.5rem; - color: var(--primary-red); - text-decoration: none; - font-size: 0.875rem; -} - -.show-replies:hover { - text-decoration: underline; -} - -.show-replies i { - font-size: 0.875rem; -} - -.comments-info { - margin-top: 1rem; - padding: 1rem; - background-color: var(--card-bg); - border-radius: 8px; - color: var(--text-secondary); - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - gap: 0.5rem; -} - -.comments-info i { - font-size: 1.5rem; - color: var(--text-secondary); -} - -.comments-info p { - margin: 0; - font-size: 0.9375rem; - max-width: 500px; - line-height: 1.5; -} - -.comments-info p:last-child { - color: #999; - font-size: 0.875rem; -} - -/* Vidéos suggérées */ -.video-suggestions { - grid-column: 2; - grid-row: 2; - align-self: start; -} - -.suggestion-list { - display: flex; - flex-direction: column; - gap: 15px; -} - -.suggested-video { - display: flex; - flex-direction: column; - text-decoration: none; - color: var(--text-color); - background-color: var(--card-bg); - border-radius: 8px; - overflow: hidden; - transition: transform 0.2s; -} - -.suggested-video:hover { - transform: translateY(-3px); - box-shadow: 0 4px 8px rgba(0,0,0,0.1); -} - -.suggested-video-thumbnail { - position: relative; - width: 100%; - padding-top: 56.25%; /* Ratio 16:9 */ -} - -.suggested-video-thumbnail img { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - object-fit: cover; -} - -.suggested-video-duration { - background-color: rgba(0, 0, 0, 0.7); - color: white; - font-size: 12px; - padding: 2px 4px; - border-radius: 3px; - display: inline-block; - margin-top: 5px; -} - -.suggested-video-info { - padding: 10px; -} - -.suggested-video-title { - font-size: 14px; - font-weight: 600; - margin: 0 0 5px 0; - line-height: 1.3; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - color: var(--text-color); -} - -.suggested-video-channel { - font-size: 0.75rem; - color: var(--text-secondary); - margin-bottom: 0.25rem; - display: flex; - align-items: center; - overflow: hidden; - line-height: 1; -} - -.suggested-video-channel .channel-name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-size: 0.75rem; - font-weight: normal; - line-height: 1.4; - display: flex; - align-items: center; -} - -.suggested-video-metadata { - display: flex; - font-size: 12px; - color: var(--text-secondary); - gap: 10px; -} - -.suggested-video-views, .suggested-video-date { - font-size: 11px; -} - -/* Vidéos associées */ -.related-videos { - grid-column: 2; -} - -.related-videos h2 { - font-size: 18px; - margin-bottom: 15px; -} - -.related-video-card { - display: flex; - margin-bottom: 15px; - cursor: pointer; -} - -.related-video-thumbnail { - width: 120px; - height: 68px; - margin-right: 10px; - position: relative; -} - -.related-video-thumbnail img { - width: 100%; - height: 100%; - object-fit: cover; - border-radius: 4px; -} - -.related-video-info { - flex: 1; -} - -.related-video-title { - font-size: 14px; - font-weight: 600; - margin-bottom: 4px; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.related-video-channel { - font-size: 12px; - color: var(--text-secondary); - margin-bottom: 2px; -} - -.related-video-metadata { - font-size: 12px; - color: var(--text-secondary); -} - -/* Media Queries */ -@media (max-width: 992px) { - .video-page { - grid-template-columns: 1fr; - display: flex; - flex-direction: column; - } - - .video-content { - grid-column: 1; - width: 100%; - order: 2; - } - - .video-suggestions { - grid-column: 1; - margin-top: 20px; - width: 100%; - order: 3; - } - - .suggestion-list { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 15px; - } - - .video-player-container { - max-width: 100%; - width: 100%; - order: 1; - } -} - -@media (max-width: 768px) { - .video-player-container { - margin-bottom: 15px; - } - - .video-player { - border-radius: 0; - } - - .video-player iframe { - border-radius: 0; - } - - .video-info { - flex-direction: column; - align-items: flex-start; - } - - .video-metadata { - width: 100%; - margin-bottom: 10px; - } - - .video-actions { - width: 100%; - justify-content: space-between; - flex-wrap: wrap; - } - - .action-button { - padding: 0.5rem; - min-width: 70px; - } - - .suggestion-list { - grid-template-columns: 1fr; - } -} - -@media (max-width: 576px) { - .video-page { - grid-template-columns: 1fr; - padding: 0; - } - - .video-player-container { - margin-bottom: 1rem; - } - - .video-content, - .video-suggestions { - padding: 1rem; - } - - .video-title { - font-size: 1.25rem; - } - - .video-actions { - justify-content: flex-start; - } - - .action-button { - padding: 0.4rem 0.6rem; - min-width: auto; - gap: 0.3rem; - } - - .video-views, .video-date { - font-size: 0.75rem; - } - - .comment { - flex-direction: column; - } - - .comment-avatar { - margin-bottom: 0.75rem; - } - - .suggested-video { - flex-direction: column; - } - - .suggested-video-thumbnail { - width: 100%; - } - - .suggested-video-title { - font-size: 0.9375rem; - } - - /* Licence responsive */ - .licence-info { - flex-direction: column; - align-items: flex-start; - gap: 0.5rem; - } - - .licence-logo { - height: 28px; - } -} - -/* Media Queries pour le menu réduit */ -@media (max-width: 1200px) and (min-width: 1001px) { - .main-content { - margin-left: 70px; - width: calc(100% - 70px); - } -} - -/* Message d'erreur */ -.error-message { - text-align: center; - padding: 50px 20px; - max-width: 500px; - margin: 0 auto; -} - -.nav-item { - display: flex; - align-items: center; - padding: 12px 15px; - font-size: 18px; - font-weight: 500; - transition: background-color 0.2s; - text-decoration: none; - color: var(--text-color); -} - -.nav-item:hover { - background-color: rgba(0, 0, 0, 0.05); -} - -.nav-item i, .nav-item img { - margin-right: 15px; - width: 24px; - text-align: center; -} - -/* Modal de téléchargement */ -.modal { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - z-index: 1000; - overflow: auto; - padding: 20px; - box-sizing: border-box; - align-items: center; - justify-content: center; -} - -.modal.show { - display: flex; -} - -.modal-content { - background-color: var(--card-bg); - border-radius: 8px; - max-width: 500px; - width: 100%; - position: relative; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); - animation: modalShow 0.3s ease; - max-height: 90vh; - display: flex; - flex-direction: column; -} - -@keyframes modalShow { - from { - opacity: 0; - transform: translateY(-20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.modal-header { - padding: 15px 20px; - border-bottom: 1px solid var(--border-color); - display: flex; - justify-content: space-between; - align-items: center; -} - -.modal-header h3 { - margin: 0; - font-size: 1.25rem; - color: var(--text-color); -} - -.modal-close { - background: none; - border: none; - font-size: 1rem; - color: var(--text-secondary); - cursor: pointer; - padding: 5px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - width: 30px; - height: 30px; - transition: background-color 0.2s; -} - -.modal-close:hover { - background-color: var(--card-bg); - color: var(--text-color); -} - -.modal-body { - padding: 20px; - overflow-y: auto; -} - -.modal-body p { - margin-top: 0; - margin-bottom: 15px; - color: var(--text-secondary); -} - -.download-options { - display: flex; - flex-direction: column; - gap: 10px; -} - -.download-option { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 15px; - background-color: var(--main-bg); - border-radius: 8px; - text-decoration: none; - color: var(--text-color); - font-weight: 500; - transition: background-color 0.2s, transform 0.2s; -} - -.download-option:hover { - background-color: var(--card-bg); - transform: translateY(-2px); -} - -.download-resolution { - display: flex; - align-items: center; - gap: 10px; -} - -.download-resolution i { - color: var(--primary-red); - font-size: 1.1rem; -} - -.download-info { - display: flex; - align-items: center; - gap: 10px; - color: var(--text-secondary); - font-size: 0.875rem; -} - -.download-info i { - color: var(--text-secondary); -} - -.modal-footer { - padding: 15px 20px; - border-top: 1px solid var(--border-color); -} - -.download-info-text { - margin: 0; - color: var(--text-secondary); - font-size: 0.8125rem; - text-align: center; -} - -.no-download-options { - text-align: center; - padding: 20px; - color: var(--text-secondary); -} - -/* Adaptations responsives */ -@media (max-width: 576px) { - .modal-content { - max-width: 100%; - } - - .download-option { - padding: 10px; - } -} - -/* Modal de partage */ -.share-link-container { - margin-bottom: 20px; -} - -.share-link-box { - display: flex; - border: 1px solid var(--border-color); - border-radius: 4px; - overflow: hidden; - margin-top: 8px; -} - -.share-link-box input { - flex: 1; - padding: 10px 12px; - border: none; - font-size: 14px; - color: var(--text-color); - background-color: var(--main-bg); -} - -.share-link-box input:focus { - outline: none; - background-color: var(--card-bg); -} - -.copy-btn { - background-color: var(--card-bg); - border: none; - border-left: 1px solid var(--border-color); - color: var(--text-secondary); - padding: 0 15px; - cursor: pointer; - transition: background-color 0.2s; -} - -.copy-btn:hover { - background-color: var(--main-bg); -} - -.copy-btn.copied { - background-color: #4CAF50; - color: white; -} - -.share-platforms-title { - margin-top: 20px; - margin-bottom: 10px; - font-weight: 500; - color: var(--text-color); -} - -.share-platforms { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 10px; - margin-bottom: 20px; -} - -.share-platform-btn { - display: flex; - flex-direction: column; - align-items: center; - text-decoration: none; - color: var(--text-color); - padding: 15px 10px; - border-radius: 8px; - background-color: var(--card-bg); - transition: transform 0.2s, background-color 0.2s; -} - -.share-platform-btn:hover { - background-color: var(--main-bg); - transform: translateY(-2px); -} - -.share-platform-btn i { - font-size: 1.5rem; - margin-bottom: 8px; - color: var(--primary-red); -} - -.share-platform-btn span { - font-size: 0.875rem; -} - -.share-embed { - margin-top: 20px; - padding-top: 20px; - border-top: 1px solid var(--border-color); -} - -@media (max-width: 576px) { - .share-platforms { - grid-template-columns: repeat(2, 1fr); - } -} diff --git a/css/wordpress-posts.css b/css/wordpress-posts.css deleted file mode 100644 index 9738299..0000000 --- a/css/wordpress-posts.css +++ /dev/null @@ -1,204 +0,0 @@ -/* Styles pour la liste compacte d'articles WordPress */ - -/* Container principal pour timeline + wordpress */ -.timeline-container { - display: grid; - grid-template-columns: 2fr 1fr; - gap: 20px; - margin: 0; - align-items: start; - width: 100%; -} - -.wordpress-container { - background: var(--card-bg); - border-radius: 12px; - border: 1px solid var(--border-color); - box-shadow: var(--card-shadow); - overflow: hidden; - height: 400px; - display: flex; - flex-direction: column; -} - -.wordpress-section { - width: 100%; - min-width: 0; -} - -/* Assurer que le container Mastodon garde son scroll */ -.timeline-container .mt-container { - height: 400px !important; - overflow-y: auto !important; -} - -.wordpress-header { - background: var(--primary-red); - color: white; - padding: 12px 16px; - font-size: 1rem; - font-weight: bold; - display: flex; - align-items: center; - gap: 8px; -} - -.wordpress-header a { - color: white; - text-decoration: none; - transition: opacity 0.2s ease; -} - -.wordpress-header a:hover { - opacity: 0.8; - text-decoration: underline; -} - -.wordpress-posts-list { - flex: 1; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: var(--primary-red) transparent; -} - -.wordpress-posts-list::-webkit-scrollbar { - width: 6px; -} - -.wordpress-posts-list::-webkit-scrollbar-track { - background: transparent; -} - -.wordpress-posts-list::-webkit-scrollbar-thumb { - background-color: var(--primary-red); - border-radius: 3px; -} - -.wordpress-post-item { - display: flex; - padding: 8px 12px; - border-bottom: 1px solid var(--border-color); - text-decoration: none; - color: inherit; - transition: background-color 0.2s ease; - gap: 8px; -} - -.wordpress-post-item:hover { - background: var(--hover-bg); - text-decoration: none; - color: inherit; -} - -.wordpress-post-item:last-child { - border-bottom: none; -} - -.wordpress-post-thumb { - width: 40px; - height: 40px; - border-radius: 6px; - overflow: hidden; - flex-shrink: 0; - background: var(--tag-bg); - display: flex; - align-items: center; - justify-content: center; - color: var(--text-secondary); -} - -.wordpress-post-thumb img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.wordpress-post-info { - flex: 1; - min-width: 0; -} - -.wordpress-post-title { - font-size: 0.8rem; - font-weight: 600; - color: var(--text-color); - margin: 0 0 4px 0; - line-height: 1.2; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - - -.wordpress-post-meta { - font-size: 0.65rem; - color: var(--text-secondary); - display: flex; - gap: 6px; -} - -.wordpress-no-posts { - text-align: center; - color: var(--text-secondary); - padding: 20px; - font-style: italic; - font-size: 0.9rem; -} - -/* Ajustement du container Mastodon */ -.mt-container { - width: 100%; - min-width: 0; -} - -/* Responsive Design */ -@media (max-width: 1024px) { - /* Mobile/Tablette: layout vertical */ - .timeline-container { - grid-template-columns: 1fr; - gap: 15px; - } - - .wordpress-section { - width: 100%; - } - - /* Maintenir la hauteur et le scroll du container Mastodon en mobile */ - .timeline-container .mt-container { - height: 400px !important; - overflow-y: auto !important; - max-height: 400px !important; - } - - .wordpress-container { - height: auto; - } -} - -@media (max-width: 768px) { - .wordpress-post-thumb { - width: 50px; - height: 50px; - } - - .wordpress-post-item { - padding: 10px 12px; - gap: 10px; - } - - .wordpress-post-title { - font-size: 0.85rem; - } - - - .wordpress-post-meta { - font-size: 0.65rem; - } - - .wordpress-header { - padding: 10px 12px; - font-size: 0.9rem; - } - -} \ No newline at end of file diff --git a/direct.php b/direct.php deleted file mode 100644 index 86b20bd..0000000 --- a/direct.php +++ /dev/null @@ -1,278 +0,0 @@ - - - - - - - Direct - <?php echo SITE_NAME; ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'Accueil', 'url' => getBaseUrl()], - ['name' => 'Direct', 'url' => getBaseUrl() . '/direct.php'] - ]; - $breadcrumbJsonLd = generateBreadcrumbJsonLd($breadcrumbs); - outputJsonLd($breadcrumbJsonLd); - ?> - - - - - - - -
- - - -
-
- -

Diffusion en direct

-
- -
- -
- EN DIRECT -
-
- -
-
-

-
- -
- -
- - <?php echo $liveStream['channel']; ?> - - -
- -
- -
- -
- -
- -
- <?php echo htmlspecialchars(NEXT_LIVE_TITLE); ?> -
- -
- - format($liveDate); - $formattedDay = ucfirst($formattedDay); - $dynamicTitle = NEXT_LIVE_TITLE . ' - ' . $formattedDay; - } else { - $dynamicTitle = NEXT_LIVE_TITLE; - } - ?> -

- format('H\hi'); - $dynamicDescription = 'Rejoignez-nous à ' . $liveHour . '. ' . NEXT_LIVE_DESCRIPTION; - } else { - $dynamicDescription = NEXT_LIVE_DESCRIPTION; - } - ?> -

- -
-

- - format($liveDate); - - $offset = $liveDate->format('P'); - echo ' (UTC' . $offset . ')'; - ?> -

- - -
- 'Pacific/Tahiti', - 'Martinique / Guadeloupe' => 'America/Martinique', - 'Guyane' => 'America/Cayenne', - 'France' => 'Europe/Paris', - 'Kanaky' => 'Pacific/Noumea' - ]; - - foreach($timezones as $name => $timezone): - $liveDateLocal = clone $liveDate; - $liveDateLocal->setTimezone(new DateTimeZone($timezone)); - - // Vérifier si c'est un jour différent - $dayDiff = $liveDateLocal->format('j') - $liveDate->format('j'); - - $dayIndicator = ''; - if ($dayDiff > 0) { - $dayIndicator = ' +1j'; - } elseif ($dayDiff < 0) { - $dayIndicator = ' -1j'; - } - ?> - - : format('H\hi'); ?> - - -
-
- - Retour à l'accueil -
-
- -
- -

Aucun direct en cours

-

Revenez plus tard pour découvrir nos prochaines diffusions en direct.

- Retour à l'accueil -
- -
-
-
- - - - - \ No newline at end of file diff --git a/doc2sveltekit-transition/AGENTS.md b/doc2sveltekit-transition/AGENTS.md new file mode 100644 index 0000000..4711cd0 --- /dev/null +++ b/doc2sveltekit-transition/AGENTS.md @@ -0,0 +1,23 @@ +You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively: + +## Available Svelte MCP Tools: + +### 1. list-sections + +Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths. +When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections. + +### 2. get-documentation + +Retrieves full documentation content for specific sections. Accepts single or multiple sections. +After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task. + +### 3. svelte-autofixer + +Analyzes Svelte code and returns issues and suggestions. +You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned. + +### 4. playground-link + +Generates a Svelte Playground link with the provided code. +After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project. diff --git a/doc2sveltekit-transition/charte-oki-design-system.md b/doc2sveltekit-transition/charte-oki-design-system.md new file mode 100644 index 0000000..2b404d0 --- /dev/null +++ b/doc2sveltekit-transition/charte-oki-design-system.md @@ -0,0 +1,213 @@ +# CHARTE OKI — DESIGN SYSTEM & HARMONISATION MULTI-PROJETS +### ORGANISATION KA INTERNATIONALE · appliquer la recette SvelteKit avec la marque OKI + +> **Nature de ce document :** référence de marque opérationnelle, destinée aux humains et aux agents de code. Il consolide le `BRAND.md` existant (valeurs mesurées sur o-k-i.net, juillet 2026 — **source canonique** pour couleurs, typo, voix) et y ajoute les tokens prêts à l'emploi, les règles de motion, l'iconographie SVG, et la matrice d'application projet par projet. **En cas de conflit : ce document prime sur la recette générique pour tout ce qui touche à la marque ; la recette prime pour tout ce qui touche à la méthode technique.** + +--- + +## 1. IDENTITÉ EN UNE PHRASE + +Un studio web militant caribéen dont la marque est un **drapeau panafricain — noir, or, vert, rouge — posé sur un fond presque noir, en capitales Archivo**. La marque est afrofuturiste caribéenne : un drapeau, pas une charte corporate. + +Trois conséquences non négociables : +1. **Le thème sombre est l'identité par défaut** ; le clair est une variante opt-in (`html.light-theme`) qui assombrit les accents pour rester WCAG AA. +2. **L'or porte toute l'interaction** — c'est la seule couleur d'action. +3. **Angles nets partout** — aucune forme organique, aucune bulle très arrondie. + +--- + +## 2. TOKENS — PRÊTS À L'EMPLOI + +### 2.1 Couleurs (mesurées sur le site réel) + +```css +:root { + /* ── Noyau (thème sombre = défaut) ── */ + --noir-oki: #0D0D0D; /* background */ + --noir-profond: #1A0F1A; /* surface */ + --blanc-creme: #FFF8E7; /* foreground */ + --line: rgba(255, 255, 255, 0.10); /* filets — jamais de gris plein */ + --or-oki: #FDB813; /* accent : SEULE couleur d'action */ + --rouge-oki: #FF1654; /* signal, ponctuation — jamais un lien/bouton */ + /* ── Étendue (parcimonie : décorations et sémantique) ── */ + --vert-oki: #00D66C; /* succès, validation, dons mensuels */ + --turquoise-caraibes: #00CED1; + --jaune-soleil: #FFD700; + --orange-flamme: #FF6B35; + --violet-nuit: #6B2D5C; + --bleu-ocean: #0077BE; + --or-clair: #FFE066; /* survol des boutons */ + --gris-sombre: #2D1B2E; + /* ── Sémantique dérivée ── */ + --muted: color-mix(in srgb, var(--blanc-creme) 70%, transparent); + --card-bg: rgba(255, 255, 255, 0.03); +} +``` + +Règles d'usage (mesurées, à faire respecter) : +- Texte secondaire = blanc crème à 70–85 % d'opacité, **pas** une couleur dédiée. +- Filets = blanc à 10 %, jamais de gris plein. +- Rouge = signaler · Vert = valider · Or = agir. Ne jamais permuter ces rôles. + +### 2.2 Typographie + +```css +:root { + --font-display: 'Archivo', 'Arial Black', sans-serif; /* 600–900, TITRES + BOUTONS */ + --font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; /* 400–800 */ +} +``` +- Titres h1–h4 : Archivo, **MAJUSCULES**, `letter-spacing: -0.01em`. +- Boutons : Archivo 700, uppercase, `letter-spacing: 0.03em`, bordure 2 px. +- Corps : Inter, `line-height: 1.5`. Graisses minimales : 400 body / 600 display (lisibilité écrans bas de gamme). +- **Self-hébergement obligatoire** : woff2 dans `fonts/` + `fonts/fonts.css`, `font-display: swap`. Aucun appel Google Fonts (contradiction avec la doctrine souveraineté — bug en cours de correction sur o-k-i.net). + +### 2.3 Formes, espacements, layout + +```css +:root { + --radius-sm: 3px; /* boutons, tags, badges */ + --radius-md: 6px; /* cartes — jamais plus */ + --border-card: 1px solid var(--line); + --border-btn: 2px solid var(--or-oki); + --space-1: 0.75rem; --space-2: 1rem; --space-3: 1.5rem; --space-4: 2rem; --space-5: 4rem; + --container: 1200px; +} +``` +- Sections : `padding: var(--space-5) var(--space-4)` · Grille : `repeat(auto-fit, minmax(300px, 1fr))`. +- **Carte OKI canonique** : fond blanc 3 %, liseré gauche 4 px d'accent (vert ou or), survol `translateY(-2px à -4px)` + bordure d'accent. +- **Tag OKI canonique** : fond or 8 % + bordure or 1 px, rayon 3 px. + +### 2.4 La flag-bar — signature n°1 + +Bandeau **6 px, 4 segments francs** : noir 0–25 % · or 25–50 % · vert 50–75 % · rouge 75–100 %. Arrêts nets, **jamais de dégradé**. + +```css +.flag-bar { + height: 6px; + background: linear-gradient(to right, + var(--noir-oki) 0 25%, var(--or-oki) 25% 50%, + var(--vert-oki) 50% 75%, var(--rouge-oki) 75% 100%); +} +``` +Placement : bord inférieur de la navigation fixe ; peut ponctuer un héros ou un pied de page. **Une occurrence visible par écran maximum.** C'est l'élément le plus reconnaissable de la marque — le diluer, c'est le tuer. + +### 2.5 Logotype + +Monogramme « OKI » : arche or, point vert, accent rouge sur noir. Fichier canonique `logos/favicon-1.png` (512×512, transparent — aussi og:image et apple-touch-icon) ; déclinaisons `logos/favicon-0.ico`, `assets/maskable_oki.png`. Les `logos/header-img-*` sont des marques de **partenaires** (Joukawouvè, Aktivist Vybz, cyber-mawonaj, KA UBUNTU) : provenance conservée, **ne jamais les utiliser comme logo OKI**. + +--- + +## 3. MOTION OKI — LES CADENCES GWOKA + +La marque ne bouge pas comme une startup : elle pulse comme un ka. Tokens obligatoires dans tout projet OKI animé : + +```css +:root { + --ease-ka: cubic-bezier(0.22, 1, 0.36, 1); /* temps fort : attaque franche, longue tenue */ + --ease-syncope: cubic-bezier(0.65, 0, 0.35, 1); /* contretemps : symétrique, urgent */ + --dur-tanbou: 120ms; /* double-croche — micro-interactions, hovers */ + --dur-mesure: 480ms; /* une mesure — entrées de section */ + --dur-phrase: 960ms; /* deux mesures — transitions de page */ +} +``` + +Règles de motion : +1. **Le stagger de marque OKI est syncopé (3+3+2)**, pas linéaire : délais `[0, 120, 300, 360, 600, 660, 900, 960]` ms pour 8 éléments. C'est subtil — c'est le but : le rythme est une identité, pas un effet. +2. Uniquement `transform` + `opacity` (recette P3/budgets). +3. `prefers-reduced-motion` : gate unique au provider, contenu statique complet (le site actuel n'a **aucune** garde — à corriger partout). +4. Durées toujours lues depuis les tokens, jamais hardcodées. + +--- + +## 4. ICONOGRAPHIE — LA RÈGLE « ZÉRO EMOJI EN PRODUCTION » + +Le site actuel utilise des emojis (✊🏿🔓🤝🏿🛡️) : rendu dépendant de l'OS, incohérent avec la précision du drapeau. **Tout projet harmonisé remplace les emojis par le set SVG OKI.** + +Spécifications du set : +- ViewBox 24×24, **stroke 2 px**, angles nets (pas de courbes organiques), une seule couleur : `currentColor` (or par défaut sur noir). +- Remplissage réservé aux pictogrammes ≤ 16 px. +- Grille de nommage en KA : les fichiers portent des noms kréyòl. + +Set initial à produire (correspondances avec l'existant) : + +| Fichier | Sujet | Remplace / usage | +|---|---|---| +| `ka.svg` | tambour ka | icône maîtresse, 404, loader | +| `lambi.svg` | conque lambi | communication, annonces, « bòkaz » | +| `zetwal.svg` | étoile 4 branches | navigation, favoris, instances fédivers | +| `mawon.svg` | silhouette/brisure de chaîne stylisée | souveraineté, valeurs | +| `lakanmou.svg` | flamme | engagement, dons ponctuels | +| `jaden.svg` | pousse/jardin | solidarité, lakou, projets | +| `kannen.svg` | canne à sucre stylisée | patrimoine, histoire | +| `lanme.svg` | vague | océan, liens internationaux, Réunion↔Gwada | +| `glo.svg` | poing levé géométrique | ✊🏿 — luttes, tarifs solidaires | +| `kle.svg` | clé/cadenas ouvert | 🔓 — liberté, open-source | +| `pawol.svg` | bulle de parole angulaire | langues, traduction, PAWÒL-NU | +| `mizik-note.svg` | note de musique | MIZIK, DJANGOKAM | + +Chaque picto livré en `` dans un sprite `icons.svg` + usage `` (une seule requête, stylable en CSS). + +--- + +## 5. VOIX & TON (canonique, reprise du BRAND.md) + +Vouvoiement direct et empathique, phrases courtes. On parle d'abord de la cause du visiteur — « Vous luttez, vous organisez, vous mobilisez » — puis de l'outil. La technique sert la lutte, jamais l'inverse. + +- **À dire :** sur mesure · tarifs adaptés à vos moyens · organisations engagées · transition numérique · écosystème numérique libre · à la hauteur. +- **À éviter :** « template générique », jargon corporate/marketing, « clé en main », startup-speak. +- Piliers : ORGANISATION KA · INTERNATIONALE · Des solutions pour chaque besoin · Le budget ne doit jamais être un frein. +- **Langues :** FR par défaut ; le KA (kréyòl) en signature, titres de sections symboliques et microcopy (404, loader, remerciements) ; EN en version dédiée. Attribut `lang` correct à chaque bascule. + +## 6. IMAGERIE (canonique) + +Illustration numérique afrofuturiste caribéenne : scènes au coucher de soleil, personnages caribéens, pochettes musicales typographiées. Couleurs chaudes saturées sur fonds sombres. **Jamais** de photo de banque d'images corporate, jamais d'illustration générique de startup, jamais d'icône ou de logo en guise d'image. Formats : AVIF/WebP, self-hébergés, `loading="lazy"` hors LCP. + +--- + +## 7. MATRICE D'APPLICATION PAR PROJET + +Harmoniser ≠ tout refaire : chaque plateforme a un canal de personnalisation officiel. N'UTILISER QUE ces canaux (jamais de fork pour du style). + +| Projet | Logiciel | Canal de theming | Application OKI | +|---|---|---|---| +| **o-k-i.net** | SvelteKit (migration en cours) | Natif | Référence absolue : tokens §2, flag-bar, village SVG, KineticText gwoka | +| **BOKANTE** | Mastodon | CSS personnalisé admin + `custom.css` | Variables Mastodon remappées sur tokens OKI : fond `--noir-oki`, accent `--or-oki`, boutons Archivo uppercase ; logo colonne = monogramme OKI ; flag-bar en haut de la colonne de composition | +| **GADE** | PeerTube | Thème CSS admin (champ « CSS personnalisé ») | Mêmes remappages ; player : contrôles or sur noir ; page À-propos = texte marque (« vidéos sans algorithme, dédiées aux cultures afro-diasporiques ») | +| **NIYAJ** | Nextcloud | App **Theming** officielle | Couleur primaire `#FDB813`, fond sombre, logo + slogan « Votre cloud, vos données » ; favicon OKI | +| **MIZIK** | Funkwhale | `custom.css` admin | Fond `--noir-oki`, accents or, player accentué or ; typographie : Inter si surchargeable, sinon système | +| **KUTE** | App PHP maison | Natif | Appliquer les tokens §2 en entier — c'est un projet first-party, aucune excuse de ne pas être au niveau d'o-k-i.net | +| **LABOLA** | Forgejo/Gitea | `custom/templates` + `custom/public/css` | Thème sombre OKI, accent or ; logo forge = monogramme ; **ne pas toucher aux templates Go au-delà du header/footer** (fragile aux upgrades) | +| **PAWÒL-NU / djangokam.pawol.nu / nyyoka.pawol.nu** | Selon stack | Natif | Charte complète + liberté artistique accrue (pochettes, kinetic type poussé) : c'est la vitrine culturelle, le motion peut y être plus riche qu'ailleurs | +| **fediverse.o-k-i.net** | Statique | Natif | **Constellation zétwal** : chaque instance = étoile SVG reliée, hover/focus = éclat or + label ; DOM/SVG léger uniquement | + +### Le footer fédéré commun (tous les sous-domaines) + +``` +[monogramme OKI] Un service libre opéré par ORGANISATION KA INTERNATIONALE · o-k-i.net +``` ++ flag-bar 6 px au-dessus du footer. Une seule ligne, `--muted`, lien or. C'est le fil rouge qui fait l'écosystème. + +### Règles cross-projets + +1. **Un domaine, une famille** : `*.o-k-i.net` = écosystème fédivers et outils ; `pawol.nu` = projets culturels/musicaux. Ne pas mélanger. +2. Chaque service porte un **nom KA + le nom du logiciel en sous-titre** (pattern existant : « BOKANTE — Mastodon », « NIYAJ — Nextcloud »). Ne jamais masquer le logiciel sous-jacent : la transparence est un pilier. +3. Favicon = monogramme OKI partout (déclinaisons ico/png/maskable). +4. og:image par service : monogramme sur `--noir-oki` + nom du service en Archivo or. +5. Toute nouvelle page statique du réseau démarre du template o-k-i.net (tokens inclus), jamais de zéro. + +--- + +## 8. CHECKLIST D'HARMONISATION D'UN PROJET (DoD marque) + +- [ ] Thème sombre par défaut, clair opt-in contrasté AA. +- [ ] Tokens §2 importés (pas de valeur recopiée à la main — un fichier `oki-tokens.css` partagé). +- [ ] Or = seule couleur d'action ; rouge/vert dans leurs rôles sémantiques. +- [ ] Archivo/Inter self-hébergées, zéro fonte externe. +- [ ] Flag-bar présente, segments francs, ≤ 1 par écran. +- [ ] Zéro emoji en interface (set SVG §4). +- [ ] Tokens motion §3 + gate `prefers-reduced-motion`. +- [ ] Footer fédéré + monogramme + favicon OKI. +- [ ] Voix conforme §5 (test : aucune occurrence de « clé en main », « solution innovante », « disruptive »). +- [ ] Zéro requête tierce au chargement (fonts, CDN, analytics) — vérifié onglet réseau. +- [ ] Nom KA + logiciel en sous-titre visible en page d'accueil du service. diff --git a/doc2sveltekit-transition/playbook-oki-sveltekit.md b/doc2sveltekit-transition/playbook-oki-sveltekit.md new file mode 100644 index 0000000..e828878 --- /dev/null +++ b/doc2sveltekit-transition/playbook-oki-sveltekit.md @@ -0,0 +1,494 @@ +# PLAYBOOK UNIFIÉ — OKI × SVELTEKIT +### Document de référence opérationnel pour agent de code — marque OKI, méthode Awwwards-grade, conventions Svelte 5, briques réutilisables + +> **Nature :** fusion de `charte-oki-design-system.md`, `recette-sveltekit-playbook-agent.md`, `recette-sveltekit-15-sites-awwwards.md`, `svelte-5_code_writer.md`, `svelte_core_bestpractices.md` et `AGENTS.md`, enrichie des leçons réelles de **6 missions** (juillet 2026) : migration o-k-i.net, refonte atlas-fediverse, finalisation exitchatcontrol, migration gwada-sirius, refonte UX/motion du jeu JWE, réparation et modernisation d'oki-podcast-reader (voir §5, §5b et §5c). Document **local, non versionné** (`.gitignore`). +> +> **Usage :** à fournir à l'agent pour toute demande du type « transforme ce projet web en projet Svelte aux couleurs OKI, style Awwwards » ou « crée un site/PWA dans cette charte ». Compléter avec le brief de mission du §7. +> +> **Priorités en cas de conflit :** la marque (§1) prime pour couleurs, typo, voix, iconographie · la méthode (§2) prime pour la technique · les budgets et l'accessibilité ne se négocient jamais. + +--- + +## 0. TYPES DE MISSION + +**A. Transformation** d'un site existant (HTML statique, WordPress, Eleventy, SPA sans DA) → SvelteKit aux couleurs OKI. Toujours commencer par l'audit (§2.4 phase 1) — le descriptif du site source est **toujours** à vérifier contre le dépôt réel (la migration o-k-i.net partait d'un brief « HTML statique » alors que le site était Eleventy + 38 JSON i18n). + +**B. Création** d'un nouveau site/PWA dans la charte OKI. Commencer au §2.4 phase 2, en copiant les briques du §4. + +**C. Finalisation** — le chantier est déjà avancé (migration à 90 %, WIP non commité). Le travail est de **finir, pas recommencer** : builder d'abord, committer l'existant cohérent, puis combler les manques (typiquement SEO et régressions du changement de générateur). + +**D. Refonte ciblée UX/motion** — la stack est déjà SvelteKit. Le travail est chirurgical : remplacer les pièces défaillantes (fond, carte, timeline, layout) et appliquer charte + motion, sans tout réécrire. + +Quatre règles transverses, apprises sur 5 projets : +1. **Contenu éditorial jamais réécrit** sans instruction explicite ; les données priment sur les suppositions (ex. KUTE = Castopod dans les JSON, pas « app PHP » comme le brief le supposait). +2. **Toujours vérifier l'existence de git en premier** (atlas-fediverse n'avait AUCUN dépôt — `git init` + commit de l'existant avant toute modification, sinon travail irréversible). +3. **Charte §7 « harmoniser ≠ tout refaire »** : si le projet a déjà un design system de la famille OKI (tokens panafricains, polices accessibilité), on ne rebrande pas — on applique méthode et qualité (exitchatcontrol, gwada-sirius). La charte complète ne s'applique qu'aux projets sans identité propre (atlas). +4. **Vérifier l'état réel avant de scoper** : sur 5 projets, 2 étaient déjà SvelteKit (atlas, JWE), 1 en migration à 90 % (exitchatcontrol), 1 à 80 % (gwada-sirius) — une seule vraie migration complète. + +--- + +## 1. MARQUE OKI (canonique — prime sur tout le reste) + +### 1.1 Identité en une phrase + +Un studio web militant caribéen dont la marque est un **drapeau panafricain — noir, or, vert, rouge — posé sur un fond presque noir, en capitales Archivo**. Afrofuturiste caribéen : un drapeau, pas une charte corporate. + +Trois conséquences non négociables : +1. **Thème sombre = identité par défaut** ; le clair est un opt-in (`html.light-theme`) qui assombrit les accents pour WCAG AA. +2. **L'or porte toute l'interaction** — seule couleur d'action. +3. **Angles nets partout** — aucune forme organique, aucune bulle très arrondie. + +### 1.2 Tokens couleurs (mesurés sur le site réel) + +```css +:root { + /* Noyau (thème sombre = défaut) */ + --noir-oki: #0D0D0D; /* background */ + --noir-profond: #1A0F1A; /* surface */ + --blanc-creme: #FFF8E7; /* foreground */ + --line: rgba(255,255,255,0.10); /* filets — jamais de gris plein */ + --or-oki: #FDB813; /* accent : SEULE couleur d'action */ + --rouge-oki: #FF1654; /* signal, ponctuation — jamais un lien/bouton */ + /* Étendue (parcimonie) */ + --vert-oki: #00D66C; /* succès, validation, dons mensuels */ + --turquoise-caraibes: #00CED1; + --jaune-soleil: #FFD700; + --orange-flamme: #FF6B35; + --violet-nuit: #6B2D5C; + --bleu-ocean: #0077BE; + --or-clair: #FFE066; /* survol des boutons */ + --gris-sombre: #2D1B2E; + /* Sémantique dérivée */ + --muted: color-mix(in srgb, var(--blanc-creme) 70%, transparent); + --card-bg: rgba(255,255,255,0.03); +} +/* Thème clair — opt-in, accents assombris WCAG AA */ +html.light-theme { + --noir-oki: #FFF8E7; --blanc-creme: #0D0D0D; + --noir-profond: #F5F0E8; --gris-sombre: #E8DDD0; + --or-oki: #B87A00; --vert-oki: #006B3D; --rouge-oki: #A01030; + --turquoise-caraibes: #006B75; --bleu-ocean: #004B7F; + --or-clair: #D99000; --jaune-soleil: #B87A00; --orange-flamme: #C85000; + --line: rgba(0,0,0,0.12); --card-bg: rgba(0,0,0,0.03); +} +``` + +Règles d'usage : texte secondaire = crème à 70-85 % (`--muted`), pas de couleur dédiée · filets = blanc 10 % · **Rouge = signaler · Vert = valider · Or = agir — ne jamais permuter**. + +### 1.3 Typographie + +```css +--font-display: 'Archivo', 'Arial Black', sans-serif; /* 600-900 : TITRES + BOUTONS */ +--font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; /* 400-800 */ +``` + +- Titres h1-h4 : Archivo, MAJUSCULES, `letter-spacing: -0.01em`. +- Boutons : Archivo 700, uppercase, `letter-spacing: 0.03em`, bordure 2 px. +- Corps : Inter, `line-height: 1.5`. +- **Self-hébergement obligatoire** : woff2 dans `static/fonts/` + `fonts.css`, `font-display: swap`, preload de la display 700. Aucun appel Google Fonts (doctrine souveraineté). Source pratique : `@fontsource/archivo` + `@fontsource/inter` (npm), copier les `*-latin-*-normal.woff2`. + +### 1.4 Formes, espacements, layout + +```css +--radius-sm: 3px; --radius-md: 6px; /* jamais plus */ +--border-card: 1px solid var(--line); +--border-btn: 2px solid var(--or-oki); +--space-1: .75rem; --space-2: 1rem; --space-3: 1.5rem; --space-4: 2rem; --space-5: 4rem; +--container: 1200px; +``` + +- Sections : `padding: var(--space-5) 0` · Grilles : `repeat(auto-fit, minmax(300px, 1fr))`. +- **Carte canonique** : fond `--card-bg`, liseré gauche 4 px d'accent, survol `translateY(-2px à -4px)` + bordure d'accent. +- **Tag canonique** : fond or 8 % + bordure or 1 px, rayon 3 px. +- **Bouton canonique** : balayage or `::after scaleX(0→1)` origin left au hover, `--ease-syncope`. + +### 1.5 Flag-bar — signature n°1 + +Bandeau **6 px, 4 segments francs** : noir 0-25 % · or 25-50 % · vert 50-75 % · rouge 75-100 %. Arrêts nets, **jamais de dégradé entre segments**. + +```css +.flag-bar { height: 6px; background: linear-gradient(to right, + #0D0D0D 0 25%, var(--or-oki) 25% 50%, var(--vert-oki) 50% 75%, var(--rouge-oki) 75% 100%); } +``` + +Placement : bord inférieur de la nav fixe + haut du footer. **Une occurrence pleine largeur visible par écran max.** Pour ponctuer ailleurs (titres de section, loader), utiliser le **flag chip** : même 4 segments, 56 × 6 px (voir §4). En thème clair, le segment noir devient `#000` pur. + +### 1.6 Logotype + +Monogramme « OKI » : arche or, point vert, accent rouge sur noir. Fichier canonique 512×512 transparent (aussi og:image, apple-touch-icon, maskable PWA). Ne jamais utiliser un logo de partenaire comme logo OKI. + +### 1.7 Motion — les cadences gwoka + +```css +--ease-ka: cubic-bezier(0.22, 1, 0.36, 1); /* temps fort : attaque franche, longue tenue */ +--ease-syncope: cubic-bezier(0.65, 0, 0.35, 1); /* contretemps : symétrique, urgent */ +--dur-tanbou: 120ms; /* double-croche — micro-interactions, hovers */ +--dur-mesure: 480ms; /* une mesure — entrées de section */ +--dur-phrase: 960ms; /* deux mesures — transitions de page, moments solennels */ +``` + +1. **Le stagger de marque est syncopé (3+3+2)** : délais `[0, 120, 300, 360, 600, 660, 900, 960]` ms pour 8 éléments (puis cycles de +1200 ms). Le rythme est une identité, pas un effet. +2. Uniquement `transform` + `opacity` (+ `clip-path` pour les masques). +3. **Gate unique `prefers-reduced-motion`** au niveau global, contenu statique complet sans JS ni motion. +4. Durées toujours lues depuis les tokens, jamais hardcodées. + +### 1.8 Iconographie — zéro emoji en production + +Set SVG OKI : viewBox 24×24, **stroke 2 px**, angles nets, `currentColor` (or par défaut), remplissage réservé aux ≤ 16 px, **noms kréyòl**. Livré en `` dans un sprite `icons.svg` + ``. + +Set de référence (existant dans o-k-i.net) : `ka` (tambour, icône maîtresse, 404/loader) · `lambi` (conque — annonces) · `zetwal` (étoile 4 branches — navigation, instances) · `mawon` (brisure de chaîne — souveraineté) · `lakanmou` (flamme — engagement, dons) · `jaden` (pousse — solidarité, projets) · `kannen` (canne — patrimoine, institutions) · `lanme` (vague — international) · `glo` (poing — luttes, tarifs solidaires) · `kle` (cadenas ouvert — liberté, open-source) · `pawol` (bulle angulaire — langues, traduction) · `mizik-note` (note — musique). + +**Pattern contenu** : si des emojis vivent dans les textes (JSON), utiliser un helper `splitLeadingEmoji()` qui convertit l'emoji décoratif en picto du sprite sans toucher au texte (implémenté dans o-k-i.net `sections/pictos.ts`). + +### 1.9 Voix & ton + +Vouvoiement direct et empathique, phrases courtes. La cause du visiteur d'abord, l'outil ensuite. **À dire :** sur mesure · tarifs adaptés à vos moyens · organisations engagées · transition numérique · écosystème numérique libre. **À éviter :** « template générique », « clé en main », jargon corporate, startup-speak. Piliers : ORGANISATION KA · INTERNATIONALE · Des solutions pour chaque besoin · Le budget ne doit jamais être un frein. **Langues :** FR par défaut ; KA (kréyòl) en signature et microcopy (404, loader, remerciements — attribut `lang="gcf"`) ; EN en version dédiée. + +### 1.10 Imagerie + +Illustration afrofuturiste caribéenne : couleurs chaudes saturées sur fonds sombres. Jamais de stock corporate, jamais d'illustration startup générique. AVIF/WebP self-hébergés, `loading="lazy"` hors LCP. + +### 1.11 Écosystème + +- Nom KA + logiciel en sous-titre (« BOKANTE — Mastodon ») : la transparence est un pilier. +- Footer fédéré sur tous les sous-domaines : `[monogramme] Un service libre opéré par ORGANISATION KA INTERNATIONALE · o-k-i.net` + flag-bar 6 px au-dessus. +- Favicon = monogramme partout. `*.o-k-i.net` = outils/fédivers ; `pawol.nu` = projets culturels (motion plus riche autorisé). + +--- + +## 2. MÉTHODE TECHNIQUE (la recette) + +### 2.1 Les 5 principes + +- **P1 — Le scroll est le moteur.** Une seule instance Lenis au layout racine, cadencée par le ticker GSAP unique. Jamais de double rAF. +- **P2 — Bake au build, pas au runtime.** Tout ce qui ne dépend pas d'une entrée live est précalculé : images responsive (vite-imagetools), textures, polices. +- **P3 — DOM d'abord, WebGL seulement où ça paie.** Zentry a fait du « 3D » primé en pur DOM (clip-path, preserve-3d, masks). Pour l'audience OKI (mobile 4G, Mali-G52) : **DOM/SVG uniquement** par défaut. +- **P4 — Le motion se designe avant les pages** (tokens §1.7 d'abord, composants ensuite). +- **P5 — Chaque état est un moment designé** : loader, 404, vide, hors-ligne — en KA avec le tambour `ka`. + +### 2.2 Interdictions absolues + +- Jamais de dépendance installée sans être importée et utilisée (les outils d'audit — lighthouse, puppeteer — s'installent en `--no-save` ou se retirent). +- Jamais de SSR désactivé globalement pour « faire marcher » une lib client. +- Jamais animer autre chose que `transform`/`opacity`/`clip-path` en JS. +- Jamais de page dont le contenu est invisible sans JavaScript. +- Jamais de service tiers (fonts, CDN, analytics) — tout asset se self-héberge. + +### 2.3 Stack cible + +``` +SvelteKit 2 + Svelte 5 (runes) + TypeScript strict +@sveltejs/adapter-static (prerender intégral, trailingSlash 'always') +vite-imagetools (AVIF/WebP responsive au build) +vite-plugin-pwa (generateSW — shell hors-ligne) +lenis + gsap (imports dynamiques uniquement) +@fontsource/* (polices woff2 copiées dans static/fonts/) +CSS vanilla : oki-tokens.css + base.css + styles scopés — pas de Tailwind +``` + +### 2.4 Workflow en 7 phases + +1. **Audit** (toujours) : pages, sections, assets, liens externes, stack mesurée, bugs de production listés AVANT toute refonte, contradictions doctrine/outillage (ex. discours anti-GAFAM + Google Fonts). Rapport validé avant de coder. +2. **Tokens & thème** : `oki-tokens.css` (§1), thème sombre défaut + clair opt-in, fonts self-hébergées, anti-FOUC par script externe (CSP). +3. **Architecture** : routes, i18n, SEO, layout. `prerender = true` partout. URLs historiques conservées. +4. **Composants** : uniquement ceux pertinents pour CE site. Chacun avec cleanup et garde reduced-motion. +5. **Motion** : transitions Svelte natives · scrub via `animation-timeline: view()/scroll()` natif d'abord, fallback GSAP ScrollTrigger en import dynamique · une seule horloge (gsap.ticker → Lenis). +6. **Performance** : budgets §2.7, pipeline images, mesure sur profil mobile. +7. **Accessibilité & dégradation** : ladder §2.8, revue clavier, contenu canvas/SVG doublé en DOM. + +### 2.5 Architecture de référence (éprouvée sur o-k-i.net) + +``` +src/ + app.html # %lang% + scripts externes (theme, lang-redirect, registerSW) + hooks.server.ts # lang fr/en via transformPageChunk (replaceAll '%lang%') + app.d.ts / imagetools.d.ts + lib/ + styles/oki-tokens.css # charte §1 — LE fichier de tokens partagé + styles/base.css # reset, primitives (btn, card, tag, flag-bar, icon), gate reduced-motion + i18n/ # bundles JSON par locale + index.ts (getBundle, localeFromPath, alternatePath) + assets/images/ # sources pour vite-imagetools + motion/ # tokens.ts, scroll.ts (Lenis), reveal.ts, tilt.ts + components/ # Seo, Nav, Footer, ResponsiveImage, motion/, icons/, sections/ + routes/ + +layout.svelte # skip-link, loader, progress, nav, footer, View Transitions, Lenis + +layout.ts # prerender = true, trailingSlash = 'always' + +error.svelte # 404/erreurs designée (KA) + offline/+page.svelte # cible navigateFallback du SW +static/ + fonts/ icons.svg images/ theme.js lang-redirect.js registerSW.js + manifest.webmanifest 404.html robots.txt sitemap.xml _headers .htaccess +``` + +### 2.6 Composants motion (contrats) + +- **KineticText** — titrage au scroll, split par **mots** (jamais caractères — apostrophes), `aria-label` sur titres h1-h4 (jamais sur span : prohibé), scrub `animation-timeline: view()` + fallback ScrollTrigger dynamique, stagger syncopé converti en plages de scroll. +- **ScrollProgressBar** — `scaleX` via rAF + écriture DOM directe (jamais d'état réactif par frame), `role="progressbar"`. +- **PageTransition** — `onNavigate` + View Transitions API, durée lue des tokens ; fallback sobre = navigation instantanée. +- **IntroLoader** — cérémonie 1re visite (`sessionStorage`), skippable, 100 % CSS pilotée par une classe posée par le script externe anti-FOUC (pas de JS inline — CSP). +- **Marquee** — bande typographique CSS pure, contenu dupliqué `aria-hidden`, pause au hover, coupée en reduced-motion. +- **use:reveal** — action IO, cascade syncopée via `--d`, état caché **uniquement** sous `html.js` + `prefers-reduced-motion: no-preference`. +- **use:tilt** — tilt 3D pointeur via variables CSS `--rx/--ry` + rAF, off tactile + reduced-motion. +- **FlagChip** — mini flag-bar 56×6 px qui se dessine (`scaleX`) à l'entrée du titre. +- **Village écosystème** — scène SVG isométrique des services, chaque bâtiment = `` focusable (`aria-label` « NOM — Logiciel »), hover/focus = liseré or + label, fallback grille accessible (reduced-motion + mobile étroit, bascule `display: none` pour éviter les doubles tab stops). +- **CursorTracker, WebGL** : non retenus pour l'audience OKI (mobile-dominante). + +### 2.7 Budgets (mobile 4G, Android entrée de gamme) + +| Ressource | Budget | Mesuré o-k-i.net | +|---|---|---| +| JS initial compressé | ≤ 170 Ko | 69 Ko gzip (+45 Ko dynamiques) | +| Poids total accueil | ≤ 2 Mo | ~0,9 Mo | +| Média hero avant interaction | ≤ 400 Ko | ~12 Ko (logo) | +| Lighthouse mobile | ≥ 90/95/95/95 | 91/100/100/100 | +| Animations JS concurrentes | ≤ 8-12, transform/opacity | OK | +| LCP / INP / CLS | < 2,5 s / < 200 ms / < 0,05 | 2,9 s / 59 ms TBT / 0,001 | + +### 2.8 Accessibilité & ladder de dégradation + +1. `saveData`/`effectiveType` ≤ 3g → images statiques, zéro préchargement. +2. JS désactivé → contenu SSR complet et lisible (états cachés conditionnés à `html.js`). +3. `prefers-reduced-motion` → gate unique : pas de Lenis, pas de loader, pas de scrub, contenu statique complet. +4. Canvas/SVG décoratifs `aria-hidden` + miroir DOM sémantique. +5. `lang` correct à chaque bascule ; focus déplacé sur le contenu après transition de route ; skip-link ; focus visible or. +6. Pièges éprouvés : `aria-label` interdit sur `` générique · `
` enfant direct de `
` · liens dans le texte soulignés (scoper la règle à `main` pour ne pas toucher nav/footer). + +### 2.9 PWA légère + +- `static/manifest.webmanifest` manuel (nom KA, monogramme any + maskable, `theme_color`/`background_color` `--noir-oki`, `display: standalone`). +- `vite-plugin-pwa` : `registerType: 'autoUpdate'`, `injectRegister: false`, `manifest: false`, workbox `navigateFallback: '/offline/index.html'`, `navigateFallbackDenylist` pour les assets binaires, `globIgnores` pour les gros fichiers (PDF), `maximumFileSizeToCacheInBytes: 3 Mo`, `cleanupOutdatedCaches`, `clientsClaim`, `skipWaiting`. +- Enregistrement par **fichier statique** `static/registerSW.js` avec chemins absolus (`navigator.serviceWorker.register('/sw.js', { scope: '/' })`), lié dans `app.html`. Ne PAS utiliser l'injection du plugin : sans `@vite-pwa/sveltekit` elle n'atteint pas le HTML pré-rendu, et le `registerSW.js` généré utilise un chemin relatif `./sw.js` cassé sur les routes imbriquées (`/en/`, `/dons/`). + +### 2.10 Sécurité & CSP + +CSP de référence (tout self-hébergé) : +``` +default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; +img-src 'self' data:; font-src 'self'; connect-src 'self'; +frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none' +``` ++ HSTS `max-age=31536000`, `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy` restrictive. Livrée en **deux formats** : `static/.htaccess` (Apache/o2switch : + `ErrorDocument 404 /404.html`, redirect HTTPS, cache immutable `/_app/` et `/fonts/`) et `static/_headers` (Cloudflare Pages). Règles : **aucun script inline** (snippets thème/langue en fichiers externes) ; `style` attributes OK (`unsafe-inline` style) ; 404 statique autonome (`static/404.html`, zéro dépendance). + +### 2.11 SEO + +Composant `Seo.svelte` : title/description par page, canonical, hreflang fr/en/x-default via mapping de paires d'URLs, OG ×10 (og:image self-hébergée), Twitter Card, JSON-LD (Organization + entités du projet). `robots.txt` + `sitemap.xml` statiques à jour. Astuce : les `` dans les template literals `{@html}` doivent être échappés (`<\/script>`). + +--- + +## 3. CONVENTIONS SVELTE 5 (obligatoires) + +- **Runes only** : `$state` (uniquement pour du réactif — sinon variable plain ; gros objets réassignés : `$state.raw`), `$derived` (jamais d'`$effect` pour calculer), `$props` (traiter les props comme changeantes : valeurs dépendantes en `$derived`). +- **Événements** : `onclick={...}` (jamais `on:click`) ; listeners window/document via `` / ``. +- **`{#each}` à clé unique** (jamais l'index). +- **CSS** : variables JS → directive `style:--var={val}` ; styles enfants via custom properties, `:global` en dernier recours ; états globaux (`html.js`, `html.light-theme`, `html.intro-pending`) via `:global(...)`. +- **États réactifs par-frame interdits** : scrollY/pointeur → variables plain + flush rAF (ou variables CSS directement). +- **Init/cleanup** : GSAP/Lenis/IO dans `onMount`/`$effect` avec cleanup symétrique (`kill()`/`revert()`/`destroy()`/`disconnect()`). +- **Liens internes** : `resolve()` / assets via `asset()` de `$app/paths` ; externes en `rel="external noopener noreferrer"` + `target="_blank"`. +- **{@html}** : uniquement sur contenu first-party (JSON du dépôt), commenté comme tel. +- **Autofixer (AGENTS.md)** : après tout composant modifié — + ```bash + npx @sveltejs/mcp list-sections # doc Svelte 5 + npx @sveltejs/mcp get-documentation "$state,$derived" + npx @sveltejs/mcp svelte-autofixer ./src/lib/MonComposant.svelte + ``` + (Échapper `\$` dans le code inline. La sortie est un objet JS, pas du JSON strict.) `npm run check` à **0 erreur / 0 warning** avant tout commit. + +--- + +## 4. BRIQUES RÉUTILISABLES (référence : dépôt o-k-i.net, branche `svelte`) + +Copier et adapter, ne pas réécrire. Chaque brique a un contrat stable. + +| Brique | Chemin dans o-k-i.net | Contrat / usage | +|---|---|---| +| Tokens charte | `src/lib/styles/oki-tokens.css` | importé en premier dans `+layout.svelte` | +| Primitives | `src/lib/styles/base.css` | `.btn .card .tag .icon .flag-bar .container .section`, gate reduced-motion, reveal | +| Fonts | `static/fonts/` + `fonts.css` | Archivo 600-900, Inter 400-800, woff2 latin, preload 700 | +| Anti-FOUC thème | `static/theme.js` | pose `.js`, `.light-theme`, `.intro-pending` ; localStorage `oki-theme` | +| Redirection langue | `static/lang-redirect.js` | préférence `oki-lang-pref`, sinon `navigator.language` | +| i18n | `src/lib/i18n/index.ts` | bundles JSON par locale ; `getBundle(locale)`, `localeFromPath`, `alternatePath` (paires FR↔EN) | +| SEO | `src/lib/components/Seo.svelte` | `` — meta + OG + Twitter + JSON-LD | +| Lang par route | `src/hooks.server.ts` | `%lang%` dans `app.html` + `transformPageChunk` | +| Lenis | `src/lib/motion/scroll.ts` | `initSmoothScroll()` (dynamic), `scrollToAnchor(hash)` | +| Tokens motion | `src/lib/motion/tokens.ts` | `prefersReducedMotion()`, `syncopatedDelay(i)` (3+3+2) | +| Reveal | `src/lib/motion/reveal.ts` | `use:reveal={index}` — cascade gwoka | +| Tilt | `src/lib/motion/tilt.ts` | `use:tilt` — CSS `--rx/--ry` | +| KineticText | `components/motion/KineticText.svelte` | `` | +| ScrollProgressBar | `components/motion/ScrollProgressBar.svelte` | global, gradient or→vert | +| IntroLoader | `components/motion/IntroLoader.svelte` | cérémonie 1re visite, skippable | +| Marquee | `components/motion/Marquee.svelte` | `` | +| FlagChip | `components/motion/FlagChip.svelte` | ornement de titre, dessin au scroll | +| Images | `components/ResponsiveImage.svelte` + `imagetools.d.ts` | `import pic from './x.png?format=avif;webp;png&w=…&as=picture'` → ``. **Le wildcard `declare module '*&as=picture'` doit vivre dans un `.d.ts` NON-module** | +| Sprite OKI | `static/icons.svg` | 12 pictos KA (§1.8) — `` | +| Logos marques | `components/icons/BrandIcon.svelte` | `` | +| Village | `components/sections/HostingVillage.svelte` | scène SVG écosystème + fallback grille | +| Nav/Footer | `components/Nav.svelte`, `Footer.svelte` | nav active (IO), dropdown accessible, thème, langue ; footer fédéré | +| Emojis→pictos | `components/sections/pictos.ts` | `splitLeadingEmoji(str)`, `pictoFor(emoji)` | +| Bridge tokens | `atlas-fediverse/src/styles/oki-bridge.css` | alias des anciens noms de tokens vers la charte — applique la charte à un projet existant **sans retoucher chaque composant** | +| Layout baké | `atlas-fediverse/scripts/build-layout.mjs` | d3-force en script node → positions déterministes figées en JSON (init hashée FNV-1a + ticks fixes = bit-identique). Rejouer quand les données changent | +| État partagé inter-sections | `atlas-fediverse/src/lib/software-modal.svelte.ts` | module `$state` + `requestX()/consumeX()` : ouvrir une modale d'un composant depuis une autre section (timeline → catalogue) | +| Layout app 100dvh | `JWE/app/src/lib/styles/jeu.css` | `height: 100dvh` (pas min-height) + overflow par panneau + overlays consolidés (une puce de statut, rangées de boutons hors de la carte, barre d'actions opaque z-indexée) — zéro scroll, zéro superposition | +| Count-up score | `JWE/app/src/lib/motion/countup.ts` | `use:compte={cible}` : rAF → écriture DOM directe, span animé `aria-hidden` + valeur finale en `.sr-only` (région `aria-live`) | +| Reveal SSR | `JWE/app/src/lib/motion/reveal.ts` | variante dont l'état initial est rendu côté serveur, masqué uniquement sous `html.js` + `no-preference` | +| i18n dossiers | `gwada-sirius/src/lib/i18n/index.ts` | catalogues au format Paraglide/inlang consommés par un routeur maison (pas de middleware) — statique-friendly, l'outillage inlang reste utilisable | +| Support volume iOS | `svelte-podcast/src/lib/volume-support.ts` | `can_set_volume()` : sonde l'écriture de `volume` sur un élément jetable (mémoïsée) — détecte iOS par capacité, pas par UA sniffing | +| PWA | `vite.config.ts` (bloc VitePWA) + `static/registerSW.js` + `static/manifest.webmanifest` | voir §2.9 | +| Headers | `static/.htaccess` + `static/_headers` | voir §2.10 | +| 404 / offline | `static/404.html` + `routes/+error.svelte` + `routes/offline/` | KA + tambour `ka`, autonomes | + +--- + +## 5. LEÇONS DE LA MIGRATION o-k-i.net (pièges déjà résolus — ne pas les ré-apprendre) + +1. **Toujours auditer le dépôt réel** : le brief disait « HTML statique », c'était Eleventy + i18n JSON. Le contenu vivait dans les données — les reprendre, pas réécrire. +2. **Les données priment sur le brief** : le village suit `services.json` (4 instances), pas les exemples du brief ; la section cible se décide d'après le contenu JSON, pas le titre supposé. +3. **vite preview meurt si l'on rebuilde pendant qu'il tourne** (il sert `.svelte-kit/output`) — redémarrer après chaque build. +4. **CSP `script-src 'self'`** : anti-FOUC, lang-redirect et registerSW en fichiers externes statiques ; aucun inline. +5. **Preview headless** : `--virtual-time-budget` fige les animations d'entrée (faux négatifs visuels) — valider le motion en temps réel via puppeteer (`--no-save`), pas en screenshot one-shot. +6. **Puppeteer QA** : `page.emulateMediaFeatures` pour reduced-motion ; le tilt se teste par `dispatchEvent(PointerEvent)` (le scroll Lenis décale `mouse.move`). +7. **eslint/autofixer `no-navigation-without-resolve`** : `resolve()` pour routes internes, `asset()` pour fichiers statiques, `rel="external"` pour sortir du scope de la règle — sans jamais envelopper une URL externe ou une ancre `#`. +8. **Grep de vérité** avant de livrer : zéro domaine tiers dans `build/` (hors liens `` métier), zéro emoji dans le HTML buildé, toutes les URLs en 200, `og:` ×10, JSON-LD présents. +9. **Lighthouse après chaque passe** (mobile) et corriger ce qui est remonté — les deux points gagnés sur o-k-i.net : `aria-label` sur span (KineticText) et liens non soulignés. +10. **Lighthouse `link-in-text-block`** : scoper `text-decoration: underline` à `main` — sinon la nav et le footer héritent de soulignés partout. + +## 5b. LEÇONS DES MISSIONS atlas / exitchatcontrol / gwada-sirius / JWE + +**Gouvernance & process** +1. **git d'abord** : atlas n'avait aucun dépôt — `git init` + commit de l'existant avant toute ligne de code. +2. **WIP non commité d'autrui** : builder d'abord ; si c'est cohérent, committer tel quel, puis améliorer par commits séparés (exitchatcontrol). +3. **Orchestration d'agents parallèles** : sérialiser tout ce qui touche `package.json` / npm (un seul agent ou le parent) ; définir les contrats (props des composants, noms de tokens, chemins) AVANT de lancer — les agents s'intègrent alors sans conflit, et découvrent même le travail des autres (module `software-modal` réutilisé en vol). +4. **Conflits de ports** : avant tout audit automatisé, vérifier ce qui tourne (`vite preview` d'un autre projet sur le même port a faussé un audit axe — vérifier le `` servi). + +**Technique SvelteKit** +5. **`paths.relative: false`** quand `paths.base` est utilisé : par défaut `$app/paths.base` vaut `".."` au prerender, ce qui casse toutes les comparaisons de pathname (`startsWith(base)`, switcher de langue, `aria-current`) et produit des hrefs relatifs bizarres dans le HTML prérendu. L'hydratation masque le bug — le vérifier dans le HTML buildé, pas seulement au clic. +6. **`lang` par route avec base path** : retirer `base` dans `hooks.server.ts` AVANT de déduire la locale du premier segment. +7. **i18n sans middleware** : un routeur par dossiers (`/`, `/en/`, `/ht/`) + loader JSON maison bat Paraglide-officiel pour le statique (URLs et prerender sous contrôle total) tout en gardant les catalogues au format inlang. +8. **Îlots → composants natifs** : le montage manuel (`mount.js` + IntersectionObserver) disparaît ; le fallback SSR (table, liste) devient le markup même du composant — meilleur pour no-JS ET pour le CLS. +9. **Leaflet sous SSR** : `await import('leaflet')` dans `onMount` (l'import statique plante côté Node), CSS en import statique OK, `L.divIcon` CSS au lieu d'images de marqueurs. Tuiles OSM = exception documentée au zéro-tiers, à faire trancher par le propriétaire. +10. **CSP d'un jeu/app riche** (adapter-node → `hooks.server.ts`) : énumérer les domaines réels en inspectant le code ET les node_modules (Mapillary a besoin de `graph.` + `tiles.` + `images.mapillary.com`, Wikimedia d'`upload.` + `commons.`) — puis jouer une manche complète en capturant la console : zéro violation exigée. +11. **Layout « app » sans scroll** : `height: 100dvh` (jamais `min-height`) + overflow géré par panneau + indices dans des panneaux scrollables internes. Les bugs de superposition mobile viennent d'overlays absolus empilés : consolider les badges en une puce, sortir les rangées de boutons de la carte, barre d'actions opaque avec `z-index` explicite, `pointer-events: none` sur tout conteneur décoratif. Tester chaque bouton par `elementFromPoint`. +12. **Le « moment de révélation »** (jeux) : ligne de distance dessinée + score en count-up + marqueur qui tombe. Count-up : rAF → DOM direct, jamais d'état réactif par frame ; span animé `aria-hidden` + valeur finale `.sr-only` dans une région `aria-live`. +13. **CSP hashée** (script postbuild qui hashe les inline) : un JSON-LD inline ajouté est hashé automatiquement — mais vérifier le compte d'empreintes après build. +14. **Icônes PNG lourdes** : `PIL.Image.quantize(256)` suffit pour des logos (121 Ko → 9 Ko, perte invisible) — toujours relire l'image après. + +**Contenu & données** +15. **Audit factuel des données** en plus de l'audit technique : la timeline d'atlas contenait 4 dates fausses et un compteur périmé (Twitter 2021→2022, Mastodon 2015→2016, PeerTube 2015→2017, Bluesky 2023→2024, « 55+ »→105). Les champs de données inutilisés (`month`, `softwareId`) sont souvent des fonctionnalités gratuites. +16. **Régressions SEO typiques au changement de générateur** : sitemap, OG hors fiches, og:image, canonical — checklist à passer systématiquement (exitchatcontrol). + +**Déploiement YunoHost** +17. Le package ne consomme que des **archives de tag** : taguer l'app (`vX.Y.Z`), pousser le tag, télécharger l'archive `/archive/<tag>.tar.gz` depuis la forge, recalculer son sha256, mettre à jour `manifest.toml` (version `X.Y.Z~ynhN` + url + sha256), commit/push le package. `autoupdate.strategy = "latest_forgejo_tag"` détecte les tags suivants. + +## 5c. LEÇONS DE LA MISSION oki-podcast-reader (lecteur audio) + +**Gouvernance & forks** +1. **Sécuriser le WIP AVANT tout** : toute la transformation applicative était non commitée sur un fork — premier commit de protection avant la première analyse détaillée (règle générale : `git add -A && git commit "WIP sécurisé"` dès qu'un working tree contient du travail non versionné). +2. **Fork de librairie → application** : purger la dette de packaging (champs `exports`/`files`/`peerDependencies`, changesets, publint, workflows CI de la lib) — le `package.json` d'une app est `private: true` et nu. +3. **Le remote `origin` d'un fork pointe chez l'auteur original** : le renommer `upstream` avant d'ajouter le remote de déploiement, sinon le push part chez l'original (échoue ou pire). + +**Upgrades de framework** +4. **Le mode compat Svelte 5 est une voie complète** : SvelteKit 1→2 + Svelte 4→5 SANS réécriture en runes — la syntaxe legacy (`on:click`, `$:`, `export let`, `<slot>`) compile à 0 erreur/0 warning. Upgrader les paquets, traiter uniquement les breaking changes (imports `vitePreprocess`, options TS supprimées, sérialisation), ne JAMAIS réécrire la syntaxe dans la même passe. La réécriture en runes est une mission séparée, facultative. +5. **Respecter le gestionnaire de paquets du projet** (yarn.lock ≠ package-lock.json) — ne pas mélanger. + +**Règle d'architecture des loads (la plus généralisable)** +6. **Universal load (`+page.ts`) = le corps brut des fetch est inliné dans le HTML** : un flux RSS de 10 Mo produisait une page de 11,9 Mo. **Server load (`+page.server.ts`) garde le fetch côté serveur** — règle d'un coup : bloat supprimé (−88 %), ET les problèmes CORS en navigation client disparus (le navigateur ne refait jamais le fetch : il lit `__data.json`). Toute donnée tierce fetchée appartient à `+page.server.ts`. Corollaire : les pages qui en dépendent ne buildent pas hors-ligne — à documenter. +7. Les redirections CORS se contrôlent à chaque saut : une 302 sans `Access-Control-Allow-Origin` tue le fetch navigateur même si la destination finale l'autorise — raison de plus pour fetcher côté serveur. + +**Audio / média** +8. **Les préférences média persistées doivent être re-appliquées en continu** (souscription), pas seulement au chargement de la source — et `?? 1`, jamais `|| 1` (un 0 persisté est une valeur légitime). +9. **`HTMLMediaElement.volume` est en lecture seule sur iOS** : détecter par capacité (sonder l'écriture sur un élément jetable, mémoïser), masquer le slider, garder le mute (`el.muted`, lui, fonctionne). Source de vérité unique dans le store persisté, synchronisée dans les deux sens. +10. **Cibles tactiles sur les lecteurs** : épaisseur visuelle ≠ zone de hit (piste fine de 10 px OK si la zone fait 44 px). Un `<input type="range">` ne repositionne PAS le thumb au tap tactile — handlers `touchstart`/`touchmove` explicites requis (tester avec `page.tap` + `hasTouch`). +11. **Media Session API est non négociable pour une app audio** (contrôles écran verrouillé) : `metadata` à chaque piste + handlers `play`/`pause`/`previoustrack`/`nexttrack`, gardé par `'mediaSession' in navigator`. +12. **SW d'une app audio** : ne JAMAIS intercepter les streams (requêtes `Range`, extensions média) — précache shell, cache-first sur pochettes uniquement. + +**Perf de listes** +13. **`content-visibility: auto` + `contain-intrinsic-size`** : le gain de perf de listes le moins cher qui existe (CSS seul). Avant toute virtualisation : pagination « charger plus » 50×50 (1022 lignes DOM → 50). + +--- + +## 6. PATTERNS AWWWARDS (distillé des 15 sites — quoi voler, à quel coût) + +| Pattern (source) | Coût | Statut pour OKI | +|---|---|---| +| DOM-only masks/clip-path « 3D » (Zentry) | 5 % du coût GPU | **Adopté** (wipes pochettes) | +| Motion system spec'd avant les pages (Zentry, P4) | 0 | **Adopté** (tokens gwoka) | +| Hover craft budgété — 5 interactions intentionnelles (Noomo) | faible | **Adopté** (balayage boutons, tilt, soulignés, village) | +| Loader-as-fiction (KPR) | faible | **Adopté** (IntroLoader) | +| États designés : 404, vide, offline (Studio375) | faible | **Adopté** (KA + tambour) | +| Dollhouse IA — sections = lieux physiques (Kriss.ai) | moyen | **Adopté** (village créole) | +| Constellation zetwal (charte §7 — prescription portails fédivers) | faible | **Adopté** (hero atlas, remplace un shader Three.js de 708 Ko) | +| Layout baké au build — positions de graphe figées (déclinaison P2) | faible | **Adopté** (carte atlas : SVG 2D déterministe, −750 Ko de JS) | +| Moment de révélation designé — ligne de distance + count-up + chute de marqueur (jeux) | faible | **Adopté** (JWE) | +| Bilingual kinetic type — la bascule de langue comme événement (Nudot) | faible | **Candidat** (évolution du lang-switch FR/KA) | +| Chaptered scroll-comic (ten.375.studio) | moyen | **Candidat** (lore DJANGOKAM, patrimoine) | +| Scroll flipbook connection-aware (Apple) | moyen | **Candidat** si séquence visuelle un jour (gating `effectiveType` obligatoire) | +| Bake noise/textures offline (David Whyte) | faible | **Candidat** (fonds génératifs bakés) | +| Backstage / process public (Immersive Garden) | faible | **Candidat** (contenu pédagogique = souveraineté) | +| Cursor-as-light, velocity particles (Unseen, Igloo) | élevé | **Rejeté** (audience tactile) | +| WebGL full-UI (Igloo), skeletal scroll camera (HAPE), flipbook géant | très élevé | **Rejeté** (budgets 4G/Mali-G52) | +| Resolution gating (KPR) | — | **Interdit** (hostile mobile) | + +--- + +## 7. TEMPLATE DE BRIEF DE MISSION (à remplir par projet) + +```markdown +# MISSION : [Transformer X / Créer Y] selon le PLAYBOOK UNIFIÉ OKI × SVELTEKIT + +## 1. Contexte +- Site/projet : [URL/dépôt] — stack actuelle mesurée : [à auditer, §2.4 phase 1] +- Identité : [association/projet, positionnement] +- Écosystème lié (liens externes à préserver) : [liste] +- Bugs de production confirmés : [liste audit] + +## 2. Contenu éditorial +- [Reprendre à l'identique / réécriture autorisée : périmètre] +- Langues : [FR / EN / KA — URLs] + +## 3. Exigences techniques +- Stack : §2.3 du playbook (défaut) — écarts éventuels : [liste] +- URLs à conserver : [liste + ancres] +- Hébergement cible : [o2switch statique / Cloudflare Pages] +- PWA : [oui/non] + +## 4. Motion & composants (sélection depuis §2.6 — rester sobre) +- [ ] KineticText titres [ ] IntroLoader [ ] Marquee [ ] Village/scène +- [ ] Autre : [préciser] — WebGL : [non par défaut] + +## 5. Périmètre de cette session +- [ ] … + +## 6. Critères d'acceptation +- DoD §8 intégralement vérifiée + [spécifiques projet] +``` + +--- + +## 8. DOD — DÉFINITION DE « TERMINÉ » (tout doit être vrai) + +**Fondations** +- [ ] Rapport d'audit livré ; bugs pré-existants corrigés ou explicitement reportés · **git vérifié/initialisé avant tout travail**. +- [ ] Tokens charte importés (fichier partagé, pas de valeurs recopiées) ; thème sombre défaut, clair opt-in AA. +- [ ] Fonts self-hébergées ; zéro requête tierce au chargement (vérifié onglet réseau + grep du build). +- [ ] `npm run check` : 0 erreur, 0 warning · `npm run build` vert · `svelte-autofixer` propre sur les fichiers modifiés. +- [ ] Si `paths.base` : `paths.relative: false` et hrefs du HTML prérendu inspectés (pas de `../` ni de double préfixe). +- [ ] **Audit factuel des données** (dates, compteurs, champs inutilisés) en plus de l'audit technique. +- [ ] Toute donnée tierce fetchée passe par `+page.server.ts` (un universal load inline le corps brut des fetch dans le HTML). + +**Marque** +- [ ] Or = seule couleur d'action ; rouge/vert dans leurs rôles · angles nets (≤ 6 px). +- [ ] Flag-bar segments francs, ≤ 1 pleine largeur par écran · zéro emoji en interface (sprite SVG). +- [ ] Tokens motion gwoka + stagger syncopé · gate `prefers-reduced-motion` vérifiée manuellement. +- [ ] Voix conforme (test : aucune occurrence de « clé en main », « solution innovante », « disruptive »). + +**Expérience & accessibilité** +- [ ] Navigation clavier complète (tab order, focus visible, skip-link, village/scène/carte focusable). +- [ ] **Zéro superposition** : chaque bouton testé par `elementFromPoint` (mobile 390 px) ; `pointer-events: none` sur tout conteneur décoratif. +- [ ] Layout app : `height: 100dvh` si « tout visible sans scroll » est exigé — `scrollHeight === innerHeight` mesuré mobile ET desktop. +- [ ] Page fonctionnelle et lisible avec JS désactivé. +- [ ] 404, vide et hors-ligne designés (KA + tambour) · loader designé si présent, skippable. +- [ ] `lang` correct par locale · hreflang · OG complet self-hébergé · JSON-LD. + +**Mesures** +- [ ] Lighthouse mobile : Performance ≥ 90, Accessibilité ≥ 95, Best Practices ≥ 95, SEO ≥ 95. +- [ ] JS initial ≤ 170 Ko gzip · accueil ≤ 2 Mo · images optimisées (aucune > 200 Ko hors hero, lazy hors LCP). +- [ ] Toutes les URLs historiques préservées ou redirigées · sitemap/robots à jour. diff --git a/doc2sveltekit-transition/prompt-kimi-cli-transformation-oki.md b/doc2sveltekit-transition/prompt-kimi-cli-transformation-oki.md new file mode 100644 index 0000000..0eb699c --- /dev/null +++ b/doc2sveltekit-transition/prompt-kimi-cli-transformation-oki.md @@ -0,0 +1,79 @@ +# PROMPT KIMI CLI — Transformation de o-k-i.net en SvelteKit + +> **Mode d'emploi :** copie l'intégralité du bloc ci-dessous (entre les lignes `---`) dans Kimi CLI, en joignant les deux documents de référence : `recette-sveltekit-playbook-agent.md` (la méthode) et `charte-oki-design-system.md` (la marque). Ajuste la section « Périmètre de cette session » selon ce que tu veux faire en une passe. + +--- + +# MISSION : Migrer o-k-i.net vers SvelteKit selon la recette et la charte OKI + +Tu es un agent de code senior. Ta mission : reconstruire le site **o-k-i.net** (ORGANISATION KA INTERNATIONALE) en SvelteKit, en suivant **à la lettre** le playbook `recette-sveltekit-playbook-agent.md` (méthode, budgets, DoD) et la charte `charte-oki-design-system.md` (couleurs, typo, voix, iconographie — elle prime pour tout ce qui est marque). + +## 1. Contexte du site existant (audit réel, juillet 2026) + +**Identité :** association tech militante guadeloupéenne — « Le numérique au service de vos luttes ». Applis web sur mesure à tarifs solidaires pour associations, collectifs et organisations militantes. Positionnement souveraineté numérique : indépendance des GAFAM, logiciels libres, hébergement éthique. L'identité visuelle est afrofuturiste caribéenne : drapeau panafricain (noir/or/vert/rouge), **thème sombre par défaut**. + +**Écosystème lié (liens externes à préserver, ne pas migrer) :** +- bokante.o-k-i.net → instance Mastodon +- gade.o-k-i.net → instance PeerTube +- mizik.o-k-i.net → instance Funkwhale +- niyaj.o-k-i.net → Nextcloud +- kute.o-k-i.net → application PHP maison +- labola.o-k-i.net → forge (code source public, dont celui du site) +- fediverse.o-k-i.net → portail fédivers +- pawol.nu + djangokam.pawol.nu + nyyoka.pawol.nu → paroles/traductions/karaokés (projet DJANGOKAM, artiste IA caribéen) + +**Stack actuelle mesurée :** HTML statique + CSS vanilla (~44 KB) + 3 petits JS vanilla (main.js, theme.js, lang-redirect.js). Hébergement o2switch (mutualisé FR). Images via BunnyCDN (`organisationka.b-cdn.net`). Headers de sécurité exemplaires à **conserver à l'identique ou renforcer** : CSP `default-src 'self'` (+ style 'unsafe-inline' + fonts.googleapis.com), HSTS, `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy` restrictive. + +**Bugs et défauts de production confirmés (à corriger dans la migration) :** +1. **CRITIQUE — Toutes les images renvoient 403** (pull zone BunnyCDN cassée) : logo header, logos partenaires, portrait DJANGOKAM, pochettes, logos clients, og:image. Décision cible : **self-héberger toutes les images** dans `static/images/` (AVIF/WebP générés au build via vite-imagetools), supprimer toute dépendance à BunnyCDN, retirer `https://organisationka.b-cdn.net` de la CSP. +2. **Google Fonts** chargé (fonts.googleapis.com + gstatic.com) alors que le site revendique l'indépendance des GAFAM → self-héberger Archivo (600-900) et Inter (400-800) en woff2, retirer les deux domaines de la CSP. +3. Aucun `prefers-reduced-motion` nulle part. +4. Iconographie 100 % emoji (✊🏿🔓🤝🏿🛡️💼🏛️) → set SVG conforme à la charte (angles nets, stroke 2 px, or sur noir). +5. Les 3 cards « publics » (Militants / Associations & Collectifs / Entreprises) apparaissent en double dans la page → une seule occurrence. +6. Lazy-loading d'images partiel (4/~14) → systématique hors LCP. + +**Contenu éditorial (conserver tel quel, sauf mention contraire) :** one-page FR à ancres — Hero (« ORGANISATION KA INTERNATIONALE », 3 badges, 2 CTA) · Solutions ×3 publics · Projets web · Partenaires (Joukawouvè, Aktivist Vybz #AKV, cyber-mawonaj) · DJANGOKAM (artiste IA caribéen, lore Wanakaera/Karukera, liens pawol.nu, plateformes musicales, presse) · Mission + 4 valeurs (Engagement, Liberté, Solidarité, Souveraineté) · Développement web sur mesure · Clients (KA UBUNTU kaubuntu.com, KA UBUNTU Hub kaubuntu.re PWA, KBM kbm.gp) · Services hébergés (BOKANTE, GADE, NIYAJ, etc.) · FAQ « Qu'est-ce que le Ka ? » · CTA final + contact (kontak@o-k-i.net, WhatsApp, Telegram, Discord, forge) · footer avec mentions légales. Pages satellites : `/dons/` (Stripe + Liberapay), `/en/` (version anglaise complète), `/mentions-legales/`. + +## 2. Exigences techniques + +- **SvelteKit + Svelte 5 runes + TypeScript**, adapter-static (prerender intégral) — le site doit pouvoir être servi depuis o2switch en statique ou Cloudflare Pages, sans serveur Node. +- **i18n FR/EN** : FR par défaut, EN sur `/en/` comme actuellement ; attributs `lang` corrects ; pas de lib lourde si un routage par dossier suffit. +- **Thème sombre par défaut** (identité de marque), thème clair en opt-in persisté en localStorage, appliqué sans FOUC via snippet dans `app.html`. +- **Conserver toutes les URLs** existantes (/, /dons/, /en/, /mentions-legales/, ancres #solutions #projets #partenaires #djangokam #hebergement #contact). +- **Headers de sécurité** : reproduire la CSP et les headers actuels (via `_headers` Cloudflare ou doc d'htaccess o2switch), en retirant les domaines devenus inutiles (Google Fonts, BunnyCDN). +- **PWA légère** : manifest + service worker de cache du shell (vite-plugin-pwa) — le site doit rester lisible hors-ligne. +- OpenGraph complet (10 balises actuelles) reconduit, og:image self-hébergée. +- Budgets du playbook §4 : JS initial ≤ 170 KB, page d'accueil ≤ 2 MB, Lighthouse mobile ≥ 90/95/95/95. + +## 3. Motion & composants (sélection depuis la bibliothèque du playbook §5) + +Applique la sélection suivante — pas plus, le site doit rester sobre : +1. **KineticText** sur les titres de sections (Archivo uppercase), split par mots, stagger piloté par les tokens gwoka de la charte (`--dur-tanbou` 120 ms de base, pattern syncopé), scrub natif `animation-timeline: view()` avec fallback ScrollTrigger. +2. **ScrollProgressBar** en haut de page, gradient or→vert de la charte. +3. **PageTransition** légère (View Transitions + fallback), durée `--dur-mesure` 480 ms. +4. **Village écosystème** : remplacer la grille de cards « Services hébergés » par une scène SVG isométrique « village créole » (case à ka = BOKANTE, karbay = GADE, silo nuage = NIYAJ, scène de musique = MIZIK…), chaque bâtiment = lien vers l'instance, hover/focus = liseré or + label. **DOM/SVG uniquement, aucun WebGL** (P3 du playbook). Fallback : grille de cards accessible pour reduced-motion/mobile étroit. +5. **Flag-bar** (signature marque, 4 segments francs 6 px) : sous la nav fixe + en haut du footer, jamais plus d'une occurrence visible par écran. +6. **404 designé** en KA (texte créole + ka/tambour SVG), page hors-ligne PWA assortie. +7. CursorTracker : **ne pas installer** (audience mobile-dominante, ROI faible). +8. WebGL : **aucun** sur cette migration. + +## 4. Périmètre de cette session + +- [ ] Scaffold SvelteKit + tokens charte + thème sombre/clair + fonts self-hébergées +- [ ] Migration de la home complète (toutes sections, contenu FR repris à l'identique) +- [ ] Pages /dons, /mentions-legales, /en (structure, contenu EN repris) +- [ ] Composants motion §3 (KineticText, ScrollProgressBar, PageTransition, village SVG) +- [ ] 404 + page hors-ligne + manifest PWA +- [ ] Headers sécurité + _headers + doc déploiement (o2switch statique ET Cloudflare Pages) +- [ ] Rapport final : Lighthouse, poids par page, écarts éventuels avec ce brief + +## 5. Critères d'acceptation + +- La DoD du playbook §8 est intégralement vérifiée. +- Zéro requête vers un domaine tiers au chargement (vérifier l'onglet réseau) : ni Google, ni BunnyCDN, ni aucun CDN. +- Toutes les images présentes et optimisées (plus aucun 403, plus aucune image > 200 KB hors hero). +- Le contenu éditorial FR est repris à l'identique (aucune réécriture non demandée), hormis la déduplication des cards « publics ». +- La nav au clavier fonctionne sur tout le village SVG (chaque bâtiment focusable, label lisible). +- `npm run build && npm run preview` : zéro erreur, zéro warning a11y. + +Commence par la Phase 1 du playbook (audit de confirmation du dépôt actuel) et présente-moi le plan avant d'écrire le moindre composant. diff --git a/doc2sveltekit-transition/recette-sveltekit-15-sites-awwwards.md b/doc2sveltekit-transition/recette-sveltekit-15-sites-awwwards.md new file mode 100644 index 0000000..e371cc8 --- /dev/null +++ b/doc2sveltekit-transition/recette-sveltekit-15-sites-awwwards.md @@ -0,0 +1,908 @@ +# 15 Award Sites → The SvelteKit Recipe +### An autopsy of Awwwards-grade websites + an actionable implementation guide for Cyber-mawonaj + +> **Method note.** Stack claims are grounded in official case studies, Awwwards entries and technical write-ups wherever they exist (footnotes at the end of the document). Where no public source exists (marked *inference*), the analysis is based on observable rendering behavior and the studio's known toolchain. Every "stealable pattern" is something you can extract without the original codebase. + +--- + +# PART 1 — SITE-BY-SITE ANALYSIS + +--- + +## 1. unseen.co — Unseen Studio (Bristol, UK) + +Unseen Studio is the rebrand of Green Chameleon, a studio with 35 SOTDs; their own site won SOTD at launch [^38^][^39^]. + +**[A] Visual language.** Dark, atmospheric base with volumetric lighting as the signature — their designers publish "volumetric lighting studies" as R&D, and that light-fog sensibility pervades the site [^39^]. Palette: near-black grounds, soft chromatic blooms (rainbow-adjacent volumetric glows), restrained UI chrome. Typography is large, confident grotesque with generous tracking; type never competes with the 3D — it floats above it. Composition is free-form asymmetry around a central 3D stage. + +**[B] Motion.** Physics-feeling, inertia-heavy. Real-time WebGL particle simulations are an in-house specialty (they teased a "real-time WebGL particle sim" before project launches) [^39^]. Scroll acts as a camera dolly through project worlds; hover states distort or illuminate the medium rather than underline links. Cursor is part of the lighting rig. + +**[C] Tech stack.** WebGL/Three.js confirmed by their own hiring and project tags (#webgl #threejs) [^39^]. Custom shader work for volumetrics and particles; DOM reserved for text/UI. Performance strategy: the studio iterates "directly in the browser" with live shader reloading. + +**[D] UX.** Scroll-driven portfolio-as-journey. Navigation is minimal; the work *is* the menu. Accessibility trade-off: heavy reliance on pointer + WebGL, text contrast sometimes sacrificed to atmosphere. Mobile gets a simplified but genuine version, not a broken one. + +**[E] Critical assessment.** +- ✅ **Strengths:** (1) volumetric light identity — instantly recognizable; (2) particle/cursor integration makes the UI feel alive; (3) brand confidence: the site demonstrates the product instead of describing it. +- ⚠️ **Weaknesses:** (1) WebGL-first means a hard floor for low-end devices; (2) atmosphere-over-contrast hurts readability on cheap screens. +- 🔓 **Stealable pattern:** **the cursor-as-light-source** — pass pointer position as a uniform to your scene shader so the user literally illuminates content. One uniform, huge perceived interactivity. + +--- + +## 2. animejs.com — Anime.js v4 (Julian Garnier) + +The v4 relaunch site (2025) went viral in r/webdev as "just incredible" — the 3D hero scene is Three.js, everything else is animated with the library itself as a living demo [^14^]. + +**[A] Visual language.** Deep-space dark UI, electric accent colors, dot-grid/blueprint textures referencing engineering drawing. Typography: technical sans with tabular numerals, kinetic type everywhere — the docs headlines themselves animate. Atmosphere: glassy panels + grain, "lab instrument" aesthetic. + +**[B] Motion.** The philosophy *is* the product: timeline-orchestrated, stagger-driven, spring-eased. The v4 API is modular — `animate`, `createTimeline`, `createScope`, `createSpring`, `stagger`, `splitText`, `createDraggable` are separate named imports [^5^]. Scroll Observer API syncs animations to scroll with multiple sync modes and thresholds [^12^]. Micro-interactions: every parameter knob in the docs is draggable and live-updates the demo. + +**[C] Tech stack.** Three.js for the hero scene; Anime.js v4 for all DOM/SVG animation [^14^]. Tree-shakeable ESM modules — you import only `animejs/animation`, `animejs/timeline`, etc. [^5^]. This modularity is itself a lesson: ship ≤ what you use. + +**[D] UX.** Documentation as experience: interactive playground embedded in the narrative. Accessibility: motion-heavy but each demo is self-contained; reduced-motion handling is left to the implementer (a gap). + +**[E] Critical assessment.** +- ✅ **Strengths:** (1) product demonstrates itself — zero stock imagery; (2) interactive docs collapse learning time; (3) spring physics defaults feel modern vs. GSAP's classic easings. +- ⚠️ **Weaknesses:** (1) animation overload can fatigue on long doc sessions; (2) no first-party reduced-motion story. +- 🔓 **Stealable pattern:** **`createScope` per component** — scope animation queries to a component root and `revert()` on cleanup. Maps 1:1 to Svelte's component lifecycle (see §6). + +--- + +## 3. immersive-g.com — Immersive Garden (Paris) + +3× Awwwards Studio of the Year, 90+ awards; the 2025 rebuild won SOTM and is fully documented in an official case study [^24^][^27^]. + +**[A] Visual language.** Minimalist-yet-atmospheric: bas-relief 3D compositions of natural elements, Roman numerals rendered in 3D as menu anchors, muted stone/bone palettes with deep shadows [^27^]. It's "digital craftsmanship" as brand — tactile, sculptural, almost geological. + +**[B] Motion.** Restrained and ceremonial. GSAP for transitions, **Lenis for scroll** [^27^]. Nothing bounces; everything glides. The wow is in scene transitions and reveal choreography, not jitter. + +**[C] Tech stack (confirmed):** Three.js + Blender/Houdini/ZBrush asset pipeline; **Vue/Nuxt frontend; GSAP + Lenis; Strapi + Node backend; Vercel deployment** [^27^]. Performance strategy is the headline: **server-side KTX GPU texture compression, channel-packed textures, gltf-transform automated exports** with custom Blender/JS scripts [^27^]. + +**[D] UX.** One-click access navigation doctrine: any case study reachable in a single interaction. The **"Backstage" section** — behind-the-scenes technical breakdowns per project — is a hidden-gem IA pattern that turns process into content [^27^]. + +**[E] Critical assessment.** +- ✅ **Strengths:** (1) compression pipeline is industry best practice (KTX + channel packing); (2) Backstage = transparency as marketing; (3) minimalism with one strong sculptural motif instead of ten effects. +- ⚠️ **Weaknesses:** (1) Strapi + Node backend adds hosting weight you'd feel on a 50 €/mo budget; (2) realism-first 3D = large asset payloads even compressed. +- 🔓 **Stealable pattern:** **the Backstage section** — document your process publicly per project. For Cyber-mawonaj this doubles as pedagogical content (open knowledge = sovereignty). + +--- + +## 4. davidwhyte.com/experience — David Whyte Experience (by Immersive Garden) + +SOTM Dec 2024; official case study available [^17^][^20^]. + +**[A] Visual language.** Watercolor paintings by Matthew Phinn as the entire visual substrate — organic pigment bleeds, paper texture, muted landscape hues. Typography is literary serif, quiet. This is the anti-neon site: proof that "immersive" doesn't mean cyber. + +**[B] Motion.** Two hero techniques: a **cursor-driven watercolor fluid simulation** — one fluid sim per visible painting, optimized via a **simulation atlas + stencil buffer** so only active paper regions compute [^20^]; and a **generative watercolor reveal** — noise-layered droplets dissolving the painting into view, with **noise baked in advance** to avoid real-time cost [^20^]. Long-press reveals video of the real location. + +**[C] Tech stack (confirmed):** Nuxt/Vue, Sass + BEM, Three.js (Blender-driven camera recreated in WebGL, custom shaders), GSAP, Lenis, WordPress CMS, Vercel [^20^]. + +**[D] UX.** Scroll through landscapes; each poem gets full-screen focus. Long-press is a deliberate slow-interaction — forces contemplation, matches the content's tempo. Accessibility: text remains real DOM text (good); the fluid sim is decorative-only (also good). + +**[E] Critical assessment.** +- ✅ **Strengths:** (1) baked-noise reveal = generative feel at zero runtime cost; (2) stencil-buffer atlas is a genuinely clever perf pattern; (3) motion tempo matched to content (slow poetry, slow interaction). +- ⚠️ **Weaknesses:** (1) fluid sim still taxes low-end GPUs; (2) long-press is undiscoverable without onboarding hints. +- 🔓 **Stealable pattern:** **bake your noise** — precompute generative textures offline (Blender/Python/ComfyUI for you), ship them as KTX2, blend in shader. Generative look, static-asset perf. Perfect fit for your ComfyUI pipeline. + +--- + +## 5. kriss.ai/home/aftercare — Kriss.ai (by Studio 28K) + +Awwwards case study available [^28^]. + +**[A] Visual language.** Deliberate rejection of "cold AI tech" aesthetics: **70s-inspired palette, organic shapes, soft warm tones** — a dental AI presented as a human service [^28^]. The whole site is a "Dollhouse": an open 3D cutaway of a dental clinic where each room (front desk, surgery, aftercare) is a content section. + +**[B] Motion.** Spatial navigation through the dollhouse: camera moves between rooms on scroll/click. Motion serves wayfinding — you always know where you are in the building, hence in the offer. + +**[C] Tech stack (confirmed):** WebGL 3D built in Blender; **deployed on Cloudflare Pages + R2 for media** — a notable choice: heavy visual assets on an edge CDN, zero traditional backend [^28^]. Figma → Blender → VSCode pipeline. + +**[D] UX.** Metaphor-driven IA: product features mapped to physical rooms. This collapses the learning curve for non-technical buyers (dentists) — the spatial metaphor carries the explanation. + +**[E] Critical assessment.** +- ✅ **Strengths:** (1) narrative pivot "from tech to human" executed in the medium itself; (2) dollhouse = site map you can see; (3) **Cloudflare Pages/R2 architecture proves you can host heavy WebGL at near-zero backend cost** — directly relevant to your budget. +- ⚠️ **Weaknesses:** (1) the 3D metaphor forces all content into rooms — awkward for dense text; (2) 70s warmth risks reading as dated outside the US market. +- 🔓 **Stealable pattern:** **the dollhouse IA** — map AKILPA's or Cyber-mawonaj's sections to physical Caribbean spaces (case à ka, marché, karbay, plage). Navigation becomes cultural storytelling. Free pattern, no shaders required. + +--- + +## 6. zentry.com — Zentry (by Resn) + +Full Awwwards case study available — and the tech decision is the most instructive of all 15 sites [^6^]. + +**[A] Visual language.** Bold, vibrant, "evil-corporation-playful": saturated color fields, chunky display type, 3D portal masks as signature motif. Sound-designed: dynamic soundtrack evolving from upbeat to ambient as you explore [^6^]. + +**[B] Motion.** Motion system designed **before** the website: every reaction, curve and principle specced upfront [^6^]. Flashy, fast, reactive — but (see below) done entirely in DOM. + +**[C] Tech stack (confirmed):** **Nuxt/Vue + Vite, proprietary utility framework — and deliberately NO WebGL.** The team needed massive DOM throughput (text, dynamic media) and chose to build the 3D portal-mask effects as **custom DOM/CSS solutions instead of Three.js**, explicitly to minimize complexity and maximize DOM performance [^6^]. + +**[D] UX.** Portal motifs as content reveals; sound as orientation layer. The lesson: "immersive" is a design outcome, not a rendering technology. + +**[E] Critical assessment.** +- ✅ **Strengths:** (1) **proof that award-grade "3D" can be pure DOM** — transform3d, clip-path, masks, perspective; (2) motion-first design process (curves before comps); (3) sound design treated as first-class UX. +- ⚠️ **Weaknesses:** (1) proprietary framework = not reproducible as-is; (2) audio autoplay remains an accessibility/UX minefield. +- 🔓 **Stealable pattern:** **DOM-only portal masks** — `clip-path` + `transform-style: preserve-3d` + a GSAP scrub timeline gives you 80% of the Zentry wow at 5% of the GPU cost. **This is your #1 pattern for 4G/low-end Gwada devices.** + +--- + +## 7. igloo.inc — Igloo Inc (by Abeto + Bureaux) + +SOTD Jul 2024 (7.92/10); unusually detailed case study + WebGPU showcase write-up [^3^][^7^][^1^]. **And it's built in Svelte — your stack.** + +**[A] Visual language.** A frozen landscape: each portfolio project encased in a **procedurally generated ice block** (custom crystal-growth algorithm inside container shapes — unique every time, scales without manual modeling) [^1^]. Chromatic aberration + frost-dissolve scene transitions; intro sequence "between sci-fi title crawl and luxury brand ad" [^1^]. + +**[B] Motion.** Camera drifts between scenes; footer is an interactive particle simulation that coalesces into different 3D shapes per hovered link, **color shifting with particle velocity** [^1^]. Text glitches via shaders; **letter-scramble swaps SDF texture offsets instead of forcing DOM relayout** — a performance decision doubling as a creative one [^1^]. + +**[C] Tech stack (confirmed):** **Three.js + Svelte + GSAP**, Houdini + Blender, plus proprietary tooling including a **custom VDB-to-browser exporter** compressing volume data smaller than a typical website image [^1^]. Entire UI rendered in WebGL. Workflow: grey-box previs animations, then iterate directly in browser with real-time shader/model reloading, continuously measuring on low-end devices [^7^]. + +**[D] UX.** Three sections only — the constraint forced a scroll experience dense with interaction [^7^]. Navigation = the journey itself. + +**[E] Critical assessment.** +- ✅ **Strengths:** (1) **Svelte at the absolute top tier of the craft** — kills any "can Svelte do this?" doubt; (2) procedural assets scale content without artists; (3) SDF-text-in-shader eliminates DOM relayout cost. +- ⚠️ **Weaknesses:** (1) full-WebGL UI = screen-reader hostile, needs a parallel DOM layer; (2) proprietary VDB pipeline is not reproducible on a budget. +- 🔓 **Stealable pattern:** **velocity-reactive particles** — map particle/element color or size to pointer *velocity*, not just position. Trivially implementable in a Svelte action (`$derived` from delta of pointer positions), huge tactile payoff. Also: **SDF text rendering** if you go WebGL-UI (troika-three-text does this for free). + +--- + +## 8. kprverse.com — KPR (by Resn) + +SOTD Dec 2022 (7.98) + SOTM; official making-of available [^35^][^45^]. + +**[A] Visual language.** "New Eden" concept-art tableaux: hand-painted assets, bold concept-art style, terminal/console easter-egg aesthetics (fake file-system loading messages, "HTTPS://KPRVERSE.COM/KPCO/..." loader strings) [^41^]. Mixes 3D characters with 2D design elements, "interactive motion revealing a hidden dimension" [^45^]. + +**[B] Motion.** **Click-and-hold** as the signature: holding reveals a second interactive layer (geography, factions, lifestyle of the world) [^45^]. Scroll moves through parallax tableaux (the Keep, the Factions, the World). Loading is theatrical: console-style progress theater. + +**[C] Tech stack.** Resn proprietary (Nuxt-era Resn = Vue-based, per their Zentry write-up [^6^]); layered 2D/3D compositing rather than full 3D scenes. + +**[D] UX.** Narrative gating: lore revealed in layers, community invited to "shape the story". Notably **hard device gating**: "Your Browser resolution is currently not supported" and portrait-only enforcement on mobile [^41^] — a controversial but deliberate trade. + +**[E] Critical assessment.** +- ✅ **Strengths:** (1) click-and-hold "second layer" is a brilliant lore-delivery mechanic; (2) loader-as-fiction (terminal theater) turns wait time into worldbuilding; (3) tableaux = content with room for community contribution. +- ⚠️ **Weaknesses:** (1) **resolution gating is hostile** — for Gwada's mobile-dominant reality this pattern is toxic; ship a degraded mode instead; (2) hand-painted asset volume = production cost. +- 🔓 **Stealable pattern:** **click-and-hold reveal layer** — pointer-down starts a GSAP timeline that cross-fades a hidden info stratum; release reverses it. One timeline, two states, infinite narrative uses (patrimoine layers: colonial map ↔ créole map). + +--- + +## 9. hape.io — HAPE (by Dogstudio/DEPT for Digimental) + +SOTD Apr 2022 (7.84, dev award animations 9.4/10); official case study available [^36^][^37^][^40^]. + +**[A] Visual language.** High-fashion-magazine web design: black/gold footer palette, editorial grids, the 3D ape treated like a couture model [^36^][^37^]. + +**[B] Motion.** **The HAPE walk is the scroll**: page opens zoomed on the ape's head; scrolling rotates the camera and dollies out to reveal the full strutting character — a walk cycle art-directed for "attitude" and **synchronized to a handpicked soundtrack** [^40^]. A "hapewalk mode" hands rotation/zoom control to the user [^40^]. + +**[C] Tech stack (confirmed):** **custom OGL micro-framework** — OGL (minimal-abstraction WebGL library) extended in-house with PBR rendering, skinning, post-processing and an animation player [^37^]. Built in 4 weeks [^37^]. + +**[D] UX.** One character, one gesture (scroll), one attitude — radical focus. Results: 57K visitors launch month, 4.58 min average session [^37^]. + +**[E] Critical assessment.** +- ✅ **Strengths:** (1) scroll-synced skeletal animation with music sync is a masterclass in single-idea execution; (2) OGL proves you don't need Three.js if your needs are narrow; (3) fashion-editorial framing elevates NFT kitsch to design. +- ⚠️ **Weaknesses:** (1) skinned 3D + PBR is heavy for low-end mobile; (2) audio-synced motion degrades silently when sound is off/blocked. +- 🔓 **Stealable pattern:** **scroll-scrubbed skeletal camera** — bind `scrollProgress` to a baked animation clip's time (`mixer.setTime(progress * duration)`). Works with any GLTF walk/dance cycle. Imagine it with a **gwoka dancer** or a lambi procession: cultural motion as scroll engine. + +--- + +## 10. labs.noomoagency.com — Noomo Labs + +Developer Award + SOTD 2024; official behind-the-scenes available [^16^][^13^]. + +**[A] Visual language.** A dreamlike underwater journey with a glass jellyfish: 3D glass materials, refractive transparency, calm ambient sound design ("sound of the glass sphere appearing", music complementing the jellyfish) [^13^]. It's a hub for experiments — R&D made public. + +**[B] Motion.** The technical doctrine: **all 3D assembled in ONE Blender scene; all non-interactive animation (camera, jellyfish, words) baked in Blender; materials baked to textures** [^13^]. Camera rigged to two Empties — one at the focal point, one animating the camera object [^13^]. Runtime = playback + interactive garnish. + +**[C] Tech stack (confirmed):** Three.js + GSAP, **explicitly avoiding React Three Fiber "to maintain flexibility"**, Nuxt framework, Blender-baked assets [^13^]. The storytelling sister site scrolls a Three.js camera through five differently-lit "rooms" of case studies [^8^]. + +**[D] UX.** Labs = experiments before they "grow up" [^8^] — an institutional pattern: public R&D as marketing. + +**[E] Critical assessment.** +- ✅ **Strengths:** (1) **bake-everything philosophy = cinematic quality at playback cost**; (2) one-scene assembly avoids runtime scene-graph juggling; (3) labs-as-content is a sustainable publishing model. +- ⚠️ **Weaknesses:** (1) baked = rigid; content changes require re-bakes; (2) glass/refraction shaders are expensive on Mali-class GPUs. +- 🔓 **Stealable pattern:** **the two-Empty camera rig** — in Blender, parent your camera to a target-Empty + path-Empty, bake, export GLTF, and in SvelteKit just scrub `cameraAnimation.time` from Lenis scroll progress. Zero camera math at runtime. Your ComfyUI/Blender pipeline already supports this. + +--- + +## 11. noomoagency.com — Noomo Agency + +Awwwards **Website of the Year** winner [^9^][^16^]. + +**[A] Visual language.** Glassmorphism at its most disciplined: glass cards on-scroll animations, 3D hover effects, a **dynamic logo incorporating handwritten elements from each team member** — personalization as brand system [^9^][^11^]. Boutique-agency warmth inside high-tech execution. + +**[B] Motion.** "Every interaction and hover has its unique purpose" [^9^] — hover states are individually art-directed (button hovers, 3D case previews, showreel controls) [^11^]. Scroll animations drive case reveals; 3D transitions between pages. + +**[C] Tech stack (confirmed):** **Nuxt 3 front AND back (Node), Three.js + GSAP** [^9^]. + +**[D] UX.** Classic agency IA (work/story/contact) elevated by transition choreography — proof you don't need experimental IA if execution is flawless. + +**[E] Critical assessment.** +- ✅ **Strengths:** (1) hover-as-portfolio: micro-interactions individually designed, not defaulted; (2) humanizing details (handwritten logo) offset tech flex; (3) full-stack Nuxt keeps ops simple. +- ⚠️ **Weaknesses:** (1) glass everywhere = blur = GPU cost on weak devices; (2) 3D page transitions can delay content access. +- 🔓 **Stealable pattern:** **budgeted hover craft** — pick your 5 most-tapped elements and give each a unique, purposeful micro-interaction; leave the rest at system defaults. Award juries notice the ratio of intentional to generic interactions. + +--- + +## 12. nudot.com.tw — Nudot (Taiwan) + +*No public case study found — this section is inference from the live site and studio profile; treat stack claims as low-confidence.* + +**[A] Visual language.** Taiwanese studio aesthetic in the lineage of Ultra Combos/Studio Pros: precise CJK+Latin bilingual typography, generous whitespace punctuated by abrupt kinetic moments, muted palette with one aggressive accent. Bilingual typesetting (Chinese vertical/horizontal + English) is itself the design flex. + +**[B] Motion.** Text-led motion: character-level reveals, marquee systems, cursor-following media previews on project lists. Motion is typographic rather than 3D. + +**[C] Tech stack (inference).** DOM/CSS + GSAP-class animation, canvas accents; likely Nuxt or similar Vue meta-framework (dominant in Taiwanese award studios). + +**[D] UX.** Project-list-as-gallery with hover media previews; bilingual toggle as first-class nav item — relevant precedent for FR/KRÉ bilingual switching. + +**[E] Critical assessment.** +- ✅ **Strengths:** (1) bilingual typography treated as design material, not i18n afterthought; (2) restraint — effects deployed sparingly; (3) fast: typography-led sites are inherently light. +- ⚠️ **Weaknesses:** (1) hover-previews have no touch equivalent (mobile gets a lesser list); (2) minimal feedback cues can confuse first-time visitors. +- 🔓 **Stealable pattern:** **bilingual kinetic type** — design your FR/KRÉ switch as an *animation event* (lines rotate/mask-swap in place) rather than a page reload. Language toggle becomes brand moment. Implement with `splitText` + a shared-layout transition. + +--- + +## 13. 375.studio/en/studio — Studio375 (Vicenza, Italy) + +SOTD Apr 30, 2026 (7.33, plus Developer Award) [^2^][^15^]. + +**[A] Visual language.** Two-color discipline (their earlier HM site was strict #000/#FFF) [^10^], clean-flat-minimal portfolio aesthetic with colorful punctuations [^10^]. Communications-agency clarity rather than tech-lab showing-off. + +**[B] Motion.** Hero image animation, mouse interaction, scroll-linked text decoration, animated 404 — a **complete system of small moments** rather than one big WebGL bet [^10^]. Dev-award scores show the craft is in transitions (animations 7.8, their strongest dev metric) [^2^]. + +**[C] Tech stack (inference).** DOM-first, GSAP-class orchestration; no heavy WebGL signature. Accessibility dev-score 7.2 is above the award-site average [^2^] — suggests semantic care. + +**[D] UX.** Conventional studio IA executed with transition polish; even the 404 is a designed moment [^10^]. + +**[E] Critical assessment.** +- ✅ **Strengths:** (1) proves SOTD is achievable without WebGL; (2) systematic micro-motion (every page state designed, incl. errors); (3) strong accessibility score for the category. +- ⚠️ **Weaknesses:** (1) less memorably distinctive than the WebGL cohort; (2) creativity scores (7.57) lag the experimental sites. +- 🔓 **Stealable pattern:** **design your error/empty/404 states with the same rigor as the hero.** For an association site (AKILPA), the 404, the "no events yet", the offline page are touchpoints where grassroots personality can outshine budget. + +--- + +## 14. ten.375.studio/en — "Ten Years Away" (Studio375) + +SOTD Jun 26, 2026 (7.32; users rated creativity 9.3) [^4^]. + +**[A] Visual language.** **An interactive comic** chronicling ten years of the studio — "real characters, real stories" [^4^]. Illustrated panels, comic-page composition translated to scroll, hand-drawn texture over digital precision. + +**[B] Motion.** Mouse trail as a signature element, page transitions between comic chapters, intro sequence [^4^]. Motion serves panel-to-panel reading rhythm — scroll as page-turn. + +**[C] Tech stack (inference).** DOM/SVG illustration + GSAP-class scrubbing; the comic format suggests asset sequencing rather than runtime 3D. + +**[D] UX.** Narrative chronology as IA: ten years = ten chapters. Personal/historical storytelling instead of service listing — an anniversary site that earns attention through vulnerability (real people, real stories). + +**[E] Critical assessment.** +- ✅ **Strengths:** (1) format courage — a comic, not a portfolio grid; (2) chaptered narrative maps perfectly to scroll; (3) authenticity as differentiator (users scored creativity 9.3, far above jury [^4^]). +- ⚠️ **Weaknesses:** (1) niche payoff — low utility for prospective clients; (2) illustrated asset production is time-expensive. +- 🔓 **Stealable pattern:** **the chaptered scroll-comic** — for AKILPA, a "10 dates du patrimoine gwada" or "l'histoire du lambi" as an interactive scroll-comic. Chapters = routes (`/histoire/[slug]`), panels = SVG/AI-generated assets (your Qwen/Seedream pipeline), transitions = shared-element morphs. High storytelling ROI per byte. + +--- + +## 15. apple.com/airpods-pro — Apple + +The canonical scroll-scrubbed product page; technique fully documented by CSS-Tricks and the GSAP community [^19^][^18^]. + +**[A] Visual language.** Product-photographic maximalism on pure black; type appears only when the product pauses. No texture tricks — the hardware render *is* the visual language. + +**[B] Motion.** **Scroll-scrubbed image sequences**: a 148-frame JPEG flipbook drawn to a fixed `<canvas>`, frame index = scroll fraction × frame count [^19^]. Content sections are one big master timeline with percentage-hand-picked timings; "fake pins" hold content in place [^18^]. requestAnimationFrame for tear-free frame swaps [^19^]. + +**[C] Tech stack (documented technique).** Canvas 2D + image preloading; no WebGL needed [^19^]. **Performance doctrine: on slow connections Apple ships a single fallback image instead of the sequence** — and offers smaller sequences per device class [^19^]. + +**[D] UX.** Scroll = product film. Information hierarchy: specs arrive only after desire is built. Accessibility: canvas content is mirrored by real DOM text below. + +**[E] Critical assessment.** +- ✅ **Strengths:** (1) the flipbook is the most copied award pattern alive — simple, robust, GPU-cheap; (2) **explicit slow-network fallback strategy** — exactly your 4G doctrine; (3) master-timeline percentage timing keeps everything in sync across viewports [^18^]. +- ⚠️ **Weaknesses:** (1) 148 frames is a data hog without fallbacks; (2) canvas imagery needs parallel DOM for a11y/SEO. +- 🔓 **Stealable pattern:** **the scroll flipbook with connection-aware loading** — `navigator.connection.effectiveType` decides: full sequence / reduced frame set / single hero image. Ship it as a Svelte action (see §5, bonus component). **This is the single most Gwada-appropriate award pattern in this list.** + +--- + +--- + +# PART 2 — THE SVELTEKIT RECIPE + +## 1. Core Principles (the common DNA) + +**Principle 1 — Scroll is the primary navigation driver.** 13 of 15 sites bind narrative progression to scroll. Canonical stack: **Lenis + GSAP ScrollTrigger, wrapped as Svelte actions/context** (Immersive Garden ships exactly this combo [^20^][^27^]). In SvelteKit: one Lenis instance in the root layout, exposed via context; never instantiate per component. + +**Principle 2 — Bake at build, not at runtime.** David Whyte bakes noise [^20^]; Noomo bakes camera, materials and character animation in Blender [^13^]; Immersive Garden compresses textures server-side to KTX [^27^]; Igloo compresses volumes to sub-image size [^1^]. Rule: **if a value doesn't depend on live user input, it has no business being computed in the browser.** Your ComfyUI/Blender pipeline is your "baking" layer — treat it as part of the build system. + +**Principle 3 — DOM first, WebGL only where it pays.** Zentry did its award-winning "3D" portals in pure DOM deliberately [^6^]; Apple's flipbook is Canvas 2D [^19^]. Reserve WebGL for: particle systems, fluid sims, real 3D assets with user-controlled cameras. Everything else: CSS transforms, clip-path, SVG. **For 4G Guadeloupe this principle is non-negotiable.** + +**Principle 4 — Motion is designed before pages.** Zentry specced its motion system before design started [^6^]; HAPE art-directed a walk cycle, then built a site around it [^40^]. Define your easing vocabulary, durations and rhythm (for you: gwoka cadences — see Part 3) as **design tokens**, before any component exists. + +**Principle 5 — Every state is a designed moment.** Studio375's animated 404 [^10^], KPR's terminal-theater loader [^41^], Noomo's per-element hover craft [^9^]. Loading, error, empty, offline: these are where personality lives when budget doesn't. + +--- + +## 2. SvelteKit Architecture Patterns + +### 2.1 Route structure for immersive experiences + +``` +src/routes/ +├── +layout.svelte ← SmoothScrollProvider (Lenis) + CursorTracker live HERE +├── +layout.ts ← export const ssr = true (default) +├── (site)/ ← layout group: classic content pages +│ ├── +page.svelte ← home (light hero, DOM effects) +│ ├── histoire/[slug]/ ← chaptered scroll-comic (Ten Years Away pattern) +│ └── patrimoine/[slug]/ ← case studies, "Backstage" pattern (Immersive Garden) +├── (immersive)/ ← layout group: canvas-heavy experiences +│ ├── +layout.svelte ← WebGLCanvas provider, ssr disabled per-page +│ └── mawonaj/+page.svelte +└── +error.svelte ← designed 404 (Principle 5) +``` + +Rules: +- **Layout groups** isolate the WebGL context to the routes that need it — the canvas provider never mounts on content pages. +- **Dynamic imports** for anything heavy: `const { WebGLCanvas } = await import('$lib/webgl/WebGLCanvas.svelte')` behind `{#await}` or `{#if browser}` gates. +- **Progressive enhancement**: every immersive route must render meaningful SSR HTML underneath the canvas (Apple mirrors its canvas with DOM text [^19^] — do the same). + +### 2.2 Where WebGL lives + +Use **Svelte context**, created once in the `(immersive)` layout: + +```ts +// $lib/webgl/context.ts +import { setContext, getContext } from 'svelte'; +import type * as THREE from 'three'; + +export interface WebGLContext { + renderer: THREE.WebGLRenderer; + scene: THREE.Scene; + camera: THREE.PerspectiveCamera; + canvas: HTMLCanvasElement; +} + +const KEY = Symbol('webgl'); +export const provideWebGL = (ctx: WebGLContext) => setContext(KEY, ctx); +export const useWebGL = () => getContext<WebGLContext>(KEY); +``` + +Child scenes register/unregister in `$effect` cleanup. **One renderer per app, many scenes.** Never two `<canvas>` WebGL contexts on one page for low-end targets. + +For Three.js in Svelte, evaluate **Threlte** (the Svelte-native Three layer) for standard scenes, and raw Three.js for custom shader-heavy work (Igloo-style). Both coexist: Threlte components render into your single context. + +### 2.3 Animation orchestration + +| Need | Tool | Why | +|---|---|---| +| Component enter/leave, list transitions | **Svelte built-in transitions** (`transition:`, `in:`/`out:`) | Zero deps, SSR-safe, cancellable | +| Scroll-scrubbed timelines, pinning | **GSAP + ScrollTrigger** | Industry standard on 11/15 sites | +| Lightweight springs, draggable, text splitting | **Anime.js v4** (modular imports) [^5^] | ~5× smaller import surface when tree-shaken; `createScope` maps to component lifecycle | +| Physics cursor / spring values | Anime.js `createSpring` or a 12-line custom spring | No GSAP needed for this | +| One shared clock | GSAP `ticker` as the single rAF driver, Lenis hooked into it | Prevents competing rAF loops | + +--- + +## 3. CSS Architecture for Award Sites + +### 3.1 Tailwind + CSS variables for dynamic theming + +```css +/* app.css — the theme lives in variables, Tailwind references them */ +:root { + /* Cyber-mawonaj tokens (see Part 3) */ + --lagune-phospho: oklch(72% 0.13 210); + --roche-volcan: oklch(18% 0.02 260); + --corail: oklch(70% 0.16 25); + --or-circuit: oklch(80% 0.14 90); + --foret-neon: oklch(55% 0.15 155); + --ease-ka: cubic-bezier(0.22, 1, 0.36, 1); /* motion tokens too */ + --dur-mesure: 480ms; +} +@theme inline { + --color-lagune: var(--lagune-phospho); + --color-volcan: var(--roche-volcan); + /* … */ +} +``` + +Theme switching = swapping a `data-theme` attribute on `<html>`, variables cascade, Tailwind utilities follow. No JS class churn, no FOUC if set in `%sveltekit.head%`. + +### 3.2 Container queries > media queries for components + +Media queries for page-level breakpoints; **container queries for every reusable component** (cards, chapters, nav). Award sites break layouts constantly because components are viewport-coupled; container queries make your `KineticText` or `ParallaxLayer` correct in a sidebar, a dialog, or a split-screen dollhouse room. + +### 3.3 CSS that replaces JS + +- **`animation-timeline: scroll()` / `view()`** — native scroll-driven animations for parallax and reveals. ⚠️ *Baseline 2023+: Chrome/Edge/FF OK; Safari 26+ only — keep a ScrollTrigger fallback for Safari < 26 (still common in Gwada's device pool).* Flag: **needs progressive enhancement wrapper.** +- **`scroll-snap`** for chaptered comics (Ten Years Away pattern) — free, native, accessible. +- **`:has()`** for stateful styling (hovered card dims siblings) instead of JS class toggles. +- **View Transitions API** for route morphs (see PageTransition, §5). ⚠️ *Chrome/Edge/Safari 18+; Firefox behind flag — design as enhancement.* + +--- + +## 4. Performance Budget & Optimization + +### 4.1 Budgets (mobile 4G, low-end Android — the Gwada baseline) + +| Resource | Budget | Rationale | +|---|---|---| +| JS initial (compressed) | **≤ 170 KB** | ~1.7 s parse on entry-level 4G Android | +| WebGL textures | ≤ 8 MB GPU RAM | use **KTX2/BasisU**, never raw PNG to GPU [^27^] | +| Models | ≤ 500 KB per scene | Draco or meshopt via gltf-transform [^27^] | +| Hero media | ≤ 400 KB before interaction | poster + lazy sequence (Apple doctrine [^19^]) | +| Total page weight (home) | ≤ 2 MB | reality check against award sites' 8-15 MB | +| Frame budget | 16 ms desktop / **target stable 30 fps on Mali-G52** | test on a real ~150 € Android, not DevTools throttling | + +### 4.2 WebGL asset pipeline + +``` +Blender → gltf-transform CLI: + gltf-transform optimize in.glb out.glb \ + --compress meshopt --texture-compress ktx2 +``` + +- **KTX2 + UASTC/ETC1S** textures (Immersive Garden's exact strategy [^27^]); channel-pack roughness/metallic/AO into one texture [^27^]. +- **LOD**: two model tiers; pick at runtime from `renderer.capabilities` + `deviceMemory`. +- **Instancing** for repeated elements (lambi shells, vegetation, stars). +- **Bake lighting** into textures wherever the camera path is fixed (Noomo doctrine [^13^]). + +### 4.3 Animation frame budget + +Concurrent JS-driven animations before jank on low-end: **~8-12 transform/opacity tweens**. Beyond that: batch into one timeline, move to CSS, or cull off-screen animations (`ScrollTrigger` with `toggleActions`, or IntersectionObserver gating). Animate **only `transform` and `opacity`** — layout properties cause reflow (Anime.js v4 docs say the same [^5^]). + +### 4.4 SSR vs CSR for canvas pages + +- Canvas/WebGL pages: **SSR the semantic content shell, hydrate the canvas client-side** (`{#if browser}` + dynamic import). Don't disable SSR route-wide unless the page is truly content-free — you lose SEO and slow-connection resilience for nothing. +- Lenis/GSAP init: `onMount` only, always. +- SvelteKit prerendering (`export const prerender = true`) for everything static → perfect on Cloudflare Pages/Netlify free tiers, mirroring Kriss.ai's edge-CDN architecture [^28^]. + +--- + +## 5. Component Library — Reusable Svelte 5 Modules + +All components: Svelte 5 runes syntax, TypeScript, reduced-motion aware, cleanup on destroy. + +### 5.1 SmoothScrollProvider — Lenis as layout-level context + +```svelte +<!-- $lib/components/SmoothScrollProvider.svelte --> +<script lang="ts"> + import { onMount, setContext, type Snippet } from 'svelte'; + import Lenis from 'lenis'; + import gsap from 'gsap'; + + let { children }: { children: Snippet } = $props(); + let lenis: Lenis | null = $state(null); + + setContext('lenis', { get: () => lenis }); + + onMount(() => { + const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; + if (reduced) return; // native scroll = the fallback + + lenis = new Lenis({ lerp: 0.1, smoothWheel: true }); + // ONE rAF driver for the whole app (Principle: shared clock) + lenis.on('scroll', () => {}); // ScrollTrigger.update hook lives in KineticText + gsap.ticker.add((t) => lenis?.raf(t * 1000)); + gsap.ticker.lagSmoothing(0); + + return () => { lenis?.destroy(); lenis = null; }; + }); +</script> + +{@render children()} +``` + +### 5.2 WebGLCanvas — single renderer, context-provided scene graph + +```svelte +<!-- $lib/webgl/WebGLCanvas.svelte --> +<script lang="ts"> + import { onMount, type Snippet } from 'svelte'; + import { provideWebGL } from './context'; + + let { children, dpr = 1.5 }: { children: Snippet; dpr?: number } = $props(); + let canvas: HTMLCanvasElement; + let ready = $state(false); + + onMount(async () => { + const THREE = await import('three'); // code-split: three never hits content pages + const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance' }); + renderer.setPixelRatio(Math.min(devicePixelRatio, dpr)); // cap DPR: free FPS on retina + + const scene = new THREE.Scene(); + const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 100); + + const onResize = () => { + camera.aspect = innerWidth / innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(innerWidth, innerHeight); + }; + onResize(); + addEventListener('resize', onResize); + + renderer.setAnimationLoop(() => renderer.render(scene, camera)); + provideWebGL({ renderer, scene, camera, canvas }); + ready = true; + + return () => { + removeEventListener('resize', onResize); + renderer.setAnimationLoop(null); + renderer.dispose(); + }; + }); +</script> + +<canvas bind:this={canvas} class="fixed inset-0 -z-10" aria-hidden="true"></canvas> +{#if ready}{@render children()}{/if} +``` + +### 5.3 KineticText — scroll-driven split typography + +```svelte +<!-- $lib/components/KineticText.svelte --> +<script lang="ts"> + import { onMount } from 'svelte'; + + let { + text, + as = 'h2', + scrub = true + }: { text: string; as?: keyof HTMLElementTagNameMap; scrub?: boolean } = $props(); + + let el: HTMLElement; + + onMount(async () => { + if (matchMedia('(prefers-reduced-motion: reduce)').matches) return; + const { animate } = await import('animejs'); + const { splitText } = await import('animejs/text'); + const { onScroll } = await import('animejs'); // Scroll Observer [^12^] + + const words = splitText({ text, by: 'word', tag: 'span' }); + el.append(...words.nodes as Node[]); + + const anim = animate(words.nodes, { + opacity: { to: 1 }, + translateY: { to: 0 }, + delay: (el, i) => i * 40, // stagger → swap for gwoka pattern (Part 3) + ease: 'out(3)', + duration: 600, + autoplay: scrub + ? onScroll({ target: el, enter: 'bottom-=10%', leave: 'top+=10%', sync: 0.5 }) + : true + }); + return () => anim.revert(); + }); +</script> + +<svelte:element this={as} bind:this={el} aria-label={text} class="kinetic">{text}</svelte:element> +``` + +### 5.4 ParallaxLayer — multi-plane depth, CSS-first + +```svelte +<!-- $lib/components/ParallaxLayer.svelte --> +<script lang="ts"> + import { onMount, type Snippet } from 'svelte'; + let { depth = 0.3, children }: { depth?: number; children: Snippet } = $props(); + let el: HTMLElement; + + onMount(async () => { + // Prefer native scroll-driven animations where supported (no JS per frame) + if (CSS.supports('animation-timeline: view()')) { + el.style.animationTimeline = 'view()'; + el.style.animationName = 'parallax-y'; + el.style.setProperty('--depth', String(depth)); + return; + } + const gsap = (await import('gsap')).default; + const { ScrollTrigger } = await import('gsap/ScrollTrigger'); + gsap.registerPlugin(ScrollTrigger); + const tween = gsap.to(el, { + yPercent: -depth * 100, + ease: 'none', + scrollTrigger: { trigger: el, start: 'top bottom', end: 'bottom top', scrub: true } + }); + return () => { tween.scrollTrigger?.kill(); tween.kill(); }; + }); +</script> + +<div bind:this={el} class="parallax-layer">{@render children()}</div> + +<style> + @keyframes parallax-y { to { transform: translateY(calc(var(--depth, 0.3) * -30%)); } } +</style> +``` + +### 5.5 CursorTracker — physics cursor + velocity uniform (Igloo pattern) + +```svelte +<!-- $lib/components/CursorTracker.svelte --> +<script lang="ts"> + import { onMount } from 'svelte'; + let dot: HTMLDivElement; + + onMount(async () => { + if (matchMedia('(pointer: coarse)').matches) return; // no cursor on touch + const { createSpring } = await import('animejs'); + const spring = createSpring({ stiffness: 120, damping: 14 }); + + let x = 0, y = 0, px = 0, py = 0; + const onMove = (e: PointerEvent) => { x = e.clientX; y = e.clientY; }; + addEventListener('pointermove', onMove, { passive: true }); + + let raf = 0; + const tick = () => { + const vx = x - px, vy = y - py; // pointer velocity + const speed = Math.min(Math.hypot(vx, vy) / 40, 1); + dot.style.transform = `translate(${spring.solve(x, px)}px, ${spring.solve(y, py)}px) scale(${1 + speed * 0.6})`; + // → also write (x, y, speed) into your WebGL uniforms here: cursor-as-light-source (Unseen pattern) + px = x; py = y; + raf = requestAnimationFrame(tick); + }; + raf = requestAnimationFrame(tick); + + return () => { removeEventListener('pointermove', onMove); cancelAnimationFrame(raf); }; + }); +</script> + +<div bind:this={dot} class="cursor-dot" aria-hidden="true"></div> +``` + +### 5.6 PageTransition — GSAP route transitions (SvelteKit-safe) + +```svelte +<!-- +layout.svelte (site group) --> +<script lang="ts"> + import { onNavigate } from '$app/navigation'; + import { page } from '$app/state'; + + onNavigate((nav) => { + // Prefer native View Transitions where available (progressive enhancement) + if (!document.startViewTransition) return; + return new Promise((resolve) => { + document.startViewTransition(async () => { resolve(); await nav.complete; }); + }); + }); +</script> + +{#key page.url.pathname} + <main class="page"><slot /></main> +{/key} + +<style> + .page { view-transition-name: page; } + ::view-transition-old(page) { animation: page-out 0.35s var(--ease-ka) both; } + ::view-transition-new(page) { animation: page-in 0.45s var(--ease-ka) both; } + @keyframes page-out { to { opacity: 0; transform: translateY(-2%); } } + @keyframes page-in { from { opacity: 0; transform: translateY(3%); } } +</style> +``` + +*GSAP alternative for browsers without View Transitions:* animate out in `onNavigate`'s returned promise, animate in via `afterNavigate`. Keep durations in your CSS custom properties so both paths share the same motion tokens. + +### 5.7 ScrollProgressBar — zétwal minimal progress + +```svelte +<!-- $lib/components/ScrollProgressBar.svelte --> +<script lang="ts"> + let progress = $state(0); + const onscroll = () => { + const max = document.documentElement.scrollHeight - innerHeight; + progress = max > 0 ? scrollY / max : 0; + }; +</script> + +<svelte:window {onscroll} /> +<div class="progress" role="progressbar" aria-valuenow={Math.round(progress * 100)} + aria-valuemin="0" aria-valuemax="100" aria-label="Progression de lecture"> + <span style:transform="scaleX({progress})"></span> +</div> + +<style> + .progress { position: fixed; inset: 0 0 auto; height: 3px; z-index: 50; } + .progress span { display: block; height: 100%; transform-origin: left; + background: linear-gradient(90deg, var(--lagune-phospho), var(--or-circuit)); } +</style> +``` + +### 5.8 Bonus — FlipbookCanvas (Apple pattern, connection-aware) + +```ts +// $lib/actions/flipbook.ts — Svelte action +export function flipbook(canvas: HTMLCanvasElement, opts: { frames: (q: 'full'|'lite') => string[]; total: number }) { + const conn = (navigator as any).connection; + const quality = conn?.effectiveType === '4g' && !conn?.saveData ? 'full' : 'lite'; + const urls = opts.frames(quality); + if (conn?.effectiveType === 'slow-2g' || conn?.effectiveType === '2g' || conn?.saveData) { + // Apple doctrine: single fallback image, no sequence [^19^] + const img = new Image(); img.src = urls[0]; + img.onload = () => canvas.getContext('2d')?.drawImage(img, 0, 0); + return {}; + } + const imgs = urls.map((u) => { const i = new Image(); i.src = u; return i; }); + const ctx = canvas.getContext('2d')!; + const draw = (p: number) => { + const f = Math.min(urls.length - 1, Math.floor(p * urls.length)); + if (imgs[f]?.complete) ctx.drawImage(imgs[f], 0, 0, canvas.width, canvas.height); + }; + const onscroll = () => { + const max = document.documentElement.scrollHeight - innerHeight; + requestAnimationFrame(() => draw(max > 0 ? scrollY / max : 0)); + }; + addEventListener('scroll', onscroll, { passive: true }); + return { destroy: () => removeEventListener('scroll', onscroll) }; +} +``` + +--- + +## 6. Svelte-Specific Translations (React/Next.js → Svelte idioms) + +| React pattern (seen on these sites) | Svelte 5 idiom | +|---|---| +| `useRef` + `useEffect(() => {...}, [])` for GSAP init | `$effect(() => { /* init */; return () => cleanup })` — or better, a **Svelte action** `use:gsapThing` | +| `useContext(WebGLContext)` | `setContext`/`getContext` with a `Symbol` key (§2.2) | +| `useState` for animation params | `$state` rune; params read inside `$effect` re-run the tween — guard with `gsap.quickTo` or `quickSetter` for per-frame writes | +| Derived uniforms (`useMemo`) | `$derived(() => computeUniforms(pointer, scroll))` — recalculates only on dependency change | +| R3F `<Canvas>` + hooks | **Threlte** for declarative scenes; raw Three in `onMount` for shader-heavy work (Noomo avoided R3F for flexibility [^13^] — same logic applies) | +| `framer-motion` layout animations | Svelte built-in `flip` (from `svelte/animate`) + `crossfade` (from `svelte/transition`) — free, SSR-safe | +| Zustand/Redux for UI state | `$state` in a `.svelte.ts` module — a class with rune fields is your store | + +**Runes vs stores for animation state:** stores still work and are fine for cross-app state; but **animation state should live in `$state`/`$derived` inside components or `.svelte.ts` modules** — runes give finer-grained reactivity (no subscriber fan-out per frame). Critical rule: **never write per-frame values (scrollY, pointer) into reactive state that drives DOM** — write them to refs/plain variables and flush via rAF, or you'll re-render at 60 Hz. + +**GSAP + runes compatibility:** GSAP mutates the DOM directly — Svelte doesn't care as long as GSAP's targets aren't nodes Svelte also patches (avoid tweening attributes Svelte controls via bindings). Wrap GSAP targets in `{@html}`-free static markup or bind via `bind:this`, init in `$effect`, kill in the cleanup function. Anime.js v4's `createScope({ root })` + `scope.revert()` is the same lifecycle shape and tree-shakes better [^5^]. + +--- + +## 7. Accessibility & Fallbacks + +1. **Screen readers in immersive sites:** canvas is `aria-hidden`; a semantic DOM mirror carries all content (Apple pattern [^19^]). If you render UI text in WebGL (Igloo), ship a visually-hidden parallel DOM (`sr-only` list of the same links/labels). +2. **Reduced motion:** one global gate — `matchMedia('(prefers-reduced-motion: reduce)')` checked in `SmoothScrollProvider` (no Lenis), `KineticText` (no split/scrub), `ParallaxLayer` (static), flipbook (poster frame). Implement it **once, at the provider level**, not per component. +3. **Graceful degradation ladder** (the Gwada ladder): + - `navigator.connection.saveData` or `effectiveType` ≤ 3g → static images, no autoplay video, no sequence preloading [^19^] + - `deviceMemory ≤ 4` or WebGL context creation fails → DOM-effects-only mode (Zentry proves DOM-only can still win awards [^6^]) + - `<noscript>` / JS-disabled → SSR content is complete and readable (this is why SSR matters) +4. **Focus management:** route transitions must move focus to the new page's `<h1>`; scroll-jacking (Lenis) must never break keyboard scrolling — Lenis preserves native scroll, which is why it's the right engine choice. +5. **Bilingual a11y:** `lang` attributes per text node (`fr` / `gcf`) — screen readers switch voices; crucial for Kréyòl content. + +--- + +## 8. Tooling Recommendations + +### Essential packages (open-source first, per your doctrine) + +```jsonc +{ + "dependencies": { + "lenis": "^1.3", // smooth scroll — the award-site standard [^20^][^27^] + "gsap": "^3.13", // timelines, ScrollTrigger, ticker + "animejs": "^4.2", // modular springs/stagger/splitText [^5^] + "three": "^0.1xx", // WebGL — dynamic-import only + "@threlte/core": "^8", // optional: declarative Three in Svelte + "troika-three-text": "^0.5" // SDF text in WebGL (Igloo pattern, if needed) + }, + "devDependencies": { + "@gltf-transform/cli": "^4",// meshopt/KTX2 pipeline (§4.2) + "vite-imagetools": "^7", // responsive AVIF/WebP at build + "sharp": "^0.33" + } +} +``` + +- **Vite plugins:** `vite-imagetools` (build-time responsive images), `vite-plugin-pwa` (offline mode = real resilience on flaky 4G, plus an installable PWA for AKILPA), basic `manualChunks` splitting `three` into its own async chunk. +- **HMR for WebGL:** Vite HMR reloads Svelte components but not your GL state — keep scene setup idempotent (init/dispose symmetric in `$effect`), store debug params in URL hash so a reload restores your camera/scrub position. Shader files as `?raw` imports hot-reload cleanly. +- **Hosting (≤50 €/mo, actually near 0 €):** **Cloudflare Pages + R2** — the exact Kriss.ai architecture [^28^]; free tier absorbs heavy media via edge CDN. Netlify/Vercel free tiers work too; avoid anything Node-server-based (Immersive Garden's Strapi [^27^] is out of budget — use a headless Git-based CMS or plain Markdown in-repo). + +--- + +# PART 3 — CYBER-MAWONAJ ADAPTATION + +How the 15-site DNA translates for a Caribbean, sovereign, 4G-first aesthetic. This is not "apply a tropical skin" — it's re-deriving each decision from the local context. + +### Color system (CSS tokens, oklch for perceptual control) + +```css +:root { + --lagune-phospho: oklch(72% 0.13 210); /* phosphorescent lagoon blue — primary accent */ + --roche-volcan: oklch(18% 0.02 260); /* volcanic rock black — dominant ground */ + --corail: oklch(70% 0.16 25); /* coral pink — secondary accent */ + --or-circuit: oklch(80% 0.14 90); /* circuit gold — data/tech highlights */ + --foret-neon: oklch(55% 0.15 155); /* deep forest green, used with neon edge-glow */ + --terre-rouge-a: oklch(45% 0.12 40); /* terre rouge gradient stop A */ + --terre-rouge-b: oklch(30% 0.09 55); /* terre rouge gradient stop B — use for section grounds */ +} +``` + +Dark-mode-first (`--roche-volcan` ground) = OLED battery saving + the bioluminescence aesthetic for free. The **arc-en-ciel / rainbow-serpent** motif belongs in *gradients and particle hues* (velocity-shifted color, Igloo pattern [^1^]) — never as static rainbow fills. + +### Cultural signifiers → concrete UI patterns + +| Signifier | Pattern (source site) | Implementation | +|---|---|---| +| **Lambi shell** | UI motif: shell-spiral as scroll progress, loader, section dividers | SVG spiral `stroke-dashoffset` scrub (createDrawable, animejs/svg [^5^]); KTX2 shell texture for WebGL hero | +| **Zétwal (stars)** | Navigation: constellation nav where sections are stars | DOM + SVG lines; hover = star brightens + label reveals; light-years cheaper than a WebGL starfield | +| **Dollhouse créole** | Kriss.ai spatial IA [^28^] | AKILPA sections as case à ka / marché / karbay / plage rooms — isometric DOM illustration, no WebGL needed | +| **Terre rouge** | Section backgrounds gradient | Pure CSS, zero cost | +| **Mawon/maroon resistance** | Visual language: glitch-free, hand-made texture; KPR's community-shaped tableaux [^45^] | UGC/content layers: community-submitted photos as parallax planes; "Backstage" section documenting the process (Immersive Garden pattern [^27^]) = transparency as sovereignty | + +### Typography (FR/KRÉ bilingual, low-res safe) + +- **Weights: ≥ 400 body, ≥ 600 display** — thin weights die on low-DPI screens (your constraint is correct). +- Suggested open fonts: **Space Grotesk** (display, tech-caribbean tension) + **Inter** or **Atkinson Hyperlegible** (body, legibility-first, open license). Atkinson is the sovereignty-aligned choice: designed for low-vision readers. +- Kinetic type via `splitText` on **words, not characters**, for Kréyòl — character-splitting can break apostrophe-heavy words (`mwen`, `tèt'w`) visually and for screen readers. Keep `aria-label` with the intact string (component §5.3 does this). +- Language switch as animation event (Nudot pattern): mask-swap lines in place. + +### Motion design — gwoka cadences as easing/timing tokens + +Award sites ship custom ease curves; yours can carry rhythm. Practical translation: + +```css +:root { + /* ka/belair-inspired: strong downbeat, syncopated release */ + --ease-ka: cubic-bezier(0.22, 1, 0.36, 1); /* downbeat: fast attack, long settle */ + --ease-syncope: cubic-bezier(0.65, 0, 0.35, 1); /* off-beat: symmetric, urgent */ + --dur-tanbou: 120ms; /* sixteenth @ ~125 BPM — micro-interactions */ + --dur-mesure: 480ms; /* one bar — entrances */ + --dur-phrase: 960ms; /* two bars — page transitions */ +} +``` + +Stagger patterns: instead of linear `i * 40ms`, use **syncopated stagger** `[0, 120, 240, 360, 480, 600, 720, 840]` → remap to `[0, 120, 300, 360, 600, 660, 900, 960]` (3+3+2 feel). Subtle, but rhythm is identity. + +### The Gwada connectivity doctrine (non-negotiables) + +1. **Connection-aware everything** — flipbook component §5.8 is the reference implementation [^19^]. +2. **DOM-first effects** — Zentry is your patron saint here [^6^]. +3. **PWA with offline shell** — an association site that works in a dead zone is a political statement (souveraineté numérique). +4. **Test hardware:** a ~150 € Android on real 4G in Gwada, not Lighthouse throttling. +5. **Edge hosting:** Cloudflare Pages/R2 [^28^] — points of presence in the Americas, near-zero cost, aligns with the 50 €/mo ceiling (realistically: < 5 €/mo). + +--- + +# PART 4 — COMPARISON MATRIX + +| # | Site | Visual | Motion | Tech | UX | Stealable pattern | +|---|---|---|---|---|---|---| +| 1 | unseen.co | Volumetric light, dark bloom | Inertial, particle-driven | Three.js [^39^] | Scroll-journey portfolio | Cursor-as-light uniform | +| 2 | animejs.com | Lab-instrument dark, kinetic type | Springs, staggers, scroll-observer | Three hero + Anime.js v4 modules [^5^][^14^] | Interactive docs | `createScope` per component | +| 3 | immersive-g.com | Bas-relief minimalism | Ceremonial glide | Nuxt, Three, GSAP, Lenis, Strapi, KTX pipeline [^27^] | One-click access + Backstage | Backstage process section | +| 4 | davidwhyte.com/experience | Watercolor organic | Fluid sim, baked-noise reveals | Nuxt, Three, GSAP, Lenis, WP [^20^] | Slow contemplative tempo | Bake generative noise offline | +| 5 | kriss.ai/aftercare | 70s warm dollhouse | Room-to-room camera | WebGL/Blender, **Cloudflare Pages+R2** [^28^] | Spatial metaphor IA | Dollhouse IA (créole rooms) | +| 6 | zentry.com | Bold saturated portals | Motion-system-first | **Nuxt, pure DOM — no WebGL** [^6^] | Portal reveals + sound | DOM-only 3D portal masks ⭐ | +| 7 | igloo.inc | Procedural ice, chromatic frost | Velocity particles, SDF glitch | **Three + Svelte + GSAP** [^1^] | 3-section scroll journey | Velocity-reactive particles | +| 8 | kprverse.com | Concept-art tableaux | Click-and-hold layers | Resn (Nuxt-class) [^45^] | Lore gating, terminal loader | Click-hold reveal layer | +| 9 | hape.io | Fashion editorial | Scroll-scrubbed walk + music sync | Custom OGL micro-framework [^37^] | Single-idea focus | Scroll-scrubbed animation clip | +| 10 | labs.noomoagency.com | Glass jellyfish dream | Baked camera playback | Three + GSAP, no R3F, Blender-baked [^13^] | Public R&D hub | Two-Empty baked camera rig | +| 11 | noomoagency.com | Disciplined glassmorphism | Per-element hover craft | Nuxt 3 full-stack, Three, GSAP [^9^] | Classic IA, flawless transitions | Budgeted hover craft (5 elements) | +| 12 | nudot.com.tw | Bilingual kinetic type | Character reveals, hover previews | DOM/GSAP-class *(inference)* | Bilingual as feature | Language-switch as animation | +| 13 | 375.studio | Two-color discipline | Systematic micro-motion | DOM-first *(inference)* | Every state designed | Designed 404/empty states | +| 14 | ten.375.studio | Interactive comic | Panel scrubbing, mouse trail | DOM/SVG *(inference)* | Chaptered narrative | Chaptered scroll-comic ⭐ | +| 15 | apple.com/airpods-pro | Photographic black | Scroll flipbook, master timeline | Canvas 2D image sequence [^19^][^18^] | Desire before specs | Connection-aware flipbook ⭐ | + +⭐ = highest ROI for Cyber-mawonaj constraints (4G, low-end, sovereignty). + +--- + +## Sources + +[^1^]: https://www.webgpu.com/showcase/igloo-inc-procedural-crystals/ — Igloo Inc: Crystal Growth Algorithms, Shader-Driven UI, and Volume Data (Feb 2026) +[^3^]: https://www.awwwards.com/sites/igloo-inc — Igloo Inc, Awwwards SOTD Jul 23 2024 +[^4^]: https://www.awwwards.com/sites/ten-years-away — Ten Years Away, Awwwards SOTD Jun 26 2026 +[^5^]: https://github.com/juliangarnier/anime/issues/1105 — Anime.js v4 LLM guiding file (API modules, createScope, splitText) +[^6^]: https://www.awwwards.com/zentry-case-study.html — Zentry case study (Resn): Nuxt/Vue/Vite, DOM-only 3D portal masks, no WebGL +[^7^]: https://www.awwwards.com/igloo-inc-case-study.html — Igloo Inc case study (Abeto × Bureaux) +[^8^]: https://www.webgpu.com/showcase/noomo-3d-storytelling-threejs/ — Noomo 3D storytelling site write-up +[^9^]: https://medium.com/@noomo-agency/noomo-agency-website-of-the-year-winner-on-awwwards-aad757327994 — Noomo: Nuxt 3, Three.js, GSAP +[^10^]: https://www.awwwards.com/sites/studio375 — Studio375 HM (2023): elements incl. animated 404 +[^11^]: https://www.awwwards.com/sites/noomo-agency — Noomo Agency SOTD Sep 21 2023, elements list +[^12^]: https://animejs.com/ — Anime.js feature docs (Scroll Observer, stagger, SVG toolset) +[^13^]: https://www.awwwards.com/noomo-labs-a-hub-for-cutting-edge-immersive-experiences.html — Noomo Labs behind the scenes: Three+GSAP, no R3F, Blender-baked, two-Empty camera +[^14^]: https://www.reddit.com/r/webdev/comments/1jqmqkn/ — r/webdev on animejs.com v4 site (Three.js hero) +[^15^]: https://www.awwwards.com/Studio375/ — Studio375 profile & awards +[^16^]: https://noomoagency.com/ — Noomo Agency honors list (WOTY, Labs awards) +[^17^]: https://www.awwwards.com/sites/david-whyte-experience — David Whyte Experience SOTD Dec 31 2024 +[^18^]: https://gsap.com/community/forums/topic/25188-airpods-image-sequence-animation-using-scrolltrigger/ — master-timeline percentage technique +[^19^]: https://css-tricks.com/lets-make-one-of-those-fancy-scrolling-animations-used-on-apple-product-pages/ — Apple flipbook technique + slow-connection fallback +[^20^]: https://www.awwwards.com/case-study-david-whyte-experience-by-immersive-garden.html — fluid-sim atlas, baked noise, full stack (Nuxt/Three/GSAP/Lenis/WP/Vercel) +[^24^]: https://www.awwwards.com/immersivegarden/ — Immersive Garden profile (Studio of the Year ×3) +[^27^]: https://www.awwwards.com/case-study-immersive-gardens-new-website.html — KTX compression, channel packing, gltf-transform; Nuxt/Three/GSAP/Lenis/Strapi/Vercel +[^28^]: https://www.awwwards.com/case-study-ai-with-a-human-touch-the-new-face-of-kriss-ai.html — Kriss.ai dollhouse concept; Cloudflare Pages + R2 +[^35^]: https://www.awwwards.com/sites/kpr — KPR SOTD Dec 26 2022 +[^36^]: https://www.awwwards.com/sites/hape — HAPE SOTD Apr 27 2022 +[^37^]: https://www.deptagency.com/case/creating-a-3d-nft-walking-hape/ — HAPE: custom OGL micro-framework, PBR, skinning; 4-week build +[^38^]: https://www.awwwards.com/directory/agency-studio/ — Unseen Studio directory stats (35 SOTD) +[^39^]: https://x.com/uns__nstudio — Unseen Studio: rebrand from GC, volumetric studies, particle sims, three.js tags +[^40^]: https://dogstudio.co/cases/hape-prime/ — HAPE walk concept, scroll camera, soundtrack sync +[^41^]: https://kprverse.com/ — KPR live site: terminal loader strings, device gating messages +[^45^]: https://www.awwwards.com/kpr-by-resn-wins-sotm.html — KPR by Resn SOTM: click-and-hold layers, tableaux, 3D+2D mix diff --git a/doc2sveltekit-transition/recette-sveltekit-playbook-agent.md b/doc2sveltekit-transition/recette-sveltekit-playbook-agent.md new file mode 100644 index 0000000..eb5ffca --- /dev/null +++ b/doc2sveltekit-transition/recette-sveltekit-playbook-agent.md @@ -0,0 +1,211 @@ +# RECETTE SVELTEKIT — PLAYBOOK POUR AGENT DE CODE +### Transformer un site web classique en expérience de niveau Awwwards + +> **Nature de ce document :** instructions opérationnelles destinées à un agent de code (Kimi CLI ou équivalent). Chaque règle est impérative et vérifiable. Ce playbook est distillé de l'analyse de 15 sites primés (Zentry, Igloo.inc, Immersive Garden, Noomo, Apple AirPods Pro, KPR, HAPE, Kriss.ai, Studio375, animejs.com, etc.). Il est volontairement autonome : tu n'as besoin d'aucun autre document pour l'appliquer, sauf si l'utilisateur fournit une charte de marque (qui alors **prime sur ce document** pour tout ce qui est couleurs, typographie, voix et iconographie). + +--- + +## 0. MISSION + +Transformer un site vitrine « classique » (HTML statique, WordPress, ou SPA sans direction artistique) en un site SvelteKit à forte identité visuelle et motion design maîtrisé, **sans sacrifier performance, accessibilité ni SEO**. Le résultat doit fonctionner parfaitement sur mobile 4G et matériel bas de gamme. + +**Interdictions absolues :** +- Ne jamais installer de dépendance sans l'importer et l'utiliser. +- Ne jamais désactiver le SSR globalement pour « faire marcher » une lib client. +- Ne jamais animer autre chose que `transform` et `opacity` en JS. +- Ne jamais livrer une page dont le contenu est invisible sans JavaScript. +- Ne jamais réintroduire de service tiers (fonts, CDN, analytics) si le projet affirme une doctrine de souveraineté — tout asset se self-héberge. + +--- + +## 1. LES 5 PRINCIPES (ADN commun des sites primés) + +**P1 — Le scroll est le moteur de navigation principal.** Une seule instance Lenis au layout racine, partagée via contexte Svelte, cadencée par le ticker GSAP unique. Jamais de double boucle rAF. + +**P2 — Bake au build, pas au runtime.** Toute valeur qui ne dépend pas d'une entrée utilisateur en direct est précalculée hors-ligne : bruit génératif baké en textures, caméras 3D bakées dans Blender (rig deux-Empty), animations squelettiques exportées en clips GLTF, textures compressées KTX2 via gltf-transform, images responsive générées au build (vite-imagetools). + +**P3 — DOM d'abord, WebGL seulement où ça paie.** Zentry a produit des effets « 3D » primés en pur DOM (clip-path, preserve-3d, masks). Réserver WebGL à : systèmes de particules, simulations de fluide, vrais assets 3D à caméra contrôlée. Un flipbook canvas 2D (pattern Apple) couvre la plupart des besoins de « produit qui tourne au scroll ». + +**P4 — Le motion se designe avant les pages.** Définir d'abord les tokens de mouvement (courbes, durées, staggers) en custom properties CSS, puis les composants. Toute durée/courbe hardcodée dans un composant est une erreur. + +**P5 — Chaque état est un moment designé.** Loader, 404, état vide, hors-ligne : autant de touchpoints de marque. Le 404 générique du framework est interdit. + +--- + +## 2. WORKFLOW EN 7 PHASES + +### Phase 1 — AUDIT (toujours commencer ici) +- [ ] Inventorier pages, sections, assets, liens externes, formulaires du site source. +- [ ] Mesurer la stack existante (headers HTTP, poids des assets, dépendances tierces). +- [ ] Identifier les bugs de production (images cassées, mixed content, liens morts) et les lister AVANT toute refonte — une migration ne doit jamais masquer un bug existant. +- [ ] Identifier les contradictions doctrine/outillage (ex. discours anti-GAFAM + Google Fonts). +- [ ] Produire un court rapport d'audit et le faire valider avant de coder. + +### Phase 2 — TOKENS & THÈME +- [ ] Créer `app.css` avec toutes les couleurs en `oklch()` (ou valeurs de la charte si fournie), typographies, échelle d'espacement, rayons, **tokens de motion** (courbes + durées). +- [ ] Déclarer le mapping Tailwind via `@theme inline` pour que les utilitaires référencent les variables. +- [ ] Thème via `data-theme` sur `<html>`, appliqué côté serveur dans `app.html` pour éviter tout FOUC. +- [ ] Fonts : woff2 self-hébergées dans `static/fonts/`, `@font-face` avec `font-display: swap`, `preload` de la display uniquement. + +### Phase 3 — ARCHITECTURE +``` +src/routes/ +├── +layout.svelte ← SmoothScrollProvider + CursorTracker (si pertinent) +├── (site)/ ← pages de contenu (SSR complet) +│ ├── +page.svelte +│ └── [section]/[slug]/ +├── (immersive)/ ← layout group : canvas lourds isolés ici +│ └── experience/+page.svelte +└── +error.svelte ← 404 designé (P5) +``` +- [ ] Layout groups pour isoler tout contexte WebGL des pages de contenu. +- [ ] Imports dynamiques (`await import('three')`) derrière `{#if browser}` pour tout ce qui dépasse ~50 KB. +- [ ] `prerender = true` sur toutes les routes statiques ; adapter `adapter-static` ou `adapter-cloudflare` selon l'hébergement cible. +- [ ] Conserver strictement les URLs existantes (redirections 301 si renommage). + +### Phase 4 — COMPOSANTS (bibliothèque de référence, §5) +- [ ] Installer uniquement les composants pertinents pour CE site — la bibliothèque n'est pas un package à copier en entier. +- [ ] Chaque composant livré avec : cleanup dans `$effect`, garde `prefers-reduced-motion`, aucune fuite de listener. + +### Phase 5 — MOTION +- [ ] Transitions d'entrée/sortie de composants : transitions Svelte natives. +- [ ] Scrub/pinning lié au scroll : GSAP ScrollTrigger (import dynamique), OU `animation-timeline: view()` natif avec fallback ScrollTrigger pour Safari ancien. +- [ ] Micro-physics (curseur, springs) : Anime.js v4 imports modulaires (`animejs`, `animejs/text`) ou spring custom de 15 lignes. +- [ ] Vérifier le budget : ≤ 8-12 tweens concurrents sur mobile bas de gamme ; culler les animations hors viewport. + +### Phase 6 — PERFORMANCE (budgets §4) +- [ ] Pipeline assets : `gltf-transform optimize --compress meshopt --texture-compress ktx2` pour tout GLB ; AVIF/WebP responsive pour les images. +- [ ] Media hero ≤ 400 KB avant interaction ; séquences d'images avec chargement conditionné par `navigator.connection`. +- [ ] Mesurer sur profil mobile 4G (CPU ×4, réseau « Fast 4G ») : LCP < 2,5 s, INP < 200 ms, CLS < 0,05. + +### Phase 7 — ACCESSIBILITÉ & DÉGRADATION (§6) +- [ ] La ladder de dégradation complète et testée. +- [ ] Revue clavier complète (tab order, focus visible, skip-link). +- [ ] Contenu canvas doublé en DOM sémantique. + +--- + +## 3. ORCHESTRATION D'ANIMATION — RÈGLES SVELTE 5 + +| Besoin | Outil | Règle | +|---|---|---| +| Entrée/sortie composant, listes | Transitions Svelte natives | Zéro dépendance, SSR-safe | +| Scrub scroll, pinning | GSAP + ScrollTrigger | Import dynamique, init dans `$effect`, kill au cleanup | +| Springs, stagger, split text | Anime.js v4 modulaire | `createScope({ root })` + `scope.revert()` au cleanup | +| Une seule horloge | `gsap.ticker` pilote Lenis | Aucune autre boucle rAF sauf canvas WebGL | + +**Règles runes impératives :** +- État d'animation : `$state` / `$derived` dans le composant ou un module `.svelte.ts`. Jamais de store Svelte legacy pour des valeurs par-frame. +- **Ne jamais écrire des valeurs par-frame (scrollY, pointeur) dans de l'état réactif qui rend du DOM** — écrire dans des variables plain et flusher via rAF. +- GSAP ne doit jamais cibler un nœud que Svelte patche via binding ; cibler via `bind:this` sur du markup statique. +- Tout init GSAP/Anime/Lenis se fait dans `$effect` (ou `onMount`) avec fonction cleanup qui `kill()`/`revert()`/`destroy()`. + +--- + +## 4. BUDGETS PERFORMANCE (mobile 4G, Android entrée de gamme) + +| Ressource | Budget | +|---|---| +| JS initial compressé | ≤ 170 KB | +| Textures GPU | ≤ 8 MB, KTX2/BasisU uniquement | +| Modèles 3D | ≤ 500 KB/scène, meshopt ou Draco | +| Média hero avant interaction | ≤ 400 KB | +| Poids total page d'accueil | ≤ 2 MB | +| Animations JS concurrentes | ≤ 8-12 (transform/opacity only) | +| DevicePixelRatio canvas | plafonné à 1.5 | +| Objectif frame rate | 30 fps stables sur Mali-G52, 60 fps desktop | + +**SSR vs CSR :** les pages canvas rendent un shell sémantique en SSR et hydratent le canvas côté client. Le prerender statique est le défaut ; l'hébergement cible est un CDN edge (Cloudflare Pages/R2, Netlify) — pas de serveur Node sauf justification explicite. + +--- + +## 5. BIBLIOTHÈQUE DE COMPOSANTS (contrats + implémentations condensées) + +### 5.1 SmoothScrollProvider — Lenis au layout racine +```svelte +<script lang="ts"> + import { onMount, setContext, type Snippet } from 'svelte'; + import Lenis from 'lenis'; + import gsap from 'gsap'; + let { children }: { children: Snippet } = $props(); + let lenis: Lenis | null = $state(null); + setContext('lenis', { get: () => lenis }); + onMount(() => { + if (matchMedia('(prefers-reduced-motion: reduce)').matches) return; + lenis = new Lenis({ lerp: 0.1 }); + gsap.ticker.add((t) => lenis?.raf(t * 1000)); + gsap.ticker.lagSmoothing(0); + return () => { lenis?.destroy(); lenis = null; }; + }); +</script> +{@render children()} +``` + +### 5.2 KineticText — titrage cinétique au scroll +Split par **mots** (jamais par caractères pour les langues à apostrophes), `aria-label` avec la chaîne intacte, scrub via ScrollTrigger ou `animation-timeline: view()` en premier choix, stagger piloté par les tokens de motion du projet. + +### 5.3 ParallaxLayer — profondeur multi-plans +CSS-first : si `CSS.supports('animation-timeline: view()')`, animer via keyframes + `view()` ; sinon fallback GSAP ScrollTrigger `scrub: true`, `yPercent` proportionnel à une prop `depth` (0–1). Toujours `transform`, jamais `top/margin`. + +### 5.4 CursorTracker — curseur physique (desktop uniquement) +Garde `matchMedia('(pointer: coarse)')` → ne rien monter sur tactile. Spring (Anime.js `createSpring` ou custom), scale modulé par la **vélocité** du pointeur (pattern Igloo). Exposer position + vélocité via contexte pour alimenter des uniforms WebGL éventuels (curseur-source-de-lumière, pattern Unseen). + +### 5.5 PageTransition — transitions de routes +`onNavigate` + View Transitions API en enhancement progressif ; fallback GSAP (out dans la promise d'`onNavigate`, in via `afterNavigate`). Durées lues depuis les custom properties CSS pour que les deux chemins partagent les mêmes tokens. + +### 5.6 ScrollProgressBar — barre de progression +`<div>` fixe + `scaleX(progress)`, `role="progressbar"` avec `aria-valuenow`, listener scroll passif, calcul dans rAF. + +### 5.7 FlipbookCanvas — séquence d'images scrubée (pattern Apple) +Action Svelte `use:flipbook`. Sélection de qualité via `navigator.connection.effectiveType` + `saveData` : `2g/slow-2g/saveData` → **image fixe unique** ; `3g` → jeu réduit ; `4g` → séquence complète. Frames dessinées sur `<canvas>` via rAF, index = fraction de scroll. Préchargement progressif (ne pas bloquer le LCP). + +### 5.8 WebGLCanvas — renderer unique (seulement si P3 le justifie) +Un seul `WebGLRenderer` pour toute l'app, fourni via `setContext` avec clé `Symbol`, scènes enfants enregistrées/retirées au cleanup, `setPixelRatio(min(dpr, 1.5))`, `setAnimationLoop` stoppé et `renderer.dispose()` au démontage. Import dynamique de `three`. + +--- + +## 6. ACCESSIBILITÉ & LADDER DE DÉGRADATION + +**Gate unique de reduced-motion** au niveau provider : si `prefers-reduced-motion: reduce` → pas de Lenis, pas de scrub, pas de flipbook (poster), contenu statique complet. Ne jamais implémenter la garde composant par composant. + +**Ladder (dans l'ordre) :** +1. `saveData` ou `effectiveType` ≤ 3g → images statiques, zéro préchargement de séquence, vidéo non autoplay. +2. `deviceMemory` ≤ 4 ou échec de création de contexte WebGL → mode DOM-effects-only. +3. JS désactivé → le contenu SSR est complet et lisible. +4. Canvas/WebGL toujours `aria-hidden` avec miroir DOM sémantique du contenu. +5. Attribut `lang` correct sur chaque bascule de langue ; focus déplacé sur le `<h1>` après chaque transition de route. + +--- + +## 7. OUTILLAGE + +```jsonc +{ + "dependencies": { + "lenis": "^1.3", + "gsap": "^3.13", + "animejs": "^4.2" + // "three": uniquement si P3 le justifie — import dynamique obligatoire + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3", + "@gltf-transform/cli": "^4", + "vite-imagetools": "^7", + "vite-plugin-pwa": "^1" // shell hors-ligne + } +} +``` +HMR/WebGL : init/dispose symétriques dans `$effect`, shaders en imports `?raw` (hot-reload propre), paramètres de debug dans le hash d'URL. + +--- + +## 8. DÉFINITION DE « TERMINÉ » (DoD — tout doit être vrai) + +- [ ] Rapport d'audit livré et bugs de production pré-existants corrigés ou explicitement reportés. +- [ ] Aucune ressource chargée depuis un domaine tiers (fonts, CDN, scripts) sauf décision documentée. +- [ ] Lighthouse mobile : Performance ≥ 90, Accessibilité ≥ 95, Best Practices ≥ 95, SEO ≥ 95. +- [ ] Navigation clavier complète + `prefers-reduced-motion` vérifié manuellement. +- [ ] Page fonctionnelle et lisible avec JS désactivé. +- [ ] 404, vide et hors-ligne designés (P5). +- [ ] Toutes les URLs historiques préservées ou redirigées. +- [ ] Build de production vert, zéro warning d'accessibilité Svelte. diff --git a/doc2sveltekit-transition/svelte-5_code_writer.md b/doc2sveltekit-transition/svelte-5_code_writer.md new file mode 100644 index 0000000..7f3eda7 --- /dev/null +++ b/doc2sveltekit-transition/svelte-5_code_writer.md @@ -0,0 +1,61 @@ +# Svelte 5 Code Writer + +## CLI Tools + +You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`: + +### List Documentation Sections + +```bash +npx @sveltejs/mcp list-sections +``` + +Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths. + +### Get Documentation + +```bash +npx @sveltejs/mcp get-documentation "<section1>,<section2>,..." +``` + +Retrieves full documentation for specified sections. Use after `list-sections` to fetch relevant docs. + +**Example:** + +```bash +npx @sveltejs/mcp get-documentation "$state,$derived,$effect" +``` + +### Svelte Autofixer + +```bash +npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options] +``` + +Analyzes Svelte code and suggests fixes for common issues. + +**Options:** + +- `--async` - Enable async Svelte mode (default: false) +- `--svelte-version` - Target version: 4 or 5 (default: 5) + +**Examples:** + +```bash +# Analyze inline code (escape $ as \$) +npx @sveltejs/mcp svelte-autofixer '<script>let count = \$state(0);</script>' + +# Analyze a file +npx @sveltejs/mcp svelte-autofixer ./src/lib/Component.svelte + +# Target Svelte 4 +npx @sveltejs/mcp svelte-autofixer ./Component.svelte --svelte-version 4 +``` + +**Important:** When passing code with runes (`$state`, `$derived`, etc.) via the terminal, escape the `$` character as `\$` to prevent shell variable substitution. + +## Workflow + +1. **Uncertain about syntax?** Run `list-sections` then `get-documentation` for relevant topics +2. **Reviewing/debugging?** Run `svelte-autofixer` on the code to detect issues +3. **Always validate** - Run `svelte-autofixer` before finalizing any Svelte component diff --git a/doc2sveltekit-transition/svelte_core_bestpractices.md b/doc2sveltekit-transition/svelte_core_bestpractices.md new file mode 100644 index 0000000..747af81 --- /dev/null +++ b/doc2sveltekit-transition/svelte_core_bestpractices.md @@ -0,0 +1,172 @@ +## `$state` + +Only use the `$state` rune for variables that should be _reactive_ — in other words, variables that cause an `$effect`, `$derived` or template expression to update. Everything else can be a normal variable. + +Objects and arrays (`$state({...})` or `$state([...])`) are made deeply reactive, meaning mutation will trigger updates. This has a trade-off: in exchange for fine-grained reactivity, the objects must be proxied, which has performance overhead. In cases where you're dealing with large objects that are only ever reassigned (rather than mutated), use `$state.raw` instead. This is often the case with API responses, for example. + +## `$derived` + +To compute something from state, use `$derived` rather than `$effect`: + +```js +// do this +let square = $derived(num * num); + +// don't do this +let square; + +$effect(() => { + square = num * num; +}); +``` + +> [!NOTE] `$derived` is given an expression, _not_ a function. If you need to use a function (because the expression is complex, for example) use `$derived.by`. + +Deriveds are writable — you can assign to them, just like `$state`, except that they will re-evaluate when their expression changes. + +If the derived expression is an object or array, it will be returned as-is — it is _not_ made deeply reactive. You can, however, use `$state` inside `$derived.by` in the rare cases that you need this. + +## `$effect` + +Effects are an escape hatch and should mostly be avoided. In particular, avoid updating state inside effects. + +- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/@attach.md) +- If you need to run some code in response to user interaction, put the code directly in an event handler or use a [function binding](references/bind.md) as appropriate +- If you need to log values for debugging purposes, use [`$inspect`](references/$inspect.md) +- If you need to observe something external to Svelte, use [`createSubscriber`](references/svelte-reactivity.md) + +Never wrap the contents of an effect in `if (browser) {...}` or similar — effects do not run on the server. + +## `$props` + +Treat props as though they will change. For example, values that depend on props should usually use `$derived`: + +```js +// @errors: 2451 +let { type } = $props(); + +// do this +let color = $derived(type === 'danger' ? 'red' : 'green'); + +// don't do this — `color` will not update if `type` changes +let color = type === 'danger' ? 'red' : 'green'; +``` + +## `$inspect.trace` + +`$inspect.trace` is a debugging tool for reactivity. If something is not updating properly or running more than it should you can add `$inspect.trace(label)` as the first line of an `$effect` or `$derived.by` (or any function they call) to trace their dependencies and discover which one triggered an update. + +## Events + +Any element attribute starting with `on` is treated as an event listener: + +```svelte +<button onclick={() => {...}}>click me</button> + +<!-- attribute shorthand also works --> +<button {onclick}>...</button> + +<!-- so do spread attributes --> +<button {...props}>...</button> +``` + +If you need to attach listeners to `window` or `document` you can use `<svelte:window>` and `<svelte:document>`: + +```svelte +<svelte:window onkeydown={...} /> +<svelte:document onvisibilitychange={...} /> +``` + +Avoid using `onMount` or `$effect` for this. + +## Snippets + +[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/@render.md) tag, or passed to components as props. They must be declared within the template. + +```svelte +{#snippet greeting(name)} + <p>hello {name}!</p> +{/snippet} + +{@render greeting('world')} +``` + +> [!NOTE] Snippets declared at the top level of a component (i.e. not inside elements or blocks) can be referenced inside `<script>`. A snippet that doesn't reference component state is also available in a `<script module>`, in which case it can be exported for use by other components. + +## Each blocks + +Prefer to use [keyed each blocks](references/each.md) — this improves performance by allowing Svelte to surgically insert or remove items rather than updating the DOM belonging to existing items. + +> [!NOTE] The key _must_ uniquely identify the object. Do not use the index as a key. + +Avoid destructuring if you need to mutate the item (with something like `bind:value={item.count}`, for example). + +## Using JavaScript variables in CSS + +If you have a JS variable that you want to use inside CSS you can set a CSS custom property with the `style:` directive. + +```svelte +<div style:--columns={columns}>...</div> +``` + +You can then reference `var(--columns)` inside the component's `<style>`. + +## Styling child components + +The CSS in a component's `<style>` is scoped to that component. If a parent component needs to control the child's styles, the preferred way is to use CSS custom properties: + +```svelte +<!-- Parent.svelte --> +<Child --color="red" /> + +<!-- Child.svelte --> +<h1>Hello</h1> + +<style> + h1 { + color: var(--color); + } +</style> +``` + +If this is impossible (for example, the child component comes from a library) you can use `:global` to override styles: + +```svelte +<div> + <Child /> +</div> + +<style> + div :global { + h1 { + color: red; + } + } +</style> +``` + +## Context + +Consider using context instead of declaring state in a shared module. This will scope the state to the part of the app that needs it, and eliminate the possibility of it leaking between users when server-side rendering. + +Use `createContext` rather than `setContext` and `getContext`, as it provides type safety. + +## Async Svelte + +If using version 5.36 or higher, you can use [await expressions](references/await-expressions.md) and [hydratable](references/hydratable.md) to use promises directly inside components. Note that these require the `experimental.async` option to be enabled in `svelte.config.js` as they are not yet considered fully stable. + +## Avoid legacy features + +Always use runes mode for new code, and avoid features that have more modern replacements: + +- use `$state` instead of implicit reactivity (e.g. `let count = 0; count += 1`) +- use `$derived` and `$effect` instead of `$:` assignments and statements (but only use effects when there is no better solution) +- use `$props` instead of `export let`, `$$props` and `$$restProps` +- use `onclick={...}` instead of `on:click={...}` +- use `{#snippet ...}` and `{@render ...}` instead of `<slot>` and `$$slots` and `<svelte:fragment>` +- use `<DynamicComponent>` instead of `<svelte:component this={DynamicComponent}>` +- use `import Self from './ThisComponent.svelte'` and `<Self>` instead of `<svelte:self>` +- use classes with `$state` fields to share reactivity between components, instead of using stores +- use `{@attach ...}` instead of `use:action` +- use clsx-style arrays and objects in `class` attributes, instead of the `class:` directive + diff --git a/docs/DEPLOIEMENT-SVELTEKIT.md b/docs/DEPLOIEMENT-SVELTEKIT.md new file mode 100644 index 0000000..5140d52 --- /dev/null +++ b/docs/DEPLOIEMENT-SVELTEKIT.md @@ -0,0 +1,52 @@ +# Déploiement — ANNU KUTE CED (SvelteKit) + +Branche `svelte` : le hub est un site **100 % statique** (SvelteKit + adapter-static, prerender intégral). Les données PeerTube / Castopod / Mastodon sont **récupérées au build** : pour rafraîchir le contenu, on rebuild (cron, webhook ou CI). + +## Build + +```bash +npm ci +npm run build # génère build/ + injecte la CSP par page (scripts/postbuild-csp.mjs) +npm run preview # vérification locale +``` + +Le dossier `build/` est le site complet, déposable tel quel sur n'importe quel hébergement statique. + +## o2switch (Apache, mutualisé) + +1. Déposer le contenu de `build/` à la racine web du domaine (`public_html/` ou équivalent). +2. Le `.htaccess` fourni dans `build/` gère : redirection HTTPS, redirections des anciennes URLs PHP (`index.php`, `video.php?uuid=…`, `categories.php?id=…`, `recherche.php?q=…`, `direct.php`, `dons.php`, `mentions-legales.php`), `ErrorDocument 404 /404.html`, headers de sécurité, cache immutable pour `/_app/` et `/fonts/`. +3. Rafraîchissement du contenu : rebuild en local ou en CI puis `rsync -av --delete build/ user@host:public_html/`. + +Exemple de cron quotidien (sur une machine de build) : + +```bash +0 5 * * * cd /chemin/annu-kute-ced && git pull && npm ci --prefer-offline && npm run build && rsync -az --delete build/ user@o2switch:public_html/ +``` + +## Cloudflare Pages + +- Build command : `npm run build` +- Build output directory : `build` +- Le fichier `_headers` (dans `build/`) applique CSP, HSTS, `X-Frame-Options`, `nosniff`, `Referrer-Policy`, `Permissions-Policy` et le cache immutable. +- Les anciennes URLs PHP peuvent être redirigées via `_redirects` si besoin (le `.htaccess` ne s'applique pas chez Cloudflare). + +## Sécurité — modèle CSP + +- En-tête global (`_headers` / `.htaccess`) : `style-src`, `img-src`, `font-src`, `connect-src 'self'`, `media-src`, `frame-src https://gade.o-k-i.net`, `frame-ancestors 'none'`, `base-uri`, `form-action`, `object-src 'none'`. +- `script-src` est délivré **par page** via une balise `<meta http-equiv="Content-Security-Policy">` injectée par `scripts/postbuild-csp.mjs` : chaque page ne déclare que les empreintes SHA-256 de ses scripts inline (script de démarrage SvelteKit + JSON-LD). Aucun `'unsafe-inline'` pour les scripts. +- Seules exceptions sortantes (chargement de ressources) : les instances de l'écosystème first-party `gade.o-k-i.net` (vignettes, embeds, flux), `kute.o-k-i.net` (audio, pochettes), `bokante.o-k-i.net` (médias Mastodon). Aucun autre domaine tiers. + +## PWA + +- `manifest.webmanifest` + service worker Workbox (`vite-plugin-pwa`, generateSW) : précache du shell, `navigateFallback: /offline/index.html`. +- Enregistrement par `static/registerSW.js` (chemins absolus), jamais par injection du plugin. + +## Vérifications avant mise en production + +```bash +npm run check # 0 erreur, 0 warning +npm run build # fetch des données + prerender + CSP +``` + +Puis, sur le build : zéro requête tierce au chargement hors instances de l'écosystème (onglet réseau), toutes les routes historiques redirigées, `sitemap.xml` et `robots.txt` servis. diff --git a/dons.php.sample b/dons.php.sample deleted file mode 100644 index 2a49658..0000000 --- a/dons.php.sample +++ /dev/null @@ -1,295 +0,0 @@ -<?php -/** - * Page de dons - Template d'exemple pour ANNU KUTE CED - * Support via LiberaPay, Ko-fi et Stripe - * - * Pour personnaliser cette page : - * 1. Copiez ce fichier vers dons.php - * 2. Activez les dons dans config.local.php : - * define('DONATIONS_ENABLED', true); - * 3. Configurez au moins une plateforme de don : - * - define('LIBERAPAY_URL', 'https://liberapay.com/votre-compte/donate'); - * - define('KOFI_URL', 'https://ko-fi.com/votre-compte'); - * - define('STRIPE_ENABLED', true); + configurez les liens STRIPE_ONE_TIME_LINKS et STRIPE_MONTHLY_LINKS - * 4. Personnalisez le contenu selon vos besoins - */ - -require_once 'includes/config.php'; -require_once 'includes/security.php'; - -// Vérifier si les dons sont activés -if (!defined('DONATIONS_ENABLED') || !DONATIONS_ENABLED) { - http_response_code(404); - include '404.php'; - exit; -} - -// Vérifier qu'au moins une plateforme de don est configurée -$hasLiberaPay = !empty(LIBERAPAY_URL); -$hasKofi = !empty(KOFI_URL); -$hasStripe = defined('STRIPE_ENABLED') && STRIPE_ENABLED; - -if (!$hasLiberaPay && !$hasKofi && !$hasStripe) { - http_response_code(500); - error_log('Aucune plateforme de don configurée'); - include '500.php'; - exit; -} - -// Définir les headers de sécurité -setSecurityHeaders(); - -// Configuration des montants et devise -$donationAmounts = defined('DONATION_AMOUNTS') ? DONATION_AMOUNTS : [1, 5, 10, 20, 50]; -$currency = defined('DONATION_CURRENCY') ? DONATION_CURRENCY : 'EUR'; -$currencySymbol = $currency === 'EUR' ? '€' : '$'; - -// Récupérer les liens Stripe s'ils sont configurés -$stripeOneTimeLinks = defined('STRIPE_ONE_TIME_LINKS') ? STRIPE_ONE_TIME_LINKS : []; -$stripeMonthlyLinks = defined('STRIPE_MONTHLY_LINKS') ? STRIPE_MONTHLY_LINKS : []; - -?> -<!DOCTYPE html> -<html lang="fr"> -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <meta name="csrf-token" content="<?php echo generateCSRFToken(); ?>"> - - <!-- PERSONNALISEZ: Titre et description de votre page de dons --> - <title>Soutenir <?php echo ORGANIZATION_NAME; ?> - Dons - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-

Soutenir

-

Votre soutien est essentiel pour maintenir le hub multimédia du podcast

-
-
- - -
- -
- - -
-
-
- -

Hébergement & Infrastructure

-

Maintenir nos serveurs, notre plateforme et nos outils numériques représente des coûts mensuels importants.

-
-
- -

Production du podcast

-

Vos contributions nous aident à assurer la régularité et la qualité des épisodes d'ANNU KUTE CED.

-
-
- -

Indépendance Numérique

-

Vos dons nous permettent de rester indépendants des plateformes commerciales et de préserver notre souveraineté numérique sur le Fédivers.

-
-
-
-
-
- - - - - - - - - - diff --git a/img/play-icon.svg b/img/play-icon.svg deleted file mode 100644 index 5f25807..0000000 --- a/img/play-icon.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/includes/config.default.php b/includes/config.default.php deleted file mode 100644 index 4c28b3f..0000000 --- a/includes/config.default.php +++ /dev/null @@ -1,286 +0,0 @@ - Nom personnalisé] -if (!defined('PRIORITY_CATEGORIES')) { - define('PRIORITY_CATEGORIES', [ - 11 => 'Actualités & Politique', - 15 => 'Science et Technologie', - 4 => 'Art', - 9 => 'Humour', - 10 => 'Divertissement' - ]); -} - -// ========================================= -// Configuration Mastodon -// ========================================= -if (!defined('MASTODON_INSTANCE_URL')) define('MASTODON_INSTANCE_URL', 'https://bokante.o-k-i.net'); -if (!defined('MASTODON_DATE_FORMAT')) define('MASTODON_DATE_FORMAT', 'fr-FR'); -if (!defined('MASTODON_BTN_SEE_MORE')) define('MASTODON_BTN_SEE_MORE', 'Voir plus de post'); -if (!defined('MASTODON_BTN_RELOAD')) define('MASTODON_BTN_RELOAD', 'Rafraichir'); -if (!defined('MASTODON_MAX_POST_FETCH')) define('MASTODON_MAX_POST_FETCH', '10'); -if (!defined('MASTODON_MAX_POST_SHOW')) define('MASTODON_MAX_POST_SHOW', '10'); - -// URL du stockage S3 pour les médias Mastodon (laissez vide pour désactiver) -// Format: https://votre-bucket.s3.region.provider.com -// if (!defined('MASTODON_S3_MEDIA_URL')) define('MASTODON_S3_MEDIA_URL', 'https://s3.eu-central-003.backblazeb2.com'); - -// Informations du site -if (!defined('SITE_NAME')) define('SITE_NAME', 'ANNU KUTE CED'); -if (!defined('SITE_DESCRIPTION')) define('SITE_DESCRIPTION', 'Hub multimédia du podcast ANNU KUTE CED'); -if (!defined('SITE_LOGO')) define('SITE_LOGO', 'img/logo.png'); -if (!defined('SITE_FAVICON')) define('SITE_FAVICON', 'img/favicon.png'); - -// Configuration du fuseau horaire par défaut -// Liste des fuseaux disponibles : https://www.php.net/manual/fr/timezones.php -if (!defined('DEFAULT_TIMEZONE')) define('DEFAULT_TIMEZONE', 'America/Guadeloupe'); - -// Réseaux sociaux -if (!defined('FACEBOOK_URL')) define('FACEBOOK_URL', '#'); -if (!defined('X_URL')) define('X_URL', '#'); -if (!defined('INSTAGRAM_URL')) define('INSTAGRAM_URL', '#'); -if (!defined('YOUTUBE_URL')) define('YOUTUBE_URL', '#'); -if (!defined('TIKTOK_URL')) define('TIKTOK_URL', '#'); -if (!defined('MASTODON_URL')) define('MASTODON_URL', 'https://bokante.o-k-i.net/@cedric'); - -// Contacts -if (!defined('CONTACT_EMAIL')) define('CONTACT_EMAIL', 'kontak@o-k-i.net'); - -// Mentions légales -if (!defined('LEGAL_COPYRIGHT')) define('LEGAL_COPYRIGHT', 'ANNU KUTE CED'); -if (!defined('LEGAL_WEBMASTER_NAME')) define('LEGAL_WEBMASTER_NAME', 'Cédric Famibelle-Pronzola'); -if (!defined('LEGAL_WEBMASTER_EMAIL')) define('LEGAL_WEBMASTER_EMAIL', 'contact@cedric-pronzola.dev'); -if (!defined('LEGAL_HOST_NAME')) define('LEGAL_HOST_NAME', 'o2Switch'); -if (!defined('LEGAL_HOST_COMPANY')) define('LEGAL_HOST_COMPANY', 'société au capital de 100 000 €'); -if (!defined('LEGAL_HOST_RCS')) define('LEGAL_HOST_RCS', 'immatriculée au RCS de Clermont-Ferrand sous le numéro 510 909 807'); -if (!defined('LEGAL_HOST_ADDRESS')) define('LEGAL_HOST_ADDRESS', '222 boulevard Gustave Flaubert, 63000 Clermont-Ferrand, France'); -if (!defined('LEGAL_CONTACT_EMAIL')) define('LEGAL_CONTACT_EMAIL', 'kontak@o-k-i.net'); -if (!defined('LEGAL_LICENSE')) define('LEGAL_LICENSE', 'GNU Affero General Public License version 3 (AGPL-V3)'); -if (!defined('LEGAL_LICENSE_URL')) define('LEGAL_LICENSE_URL', 'https://www.gnu.org/licenses/agpl-3.0.html'); -if (!defined('LEGAL_SOURCE_CODE_URL')) define('LEGAL_SOURCE_CODE_URL', 'https://labola.o-k-i.net/cedric/annu-kute-ced'); -if (!defined('LEGAL_SERVICE_DESCRIPTION')) define('LEGAL_SERVICE_DESCRIPTION', 'est le hub multimédia du podcast ANNU KUTE CED.'); - -// Fonctionnalités -define('ENABLE_SEARCH', true); -if (!defined('ENABLE_USER_ACCOUNTS')) define('ENABLE_USER_ACCOUNTS', false); - -// Cache -if (!defined('CACHE_ENABLED')) define('CACHE_ENABLED', false); -if (!defined('CACHE_DURATION')) define('CACHE_DURATION', 3600); // En secondes (1 heure) - -// ========================================= -// Configuration de la section Hero (bannière d'accueil) -// ========================================= - -// Type de contenu à afficher dans la section hero -// Options: 'live' (direct PeerTube), 'playlist' (playlist audio/vidéo), 'video' (vidéo unique), 'none' (masquer) -if (!defined('HERO_TYPE')) define('HERO_TYPE', 'live'); - -// Configuration pour le direct (HERO_TYPE = 'live') -if (!defined('LIVE_ACCOUNT_NAME')) define('LIVE_ACCOUNT_NAME', 'annu_kute_ced'); - -// Configuration pour une vidéo unique (HERO_TYPE = 'video') -if (!defined('HERO_VIDEO_ID')) define('HERO_VIDEO_ID', '1aJ2u9euwF9fWKQhFxwFio'); -if (!defined('HERO_VIDEO_TITLE')) define('HERO_VIDEO_TITLE', 'Vidéo de présentation'); - -// Configuration pour les playlists (HERO_TYPE = 'playlist') -// Type de plateforme: 'peertube', 'funkwhale', 'castopod' -if (!defined('PLAYLIST_PLATFORM')) define('PLAYLIST_PLATFORM', 'peertube'); - -// URL de base de la plateforme de playlist -if (!defined('PLAYLIST_INSTANCE_URL')) define('PLAYLIST_INSTANCE_URL', ''); - -// ID de la playlist à afficher -if (!defined('PLAYLIST_ID')) define('PLAYLIST_ID', ''); - -// Titre de la playlist (optionnel, sinon récupéré via API) -if (!defined('PLAYLIST_TITLE')) define('PLAYLIST_TITLE', ''); - -// Description de la playlist (optionnel) -if (!defined('PLAYLIST_DESCRIPTION')) define('PLAYLIST_DESCRIPTION', ''); - -// ========================================= -// Configuration Castopod -// ========================================= - -// Activer l'affichage des podcasts Castopod -if (!defined('CASTOPOD_ENABLED')) define('CASTOPOD_ENABLED', true); - -// URL de l'instance Castopod -if (!defined('CASTOPOD_URL')) define('CASTOPOD_URL', 'https://kute.o-k-i.net'); - -// Liste des slugs de podcasts à afficher (tableau) -// Format: ['slug1', 'slug2', 'slug3'] -// Les épisodes de tous les podcasts seront mélangés et triés par date -if (!defined('CASTOPOD_PODCAST_SLUGS')) { - define('CASTOPOD_PODCAST_SLUGS', ['annu_kute_cedric']); -} - -// Nombre d'épisodes à afficher (total, tous podcasts confondus) -if (!defined('CASTOPOD_EPISODES_COUNT')) define('CASTOPOD_EPISODES_COUNT', 5); - -// ========================================= -// Configuration Funkwhale -// ========================================= - -// Activer l'affichage des morceaux Funkwhale -if (!defined('FUNKWHALE_ENABLED')) define('FUNKWHALE_ENABLED', false); - -// URL de l'instance Funkwhale -if (!defined('FUNKWHALE_URL')) define('FUNKWHALE_URL', 'https://mizik.o-k-i.net'); - -// Nombre de morceaux à afficher -if (!defined('FUNKWHALE_TRACKS_COUNT')) define('FUNKWHALE_TRACKS_COUNT', 10); - -// Tags pour filtrer les vidéos selon les catégories -if (!defined('TAG_SHORT')) define('TAG_SHORT', 'short'); - -// Hashtags importants à afficher dans la sidebar, footer et menu mobile -if (!defined('IMPORTANT_TAGS')) { - define('IMPORTANT_TAGS', [ - 'ANNUKUTECED', - 'podcast', - 'fediverse', - 'audio', - 'vidéo' - ]); -} - -// Hashtags populaires à afficher sur la page d'accueil -if (!defined('POPULAR_TAGS')) { - define('POPULAR_TAGS', [ - 'podcast', - 'annukuteced', - 'fediverse', - 'audio' - ]); -} - -// ========================================= -// Système de compte à rebours / maintenance -// ========================================= - -// Activer le mode compte à rebours (true/false) - valeur par défaut -if (!defined('COUNTDOWN_ENABLED')) define('COUNTDOWN_ENABLED', false); - -// Date de fin du compte à rebours par défaut (format: Y-m-d H:i:s) -if (!defined('COUNTDOWN_TARGET_DATE')) define('COUNTDOWN_TARGET_DATE', '2025-10-11 00:00:00'); - -// Territoires et fuseaux horaires par défaut pour la page de compte à rebours -if (!defined('COUNTDOWN_TIMEZONES')) { - define('COUNTDOWN_TIMEZONES', [ - 'Martinique / Guadeloupe' => 'America/Martinique', - 'Guyane' => 'America/Cayenne', - 'France' => 'Europe/Paris', - 'Ma\'ohi Nui' => 'Pacific/Tahiti', - 'Kanaky' => 'Pacific/Noumea' - ]); -} - -// ========================================= -// Intégration WordPress par défaut -// ========================================= - -// URL du site WordPress par défaut -if (!defined('WORDPRESS_URL')) define('WORDPRESS_URL', ''); - -// Nombre d'articles WordPress à afficher par défaut -if (!defined('WORDPRESS_POSTS_COUNT')) define('WORDPRESS_POSTS_COUNT', 6); - -// Activation des articles WordPress par défaut -if (!defined('WORDPRESS_ENABLED')) define('WORDPRESS_ENABLED', false); - -// ========================================= -// Système de dons par défaut -// ========================================= - -// Activation du système de dons par défaut -if (!defined('DONATIONS_ENABLED')) define('DONATIONS_ENABLED', false); - -// URLs des plateformes de don -if (!defined('LIBERAPAY_URL')) define('LIBERAPAY_URL', ''); // Ex: https://liberapay.com/votre-compte/donate -if (!defined('KOFI_URL')) define('KOFI_URL', ''); // Ex: https://ko-fi.com/votre-compte -if (!defined('STRIPE_ENABLED')) define('STRIPE_ENABLED', false); - -// Configuration Stripe -if (!defined('STRIPE_ONE_TIME_LINKS')) { - define('STRIPE_ONE_TIME_LINKS', [ - 1 => '', // Lien Stripe pour don de 1€ - 5 => '', // Lien Stripe pour don de 5€ - 10 => '', // Lien Stripe pour don de 10€ - 20 => '', // Lien Stripe pour don de 20€ - 50 => '', // Lien Stripe pour don de 50€ - 'custom' => '' // Lien Stripe pour montant personnalisé - ]); -} - -if (!defined('STRIPE_MONTHLY_LINKS')) { - define('STRIPE_MONTHLY_LINKS', [ - 1 => '', // Lien Stripe pour don mensuel de 1€ - 5 => '', // Lien Stripe pour don mensuel de 5€ - 10 => '', // Lien Stripe pour don mensuel de 10€ - 20 => '', // Lien Stripe pour don mensuel de 20€ - 50 => '', // Lien Stripe pour don mensuel de 50€ - ]); -} - -// Montants de dons suggérés par défaut -if (!defined('DONATION_AMOUNTS')) define('DONATION_AMOUNTS', [1, 5, 10, 20, 50]); - -// Devise par défaut -if (!defined('DONATION_CURRENCY')) define('DONATION_CURRENCY', 'EUR'); - -// ========================================= -// Annonce du prochain live -// ========================================= - -// Activer/désactiver l'annonce du prochain live par défaut -if (!defined('NEXT_LIVE_ENABLED')) define('NEXT_LIVE_ENABLED', false); - -// Titre de l'annonce du prochain live -if (!defined('NEXT_LIVE_TITLE')) define('NEXT_LIVE_TITLE', 'Prochain live'); - -// Description de l'annonce du prochain live -if (!defined('NEXT_LIVE_DESCRIPTION')) define('NEXT_LIVE_DESCRIPTION', 'Rejoignez-nous pour notre prochain live !'); - -// Date du prochain live (format: Y-m-d H:i:s) -if (!defined('NEXT_LIVE_DATE')) define('NEXT_LIVE_DATE', ''); - -// Chemin vers l'image d'annonce du prochain live (relatif à la racine du site) -// Exemple: 'uploads/next-live.jpg' -if (!defined('NEXT_LIVE_IMAGE')) define('NEXT_LIVE_IMAGE', 'uploads/next-live.jpg'); diff --git a/includes/config.local.php.sample b/includes/config.local.php.sample deleted file mode 100644 index 059e64e..0000000 --- a/includes/config.local.php.sample +++ /dev/null @@ -1,437 +0,0 @@ - https://mon_instance_peertube.fr/api/v1/videos/categories) -// 1 : Music -// 2 : Films -// 3 : Vehicles -// 4 : Art -// 5 : Sports -// 6 : Travels -// 7 : Gaming -// 8 : People -// 9 : Comedy -// 10 : Entertainment -// 11 : News & Politics -// 12 : How To -// 13 : Education -// 14 : Activism -// 15 : Science & Technology -// 16 : Animals -// 17 : Kids -// 18 : Food -define('PRIORITY_CATEGORIES', [ - 11 => 'Actualités & Politique', - 15 => 'Science et Technologie', - 4 => 'Art', - 9 => 'Humour', - 10 => 'Divertissement' - // Ajoutez d'autres catégories selon vos besoins -]); - -// ========================================= -// Informations du site -// ========================================= - -// Nom du site -// define('SITE_NAME', 'ANNU KUTE CED'); - -// Description du site -// define('SITE_DESCRIPTION', 'Hub multimédia du podcast ANNU KUTE CED'); - -// Logo du site -// define('SITE_LOGO', 'img/logo.png'); - -// Favicon du site -// define('SITE_FAVICON', 'img/favicon.png'); - -// Fuseau horaire par défaut pour l'affichage des dates -// Liste des fuseaux disponibles : https://www.php.net/manual/fr/timezones.php -// define('DEFAULT_TIMEZONE', 'America/Guadeloupe'); - -// ========================================= -// Réseaux sociaux -// ========================================= - -// URL de la page Facebook -// define('FACEBOOK_URL', 'https://facebook.com/votrepage'); - -// URL du compte X (anciennement Twitter) -// define('X_URL', 'https://x.com/votrecompte'); - -// URL du compte Instagram -// define('INSTAGRAM_URL', 'https://instagram.com/votrecompte'); - -// URL de la chaîne YouTube -// define('YOUTUBE_URL', 'https://youtube.com/votrechaine'); - -// URL du compte TikTok -// define('TIKTOK_URL', 'https://tiktok.com/@votrecompte'); - -// URL du compte Mastodon -// Compte BOKANTE du podcast : https://bokante.o-k-i.net/@cedric -// define('MASTODON_URL', 'https://bokante.o-k-i.net/@cedric'); - -// ========================================= -// Contact -// ========================================= - -// Email de contact -// define('CONTACT_EMAIL', 'kontak@o-k-i.net'); - -// ========================================= -// Fonctionnalités -// ========================================= - -// Activer/désactiver les commentaires -// define('ENABLE_COMMENTS', true); - -// Activer/désactiver la recherche -// define('ENABLE_SEARCH', true); - -// Activer/désactiver les comptes utilisateurs -// define('ENABLE_USER_ACCOUNTS', false); - -// ========================================= -// Cache -// ========================================= - -// Activer/désactiver le cache -// define('CACHE_ENABLED', false); - -// Durée du cache en secondes -// define('CACHE_DURATION', 3600); // 1 heure - -// ========================================= -// Configuration Mastodon -// ========================================= - -// URL de l'instance Mastodon -// Instance BOKANTE : https://bokante.o-k-i.net -// define('MASTODON_INSTANCE_URL', 'https://bokante.o-k-i.net'); - -// Format de date pour l'affichage des posts -// define('MASTODON_DATE_FORMAT', 'fr-FR'); - -// Texte du bouton "Voir plus" -// define('MASTODON_BTN_SEE_MORE', 'Voir plus de post'); - -// Texte du bouton "Rafraichir" -// define('MASTODON_BTN_RELOAD', 'Rafraichir'); - -// Nombre maximum de posts à récupérer -// define('MASTODON_MAX_POST_FETCH', '10'); - -// Nombre maximum de posts à afficher -// define('MASTODON_MAX_POST_SHOW', '10'); - -// URL du stockage S3 pour les médias Mastodon (optionnel) -// Format: https://votre-bucket.s3.region.provider.com -// Laissez vide ou commentez pour désactiver -// define('MASTODON_S3_MEDIA_URL', 'https://s3.eu-central-003.backblazeb2.com'); - -// ========================================= -// Mentions légales -// ========================================= - -// define('LEGAL_COPYRIGHT', 'ANNU KUTE CED'); -// define('LEGAL_WEBMASTER_NAME', 'Cédric Famibelle-Pronzola'); -// define('LEGAL_WEBMASTER_EMAIL', 'contact@cedric-pronzola.dev'); -// define('LEGAL_HOST_NAME', 'o2Switch'); -// define('LEGAL_HOST_COMPANY', 'société au capital de 100 000 €'); -// define('LEGAL_HOST_RCS', 'immatriculée au RCS de Clermont-Ferrand sous le numéro 510 909 807'); -// define('LEGAL_HOST_ADDRESS', '222 boulevard Gustave Flaubert, 63000 Clermont-Ferrand, France'); -// define('LEGAL_CONTACT_EMAIL', 'kontak@o-k-i.net'); -// define('LEGAL_LICENSE', 'GNU Affero General Public License version 3 (AGPL-V3)'); -// define('LEGAL_LICENSE_URL', 'https://www.gnu.org/licenses/agpl-3.0.html'); -// Source du fork sur LaBola -// define('LEGAL_SOURCE_CODE_URL', 'https://labola.o-k-i.net/cedric/annu-kute-ced'); -// define('LEGAL_SERVICE_DESCRIPTION', ' est le hub multimédia du podcast ANNU KUTE CED.'); - -// ========================================= -// Système de compte à rebours / maintenance -// ========================================= - -// Activer le mode compte à rebours (true/false) -define('COUNTDOWN_ENABLED', false); - -// Date de fin du compte à rebours (format: Y-m-d H:i:s) -define('COUNTDOWN_TARGET_DATE', '2025-10-11 00:00:00'); - -// Territoires et fuseaux horaires à afficher sur la page de compte à rebours -define('COUNTDOWN_TIMEZONES', [ - 'Martinique / Guadeloupe' => 'America/Martinique', - 'Guyane' => 'America/Cayenne', - 'France' => 'Europe/Paris', - 'Ma\'ohi Nui' => 'Pacific/Tahiti', - 'Kanaky' => 'Pacific/Noumea' -]); - -// ========================================= -// Intégration WordPress -// ========================================= - -// Activer/désactiver l'affichage des articles WordPress -define('WORDPRESS_ENABLED', false); - -// Nombre d'articles WordPress à afficher -// define('WORDPRESS_POSTS_COUNT', 6); - -// URL du site WordPress pour récupérer les articles (sans trailing slash) -// define('WORDPRESS_URL', 'https://votre-site-wordpress.com'); - -// ========================================= -// Intégration Castopod (Podcasts) -// ========================================= - -// Activer/désactiver l'affichage des podcasts Castopod -// define('CASTOPOD_ENABLED', true); - -// URL de l'instance Castopod -// Instance KUTE : https://kute.o-k-i.net -// define('CASTOPOD_URL', 'https://kute.o-k-i.net'); - -// Liste des slugs de podcasts à afficher (tableau) -// Le podcast ANNU KUTE CED est accessible via : https://kute.o-k-i.net/@annu_kute_cedric -// Le feed RSS est : https://kute.o-k-i.net/@annu_kute_cedric/feed -// Les épisodes de tous les podcasts seront mélangés et triés par date -// define('CASTOPOD_PODCAST_SLUGS', [ -// 'annu_kute_cedric' -// ]); - -// Nombre d'épisodes à afficher (total, tous podcasts confondus) -// define('CASTOPOD_EPISODES_COUNT', 5); - -// IMPORTANT : Le cache est fortement recommandé pour éviter le rate limiting de Castopod -// Castopod applique un rate limit strict (HTTP 429) sur les requêtes RSS -// Avec le cache activé, les données sont mises en cache pendant CACHE_DURATION secondes -// define('CACHE_ENABLED', true); -// define('CACHE_DURATION', 3600); // 1 heure recommandé - -// ========================================= -// Intégration Funkwhale (Musique) -// ========================================= - -// Activer/désactiver l'affichage des morceaux Funkwhale -// define('FUNKWHALE_ENABLED', true); - -// URL de l'instance Funkwhale -// define('FUNKWHALE_URL', 'https://mizik.o-k-i.net'); - -// Nombre de morceaux aléatoires à afficher -// define('FUNKWHALE_TRACKS_COUNT', 10); - -// ========================================= -// Système de dons -// ========================================= - -// Activer/désactiver le système de dons -// define('DONATIONS_ENABLED', true); - -// URLs des plateformes de don -// define('LIBERAPAY_URL', 'https://liberapay.com/votre-compte/donate'); -// define('KOFI_URL', 'https://ko-fi.com/votre-compte'); - -// Activer/désactiver les dons via Stripe -// define('STRIPE_ENABLED', false); - -// Liens Stripe pour les dons ponctuels -// define('STRIPE_ONE_TIME_LINKS', [ -// 1 => 'https://buy.stripe.com/xxxxx', // Lien pour don de 1€ -// 5 => 'https://buy.stripe.com/xxxxx', // Lien pour don de 5€ -// 10 => 'https://buy.stripe.com/xxxxx', // Lien pour don de 10€ -// 20 => 'https://buy.stripe.com/xxxxx', // Lien pour don de 20€ -// 50 => 'https://buy.stripe.com/xxxxx', // Lien pour don de 50€ -// 'custom' => 'https://buy.stripe.com/xxxxx' // Lien pour montant personnalisé -// ]); - -// Liens Stripe pour les dons mensuels (récurrents) -// define('STRIPE_MONTHLY_LINKS', [ -// 1 => 'https://buy.stripe.com/xxxxx', // Lien pour don mensuel de 1€ -// 5 => 'https://buy.stripe.com/xxxxx', // Lien pour don mensuel de 5€ -// 10 => 'https://buy.stripe.com/xxxxx', // Lien pour don mensuel de 10€ -// 20 => 'https://buy.stripe.com/xxxxx', // Lien pour don mensuel de 20€ -// 50 => 'https://buy.stripe.com/xxxxx', // Lien pour don mensuel de 50€ -// ]); - -// Montants de dons suggérés (en euros par défaut) -// define('DONATION_AMOUNTS', [1, 5, 10, 20, 50]); - -// Devise pour les dons -// define('DONATION_CURRENCY', 'EUR'); - -// ========================================= -// Texte de présentation du podcast -// ========================================= - -// Titre de la section de présentation (par défaut: "À propos") -// define('MOVEMENT_TITLE', 'Le podcast ANNU KUTE CED'); - -// Premier paragraphe de description (pour désactiver le bloc de présentation, commentez cette ligne) -// define('MOVEMENT_DESCRIPTION', 'ANNU KUTE CED est un podcast diffusé sur le Fédivers.'); - -// Deuxième paragraphe de description (optionnel) -// define('MOVEMENT_DESCRIPTION_2', 'Retrouvez nos épisodes sur Castopod, nos vidéos sur PeerTube et nos actualités sur Mastodon.'); - -// Image du podcast à afficher dans la section de présentation -// define('MOVEMENT_IMAGE', 'img/movement_presentation.png'); - -// Texte alternatif pour l'image (accessibilité) -// define('MOVEMENT_IMAGE_ALT', 'Logo du podcast ANNU KUTE CED'); - -// Légende de l'image (peut contenir du HTML simple comme
) -// define('MOVEMENT_CAPTION', 'ANNU KUTE CED'); - - -// ========================================= -// Annonce du prochain live -// ========================================= - -// Activer/désactiver l'annonce du prochain live par défaut -// define('NEXT_LIVE_ENABLED', true); - -// Titre de l'annonce du prochain live -// define('NEXT_LIVE_TITLE', 'Prochain live'); - -// Description de l'annonce du prochain live -// define('NEXT_LIVE_DESCRIPTION', 'Rejoignez-nous pour le prochain enregistrement d\'ANNU KUTE CED !'); - -// Date du prochain live (format: Y-m-d H:i:s) -// define('NEXT_LIVE_DATE', '2025-10-11 10:00:00'); - -// Chemin vers l'image d'annonce du prochain live (relatif à la racine du site) -// Exemple: 'uploads/next-live.jpg' -// define('NEXT_LIVE_IMAGE', 'uploads/next-live.jpeg'); diff --git a/includes/config.php b/includes/config.php deleted file mode 100644 index ecf33c0..0000000 --- a/includes/config.php +++ /dev/null @@ -1,1012 +0,0 @@ - 'Musique', - 'Films' => 'Films', - 'Vehicles' => 'Véhicules', - 'Art' => 'Art', - 'Sports' => 'Sports', - 'Travels' => 'Voyages', - 'Gaming' => 'Jeux vidéo', - 'People' => 'Personnes', - 'Comedy' => 'Humour', - 'Entertainment' => 'Divertissement', - 'News & Politics' => 'Actualités & Politique', - 'How To' => 'Tutoriels', - 'Education' => 'Éducation', - 'Activism' => 'Activisme', - 'Science & Technology' => 'Science & Technologie', - 'Animals' => 'Animaux', - 'Kids' => 'Enfants', - 'Food' => 'Cuisine', - ]; - - // Si une constante PRIORITY_CATEGORIES est définie, utiliser ces traductions - if (defined('PRIORITY_CATEGORIES')) { - $priorityCategories = PRIORITY_CATEGORIES; - foreach ($priorityCategories as $id => $name) { - // Trouver la clé anglaise correspondant à l'ID - $englishName = array_search($id, array_keys($categories)); - if ($englishName !== false) { - $translations[$englishName] = $name; - } - } - } - - $result = []; - foreach ($categories as $key => $name) { - // Utiliser la traduction si disponible, sinon garder le nom original - $translatedName = isset($translations[$name]) ? $translations[$name] : $name; - $result[$key] = $translatedName; - } - - return $result; -} - -/** - * Version originale pour appeler l'API PeerTube (sans cache) - * Cette fonction est maintenant utilisée en interne par callPeerTubeApi - * - * @param string $endpoint Point de terminaison de l'API - * @param array $params Paramètres optionnels pour la requête - * @return array Données retournées par l'API - */ -function callPeerTubeApiOriginal($endpoint, $params = []) { - // Validation de l'URL de base PeerTube pour prévenir SSRF - if (!isValidPeerTubeUrl(PEERTUBE_URL)) { - error_log('SECURITY: Invalid PeerTube URL detected: ' . PEERTUBE_URL); - return []; - } - - // Nettoyer et valider l'endpoint - $endpoint = ltrim($endpoint, '/'); - if (!isValidApiEndpoint($endpoint)) { - error_log('SECURITY: Invalid API endpoint detected: ' . $endpoint); - return []; - } - - $url = PEERTUBE_URL . '/api/v1/' . $endpoint; - - // Ajouter les paramètres à l'URL - if (!empty($params)) { - $url .= '?' . http_build_query($params); - } - - // Initialiser cURL avec options de sécurité - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 30); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); - curl_setopt($ch, CURLOPT_MAXREDIRS, 0); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); - curl_setopt($ch, CURLOPT_USERAGENT, 'fediverse-oki/1.0'); - - // Ajouter la clé API si définie - if (defined('API_KEY') && !empty(API_KEY)) { - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Authorization: ApiKey ' . API_KEY - ]); - } - - // Exécuter la requête - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $error = curl_error($ch); - curl_close($ch); - - // Traiter la réponse - if ($response === false || !empty($error)) { - error_log('PeerTube API error: ' . $error); - return []; - } - - if ($httpCode < 200 || $httpCode >= 300) { - error_log('PeerTube API HTTP error: ' . $httpCode); - return []; - } - - // Décoder la réponse JSON - $data = json_decode($response, true); - - return $data ?: []; -} - -/** - * Fonction utilitaire pour appeler l'API PeerTube avec cache - * - * @param string $endpoint Point de terminaison de l'API - * @param array $params Paramètres optionnels pour la requête - * @return array Données retournées par l'API - */ -function callPeerTubeApi($endpoint, $params = []) { - // Utiliser la fonction cachée si disponible - if (function_exists('callPeerTubeApiCached')) { - return callPeerTubeApiCached($endpoint, $params); - } - - // Fallback vers la version originale - return callPeerTubeApiOriginal($endpoint, $params); -} - -/** - * Valide l'URL PeerTube pour prévenir les attaques SSRF - * - * @param string $url URL à valider - * @return bool True si l'URL est valide et sûre - */ -function isValidPeerTubeUrl($url) { - // Vérifier que l'URL est bien formée - $parsed = parse_url($url); - if (!$parsed || !isset($parsed['scheme']) || !isset($parsed['host'])) { - return false; - } - - // Autoriser uniquement HTTPS (ou HTTP en développement) - if (!in_array($parsed['scheme'], ['https', 'http'])) { - return false; - } - - // Bloquer les adresses IP privées et locales - $host = $parsed['host']; - if (filter_var($host, FILTER_VALIDATE_IP)) { - if (!filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { - return false; - } - } - - // Bloquer localhost et autres domaines dangereux - $blockedHosts = ['localhost', '127.0.0.1', '::1', '0.0.0.0', 'metadata.google.internal']; - if (in_array(strtolower($host), $blockedHosts)) { - return false; - } - - return true; -} - -/** - * Valide l'endpoint API pour prévenir l'injection de chemins - * - * @param string $endpoint Endpoint à valider - * @return bool True si l'endpoint est valide - */ -function isValidApiEndpoint($endpoint) { - // Bloquer les tentatives de path traversal - if (strpos($endpoint, '..') !== false || strpos($endpoint, '//') !== false) { - return false; - } - - // Autoriser uniquement les caractères alphanumériques, tirets, underscores et slashes - if (!preg_match('/^[a-zA-Z0-9\/_-]+$/', $endpoint)) { - return false; - } - - // Liste blanche des endpoints autorisés - $allowedEndpoints = [ - 'videos', - 'videos/categories', - 'search/videos', - 'videos/.*', // Pour les endpoints dynamiques comme videos/{id} - 'videos/.*/comment-threads', // Pour les commentaires - 'accounts', - 'accounts/.*/videos' // Pour les vidéos d'un compte spécifique - ]; - - foreach ($allowedEndpoints as $pattern) { - // Remplacer les .* par des marqueurs temporaires - $tempPattern = str_replace('.*', '__WILDCARD__', $pattern); - // Échapper les caractères spéciaux regex - $escapedPattern = preg_quote($tempPattern, '/'); - // Remettre les wildcards en place - $regexPattern = str_replace('__WILDCARD__', '.*', $escapedPattern); - - if (preg_match('/^' . $regexPattern . '$/', $endpoint)) { - return true; - } - } - - return false; -} - -/** - * Récupère les catégories depuis l'API PeerTube - * - * @return array Liste des catégories - */ -function getCategories() { - // Utiliser les catégories déjà récupérées - $categories = PEERTUBE_CATEGORIES; - - $result = []; - foreach ($categories as $key => $name) { - $result[] = [ - 'id' => $key, - 'name' => $name - ]; - } - - return $result; -} - -/** - * Récupère les vidéos récentes depuis l'API PeerTube - * - * @param int $count Nombre de vidéos à récupérer - * @return array Liste des vidéos récentes - */ -function getRecentVideos($count = RECENT_VIDEOS_COUNT) { - // Récupérer les vidéos récentes - $data = callPeerTubeApi('videos', [ - 'sort' => '-publishedAt', - 'count' => $count, - 'isLocal' => true - ]); - - return formatVideosData($data['data'] ?? []); -} - -/** - * Récupère les vidéos tendances depuis l'API PeerTube - * - * @param int $count Nombre de vidéos à récupérer - * @return array Liste des vidéos tendances - */ -function getTrendingVideos($count = TRENDING_VIDEOS_COUNT) { - // Récupérer les vidéos tendances - $data = callPeerTubeApi('videos', [ - 'sort' => '-trending', - 'count' => $count, - 'isLocal' => true - ]); - - return formatVideosData($data['data'] ?? []); -} - -/** - * Récupère les vidéos avec un tag spécifique depuis l'API PeerTube - * - * @param string $tag Tag à filtrer - * @param int $count Nombre de vidéos à récupérer - * @return array Liste des vidéos - */ -function getVideosByTag($tag, $count) { - // Récupérer les vidéos par tag - $data = callPeerTubeApi('videos', [ - 'tagsOneOf' => $tag, - 'count' => $count, - 'isLocal' => true - ]); - - return formatVideosData($data['data'] ?? []); -} - -/** - * Récupère les shorts (vidéos courtes) depuis l'API PeerTube - * Les shorts sont des vidéos locales de moins de 2 minutes - * - * @param int $count Nombre de shorts à récupérer - * @return array Liste des shorts - */ -function getShorts($count = SHORTS_COUNT) { - // Récupérer plus de vidéos que nécessaire pour pouvoir filtrer - $data = callPeerTubeApi('videos', [ - 'sort' => '-publishedAt', // Les plus récentes d'abord - 'count' => SHORTS_COUNT_SEARCH, - 'isLocal' => true - ]); - - // Formater les données - $allVideos = formatVideosData($data['data'] ?? []); - - // Filtrer pour ne garder que les vidéos de moins de 2 minutes (120 secondes) et en mode portrait - $shortVideos = array_filter($allVideos, function($video) { - // Vérifier la durée (moins de 2 minutes) - $durationOk = $video['duration'] < SHORTS_MAX_DURATION; - - // Vérifier le ratio (mode portrait) - $ratioOk = isset($video['aspectRatio']) && $video['aspectRatio'] <= 1; - - return $durationOk && $ratioOk; - }); - - // Limiter au nombre demandé - return array_slice($shortVideos, 0, $count); -} - -/** - * Récupère les vidéos sur l'indépendance depuis l'API PeerTube - * - * @param int $count Nombre de vidéos à récupérer - * @return array Liste des vidéos sur l'indépendance - */ -function getIndependenceVideos($count = INDEPENDENCE_VIDEOS_COUNT) { - // Récupérer les vidéos sur l'indépendance - return getVideosByTag(TAG_INDEPENDENCE, $count); -} - -/** - * Vérifie s'il y a un direct en cours du compte LIVE_ACCOUNT_NAME sur l'instance PeerTube - * - * @return array|null Informations sur le direct en cours ou null si aucun direct - */ -function getLiveStream() { - // Récupérer les lives du compte spécifié - $accountName = LIVE_ACCOUNT_NAME; - $data = callPeerTubeApi('accounts/' . $accountName . '/videos', [ - 'count' => 1, - 'isLocal' => true, - 'isLive' => true, // Filtrer uniquement les lives - 'sort' => '-publishedAt' // Les plus récents en premier - ]); - - // Vérifier si on a des résultats - if (empty($data['data']) || count($data['data']) === 0) { - return null; - } - - // Formater les données du live - $liveData = formatVideosData($data['data']); - - // Filtrer pour ne garder que les lives en cours - $activeLives = array_filter($liveData, function($video) { - return isset($video['isLive']) && $video['isLive'] === true; - }); - - // Retourner le premier live trouvé - return !empty($activeLives) ? reset($activeLives) : null; -} - -/** - * Formate les données brutes des vidéos venant de l'API - * - * @param array $videosData Données brutes des vidéos - * @return array Données formatées - */ -function formatVideosData($videosData) { - $videos = []; - - foreach ($videosData as $video) { - // Récupérer la vignette (thumbnail) - $thumbnail = isset($video['previewPath']) - ? PEERTUBE_URL . $video['previewPath'] - : 'img/default-thumbnail.jpg'; - - // Récupérer l'avatar de la chaîne - $channelAvatar = isset($video['channel']['avatars'][0]['path']) && isset($video['channel']['avatars'][0]['path']) - ? PEERTUBE_URL . $video['channel']['avatars'][0]['path'] - : 'img/default-avatar.png'; - - // Formater les données - $videos[] = [ - 'id' => $video['uuid'], - 'title' => $video['name'], - 'thumbnail' => $thumbnail, - 'duration' => $video['duration'], - 'channel' => $video['channel']['displayName'], - 'channelAvatar' => $channelAvatar, - 'views' => $video['views'], - 'date' => $video['publishedAt'], - 'aspectRatio' => $video['aspectRatio'], - 'description' => $video['description'] ?? '', - 'tags' => $video['tags'] ?? [], - 'isLive' => isset($video['isLive']) ? $video['isLive'] : false - ]; - } - - return $videos; -} - -// Fonctions utilitaires pour formater les données d'affichage -function formatDuration($seconds) { - $hours = floor($seconds / 3600); - $minutes = floor(($seconds % 3600) / 60); - $remainingSeconds = $seconds % 60; - - if ($hours > 0) { - return sprintf('%d:%02d:%02d', $hours, $minutes, $remainingSeconds); - } else { - return sprintf('%d:%02d', $minutes, $remainingSeconds); - } -} - -function formatViewCount($views) { - if ($views >= 1000000) { - return round($views / 1000000, 1) . 'M'; - } elseif ($views >= 1000) { - return round($views / 1000, 1) . 'K'; - } else { - return $views; - } -} - -function formatDate($dateString) { - $date = new DateTime($dateString); - $now = new DateTime(); - $interval = $now->diff($date); - - if ($interval->days == 0) { - return 'Aujourd\'hui'; - } elseif ($interval->days == 1) { - return 'Hier'; - } elseif ($interval->days < 7) { - return 'Il y a ' . $interval->days . ' jours'; - } elseif ($interval->days < 30) { - $weeks = floor($interval->days / 7); - return 'Il y a ' . $weeks . ' semaine' . ($weeks > 1 ? 's' : ''); - } elseif ($interval->days < 365) { - $months = floor($interval->days / 30); - return 'Il y a ' . $months . ' mois'; - } else { - $years = floor($interval->days / 365); - return 'Il y a ' . $years . ' an' . ($years > 1 ? 's' : ''); - } -} - -/** - * Récupère les vidéos d'une catégorie spécifique depuis l'API PeerTube - * - * @param int $categoryId Identifiant de la catégorie - * @param int $count Nombre de vidéos à récupérer - * @return array Liste des vidéos de la catégorie - */ -function getVideosByCategory($categoryId, $count = CATEGORY_VIDEOS_COUNT) { - // Récupérer les vidéos par catégorie - $data = callPeerTubeApi('videos', [ - 'categoryOneOf' => $categoryId, - 'count' => $count, - 'sort' => '-publishedAt', // Les plus récentes d'abord - 'isLocal' => true - ]); - - return formatVideosData($data['data'] ?? []); -} - -/** - * Récupère la liste des catégories à afficher (triées selon les priorités) - * - * @return array Liste des catégories avec id, name et videos - */ -function getDisplayCategories() { - $categories = []; - $priorityCategories = PRIORITY_CATEGORIES; - $allCategories = PEERTUBE_CATEGORIES; - - // Ajouter uniquement les catégories prioritaires dans l'ordre défini - foreach ($priorityCategories as $catId => $categoryName) { - $videos = getVideosByCategory($catId); - - // N'ajouter que les catégories qui ont des vidéos - if (!empty($videos)) { - $categories[] = [ - 'id' => $catId, - 'name' => $categoryName, - 'videos' => $videos - ]; - } - } - - return $categories; -} - -/** - * Récupère les commentaires d'une vidéo - * @param string $videoId ID de la vidéo - * @return array Tableau des commentaires - */ -function getVideoComments($videoId) { - $endpoint = "videos/{$videoId}/comment-threads"; - $response = callPeerTubeApi($endpoint); - - if (!$response || !isset($response['data'])) { - return []; - } - - return $response['data']; -} - -/** - * Récupère les options de téléchargement pour une vidéo - * @param string $videoId ID de la vidéo - * @return array Options de téléchargement - */ -function getVideoDownloadOptions($videoId) { - // Récupérer les informations complètes de la vidéo - $videoData = callPeerTubeApi('videos/' . $videoId); - - $downloadOptions = []; - - // Ajouter les fichiers directs s'ils existent - if (isset($videoData['files']) && !empty($videoData['files'])) { - foreach ($videoData['files'] as $file) { - if (isset($file['fileDownloadUrl']) && !empty($file['fileDownloadUrl'])) { - $downloadOptions[] = [ - 'type' => 'direct', - 'url' => PEERTUBE_URL . $file['fileDownloadUrl'], - 'resolution' => isset($file['resolution']['label']) ? $file['resolution']['label'] : 'Original', - 'size' => isset($file['size']) ? formatFileSize($file['size']) : 'Inconnu' - ]; - } - } - } - - // Ajouter les playlists de streaming s'ils existent - if (isset($videoData['streamingPlaylists']) && !empty($videoData['streamingPlaylists'])) { - foreach ($videoData['streamingPlaylists'] as $playlist) { - if (isset($playlist['files']) && !empty($playlist['files'])) { - foreach ($playlist['files'] as $file) { - if (isset($file['fileDownloadUrl']) && !empty($file['fileDownloadUrl'])) { - $downloadOptions[] = [ - 'type' => 'hls', - 'url' => $file['fileDownloadUrl'], - 'resolution' => isset($file['resolution']['label']) ? $file['resolution']['label'] : 'Original', - 'size' => isset($file['size']) ? formatFileSize($file['size']) : 'Inconnu' - ]; - } - } - } - } - } - - return $downloadOptions; -} - -/** - * Formate la taille d'un fichier en format lisible - * @param int $bytes Taille en octets - * @return string Taille formatée - */ -function formatFileSize($bytes) { - $units = ['B', 'KB', 'MB', 'GB', 'TB']; - - $bytes = max($bytes, 0); - $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); - $pow = min($pow, count($units) - 1); - - $bytes /= (1 << (10 * $pow)); - - return round($bytes, 2) . ' ' . $units[$pow]; -} - -/** - * Recherche des vidéos selon un critère - * - * @param string $query Terme de recherche - * @param int $count Nombre de vidéos à récupérer - * @param int $start Index de départ pour la pagination - * @return array Liste des vidéos correspondant à la recherche - */ -function searchVideos($query, $count = COUNT_VIDEO_SEARCH, $start = 0) { - if (empty($query)) { - return []; - } - - // Vérifier si la recherche concerne un tag (commence par #) - $isTagSearch = false; - if (substr($query, 0, 1) === '#') { - $isTagSearch = true; - $tag = substr($query, 1); // Enlever le # du début - - // Récupérer les vidéos avec ce tag via l'API - $data = callPeerTubeApi('videos', [ - 'tagsOneOf' => $tag, - 'count' => $count, - 'start' => $start, - 'isLocal' => true, // Uniquement les vidéos locales - 'sort' => '-publishedAt' // Les plus récentes d'abord - ]); - - return formatVideosData($data['data'] ?? []); - } - - // Recherche normale (pas un tag) - $data = callPeerTubeApi('search/videos', [ - 'search' => $query, - 'count' => $count, - 'start' => $start, - 'isLocal' => true, // Uniquement les vidéos locales - 'sort' => '-publishedAt' // Les plus récentes d'abord - ]); - return formatVideosData($data['data'] ?? []); -} - -/** - * Récupère les derniers épisodes d'un ou plusieurs podcasts Castopod via leurs feeds RSS - * - * @param string $castopodUrl URL de l'instance Castopod - * @param array|string $podcastSlugs Slug(s) du/des podcast(s) - tableau ou chaîne unique - * @param int $count Nombre d'épisodes à récupérer (total) - * @return array Liste des épisodes formatés, triés par date - */ -function getCastopodEpisodes($castopodUrl = null, $podcastSlugs = null, $count = 5) { - if (!defined('CASTOPOD_ENABLED') || !CASTOPOD_ENABLED) { - return []; - } - - $castopodUrl = $castopodUrl ?? CASTOPOD_URL; - $podcastSlugs = $podcastSlugs ?? (defined('CASTOPOD_PODCAST_SLUGS') ? CASTOPOD_PODCAST_SLUGS : ['annu_kute_cedric']); - $count = $count ?? CASTOPOD_EPISODES_COUNT; - - // Convertir en tableau si c'est une chaîne unique (rétrocompatibilité) - if (is_string($podcastSlugs)) { - $podcastSlugs = [$podcastSlugs]; - } - - // Clé de cache unique pour la combinaison de podcasts - $cacheKey = 'castopod_' . md5($castopodUrl . implode('_', $podcastSlugs) . '_' . $count); - - // Vérifier le cache - if (defined('CACHE_ENABLED') && CACHE_ENABLED) { - $cachedData = getFromCache($cacheKey); - if ($cachedData !== null) { - return $cachedData; - } - } - - $allEpisodes = []; - - // Itérer sur chaque podcast - foreach ($podcastSlugs as $index => $podcastSlug) { - try { - // Ajouter un délai entre les requêtes pour éviter le rate limiting (sauf pour la première) - // Note: Castopod a un rate limit strict, le cache est fortement recommandé - if ($index > 0) { - sleep(5); // 5 secondes de délai minimum pour Castopod - } - - // Construire l'URL du feed RSS pour ce podcast - $feedUrl = rtrim($castopodUrl, '/') . '/@' . $podcastSlug . '/feed'; - - // Récupérer le feed RSS - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $feedUrl); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - $xmlContent = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - if ($httpCode !== 200 || !$xmlContent) { - continue; // Passer au podcast suivant en cas d'erreur - } - - $episodes = []; - - // Vérifier si SimpleXML est disponible - if (function_exists('simplexml_load_string')) { - // Parser avec SimpleXML (méthode recommandée) - $xml = simplexml_load_string($xmlContent); - if (!$xml) { - continue; // Passer au podcast suivant - } - - // Enregistrer les namespaces - $namespaces = $xml->getNamespaces(true); - - // Extraire les informations du podcast - $podcastTitle = (string) $xml->channel->title; - $podcastImage = (string) $xml->channel->image->url; - $podcastLink = (string) $xml->channel->link; - - // Parcourir les items (épisodes) - on récupère tous pour trier après - foreach ($xml->channel->item as $item) { - - // Extraire les infos de l'épisode - $itunesNs = $item->children($namespaces['itunes'] ?? 'http://www.itunes.com/dtds/podcast-1.0.dtd'); - - // Récupérer l'image de l'épisode - $episodeImage = $podcastImage; - if (isset($itunesNs->image)) { - $imageAttrs = $itunesNs->image->attributes(); - if (isset($imageAttrs['href'])) { - $episodeImage = (string) $imageAttrs['href']; - } - } - - $episode = [ - 'title' => (string) $item->title, - 'link' => (string) $item->link, - 'pubDate' => (string) $item->pubDate, - 'description' => strip_tags((string) $item->description), - 'duration' => (string) $itunesNs->duration ?? '', - 'image' => $episodeImage, - 'audioUrl' => (string) $item->enclosure['url'] ?? '', - 'podcastTitle' => $podcastTitle, - 'podcastLink' => $podcastLink, - 'podcastSlug' => $podcastSlug, - ]; - - // Formater la date - if ($episode['pubDate']) { - try { - $date = new DateTime($episode['pubDate']); - $episode['formattedDate'] = $date->format('d/m/Y'); - $episode['timestamp'] = $date->getTimestamp(); - } catch (Exception $e) { - $episode['formattedDate'] = ''; - $episode['timestamp'] = 0; - } - } - - // Formater la durée - if ($episode['duration']) { - $episode['formattedDuration'] = formatDuration($episode['duration']); - } else { - $episode['formattedDuration'] = ''; - } - - // Limiter la description - if (strlen($episode['description']) > 150) { - $episode['description'] = substr($episode['description'], 0, 150) . '...'; - } - - $episodes[] = $episode; - } - } else { - // Fallback: Parser avec regex (moins fiable mais fonctionne sans SimpleXML) - // Extraire les infos du podcast - preg_match('/.*?(.*?)<\/title>/s', $xmlContent, $podcastTitleMatch); - preg_match('/<channel>.*?<link>(.*?)<\/link>/s', $xmlContent, $podcastLinkMatch); - preg_match('/<image>.*?<url>(.*?)<\/url>/s', $xmlContent, $podcastImageMatch); - - $podcastTitle = $podcastTitleMatch[1] ?? ''; - $podcastLink = $podcastLinkMatch[1] ?? ''; - $podcastImage = $podcastImageMatch[1] ?? ''; - - // Extraire tous les items - preg_match_all('/<item>(.*?)<\/item>/s', $xmlContent, $items); - - foreach ($items[1] as $itemContent) { - // Extraire les données de chaque item - preg_match('/<title>(.*?)<\/title>/', $itemContent, $titleMatch); - preg_match('/<link>(.*?)<\/link>/', $itemContent, $linkMatch); - preg_match('/<pubDate>(.*?)<\/pubDate>/', $itemContent, $dateMatch); - preg_match('/<description>(.*?)<\/description>/s', $itemContent, $descMatch); - preg_match('/<itunes:duration>(.*?)<\/itunes:duration>/', $itemContent, $durationMatch); - preg_match('/<itunes:image[^>]*href=["\']([^"\']*)["\']/', $itemContent, $imageMatch); - preg_match('/<enclosure[^>]*url=["\']([^"\']*)["\']/', $itemContent, $audioMatch); - - $episode = [ - 'title' => $titleMatch[1] ?? '', - 'link' => $linkMatch[1] ?? '', - 'pubDate' => $dateMatch[1] ?? '', - 'description' => strip_tags($descMatch[1] ?? ''), - 'duration' => $durationMatch[1] ?? '', - 'image' => $imageMatch[1] ?? $podcastImage, - 'audioUrl' => $audioMatch[1] ?? '', - 'podcastTitle' => $podcastTitle, - 'podcastLink' => $podcastLink, - 'podcastSlug' => $podcastSlug, - ]; - - // Formater la date - if ($episode['pubDate']) { - try { - $date = new DateTime($episode['pubDate']); - $episode['formattedDate'] = $date->format('d/m/Y'); - $episode['timestamp'] = $date->getTimestamp(); - } catch (Exception $e) { - $episode['formattedDate'] = ''; - $episode['timestamp'] = 0; - } - } - - // Formater la durée - if ($episode['duration']) { - $episode['formattedDuration'] = formatDuration($episode['duration']); - } else { - $episode['formattedDuration'] = ''; - } - - // Limiter la description - if (strlen($episode['description']) > 150) { - $episode['description'] = substr($episode['description'], 0, 150) . '...'; - } - - $episodes[] = $episode; - } - } - - // Ajouter les épisodes de ce podcast à la liste globale - $allEpisodes = array_merge($allEpisodes, $episodes); - - } catch (Exception $e) { - error_log('Erreur lors de la récupération des épisodes Castopod pour ' . $podcastSlug . ': ' . $e->getMessage()); - // Continuer avec le podcast suivant - } - } - - // Trier tous les épisodes par date (du plus récent au plus ancien) - usort($allEpisodes, function($a, $b) { - return ($b['timestamp'] ?? 0) - ($a['timestamp'] ?? 0); - }); - - // Limiter au nombre demandé - $allEpisodes = array_slice($allEpisodes, 0, $count); - - // Mettre en cache - if (defined('CACHE_ENABLED') && CACHE_ENABLED) { - saveToCache($cacheKey, $allEpisodes); - } - - return $allEpisodes; -} - -/** - * Récupère des morceaux aléatoires depuis Funkwhale via l'API - * - * @param string $funkwhaleUrl URL de l'instance Funkwhale - * @param int $count Nombre de morceaux à récupérer - * @return array Liste des morceaux formatés - */ -function getFunkwhaleTracks($funkwhaleUrl = null, $count = null) { - if (!defined('FUNKWHALE_ENABLED') || !FUNKWHALE_ENABLED) { - return []; - } - - $funkwhaleUrl = $funkwhaleUrl ?? FUNKWHALE_URL; - $count = $count ?? FUNKWHALE_TRACKS_COUNT; - - // Clé de cache - on récupère un grand nombre de morceaux pour le cache - $cacheKey = 'funkwhale_' . md5($funkwhaleUrl); - $cacheFetchSize = 50; // Nombre de morceaux à mettre en cache - - // Vérifier le cache - $allTracks = null; - if (defined('CACHE_ENABLED') && CACHE_ENABLED) { - $allTracks = getFromCache($cacheKey); - } - - // Si pas de cache, récupérer depuis l'API - if ($allTracks === null) { - try { - // Construire l'URL de l'API avec filtre pour morceaux locaux uniquement - // Le paramètre scope=me récupère uniquement les morceaux de l'instance locale - $apiUrl = rtrim($funkwhaleUrl, '/') . '/api/v1/tracks?ordering=random&page_size=' . $cacheFetchSize . '&scope=all'; - - // Récupérer les morceaux via cURL - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $apiUrl); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - $jsonContent = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - if ($httpCode !== 200 || !$jsonContent) { - return []; - } - - // Parser le JSON - $data = json_decode($jsonContent, true); - if (!$data || !isset($data['results'])) { - return []; - } - - $allTracks = []; - - // Extraire le domaine de l'instance locale pour filtrer - $localDomain = parse_url($funkwhaleUrl, PHP_URL_HOST); - - // Parcourir les morceaux et filtrer pour garder uniquement les morceaux locaux - foreach ($data['results'] as $item) { - // Vérifier si le morceau est local en comparant le domaine - // Les morceaux fédérés ont généralement un domaine différent dans leur URL - $isLocal = true; - - // Vérifier via l'URL de l'artiste ou de l'album - if (isset($item['artist']['channel'])) { - $artistDomain = parse_url($item['artist']['channel'], PHP_URL_HOST); - if ($artistDomain && $artistDomain !== $localDomain) { - $isLocal = false; - } - } - - // Ou vérifier via le champ is_local si disponible - if (isset($item['is_local']) && !$item['is_local']) { - $isLocal = false; - } - - // Ne garder que les morceaux locaux - if ($isLocal) { - // Récupérer l'URL d'écoute depuis l'API (peut être relative) - $listenUrl = $item['listen_url'] ?? ''; - - // Si l'URL est relative, la transformer en URL absolue - if (!empty($listenUrl) && strpos($listenUrl, 'http') !== 0) { - $listenUrl = rtrim($funkwhaleUrl, '/') . $listenUrl; - } - - $track = [ - 'title' => $item['title'] ?? '', - 'artist' => $item['artist']['name'] ?? 'Artiste inconnu', - 'album' => $item['album']['title'] ?? '', - 'cover' => $item['album']['cover']['urls']['medium_square_crop'] ?? '', - 'duration' => $item['uploads'][0]['duration'] ?? 0, - 'link' => rtrim($funkwhaleUrl, '/') . '/library/tracks/' . ($item['id'] ?? ''), - 'trackId' => $item['id'] ?? 0, - 'audioUrl' => $listenUrl, - ]; - - // Formater la durée - if ($track['duration']) { - $track['formattedDuration'] = formatDuration($track['duration']); - } else { - $track['formattedDuration'] = ''; - } - - $allTracks[] = $track; - } - } - - // Mettre en cache tous les morceaux locaux - if (defined('CACHE_ENABLED') && CACHE_ENABLED) { - saveToCache($cacheKey, $allTracks); - } - - } catch (Exception $e) { - error_log('Erreur lors de la récupération des morceaux Funkwhale: ' . $e->getMessage()); - return []; - } - } - - // Si pas assez de morceaux, retourner ce qu'on a - if (empty($allTracks)) { - return []; - } - - // Sélectionner aléatoirement $count morceaux parmi tous les morceaux en cache - $trackCount = count($allTracks); - if ($trackCount <= $count) { - return $allTracks; - } - - // Mélanger et prendre les N premiers - shuffle($allTracks); - return array_slice($allTracks, 0, $count); -} -?> diff --git a/includes/featured-videos.php b/includes/featured-videos.php deleted file mode 100644 index 8a6f62d..0000000 --- a/includes/featured-videos.php +++ /dev/null @@ -1,77 +0,0 @@ -<?php -// Inclure la configuration si ce n'est pas déjà fait -if (!function_exists('getTrendingVideos')) { - require_once __DIR__ . '/config.php'; -} - -// Récupérer les vidéos tendances depuis l'API PeerTube -$featuredVideos = getTrendingVideos(FEATURED_VIDEOS_COUNT); - -// Affichage des vidéos -foreach ($featuredVideos as $video): -?> - <div class="video-card" data-video-id="<?php echo $video['id']; ?>"> - <div class="video-thumbnail"> - <img src="<?php echo $video['thumbnail']; ?>" alt="<?php echo $video['title']; ?>" data-src="<?php echo $video['thumbnail']; ?>"> - <div class="video-duration"><?php echo formatDuration($video['duration']); ?></div> - </div> - <div class="video-info"> - <h3 class="video-title"><?php echo $video['title']; ?></h3> - <div class="video-channel"><?php echo $video['channel']; ?></div> - <div class="video-metadata"> - <?php if (defined('SHOW_VIDEO_VIEWS') && SHOW_VIDEO_VIEWS): ?> - <span class="video-views"><?php echo formatViewCount($video['views']); ?> vues</span> - <?php endif; ?> - <span class="video-date"><?php echo formatDate($video['date']); ?></span> - </div> - </div> - </div> -<?php endforeach; ?> - -<?php -// Fonctions utilitaires (dans un vrai projet, ces fonctions seraient dans un fichier séparé) -function formatDuration($seconds) { - $hours = floor($seconds / 3600); - $minutes = floor(($seconds % 3600) / 60); - $remainingSeconds = $seconds % 60; - - if ($hours > 0) { - return sprintf('%d:%02d:%02d', $hours, $minutes, $remainingSeconds); - } else { - return sprintf('%d:%02d', $minutes, $remainingSeconds); - } -} - -function formatViewCount($views) { - if ($views >= 1000000) { - return round($views / 1000000, 1) . 'M'; - } elseif ($views >= 1000) { - return round($views / 1000, 1) . 'K'; - } else { - return $views; - } -} - -function formatDate($dateString) { - $date = new DateTime($dateString); - $now = new DateTime(); - $interval = $now->diff($date); - - if ($interval->days == 0) { - return 'Aujourd\'hui'; - } elseif ($interval->days == 1) { - return 'Hier'; - } elseif ($interval->days < 7) { - return 'Il y a ' . $interval->days . ' jours'; - } elseif ($interval->days < 30) { - $weeks = floor($interval->days / 7); - return 'Il y a ' . $weeks . ' semaine' . ($weeks > 1 ? 's' : ''); - } elseif ($interval->days < 365) { - $months = floor($interval->days / 30); - return 'Il y a ' . $months . ' mois'; - } else { - $years = floor($interval->days / 365); - return 'Il y a ' . $years . ' an' . ($years > 1 ? 's' : ''); - } -} -?> \ No newline at end of file diff --git a/includes/footer.php b/includes/footer.php deleted file mode 100644 index 1a18bb7..0000000 --- a/includes/footer.php +++ /dev/null @@ -1,99 +0,0 @@ -<!-- Footer --> -<div class="footer"> - <div class="footer-header"> - <div class="footer-logo"> - <img src="img/logo.png" alt="<?php echo SITE_NAME; ?>"> - </div> - - <div class="footer-contact-info"> - <div class="footer-contact">CONTACT</div> - <div class="footer-email"><a href="mailto:<?php echo CONTACT_EMAIL; ?>"><?php echo CONTACT_EMAIL; ?></a></div> - </div> - </div> - - <div class="footer-columns"> - <div class="footer-column"> - <h3 class="footer-title">Catégories</h3> - <div> - <ul class="footer-links"> - <li><a href="index.php" <?php echo ($currentPage === 'index.php') ? 'class="active"' : ''; ?>>Accueil</a></li> - <?php if (!defined('HERO_TYPE') || HERO_TYPE === 'live'): ?> - <li><a href="direct.php" <?php echo ($currentPage === 'direct.php') ? 'class="active"' : ''; ?>>Direct</a></li> - <?php endif; ?> - - <?php - if (defined('PRIORITY_CATEGORIES') && !empty(PRIORITY_CATEGORIES)) { - foreach (PRIORITY_CATEGORIES as $id => $name) { - $isActive = ($currentPage === 'categories.php' && $currentCategoryId === $id); - echo '<li><a href="categories.php?id=' . $id . '"' . ($isActive ? ' class="active"' : '') . '>' . htmlspecialchars($name) . '</a></li>'; - } - } - ?> - </ul> - </div> - </div> - - <div class="footer-column"> - <h3 class="footer-title">Hashtags</h3> - <div> - <ul class="footer-links"> - <?php - if (defined('IMPORTANT_TAGS') && !empty(IMPORTANT_TAGS)): - foreach (IMPORTANT_TAGS as $tag): - $encodedTag = urlencode('#' . $tag); - $isActive = ($isTagSearch && strtolower($currentTag) === strtolower($tag)); - ?> - <li><a href="recherche.php?q=<?php echo $encodedTag; ?>" <?php echo $isActive ? 'class="active"' : ''; ?>><?php echo htmlspecialchars($tag); ?></a></li> - <?php - endforeach; - endif; - ?> - </ul> - </div> - </div> - - <div class="footer-column"> - <h3 class="footer-title">Informations légales</h3> - <div> - <ul class="footer-links"> - <li><a href="mentions-legales.php" <?php echo ($currentPage === 'mentions-legales.php') ? 'class="active"' : ''; ?>>Mentions légales</a></li> - <li> - <a href="<?php echo LEGAL_SOURCE_CODE_URL; ?>" target="_blank" rel="noopener noreferrer"> - <i class="fab fa-git-alt"></i> Code source - </a> - </li> - </ul> - </div> - </div> - </div> - - <div class="footer-social"> - <?php if (!empty(MASTODON_URL) && MASTODON_URL !== '#'): ?> - <a target="_blank" rel="me noreferrer" href="<?php echo MASTODON_URL; ?>"><i class="fab fa-mastodon icon-mastodon"></i></a> - <?php endif; ?> - - <?php if (!empty(FACEBOOK_URL) && FACEBOOK_URL !== '#'): ?> - <a target="_blank" rel="noreferrer" href="<?php echo FACEBOOK_URL; ?>"><i class="fab fa-facebook icon-facebook"></i></a> - <?php endif; ?> - - <?php if (!empty(YOUTUBE_URL) && YOUTUBE_URL !== '#'): ?> - <a target="_blank" rel="noreferrer" href="<?php echo YOUTUBE_URL; ?>"><i class="fab fa-youtube icon-youtube"></i></a> - <?php endif; ?> - - <?php if (!empty(INSTAGRAM_URL) && INSTAGRAM_URL !== '#'): ?> - <a target="_blank" rel="noreferrer" href="<?php echo INSTAGRAM_URL; ?>"><i class="fab fa-instagram icon-instagram"></i></a> - <?php endif; ?> - - <?php if (!empty(X_URL) && X_URL !== '#'): ?> - <a target="_blank" rel="noreferrer" href="<?php echo X_URL; ?>"><i class="fab fa-x-twitter icon-x"></i></a> - <?php endif; ?> - - <?php if (!empty(TIKTOK_URL) && TIKTOK_URL !== '#'): ?> - <a target="_blank" rel="noreferrer" href="<?php echo TIKTOK_URL; ?>"><i class="fab fa-tiktok icon-tiktok"></i></a> - <?php endif; ?> - </div> - - <div class="footer-copyright"> - <?php echo LEGAL_COPYRIGHT; ?> <?php echo date('Y'); ?> - Licence libre <a href="<?php echo LEGAL_LICENSE_URL; ?>" target="_blank" rel="noopener noreferrer">GNU AGPL-V3</a> - </div> -</div> diff --git a/includes/header.php b/includes/header.php deleted file mode 100644 index ff83205..0000000 --- a/includes/header.php +++ /dev/null @@ -1,84 +0,0 @@ -<!-- Header avec barre de recherche et icônes --> -<header class="header" role="banner"> - <div class="search-container"> - <form action="recherche.php" method="get" role="search" aria-label="Recherche de vidéos"> - <label for="search-input" class="sr-only">Rechercher des vidéos</label> - <input type="text" id="search-input" name="q" placeholder="Rechercher..." aria-describedby="search-help"> - <button type="submit" aria-label="Lancer la recherche"> - <i class="fas fa-search" aria-hidden="true"></i> - </button> - <div id="search-help" class="sr-only">Tapez vos mots-clés pour rechercher des vidéos</div> - </form> - </div> - - <nav class="social-icons" aria-label="Réseaux sociaux"> - <?php if (!empty(MASTODON_URL) && MASTODON_URL !== '#'): ?> - <a target="_blank" rel="me noreferrer" href="<?php echo MASTODON_URL; ?>" class="icon-button" aria-label="Suivre sur Mastodon"> - <i class="fab fa-mastodon icon-mastodon" aria-hidden="true"></i> - </a> - <?php endif; ?> - - <?php if (!empty(INSTAGRAM_URL) && INSTAGRAM_URL !== '#'): ?> - <a target="_blank" rel="noreferrer" href="<?php echo INSTAGRAM_URL; ?>" class="icon-button" aria-label="Suivre sur Instagram"> - <i class="fab fa-instagram icon-instagram" aria-hidden="true"></i> - </a> - <?php endif; ?> - - <?php if (!empty(TIKTOK_URL) && TIKTOK_URL !== '#'): ?> - <a target="_blank" rel="noreferrer" href="<?php echo TIKTOK_URL; ?>" class="icon-button" aria-label="Suivre sur TikTok"> - <i class="fab fa-tiktok icon-tiktok" aria-hidden="true"></i> - </a> - <?php endif; ?> - - <?php - // Vérifier s'il y a au moins un réseau social dans le menu déroulant - $hasDropdownSocial = (!empty(FACEBOOK_URL) && FACEBOOK_URL !== '#') || - (!empty(YOUTUBE_URL) && YOUTUBE_URL !== '#') || - (!empty(X_URL) && X_URL !== '#'); - - if ($hasDropdownSocial): - ?> - <div class="more-social-container"> - <button type="button" class="icon-button more-social-toggle" aria-expanded="false" aria-controls="social-dropdown" aria-label="Voir plus de réseaux sociaux"> - <i class="fas fa-ellipsis-h" aria-hidden="true"></i> - </button> - <div id="social-dropdown" class="more-social-dropdown" role="menu"> - <?php if (!empty(FACEBOOK_URL) && FACEBOOK_URL !== '#'): ?> - <a target="_blank" rel="noreferrer" href="<?php echo FACEBOOK_URL; ?>" class="more-social-item" role="menuitem"> - <i class="fab fa-facebook icon-facebook" aria-hidden="true"></i> Facebook - </a> - <?php endif; ?> - - <?php if (!empty(YOUTUBE_URL) && YOUTUBE_URL !== '#'): ?> - <a target="_blank" rel="noreferrer" href="<?php echo YOUTUBE_URL; ?>" class="more-social-item" role="menuitem"> - <i class="fab fa-youtube icon-youtube" aria-hidden="true"></i> YouTube - </a> - <?php endif; ?> - - <?php if (!empty(X_URL) && X_URL !== '#'): ?> - <a target="_blank" rel="noreferrer" href="<?php echo X_URL; ?>" class="more-social-item" role="menuitem"> - <i class="fab fa-x-twitter icon-x" aria-hidden="true"></i> X - </a> - <?php endif; ?> - </div> - </div> - <?php endif; ?> - </nav> - - <div class="action-icons"> - <?php if (defined('DONATIONS_ENABLED') && DONATIONS_ENABLED): ?> - <a href="dons.php" class="icon-button donation-link" aria-label="Soutenir <?php echo SITE_NAME; ?>" title="Faire un don"> - <i class="fas fa-heart" aria-hidden="true"></i> - </a> - <?php endif; ?> - <button id="theme-toggle" class="icon-button" aria-label="Basculer entre mode clair et sombre" title="Changer le thème"> - <i class="fas fa-sun" aria-hidden="true"></i> - </button> - <button id="install-pwa" class="icon-button install-pwa-button" style="display: none;" nonce="<?php echo getCspNonce(); ?>" aria-label="Installer l'application"> - <i class="fas fa-download" aria-hidden="true"></i> - </button> - <button class="mobile-menu-toggle" aria-expanded="false" aria-controls="mobile-menu" aria-label="Ouvrir le menu de navigation"> - <i class="fas fa-bars" aria-hidden="true"></i> - </button> - </div> -</header> diff --git a/includes/hero-section.php b/includes/hero-section.php deleted file mode 100644 index 92392a4..0000000 --- a/includes/hero-section.php +++ /dev/null @@ -1,291 +0,0 @@ -<?php -/** - * Section Hero - Affichage conditionnel (Direct / Playlist / Vidéo / Rien) - * - * Ce fichier gère l'affichage de la section hero en fonction de la configuration: - * - HERO_TYPE = 'live': Affiche le direct PeerTube - * - HERO_TYPE = 'video': Affiche une vidéo PeerTube unique - * - HERO_TYPE = 'playlist': Affiche une playlist (PeerTube/Funkwhale/Castopod) - * - HERO_TYPE = 'none': N'affiche rien - */ - -// Type de hero défini dans config -$heroType = defined('HERO_TYPE') ? HERO_TYPE : 'live'; - -// Si le type est 'none', ne rien afficher -if ($heroType === 'none') { - return; -} -?> - -<section class="hero" aria-labelledby="hero-section-title"> - <h2 id="hero-section-title" class="sr-only"> - <?php - if ($heroType === 'live') { - echo 'Diffusion en direct'; - } elseif ($heroType === 'video') { - echo 'Vidéo'; - } else { - echo 'Playlist'; - } - ?> - </h2> - - <?php if ($heroType === 'live'): ?> - <!-- Mode DIRECT --> - <?php - // Vérifier s'il y a un direct en cours - $liveStream = getLiveStream(); - - if ($liveStream) { - // Afficher le direct en cours - ?> - <div class="live-badge"> - <i class="fas fa-circle"></i> DIRECT - </div> - <div class="hero-video-container"> - <iframe - src="<?php echo PEERTUBE_URL; ?>/videos/embed/<?php echo $liveStream['id']; ?>?autoplay=1&muted=1" - frameborder="0" - allowfullscreen="allowfullscreen" - allow="autoplay; fullscreen" - title="Diffusion en direct: <?php echo htmlspecialchars($liveStream['title']); ?>" - aria-describedby="live-description"> - </iframe> - <div id="live-description" class="sr-only"> - Lecteur vidéo pour la diffusion en direct de <?php echo htmlspecialchars($liveStream['channel']); ?> - </div> - </div> - <div class="hero-video-info"> - <h2><?php echo htmlspecialchars($liveStream['title']); ?></h2> - <div class="hero-channel-info"> - <?php if (strpos($liveStream['channelAvatar'], 'default-avatar.png') !== false || empty($liveStream['channelAvatar'])): ?> - <div class="channel-avatar-placeholder" role="img" aria-label="Avatar par défaut"> - <i class="fas fa-user-circle" aria-hidden="true"></i> - </div> - <?php else: ?> - <img src="<?php echo $liveStream['channelAvatar']; ?>" alt="Avatar de la chaîne <?php echo htmlspecialchars($liveStream['channel']); ?>" class="channel-avatar"> - <?php endif; ?> - <span class="channel-name"><?php echo htmlspecialchars($liveStream['channel']); ?></span> - </div> - </div> - <?php - } else { - // Aucun direct en cours - vérifier s'il y a une annonce configurée - $showNextLiveAnnouncement = defined('NEXT_LIVE_ENABLED') && NEXT_LIVE_ENABLED === true; - - if ($showNextLiveAnnouncement) { - // Afficher l'annonce du prochain live - // Définir l'image de fond si disponible - $bgImageStyle = ''; - if (!empty(NEXT_LIVE_IMAGE) && file_exists(NEXT_LIVE_IMAGE)) { - $bgImageStyle = 'background-image: url(\'' . htmlspecialchars(NEXT_LIVE_IMAGE) . '\');'; - } - ?> - <div class="hero-next-live" style="<?php echo $bgImageStyle; ?>" nonce="<?php echo getCspNonce(); ?>"> - <?php if (!empty(NEXT_LIVE_IMAGE) && file_exists(NEXT_LIVE_IMAGE)): ?> - <div class="hero-next-live-image-container"> - <img src="<?php echo htmlspecialchars(NEXT_LIVE_IMAGE); ?>" - alt="<?php echo htmlspecialchars(NEXT_LIVE_TITLE); ?>" - class="hero-next-live-image"> - </div> - <?php endif; ?> - <div class="hero-next-live-content"> - <i class="fas fa-calendar-alt"></i> - <?php - if (!empty(NEXT_LIVE_DATE)) { - $liveDate = new DateTime(NEXT_LIVE_DATE, new DateTimeZone(DEFAULT_TIMEZONE)); - $dayFormatter = new IntlDateFormatter( - 'fr_FR', - IntlDateFormatter::FULL, - IntlDateFormatter::NONE, - DEFAULT_TIMEZONE, - IntlDateFormatter::GREGORIAN, - 'EEEE d MMMM' - ); - $formattedDay = $dayFormatter->format($liveDate); - $formattedDay = ucfirst($formattedDay); - $dynamicTitle = NEXT_LIVE_TITLE . ' - ' . $formattedDay; - } else { - $dynamicTitle = NEXT_LIVE_TITLE; - } - ?> - <h2><?php echo htmlspecialchars($dynamicTitle); ?></h2> - <?php - if (!empty(NEXT_LIVE_DATE)) { - $liveHour = $liveDate->format('H\hi'); - $dynamicDescription = 'Rejoignez-nous à ' . $liveHour . '. ' . NEXT_LIVE_DESCRIPTION; - } else { - $dynamicDescription = NEXT_LIVE_DESCRIPTION; - } - ?> - <p><?php echo nl2br(htmlspecialchars($dynamicDescription)); ?></p> - <?php if (!empty(NEXT_LIVE_DATE)): ?> - <div class="hero-next-live-datetime"> - <p class="hero-next-live-date"> - <i class="fas fa-clock"></i> - <?php - $formatter = new IntlDateFormatter( - 'fr_FR', - IntlDateFormatter::FULL, - IntlDateFormatter::SHORT, - DEFAULT_TIMEZONE - ); - echo $formatter->format($liveDate); - - $offset = $liveDate->format('P'); - echo ' <span class="utc-offset">(UTC' . $offset . ')</span>'; - ?> - </p> - - <!-- Autres fuseaux horaires --> - <div class="hero-next-live-timezones"> - <?php - // Ordre croissant : du plus en retard au plus en avance - $timezones = [ - 'Ma\'ohi Nui' => 'Pacific/Tahiti', - 'Martinique / Guadeloupe' => 'America/Martinique', - 'Guyane' => 'America/Cayenne', - 'France' => 'Europe/Paris', - 'Kanaky' => 'Pacific/Noumea' - ]; - - foreach($timezones as $name => $timezone): - $liveDateLocal = clone $liveDate; - $liveDateLocal->setTimezone(new DateTimeZone($timezone)); - $dayDiff = $liveDateLocal->format('j') - $liveDate->format('j'); - - $dayIndicator = ''; - if ($dayDiff > 0) { - $dayIndicator = ' <span class="day-shift">+1j</span>'; - } elseif ($dayDiff < 0) { - $dayIndicator = ' <span class="day-shift">-1j</span>'; - } - ?> - <span class="hero-timezone-item"> - <strong><?php echo $name; ?> :</strong> <?php echo $liveDateLocal->format('H\hi'); ?><?php echo $dayIndicator; ?> - </span> - <?php endforeach; ?> - </div> - </div> - <?php endif; ?> - </div> - </div> - <?php - } else { - ?> - <div class="hero-no-live"> - <i class="fas fa-tv"></i> - <h2>Aucun direct en cours</h2> - <p>Revenez plus tard pour découvrir nos prochaines diffusions en direct.</p> - </div> - <?php - } - } - ?> - - <?php elseif ($heroType === 'playlist'): ?> - <!-- Mode PLAYLIST --> - <?php - $platform = defined('PLAYLIST_PLATFORM') ? PLAYLIST_PLATFORM : 'peertube'; - $instanceUrl = defined('PLAYLIST_INSTANCE_URL') ? PLAYLIST_INSTANCE_URL : ''; - $playlistId = defined('PLAYLIST_ID') ? PLAYLIST_ID : ''; - $playlistTitle = defined('PLAYLIST_TITLE') ? PLAYLIST_TITLE : 'Playlist'; - $playlistDescription = defined('PLAYLIST_DESCRIPTION') ? PLAYLIST_DESCRIPTION : ''; - - if (!empty($instanceUrl) && !empty($playlistId)): - ?> - <div class="hero-playlist"> - <div class="hero-playlist-header"> - <i class="fas fa-list-music"></i> - <div class="hero-playlist-info"> - <h2><?php echo htmlspecialchars($playlistTitle); ?></h2> - <?php if (!empty($playlistDescription)): ?> - <p><?php echo htmlspecialchars($playlistDescription); ?></p> - <?php endif; ?> - </div> - </div> - - <div class="hero-playlist-container"> - <?php if ($platform === 'peertube'): ?> - <!-- Embed PeerTube Playlist --> - <iframe - src="<?php echo rtrim($instanceUrl, '/'); ?>/video-playlists/embed/<?php echo $playlistId; ?>" - frameborder="0" - allowfullscreen="allowfullscreen" - sandbox="allow-same-origin allow-scripts allow-popups allow-forms" - title="<?php echo htmlspecialchars($playlistTitle); ?>"> - </iframe> - - <?php elseif ($platform === 'funkwhale'): ?> - <!-- Embed Funkwhale Playlist --> - <iframe - src="<?php echo rtrim($instanceUrl, '/'); ?>/front/embed.html?&type=playlist&id=<?php echo $playlistId; ?>" - frameborder="0" - allowfullscreen="allowfullscreen" - title="<?php echo htmlspecialchars($playlistTitle); ?>"> - </iframe> - - <?php elseif ($platform === 'castopod'): ?> - <!-- Embed Castopod Podcast --> - <iframe - src="<?php echo rtrim($instanceUrl, '/'); ?>/<?php echo $playlistId; ?>/embed/dark" - frameborder="0" - allowfullscreen="allowfullscreen" - title="<?php echo htmlspecialchars($playlistTitle); ?>"> - </iframe> - - <?php endif; ?> - </div> - - <div class="hero-playlist-footer"> - <a href="<?php - if ($platform === 'peertube') { - echo rtrim($instanceUrl, '/') . '/w/p/' . $playlistId; - } elseif ($platform === 'funkwhale') { - echo rtrim($instanceUrl, '/') . '/library/playlists/' . $playlistId; - } elseif ($platform === 'castopod') { - echo rtrim($instanceUrl, '/') . '/' . $playlistId; - } - ?>" target="_blank" rel="noopener noreferrer" class="hero-playlist-link"> - <i class="fas fa-external-link-alt"></i> - Voir sur <?php echo ucfirst($platform); ?> - </a> - </div> - </div> - <?php else: ?> - <div class="hero-no-live"> - <i class="fas fa-list-music"></i> - <h2>Playlist non configurée</h2> - <p>Configurez PLAYLIST_INSTANCE_URL et PLAYLIST_ID dans votre fichier de configuration.</p> - </div> - <?php endif; ?> - - <?php elseif ($heroType === 'video'): ?> - <!-- Mode VIDÉO --> - <?php - $videoId = defined('HERO_VIDEO_ID') ? HERO_VIDEO_ID : ''; - $videoTitle = defined('HERO_VIDEO_TITLE') ? HERO_VIDEO_TITLE : 'Vidéo de présentation'; - - if (!empty($videoId)): - ?> - <div class="hero-video-container"> - <iframe - src="<?php echo PEERTUBE_URL; ?>/videos/embed/<?php echo $videoId; ?>" - frameborder="0" - allowfullscreen="allowfullscreen" - sandbox="allow-same-origin allow-scripts allow-popups" - allow="autoplay; fullscreen" - title="<?php echo htmlspecialchars($videoTitle); ?>"> - </iframe> - </div> - <?php else: ?> - <div class="hero-no-live"> - <i class="fas fa-video"></i> - <h2>Vidéo non configurée</h2> - <p>Configurez HERO_VIDEO_ID dans votre fichier de configuration.</p> - </div> - <?php endif; ?> - - <?php endif; ?> -</section> diff --git a/includes/lib/markdown.php b/includes/lib/markdown.php deleted file mode 100644 index c2968d7..0000000 --- a/includes/lib/markdown.php +++ /dev/null @@ -1,94 +0,0 @@ -<?php -/** - * Fonction simple pour convertir du texte Markdown en HTML - * Adaptée pour PeerTube qui ne prend en charge qu'un sous-ensemble limité d'éléments: - * - Emphase (italique/gras) - * - Liens - * - Retours à la ligne - * - Listes - * - * @param string $markdown Le texte au format Markdown à convertir - * @return string Le texte converti en HTML - */ -function markdown_to_html($markdown) { - // Échapper tout le contenu pour éviter les injections XSS - $markdown = htmlspecialchars($markdown, ENT_QUOTES, 'UTF-8'); - - // Tableau pour stocker les liens convertis - $links = []; - $link_count = 0; - - // Conversion des liens Markdown [texte](url) - $markdown = preg_replace_callback('/\[([^\]]+)\]\(([^)]+)\)/s', function($matches) use (&$links, &$link_count) { - $text = $matches[1]; - $url = $matches[2]; - - // Assurer que l'URL est correctement formée - if (!preg_match('/^https?:\/\//i', $url)) { - // Ajouter http:// si l'URL ne commence pas par http:// ou https:// - $url = 'http://' . $url; - } - - $placeholder = "___LINK_{$link_count}___"; - $links[$placeholder] = '<a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '" target="_blank" rel="noopener noreferrer">' . $text . '</a>'; - $link_count++; - - return $placeholder; - }, $markdown); - - // Détection et conversion des URLs brutes en liens cliquables - // 1. URLs avec protocole http/https - $protocolUrlPattern = '/(https?:\/\/[^\s<]+[^\s<\.)])/i'; - $markdown = preg_replace_callback($protocolUrlPattern, function($matches) use (&$links, &$link_count) { - $url = $matches[1]; - - $placeholder = "___LINK_{$link_count}___"; - $links[$placeholder] = '<a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '" target="_blank" rel="noopener noreferrer">' . $url . '</a>'; - $link_count++; - - return $placeholder; - }, $markdown); - - // 2. Domaines sans protocole (comme "o-k-i.net", "gong.gp", "NUVEL.NU") - // Détecte les domaines avec TLD communs qui ne font pas partie d'autre chose - $domainPattern = '/\b([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+([a-zA-Z]{2,63})\b/'; - $markdown = preg_replace_callback($domainPattern, function($matches) use (&$links, &$link_count) { - $domain = $matches[0]; - - // Éviter de convertir des éléments qui ressemblent à des versions/numéros ou qui sont déjà dans des liens - if (preg_match('/^v?\d+\.\d+/', $domain) || strpos($domain, '___LINK_') !== false) { - return $domain; - } - - $placeholder = "___LINK_{$link_count}___"; - $links[$placeholder] = '<a href="http://' . htmlspecialchars($domain, ENT_QUOTES, 'UTF-8') . '" target="_blank" rel="noopener noreferrer">' . $domain . '</a>'; - $link_count++; - - return $placeholder; - }, $markdown); - - // Conversion du texte en gras et italique (**texte** et *texte*) - $markdown = preg_replace('/\*\*(.*?)\*\*/s', '<strong>$1</strong>', $markdown); - $markdown = preg_replace('/\*(.*?)\*/s', '<em>$1</em>', $markdown); - - // Conversion des listes à puces - $markdown = preg_replace('/^- (.*?)$/m', '<li>$1</li>', $markdown); - $markdown = preg_replace('/(<li>.*?<\/li>\n?)+/s', '<ul>$0</ul>', $markdown); - - // Conversion des listes numérotées - $markdown = preg_replace('/^\d+\. (.*?)$/m', '<li>$1</li>', $markdown); - $markdown = preg_replace('/(<li>.*?<\/li>\n?)+/s', '<ol>$0</ol>', $markdown); - - // Gestion des retours à la ligne - $markdown = nl2br($markdown); - - // Nettoyage des balises br dans les listes - $markdown = preg_replace('/<\/li><br \/>/', '</li>', $markdown); - - // Restaurer les liens - foreach ($links as $placeholder => $link) { - $markdown = str_replace($placeholder, $link, $markdown); - } - - return $markdown; -} \ No newline at end of file diff --git a/includes/mobile-menu.php b/includes/mobile-menu.php deleted file mode 100644 index 79dadfc..0000000 --- a/includes/mobile-menu.php +++ /dev/null @@ -1,124 +0,0 @@ -<!-- Menu mobile (masqué par défaut) --> -<div class="mobile-menu"> - <button class="mobile-menu-close"> - <i class="fas fa-times"></i> - </button> - - <div class="search-container"> - <form action="recherche.php" method="get"> - <input type="text" name="q" placeholder="Rechercher..."> - <button type="submit"><i class="fas fa-search"></i></button> - </form> - </div> - - <div> - <a href="index.php" class="nav-item <?php echo ($currentPage === 'index.php') ? 'active' : ''; ?>"> - <i class="fas fa-home"></i> Accueil - </a> - - <?php if (!defined('HERO_TYPE') || HERO_TYPE === 'live'): ?> - <a href="direct.php" class="nav-item <?php echo ($currentPage === 'direct.php') ? 'active' : ''; ?>"> - <i class="fas fa-broadcast-tower"></i> Direct - </a> - <?php endif; ?> - - <hr class="nav-divider"> - - <?php - // Afficher les catégories prioritaires - if (defined('PRIORITY_CATEGORIES') && !empty(PRIORITY_CATEGORIES)) { - // Tableau associatif des icônes pour les catégories - $categoryIcons = [ - 1 => 'fas fa-music', // Musique - 2 => 'fas fa-film', // Films - 3 => 'fas fa-car', // Véhicules - 4 => 'fas fa-palette', // Jeux - 5 => 'fas fa-running', // Sport - 6 => 'fas fa-laugh', // Humour - 7 => 'fas fa-gamepad', // Art - 8 => 'fas fa-person', // Personnalités - 9 => 'fas fa-face-grin-tears', // Comédie - 10 => 'fas fa-tv', // Divertissement - 11 => 'fas fa-globe', // Actualité & Politique - 12 => 'fas fa-chalkboard-user', // Tutoriel - 13 => 'fas fa-graduation-cap', // Education - 14 => 'fas fa-fist-raised', // Activisme - 15 => 'fas fa-microscope', // Science & Technologie - 16 => 'fas fa-paw', // Animaux - 17 => 'fas fa-child', // Enfants - 18 => 'fas fa-utensils' // Cuisine - ]; - - foreach (PRIORITY_CATEGORIES as $id => $name) { - $isActive = ($currentPage === 'categories.php' && $currentCategoryId === $id); - $icon = isset($categoryIcons[$id]) ? $categoryIcons[$id] : 'fas fa-folder'; - echo '<a href="categories.php?id=' . $id . '" class="nav-item ' . ($isActive ? 'active' : '') . '">'; - echo '<i class="' . $icon . '"></i> ' . htmlspecialchars($name); - echo '</a>'; - } - } - ?> - - <hr class="nav-divider"> - </div> - - <div> - <h3 class="mobile-section-title">Hashtags populaires</h3> - <?php - if (defined('IMPORTANT_TAGS') && !empty(IMPORTANT_TAGS)): - foreach (IMPORTANT_TAGS as $tag): - $encodedTag = urlencode('#' . $tag); - $isActive = ($isTagSearch && strtolower($currentTag) === strtolower($tag)); - ?> - <a href="recherche.php?q=<?php echo $encodedTag; ?>" class="nav-item <?php echo $isActive ? 'active' : ''; ?>"> - <i class="fas fa-hashtag"></i> <?php echo htmlspecialchars($tag); ?> - </a> - <?php - endforeach; - endif; - ?> - </div> - - <hr class="nav-divider"> - - <?php - // Vérifier s'il y a au moins un réseau social à afficher - $hasSocialNetworks = (!empty(MASTODON_URL) && MASTODON_URL !== '#') || - (!empty(FACEBOOK_URL) && FACEBOOK_URL !== '#') || - (!empty(INSTAGRAM_URL) && INSTAGRAM_URL !== '#') || - (!empty(TIKTOK_URL) && TIKTOK_URL !== '#') || - (!empty(YOUTUBE_URL) && YOUTUBE_URL !== '#') || - (!empty(X_URL) && X_URL !== '#'); - - if ($hasSocialNetworks): - ?> - <div> - <h3 class="mobile-section-title">Suivez-nous</h3> - <div class="mobile-social-icons"> - <?php if (!empty(MASTODON_URL) && MASTODON_URL !== '#'): ?> - <a target="_blank" rel="me noreferrer" href="<?php echo MASTODON_URL; ?>"><i class="fab fa-mastodon icon-mastodon"></i></a> - <?php endif; ?> - - <?php if (!empty(FACEBOOK_URL) && FACEBOOK_URL !== '#'): ?> - <a target="_blank" rel="noreferrer" href="<?php echo FACEBOOK_URL; ?>"><i class="fab fa-facebook icon-facebook"></i></a> - <?php endif; ?> - - <?php if (!empty(INSTAGRAM_URL) && INSTAGRAM_URL !== '#'): ?> - <a target="_blank" rel="noreferrer" href="<?php echo INSTAGRAM_URL; ?>"><i class="fab fa-instagram icon-instagram"></i></a> - <?php endif; ?> - - <?php if (!empty(TIKTOK_URL) && TIKTOK_URL !== '#'): ?> - <a target="_blank" rel="noreferrer" href="<?php echo TIKTOK_URL; ?>"><i class="fab fa-tiktok icon-tiktok"></i></a> - <?php endif; ?> - - <?php if (!empty(YOUTUBE_URL) && YOUTUBE_URL !== '#'): ?> - <a target="_blank" rel="noreferrer" href="<?php echo YOUTUBE_URL; ?>"><i class="fab fa-youtube icon-youtube"></i></a> - <?php endif; ?> - - <?php if (!empty(X_URL) && X_URL !== '#'): ?> - <a target="_blank" rel="noreferrer" href="<?php echo X_URL; ?>"><i class="fab fa-x-twitter icon-x"></i></a> - <?php endif; ?> - </div> - </div> - <?php endif; ?> -</div> diff --git a/includes/pwa-init.php b/includes/pwa-init.php deleted file mode 100644 index 53b12a0..0000000 --- a/includes/pwa-init.php +++ /dev/null @@ -1,82 +0,0 @@ -<?php -// Fichier d'initialisation PWA à inclure dans toutes les pages -function addPWAHeaders() { - // Meta tags PWA - echo '<meta name="mobile-web-app-capable" content="yes">' . "\n"; - echo '<meta name="apple-mobile-web-app-capable" content="yes">' . "\n"; - echo '<meta name="apple-mobile-web-app-status-bar-style" content="default">' . "\n"; - echo '<meta name="apple-mobile-web-app-title" content="' . SITE_NAME . '">' . "\n"; - echo '<meta name="application-name" content="' . SITE_NAME . '">' . "\n"; - echo '<meta name="msapplication-TileColor" content="#FF0000">' . "\n"; - echo '<meta name="msapplication-config" content="browserconfig.xml">' . "\n"; - echo '<meta name="theme-color" content="#FF0000">' . "\n"; - - // Manifest - echo '<link rel="manifest" href="site.webmanifest">' . "\n"; -} - -function addPWAScripts() { - ?> - <!-- PWA Service Worker --> - <script nonce="<?php echo getCspNonce(); ?>"> - if ('serviceWorker' in navigator) { - window.addEventListener('load', function() { - navigator.serviceWorker.register('/sw.js') - .then(function(registration) { - console.log('Service Worker enregistré avec succès:', registration.scope); - - // Écouter les mises à jour - registration.addEventListener('updatefound', function() { - const newWorker = registration.installing; - newWorker.addEventListener('statechange', function() { - if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { - // Nouvelle version disponible - console.log('Nouvelle version disponible'); - if (confirm('Une nouvelle version est disponible. Voulez-vous recharger la page ?')) { - window.location.reload(); - } - } - }); - }); - }) - .catch(function(err) { - console.log('Échec de l\'enregistrement du Service Worker:', err); - }); - }); - } - - // Gestion de l'installation PWA - let deferredPrompt; - const installButton = document.getElementById('install-pwa'); - - window.addEventListener('beforeinstallprompt', function(e) { - e.preventDefault(); - deferredPrompt = e; - - // Afficher le bouton d'installation s'il existe - if (installButton) { - installButton.style.display = 'block'; - installButton.addEventListener('click', function() { - deferredPrompt.prompt(); - deferredPrompt.userChoice.then(function(choiceResult) { - if (choiceResult.outcome === 'accepted') { - console.log('PWA installée'); - } - deferredPrompt = null; - installButton.style.display = 'none'; - }); - }); - } - }); - - // Masquer le bouton après installation - window.addEventListener('appinstalled', function() { - console.log('PWA installée avec succès'); - if (installButton) { - installButton.style.display = 'none'; - } - }); - </script> - <?php -} -?> \ No newline at end of file diff --git a/includes/recent-videos.php b/includes/recent-videos.php deleted file mode 100644 index eeba88c..0000000 --- a/includes/recent-videos.php +++ /dev/null @@ -1,29 +0,0 @@ -<?php -// Inclure la configuration si ce n'est pas déjà fait -if (!function_exists('getRecentVideos')) { - require_once __DIR__ . '/config.php'; -} - -// Récupérer les vidéos récentes depuis l'API PeerTube -$recentVideos = getRecentVideos(); - -// Affichage des vidéos -foreach ($recentVideos as $video): -?> - <div class="video-card" data-video-id="<?php echo $video['id']; ?>"> - <div class="video-thumbnail"> - <img src="<?php echo $video['thumbnail']; ?>" alt="<?php echo $video['title']; ?>" data-src="<?php echo $video['thumbnail']; ?>"> - <div class="video-duration"><?php echo formatDuration($video['duration']); ?></div> - </div> - <div class="video-info"> - <h3 class="video-title"><?php echo $video['title']; ?></h3> - <div class="video-channel"><?php echo $video['channel']; ?></div> - <div class="video-metadata"> - <?php if (defined('SHOW_VIDEO_VIEWS') && SHOW_VIDEO_VIEWS): ?> - <span class="video-views"><?php echo formatViewCount($video['views']); ?> vues</span> - <?php endif; ?> - <span class="video-date"><?php echo formatDate($video['date']); ?></span> - </div> - </div> - </div> -<?php endforeach; ?> \ No newline at end of file diff --git a/includes/security.php b/includes/security.php deleted file mode 100644 index dd9e98b..0000000 --- a/includes/security.php +++ /dev/null @@ -1,327 +0,0 @@ -<?php -/** - * Fonctions de sécurité pour la validation et l'assainissement des entrées - */ - -/** - * Valide et assainit un ID de vidéo UUID - * - * @param string $id ID à valider - * @return string|false ID validé ou false si invalide - */ -function validateVideoId($id) { - if (empty($id)) { - return false; - } - - // Nettoyer l'entrée - $id = trim($id); - - // Vérifier le format UUID (format PeerTube) - if (!preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i', $id)) { - return false; - } - - return $id; -} - -/** - * Valide et assainit une requête de recherche - * - * @param string $query Requête à valider - * @return string|false Requête validée ou false si invalide - */ -function validateSearchQuery($query) { - if (empty($query)) { - return false; - } - - // Nettoyer l'entrée - $query = trim($query); - - // Limiter la longueur - if (strlen($query) > 200) { - return false; - } - - // Supprimer les caractères dangereux mais garder les caractères utiles pour la recherche - $query = preg_replace('/[<>"\']/', '', $query); - - return $query; -} - -/** - * Valide un numéro de page - * - * @param mixed $page Page à valider - * @return int Page validée (minimum 1) - */ -function validatePageNumber($page) { - $page = intval($page); - return max(1, $page); -} - -/** - * Valide un ID de catégorie - * - * @param mixed $categoryId ID de catégorie à valider - * @return int|false ID validé ou false si invalide - */ -function validateCategoryId($categoryId) { - $categoryId = intval($categoryId); - - // Les IDs de catégorie PeerTube sont entre 1 et 20 - if ($categoryId < 1 || $categoryId > 20) { - return false; - } - - return $categoryId; -} - -/** - * Valide et assainit un User-Agent - * - * @param string $userAgent User-Agent à valider - * @return bool True si valide - */ -function validateUserAgent($userAgent) { - if (empty($userAgent)) { - return false; - } - - // Bloquer les User-Agents suspects - $blockedPatterns = [ - '/curl/i', - '/wget/i', - '/python/i', - '/bot/i', - '/scanner/i', - '/sqlmap/i' - ]; - - foreach ($blockedPatterns as $pattern) { - if (preg_match($pattern, $userAgent)) { - return false; - } - } - - return true; -} - -/** - * Valide les en-têtes HTTP pour détecter les tentatives d'attaque - * - * @return bool True si les en-têtes sont sûrs - */ -function validateHttpHeaders() { - // Vérifier le User-Agent - $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; - if (!validateUserAgent($userAgent)) { - error_log('SECURITY: Suspicious User-Agent detected: ' . $userAgent); - return false; - } - - // Vérifier les en-têtes suspects - $suspiciousHeaders = [ - 'HTTP_X_FORWARDED_FOR', - 'HTTP_X_REAL_IP', - 'HTTP_CLIENT_IP' - ]; - - foreach ($suspiciousHeaders as $header) { - if (isset($_SERVER[$header])) { - $value = $_SERVER[$header]; - // Bloquer les IPs privées dans les en-têtes de forwarding - if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) { - error_log('SECURITY: Suspicious IP in header ' . $header . ': ' . $value); - } - } - } - - return true; -} - -/** - * Génère un token CSRF sécurisé - * - * @return string Token CSRF - */ -function generateCSRFToken() { - // Démarrer la session seulement si les en-têtes n'ont pas été envoyés - if (session_status() === PHP_SESSION_NONE && !headers_sent()) { - session_start(); - } - - if (!isset($_SESSION['csrf_token'])) { - $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); - } - - return $_SESSION['csrf_token']; -} - -/** - * Valide un token CSRF - * - * @param string $token Token à valider - * @return bool True si le token est valide - */ -function validateCSRFToken($token) { - if (session_status() === PHP_SESSION_NONE && !headers_sent()) { - session_start(); - } - - if (!isset($_SESSION['csrf_token'])) { - return false; - } - - return hash_equals($_SESSION['csrf_token'], $token); -} - -/** - * Génère ou récupère le nonce CSP de la requête courante. - * Doit être appelé après setSecurityHeaders() pour que le nonce soit envoyé dans le header CSP. - * - * @return string Nonce CSP - */ -function getCspNonce() { - if (!isset($GLOBALS['csp_nonce'])) { - $GLOBALS['csp_nonce'] = base64_encode(random_bytes(16)); - } - return $GLOBALS['csp_nonce']; -} - -/** - * Applique des en-têtes de sécurité HTTP - */ -function setSecurityHeaders() { - // Protection contre le clickjacking (permettre les iframes du même site) - header('X-Frame-Options: SAMEORIGIN'); - - // Protection contre le MIME sniffing - header('X-Content-Type-Options: nosniff'); - - // Protection XSS basique - header('X-XSS-Protection: 1; mode=block'); - - // Politique de référent - header('Referrer-Policy: strict-origin-when-cross-origin'); - - // Isolation cross-origin - header('Cross-Origin-Resource-Policy: same-origin'); - header('Cross-Origin-Opener-Policy: same-origin'); - // COEP retiré : require-corp bloque les iframes/images/vidéos cross-origin (PeerTube, Mastodon, Castopod, Funkwhale) - // car ces services ne renvoient pas le header CORP approprié. - - // Permissions Policy (feature policy) - header('Permissions-Policy: accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), encrypted-media=(), fullscreen=(self), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(self), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()'); - - // Content Security Policy avec support Mastodon et PeerTube - $nonce = getCspNonce(); - $mastodonDomain = ''; - $peertubeDomain = ''; - - // Extraire le domaine Mastodon si configuré - if (defined('MASTODON_INSTANCE_URL')) { - $mastodonParsed = parse_url(MASTODON_INSTANCE_URL); - if ($mastodonParsed && isset($mastodonParsed['host'])) { - $mastodonDomain = $mastodonParsed['scheme'] . '://' . $mastodonParsed['host']; - } - } - - // Extraire le domaine PeerTube si configuré - if (defined('PEERTUBE_URL')) { - $peertubeParsed = parse_url(PEERTUBE_URL); - if ($peertubeParsed && isset($peertubeParsed['host'])) { - $peertubeDomain = $peertubeParsed['scheme'] . '://' . $peertubeParsed['host']; - } - } - - // Détecter si on est en développement local - $isLocalDev = in_array($_SERVER['HTTP_HOST'] ?? '', ['127.0.0.1:8080', '127.0.0.1:8001', 'localhost:8080', 'localhost:8001', '127.0.0.1', 'localhost']); - - $csp = "default-src 'self'; "; - $csp .= "style-src 'self' 'nonce-{$nonce}' https://cdnjs.cloudflare.com; "; - $csp .= "script-src 'self' 'nonce-{$nonce}' https://cdnjs.cloudflare.com https://plausible.io; "; - - // Images : autoriser les domaines connus plus HTTPS général pour le contenu fédéré - $imgSrc = "'self' data: " . ($mastodonDomain ? $mastodonDomain : '') . " " . ($peertubeDomain ? $peertubeDomain : ''); - if ($isLocalDev) { - $imgSrc .= " https: http:"; - } else { - $imgSrc .= " https:"; - } - $csp .= "img-src " . $imgSrc . "; "; - - $csp .= "font-src 'self' https://cdnjs.cloudflare.com; "; - - // Frames : autoriser PeerTube uniquement - $frameSrc = "'self' " . ($peertubeDomain ? $peertubeDomain : ''); - if ($isLocalDev) { - $frameSrc .= " https: http:"; - } - $csp .= "frame-src " . $frameSrc . "; "; - - // Connexions : autoriser Mastodon, PeerTube et Plausible - $connectSrc = "'self' https://plausible.io " . ($mastodonDomain ? $mastodonDomain : '') . " " . ($peertubeDomain ? $peertubeDomain : ''); - if ($isLocalDev) { - $connectSrc .= " ws: wss:"; // WebSockets pour le dev - } - $csp .= "connect-src " . $connectSrc . "; "; - - // Médias : autoriser 'self', Mastodon, PeerTube et S3 Mastodon - $mediaSrc = "'self'"; - - if ($mastodonDomain) { - $mediaSrc .= " " . $mastodonDomain; - } - - if ($peertubeDomain) { - $mediaSrc .= " " . $peertubeDomain; - } - - // Ajouter l'URL S3 Mastodon si configurée (pour les médias externalisés) - if (defined('MASTODON_S3_MEDIA_URL') && !empty(MASTODON_S3_MEDIA_URL)) { - $s3Parsed = parse_url(MASTODON_S3_MEDIA_URL); - if ($s3Parsed && isset($s3Parsed['host'])) { - $s3Domain = $s3Parsed['scheme'] . '://' . $s3Parsed['host']; - $mediaSrc .= " " . $s3Domain; - } - } - - if ($isLocalDev) { - $mediaSrc .= " https: http:"; - } else { - $mediaSrc .= " https:"; - } - $csp .= "media-src " . $mediaSrc . "; "; - - $csp .= "object-src 'none'; "; - $csp .= "base-uri 'self'; "; - $csp .= "frame-ancestors 'self';"; - - header('Content-Security-Policy: ' . $csp); - - // HTTPS strict transport security (seulement si HTTPS) - if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') { - header('Strict-Transport-Security: max-age=63072000; includeSubDomains; preload'); - } -} - -/** - * Valide l'origine de la requête pour les requêtes AJAX - * - * @return bool True si l'origine est valide - */ -function validateAjaxOrigin() { - $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; - $host = $_SERVER['HTTP_HOST'] ?? ''; - - if (empty($origin) || empty($host)) { - return false; - } - - $expectedOrigin = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . $host; - - return $origin === $expectedOrigin; -} -?> \ No newline at end of file diff --git a/includes/sidebar.php b/includes/sidebar.php deleted file mode 100644 index 92cd4bf..0000000 --- a/includes/sidebar.php +++ /dev/null @@ -1,91 +0,0 @@ -<!-- Sidebar de navigation --> -<nav class="sidebar" role="navigation" aria-label="Navigation principale"> - <a href="/" class="logo" aria-label="Retour à l'accueil"> - <img src="img/logo.png" alt="Logo <?php echo SITE_NAME; ?>"> - </a> - - <?php - // Détecter la page courante et ses paramètres - $currentPage = basename($_SERVER['PHP_SELF']); - $currentCategoryId = isset($_GET['id']) ? intval($_GET['id']) : null; - $currentQuery = isset($_GET['q']) ? trim($_GET['q']) : ''; - $isTagSearch = !empty($currentQuery) && substr($currentQuery, 0, 1) === '#'; - $currentTag = $isTagSearch ? substr($currentQuery, 1) : ''; - ?> - - <div class="sidebar-nav"> - <a href="/" class="nav-item <?php echo ($currentPage === 'index.php') ? 'active' : ''; ?>" data-title="Accueil" aria-current="<?php echo ($currentPage === 'index.php') ? 'page' : 'false'; ?>"> - <i class="fas fa-home" aria-hidden="true"></i> <span>Accueil</span> - </a> - - <?php if (!defined('HERO_TYPE') || HERO_TYPE === 'live'): ?> - <a href="direct.php" class="nav-item <?php echo ($currentPage === 'direct.php') ? 'active' : ''; ?>" data-title="Direct" aria-current="<?php echo ($currentPage === 'direct.php') ? 'page' : 'false'; ?>"> - <i class="fas fa-broadcast-tower" aria-hidden="true"></i> <span>Direct</span> - </a> - <?php endif; ?> - - <?php if (defined('DONATIONS_ENABLED') && DONATIONS_ENABLED): ?> - <a href="dons.php" class="nav-item donation-nav-link <?php echo ($currentPage === 'dons.php') ? 'active' : ''; ?>" data-title="Soutenir" aria-current="<?php echo ($currentPage === 'dons.php') ? 'page' : 'false'; ?>"> - <i class="fas fa-heart" aria-hidden="true"></i> <span>Soutenir</span> - </a> - <?php endif; ?> - - <div class="nav-divider"></div> - - <?php - // Afficher les catégories prioritaires - if (defined('PRIORITY_CATEGORIES') && !empty(PRIORITY_CATEGORIES)) { - // Tableau associatif des icônes pour les catégories - $categoryIcons = [ - 1 => 'fas fa-music', // Musique - 2 => 'fas fa-film', // Films - 3 => 'fas fa-car', // Véhicules - 4 => 'fas fa-palette', // Jeux - 5 => 'fas fa-running', // Sport - 6 => 'fas fa-laugh', // Humour - 7 => 'fas fa-gamepad', // Art - 8 => 'fas fa-person', // Personnalités - 9 => 'fas fa-face-grin-tears', // Comédie - 10 => 'fas fa-tv', // Divertissement - 11 => 'fas fa-globe', // Actualité & Politique - 12 => 'fas fa-chalkboard-user', // Tutoriel - 13 => 'fas fa-graduation-cap', // Education - 14 => 'fas fa-fist-raised', // Activisme - 15 => 'fas fa-microscope', // Science & Technologie - 16 => 'fas fa-paw', // Animaux - 17 => 'fas fa-child', // Enfants - 18 => 'fas fa-utensils' // Cuisine - ]; - - foreach (PRIORITY_CATEGORIES as $id => $name) { - $isActive = ($currentPage === 'categories.php' && $currentCategoryId === $id); - $icon = isset($categoryIcons[$id]) ? $categoryIcons[$id] : 'fas fa-folder'; - echo '<a href="categories.php?id=' . $id . '" class="nav-item ' . ($isActive ? 'active' : '') . '" data-title="' . htmlspecialchars($name) . '" aria-current="' . ($isActive ? 'page' : 'false') . '">'; - echo '<i class="' . $icon . '" aria-hidden="true"></i> <span>' . htmlspecialchars($name) . '</span>'; - echo '</a>'; - } - } - ?> - - <div class="nav-divider"></div> - - <div class="category-title" role="heading" aria-level="2"> - <i class="fas fa-hashtag" aria-hidden="true"></i> <span>Hashtags</span> - </div> - - <?php - if (defined('IMPORTANT_TAGS') && !empty(IMPORTANT_TAGS)): - foreach (IMPORTANT_TAGS as $tag): - $encodedTag = urlencode('#' . $tag); - $isActive = ($isTagSearch && strtolower($currentTag) === strtolower($tag)); - ?> - <a href="recherche.php?q=<?php echo $encodedTag; ?>" class="tag-item <?php echo $isActive ? 'active' : ''; ?>" data-title="<?php echo htmlspecialchars($tag); ?>" aria-current="<?php echo $isActive ? 'page' : 'false'; ?>"> - <i class="fas fa-hashtag tag-icon" aria-hidden="true"></i> <span><?php echo htmlspecialchars($tag); ?></span> - </a> - <?php - endforeach; - endif; - ?> - </div> - -</nav> diff --git a/includes/simple-cache.php b/includes/simple-cache.php deleted file mode 100644 index 0112596..0000000 --- a/includes/simple-cache.php +++ /dev/null @@ -1,187 +0,0 @@ -<?php -/** - * Cache simple pour les API PeerTube - optimisation minimale - */ - -class SimpleAPICache { - private $cacheDir; - private $enabled; - - public function __construct() { - $this->cacheDir = __DIR__ . '/../cache/api'; - $this->enabled = true; - - // Créer le répertoire de cache s'il n'existe pas - if (!is_dir($this->cacheDir)) { - mkdir($this->cacheDir, 0755, true); - } - } - - /** - * Génère une clé de cache - */ - private function getCacheKey($endpoint, $params = []) { - $key = $endpoint; - if (!empty($params)) { - ksort($params); - $key .= '_' . md5(serialize($params)); - } - return 'cache_' . md5($key) . '.json'; - } - - /** - * Récupère depuis le cache - */ - public function get($endpoint, $params = []) { - if (!$this->enabled) return null; - - $file = $this->cacheDir . '/' . $this->getCacheKey($endpoint, $params); - - if (!file_exists($file)) { - return null; - } - - $data = json_decode(file_get_contents($file), true); - - // Vérifier l'expiration - if (isset($data['expires']) && time() > $data['expires']) { - unlink($file); - return null; - } - - return $data['content'] ?? null; - } - - /** - * Stocke dans le cache - */ - public function set($endpoint, $params, $content, $ttl = 300) { - if (!$this->enabled) return; - - $file = $this->cacheDir . '/' . $this->getCacheKey($endpoint, $params); - - $data = [ - 'content' => $content, - 'expires' => time() + $ttl, - 'created' => time() - ]; - - file_put_contents($file, json_encode($data)); - } - - /** - * Nettoie le cache expiré - */ - public function cleanup() { - $files = glob($this->cacheDir . '/cache_*.json'); - $cleaned = 0; - - foreach ($files as $file) { - $data = json_decode(file_get_contents($file), true); - if (isset($data['expires']) && time() > $data['expires']) { - unlink($file); - $cleaned++; - } - } - - return $cleaned; - } -} - -// Instance globale -$GLOBALS['simple_api_cache'] = new SimpleAPICache(); - -/** - * Version cachée de callPeerTubeApi - remplace l'originale - */ -function callPeerTubeApiCached($endpoint, $params = []) { - $cache = $GLOBALS['simple_api_cache']; - - // TTL selon le type de contenu - $ttlMap = [ - 'videos/categories' => 3600, // 1 heure - 'videos' => 600, // 10 minutes (augmenté) - 'search/videos' => 600, // 10 minutes - 'wp-posts' => 900, // 15 minutes pour WordPress - 'accounts' => 300 // 5 minutes pour live streams - ]; - - // TTL dynamique selon l'endpoint - $ttl = 300; // Défaut 5 minutes - foreach ($ttlMap as $pattern => $time) { - if (strpos($endpoint, $pattern) !== false) { - $ttl = $time; - break; - } - } - - // Essayer le cache d'abord - $cached = $cache->get($endpoint, $params); - if ($cached !== null) { - return $cached; - } - - // Appeler l'API originale - $data = callPeerTubeApiOriginal($endpoint, $params); - - // Mettre en cache si on a des données - if (!empty($data)) { - $cache->set($endpoint, $params, $data, $ttl); - } - - return $data; -} - -/** - * Cache générique pour tout type d'API - */ -function callApiCached($cacheKey, $callable, $ttl = 900) { - $cache = $GLOBALS['simple_api_cache']; - - // Essayer le cache d'abord - $cached = $cache->get($cacheKey, []); - if ($cached !== null) { - return $cached; - } - - // Appeler la fonction fournie - try { - $data = call_user_func($callable); - } catch (Exception $e) { - error_log('API call failed for ' . $cacheKey . ': ' . $e->getMessage()); - return []; - } - - // Mettre en cache si on a des données - if (!empty($data)) { - $cache->set($cacheKey, [], $data, $ttl); - } - - return $data; -} - -/** - * Helpers simples pour le cache générique - */ -function getFromCache($key) { - if (!defined('CACHE_ENABLED') || !CACHE_ENABLED) { - return null; - } - $cache = $GLOBALS['simple_api_cache']; - return $cache->get($key, []); -} - -function saveToCache($key, $data, $ttl = null) { - if (!defined('CACHE_ENABLED') || !CACHE_ENABLED) { - return; - } - $ttl = $ttl ?? (defined('CACHE_DURATION') ? CACHE_DURATION : 3600); - $cache = $GLOBALS['simple_api_cache']; - $cache->set($key, [], $data, $ttl); -} - -// Nettoyage automatique occasionnel -if (rand(1, 100) === 1) { - $GLOBALS['simple_api_cache']->cleanup(); -} -?> \ No newline at end of file diff --git a/includes/structured-data.php b/includes/structured-data.php deleted file mode 100644 index 14c45a2..0000000 --- a/includes/structured-data.php +++ /dev/null @@ -1,320 +0,0 @@ -<?php - -/** - * Fonctions pour générer des données structurées JSON-LD - * pour améliorer le SEO et l'affichage dans les moteurs de recherche - */ - -/** - * Génère le JSON-LD pour un objet WebSite - * - * @return string JSON-LD pour le site web - */ -function generateWebSiteJsonLd() { - $baseUrl = getBaseUrl(); - - $data = [ - "@context" => "https://schema.org", - "@type" => "WebSite", - "name" => SITE_NAME, - "description" => SITE_DESCRIPTION, - "url" => $baseUrl, - "potentialAction" => [ - "@type" => "SearchAction", - "target" => [ - "@type" => "EntryPoint", - "urlTemplate" => $baseUrl . "/recherche.php?q={search_term_string}" - ], - "query-input" => "required name=search_term_string" - ], - "publisher" => [ - "@type" => "Organization", - "name" => "OKI", - "url" => $baseUrl, - "logo" => [ - "@type" => "ImageObject", - "url" => $baseUrl . "/img/logo.png" - ] - ] - ]; - - return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); -} - -/** - * Génère le JSON-LD pour un objet VideoObject - * - * @param array $videoData Données de la vidéo depuis l'API PeerTube - * @param array $video Données formatées de la vidéo - * @return string JSON-LD pour la vidéo - */ -function generateVideoObjectJsonLd($videoData, $video) { - $baseUrl = getBaseUrl(); - $videoUrl = $baseUrl . "/video.php?id=" . $video['id']; - - // Construire l'URL de la vignette - $thumbnailUrl = isset($videoData['thumbnailPath']) - ? PEERTUBE_URL . $videoData['thumbnailPath'] - : $baseUrl . "/img/default-thumbnail.jpg"; - - // Formater la durée en format ISO 8601 (PT1H30M pour 1h30min) - $duration = formatDurationISO8601($video['duration'] ?? 0); - - // Construire les données de base - $data = [ - "@context" => "https://schema.org", - "@type" => "VideoObject", - "name" => $video['title'], - "description" => !empty($video['description']) - ? truncateText(strip_tags($video['description']), 300) - : "Regardez cette vidéo sur " . SITE_NAME, - "url" => $videoUrl, - "thumbnailUrl" => $thumbnailUrl, - "uploadDate" => formatDateISO8601($video['date']), - "duration" => $duration, - "publisher" => [ - "@type" => "Organization", - "name" => SITE_NAME, - "url" => $baseUrl, - "logo" => [ - "@type" => "ImageObject", - "url" => $baseUrl . "/img/logo.png" - ] - ] - ]; - - // Ajouter les informations de la chaîne/créateur - if (!empty($video['channel'])) { - $data["creator"] = [ - "@type" => "Person", - "name" => $video['channel'], - "url" => PEERTUBE_URL . "/c/" . ($videoData['channel']['name'] ?? '') - ]; - } - - // Ajouter les statistiques d'interaction - if (isset($video['views']) && $video['views'] > 0) { - $data["interactionStatistic"] = [ - "@type" => "InteractionCounter", - "interactionType" => [ - "@type" => "WatchAction" - ], - "userInteractionCount" => $video['views'] - ]; - } - - // Ajouter les likes si disponibles - if (isset($video['likes']) && $video['likes'] > 0) { - if (!isset($data["interactionStatistic"])) { - $data["interactionStatistic"] = []; - } else { - // Convertir en array si c'était un seul élément - if (isset($data["interactionStatistic"]["@type"])) { - $data["interactionStatistic"] = [$data["interactionStatistic"]]; - } - } - - $data["interactionStatistic"][] = [ - "@type" => "InteractionCounter", - "interactionType" => [ - "@type" => "LikeAction" - ], - "userInteractionCount" => $video['likes'] - ]; - } - - // Ajouter les tags/mots-clés si disponibles - if (!empty($video['tags'])) { - $data["keywords"] = implode(", ", $video['tags']); - } - - // Ajouter les informations de licence si disponibles - if (isset($videoData['licence']) && !empty($videoData['licence'])) { - $licenceId = $videoData['licence']['id']; - $licenceMapping = [ - 1 => "https://creativecommons.org/licenses/by/4.0/", - 2 => "https://creativecommons.org/licenses/by-sa/4.0/", - 3 => "https://creativecommons.org/licenses/by-nd/4.0/", - 4 => "https://creativecommons.org/licenses/by-nc/4.0/", - 5 => "https://creativecommons.org/licenses/by-nc-sa/4.0/", - 6 => "https://creativecommons.org/licenses/by-nc-nd/4.0/", - 7 => "https://creativecommons.org/publicdomain/zero/1.0/" - ]; - - if (isset($licenceMapping[$licenceId])) { - $data["license"] = $licenceMapping[$licenceId]; - } - } - - // Ajouter la catégorie si disponible - if (isset($videoData['category']) && !empty($videoData['category'])) { - $data["genre"] = $videoData['category']['label'] ?? 'Vidéo'; - } - - // Ajouter les dimensions si c'est un short (format portrait) - if (isset($video['aspectRatio']) && $video['aspectRatio'] <= 1) { - $data["videoFrameSize"] = "Portrait"; - } - - return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); -} - -/** - * Génère le JSON-LD pour les fils d'Ariane (BreadcrumbList) - * - * @param array $breadcrumbs Tableau des fils d'Ariane [['name' => 'Nom', 'url' => 'URL']] - * @return string JSON-LD pour les fils d'Ariane - */ -function generateBreadcrumbJsonLd($breadcrumbs) { - $baseUrl = getBaseUrl(); - - $listItems = []; - foreach ($breadcrumbs as $index => $crumb) { - $listItems[] = [ - "@type" => "ListItem", - "position" => $index + 1, - "name" => $crumb['name'], - "item" => $crumb['url'] - ]; - } - - $data = [ - "@context" => "https://schema.org", - "@type" => "BreadcrumbList", - "itemListElement" => $listItems - ]; - - return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); -} - -/** - * Génère le JSON-LD pour une page de collection de vidéos - * - * @param string $name Nom de la collection - * @param string $description Description de la collection - * @param array $videos Tableau des vidéos - * @param string $url URL de la page de collection - * @return string JSON-LD pour la collection - */ -function generateVideoCollectionJsonLd($name, $description, $videos, $url) { - $baseUrl = getBaseUrl(); - - $videoItems = []; - foreach ($videos as $video) { - $videoItems[] = [ - "@type" => "VideoObject", - "name" => $video['title'], - "url" => $baseUrl . "/video.php?id=" . $video['id'], - "thumbnailUrl" => $video['thumbnail'], - "uploadDate" => formatDateISO8601($video['date']), - "duration" => formatDurationISO8601($video['duration'] ?? 0) - ]; - } - - $data = [ - "@context" => "https://schema.org", - "@type" => "CollectionPage", - "name" => $name, - "description" => $description, - "url" => $url, - "mainEntity" => [ - "@type" => "ItemList", - "itemListElement" => $videoItems, - "numberOfItems" => count($videos) - ], - "publisher" => [ - "@type" => "Organization", - "name" => SITE_NAME, - "url" => $baseUrl - ] - ]; - - return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); -} - -/** - * Formate une durée en secondes au format ISO 8601 (PTnHnMnS) - * - * @param int $seconds Durée en secondes - * @return string Durée au format ISO 8601 - */ -function formatDurationISO8601($seconds) { - $hours = floor($seconds / 3600); - $minutes = floor(($seconds % 3600) / 60); - $remainingSeconds = $seconds % 60; - - $duration = 'PT'; - - if ($hours > 0) { - $duration .= $hours . 'H'; - } - if ($minutes > 0) { - $duration .= $minutes . 'M'; - } - if ($remainingSeconds > 0 || ($hours === 0 && $minutes === 0)) { - $duration .= $remainingSeconds . 'S'; - } - - return $duration; -} - -/** - * Formate une date au format ISO 8601 - * - * @param string $dateString Date à formater - * @return string Date au format ISO 8601 - */ -function formatDateISO8601($dateString) { - try { - $date = new DateTime($dateString); - return $date->format('c'); // Format ISO 8601 - } catch (Exception $e) { - // En cas d'erreur, retourner la date actuelle - return (new DateTime())->format('c'); - } -} - -/** - * Tronque un texte à une longueur donnée - * - * @param string $text Texte à tronquer - * @param int $length Longueur maximale - * @return string Texte tronqué - */ -function truncateText($text, $length = 200) { - if (strlen($text) <= $length) { - return $text; - } - - $truncated = substr($text, 0, $length); - $lastSpace = strrpos($truncated, ' '); - - if ($lastSpace !== false) { - $truncated = substr($truncated, 0, $lastSpace); - } - - return $truncated . '...'; -} - -/** - * Obtient l'URL de base du site - * - * @return string URL de base - */ -function getBaseUrl() { - $scheme = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https' : 'http'; - $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; - return $scheme . '://' . $host; -} - -/** - * Génère et affiche un script JSON-LD - * - * @param string $jsonLd Données JSON-LD - */ -function outputJsonLd($jsonLd) { - $nonce = getCspNonce(); - echo '<script type="application/ld+json" nonce="' . $nonce . '">' . "\n" . $jsonLd . "\n" . '</script>' . "\n"; -} - -?> diff --git a/includes/wordpress.php b/includes/wordpress.php deleted file mode 100644 index 5c7c374..0000000 --- a/includes/wordpress.php +++ /dev/null @@ -1,202 +0,0 @@ -<?php -/** - * Fonctions pour l'intégration WordPress via l'API REST - */ - -/** - * Appelle l'API WordPress REST pour récupérer les articles - * - * @param string $endpoint Point de terminaison de l'API (ex: 'posts') - * @param array $params Paramètres optionnels pour la requête - * @return array Données retournées par l'API - */ -function callWordPressApi($endpoint, $params = []) { - // Vérifier que WordPress est activé et configuré - if (!defined('WORDPRESS_ENABLED') || !WORDPRESS_ENABLED || empty(WORDPRESS_URL)) { - return []; - } - - // Validation de l'URL WordPress pour prévenir SSRF - if (!isValidWordPressUrl(WORDPRESS_URL)) { - error_log('SECURITY: Invalid WordPress URL detected: ' . WORDPRESS_URL); - return []; - } - - // Construire l'URL de l'API - $url = WORDPRESS_URL . '/wp-json/wp/v2/' . ltrim($endpoint, '/'); - - // Ajouter les paramètres à l'URL - if (!empty($params)) { - $url .= '?' . http_build_query($params); - } - - // Initialiser cURL avec options de sécurité - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 30); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); - curl_setopt($ch, CURLOPT_MAXREDIRS, 0); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); - curl_setopt($ch, CURLOPT_USERAGENT, 'fediverse-oki-WordPress-Integration/1.0'); - - // Exécuter la requête - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); - $error = curl_error($ch); - curl_close($ch); - - // Traiter la réponse - if ($response === false || !empty($error)) { - error_log('WordPress API error: ' . $error); - return []; - } - - if ($httpCode < 200 || $httpCode >= 300) { - error_log('WordPress API HTTP error: ' . $httpCode . ' for URL: ' . $url); - return []; - } - - // Validation du Content-Type - if (!str_contains($contentType, 'application/json')) { - error_log('WordPress API error: Invalid content type: ' . $contentType . ' for URL: ' . $url); - return []; - } - - // Décoder la réponse JSON - $data = json_decode($response, true); - if (json_last_error() !== JSON_ERROR_NONE) { - error_log('WordPress API JSON decode error: ' . json_last_error_msg() . ' for URL: ' . $url); - return []; - } - - return $data ?: []; -} - -/** - * Valide l'URL WordPress pour prévenir les attaques SSRF - * - * @param string $url URL à valider - * @return bool True si l'URL est valide et sûre - */ -function isValidWordPressUrl($url) { - // Vérifier que l'URL est bien formée - $parsed = parse_url($url); - if (!$parsed || !isset($parsed['scheme']) || !isset($parsed['host'])) { - return false; - } - - // Autoriser uniquement HTTPS (ou HTTP en développement) - if (!in_array($parsed['scheme'], ['https', 'http'])) { - return false; - } - - // Bloquer les adresses IP privées et locales - $host = $parsed['host']; - if (filter_var($host, FILTER_VALIDATE_IP)) { - if (!filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { - return false; - } - } - - // Bloquer localhost et autres domaines dangereux - $blockedHosts = ['localhost', '127.0.0.1', '::1', '0.0.0.0', 'metadata.google.internal']; - if (in_array(strtolower($host), $blockedHosts)) { - return false; - } - - return true; -} - -/** - * Récupère les articles WordPress récents - * - * @param int $count Nombre d'articles à récupérer - * @return array Liste des articles formatés - */ -function getWordPressPosts($count = null) { - if ($count === null) { - $count = defined('WORDPRESS_POSTS_COUNT') ? WORDPRESS_POSTS_COUNT : 6; - } - - // Utiliser le cache pour les posts WordPress - return callApiCached('wp-posts-' . $count, function() use ($count) { - // Récupérer les posts via l'API WordPress REST - $posts = callWordPressApi('posts', [ - 'per_page' => $count, - '_embed' => 'true' // Inclure les médias embarqués (featured image) - ]); - - return formatWordPressPosts($posts); - }, 900); // 15 minutes de cache -} - -/** - * Formate les données des articles WordPress - * - * @param array $posts Données brutes des articles - * @return array Articles formatés - */ -function formatWordPressPosts($posts) { - if (empty($posts)) { - return []; - } - - $formattedPosts = []; - - foreach ($posts as $post) { - // Récupérer l'image mise en avant - $featuredImage = ''; - if (isset($post['_embedded']['wp:featuredmedia'][0]['source_url'])) { - $featuredImage = $post['_embedded']['wp:featuredmedia'][0]['source_url']; - } - - // Nettoyer l'extrait HTML - $excerpt = ''; - if (isset($post['excerpt']['rendered'])) { - $excerpt = wp_strip_all_tags($post['excerpt']['rendered']); - $excerpt = trim($excerpt); - // Limiter à 150 caractères - if (strlen($excerpt) > 150) { - $excerpt = substr($excerpt, 0, 147) . '...'; - } - } - - // Formater la date - $date = ''; - if (isset($post['date'])) { - $dateObj = new DateTime($post['date']); - $date = $dateObj->format('d/m/Y'); - } - - $formattedPosts[] = [ - 'id' => $post['id'], - 'title' => html_entity_decode($post['title']['rendered'], ENT_QUOTES, 'UTF-8'), - 'excerpt' => $excerpt, - 'featured_image' => $featuredImage, - 'link' => $post['link'], - 'date' => $date, - 'author' => isset($post['_embedded']['author'][0]['name']) ? $post['_embedded']['author'][0]['name'] : '' - ]; - } - - return $formattedPosts; -} - -/** - * Fonction utilitaire pour nettoyer le HTML (similaire à wp_strip_all_tags de WordPress) - * - * @param string $string Chaîne à nettoyer - * @return string Chaîne nettoyée - */ -function wp_strip_all_tags($string) { - $string = preg_replace('@<(script|style)[^>]*?>.*?</\\1>@si', '', $string); - $string = strip_tags($string); - $string = preg_replace('/\s+/', ' ', $string); - return trim($string); -} - -?> diff --git a/index.php b/index.php deleted file mode 100644 index f37e4ec..0000000 --- a/index.php +++ /dev/null @@ -1,747 +0,0 @@ -<?php -// Inclure la configuration -require_once 'includes/config.php'; - -// Vérifier si le mode compte à rebours est activé -if (defined('COUNTDOWN_ENABLED') && COUNTDOWN_ENABLED === true) { - // Rediriger vers la page de compte à rebours - require_once 'countdown.php'; - exit; -} - -// Inclure les fonctions de données structurées -require_once 'includes/structured-data.php'; -// Appliquer les en-têtes de sécurité -setSecurityHeaders(); -?> -<!DOCTYPE html> -<html lang="fr"> -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <meta name="csrf-token" content="<?php echo generateCSRFToken(); ?>"> - <title><?php echo SITE_NAME; ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'Accueil', 'url' => getBaseUrl()] - ]; - $breadcrumbJsonLd = generateBreadcrumbJsonLd($breadcrumbs); - outputJsonLd($breadcrumbJsonLd); - ?> - - - - - - - - - - - - - - - -
- - -
-
- - - - - -
-
-
- - Podcasts - - - - - -
-
- Aucun épisode disponible
'; - } else { - foreach ($castopodEpisodes as $index => $episode): - ?> -
- - - -
-

- -

- - -
- -
- - -
- - - - - -
-
- - - - - - - -
- -
-
-
- - - - -
-
-
- - Morceaux - - - - - -
-
- Aucun morceau disponible
'; - } else { - foreach ($funkwhaleTracks as $index => $track): - ?> -
- - - -
-

- -

- -
- -
- -
- - - - - -
-
- - - - - - - -
- -
-
-
- - - -
-
-
-

Fil d'actualités

-
-
-
-
-
-
- - - -
-
-
- - Articles - - - - -
-
- Aucun article disponible
'; - } else { - foreach ($wordpressPosts as $post): - ?> - - -
- - <?php echo htmlspecialchars($post['title']); ?> - - - -
- - -
- -
-
- - - - - - - -
- - # - -
- - -
- - - -
-
- -

Shorts

-
- - - - 1): ?> - - - -
- - -
- - -
-
- -

Dernières vidéos

-
- -
- Aucune vidéo disponible pour le moment
'; - } else { - foreach ($recentVideos as $video): - ?> -
-
- Miniature de la vidéo: <?php echo htmlspecialchars($video['title']); ?> - -
- -
-
-
-

-
- -
- -
- - <?php echo $video['channel']; ?> - - -
- -
-
- - - - -
- - -
- - -
-
- - -
- -
- Aucune vidéo disponible pour le moment
'; - } else { - foreach ($trendingVideos as $video): - ?> -
-
- <?php echo $video['title']; ?> -
- -
-
-
-
-

-
- -
- -
- - <?php echo $video['channel']; ?> - - -
- -
-
- - - - -
- - -
- - - - -
-
- -

-
- -
- -
-
- <?php echo $video['title']; ?> -
- -
-
-
-
-

-
- -
- -
- - <?php echo $video['channel']; ?> - - -
- -
-
- -
- - -
- - -
- - - - -
- - -
-

- -

- - -

- - - -
- <?php echo defined('MOVEMENT_IMAGE_ALT') ? MOVEMENT_IMAGE_ALT : ''; ?> - -
- -
- -
- -
- - - - -
-
- - - - - - - - - - - - - - - - diff --git a/js/audio-player.js b/js/audio-player.js deleted file mode 100644 index 86a4493..0000000 --- a/js/audio-player.js +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Lecteur audio intégré universel pour Funkwhale et Castopod - * Permet de lire les morceaux/épisodes directement dans la page - */ - -(function() { - 'use strict'; - - let currentlyPlayingItem = null; - let currentAudioElement = null; - - /** - * Arrête l'élément en cours de lecture - */ - function stopCurrentItem() { - if (currentAudioElement) { - currentAudioElement.pause(); - currentAudioElement.currentTime = 0; - } - if (currentlyPlayingItem) { - currentlyPlayingItem.classList.remove('playing'); - currentlyPlayingItem = null; - } - } - - /** - * Démarre la lecture d'un élément - */ - function playItem(item, audioElement) { - // Arrêter l'élément actuel s'il y en a un - if (currentlyPlayingItem && currentlyPlayingItem !== item) { - stopCurrentItem(); - } - - currentlyPlayingItem = item; - currentAudioElement = audioElement; - - // Marquer comme en cours de lecture - item.classList.add('playing'); - - // Démarrer la lecture - audioElement.play().catch(function(error) { - console.error('Erreur lors de la lecture:', error); - stopCurrentItem(); - }); - } - - /** - * Met en pause l'élément - */ - function pauseItem(item, audioElement) { - audioElement.pause(); - item.classList.remove('playing'); - } - - /** - * Initialise les contrôles du lecteur pour un type d'élément - */ - function initPlayerForType(itemSelector, buttonSelector) { - const items = document.querySelectorAll(itemSelector); - - items.forEach(function(item) { - const playButton = item.querySelector(buttonSelector); - const audioElement = item.querySelector('audio'); - - if (!playButton || !audioElement) { - return; - } - - // Gestion du clic sur le bouton play/pause - playButton.addEventListener('click', function(e) { - e.preventDefault(); - e.stopPropagation(); - - if (item.classList.contains('playing')) { - pauseItem(item, audioElement); - } else { - playItem(item, audioElement); - } - }); - - // Gérer la fin de la lecture - audioElement.addEventListener('ended', function() { - item.classList.remove('playing'); - - // Passer automatiquement à l'élément suivant - const nextItem = item.nextElementSibling; - if (nextItem && (nextItem.classList.contains('funkwhale-track-item') || - nextItem.classList.contains('castopod-episode-item'))) { - const nextAudio = nextItem.querySelector('audio'); - if (nextAudio) { - playItem(nextItem, nextAudio); - } - } else { - // Réinitialiser si c'était le dernier élément - currentlyPlayingItem = null; - currentAudioElement = null; - } - }); - - // Gérer les erreurs de chargement - audioElement.addEventListener('error', function(e) { - console.error('Erreur de chargement audio:', e); - item.classList.remove('playing'); - - // Afficher un message d'erreur temporaire - const titleSelector = item.classList.contains('funkwhale-track-item') ? - '.funkwhale-track-title' : '.castopod-episode-title'; - const title = item.querySelector(titleSelector); - - if (title) { - const originalText = title.textContent; - title.textContent = 'Erreur de lecture'; - title.style.color = '#ff0000'; - - setTimeout(function() { - title.textContent = originalText; - title.style.color = ''; - }, 3000); - } - }); - }); - } - - /** - * Initialise tous les lecteurs - */ - function initPlayer() { - // Initialiser le lecteur Funkwhale - initPlayerForType('.funkwhale-track-item', '.funkwhale-play-btn'); - - // Initialiser le lecteur Castopod - initPlayerForType('.castopod-episode-item', '.castopod-play-btn'); - } - - // Initialiser le lecteur quand le DOM est prêt - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initPlayer); - } else { - initPlayer(); - } - - // Nettoyer à la fermeture de la page - window.addEventListener('beforeunload', function() { - stopCurrentItem(); - }); -})(); diff --git a/js/categories.js b/js/categories.js deleted file mode 100644 index eeff00d..0000000 --- a/js/categories.js +++ /dev/null @@ -1,97 +0,0 @@ -document.addEventListener("DOMContentLoaded", () => { - // Gestion des clics sur les vidéos - const videoCards = document.querySelectorAll(".video-card"); - - for (const videoCard of videoCards) { - videoCard.addEventListener("click", function () { - const videoId = this.dataset.videoId; - if (videoId) { - window.location.href = `video.php?id=${videoId}`; - } - }); - } - // Gestion du bouton "Voir plus" - const viewMoreBtn = document.querySelector(".view-more"); - if (viewMoreBtn) { - viewMoreBtn.addEventListener("click", function () { - const page = Number.parseInt(this.dataset.page); - const categoryId = - document.querySelector(".video-section").dataset.categoryId; - - // Changer le texte du bouton pendant le chargement - this.textContent = "Chargement..."; - this.disabled = true; - - // Préparer les données avec token CSRF - const formData = new FormData(); - formData.append('csrf_token', document.querySelector('meta[name="csrf-token"]').getAttribute('content')); - - // Faire la requête AJAX - fetch( - `ajax/load-more-videos.php?type=category&page=${page}&category=${categoryId}`, - { - method: 'POST', - headers: { - "X-Requested-With": "XMLHttpRequest", - }, - body: formData - } - ) - .then((response) => response.json()) - .then((data) => { - if (data.success) { - // Ajouter les nouvelles vidéos à la grille - const videoGrid = document.querySelector(".video-grid"); - const tempDiv = document.createElement("div"); - tempDiv.innerHTML = data.html; - - // Ajouter chaque vidéo à la grille - while (tempDiv.firstChild) { - videoGrid.appendChild(tempDiv.firstChild); - } - - // Mettre à jour le numéro de page - this.dataset.page = data.page + 1; - - // Réinitialiser le texte du bouton - this.textContent = "Voir plus"; - this.disabled = false; - - // Si plus de vidéos à charger, masquer le bouton - if (!data.hasMore) { - this.style.display = "none"; - } - - // Initialiser les clics sur les nouvelles vidéos - const cards = document.querySelectorAll(".video-card:not([data-click-initialized])") - - for (const card of cards) { - card.setAttribute("data-click-initialized", "true"); - card.addEventListener("click", function () { - const videoId = this.dataset.videoId; - if (videoId) { - window.location.href = `video.php?id=${videoId}`; - } - }); - - } - } else { - // Gérer l'erreur - this.textContent = "Erreur lors du chargement"; - setTimeout(() => { - this.textContent = "Voir plus"; - this.disabled = false; - }, 2000); - } - }) - .catch((error) => { - console.error("Erreur:", error); - this.textContent = "Erreur lors du chargement"; - setTimeout(() => { - this.textContent = "Voir plus"; - this.disabled = false; - }, 2000); - }); - }); - } -}); diff --git a/js/countdown.js b/js/countdown.js deleted file mode 100644 index b2d8862..0000000 --- a/js/countdown.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Script de compte à rebours pour la page de maintenance - */ - -class CountdownTimer { - constructor(targetDate) { - this.targetDate = new Date(targetDate).getTime(); - this.elements = { - days: document.getElementById('countdown-days'), - hours: document.getElementById('countdown-hours'), - minutes: document.getElementById('countdown-minutes'), - seconds: document.getElementById('countdown-seconds') - }; - - this.start(); - } - - start() { - // Mise à jour immédiate - this.updateTimer(); - - // Mise à jour chaque seconde - this.interval = setInterval(() => { - this.updateTimer(); - }, 1000); - } - - updateTimer() { - const now = new Date().getTime(); - const distance = this.targetDate - now; - - if (distance < 0) { - // Le compte à rebours est terminé - this.stop(); - this.onComplete(); - return; - } - - const days = Math.floor(distance / (1000 * 60 * 60 * 24)); - const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); - const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((distance % (1000 * 60)) / 1000); - - // Mise à jour des éléments du DOM - if (this.elements.days) this.elements.days.textContent = this.formatNumber(days); - if (this.elements.hours) this.elements.hours.textContent = this.formatNumber(hours); - if (this.elements.minutes) this.elements.minutes.textContent = this.formatNumber(minutes); - if (this.elements.seconds) this.elements.seconds.textContent = this.formatNumber(seconds); - } - - formatNumber(num) { - return num.toString().padStart(2, '0'); - } - - stop() { - if (this.interval) { - clearInterval(this.interval); - } - } - - onComplete() { - // Rediriger vers la page principale quand le compte à rebours est terminé - window.location.href = '/'; - } -} - -// Animation des éléments au chargement -document.addEventListener('DOMContentLoaded', function() { - // Initialiser le compte à rebours si les éléments existent - if (document.getElementById('countdown-days')) { - // La date cible est fournie par PHP - if (typeof COUNTDOWN_TARGET_DATE !== 'undefined') { - new CountdownTimer(COUNTDOWN_TARGET_DATE); - } - } - - // Animation d'apparition progressive - const elements = document.querySelectorAll('.countdown-container > *'); - elements.forEach((element, index) => { - element.style.opacity = '0'; - element.style.transform = 'translateY(30px)'; - - setTimeout(() => { - element.style.transition = 'opacity 0.8s ease, transform 0.8s ease'; - element.style.opacity = '1'; - element.style.transform = 'translateY(0)'; - }, 100 * (index + 1)); - }); -}); - -// Gérer la fermeture propre du timer -window.addEventListener('beforeunload', function() { - // Arrêter le timer si il existe - if (window.countdownTimer) { - window.countdownTimer.stop(); - } -}); diff --git a/js/main.js b/js/main.js deleted file mode 100644 index 069bac3..0000000 --- a/js/main.js +++ /dev/null @@ -1,529 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - // Détection de l'état de connexion - function updateOnlineStatus() { - const offlineIndicator = document.querySelector('.offline-indicator'); - - if (!navigator.onLine) { - if (!offlineIndicator) { - const indicator = document.createElement('div'); - indicator.className = 'offline-indicator show'; - indicator.textContent = 'Mode hors ligne - Fonctionnalités limitées'; - document.body.insertBefore(indicator, document.body.firstChild); - } - } else { - if (offlineIndicator) { - offlineIndicator.remove(); - } - } - } - - // Écouter les changements de connexion - window.addEventListener('online', updateOnlineStatus); - window.addEventListener('offline', updateOnlineStatus); - - // Vérifier l'état initial - updateOnlineStatus(); - // Gestion du menu mobile - const mobileMenuToggle = document.querySelector('.mobile-menu-toggle'); - const mobileMenu = document.querySelector('.mobile-menu'); - const mobileMenuClose = document.querySelector('.mobile-menu-close'); - - if (mobileMenuToggle) { - mobileMenuToggle.addEventListener('click', function() { - mobileMenu.classList.add('active'); - document.body.style.overflow = 'hidden'; // Empêche le défilement de la page - }); - } - - if (mobileMenuClose) { - mobileMenuClose.addEventListener('click', function() { - mobileMenu.classList.remove('active'); - document.body.style.overflow = ''; // Réactive le défilement - }); - } - - // Gestion du theme (mode clair/sombre) - const themeToggle = document.querySelector('#theme-toggle'); - const html = document.documentElement; - - // Charger le thème sauvegardé ou détecter la préférence système - function initTheme() { - const savedTheme = localStorage.getItem('theme'); - const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - - let currentTheme = savedTheme; - if (!savedTheme) { - currentTheme = systemPrefersDark ? 'dark' : 'light'; - } - - applyTheme(currentTheme); - updateThemeIcon(currentTheme); - } - - // Appliquer le thème - function applyTheme(theme) { - if (theme === 'dark') { - html.setAttribute('data-theme', 'dark'); - } else { - html.removeAttribute('data-theme'); - } - } - - // Mettre à jour l'icône du bouton - function updateThemeIcon(theme) { - if (themeToggle) { - const icon = themeToggle.querySelector('i'); - if (theme === 'dark') { - icon.classList.remove('fa-sun'); - icon.classList.add('fa-moon'); - themeToggle.setAttribute('aria-label', 'Basculer vers le mode clair'); - themeToggle.setAttribute('title', 'Mode clair'); - } else { - icon.classList.remove('fa-moon'); - icon.classList.add('fa-sun'); - themeToggle.setAttribute('aria-label', 'Basculer vers le mode sombre'); - themeToggle.setAttribute('title', 'Mode sombre'); - } - } - } - - // Basculer le thème - function toggleTheme() { - const currentTheme = html.getAttribute('data-theme'); - const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; - - applyTheme(newTheme); - updateThemeIcon(newTheme); - localStorage.setItem('theme', newTheme); - } - - // Event listener pour le bouton - if (themeToggle) { - themeToggle.addEventListener('click', toggleTheme); - } - - // Écouter les changements de préférence système - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) { - if (!localStorage.getItem('theme')) { - const newTheme = e.matches ? 'dark' : 'light'; - applyTheme(newTheme); - updateThemeIcon(newTheme); - } - }); - - // Initialiser le thème - initTheme(); - - // Gestion des carousels - const carousels = document.querySelectorAll('.carousel'); - - carousels.forEach(carousel => { - const container = carousel.querySelector('.carousel-container'); - const dots = carousel.querySelectorAll('.carousel-dot'); - const items = carousel.querySelectorAll('.carousel-item'); - - if (dots.length > 0 && items.length > 0) { - let currentIndex = 0; - let startX = 0; - let startY = 0; - let currentX = 0; - let isDragging = false; - let startTransform = 0; - let isTap = true; // Nouveau: détecter si c'est un tap simple - let touchStartTime = 0; - let touchTarget = null; // Stocker la cible du touch - - function getItemWidth() { - return items[0].offsetWidth + parseInt(getComputedStyle(items[0]).marginRight); - } - - function updateCarousel(index) { - const itemWidth = getItemWidth(); - const newTransform = -index * itemWidth; - container.style.transform = `translateX(${newTransform}px)`; - - // Mettre à jour les dots - dots.forEach(d => d.classList.remove('active')); - if (dots[index]) { - dots[index].classList.add('active'); - } - - currentIndex = index; - } - - dots.forEach((dot, index) => { - dot.addEventListener('click', function() { - updateCarousel(index); - }); - }); - - container.addEventListener('touchstart', function(e) { - isDragging = true; - isTap = true; - touchStartTime = Date.now(); - touchTarget = e.target; // Capturer la cible du touch - startX = e.touches[0].clientX; - startY = e.touches[0].clientY; - currentX = startX; // IMPORTANT: initialiser currentX à startX pour éviter les faux swipes - const transform = window.getComputedStyle(container).transform; - - if (transform !== 'none') { - const matrix = new DOMMatrix(transform); - startTransform = matrix.m41; - } else { - startTransform = 0; - } - container.style.transition = 'none'; - }, { passive: true }); - - container.addEventListener('touchmove', function(e) { - if (!isDragging) return; - - currentX = e.touches[0].clientX; - const currentY = e.touches[0].clientY; - const diffX = Math.abs(currentX - startX); - const diffY = Math.abs(currentY - startY); - - // Si le mouvement dépasse 10px, ce n'est plus un tap - if (diffX > 10 || diffY > 10) { - isTap = false; - } - - const diff = currentX - startX; - const newTransform = startTransform + diff; - - container.style.transform = `translateX(${newTransform}px)`; - }, { passive: true }); - - container.addEventListener('touchend', function(e) { - if (!isDragging) return; - isDragging = false; - - container.style.transition = 'transform 0.5s ease'; - - const diff = currentX - startX; - const threshold = 50; // Distance minimale pour considérer un swipe - const tapThreshold = 10; // Distance maximale pour un tap - const tapTimeThreshold = 300; // Temps maximal pour un tap (ms) - const touchDuration = Date.now() - touchStartTime; - - // Si c'est un tap (mouvement minimal et rapide) - if (isTap && Math.abs(diff) < tapThreshold && touchDuration < tapTimeThreshold) { - // Vérifier si le tap était sur une video-card - let videoCard = touchTarget; - - // Remonter le DOM pour trouver la video-card - while (videoCard && !videoCard.classList.contains('video-card')) { - videoCard = videoCard.parentElement; - // Arrêter si on sort du container - if (videoCard === container || !videoCard) break; - } - - // Si on a trouvé une video-card, naviguer vers la vidéo - if (videoCard && videoCard.classList.contains('video-card')) { - const videoId = videoCard.dataset.videoId; - if (videoId) { - window.location.href = 'video.php?id=' + videoId; - return; - } - } - - // Sinon, juste repositionner le carousel - updateCarousel(currentIndex); - currentX = 0; - isTap = true; // Réinitialiser pour le prochain tap - return; - } - - // Gérer comme un swipe normal - if (Math.abs(diff) > threshold) { - if (diff > 0 && currentIndex > 0) { - // Swipe vers la droite - item précédent - updateCarousel(currentIndex - 1); - } else if (diff < 0 && currentIndex < items.length - 1) { - // Swipe vers la gauche - item suivant - updateCarousel(currentIndex + 1); - } else { - // Retour à la position actuelle - updateCarousel(currentIndex); - } - } else { - // Mouvement trop petit, retour à la position actuelle - updateCarousel(currentIndex); - } - - currentX = 0; - isTap = true; // Réinitialiser pour le prochain tap - }, { passive: true }); - - // Support du drag sur desktop (optionnel) - container.addEventListener('mousedown', function(e) { - isDragging = true; - isTap = true; - touchStartTime = Date.now(); - touchTarget = e.target; // Capturer la cible - startX = e.clientX; - startY = e.clientY; - currentX = startX; // IMPORTANT: initialiser currentX à startX pour éviter les faux swipes - const transform = window.getComputedStyle(container).transform; - if (transform !== 'none') { - const matrix = new DOMMatrix(transform); - startTransform = matrix.m41; - } else { - startTransform = 0; - } - container.style.transition = 'none'; - container.style.cursor = 'grabbing'; - e.preventDefault(); - }); - - document.addEventListener('mousemove', function(e) { - if (!isDragging) return; - - currentX = e.clientX; - const currentY = e.clientY; - const diffX = Math.abs(currentX - startX); - const diffY = Math.abs(currentY - startY); - - // Si le mouvement dépasse 10px, ce n'est plus un clic simple - if (diffX > 10 || diffY > 10) { - isTap = false; - } - - const diff = currentX - startX; - const newTransform = startTransform + diff; - - container.style.transform = `translateX(${newTransform}px)`; - }); - - document.addEventListener('mouseup', function(e) { - if (!isDragging) return; - isDragging = false; - - container.style.transition = 'transform 0.5s ease'; - container.style.cursor = 'grab'; - - const diff = currentX - startX; - const threshold = 50; - const tapThreshold = 10; - const tapTimeThreshold = 300; - const clickDuration = Date.now() - touchStartTime; - - // Si c'est un clic simple (mouvement minimal et rapide) - if (isTap && Math.abs(diff) < tapThreshold && clickDuration < tapTimeThreshold) { - // Vérifier si le clic était sur une video-card - let videoCard = touchTarget; - - // Remonter le DOM pour trouver la video-card - while (videoCard && !videoCard.classList.contains('video-card')) { - videoCard = videoCard.parentElement; - if (videoCard === container || !videoCard) break; - } - - // Si on a trouvé une video-card, naviguer vers la vidéo - if (videoCard && videoCard.classList.contains('video-card')) { - const videoId = videoCard.dataset.videoId; - if (videoId) { - window.location.href = 'video.php?id=' + videoId; - return; - } - } - - updateCarousel(currentIndex); - currentX = 0; - isTap = true; // Réinitialiser pour le prochain tap - return; - } - - // Gérer comme un drag normal - if (Math.abs(diff) > threshold) { - if (diff > 0 && currentIndex > 0) { - updateCarousel(currentIndex - 1); - } else if (diff < 0 && currentIndex < items.length - 1) { - updateCarousel(currentIndex + 1); - } else { - updateCarousel(currentIndex); - } - } else { - updateCarousel(currentIndex); - } - - currentX = 0; - isTap = true; // Réinitialiser pour le prochain tap - }); - } - }); - - // Gestion des clics sur les vidéos - function initVideoCardClicks() { - const videoCards = document.querySelectorAll('.video-card'); - - videoCards.forEach(card => { - // Ne pas ajouter de listener click sur les cartes dans un carousel - // car elles sont déjà gérées par le carousel lui-même - const isInCarousel = card.closest('.carousel-container'); - - if (!isInCarousel && !card.hasAttribute('data-click-initialized')) { - card.setAttribute('data-click-initialized', 'true'); - card.addEventListener('click', function() { - const videoId = this.dataset.videoId; - if (videoId) { - window.location.href = 'video.php?id=' + videoId; - } - }); - } - }); - } - - // Initialiser les clics sur les vidéos existantes - initVideoCardClicks(); - - // Lazy loading des images - function initLazyLoading() { - if ('IntersectionObserver' in window) { - const imageObserver = new IntersectionObserver((entries, observer) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - const img = entry.target; - const src = img.getAttribute('data-src'); - if (src) { - img.src = src; - img.removeAttribute('data-src'); - } - observer.unobserve(img); - } - }); - }); - - document.querySelectorAll('img[data-src]').forEach(img => { - imageObserver.observe(img); - }); - } else { - // Fallback pour les navigateurs qui ne supportent pas IntersectionObserver - document.querySelectorAll('img[data-src]').forEach(img => { - const src = img.getAttribute('data-src'); - if (src) { - img.src = src; - img.removeAttribute('data-src'); - } - }); - } - } - - // Initialiser le lazy loading - initLazyLoading(); - - // Gestion des boutons "Voir plus" - const viewMoreButtons = document.querySelectorAll('.view-more'); - - viewMoreButtons.forEach(button => { - // Déterminer le type de vidéos à charger - const section = button.closest('.video-section'); - const sectionTitle = section.querySelector('.section-title').textContent.trim().toLowerCase(); - const videoGrid = section.querySelector('.video-grid'); - let videoType = ''; - let categoryId = null; - - if (sectionTitle.includes('dernières')) { - videoType = 'recent'; - } else if (sectionTitle.includes('tendances')) { - videoType = 'trending'; - } else if (sectionTitle.includes('indépendance')) { - videoType = 'independence'; - } else { - // Vérifier si c'est une section de catégorie - const categorySection = section.querySelector('[data-category-id]'); - if (categorySection) { - videoType = 'category'; - categoryId = categorySection.dataset.categoryId; - } else if (section.hasAttribute('data-category-id')) { - videoType = 'category'; - categoryId = section.dataset.categoryId; - } - } - - // Si aucun type reconnu, ne pas configurer l'événement - if (!videoType) return; - - // Stocker le numéro de page actuel - button.dataset.page = '1'; - - button.addEventListener('click', function() { - const page = parseInt(this.dataset.page); - - // Changer le texte du bouton pendant le chargement - button.textContent = 'Chargement...'; - button.disabled = true; - - // Préparer l'URL avec les paramètres - let url = `ajax/load-more-videos.php?type=${videoType}&page=${page}`; - if (videoType === 'category' && categoryId) { - url += `&category=${categoryId}`; - } - - // Préparer les données avec token CSRF - const formData = new FormData(); - formData.append('csrf_token', document.querySelector('meta[name="csrf-token"]').getAttribute('content')); - - // Faire la requête AJAX - fetch(url, { - method: 'POST', - headers: { - 'X-Requested-With': 'XMLHttpRequest' - }, - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - // Ajouter les nouvelles vidéos à la grille - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = data.html; - - // Ajouter chaque vidéo à la grille - while (tempDiv.firstChild) { - videoGrid.appendChild(tempDiv.firstChild); - } - - // Mettre à jour le numéro de page - this.dataset.page = data.page + 1; - - // Réinitialiser le texte du bouton - button.textContent = 'Voir plus'; - button.disabled = false; - - // Si plus de vidéos à charger, masquer le bouton - if (!data.hasMore) { - button.style.display = 'none'; - } - - // Initialiser les clics sur les nouvelles vidéos - initVideoCardClicks(); - // Initialiser le lazy loading pour les nouvelles images - initLazyLoading(); - } else { - // En cas d'erreur, afficher un message et réactiver le bouton - console.error('Erreur lors du chargement des vidéos:', data.error); - button.textContent = 'Voir plus'; - button.disabled = false; - } - }) - .catch(error => { - console.error('Erreur lors de la requête AJAX:', error); - button.textContent = 'Voir plus'; - button.disabled = false; - }); - }); - }); - - // Scroll vers le haut avec effet moderne quand on clique sur le logo du footer - const footerLogo = document.querySelector('.footer-logo'); - if (footerLogo) { - footerLogo.addEventListener('click', function() { - window.scrollTo({ - top: 0, - behavior: 'smooth' - }); - }); - } -}); \ No newline at end of file diff --git a/js/mastodon-config.php b/js/mastodon-config.php deleted file mode 100644 index 5e36a6c..0000000 --- a/js/mastodon-config.php +++ /dev/null @@ -1,23 +0,0 @@ - -window.addEventListener("load", () => { - const mastodonTimeline = new MastodonTimeline.Init({ - instanceUrl: '', - dateFormatLocale: '', - btnSeeMore: "", - btnReload: "", - maxNbPostFetch: '', - maxNbPostShow: '', - defaultTheme: 'dark', - ignorePaginationLink: true // Ignorer l'en-tête Link pour les versions Mastodon v4+ - }); -}); diff --git a/js/mastodon-timeline.umd.js b/js/mastodon-timeline.umd.js deleted file mode 100644 index 6338ae9..0000000 --- a/js/mastodon-timeline.umd.js +++ /dev/null @@ -1,9 +0,0 @@ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).MastodonTimeline={})}(this,(function(t){"use strict"; - /** - * Mastodon embed timeline - * @author idotj - * @version 4.7.0 - * @url https://gitlab.com/idotj/mastodon-embed-timeline - * @license GNU AGPLv3 - */t.Init=class{constructor(t={}){this.defaultSettings={mtContainerId:"mt-container",instanceUrl:"https://mastodon.social",timelineType:"local",userId:"",profileName:"",hashtagName:"",spinnerClass:"mt-loading-spinner",defaultTheme:"auto",maxNbPostFetch:"20",maxNbPostShow:"20",dateFormatLocale:"en-GB",dateFormatOptions:{day:"2-digit",month:"short",year:"numeric"},hideUnlisted:!1,hideReblog:!1,hideReplies:!1,hidePinnedPosts:!1,hideUserAccount:!1,txtMaxLines:"",filterByLanguage:"",btnShowMore:"SHOW MORE",btnShowLess:"SHOW LESS",markdownBlockquote:!1,hideEmojos:!1,btnShowContent:"SHOW CONTENT",hideVideoPreview:!1,btnPlayVideoTxt:"Load and play video",hidePreviewLink:!1,previewMaxLines:"",hideCounterBar:!1,disableCarousel:!1,carouselCloseTxt:"Close carousel",carouselPrevTxt:"Previous media item",carouselNextTxt:"Next media item",btnSeeMore:"See more posts at Mastodon",btnReload:"Refresh",insistSearchContainer:!1,insistSearchContainerTime:"3000",ignorePaginationLink:!1},this.mtSettings={...this.defaultSettings,...t},this.#t(),this.linkHeader={},this.mtContainerNode="",this.mtBodyNode="",this.fetchedData={},this.#e((()=>{this.#i()}))}#t(){Number(this.mtSettings.maxNbPostShow)>Number(this.mtSettings.maxNbPostFetch)&&(console.error(`Please check your settings! The maximum number of posts to show is bigger than the maximum number of posts to fetch. Changing the value of "maxNbPostFetch" to: ${this.mtSettings.maxNbPostShow}`),this.mtSettings.maxNbPostFetch=this.mtSettings.maxNbPostShow)}#e(t){"undefined"!=typeof document&&"complete"===document.readyState?t():"undefined"!=typeof document&&"complete"!==document.readyState&&document.addEventListener("DOMContentLoaded",t())}#i(){const t=()=>{this.mtContainerNode=document.getElementById(this.mtSettings.mtContainerId),this.mtBodyNode=this.mtContainerNode.getElementsByClassName("mt-body")[0],this.#s(),this.#a("newTimeline")};if(this.mtSettings.insistSearchContainer){const e=performance.now(),i=()=>{if(document.getElementById(this.mtSettings.mtContainerId))t();else{performance.now()-e container with id: "${this.mtSettings.mtContainerId}" after several attempts for ${this.mtSettings.insistSearchContainerTime/1e3} seconds`)}};i()}else document.getElementById(this.mtSettings.mtContainerId)?t():console.error(`Impossible to find the
container with id: "${this.mtSettings.mtContainerId}". Please try to add the option 'insistSearchContainer: true' when initializing the script`)}mtUpdate(){this.#e((()=>{this.mtBodyNode.replaceChildren(),this.mtBodyNode.insertAdjacentHTML("afterbegin",'
'),this.#a("updateTimeline")}))}mtColorTheme(t){this.#e((()=>{this.mtContainerNode.setAttribute("data-theme",t)}))}#s(){if("auto"===this.mtSettings.defaultTheme){let t=window.matchMedia("(prefers-color-scheme: dark)");t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light"),t.addEventListener("change",(t=>{t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light")}))}else this.mtColorTheme(this.mtSettings.defaultTheme)}#o(){return new Promise(((t,e)=>{const i=this.mtSettings.instanceUrl?`${this.mtSettings.instanceUrl}/api/v1/`:this.#n("Cette fonctionnalité n'est pas encore disponible","⌛"),s=this.#r(i),a=Object.entries(s).map((([t,i])=>this.#l(i,t).then((e=>({[t]:e}))).catch((i=>(e(new Error("Something went wrong getting the timeline data.")),this.#n(i.message),{[t]:[]})))));Promise.all(a).then((async e=>{if(this.fetchedData=e.reduce(((t,e)=>({...t,...e})),{}),!this.mtSettings.hidePinnedPosts&&void 0!==this.fetchedData.pinned?.length&&0!==this.fetchedData.pinned.length){const t=this.fetchedData.pinned.map((t=>({...t,pinned:!0})));this.fetchedData.timeline=[...t,...this.fetchedData.timeline]}if(this.#d())t();else{do{await this.#m()}while(!this.#d()&&this.linkHeader.next);t()}}))}))}#r(t){const{timelineType:e,userId:i,hashtagName:s,hideReblog:a,hideReplies:o,maxNbPostFetch:n,hidePinnedPosts:r,hideEmojos:l}=this.mtSettings,d={};switch(e){case"profile":if(!i){this.#n("Please check your userId value","⚠️");break}d.timeline=`${t}accounts/${i}/statuses?limit=${n}`,r||(d.pinned=`${t}accounts/${i}/statuses?pinned=true`);break;case"hashtag":if(!s){this.#n("Please check your hashtagName value","⚠️");break}d.timeline=`${t}timelines/tag/${s}?limit=${n}`;break;case"local":d.timeline=`${t}timelines/public?local=true&limit=${n}`;break;default:this.#n("Please check your timelineType value","⚠️")}return a&&(d.timeline+="&exclude_reblogs=true"),o&&(d.timeline+="&exclude_replies=true"),l||(d.emojos=`${t}custom_emojis`),d}async#l(t,e){const i=await fetch(t);if(!i.ok)throw new Error(`\n Failed to fetch the following Url:
${t}
Error status: ${i.status}
Error message: ${i.statusText}\n `);const s=await i.json();return"timeline"===e&&i.headers.get("Link")&&!this.mtSettings.ignorePaginationLink&&(this.linkHeader=this.#h(i.headers.get("Link"))),s}#d(){return this.fetchedData.timeline.length>=Number(this.mtSettings.maxNbPostFetch)}#m(){return new Promise((t=>{this.linkHeader.next?this.#l(this.linkHeader.next,"timeline").then((e=>{this.fetchedData.timeline=[...this.fetchedData.timeline,...e],t()})).catch((t=>(reject(new Error("Something went wrong fetching more posts.")),this.#n(t.message),{[key]:[]}))):t()}))}#h(t){const e=t.split(", ").map((t=>t.split("; "))).map((t=>[t[1].replace(/"/g,"").replace("rel=",""),t[0].slice(1,-1)]));return Object.fromEntries(e)}async#a(t){await this.#o();const{hideUnlisted:e,maxNbPostShow:i,filterByLanguage:s}=this.mtSettings,a=this.fetchedData.timeline;this.mtBodyNode.replaceChildren();if(a.filter((t=>{const i="public"===t.visibility||!e&&"unlisted"===t.visibility,a=t.language||(t.reblog?t.reblog.language:null);return i&&(""===s||a===s)})).forEach(((t,e)=>{e${a?.length||0} posts have been fetched from the server
This may be due to an incorrect configuration with the parameters or with the filters applied (to hide certains type of posts)`;this.#n(t,"📭")}}#g(){"0"!==this.mtSettings.txtMaxLines&&0!==this.mtSettings.txtMaxLines.length&&this.mtBodyNode.parentNode.style.setProperty("--mt-txt-max-lines",this.mtSettings.txtMaxLines),"0"!==this.mtSettings.previewMaxLines&&0!==this.mtSettings.previewMaxLines.length&&this.mtBodyNode.parentNode.style.setProperty("--mt-preview-max-lines",this.mtSettings.previewMaxLines)}#u(t){const e=this.mtBodyNode.getElementsByTagName("article");for(let i=0;i
'+this.#f(h)+' avatar
'+(i?'
'+this.#f(t.account.username)+' avatar
':"")+"
",u='
'+(!this.mtSettings.hideEmojos&&c?this.#x(c,p):c||h)+""+(this.mtSettings.hideUserAccount?"":'
")+"
",v=this.#y(o),b='
'+(t.pinned?'':"")+'"+(t.edited_at?" *":"")+"
",w=this.mtSettings.hidePreviewLink||!t.card&&!t.reblog?.card?"":this.#S(i?t.reblog.card:t.card),f="0"!==this.mtSettings.txtMaxLines&&this.mtSettings.txtMaxLines.length?" truncate":"";let x="";const y=s.spoiler_text?s.spoiler_text:s.content,S='
'+this.#L(s.content)+w+"
";y&&(x='
'+this.#L(y)+(s.spoiler_text?S:"")+"
");const L=[...t.media_attachments,...t.reblog?.media_attachments||[]].map((t=>this.#T(t,s.sensitive))).join(""),T=L?`
${L}
`:"",N=t.poll?'
    '+t.poll.options.map((function(t){return"
  • "+t.title+"
  • "})).join("")+"
":"",M=this.mtSettings.hideCounterBar?"":'
'+this.#N("replies",n)+this.#N("reblog",r)+this.#N("favorites",l)+"
";return'
'+g+u+b+"
"+x+T+N+(s.spoiler_text?"":w)+M+"
"}#N(t,e){return`
${{replies:'',reblog:'',favorites:''}[t]}${e}
`}#M(t,e){function i(t,e){let i=e.replace(/\s+/g,"").toLowerCase();return!(!["src","href","xlink:href"].includes(t)||!i.includes("javascript:")&&!i.includes("data:"))||(!!t.startsWith("on")||void 0)}function s(t){let e=t.attributes;for(let{name:s,value:a}of e)i(s,a)&&t.removeAttribute(s)}let a=(new DOMParser).parseFromString(t,"text/html").body||document.createElement("body");return function(t){let e=t.querySelectorAll("script");for(let t of e)t.remove()}(a),function t(e){let i=e.children;for(let e of i)s(e),t(e)}(a),e?a.childNodes:a.innerHTML}#L(t){let e=t;return e=this.#M(e,!1),e=this.#k(e),this.mtSettings.hideEmojos||(e=this.#x(e,this.fetchedData.emojos)),this.mtSettings.markdownBlockquote&&(e=this.#C(e,"

>","

","

","

")),e}#k(t){let e=t.replaceAll('rel="tag"','rel="tag" target="_blank"');return e=e.replaceAll('class="u-url mention"','class="u-url mention" target="_blank"'),e}#C(t,e,i,s,a){if(t.includes(e)){const o=new RegExp(e+"(.*?)"+i,"gi");return t.replace(o,s+"$1"+a)}return t}#f(t){return(t??"").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}#x(t,e){if(t.includes(":")){for(const i of e){const e=new RegExp(`\\:${i.shortcode}\\:`,"g");t=t.replace(e,`Emoji ${i.shortcode}`)}return t}return t}#y(t){const e=new Date(t);return new Intl.DateTimeFormat(this.mtSettings.dateFormatLocale,this.mtSettings.dateFormatOptions).format(e)}#T(t,e=!1){const{type:i,url:s,preview_url:a,description:o,meta:n}=t,{original:r,small:l}=n,{spinnerClass:d,btnShowContent:m,btnPlayVideoTxt:h,hideVideoPreview:c}=this.mtSettings,p='",g=' class="mt-post-media '+(e?"mt-post-media-spoiler ":"")+(d||"")+'" data-media-type="'+i+'" data-media-url-hd="'+s+'"'+(o?' data-media-alt-txt="'+this.#f(o)+'"':"")+' data-media-width-hd="'+r.width+'" data-media-height-hd="'+r.height+'" style="padding-top: calc(100%/'+l?.aspect+')">';return"image"===i?"
':"audio"===i?'
'+(e?p:"")+''+(a?''+(o?this.#f(o):':"")+"
":"video"===i||"gifv"===i?c?"':"':""}#P(t,e){let i=document.createElement("dialog");i.id=t,i.classList.add("mt-dialog"),i.dataset.theme=this.mtContainerNode.getAttribute("data-theme"),i.innerHTML=e,document.body.prepend(i),i.showModal(),i.addEventListener("close",(()=>{document.body.removeChild(i)}))}#E(t){const e=Array.from(t.target.parentNode.parentNode.children).filter((t=>!t.classList.contains("mt-post-media-spoiler"))),i=e.indexOf(t.target.parentNode)+1;let s=[];e.forEach(((t,e)=>{let i="";i="gifv"===t.getAttribute("data-media-type")||"video"===t.getAttribute("data-media-type")?`\n \n `:`\n ${t.getAttribute(\n `;const a=`\n \n `;s.push(a)}));const a=`\n \n\n \n\n \n\n \n `;this.#P("mt-carousel",a),s.length>=2&&this.#$(e.length,i)}#$(t,e){let i=e;const s=document.getElementById("mt-carousel-scroll");let a=0,o=!1;const n=document.getElementById("mt-carousel-prev"),r=document.getElementById("mt-carousel-next"),l=(t,e="smooth")=>{document.getElementById("mt-carousel-"+t).scrollIntoView({behavior:e})};l(i,"instant");const d=()=>{clearTimeout(a),a=setTimeout((()=>{o&&(i=(()=>{const t=(s.scrollLeft+s.clientWidth)/s.clientWidth;return Math.round(t+Number.EPSILON)})(),m()),o=!0}),60)};s.addEventListener("scroll",d);const m=()=>{n.hidden=1===i,r.hidden=i===t},h=e=>{const s=e.target.closest("button")?.id;"mt-carousel-next"===s?(o=!1,++i,i>t&&(i=t),l(i),m()):"mt-carousel-prev"===s&&(o=!1,--i,i<1&&(i=1),l(i),m()),"mt-carousel-close"===s&&p()};document.addEventListener("click",h);const c=t=>{"Escape"!==t.key&&27!==t.keyCode||p()};document.addEventListener("keydown",c);const p=()=>{s.removeEventListener("scroll",d),document.removeEventListener("click",h),document.removeEventListener("keydown",c)}}#B(t){const e=t.target.closest("[data-media-type]"),i=e.dataset.mediaUrlHd;e.replaceChildren(),e.innerHTML=``}#A(t){const e=t.target,i=e.nextSibling,s="true"===e.getAttribute("aria-expanded");i.classList.toggle("spoiler-txt-hidden",s),i.classList.toggle("spoiler-txt-visible",!s),e.setAttribute("aria-expanded",!s),e.textContent=s?this.mtSettings.btnShowMore:this.mtSettings.btnShowLess}#H(t){const e=t.target;e.parentNode.classList.toggle("mt-post-media-spoiler",!e.classList.contains("mt-btn-spoiler-media-show"))}#S(t){const{url:e,image:i,image_description:s,provider_name:a,title:o,description:n,author_name:r}=t,{previewMaxLines:l,spinnerClass:d}=this.mtSettings;let m="";return"0"!==l&&n&&(m=''+this.#q(n)+""),''+(i?'
'+this.#f(s)+'
':'
📄
')+'
'+(a?''+this.#q(a)+"":"")+''+o+""+m+(r?''+this.#q(r)+"":"")+"
"}#q(t){return(new DOMParser).parseFromString(t,"text/html").body.textContent}#b(){const{btnSeeMore:t,btnReload:e,timelineType:i,profileName:s,hashtagName:a,instanceUrl:o}=this.mtSettings;let n="",r="";if(t){let e="";switch(i){case"profile":s?e=s:this.#n("Please check your profileName value","⚠️");break;case"hashtag":e="tags/"+a;break;case"local":e="public/local"}n=''+t+""}if(e&&(r='"),this.mtBodyNode.parentNode.insertAdjacentHTML("beforeend",'"),e){const t=this.mtContainerNode.querySelector(".btn-refresh");t&&t.addEventListener("click",(()=>this.mtUpdate()))}}#v(){this.mtBodyNode.addEventListener("click",(t=>{const e=t.target,i=e.localName,s=e.parentNode;("article"==i||"article"==e.offsetParent?.localName||this.mtSettings.disableCarousel&&"image"===s.getAttribute("data-media-type"))&&this.#D(t),e.classList.contains("mt-btn-spoiler-txt")&&this.#A(t),e.classList.contains("mt-btn-spoiler-media")&&this.#H(t),this.mtSettings.disableCarousel||"img"!=i||"image"!==s.getAttribute("data-media-type")&&"audio"!==s.getAttribute("data-media-type")||this.#E(t),("mt-btn-play"==e.className||"svg"==i&&"mt-btn-play"==s.className||"path"==i&&"mt-btn-play"==s.parentNode.className||"img"==i&&("video"===s.getAttribute("data-media-type")||"gifv"===s.getAttribute("data-media-type")))&&this.#B(t)})),this.mtBodyNode.addEventListener("keydown",(t=>{const e=t.target.localName;"Enter"===t.key&&"article"==e&&this.#D(t)}))}#D(t){const e=t.target.closest(".mt-post")?.dataset.location;if(!e)return;const i=t.target.localName;if("a"===i||"span"===i||"button"===i||"bdi"===i||"time"===i)return;const s=t.target.className;if("mt-post-media-spoiler"===s||"mt-post-preview-noImage"===s)return;const a=t.target.parentNode?.className;"mt-post-avatar-image-big"!==a&&"mt-post-avatar-image-small"!==a&&"mt-post-header-user-name"!==a&&"mt-post-preview-image"!==a&&"mt-post-preview"!==a&&window.open(e,"_blank","noopener")}#p(){const t=e=>{e.target.parentNode.classList.remove(this.mtSettings.spinnerClass),e.target.removeEventListener("load",t),e.target.removeEventListener("error",t)};this.mtBodyNode.querySelectorAll(`.${this.mtSettings.spinnerClass} > img`).forEach((e=>{e.addEventListener("load",t),e.addEventListener("error",t)}))}#n(t,e){const i=e||"❌";throw this.mtBodyNode.innerHTML=`\n
\n ${i}\n Oups !\n
Erreur ! Réessayez ultérieurement.
\n
`,this.mtBodyNode.setAttribute("role","none"),new Error("Stopping the script due to an error building the timeline.")}}})); - \ No newline at end of file diff --git a/js/pleroma-adapter.js b/js/pleroma-adapter.js deleted file mode 100644 index 7644b2a..0000000 --- a/js/pleroma-adapter.js +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Pleroma to Mastodon API Response Adapter - * - * This script intercepts fetch requests to Pleroma instances and adapts - * the responses to match the structure expected by mastodon-timeline.umd.js - */ - -(function() { - 'use strict'; - - // Store original fetch - const originalFetch = window.fetch; - - // Override fetch globally - window.fetch = async function(...args) { - const response = await originalFetch.apply(this, args); - - // Only intercept Pleroma API timeline and emoji requests - const url = args[0]; - if (typeof url === 'string' && - (url.includes('/api/v1/timelines/') || - url.includes('/api/v1/accounts/') && url.includes('/statuses'))) { - - // Clone the response so we can modify it - const clonedResponse = response.clone(); - - try { - const data = await clonedResponse.json(); - - // Adapt the response structure - const adaptedData = Array.isArray(data) - ? data.map(adaptPost) - : adaptPost(data); - - // Create a new response with adapted data - return new Response(JSON.stringify(adaptedData), { - status: response.status, - statusText: response.statusText, - headers: response.headers - }); - } catch (error) { - console.warn('Pleroma adapter: Failed to parse response, returning original', error); - return response; - } - } - - return response; - }; - - /** - * Adapt a single Pleroma post to Mastodon format - */ - function adaptPost(post) { - if (!post || typeof post !== 'object') { - return post; - } - - // Create a copy to avoid mutating the original - const adapted = { ...post }; - - // Adapt media attachments - if (adapted.media_attachments && Array.isArray(adapted.media_attachments)) { - adapted.media_attachments = adapted.media_attachments.map(adaptMediaAttachment); - } - - // Adapt reblog if present - if (adapted.reblog) { - adapted.reblog = adaptPost(adapted.reblog); - } - - return adapted; - } - - /** - * Adapt media attachment to include missing meta fields - */ - function adaptMediaAttachment(attachment) { - if (!attachment || typeof attachment !== 'object') { - return attachment; - } - - // If meta already exists and has required fields, return as-is - if (attachment.meta?.original?.width && attachment.meta?.small?.aspect) { - return attachment; - } - - // Create adapted attachment - const adapted = { ...attachment }; - - // Default dimensions (will work for most cases) - const defaultWidth = 1280; - const defaultHeight = 720; - const defaultAspect = defaultWidth / defaultHeight; - - // Try to extract dimensions from Pleroma's meta if available - let width = defaultWidth; - let height = defaultHeight; - - if (attachment.pleroma?.mime_type) { - // For videos, use 16:9 ratio - if (attachment.pleroma.mime_type.startsWith('video/')) { - width = 1920; - height = 1080; - } - // For images, use standard web dimensions - else if (attachment.pleroma.mime_type.startsWith('image/')) { - width = 1200; - height = 800; - } - } - - // Ensure meta structure exists - adapted.meta = adapted.meta || {}; - - // Add original dimensions - adapted.meta.original = adapted.meta.original || { - width: width, - height: height, - aspect: width / height - }; - - // Add small (preview) dimensions - adapted.meta.small = adapted.meta.small || { - width: Math.floor(width / 2), - height: Math.floor(height / 2), - aspect: width / height - }; - - return adapted; - } - - console.log('Pleroma adapter initialized: API responses will be adapted for Mastodon compatibility'); -})(); diff --git a/js/search.js b/js/search.js deleted file mode 100644 index 4aa467f..0000000 --- a/js/search.js +++ /dev/null @@ -1,12 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - // Gestion des clics sur les vidéos - const videoCards = document.querySelectorAll('.video-card'); - videoCards.forEach(card => { - card.addEventListener('click', function() { - const videoId = this.dataset.videoId; - if (videoId) { - window.location.href = 'video.php?id=' + videoId; - } - }); - }); -}); diff --git a/mentions-legales.php.sample b/mentions-legales.php.sample deleted file mode 100644 index d611c3e..0000000 --- a/mentions-legales.php.sample +++ /dev/null @@ -1,247 +0,0 @@ - - - - - - Mentions Légales - ANNU KUTE CED - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- -

Mentions Légales

-
- -
-

1. Acceptation des conditions

-

- En accédant et en utilisant le site web , vous acceptez les présentes conditions générales d'utilisation. - Si vous n'acceptez pas ces conditions, veuillez ne pas utiliser ce site. -

- -

2. Description du service

-

- - Le site agrège les contenus du podcast disponibles sur PeerTube, Castopod et Mastodon. -

- -

3. Responsabilité des utilisateurs

-

- Les utilisateurs s'engagent à ne pas utiliser le site à des fins illégales ou préjudiciables. - L'utilisateur est seul responsable de tout contenu qu'il pourrait partager via les fonctionnalités - interactives du site, le cas échéant. -

- -

4. Modification des conditions

-

- - , se réserve le droit de modifier ces conditions à tout moment. Les utilisateurs seront - informés des modifications importantes. L'utilisation continue du site après une modification des - conditions constitue une acceptation de ces modifications. -

- -

5. Loi applicable et juridiction

-

- Les présentes conditions sont régies par le droit français (pour l'instant). Tout litige relatif à l'interprétation - ou à l'exécution des présentes conditions sera soumis aux tribunaux compétents. -

- -

6. Licences Creative Commons

-

- Certains contenus proposés sur sont disponibles sous licence Creative Commons. - Les licences Creative Commons permettent aux créateurs de contenu de partager leur travail tout - en définissant les conditions d'utilisation. -

- -
-
- - - -

CC0 (Domaine Public)

-

Aucun droit d'auteur. Vous pouvez copier, modifier, distribuer et représenter l'œuvre, même à des fins commerciales, sans demander d'autorisation.

-
- -
- - - -

CC BY (Attribution)

-

Vous êtes autorisé à partager et adapter l'œuvre, même à des fins commerciales, à condition de créditer l'auteur original.

-
- -
- - - -

CC BY-SA (Attribution - Partage dans les Mêmes Conditions)

-

Vous êtes autorisé à partager et adapter l'œuvre, même à des fins commerciales, à condition de créditer l'auteur original et de partager votre travail sous la même licence.

-
- -
- - - -

CC BY-ND (Attribution - Pas de Modification)

-

Vous êtes autorisé à partager l'œuvre, même à des fins commerciales, à condition de créditer l'auteur original et de ne pas modifier l'œuvre.

-
- -
- - - -

CC BY-NC (Attribution - Pas d'Utilisation Commerciale)

-

Vous êtes autorisé à partager et adapter l'œuvre à des fins non commerciales, à condition de créditer l'auteur original.

-
- -
- - - -

CC BY-NC-SA (Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions)

-

Vous êtes autorisé à partager et adapter l'œuvre à des fins non commerciales, à condition de créditer l'auteur original et de partager votre travail sous la même licence.

-
- -
- - - -

CC BY-NC-ND (Attribution - Pas d'Utilisation Commerciale - Pas de Modification)

-

Vous êtes autorisé à partager l'œuvre à des fins non commerciales, à condition de créditer l'auteur original et de ne pas modifier l'œuvre.

-
-
- -

- Pour plus d'informations sur les licences Creative Commons, veuillez consulter le site officiel : - Creative Commons. -

- -

7. Hébergement et Webmaster

-

- Ce site web est hébergé par , , , dont le siège social est situé au . -

- -

- Le webmaster du site est . Pour toute question technique concernant le site, vous pouvez le contacter via l'adresse email . -

- -

8. Licence du site

-

- Le code source du site est publié sous licence . Cette licence garantit à tous les utilisateurs la liberté d'exécuter, d'étudier, de modifier et de redistribuer le logiciel, à condition que les modifications soient également partagées sous les mêmes conditions. -

- -

- La licence AGPL-V3 étend la licence GPL standard en imposant que le code source soit disponible pour tous ceux qui interagissent avec le programme à travers un réseau. Pour consulter l'intégralité de la licence, vous pouvez visiter le site officiel de GNU. -

- -

- Le code source de ce site est disponible sur LaBola : -

- -

- ANNU KUTE CED est un fork de FEDIVERSE OKI, développé par l'ORGANISATION KA INTERNATIONALE (OKI) sous licence AGPL-V3. Le fork est maintenu par le propriétaire du dépôt cedric (Cédric Famibelle-Pronzola), en accord avec la licence d'origine. -

- -

9. Icônes et marques tierces

-

- Font Awesome : Les icônes utilisées sur ce site proviennent de Font Awesome, distribuées sous licence SIL OFL 1.1, compatible avec notre licence AGPL-V3. -

- -

- Marques déposées : Les noms et logos des réseaux sociaux (Facebook, YouTube, Instagram, TikTok, X/Twitter, Mastodon) sont des marques déposées de leurs propriétaires respectifs. Leur utilisation sur ce site se limite à l'identification de nos comptes officiels sur ces plateformes et relève de l'usage loyal. -

- -

10. Vie privée, cookies et publicité

-

- Cookies : Ce site utilise uniquement les cookies de session PHP, indispensables au bon fonctionnement technique du site. Ces cookies temporaires disparaissent dès que vous fermez votre navigateur - comme par magie ! 🍪✨ -
Aucun cookie publicitaire, de tracking ou de pistage n'est utilisé. L'extension "I still don't care about cookies" reste donc largement inutile sur notre site - vous n'aurez jamais à vous soucier des bannières de consentement chez nous ! -

- -

- Publicité : Nous avons fait le choix de ne diffuser aucune publicité sur . Votre expérience de navigation est entièrement libre de toute annonce commerciale. -

- -

- Bloqueurs de publicité : Si vous utilisez un bloqueur de publicité, celui-ci ne bloquera que d'éventuels outils d'analyse tels que Plausible, qui respectent votre vie privée et ne collectent que des données anonymes à des fins statistiques. -
Nous vous invitons donc gentiment à le désactiver pour notre site - promis, nous ne vous montrerons pas de pubs pour de la vanille bourbon que vous avez regardée il y a trois semaines ! -
Vos données nous aident simplement à savoir si quelqu'un regarde ce qu'on fait... 😊 -

- -

- Adresse IP et technologie P2P : Notre plateforme utilise la technologie P2P (Peer-to-Peer) pour diffuser les vidéos. -
Si vous avez déjà utilisé μTorrent, eMule, iMesh, Shareaza ou tout autre logiciel de partage de fichiers, vous connaissez le principe : au lieu d'avoir un seul serveur qui s'épuise à envoyer la vidéo à tout le monde, chaque utilisateur qui regarde une vidéo aide aussi à la diffuser aux autres. -
C'est comme si, au lieu d'avoir un seul serveur, tout le monde devenait un mini-serveur ! -

- -

- Pour que cette magie opère, nous devons utiliser votre adresse IP - ne vous inquiétez pas, nous ne la revendons pas à des sites qui veulent vous bombarder de publicités pour des lentilles de Cilaos. -
Elle sert uniquement à établir ces connexions P2P qui rendent le visionnage de vidéos plus fluide pour tout le monde. -

- -

11. Contact

-

- Pour toute question concernant ces conditions générales d'utilisation, veuillez nous contacter à - l'adresse suivante : -

- -

- Date de dernière mise à jour : VOTRE-DATE-MAJ -

-
-
- - - - - - diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..75fba85 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6567 @@ +{ + "name": "annu-kute-ced", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "annu-kute-ced", + "version": "0.0.1", + "dependencies": { + "@fontsource/archivo": "^5.3.0", + "@fontsource/inter": "^5.3.0" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.63.0", + "@sveltejs/vite-plugin-svelte": "^7.1.2", + "svelte": "^5.56.1", + "svelte-check": "^4.6.0", + "typescript": "^6.0.3", + "vite": "^8.0.16", + "vite-plugin-pwa": "^1.3.0" + } + }, + "node_modules/@apideck/better-ajv-errors": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.7.tgz", + "integrity": "sha512-TajUJwGWbDwkCx/CZi7tRE8PVB7simCvKJfHUsSdvps+aTM/PDPP4gkLmKnc+x3CE//y9i/nj74GqdL/hwk7Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonpointer": "^5.0.1", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", + "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", + "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fontsource/archivo": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/archivo/-/archivo-5.3.0.tgz", + "integrity": "sha512-5DIMgPVJRi62OqdOVoogCFxP73EkNM/E0YVTSDIQlDEJfDbxqZduwM/YoNZwC9Sx1CDgpKinqf8ckRY1QNIecw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-RofMylZmjlJEfELXeNHFWBRcSs75rGU/6bV2S2jfnvv/3rPXPGe0LgUJTklcHZ9lM4OZmAVFhcJPnACfb91A3g==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-babel": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.1.0.tgz", + "integrity": "sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@rollup/pluginutils": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz", + "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", + "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^7.0.3", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz", + "integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-static": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", + "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.70.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.70.1.tgz", + "integrity": "sha512-nY9SPHGOZro3doud9vZXDBwl9tCZIouuJztjgSHs6PAIrv9M/z5O7eOhPV5xU7CgVHA976Jwu3BA1hIFvXztkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.9", + "@types/cookie": "^0.6.0", + "acorn": "^8.16.0", + "cookie": "^0.6.0", + "devalue": "^5.8.1", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "set-cookie-parser": "^3.0.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3 || ^6.0.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/load-config": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.0.tgz", + "integrity": "sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.2.0.tgz", + "integrity": "sha512-1SpkuMSRLfugrVX+IrKfE1RUegzo8AQzKQ6qQPfVzbcWi5IhuTPaKb5ZrLpucleFznkc4/RTeSPoRnGWFxX+EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "magic-string": "^0.30.21", + "obug": "^2.1.0", + "vitefu": "^1.1.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" + } + }, + "node_modules/@trickfilm400/rollup-plugin-off-main-thread": { + "version": "3.0.0-pre1", + "resolved": "https://registry.npmjs.org/@trickfilm400/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-3.0.0-pre1.tgz", + "integrity": "sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ejs": "^3.1.10", + "json5": "^2.2.3", + "magic-string": "^0.30.21", + "string.prototype.matchall": "^4.0.12" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz", + "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "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/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.2.tgz", + "integrity": "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.395", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz", + "integrity": "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.0.tgz", + "integrity": "sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eta": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-4.6.0.tgz", + "integrity": "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/bgub/eta?sponsor=1" + } + }, + "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-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true, + "license": "ISC" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.7.tgz", + "integrity": "sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", + "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/smob": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz", + "integrity": "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0.tgz", + "integrity": "sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.56.7", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.7.tgz", + "integrity": "sha512-5qERUZX80oQj6XrDMUmD2Uhd/cIpCPDWWKBK3ZHmyRUC9apPyamWM8xMo31mbWsIQxwG2hVoSnOJ/EcnhVkkzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.10", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.8.1", + "esm-env": "^1.2.1", + "esrap": "^2.2.12", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.3.tgz", + "integrity": "sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.0", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz", + "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-pwa": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.3.0.tgz", + "integrity": "sha512-c5kMgN+ITrOtHXp8PAtk2uOIEea6XjP/unCGxOWWBzQ6qa65qj/awHg0wf+QF9E/2u9vh86LqxPwzEPNbM2r5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.6", + "pretty-bytes": "^6.1.1", + "tinyglobby": "^0.2.10", + "workbox-build": "^7.4.1", + "workbox-window": "^7.4.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vite-pwa/assets-generator": "^1.0.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "workbox-build": "^7.4.1", + "workbox-window": "^7.4.1" + }, + "peerDependenciesMeta": { + "@vite-pwa/assets-generator": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/workbox-background-sync": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.1.tgz", + "integrity": "sha512-HhT7KE8tOWDm02wRNshXUnUPofMlhenF2DBdUnDPOubhizzPeItkYTmAB6td1Z2cjYPa98vzEiPLEuzn5hN66g==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.1.tgz", + "integrity": "sha512-uAlgslKLvbQY+suirIdnBCSYrcgBhjp81Nj4l1lj/Jmj0MJO2CJERnCJjT0GFVwmReV0N+zs78K6gqd5gr9/+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-build": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.1.tgz", + "integrity": "sha512-SDhxIvEAde9Gy/5w4Yo1Jh/M49Z0qE3q0oteyE8zGq0DScxFqVBcCtIXFuLtmtxRQZCMbf0prco4VyEu3KBQuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.24.4", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "@rollup/plugin-replace": "^6.0.3", + "@rollup/plugin-terser": "^1.0.0", + "@trickfilm400/rollup-plugin-off-main-thread": "^3.0.0-pre1", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "eta": "^4.5.1", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^11.0.1", + "pretty-bytes": "^5.3.0", + "rollup": "^4.53.3", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "7.4.1", + "workbox-broadcast-update": "7.4.1", + "workbox-cacheable-response": "7.4.1", + "workbox-core": "7.4.1", + "workbox-expiration": "7.4.1", + "workbox-google-analytics": "7.4.1", + "workbox-navigation-preload": "7.4.1", + "workbox-precaching": "7.4.1", + "workbox-range-requests": "7.4.1", + "workbox-recipes": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1", + "workbox-streams": "7.4.1", + "workbox-sw": "7.4.1", + "workbox-window": "7.4.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/workbox-build/node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.1.tgz", + "integrity": "sha512-8xaFoJdDc2OjrlbbL3gEeBO1WKcMwRqwLRupgqahYXu75yXajPLuwrbXMrIGZuWYXrQwk0xDjOxZ/ujCy/oJYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-core": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.1.tgz", + "integrity": "sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-expiration": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.1.tgz", + "integrity": "sha512-lRKUF7b+OGbeXkQk1s6MHXOa3d7Xxf7Of31W6c6hCfipfIyrtdWZ89stq21AHZMaoG7VNFoHply4Ox+rU31TWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-google-analytics": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.1.tgz", + "integrity": "sha512-Mks1JwLEt++ZAkF6sS1OpSh9RtAMIsiDgRpK+codiHGIPXeaUOgi4cPc3GFadUl8V5QPeypEk8Oxgl3HlwVzHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-background-sync": "7.4.1", + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.1.tgz", + "integrity": "sha512-C4KVsjPcYKJOhr631AxR9XoG2rLF3QiTk5aMv36MXOjtWvm8axwNFAtKUPGsWUwLXXAMgYM1En7fsvndaXeXRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-precaching": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.1.tgz", + "integrity": "sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-range-requests": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.1.tgz", + "integrity": "sha512-7i2oxAUE82gHdAJBCAQ04JzNOdRPqzuOzGfoUyJpFSmeqBNYGPrAH8GPoPjUQTfp+NycwrD2H68VtuF8qxv0vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-recipes": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.1.tgz", + "integrity": "sha512-gnbVfmV4/TtmQaM4x9AtuXhcdstJsep3XMVeztOrQVPT+R6+6DeBjGTCQ7fFCXm+4GEHUA5VEBTyi5+4gWGeog==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-cacheable-response": "7.4.1", + "workbox-core": "7.4.1", + "workbox-expiration": "7.4.1", + "workbox-precaching": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-routing": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.1.tgz", + "integrity": "sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-strategies": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.1.tgz", + "integrity": "sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-streams": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.1.tgz", + "integrity": "sha512-HWWtraKUbJknd9kgqGcpQ3G114HOPYvqs8HaJMDs2ebLNAimDkVDaWfAXE6Ybl+m8U6KsCE6pWyLYuigWmnAXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1" + } + }, + "node_modules/workbox-sw": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.1.tgz", + "integrity": "sha512-fez5f2DUlDJWTFYkCWQpY10N8gtztd849NswCbVFk0QlcSM4HT5A8x4g4ii650yem4I8tHY0R7JZahwp3ltIPw==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-window": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.1.tgz", + "integrity": "sha512-notZDH2u8VXaqyuD7xaqIfEFi6SRM4SUSd7ewe9PDsVqADuepxX2ZMY3uvuZGxzY5ZOsGC/vD3A/3smFtJt4/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "7.4.1" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ee52076 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "annu-kute-ced", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "postbuild": "node scripts/postbuild-csp.mjs" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.63.0", + "@sveltejs/vite-plugin-svelte": "^7.1.2", + "svelte": "^5.56.1", + "svelte-check": "^4.6.0", + "typescript": "^6.0.3", + "vite": "^8.0.16", + "vite-plugin-pwa": "^1.3.0" + }, + "dependencies": { + "@fontsource/archivo": "^5.3.0", + "@fontsource/inter": "^5.3.0" + } +} diff --git a/recherche.php b/recherche.php deleted file mode 100644 index 8670d4d..0000000 --- a/recherche.php +++ /dev/null @@ -1,265 +0,0 @@ - 0) { - $currentPageVideos = array_slice($searchResults, $startIndex, VIDEOS_PER_PAGE); -} -?> - - - - - - - <?php echo !empty($query) ? 'Recherche: ' . htmlspecialchars($query) . ' - ' : 'Recherche - '; ?><?php echo SITE_NAME; ?> - - - - - - - - - - - - - - "> - - - - - - - - - - "> - - - - - 'Accueil', 'url' => getBaseUrl()], - ['name' => 'Recherche', 'url' => getBaseUrl() . '/recherche.php'], - ['name' => $searchTitle, 'url' => $searchUrl] - ]; - $breadcrumbJsonLd = generateBreadcrumbJsonLd($breadcrumbs); - outputJsonLd($breadcrumbJsonLd); - ?> - - - - - - - - - -
- - - -
-
- - - -

Vidéos avec le hashtag : ""

- -

Résultats pour : ""

- - -

Rechercher des vidéos

- -
- - - 0): ?> -
- -

vidéo 1 ? 's' : ''; ?> avec le hashtag #

- -

résultat 1 ? 's' : ''; ?> trouvé 1 ? 's' : ''; ?>

- -
- -
- -
-
- <?php echo htmlspecialchars($video['title']); ?> -
- -
- -
-
-

-
- -
- -
- - <?php echo htmlspecialchars($video['channel']); ?> - - -
- -
-
- -
- - 1): ?> - - - -
- -

Aucun résultat trouvé

- -

Aucune vidéo n'a été trouvée avec le hashtag "#".

- -

Aucune vidéo ne correspond à votre recherche "".

- -

Essayez avec d'autres termes ou consultez les vidéos populaires sur la page d'accueil.

-
- - -
- -

Effectuez une recherche

-

Utilisez le formulaire ci-dessus pour rechercher des vidéos par titre, description ou tags.

-

Astuce : Pour rechercher par tag, commencez votre recherche par # (exemple: #réunion)

-
- -
-
- - - - - - - - diff --git a/robots.txt.sample b/robots.txt.sample deleted file mode 100644 index f485e88..0000000 --- a/robots.txt.sample +++ /dev/null @@ -1,25 +0,0 @@ -User-agent: * -Allow: / -Allow: /index -Allow: /index.php -Allow: /direct -Allow: /direct.php -Allow: /categories -Allow: /categories.php -Allow: /video -Allow: /video.php -Allow: /recherche -Allow: /recherche.php -Allow: /mentions-legales -Allow: /mentions-legales.php -Allow: /img/ -Disallow: /includes/ -Disallow: /js/ -Disallow: /css/ -Disallow: /ajax/ -Disallow: /docs/ -Disallow: /conf/ -Disallow: /cache/ - -# Sitemap -Sitemap: https://annukuteced.buzz/sitemap.xml diff --git a/scripts/postbuild-csp.mjs b/scripts/postbuild-csp.mjs new file mode 100644 index 0000000..da48462 --- /dev/null +++ b/scripts/postbuild-csp.mjs @@ -0,0 +1,44 @@ +import { createHash } from 'node:crypto'; +import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const BUILD_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'build'); +const scriptRe = /]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/gi; + +function* walk(dir) { + for (const entry of readdirSync(dir)) { + const path = join(dir, entry); + if (statSync(path).isDirectory()) yield* walk(path); + else if (entry.endsWith('.html')) yield path; + } +} + +let pages = 0; + +for (const file of walk(BUILD_DIR)) { + const html = readFileSync(file, 'utf8'); + const hashes = new Set(); + + for (const match of html.matchAll(scriptRe)) { + const content = match[1]; + if (!content.trim()) continue; + hashes.add(`'sha256-${createHash('sha256').update(content).digest('base64')}'`); + } + + if (hashes.size === 0) continue; + + const csp = `default-src 'self'; script-src 'self' ${[...hashes].join(' ')}`; + const meta = ``; + + const anchor = html.match(//); + if (!anchor) { + console.warn(`[csp] pas d'ancre charset dans ${file}, page ignorée`); + continue; + } + + writeFileSync(file, html.replace(anchor[0], `${anchor[0]}\n\t\t${meta}`)); + pages++; +} + +console.log(`[csp] meta CSP injectée dans ${pages} page(s)`); diff --git a/site.webmanifest.sample b/site.webmanifest.sample deleted file mode 100644 index 3676064..0000000 --- a/site.webmanifest.sample +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "ANNU KUTE CED - Hub multimédia du podcast", - "short_name": "ANNU KUTE CED", - "description": "Hub multimédia du podcast ANNU KUTE CED", - "start_url": "/", - "display": "standalone", - "background_color": "#ffffff", - "theme_color": "#FF0000", - "orientation": "portrait-primary", - "icons": [ - { - "src": "img/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable any" - }, - { - "src": "img/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable any" - }, - { - "src": "img/apple-touch-icon.png", - "sizes": "180x180", - "type": "image/png" - }, - { - "src": "img/favicon-32x32.png", - "sizes": "32x32", - "type": "image/png" - }, - { - "src": "img/favicon-16x16.png", - "sizes": "16x16", - "type": "image/png" - } - ], - "categories": ["entertainment", "news", "social"], - "lang": "fr", - "scope": "/", - "prefer_related_applications": false -} diff --git a/sitemap.xml.sample b/sitemap.xml.sample deleted file mode 100644 index 5f973e6..0000000 --- a/sitemap.xml.sample +++ /dev/null @@ -1,70 +0,0 @@ - - - - https://annukuteced.buzz/ - daily - 1.0 - - - https://annukuteced.buzz/index - daily - 1.0 - - - https://annukuteced.buzz/index.php - daily - 1.0 - - - https://annukuteced.buzz/direct - hourly - 0.9 - - - https://annukuteced.buzz/direct.php - hourly - 0.9 - - - https://annukuteced.buzz/mentions-legales - monthly - 0.3 - - - https://annukuteced.buzz/mentions-legales.php - monthly - 0.3 - - - https://annukuteced.buzz/recherche - weekly - 0.8 - - - https://annukuteced.buzz/recherche.php - weekly - 0.8 - - - https://annukuteced.buzz/categories - weekly - 0.8 - - - https://annukuteced.buzz/categories.php - weekly - 0.8 - - - https://annukuteced.buzz/video - weekly - 0.7 - - - https://annukuteced.buzz/video.php - weekly - 0.7 - - - diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..6e8f2cf --- /dev/null +++ b/src/app.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..b3c21e8 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,9 @@ +import type { Handle } from '@sveltejs/kit'; + +export const handle: Handle = async ({ event, resolve }) => { + const firstSegment = event.url.pathname.split('/')[1]; + const lang = firstSegment === 'en' ? 'en' : 'fr'; + return resolve(event, { + transformPageChunk: ({ html }) => html.replaceAll('%lang%', lang) + }); +}; diff --git a/src/lib/components/AudioPlayer.svelte b/src/lib/components/AudioPlayer.svelte new file mode 100644 index 0000000..39d9eed --- /dev/null +++ b/src/lib/components/AudioPlayer.svelte @@ -0,0 +1,317 @@ + + + + +
+
    + {#each episodes as episode, i (episode.id)} +
  • + {#if episode.image} + {format(t.a11y.episodeCover, + {/if} +
    +

    + + {episode.title} + +

    +

    + {format(t.podcast.episodeFrom, { date: formatDate(episode.date, locale) })} + {#if episode.duration > 0}· {formatDuration(episode.duration)}{/if} +

    +
    + +
  • + {/each} +
+ + {#if current} +
+
+ {t.podcast.nowPlaying} + {current.title} +
+ +
+ {formatDuration(currentTime)} / {formatDuration(duration || current.duration)} + + {#if canSetVolume} + + {/if} +
+
+ {/if} +
+ + diff --git a/src/lib/components/CountdownClock.svelte b/src/lib/components/CountdownClock.svelte new file mode 100644 index 0000000..279c420 --- /dev/null +++ b/src/lib/components/CountdownClock.svelte @@ -0,0 +1,80 @@ + + +{#if remaining} +
+ {#each [['days', remaining.days], ['hours', remaining.hours], ['minutes', remaining.minutes], ['seconds', remaining.seconds]] as [key, value] (key)} +
+ {String(value).padStart(2, '0')} + {labels[key as keyof typeof labels]} +
+ {/each} +
+{/if} + + diff --git a/src/lib/components/Footer.svelte b/src/lib/components/Footer.svelte new file mode 100644 index 0000000..48fcd2c --- /dev/null +++ b/src/lib/components/Footer.svelte @@ -0,0 +1,235 @@ + + + + + + diff --git a/src/lib/components/HeroSection.svelte b/src/lib/components/HeroSection.svelte new file mode 100644 index 0000000..de5379c --- /dev/null +++ b/src/lib/components/HeroSection.svelte @@ -0,0 +1,109 @@ + + +{#if embedSrc} +
+ {#if hero.type === 'live' && live} +

+ + {t.home.liveNow} + {t.home.watchLive} +

+ {/if} +
+ +
+
+{/if} + + diff --git a/src/lib/components/Icon.svelte b/src/lib/components/Icon.svelte new file mode 100644 index 0000000..3418bfa --- /dev/null +++ b/src/lib/components/Icon.svelte @@ -0,0 +1,10 @@ + + + diff --git a/src/lib/components/MastodonTimeline.svelte b/src/lib/components/MastodonTimeline.svelte new file mode 100644 index 0000000..f9a9bca --- /dev/null +++ b/src/lib/components/MastodonTimeline.svelte @@ -0,0 +1,107 @@ + + +
    + {#each posts as post (post.id)} +
  1. +
    + + +
    + +
    {@html post.contentHtml}
    + {#if post.media.length > 0} +
    + {#each post.media as media (media.url)} + {media.description + {/each} +
    + {/if} +
  2. + {/each} +
+ +

+ + {t.home.seeMorePosts} + +

+ + diff --git a/src/lib/components/Nav.svelte b/src/lib/components/Nav.svelte new file mode 100644 index 0000000..0ec5a2c --- /dev/null +++ b/src/lib/components/Nav.svelte @@ -0,0 +1,219 @@ + + +
+
+
+ + + +
{t.nav.searchHelp}
+
+
+ + + +
+ {#if donations.enabled} + + + + {/if} + + {#if installPrompt} + + {/if} + +
+
+ + + diff --git a/src/lib/components/Seo.svelte b/src/lib/components/Seo.svelte new file mode 100644 index 0000000..b61c7f8 --- /dev/null +++ b/src/lib/components/Seo.svelte @@ -0,0 +1,63 @@ + + + + {title} + + + + + + + + + + + + + + + + + + + + + + + {#if jsonLdHtml} + + {@html ` + + + + diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte new file mode 100644 index 0000000..5b2fa4f --- /dev/null +++ b/src/lib/components/Sidebar.svelte @@ -0,0 +1,169 @@ + + + + + diff --git a/src/lib/components/VideoCard.svelte b/src/lib/components/VideoCard.svelte new file mode 100644 index 0000000..14e299a --- /dev/null +++ b/src/lib/components/VideoCard.svelte @@ -0,0 +1,147 @@ + + + + + diff --git a/src/lib/components/motion/FlagChip.svelte b/src/lib/components/motion/FlagChip.svelte new file mode 100644 index 0000000..8b6e844 --- /dev/null +++ b/src/lib/components/motion/FlagChip.svelte @@ -0,0 +1,51 @@ + + + diff --git a/src/lib/components/motion/KineticText.svelte b/src/lib/components/motion/KineticText.svelte new file mode 100644 index 0000000..7eee278 --- /dev/null +++ b/src/lib/components/motion/KineticText.svelte @@ -0,0 +1,67 @@ + + + + {#each words as word, i (`${i}-${word}`)} + + {/each} + + + diff --git a/src/lib/components/motion/ScrollProgressBar.svelte b/src/lib/components/motion/ScrollProgressBar.svelte new file mode 100644 index 0000000..6b3562d --- /dev/null +++ b/src/lib/components/motion/ScrollProgressBar.svelte @@ -0,0 +1,70 @@ + + +
+ + diff --git a/src/lib/config.ts b/src/lib/config.ts new file mode 100644 index 0000000..cc2e869 --- /dev/null +++ b/src/lib/config.ts @@ -0,0 +1,186 @@ +export const site = { + hostName: 'annukuteced.buzz', + baseUrl: 'https://annukuteced.buzz', + name: 'ANNU KUTE CED', + shortName: 'ANNU KUTE CED', + description: { + fr: 'Hub multimédia du podcast ANNU KUTE CED', + en: 'Multimedia hub for the ANNU KUTE CED podcast' + }, + logo: '/images/logo.png', + ogImage: '/images/android-chrome-512x512.png', + contactEmail: 'kontak@o-k-i.net', + licenseName: 'GNU Affero General Public License version 3 (AGPL-V3)', + licenseUrl: 'https://www.gnu.org/licenses/agpl-3.0.html', + sourceCodeUrl: 'https://labola.o-k-i.net/cedric/annu-kute-ced', + defaultTimezone: 'America/Guadeloupe' +} as const; + +export const legal = { + copyright: 'ANNU KUTE CED', + webmasterName: 'Cédric Famibelle-Pronzola', + webmasterEmail: 'contact@cedric-pronzola.dev', + hostName: 'o2Switch', + hostCompany: 'société au capital de 100 000 €', + hostRcs: 'immatriculée au RCS de Clermont-Ferrand sous le numéro 510 909 807', + hostAddress: '222 boulevard Gustave Flaubert, 63000 Clermont-Ferrand, France', + contactEmail: 'kontak@o-k-i.net' +} as const; + +export const peertube = { + url: 'https://gade.o-k-i.net', + displayName: 'gade.o-k-i.net', + channelHandle: 'annu_kute_ced', + channelUrl: 'https://gade.o-k-i.net/c/annu_kute_ced/videos', + liveAccountName: 'annu_kute_ced', + showVideoViews: false, + tagShort: 'short', + tagIndependence: 'indépendance', + shortsMaxDuration: 180, + counts: { + recent: 6, + trending: 6, + shorts: 6, + independence: 6, + category: 6, + perPage: 12, + search: 20, + searchPool: 200 + } +} as const; + +export const hero = { + type: 'live' as 'live' | 'video' | 'playlist' | 'none', + videoId: '1aJ2u9euwF9fWKQhFxwFio', + videoTitle: 'Vidéo de présentation', + playlistPlatform: 'peertube' as 'peertube' | 'funkwhale' | 'castopod', + playlistInstanceUrl: '', + playlistId: '', + playlistTitle: '', + playlistDescription: '' +}; + +export const castopod = { + enabled: true, + url: 'https://kute.o-k-i.net', + podcastSlugs: ['annu_kute_cedric'], + podcastUrl: 'https://kute.o-k-i.net/@annu_kute_cedric', + episodesCount: 5 +} as const; + +export const mastodon = { + instanceUrl: 'https://bokante.o-k-i.net', + accountUrl: 'https://bokante.o-k-i.net/@cedric', + accountAcct: 'cedric', + maxPosts: 10 +} as const; + +export const social = { + mastodon: mastodon.accountUrl, + facebook: '', + youtube: '', + instagram: '', + x: '', + tiktok: '' +} as const; + +export const donations = { + enabled: true, + liberapayUrl: '', + kofiUrl: '', + stripeEnabled: false, + amounts: [1, 5, 10, 20, 50], + currency: 'EUR', + stripeOneTimeLinks: {} as Record, + stripeMonthlyLinks: {} as Record +}; + +export const nextLive = { + enabled: false, + title: 'Prochain live', + description: 'Rejoignez-nous pour notre prochain live !', + date: '', + image: '/images/next-live.jpg', + timezones: { + "Ma'ohi Nui": 'Pacific/Tahiti', + 'Martinique / Guadeloupe': 'America/Martinique', + Guyane: 'America/Cayenne', + France: 'Europe/Paris', + Kanaky: 'Pacific/Noumea' + } as Record +}; + +export const categories: ReadonlyArray<{ id: number; fr: string; en: string }> = [ + { id: 11, fr: 'Actualité & Politique', en: 'News & Politics' }, + { id: 15, fr: 'Science & Technologie', en: 'Science & Technology' }, + { id: 4, fr: 'Jeux', en: 'Gaming' }, + { id: 9, fr: 'Comédie', en: 'Comedy' }, + { id: 10, fr: 'Divertissement', en: 'Entertainment' } +]; + +export const categoryNames: Readonly> = { + 1: { fr: 'Musique', en: 'Music' }, + 2: { fr: 'Films', en: 'Films' }, + 3: { fr: 'Véhicules', en: 'Vehicles' }, + 4: { fr: 'Jeux', en: 'Gaming' }, + 5: { fr: 'Sport', en: 'Sports' }, + 6: { fr: 'Humour', en: 'Humor' }, + 7: { fr: 'Art', en: 'Art' }, + 8: { fr: 'Personnalités', en: 'People' }, + 9: { fr: 'Comédie', en: 'Comedy' }, + 10: { fr: 'Divertissement', en: 'Entertainment' }, + 11: { fr: 'Actualité & Politique', en: 'News & Politics' }, + 12: { fr: 'Tutoriel', en: 'How-to' }, + 13: { fr: 'Éducation', en: 'Education' }, + 14: { fr: 'Activisme', en: 'Activism' }, + 15: { fr: 'Science & Technologie', en: 'Science & Technology' }, + 16: { fr: 'Animaux', en: 'Animals' }, + 17: { fr: 'Enfants', en: 'Kids' }, + 18: { fr: 'Cuisine', en: 'Cooking' } +}; + +export const categoryIcons: Readonly> = { + 1: 'mizik-note', + 2: 'zetwal', + 3: 'zetwal', + 4: 'zetwal', + 5: 'zetwal', + 6: 'pawol', + 7: 'jaden', + 8: 'glo', + 9: 'pawol', + 10: 'zetwal', + 11: 'lanme', + 12: 'kle', + 13: 'jaden', + 14: 'glo', + 15: 'kle', + 16: 'jaden', + 17: 'jaden', + 18: 'lakanmou' +}; + +export const importantTags = ['ANNUKUTECED', 'podcast', 'fediverse', 'audio', 'vidéo'] as const; + +export const popularTags = ['podcast', 'annukuteced', 'fediverse', 'audio'] as const; + +export const instances = [ + { + name: 'GADE', + software: 'PeerTube', + url: peertube.url, + icon: 'zetwal' + }, + { + name: 'KUTE', + software: 'Castopod', + url: castopod.url, + icon: 'ka' + }, + { + name: 'BOKANTE', + software: 'Mastodon', + url: mastodon.instanceUrl, + icon: 'lambi' + } +] as const; diff --git a/src/lib/i18n/en.json b/src/lib/i18n/en.json new file mode 100644 index 0000000..312a92d --- /dev/null +++ b/src/lib/i18n/en.json @@ -0,0 +1,164 @@ +{ + "meta": { + "homeTitle": "ANNU KUTE CED — Podcast multimedia hub", + "homeDescription": "Videos, audio episodes and news from the ANNU KUTE CED podcast: the PeerTube channel, the Castopod podcast and the Mastodon timeline gathered in one place, free from big platforms.", + "videoDescription": "Watch “{title}” on the ANNU KUTE CED multimedia hub.", + "categoryTitle": "{name} category", + "categoryDescription": "Videos from the {name} category on the ANNU KUTE CED hub.", + "searchTitle": "Search", + "searchDescription": "Search videos on the ANNU KUTE CED multimedia hub.", + "directTitle": "Live", + "directDescription": "The current ANNU KUTE CED live stream and the next live announcement.", + "donsTitle": "Support us", + "donsDescription": "Support the ANNU KUTE CED podcast: one-time or monthly donations.", + "legalTitle": "Legal notice", + "legalDescription": "Legal notice of the ANNU KUTE CED multimedia hub.", + "offlineTitle": "Offline", + "notFoundTitle": "Page not found" + }, + "nav": { + "home": "Home", + "direct": "Live", + "donate": "Support", + "categories": "Categories", + "hashtags": "Hashtags", + "searchPlaceholder": "Search…", + "searchLabel": "Video search", + "searchButton": "Run search", + "searchHelp": "Type keywords to search videos. Prefix with # to search a hashtag.", + "socialLabel": "Social networks", + "mainLabel": "Main navigation", + "openMenu": "Open navigation menu", + "closeMenu": "Close menu", + "themeToggle": "Toggle dark and light mode", + "installApp": "Install the app", + "langSwitch": "Version française", + "backHome": "Back to home", + "moreSocial": "More social networks", + "skipToContent": "Skip to content" + }, + "home": { + "shorts": "Shorts", + "recent": "Recent videos", + "trending": "Trending", + "podcast": "Latest episodes", + "podcastSubtitle": "The audio podcast on KUTE — Castopod", + "timeline": "News", + "timelineSubtitle": "The Mastodon timeline on BOKANTE", + "seeMore": "See more", + "seeAllVideos": "All videos on GADE", + "seeAllEpisodes": "All episodes on KUTE", + "seeMorePosts": "See more posts", + "popularTags": "Popular hashtags", + "about": "About", + "liveNow": "LIVE", + "watchLive": "Watch the live" + }, + "video": { + "views": "{count} views", + "publishedOn": "Published on {date}", + "download": "Download", + "share": "Share", + "copyLink": "Copy link", + "copied": "Link copied", + "embedCode": "Embed code", + "comments": "Comments", + "noComments": "No comments yet.", + "suggested": "Suggested videos", + "license": "License", + "category": "Category", + "tags": "Hashtags", + "watchOnPeertube": "Watch on GADE — PeerTube", + "duration": "Duration", + "by": "by {channel}" + }, + "category": { + "videosIn": "Videos in category", + "empty": "No video in this category yet." + }, + "search": { + "title": "Search", + "resultsFor": "Results for “{query}”", + "tagResults": "Videos with hashtag #{tag}", + "noResults": "No result for “{query}”.", + "hint": "Search by keywords or by hashtag with the # prefix.", + "loading": "Loading the search index…", + "resultCount": "{count} result(s)" + }, + "direct": { + "title": "Live", + "liveNow": "The live stream is on", + "noLive": "No live right now", + "noLiveText": "The next live has not been announced yet. Follow us on Mastodon to be notified.", + "nextLive": "Next live", + "followForNews": "Follow on Mastodon", + "inTimezones": "The schedule in your territory" + }, + "podcast": { + "listen": "Listen", + "play": "Play episode {title}", + "pause": "Pause", + "next": "Next episode", + "nowPlaying": "Now playing", + "episodeFrom": "Episode from {date}", + "mute": "Mute", + "unmute": "Unmute" + }, + "timeline": { + "reload": "Refresh", + "postFrom": "Post from {date}", + "mediaAlt": "Post media" + }, + "donate": { + "title": "Support ANNU KUTE CED", + "intro": "Budget should never be a barrier — but your donations help us maintain the podcast, the hosting and the free tools.", + "oneTime": "One-time donation", + "monthly": "Monthly donation", + "liberapay": "Support via Liberapay", + "kofi": "Support via Ko-fi", + "custom": "Custom amount", + "whyTitle": "Why donate", + "whyText": "Your donations fund ethical hosting, production time and the free tools of the ecosystem.", + "contact": "For other ways to support, write to us:", + "notConfigured": "The donations page is being set up. Contact us to support the podcast." + }, + "legal": { + "title": "Legal notice", + "editor": "Site publisher", + "host": "Hosting", + "contact": "Contact", + "license": "License", + "sourceCode": "Source code", + "data": "Personal data", + "dataText": "This site sets no tracking cookie and collects no personal data. Multimedia content is served by the ecosystem instances (GADE, KUTE, BOKANTE).", + "serviceDescription": "is the multimedia hub of the ANNU KUTE CED podcast." + }, + "footer": { + "contact": "CONTACT", + "categories": "Categories", + "hashtags": "Hashtags", + "legal": "Legal information", + "legalNotice": "Legal notice", + "sourceCode": "Source code", + "copyright": "Free license GNU AGPL-V3", + "federated": "A free service operated by ORGANISATION KA INTERNATIONALE", + "instances": "The ecosystem" + }, + "offline": { + "title": "Pa gen koneksyon", + "text": "You are offline. Pages you already visited remain available.", + "cta": "Back to home" + }, + "notFound": { + "title": "Paj la pa la", + "text": "This page does not exist or has been moved.", + "cta": "Back to home" + }, + "a11y": { + "videoThumbnail": "Thumbnail of video {title}", + "channelAvatar": "Avatar of channel {channel}", + "logo": "ANNU KUTE CED logo", + "episodeCover": "Cover of episode {title}", + "progressLabel": "Page reading progress" + } +} diff --git a/src/lib/i18n/fr.json b/src/lib/i18n/fr.json new file mode 100644 index 0000000..0685930 --- /dev/null +++ b/src/lib/i18n/fr.json @@ -0,0 +1,164 @@ +{ + "meta": { + "homeTitle": "ANNU KUTE CED — Hub multimédia du podcast", + "homeDescription": "Vidéos, épisodes audio et actualités du podcast ANNU KUTE CED : la chaîne PeerTube, le podcast Castopod et la timeline Mastodon réunis en un seul lieu, sans dépendre des grandes plateformes.", + "videoDescription": "Regarder « {title} » sur le hub multimédia ANNU KUTE CED.", + "categoryTitle": "Catégorie {name}", + "categoryDescription": "Vidéos de la catégorie {name} sur le hub ANNU KUTE CED.", + "searchTitle": "Recherche", + "searchDescription": "Rechercher des vidéos dans le hub multimédia ANNU KUTE CED.", + "directTitle": "Direct", + "directDescription": "Le direct d'ANNU KUTE CED en cours et l'annonce du prochain live.", + "donsTitle": "Soutenir", + "donsDescription": "Soutenir le podcast ANNU KUTE CED : dons ponctuels ou mensuels.", + "legalTitle": "Mentions légales", + "legalDescription": "Mentions légales du hub multimédia ANNU KUTE CED.", + "offlineTitle": "Hors ligne", + "notFoundTitle": "Page introuvable" + }, + "nav": { + "home": "Accueil", + "direct": "Direct", + "donate": "Soutenir", + "categories": "Catégories", + "hashtags": "Hashtags", + "searchPlaceholder": "Rechercher…", + "searchLabel": "Recherche de vidéos", + "searchButton": "Lancer la recherche", + "searchHelp": "Tapez vos mots-clés pour rechercher des vidéos. Préfixez par # pour chercher un hashtag.", + "socialLabel": "Réseaux sociaux", + "mainLabel": "Navigation principale", + "openMenu": "Ouvrir le menu de navigation", + "closeMenu": "Fermer le menu", + "themeToggle": "Basculer entre mode sombre et clair", + "installApp": "Installer l'application", + "langSwitch": "English version", + "backHome": "Retour à l'accueil", + "moreSocial": "Voir plus de réseaux sociaux", + "skipToContent": "Aller au contenu" + }, + "home": { + "shorts": "Shorts", + "recent": "Vidéos récentes", + "trending": "Tendances", + "podcast": "Derniers épisodes", + "podcastSubtitle": "Le podcast audio sur KUTE — Castopod", + "timeline": "Actualités", + "timelineSubtitle": "La timeline Mastodon sur BOKANTE", + "seeMore": "Voir plus", + "seeAllVideos": "Toutes les vidéos sur GADE", + "seeAllEpisodes": "Tous les épisodes sur KUTE", + "seeMorePosts": "Voir plus de posts", + "popularTags": "Hashtags populaires", + "about": "À propos", + "liveNow": "EN DIRECT", + "watchLive": "Regarder le direct" + }, + "video": { + "views": "{count} vues", + "publishedOn": "Publié le {date}", + "download": "Télécharger", + "share": "Partager", + "copyLink": "Copier le lien", + "copied": "Lien copié", + "embedCode": "Code d'intégration", + "comments": "Commentaires", + "noComments": "Aucun commentaire pour le moment.", + "suggested": "Vidéos suggérées", + "license": "Licence", + "category": "Catégorie", + "tags": "Hashtags", + "watchOnPeertube": "Regarder sur GADE — PeerTube", + "duration": "Durée", + "by": "par {channel}" + }, + "category": { + "videosIn": "Vidéos de la catégorie", + "empty": "Aucune vidéo dans cette catégorie pour le moment." + }, + "search": { + "title": "Recherche", + "resultsFor": "Résultats pour « {query} »", + "tagResults": "Vidéos avec le hashtag #{tag}", + "noResults": "Aucun résultat pour « {query} ».", + "hint": "Recherchez par mots-clés ou par hashtag avec le préfixe #.", + "loading": "Chargement de l'index de recherche…", + "resultCount": "{count} résultat(s)" + }, + "direct": { + "title": "Direct", + "liveNow": "Le direct est en cours", + "noLive": "Aucun direct en cours", + "noLiveText": "Le prochain live n'est pas encore annoncé. Suivez-nous sur Mastodon pour être prévenu.", + "nextLive": "Prochain live", + "followForNews": "Suivre sur Mastodon", + "inTimezones": "L'horaire dans votre territoire" + }, + "podcast": { + "listen": "Écouter", + "play": "Lire l'épisode {title}", + "pause": "Mettre en pause", + "next": "Épisode suivant", + "nowPlaying": "En cours de lecture", + "episodeFrom": "Épisode du {date}", + "mute": "Couper le son", + "unmute": "Rétablir le son" + }, + "timeline": { + "reload": "Rafraîchir", + "postFrom": "Post du {date}", + "mediaAlt": "Média du post" + }, + "donate": { + "title": "Soutenir ANNU KUTE CED", + "intro": "Le budget ne doit jamais être un frein — mais vos dons nous aident à maintenir le podcast, l'hébergement et les outils libres.", + "oneTime": "Don ponctuel", + "monthly": "Don mensuel", + "liberapay": "Soutenir via Liberapay", + "kofi": "Soutenir via Ko-fi", + "custom": "Montant libre", + "whyTitle": "Pourquoi donner", + "whyText": "Vos dons financent l'hébergement éthique, le temps de production et les outils libres de l'écosystème.", + "contact": "Pour d'autres moyens de soutien, écrivez-nous :", + "notConfigured": "La page de dons est en cours de configuration. Contactez-nous pour soutenir le podcast." + }, + "legal": { + "title": "Mentions légales", + "editor": "Éditeur du site", + "host": "Hébergement", + "contact": "Contact", + "license": "Licence", + "sourceCode": "Code source", + "data": "Données personnelles", + "dataText": "Ce site ne dépose aucun cookie de suivi et ne collecte aucune donnée personnelle. Les contenus multimédias sont servis par les instances de l'écosystème (GADE, KUTE, BOKANTE).", + "serviceDescription": "est le hub multimédia du podcast ANNU KUTE CED." + }, + "footer": { + "contact": "CONTACT", + "categories": "Catégories", + "hashtags": "Hashtags", + "legal": "Informations légales", + "legalNotice": "Mentions légales", + "sourceCode": "Code source", + "copyright": "Licence libre GNU AGPL-V3", + "federated": "Un service libre opéré par ORGANISATION KA INTERNATIONALE", + "instances": "L'écosystème" + }, + "offline": { + "title": "Pa gen koneksyon", + "text": "Vous êtes hors ligne. Les pages déjà visitées restent disponibles.", + "cta": "Retour à l'accueil" + }, + "notFound": { + "title": "Paj la pa la", + "text": "Cette page n'existe pas ou a été déplacée.", + "cta": "Retour à l'accueil" + }, + "a11y": { + "videoThumbnail": "Vignette de la vidéo {title}", + "channelAvatar": "Avatar de la chaîne {channel}", + "logo": "Logo ANNU KUTE CED", + "episodeCover": "Pochette de l'épisode {title}", + "progressLabel": "Progression de lecture de la page" + } +} diff --git a/src/lib/i18n/index.ts b/src/lib/i18n/index.ts new file mode 100644 index 0000000..e1c3e26 --- /dev/null +++ b/src/lib/i18n/index.ts @@ -0,0 +1,75 @@ +import { resolve } from '$app/paths'; +import type { Locale } from '$lib/types'; +import fr from './fr.json'; +import en from './en.json'; + +export type Bundle = typeof fr; + +const bundles: Record = { fr, en: en as Bundle }; + +export const locales: Locale[] = ['fr', 'en']; +export const defaultLocale: Locale = 'fr'; + +export function getBundle(locale: Locale): Bundle { + return bundles[locale] ?? bundles[defaultLocale]; +} + +export function localeFromPath(pathname: string): Locale { + return pathname.split('/')[1] === 'en' ? 'en' : 'fr'; +} + +export function stripLocale(pathname: string): string { + if (pathname === '/en') return '/'; + if (pathname.startsWith('/en/')) return pathname.slice(3); + return pathname; +} + +export function hrefFor(locale: Locale, path: string): string { + const clean = path.startsWith('/') ? path : `/${path}`; + const full = locale === 'en' ? `/en${clean === '/' ? '/' : clean}` : clean; + const [pathname, rest = ''] = full.split(/(?=[?#])/); + const withSlash = pathname.endsWith('/') ? pathname : `${pathname}/`; + // Chemin interne calculé (locale préfixée) : cast requis par le typage des routes + return resolve(`${withSlash}${rest}` as '/'); +} + +export function alternatePath(pathname: string, target: Locale): string { + const bare = stripLocale(pathname); + return hrefFor(target, bare); +} + +export function t(template: string, vars: Record = {}): string { + return template.replace(/\{(\w+)\}/g, (_, key) => String(vars[key] ?? `{${key}}`)); +} + +export function formatDate(dateString: string, locale: Locale): string { + const date = new Date(dateString); + if (Number.isNaN(date.getTime())) return dateString; + return new Intl.DateTimeFormat(locale === 'en' ? 'en-GB' : 'fr-FR', { + day: 'numeric', + month: 'long', + year: 'numeric' + }).format(date); +} + +export function formatDuration(seconds: number): string { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = Math.floor(seconds % 60); + if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; + return `${m}:${String(s).padStart(2, '0')}`; +} + +export function formatViewCount(views: number): string { + if (views >= 1_000_000) return `${(views / 1_000_000).toFixed(1)}M`; + if (views >= 1_000) return `${(views / 1_000).toFixed(1)}K`; + return String(views); +} + +export function formatFileSize(bytes: number): string { + if (bytes <= 0) return ''; + const units = ['o', 'Ko', 'Mo', 'Go']; + const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); + const value = bytes / 1024 ** exponent; + return `${value.toFixed(value >= 10 || exponent === 0 ? 0 : 1)} ${units[exponent]}`; +} diff --git a/src/lib/motion/reveal.ts b/src/lib/motion/reveal.ts new file mode 100644 index 0000000..ce0b729 --- /dev/null +++ b/src/lib/motion/reveal.ts @@ -0,0 +1,27 @@ +import { prefersReducedMotion, syncopatedDelay } from './tokens'; + +export function reveal(node: HTMLElement, index = 0) { + if (prefersReducedMotion()) return; + + node.classList.add('reveal'); + node.style.setProperty('--d', `${syncopatedDelay(index)}ms`); + + const observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + node.classList.add('is-visible'); + observer.disconnect(); + } + } + }, + { threshold: 0.1, rootMargin: '0px 0px -8% 0px' } + ); + observer.observe(node); + + return { + destroy() { + observer.disconnect(); + } + }; +} diff --git a/src/lib/motion/tokens.ts b/src/lib/motion/tokens.ts new file mode 100644 index 0000000..525e115 --- /dev/null +++ b/src/lib/motion/tokens.ts @@ -0,0 +1,11 @@ +export function prefersReducedMotion(): boolean { + if (typeof window === 'undefined') return true; + return window.matchMedia('(prefers-reduced-motion: reduce)').matches; +} + +const SYNCOPATED_PATTERN = [0, 120, 300, 360, 600, 660, 900, 960]; + +export function syncopatedDelay(index: number): number { + const cycle = Math.floor(index / SYNCOPATED_PATTERN.length); + return SYNCOPATED_PATTERN[index % SYNCOPATED_PATTERN.length] + cycle * 1200; +} diff --git a/src/lib/server/castopod.ts b/src/lib/server/castopod.ts new file mode 100644 index 0000000..f648bae --- /dev/null +++ b/src/lib/server/castopod.ts @@ -0,0 +1,84 @@ +import { castopod } from '$lib/config'; +import { fetchText } from './http'; +import type { Episode } from '$lib/types'; + +function tag(xml: string, name: string): string { + const cdata = new RegExp(`<${name}[^>]*>`, 'i'); + const plain = new RegExp(`<${name}[^>]*>([\\s\\S]*?)`, 'i'); + const match = xml.match(cdata) ?? xml.match(plain); + return match?.[1]?.trim() ?? ''; +} + +function attr(xml: string, tagName: string, attrName: string): string { + const tagMatch = xml.match(new RegExp(`<${tagName}\\b[^>]*>`, 'i')); + if (!tagMatch) return ''; + const attrMatch = tagMatch[0].match(new RegExp(`${attrName}="([^"]*)"`, 'i')); + return attrMatch?.[1] ?? ''; +} + +function stripTags(html: string): string { + return html + .replace(/<[^>]+>/g, ' ') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/�?39;|'/g, "'") + .replace(/\s+/g, ' ') + .trim(); +} + +function parseDuration(raw: string): number { + if (!raw) return 0; + const parts = raw.split(':').map(Number); + if (parts.some(Number.isNaN)) return 0; + if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]; + if (parts.length === 2) return parts[0] * 60 + parts[1]; + return parts[0] ?? 0; +} + +async function getEpisodesForSlug(slug: string): Promise { + const feedUrl = `${castopod.url}/@${slug}/feed`; + const xml = await fetchText(feedUrl); + if (!xml) return []; + + const channelImage = + tag(xml.split(']/i).slice(1); + + return items.map((item, index) => { + const body = item.split('')[0] ?? item; + const title = tag(body, 'title'); + const link = tag(body, 'link'); + const guid = tag(body, 'guid') || link || `${slug}-${index}`; + const description = stripTags(tag(body, 'description') || tag(body, 'itunes:summary')); + return { + id: guid, + title, + description, + audioUrl: attr(body, 'enclosure', 'url'), + audioType: attr(body, 'enclosure', 'type') || 'audio/mpeg', + duration: parseDuration(tag(body, 'itunes:duration')), + date: tag(body, 'pubDate'), + image: attr(body, 'itunes:image', 'href') || channelImage || null, + link, + podcast: slug + }; + }); +} + +export async function getCastopodEpisodes(count = castopod.episodesCount): Promise { + const all: Episode[] = []; + for (const slug of castopod.podcastSlugs) { + const episodes = await getEpisodesForSlug(slug); + all.push(...episodes); + if (castopod.podcastSlugs.length > 1) { + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + } + return all + .filter((episode) => episode.audioUrl) + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + .slice(0, count); +} diff --git a/src/lib/server/http.ts b/src/lib/server/http.ts new file mode 100644 index 0000000..b191d79 --- /dev/null +++ b/src/lib/server/http.ts @@ -0,0 +1,108 @@ +const UA = 'annu-kute-ced/2.0 (+https://annukuteced.buzz)'; +const MAX_CONCURRENT = 4; +const MAX_RETRIES = 6; + +let active = 0; +const waiting: (() => void)[] = []; +const cache = new Map>(); + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function limited(fn: () => Promise): Promise { + if (active >= MAX_CONCURRENT) { + await new Promise((resolve) => waiting.push(resolve)); + } + active++; + try { + return await fn(); + } finally { + active--; + waiting.shift()?.(); + } +} + +async function doFetch(url: string, accept: string, attempt = 0): Promise { + try { + const res = await fetch(url, { + headers: { 'User-Agent': UA, Accept: accept }, + signal: AbortSignal.timeout(30_000) + }); + if (res.status === 429 && attempt < MAX_RETRIES) { + const retryAfter = Number(res.headers.get('retry-after')) || (attempt + 1) * 2; + console.warn(`[data] HTTP 429 pour ${url} — nouvelle tentative dans ${retryAfter}s`); + await sleep(retryAfter * 1000); + return doFetch(url, accept, attempt + 1); + } + if (!res.ok) { + console.error(`[data] HTTP ${res.status} for ${url}`); + return null; + } + return await res.text(); + } catch (err) { + if (attempt < 2) { + await sleep(1000 * (attempt + 1)); + return doFetch(url, accept, attempt + 1); + } + console.error(`[data] fetch failed for ${url}:`, err); + return null; + } +} + +function cached(url: string, accept: string): Promise { + let pending = cache.get(url); + if (!pending) { + pending = limited(() => doFetch(url, accept)); + cache.set(url, pending); + } + return pending; +} + +export async function fetchJson(url: string): Promise { + const text = await cached(url, 'application/json'); + if (text === null) return null; + try { + return JSON.parse(text) as T; + } catch { + console.error(`[data] invalid JSON for ${url}`); + return null; + } +} + +export async function fetchText(url: string): Promise { + return cached(url, 'text/xml, application/rss+xml, text/plain, */*'); +} + +export function escapeHtml(input: string): string { + return input + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +export function sanitizeHtml(html: string): string { + return html + .replace(/<(script|style|iframe|object|embed|form|link|meta)[^>]*>[\s\S]*?<\/\1>/gi, '') + .replace(/<(script|style|iframe|object|embed|form|link|meta)[^>]*\/?>/gi, '') + .replace(/\son\w+="[^"]*"/gi, '') + .replace(/\son\w+='[^']*'/gi, '') + .replace(/(href|src)="javascript:[^"]*"/gi, '$1="#"'); +} + +export function markdownToHtml(markdown: string): string { + const escaped = escapeHtml(markdown.trim()); + const blocks = escaped.split(/\n{2,}/); + return blocks + .map((block) => { + let html = block + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/\*([^*]+)\*/g, '$1') + .replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '$1') + .replaceAll('\n', '
'); + return `

${html}

`; + }) + .join(''); +} diff --git a/src/lib/server/mastodon.ts b/src/lib/server/mastodon.ts new file mode 100644 index 0000000..1a25070 --- /dev/null +++ b/src/lib/server/mastodon.ts @@ -0,0 +1,57 @@ +import { mastodon } from '$lib/config'; +import { fetchJson, sanitizeHtml } from './http'; +import type { Post } from '$lib/types'; + +interface MastodonMedia { + url?: string; + preview_url?: string; + description?: string; + type?: string; +} + +interface MastodonStatus { + id: string; + url?: string; + created_at?: string; + content?: string; + reblog?: MastodonStatus | null; + media_attachments?: MastodonMedia[]; + replies_count?: number; + reblogs_count?: number; + favourites_count?: number; +} + +interface MastodonAccount { + id: string; +} + +export async function getMastodonPosts(count = mastodon.maxPosts): Promise { + const account = await fetchJson( + `${mastodon.instanceUrl}/api/v1/accounts/lookup?acct=${encodeURIComponent(mastodon.accountAcct)}` + ); + if (!account?.id) return []; + + const statuses = await fetchJson( + `${mastodon.instanceUrl}/api/v1/accounts/${account.id}/statuses?limit=${count}&exclude_replies=true` + ); + + return (statuses ?? []).map((status) => { + const source = status.reblog ?? status; + return { + id: status.id, + url: status.url ?? mastodon.accountUrl, + date: status.created_at ?? '', + contentHtml: sanitizeHtml(source.content ?? ''), + media: (source.media_attachments ?? []) + .filter((media) => media.url) + .map((media) => ({ + url: media.url as string, + previewUrl: media.preview_url ?? null, + description: media.description ?? '' + })), + repliesCount: status.replies_count ?? 0, + reblogsCount: status.reblogs_count ?? 0, + favouritesCount: status.favourites_count ?? 0 + }; + }); +} diff --git a/src/lib/server/peertube.ts b/src/lib/server/peertube.ts new file mode 100644 index 0000000..7eb243d --- /dev/null +++ b/src/lib/server/peertube.ts @@ -0,0 +1,235 @@ +import { peertube } from '$lib/config'; +import { fetchJson, markdownToHtml, sanitizeHtml } from './http'; +import type { + DownloadOption, + SearchEntry, + VideoComment, + VideoDetails, + VideoSummary +} from '$lib/types'; + +const BASE = peertube.url; + +interface PtAvatar { + path?: string; +} + +interface PtVideo { + uuid: string; + name: string; + previewPath?: string; + duration: number; + views: number; + publishedAt: string; + aspectRatio?: number; + description?: string; + tags?: string[]; + isLive?: boolean; + channel?: { displayName?: string; avatars?: PtAvatar[] }; +} + +interface PtVideoList { + data?: PtVideo[]; + total?: number; +} + +async function callApi(endpoint: string, params: Record = {}) { + const query = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) query.set(key, String(value)); + const url = `${BASE}/api/v1/${endpoint}${query.size ? `?${query}` : ''}`; + return fetchJson(url); +} + +function formatVideos(videos: PtVideo[] = []): VideoSummary[] { + return videos.map((video) => ({ + id: video.uuid, + title: video.name, + thumbnail: video.previewPath ? `${BASE}${video.previewPath}` : '/images/logo.png', + duration: video.duration, + channel: video.channel?.displayName ?? '', + channelAvatar: video.channel?.avatars?.[0]?.path + ? `${BASE}${video.channel.avatars[0].path}` + : '/images/logo.png', + views: video.views, + date: video.publishedAt, + aspectRatio: video.aspectRatio ?? 16 / 9, + description: video.description ?? '', + tags: video.tags ?? [], + isLive: video.isLive ?? false + })); +} + +export async function getRecentVideos(count: number = peertube.counts.recent): Promise { + const data = await callApi('videos', { sort: '-publishedAt', count, isLocal: true }); + return formatVideos(data?.data); +} + +export async function getTrendingVideos(count: number = peertube.counts.trending): Promise { + const data = await callApi('videos', { sort: '-trending', count, isLocal: true }); + return formatVideos(data?.data); +} + +export async function getVideosByTag(tag: string, count: number): Promise { + const data = await callApi('videos', { tagsOneOf: tag, count, isLocal: true }); + return formatVideos(data?.data); +} + +export async function getShorts(count: number = peertube.counts.shorts): Promise { + const data = await callApi('videos', { + sort: '-publishedAt', + count: 100, + isLocal: true + }); + const shorts = formatVideos(data?.data).filter( + (video) => video.duration < peertube.shortsMaxDuration && video.aspectRatio <= 1 + ); + return shorts.slice(0, count); +} + +export async function getIndependenceVideos( + count: number = peertube.counts.independence +): Promise { + return getVideosByTag(peertube.tagIndependence, count); +} + +export async function getVideosByCategory( + categoryId: number, + count: number = peertube.counts.category +): Promise { + const data = await callApi('videos', { + categoryOneOf: categoryId, + sort: '-publishedAt', + count, + isLocal: true + }); + return formatVideos(data?.data); +} + +export async function getLiveStream(): Promise { + const data = await callApi(`video-channels/${peertube.channelHandle}/videos`, { + count: 1, + isLocal: true, + isLive: true, + sort: '-publishedAt' + }); + const live = formatVideos(data?.data).find((video) => video.isLive); + return live ?? null; +} + +export async function getAllVideos(maxCount: number = peertube.counts.searchPool): Promise { + const videos: VideoSummary[] = []; + let start = 0; + const pageSize = 100; + while (videos.length < maxCount) { + const data = await callApi('videos', { + sort: '-publishedAt', + count: pageSize, + start, + isLocal: true + }); + const page = formatVideos(data?.data); + videos.push(...page); + if (page.length < pageSize) break; + start += pageSize; + } + return videos.slice(0, maxCount); +} + +export async function getSearchIndex(): Promise { + const videos = await getAllVideos(); + return videos.map((video) => ({ + id: video.id, + title: video.title, + description: video.description, + tags: video.tags, + thumbnail: video.thumbnail, + duration: video.duration, + date: video.date, + channel: video.channel + })); +} + +interface PtVideoDetails extends PtVideo { + category?: { id?: number; label?: string }; + licence?: { id?: number; label?: string }; + language?: { id?: string; label?: string }; + files?: { resolution?: { id?: number; label?: string }; size?: number; fileDownloadUrl?: string }[]; + streamingPlaylists?: { playlistUrl?: string; files?: { resolution?: { id?: number }; size?: number }[] }[]; +} + +const CREATIVE_COMMONS: Record = { + 1: { label: 'CC-BY', url: 'https://creativecommons.org/licenses/by/4.0/' }, + 2: { label: 'CC-BY-SA', url: 'https://creativecommons.org/licenses/by-sa/4.0/' }, + 3: { label: 'CC-BY-ND', url: 'https://creativecommons.org/licenses/by-nd/4.0/' }, + 4: { label: 'CC-BY-NC', url: 'https://creativecommons.org/licenses/by-nc/4.0/' }, + 5: { label: 'CC-BY-NC-SA', url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/' }, + 6: { label: 'CC-BY-NC-ND', url: 'https://creativecommons.org/licenses/by-nc-nd/4.0/' }, + 7: { label: 'CC0', url: 'https://creativecommons.org/publicdomain/zero/1.0/' } +}; + +export async function getVideoById(uuid: string): Promise { + const video = await fetchJson(`${BASE}/api/v1/videos/${uuid}`); + if (!video?.uuid) return null; + + const [summary] = formatVideos([video]); + const downloads: DownloadOption[] = []; + + for (const file of video.files ?? []) { + if (!file.fileDownloadUrl) continue; + downloads.push({ + label: file.resolution?.label ?? `${file.resolution?.id ?? '?'}p`, + url: file.fileDownloadUrl, + size: file.size ?? 0, + resolution: file.resolution?.id ?? 0, + type: 'file' + }); + } + + for (const playlist of video.streamingPlaylists ?? []) { + if (!playlist.playlistUrl) continue; + const size = (playlist.files ?? []).reduce((sum, f) => sum + (f.size ?? 0), 0); + downloads.push({ + label: 'HLS', + url: playlist.playlistUrl, + size, + resolution: 0, + type: 'hls' + }); + } + + const licence = video.licence?.id ? CREATIVE_COMMONS[video.licence.id] : undefined; + + return { + ...summary, + descriptionHtml: markdownToHtml(video.description ?? ''), + category: video.category?.id ?? null, + licence: licence ?? null, + embedUrl: `${BASE}/videos/embed/${video.uuid}`, + downloads, + language: video.language?.label ?? null + }; +} + +interface PtComment { + id: string | number; + text?: string; + createdAt?: string; + url?: string; + account?: { displayName?: string; url?: string; avatars?: PtAvatar[] }; +} + +export async function getVideoComments(uuid: string): Promise { + const data = await fetchJson<{ data?: PtComment[] }>( + `${BASE}/api/v1/videos/${uuid}/comment-threads?sort=-createdAt` + ); + return (data?.data ?? []).map((comment) => ({ + id: String(comment.id), + author: comment.account?.displayName ?? '', + authorAvatar: comment.account?.avatars?.[0]?.path + ? `${BASE}${comment.account.avatars[0].path}` + : null, + authorUrl: comment.account?.url ?? null, + date: comment.createdAt ?? '', + text: sanitizeHtml(comment.text ?? '') + })); +} diff --git a/src/lib/styles/base.css b/src/lib/styles/base.css new file mode 100644 index 0000000..081ada4 --- /dev/null +++ b/src/lib/styles/base.css @@ -0,0 +1,285 @@ +*, +*::before, +*::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: var(--font-body); + line-height: 1.5; + color: var(--fg); + background-color: var(--bg); + overflow-x: hidden; + min-height: 100dvh; + display: flex; + flex-direction: column; +} + +img, +video, +svg { + max-width: 100%; + height: auto; +} + +a { + color: var(--fg); + text-decoration: none; +} + +main a[href^="http"], +main p a, +main li a { + text-decoration: underline; + text-underline-offset: 0.15em; +} + +button { + font: inherit; + color: inherit; + background: none; + border: none; + cursor: pointer; +} + +input, +select, +textarea { + font: inherit; +} + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.sr-only { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; +} + +.skip-link { + position: absolute; + top: -100px; + left: var(--space-2); + z-index: 300; + padding: var(--space-1) var(--space-2); + background: var(--accent); + color: #0d0d0d; + font-family: var(--font-display); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.03em; + border-radius: var(--radius-sm); + transition: top var(--dur-tanbou) var(--ease-ka); +} + +.skip-link:focus { + top: var(--space-1); +} + +.container { + width: 100%; + max-width: var(--container); + margin: 0 auto; + padding: 0 var(--space-3); +} + +.section { + padding: var(--space-4) 0; +} + +.section-title { + font-family: var(--font-display); + font-weight: 800; + text-transform: uppercase; + letter-spacing: -0.01em; + font-size: clamp(1.4rem, 3vw, 2rem); + display: flex; + align-items: center; + gap: var(--space-2); + margin-bottom: var(--space-3); +} + +/* Flag-bar — signature n°1 : 6 px, 4 segments francs */ +.flag-bar { + height: 6px; + background: linear-gradient( + to right, + var(--noir-oki) 0 25%, + var(--or-oki) 25% 50%, + var(--vert-oki) 50% 75%, + var(--rouge-oki) 75% 100% + ); +} + +html.light-theme .flag-bar { + background: linear-gradient( + to right, + #000 0 25%, + var(--or-oki) 25% 50%, + var(--vert-oki) 50% 75%, + var(--rouge-oki) 75% 100% + ); +} + +/* Bouton canonique : balayage or */ +.btn { + display: inline-flex; + align-items: center; + gap: var(--space-1); + position: relative; + overflow: hidden; + padding: var(--space-1) var(--space-3); + font-family: var(--font-display); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.03em; + font-size: 0.9rem; + color: var(--accent); + border: var(--border-btn); + border-radius: var(--radius-sm); + transition: color var(--dur-tanbou) var(--ease-syncope); +} + +.btn::after { + content: ''; + position: absolute; + inset: 0; + background: var(--accent); + transform: scaleX(0); + transform-origin: left; + transition: transform var(--dur-tanbou) var(--ease-syncope); + z-index: 0; +} + +.btn:hover, +.btn:focus-visible { + color: #0d0d0d; +} + +.btn:hover::after, +.btn:focus-visible::after { + transform: scaleX(1); +} + +.btn > * { + position: relative; + z-index: 1; +} + +.btn--solid { + background: var(--accent); + color: #0d0d0d; +} + +.btn--solid::after { + background: var(--or-clair); +} + +/* Carte canonique */ +.card { + background: var(--card-bg); + border: var(--border-card); + border-left: 4px solid var(--accent); + border-radius: var(--radius-md); + transition: + transform var(--dur-tanbou) var(--ease-ka), + border-color var(--dur-tanbou) var(--ease-ka); +} + +.card:hover { + transform: translateY(-2px); + border-color: var(--accent); +} + +/* Tag canonique */ +.tag { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.15rem 0.5rem; + font-size: 0.8rem; + background: color-mix(in srgb, var(--or-oki) 8%, transparent); + border: 1px solid var(--or-oki); + border-radius: var(--radius-sm); + color: var(--fg); +} + +/* Icônes */ +.icon { + width: 1.25em; + height: 1.25em; + flex-shrink: 0; + color: var(--accent); + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: square; + stroke-linejoin: miter; +} + +.icon--inline { + width: 1em; + height: 1em; + vertical-align: -0.15em; +} + +.text-muted { + color: var(--muted); +} + +.grid-cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: var(--space-3); +} + +/* Reveal — état caché uniquement sous html.js + motion autorisé */ +html.js .reveal { + opacity: 0; + transform: translateY(16px); + transition: + opacity var(--dur-mesure) var(--ease-ka), + transform var(--dur-mesure) var(--ease-ka); + transition-delay: var(--d, 0ms); +} + +html.js .reveal.is-visible { + opacity: 1; + transform: none; +} + +/* Gate unique prefers-reduced-motion */ +@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; + transition-delay: 0ms !important; + } + + html.js .reveal { + opacity: 1; + transform: none; + } +} diff --git a/src/lib/styles/oki-tokens.css b/src/lib/styles/oki-tokens.css new file mode 100644 index 0000000..dc78cb9 --- /dev/null +++ b/src/lib/styles/oki-tokens.css @@ -0,0 +1,72 @@ +:root { + /* Noyau (thème sombre = défaut) */ + --noir-oki: #0d0d0d; + --noir-profond: #1a0f1a; + --blanc-creme: #fff8e7; + --line: rgba(255, 255, 255, 0.1); + --or-oki: #fdb813; + --rouge-oki: #ff1654; + + /* Étendue (parcimonie) */ + --vert-oki: #00d66c; + --turquoise-caraibes: #00ced1; + --jaune-soleil: #ffd700; + --orange-flamme: #ff6b35; + --violet-nuit: #6b2d5c; + --bleu-ocean: #0077be; + --or-clair: #ffe066; + --gris-sombre: #2d1b2e; + + /* Sémantique dérivée */ + --muted: color-mix(in srgb, var(--blanc-creme) 70%, transparent); + --card-bg: rgba(255, 255, 255, 0.03); + + /* Typographie */ + --font-display: 'Archivo', 'Arial Black', sans-serif; + --font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + + /* Formes, espacements, layout */ + --radius-sm: 3px; + --radius-md: 6px; + --border-card: 1px solid var(--line); + --border-btn: 2px solid var(--or-oki); + --space-1: 0.75rem; + --space-2: 1rem; + --space-3: 1.5rem; + --space-4: 2rem; + --space-5: 4rem; + --container: 1200px; + + /* Motion — cadences gwoka */ + --ease-ka: cubic-bezier(0.22, 1, 0.36, 1); + --ease-syncope: cubic-bezier(0.65, 0, 0.35, 1); + --dur-tanbou: 120ms; + --dur-mesure: 480ms; + --dur-phrase: 960ms; + + /* Application */ + --bg: var(--noir-oki); + --surface: var(--noir-profond); + --fg: var(--blanc-creme); + --accent: var(--or-oki); + --header-h: 56px; + --sidebar-w: 240px; +} + +/* Thème clair — opt-in, accents assombris WCAG AA */ +html.light-theme { + --noir-oki: #fff8e7; + --blanc-creme: #0d0d0d; + --noir-profond: #f5f0e8; + --gris-sombre: #e8ddd0; + --or-oki: #b87a00; + --vert-oki: #006b3d; + --rouge-oki: #a01030; + --turquoise-caraibes: #006b75; + --bleu-ocean: #004b7f; + --or-clair: #d99000; + --jaune-soleil: #b87a00; + --orange-flamme: #c85000; + --line: rgba(0, 0, 0, 0.12); + --card-bg: rgba(0, 0, 0, 0.03); +} diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..34be962 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,82 @@ +export interface VideoSummary { + id: string; + title: string; + thumbnail: string; + duration: number; + channel: string; + channelAvatar: string; + views: number; + date: string; + aspectRatio: number; + description: string; + tags: string[]; + isLive: boolean; +} + +export interface DownloadOption { + label: string; + url: string; + size: number; + resolution: number; + type: 'file' | 'hls'; +} + +export interface VideoComment { + id: string; + author: string; + authorAvatar: string | null; + authorUrl: string | null; + date: string; + text: string; +} + +export interface VideoDetails extends VideoSummary { + descriptionHtml: string; + category: number | null; + licence: { label: string; url: string } | null; + embedUrl: string; + downloads: DownloadOption[]; + language: string | null; +} + +export interface Episode { + id: string; + title: string; + description: string; + audioUrl: string; + audioType: string; + duration: number; + date: string; + image: string | null; + link: string; + podcast: string; +} + +export interface Post { + id: string; + url: string; + date: string; + contentHtml: string; + media: { url: string; previewUrl: string | null; description: string }[]; + repliesCount: number; + reblogsCount: number; + favouritesCount: number; +} + +export interface CategoryInfo { + id: number; + name: string; +} + +export interface SearchEntry { + id: string; + title: string; + description: string; + tags: string[]; + thumbnail: string; + duration: number; + date: string; + channel: string; +} + +export type Locale = 'fr' | 'en'; diff --git a/src/params/locale.ts b/src/params/locale.ts new file mode 100644 index 0000000..84d0014 --- /dev/null +++ b/src/params/locale.ts @@ -0,0 +1,3 @@ +import type { ParamMatcher } from '@sveltejs/kit'; + +export const match: ParamMatcher = (param) => param === 'en'; diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100644 index 0000000..c83cf55 --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,61 @@ + + + + {page.status} — {t.meta.notFoundTitle} + + + + +
+ +

{page.status}

+

{t.notFound.title}

+

{t.notFound.text}

+ + {t.notFound.cta} + +
+ + + diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts new file mode 100644 index 0000000..ba58d86 --- /dev/null +++ b/src/routes/+layout.ts @@ -0,0 +1,2 @@ +export const prerender = true; +export const trailingSlash = 'always'; diff --git a/src/routes/[[locale=locale]]/+layout.svelte b/src/routes/[[locale=locale]]/+layout.svelte new file mode 100644 index 0000000..a683001 --- /dev/null +++ b/src/routes/[[locale=locale]]/+layout.svelte @@ -0,0 +1,175 @@ + + + + + + + + + +