feat: add collapsible map controls

This commit is contained in:
2026-07-21 03:02:49 +01:00
parent 4ddb7fc2d8
commit 41ded45b09
5 changed files with 171 additions and 10 deletions
+1
View File
@@ -9,6 +9,7 @@ COPY server.js ./
COPY index.html ./ COPY index.html ./
COPY app.js ./ COPY app.js ./
COPY styles.css ./ COPY styles.css ./
COPY assets ./assets
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
ENV PORT=4173 ENV PORT=4173
+21
View File
@@ -4,6 +4,10 @@ const API_WMS = "/api/wms";
const PARCEL_LAYER_NAME = "xpro:parcelas"; const PARCEL_LAYER_NAME = "xpro:parcelas";
const els = { const els = {
appShell: document.querySelector(".app-shell"),
sidebar: document.getElementById("sidebar"),
sidebarOpen: document.getElementById("sidebar-open"),
sidebarClose: document.getElementById("sidebar-close"),
status: document.getElementById("status"), status: document.getElementById("status"),
layerList: document.getElementById("layer-list"), layerList: document.getElementById("layer-list"),
layerCount: document.getElementById("layer-count"), layerCount: document.getElementById("layer-count"),
@@ -77,9 +81,26 @@ let selectedLayers = [];
els.parcelSearchForm.addEventListener("submit", searchParcela); els.parcelSearchForm.addEventListener("submit", searchParcela);
els.backgroundSelect.addEventListener("change", syncBackground); els.backgroundSelect.addEventListener("change", syncBackground);
els.sidebarOpen.addEventListener("click", () => setSidebarCollapsed(false));
els.sidebarClose.addEventListener("click", () => setSidebarCollapsed(true));
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !els.appShell.classList.contains("sidebar-is-collapsed")) {
setSidebarCollapsed(true);
}
});
loadLayers(); loadLayers();
function setSidebarCollapsed(collapsed) {
els.appShell.classList.toggle("sidebar-is-collapsed", collapsed);
els.sidebar.inert = collapsed;
els.sidebarOpen.setAttribute("aria-expanded", String(!collapsed));
if (collapsed) {
els.sidebarOpen.focus();
}
}
async function searchParcela(event) { async function searchParcela(event) {
event.preventDefault(); event.preventDefault();
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 66.295753 33.036644">
<g transform="translate(-15.909682 -167.46353)">
<path fill="#1574c3" d="m 51.662106,186.67656 h 1.857378 v -3.80966 h 0.59653 c 1.423537,0 2.074297,-0.10846 2.630155,-0.4474 0.81345,-0.51518 1.315077,-1.51844 1.315077,-2.67082 0,-1.15239 -0.48807,-2.10142 -1.35575,-2.63016 -0.569415,-0.33894 -1.24729,-0.46095 -2.562367,-0.46095 h -2.481023 z m 1.857378,-5.58569 v -2.65727 h 0.623645 c 0.610087,0 0.894795,0.0271 1.179502,0.0949 0.5423,0.14913 0.86768,0.62365 0.86768,1.24729 0,0.48807 -0.21692,0.86768 -0.610087,1.0846 -0.284708,0.14913 -0.786335,0.23048 -1.51844,0.23048 z m 5.500524,5.58569 h 1.803148 v -4.17571 c -0.04067,-1.11172 0.528742,-1.73536 1.613342,-1.77603 v -1.73536 h -0.135575 c -0.772777,0 -1.152387,0.21692 -1.6269,0.90835 v -0.73211 h -1.654015 z m 7.683272,-7.6871 c -2.182758,0 -3.972348,1.77603 -3.972348,3.93167 0,2.1692 1.78959,3.93168 3.985905,3.93168 2.182758,0 3.999463,-1.76248 3.999463,-3.87745 0,-2.23699 -1.748918,-3.9859 -4.01302,-3.9859 z m 0.01356,1.65401 c 1.206618,0 2.182758,1.01681 2.182758,2.27766 0,1.26085 -0.97614,2.27766 -2.1692,2.27766 -1.220175,0 -2.182758,-1.01681 -2.182758,-2.30477 0,-1.23374 0.97614,-2.25055 2.1692,-2.25055 z" transform="matrix(1.729104 0 0 1.729104 -47.763707 -130.17409)"/>
<path fill="#f8a621" d="m 41.157585,186.71507 h 2.209873 l 2.33189,-3.45716 2.399677,3.45716 h 2.155643 l -3.47072,-4.98916 3.47072,-5.02983 h -2.155643 l -2.399677,3.49784 -2.33189,-3.49784 h -2.209873 l 3.443605,5.02983 z" transform="matrix(1.729104 0 0 1.729104 -47.763707 -130.17409)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+21 -2
View File
@@ -9,9 +9,28 @@
</head> </head>
<body> <body>
<div class="app-shell"> <div class="app-shell">
<aside class="sidebar"> <button
id="sidebar-open"
class="sidebar-toggle sidebar-open"
type="button"
aria-controls="sidebar"
aria-expanded="true"
>
<span aria-hidden="true"></span>
<span class="visually-hidden">Open map controls</span>
</button>
<aside id="sidebar" class="sidebar" aria-label="Map controls">
<button id="sidebar-close" class="sidebar-toggle sidebar-close" type="button">
<span aria-hidden="true">×</span>
<span class="visually-hidden">Close map controls</span>
</button>
<div class="brand"> <div class="brand">
<h1>XPro Map Visualizer</h1> <h1>
<img class="brand-logo" src="./assets/brand/xpro-logo-simple.svg" alt="XPro" />
<span class="visually-hidden">XPro</span>
</h1>
<p class="brand-subtitle">Map Visualizer</p>
</div> </div>
<section class="panel"> <section class="panel">
+122 -8
View File
@@ -1,7 +1,7 @@
:root { :root {
color-scheme: light; color-scheme: light;
--bg: #f3efe3; --bg: #f3efe3;
--panel: rgba(253, 249, 240, 0.92); --panel: rgba(253, 249, 240, 0.75);
--panel-border: rgba(92, 73, 49, 0.18); --panel-border: rgba(92, 73, 49, 0.18);
--ink: #1f2a2e; --ink: #1f2a2e;
--muted: #5f6b6f; --muted: #5f6b6f;
@@ -30,10 +30,8 @@ body {
} }
.app-shell { .app-shell {
display: grid; position: relative;
grid-template-columns: minmax(320px, 380px) 1fr;
min-height: 100vh; min-height: 100vh;
gap: 24px;
padding: 24px; padding: 24px;
} }
@@ -43,9 +41,70 @@ body {
} }
.sidebar { .sidebar {
position: fixed;
z-index: 10;
top: 24px;
bottom: 24px;
left: 24px;
width: min(266px, calc(100vw - 48px));
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
overflow-y: auto;
padding: 8px;
transition: transform 240ms ease;
backdrop-filter: blur(3px);
}
.sidebar-toggle {
position: fixed;
z-index: 11;
display: grid;
width: 42px;
height: 42px;
padding: 0;
place-items: center;
border: 1px solid var(--panel-border);
border-radius: 14px;
background: var(--panel);
color: var(--ink);
font-size: 1.65rem;
font-weight: 500;
line-height: 1;
box-shadow: var(--shadow);
}
.sidebar-toggle:hover {
transform: translateY(-1px);
}
.sidebar-open {
top: 32px;
left: 32px;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 180ms ease;
}
.sidebar-close {
position: absolute;
top: 18px;
right: 18px;
}
.sidebar-close span[aria-hidden="true"] {
transform: translateY(-1px);
}
.app-shell.sidebar-is-collapsed .sidebar {
transform: translateX(calc(-100% - 24px));
}
.app-shell.sidebar-is-collapsed .sidebar-open {
opacity: 1;
visibility: visible;
pointer-events: auto;
} }
.brand, .brand,
@@ -61,6 +120,32 @@ body {
padding: 24px; padding: 24px;
} }
.brand-logo {
display: block;
width: min(100%, 178px);
height: auto;
}
.brand-subtitle {
margin-top: 8px;
color: var(--muted);
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.04em;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
h1, h1,
h2, h2,
p { p {
@@ -210,11 +295,12 @@ button:disabled {
} }
.map-wrap { .map-wrap {
position: relative; position: fixed;
z-index: 0;
inset: 24px;
overflow: hidden; overflow: hidden;
border-radius: 32px; border-radius: 32px;
box-shadow: var(--shadow); box-shadow: var(--shadow);
min-height: 70vh;
} }
#map { #map {
@@ -222,12 +308,40 @@ button:disabled {
inset: 0; inset: 0;
} }
.map-wrap .ol-zoom {
top: 16px;
right: 16px;
left: auto;
}
@media (max-width: 920px) { @media (max-width: 920px) {
.app-shell { .app-shell {
grid-template-columns: 1fr; padding: 16px;
} }
.map-wrap { .map-wrap {
min-height: 62vh; inset: 16px;
border-radius: 24px;
}
.map-wrap .ol-zoom {
top: 12px;
right: 12px;
}
.sidebar {
top: 16px;
bottom: 16px;
left: 16px;
width: min(266px, calc(100vw - 32px));
}
.sidebar-open {
top: 24px;
left: 24px;
}
.app-shell.sidebar-is-collapsed .sidebar {
transform: translateX(calc(-100% - 16px));
} }
} }