|
|
|
|
@ -1,5 +1,5 @@
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, computed, onMounted, onBeforeUnmount, inject, toRef } from 'vue'
|
|
|
|
|
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount, inject, toRef } from 'vue'
|
|
|
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
|
import { useShadowStyles } from '../../composables/useShadowStyles.js'
|
|
|
|
|
import { useLegendData } from '../../composables/legends/useLegendData.js'
|
|
|
|
|
@ -21,21 +21,26 @@ 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 = 30% (the sheet's open height) + 16px gap. The Mapbox watermark lives bottom-right
|
|
|
|
|
and only rises by the collapsed strip, so nothing else needs room under this image. */
|
|
|
|
|
.floating-mini-image{position:absolute;inset-inline-start:16px;bottom:calc(30% + 16px);z-index:999;pointer-events:auto;cursor:pointer;}
|
|
|
|
|
/* 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;}
|
|
|
|
|
.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;}
|
|
|
|
|
/* Sits just above the sheet's top edge, tracking it via --legend-sheet-h. Text only, so it stays
|
|
|
|
|
pointer-events:none with the root — nothing here is tappable and it must never eat a map drag.
|
|
|
|
|
The scrim keeps it legible over an arbitrary basemap without becoming another panel. */
|
|
|
|
|
.approx-pin-note--floating{position:absolute;inset-inline:0;bottom:calc(var(--legend-sheet-h,0px) + 6px);z-index:1001;margin:0 10px !important;text-align:center;pointer-events:none;text-shadow:0 1px 2px rgba(255,255,255,0.85);}
|
|
|
|
|
/* The note's own text colour flips with the dark prop, so the scrim has to flip with it: a white
|
|
|
|
|
halo behind light text is a smear, not legibility. (No backticks in this comment: the whole
|
|
|
|
|
block is a JS template literal and a backtick would end it — which is what it just did.) */
|
|
|
|
|
.approx-pin-note--floating-dark{text-shadow:0 1px 2px rgba(0,0,0,0.85);}
|
|
|
|
|
/* Sits IN the credit strip: under the sheet, just above the map's bottom controls. Above the sheet it
|
|
|
|
|
collided with the pin-hint pill, which lifts itself to clear the sheet's top edge (measured at 390 on
|
|
|
|
|
Simple Research: the pill sat on the caption's text). Down here it is on screen in every sheet state
|
|
|
|
|
and the pill never reaches it. The strip is measured WITH it (--legend-credit-strip = the controls
|
|
|
|
|
+ this note), so the sheet sits above both. Text only, so it stays pointer-events:none with the
|
|
|
|
|
root — nothing here is tappable and it must never eat a map drag. It sits on a chip, like the credit
|
|
|
|
|
beside it: a text halo alone left the light theme's grey text unreadable on a dark basemap (Prayer
|
|
|
|
|
Progress at 390). */
|
|
|
|
|
.approx-pin-note--floating{position:absolute;inset-inline:0;bottom:var(--legend-credit-controls,0px);z-index:1001;box-sizing:border-box;width:fit-content;max-width:calc(100% - 20px);margin:0 auto 2px !important;padding:2px 8px;border-radius:6px;background:rgba(255,255,255,0.88);text-align:center;pointer-events:none;}
|
|
|
|
|
/* The note's own text colour flips with the dark prop, so the chip has to flip with it: light text on
|
|
|
|
|
a white chip is a smear, not legibility. (No backticks in this comment: the whole block is a JS
|
|
|
|
|
template literal and a backtick would end it — which is what it just did.) */
|
|
|
|
|
.approx-pin-note--floating-dark{background:rgba(20,24,31,0.82);}
|
|
|
|
|
.legend-sheet-root>*{pointer-events:auto;}
|
|
|
|
|
/* ...except the floating note. The rule above re-enables pointer events for every direct child of
|
|
|
|
|
the root, at the same specificity as the note's own rule and AFTER it, so it was winning and the
|
|
|
|
|
@ -43,6 +48,13 @@ useShadowStyles(`
|
|
|
|
|
nothing tappable in it; stated here, after the rule that would otherwise override it. */
|
|
|
|
|
.legend-sheet-root>.approx-pin-note--floating{pointer-events:none;}
|
|
|
|
|
|
|
|
|
|
/* ★ 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. */
|
|
|
|
|
/* ── 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
|
|
|
|
|
@ -70,12 +82,12 @@ useShadowStyles(`
|
|
|
|
|
Both are read from published variables — --legend-sheet-h (this component's own ResizeObserver)
|
|
|
|
|
and --map-fab-clearance (VerticalTabsLayout, set ONLY while the circle renders) — so a map with
|
|
|
|
|
no sheet and no fab still moves nothing. The 48px cap on the sheet term is kept as it was. */
|
|
|
|
|
.mapboxgl-ctrl-bottom-left,.mapboxgl-ctrl-bottom-right,.maplibregl-ctrl-bottom-left,.maplibregl-ctrl-bottom-right{transform:translateY(calc(-1 * max(min(var(--legend-sheet-h,0px),48px), var(--map-fab-clearance,0px))));}
|
|
|
|
|
.mapboxgl-ctrl-bottom-left,.mapboxgl-ctrl-bottom-right,.maplibregl-ctrl-bottom-left,.maplibregl-ctrl-bottom-right{transform:translateY(calc(-1 * var(--map-fab-clearance,0px)));}
|
|
|
|
|
/* The scroll-edge gutters (the hoverable side strips, z-index 2) run the full height of the
|
|
|
|
|
map and would otherwise swallow taps meant for the attribution in the corner: the corner
|
|
|
|
|
controls sit one tier above them. (Moves to the shared z tiers with the token work.) */
|
|
|
|
|
.mapboxgl-ctrl-bottom-left,.mapboxgl-ctrl-bottom-right,.maplibregl-ctrl-bottom-left,.maplibregl-ctrl-bottom-right{z-index:3;}
|
|
|
|
|
.mapboxgl-ctrl-bottom-right,.maplibregl-ctrl-bottom-right{display:flex;flex-direction:row;align-items:center;justify-content:flex-end;gap:2px;opacity:0.72;transform:translateY(calc(-1 * min(var(--legend-sheet-h,0px),48px))) scale(0.85);transform-origin:bottom right;transition:opacity 0.15s;}
|
|
|
|
|
.mapboxgl-ctrl-bottom-right,.maplibregl-ctrl-bottom-right{display:flex;flex-direction:row;align-items:center;justify-content:flex-end;gap:2px;opacity:0.72;transform:translateY(calc(-1 * var(--map-fab-clearance,0px))) scale(0.85);transform-origin:bottom right;transition:opacity 0.15s;}
|
|
|
|
|
.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;}
|
|
|
|
|
@ -91,12 +103,18 @@ 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:0;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));}
|
|
|
|
|
.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));}
|
|
|
|
|
/* 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
|
|
|
|
|
mid-animation value. Snapping on release re-enables the transition. */
|
|
|
|
|
.legend-mobile-sheet.dragging{transition:none!important;}
|
|
|
|
|
/* CONTENT-FIT OPEN STATE (mobile pass 2026-09-30): at rest in 'open' the sheet takes its content's
|
|
|
|
|
height, never more than 45% of the map, instead of a fixed 30% (a three-row legend sat in a mostly
|
|
|
|
|
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)));}
|
|
|
|
|
|
|
|
|
|
/* Drag strip — thin top overlay holding the pull-tab handle + touch
|
|
|
|
|
gestures. z-index puts it above content. */
|
|
|
|
|
@ -164,7 +182,9 @@ 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;}
|
|
|
|
|
.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;}
|
|
|
|
|
/* 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
|
|
|
|
|
their inner scroll containers (.stl-rows / .lrg-items) bound correctly.
|
|
|
|
|
PeopleGroupDetail keeps its natural size and lets .legend-content scroll. */
|
|
|
|
|
@ -552,7 +572,7 @@ const SNAP_MIN_PX = 24
|
|
|
|
|
// the .dragging class flag).
|
|
|
|
|
const sheetRef = ref(null)
|
|
|
|
|
const isDragging = ref(false)
|
|
|
|
|
const drag = { pending: null, active: false, startY: 0, startH: 0, startState: '', containerH: 0, maxH: 0, lastH: 0, contentEl: null }
|
|
|
|
|
const drag = { pending: null, active: false, startY: 0, startH: 0, startState: '', containerH: 0, maxH: 0, lastH: 0 }
|
|
|
|
|
let lastDragEndAt = 0
|
|
|
|
|
// A drag that starts on a button (caret / close X / strip) can still be followed
|
|
|
|
|
// by a synthesized click on release in some browsers — ignore it.
|
|
|
|
|
@ -573,6 +593,22 @@ const legendExpanded = computed(() => legendState.value !== 'collapsed')
|
|
|
|
|
const legendHeight = computed(() => uiStore.legendHeight)
|
|
|
|
|
const caretRotation = computed(() => uiStore.caretRotation)
|
|
|
|
|
const showPullTab = computed(() => uiStore.showPullTab)
|
|
|
|
|
// The content-fit open state (CSS .fit): open, at rest, with no height left behind by a drag.
|
|
|
|
|
const fitOpen = computed(() => legendState.value === 'open' && !uiStore._customLegendHeight)
|
|
|
|
|
// ★ ONE PHONE START STATE: COLLAPSED (mobile pass 2026-09-30). Every map's own mount code means
|
|
|
|
|
// "collapsed on a phone" (if (uiStore.isMobile) collapseLegend()), but uiStore.init() measures the
|
|
|
|
|
// breakpoint in a ResizeObserver whose first callback runs AFTER mount, so a map that asks right after
|
|
|
|
|
// init() reads the 'desktop' default and opens the sheet: Simple Research started open on every load
|
|
|
|
|
// at 390, while Prayer Progress, which measures synchronously, started collapsed. So the sheet applies
|
|
|
|
|
// the phone start state itself, once, the first time it sees the phone breakpoint, unless a people
|
|
|
|
|
// group is already selected. Collapsed because on a phone the map is the content: the collapsed strip
|
|
|
|
|
// still names the map, and the caption and the credit stay on screen.
|
|
|
|
|
let _phoneStartApplied = false
|
|
|
|
|
watch(() => uiStore.isMobile, (mobile) => {
|
|
|
|
|
if (!mobile || _phoneStartApplied) return
|
|
|
|
|
_phoneStartApplied = true
|
|
|
|
|
if (!uiStore.selectedPeopleGroup) uiStore.collapseLegend()
|
|
|
|
|
}, { immediate: true })
|
|
|
|
|
const legendMode = computed(() => uiStore.legendMode)
|
|
|
|
|
const selectedPeopleGroup = computed(() => uiStore.selectedPeopleGroup)
|
|
|
|
|
// APPROXPIN: the other groups on the selected pin's coordinate, from the roster THIS map draws.
|
|
|
|
|
@ -638,13 +674,10 @@ const miniImageUrl = computed(() => {
|
|
|
|
|
// stale custom px height was left behind.
|
|
|
|
|
//
|
|
|
|
|
// Gesture model:
|
|
|
|
|
// touchstart in the top GRAB_ZONE_PX of the sheet → this gesture is a resize.
|
|
|
|
|
// Below the zone, in DETAIL mode, the gesture is provisional: the
|
|
|
|
|
// FIRST touchmove decides (that is the only cancelable one once the
|
|
|
|
|
// browser starts a native scroll) — a pull UP while the sheet can
|
|
|
|
|
// still grow, or a pull DOWN with the content scrolled to its top,
|
|
|
|
|
// resizes the sheet; anything else is a normal content scroll.
|
|
|
|
|
// Non-detail tabs keep content scrolling untouched (qa 2026-05-03).
|
|
|
|
|
// touchstart in the top GRAB_ZONE_PX of the sheet (the handle: drag strip + title row) → this
|
|
|
|
|
// gesture is a resize. Anywhere below the handle the content scrolls, in every mode
|
|
|
|
|
// (mobile pass 2026-09-30: the detail view's content used to resize the sheet when it
|
|
|
|
|
// could not scroll that way, so a thumb reading the list moved the sheet instead).
|
|
|
|
|
// touchmove .dragging (transition:none) + an explicit px height every move,
|
|
|
|
|
// clamped to [COLLAPSED_PX, container - TOP_RESERVE_PX].
|
|
|
|
|
// touchend snap. Detail mode = ladder open ↔ fullyOpen with hysteresis
|
|
|
|
|
@ -658,9 +691,8 @@ function onSheetTouchStart(event) {
|
|
|
|
|
const sheet = sheetRef.value
|
|
|
|
|
if (!t || !sheet) return
|
|
|
|
|
const inGrabZone = (t.clientY - sheet.getBoundingClientRect().top) <= GRAB_ZONE_PX
|
|
|
|
|
const mode = inGrabZone ? 'grab' : (legendMode.value === 'detail' ? 'content' : null)
|
|
|
|
|
if (!mode) return
|
|
|
|
|
drag.pending = mode
|
|
|
|
|
if (!inGrabZone) return
|
|
|
|
|
drag.pending = 'grab'
|
|
|
|
|
drag.active = false
|
|
|
|
|
drag.startY = t.clientY
|
|
|
|
|
// offsetHeight is the CURRENT visual height even mid-transition, so a drag
|
|
|
|
|
@ -669,8 +701,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
|
|
|
|
|
drag.contentEl = sheet.querySelector('.legend-content')
|
|
|
|
|
drag.maxH = drag.containerH - TOP_RESERVE_PX - creditStripPx
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onSheetTouchMove(event) {
|
|
|
|
|
@ -680,13 +711,7 @@ function onSheetTouchMove(event) {
|
|
|
|
|
const dy = drag.startY - t.clientY // + = finger moved up = sheet grows
|
|
|
|
|
if (!drag.active) {
|
|
|
|
|
if (dy === 0) return
|
|
|
|
|
let take = drag.pending === 'grab'
|
|
|
|
|
if (drag.pending === 'content') {
|
|
|
|
|
const atTop = !drag.contentEl || drag.contentEl.scrollTop <= 0
|
|
|
|
|
take = dy > 0 ? drag.startH < drag.maxH - 1 : atTop
|
|
|
|
|
}
|
|
|
|
|
drag.pending = null
|
|
|
|
|
if (!take) return // native content scroll owns this gesture
|
|
|
|
|
drag.active = true
|
|
|
|
|
isDragging.value = true // .dragging → transition:none, same flush as the height
|
|
|
|
|
}
|
|
|
|
|
@ -704,7 +729,7 @@ function onSheetTouchEnd() {
|
|
|
|
|
lastDragEndAt = Date.now()
|
|
|
|
|
const h = drag.lastH
|
|
|
|
|
const travel = h - drag.startH
|
|
|
|
|
const openH = drag.containerH * 0.3 // the 'open' resting height (uiStore '30%')
|
|
|
|
|
const openH = drag.containerH * 0.3 // the collapse threshold's basis: half of this = 15% of the map
|
|
|
|
|
|
|
|
|
|
if (legendMode.value !== 'detail') {
|
|
|
|
|
// Free-drag tabs: leave the sheet where it was released (custom px stays,
|
|
|
|
|
@ -725,6 +750,68 @@ 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.
|
|
|
|
|
// The pin caption sits in the strip too, just above the controls (see the template), so the strip
|
|
|
|
|
// is the controls plus the caption: --legend-credit-controls places the caption, --legend-credit-strip
|
|
|
|
|
// places the sheet.
|
|
|
|
|
const STRIP_VAR = '--legend-credit-strip'
|
|
|
|
|
const CONTROLS_VAR = '--legend-credit-controls'
|
|
|
|
|
const CORNERS = '.maplibregl-ctrl-bottom-left, .maplibregl-ctrl-bottom-right, .mapboxgl-ctrl-bottom-left, .mapboxgl-ctrl-bottom-right'
|
|
|
|
|
let creditStripPx = 0
|
|
|
|
|
let creditControlsPx = 0
|
|
|
|
|
let _stripRO = null
|
|
|
|
|
let _stripTimers = []
|
|
|
|
|
const _stripObserved = new Set()
|
|
|
|
|
// The caption shows outside the sheet whenever the sheet's own copy is not on screen.
|
|
|
|
|
const floatingNoteShown = computed(() => !(legendMode.value === 'detail' && selectedPeopleGroup.value && legendState.value !== 'collapsed'))
|
|
|
|
|
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 controls = strip > 0 ? Math.ceil(strip) + 2 : 0
|
|
|
|
|
const note = area.querySelector(':scope > .approx-pin-note--floating')
|
|
|
|
|
const noteH = note ? Math.ceil(note.getBoundingClientRect().height) : 0
|
|
|
|
|
if (note && _stripRO && !_stripObserved.has(note)) { _stripRO.observe(note); _stripObserved.add(note) }
|
|
|
|
|
// The caption's box, its 2px bottom margin and a 2px gap under the sheet.
|
|
|
|
|
const px = controls + (noteH > 0 ? noteH + 4 : 0)
|
|
|
|
|
if (controls !== creditControlsPx) {
|
|
|
|
|
creditControlsPx = controls
|
|
|
|
|
_sheetVarTarget.style.setProperty(CONTROLS_VAR, controls + 'px')
|
|
|
|
|
}
|
|
|
|
|
if (px !== creditStripPx) {
|
|
|
|
|
creditStripPx = px
|
|
|
|
|
_sheetVarTarget.style.setProperty(STRIP_VAR, px + 'px')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
watch(floatingNoteShown, () => nextTick(measureCreditStrip))
|
|
|
|
|
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)
|
|
|
|
|
_sheetVarTarget.style.removeProperty(CONTROLS_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
|
|
|
|
|
@ -767,23 +854,26 @@ onBeforeUnmount(() => {
|
|
|
|
|
<!-- Containment wrapper: ensures position:absolute children are clipped to the map area
|
|
|
|
|
and never escape the shadow DOM host / staging frame. -->
|
|
|
|
|
<div class="legend-sheet-root">
|
|
|
|
|
<!-- ★ THE NOTE FLOATS ABOVE THE SHEET, IT DOES NOT SIT INSIDE IT. It used to be the last child
|
|
|
|
|
<!-- ★ THE NOTE FLOATS OUTSIDE THE SHEET, IT DOES NOT SIT INSIDE IT. It used to be the last child
|
|
|
|
|
of .legend-content, a natural-height sibling of a flex:1 options panel — so every pixel it
|
|
|
|
|
took was a pixel the options' inner scroller lost, and the reader scrolled further inside
|
|
|
|
|
the tree to reach the same rows. Moving it WITHIN the sheet would not have helped: the
|
|
|
|
|
sheet's height is fixed at 30%, so anything inside it costs the options the same either
|
|
|
|
|
way. Out here it costs them nothing and the options keep the full sheet.
|
|
|
|
|
Positioned off --legend-sheet-h, which a ResizeObserver already writes for the engine's
|
|
|
|
|
own bottom controls, so it tracks the sheet as it opens, drags and collapses rather than
|
|
|
|
|
assuming a height. -->
|
|
|
|
|
the tree to reach the same rows. Out here it costs them nothing and the options keep the
|
|
|
|
|
full sheet.
|
|
|
|
|
★ UNDER THE SHEET, IN THE CREDIT STRIP, IN EVERY SHEET STATE (mobile pass 2026-09-30). It
|
|
|
|
|
used to float above the sheet and hide while the sheet was collapsed; with the tree's copy
|
|
|
|
|
switched off, a collapsed sheet showed the approximate-location caveat 0 times (measured at
|
|
|
|
|
390 on Simple Research and Prayer Progress), and an open one put it under the pin-hint pill.
|
|
|
|
|
It now shows whenever the sheet's own copy is not on screen: always, except in an open detail
|
|
|
|
|
view, which keeps its copy inside the sheet. A collapsed detail view carries the same
|
|
|
|
|
shared-point wording here. measureCreditStrip() counts its height into the strip. -->
|
|
|
|
|
<ApproxPinNote
|
|
|
|
|
v-if="legendMode !== 'detail' && legendState !== 'collapsed'"
|
|
|
|
|
v-if="floatingNoteShown"
|
|
|
|
|
class="approx-pin-note--floating"
|
|
|
|
|
:class="{ 'approx-pin-note--floating-dark': isDark }"
|
|
|
|
|
:shared="legendMode === 'detail' ? selectedShared : []"
|
|
|
|
|
:dark="isDark"
|
|
|
|
|
/>
|
|
|
|
|
<!-- Floating mini-image: fixed 16px above the sheet's resting position (30%).
|
|
|
|
|
The sheet slides UP over it — the image never moves. -->
|
|
|
|
|
<!-- Floating mini-image: 16px above the open sheet's top edge (it shows only in that state). -->
|
|
|
|
|
<div
|
|
|
|
|
v-if="legendMode === 'detail' && selectedPeopleGroup && legendState === 'open' && miniImageUrl"
|
|
|
|
|
class="floating-mini-image"
|
|
|
|
|
@ -802,8 +892,8 @@ onBeforeUnmount(() => {
|
|
|
|
|
<div
|
|
|
|
|
ref="sheetRef"
|
|
|
|
|
class="legend-mobile-sheet"
|
|
|
|
|
:class="[legendState, { 'sheet-dark': isDark, dragging: isDragging }]"
|
|
|
|
|
:style="{ height: legendHeight, '--legend-sheet-top-reserve': TOP_RESERVE_PX + 'px' }"
|
|
|
|
|
:class="[legendState, { 'sheet-dark': isDark, dragging: isDragging, fit: fitOpen }]"
|
|
|
|
|
:style="{ height: fitOpen ? 'auto' : legendHeight, '--legend-sheet-top-reserve': TOP_RESERVE_PX + 'px' }"
|
|
|
|
|
@touchstart="onSheetTouchStart"
|
|
|
|
|
@touchmove="onSheetTouchMove"
|
|
|
|
|
@touchend="onSheetTouchEnd"
|
|
|
|
|
@ -890,9 +980,15 @@ onBeforeUnmount(() => {
|
|
|
|
|
<!-- Semantic-tree tabs (Languages, People Groups, WAGF Regions):
|
|
|
|
|
SemanticTreeLegend (PPLR-ported, R4). Caret injected via
|
|
|
|
|
#title-caret slot so it sits in column 1 of the .stl-titlebar,
|
|
|
|
|
vertically aligned with the title text. -->
|
|
|
|
|
vertically aligned with the title text.
|
|
|
|
|
★ :pin-note="false": this sheet already shows the pin caption ONCE, floating above it
|
|
|
|
|
(the ApproxPinNote at the top of this template). The tree's own copy printed the same
|
|
|
|
|
sentence a second time inside the open sheet (measured on Prayer Progress at 390, en and
|
|
|
|
|
ar: 2 visible copies with the sheet open). The detail view keeps its own note, which
|
|
|
|
|
carries the shared-point wording. -->
|
|
|
|
|
<SemanticTreeLegend
|
|
|
|
|
v-else-if="legendType !== 'default'"
|
|
|
|
|
:pin-note="false"
|
|
|
|
|
:nodes="treeForMobile"
|
|
|
|
|
:tabs="tabsForMobile"
|
|
|
|
|
:default-tab="defaultTab"
|
|
|
|
|
|