From 9fc4bbe30025da21d5c4c0d9baaee0369de65a3d Mon Sep 17 00:00:00 2001 From: Marc Osenberg Date: Mon, 28 Sep 2026 10:43:48 +0200 Subject: [PATCH 1/6] fix(ui): make the map's filter panel foldable and start it folded on phones On viewports below 430px the map's filter panel (basemap controls plus every listing filter) covered the whole map, leaving nothing to pan or tap. Make it foldable behind a checkbox-driven CSS toggle rather than React state, so it starts folded on a phone and stays open everywhere else with no resize listener needed in either direction: Map.mobile.less pins the body open above the breakpoint and hides the toggle there. The body's own "Map" heading is hidden on a phone too, since it would otherwise repeat the fold label word for word once unfolded. New locale key `map.foldToggle` (the toggle's aria-label), added to all six languages to keep test/ui/locales.test.js passing. Co-Authored-By: Claude Sonnet 5 --- ui/src/locales/de.json | 1 + ui/src/locales/en.json | 1 + ui/src/locales/es.json | 1 + ui/src/locales/fr.json | 1 + ui/src/locales/it.json | 1 + ui/src/locales/tr.json | 1 + ui/src/views/listings/Map.jsx | 216 +++++++++++++++----------- ui/src/views/listings/Map.mobile.less | 67 ++++++++ 8 files changed, 195 insertions(+), 94 deletions(-) create mode 100644 ui/src/views/listings/Map.mobile.less diff --git a/ui/src/locales/de.json b/ui/src/locales/de.json index 10449a1a..956f6798 100644 --- a/ui/src/locales/de.json +++ b/ui/src/locales/de.json @@ -437,6 +437,7 @@ "map.noHomeAddressLink": "Benutzereinstellungen", "map.noHomeAddressAfter": ", um den Entfernungsfilter zu nutzen.", "map.groupMap": "Karte", + "map.foldToggle": "Kartenfilter ein- oder ausblenden", "map.groupListings": "Inserate", "map.searchPlaceholder": "Adresse oder Ort suchen", "map.searchLabel": "Adresssuche auf der Karte", diff --git a/ui/src/locales/en.json b/ui/src/locales/en.json index fba73de7..40f66d9d 100644 --- a/ui/src/locales/en.json +++ b/ui/src/locales/en.json @@ -437,6 +437,7 @@ "map.noHomeAddressLink": "user settings", "map.noHomeAddressAfter": " to use the distance filter.", "map.groupMap": "Map", + "map.foldToggle": "Show or hide the map filters", "map.groupListings": "Listings", "map.searchPlaceholder": "Search an address or place", "map.searchLabel": "Address search on the map", diff --git a/ui/src/locales/es.json b/ui/src/locales/es.json index 722498d0..c61150ae 100644 --- a/ui/src/locales/es.json +++ b/ui/src/locales/es.json @@ -437,6 +437,7 @@ "map.noHomeAddressLink": "los ajustes de usuario", "map.noHomeAddressAfter": " para usar el filtro de distancia.", "map.groupMap": "Mapa", + "map.foldToggle": "Mostrar u ocultar los filtros del mapa", "map.groupListings": "Anuncios", "map.searchPlaceholder": "Buscar una dirección o un lugar", "map.searchLabel": "Búsqueda de direcciones en el mapa", diff --git a/ui/src/locales/fr.json b/ui/src/locales/fr.json index 80a04d75..cd2be30f 100644 --- a/ui/src/locales/fr.json +++ b/ui/src/locales/fr.json @@ -437,6 +437,7 @@ "map.noHomeAddressLink": "les paramètres utilisateur", "map.noHomeAddressAfter": " pour utiliser le filtre de distance.", "map.groupMap": "Carte", + "map.foldToggle": "Afficher ou masquer les filtres de la carte", "map.groupListings": "Annonces", "map.searchPlaceholder": "Rechercher une adresse ou un lieu", "map.searchLabel": "Recherche d'adresse sur la carte", diff --git a/ui/src/locales/it.json b/ui/src/locales/it.json index 6f4f1a23..6646be41 100644 --- a/ui/src/locales/it.json +++ b/ui/src/locales/it.json @@ -437,6 +437,7 @@ "map.noHomeAddressLink": "impostazioni utente", "map.noHomeAddressAfter": " per usare il filtro per distanza.", "map.groupMap": "Mappa", + "map.foldToggle": "Mostra o nascondi i filtri della mappa", "map.groupListings": "Annunci", "map.searchPlaceholder": "Cerca un indirizzo o un luogo", "map.searchLabel": "Ricerca di indirizzi sulla mappa", diff --git a/ui/src/locales/tr.json b/ui/src/locales/tr.json index 37bcf562..66073257 100644 --- a/ui/src/locales/tr.json +++ b/ui/src/locales/tr.json @@ -406,6 +406,7 @@ "map.noHomeAddressLink": "kullanıcı ayarlarından", "map.noHomeAddressAfter": " uzaklık filtresini kullanmak için yapılandırın.", "map.groupMap": "Harita", + "map.foldToggle": "Harita filtrelerini göster veya gizle", "map.groupListings": "İlanlar", "map.searchPlaceholder": "Adres veya yer ara", "map.searchLabel": "Haritada adres arama", diff --git a/ui/src/views/listings/Map.jsx b/ui/src/views/listings/Map.jsx index 60a23cfe..3a0e12df 100644 --- a/ui/src/views/listings/Map.jsx +++ b/ui/src/views/listings/Map.jsx @@ -17,10 +17,12 @@ import { groupListingsByPosition, } from './mapUtils.js'; import { Select, Switch, Toast, Typography } from '@douyinfe/semi-ui-19'; +import { IconChevronDown } from '@douyinfe/semi-icons'; import _RangeSlider from 'react-range-slider-input'; import 'react-range-slider-input/dist/style.css'; import './Map.less'; +import './Map.mobile.less'; import { xhrDelete, errorMessage } from '../../services/xhr.js'; import { Link, useNavigate, useSearchParams } from 'react-router'; import ListingDeletionModal from '../../components/ListingDeletionModal.jsx'; @@ -661,112 +663,138 @@ export default function MapView() { what the map is showing, so they read as one panel with a line between them rather than as two identical boxes four pixels apart, neither of them with a heading. */
- {/* The fullscreen toggle rides on this heading rather than floating above the - panel: it is a control over the map as a whole, and this is the line that names - the map. */} -
+ {/* The fold toggle: a plain checkbox driving CSS, not React state, so the panel + starts folded on a phone and stays open everywhere else without a resize + listener - see Map.mobile.less for the media query that pins it open above the + breakpoint and the one that ties `:checked` to the body's visibility. + Unchecked by default: nothing here reads the fold state back, it is view, not + data, so there is nothing to initialise beyond the folded starting point. */} + +
- {controls} - -
- -
{t('map.groupListings')}
- -
- - {t('map.filterJobLabel')} - - -
+ + + +
+ {/* The fullscreen toggle rides on this heading rather than floating above the + panel: it is a control over the map as a whole, and this is the line that + names the map. The text repeats the fold label above once unfolded, so + Map.mobile.less hides it below the fold breakpoint and leaves the button. */} +
+ {t('map.groupMap')} + {expandButton} +
+ {controls} + +
+ +
{t('map.groupListings')}
+ +
+ + {t('map.filterJobLabel')} + + +
- {/* Disabled rather than hidden, and it says why one line below. A control that + {/* Disabled rather than hidden, and it says why one line below. A control that cannot work is the honest place for that sentence - it used to be a full-width banner above the map, on every visit, for a fact that never changes. */} -
- - {t('map.filterDistanceLabel')} - - -
+
+ + {t('map.filterDistanceLabel')} + + +
- {/* Locked rather than hidden, for the same reason as the ring above. Unlike the + {/* Locked rather than hidden, for the same reason as the ring above. Unlike the distance ring, which recolours pins, this one hides them: a commute ceiling is asked as "show me only what I could live with". */} -
- - {t('map.filterCommuteLabel')} - - -
+
+ + {t('map.filterCommuteLabel')} + + +
-
- - {t('map.filterPriceLabel')} - -
-
- {formatEuroCompact(priceRange[0], locale)} - {formatEuroCompact(priceRange[1] || getMaxPrice(), locale)} +
+ + {t('map.filterPriceLabel')} + +
+
+ {formatEuroCompact(priceRange[0], locale)} + {formatEuroCompact(priceRange[1] || getMaxPrice(), locale)} +
+
-
-
- {!hasHome && ( -
- {t('map.noHomeAddressBefore')} - {t('map.noHomeAddressLink')} - {t('map.noHomeAddressAfter')} -
- )} + {!hasHome && ( +
+ {t('map.noHomeAddressBefore')} + {t('map.noHomeAddressLink')} + {t('map.noHomeAddressAfter')} +
+ )} -
- 0 && hasHome} hasHome={hasHome} /> +
+ 0 && hasHome} hasHome={hasHome} /> +
)} /> diff --git a/ui/src/views/listings/Map.mobile.less b/ui/src/views/listings/Map.mobile.less new file mode 100644 index 00000000..989b5bbd --- /dev/null +++ b/ui/src/views/listings/Map.mobile.less @@ -0,0 +1,67 @@ +@import '../../tokens.less'; + +/* The filter panel's fold/unfold. A checkbox drives it, not React state: the panel starts folded + on a phone and pinned open everywhere else through the media query below, with no resize + listener needed for either direction. Kept in its own file and behind a single media query so + upstream merges stay simple. */ + +.map-panel__fold-input { + /* Not `display: none` - a hidden checkbox is not focusable, and the label above still needs to + toggle it from the keyboard. */ + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.map-panel__fold-label { + display: none; +} + +@media (max-width: 430px) { + .map-panel__fold-label { + display: flex; + align-items: center; + justify-content: space-between; + gap: @space-2; + cursor: pointer; + font-size: 10.5px; + font-weight: 600; + letter-spacing: 0.07em; + text-transform: uppercase; + color: @color-faint; + } + + .map-panel__fold-chevron { + transition: transform @transition-fast; + } + + /* Folded by default (the checkbox above starts checked; see Map.jsx). Everything below the + label - the map controls and every listing filter - is the part that used to cover the whole + map on a phone. */ + .map-panel__body { + display: none; + } + + .map-panel__fold-input:checked ~ .map-panel__fold-label .map-panel__fold-chevron { + transform: rotate(180deg); + } + + .map-panel__fold-input:checked ~ .map-panel__body { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 12px; + } + + /* The unfolded body's own "Map" heading would repeat the fold label word for word; only its + fullscreen button still earns a row here. */ + .map-panel__body > .map-panel__groupTitle:first-child .map-panel__groupTitle-text { + display: none; + } +} From c4dfb549a5271d42a6663032a5e096662af65649 Mon Sep 17 00:00:00 2001 From: Marc Osenberg Date: Mon, 28 Sep 2026 11:01:30 +0200 Subject: [PATCH 2/6] fix(ui): move the mobile filter panel to a bottom popup, clear of the map's own controls The folded pill, and the panel it opens into, sat in the same top-right column the desktop dropdown uses. At 430px that column is nearly as wide as the map, so both landed on top of the zoom/compass controls in the opposite corner (pushed there by the search box) - the same overlap the fold was meant to fix, just for a shorter box. Anchor both to the bottom-left instead: folded, a compact pill sized to its text rather than the full-width bar; open, a bottom sheet with its own scroll rather than pushing further down the page. Bottom-left also keeps clear of the attribution control's "i" in the opposite corner, which runs its full text - not yet the icon it collapses to - until the map is first panned or zoomed. Positioned absolute against `.map-shell__ui` (stretched to the map's own bounds here, where the panel is its only child) rather than fixed against the viewport, which runs under the app's own sidebar and would have put the panel half behind it. Co-Authored-By: Claude Sonnet 5 --- ui/src/views/listings/Map.mobile.less | 70 ++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/ui/src/views/listings/Map.mobile.less b/ui/src/views/listings/Map.mobile.less index 989b5bbd..698aed9a 100644 --- a/ui/src/views/listings/Map.mobile.less +++ b/ui/src/views/listings/Map.mobile.less @@ -24,26 +24,69 @@ } @media (max-width: 430px) { + /* `.map-panel` positions itself against this box (see below), which on every other width holds + it to the shape a single dropdown needs. With the panel now absolute inside it, an empty + `.map-shell__ui` has nothing left to size itself by and collapses to zero height - stretching + it to the map's own bounds instead gives `.map-panel`'s `left`/`right`/`bottom` a frame the + size of the map to measure from. */ + .map-shell__ui { + inset: 0; + width: auto; + max-height: none; + overflow: visible; + } + + /* + * Bottom-left rather than the top-right dropdown the desktop panel is: at this width the panel + * is nearly as wide as the map itself (`.map-shell__ui`'s `min(300px, calc(100% - 24px))`), and + * anchored under the search box it landed in the same band as MapLibre's own zoom and compass + * controls, which the search pushes to that exact spot. Bottom-left keeps clear of both that + * band and the attribution control's "i", which lives in the opposite bottom corner. + * + * Absolute against `.map-shell__ui` (stretched to the map's own bounds above), not fixed + * against the viewport - the viewport runs under the app's own sidebar, and `fixed`'s + * `left`/`right` measure from its edges, not the map's, which put the panel half behind it. + */ + .map-panel { + position: absolute; + left: 12px; + right: 12px; + top: auto; + /* Clears MapLibre's own attribution strip along the bottom edge. It only collapses to its "i" + icon once the map is panned or zoomed; until then - which on a page nobody has touched yet + is exactly the state a screenshot catches - it runs its full text at up to two lines, and + the offset has to clear that, not the icon it becomes afterwards. */ + bottom: 116px; + width: auto; + max-width: none; + z-index: 15; + } + + /* Folded: a compact pill sized to its own text, not the full-width bar the box is the rest of + the time - a bar that wide next to the left edge would have reached for the same corner the + search and the zoom controls already own. */ .map-panel__fold-label { - display: flex; + display: inline-flex; + width: fit-content; align-items: center; - justify-content: space-between; gap: @space-2; + padding: 10px 16px; + border-radius: 999px; cursor: pointer; + box-shadow: @shadow-sm; font-size: 10.5px; font-weight: 600; letter-spacing: 0.07em; text-transform: uppercase; - color: @color-faint; + color: @color-map-panel-text; } .map-panel__fold-chevron { transition: transform @transition-fast; } - /* Folded by default (the checkbox above starts checked; see Map.jsx). Everything below the - label - the map controls and every listing filter - is the part that used to cover the whole - map on a phone. */ + /* Folded by default (the checkbox above starts unchecked). Everything below the label - the map + controls and every listing filter - is the part that used to cover the whole map on a phone. */ .map-panel__body { display: none; } @@ -52,14 +95,27 @@ transform: rotate(180deg); } + /* Open: the pill grows into the popup rather than staying pinned to its own corner, so it reads + as one control changing shape rather than a second, different box appearing next to it. */ + .map-panel:has(.map-panel__fold-input:checked) { + width: auto; + /* Relative to `.map-shell__ui`'s own height, which now matches the map's, not the viewport's - + the two differ by the headline above the map and the app's own chrome around it. 116px is + the `bottom` offset above, already spent before this box's height is even measured; 100px + leaves the search box and MapLibre's own zoom/compass controls reachable above it rather + than sliding the sheet in under them. */ + max-height: calc(100% - 116px - 100px); + } + .map-panel__fold-input:checked ~ .map-panel__body { display: flex; flex-direction: column; gap: 12px; margin-top: 12px; + overflow-y: auto; } - /* The unfolded body's own "Map" heading would repeat the fold label word for word; only its + /* The body's own "Map" heading would repeat the fold label word for word once open; only its fullscreen button still earns a row here. */ .map-panel__body > .map-panel__groupTitle:first-child .map-panel__groupTitle-text { display: none; From d89f552f3e2f3ee79d53e40747be5198abfa8f60 Mon Sep 17 00:00:00 2001 From: Marc Osenberg Date: Mon, 28 Sep 2026 11:14:35 +0200 Subject: [PATCH 3/6] fix(ui): align the folded filter pill with the attribution control Moved from its earlier spot well above the bottom edge - which existed only to clear the attribution control's expanded text - to the same row as its "i", on the opposite corner. The two still overlap until the map is first panned or zoomed, since that text runs its full width until then; the pill's ancestor already outranks the attribution control's own stacking context (`.map-shell__ui` at z-index 10 vs. the control's z-index 2, see Map.less), so the pill stays on top and clickable rather than the reverse. Co-Authored-By: Claude Sonnet 5 --- ui/src/views/listings/Map.mobile.less | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/ui/src/views/listings/Map.mobile.less b/ui/src/views/listings/Map.mobile.less index 698aed9a..4303e901 100644 --- a/ui/src/views/listings/Map.mobile.less +++ b/ui/src/views/listings/Map.mobile.less @@ -40,8 +40,13 @@ * Bottom-left rather than the top-right dropdown the desktop panel is: at this width the panel * is nearly as wide as the map itself (`.map-shell__ui`'s `min(300px, calc(100% - 24px))`), and * anchored under the search box it landed in the same band as MapLibre's own zoom and compass - * controls, which the search pushes to that exact spot. Bottom-left keeps clear of both that - * band and the attribution control's "i", which lives in the opposite bottom corner. + * controls, which the search pushes to that exact spot. + * + * `bottom: 10px` matches MapLibre's own margin around its bottom-right attribution control, so + * the folded pill sits in the same row as the "i" rather than floating above it. That row is + * exactly where the attribution's *expanded* text reaches to before the map is first panned or + * zoomed - z-index settles which one wins, so see below for that rather than moving the pill to + * dodge it. * * Absolute against `.map-shell__ui` (stretched to the map's own bounds above), not fixed * against the viewport - the viewport runs under the app's own sidebar, and `fixed`'s @@ -52,13 +57,13 @@ left: 12px; right: 12px; top: auto; - /* Clears MapLibre's own attribution strip along the bottom edge. It only collapses to its "i" - icon once the map is panned or zoomed; until then - which on a page nobody has touched yet - is exactly the state a screenshot catches - it runs its full text at up to two lines, and - the offset has to clear that, not the icon it becomes afterwards. */ - bottom: 116px; + bottom: 10px; width: auto; max-width: none; + /* MapLibre's own bottom-right corner (which the attribution sits in) carries `z-index: 2`; + `.map-shell__ui` already outranks it at `z-index: 10` (see Map.less), which is what + actually keeps the pill clickable when the two overlap. This is set high too so that stays + true regardless of where in `.map-shell__ui` a future change puts this panel. */ z-index: 15; } @@ -100,11 +105,12 @@ .map-panel:has(.map-panel__fold-input:checked) { width: auto; /* Relative to `.map-shell__ui`'s own height, which now matches the map's, not the viewport's - - the two differ by the headline above the map and the app's own chrome around it. 116px is + the two differ by the headline above the map and the app's own chrome around it. 10px is the `bottom` offset above, already spent before this box's height is even measured; 100px leaves the search box and MapLibre's own zoom/compass controls reachable above it rather - than sliding the sheet in under them. */ - max-height: calc(100% - 116px - 100px); + than sliding the sheet in under them. Open, the sheet reaches past the attribution control + same as it would past anything else the map draws under it. */ + max-height: calc(100% - 10px - 100px); } .map-panel__fold-input:checked ~ .map-panel__body { From 23490507d7ca5b8ff906263f2251430b788404f4 Mon Sep 17 00:00:00 2001 From: Marc Osenberg Date: Mon, 28 Sep 2026 11:28:48 +0200 Subject: [PATCH 4/6] fix(ui): decouple the mobile pill from the popup it opens `.map-panel` drew one growing box for both: folded, its own padding and background wrapped the pill in a visible card of its own, and open, the same box grew to include the filters below it, carrying the pill up from the map's bottom edge to the top of that box. Split them instead. `.map-panel` itself now renders nothing (`display: contents`) - the pill and the popup are its own two children, each positioned and boxed independently against `.map-shell__ui`. The pill stays put in the map's bottom-left corner whether the popup is open or not; the popup is its own floating box anchored just above the pill's fixed position rather than one shape growing out of it. Co-Authored-By: Claude Sonnet 5 --- ui/src/views/listings/Map.mobile.less | 104 +++++++++++++------------- 1 file changed, 51 insertions(+), 53 deletions(-) diff --git a/ui/src/views/listings/Map.mobile.less b/ui/src/views/listings/Map.mobile.less index 4303e901..7564c4ed 100644 --- a/ui/src/views/listings/Map.mobile.less +++ b/ui/src/views/listings/Map.mobile.less @@ -24,11 +24,11 @@ } @media (max-width: 430px) { - /* `.map-panel` positions itself against this box (see below), which on every other width holds - it to the shape a single dropdown needs. With the panel now absolute inside it, an empty - `.map-shell__ui` has nothing left to size itself by and collapses to zero height - stretching - it to the map's own bounds instead gives `.map-panel`'s `left`/`right`/`bottom` a frame the - size of the map to measure from. */ + /* `.map-panel__fold-label` and `.map-panel__body` position themselves against this box (see + below), which on every other width holds the single dropdown box to the shape it needs. + Stretched to the map's own bounds here instead, so each of the two has the whole map to place + itself against rather than the collapsed, zero-size box an otherwise-empty `.map-shell__ui` + would leave them with once neither is `.map-panel`'s own in-flow child any more. */ .map-shell__ui { inset: 0; width: auto; @@ -36,41 +36,29 @@ overflow: visible; } - /* - * Bottom-left rather than the top-right dropdown the desktop panel is: at this width the panel - * is nearly as wide as the map itself (`.map-shell__ui`'s `min(300px, calc(100% - 24px))`), and - * anchored under the search box it landed in the same band as MapLibre's own zoom and compass - * controls, which the search pushes to that exact spot. - * - * `bottom: 10px` matches MapLibre's own margin around its bottom-right attribution control, so - * the folded pill sits in the same row as the "i" rather than floating above it. That row is - * exactly where the attribution's *expanded* text reaches to before the map is first panned or - * zoomed - z-index settles which one wins, so see below for that rather than moving the pill to - * dodge it. - * - * Absolute against `.map-shell__ui` (stretched to the map's own bounds above), not fixed - * against the viewport - the viewport runs under the app's own sidebar, and `fixed`'s - * `left`/`right` measure from its edges, not the map's, which put the panel half behind it. - */ + /* `.map-panel` drew the desktop dropdown's own box - background, border, blur, padding - and on + a phone that box was wrapping the pill too, a card behind a chip nobody asked for. `contents` + removes the box without removing what is inside it: the pill and the popup below still exist + and still respond to the checkbox, just no longer sharing one growing shape or one position. + Each now carries its own box straight against `.map-shell__ui` instead. */ .map-panel { + display: contents; + } + + /* The pill: fixed to the map's bottom-left corner and never resized by the popup opening, unlike + the single box this used to be one growing piece of. Its own background, border and blur are + what `.map-panel` supplied before - moved here now that the box that grows is a separate + element. */ + .map-panel__fold-label { position: absolute; left: 12px; - right: 12px; - top: auto; + /* Matches MapLibre's own margin around its bottom-right attribution control, so the pill sits + in the same row as the "i" on the opposite corner rather than floating above it. The two + overlap until the map is first panned or zoomed, since the attribution runs its full text + rather than collapsing to that icon until then; z-index below is what keeps the pill on top + and clickable through that, rather than moving to dodge it. */ bottom: 10px; - width: auto; - max-width: none; - /* MapLibre's own bottom-right corner (which the attribution sits in) carries `z-index: 2`; - `.map-shell__ui` already outranks it at `z-index: 10` (see Map.less), which is what - actually keeps the pill clickable when the two overlap. This is set high too so that stays - true regardless of where in `.map-shell__ui` a future change puts this panel. */ z-index: 15; - } - - /* Folded: a compact pill sized to its own text, not the full-width bar the box is the rest of - the time - a bar that wide next to the left edge would have reached for the same corner the - search and the zoom controls already own. */ - .map-panel__fold-label { display: inline-flex; width: fit-content; align-items: center; @@ -78,6 +66,10 @@ padding: 10px 16px; border-radius: 999px; cursor: pointer; + background: @color-map-panel; + border: 1px solid @color-border; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); box-shadow: @shadow-sm; font-size: 10.5px; font-weight: 600; @@ -90,35 +82,41 @@ transition: transform @transition-fast; } - /* Folded by default (the checkbox above starts unchecked). Everything below the label - the map - controls and every listing filter - is the part that used to cover the whole map on a phone. */ - .map-panel__body { - display: none; - } - .map-panel__fold-input:checked ~ .map-panel__fold-label .map-panel__fold-chevron { transform: rotate(180deg); } - /* Open: the pill grows into the popup rather than staying pinned to its own corner, so it reads - as one control changing shape rather than a second, different box appearing next to it. */ - .map-panel:has(.map-panel__fold-input:checked) { - width: auto; - /* Relative to `.map-shell__ui`'s own height, which now matches the map's, not the viewport's - - the two differ by the headline above the map and the app's own chrome around it. 10px is - the `bottom` offset above, already spent before this box's height is even measured; 100px - leaves the search box and MapLibre's own zoom/compass controls reachable above it rather - than sliding the sheet in under them. Open, the sheet reaches past the attribution control - same as it would past anything else the map draws under it. */ - max-height: calc(100% - 10px - 100px); + /* Closed by default (the checkbox above starts unchecked) and, unlike the pill, not part of the + page until then - the map controls and every listing filter are what used to cover the whole + map on a phone. */ + .map-panel__body { + display: none; } + /* Open: its own floating box - background, border, blur, shadow, the same look `.map-panel` + drew before - anchored just above the pill rather than grown out of it, so the pill stays put + in its own corner while this appears and disappears above it. */ .map-panel__fold-input:checked ~ .map-panel__body { display: flex; flex-direction: column; gap: 12px; - margin-top: 12px; + position: absolute; + left: 12px; + right: 12px; + /* Pill height (36px) + its own 10px margin + a little breathing room between the two boxes. */ + bottom: 56px; + /* Leaves the search box and MapLibre's own zoom/compass controls reachable above it rather + than sliding the sheet in under them. */ + max-height: calc(100% - 56px - 100px); overflow-y: auto; + padding: 14px 16px; + background: @color-map-panel; + border: 1px solid @color-border; + border-radius: @radius-card; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: @shadow-md; + z-index: 15; } /* The body's own "Map" heading would repeat the fold label word for word once open; only its From 69323e520fb53b9b1956d00e484502c4c94a0811 Mon Sep 17 00:00:00 2001 From: Marc Osenberg Date: Mon, 28 Sep 2026 11:36:21 +0200 Subject: [PATCH 5/6] fix(ui): keep the attribution control collapsed to its "i" on load, on narrow maps Its own compact mode still opened on load with its full attribution text showing rather than the icon it collapses to on the first pan or zoom - a paragraph of text sitting over the map, on a page nobody had touched yet. That open state runs on MapLibre's own `maplibregl-compact-show` class, not the `
` element's native `open` state (maplibre-gl.css keys the text's `display` off the class alone), so removing it once, right after the control is created, is what actually collapses it; `_toggleAttribution` still adds and removes that same class on a click, so the "i" keeps opening and closing it exactly as before. Gated to maps at or under 430px, the breakpoint Map.mobile.less's own fold already uses: a wide desktop embed keeps the attribution it had. Co-Authored-By: Claude Sonnet 5 --- ui/src/components/map/Map.jsx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ui/src/components/map/Map.jsx b/ui/src/components/map/Map.jsx index 4a38f4b9..080d18df 100644 --- a/ui/src/components/map/Map.jsx +++ b/ui/src/components/map/Map.jsx @@ -301,6 +301,26 @@ export default function Map({ cooperativeGestures, }); + // MapLibre's own attribution control opens itself - a paragraph of text over the map, not the + // "i" it collapses to - until the map is first panned or zoomed. That visibility runs on its + // own `maplibregl-compact-show` class rather than the `
` element's native `open` + // state (maplibre-gl.css keys the attribution text's `display` off the class alone), so + // removing the class is what actually collapses it; the class is what `_toggleAttribution` + // re-adds on a click, so the "i" still opens and closes it same as before. + // + // Only below the width Map.mobile.less's own breakpoint uses: a map that size is small enough + // that the full text is the map, and wide desktop embeds keep the attribution they had. Added + // synchronously (`AttributionControl.onAdd` runs, and adds the class, inside the `Map` + // constructor above, before the style has even loaded) and again on `load` in case MapLibre + // re-adds it once the style is ready. + const collapseAttribution = () => { + const container = mapContainerRef.current; + if (!container || container.offsetWidth > 430) return; + container.querySelector('.maplibregl-ctrl-attrib')?.classList.remove('maplibregl-compact-show'); + }; + collapseAttribution(); + mapRef.current.once('load', collapseAttribution); + // Left, because the panels now live in the top right corner and a zoom button hiding behind // them helps nobody. Where drawing is enabled MapLibre simply stacks these below its tools. mapRef.current.addControl( From 3b7aaed0b0b72408a96190bb2f371785f298bbc4 Mon Sep 17 00:00:00 2001 From: Marc Osenberg Date: Mon, 28 Sep 2026 14:13:07 +0200 Subject: [PATCH 6/6] test: update the fullscreen-toggle assertion for the wrapped heading text Map.jsx now wraps the group heading's text in its own span (a prior commit, so Map.mobile.less can hide the text alone on a phone and leave the fullscreen button next to it visible) - the assertion checking the two sit on the same heading needed the closing tag added to keep matching. ai:generated Which AI: Claude Code, Sonnet 5 How much: the whole diff, one line plus its comment Why: fixing a test broken by an earlier Claude Code change in this same PR Co-Authored-By: Claude Sonnet 5 --- test/ui/mapTheme.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/ui/mapTheme.test.js b/test/ui/mapTheme.test.js index 6e6358b0..e7a323a8 100644 --- a/test/ui/mapTheme.test.js +++ b/test/ui/mapTheme.test.js @@ -232,7 +232,9 @@ describe('the page', () => { it('carries the fullscreen toggle on the first heading, not floating above the box', () => { expect(viewJsx).toMatch(/panels=\{\(controls, expandButton\) =>/); - expect(viewJsx).toMatch(/\{t\('map\.groupMap'\)\}\s*\{expandButton\}/); + // The text is wrapped in its own span so a phone can hide it without hiding the button next to + // it - see Map.mobile.less, which repeats it as the fold pill's own label below 430px. + expect(viewJsx).toMatch(/\{t\('map\.groupMap'\)\}<\/span>\s*\{expandButton\}/); // The map renders it itself only where no panel took it. expect(mapJsx).toMatch(/\{!controlsInPanels && expandButton\}/); // Smaller in there than on the map, and it needs two classes to beat Semi's own height.