Phones: the search bar sits in its own header band; the share menu is titled Share and shows its content without scrolling

This commit is contained in:
swift-river542 2026-09-16 23:02:09 -05:00
parent 0bad29434d
commit a025b69e14
6 changed files with 37 additions and 12 deletions

View file

@ -218,7 +218,7 @@ useShadowStyles(`
.dg-section-header-item { font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.6px;color:#9ca3af;padding:2px 0; }
.dsm-dark .dg-section-header-item { color:rgba(243,243,241,0.45); }
@media(max-width:767px){
.mapboxgl-ctrl-top-left { top:10px!important;left:3px!important;right:3px!important;width:calc(100% - 6px)!important;max-width:none!important; }
/* The phone search band (a header strip that holds the pill) is owned by GeocoderComponent. */
/* Search-bar height (exactly 35px, desktop AND mobile) is now owned by the shared
GeocoderComponent.vue injection 'geocoder-height-35' — no per-profile @media
height hack needed (replaces bug-searchbar-mobile-height). */

View file

@ -120,7 +120,7 @@ useShadowStyles(`
mobile) is now owned by the shared GeocoderComponent.vue injection
'geocoder-height-35' — no per-profile @media height hack (replaces
bug-searchbar-mobile-height). */
.mapboxgl-ctrl-top-left { top:10px!important;left:3px!important;right:3px!important;width:calc(100% - 6px)!important;max-width:none!important; }
/* The phone search band (a header strip that holds the pill) is owned by GeocoderComponent. */
}
/* ── Dark mode: root class drives all child overrides ── */

View file

@ -136,8 +136,14 @@ useShadowStyles(dualEngine(
// MOBILE — go nearly edge-to-edge. Also shared: a phone has no room for a dead
// side margin, and a profile that simply forgets this rule (as one did) silently
// renders its DESKTOP width on mobile.
+ '@media(max-width:767px){.mapboxgl-ctrl-top-left{top:10px!important;left:3px!important;'
+ 'right:3px!important;width:calc(100% - 6px)!important;max-width:none!important;}}'
// MOBILE — the band becomes a header strip across the top of the map that HOLDS the pill:
// the strip spans the full width, its padding insets the pill, so the pill reads as sitting
// IN a header rather than floating edge-to-edge. Desktop keeps the bare positioned slot.
+ '@media(max-width:767px){.dg-search-band{top:0!important;left:0!important;right:0!important;'
+ 'width:100%!important;max-width:none!important;margin:0!important;box-sizing:border-box!important;'
+ 'padding:9px 14px!important;background:#fff;box-shadow:0 1px 4px rgba(0,0,0,0.10);}'
+ '.dg-search-band--dark{background:#3b463d;box-shadow:0 1px 4px rgba(0,0,0,0.35);}'
+ '.dg-search-band .mapboxgl-ctrl,.dg-search-band .maplibregl-ctrl{margin:0!important;float:none!important;}}'
// Light (default): rounded white pill + soft shadow.
+ '.mapboxgl-ctrl-geocoder{border-radius:999px!important;background:#fff;'
+ 'box-shadow:0 2px 8px rgba(0,0,0,0.15)!important;transition:box-shadow .12s,border-color .12s;}'
@ -653,9 +659,23 @@ onMounted(async () => {
}
} else {
map.addControl(geocoder.value, 'top-left')
tagSearchBand()
}
})
// THE SEARCH BAND — the control container the engine creates for the 'top-left' slot is the
// search bar's own row. Tagging it with a class of ours (instead of styling the engine's
// .mapboxgl-ctrl-top-left directly) gives the band one owner and one name, so a profile never
// has to know how the engine names its corners. On phones the band becomes a header strip that
// holds the pill (see the 'geocoder-band' styles); on desktop it stays a bare positioned slot.
function tagSearchBand() {
const el = geocoder.value?.container?.parentElement
if (!el) return
el.classList.add('dg-search-band')
el.classList.toggle('dg-search-band--dark', !!props.isDark)
}
watch(() => props.isDark, () => tagSearchBand())
// Mapbox Geocoder sets `placeholder` once at construction; it has no public
// updater method. Watch the effective placeholder and patch the DOM input
// directly so the per-map prop can change as the user switches profile tabs.

View file

@ -7,8 +7,9 @@
• It renders INSIDE the map area (teleported out of the toolbar into the same shadow
root), so it never escapes the map's walls, never uses position:fixed, and scrolls
with the map when the host page scrolls.
• Its top edge is the toolbar's top edge and its max height is the toolbar column's
height — every menu of a map shares the same band, whichever button opened it.
• Its top edge is the toolbar's top edge. By default it is as tall as its content needs
(bounded only by the map's bottom edge — no scrolling); `maxHeight` caps it lower and
`matchToolbar` limits it to the toolbar column's height (content then scrolls).
• It opens on the toolbar's inner side: to the LEFT of right-hand controls, to the
RIGHT of left-hand controls. The side is measured, so a right-to-left page that
mirrors the toolbar mirrors the menu as well.
@ -18,8 +19,9 @@
Props: open (Boolean), title (String), isDark (Boolean),
width (Number, 340) — preferred width; shrinks to what fits (mobile friendly)
maxHeight (Number|null) — cap below the toolbar column's height (null = the column)
height (Number|null) — a fixed height instead of fitting the content
maxHeight (Number|null) — cap the height (null = fit the content within the map)
height (Number|null) — a fixed height instead of fitting the content
matchToolbar (Boolean) — limit the height to the toolbar column (content scrolls)
Emits: close
Slot: the menu content (tabs, buttons, text) — chrome (header, close) is provided here.
-->
@ -52,6 +54,7 @@ const props = defineProps({
isDark: { type: Boolean, default: false },
maxHeight: { type: Number, default: null },
height: { type: Number, default: null },
matchToolbar: { type: Boolean, default: false },
})
const emit = defineEmits(['close'])
@ -73,7 +76,8 @@ function place() {
const offset = onRight ? (ar.right - tr.left + gap) : (tr.right - ar.left + gap)
const width = Math.max(200, Math.min(props.width, ar.width - offset - edge))
const top = Math.max(0, tr.top - ar.top)
let maxHeight = Math.max(120, Math.min(tr.height, ar.height - top - edge))
let maxHeight = Math.max(120, ar.height - top - edge) // fit the content inside the map
if (props.matchToolbar) maxHeight = Math.max(120, Math.min(maxHeight, tr.height))
if (props.maxHeight) maxHeight = Math.min(maxHeight, props.maxHeight)
style.value = {
top: `${Math.round(top)}px`,

View file

@ -426,7 +426,7 @@ async function copyEmbed() {
ref="btnEl"
:is-dark="isDark"
:active="isOpen"
:title="t('buttons.shareEmbed')"
:title="t('buttons.share')"
@click="toggle"
>
<!-- Share icon: box with upward arrow (iOS / Material standard) -->
@ -441,7 +441,7 @@ async function copyEmbed() {
</MapControlButton>
<!-- =================== Menu (shared MapControlMenu owns placement + chrome) =================== -->
<MapControlMenu :open="isOpen" :title="t('buttons.shareEmbed')" :is-dark="isDark" @close="close">
<MapControlMenu :open="isOpen" :title="t('buttons.share')" :is-dark="isDark" @close="close">
<div ref="popoverEl" class="share-pop" :class="{ light: !isDark }">
<!-- The multiplication path — one line each, top of the menu -->
<ul class="share-steps">

View file

@ -28,5 +28,6 @@
"Train them to embed it too",
"Train them to train others to embed it too",
"Multiply maps everywhere for Jesus!"
]
],
"share": "Share"
}