Prayer Walk Planner: on a phone the zone list opens from a ☰ Zones button instead of being hidden

On a narrow screen the list used to be display:none, so a walker could not see, select, rename or delete a zone there. It is now a drawer over the map from the reading-end edge (right in English, left in Arabic), behind a ☰ Zones (n) button that leads the toolbar; a tap outside, × or Escape closes it and focus returns to the button. On a wide screen it is the sidebar as before, its border now on the reading side. Two words in the planner's own 11-locale strings: zones, closeZones.
This commit is contained in:
Doxa Maps 2026-09-29 02:40:02 +00:00
parent 1bbc5ff871
commit 7b5de8bcfa
2 changed files with 42 additions and 14 deletions

View file

@ -106,6 +106,14 @@ const uiLocale = computed(() => {
const uiDir = computed(() => (uiLocale.value === 'ar' ? 'rtl' : 'ltr'))
const tr = (key, params) => translate(uiLocale.value, key, params)
const moreOpen = ref(false) // the phone "More" menu
// PHONE: the zone list folds behind a ☰ button (Driver, test round 2 #8) and opens as a drawer over the
// map; on a wide screen it is the sidebar as before. It used to be simply hidden on a phone, so a
// walker could not see, select, rename or delete a zone there.
const listOpen = ref(false)
const zonesBtn = ref(null), zonesClose = ref(null)
function openZones() { listOpen.value = true; nextTick(() => zonesClose.value?.focus?.()) }
function closeZones() { listOpen.value = false; nextTick(() => zonesBtn.value?.focus?.()) }
function onZonesKey(e) { if (e.key === 'Escape' && listOpen.value) { e.preventDefault(); e.stopPropagation(); closeZones() } }
// The shared controls (Share, Full screen) label themselves through the vue-i18n instance index.js
// installs, which reads only the PAGE language. Inside the Research Gallery the language arrives in
@ -644,7 +652,8 @@ useShadowStyles(
'.pwp-map-el{position:absolute;inset:0}' +
'.pwp-map-controls{position:absolute;top:10px;right:10px;z-index:4;display:flex;flex-direction:column;gap:8px}' +
'.pwp[dir=rtl] .pwp-map-controls{right:auto;left:10px}' +
'.pwp-list{width:260px;flex-shrink:0;overflow-y:auto;background:#fff;border-left:1px solid #d1d4cd;padding:8px}' +
'.pwp-list{width:260px;flex-shrink:0;overflow-y:auto;background:#fff;border-inline-start:1px solid #d1d4cd;padding:8px}' +
'.pwp-zones-btn,.pwp-list-head,.pwp-backdrop{display:none}' +
'.pwp-list-empty{padding:16px 10px;font-size:12px;color:#6d766e}' +
'.pwp-item{display:flex;align-items:center;gap:8px;padding:8px;border-radius:6px;margin-bottom:4px;' +
'border:1px solid transparent;cursor:pointer}' +
@ -654,7 +663,18 @@ useShadowStyles(
'.pwp-item-title-input{flex:1;font-size:13px;font-weight:600;padding:2px 4px;border:1px solid #92b195;border-radius:4px}' +
'.pwp-item-del{border:none;background:transparent;color:#8a3b3b;cursor:pointer;font-size:14px;padding:2px 6px;border-radius:4px}' +
'.pwp-item-del:hover{background:rgba(138,59,59,.12)}' +
'@media (max-width:640px){.pwp-list{display:none}}' +
// PHONE: the list is a drawer from the reading-end edge, over the map, behind the ☰ Zones button.
'@media (max-width:640px){' +
'.pwp-list{display:none;position:absolute;inset-block:0;inset-inline-end:0;width:min(300px,86%);z-index:7;' +
'box-shadow:0 0 24px rgba(0,0,0,.22);border-inline-start:none}' +
'.pwp-list.is-open{display:block}' +
'.pwp-list-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:2px 2px 8px;' +
'margin-bottom:6px;border-bottom:1px solid #eceee9;font-size:13px;font-weight:700}' +
'.pwp-list-x{border:none;background:transparent;font-size:20px;line-height:1;cursor:pointer;color:#6d766e;' +
'min-width:36px;min-height:36px;border-radius:6px}' +
'.pwp-backdrop{display:block;position:absolute;inset:0;z-index:6;background:rgba(20,28,22,.28)}' +
'.pwp-zones-btn{display:inline-flex;order:-3}' +
'}' +
// PHONE: ONE ROW, SCROLLED SIDEWAYS, instead of a toolbar that wrapped to three rows (139px of a
// 844px screen, measured 2026-09-25) above the map a walker is trying to read. Nothing is clipped:
// the row scrolls, labels never truncate. The two actions a walk needs, Draw and Print, lead the
@ -679,6 +699,9 @@ useShadowStyles(
<template>
<div class="pwp" :dir="uiDir" :lang="uiLocale">
<div class="pwp-toolbar">
<!-- PHONE: the zone list's hamburger (hidden on a wide screen, where the list is a sidebar). -->
<button ref="zonesBtn" type="button" class="pwp-btn pwp-zones-btn" :aria-expanded="listOpen ? 'true' : 'false'"
aria-controls="pwp-zone-list" @click="listOpen ? closeZones() : openZones()">☰ {{ tr('zones', { n: polygons.length }) }}</button>
<button type="button" class="pwp-btn pwp-draw" :class="{ 'is-active': drawing }"
:aria-pressed="drawing" @click="onDrawClick">
{{ drawing ? tr('drawing') : tr('draw') }}
@ -736,7 +759,12 @@ useShadowStyles(
<!-- The "?" asks the host for this map's help (doxa:help); it shows only where a host answers. -->
<div class="pwp-map-controls"><FullscreenButton :map-container="mapContainer" /><HelpButton hosted /></div>
</div>
<div class="pwp-list">
<div v-if="listOpen" class="pwp-backdrop" @click="closeZones"></div>
<div id="pwp-zone-list" class="pwp-list" :class="{ 'is-open': listOpen }" @keydown="onZonesKey">
<div class="pwp-list-head">
<span>{{ tr('zones', { n: polygons.length }) }}</span>
<button ref="zonesClose" type="button" class="pwp-list-x" :aria-label="tr('closeZones')" @click="closeZones">×</button>
</div>
<div v-if="polygons.length === 0" class="pwp-list-empty">
{{ tr('emptyList') }}
</div>

View file

@ -12,7 +12,7 @@ export const STRINGS = {
en: {
draw: 'Draw polygon', drawing: 'Drawing… (click map)', finish: 'Finish polygon ({n} pts)',
deleteSelected: 'Delete selected', deleteSelectedAria: 'Delete selected polygons',
more: 'More', selectAll: 'Select all', clearSelection: 'Clear selection', countSelected: '{n} / {total} selected',
more: 'More', zones: 'Zones ({n})', closeZones: 'Close the zone list', selectAll: 'Select all', clearSelection: 'Clear selection', countSelected: '{n} / {total} selected',
printSelected: 'Print selected', printAria: 'Print selected prayer-walking zones',
emptyList: 'No zones yet — draw a polygon on the map to start a prayer-walking plan.',
delete: 'Delete', deleteAria: 'Delete {title}', zone: 'Zone {n}',
@ -31,7 +31,7 @@ export const STRINGS = {
es: {
draw: 'Dibujar polígono', drawing: 'Dibujando… (haz clic en el mapa)', finish: 'Terminar polígono ({n} puntos)',
deleteSelected: 'Eliminar selección', deleteSelectedAria: 'Eliminar los polígonos seleccionados',
more: 'Más', selectAll: 'Seleccionar todo', clearSelection: 'Quitar selección', countSelected: '{n} / {total} seleccionadas',
more: 'Más', zones: 'Zonas ({n})', closeZones: 'Cerrar la lista de zonas', selectAll: 'Seleccionar todo', clearSelection: 'Quitar selección', countSelected: '{n} / {total} seleccionadas',
printSelected: 'Imprimir selección', printAria: 'Imprimir las zonas de caminata de oración seleccionadas',
emptyList: 'Aún no hay zonas: dibuja un polígono en el mapa para empezar un plan de caminata de oración.',
delete: 'Eliminar', deleteAria: 'Eliminar {title}', zone: 'Zona {n}',
@ -50,7 +50,7 @@ export const STRINGS = {
fr: {
draw: 'Dessiner un polygone', drawing: 'Dessin en cours… (cliquez sur la carte)', finish: 'Terminer le polygone ({n} points)',
deleteSelected: 'Supprimer la sélection', deleteSelectedAria: 'Supprimer les polygones sélectionnés',
more: 'Plus', selectAll: 'Tout sélectionner', clearSelection: 'Effacer la sélection', countSelected: '{n} / {total} sélectionnées',
more: 'Plus', zones: 'Zones ({n})', closeZones: 'Fermer la liste des zones', selectAll: 'Tout sélectionner', clearSelection: 'Effacer la sélection', countSelected: '{n} / {total} sélectionnées',
printSelected: 'Imprimer la sélection', printAria: 'Imprimer les zones de marche de prière sélectionnées',
emptyList: 'Aucune zone pour l’instant — dessinez un polygone sur la carte pour commencer un plan de marche de prière.',
delete: 'Supprimer', deleteAria: 'Supprimer {title}', zone: 'Zone {n}',
@ -69,7 +69,7 @@ export const STRINGS = {
de: {
draw: 'Polygon zeichnen', drawing: 'Zeichnen… (auf die Karte klicken)', finish: 'Polygon abschließen ({n} Punkte)',
deleteSelected: 'Auswahl löschen', deleteSelectedAria: 'Ausgewählte Polygone löschen',
more: 'Mehr', selectAll: 'Alle auswählen', clearSelection: 'Auswahl aufheben', countSelected: '{n} / {total} ausgewählt',
more: 'Mehr', zones: 'Zonen ({n})', closeZones: 'Zonenliste schließen', selectAll: 'Alle auswählen', clearSelection: 'Auswahl aufheben', countSelected: '{n} / {total} ausgewählt',
printSelected: 'Auswahl drucken', printAria: 'Ausgewählte Gebetsspaziergang-Zonen drucken',
emptyList: 'Noch keine Zonen — zeichnen Sie ein Polygon auf der Karte, um einen Gebetsspaziergang zu planen.',
delete: 'Löschen', deleteAria: '{title} löschen', zone: 'Zone {n}',
@ -88,7 +88,7 @@ export const STRINGS = {
pt: {
draw: 'Desenhar polígono', drawing: 'Desenhando… (clique no mapa)', finish: 'Concluir polígono ({n} pontos)',
deleteSelected: 'Excluir seleção', deleteSelectedAria: 'Excluir os polígonos selecionados',
more: 'Mais', selectAll: 'Selecionar tudo', clearSelection: 'Limpar seleção', countSelected: '{n} / {total} selecionadas',
more: 'Mais', zones: 'Zonas ({n})', closeZones: 'Fechar a lista de zonas', selectAll: 'Selecionar tudo', clearSelection: 'Limpar seleção', countSelected: '{n} / {total} selecionadas',
printSelected: 'Imprimir seleção', printAria: 'Imprimir as zonas de caminhada de oração selecionadas',
emptyList: 'Ainda não há zonas — desenhe um polígono no mapa para começar um plano de caminhada de oração.',
delete: 'Excluir', deleteAria: 'Excluir {title}', zone: 'Zona {n}',
@ -107,7 +107,7 @@ export const STRINGS = {
it: {
draw: 'Disegna poligono', drawing: 'Disegno in corso… (clicca sulla mappa)', finish: 'Completa poligono ({n} punti)',
deleteSelected: 'Elimina selezionati', deleteSelectedAria: 'Elimina i poligoni selezionati',
more: 'Altro', selectAll: 'Seleziona tutto', clearSelection: 'Deseleziona', countSelected: '{n} / {total} selezionate',
more: 'Altro', zones: 'Zone ({n})', closeZones: 'Chiudi l’elenco delle zone', selectAll: 'Seleziona tutto', clearSelection: 'Deseleziona', countSelected: '{n} / {total} selezionate',
printSelected: 'Stampa selezionate', printAria: 'Stampa le zone di passeggiata di preghiera selezionate',
emptyList: 'Ancora nessuna zona — disegna un poligono sulla mappa per iniziare un piano di passeggiata di preghiera.',
delete: 'Elimina', deleteAria: 'Elimina {title}', zone: 'Zona {n}',
@ -126,7 +126,7 @@ export const STRINGS = {
ro: {
draw: 'Desenează poligon', drawing: 'Se desenează… (dă clic pe hartă)', finish: 'Finalizează poligonul ({n} puncte)',
deleteSelected: 'Șterge selecția', deleteSelectedAria: 'Șterge poligoanele selectate',
more: 'Mai mult', selectAll: 'Selectează tot', clearSelection: 'Anulează selecția', countSelected: '{n} / {total} selectate',
more: 'Mai mult', zones: 'Zone ({n})', closeZones: 'Închideți lista zonelor', selectAll: 'Selectează tot', clearSelection: 'Anulează selecția', countSelected: '{n} / {total} selectate',
printSelected: 'Tipărește selecția', printAria: 'Tipărește zonele de plimbare de rugăciune selectate',
emptyList: 'Încă nu există zone — desenează un poligon pe hartă pentru a începe un plan de plimbare de rugăciune.',
delete: 'Șterge', deleteAria: 'Șterge {title}', zone: 'Zona {n}',
@ -145,7 +145,7 @@ export const STRINGS = {
ru: {
draw: 'Нарисовать область', drawing: 'Рисование… (щёлкайте по карте)', finish: 'Завершить область (точек: {n})',
deleteSelected: 'Удалить выбранные', deleteSelectedAria: 'Удалить выбранные области',
more: 'Ещё', selectAll: 'Выбрать все', clearSelection: 'Снять выбор', countSelected: 'Выбрано: {n} из {total}',
more: 'Ещё', zones: 'Зоны ({n})', closeZones: 'Закрыть список зон', selectAll: 'Выбрать все', clearSelection: 'Снять выбор', countSelected: 'Выбрано: {n} из {total}',
printSelected: 'Печать выбранных', printAria: 'Печать выбранных зон молитвенной прогулки',
emptyList: 'Зон пока нет — нарисуйте область на карте, чтобы начать план молитвенной прогулки.',
delete: 'Удалить', deleteAria: 'Удалить: {title}', zone: 'Зона {n}',
@ -164,7 +164,7 @@ export const STRINGS = {
ar: {
draw: 'ارسم مضلّعًا', drawing: 'جارٍ الرسم… (انقر على الخريطة)', finish: 'إنهاء المضلّع ({n} نقاط)',
deleteSelected: 'حذف المحدد', deleteSelectedAria: 'حذف المضلّعات المحددة',
more: 'المزيد', selectAll: 'تحديد الكل', clearSelection: 'إلغاء التحديد', countSelected: '{n} / {total} محدد',
more: 'المزيد', zones: 'المناطق ({n})', closeZones: 'إغلاق قائمة المناطق', selectAll: 'تحديد الكل', clearSelection: 'إلغاء التحديد', countSelected: '{n} / {total} محدد',
printSelected: 'طباعة المحدد', printAria: 'طباعة مناطق جولات الصلاة المحددة',
emptyList: 'لا توجد مناطق بعد — ارسم مضلّعًا على الخريطة لتبدأ خطة جولة صلاة.',
delete: 'حذف', deleteAria: 'حذف {title}', zone: 'المنطقة {n}',
@ -183,7 +183,7 @@ export const STRINGS = {
hi: {
draw: 'बहुभुज बनाएँ', drawing: 'बनाया जा रहा है… (मानचित्र पर क्लिक करें)', finish: 'बहुभुज पूरा करें ({n} बिंदु)',
deleteSelected: 'चयनित हटाएँ', deleteSelectedAria: 'चयनित बहुभुज हटाएँ',
more: 'और', selectAll: 'सभी चुनें', clearSelection: 'चयन हटाएँ', countSelected: '{n} / {total} चयनित',
more: 'और', zones: 'क्षेत्र ({n})', closeZones: 'क्षेत्रों की सूची बंद करें', selectAll: 'सभी चुनें', clearSelection: 'चयन हटाएँ', countSelected: '{n} / {total} चयनित',
printSelected: 'चयनित प्रिंट करें', printAria: 'चयनित प्रार्थना पदयात्रा क्षेत्र प्रिंट करें',
emptyList: 'अभी कोई क्षेत्र नहीं — प्रार्थना पदयात्रा की योजना शुरू करने के लिए मानचित्र पर बहुभुज बनाएँ।',
delete: 'हटाएँ', deleteAria: '{title} हटाएँ', zone: 'क्षेत्र {n}',
@ -202,7 +202,7 @@ export const STRINGS = {
zh: {
draw: '绘制多边形', drawing: '正在绘制…(点击地图)', finish: '完成多边形({n} 个点)',
deleteSelected: '删除所选', deleteSelectedAria: '删除所选多边形',
more: '更多', selectAll: '全选', clearSelection: '清除选择', countSelected: '已选 {n} / {total}',
more: '更多', zones: '区域({n})', closeZones: '关闭区域列表', selectAll: '全选', clearSelection: '清除选择', countSelected: '已选 {n} / {total}',
printSelected: '打印所选', printAria: '打印所选的祷告行走区域',
emptyList: '还没有区域——在地图上绘制一个多边形,开始制定祷告行走计划。',
delete: '删除', deleteAria: '删除{title}', zone: '区域 {n}',