Phones: the legend sheet sits at the map's bottom, and the engine's credit goes to the top

Every bottom placement of the engine's credit failed a reader on a phone: under
the open sheet it was covered, lifted above the sheet it floated mid-map, and
in a strip below the sheet it pushed the legend off the map's bottom edge. On a
phone both engines' bottom corners now move to the top corner on the start
side, opposite the map's own control column (left; right in RTL), and the sheet
sits flush at the map's bottom, starting beside the round menu button
(--map-fab-beside) so the button covers none of it. The move is !important so a
plugin's own lift of its bottom corners cannot push the credit off the top.
Desktop is unchanged.
This commit is contained in:
Doxa Maps 2026-09-30 07:03:28 +00:00
parent 037dfad3bc
commit 7978e5e41a

View file

@ -21,21 +21,21 @@ useShadowStyles(`
the SAME component desktop uses, with all the same default CSS. Mobile
no longer overrides any .lrg-* rule — the bottom sheet is purely
mobile chrome (pull tab, collapse/expand caret, touch drag). ── */
/* bottom = the sheet's height (it shows only while the sheet is open, whose height now fits its
content) + the credit strip under the sheet + a 16px gap. */
.floating-mini-image{position:absolute;inset-inline-start:16px;bottom:calc(var(--legend-sheet-h,30%) + var(--legend-credit-strip,0px) + 16px);z-index:999;pointer-events:auto;cursor:pointer;}
/* bottom = the sheet's height (it shows only while the sheet is open, whose height fits its content)
+ a 16px gap. */
.floating-mini-image{position:absolute;inset-inline-start:16px;bottom:calc(var(--legend-sheet-h,30%) + 16px);z-index:999;pointer-events:auto;cursor:pointer;}
.mini-image{width:120px;height:150px;object-fit:cover;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.25);display:block;}
/* Containment root: clamps absolute children to the map area. */
.legend-sheet-root{position:absolute;inset:0;pointer-events:none;overflow:hidden;}
.legend-sheet-root>*{pointer-events:auto;}
/* ★ THE CREDIT STRIP (mobile pass 2026-09-30) REPLACES THE CAPPED LIFT BELOW. The corners used to rise by
the sheet height capped at 48px, so an OPEN sheet (30%) still covered the credit and the Mapbox
wordmark (measured at 390 on Simple Research: the top element at both was the sheet's content),
while an uncapped lift would float the credit into the middle of the map. Now the corners stay at the
map's bottom edge and the SHEET sits above them: --legend-credit-strip is measured from the real boxes
of every visible bottom control (both engines' wordmark and credit, and our own credit line), so the
credit is visible whatever the sheet does, and never mid-map. The notes below are history. */
/* ★ ON A PHONE THE ENGINE'S CREDIT GOES TO THE TOP, AND THE SHEET SITS AT THE BOTTOM (mobile pass
2026-09-30). Every bottom placement failed a reader: under the open sheet the credit and the Mapbox
wordmark were covered; lifted above it they floated mid-map; in a strip below it they pushed the legend
off the map's bottom edge. So on a phone both engines' bottom corners move to the TOP corner on the
START side, opposite the map's own control column (left; right in RTL), compact and always visible,
and the sheet sits flush at the map's bottom, beside the round menu button (see the @media block after
the desktop rules). The desktop rules below are unchanged; the notes in them are history. */
/* ── Attribution stays visible (bug 2026-09-16) ──
The sheet is a bottom overlay, so the engine's bottom-left logo / bottom-right
attribution corners ended up UNDER it in every state (even the 48px collapsed
@ -72,6 +72,13 @@ useShadowStyles(`
.mapboxgl-ctrl-bottom-right:hover,.maplibregl-ctrl-bottom-right:hover{opacity:1;}
.mapboxgl-ctrl-bottom-right .mapboxgl-ctrl,.maplibregl-ctrl-bottom-right .maplibregl-ctrl{margin:0 6px 6px 0;}
.mapboxgl-ctrl-bottom-right .mapboxgl-ctrl-attrib.mapboxgl-compact{margin-right:6px;}
/* !important on the move: a plugin may lift its own bottom corners by the round button's height with a
plain rule of the same specificity (a translateY on the corner container), and up here that lift would
push the credit off the top of the map. The corners are not at the bottom on a phone any more. */
@media (max-width:767.98px){
.mapboxgl-ctrl-bottom-left,.mapboxgl-ctrl-bottom-right,.maplibregl-ctrl-bottom-left,.maplibregl-ctrl-bottom-right{top:0 !important;bottom:auto !important;inset-inline-start:0 !important;inset-inline-end:auto !important;transform:none !important;justify-content:flex-start;}
.mapboxgl-ctrl-bottom-left .mapboxgl-ctrl,.mapboxgl-ctrl-bottom-right .mapboxgl-ctrl,.maplibregl-ctrl-bottom-left .maplibregl-ctrl,.maplibregl-ctrl-bottom-right .maplibregl-ctrl{margin:8px 0 0;margin-inline:8px 0;}
}
/* In the sheet the legend's rows fill the full width: the clear "x" column does not reserve
space on the right — it floats over the row's right edge only when a row is selected. */
.legend-mobile-sheet .stl-row{position:relative;}
@ -84,7 +91,7 @@ useShadowStyles(`
max-height reads --legend-sheet-top-reserve, set on this element from the
TOP_RESERVE_PX constant in the script — the SAME number the touch-drag clamp
uses, so CSS and JS can never disagree about how tall the sheet may get. */
.legend-mobile-sheet{position:absolute;bottom:var(--legend-credit-strip,0px);left:0;right:0;z-index:1000;background:white;border-top-left-radius:var(--map-radius,16px);border-top-right-radius:var(--map-radius,16px);box-shadow:0 -4px 20px rgba(0,0,0,0.2);transition:height 0.3s ease-out;display:flex;flex-direction:column;overflow:hidden;max-height:calc(100% - var(--legend-sheet-top-reserve,80px) - var(--legend-credit-strip,0px));}
.legend-mobile-sheet{position:absolute;bottom:0;inset-inline-start:var(--map-fab-beside,0px);inset-inline-end:0;z-index:1000;background:white;border-top-left-radius:var(--map-radius,16px);border-top-right-radius:var(--map-radius,16px);box-shadow:0 -4px 20px rgba(0,0,0,0.2);transition:height 0.3s ease-out;display:flex;flex-direction:column;overflow:hidden;max-height:calc(100% - var(--legend-sheet-top-reserve,80px));}
/* While a finger is resizing the sheet the height must be applied INSTANTLY —
with the 0.3s transition left on, the measured height lagged the finger by
14-30px (mobile-sheet-probe baseline 2026-09-16) and the release logic read a
@ -95,7 +102,7 @@ useShadowStyles(`
empty sheet; a long tree scrolled inside one). A change to or from the fitted height snaps: a height
transition cannot run to auto, and interpolate-size:allow-keywords left the sheet stuck at the 48px
it started from (measured in Chrome 149, collapsed to open on Simple Research). */
.legend-mobile-sheet.fit{max-height:min(45%, calc(100% - var(--legend-sheet-top-reserve,80px) - var(--legend-credit-strip,0px)));}
.legend-mobile-sheet.fit{max-height:min(45%, calc(100% - var(--legend-sheet-top-reserve,80px)));}
/* Drag strip — thin top overlay holding the pull-tab handle + touch
gestures. z-index puts it above content. */
@ -163,7 +170,7 @@ useShadowStyles(`
The layout publishes --map-fab-clearance only while that button is rendered, so this reserves
space ONLY when there is something to clear: no circle, no wasted whitespace, and the padding
is additive so nothing that works today moves. */
.legend-content{flex:1;min-height:0;display:flex;flex-direction:column;overflow-y:auto;overflow-x:hidden;padding:12px 0 calc(12px + var(--map-fab-clearance, 0px));-webkit-overflow-scrolling:touch;overscroll-behavior:contain;}
.legend-content{flex:1;min-height:0;display:flex;flex-direction:column;overflow-y:auto;overflow-x:hidden;padding:12px 0;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;}
/* overscroll-behavior:contain on the LIST, not on the pane: a fling that reaches the list's end stops
there instead of scrolling the page behind the map (and, on a phone, collapsing its URL bar). */
/* Constrain only the table/tree shells to the .legend-content viewport so
@ -682,7 +689,7 @@ function onSheetTouchStart(event) {
drag.lastH = drag.startH
drag.startState = legendState.value
drag.containerH = sheet.parentElement?.offsetHeight || window.innerHeight
drag.maxH = drag.containerH - TOP_RESERVE_PX - creditStripPx
drag.maxH = drag.containerH - TOP_RESERVE_PX
}
function onSheetTouchMove(event) {
@ -731,50 +738,6 @@ function snapTo(state) {
else uiStore.openLegend()
}
// ── Credit strip → host CSS var: the sheet sits ABOVE the engine's bottom controls ───
// The strip is the distance from the map area's bottom edge to the top of the tallest visible bottom
// control (each control's own box, so the corner container's margins count and an empty corner counts
// nothing). The engines add and resize these after load (the attribution loads its sources, compacts,
// expands on tap), so it is re-measured on settle timers and whenever an observed control resizes.
const STRIP_VAR = '--legend-credit-strip'
const CORNERS = '.maplibregl-ctrl-bottom-left, .maplibregl-ctrl-bottom-right, .mapboxgl-ctrl-bottom-left, .mapboxgl-ctrl-bottom-right'
let creditStripPx = 0
let _stripRO = null
let _stripTimers = []
const _stripObserved = new Set()
function measureCreditStrip() {
const sheet = sheetRef.value
const area = sheet && sheet.parentElement
const root = sheet && sheet.getRootNode()
if (!area || !root || !root.querySelectorAll || !_sheetVarTarget) return
const bottom = area.getBoundingClientRect().bottom
let strip = 0
for (const corner of root.querySelectorAll(CORNERS)) {
for (const ctl of corner.children) {
const r = ctl.getBoundingClientRect()
if (r.width > 0 && r.height > 0) strip = Math.max(strip, bottom - r.top)
if (_stripRO && !_stripObserved.has(ctl)) { _stripRO.observe(ctl); _stripObserved.add(ctl) }
}
}
const px = strip > 0 ? Math.ceil(strip) + 2 : 0
if (px !== creditStripPx) {
creditStripPx = px
_sheetVarTarget.style.setProperty(STRIP_VAR, px + 'px')
}
}
onMounted(() => {
if (typeof ResizeObserver !== 'undefined') _stripRO = new ResizeObserver(() => measureCreditStrip())
_stripTimers = [0, 500, 1500, 3000, 6000, 12000].map((ms) => setTimeout(measureCreditStrip, ms))
})
onBeforeUnmount(() => {
_stripTimers.forEach(clearTimeout); _stripTimers = []
if (_stripRO) { _stripRO.disconnect(); _stripRO = null }
_stripObserved.clear()
if (_sheetVarTarget && _sheetVarTarget.__legendSheetVarOwner === _observedSheet) {
_sheetVarTarget.style.removeProperty(STRIP_VAR)
}
})
// ── Sheet height → host CSS var (keeps the engine's attribution visible) ───
// Writes --legend-sheet-h on the custom-element host (the CSS rule near the top
// of the stylesheet lifts the bottom control corners by it). Only a sheet with a