feat(web): add ocean telemetry dashboard
This commit is contained in:
parent
e1f728ff9b
commit
3355f8b7ef
7 changed files with 1140 additions and 0 deletions
242
apps/web/app.js
Normal file
242
apps/web/app.js
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
(() => {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const CACHE_KEY = "nereus.telemetry.v1";
|
||||||
|
const state = {
|
||||||
|
buoys: [],
|
||||||
|
readings: [],
|
||||||
|
markers: [],
|
||||||
|
responses: [],
|
||||||
|
totalErrors: 0,
|
||||||
|
connectivity: "offline",
|
||||||
|
version: null,
|
||||||
|
lastDataAt: null,
|
||||||
|
healthPolling: false,
|
||||||
|
dataPolling: false,
|
||||||
|
probes: {
|
||||||
|
liveness: {ok: null, changedAt: null},
|
||||||
|
readiness: {ok: null, changedAt: null}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const elements = Object.fromEntries([
|
||||||
|
"clock", "version-value", "version-note", "liveness-dot", "liveness-time",
|
||||||
|
"readiness-dot", "readiness-time", "error-total", "error-rate", "error-state",
|
||||||
|
"buoy-count", "data-age", "map-state", "telemetry-state", "latest-temp",
|
||||||
|
"latest-wave", "latest-salinity", "readings-chart"
|
||||||
|
].map((id) => [id, document.getElementById(id)]));
|
||||||
|
|
||||||
|
const map = L.map("map", {zoomControl: false, attributionControl: true, scrollWheelZoom: false}).setView([39.3, -4.2], 5);
|
||||||
|
map.attributionControl.addAttribution("Natural Earth");
|
||||||
|
if (window.NEREUS_LAND) {
|
||||||
|
L.geoJSON(window.NEREUS_LAND, {
|
||||||
|
interactive: false,
|
||||||
|
style: {className: "landmass", fillColor: "#183640", fillOpacity: .72, color: "#41636a", weight: .7}
|
||||||
|
}).addTo(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreCache() {
|
||||||
|
try {
|
||||||
|
const cached = JSON.parse(localStorage.getItem(CACHE_KEY));
|
||||||
|
if (!cached || !Array.isArray(cached.buoys) || !Array.isArray(cached.readings)) return;
|
||||||
|
state.buoys = cached.buoys;
|
||||||
|
state.readings = cached.readings;
|
||||||
|
state.lastDataAt = cached.savedAt || null;
|
||||||
|
renderTelemetry(true);
|
||||||
|
} catch (_) {
|
||||||
|
// A damaged cache is equivalent to no cache and should stay invisible.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCache() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(CACHE_KEY, JSON.stringify({buoys: state.buoys, readings: state.readings, savedAt: state.lastDataAt}));
|
||||||
|
} catch (_) {
|
||||||
|
// Private browsing and storage quotas must not affect the dashboard.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiFetch(path) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
try {
|
||||||
|
const response = await fetch(path, {headers: {Accept: "application/json"}, cache: "no-store"});
|
||||||
|
state.responses.push({at: startedAt, error: !response.ok});
|
||||||
|
if (!response.ok) {
|
||||||
|
state.totalErrors += 1;
|
||||||
|
throw Object.assign(new Error("API response was not successful"), {httpFailure: true});
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
} catch (error) {
|
||||||
|
if (!error.httpFailure) state.connectivity = "offline";
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
renderErrors();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function probe(path, includeVersion = false) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(path, {cache: "no-store"});
|
||||||
|
return {ok: response.status === 200, version: includeVersion ? response.headers.get("X-Nereus-Version") : null};
|
||||||
|
} catch (_) {
|
||||||
|
return {ok: false, version: null};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProbe(name, ok) {
|
||||||
|
const probeState = state.probes[name];
|
||||||
|
if (probeState.ok !== ok) {
|
||||||
|
probeState.ok = ok;
|
||||||
|
probeState.changedAt = new Date();
|
||||||
|
}
|
||||||
|
elements[`${name}-dot`].className = `indicator ${ok ? "ok" : "bad"}`;
|
||||||
|
elements[`${name}-time`].textContent = probeState.changedAt ? `Changed ${formatTime(probeState.changedAt)}` : "No transition yet";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollHealth() {
|
||||||
|
if (state.healthPolling) return;
|
||||||
|
state.healthPolling = true;
|
||||||
|
try {
|
||||||
|
const [live, ready] = await Promise.all([probe("/healthz", true), probe("/readyz")]);
|
||||||
|
if (live.version) state.version = live.version;
|
||||||
|
updateProbe("liveness", live.ok);
|
||||||
|
updateProbe("readiness", ready.ok);
|
||||||
|
if (!live.ok) state.connectivity = "offline";
|
||||||
|
renderConnection();
|
||||||
|
} finally {
|
||||||
|
state.healthPolling = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollData() {
|
||||||
|
if (state.dataPolling) return;
|
||||||
|
state.dataPolling = true;
|
||||||
|
try {
|
||||||
|
const [buoys, readings] = await Promise.all([
|
||||||
|
apiFetch("/api/v1/buoys"),
|
||||||
|
apiFetch("/api/v1/readings?limit=500")
|
||||||
|
]);
|
||||||
|
state.buoys = Array.isArray(buoys) ? buoys : [];
|
||||||
|
state.readings = Array.isArray(readings) ? readings : [];
|
||||||
|
state.lastDataAt = new Date().toISOString();
|
||||||
|
state.connectivity = "online";
|
||||||
|
saveCache();
|
||||||
|
renderTelemetry(false);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.httpFailure) state.connectivity = "degraded";
|
||||||
|
renderTelemetry(true);
|
||||||
|
} finally {
|
||||||
|
state.dataPolling = false;
|
||||||
|
renderConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderConnection() {
|
||||||
|
const online = state.connectivity === "online";
|
||||||
|
const reachable = state.connectivity !== "offline";
|
||||||
|
elements["telemetry-state"].className = `connection-pill ${state.connectivity}`;
|
||||||
|
elements["telemetry-state"].textContent = state.connectivity === "online" ? "Live" : state.connectivity === "degraded" ? "API errors" : "Offline";
|
||||||
|
elements["version-value"].textContent = reachable ? state.version || "unavailable" : "offline";
|
||||||
|
elements["version-note"].textContent = reachable ? (state.version ? "Reported by API" : "Version unavailable") : "Cluster unavailable";
|
||||||
|
document.querySelector(".map-card").classList.toggle("stale", !online);
|
||||||
|
document.querySelector(".telemetry-card").classList.toggle("stale", !online);
|
||||||
|
renderErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderErrors() {
|
||||||
|
const cutoff = Date.now() - 60000;
|
||||||
|
state.responses = state.responses.filter((event) => event.at >= cutoff);
|
||||||
|
const failed = state.responses.filter((event) => event.error).length;
|
||||||
|
const rate = state.responses.length ? failed / state.responses.length : 0;
|
||||||
|
elements["error-total"].textContent = state.totalErrors.toLocaleString();
|
||||||
|
elements["error-rate"].textContent = `${(rate * 100).toFixed(1)}%`;
|
||||||
|
elements["error-state"].textContent = state.connectivity === "offline"
|
||||||
|
? "API unreachable"
|
||||||
|
: failed > 0 ? "API responding with errors" : "API responses healthy";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTelemetry(stale) {
|
||||||
|
renderMap(stale);
|
||||||
|
drawChart();
|
||||||
|
const latest = [...state.readings].sort((a, b) => new Date(b.recorded_at) - new Date(a.recorded_at))[0];
|
||||||
|
elements["latest-temp"].textContent = numberOrDash(latest?.water_temp, 1);
|
||||||
|
elements["latest-wave"].textContent = numberOrDash(latest?.wave_height, 1);
|
||||||
|
elements["latest-salinity"].textContent = numberOrDash(latest?.salinity, 1);
|
||||||
|
elements["buoy-count"].textContent = `${state.buoys.length} ${state.buoys.length === 1 ? "buoy" : "buoys"}`;
|
||||||
|
elements["data-age"].textContent = state.lastDataAt ? `Updated ${formatTime(new Date(state.lastDataAt))}` : "No telemetry";
|
||||||
|
elements["map-state"].textContent = state.buoys.length ? (stale ? "Showing last known positions" : "Live positions") : "Waiting for buoy positions";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMap() {
|
||||||
|
state.markers.forEach((marker) => marker.remove());
|
||||||
|
const latest = new Map();
|
||||||
|
state.readings.forEach((reading) => {
|
||||||
|
const previous = latest.get(reading.buoy_id);
|
||||||
|
if (!previous || new Date(reading.recorded_at) > new Date(previous.recorded_at)) latest.set(reading.buoy_id, reading);
|
||||||
|
});
|
||||||
|
state.markers = state.buoys.filter((buoy) => Number.isFinite(buoy.latitude) && Number.isFinite(buoy.longitude)).map((buoy) => {
|
||||||
|
const reading = latest.get(buoy.id);
|
||||||
|
const details = reading ? `${numberOrDash(reading.water_temp, 1)} °C · ${numberOrDash(reading.wave_height, 1)} m · ${numberOrDash(reading.salinity, 1)} PSU<br><small>${formatTime(new Date(reading.recorded_at))}</small>` : "Awaiting first sample";
|
||||||
|
const icon = L.divIcon({className: "", html: '<div class="buoy-marker"></div>', iconSize: [13, 13], iconAnchor: [6, 6]});
|
||||||
|
return L.marker([buoy.latitude, buoy.longitude], {icon}).bindTooltip(`<strong>${escapeText(buoy.name)}</strong><br>${buoy.latitude.toFixed(2)}, ${buoy.longitude.toFixed(2)}<br>${details}`, {direction: "top"}).addTo(map);
|
||||||
|
});
|
||||||
|
if (state.markers.length) {
|
||||||
|
const bounds = L.featureGroup(state.markers).getBounds();
|
||||||
|
if (bounds.isValid()) map.fitBounds(bounds.pad(.22), {maxZoom: 7, animate: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawChart() {
|
||||||
|
const canvas = elements["readings-chart"];
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const ratio = window.devicePixelRatio || 1;
|
||||||
|
canvas.width = Math.max(1, Math.floor(rect.width * ratio));
|
||||||
|
canvas.height = Math.max(1, Math.floor(rect.height * ratio));
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
context.scale(ratio, ratio);
|
||||||
|
const width = rect.width, height = rect.height, pad = {top: 13, right: 13, bottom: 23, left: 34};
|
||||||
|
const values = [...state.readings].sort((a, b) => new Date(a.recorded_at) - new Date(b.recorded_at)).slice(-45);
|
||||||
|
context.clearRect(0, 0, width, height);
|
||||||
|
context.strokeStyle = "rgba(121, 173, 180, .12)";
|
||||||
|
context.lineWidth = 1;
|
||||||
|
for (let i = 0; i < 5; i += 1) {
|
||||||
|
const y = pad.top + ((height - pad.top - pad.bottom) * i / 4);
|
||||||
|
context.beginPath(); context.moveTo(pad.left, y); context.lineTo(width - pad.right, y); context.stroke();
|
||||||
|
}
|
||||||
|
if (values.length < 2) {
|
||||||
|
context.fillStyle = "#789aa1"; context.font = "11px system-ui"; context.textAlign = "center";
|
||||||
|
context.fillText("Telemetry will appear when the cluster is online", width / 2, height / 2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const plot = (field, color, min, max) => {
|
||||||
|
const spread = max - min;
|
||||||
|
context.beginPath(); context.strokeStyle = color; context.lineWidth = 2; context.lineJoin = "round";
|
||||||
|
values.forEach((item, index) => {
|
||||||
|
const x = pad.left + ((width - pad.left - pad.right) * index / (values.length - 1));
|
||||||
|
const y = pad.top + (height - pad.top - pad.bottom) * (1 - (Number(item[field]) - min) / spread);
|
||||||
|
if (index === 0) context.moveTo(x, y); else context.lineTo(x, y);
|
||||||
|
});
|
||||||
|
context.stroke();
|
||||||
|
};
|
||||||
|
plot("water_temp", "#55e4d7", 10, 30);
|
||||||
|
plot("wave_height", "#55a9ff", 0, 6);
|
||||||
|
context.fillStyle = "#668d95"; context.font = "9px system-ui"; context.textAlign = "left";
|
||||||
|
context.fillText(formatTime(new Date(values[0].recorded_at)), pad.left, height - 5);
|
||||||
|
context.textAlign = "right"; context.fillText(formatTime(new Date(values.at(-1).recorded_at)), width - pad.right, height - 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(date) { return new Intl.DateTimeFormat(undefined, {hour: "2-digit", minute: "2-digit", second: "2-digit"}).format(date); }
|
||||||
|
function numberOrDash(value, digits) { return Number.isFinite(Number(value)) ? Number(value).toFixed(digits) : "--"; }
|
||||||
|
function escapeText(value) { const node = document.createElement("span"); node.textContent = String(value || "Unnamed buoy"); return node.innerHTML; }
|
||||||
|
|
||||||
|
restoreCache();
|
||||||
|
renderConnection();
|
||||||
|
pollHealth();
|
||||||
|
pollData();
|
||||||
|
setInterval(pollHealth, 2000);
|
||||||
|
setInterval(pollData, 5000);
|
||||||
|
setInterval(renderErrors, 1000);
|
||||||
|
setInterval(() => { elements.clock.textContent = formatTime(new Date()); }, 1000);
|
||||||
|
elements.clock.textContent = formatTime(new Date());
|
||||||
|
window.addEventListener("resize", () => { map.invalidateSize(false); drawChart(); });
|
||||||
|
})();
|
||||||
1
apps/web/assets/ne_110m_land.js
Normal file
1
apps/web/assets/ne_110m_land.js
Normal file
File diff suppressed because one or more lines are too long
101
apps/web/index.html
Normal file
101
apps/web/index.html
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="color-scheme" content="dark">
|
||||||
|
<title>Nereus Ocean Telemetry</title>
|
||||||
|
<link rel="stylesheet" href="vendor/leaflet.css">
|
||||||
|
<link rel="stylesheet" href="styles.css?v=20260824-2">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="dashboard">
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="brand">
|
||||||
|
<span class="brand-mark" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 48 48"><path d="M8 29c7-8 14-8 21 0 5 5 9 5 13 1M10 20c5-6 10-6 15 0 4 4 8 4 12 0"/></svg>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Ocean intelligence</p>
|
||||||
|
<h1>Nereus</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="dev-link" href="https://fiwlabs.dev" target="_blank" rel="noopener noreferrer"><span>fiwlabs.dev</span><b aria-hidden="true">↗</b></a>
|
||||||
|
<div class="clock-block">
|
||||||
|
<span>Local time</span>
|
||||||
|
<strong id="clock">--:--:--</strong>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="status-grid" aria-label="System status">
|
||||||
|
<article class="glass status-card version-card">
|
||||||
|
<div class="status-icon">V</div>
|
||||||
|
<div>
|
||||||
|
<p class="card-label">API version</p>
|
||||||
|
<strong id="version-value">offline</strong>
|
||||||
|
<span id="version-note">Cluster unavailable</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="glass status-card health-card">
|
||||||
|
<div>
|
||||||
|
<p class="card-label">Cluster health</p>
|
||||||
|
<strong>Service probes</strong>
|
||||||
|
</div>
|
||||||
|
<div class="probes">
|
||||||
|
<div class="probe">
|
||||||
|
<span id="liveness-dot" class="indicator unknown"></span>
|
||||||
|
<div><b>Liveness</b><time id="liveness-time">No transition yet</time></div>
|
||||||
|
</div>
|
||||||
|
<div class="probe">
|
||||||
|
<span id="readiness-dot" class="indicator unknown"></span>
|
||||||
|
<div><b>Readiness</b><time id="readiness-time">No transition yet</time></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="glass status-card error-card">
|
||||||
|
<div>
|
||||||
|
<p class="card-label">Client-side API errors</p>
|
||||||
|
<strong id="error-total">0</strong>
|
||||||
|
<span>non-2xx responses</span>
|
||||||
|
</div>
|
||||||
|
<div class="error-rate">
|
||||||
|
<span id="error-rate">0.0%</span>
|
||||||
|
<small>rolling 60s</small>
|
||||||
|
<em id="error-state">Waiting for API</em>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="workspace">
|
||||||
|
<article class="glass map-card">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<div><p class="card-label">Active network</p><h2>Buoy positions</h2></div>
|
||||||
|
<div class="panel-meta"><span id="buoy-count">0 buoys</span><span id="data-age">No telemetry</span></div>
|
||||||
|
</div>
|
||||||
|
<div id="map" aria-label="Map of buoy positions"></div>
|
||||||
|
<div id="map-state" class="quiet-state">Waiting for buoy positions</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="glass telemetry-card">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<div><p class="card-label">Latest samples</p><h2>Water conditions</h2></div>
|
||||||
|
<span id="telemetry-state" class="connection-pill offline">Offline</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend" aria-hidden="true"><span class="temperature">Water temperature</span><span class="waves">Wave height</span></div>
|
||||||
|
<div class="chart-wrap"><canvas id="readings-chart" aria-label="Recent water temperature and wave height chart"></canvas></div>
|
||||||
|
<div class="telemetry-summary">
|
||||||
|
<div><span>Temperature</span><strong id="latest-temp">--</strong><small>°C</small></div>
|
||||||
|
<div><span>Wave height</span><strong id="latest-wave">--</strong><small>m</small></div>
|
||||||
|
<div><span>Salinity</span><strong id="latest-salinity">--</strong><small>PSU</small></div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="vendor/leaflet.js"></script>
|
||||||
|
<script src="assets/ne_110m_land.js"></script>
|
||||||
|
<script src="app.js?v=20260824-3"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
103
apps/web/styles.css
Normal file
103
apps/web/styles.css
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
background: #07151d;
|
||||||
|
color: #d8e3e3;
|
||||||
|
--ink: #07151d;
|
||||||
|
--panel: #0b202a;
|
||||||
|
--panel-deep: #081a23;
|
||||||
|
--rule: #27434b;
|
||||||
|
--rule-strong: #3c6269;
|
||||||
|
--muted: #7f999d;
|
||||||
|
--signal: #52c8bc;
|
||||||
|
--blue: #5aa9d6;
|
||||||
|
--warn: #d6a85a;
|
||||||
|
--bad: #db7770;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
|
||||||
|
body {
|
||||||
|
min-width: 1280px;
|
||||||
|
background-color: var(--ink);
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(85, 126, 134, .045) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(85, 126, 134, .045) 1px, transparent 1px);
|
||||||
|
background-size: 32px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ocean-background { display: none; }
|
||||||
|
.dashboard { height: 100vh; padding: 0 26px 24px; display: grid; grid-template-rows: 74px 102px minmax(0, 1fr); gap: 12px; }
|
||||||
|
.topbar { position: relative; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--rule-strong); }
|
||||||
|
.dev-link { position: absolute; top: 50%; left: 50%; display: flex; align-items: center; gap: 9px; padding: 9px 13px 8px 15px; transform: translate(-50%, -50%); border: 1px solid var(--rule-strong); color: #9cb3b6; background: #0a1c25; box-shadow: inset 2px 0 0 var(--signal); font: 9px ui-monospace, SFMono-Regular, Consolas, monospace; letter-spacing: .16em; text-decoration: none; text-transform: uppercase; transition: color .2s ease, border-color .2s ease, background-color .2s ease; }
|
||||||
|
.dev-link b { color: var(--signal); font-size: 13px; font-weight: 400; line-height: .7; }
|
||||||
|
.dev-link:hover, .dev-link:focus-visible { border-color: var(--signal); color: #e0f5f2; background: #102b34; outline: none; }
|
||||||
|
.brand { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.brand-mark { width: 36px; height: 36px; display: grid; place-items: center; border: 1px solid var(--rule-strong); }
|
||||||
|
.brand-mark svg { width: 24px; fill: none; stroke: var(--signal); stroke-width: 1.7; stroke-linecap: square; }
|
||||||
|
.eyebrow, .card-label { margin: 0 0 4px; color: var(--muted); font: 9px ui-monospace, SFMono-Regular, Consolas, monospace; letter-spacing: .15em; text-transform: uppercase; }
|
||||||
|
h1 { margin: 0; font-size: 22px; line-height: 1; letter-spacing: .08em; text-transform: uppercase; }
|
||||||
|
.clock-block { text-align: right; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }
|
||||||
|
.clock-block span { display: block; color: var(--muted); font-size: 8px; letter-spacing: .14em; text-transform: uppercase; }
|
||||||
|
.clock-block strong { font-size: 16px; font-weight: 500; font-variant-numeric: tabular-nums; }
|
||||||
|
|
||||||
|
.glass { border: 1px solid var(--rule); background: var(--panel); box-shadow: none; backdrop-filter: none; }
|
||||||
|
.status-grid { display: grid; grid-template-columns: .72fr 1.48fr 1.1fr; gap: 12px; }
|
||||||
|
.status-card { position: relative; min-width: 0; padding: 14px 16px; display: flex; align-items: center; justify-content: space-between; border-radius: 0; }
|
||||||
|
.status-card::before { content: ""; position: absolute; top: -1px; left: -1px; width: 54px; height: 2px; background: var(--signal); }
|
||||||
|
.status-card strong { display: block; font: 600 16px ui-monospace, SFMono-Regular, Consolas, monospace; }
|
||||||
|
.status-card > div > span:not(.indicator) { color: var(--muted); font-size: 10px; }
|
||||||
|
.status-icon { display: none; }
|
||||||
|
.version-card { justify-content: flex-start; border-left-color: var(--rule-strong); }
|
||||||
|
.version-card strong { color: var(--bad); text-transform: uppercase; }
|
||||||
|
.probes { display: flex; align-items: center; gap: 34px; }
|
||||||
|
.probe { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.probe b { display: block; font: 600 11px ui-monospace, SFMono-Regular, Consolas, monospace; text-transform: uppercase; }
|
||||||
|
.probe time { display: block; width: 132px; margin-top: 4px; color: var(--muted); font: 8px ui-monospace, SFMono-Regular, Consolas, monospace; }
|
||||||
|
.indicator { width: 8px; height: 8px; border-radius: 50%; background: #64747a; transition: background-color .2s ease; }
|
||||||
|
.indicator.ok { background: var(--signal); box-shadow: none; }
|
||||||
|
.indicator.bad { background: var(--bad); box-shadow: none; }
|
||||||
|
.error-card strong { font-size: 27px; font-weight: 500; font-variant-numeric: tabular-nums; }
|
||||||
|
.error-rate { min-width: 144px; padding-left: 18px; border-left: 1px solid var(--rule); text-align: left; }
|
||||||
|
.error-rate > span { display: block; color: var(--signal); font: 500 20px ui-monospace, SFMono-Regular, Consolas, monospace; font-variant-numeric: tabular-nums; }
|
||||||
|
.error-rate small { color: var(--muted); font: 8px ui-monospace, SFMono-Regular, Consolas, monospace; text-transform: uppercase; }
|
||||||
|
.error-rate em { display: block; margin-top: 7px; color: #9cb0b2; font-size: 9px; font-style: normal; }
|
||||||
|
|
||||||
|
.workspace { min-height: 0; display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(400px, .85fr); gap: 12px; }
|
||||||
|
.map-card, .telemetry-card { position: relative; min-height: 0; overflow: hidden; border-radius: 0; }
|
||||||
|
.panel-heading { height: 66px; padding: 14px 16px 11px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--rule); background: var(--panel-deep); }
|
||||||
|
.panel-heading h2 { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: .03em; text-transform: uppercase; }
|
||||||
|
.panel-meta { display: flex; gap: 18px; }
|
||||||
|
.panel-meta span, .connection-pill { padding: 0; border: 0; border-radius: 0; color: var(--muted); background: transparent; font: 9px ui-monospace, SFMono-Regular, Consolas, monospace; text-transform: uppercase; }
|
||||||
|
.panel-meta span + span { padding-left: 18px; border-left: 1px solid var(--rule); }
|
||||||
|
#map { height: calc(100% - 66px); background: #091b24; transition: opacity .2s ease; }
|
||||||
|
#map::before { content: ""; position: absolute; inset: 0; z-index: 200; pointer-events: none; background-image: linear-gradient(rgba(96,137,143,.1) 1px, transparent 1px), linear-gradient(90deg, rgba(96,137,143,.1) 1px, transparent 1px); background-size: 64px 64px; }
|
||||||
|
.map-card.stale #map { filter: saturate(.3) brightness(.72); }
|
||||||
|
.quiet-state { position: absolute; left: 16px; bottom: 14px; z-index: 500; padding: 6px 9px; border: 1px solid var(--rule-strong); color: #9db0b2; background: rgba(7, 21, 29, .92); font: 8px ui-monospace, SFMono-Regular, Consolas, monospace; letter-spacing: .08em; text-transform: uppercase; pointer-events: none; }
|
||||||
|
.quiet-state.hidden { opacity: 0; }
|
||||||
|
.leaflet-container { font-family: Arial, Helvetica, sans-serif; }
|
||||||
|
.leaflet-control-attribution { color: #6b8589 !important; background: rgba(7,21,29,.84) !important; font-size: 8px !important; }
|
||||||
|
.leaflet-control-attribution a { color: #83a5a8 !important; }
|
||||||
|
.landmass { fill: #183640; fill-opacity: .72; stroke: #41636a; stroke-width: .7; }
|
||||||
|
.buoy-marker { width: 9px; height: 9px; border: 1px solid #d9fffa; border-radius: 50%; background: var(--signal); animation: pulse 2s steps(2, end) infinite; }
|
||||||
|
@keyframes pulse { 50% { outline: 4px solid rgba(82, 200, 188, .22); } }
|
||||||
|
|
||||||
|
.telemetry-card { display: grid; grid-template-rows: 66px 28px minmax(0, 1fr) 72px; }
|
||||||
|
.connection-pill::before { content: "●"; margin-right: 6px; }
|
||||||
|
.connection-pill.online { color: var(--signal); }
|
||||||
|
.connection-pill.degraded { color: var(--warn); }
|
||||||
|
.connection-pill.offline { color: var(--bad); }
|
||||||
|
.legend { display: flex; align-items: center; gap: 24px; padding: 0 16px; border-bottom: 1px solid rgba(39,67,75,.65); color: var(--muted); font: 8px ui-monospace, SFMono-Regular, Consolas, monospace; text-transform: uppercase; }
|
||||||
|
.legend span::before { content: ""; display: inline-block; width: 14px; height: 2px; margin: 0 7px 2px 0; background: var(--signal); }
|
||||||
|
.legend .waves::before { background: var(--blue); }
|
||||||
|
.chart-wrap { min-height: 0; padding: 8px 10px 0; transition: opacity .2s ease; }
|
||||||
|
.telemetry-card.stale .chart-wrap { opacity: .35; }
|
||||||
|
#readings-chart { width: 100%; height: 100%; }
|
||||||
|
.telemetry-summary { margin: 0; display: grid; grid-template-columns: repeat(3, 1fr); overflow: hidden; border-top: 1px solid var(--rule); background: var(--panel-deep); }
|
||||||
|
.telemetry-summary div { padding: 11px 13px; border-right: 1px solid var(--rule); }
|
||||||
|
.telemetry-summary div:last-child { border: 0; }
|
||||||
|
.telemetry-summary span { display: block; color: var(--muted); font: 8px ui-monospace, SFMono-Regular, Consolas, monospace; text-transform: uppercase; }
|
||||||
|
.telemetry-summary strong { display: inline-block; margin-top: 5px; font: 500 17px ui-monospace, SFMono-Regular, Consolas, monospace; font-variant-numeric: tabular-nums; }
|
||||||
|
.telemetry-summary small { margin-left: 3px; color: var(--muted); font: 8px ui-monospace, SFMono-Regular, Consolas, monospace; }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; } }
|
||||||
26
apps/web/vendor/LEAFLET-LICENSE
vendored
Normal file
26
apps/web/vendor/LEAFLET-LICENSE
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
BSD 2-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2010-2023, Volodymyr Agafonkin
|
||||||
|
Copyright (c) 2010-2011, CloudMade
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
661
apps/web/vendor/leaflet.css
vendored
Normal file
661
apps/web/vendor/leaflet.css
vendored
Normal file
|
|
@ -0,0 +1,661 @@
|
||||||
|
/* required styles */
|
||||||
|
|
||||||
|
.leaflet-pane,
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-tile-container,
|
||||||
|
.leaflet-pane > svg,
|
||||||
|
.leaflet-pane > canvas,
|
||||||
|
.leaflet-zoom-box,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-layer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
/* Prevents IE11 from highlighting tiles in blue */
|
||||||
|
.leaflet-tile::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||||
|
.leaflet-safari .leaflet-tile {
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
}
|
||||||
|
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||||
|
.leaflet-safari .leaflet-tile-container {
|
||||||
|
width: 1600px;
|
||||||
|
height: 1600px;
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||||
|
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||||
|
.leaflet-container .leaflet-overlay-pane svg {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
.leaflet-container .leaflet-marker-pane img,
|
||||||
|
.leaflet-container .leaflet-shadow-pane img,
|
||||||
|
.leaflet-container .leaflet-tile-pane img,
|
||||||
|
.leaflet-container img.leaflet-image-layer,
|
||||||
|
.leaflet-container .leaflet-tile {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
width: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container img.leaflet-tile {
|
||||||
|
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
||||||
|
mix-blend-mode: plus-lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: pan-x pan-y;
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag {
|
||||||
|
-ms-touch-action: pinch-zoom;
|
||||||
|
/* Fallback for FF which doesn't support pinch-zoom */
|
||||||
|
touch-action: none;
|
||||||
|
touch-action: pinch-zoom;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tile {
|
||||||
|
filter: inherit;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile-loaded {
|
||||||
|
visibility: inherit;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 800;
|
||||||
|
}
|
||||||
|
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||||
|
.leaflet-overlay-pane svg {
|
||||||
|
-moz-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-pane { z-index: 400; }
|
||||||
|
|
||||||
|
.leaflet-tile-pane { z-index: 200; }
|
||||||
|
.leaflet-overlay-pane { z-index: 400; }
|
||||||
|
.leaflet-shadow-pane { z-index: 500; }
|
||||||
|
.leaflet-marker-pane { z-index: 600; }
|
||||||
|
.leaflet-tooltip-pane { z-index: 650; }
|
||||||
|
.leaflet-popup-pane { z-index: 700; }
|
||||||
|
|
||||||
|
.leaflet-map-pane canvas { z-index: 100; }
|
||||||
|
.leaflet-map-pane svg { z-index: 200; }
|
||||||
|
|
||||||
|
.leaflet-vml-shape {
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
.lvml {
|
||||||
|
behavior: url(#default#VML);
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* control positioning */
|
||||||
|
|
||||||
|
.leaflet-control {
|
||||||
|
position: relative;
|
||||||
|
z-index: 800;
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-top,
|
||||||
|
.leaflet-bottom {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.leaflet-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.leaflet-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control {
|
||||||
|
float: left;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.leaflet-top .leaflet-control {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* zoom and fade animations */
|
||||||
|
|
||||||
|
.leaflet-fade-anim .leaflet-popup {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: opacity 0.2s linear;
|
||||||
|
-moz-transition: opacity 0.2s linear;
|
||||||
|
transition: opacity 0.2s linear;
|
||||||
|
}
|
||||||
|
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-animated {
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
-ms-transform-origin: 0 0;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
svg.leaflet-zoom-animated {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||||
|
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
}
|
||||||
|
.leaflet-zoom-anim .leaflet-tile,
|
||||||
|
.leaflet-pan-anim .leaflet-tile {
|
||||||
|
-webkit-transition: none;
|
||||||
|
-moz-transition: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* cursors */
|
||||||
|
|
||||||
|
.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.leaflet-grab {
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.leaflet-crosshair,
|
||||||
|
.leaflet-crosshair .leaflet-interactive {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.leaflet-popup-pane,
|
||||||
|
.leaflet-control {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
.leaflet-dragging .leaflet-grab,
|
||||||
|
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||||
|
.leaflet-dragging .leaflet-marker-draggable {
|
||||||
|
cursor: move;
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
cursor: -moz-grabbing;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* marker & overlays interactivity */
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-pane > svg path,
|
||||||
|
.leaflet-tile-container {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-marker-icon.leaflet-interactive,
|
||||||
|
.leaflet-image-layer.leaflet-interactive,
|
||||||
|
.leaflet-pane > svg path.leaflet-interactive,
|
||||||
|
svg.leaflet-image-layer.leaflet-interactive path {
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* visual tweaks */
|
||||||
|
|
||||||
|
.leaflet-container {
|
||||||
|
background: #ddd;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
color: #0078A8;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
border: 2px dotted #38f;
|
||||||
|
background: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general typography */
|
||||||
|
.leaflet-container {
|
||||||
|
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general toolbar styles */
|
||||||
|
|
||||||
|
.leaflet-bar {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
line-height: 26px;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.leaflet-bar a,
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-position: 50% 50%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:hover,
|
||||||
|
.leaflet-bar a:focus {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.leaflet-bar a.leaflet-disabled {
|
||||||
|
cursor: default;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-bar a {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* zoom control */
|
||||||
|
|
||||||
|
.leaflet-control-zoom-in,
|
||||||
|
.leaflet-control-zoom-out {
|
||||||
|
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||||
|
text-indent: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* layers control */
|
||||||
|
|
||||||
|
.leaflet-control-layers {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers.png);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
.leaflet-retina .leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers-2x.png);
|
||||||
|
background-size: 26px 26px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers-toggle {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers .leaflet-control-layers-list,
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded {
|
||||||
|
padding: 6px 10px 6px 6px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-scrollbar {
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-selector {
|
||||||
|
margin-top: 2px;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-size: 1.08333em;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-separator {
|
||||||
|
height: 0;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin: 5px -10px 5px -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default icon URLs */
|
||||||
|
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||||
|
background-image: url(images/marker-icon.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* attribution and scale controls */
|
||||||
|
|
||||||
|
.leaflet-container .leaflet-control-attribution {
|
||||||
|
background: #fff;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution,
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
padding: 0 5px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a:hover,
|
||||||
|
.leaflet-control-attribution a:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.leaflet-attribution-flag {
|
||||||
|
display: inline !important;
|
||||||
|
vertical-align: baseline !important;
|
||||||
|
width: 1em;
|
||||||
|
height: 0.6669em;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control-scale {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control-scale {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
border: 2px solid #777;
|
||||||
|
border-top: none;
|
||||||
|
line-height: 1.1;
|
||||||
|
padding: 2px 5px 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
text-shadow: 1px 1px #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child) {
|
||||||
|
border-top: 2px solid #777;
|
||||||
|
border-bottom: none;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||||
|
border-bottom: 2px solid #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-attribution,
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
border: 2px solid rgba(0,0,0,0.2);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* popup */
|
||||||
|
|
||||||
|
.leaflet-popup {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper {
|
||||||
|
padding: 1px;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content {
|
||||||
|
margin: 13px 24px 13px 20px;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-size: 13px;
|
||||||
|
font-size: 1.08333em;
|
||||||
|
min-height: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content p {
|
||||||
|
margin: 17px 0;
|
||||||
|
margin: 1.3em 0;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip-container {
|
||||||
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-left: -20px;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
padding: 1px;
|
||||||
|
|
||||||
|
margin: -10px auto 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
-moz-transform: rotate(45deg);
|
||||||
|
-ms-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
background: white;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font: 16px/24px Tahoma, Verdana, sans-serif;
|
||||||
|
color: #757575;
|
||||||
|
text-decoration: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||||
|
color: #585858;
|
||||||
|
}
|
||||||
|
.leaflet-popup-scrolled {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||||
|
-ms-zoom: 1;
|
||||||
|
}
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
width: 24px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||||
|
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-control-zoom,
|
||||||
|
.leaflet-oldie .leaflet-control-layers,
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
border: 1px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* div icon */
|
||||||
|
|
||||||
|
.leaflet-div-icon {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Tooltip */
|
||||||
|
/* Base styles for the element that has a tooltip */
|
||||||
|
.leaflet-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #222;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tooltip.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before,
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
border: 6px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Directions */
|
||||||
|
|
||||||
|
.leaflet-tooltip-bottom {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top {
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
bottom: 0;
|
||||||
|
margin-bottom: -12px;
|
||||||
|
border-top-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before {
|
||||||
|
top: 0;
|
||||||
|
margin-top: -12px;
|
||||||
|
margin-left: -6px;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left {
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before {
|
||||||
|
right: 0;
|
||||||
|
margin-right: -12px;
|
||||||
|
border-left-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
left: 0;
|
||||||
|
margin-left: -12px;
|
||||||
|
border-right-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Printing */
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
/* Prevent printers from removing background-images of controls. */
|
||||||
|
.leaflet-control {
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
apps/web/vendor/leaflet.js
vendored
Normal file
6
apps/web/vendor/leaflet.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue