Bouton flottant « Fond » avec la vignette du fond de carte en cours, et son menu — choix du fond par de vraies tuiles de la zone, et exagération du relief 3D.
stableÀ valider

Installation
Copie le fichier dans ton projet, ainsi que les tokens qu’il utilise.
import { useRef, useState } from 'react';
import { useDismiss } from '../hooks/useDismiss';
import { BASEMAPS, tileAt, type Basemap } from '../lib/mapOptions';
import { usePresence } from '../hooks/usePresence';
import type { Region } from '../lib/sources/types';
import { fmt } from '../lib/format';
interface Props {
basemap: Basemap;
onBasemap: (b: Basemap) => void;
exaggeration: number;
onExaggeration: (x: number) => void;
/** point de la trace utilisé pour les vignettes (vraies tuiles de chaque fond) */
center: { lat: number; lon: number };
/** pays majoritaire de la trace et fonds qui y sont accessibles (vignettes des fonds assemblés) */
region: { region: Region | null; basemaps: Basemap[] } | undefined;
}
const THUMB_ZOOM = 13;
/** Bouton flottant « Fond » et son menu de fonds de carte, façon Komoot. */
export default function MapLayersMenu({ basemap, onBasemap, exaggeration, onExaggeration, center, region }: Props) {
const [open, setOpen] = useState(false);
const root = useRef<HTMLDivElement>(null);
useDismiss(open, root, () => setOpen(false));
// reste affiché le temps de l'animation de fermeture
const menu = usePresence(open, 150);
const thumb = (id: Basemap) => tileAt(BASEMAPS.find((b) => b.id === id)!, center.lat, center.lon, THUMB_ZOOM, region?.basemaps.includes(id) ? region.region : null);
return (
<div ref={root} className="map-layers">
<button
className={`map-layers-btn${open ? ' open' : ''}`}
onClick={() => setOpen((o) => !o)}
aria-expanded={open}
aria-haspopup="dialog"
aria-controls="map-layers-menu"
aria-label="Fond de carte et relief"
title="Fond de carte et relief"
style={{ backgroundImage: `url("${thumb(basemap)}")` }}
>
<span aria-hidden="true">Fond</span>
</button>
{menu.mounted && (
<div id="map-layers-menu" className={`map-menu map-layers-menu${menu.closing ? ' closing' : ''}`} inert={menu.closing} role="dialog" aria-label="Fond de carte et relief">
<div className="map-menu-head">
<h2>Fond de carte et relief</h2>
<button className="map-menu-close" onClick={() => setOpen(false)} aria-label="Fermer">
<svg viewBox="0 0 20 20" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" aria-hidden="true">
<path d="M5 5l10 10M15 5L5 15" />
</svg>
</button>
</div>
<h3>Fond de carte</h3>
<div className="basemap-grid" role="radiogroup" aria-label="Fond de carte">
{BASEMAPS.map((b) => (
<button key={b.id} role="radio" aria-checked={b.id === basemap} className="basemap-tile" onClick={() => onBasemap(b.id)}>
<img src={thumb(b.id)} alt="" loading="lazy" draggable={false} />
<span>{b.label}</span>
</button>
))}
</div>
<h3>Relief 3D</h3>
<label className="relief-row">
<span className="map-menu-text">
<b>Exagération ×{fmt(exaggeration, 1)}</b>
<small>Accentue le relief pour mieux lire les pentes</small>
</span>
<input type="range" min={1} max={3} step={0.1} value={exaggeration} onChange={(e) => onExaggeration(parseFloat(e.target.value))} />
</label>
</div>
)}
</div>
);
}
Tokens : --panel --text --muted --line --accent --chip --shadow --ease-out
Utilisation
import MapLayersMenu from './components/MapLayersMenu';
<MapLayersMenu basemap={basemap} onBasemap={setBasemap} exaggeration={exaggeration} onExaggeration={setExaggeration} center={center} region={region} />Anatomie
| # | Élément | Rôle |
|---|---|---|
| 1 | Bouton | Vignette du fond en cours, à la position de la trace, et le mot « Fond ». |
| 2 | Fonds de carte | Grille de vignettes en radiogroup, une vraie tuile de chaque fond à l’endroit de la trace. |
| 3 | Relief 3D | Curseur d’exagération, de ×1 à ×3. |
Guidelines
- SHOULDMontrer chaque fond par une tuile de l’endroit de la trace plutôt que par une image générique.
Référence de l’API
| Prop | Type | Défaut |
|---|---|---|
basemap | Basemap | requis |
onBasemap | (b: Basemap) => void | requis |
exaggeration | number | requis |
onExaggeration | (x: number) => void | requis |
centerpoint de la trace utilisé pour les vignettes (vraies tuiles de chaque fond) | { lat: number; lon: number } | requis |
regionpays majoritaire de la trace et fonds qui y sont accessibles (vignettes des fonds assemblés) | { region: Region | null; basemaps: Basemap[] } | undefined | requis |