feat(design): refonte hi-fi (tokens, pills glyphes, dark mode, accueil)
All checks were successful
Deploy info-canicule / deploy (push) Successful in 1m30s
All checks were successful
Deploy info-canicule / deploy (push) Successful in 1m30s
Adopte le design system livré par Claude Design (Info Canicule.html) : palette --paper/--brand/--ink + ramp vigilance, Public Sans + Manrope, header sticky blurred avec toggle clair/sombre, pills vigilance avec glyphes ●▲◆■ (a11y daltonisme), home restructurée (hero, stat tiles, map + sidebar avec recherche département, liste filtrable, CTA conseils). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a8830a4f34
commit
72b3785499
7 changed files with 1003 additions and 250 deletions
|
|
@ -55,17 +55,15 @@ for (const [code] of entries) {
|
||||||
{
|
{
|
||||||
entries.map(([code, dept]) => {
|
entries.map(([code, dept]) => {
|
||||||
const colorId = colorsByDept.get(code) ?? 1;
|
const colorId = colorsByDept.get(code) ?? 1;
|
||||||
const color = COLORS[colorId];
|
|
||||||
return (
|
return (
|
||||||
<a href={`/departement/${code}`} class="cursor-pointer">
|
<a href={`/departement/${code}`} class="cursor-pointer">
|
||||||
<path
|
<path
|
||||||
d={dept.d}
|
d={dept.d}
|
||||||
data-code={code}
|
data-code={code}
|
||||||
data-name={dept.name}
|
data-name={dept.name}
|
||||||
fill={color.hex}
|
class:list={['france-map-dept', `france-map-fill-${colorId}`]}
|
||||||
stroke="#ffffff"
|
stroke="var(--paper-2)"
|
||||||
stroke-width="0.8"
|
stroke-width="0.5"
|
||||||
class="transition-opacity hover:opacity-80"
|
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,17 @@ interface Props {
|
||||||
colorId: ColorId;
|
colorId: ColorId;
|
||||||
phenomenonId?: PhenomenonId;
|
phenomenonId?: PhenomenonId;
|
||||||
showLevel?: boolean;
|
showLevel?: boolean;
|
||||||
|
size?: 'sm' | 'lg';
|
||||||
}
|
}
|
||||||
|
|
||||||
const { colorId, phenomenonId, showLevel = false } = Astro.props;
|
const { colorId, phenomenonId, showLevel = false, size = 'sm' } = Astro.props;
|
||||||
const phenomenon = phenomenonId ? PHENOMENA[phenomenonId] : null;
|
const phenomenon = phenomenonId ? PHENOMENA[phenomenonId] : null;
|
||||||
const colorClass = `vigilance-chip-${colorId}`;
|
// Glyphes redondants à la couleur (accessibilité daltonisme + niveaux de gris).
|
||||||
|
const GLYPHS: Record<ColorId, string> = { 1: '●', 2: '▲', 3: '◆', 4: '■' };
|
||||||
---
|
---
|
||||||
|
|
||||||
<span class:list={['vigilance-chip', colorClass]}>
|
<span class:list={['vigilance-chip', `vigilance-chip-${colorId}`, size === 'lg' && 'vigilance-chip-lg']}>
|
||||||
{phenomenon && <span aria-hidden="true">{phenomenon.emoji}</span>}
|
<span class="glyph" aria-hidden="true">{GLYPHS[colorId]}</span>
|
||||||
{phenomenon ? phenomenon.label : COLORS[colorId].name}
|
{phenomenon ? phenomenon.label : COLORS[colorId].name}
|
||||||
{showLevel && <span class="text-xs opacity-75">— {COLOR_LABEL[colorId]}</span>}
|
{showLevel && <span class="text-xs opacity-75">— {COLOR_LABEL[colorId]}</span>}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,28 @@
|
||||||
---
|
---
|
||||||
import { COLORS, COLOR_LABEL } from '../lib/phenomena';
|
import VigilanceChip from './VigilanceChip.astro';
|
||||||
|
import { COLOR_LABEL } from '../lib/phenomena';
|
||||||
import type { ColorId } from '../lib/phenomena';
|
import type { ColorId } from '../lib/phenomena';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
inline?: boolean;
|
||||||
|
}
|
||||||
|
const { inline = false } = Astro.props;
|
||||||
const levels: ColorId[] = [1, 2, 3, 4];
|
const levels: ColorId[] = [1, 2, 3, 4];
|
||||||
|
const NAMES: Record<ColorId, string> = { 1: 'Vert', 2: 'Jaune', 3: 'Orange', 4: 'Rouge' };
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-3 text-sm">
|
<div class:list={['flex flex-wrap items-center gap-x-4 gap-y-2 text-sm', inline ? 'flex-row' : 'flex-col items-start sm:flex-row sm:items-center']}>
|
||||||
<span class="font-semibold text-slate-700">Niveaux :</span>
|
<span class="kicker shrink-0">Légende</span>
|
||||||
{
|
{
|
||||||
levels.map((id) => (
|
levels.map((id) => (
|
||||||
<span class="inline-flex items-center gap-2">
|
<span class="inline-flex items-center gap-2">
|
||||||
<span
|
<VigilanceChip colorId={id} />
|
||||||
class="inline-block h-4 w-4 rounded border border-slate-300"
|
<span style="color: var(--ink-soft); font-size: 0.85rem;" class="hidden sm:inline">
|
||||||
style={`background-color: ${COLORS[id].hex};`}
|
{NAMES[id]} — {COLOR_LABEL[id]}
|
||||||
aria-hidden="true"
|
</span>
|
||||||
/>
|
<span style="color: var(--ink-soft); font-size: 0.85rem;" class="sm:hidden">
|
||||||
<span class="capitalize text-slate-700">{COLORS[id].name}</span>
|
{NAMES[id]}
|
||||||
<span class="hidden text-xs text-slate-500 sm:inline">— {COLOR_LABEL[id]}</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ const fullOgImage = ogImage.startsWith('http') ? ogImage : `${SITE}${ogImage}`;
|
||||||
const umamiId = process.env.UMAMI_WEBSITE_ID;
|
const umamiId = process.env.UMAMI_WEBSITE_ID;
|
||||||
const umamiSrc = process.env.UMAMI_SRC ?? 'https://analytics.nocleus.com/script.js';
|
const umamiSrc = process.env.UMAMI_SRC ?? 'https://analytics.nocleus.com/script.js';
|
||||||
|
|
||||||
|
const path = Astro.url.pathname;
|
||||||
|
const isActive = (p: string) => (p === '/' ? path === '/' : path.startsWith(p));
|
||||||
|
|
||||||
const jsonLd = {
|
const jsonLd = {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@graph': [
|
'@graph': [
|
||||||
|
|
@ -44,7 +47,7 @@ const jsonLd = {
|
||||||
'@type': 'Service',
|
'@type': 'Service',
|
||||||
'@id': `${SITE}/#service`,
|
'@id': `${SITE}/#service`,
|
||||||
name: 'Info Canicule',
|
name: 'Info Canicule',
|
||||||
serviceType: 'Service d\'information météorologique grand public',
|
serviceType: "Service d'information météorologique grand public",
|
||||||
areaServed: { '@type': 'Country', name: 'France' },
|
areaServed: { '@type': 'Country', name: 'France' },
|
||||||
audience: { '@type': 'PeopleAudience', audienceType: 'Grand public, personnes fragiles' },
|
audience: { '@type': 'PeopleAudience', audienceType: 'Grand public, personnes fragiles' },
|
||||||
provider: {
|
provider: {
|
||||||
|
|
@ -69,7 +72,7 @@ const jsonLd = {
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
{noindex ? <meta name="robots" content="noindex, nofollow" /> : <meta name="robots" content="index, follow, max-image-preview:large" />}
|
{noindex ? <meta name="robots" content="noindex, nofollow" /> : <meta name="robots" content="index, follow, max-image-preview:large" />}
|
||||||
<link rel="canonical" href={canonicalUrl} />
|
<link rel="canonical" href={canonicalUrl} />
|
||||||
|
|
@ -90,68 +93,234 @@ const jsonLd = {
|
||||||
<meta name="twitter:description" content={description} />
|
<meta name="twitter:description" content={description} />
|
||||||
<meta name="twitter:image" content={fullOgImage} />
|
<meta name="twitter:image" content={fullOgImage} />
|
||||||
|
|
||||||
<meta name="theme-color" content="#ea580c" />
|
<meta name="theme-color" content="#fdfaf2" media="(prefers-color-scheme: light)" />
|
||||||
|
<meta name="theme-color" content="#14110d" media="(prefers-color-scheme: dark)" />
|
||||||
<meta name="format-detection" content="telephone=yes" />
|
<meta name="format-detection" content="telephone=yes" />
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;500;600;700&family=Manrope:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
|
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
|
||||||
|
|
||||||
|
{/* Avoid flash of light theme on dark-preferring devices */}
|
||||||
|
<script is:inline>
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
var saved = localStorage.getItem('ic-theme');
|
||||||
|
var mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
var theme = saved || (mql.matches ? 'dark' : 'light');
|
||||||
|
if (theme === 'dark') document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
} catch (_) {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
{umamiId && (
|
{umamiId && (
|
||||||
<script defer src={umamiSrc} data-website-id={umamiId} data-do-not-track="true"></script>
|
<script defer src={umamiSrc} data-website-id={umamiId} data-do-not-track="true"></script>
|
||||||
)}
|
)}
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen flex flex-col">
|
<body class="flex flex-col" style="min-height: 100dvh;">
|
||||||
<header class="border-b border-slate-200 bg-white">
|
<a href="#main" class="skip-link">Aller au contenu principal</a>
|
||||||
<div class="container-tight flex flex-wrap items-center justify-between gap-3 py-4">
|
|
||||||
<a href="/" class="flex items-center gap-2 no-underline">
|
<header class="site-header">
|
||||||
<img src="/favicon.svg" alt="" width="32" height="32" class="h-8 w-8" />
|
<div class="container-tight flex items-center justify-between gap-3 py-3.5">
|
||||||
<span class="text-lg font-bold text-canicule-700">Info Canicule</span>
|
<a href="/" class="inline-flex items-center gap-2.5 font-display text-[1.1rem] font-extrabold no-underline" style="color: var(--ink);">
|
||||||
|
<svg width="28" height="28" viewBox="0 0 32 32" aria-hidden="true">
|
||||||
|
<circle cx="16" cy="16" r="14" fill="var(--sun)" />
|
||||||
|
<circle cx="16" cy="16" r="11" fill="var(--brand)" />
|
||||||
|
<path d="M16 7 C12 11 12 16 16 21 C20 16 20 11 16 7 Z" fill="#fff" />
|
||||||
|
</svg>
|
||||||
|
<span>Info Canicule</span>
|
||||||
</a>
|
</a>
|
||||||
<nav class="flex flex-wrap items-center gap-x-5 gap-y-1 text-sm font-medium text-slate-600">
|
|
||||||
<a href="/">Carte</a>
|
<nav id="nav-main" class="nav-links flex items-center gap-1" aria-label="Navigation principale">
|
||||||
<a href="/conseils">Conseils</a>
|
<a href="/" class:list={['nav-link', isActive('/') && 'is-active']}>Carte</a>
|
||||||
<a href="/a-propos">À propos</a>
|
<a href="/conseils" class:list={['nav-link', isActive('/conseils') && 'is-active']}>Conseils</a>
|
||||||
<a href="/soutenir" class="text-canicule-700">☕ Soutenir</a>
|
<a href="/a-propos" class:list={['nav-link', isActive('/a-propos') && 'is-active']}>À propos</a>
|
||||||
|
<a href="/soutenir" class:list={['nav-link', isActive('/soutenir') && 'is-active']} style="color: var(--brand-deep);">☕ Soutenir</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
id="theme-toggle"
|
||||||
|
type="button"
|
||||||
|
class="icon-btn"
|
||||||
|
aria-label="Basculer entre mode clair et mode sombre"
|
||||||
|
title="Basculer mode clair / sombre"
|
||||||
|
>
|
||||||
|
<svg id="theme-icon-sun" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="4" />
|
||||||
|
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" />
|
||||||
|
</svg>
|
||||||
|
<svg id="theme-icon-moon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="display:none;">
|
||||||
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="nav-toggle"
|
||||||
|
type="button"
|
||||||
|
class="icon-btn"
|
||||||
|
data-mobile-only
|
||||||
|
aria-label="Ouvrir le menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="nav-main"
|
||||||
|
>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<line x1="3" y1="6" x2="21" y2="6" />
|
||||||
|
<line x1="3" y1="12" x2="21" y2="12" />
|
||||||
|
<line x1="3" y1="18" x2="21" y2="18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="flex-1">
|
|
||||||
|
<main id="main" class="flex-1">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
<footer class="border-t border-slate-200 bg-white">
|
|
||||||
<div class="container-tight py-6 text-sm text-slate-500">
|
<footer class="site-footer">
|
||||||
<div class="grid gap-4 sm:grid-cols-3">
|
<div class="container-tight py-8">
|
||||||
|
<div class="grid gap-8" style="grid-template-columns: 1.4fr 1fr 1fr 1fr;">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2">
|
<a href="/" class="inline-flex items-center gap-2.5 font-display text-[1.05rem] font-extrabold no-underline" style="color: var(--ink);">
|
||||||
<img src="/favicon.svg" alt="" width="28" height="28" class="h-7 w-7" />
|
<svg width="24" height="24" viewBox="0 0 32 32" aria-hidden="true">
|
||||||
<p class="font-semibold text-slate-700">Info Canicule</p>
|
<circle cx="16" cy="16" r="14" fill="var(--sun)" />
|
||||||
</div>
|
<circle cx="16" cy="16" r="11" fill="var(--brand)" />
|
||||||
<p class="mt-1 text-xs">
|
<path d="M16 7 C12 11 12 16 16 21 C20 16 20 11 16 7 Z" fill="#fff" />
|
||||||
|
</svg>
|
||||||
|
Info Canicule
|
||||||
|
</a>
|
||||||
|
<p class="mt-2 text-sm" style="color: var(--ink-soft); max-width: 36ch;">
|
||||||
Service d'information publique gratuit, sans publicité, non lucratif.
|
Service d'information publique gratuit, sans publicité, non lucratif.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold text-slate-700">Liens</p>
|
<h4>Le site</h4>
|
||||||
<ul class="mt-1 space-y-0.5">
|
<ul class="flex flex-col gap-2 text-sm">
|
||||||
<li><a href="/a-propos">À propos</a></li>
|
<li><a href="/a-propos" style="color: var(--ink-2);">À propos</a></li>
|
||||||
<li><a href="/mentions-legales">Mentions légales</a></li>
|
<li><a href="/conseils" style="color: var(--ink-2);">Conseils par phénomène</a></li>
|
||||||
<li><a href="/dependances">Dépendances</a></li>
|
<li><a href="/soutenir" style="color: var(--ink-2);">☕ Soutenir le projet</a></li>
|
||||||
<li><a href="/soutenir">☕ Soutenir sur Ko-fi</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold text-slate-700">Données</p>
|
<h4>Mentions</h4>
|
||||||
<p class="mt-1 text-xs">
|
<ul class="flex flex-col gap-2 text-sm">
|
||||||
<a href="https://meteo.data.gouv.fr/" rel="noopener">Météo France</a>
|
<li><a href="/mentions-legales" style="color: var(--ink-2);">Mentions légales</a></li>
|
||||||
— <a href="https://www.etalab.gouv.fr/licence-ouverte-open-licence/" rel="noopener">Licence Ouverte 2.0</a>.
|
<li><a href="/dependances" style="color: var(--ink-2);">Dépendances</a></li>
|
||||||
En urgence : <a href="tel:112" class="text-canicule-700 font-semibold">112</a>.
|
<li><a href={`${SITE}/sitemap-index.xml`} style="color: var(--ink-2);">Plan du site</a></li>
|
||||||
</p>
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4>Données</h4>
|
||||||
|
<ul class="flex flex-col gap-2 text-sm">
|
||||||
|
<li>
|
||||||
|
<a href="https://meteo.data.gouv.fr/" rel="noopener" style="color: var(--ink-2);">Météo France (open data)</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://www.etalab.gouv.fr/licence-ouverte-open-licence/" rel="noopener" style="color: var(--ink-2);">Licence Ouverte 2.0</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
En urgence : <a href="tel:112" style="color: var(--brand-deep); font-weight: 600;">112</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-4 text-xs text-slate-400">
|
<div class="mt-7 flex flex-wrap items-center justify-between gap-3 border-t pt-5 text-xs" style="border-color: var(--line); color: var(--ink-soft);">
|
||||||
Édité à titre personnel, sans but lucratif —
|
<span>Édité à titre personnel, sans but lucratif.</span>
|
||||||
<a href="/mentions-legales" class="text-canicule-700">mentions légales</a>.
|
<span>
|
||||||
</p>
|
Données officielles · <a href="https://vigilance.meteofrance.fr/" rel="noopener" style="color: var(--brand-deep);">vigilance.meteofrance.fr</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Header nav links */
|
||||||
|
.nav-link {
|
||||||
|
color: var(--ink-2);
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.nav-link:hover { background: var(--paper-warm); color: var(--ink); border-bottom: none; }
|
||||||
|
.nav-link.is-active { color: var(--ink); background: var(--paper-warm); }
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.nav-links {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.nav-links.open {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
position: fixed;
|
||||||
|
inset: 60px 0 auto 0;
|
||||||
|
background: var(--paper-2);
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
box-shadow: var(--sh-3);
|
||||||
|
gap: 4px;
|
||||||
|
z-index: 49;
|
||||||
|
}
|
||||||
|
.nav-links.open .nav-link { width: 100%; padding: 14px 18px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-mobile-only] { display: none !important; }
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
[data-mobile-only] { display: inline-flex !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.site-footer > div > div:first-child { grid-template-columns: 1fr 1fr !important; }
|
||||||
|
}
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.site-footer .grid { grid-template-columns: 1fr 1fr !important; }
|
||||||
|
}
|
||||||
|
@media (max-width: 540px) {
|
||||||
|
.site-footer .grid { grid-template-columns: 1fr !important; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
(function () {
|
||||||
|
var root = document.documentElement;
|
||||||
|
var toggle = document.getElementById('theme-toggle');
|
||||||
|
var iconSun = document.getElementById('theme-icon-sun');
|
||||||
|
var iconMoon = document.getElementById('theme-icon-moon');
|
||||||
|
function syncIcons() {
|
||||||
|
var dark = root.getAttribute('data-theme') === 'dark';
|
||||||
|
if (iconSun) iconSun.style.display = dark ? 'none' : '';
|
||||||
|
if (iconMoon) iconMoon.style.display = dark ? '' : 'none';
|
||||||
|
}
|
||||||
|
syncIcons();
|
||||||
|
if (toggle) {
|
||||||
|
toggle.addEventListener('click', function () {
|
||||||
|
var dark = root.getAttribute('data-theme') === 'dark';
|
||||||
|
if (dark) { root.removeAttribute('data-theme'); }
|
||||||
|
else { root.setAttribute('data-theme', 'dark'); }
|
||||||
|
try { localStorage.setItem('ic-theme', dark ? 'light' : 'dark'); } catch (_) {}
|
||||||
|
syncIcons();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var navToggle = document.getElementById('nav-toggle');
|
||||||
|
var nav = document.getElementById('nav-main');
|
||||||
|
if (navToggle && nav) {
|
||||||
|
navToggle.addEventListener('click', function () {
|
||||||
|
var open = nav.classList.toggle('open');
|
||||||
|
navToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||||
|
});
|
||||||
|
nav.addEventListener('click', function (e) {
|
||||||
|
if (e.target && e.target.tagName === 'A') {
|
||||||
|
nav.classList.remove('open');
|
||||||
|
navToggle.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,15 @@
|
||||||
import Base from '../layouts/Base.astro';
|
import Base from '../layouts/Base.astro';
|
||||||
import FranceMap from '../components/FranceMap.astro';
|
import FranceMap from '../components/FranceMap.astro';
|
||||||
import DepartementGrid from '../components/DepartementGrid.astro';
|
import DepartementGrid from '../components/DepartementGrid.astro';
|
||||||
import VigilanceLegend from '../components/VigilanceLegend.astro';
|
|
||||||
import VigilanceChip from '../components/VigilanceChip.astro';
|
import VigilanceChip from '../components/VigilanceChip.astro';
|
||||||
import { getVigilanceSnapshot, maxColorByDepartement, activeAlerts, currentEcheance } from '../lib/vigilance';
|
import { getVigilanceSnapshot, maxColorByDepartement, currentEcheance } from '../lib/vigilance';
|
||||||
import { getDepartement } from '../lib/departements';
|
import { getDepartement, DEPARTEMENTS } from '../lib/departements';
|
||||||
|
import { PHENOMENA } from '../lib/phenomena';
|
||||||
import type { VigilanceAlert } from '../lib/vigilance';
|
import type { VigilanceAlert } from '../lib/vigilance';
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
// Force la revalidation côté navigateur — la page change toutes les ~5 min
|
// Force la revalidation côté navigateur — la page change toutes les ~5 min.
|
||||||
// (cache Valkey TTL + bulletins MF), donc on évite le cache local agressif
|
|
||||||
// qui faisait servir une carte de la veille.
|
|
||||||
Astro.response.headers.set('Cache-Control', 'public, max-age=60, must-revalidate');
|
Astro.response.headers.set('Cache-Control', 'public, max-age=60, must-revalidate');
|
||||||
|
|
||||||
let snapshot;
|
let snapshot;
|
||||||
|
|
@ -23,14 +21,9 @@ try {
|
||||||
error = (e as Error).message;
|
error = (e as Error).message;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine l'écheance "aujourd'hui réel" — peut être J ou J1 selon l'heure
|
|
||||||
// (entre minuit et la publication du bulletin suivant ~6h, J du bulletin = hier).
|
|
||||||
const todayEch: 'J' | 'J1' = snapshot ? currentEcheance(snapshot) : 'J';
|
const todayEch: 'J' | 'J1' = snapshot ? currentEcheance(snapshot) : 'J';
|
||||||
// "Demain" n'existe que quand `todayEch === 'J'` : le bulletin n'a pas de J2.
|
|
||||||
// Entre minuit et la pub ~6h, on est sur J1 = aujourd'hui réel, donc pas de "demain" dispo.
|
|
||||||
const tomorrowAvailable = todayEch === 'J';
|
const tomorrowAvailable = todayEch === 'J';
|
||||||
|
|
||||||
// Onglet sélectionné via ?echeance=tomorrow ; défaut = today.
|
|
||||||
const requestedView = new URL(Astro.request.url).searchParams.get('echeance');
|
const requestedView = new URL(Astro.request.url).searchParams.get('echeance');
|
||||||
const view: 'today' | 'tomorrow' =
|
const view: 'today' | 'tomorrow' =
|
||||||
requestedView === 'tomorrow' && tomorrowAvailable ? 'tomorrow' : 'today';
|
requestedView === 'tomorrow' && tomorrowAvailable ? 'tomorrow' : 'today';
|
||||||
|
|
@ -52,7 +45,6 @@ if (snapshot) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tri des départements actifs par code (2A/2B inséré après 19).
|
|
||||||
function deptSortKey(code: string): string {
|
function deptSortKey(code: string): string {
|
||||||
if (code === '2A') return '19.1';
|
if (code === '2A') return '19.1';
|
||||||
if (code === '2B') return '19.2';
|
if (code === '2B') return '19.2';
|
||||||
|
|
@ -62,9 +54,33 @@ const activeDeptCodes = [...alertsByDept.keys()].sort((a, b) =>
|
||||||
deptSortKey(a).localeCompare(deptSortKey(b), 'en', { numeric: true }),
|
deptSortKey(a).localeCompare(deptSortKey(b), 'en', { numeric: true }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const totalAlertes = activeDeptCodes.length;
|
||||||
const canicule = alertsToday.filter((a) => a.phenomenonId === 6).length;
|
const canicule = alertsToday.filter((a) => a.phenomenonId === 6).length;
|
||||||
const orages = alertsToday.filter((a) => a.phenomenonId === 3).length;
|
const orages = alertsToday.filter((a) => a.phenomenonId === 3).length;
|
||||||
const orange = alertsToday.filter((a) => a.colorId >= 3).length;
|
const orange = alertsToday.filter((a) => a.colorId >= 3).length;
|
||||||
|
|
||||||
|
// Phénomène dominant (le plus représenté en alerte, niveau max gardé pour la pill)
|
||||||
|
let dominantId: number | null = null;
|
||||||
|
let dominantColor = 1;
|
||||||
|
if (alertsToday.length > 0) {
|
||||||
|
const tally = new Map<number, { count: number; maxColor: number }>();
|
||||||
|
for (const a of alertsToday) {
|
||||||
|
const t = tally.get(a.phenomenonId) ?? { count: 0, maxColor: 1 };
|
||||||
|
t.count += 1;
|
||||||
|
if (a.colorId > t.maxColor) t.maxColor = a.colorId;
|
||||||
|
tally.set(a.phenomenonId, t);
|
||||||
|
}
|
||||||
|
let bestCount = 0;
|
||||||
|
for (const [id, t] of tally) {
|
||||||
|
if (t.count > bestCount) {
|
||||||
|
bestCount = t.count;
|
||||||
|
dominantId = id;
|
||||||
|
dominantColor = t.maxColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dominant = dominantId ? PHENOMENA[dominantId as keyof typeof PHENOMENA] : null;
|
||||||
|
|
||||||
const productDate = snapshot?.productDatetime
|
const productDate = snapshot?.productDatetime
|
||||||
? new Date(snapshot.productDatetime).toLocaleString('fr-FR', {
|
? new Date(snapshot.productDatetime).toLocaleString('fr-FR', {
|
||||||
dateStyle: 'long',
|
dateStyle: 'long',
|
||||||
|
|
@ -73,200 +89,409 @@ const productDate = snapshot?.productDatetime
|
||||||
})
|
})
|
||||||
: 'inconnu';
|
: 'inconnu';
|
||||||
|
|
||||||
// Date de validité de l'écheance affichée (pour clarifier au visiteur).
|
|
||||||
// On la dérive du "maintenant" Paris (+1 jour si l'onglet "demain" est actif),
|
|
||||||
// pas d'un sample d'alerte : en jour calme tout est vert, donc pas d'alerte sample.
|
|
||||||
const fmtParisDate = (d: Date) =>
|
const fmtParisDate = (d: Date) =>
|
||||||
d.toLocaleDateString('fr-FR', {
|
d.toLocaleDateString('fr-FR', {
|
||||||
weekday: 'long', day: 'numeric', month: 'long', timeZone: 'Europe/Paris',
|
weekday: 'long', day: 'numeric', month: 'long', timeZone: 'Europe/Paris',
|
||||||
});
|
});
|
||||||
|
const fmtParisDateShort = (d: Date) =>
|
||||||
|
d.toLocaleDateString('fr-FR', {
|
||||||
|
weekday: 'short', day: 'numeric', month: 'short', timeZone: 'Europe/Paris',
|
||||||
|
});
|
||||||
const nowParis = new Date();
|
const nowParis = new Date();
|
||||||
const tomorrowParis = new Date(nowParis.getTime() + 86_400_000);
|
const tomorrowParis = new Date(nowParis.getTime() + 86_400_000);
|
||||||
const todayLabel = fmtParisDate(nowParis);
|
const todayLabel = fmtParisDate(nowParis);
|
||||||
const tomorrowLabel = fmtParisDate(tomorrowParis);
|
const tomorrowLabel = fmtParisDate(tomorrowParis);
|
||||||
const dayLabel = view === 'tomorrow' ? tomorrowLabel : todayLabel;
|
const todayShort = fmtParisDateShort(nowParis);
|
||||||
|
const tomorrowShort = fmtParisDateShort(tomorrowParis);
|
||||||
|
|
||||||
|
// Données pour la recherche département (sérialisées vers le JS client).
|
||||||
|
const allDepts = DEPARTEMENTS.map((d) => ({ code: d.code, name: d.name }));
|
||||||
---
|
---
|
||||||
|
|
||||||
<Base>
|
<Base>
|
||||||
<section class="bg-gradient-to-b from-canicule-50 to-white">
|
<section class="container-tight" style="padding-block: clamp(28px, 5vw, 56px) 28px;">
|
||||||
<div class="container-tight py-10">
|
{/* Hero */}
|
||||||
<h1 class="text-3xl font-bold text-slate-900 sm:text-4xl">
|
<div class="flex flex-col gap-3" style="margin-bottom: 28px;">
|
||||||
Vigilance Météo France en temps réel
|
<div class="kicker">Vigilance Météo France · <span class="capitalize">{todayLabel}</span></div>
|
||||||
|
<h1>
|
||||||
|
Toutes les alertes météo,
|
||||||
|
<span style="color: var(--brand-deep);">en clair</span>.
|
||||||
</h1>
|
</h1>
|
||||||
<p class="mt-2 max-w-2xl text-slate-600">
|
<p style="font-size: clamp(1rem, 0.9rem + 0.4vw, 1.18rem); color: var(--ink-2); max-width: 720px; line-height: 1.55;">
|
||||||
Carte des alertes Vigilance par département, conseils officiels et numéros d'urgence.
|
Canicule, orages, vent, pluie, neige, avalanches : retrouvez la vigilance de chaque département, ce qu'il faut faire, et prévenez un proche en quelques secondes.
|
||||||
{dayLabel && (
|
|
||||||
<>
|
|
||||||
Affichage pour <strong class="capitalize">{dayLabel}</strong>.
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-1 text-xs text-slate-500">
|
<p class="kicker" style="text-transform: none; letter-spacing: 0; font-size: 0.78rem;">
|
||||||
Bulletin Météo France émis le {productDate} (heure de Paris).
|
Bulletin Météo France émis le {productDate} (heure de Paris).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
{
|
{/* Day tabs */}
|
||||||
!error && snapshot && (
|
{!error && snapshot && (
|
||||||
<section class="container-tight pt-6">
|
<div role="tablist" aria-label="Échéance de la vigilance" class="flex flex-wrap gap-2" style="margin-bottom: 20px;">
|
||||||
<div role="tablist" aria-label="Échéance de la vigilance" class="inline-flex rounded-lg border border-slate-200 bg-white p-1 text-sm shadow-sm">
|
<a
|
||||||
|
href="/"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={view === 'today' ? 'true' : 'false'}
|
||||||
|
class:list={['ic-btn ic-btn-sm', view === 'today' ? 'ic-btn-primary' : 'ic-btn-ghost']}
|
||||||
|
style="text-decoration: none;"
|
||||||
|
>
|
||||||
|
Aujourd'hui · <span class="capitalize">{todayShort}</span>
|
||||||
|
</a>
|
||||||
|
{tomorrowAvailable ? (
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/?echeance=tomorrow"
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={view === 'today' ? 'true' : 'false'}
|
aria-selected={view === 'tomorrow' ? 'true' : 'false'}
|
||||||
class:list={[
|
class:list={['ic-btn ic-btn-sm', view === 'tomorrow' ? 'ic-btn-primary' : 'ic-btn-ghost']}
|
||||||
'rounded-md px-4 py-1.5 font-medium no-underline transition-colors',
|
style="text-decoration: none;"
|
||||||
view === 'today'
|
|
||||||
? 'bg-canicule-700 text-white'
|
|
||||||
: 'text-slate-700 hover:bg-slate-100',
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
Aujourd'hui
|
Demain · <span class="capitalize">{tomorrowShort}</span>
|
||||||
<span class="ml-1 hidden text-xs opacity-80 sm:inline capitalize">· {todayLabel}</span>
|
|
||||||
</a>
|
</a>
|
||||||
{tomorrowAvailable ? (
|
) : (
|
||||||
<a
|
<span
|
||||||
href="/?echeance=tomorrow"
|
role="tab"
|
||||||
role="tab"
|
aria-selected="false"
|
||||||
aria-selected={view === 'tomorrow' ? 'true' : 'false'}
|
aria-disabled="true"
|
||||||
class:list={[
|
title="L'échéance « demain » n'est pas encore disponible : Météo France publie le prochain bulletin vers 6h."
|
||||||
'rounded-md px-4 py-1.5 font-medium no-underline transition-colors',
|
class="ic-btn ic-btn-sm ic-btn-ghost"
|
||||||
view === 'tomorrow'
|
style="opacity: 0.55; cursor: not-allowed;"
|
||||||
? 'bg-canicule-700 text-white'
|
>
|
||||||
: 'text-slate-700 hover:bg-slate-100',
|
Demain · en attente du prochain bulletin
|
||||||
]}
|
</span>
|
||||||
>
|
)}
|
||||||
Demain
|
</div>
|
||||||
<span class="ml-1 hidden text-xs opacity-80 sm:inline capitalize">· {tomorrowLabel}</span>
|
)}
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
role="tab"
|
|
||||||
aria-selected="false"
|
|
||||||
aria-disabled="true"
|
|
||||||
title="L'échéance « demain » n'est plus disponible : Météo France publie le prochain bulletin vers 6h, qui couvrira à nouveau aujourd'hui + demain."
|
|
||||||
class="cursor-not-allowed rounded-md px-4 py-1.5 font-medium text-slate-400"
|
|
||||||
>
|
|
||||||
Demain
|
|
||||||
<span class="ml-1 hidden text-xs sm:inline">· en attente du prochain bulletin</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
<section class="container-tight py-8">
|
{error && (
|
||||||
{
|
<div class="v-block v-rouge" style="margin-bottom: 24px;">
|
||||||
error && (
|
<strong>Données Météo France momentanément indisponibles.</strong>
|
||||||
<div class="rounded border border-red-200 bg-red-50 p-4 text-red-800">
|
<p style="margin-top: 6px; font-size: 0.92rem;">
|
||||||
<strong>Données indisponibles.</strong> Réessayer dans quelques minutes. Détail technique :
|
Réessayez dans quelques minutes. En urgence, consultez
|
||||||
{error}
|
<a href="https://vigilance.meteofrance.fr/" rel="noopener" style="color: inherit; text-decoration: underline;">vigilance.meteofrance.fr</a>.
|
||||||
</div>
|
</p>
|
||||||
)
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
{
|
{/* Stat tiles */}
|
||||||
!error && (
|
{!error && (
|
||||||
<div class="grid gap-4 sm:grid-cols-3">
|
<div class="grid gap-3" style="grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); margin-bottom: 24px;">
|
||||||
<div class="rounded-lg border border-slate-200 bg-white p-4">
|
<div class="ic-card" style="padding: 18px;">
|
||||||
<div class="text-2xl font-bold text-canicule-700">{canicule}</div>
|
<div style="font-family: var(--font-display); font-weight: 800; font-size: clamp(1.6rem, 1.3rem + 1.2vw, 2.4rem); line-height: 1; color: var(--v-jaune); letter-spacing: -0.02em;">
|
||||||
<div class="text-sm text-slate-600">départements en vigilance canicule</div>
|
{totalAlertes}
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg border border-slate-200 bg-white p-4">
|
<div style="color: var(--ink-soft); font-size: 0.88rem; font-weight: 500; margin-top: 6px;">
|
||||||
<div class="text-2xl font-bold text-orange-700">{orange}</div>
|
départements en alerte
|
||||||
<div class="text-sm text-slate-600">en niveau orange ou rouge (tous phénomènes)</div>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-lg border border-slate-200 bg-white p-4">
|
|
||||||
<div class="text-2xl font-bold text-yellow-700">{orages}</div>
|
|
||||||
<div class="text-sm text-slate-600">en vigilance orages</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
<div class="ic-card" style="padding: 18px;">
|
||||||
}
|
<div style="font-family: var(--font-display); font-weight: 800; font-size: clamp(1.6rem, 1.3rem + 1.2vw, 2.4rem); line-height: 1; color: var(--v-orange); letter-spacing: -0.02em;">
|
||||||
|
{orange}
|
||||||
|
</div>
|
||||||
|
<div style="color: var(--ink-soft); font-size: 0.88rem; font-weight: 500; margin-top: 6px;">
|
||||||
|
en orange ou rouge
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ic-card" style="padding: 18px;">
|
||||||
|
<div style="font-family: var(--font-display); font-weight: 800; font-size: clamp(1.6rem, 1.3rem + 1.2vw, 2.4rem); line-height: 1; color: var(--v-orange); letter-spacing: -0.02em;">
|
||||||
|
{canicule}
|
||||||
|
</div>
|
||||||
|
<div style="color: var(--ink-soft); font-size: 0.88rem; font-weight: 500; margin-top: 6px;">
|
||||||
|
touchés par canicule
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ic-card" style="padding: 18px;">
|
||||||
|
<div style="font-family: var(--font-display); font-weight: 800; font-size: clamp(1.6rem, 1.3rem + 1.2vw, 2.4rem); line-height: 1; color: var(--v-jaune); letter-spacing: -0.02em;">
|
||||||
|
{orages}
|
||||||
|
</div>
|
||||||
|
<div style="color: var(--ink-soft); font-size: 0.88rem; font-weight: 500; margin-top: 6px;">
|
||||||
|
vigilance orages
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Map + sidebar */}
|
||||||
|
{!error && (
|
||||||
|
<div class="grid items-start gap-6 home-grid" style="grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);">
|
||||||
|
<div class="ic-card" style="padding: clamp(8px, 2vw, 18px);">
|
||||||
|
<FranceMap colorsByDept={colorsByDept} alertsByDept={alertsByDept} />
|
||||||
|
<p style="margin-top: 10px; color: var(--ink-soft); font-size: 0.82rem; text-align: center;">
|
||||||
|
Survolez ou tapotez un département pour le détail, cliquez pour la page complète.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="ic-card">
|
||||||
|
<div class="kicker" style="margin-bottom: 12px;">Légende vigilance</div>
|
||||||
|
<div class="flex flex-col gap-2.5">
|
||||||
|
<div class="flex items-center gap-2.5"><VigilanceChip colorId={1} /> <span style="color: var(--ink-soft); font-size: 0.88rem;">Pas de vigilance particulière</span></div>
|
||||||
|
<div class="flex items-center gap-2.5"><VigilanceChip colorId={2} /> <span style="color: var(--ink-soft); font-size: 0.88rem;">Soyez attentif</span></div>
|
||||||
|
<div class="flex items-center gap-2.5"><VigilanceChip colorId={3} /> <span style="color: var(--ink-soft); font-size: 0.88rem;">Soyez très vigilant</span></div>
|
||||||
|
<div class="flex items-center gap-2.5"><VigilanceChip colorId={4} /> <span style="color: var(--ink-soft); font-size: 0.88rem;">Vigilance absolue</span></div>
|
||||||
|
</div>
|
||||||
|
<p style="color: var(--ink-soft); font-size: 0.78rem; margin-top: 12px; line-height: 1.45;">
|
||||||
|
Chaque niveau a une <strong>couleur</strong> et un <strong>symbole</strong> (●▲◆■) — afin de rester lisible en cas de daltonisme ou d'impression en niveaux de gris.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ic-card">
|
||||||
|
<div class="kicker">Êtes-vous concerné ?</div>
|
||||||
|
<h3 style="margin-top: 8px; margin-bottom: 12px;">Voir mon département</h3>
|
||||||
|
<div class="ic-input" style="position: relative;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="color: var(--ink-mute); flex-shrink: 0;">
|
||||||
|
<circle cx="11" cy="11" r="7" />
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
id="dept-search-input"
|
||||||
|
type="search"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="Tapez 04 ou Alpes-de-Haute-Provence…"
|
||||||
|
aria-label="Rechercher un département"
|
||||||
|
aria-controls="dept-search-results"
|
||||||
|
aria-autocomplete="list"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
id="dept-search-results"
|
||||||
|
role="listbox"
|
||||||
|
aria-label="Résultats"
|
||||||
|
style="display:none; position: absolute; left: 0; right: 0; margin-top: 6px; background: var(--paper-2); border: 1px solid var(--line-strong); border-radius: var(--r-md); box-shadow: var(--sh-3); list-style: none; padding: 6px; z-index: 20; max-height: 260px; overflow-y: auto;"
|
||||||
|
></ul>
|
||||||
|
<p style="color: var(--ink-soft); font-size: 0.82rem; margin-top: 10px;">
|
||||||
|
Cliquez aussi sur la carte ou choisissez dans la liste ci-dessous.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-3" style="grid-template-columns: 1fr 1fr;">
|
||||||
|
<div class="ic-card" style="padding: 16px;">
|
||||||
|
<div class="kicker">Phénomène dominant</div>
|
||||||
|
{dominant ? (
|
||||||
|
<>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<strong style="font-size: 1.05rem;">{dominant.label}</strong>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 8px;">
|
||||||
|
<VigilanceChip colorId={dominantColor as 1|2|3|4} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<strong style="font-size: 1.05rem;">Calme</strong>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 8px;">
|
||||||
|
<VigilanceChip colorId={1} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="ic-card" style="padding: 16px;">
|
||||||
|
<div class="kicker">Échéance</div>
|
||||||
|
<div style="font-family: var(--font-display); font-weight: 800; font-size: 1.4rem; margin-top: 10px; line-height: 1.05; letter-spacing: -0.02em; text-transform: capitalize;">
|
||||||
|
{view === 'tomorrow' ? tomorrowShort : todayShort}
|
||||||
|
</div>
|
||||||
|
<div style="color: var(--ink-soft); font-size: 0.85rem; margin-top: 6px;">
|
||||||
|
{view === 'tomorrow' ? 'Prévisions de demain' : "Données d'aujourd'hui"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Vue par région (liste repliable) */}
|
||||||
|
{!error && (
|
||||||
|
<details class="ic-card" style="margin-top: 24px;">
|
||||||
|
<summary style="cursor: pointer; font-weight: 600; color: var(--ink-2);">
|
||||||
|
Vue par région (liste complète)
|
||||||
|
</summary>
|
||||||
|
<div style="margin-top: 16px;">
|
||||||
|
<DepartementGrid colorsByDept={colorsByDept} />
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{
|
{/* Liste des départements en alerte */}
|
||||||
!error && (
|
{!error && activeDeptCodes.length > 0 && (
|
||||||
<>
|
<section class="container-tight" style="padding-block: 32px;" id="liste-depts">
|
||||||
<section class="container-tight pb-2">
|
<div class="flex flex-wrap items-center justify-between gap-3" style="margin-bottom: 16px;">
|
||||||
<div class="mb-2 flex flex-wrap items-center justify-between gap-3">
|
<h2>Départements en alerte ({activeDeptCodes.length})</h2>
|
||||||
<h2 class="text-xl font-semibold text-slate-900 capitalize">
|
<div class="ic-input" style="max-width: 320px;">
|
||||||
Niveau par département{dayLabel ? ` — ${dayLabel}` : ''}
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="color: var(--ink-mute); flex-shrink: 0;">
|
||||||
</h2>
|
<circle cx="11" cy="11" r="7" />
|
||||||
<VigilanceLegend />
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||||
</div>
|
</svg>
|
||||||
<p class="text-sm text-slate-500">
|
<input
|
||||||
Survolez un département pour voir le détail des alertes, cliquez pour la page complète.
|
id="dept-list-filter"
|
||||||
</p>
|
type="search"
|
||||||
</section>
|
autocomplete="off"
|
||||||
<div class="container-tight pb-4">
|
placeholder="Filtrer par numéro ou nom…"
|
||||||
<FranceMap colorsByDept={colorsByDept} alertsByDept={alertsByDept} />
|
aria-label="Filtrer les départements en alerte"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<section class="container-tight pb-8">
|
</div>
|
||||||
<details class="rounded border border-slate-200 bg-white p-4">
|
<p style="color: var(--ink-soft); font-size: 0.92rem; margin-bottom: 12px;">
|
||||||
<summary class="cursor-pointer font-medium text-slate-700">
|
Triés par numéro. Un département peut cumuler plusieurs phénomènes (ex : canicule + orages).
|
||||||
Vue par région (liste)
|
</p>
|
||||||
</summary>
|
<div class="ic-card" style="padding: 6px;">
|
||||||
<div class="mt-4">
|
<ul id="dept-list" class="grid gap-x-4 list-dept" style="grid-template-columns: 1fr 1fr; list-style: none; padding: 0; margin: 0;">
|
||||||
<DepartementGrid colorsByDept={colorsByDept} />
|
{activeDeptCodes.map((code) => {
|
||||||
</div>
|
const dept = getDepartement(code);
|
||||||
</details>
|
if (!dept) return null;
|
||||||
<p class="mt-4 text-center text-xs text-slate-500">
|
const alerts = (alertsByDept.get(code) ?? []).slice().sort((a, b) => b.colorId - a.colorId);
|
||||||
Source officielle :
|
return (
|
||||||
<a href="https://vigilance.meteofrance.fr/" rel="noopener" class="text-canicule-700 font-medium">
|
<li data-search={`${code} ${dept.name}`.toLowerCase()}>
|
||||||
vigilance.meteofrance.fr
|
<a
|
||||||
</a>
|
href={`/departement/${code}`}
|
||||||
— toujours s'y référer en cas d'urgence.
|
class="dept-row flex flex-wrap items-center justify-between gap-3"
|
||||||
</p>
|
style="padding: 12px 14px; border-bottom: 1px dashed var(--line); border-radius: 6px; color: inherit; text-decoration: none; transition: background-color .12s;"
|
||||||
<p class="mt-2 text-center text-xs text-slate-500">
|
>
|
||||||
<strong>Outre-mer non couvert</strong> par cette source open data :
|
<span class="inline-flex items-baseline gap-2.5" style="min-width: 0;">
|
||||||
<a href="/departement/971" class="text-canicule-700">Guadeloupe</a> ·
|
<span style="font-family: var(--font-mono); font-size: 0.85rem; color: var(--ink-mute); min-width: 28px;">{code}</span>
|
||||||
<a href="/departement/972" class="text-canicule-700">Martinique</a> ·
|
<span style="font-weight: 600;">{dept.name}</span>
|
||||||
<a href="/departement/973" class="text-canicule-700">Guyane</a> ·
|
</span>
|
||||||
<a href="/departement/974" class="text-canicule-700">La Réunion</a> ·
|
<span class="inline-flex flex-wrap items-center gap-1.5" style="flex-shrink: 0;">
|
||||||
<a href="/departement/976" class="text-canicule-700">Mayotte</a> →
|
{alerts.map((a) => (
|
||||||
<a href="https://vigilance.meteofrance.fr/" rel="noopener" class="text-canicule-700">vigilance.meteofrance.fr</a>
|
<VigilanceChip colorId={a.colorId} phenomenonId={a.phenomenonId} />
|
||||||
</p>
|
))}
|
||||||
</section>
|
</span>
|
||||||
</>
|
</a>
|
||||||
)
|
</li>
|
||||||
}
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<p id="dept-list-empty" style="display:none; padding: 16px; text-align: center; color: var(--ink-soft);">
|
||||||
|
Aucun département ne correspond.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{
|
{/* CTA banner */}
|
||||||
!error && activeDeptCodes.length > 0 && (
|
<section class="container-tight" style="padding-block: 16px 32px;">
|
||||||
<section class="border-t border-slate-200 bg-white">
|
<div class="ic-card-soft flex flex-wrap items-center justify-between gap-4" style="padding: 28px;">
|
||||||
<div class="container-tight py-8">
|
<div>
|
||||||
<h2 class="mb-4 text-xl font-semibold text-slate-900">
|
<h3 style="margin-bottom: 6px;">Vous connaissez quelqu'un de fragile ?</h3>
|
||||||
Départements en alerte ({activeDeptCodes.length})
|
<p style="color: var(--ink-soft); max-width: 540px;">
|
||||||
</h2>
|
Retrouvez les bons gestes par phénomène (canicule, orages, vent…) à imprimer ou à transmettre à un proche.
|
||||||
<p class="mb-4 text-sm text-slate-500">
|
</p>
|
||||||
Triés par numéro de département. Un département peut cumuler plusieurs phénomènes (ex: canicule + orages).
|
</div>
|
||||||
</p>
|
<a href="/conseils" class="ic-btn ic-btn-brand ic-btn-lg" style="text-decoration: none;">
|
||||||
<ul class="space-y-2">
|
Voir les conseils →
|
||||||
{activeDeptCodes.map((code) => {
|
</a>
|
||||||
const dept = getDepartement(code);
|
</div>
|
||||||
if (!dept) return null;
|
</section>
|
||||||
const alerts = (alertsByDept.get(code) ?? []).slice().sort((a, b) => b.colorId - a.colorId);
|
|
||||||
return (
|
{/* Outre-mer note */}
|
||||||
<li class="rounded border border-slate-200 px-3 py-2">
|
<section class="container-tight" style="padding-block: 0 24px;">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<p style="color: var(--ink-soft); font-size: 0.82rem; text-align: center;">
|
||||||
<a href={`/departement/${code}`} class="font-medium no-underline">
|
Source officielle :
|
||||||
<span class="font-mono text-slate-500">{code}</span> · {dept.name}
|
<a href="https://vigilance.meteofrance.fr/" rel="noopener" style="color: var(--brand-deep); font-weight: 500;">vigilance.meteofrance.fr</a>
|
||||||
</a>
|
— toujours s'y référer en cas d'urgence.
|
||||||
<div class="flex flex-wrap gap-1.5">
|
</p>
|
||||||
{alerts.map((a) => (
|
<p style="color: var(--ink-soft); font-size: 0.82rem; text-align: center; margin-top: 8px;">
|
||||||
<VigilanceChip colorId={a.colorId} phenomenonId={a.phenomenonId} />
|
<strong>Outre-mer non couvert</strong> par cette source open data :
|
||||||
))}
|
<a href="/departement/971" style="color: var(--brand-deep);">Guadeloupe</a> ·
|
||||||
</div>
|
<a href="/departement/972" style="color: var(--brand-deep);">Martinique</a> ·
|
||||||
</div>
|
<a href="/departement/973" style="color: var(--brand-deep);">Guyane</a> ·
|
||||||
</li>
|
<a href="/departement/974" style="color: var(--brand-deep);">La Réunion</a> ·
|
||||||
);
|
<a href="/departement/976" style="color: var(--brand-deep);">Mayotte</a>
|
||||||
})}
|
</p>
|
||||||
</ul>
|
</section>
|
||||||
</div>
|
|
||||||
</section>
|
<script type="application/json" id="ic-all-depts" set:html={JSON.stringify(allDepts)} />
|
||||||
)
|
|
||||||
}
|
<style>
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.home-grid { grid-template-columns: 1fr !important; }
|
||||||
|
}
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.list-dept { grid-template-columns: 1fr !important; }
|
||||||
|
}
|
||||||
|
.dept-row:hover, .dept-row:focus-visible {
|
||||||
|
background-color: var(--paper-warm);
|
||||||
|
border-bottom-color: var(--line-strong);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
(function () {
|
||||||
|
// Filter for "liste des départements en alerte"
|
||||||
|
var filter = document.getElementById('dept-list-filter');
|
||||||
|
var list = document.getElementById('dept-list');
|
||||||
|
var empty = document.getElementById('dept-list-empty');
|
||||||
|
if (filter && list) {
|
||||||
|
filter.addEventListener('input', function () {
|
||||||
|
var q = filter.value.trim().toLowerCase();
|
||||||
|
var shown = 0;
|
||||||
|
list.querySelectorAll('li').forEach(function (li) {
|
||||||
|
var hit = !q || li.getAttribute('data-search').indexOf(q) !== -1;
|
||||||
|
li.style.display = hit ? '' : 'none';
|
||||||
|
if (hit) shown++;
|
||||||
|
});
|
||||||
|
if (empty) empty.style.display = shown === 0 ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search-and-go for the sidebar dept search
|
||||||
|
var dataEl = document.getElementById('ic-all-depts');
|
||||||
|
var input = document.getElementById('dept-search-input');
|
||||||
|
var results = document.getElementById('dept-search-results');
|
||||||
|
if (!dataEl || !input || !results) return;
|
||||||
|
var ALL = [];
|
||||||
|
try { ALL = JSON.parse(dataEl.textContent || '[]'); } catch (_) {}
|
||||||
|
var active = 0;
|
||||||
|
var matches = [];
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (!matches.length) { results.style.display = 'none'; results.innerHTML = ''; return; }
|
||||||
|
results.style.display = 'block';
|
||||||
|
results.innerHTML = matches.map(function (d, i) {
|
||||||
|
return '<li role="option" aria-selected="' + (i === active) + '">' +
|
||||||
|
'<a href="/departement/' + d.code + '" data-i="' + i + '" style="' +
|
||||||
|
'display:flex; gap:10px; align-items:center; width:100%; padding:8px 10px;' +
|
||||||
|
'border-radius:6px; text-decoration:none; color:inherit;' +
|
||||||
|
(i === active ? 'background:var(--paper-warm);' : '') + '">' +
|
||||||
|
'<span style="font-family:var(--font-mono);font-size:0.85rem;color:var(--ink-mute);min-width:28px;">' + d.code + '</span>' +
|
||||||
|
'<span style="font-weight:500;">' + d.name + '</span>' +
|
||||||
|
'</a>' +
|
||||||
|
'</li>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener('input', function () {
|
||||||
|
var q = input.value.trim().toLowerCase();
|
||||||
|
matches = !q ? [] : ALL.filter(function (d) {
|
||||||
|
return (d.code + ' ' + d.name).toLowerCase().indexOf(q) !== -1;
|
||||||
|
}).slice(0, 8);
|
||||||
|
active = 0;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('keydown', function (e) {
|
||||||
|
if (!matches.length) return;
|
||||||
|
if (e.key === 'ArrowDown') { e.preventDefault(); active = Math.min(matches.length - 1, active + 1); render(); }
|
||||||
|
if (e.key === 'ArrowUp') { e.preventDefault(); active = Math.max(0, active - 1); render(); }
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
window.location.href = '/departement/' + matches[active].code;
|
||||||
|
}
|
||||||
|
if (e.key === 'Escape') { matches = []; render(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
results.addEventListener('mouseover', function (e) {
|
||||||
|
var a = e.target.closest('a[data-i]');
|
||||||
|
if (a) { active = +a.getAttribute('data-i'); render(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
if (!input.contains(e.target) && !results.contains(e.target)) {
|
||||||
|
matches = [];
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</Base>
|
</Base>
|
||||||
|
|
|
||||||
|
|
@ -2,35 +2,386 @@
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* ─── Design tokens ───────────────────────────────────────── */
|
||||||
|
:root {
|
||||||
|
--paper: #fdfaf2;
|
||||||
|
--paper-2: #ffffff;
|
||||||
|
--paper-elev: #ffffff;
|
||||||
|
--paper-warm: #f8f1e1;
|
||||||
|
--ink: #1b1814;
|
||||||
|
--ink-2: #3a342c;
|
||||||
|
--ink-soft: #6b6457;
|
||||||
|
--ink-mute: #968d7d;
|
||||||
|
--line: #ebe2cb;
|
||||||
|
--line-strong: #d5c9aa;
|
||||||
|
|
||||||
|
--brand: #d97757;
|
||||||
|
--brand-deep: #b45a3a;
|
||||||
|
--brand-soft: #fde2d3;
|
||||||
|
--brand-tint: #fff1e8;
|
||||||
|
--brand-ink: #6b2c11;
|
||||||
|
|
||||||
|
--sun: #f4c97d;
|
||||||
|
--sun-deep: #c98e2a;
|
||||||
|
|
||||||
|
--v-vert: #2f7d3a;
|
||||||
|
--v-vert-bg: #e8f3e6;
|
||||||
|
--v-vert-ink: #1b4a22;
|
||||||
|
|
||||||
|
--v-jaune: #b08a00;
|
||||||
|
--v-jaune-bg: #fff5cf;
|
||||||
|
--v-jaune-ink: #5e4900;
|
||||||
|
|
||||||
|
--v-orange: #c25f00;
|
||||||
|
--v-orange-bg: #ffd9b3;
|
||||||
|
--v-orange-ink: #5a2a00;
|
||||||
|
|
||||||
|
--v-rouge: #c01818;
|
||||||
|
--v-rouge-bg: #ffb3b3;
|
||||||
|
--v-rouge-ink: #5a0a0a;
|
||||||
|
|
||||||
|
/* Vivid map fills — closer to Météo France's signage */
|
||||||
|
--map-vert: #c8e3c4;
|
||||||
|
--map-jaune: #ffdf3d;
|
||||||
|
--map-orange: #ff9024;
|
||||||
|
--map-rouge: #e2231a;
|
||||||
|
|
||||||
|
--font-ui: 'Public Sans', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
--font-display: 'Manrope', 'Public Sans', system-ui, sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
||||||
|
|
||||||
|
--r-md: 10px;
|
||||||
|
--r-lg: 16px;
|
||||||
|
--r-pill: 999px;
|
||||||
|
|
||||||
|
--sh-1: 0 1px 2px rgba(27, 24, 20, 0.04);
|
||||||
|
--sh-2: 0 2px 8px rgba(27, 24, 20, 0.06), 0 1px 2px rgba(27, 24, 20, 0.04);
|
||||||
|
--sh-3: 0 12px 32px rgba(27, 24, 20, 0.10), 0 2px 6px rgba(27, 24, 20, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] {
|
||||||
|
--paper: #14110d;
|
||||||
|
--paper-2: #1c1814;
|
||||||
|
--paper-elev: #221d18;
|
||||||
|
--paper-warm: #2a221a;
|
||||||
|
--ink: #f7f1e2;
|
||||||
|
--ink-2: #d8cfba;
|
||||||
|
--ink-soft: #a89e89;
|
||||||
|
--ink-mute: #756e60;
|
||||||
|
--line: #2f2820;
|
||||||
|
--line-strong: #46402f;
|
||||||
|
|
||||||
|
--brand: #ec9272;
|
||||||
|
--brand-deep: #f4b39a;
|
||||||
|
--brand-soft: #3d2317;
|
||||||
|
--brand-tint: #2a1b10;
|
||||||
|
--brand-ink: #ffd4c1;
|
||||||
|
|
||||||
|
--v-vert: #7dd99a;
|
||||||
|
--v-vert-bg: #15301b;
|
||||||
|
--v-vert-ink: #b8e8c5;
|
||||||
|
|
||||||
|
--v-jaune: #f0c860;
|
||||||
|
--v-jaune-bg: #36290c;
|
||||||
|
--v-jaune-ink: #ffe6a3;
|
||||||
|
|
||||||
|
--v-orange: #ffa970;
|
||||||
|
--v-orange-bg: #3a1d0a;
|
||||||
|
--v-orange-ink: #ffd3b3;
|
||||||
|
|
||||||
|
--v-rouge: #ff8a8a;
|
||||||
|
--v-rouge-bg: #381212;
|
||||||
|
--v-rouge-ink: #ffc4c4;
|
||||||
|
|
||||||
|
--map-vert: #2d5a35;
|
||||||
|
--map-jaune: #f0c860;
|
||||||
|
--map-orange: #ff9050;
|
||||||
|
--map-rouge: #e85050;
|
||||||
|
|
||||||
|
--sh-2: 0 2px 8px rgba(0, 0, 0, 0.35), 0 1px 2px rgba(0, 0, 0, 0.25);
|
||||||
|
--sh-3: 0 12px 32px rgba(0, 0, 0, 0.45), 0 2px 6px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
html {
|
html {
|
||||||
@apply text-slate-900 antialiased;
|
background: var(--paper);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 1.55;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
@apply antialiased;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-slate-50;
|
background: var(--paper);
|
||||||
|
color: var(--ink);
|
||||||
|
min-height: 100dvh;
|
||||||
|
transition: background-color .25s ease, color .25s ease;
|
||||||
|
}
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
html { font-size: 16px; }
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4 {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
color: var(--ink);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(2rem, 1.4rem + 2.5vw, 3.25rem);
|
||||||
|
line-height: 1.05;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: clamp(1.5rem, 1.1rem + 1.5vw, 2.25rem);
|
||||||
|
line-height: 1.15;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: clamp(1.15rem, 1rem + 0.6vw, 1.5rem);
|
||||||
|
line-height: 1.25;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
@apply underline-offset-2 hover:underline;
|
color: var(--brand-deep);
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
transition: border-color .15s;
|
||||||
|
}
|
||||||
|
a:hover { border-bottom-color: currentColor; }
|
||||||
|
*:focus { outline: none; }
|
||||||
|
*:focus-visible {
|
||||||
|
outline: 3px solid var(--brand);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.container-tight {
|
.container-tight {
|
||||||
@apply mx-auto w-full max-w-5xl px-4 sm:px-6;
|
max-width: 1240px;
|
||||||
|
margin-inline: auto;
|
||||||
|
width: 100%;
|
||||||
|
padding-inline: clamp(16px, 4vw, 32px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kicker {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ic-card {
|
||||||
|
background: var(--paper-elev);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
box-shadow: var(--sh-1);
|
||||||
|
padding: 20px;
|
||||||
|
transition: border-color .15s, box-shadow .15s, transform .15s;
|
||||||
|
}
|
||||||
|
.ic-card-soft {
|
||||||
|
background: var(--paper-warm);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.ic-card-interactive { cursor: pointer; }
|
||||||
|
.ic-card-interactive:hover {
|
||||||
|
border-color: var(--ink-soft);
|
||||||
|
box-shadow: var(--sh-2);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.ic-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 18px;
|
||||||
|
border-radius: var(--r-pill);
|
||||||
|
border: 1.5px solid transparent;
|
||||||
|
font: 600 0.95rem/1 var(--font-ui);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform .08s ease, box-shadow .15s, background-color .15s, border-color .15s, color .15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.ic-btn:hover { transform: translateY(-1px); border-bottom-color: transparent; }
|
||||||
|
.ic-btn:active { transform: translateY(0); }
|
||||||
|
.ic-btn-primary { background: var(--ink); color: var(--paper); border-color: var(--ink); }
|
||||||
|
.ic-btn-primary:hover { background: var(--ink-2); border-color: var(--ink-2); box-shadow: var(--sh-2); }
|
||||||
|
.ic-btn-brand { background: var(--brand); color: #fff; border-color: var(--brand); }
|
||||||
|
.ic-btn-brand:hover { background: var(--brand-deep); border-color: var(--brand-deep); box-shadow: var(--sh-2); }
|
||||||
|
.ic-btn-ghost { background: transparent; color: var(--ink); border-color: var(--line-strong); }
|
||||||
|
.ic-btn-ghost:hover { background: var(--paper-warm); border-color: var(--ink-soft); }
|
||||||
|
.ic-btn-sm { min-height: 36px; padding: 0 14px; font-size: 0.85rem; }
|
||||||
|
.ic-btn-lg { min-height: 52px; padding: 0 24px; font-size: 1rem; }
|
||||||
|
|
||||||
|
/* Vigilance pill (legend + chip) */
|
||||||
.vigilance-chip {
|
.vigilance-chip {
|
||||||
@apply inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-medium ring-1 ring-inset;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: var(--r-pill);
|
||||||
|
border: 1.5px solid;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
background: var(--paper-elev);
|
||||||
}
|
}
|
||||||
.vigilance-chip-1 {
|
.vigilance-chip .glyph { font-size: 0.75em; line-height: 1; }
|
||||||
@apply bg-green-50 text-green-800 ring-green-200;
|
.vigilance-chip-lg { padding: 8px 16px; font-size: 0.95rem; }
|
||||||
|
.vigilance-chip-lg .glyph { font-size: 0.85em; }
|
||||||
|
|
||||||
|
.vigilance-chip-1 { color: var(--v-vert); background: var(--v-vert-bg); border-color: var(--v-vert); }
|
||||||
|
.vigilance-chip-2 { color: var(--v-jaune); background: var(--v-jaune-bg); border-color: var(--v-jaune); }
|
||||||
|
.vigilance-chip-3 { color: var(--v-orange); background: var(--v-orange-bg); border-color: var(--v-orange); }
|
||||||
|
.vigilance-chip-4 { color: var(--v-rouge); background: var(--v-rouge-bg); border-color: var(--v-rouge); }
|
||||||
|
|
||||||
|
/* Vigilance block (colored card for hero alerts) */
|
||||||
|
.v-block {
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
border-width: 1.5px;
|
||||||
|
border-style: solid;
|
||||||
|
padding: 20px;
|
||||||
}
|
}
|
||||||
.vigilance-chip-2 {
|
.v-vert { background-color: var(--v-vert-bg); border-color: var(--v-vert); color: var(--v-vert-ink); }
|
||||||
@apply bg-yellow-50 text-yellow-900 ring-yellow-300;
|
.v-jaune { background-color: var(--v-jaune-bg); border-color: var(--v-jaune); color: var(--v-jaune-ink); }
|
||||||
|
.v-orange { background-color: var(--v-orange-bg); border-color: var(--v-orange); color: var(--v-orange-ink); }
|
||||||
|
.v-rouge { background-color: var(--v-rouge-bg); border-color: var(--v-rouge); color: var(--v-rouge-ink); }
|
||||||
|
|
||||||
|
/* Input */
|
||||||
|
.ic-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 44px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-radius: var(--r-pill);
|
||||||
|
border: 1.5px solid var(--line-strong);
|
||||||
|
background: var(--paper-2);
|
||||||
|
font: 500 1rem var(--font-ui);
|
||||||
|
color: var(--ink);
|
||||||
|
transition: border-color .15s, box-shadow .15s;
|
||||||
}
|
}
|
||||||
.vigilance-chip-3 {
|
.ic-input:focus-within {
|
||||||
@apply bg-orange-50 text-orange-900 ring-orange-300;
|
border-color: var(--brand);
|
||||||
|
box-shadow: 0 0 0 4px var(--brand-tint);
|
||||||
}
|
}
|
||||||
.vigilance-chip-4 {
|
.ic-input input {
|
||||||
@apply bg-red-50 text-red-900 ring-red-300;
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.ic-input input::placeholder { color: var(--ink-mute); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skip link */
|
||||||
|
.skip-link {
|
||||||
|
position: absolute;
|
||||||
|
top: -100px;
|
||||||
|
left: 8px;
|
||||||
|
background: var(--ink);
|
||||||
|
color: var(--paper);
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
font-weight: 600;
|
||||||
|
z-index: 1000;
|
||||||
|
transition: top .15s;
|
||||||
|
}
|
||||||
|
.skip-link:focus { top: 8px; border-bottom: none; }
|
||||||
|
|
||||||
|
/* Dept row hover (homepage list) */
|
||||||
|
.dept-row:hover, .dept-row:focus-visible {
|
||||||
|
background-color: var(--paper-warm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sticky blurred header */
|
||||||
|
.site-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background: color-mix(in srgb, var(--paper) 90%, transparent);
|
||||||
|
backdrop-filter: saturate(140%) blur(12px);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.site-footer {
|
||||||
|
margin-top: 80px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
background: var(--paper-warm);
|
||||||
|
}
|
||||||
|
.site-footer h4 {
|
||||||
|
color: var(--ink-soft);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map fills (vivid, MF-like) */
|
||||||
|
.france-map-dept {
|
||||||
|
transition: filter .15s, stroke-width .15s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.france-map-dept:hover, .france-map-dept:focus {
|
||||||
|
stroke: var(--ink);
|
||||||
|
stroke-width: 1.2;
|
||||||
|
filter: brightness(1.05);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.france-map-fill-1 { fill: var(--map-vert); }
|
||||||
|
.france-map-fill-2 { fill: var(--map-jaune); }
|
||||||
|
.france-map-fill-3 { fill: var(--map-orange); }
|
||||||
|
.france-map-fill-4 { fill: var(--map-rouge); }
|
||||||
|
|
||||||
|
/* Icon button (header toggles) */
|
||||||
|
.icon-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--r-pill);
|
||||||
|
background: transparent;
|
||||||
|
border: 1.5px solid var(--line-strong);
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--ink);
|
||||||
|
transition: background .15s, border-color .15s;
|
||||||
|
}
|
||||||
|
.icon-btn:hover { background: var(--paper-warm); border-color: var(--ink-soft); }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*, *::before, *::after {
|
||||||
|
animation-duration: .01ms !important;
|
||||||
|
transition-duration: .01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.site-header, .site-footer, .no-print { display: none !important; }
|
||||||
|
body { background: white; color: black; font-size: 12pt; }
|
||||||
|
.v-block, .ic-card { box-shadow: none !important; break-inside: avoid; }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'sans-serif'],
|
sans: ['"Public Sans"', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'sans-serif'],
|
||||||
|
display: ['Manrope', '"Public Sans"', 'system-ui', 'sans-serif'],
|
||||||
|
mono: ['"JetBrains Mono"', 'ui-monospace', 'monospace'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue