Compare commits

...

1 Commits

Author SHA1 Message Date
sucupira 2947482f50 feat: migration complète 11ty → SvelteKit (v3)
Deploy to GitHub Pages / build (push) Failing after 23s
Deploy to GitHub Pages / deploy (push) Has been skipped
- SvelteKit 2 + Svelte 5 runes + TS, adapter-static, paths.base '/gwada-sirius' (relative: false)
- 5 pages × 3 locales (/, /en/, /ht/) : catalogues inlang repris tels quels, routeur i18n maison avec fallback ht→fr
- Îlots → composants natifs avec fallback SSR (calculateur, carte Leaflet, globe) ; Leaflet en import dynamique
- Dette corrigée : aria-current (bug legacy), polices en double, SEO complet (Seo, hreflang ×3, OG, Twitter, robots, sitemap 15 URLs)
- PWA : manifest + service-worker v3 (stale-while-revalidate)
- CI GitHub Pages mise à jour (build/), README et NOTES_PROJET réécrits
- Lighthouse mobile : 98/95/100/100 — svelte-check 0/0, Leaflet vérifié (2 cartes, tuiles OSM), no-JS vérifié
2026-07-21 14:41:24 -04:00
97 changed files with 3493 additions and 4926 deletions
+1 -3
View File
@@ -31,13 +31,11 @@ jobs:
- name: Build
run: npm run build
env:
BASE_PATH: /${{ github.event.repository.name }}
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./_site
path: ./build
deploy:
environment:
+4 -1
View File
@@ -37,4 +37,7 @@ Thumbs.db
# Temporaires
*.tmp
*.temp
.cache/
.cache/
# Mémo local (infra, ne pas pousser)
GITEA.md
qa.tmp.mjs
+16
View File
@@ -2,6 +2,22 @@
## 📋 Vue d'ensemble
**Version 3 (branche `svelte`)** : migration complète 11ty → **SvelteKit 2 + Svelte 5 + TypeScript**, pré-rendu statique intégral (`adapter-static`). Les îlots deviennent des composants natifs avec fallback SSR (fin du montage manuel par `mount.js`/IntersectionObserver). L'i18n Paraglide est remplacée par un routeur maison (`src/lib/i18n/index.ts`) qui consomme **les mêmes catalogues** `src/lib/i18n/{fr,en,ht}.json` (format inlang conservé, éditables avec les outils inlang) : FR à `/`, EN `/en/`, HT `/ht/`, fallback fr automatique. Le sous-répertoire GitHub Pages est géré nativement par `paths.base: '/gwada-sirius'` (les contournements v2 — filtre `withBase`, plugin `restorePassthroughCopy`, alias `/src` — ont disparu). La v2 (11ty) est décrite ci-dessous à titre historique.
**Ce qui change en v3 par rapport aux notes v2 :**
- §1 i18n : plus de compilation Paraglide — les catalogues JSON sont importés tels quels, `t(locale, key, vars?)` gère le fallback ht→fr.
- §2 alias `/src` : sans objet (plus de Vite-sur-sortie).
- §6 BASE_PATH : remplacé par `paths.base` en dur dans `svelte.config.js` ; liens internes via `resolve()` de `$app/paths`, assets via `%sveltekit.assets%`/`asset()`.
- Polices : déclarées une seule fois (`static/fonts/fonts.css`) — la duplication passthrough/bundle de la v2 est corrigée.
- `aria-current` : bug corrigé (comparaison pathname réel, voir `Nav.svelte`).
- Le piège UTC-4 (§3) reste d'actualité : il vit dans `src/lib/dates.ts` et `SiriusPredictions.svelte` — parse par composants, jamais `new Date("YYYY-MM-DD")`.
- §5 cache : la discipline d'incrémentation de `CACHE_NAME` s'applique à `src/service-worker.js` (actuellement `gwada-sirius-v3`).
- SEO ajouté en v3 : `Seo.svelte` (canonical, hreflang ×3, OG, Twitter), `static/robots.txt`, `static/sitemap.xml` (15 URLs).
---
## Historique — v2 (11ty)
Refonte complète (v2) : passage d'une SPA Vite+Svelte 4 à un site **11ty** statique avec **Svelte 5 en îlots**, un design system CSS natif (oklch, `@layer`, container queries) et une i18n compilée (Paraglide/inlang). Conforme à la doctrine UI/UX OKI : nav à 5 entrées, fonctionne sans JavaScript pour tout le contenu éditorial, budget JS minimal.
## 🏗️ Architecture
+25 -21
View File
@@ -13,41 +13,45 @@ Ce site permet de :
## 🚀 Stack technique
- **[11ty (Eleventy)](https://www.11ty.dev/)** — génération du site statique, contenu et navigation en HTML/CSS, fonctionnel sans JavaScript
- **[Svelte 5](https://svelte.dev/)** en îlots — hydratation ciblée pour les 3 seuls widgets interactifs (calculateur de prédictions, carte d'observation, globe de la vague planétaire), via `@11ty/eleventy-plugin-vite`
- **[Paraglide (inlang)](https://paraglidejs.com/)** — i18n compilée, FR (défaut) / EN / Kreyòl Ayisyen, routée par URL (`/`, `/en/`, `/ht/`)
- **CSS natif** — tokens `oklch()`, cascade `@layer`, container queries, aucun framework CSS ni préprocesseur
- Polices auto-hébergées (Fraunces, Atkinson Hyperlegible), zéro dépendance à Google Fonts
Version 3, reconstruite avec **[SvelteKit 2 + Svelte 5 (runes) + TypeScript](https://svelte.dev/)**, entièrement pré-rendue en statique (`adapter-static`). L'ancienne version 11ty est conservée sur la branche `main`.
Conforme à la doctrine UI/UX OKI (voir document de référence interne) : navigation à 5 entrées, cibles tactiles ≥ 48 px, contraste AA/AAA, `prefers-reduced-motion` respecté, budget JS minimal.
- **SvelteKit statique** — tout le contenu éditorial fonctionne sans JavaScript ; les 3 widgets (calculateur, carte, globe) sont des composants Svelte natifs avec fallback SSR (plus d'îlots montés à la main)
- **i18n maison** — les catalogues au format Paraglide/inlang (`src/lib/i18n/{fr,en,ht}.json`, éditables avec les outils inlang) consommés par un routeur par dossiers : FR à `/`, EN sur `/en/`, Kreyòl sur `/ht/` (retombe sur le fr pour le contenu long)
- **CSS natif** — tokens `oklch()`, cascade `@layer`, container queries, aucun framework
- **Leaflet** en import dynamique côté client uniquement (tuiles OSM — exception documentée à la règle zéro-tiers)
- Polices auto-hébergées (Fraunces, Atkinson Hyperlegible), zéro Google Fonts
- **PWA** : manifest + service worker stale-while-revalidate (`src/service-worker.js`, incrémenter `CACHE_NAME` à chaque évolution)
- **SEO** : canonical, hreflang ×3 + x-default, Open Graph, Twitter Cards (`src/lib/components/Seo.svelte`), `robots.txt` + `sitemap.xml` (15 URLs)
Conforme à la doctrine UI/UX OKI : navigation à 5 entrées, cibles tactiles ≥ 48 px, contraste AA/AAA, `prefers-reduced-motion` respecté, budget JS minimal.
## 📦 Installation
```bash
npm install
npm run dev # serveur de développement sur http://localhost:8080
npm run build # build de production dans _site/
npm run preview # prévisualiser le build de production
npm run dev # http://localhost:5173/gwada-sirius/
npm run build # build de production dans build/
npm run preview # prévisualiser le build
npm run check # types + accessibilité (svelte-check)
```
## 🗂️ Structure
```
src/
├── _data/ # données globales (sites d'observation, navigation)
├── _includes/ # layout de base, partiels (nav, footer)
├── styles/ # design system CSS (tokens, reset, base, layouts, composants)
├── islands/ # composants Svelte 5 hydratés côté client (prédictions, carte, globe)
├── paraglide/ # messages i18n compilés (généré, ignoré par git)
├── index.njk # accueil
├── sirius-science.njk # astronomie + méthode de calcul
── culture-savoirs.njk# héritage culturel + savoir dogon
├── observer.njk # calculateur, carte, guide d'observation
└── associations.njk # associations qui fêtent le Wep Ronpet
messages/ # catalogues de traduction (fr/en/ht), source pour Paraglide
project.inlang/ # configuration du projet i18n
├── lib/
│ ├── i18n/ # catalogues fr/en/ht (format Paraglide) + routeur localisé
│ ├── data/ # sites d'observation, villes de la vague, prochain lever, nav
│ ├── styles/ # design system CSS (tokens, base, layouts, composants, thème oki)
│ ├── components/ # Nav, Footer, Seo, pages/ (une par page), islands/ (3 widgets)
│ └── dates.ts # formatage local des dates (piège UTC-4, voir NOTES_PROJET)
├── routes/ # / (fr), en/, ht/ — 5 pages par locale
── service-worker.js # PWA (stale-while-revalidate)
static/ # favicon.svg, manifest.webmanifest, robots.txt, sitemap.xml, fonts/
```
Le déploiement GitHub Pages (sous-répertoire `/gwada-sirius/`) est géré par `paths.base` dans `svelte.config.js` + `.github/workflows/deploy.yml`.
## 👥 Crédits
Projet réalisé avec ❤️ par [cyber-mawonaj](https://github.com/cyber-mawonaj) et [OKI](https://o-k-i.net).
-112
View File
@@ -1,112 +0,0 @@
import fs from "node:fs";
import path from "node:path";
import EleventyVitePlugin from "@11ty/eleventy-plugin-vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import * as m from "./src/paraglide/messages.js";
import { locales, baseLocale } from "./src/paraglide/runtime.js";
const OUTPUT_DIR = "_site";
// GitHub Pages project sites are served under /<repo-name>/, not at the
// domain root — set BASE_PATH (no trailing slash, e.g. "/gwada-sirius")
// in CI to prefix internal links and Vite's own asset URLs accordingly.
// Left empty for local dev and for a future custom-domain deployment.
const BASE_PATH = (process.env.BASE_PATH || "").replace(/\/+$/, "");
// Vite's build wipes the output dir (emptyOutDir) after 11ty has already
// copied passthrough assets into it — re-copy them once Vite is done.
// https://github.com/11ty/eleventy-plugin-vite/issues/42
function restorePassthroughCopy(copies) {
return {
name: "restore-passthrough-copy",
apply: "build",
closeBundle() {
for (const [from, to] of copies) {
if (!fs.existsSync(from)) continue;
fs.cpSync(from, path.join(OUTPUT_DIR, to), {
recursive: true,
force: true,
});
}
},
};
}
export default function (eleventyConfig) {
eleventyConfig.addPlugin(EleventyVitePlugin, {
viteOptions: {
appType: "mpa",
plugins: [
svelte(),
restorePassthroughCopy([
["src/assets", "assets"],
["public", "."],
]),
],
resolve: {
alias: {
"/src": path.resolve(".", "src"),
},
},
base: BASE_PATH ? `${BASE_PATH}/` : "/",
build: {
emptyOutDir: false,
},
},
});
eleventyConfig.ignores.add("src/paraglide/**");
eleventyConfig.ignores.add("src/islands/**");
eleventyConfig.addPassthroughCopy("src/assets");
eleventyConfig.addPassthroughCopy({ public: "." });
// Trilingual message lookup for Nunjucks templates: {{ "hero_title1" | t(locale) }}
eleventyConfig.addFilter("t", (key, locale, params = {}) => {
const fn = m[key];
if (!fn) return key;
return fn(params, { locale });
});
eleventyConfig.addGlobalData("locales", locales);
eleventyConfig.addGlobalData("baseLocale", baseLocale);
eleventyConfig.addGlobalData("currentYear", () => new Date().getFullYear());
eleventyConfig.addGlobalData("basePath", BASE_PATH);
eleventyConfig.addFilter("localeHref", (pagePath, locale) => {
const clean = pagePath.replace(/^\/+/, "");
const localePath = locale === baseLocale ? `/${clean}` : `/${locale}/${clean}`;
return BASE_PATH + localePath;
});
// For static/passthrough assets referenced by absolute path outside of
// Vite's own bundling (favicon, manifest, service worker, font preloads —
// anything marked vite-ignore in layouts/base.njk).
eleventyConfig.addFilter("withBase", (assetPath) => BASE_PATH + assetPath);
eleventyConfig.addFilter("dump", (value) => JSON.stringify(value).replace(/</g, "\\u003c"));
eleventyConfig.addFilter("formatDate", (isoDate, locale, options = {}) => {
// new Date("YYYY-MM-DD") parses as UTC midnight, which can roll back a
// day once formatted in a timezone behind UTC (e.g. Guadeloupe, UTC-4).
// Build the date from its local components instead.
const [year, month, day] = isoDate.split("-").map(Number);
return new Date(year, month - 1, day).toLocaleDateString(
locale === "ht" ? "fr" : locale,
{ day: "numeric", month: "long", year: "numeric", ...options }
);
});
return {
dir: {
input: "src",
output: OUTPUT_DIR,
includes: "_includes",
data: "_data",
},
templateFormats: ["njk", "md"],
markdownTemplateEngine: "njk",
htmlTemplateEngine: "njk",
};
}
+1415 -3565
View File
File diff suppressed because it is too large Load Diff
+24 -24
View File
@@ -1,26 +1,26 @@
{
"name": "gwada-sirius",
"version": "2.0.0",
"type": "module",
"scripts": {
"messages": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide",
"dev": "npm run messages && eleventy --serve",
"build": "npm run messages && eleventy",
"preview": "npx serve _site",
"deploy": "npm run build && gh-pages -d _site"
},
"devDependencies": {
"@11ty/eleventy": "^3.1.6",
"@11ty/eleventy-plugin-vite": "^8.0.0",
"@fontsource-variable/fraunces": "^5.2.9",
"@fontsource/atkinson-hyperlegible": "^5.2.8",
"@inlang/paraglide-js": "^2.20.2",
"@sveltejs/vite-plugin-svelte": "^7.1.4",
"gh-pages": "^6.1.1",
"svelte": "^5.56.4",
"vite": "^8.1.3"
},
"dependencies": {
"leaflet": "^1.9.4"
}
"name": "gwada-sirius",
"version": "3.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.70.1",
"@sveltejs/vite-plugin-svelte": "^7.2.0",
"@types/leaflet": "^1.9.21",
"svelte": "^5.56.7",
"svelte-check": "^4.7.3",
"typescript": "^6.0.3",
"vite": "^8.1.5"
},
"dependencies": {
"leaflet": "^1.9.4"
}
}
-11
View File
@@ -1,11 +0,0 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"baseLocale": "fr",
"locales": ["fr", "en", "ht"],
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{locale}.json"
}
}
-12
View File
@@ -1,12 +0,0 @@
{
"name": "Sirius en Guadeloupe — Wep Ronpet",
"short_name": "Sirius Gwada",
"description": "Lever héliaque de Sirius en Guadeloupe : astronomie, culture et observation.",
"start_url": ".",
"display": "standalone",
"background_color": "#141a33",
"theme_color": "#141a33",
"icons": [
{ "src": "favicon.svg", "sizes": "any", "type": "image/svg+xml" }
]
}
-36
View File
@@ -1,36 +0,0 @@
// Service worker minimal : résilience réseau (3G/4G irrégulier) et usage
// hors-ligne des pages déjà visitées. Stratégie stale-while-revalidate.
const CACHE_NAME = "gwada-sirius-v2";
self.addEventListener("install", (event) => {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)))
)
);
self.clients.claim();
});
self.addEventListener("fetch", (event) => {
if (event.request.method !== "GET") return;
const url = new URL(event.request.url);
if (url.origin !== self.location.origin) return;
event.respondWith(
caches.open(CACHE_NAME).then(async (cache) => {
const cached = await cache.match(event.request);
const network = fetch(event.request)
.then((response) => {
if (response.ok) cache.put(event.request, response.clone());
return response;
})
.catch(() => cached);
return cached || network;
})
);
});
-36
View File
@@ -1,36 +0,0 @@
<!doctype html>
<html lang="{{ locale }}" data-theme="oki">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ pageTitle }} — {{ "meta_title" | t(locale) }}</title>
<meta name="description" content="{{ 'meta_description' | t(locale) }}">
<link vite-ignore rel="icon" href="{{ '/favicon.svg' | withBase }}" type="image/svg+xml">
<link vite-ignore rel="manifest" href="{{ '/manifest.webmanifest' | withBase }}">
{% for loc in locales %}
<link vite-ignore rel="alternate" hreflang="{{ loc }}" href="{{ slug | localeHref(loc) }}">
{% endfor %}
<link vite-ignore rel="preload" href="{{ '/assets/fonts/fraunces-variable.woff2' | withBase }}" as="font" type="font/woff2" crossorigin>
<link vite-ignore rel="preload" href="{{ '/assets/fonts/atkinson-400.woff2' | withBase }}" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/src/styles/app.css">
</head>
<body>
<a class="skip-link" href="#main-content">{{ "skip_to_content" | t(locale) }}</a>
{% include "partials/nav.njk" %}
<main id="main-content">
{{ content | safe }}
</main>
{% include "partials/footer.njk" %}
<script>
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => navigator.serviceWorker.register("{{ '/sw.js' | withBase }}"));
}
</script>
</body>
</html>
-49
View File
@@ -1,49 +0,0 @@
<footer class="site-footer">
<div class="container footer-grid">
<div class="footer-section">
<h2>Sirius Gwada</h2>
<p>{{ "footer_tagline" | t(locale) }}</p>
</div>
<div class="footer-section">
<h3>{{ "footer_links_title" | t(locale) }}</h3>
<ul>
{% for item in nav %}
<li><a href="{{ item.path | localeHref(locale) }}">{{ item.key | t(locale) }}</a></li>
{% endfor %}
</ul>
</div>
<div class="footer-section">
<h3>{{ "footer_resources_title" | t(locale) }}</h3>
<ul>
<li><a href="https://stellarium.org" target="_blank" rel="noopener">{{ "footer_resources_stellarium" | t(locale) }}</a></li>
<li><a href="{{ '/observer/' | localeHref(locale) }}">{{ "footer_resources_guide" | t(locale) }}</a></li>
<li><a href="{{ '/sirius-science/' | localeHref(locale) }}">{{ "footer_resources_references" | t(locale) }}</a></li>
<li><a href="mailto:contact@o-k-i.net">{{ "footer_resources_contact" | t(locale) }}</a></li>
</ul>
</div>
<div class="footer-section">
<h3>{{ "footer_next_title" | t(locale) }}</h3>
<div class="countdown">
<div class="countdown-item">
<span class="countdown-value">{{ nextRising.date | formatDate(locale, { day: "numeric", month: "long" }) }}</span>
<span class="countdown-label">{{ nextRising.time }} · {{ nextRising.year }}</span>
</div>
</div>
</div>
</div>
<div class="container footer-bottom">
<p class="pattern-strip kente" aria-hidden="true"></p>
<p>&copy; {{ currentYear }} {{ "footer_rights" | t(locale) }}</p>
<p class="footer-credits">
{{ "footer_copyright_prefix" | t(locale) }}
<a href="https://github.com/cyber-mawonaj" target="_blank" rel="noopener">cyber-mawonaj</a>
{{ "footer_and" | t(locale) }}
<a href="https://o-k-i.net" target="_blank" rel="noopener">OKI</a>
</p>
<p class="footer-quote">{{ "footer_quote" | t(locale) }}</p>
</div>
</footer>
-36
View File
@@ -1,36 +0,0 @@
<header class="nav">
<div class="container nav-inner">
<a class="nav-brand" href="{{ '' | localeHref(locale) }}">
<span aria-hidden="true">☥</span> Sirius Gwada
</a>
<details class="nav-disclosure">
<summary class="nav-toggle">
<span class="visually-hidden">Menu</span>
<span aria-hidden="true" class="nav-toggle-icon"></span>
</summary>
<ul class="nav-menu">
{% for item in nav %}
<li>
<a href="{{ item.path | localeHref(locale) }}"
{% if pageKey == item.key %}aria-current="page"{% endif %}>
{{ item.key | t(locale) }}
</a>
</li>
{% endfor %}
</ul>
</details>
<ul class="nav-lang">
{% for loc in locales %}
<li>
<a href="{{ slug | localeHref(loc) }}"
lang="{{ loc }}"
{% if loc == locale %}aria-current="true" class="active"{% endif %}>
{{ ("lang_name_" + loc) | t(locale) }}
</a>
</li>
{% endfor %}
</ul>
</div>
</header>
+12
View File
@@ -0,0 +1,12 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
+21
View File
@@ -0,0 +1,21 @@
<!doctype html>
<html lang="%lang%" data-theme="oki">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="%sveltekit.assets%/favicon.svg" />
<link rel="manifest" href="%sveltekit.assets%/manifest.webmanifest" />
<link
rel="preload"
href="%sveltekit.assets%/fonts/fraunces-variable.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link rel="stylesheet" href="%sveltekit.assets%/fonts/fonts.css" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
-79
View File
@@ -1,79 +0,0 @@
---
pagination:
data: locales
size: 1
alias: locale
permalink: "{% if locale == baseLocale %}/associations/index.html{% else %}/{{ locale }}/associations/index.html{% endif %}"
layout: layouts/base.njk
pageKey: associations
slug: "associations/"
eleventyComputed:
pageTitle: "{{ 'associations_title' | t(locale) }}"
---
<section class="section">
<div class="container">
<div class="section-header center">
<p class="section-eyebrow">Wep Ronpet</p>
<h1>{{ "associations_title" | t(locale) }}</h1>
<p class="pattern-strip bogolan" aria-hidden="true" style="margin-inline:auto"></p>
<p class="section-subtitle">{{ "associations_subtitle" | t(locale) }}</p>
</div>
<div class="grid-auto">
<div class="card">
<h2>{{ "associations_events_title" | t(locale) }}</h2>
<div class="stack" style="--stack-space: 0">
<a class="event-link" href="https://my.weezevent.com/nouvel-an-kamite" target="_blank" rel="noopener noreferrer">
<span class="event-icon" aria-hidden="true">🎊</span>
<span class="event-info">
<span class="event-title">{{ "associations_event1_title" | t(locale) }}</span>
<span class="event-platform">{{ "associations_event1_organizer" | t(locale) }}</span>
<span>{{ "associations_event1_desc" | t(locale) }}</span>
</span>
</a>
<a class="event-link" href="https://my.weezevent.com/nouvel-an-kamit" target="_blank" rel="noopener noreferrer">
<span class="event-icon" aria-hidden="true">✨</span>
<span class="event-info">
<span class="event-title">{{ "associations_event2_title" | t(locale) }}</span>
<span class="event-platform">{{ "associations_event2_organizer" | t(locale) }}</span>
<span>{{ "associations_event2_desc" | t(locale) }}</span>
</span>
</a>
</div>
</div>
<div class="card">
<h2>{{ "associations_community_title" | t(locale) }}</h2>
<div class="stack" style="--stack-space: 0">
<a class="community-link" href="https://14eme-membre.net/site-web/#événements" target="_blank" rel="noopener noreferrer">
<span class="community-icon" aria-hidden="true">🌍</span>
<span class="community-info">
<span class="community-title">{{ "associations_link1_title" | t(locale) }}</span>
<span>{{ "associations_link1_desc" | t(locale) }}</span>
</span>
</a>
<a class="community-link" href="https://www.facebook.com/people/Mouvement-Politique-Spirituel-MUN" target="_blank" rel="noopener noreferrer">
<span class="community-icon" aria-hidden="true">📘</span>
<span class="community-info">
<span class="community-title">{{ "associations_link2_title" | t(locale) }}</span>
<span>{{ "associations_link2_desc" | t(locale) }}</span>
</span>
</a>
</div>
</div>
</div>
<h2 class="center" style="margin-top: var(--space-7)">{{ "associations_video_title" | t(locale) }}</h2>
<div class="video-frame card" style="margin-top: var(--space-4)">
<iframe
src="https://www.youtube.com/embed/0_8kWlDTf94"
title="{{ 'associations_video_title' | t(locale) }}"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
loading="lazy"
></iframe>
</div>
<p class="associations-note center">{{ "associations_note" | t(locale) }}</p>
</div>
</section>
-192
View File
@@ -1,192 +0,0 @@
---
pagination:
data: locales
size: 1
alias: locale
permalink: "{% if locale == baseLocale %}/culture-savoirs/index.html{% else %}/{{ locale }}/culture-savoirs/index.html{% endif %}"
layout: layouts/base.njk
pageKey: culture
slug: "culture-savoirs/"
eleventyComputed:
pageTitle: "{{ 'culture_title' | t(locale) }}"
---
<section class="section">
<div class="container">
<div class="section-header center">
<p class="section-eyebrow">Sirius Gwada</p>
<h1>{{ "culture_title" | t(locale) }}</h1>
<p class="pattern-strip adinkra" aria-hidden="true" style="margin-inline:auto"></p>
<p class="section-subtitle">{{ "culture_subtitle" | t(locale) }}</p>
</div>
<div class="grid-auto">
<div class="feature-card">
<div style="font-size: var(--text-4)" aria-hidden="true">🏺</div>
<h3>{{ "culture_egypt_title" | t(locale) }}</h3>
<p>{{ "culture_egypt_desc" | t(locale) }}</p>
</div>
<div class="feature-card">
<div style="font-size: var(--text-4)" aria-hidden="true">🌍</div>
<h3>{{ "culture_dogon_title" | t(locale) }}</h3>
<p>{{ "culture_dogon_desc" | t(locale) }}</p>
</div>
<div class="feature-card">
<div style="font-size: var(--text-4)" aria-hidden="true">⛵</div>
<h3>{{ "culture_caribbean_title" | t(locale) }}</h3>
<p>{{ "culture_caribbean_desc" | t(locale) }}</p>
</div>
</div>
<h2 class="center" style="margin-top: var(--space-7)">{{ "culture_timeline_title" | t(locale) }}</h2>
<div class="timeline-v" style="margin-top: var(--space-5)">
<div class="timeline-v-item">
<p class="timeline-v-date">{{ "culture_timeline_1_date" | t(locale) }}</p>
<h4>{{ "culture_timeline_1_title" | t(locale) }}</h4>
<p>{{ "culture_timeline_1_desc" | t(locale) }}</p>
</div>
<div class="timeline-v-item">
<p class="timeline-v-date">{{ "culture_timeline_2_date" | t(locale) }}</p>
<h4>{{ "culture_timeline_2_title" | t(locale) }}</h4>
<p>{{ "culture_timeline_2_desc" | t(locale) }}</p>
</div>
<div class="timeline-v-item">
<p class="timeline-v-date">{{ "culture_timeline_3_date" | t(locale) }}</p>
<h4>{{ "culture_timeline_3_title" | t(locale) }}</h4>
<p>{{ "culture_timeline_3_desc" | t(locale) }}</p>
</div>
<div class="timeline-v-item">
<p class="timeline-v-date">{{ "culture_timeline_4_date" | t(locale) }}</p>
<h4>{{ "culture_timeline_4_title" | t(locale) }}</h4>
<p>{{ "culture_timeline_4_desc" | t(locale) }}</p>
</div>
</div>
</div>
</section>
<section class="section" style="background: var(--surface-sunk)">
<div class="container">
<div class="section-header center">
<h2>{{ "dogon_title" | t(locale) }}</h2>
<p class="pattern-strip kente" aria-hidden="true" style="margin-inline:auto"></p>
<p class="section-subtitle">{{ "dogon_intro" | t(locale) }}</p>
</div>
<fieldset class="tabs">
<legend class="visually-hidden">{{ "dogon_title" | t(locale) }}</legend>
<input type="radio" name="dogon-tab" id="dogon-tab-cosmogony" class="tab-input" checked>
<input type="radio" name="dogon-tab" id="dogon-tab-system" class="tab-input">
<input type="radio" name="dogon-tab" id="dogon-tab-astronomy" class="tab-input">
<input type="radio" name="dogon-tab" id="dogon-tab-heritage" class="tab-input">
<div class="tab-list" role="tablist">
<label class="tab-label" for="dogon-tab-cosmogony">{{ "dogon_tab_cosmogony" | t(locale) }}</label>
<label class="tab-label" for="dogon-tab-system">{{ "dogon_tab_system" | t(locale) }}</label>
<label class="tab-label" for="dogon-tab-astronomy">{{ "dogon_tab_astronomy" | t(locale) }}</label>
<label class="tab-label" for="dogon-tab-heritage">{{ "dogon_tab_heritage" | t(locale) }}</label>
</div>
<div class="tab-panels">
<div class="tab-panel prose center">
<p>{{ "dogon_cosmogony_p1" | t(locale) }}</p>
<p>{{ "dogon_cosmogony_p2" | t(locale) }}</p>
<div class="card">
<h3>{{ "dogon_cosmogony_nommo_title" | t(locale) }}</h3>
<p>{{ "dogon_cosmogony_nommo_desc" | t(locale) }}</p>
</div>
</div>
<div class="tab-panel">
<div class="grid-auto">
<div class="info-card">
<h3>{{ "dogon_system_sigutolo_title" | t(locale) }}</h3>
<p>{{ "dogon_system_sigutolo_desc" | t(locale) }}</p>
</div>
<div class="info-card">
<h3>{{ "dogon_system_potolo_title" | t(locale) }}</h3>
<p>{{ "dogon_system_potolo_desc" | t(locale) }}</p>
</div>
<div class="info-card">
<h3>{{ "dogon_system_emmeya_title" | t(locale) }}</h3>
<p>{{ "dogon_system_emmeya_desc" | t(locale) }}</p>
</div>
</div>
<div class="card prose center" style="margin-top: var(--space-4)">
<h3>{{ "dogon_mystery_title" | t(locale) }}</h3>
<div class="grid-auto">
<div>
<h4>{{ "dogon_mystery_theory1_title" | t(locale) }}</h4>
<p>{{ "dogon_mystery_theory1_desc" | t(locale) }}</p>
</div>
<div>
<h4>{{ "dogon_mystery_theory2_title" | t(locale) }}</h4>
<p>{{ "dogon_mystery_theory2_desc" | t(locale) }}</p>
</div>
<div>
<h4>{{ "dogon_mystery_theory3_title" | t(locale) }}</h4>
<p>{{ "dogon_mystery_theory3_desc" | t(locale) }}</p>
</div>
</div>
</div>
<div class="card prose center" style="margin-top: var(--space-4)">
<h3>{{ "dogon_calc_example_title" | t(locale) }}</h3>
<p>{{ "dogon_calc_example_text" | t(locale) }}</p>
</div>
</div>
<div class="tab-panel">
<div class="grid-auto">
<div class="info-card">
<h3>{{ "dogon_astronomy_geometry_title" | t(locale) }}</h3>
<p>{{ "dogon_astronomy_geometry_desc" | t(locale) }}</p>
</div>
<div class="info-card">
<h3>{{ "dogon_astronomy_calendar_title" | t(locale) }}</h3>
<p>{{ "dogon_astronomy_calendar_desc" | t(locale) }}</p>
</div>
<div class="info-card">
<h3>{{ "dogon_astronomy_planets_title" | t(locale) }}</h3>
<p>{{ "dogon_astronomy_planets_desc" | t(locale) }}</p>
</div>
<div class="info-card">
<h3>{{ "dogon_astronomy_universe_title" | t(locale) }}</h3>
<p>{{ "dogon_astronomy_universe_desc" | t(locale) }}</p>
</div>
</div>
</div>
<div class="tab-panel">
<div class="timeline-v">
<div class="timeline-v-item">
<p class="timeline-v-date">{{ "dogon_heritage_1_date" | t(locale) }}</p>
<p>{{ "dogon_heritage_1_desc" | t(locale) }}</p>
</div>
<div class="timeline-v-item">
<p class="timeline-v-date">{{ "dogon_heritage_2_date" | t(locale) }}</p>
<p>{{ "dogon_heritage_2_desc" | t(locale) }}</p>
</div>
<div class="timeline-v-item">
<p class="timeline-v-date">{{ "dogon_heritage_3_date" | t(locale) }}</p>
<p>{{ "dogon_heritage_3_desc" | t(locale) }}</p>
</div>
<div class="timeline-v-item">
<p class="timeline-v-date">{{ "dogon_heritage_4_date" | t(locale) }}</p>
<p>{{ "dogon_heritage_4_desc" | t(locale) }}</p>
</div>
</div>
<div class="card prose center" style="margin-top: var(--space-5)">
<h3>{{ "dogon_heritage_diaspora_title" | t(locale) }}</h3>
<p>{{ "dogon_heritage_diaspora_desc" | t(locale) }}</p>
<div class="grid-auto" style="margin-top: var(--space-3); text-align: left">
<p>🇭🇹 {{ "dogon_heritage_diaspora_haiti" | t(locale) }}</p>
<p>🇬🇵 {{ "dogon_heritage_diaspora_guadeloupe" | t(locale) }}</p>
<p>🇲🇶 {{ "dogon_heritage_diaspora_martinique" | t(locale) }}</p>
<p>🇯🇲 {{ "dogon_heritage_diaspora_jamaica" | t(locale) }}</p>
</div>
</div>
</div>
</div>
</fieldset>
</div>
</section>
+14
View File
@@ -0,0 +1,14 @@
import { base } from '$app/paths';
import type { Handle } from '@sveltejs/kit';
const LANGS = ['fr', 'en', 'ht'] as const;
export const handle: Handle = async ({ event, resolve }) => {
// event.url.pathname inclut le base path (/gwada-sirius/…) — le retirer d'abord
const stripped = event.url.pathname.slice(base.length);
const segment = stripped.split('/').filter(Boolean)[0];
const lang = LANGS.includes(segment as (typeof LANGS)[number]) ? segment : 'fr';
return resolve(event, {
transformPageChunk: ({ html }) => html.replaceAll('%lang%', lang)
});
};
-93
View File
@@ -1,93 +0,0 @@
---
pagination:
data: locales
size: 1
alias: locale
permalink: "{% if locale == baseLocale %}/index.html{% else %}/{{ locale }}/index.html{% endif %}"
layout: layouts/base.njk
pageKey: home
slug: ""
eleventyComputed:
pageTitle: "{{ 'hero_title1' | t(locale) }} {{ 'hero_title2' | t(locale) }}"
---
<section class="hero">
<div class="hero-orbits" aria-hidden="true">
<div class="sun"></div>
<div class="orbit" style="width:5rem;height:5rem;animation-duration:6s"><span></span></div>
<div class="orbit" style="width:8rem;height:8rem;animation-duration:10s"><span></span></div>
<div class="orbit" style="width:11rem;height:11rem;animation-duration:14s"><span></span></div>
<div class="orbit" style="width:14rem;height:14rem;animation-duration:18s"><span></span></div>
<div class="orbit sirius" style="width:18rem;height:18rem;animation-duration:26s"><span></span></div>
</div>
<div class="container center hero-content">
<p class="section-eyebrow">{{ "hero_eyebrow" | t(locale) }}</p>
<h1 class="hero-title">
<span>{{ "hero_title1" | t(locale) }}</span>
<span class="accent">{{ "hero_title2" | t(locale) }}</span>
</h1>
<p class="hero-subtitle">{{ "hero_subtitle" | t(locale) }}</p>
<p class="hero-description">{{ "hero_description" | t(locale) }}</p>
<p class="hero-date">
{{ "hero_date_label" | t(locale) }}
<strong>{{ nextRising.date | formatDate(locale) }} · {{ nextRising.time }}</strong>
</p>
<div class="cluster hero-cta">
<a class="btn btn-primary comet-ring" href="{{ '/observer/' | localeHref(locale) }}">{{ "hero_cta_predictions" | t(locale) }}</a>
<a class="btn btn-outline" href="{{ '/sirius-science/' | localeHref(locale) }}">{{ "hero_cta_learn" | t(locale) }}</a>
</div>
</div>
</section>
<section class="section" id="intro">
<div class="container">
<div class="section-header center">
<h2>{{ "home_intro_title" | t(locale) }}</h2>
<p class="pattern-strip kente" aria-hidden="true" style="margin-inline:auto"></p>
<p class="section-subtitle">{{ "home_intro_subtitle" | t(locale) }}</p>
</div>
<div class="prose center stack" style="--stack-space: var(--space-3)">
<p>{{ "home_intro_p1" | t(locale) | safe }}</p>
<p>{{ "home_intro_p2" | t(locale) }}</p>
<p>{{ "home_intro_p3" | t(locale) }}</p>
</div>
<div class="grid-auto" style="margin-top: var(--space-6)">
<div class="feature-card">
<h3>{{ "feature_brightest_title" | t(locale) }}</h3>
<p>{{ "feature_brightest_desc" | t(locale) }}</p>
</div>
<div class="feature-card">
<h3>{{ "feature_rising_title" | t(locale) }}</h3>
<p>{{ "feature_rising_desc" | t(locale) }}</p>
</div>
<div class="feature-card">
<h3>{{ "feature_heritage_title" | t(locale) }}</h3>
<p>{{ "feature_heritage_desc" | t(locale) }}</p>
</div>
</div>
</div>
</section>
<section class="section" style="background: var(--surface-sunk)">
<div class="container">
<div class="section-header center">
<h2>{{ "wave_showcase_title" | t(locale) }}</h2>
<p class="section-subtitle">{{ "wave_showcase_subtitle" | t(locale) }}</p>
</div>
<ul class="wave-badges">
{% for city in waveCities %}
<li class="wave-badge wave-badge-{{ loop.index0 % 3 }}">
<span class="wave-badge-name">{{ city.name }}</span>
<span class="wave-badge-date">{{ city.date }}</span>
</li>
{% endfor %}
</ul>
<p class="center" style="margin-top: var(--space-5)">
<a class="btn btn-outline" href="{{ '/observer/' | localeHref(locale) }}">{{ "wave_showcase_cta" | t(locale) }}</a>
</p>
</div>
</section>
-81
View File
@@ -1,81 +0,0 @@
<script>
import { onMount, onDestroy } from "svelte";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import { waveCities } from "./world.js";
let mapContainer = $state();
let map;
function markerIcon(active) {
return L.divIcon({
className: "wave-marker" + (active ? " wave-marker-active" : ""),
iconSize: active ? [20, 20] : [14, 14],
iconAnchor: active ? [10, 10] : [7, 7],
});
}
onMount(() => {
map = L.map(mapContainer, { scrollWheelZoom: false });
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 18,
}).addTo(map);
const markers = waveCities.map((city) => {
const isCaribbean = city.name === "Guadeloupe" || city.name === "Martinique";
const marker = L.marker([city.lat, city.lon], {
icon: markerIcon(isCaribbean),
}).addTo(map);
marker.bindPopup(`<strong>${city.name}</strong><br>${city.date}`);
marker.on("popupopen", () => marker.setIcon(markerIcon(true)));
marker.on("popupclose", () => marker.setIcon(markerIcon(isCaribbean)));
return marker;
});
const group = L.featureGroup(markers);
map.fitBounds(group.getBounds().pad(0.15));
});
onDestroy(() => {
map?.remove();
});
</script>
<div
bind:this={mapContainer}
class="wave-map"
role="group"
aria-label="Carte de la progression du lever héliaque à travers l'Afrique et les Caraïbes"
></div>
<style>
.wave-map {
width: 100%;
height: 22rem;
border-radius: var(--radius-md);
overflow: hidden;
}
:global(.wave-marker) {
background: var(--caribbean-teal);
border: 2px solid oklch(20% 0.03 265);
border-radius: 50%;
box-shadow: var(--shadow-sm);
}
:global(.wave-marker-active) {
background: var(--gold-surface);
}
:global(.leaflet-popup-content-wrapper) {
background: var(--surface-raised);
color: var(--ink);
border-radius: var(--radius-sm);
}
:global(.leaflet-popup-tip) {
background: var(--surface-raised);
}
</style>
-24
View File
@@ -1,24 +0,0 @@
const container = document.querySelector('[data-island="globe"]');
if (container && "IntersectionObserver" in window) {
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
observer.disconnect();
Promise.all([
import("svelte"),
import("./SiriusGlobe.svelte"),
]).then(([{ mount }, { default: SiriusGlobe }]) => {
const target = container.querySelector(".island-app");
mount(SiriusGlobe, { target });
container.classList.add("is-enhanced");
});
}
},
{ rootMargin: "200px" }
);
observer.observe(container);
}
-1
View File
@@ -1 +0,0 @@
export { default as waveCities } from "../../_data/waveCities.js";
-81
View File
@@ -1,81 +0,0 @@
<script>
import { onMount, onDestroy } from "svelte";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
let { sites, locale } = $props();
let mapContainer = $state();
let map;
function markerIcon(active) {
return L.divIcon({
className: "site-marker" + (active ? " site-marker-active" : ""),
iconSize: active ? [22, 22] : [16, 16],
iconAnchor: active ? [11, 11] : [8, 8],
});
}
onMount(() => {
map = L.map(mapContainer, {
scrollWheelZoom: false,
});
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 18,
}).addTo(map);
const markers = sites.map((site) => {
const marker = L.marker([site.lat, site.lon], { icon: markerIcon(false) }).addTo(map);
const desc = site[`description_${locale}`] ?? site.description_fr;
marker.bindPopup(
`<strong>${site.name}</strong><br>${desc}<br><small style="color: var(--ink-dim)">${site.lat}°N, ${site.lon}°O — ${site.alt} m</small>`
);
marker.on("popupopen", () => marker.setIcon(markerIcon(true)));
marker.on("popupclose", () => marker.setIcon(markerIcon(false)));
return marker;
});
const group = L.featureGroup(markers);
map.fitBounds(group.getBounds().pad(0.2));
});
onDestroy(() => {
map?.remove();
});
</script>
<div class="observation-map">
<div bind:this={mapContainer} class="leaflet-container-host" role="group" aria-label="Carte des sites d'observation de Sirius en Guadeloupe"></div>
</div>
<style>
.leaflet-container-host {
width: 100%;
height: 22rem;
border-radius: var(--radius-md);
overflow: hidden;
}
:global(.site-marker) {
background: var(--gold-surface);
border: 2px solid oklch(20% 0.03 265);
border-radius: 50%;
box-shadow: var(--shadow-sm);
}
:global(.site-marker-active) {
background: var(--caribbean-teal);
}
:global(.leaflet-popup-content-wrapper) {
background: var(--surface-raised);
color: var(--ink);
border-radius: var(--radius-sm);
}
:global(.leaflet-popup-tip) {
background: var(--surface-raised);
}
</style>
-31
View File
@@ -1,31 +0,0 @@
const container = document.querySelector('[data-island="map"]');
if (container && "IntersectionObserver" in window) {
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
observer.disconnect();
Promise.all([
import("svelte"),
import("./ObservationMap.svelte"),
]).then(([{ mount }, { default: ObservationMap }]) => {
const target = container.querySelector(".island-app");
const sitesData = JSON.parse(document.getElementById("sites-data").textContent);
const locale = document.documentElement.lang;
mount(ObservationMap, {
target,
props: { sites: sitesData, locale },
});
container.classList.add("is-enhanced");
});
}
},
{ rootMargin: "200px" }
);
observer.observe(container);
}
@@ -1,92 +0,0 @@
<script>
import { untrack } from "svelte";
let { sites, year, locale, labels } = $props();
let selectedId = $state(untrack(() => sites[0].id));
let selected = $derived(sites.find((s) => s.id === selectedId));
let prediction = $derived(selected.predictions[year]);
function formatDate(iso) {
const [y, m, d] = iso.split("-").map(Number);
return new Date(y, m - 1, d).toLocaleDateString(locale === "ht" ? "fr" : locale, {
day: "numeric",
month: "long",
year: "numeric",
});
}
function windowLabel(iso) {
const [y, m, d] = iso.split("-").map(Number);
const start = new Date(y, m - 1, d - 2);
const end = new Date(y, m - 1, d + 2);
const fmt = (date) => date.toLocaleDateString(locale === "ht" ? "fr" : locale, { day: "numeric", month: "short" });
return `${fmt(start)} ${fmt(end)}`;
}
</script>
<div class="calculator card">
<label class="calculator-label" for="site-select">{labels.select}</label>
<select id="site-select" bind:value={selectedId} class="calculator-select">
{#each sites as site}
<option value={site.id}>{site.name}</option>
{/each}
</select>
{#if prediction}
<div class="calculator-result">
<p class="calculator-result-label">{labels.heliacal}</p>
<p class="calculator-result-value">{formatDate(prediction.date)} · {prediction.time}</p>
<p class="calculator-result-label">{labels.window}</p>
<p class="calculator-result-window">{windowLabel(prediction.date)}</p>
</div>
{/if}
</div>
<style>
.calculator {
max-width: 28rem;
margin-inline: auto;
}
.calculator-label {
display: block;
font-weight: 700;
margin-bottom: var(--space-2);
}
.calculator-select {
width: 100%;
min-height: var(--tap-target);
padding-inline: var(--space-3);
border-radius: var(--radius-sm);
border: 2px solid var(--border);
background: var(--surface);
color: var(--ink);
font-size: var(--text-1);
}
.calculator-result {
margin-top: var(--space-4);
padding-top: var(--space-4);
border-top: 1px solid var(--border);
text-align: center;
}
.calculator-result-label {
font-size: var(--text-0);
color: var(--ink-soft);
margin-top: var(--space-2);
}
.calculator-result-value {
font-family: var(--font-display);
font-size: var(--text-3);
color: var(--gold);
}
.calculator-result-window {
font-weight: 600;
}
</style>
-24
View File
@@ -1,24 +0,0 @@
import { mount } from "svelte";
import PredictionsCalculator from "./PredictionsCalculator.svelte";
const container = document.querySelector('[data-island="predictions"]');
if (container) {
const target = container.querySelector(".island-app");
const sitesData = JSON.parse(document.getElementById("sites-data").textContent);
mount(PredictionsCalculator, {
target,
props: {
sites: sitesData,
year: Number(target.dataset.year),
locale: target.dataset.locale,
labels: {
select: target.dataset.selectLabel,
heliacal: target.dataset.heliacalLabel,
window: target.dataset.windowLabel,
},
},
});
container.classList.add("is-enhanced");
}
+93
View File
@@ -0,0 +1,93 @@
<script lang="ts">
import { resolve } from '$app/paths';
import nav from '$lib/data/nav.js';
import nextRising from '$lib/data/nextRising.js';
import { formatDate } from '$lib/dates';
import { localizedPath, t, type Locale } from '$lib/i18n';
interface Props {
locale: Locale;
}
const { locale }: Props = $props();
// Calculé au build (prerender) — voir NOTES_PROJET §3 pour la logique dates.
const rising = nextRising();
const currentYear = new Date().getFullYear();
</script>
<footer class="site-footer">
<div class="container footer-grid">
<div class="footer-section">
<h2>Sirius Gwada</h2>
<p>{t(locale, 'footer_tagline')}</p>
</div>
<div class="footer-section">
<h3>{t(locale, 'footer_links_title')}</h3>
<ul>
{#each nav as item (item.key)}
<li>
<a href={resolve(localizedPath(locale, item.path))}>{t(locale, item.key)}</a>
</li>
{/each}
</ul>
</div>
<div class="footer-section">
<h3>{t(locale, 'footer_resources_title')}</h3>
<ul>
<li>
<a href="https://stellarium.org" target="_blank" rel="noopener"
>{t(locale, 'footer_resources_stellarium')}</a
>
</li>
<li>
<a href={resolve(localizedPath(locale, '/observer/'))}
>{t(locale, 'footer_resources_guide')}</a
>
</li>
<li>
<a href={resolve(localizedPath(locale, '/sirius-science/'))}
>{t(locale, 'footer_resources_references')}</a
>
</li>
<li>
<a href="mailto:contact@o-k-i.net">{t(locale, 'footer_resources_contact')}</a>
</li>
</ul>
</div>
<div class="footer-section">
<h3>{t(locale, 'footer_next_title')}</h3>
<div class="countdown">
<div class="countdown-item">
<span class="countdown-value"
>{formatDate(rising.date, locale, { day: 'numeric', month: 'long' })}</span
>
<span class="countdown-label">{rising.time} · {rising.year}</span>
</div>
</div>
</div>
</div>
<div class="container footer-bottom">
<p class="pattern-strip kente" aria-hidden="true"></p>
<p>&copy; {currentYear} {t(locale, 'footer_rights')}</p>
<p class="footer-credits">
{t(locale, 'footer_copyright_prefix')}
<a href="https://github.com/cyber-mawonaj" target="_blank" rel="noopener">cyber-mawonaj</a>
{t(locale, 'footer_and')}
<a href="https://o-k-i.net" target="_blank" rel="noopener">OKI</a>
</p>
<p class="footer-quote">{t(locale, 'footer_quote')}</p>
</div>
</footer>
<style>
/* Liens dans le corps de texte : distinguables autrement que par la couleur (WCAG) */
.footer-credits a {
text-decoration: underline;
text-underline-offset: 0.15em;
}
</style>
+78
View File
@@ -0,0 +1,78 @@
<script lang="ts">
import { base, resolve } from '$app/paths';
import { page } from '$app/state';
import nav from '$lib/data/nav.js';
import {
LOCALES,
localizedPath,
switchLocalePath,
t,
type Locale
} from '$lib/i18n';
interface Props {
locale: Locale;
}
const { locale }: Props = $props();
// Pathname courant sans le base path (/gwada-sirius), pour comparer aux
// chemins de nav et basculer de locale.
const pathname = $derived.by(() => {
const p = page.url.pathname.startsWith(base)
? page.url.pathname.slice(base.length)
: page.url.pathname;
return p || '/';
});
const normalize = (s: string) => s.replace(/\/+$/, '') || '/';
// Correction du bug aria-current du legacy : l'ancien partial comparait
// `pageKey` (« home ») avec `item.key` (« nav_home ») — toujours faux,
// l'entrée active n'était jamais marquée. On compare ici le pathname
// réel au chemin localisé de l'entrée.
const isCurrent = (path: string) =>
normalize(pathname) === normalize(localizedPath(locale, path));
</script>
<header class="nav">
<div class="container nav-inner">
<a class="nav-brand" href={resolve(localizedPath(locale, '/'))}>
<span aria-hidden="true"></span> Sirius Gwada
</a>
<details class="nav-disclosure">
<summary class="nav-toggle">
<span class="visually-hidden">Menu</span>
<span aria-hidden="true" class="nav-toggle-icon"></span>
</summary>
<ul class="nav-menu">
{#each nav as item (item.key)}
<li>
<a
href={resolve(localizedPath(locale, item.path))}
aria-current={isCurrent(item.path) ? 'page' : undefined}
>
{t(locale, item.key)}
</a>
</li>
{/each}
</ul>
</details>
<ul class="nav-lang">
{#each LOCALES as loc (loc)}
<li>
<a
href={resolve(switchLocalePath(pathname, loc))}
lang={loc}
aria-current={loc === locale ? 'true' : undefined}
class:active={loc === locale}
>
{t(locale, `lang_name_${loc}`)}
</a>
</li>
{/each}
</ul>
</div>
</header>
+51
View File
@@ -0,0 +1,51 @@
<script lang="ts">
import { LOCALES, localizedPath, type Locale } from '$lib/i18n';
/**
* Métadonnées SEO d'une page : title, description, canonical absolu,
* hreflang ×3 (+ x-default), Open Graph et Twitter summary.
* Pas d'og:image : le site n'a pas d'image de partage.
*/
interface Props {
locale: Locale;
title: string;
description: string;
/** Chemin de la page sans locale, ex. `/observer/`. */
path: string;
}
const { locale, title, description, path }: Props = $props();
// URL de production (GitHub Pages, inclut déjà le base path /gwada-sirius).
const SITE = 'https://cyber-mawonaj.github.io/gwada-sirius';
const OG_LOCALES: Record<Locale, string> = {
fr: 'fr_FR',
en: 'en_US',
ht: 'ht_HT'
};
const canonical = $derived(SITE + localizedPath(locale, path));
</script>
<svelte:head>
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
{#each LOCALES as loc (loc)}
<link rel="alternate" hreflang={loc} href={SITE + localizedPath(loc, path)} />
{/each}
<link rel="alternate" hreflang="x-default" href={SITE + localizedPath('fr', path)} />
<meta property="og:type" content="website" />
<meta property="og:url" content={canonical} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:locale" content={OG_LOCALES[locale]} />
<meta property="og:site_name" content="Sirius Gwada" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
</svelte:head>
@@ -0,0 +1,138 @@
<script lang="ts">
import { onMount } from 'svelte';
import 'leaflet/dist/leaflet.css';
import type { Map as LeafletMap } from 'leaflet';
import { t, type Locale } from '$lib/i18n';
import sites from '$lib/data/sites.js';
let { locale }: { locale: Locale } = $props();
let mapContainer: HTMLDivElement | undefined = $state();
let enhanced = $state(false);
function description(site: (typeof sites)[number]): string {
const desc = site as unknown as Record<string, string | undefined>;
return desc[`description_${locale}`] ?? site.description_fr;
}
onMount(() => {
let cancelled = false;
let map: LeafletMap | undefined;
(async () => {
// Leaflet touche `window` à l'évaluation : import dynamique, jamais de SSR.
const L = await import('leaflet');
if (cancelled || !mapContainer) return;
const markerIcon = (active: boolean) =>
L.divIcon({
className: 'site-marker' + (active ? ' site-marker-active' : ''),
iconSize: active ? [22, 22] : [16, 16],
iconAnchor: active ? [11, 11] : [8, 8]
});
map = L.map(mapContainer, {
scrollWheelZoom: false
});
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 18
}).addTo(map);
const markers = sites.map((site) => {
const marker = L.marker([site.lat, site.lon], { icon: markerIcon(false) }).addTo(
map as LeafletMap
);
marker.bindPopup(
`<strong>${site.name}</strong><br>${description(site)}<br><small style="color: var(--ink-dim)">${site.lat}°N, ${site.lon}°O — ${site.alt} m</small>`
);
marker.on('popupopen', () => marker.setIcon(markerIcon(true)));
marker.on('popupclose', () => marker.setIcon(markerIcon(false)));
return marker;
});
const group = L.featureGroup(markers);
map.fitBounds(group.getBounds().pad(0.2));
enhanced = true;
})();
return () => {
cancelled = true;
map?.remove();
};
});
</script>
<div class="observation-map">
<div
bind:this={mapContainer}
class="leaflet-container-host"
role="group"
aria-label={t(locale, 'observer_map_title')}
></div>
<!-- Repli no-JS : liste statique des sites, retirée une fois la carte montée. -->
{#if !enhanced}
<ul class="site-list grid-auto">
{#each sites as site (site.id)}
<li class="card">
<h3>{site.name}</h3>
<p>{description(site)}</p>
<p class="coords">{site.lat}°N, {site.lon}°O — {site.alt} m</p>
</li>
{/each}
</ul>
{/if}
</div>
<style>
/* Hauteur fixe identique au legacy : réserve l'espace dès le SSR (zéro CLS). */
.leaflet-container-host {
width: 100%;
height: 22rem;
border-radius: var(--radius-md);
overflow: hidden;
background: var(--surface-sunk);
}
.site-list {
margin-top: var(--space-4);
list-style: none;
}
.coords {
font-size: var(--text-0);
color: var(--ink-dim);
}
:global(.site-marker) {
background: var(--gold-surface);
border: 2px solid oklch(20% 0.03 265);
border-radius: 50%;
box-shadow: var(--shadow-sm);
}
:global(.site-marker-active) {
background: var(--caribbean-teal);
}
:global(.leaflet-popup-content-wrapper) {
background: var(--surface-raised);
color: var(--ink);
border-radius: var(--radius-sm);
}
:global(.leaflet-popup-tip) {
background: var(--surface-raised);
}
@media (prefers-reduced-motion: reduce) {
.observation-map :global(.leaflet-fade-anim),
.observation-map :global(.leaflet-popup),
.observation-map :global(.leaflet-zoom-anim) {
transition: none !important;
}
}
</style>
@@ -0,0 +1,133 @@
<script lang="ts">
import { onMount } from 'svelte';
import 'leaflet/dist/leaflet.css';
import type { Map as LeafletMap } from 'leaflet';
import { t, type Locale } from '$lib/i18n';
import waveCities from '$lib/data/waveCities.js';
let { locale }: { locale: Locale } = $props();
let mapContainer: HTMLDivElement | undefined = $state();
let enhanced = $state(false);
function isCaribbean(city: (typeof waveCities)[number]): boolean {
return city.name === 'Guadeloupe' || city.name === 'Martinique';
}
onMount(() => {
let cancelled = false;
let map: LeafletMap | undefined;
(async () => {
// Leaflet touche `window` à l'évaluation : import dynamique, jamais de SSR.
const L = await import('leaflet');
if (cancelled || !mapContainer) return;
const markerIcon = (active: boolean) =>
L.divIcon({
className: 'wave-marker' + (active ? ' wave-marker-active' : ''),
iconSize: active ? [20, 20] : [14, 14],
iconAnchor: active ? [10, 10] : [7, 7]
});
map = L.map(mapContainer, { scrollWheelZoom: false });
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 18
}).addTo(map);
const markers = waveCities.map((city) => {
const marker = L.marker([city.lat, city.lon], {
icon: markerIcon(isCaribbean(city))
}).addTo(map as LeafletMap);
marker.bindPopup(`<strong>${city.name}</strong><br>${city.date}`);
marker.on('popupopen', () => marker.setIcon(markerIcon(true)));
marker.on('popupclose', () => marker.setIcon(markerIcon(isCaribbean(city))));
return marker;
});
const group = L.featureGroup(markers);
map.fitBounds(group.getBounds().pad(0.15));
enhanced = true;
})();
return () => {
cancelled = true;
map?.remove();
};
});
</script>
<div class="sirius-globe">
<div
bind:this={mapContainer}
class="wave-map"
role="group"
aria-label={t(locale, 'globe_title')}
></div>
<!-- Repli no-JS : liste statique de la vague (ville — date), retirée une
fois la carte montée. -->
{#if !enhanced}
<ul class="wave-cities grid-auto">
{#each waveCities as city (city.name)}
<li class="card">
<h3>{city.name}</h3>
<p class="wave-date">{city.date}</p>
</li>
{/each}
</ul>
{/if}
</div>
<style>
/* Hauteur fixe identique au legacy : réserve l'espace dès le SSR (zéro CLS). */
.wave-map {
width: 100%;
height: 22rem;
border-radius: var(--radius-md);
overflow: hidden;
background: var(--surface-sunk);
}
.wave-cities {
margin-top: var(--space-4);
list-style: none;
}
.wave-date {
font-size: var(--text-0);
color: var(--ink-dim);
}
:global(.wave-marker) {
background: var(--caribbean-teal);
border: 2px solid oklch(20% 0.03 265);
border-radius: 50%;
box-shadow: var(--shadow-sm);
}
:global(.wave-marker-active) {
background: var(--gold-surface);
}
:global(.leaflet-popup-content-wrapper) {
background: var(--surface-raised);
color: var(--ink);
border-radius: var(--radius-sm);
}
:global(.leaflet-popup-tip) {
background: var(--surface-raised);
}
@media (prefers-reduced-motion: reduce) {
.sirius-globe :global(.leaflet-fade-anim),
.sirius-globe :global(.leaflet-popup),
.sirius-globe :global(.leaflet-zoom-anim) {
transition: none !important;
}
}
</style>
@@ -0,0 +1,195 @@
<script lang="ts">
import { t, type Locale } from '$lib/i18n';
import sites from '$lib/data/sites.js';
import nextRising from '$lib/data/nextRising.js';
let { locale }: { locale: Locale } = $props();
type Prediction = { date: string; time: string };
// Année du prochain lever héliaque (calculée au build, comme le 11ty legacy).
const { year } = nextRising();
let selectedId = $state(sites[0].id);
let selected = $derived(sites.find((s) => s.id === selectedId) ?? sites[0]);
let prediction = $derived(
(selected.predictions as unknown as Record<number, Prediction>)[year]
);
function predictionOf(site: (typeof sites)[number]): Prediction | undefined {
return (site.predictions as unknown as Record<number, Prediction>)[year];
}
function description(site: (typeof sites)[number]): string {
const desc = site as unknown as Record<string, string | undefined>;
return desc[`description_${locale}`] ?? site.description_fr;
}
// Piège UTC-4 (NOTES_PROJET §3) : une date ISO sans heure est interprétée en
// UTC et peut reculer d'un jour une fois formatée en heure de Guadeloupe.
// Toujours parser les composants et construire la date en heure locale.
function formatDate(iso: string): string {
const [y, m, d] = iso.split('-').map(Number);
return new Date(y, m - 1, d).toLocaleDateString(locale === 'ht' ? 'fr' : locale, {
day: 'numeric',
month: 'long',
year: 'numeric'
});
}
function windowLabel(iso: string): string {
const [y, m, d] = iso.split('-').map(Number);
const start = new Date(y, m - 1, d - 2);
const end = new Date(y, m - 1, d + 2);
const fmt = (date: Date) =>
date.toLocaleDateString(locale === 'ht' ? 'fr' : locale, {
day: 'numeric',
month: 'short'
});
return `${fmt(start)} ${fmt(end)}`;
}
</script>
<div class="calculator card">
<label class="calculator-label" for="site-select"
>{t(locale, 'observer_calculator_select_label')}</label
>
<select id="site-select" bind:value={selectedId} class="calculator-select">
{#each sites as site (site.id)}
<option value={site.id}>{site.name}</option>
{/each}
</select>
{#if prediction}
<div class="calculator-result" aria-live="polite">
<p class="calculator-result-label">{t(locale, 'observer_calculator_result_heliacal')}</p>
<p class="calculator-result-value">{formatDate(prediction.date)} · {prediction.time}</p>
<p class="calculator-result-label">{t(locale, 'observer_calculator_result_window')}</p>
<p class="calculator-result-window">{windowLabel(prediction.date)}</p>
</div>
{/if}
</div>
<!-- Table complète : toujours rendue (SSR), c'est le repli no-JS. À
l'hydratation, la ligne du site sélectionné est mise en valeur. -->
<div class="predictions-table card">
<table>
<caption class="visually-hidden">{t(locale, 'observer_calculator_title')}</caption>
<thead>
<tr>
<th scope="col">{t(locale, 'observer_calculator_table_site')}</th>
<th scope="col">{t(locale, 'observer_calculator_table_date')}</th>
<th scope="col">{t(locale, 'observer_calculator_table_time')}</th>
</tr>
</thead>
<tbody>
{#each sites as site (site.id)}
{@const p = predictionOf(site)}
<tr class:selected={site.id === selectedId}>
<th scope="row">
{site.name}
<div class="site-desc">{description(site)}</div>
</th>
{#if p}
<td>{formatDate(p.date)}</td>
<td>{p.time}</td>
{/if}
</tr>
{/each}
</tbody>
</table>
</div>
<style>
.calculator {
max-width: 28rem;
margin-inline: auto;
}
.calculator-label {
display: block;
font-weight: 700;
margin-bottom: var(--space-2);
}
.calculator-select {
width: 100%;
min-height: var(--tap-target);
padding-inline: var(--space-3);
border-radius: var(--radius-sm);
border: 2px solid var(--border);
background: var(--surface);
color: var(--ink);
font-size: var(--text-1);
}
.calculator-result {
margin-top: var(--space-4);
padding-top: var(--space-4);
border-top: 1px solid var(--border);
text-align: center;
}
.calculator-result-label {
font-size: var(--text-0);
color: var(--ink-soft);
margin-top: var(--space-2);
}
.calculator-result-value {
font-family: var(--font-display);
font-size: var(--text-3);
color: var(--gold);
}
.calculator-result-window {
font-weight: 600;
}
.predictions-table {
overflow-x: auto;
margin-top: var(--space-4);
}
table {
width: 100%;
border-collapse: collapse;
}
thead tr {
text-align: left;
border-bottom: 2px solid var(--border);
}
th,
td {
padding: var(--space-2);
}
tbody tr {
border-bottom: 1px solid var(--border);
transition: background-color 0.2s ease;
}
tbody tr.selected {
background: var(--gold-surface);
}
tbody th {
font-weight: 600;
text-align: left;
}
.site-desc {
font-size: var(--text-0);
color: var(--ink-soft);
font-weight: 400;
}
@media (prefers-reduced-motion: reduce) {
tbody tr {
transition: none;
}
}
</style>
@@ -0,0 +1,93 @@
<script lang="ts">
import { t, type Locale } from '$lib/i18n';
let { locale }: { locale: Locale } = $props();
</script>
<section class="section">
<div class="container">
<div class="section-header center">
<p class="section-eyebrow">Wep Ronpet</p>
<h1>{t(locale, 'associations_title')}</h1>
<p class="pattern-strip bogolan" aria-hidden="true" style="margin-inline:auto"></p>
<p class="section-subtitle">{t(locale, 'associations_subtitle')}</p>
</div>
<div class="grid-auto">
<div class="card">
<h2>{t(locale, 'associations_events_title')}</h2>
<div class="stack" style="--stack-space: 0">
<a
class="event-link"
href="https://my.weezevent.com/nouvel-an-kamite"
target="_blank"
rel="noopener noreferrer"
>
<span class="event-icon" aria-hidden="true">🎊</span>
<span class="event-info">
<span class="event-title">{t(locale, 'associations_event1_title')}</span>
<span class="event-platform">{t(locale, 'associations_event1_organizer')}</span>
<span>{t(locale, 'associations_event1_desc')}</span>
</span>
</a>
<a
class="event-link"
href="https://my.weezevent.com/nouvel-an-kamit"
target="_blank"
rel="noopener noreferrer"
>
<span class="event-icon" aria-hidden="true"></span>
<span class="event-info">
<span class="event-title">{t(locale, 'associations_event2_title')}</span>
<span class="event-platform">{t(locale, 'associations_event2_organizer')}</span>
<span>{t(locale, 'associations_event2_desc')}</span>
</span>
</a>
</div>
</div>
<div class="card">
<h2>{t(locale, 'associations_community_title')}</h2>
<div class="stack" style="--stack-space: 0">
<a
class="community-link"
href="https://14eme-membre.net/site-web/#événements"
target="_blank"
rel="noopener noreferrer"
>
<span class="community-icon" aria-hidden="true">🌍</span>
<span class="community-info">
<span class="community-title">{t(locale, 'associations_link1_title')}</span>
<span>{t(locale, 'associations_link1_desc')}</span>
</span>
</a>
<a
class="community-link"
href="https://www.facebook.com/people/Mouvement-Politique-Spirituel-MUN"
target="_blank"
rel="noopener noreferrer"
>
<span class="community-icon" aria-hidden="true">📘</span>
<span class="community-info">
<span class="community-title">{t(locale, 'associations_link2_title')}</span>
<span>{t(locale, 'associations_link2_desc')}</span>
</span>
</a>
</div>
</div>
</div>
<h2 class="center" style="margin-top: var(--space-7)">{t(locale, 'associations_video_title')}</h2>
<div class="video-frame card" style="margin-top: var(--space-4)">
<iframe
src="https://www.youtube.com/embed/0_8kWlDTf94"
title={t(locale, 'associations_video_title')}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
loading="lazy"
></iframe>
</div>
<p class="associations-note center">{t(locale, 'associations_note')}</p>
</div>
</section>
+186
View File
@@ -0,0 +1,186 @@
<script lang="ts">
import { t, type Locale } from '$lib/i18n';
let { locale }: { locale: Locale } = $props();
</script>
<section class="section">
<div class="container">
<div class="section-header center">
<p class="section-eyebrow">Sirius Gwada</p>
<h1>{t(locale, 'culture_title')}</h1>
<p class="pattern-strip adinkra" aria-hidden="true" style="margin-inline:auto"></p>
<p class="section-subtitle">{t(locale, 'culture_subtitle')}</p>
</div>
<div class="grid-auto">
<div class="feature-card">
<div style="font-size: var(--text-4)" aria-hidden="true">🏺</div>
<h3>{t(locale, 'culture_egypt_title')}</h3>
<p>{t(locale, 'culture_egypt_desc')}</p>
</div>
<div class="feature-card">
<div style="font-size: var(--text-4)" aria-hidden="true">🌍</div>
<h3>{t(locale, 'culture_dogon_title')}</h3>
<p>{t(locale, 'culture_dogon_desc')}</p>
</div>
<div class="feature-card">
<div style="font-size: var(--text-4)" aria-hidden="true"></div>
<h3>{t(locale, 'culture_caribbean_title')}</h3>
<p>{t(locale, 'culture_caribbean_desc')}</p>
</div>
</div>
<h2 class="center" style="margin-top: var(--space-7)">{t(locale, 'culture_timeline_title')}</h2>
<div class="timeline-v" style="margin-top: var(--space-5)">
<div class="timeline-v-item">
<p class="timeline-v-date">{t(locale, 'culture_timeline_1_date')}</p>
<h4>{t(locale, 'culture_timeline_1_title')}</h4>
<p>{t(locale, 'culture_timeline_1_desc')}</p>
</div>
<div class="timeline-v-item">
<p class="timeline-v-date">{t(locale, 'culture_timeline_2_date')}</p>
<h4>{t(locale, 'culture_timeline_2_title')}</h4>
<p>{t(locale, 'culture_timeline_2_desc')}</p>
</div>
<div class="timeline-v-item">
<p class="timeline-v-date">{t(locale, 'culture_timeline_3_date')}</p>
<h4>{t(locale, 'culture_timeline_3_title')}</h4>
<p>{t(locale, 'culture_timeline_3_desc')}</p>
</div>
<div class="timeline-v-item">
<p class="timeline-v-date">{t(locale, 'culture_timeline_4_date')}</p>
<h4>{t(locale, 'culture_timeline_4_title')}</h4>
<p>{t(locale, 'culture_timeline_4_desc')}</p>
</div>
</div>
</div>
</section>
<section class="section" style="background: var(--surface-sunk)">
<div class="container">
<div class="section-header center">
<h2>{t(locale, 'dogon_title')}</h2>
<p class="pattern-strip kente" aria-hidden="true" style="margin-inline:auto"></p>
<p class="section-subtitle">{t(locale, 'dogon_intro')}</p>
</div>
<fieldset class="tabs">
<legend class="visually-hidden">{t(locale, 'dogon_title')}</legend>
<input type="radio" name="dogon-tab" id="dogon-tab-cosmogony" class="tab-input" checked />
<input type="radio" name="dogon-tab" id="dogon-tab-system" class="tab-input" />
<input type="radio" name="dogon-tab" id="dogon-tab-astronomy" class="tab-input" />
<input type="radio" name="dogon-tab" id="dogon-tab-heritage" class="tab-input" />
<div class="tab-list" role="tablist">
<label class="tab-label" for="dogon-tab-cosmogony">{t(locale, 'dogon_tab_cosmogony')}</label>
<label class="tab-label" for="dogon-tab-system">{t(locale, 'dogon_tab_system')}</label>
<label class="tab-label" for="dogon-tab-astronomy">{t(locale, 'dogon_tab_astronomy')}</label>
<label class="tab-label" for="dogon-tab-heritage">{t(locale, 'dogon_tab_heritage')}</label>
</div>
<div class="tab-panels">
<div class="tab-panel prose center">
<p>{t(locale, 'dogon_cosmogony_p1')}</p>
<p>{t(locale, 'dogon_cosmogony_p2')}</p>
<div class="card">
<h3>{t(locale, 'dogon_cosmogony_nommo_title')}</h3>
<p>{t(locale, 'dogon_cosmogony_nommo_desc')}</p>
</div>
</div>
<div class="tab-panel">
<div class="grid-auto">
<div class="info-card">
<h3>{t(locale, 'dogon_system_sigutolo_title')}</h3>
<p>{t(locale, 'dogon_system_sigutolo_desc')}</p>
</div>
<div class="info-card">
<h3>{t(locale, 'dogon_system_potolo_title')}</h3>
<p>{t(locale, 'dogon_system_potolo_desc')}</p>
</div>
<div class="info-card">
<h3>{t(locale, 'dogon_system_emmeya_title')}</h3>
<p>{t(locale, 'dogon_system_emmeya_desc')}</p>
</div>
</div>
<div class="card prose center" style="margin-top: var(--space-4)">
<h3>{t(locale, 'dogon_mystery_title')}</h3>
<div class="grid-auto">
<div>
<h4>{t(locale, 'dogon_mystery_theory1_title')}</h4>
<p>{t(locale, 'dogon_mystery_theory1_desc')}</p>
</div>
<div>
<h4>{t(locale, 'dogon_mystery_theory2_title')}</h4>
<p>{t(locale, 'dogon_mystery_theory2_desc')}</p>
</div>
<div>
<h4>{t(locale, 'dogon_mystery_theory3_title')}</h4>
<p>{t(locale, 'dogon_mystery_theory3_desc')}</p>
</div>
</div>
</div>
<div class="card prose center" style="margin-top: var(--space-4)">
<h3>{t(locale, 'dogon_calc_example_title')}</h3>
<p>{t(locale, 'dogon_calc_example_text')}</p>
</div>
</div>
<div class="tab-panel">
<div class="grid-auto">
<div class="info-card">
<h3>{t(locale, 'dogon_astronomy_geometry_title')}</h3>
<p>{t(locale, 'dogon_astronomy_geometry_desc')}</p>
</div>
<div class="info-card">
<h3>{t(locale, 'dogon_astronomy_calendar_title')}</h3>
<p>{t(locale, 'dogon_astronomy_calendar_desc')}</p>
</div>
<div class="info-card">
<h3>{t(locale, 'dogon_astronomy_planets_title')}</h3>
<p>{t(locale, 'dogon_astronomy_planets_desc')}</p>
</div>
<div class="info-card">
<h3>{t(locale, 'dogon_astronomy_universe_title')}</h3>
<p>{t(locale, 'dogon_astronomy_universe_desc')}</p>
</div>
</div>
</div>
<div class="tab-panel">
<div class="timeline-v">
<div class="timeline-v-item">
<p class="timeline-v-date">{t(locale, 'dogon_heritage_1_date')}</p>
<p>{t(locale, 'dogon_heritage_1_desc')}</p>
</div>
<div class="timeline-v-item">
<p class="timeline-v-date">{t(locale, 'dogon_heritage_2_date')}</p>
<p>{t(locale, 'dogon_heritage_2_desc')}</p>
</div>
<div class="timeline-v-item">
<p class="timeline-v-date">{t(locale, 'dogon_heritage_3_date')}</p>
<p>{t(locale, 'dogon_heritage_3_desc')}</p>
</div>
<div class="timeline-v-item">
<p class="timeline-v-date">{t(locale, 'dogon_heritage_4_date')}</p>
<p>{t(locale, 'dogon_heritage_4_desc')}</p>
</div>
</div>
<div class="card prose center" style="margin-top: var(--space-5)">
<h3>{t(locale, 'dogon_heritage_diaspora_title')}</h3>
<p>{t(locale, 'dogon_heritage_diaspora_desc')}</p>
<div class="grid-auto" style="margin-top: var(--space-3); text-align: left">
<p>🇭🇹 {t(locale, 'dogon_heritage_diaspora_haiti')}</p>
<p>🇬🇵 {t(locale, 'dogon_heritage_diaspora_guadeloupe')}</p>
<p>🇲🇶 {t(locale, 'dogon_heritage_diaspora_martinique')}</p>
<p>🇯🇲 {t(locale, 'dogon_heritage_diaspora_jamaica')}</p>
</div>
</div>
</div>
</div>
</fieldset>
</div>
</section>
+133
View File
@@ -0,0 +1,133 @@
<script lang="ts">
import { resolve } from '$app/paths';
import nextRising from '$lib/data/nextRising.js';
import waveCities from '$lib/data/waveCities.js';
import { formatDate } from '$lib/dates';
import { localizedPath, t, type Locale } from '$lib/i18n';
/**
* Page d'accueil — portage de legacy/src/index.njk. Factorisée ici ;
* les routes `/`, `/en/` et `/ht/` ne sont que des coquilles qui
* passent leur locale (et leurs métadonnées via <Seo>).
*/
interface Props {
locale: Locale;
}
const { locale }: Props = $props();
// Calculé au build (prerender) — logique identique à l'ancien site.
const rising = nextRising();
</script>
<section class="hero">
<div class="hero-orbits" aria-hidden="true">
<div class="sun"></div>
<div class="orbit" style="width:5rem;height:5rem;animation-duration:6s"><span></span></div>
<div class="orbit" style="width:8rem;height:8rem;animation-duration:10s"><span></span></div>
<div class="orbit" style="width:11rem;height:11rem;animation-duration:14s"><span></span></div>
<div class="orbit" style="width:14rem;height:14rem;animation-duration:18s"><span></span></div>
<div class="orbit sirius" style="width:18rem;height:18rem;animation-duration:26s">
<span></span>
</div>
</div>
<div class="container center hero-content">
<p class="section-eyebrow">{t(locale, 'hero_eyebrow')}</p>
<h1 class="hero-title">
<span>{t(locale, 'hero_title1')}</span>
<span class="accent">{t(locale, 'hero_title2')}</span>
</h1>
<p class="hero-subtitle">{t(locale, 'hero_subtitle')}</p>
<p class="hero-description">{t(locale, 'hero_description')}</p>
<p class="hero-date">
{t(locale, 'hero_date_label')}
<strong>{formatDate(rising.date, locale)} · {rising.time}</strong>
</p>
<div class="cluster hero-cta">
<a class="btn btn-primary comet-ring" href={resolve(localizedPath(locale, '/observer/'))}
>{t(locale, 'hero_cta_predictions')}</a
>
<a class="btn btn-outline" href={resolve(localizedPath(locale, '/sirius-science/'))}
>{t(locale, 'hero_cta_learn')}</a
>
</div>
</div>
</section>
<section class="section" id="intro">
<div class="container">
<div class="section-header center">
<h2>{t(locale, 'home_intro_title')}</h2>
<p class="pattern-strip kente" aria-hidden="true" style="margin-inline:auto"></p>
<p class="section-subtitle">{t(locale, 'home_intro_subtitle')}</p>
</div>
<div class="prose center stack" style="--stack-space: var(--space-3)">
<!-- home_intro_p1 contient du <strong> (| safe dans le legacy) -->
<p>{@html t(locale, 'home_intro_p1')}</p>
<p>{t(locale, 'home_intro_p2')}</p>
<p>{t(locale, 'home_intro_p3')}</p>
</div>
<div class="grid-auto" style="margin-top: var(--space-6)">
<div class="feature-card reveal">
<h3>{t(locale, 'feature_brightest_title')}</h3>
<p>{t(locale, 'feature_brightest_desc')}</p>
</div>
<div class="feature-card reveal">
<h3>{t(locale, 'feature_rising_title')}</h3>
<p>{t(locale, 'feature_rising_desc')}</p>
</div>
<div class="feature-card reveal">
<h3>{t(locale, 'feature_heritage_title')}</h3>
<p>{t(locale, 'feature_heritage_desc')}</p>
</div>
</div>
</div>
</section>
<section class="section" style="background: var(--surface-sunk)">
<div class="container">
<div class="section-header center">
<h2>{t(locale, 'wave_showcase_title')}</h2>
<p class="section-subtitle">{t(locale, 'wave_showcase_subtitle')}</p>
</div>
<ul class="wave-badges">
{#each waveCities as city, i (city.name)}
<li class="wave-badge wave-badge-{i % 3}">
<span class="wave-badge-name">{city.name}</span>
<span class="wave-badge-date">{city.date}</span>
</li>
{/each}
</ul>
<p class="center" style="margin-top: var(--space-5)">
<a class="btn btn-outline" href={resolve(localizedPath(locale, '/observer/'))}
>{t(locale, 'wave_showcase_cta')}</a
>
</p>
</div>
</section>
<style>
/* Reveal d'entrée au scroll, CSS pur : sans effet si animation-timeline
n'est pas supporté, désactivé en reduced-motion. */
@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
.reveal {
animation: reveal-rise linear both;
animation-timeline: view();
animation-range: entry 0% entry 60%;
}
}
}
@keyframes reveal-rise {
from {
opacity: 0;
transform: translateY(1.5rem);
}
}
</style>
+138
View File
@@ -0,0 +1,138 @@
<script lang="ts">
import { t, type Locale } from '$lib/i18n';
import SiriusPredictions from '$lib/components/islands/SiriusPredictions.svelte';
import ObservationMap from '$lib/components/islands/ObservationMap.svelte';
import SiriusGlobe from '$lib/components/islands/SiriusGlobe.svelte';
let { locale }: { locale: Locale } = $props();
</script>
<section class="section">
<div class="container">
<div class="section-header center">
<p class="section-eyebrow">Sirius Gwada</p>
<h1>{t(locale, 'observer_title')}</h1>
<p class="pattern-strip kente" aria-hidden="true" style="margin-inline:auto"></p>
<p class="section-subtitle">{t(locale, 'observer_subtitle')}</p>
</div>
<div id="predictions-island">
<h2 class="center">{t(locale, 'observer_calculator_title')}</h2>
<p class="section-subtitle center">{t(locale, 'observer_calculator_subtitle')}</p>
<div style="margin-top: var(--space-4)">
<SiriusPredictions {locale} />
</div>
</div>
<div id="map-island" style="margin-top: var(--space-7)">
<h2 class="center">{t(locale, 'observer_map_title')}</h2>
<p class="section-subtitle center">{t(locale, 'observer_map_subtitle')}</p>
<div style="margin-top: var(--space-4)">
<ObservationMap {locale} />
</div>
</div>
<h2 class="center" style="margin-top: var(--space-7)">{t(locale, 'observer_timeline_title')}</h2>
<div class="timeline-h card" style="margin-top: var(--space-4)">
<div class="timeline-h-step">
<div class="timeline-h-icon" aria-hidden="true">🌙</div>
<p class="timeline-h-time">{t(locale, 'observer_phase1_time')}</p>
<h4>{t(locale, 'observer_phase1_title')}</h4>
<p>{t(locale, 'observer_phase1_desc')}</p>
</div>
<div class="timeline-h-step">
<div class="timeline-h-icon" aria-hidden="true">🔭</div>
<p class="timeline-h-time">{t(locale, 'observer_phase2_time')}</p>
<h4>{t(locale, 'observer_phase2_title')}</h4>
<p>{t(locale, 'observer_phase2_desc')}</p>
</div>
<div class="timeline-h-step">
<div class="timeline-h-icon" aria-hidden="true"></div>
<p class="timeline-h-time">{t(locale, 'observer_phase3_time')}</p>
<h4>{t(locale, 'observer_phase3_title')}</h4>
<p>{t(locale, 'observer_phase3_desc')}</p>
</div>
<div class="timeline-h-step">
<div class="timeline-h-icon" aria-hidden="true"></div>
<p class="timeline-h-time">{t(locale, 'observer_phase4_time')}</p>
<h4>{t(locale, 'observer_phase4_title')}</h4>
<p>{t(locale, 'observer_phase4_desc')}</p>
</div>
</div>
<h2 class="center" style="margin-top: var(--space-7)">{t(locale, 'observer_equipment_title')}</h2>
<div class="grid-auto" style="margin-top: var(--space-4)">
<div class="card info-card essential">
<div class="info-card-header">
<h3>{t(locale, 'observer_equipment_lamp_name')}</h3>
<span class="badge">{t(locale, 'observer_equipment_essential_badge')}</span>
</div>
<p>{t(locale, 'observer_equipment_lamp_desc')}</p>
</div>
<div class="card info-card essential">
<div class="info-card-header">
<h3>{t(locale, 'observer_equipment_compass_name')}</h3>
<span class="badge">{t(locale, 'observer_equipment_essential_badge')}</span>
</div>
<p>{t(locale, 'observer_equipment_compass_desc')}</p>
</div>
<div class="card info-card essential">
<div class="info-card-header">
<h3>{t(locale, 'observer_equipment_clothes_name')}</h3>
<span class="badge">{t(locale, 'observer_equipment_essential_badge')}</span>
</div>
<p>{t(locale, 'observer_equipment_clothes_desc')}</p>
</div>
<div class="card info-card">
<h3>{t(locale, 'observer_equipment_binoculars_name')}</h3>
<p>{t(locale, 'observer_equipment_binoculars_desc')}</p>
</div>
<div class="card info-card">
<h3>{t(locale, 'observer_equipment_notebook_name')}</h3>
<p>{t(locale, 'observer_equipment_notebook_desc')}</p>
</div>
</div>
<h2 class="center" style="margin-top: var(--space-7)">{t(locale, 'observer_tips_title')}</h2>
<div class="grid-auto" style="margin-top: var(--space-4)">
<div class="feature-card">
<div style="font-size: var(--text-4)" aria-hidden="true">🌤️</div>
<h3>{t(locale, 'observer_tip_weather_title')}</h3>
<p>{t(locale, 'observer_tip_weather_desc')}</p>
</div>
<div class="feature-card">
<div style="font-size: var(--text-4)" aria-hidden="true">🌑</div>
<h3>{t(locale, 'observer_tip_moon_title')}</h3>
<p>{t(locale, 'observer_tip_moon_desc')}</p>
</div>
<div class="feature-card">
<div style="font-size: var(--text-4)" aria-hidden="true">📱</div>
<h3>{t(locale, 'observer_tip_apps_title')}</h3>
<p>{t(locale, 'observer_tip_apps_desc')}</p>
</div>
<div class="feature-card">
<div style="font-size: var(--text-4)" aria-hidden="true">👥</div>
<h3>{t(locale, 'observer_tip_group_title')}</h3>
<p>{t(locale, 'observer_tip_group_desc')}</p>
</div>
</div>
</div>
</section>
<section class="section" id="globe-island" style="background: var(--surface-sunk)">
<div class="container">
<div class="section-header center">
<h2>{t(locale, 'globe_title')}</h2>
<p class="section-subtitle">{t(locale, 'globe_p1')}</p>
</div>
<p class="prose center">{t(locale, 'globe_p2')}</p>
<div style="margin-top: var(--space-4)">
<SiriusGlobe {locale} />
</div>
<p class="center" style="color: var(--ink-soft); font-size: var(--text-0)">
{t(locale, 'globe_controls_hint')}
</p>
</div>
</section>
+108
View File
@@ -0,0 +1,108 @@
<script lang="ts">
import { t, type Locale } from '$lib/i18n';
let { locale }: { locale: Locale } = $props();
</script>
<section class="section">
<div class="container">
<div class="section-header center">
<p class="section-eyebrow">Sirius Gwada</p>
<h1>{t(locale, 'science_title')}</h1>
<p class="pattern-strip bogolan" aria-hidden="true" style="margin-inline:auto"></p>
<p class="section-subtitle">{t(locale, 'science_subtitle')}</p>
</div>
<fieldset class="tabs">
<legend class="visually-hidden">{t(locale, 'science_title')}</legend>
<input type="radio" name="science-tab" id="tab-mechanics" class="tab-input" checked />
<input type="radio" name="science-tab" id="tab-arcus" class="tab-input" />
<input type="radio" name="science-tab" id="tab-factors" class="tab-input" />
<input type="radio" name="science-tab" id="tab-calc" class="tab-input" />
<div class="tab-list" role="tablist">
<label class="tab-label" for="tab-mechanics">{t(locale, 'science_tab_mechanics')}</label>
<label class="tab-label" for="tab-arcus">{t(locale, 'science_tab_arcus')}</label>
<label class="tab-label" for="tab-factors">{t(locale, 'science_tab_factors')}</label>
<label class="tab-label" for="tab-calc">{t(locale, 'science_tab_calc')}</label>
</div>
<div class="tab-panels">
<div class="tab-panel prose center">
<h2>{t(locale, 'science_mechanics_title')}</h2>
<p>{t(locale, 'science_mechanics_p1')}</p>
<div class="diagram-orbit card" aria-hidden="true">
<div class="ring"></div>
<div class="body sun">☀️<span>{t(locale, 'science_diagram_sun')}</span></div>
<div class="body earth">🌍<span>{t(locale, 'science_diagram_earth')}</span></div>
<div class="body sirius"><span>{t(locale, 'science_diagram_sirius')}</span></div>
</div>
<p>{t(locale, 'science_mechanics_p2')}</p>
</div>
<div class="tab-panel prose center">
<h2>{t(locale, 'science_arcus_title')}</h2>
<p>{t(locale, 'science_arcus_p1')}</p>
<div class="diagram-arcus card" aria-hidden="true">
<span class="horizon-label">{t(locale, 'science_arcus_horizon_label')}</span>
<div class="angle"></div>
<span class="angle-label">9-10°</span>
<span class="sirius-mark"></span>
<span class="sun-mark">☀️</span>
</div>
<p>{t(locale, 'science_arcus_p2')}</p>
</div>
<div class="tab-panel">
<div class="grid-auto">
<div class="info-card">
<h3>{t(locale, 'science_factor_magnitude_title')}</h3>
<p>{t(locale, 'science_factor_magnitude_desc')}</p>
</div>
<div class="info-card">
<h3>{t(locale, 'science_factor_latitude_title')}</h3>
<p>{t(locale, 'science_factor_latitude_desc')}</p>
</div>
<div class="info-card">
<h3>{t(locale, 'science_factor_atmosphere_title')}</h3>
<p>{t(locale, 'science_factor_atmosphere_desc')}</p>
</div>
</div>
</div>
<div class="tab-panel prose center">
<h2>{t(locale, 'science_calc_title')}</h2>
<div class="card">
<h3>{t(locale, 'science_calc_formula_label')}</h3>
<p style="font-family: ui-monospace, monospace">{t(locale, 'science_calc_formula')}</p>
</div>
<div class="card" style="margin-top: var(--space-3)">
<h3>{t(locale, 'science_calc_step1_title')}</h3>
<p style="font-family: ui-monospace, monospace">{t(locale, 'science_calc_step1_value')}</p>
</div>
<div class="card" style="margin-top: var(--space-3)">
<h3>{t(locale, 'science_calc_step2_title')}</h3>
<p style="font-family: ui-monospace, monospace">
{t(locale, 'science_calc_step2_ra')}<br />
{t(locale, 'science_calc_step2_dec')}
</p>
</div>
<div class="card" style="margin-top: var(--space-3)">
<h3>{t(locale, 'science_calc_arcus_title')}</h3>
<p>{t(locale, 'science_calc_arcus_desc')}</p>
</div>
<div class="card" style="margin-top: var(--space-3)">
<h3>{t(locale, 'science_calc_sources_title')}</h3>
<p>{t(locale, 'science_calc_sources_text')}</p>
</div>
</div>
</div>
</fieldset>
</div>
</section>
@@ -4,13 +4,15 @@ import sites from "./sites.js";
// comme site de référence — les 7 sites ne divergent que de quelques minutes).
export default function () {
const reference = sites[0];
/** @type {Record<number, { date: string; time: string }>} */
const predictions = reference.predictions;
const today = new Date();
const years = Object.keys(reference.predictions)
const years = Object.keys(predictions)
.map(Number)
.sort((a, b) => a - b);
for (const year of years) {
const p = reference.predictions[year];
const p = predictions[year];
const when = new Date(`${p.date}T${p.time}:00`);
if (when >= today) {
return { ...p, year };
@@ -18,5 +20,5 @@ export default function () {
}
const lastYear = years[years.length - 1];
return { ...reference.predictions[lastYear], year: lastYear };
return { ...predictions[lastYear], year: lastYear };
}
+24
View File
@@ -0,0 +1,24 @@
import type { Locale } from '$lib/i18n';
/**
* Formate une date ISO `YYYY-MM-DD` pour l'affichage.
*
* Piège documenté (NOTES_PROJET §3) : `new Date("2026-07-22")` est parsé en
* UTC minuit, ce qui peut reculer d'un jour une fois formaté dans un fuseau
* derrière UTC (Guadeloupe, UTC-4). On construit donc la date à partir de
* ses composants, en heure locale — logique reprise à l'identique du filtre
* `formatDate` de l'ancien eleventy.config.js.
*/
export function formatDate(
isoDate: string,
locale: Locale,
options: Intl.DateTimeFormatOptions = {}
): string {
const [year, month, day] = isoDate.split('-').map(Number);
return new Date(year, month - 1, day).toLocaleDateString(locale === 'ht' ? 'fr' : locale, {
day: 'numeric',
month: 'long',
year: 'numeric',
...options
});
}
@@ -3,6 +3,14 @@
"meta_title": "Sirius in Guadeloupe — Wep Ronpet, the heliacal rising",
"meta_description": "Calculate and understand the heliacal rising of Sirius in Guadeloupe: astronomy, Egyptian and Dogon heritage, observation guide.",
"meta_science_title": "Sirius & Science",
"meta_science_description": "The mechanics of Sirius's heliacal rising explained: Arcus Visionis, visibility factors and calculation method for Guadeloupe.",
"meta_culture_title": "Culture & Knowledge",
"meta_culture_description": "Sirius across cultures: ancient Egypt, Dogon astronomical knowledge and the Caribbean heritage of Wep Ronpet.",
"meta_observer_title": "Observe",
"meta_observer_description": "Dates and times of Sirius's heliacal rising for 7 observation sites in Guadeloupe, map, equipment and practical tips.",
"meta_associations_title": "Associations celebrating Wep Ronpet",
"meta_associations_description": "Events, communities and associations celebrating Wep Ronpet, the Kamit New Year, in Guadeloupe and beyond.",
"skip_to_content": "Skip to content",
"lang_name_fr": "Français",
@@ -3,6 +3,14 @@
"meta_title": "Sirius en Guadeloupe — Wep Ronpet, le lever héliaque",
"meta_description": "Calculez et comprenez le lever héliaque de Sirius en Guadeloupe : astronomie, héritage égyptien et dogon, guide d'observation.",
"meta_science_title": "Sirius & Science",
"meta_science_description": "La mécanique du lever héliaque de Sirius expliquée : Arcus Visionis, facteurs de visibilité et méthode de calcul pour la Guadeloupe.",
"meta_culture_title": "Culture & Savoirs",
"meta_culture_description": "Sirius dans les cultures : Égypte antique, savoirs astronomiques dogon et héritage caribéen du Wep Ronpet.",
"meta_observer_title": "Observer",
"meta_observer_description": "Dates et heures du lever héliaque de Sirius pour 7 sites d'observation en Guadeloupe, carte, équipement et conseils pratiques.",
"meta_associations_title": "Associations qui fêtent le Wep Ronpet",
"meta_associations_description": "Événements, communautés et associations qui célèbrent le Wep Ronpet, le Nouvel An kamite, en Guadeloupe et au-delà.",
"skip_to_content": "Aller au contenu",
"lang_name_fr": "Français",
+71
View File
@@ -0,0 +1,71 @@
/**
* i18n — catalogues Paraglide (format message-format JSON) consommés
* directement, routage par dossiers : `/` = fr, `/en/`, `/ht/`.
* Le créole (ht) couvre le chrome ; le contenu long retombe sur le fr
* (fallback documenté dans NOTES_PROJET).
*/
import en from './en.json';
import fr from './fr.json';
import ht from './ht.json';
import type { Pathname } from '$app/types';
export const LOCALES = ['fr', 'en', 'ht'] as const;
export type Locale = (typeof LOCALES)[number];
export const DEFAULT_LOCALE: Locale = 'fr';
type Messages = Record<string, string>;
const BUNDLES: Record<Locale, Messages> = {
fr: fr as Messages,
en: en as Messages,
ht: ht as Messages
};
export function isLocale(value: string): value is Locale {
return (LOCALES as readonly string[]).includes(value);
}
/** Locale déduite du chemin (`/en/...` → en, `/ht/...` → ht, sinon fr). */
export function localeFromPath(pathname: string): Locale {
const segment = pathname.split('/').filter(Boolean)[0] ?? '';
return isLocale(segment) ? segment : DEFAULT_LOCALE;
}
export function getMessages(locale: Locale): Messages {
return BUNDLES[locale] ?? BUNDLES[DEFAULT_LOCALE];
}
/**
* Traduit une clé : locale demandée, fallback fr, fallback clé.
* Interpolation `{name}` via `vars`.
*/
export function t(locale: Locale, key: string, vars?: Record<string, string | number>): string {
const raw = BUNDLES[locale]?.[key] ?? BUNDLES[DEFAULT_LOCALE][key] ?? key;
if (!vars) return raw;
return raw.replace(/\{(\w+)\}/g, (_, name) => String(vars[name] ?? `{${name}}`));
}
/** Préfixe d'URL d'une locale (`''` pour fr, `/en`, `/ht`). */
export function localePrefix(locale: Locale): string {
return locale === DEFAULT_LOCALE ? '' : `/${locale}`;
}
/** Chemin localisé (`/observer/` + en → `/en/observer/`). */
export function localizedPath(locale: Locale, path: string): Pathname {
const prefix = localePrefix(locale);
return `${prefix}${path === '/' ? '/' : path}` as Pathname;
}
/** Chemin de la même page dans une autre locale. */
export function switchLocalePath(pathname: string, target: Locale): Pathname {
const current = localeFromPath(pathname);
const prefix = localePrefix(current);
const bare = prefix ? pathname.slice(prefix.length) : pathname;
return (localePrefix(target) + (bare || '/')) as Pathname;
}
/** Libellé natif d'une locale (lang_name_*). */
export function localeLabel(locale: Locale): string {
return t(locale, `lang_name_${locale}`);
}
@@ -1,26 +1,6 @@
@font-face {
font-family: "Fraunces Variable";
src: url("/assets/fonts/fraunces-variable.woff2") format("woff2-variations");
font-weight: 300 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Atkinson Hyperlegible";
src: url("/assets/fonts/atkinson-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Atkinson Hyperlegible";
src: url("/assets/fonts/atkinson-700.woff2") format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* Les @font-face vivent dans /fonts/fonts.css (static/), chargé par app.html
ne pas les dupliquer ici (l'ancien build 11ty servait les polices en double,
sous /assets/fonts/, avec des URLs non préfixées par le base path). */
html {
font-size: 100%;
-180
View File
@@ -1,180 +0,0 @@
---
pagination:
data: locales
size: 1
alias: locale
permalink: "{% if locale == baseLocale %}/observer/index.html{% else %}/{{ locale }}/observer/index.html{% endif %}"
layout: layouts/base.njk
pageKey: observer
slug: "observer/"
eleventyComputed:
pageTitle: "{{ 'observer_title' | t(locale) }}"
---
<section class="section">
<div class="container">
<div class="section-header center">
<p class="section-eyebrow">Sirius Gwada</p>
<h1>{{ "observer_title" | t(locale) }}</h1>
<p class="pattern-strip kente" aria-hidden="true" style="margin-inline:auto"></p>
<p class="section-subtitle">{{ "observer_subtitle" | t(locale) }}</p>
</div>
<script type="application/json" id="sites-data">{{ sites | dump | safe }}</script>
<div id="predictions-island" data-island="predictions">
<h2 class="center">{{ "observer_calculator_title" | t(locale) }}</h2>
<p class="section-subtitle center">{{ "observer_calculator_subtitle" | t(locale) }}</p>
<div class="island-app" data-year="{{ nextRising.year }}" data-locale="{{ locale }}"
data-select-label="{{ 'observer_calculator_select_label' | t(locale) }}"
data-heliacal-label="{{ 'observer_calculator_result_heliacal' | t(locale) }}"
data-window-label="{{ 'observer_calculator_result_window' | t(locale) }}"
style="margin-top: var(--space-4)"></div>
<div class="island-fallback card" style="overflow-x: auto; margin-top: var(--space-4)">
<table style="width: 100%; border-collapse: collapse">
<caption class="visually-hidden">{{ "observer_calculator_title" | t(locale) }}</caption>
<thead>
<tr style="text-align: left; border-bottom: 2px solid var(--border)">
<th style="padding: var(--space-2)" scope="col">{{ "observer_calculator_table_site" | t(locale) }}</th>
<th style="padding: var(--space-2)" scope="col">{{ "observer_calculator_table_date" | t(locale) }}</th>
<th style="padding: var(--space-2)" scope="col">{{ "observer_calculator_table_time" | t(locale) }}</th>
</tr>
</thead>
<tbody>
{% for site in sites %}
<tr style="border-bottom: 1px solid var(--border)">
<th scope="row" style="padding: var(--space-2); font-weight: 600; text-align: left">
{{ site.name }}
<div style="font-size: var(--text-0); color: var(--ink-soft); font-weight: 400">
{{ site["description_" + locale] or site.description_fr }}
</div>
</th>
<td style="padding: var(--space-2)">{{ site.predictions[nextRising.year].date | formatDate(locale) }}</td>
<td style="padding: var(--space-2)">{{ site.predictions[nextRising.year].time }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div id="map-island" data-island="map" style="margin-top: var(--space-7)">
<h2 class="center">{{ "observer_map_title" | t(locale) }}</h2>
<p class="section-subtitle center">{{ "observer_map_subtitle" | t(locale) }}</p>
<div class="island-app" style="margin-top: var(--space-4)"></div>
<ul class="island-fallback grid-auto" style="margin-top: var(--space-4); list-style: none">
{% for site in sites %}
<li class="card">
<h3>{{ site.name }}</h3>
<p>{{ site["description_" + locale] or site.description_fr }}</p>
<p style="font-size: var(--text-0); color: var(--ink-dim)">{{ site.lat }}°N, {{ site.lon }}°O — {{ site.alt }} m</p>
</li>
{% endfor %}
</ul>
</div>
<h2 class="center" style="margin-top: var(--space-7)">{{ "observer_timeline_title" | t(locale) }}</h2>
<div class="timeline-h card" style="margin-top: var(--space-4)">
<div class="timeline-h-step">
<div class="timeline-h-icon" aria-hidden="true">🌙</div>
<p class="timeline-h-time">{{ "observer_phase1_time" | t(locale) }}</p>
<h4>{{ "observer_phase1_title" | t(locale) }}</h4>
<p>{{ "observer_phase1_desc" | t(locale) }}</p>
</div>
<div class="timeline-h-step">
<div class="timeline-h-icon" aria-hidden="true">🔭</div>
<p class="timeline-h-time">{{ "observer_phase2_time" | t(locale) }}</p>
<h4>{{ "observer_phase2_title" | t(locale) }}</h4>
<p>{{ "observer_phase2_desc" | t(locale) }}</p>
</div>
<div class="timeline-h-step">
<div class="timeline-h-icon" aria-hidden="true">⭐</div>
<p class="timeline-h-time">{{ "observer_phase3_time" | t(locale) }}</p>
<h4>{{ "observer_phase3_title" | t(locale) }}</h4>
<p>{{ "observer_phase3_desc" | t(locale) }}</p>
</div>
<div class="timeline-h-step">
<div class="timeline-h-icon" aria-hidden="true">✨</div>
<p class="timeline-h-time">{{ "observer_phase4_time" | t(locale) }}</p>
<h4>{{ "observer_phase4_title" | t(locale) }}</h4>
<p>{{ "observer_phase4_desc" | t(locale) }}</p>
</div>
</div>
<h2 class="center" style="margin-top: var(--space-7)">{{ "observer_equipment_title" | t(locale) }}</h2>
<div class="grid-auto" style="margin-top: var(--space-4)">
<div class="card info-card essential">
<div class="info-card-header">
<h3>{{ "observer_equipment_lamp_name" | t(locale) }}</h3>
<span class="badge">{{ "observer_equipment_essential_badge" | t(locale) }}</span>
</div>
<p>{{ "observer_equipment_lamp_desc" | t(locale) }}</p>
</div>
<div class="card info-card essential">
<div class="info-card-header">
<h3>{{ "observer_equipment_compass_name" | t(locale) }}</h3>
<span class="badge">{{ "observer_equipment_essential_badge" | t(locale) }}</span>
</div>
<p>{{ "observer_equipment_compass_desc" | t(locale) }}</p>
</div>
<div class="card info-card essential">
<div class="info-card-header">
<h3>{{ "observer_equipment_clothes_name" | t(locale) }}</h3>
<span class="badge">{{ "observer_equipment_essential_badge" | t(locale) }}</span>
</div>
<p>{{ "observer_equipment_clothes_desc" | t(locale) }}</p>
</div>
<div class="card info-card">
<h3>{{ "observer_equipment_binoculars_name" | t(locale) }}</h3>
<p>{{ "observer_equipment_binoculars_desc" | t(locale) }}</p>
</div>
<div class="card info-card">
<h3>{{ "observer_equipment_notebook_name" | t(locale) }}</h3>
<p>{{ "observer_equipment_notebook_desc" | t(locale) }}</p>
</div>
</div>
<h2 class="center" style="margin-top: var(--space-7)">{{ "observer_tips_title" | t(locale) }}</h2>
<div class="grid-auto" style="margin-top: var(--space-4)">
<div class="feature-card">
<div style="font-size: var(--text-4)" aria-hidden="true">🌤️</div>
<h3>{{ "observer_tip_weather_title" | t(locale) }}</h3>
<p>{{ "observer_tip_weather_desc" | t(locale) }}</p>
</div>
<div class="feature-card">
<div style="font-size: var(--text-4)" aria-hidden="true">🌑</div>
<h3>{{ "observer_tip_moon_title" | t(locale) }}</h3>
<p>{{ "observer_tip_moon_desc" | t(locale) }}</p>
</div>
<div class="feature-card">
<div style="font-size: var(--text-4)" aria-hidden="true">📱</div>
<h3>{{ "observer_tip_apps_title" | t(locale) }}</h3>
<p>{{ "observer_tip_apps_desc" | t(locale) }}</p>
</div>
<div class="feature-card">
<div style="font-size: var(--text-4)" aria-hidden="true">👥</div>
<h3>{{ "observer_tip_group_title" | t(locale) }}</h3>
<p>{{ "observer_tip_group_desc" | t(locale) }}</p>
</div>
</div>
</div>
</section>
<section class="section" id="globe-island" data-island="globe" style="background: var(--surface-sunk)">
<div class="container">
<div class="section-header center">
<h2>{{ "globe_title" | t(locale) }}</h2>
<p class="section-subtitle">{{ "globe_p1" | t(locale) }}</p>
</div>
<p class="prose center">{{ "globe_p2" | t(locale) }}</p>
<div class="island-app" style="margin-top: var(--space-4)"></div>
<p class="center" style="color: var(--ink-soft); font-size: var(--text-0)">{{ "globe_controls_hint" | t(locale) }}</p>
</div>
</section>
<script type="module" src="/src/islands/predictions/mount.js"></script>
<script type="module" src="/src/islands/map/mount.js"></script>
<script type="module" src="/src/islands/globe/mount.js"></script>
+35
View File
@@ -0,0 +1,35 @@
<script lang="ts">
import '$lib/styles/app.css';
import { base } from '$app/paths';
import { page } from '$app/state';
import Footer from '$lib/components/Footer.svelte';
import Nav from '$lib/components/Nav.svelte';
import { localeFromPath, t } from '$lib/i18n';
import type { Snippet } from 'svelte';
interface Props {
children: Snippet;
}
const { children }: Props = $props();
// page.url.pathname inclut le base path (/gwada-sirius) : on le retire
// avant de déduire la locale du premier segment.
const locale = $derived(
localeFromPath(
page.url.pathname.startsWith(base)
? page.url.pathname.slice(base.length) || '/'
: page.url.pathname
)
);
</script>
<a class="skip-link" href="#main-content">{t(locale, 'skip_to_content')}</a>
<Nav {locale} />
<main id="main-content">
{@render children()}
</main>
<Footer {locale} />
+2
View File
@@ -0,0 +1,2 @@
export const prerender = true;
export const trailingSlash = 'always';
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import Home from '$lib/components/pages/Home.svelte';
import Seo from '$lib/components/Seo.svelte';
import { t, type Locale } from '$lib/i18n';
const locale: Locale = 'fr';
</script>
<Seo
{locale}
title="{t(locale, 'hero_title1')} {t(locale, 'hero_title2')}{t(locale, 'meta_title')}"
description={t(locale, 'meta_description')}
path="/"
/>
<Home {locale} />
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import Seo from '$lib/components/Seo.svelte';
import Associations from '$lib/components/pages/Associations.svelte';
import { t, type Locale } from '$lib/i18n';
const locale: Locale = 'fr';
</script>
<Seo
{locale}
title={t(locale, 'meta_associations_title')}
description={t(locale, 'meta_associations_description')}
path="/associations/"
/>
<Associations {locale} />
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import Seo from '$lib/components/Seo.svelte';
import Culture from '$lib/components/pages/Culture.svelte';
import { t, type Locale } from '$lib/i18n';
const locale: Locale = 'fr';
</script>
<Seo
{locale}
title={t(locale, 'meta_culture_title')}
description={t(locale, 'meta_culture_description')}
path="/culture-savoirs/"
/>
<Culture {locale} />
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import Home from '$lib/components/pages/Home.svelte';
import Seo from '$lib/components/Seo.svelte';
import { t, type Locale } from '$lib/i18n';
const locale: Locale = 'en';
</script>
<Seo
{locale}
title="{t(locale, 'hero_title1')} {t(locale, 'hero_title2')}{t(locale, 'meta_title')}"
description={t(locale, 'meta_description')}
path="/"
/>
<Home {locale} />
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import Seo from '$lib/components/Seo.svelte';
import Associations from '$lib/components/pages/Associations.svelte';
import { t, type Locale } from '$lib/i18n';
const locale: Locale = 'en';
</script>
<Seo
{locale}
title={t(locale, 'meta_associations_title')}
description={t(locale, 'meta_associations_description')}
path="/associations/"
/>
<Associations {locale} />
@@ -0,0 +1,16 @@
<script lang="ts">
import Seo from '$lib/components/Seo.svelte';
import Culture from '$lib/components/pages/Culture.svelte';
import { t, type Locale } from '$lib/i18n';
const locale: Locale = 'en';
</script>
<Seo
{locale}
title={t(locale, 'meta_culture_title')}
description={t(locale, 'meta_culture_description')}
path="/culture-savoirs/"
/>
<Culture {locale} />
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import Seo from '$lib/components/Seo.svelte';
import Observer from '$lib/components/pages/Observer.svelte';
import { t, type Locale } from '$lib/i18n';
const locale: Locale = 'en';
</script>
<Seo
{locale}
title={t(locale, 'meta_observer_title')}
description={t(locale, 'meta_observer_description')}
path="/observer/"
/>
<Observer {locale} />
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import Seo from '$lib/components/Seo.svelte';
import Science from '$lib/components/pages/Science.svelte';
import { t, type Locale } from '$lib/i18n';
const locale: Locale = 'en';
</script>
<Seo
{locale}
title={t(locale, 'meta_science_title')}
description={t(locale, 'meta_science_description')}
path="/sirius-science/"
/>
<Science {locale} />
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import Home from '$lib/components/pages/Home.svelte';
import Seo from '$lib/components/Seo.svelte';
import { t, type Locale } from '$lib/i18n';
const locale: Locale = 'ht';
</script>
<Seo
{locale}
title="{t(locale, 'hero_title1')} {t(locale, 'hero_title2')}{t(locale, 'meta_title')}"
description={t(locale, 'meta_description')}
path="/"
/>
<Home {locale} />
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import Seo from '$lib/components/Seo.svelte';
import Associations from '$lib/components/pages/Associations.svelte';
import { t, type Locale } from '$lib/i18n';
const locale: Locale = 'ht';
</script>
<Seo
{locale}
title={t(locale, 'meta_associations_title')}
description={t(locale, 'meta_associations_description')}
path="/associations/"
/>
<Associations {locale} />
@@ -0,0 +1,16 @@
<script lang="ts">
import Seo from '$lib/components/Seo.svelte';
import Culture from '$lib/components/pages/Culture.svelte';
import { t, type Locale } from '$lib/i18n';
const locale: Locale = 'ht';
</script>
<Seo
{locale}
title={t(locale, 'meta_culture_title')}
description={t(locale, 'meta_culture_description')}
path="/culture-savoirs/"
/>
<Culture {locale} />
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import Seo from '$lib/components/Seo.svelte';
import Observer from '$lib/components/pages/Observer.svelte';
import { t, type Locale } from '$lib/i18n';
const locale: Locale = 'ht';
</script>
<Seo
{locale}
title={t(locale, 'meta_observer_title')}
description={t(locale, 'meta_observer_description')}
path="/observer/"
/>
<Observer {locale} />
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import Seo from '$lib/components/Seo.svelte';
import Science from '$lib/components/pages/Science.svelte';
import { t, type Locale } from '$lib/i18n';
const locale: Locale = 'ht';
</script>
<Seo
{locale}
title={t(locale, 'meta_science_title')}
description={t(locale, 'meta_science_description')}
path="/sirius-science/"
/>
<Science {locale} />
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import Seo from '$lib/components/Seo.svelte';
import Observer from '$lib/components/pages/Observer.svelte';
import { t, type Locale } from '$lib/i18n';
const locale: Locale = 'fr';
</script>
<Seo
{locale}
title={t(locale, 'meta_observer_title')}
description={t(locale, 'meta_observer_description')}
path="/observer/"
/>
<Observer {locale} />
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import Seo from '$lib/components/Seo.svelte';
import Science from '$lib/components/pages/Science.svelte';
import { t, type Locale } from '$lib/i18n';
const locale: Locale = 'fr';
</script>
<Seo
{locale}
title={t(locale, 'meta_science_title')}
description={t(locale, 'meta_science_description')}
path="/sirius-science/"
/>
<Science {locale} />
+39
View File
@@ -0,0 +1,39 @@
// Service worker minimal : résilience réseau (3G/4G irrégulier) et usage
// hors-ligne des pages déjà visitées. Stratégie stale-while-revalidate.
// NOTES_PROJET §5 : incrémenter CACHE_NAME à chaque évolution du SW.
const CACHE_NAME = "gwada-sirius-v3";
self.addEventListener("install", () => {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches
.keys()
.then((keys) =>
Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)))
)
);
self.clients.claim();
});
self.addEventListener("fetch", (event) => {
if (event.request.method !== "GET") return;
const url = new URL(event.request.url);
if (url.origin !== self.location.origin) return;
event.respondWith(
caches.open(CACHE_NAME).then(async (cache) => {
const cached = await cache.match(event.request);
const network = fetch(event.request)
.then((response) => {
if (response.ok) cache.put(event.request, response.clone());
return response;
})
.catch(() => cached);
return cached || network;
})
);
});
-114
View File
@@ -1,114 +0,0 @@
---
pagination:
data: locales
size: 1
alias: locale
permalink: "{% if locale == baseLocale %}/sirius-science/index.html{% else %}/{{ locale }}/sirius-science/index.html{% endif %}"
layout: layouts/base.njk
pageKey: science
slug: "sirius-science/"
eleventyComputed:
pageTitle: "{{ 'science_title' | t(locale) }}"
---
<section class="section">
<div class="container">
<div class="section-header center">
<p class="section-eyebrow">Sirius Gwada</p>
<h1>{{ "science_title" | t(locale) }}</h1>
<p class="pattern-strip bogolan" aria-hidden="true" style="margin-inline:auto"></p>
<p class="section-subtitle">{{ "science_subtitle" | t(locale) }}</p>
</div>
<fieldset class="tabs">
<legend class="visually-hidden">{{ "science_title" | t(locale) }}</legend>
<input type="radio" name="science-tab" id="tab-mechanics" class="tab-input" checked>
<input type="radio" name="science-tab" id="tab-arcus" class="tab-input">
<input type="radio" name="science-tab" id="tab-factors" class="tab-input">
<input type="radio" name="science-tab" id="tab-calc" class="tab-input">
<div class="tab-list" role="tablist">
<label class="tab-label" for="tab-mechanics">{{ "science_tab_mechanics" | t(locale) }}</label>
<label class="tab-label" for="tab-arcus">{{ "science_tab_arcus" | t(locale) }}</label>
<label class="tab-label" for="tab-factors">{{ "science_tab_factors" | t(locale) }}</label>
<label class="tab-label" for="tab-calc">{{ "science_tab_calc" | t(locale) }}</label>
</div>
<div class="tab-panels">
<div class="tab-panel prose center">
<h2>{{ "science_mechanics_title" | t(locale) }}</h2>
<p>{{ "science_mechanics_p1" | t(locale) }}</p>
<div class="diagram-orbit card" aria-hidden="true">
<div class="ring"></div>
<div class="body sun">☀️<span>{{ "science_diagram_sun" | t(locale) }}</span></div>
<div class="body earth">🌍<span>{{ "science_diagram_earth" | t(locale) }}</span></div>
<div class="body sirius">⭐<span>{{ "science_diagram_sirius" | t(locale) }}</span></div>
</div>
<p>{{ "science_mechanics_p2" | t(locale) }}</p>
</div>
<div class="tab-panel prose center">
<h2>{{ "science_arcus_title" | t(locale) }}</h2>
<p>{{ "science_arcus_p1" | t(locale) }}</p>
<div class="diagram-arcus card" aria-hidden="true">
<span class="horizon-label">{{ "science_arcus_horizon_label" | t(locale) }}</span>
<div class="angle"></div>
<span class="angle-label">9-10°</span>
<span class="sirius-mark">⭐</span>
<span class="sun-mark">☀️</span>
</div>
<p>{{ "science_arcus_p2" | t(locale) }}</p>
</div>
<div class="tab-panel">
<div class="grid-auto">
<div class="info-card">
<h3>{{ "science_factor_magnitude_title" | t(locale) }}</h3>
<p>{{ "science_factor_magnitude_desc" | t(locale) }}</p>
</div>
<div class="info-card">
<h3>{{ "science_factor_latitude_title" | t(locale) }}</h3>
<p>{{ "science_factor_latitude_desc" | t(locale) }}</p>
</div>
<div class="info-card">
<h3>{{ "science_factor_atmosphere_title" | t(locale) }}</h3>
<p>{{ "science_factor_atmosphere_desc" | t(locale) }}</p>
</div>
</div>
</div>
<div class="tab-panel prose center">
<h2>{{ "science_calc_title" | t(locale) }}</h2>
<div class="card">
<h3>{{ "science_calc_formula_label" | t(locale) }}</h3>
<p style="font-family: ui-monospace, monospace">{{ "science_calc_formula" | t(locale) }}</p>
</div>
<div class="card" style="margin-top: var(--space-3)">
<h3>{{ "science_calc_step1_title" | t(locale) }}</h3>
<p style="font-family: ui-monospace, monospace">{{ "science_calc_step1_value" | t(locale) }}</p>
</div>
<div class="card" style="margin-top: var(--space-3)">
<h3>{{ "science_calc_step2_title" | t(locale) }}</h3>
<p style="font-family: ui-monospace, monospace">
{{ "science_calc_step2_ra" | t(locale) }}<br>
{{ "science_calc_step2_dec" | t(locale) }}
</p>
</div>
<div class="card" style="margin-top: var(--space-3)">
<h3>{{ "science_calc_arcus_title" | t(locale) }}</h3>
<p>{{ "science_calc_arcus_desc" | t(locale) }}</p>
</div>
<div class="card" style="margin-top: var(--space-3)">
<h3>{{ "science_calc_sources_title" | t(locale) }}</h3>
<p>{{ "science_calc_sources_text" | t(locale) }}</p>
</div>
</div>
</div>
</fieldset>
</div>
</section>
+3 -2
View File
@@ -9,12 +9,13 @@ if [ ! -d "node_modules" ]; then
fi
echo "🚀 Lancement du serveur de développement..."
echo "📱 L'application sera accessible sur http://localhost:8080"
echo "📱 L'application sera accessible sur http://localhost:5173/gwada-sirius/"
echo ""
echo "Commandes disponibles:"
echo " - Ctrl+C : Arrêter le serveur"
echo " - npm run build : Créer une version de production (11ty + Vite -> _site/)"
echo " - npm run build : Créer une version de production (SvelteKit -> build/)"
echo " - npm run preview : Prévisualiser la version de production"
echo " - npm run check : Vérifier types et accessibilité"
echo ""
npm run dev

Before

Width:  |  Height:  |  Size: 234 B

After

Width:  |  Height:  |  Size: 234 B

+22
View File
@@ -0,0 +1,22 @@
/* Fraunces variable (display, titres) + Atkinson Hyperlegible (corps) — auto-hébergées */
@font-face {
font-family: 'Fraunces';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/gwada-sirius/fonts/fraunces-variable.woff2') format('woff2');
}
@font-face {
font-family: 'Atkinson Hyperlegible';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/gwada-sirius/fonts/atkinson-400.woff2') format('woff2');
}
@font-face {
font-family: 'Atkinson Hyperlegible';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/gwada-sirius/fonts/atkinson-700.woff2') format('woff2');
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Sirius en Guadeloupe — Wep Ronpet",
"short_name": "Sirius Gwada",
"description": "Lever héliaque de Sirius en Guadeloupe : astronomie, culture et observation.",
"start_url": ".",
"display": "standalone",
"background_color": "#141a33",
"theme_color": "#141a33",
"icons": [{ "src": "favicon.svg", "sizes": "any", "type": "image/svg+xml" }]
}
+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://cyber-mawonaj.github.io/gwada-sirius/sitemap.xml
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://cyber-mawonaj.github.io/gwada-sirius/</loc>
<lastmod>2026-07-21</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://cyber-mawonaj.github.io/gwada-sirius/sirius-science/</loc>
<lastmod>2026-07-21</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://cyber-mawonaj.github.io/gwada-sirius/culture-savoirs/</loc>
<lastmod>2026-07-21</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://cyber-mawonaj.github.io/gwada-sirius/observer/</loc>
<lastmod>2026-07-21</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://cyber-mawonaj.github.io/gwada-sirius/associations/</loc>
<lastmod>2026-07-21</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://cyber-mawonaj.github.io/gwada-sirius/en/</loc>
<lastmod>2026-07-21</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://cyber-mawonaj.github.io/gwada-sirius/en/sirius-science/</loc>
<lastmod>2026-07-21</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://cyber-mawonaj.github.io/gwada-sirius/en/culture-savoirs/</loc>
<lastmod>2026-07-21</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://cyber-mawonaj.github.io/gwada-sirius/en/observer/</loc>
<lastmod>2026-07-21</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://cyber-mawonaj.github.io/gwada-sirius/en/associations/</loc>
<lastmod>2026-07-21</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://cyber-mawonaj.github.io/gwada-sirius/ht/</loc>
<lastmod>2026-07-21</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://cyber-mawonaj.github.io/gwada-sirius/ht/sirius-science/</loc>
<lastmod>2026-07-21</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://cyber-mawonaj.github.io/gwada-sirius/ht/culture-savoirs/</loc>
<lastmod>2026-07-21</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://cyber-mawonaj.github.io/gwada-sirius/ht/observer/</loc>
<lastmod>2026-07-21</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://cyber-mawonaj.github.io/gwada-sirius/ht/associations/</loc>
<lastmod>2026-07-21</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>
+28
View File
@@ -0,0 +1,28 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
precompress: false,
strict: true
}),
// GitHub Pages : le site est servi sous /gwada-sirius/
// relative: false — chemins absolus partout (sinon $app/paths.base vaut ".."
// au prerender et les comparaisons de pathname se cassent).
paths: {
base: '/gwada-sirius',
relative: false
},
prerender: {
handleHttpError: 'warn',
handleMissingId: 'warn'
}
}
};
export default config;
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
}
+6
View File
@@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});