Corriger la revue stage1 de la refonte : fiche de dalle, thème du fond, cadre d'export, tokens de couleur
Retour/Échap depuis la fiche détaillée ne retouche plus la hauteur du volet téléphone (openTab remplacé par un rétablissement direct de l'onglet) ; le fond OSM suit le thème sauf choix explicite (migration du localStorage et des défauts serveur en v3, plus de dark:true hérité de l'ex-webapp) et part du bon thème dès sa création (plus de flash sombre) ; l'onglet Export mémorisé ne recadre plus la vue au premier rendu (lien partagé préservé) et referme son cadre pendant que la fiche détaillée est ouverte ; la qualité affichée suit le nom réel de la dalle rendue ; un second clic referme la fiche au lieu de déplacer son contour derrière elle ; couleurs en dur restantes tokenisées (--tile-sel, --gps, .gen-log) et lisibles en thème clair ; localStorage corrompu (lidar-panel, lidar-print) n'y casse plus l'interface. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -19,6 +19,10 @@
|
||||
--gen-pending: #0b8fc0;
|
||||
--gen-failed: #c8452c;
|
||||
--gen-sel: var(--accent);
|
||||
/* Contour de sélection de dalle : plus sombre en thème clair (lisible sur
|
||||
le fond OSM clair), jaune franc en thème sombre. */
|
||||
--tile-sel: #b8860b;
|
||||
--gps: #39d98a;
|
||||
--fs-xs: 11px; --fs-sm: 12px; --fs-md: 13px; --fs-lg: 16px;
|
||||
--r-sm: 6px; --r-md: 10px; --r-full: 999px;
|
||||
--sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
|
||||
@ -34,6 +38,7 @@
|
||||
--border: rgba(255, 255, 255, 0.12); --text: #d9dee6; --muted: #8b94a3;
|
||||
--accent: #5aa9e6; --on-accent: #0b0e13; --track: rgba(255, 255, 255, 0.18);
|
||||
--print: #ffd166; --gen-running: #f4a261; --gen-pending: #4cc9f0; --gen-failed: #e76f51;
|
||||
--tile-sel: #ffd24a;
|
||||
--shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.4); --shadow-lg: 0 6px 24px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
}
|
||||
@ -43,6 +48,7 @@
|
||||
--border: rgba(255, 255, 255, 0.12); --text: #d9dee6; --muted: #8b94a3;
|
||||
--accent: #5aa9e6; --on-accent: #0b0e13; --track: rgba(255, 255, 255, 0.18);
|
||||
--print: #ffd166; --gen-running: #f4a261; --gen-pending: #4cc9f0; --gen-failed: #e76f51;
|
||||
--tile-sel: #ffd24a;
|
||||
--shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.4); --shadow-lg: 0 6px 24px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
@ -188,6 +194,10 @@ input[type="range"]::-moz-range-thumb { width: 10px; height: 10px; border-radius
|
||||
|
||||
/* --- génération de tuiles ------------------------------------------------ */
|
||||
.print-msg { font-size: var(--fs-sm); margin-left: 8px; }
|
||||
/* #1b1b1b (glyphe + bordure) reste en dur volontairement : --print est un jaune/ambre
|
||||
proche dans les deux thèmes (#d99a00 clair, #ffd166 sombre), un texte sombre fixe
|
||||
y garde un contraste correct dans les deux cas — un token --text/--on-accent
|
||||
changerait de sens (clair en thème sombre) et casserait ce contraste. */
|
||||
.print-handle { display: flex; align-items: center; justify-content: center; cursor: move;
|
||||
font-size: var(--fs-lg); color: #1b1b1b; background: var(--print); border: 2px solid #1b1b1b;
|
||||
border-radius: var(--r-full); box-shadow: 0 1px 6px rgba(0,0,0,0.5); }
|
||||
@ -198,8 +208,8 @@ body.drawing .leaflet-container { cursor: crosshair !important; }
|
||||
.chk { display: flex; gap: 7px; align-items: center; font-size: var(--fs-xs);
|
||||
color: var(--muted); margin: 4px 0; cursor: pointer; }
|
||||
.chk input { accent-color: var(--accent); }
|
||||
.gen-log { font: var(--fs-xs)/1.5 ui-monospace, "SF Mono", Menlo, monospace; color: var(--muted);
|
||||
background: rgba(0,0,0,0.35); border: 1px solid var(--border); border-radius: var(--r-sm);
|
||||
.gen-log { font: var(--fs-xs)/1.5 ui-monospace, "SF Mono", Menlo, monospace; color: var(--text);
|
||||
background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r-sm);
|
||||
padding: 6px 8px; max-height: 130px; overflow-y: auto; white-space: pre-wrap;
|
||||
word-break: break-all; margin: 6px 0; }
|
||||
#tools button.busy { color: var(--accent); border-color: var(--accent); }
|
||||
|
||||
@ -44,6 +44,10 @@ const PREC_LEVELS = 16;
|
||||
|
||||
let META = null;
|
||||
let STATE = null;
|
||||
// Vrai jusqu'au premier rendu du panneau au démarrage : évite qu'un onglet
|
||||
// Export mémorisé recadre la vue par-dessus un lien partagé ou l'URL initiale
|
||||
// (cf. printSetOpen).
|
||||
let BOOTSTRAPPING = true;
|
||||
|
||||
// --- thème ------------------------------------------------------------------
|
||||
// auto = suit le système ; ☀/☾ force clair ou sombre ; 3ᵉ appui : auto.
|
||||
@ -174,7 +178,10 @@ ZoomAnimTileLayer.addInitHook(function () {
|
||||
});
|
||||
|
||||
const base = new ZoomAnimTileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19, maxNativeZoom: 19, keepBuffer: 2, className: 'base-dark',
|
||||
// Classe posée dès la création d'après le thème courant (pas un « dark » figé) :
|
||||
// sinon le fond flashe sombre en thème clair le temps que /api/map/meta réponde
|
||||
// (et pour de bon si l'appel échoue). applyLayers() la corrige ensuite.
|
||||
maxZoom: 19, maxNativeZoom: 19, keepBuffer: 2, className: themeIsDark() ? 'base-dark' : '',
|
||||
// Sinon _setView recrée un niveau en cours d'animation malgré noUpdate.
|
||||
updateWhenZooming: false,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
@ -210,7 +217,9 @@ function defaultState(meta) {
|
||||
precOpacity: clamp01(meta.default_precision_opacity, 0.6),
|
||||
base: { on: b.on !== undefined ? !!b.on : true,
|
||||
opacity: b.opacity !== undefined ? Number(b.opacity) : 0.85,
|
||||
dark: b.dark !== undefined ? !!b.dark : null },
|
||||
// null = suit le thème (choix explicite seulement si b.dark est déjà
|
||||
// un booléen posé côté serveur).
|
||||
dark: b.dark == null ? null : !!b.dark },
|
||||
};
|
||||
}
|
||||
|
||||
@ -219,10 +228,15 @@ function loadState(meta) {
|
||||
let st = def;
|
||||
const s = lsGet(LS_KEY, null);
|
||||
if (s) {
|
||||
// Ancien format (avant v3) : l'ex-webapp persistait base.dark:true à chaque
|
||||
// sauvegarde, jamais un vrai choix de l'utilisateur — ignoré ici (le fond
|
||||
// suit alors le thème) ; v3 seulement écrit un dark explicite (voir saveState).
|
||||
const sBase = Object.assign({}, s.base || {});
|
||||
if (s.v !== 3) delete sBase.dark;
|
||||
st = { main: validMain(meta, s.main) || def.main,
|
||||
mode: validMode(s.mode) || def.mode,
|
||||
precOpacity: clamp01(s.precOpacity, def.precOpacity),
|
||||
base: Object.assign({}, def.base, s.base || {}) };
|
||||
base: Object.assign({}, def.base, sBase) };
|
||||
}
|
||||
// Lien partagé : prime sur l'état local (les anciens liens &L=… de la
|
||||
// pile de couches sont ignorés, la vue s'ouvre alors sur l'état courant).
|
||||
@ -236,7 +250,9 @@ function loadState(meta) {
|
||||
}
|
||||
|
||||
function saveState() {
|
||||
lsSet(LS_KEY, STATE);
|
||||
// v: 3 marque un format où base.dark, quand posé, est un vrai choix de
|
||||
// l'utilisateur (voir loadState) — jamais mélangé à STATE lui-même.
|
||||
lsSet(LS_KEY, Object.assign({ v: 3 }, STATE));
|
||||
}
|
||||
|
||||
// --- hash de vue ----------------------------------------------------------
|
||||
@ -488,7 +504,7 @@ function selectTileOutline(data) {
|
||||
if (!data || !data.corners) return;
|
||||
// Panneau dédié au-dessus de la pile LiDAR : dans l'overlayPane par défaut
|
||||
// (z-index 400, comme lidarStack), le contour passait sous les tuiles.
|
||||
tileSel = L.polygon(data.corners, { pane: 'tileSel', color: '#ffd24a', weight: 2,
|
||||
tileSel = L.polygon(data.corners, { pane: 'tileSel', color: cssVar('--tile-sel'), weight: 2,
|
||||
fill: false, dashArray: '6 4',
|
||||
interactive: false }).addTo(map);
|
||||
}
|
||||
@ -586,6 +602,10 @@ let tileReturnTab = null; // onglet à rouvrir après « ← Retour »
|
||||
|
||||
function showTilePopup(data, latlng) {
|
||||
lastTileQuery = latlng || lastTileQuery;
|
||||
// Un nouveau clic pendant que la fiche détaillée d'une AUTRE dalle est
|
||||
// ouverte : on la referme (sinon le contour bouge sur la carte alors que
|
||||
// la fiche affiche encore l'ancienne dalle).
|
||||
if (!el('tileView').hidden) restoreTabAfterDetails();
|
||||
if (tilePopup) tilePopup.remove();
|
||||
const html = !data
|
||||
? '<div class="tile-pop">Serveur injoignable : réessayez dans un instant.</div>'
|
||||
@ -614,6 +634,10 @@ function openTileDetails() {
|
||||
tileReturnTab = PANEL.tab;
|
||||
for (const t of ['view', 'print', 'gen', 'share']) el('tab-' + t).hidden = true;
|
||||
el('tileView').hidden = false;
|
||||
// La fiche masque l'onglet Export : son cadre et sa poignée n'ont rien à
|
||||
// faire sur la carte tant qu'elle est ouverte (restoreTabAfterDetails les
|
||||
// rétablit via panel:tab en revenant sur l'onglet Export).
|
||||
printSetOpen(false);
|
||||
if (isPhone() && PANEL.sheet === 'closed') setSheet('half');
|
||||
if (!isPhone() && PANEL.collapsed) setCollapsed(false);
|
||||
showTileInfo(data, lastTileQuery);
|
||||
@ -627,12 +651,22 @@ function hideTileView() {
|
||||
if (tileSel) { map.removeLayer(tileSel); tileSel = null; }
|
||||
}
|
||||
|
||||
// Referme la fiche détaillée et rétablit l'onglet d'origine SANS passer par
|
||||
// openTab : celui-ci retouche la hauteur du volet quand l'onglet visé est déjà
|
||||
// l'onglet actif (règle « re-toucher l'onglet actif ») — ce qui n'a pas lieu
|
||||
// d'être ici, on rouvre simplement ce qui était affiché avant.
|
||||
function restoreTabAfterDetails() {
|
||||
hideTileView();
|
||||
PANEL.tab = PANEL.want = tileReturnTab || 'view';
|
||||
panelSave();
|
||||
panelRender();
|
||||
}
|
||||
|
||||
// Ferme la bulle, sinon la fiche ; renvoie true si quelque chose a été fermé.
|
||||
function closeTileViews() {
|
||||
if (tilePopup) { tilePopup.remove(); tilePopup = null; return true; }
|
||||
if (!el('tileView').hidden) {
|
||||
hideTileView();
|
||||
openTab(tileReturnTab || 'view');
|
||||
restoreTabAfterDetails();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -1039,12 +1073,18 @@ function printSave() {
|
||||
}
|
||||
|
||||
function printLoad() {
|
||||
const s = lsGet(PRINT_LS, {});
|
||||
const raw = lsGet(PRINT_LS, {});
|
||||
// Valeur corrompue (clé partagée avec une autre appli, quota dépassé
|
||||
// pendant l'écriture…) : objet non conforme ignoré plutôt que planté.
|
||||
const s = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
||||
for (const [k, id] of [['paper', 'printPaper'], ['orient', 'printOrient'],
|
||||
['scale', 'printScale'], ['title', 'printTitle']]) {
|
||||
if (s[k] != null) el(id).value = s[k];
|
||||
if (typeof s[k] === 'string') el(id).value = s[k];
|
||||
}
|
||||
if (Array.isArray(s.center) && s.center.length === 2 &&
|
||||
s.center.every((n) => typeof n === 'number' && Number.isFinite(n))) {
|
||||
PRINT.center = L.latLng(s.center);
|
||||
}
|
||||
if (Array.isArray(s.center) && s.center.length === 2) PRINT.center = L.latLng(s.center);
|
||||
}
|
||||
|
||||
// Recadre sur le cadre en laissant libre la place du panneau (colonne à
|
||||
@ -1122,13 +1162,16 @@ function printCenterHere() {
|
||||
|
||||
// Cadre affiché tant que l'onglet Export PDF est visible (événement panel:tab,
|
||||
// émis à chaque rendu du panneau : on ne fait rien si l'état est inchangé).
|
||||
function printSetOpen(on) {
|
||||
// fit : recadrer la vue sur le cadre une fois posé — faux au tout premier
|
||||
// rendu (onglet Export mémorisé) pour ne pas écraser le zoom d'un lien
|
||||
// partagé ou de l'URL au chargement.
|
||||
function printSetOpen(on, fit) {
|
||||
if (PRINT.on === on) return;
|
||||
PRINT.on = on;
|
||||
if (on) {
|
||||
// cadre gardé s'il est encore dans la vue, sinon posé au centre
|
||||
if (!PRINT.center || !map.getBounds().contains(PRINT.center)) PRINT.center = map.getCenter();
|
||||
printRefreshFrame(true);
|
||||
printRefreshFrame(fit !== false);
|
||||
} else {
|
||||
if (PRINT.rect) PRINT.rect.remove();
|
||||
if (PRINT.handle) PRINT.handle.remove();
|
||||
@ -1174,13 +1217,25 @@ el('printTitle').addEventListener('change', printSave);
|
||||
printLoad();
|
||||
|
||||
document.addEventListener('panel:tab', (e) => {
|
||||
printSetOpen(e.detail.visible === 'print');
|
||||
printSetOpen(e.detail.visible === 'print', !BOOTSTRAPPING);
|
||||
if (e.detail.visible !== 'gen' && genDrawing) genSetDrawing(false);
|
||||
});
|
||||
|
||||
// --- panneau à onglets --------------------------------------------------------
|
||||
const PANEL_LS = 'lidar-panel';
|
||||
const PANEL = Object.assign({ tab: 'view', collapsed: false, sheet: 'half' }, lsGet(PANEL_LS, {}));
|
||||
// Ne retient que les clés connues, du bon type et (pour sheet) dans la liste
|
||||
// valide : une valeur corrompue casserait sinon le CSS (panel.dataset.sheet
|
||||
// inconnu de map.css) ou l'onglet affiché.
|
||||
function _sanitizedPanelLS() {
|
||||
const raw = lsGet(PANEL_LS, {});
|
||||
const s = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
||||
const out = {};
|
||||
if (typeof s.tab === 'string') out.tab = s.tab;
|
||||
if (typeof s.collapsed === 'boolean') out.collapsed = s.collapsed;
|
||||
if (['closed', 'half', 'full'].includes(s.sheet)) out.sheet = s.sheet;
|
||||
return out;
|
||||
}
|
||||
const PANEL = Object.assign({ tab: 'view', collapsed: false, sheet: 'half' }, _sanitizedPanelLS());
|
||||
// want = onglet choisi (mémorisé) ; tab = onglet affiché. Génération encore
|
||||
// inconnue au démarrage (api/status) : Affichage en attendant, puis l'onglet
|
||||
// choisi revient dès qu'il est proposé.
|
||||
@ -1314,6 +1369,7 @@ fetch('api/map/meta').then(r => r.json()).then(meta => {
|
||||
renderPanel();
|
||||
renderBaseRow();
|
||||
panelRender();
|
||||
BOOTSTRAPPING = false;
|
||||
renderUseCard();
|
||||
genFillViz();
|
||||
// Droits de génération : boutons affichés si le serveur peut générer
|
||||
@ -1449,7 +1505,7 @@ el('gps').addEventListener('click', () => {
|
||||
map.setView(ll, Math.max(map.getZoom(), 16));
|
||||
if (window._gpsMarker) window._gpsMarker.remove();
|
||||
window._gpsMarker = L.circleMarker(ll, {
|
||||
radius: 6, color: '#39d98a', fillColor: '#39d98a', fillOpacity: 0.9
|
||||
radius: 6, color: cssVar('--gps'), fillColor: cssVar('--gps'), fillOpacity: 0.9
|
||||
}).addTo(map);
|
||||
}, (err) => {
|
||||
btn.classList.remove('busy');
|
||||
|
||||
Reference in New Issue
Block a user