doxa-maps/library/components
Doxa Maps a71cc4b5e8 Pan the map when a popup would open half outside it
The popup is built with no explicit anchor, so the engine puts it above the pin
when there is room and below when there is not. Measured on the drop-in: a 452px
popup on a pin in the lower half of a 780px map fits NEITHER way, so it opens
below and the map's overflow:hidden cuts off the Pray and Full Profile buttons.
A real click at their centres then lands on the page behind, though a scripted
click still navigates -- so the buttons look present and are not reachable.

Moving the PIN is what makes an anchor fit, so the map pans rather than the
popup changing. Its design is untouched, which is what keeps this a clone; it is
what Leaflet does by default. The alternative, a shorter popup, would have been
a visible departure and a product decision.

The pan is the smallest one that works, and most pins need none: on a 780px map
only a narrow band moves at all, by 74px at the worst position rather than a
jump. It is animated over 200ms.

It resolves at every width this component renders at. The container is
max(780, 16/9 of width) at >=768px and twice the width below that, so even at
320px wide there is 640px of container against a 452px popup -- 164px of
headroom, and the popup would have to grow by that much before any width could
overflow. So no inner scroll and no residual case.

It returns 0 rather than panning when the popup is TALLER than its container: a
host can give the map a short slot, and there a pan would only move the clipping
around. The failure stays what it is today instead of becoming a clipped popup
and a camera that jumped. Unmeasurable inputs -- a map mid-load, a zero-height
container, an unprojectable point -- also return 0 rather than throwing, and the
pan itself is wrapped, because a pan is a nicety and must never break a click.

The offset lives in one constant now. The pan arithmetic has to use the same
number the popup was built with, and two literals drift.

Proven against the shipping function, lifted out of this file by source rather
than restated: 16 checks, including every pin position at 320, 390, 768 and
1440px wide -- none left clipped. The fit check is stated independently of the
function, because an earlier version asked the function whether it had
succeeded, and that hid a dropped offset: the pan was 12px short and its own
test 12px lax, so the two agreed while the button still clipped. Mutation-tested
four ways -- dropped offset, zeroed offset constant, flipped sign, removed
squeeze guard -- each caught.

Both consumers rebuilt: country-map +623 B, regions-dashboard +672 B.
2026-09-29 09:18:56 +00:00
..
charts library/ keeps the reason and drops the speaker: no comment quotes or cites the Driver 2026-09-29 05:26:25 +00:00
dashboards library/ keeps the reason and drops the speaker: no comment quotes or cites the Driver 2026-09-29 05:26:25 +00:00
hints library/ keeps the reason and drops the speaker: no comment quotes or cites the Driver 2026-09-29 05:26:25 +00:00
legends Size the legend's compact population to its fixed column in every locale 2026-09-29 08:33:44 +00:00
map-controls library/ keeps the reason and drops the speaker: no comment quotes or cites the Driver 2026-09-29 05:26:25 +00:00
maps Pan the map when a popup would open half outside it 2026-09-29 09:18:56 +00:00
poster Speak the reader's language on the UPG-100 map, its wall and the print HUD 2026-09-29 08:33:43 +00:00
qa Comments state their findings instead of citing files outside the repo 2026-09-29 03:46:37 +00:00
regions library/ keeps the reason and drops the speaker: no comment quotes or cites the Driver 2026-09-29 05:26:25 +00:00
search The browser-side generator keeps the reason and drops the speaker 2026-09-29 05:26:27 +00:00
tables DataTable + StackLayout: physical left/right -> logical, where the rule mirrors 2026-09-28 09:48:45 +00:00
BackToTopButton.vue Shipped comments describe the behaviour instead of quoting a person 2026-09-28 17:40:35 +00:00
LargerScreenNotice.vue The phone notice holds its place above any profile, and pads the right side in Arabic 2026-09-29 08:11:49 +00:00
PeopleGroupDetail.vue Every animating library component now honours prefers-reduced-motion itself 2026-09-29 01:38:07 +00:00
README.md consolidate docs into documentation/ and deployment shells into deployment/ 2026-09-21 23:23:23 +00:00
ResearchMapFilterPanel.vue Four shared controls get a 24x24 target at every pointer, not only on touch (WCAG 2.5.8) 2026-09-26 13:35:57 +00:00
ResearchMapSideMenu.vue Every animating library component now honours prefers-reduced-motion itself 2026-09-29 01:38:07 +00:00
SideMenuDrawer.vue Every animating library component now honours prefers-reduced-motion itself 2026-09-29 01:38:07 +00:00
ViewSelect.vue Every animating library component now honours prefers-reduced-motion itself 2026-09-29 01:38:07 +00:00
WARNING.md Complete app-profiles->plugin-designs sweep across comments/docs; build green; layouts kept at root (load-bearing) 2026-09-17 18:18:19 +00:00

library/components/ — shared Vue components

The map UI, built from parameterized pieces every app-profile reuses. Fix one here and every map benefits — never edit a shared component to customize one map.

  library/components/
  ┌───────────────────────────────────────────┐
  │ legends/        map-controls/   poster/      │
  │ charts/         tables/                       │
  │ PeopleGroupDetail.vue   SideMenuDrawer.vue    │
  │ ResearchMapFilterPanel.vue  …SideMenu.vue     │
  └───────────────────────────────────────────┘
     reused by every app-profile via @map/components/…
  • legends/, map-controls/, poster/ each have their own README.
  • Every component uses <script setup>, calls useShadowStyles(), and reaches stores via inject('mapStore' | 'uiStore' | 'dataStore') — never a direct store import.

Full catalog + conventions: ../../documentation/architecture/components.md · per-file roles: ../../documentation/reference.md.