Plugin Studio: collapse the header to give the plugin the whole page, and bring it back in one click

A builder previewing a plugin full screen can hide the studio header (the props panel: language, map
engine, Mapbox token). The header's new "Hide header" button collapses it, as the Research Gallery runs
headerless. A "Show header" button then stays pinned in the top corner, above every plugin, so the
props are always one click away. The choice is remembered in this browser (plugin-studio:header-hidden).
Strings in all 11 studio catalogues. BUNDLE=plugin-studio builds.
This commit is contained in:
Doxa Maps 2026-09-26 04:38:11 +00:00
parent 65017fefa4
commit c9dfb668b3
12 changed files with 59 additions and 12 deletions

View file

@ -46,5 +46,7 @@
"engineDefault": "الافتراضي لكل خريطة",
"engineMapboxNote": "يحتاج إلى رمز",
"engineMaplibreNote": "لا يحتاج إلى رمز",
"tokenNotNeeded": "تم اختيار MapLibre: خرائطه لا تحتاج إلى رمز Mapbox"
"tokenNotNeeded": "تم اختيار MapLibre: خرائطه لا تحتاج إلى رمز Mapbox",
"hideHeader": "إخفاء الشريط العلوي",
"showHeader": "إظهار الشريط العلوي"
}

View file

@ -46,5 +46,7 @@
"engineDefault": "Je Karte eigenes",
"engineMapboxNote": "braucht einen Token",
"engineMaplibreNote": "kein Token nötig",
"tokenNotNeeded": "MapLibre ist gewählt: Seine Karten brauchen keinen Mapbox-Token"
"tokenNotNeeded": "MapLibre ist gewählt: Seine Karten brauchen keinen Mapbox-Token",
"hideHeader": "Kopfzeile ausblenden",
"showHeader": "Kopfzeile einblenden"
}

View file

@ -46,5 +46,7 @@
"engineDefault": "Each map’s own",
"engineMapboxNote": "needs a token",
"engineMaplibreNote": "no token needed",
"tokenNotNeeded": "MapLibre is selected: its maps need no Mapbox token"
"tokenNotNeeded": "MapLibre is selected: its maps need no Mapbox token",
"hideHeader": "Hide header",
"showHeader": "Show header"
}

View file

@ -46,5 +46,7 @@
"engineDefault": "El de cada mapa",
"engineMapboxNote": "necesita un token",
"engineMaplibreNote": "no necesita token",
"tokenNotNeeded": "MapLibre está seleccionado: sus mapas no necesitan token de Mapbox"
"tokenNotNeeded": "MapLibre está seleccionado: sus mapas no necesitan token de Mapbox",
"hideHeader": "Ocultar encabezado",
"showHeader": "Mostrar encabezado"
}

View file

@ -46,5 +46,7 @@
"engineDefault": "Celui de chaque carte",
"engineMapboxNote": "nécessite un jeton",
"engineMaplibreNote": "aucun jeton requis",
"tokenNotNeeded": "MapLibre est sélectionné : ses cartes n’ont pas besoin de jeton Mapbox"
"tokenNotNeeded": "MapLibre est sélectionné : ses cartes n’ont pas besoin de jeton Mapbox",
"hideHeader": "Masquer l'en-tête",
"showHeader": "Afficher l'en-tête"
}

View file

@ -46,5 +46,7 @@
"engineDefault": "हर मानचित्र का अपना",
"engineMapboxNote": "टोकन चाहिए",
"engineMaplibreNote": "टोकन की ज़रूरत नहीं",
"tokenNotNeeded": "MapLibre चुना गया है: इसके मानचित्रों को Mapbox टोकन नहीं चाहिए"
"tokenNotNeeded": "MapLibre चुना गया है: इसके मानचित्रों को Mapbox टोकन नहीं चाहिए",
"hideHeader": "हेडर छिपाएँ",
"showHeader": "हेडर दिखाएँ"
}

View file

@ -46,5 +46,7 @@
"engineDefault": "Quello di ogni mappa",
"engineMapboxNote": "richiede un token",
"engineMaplibreNote": "nessun token necessario",
"tokenNotNeeded": "MapLibre è selezionato: le sue mappe non richiedono un token Mapbox"
"tokenNotNeeded": "MapLibre è selezionato: le sue mappe non richiedono un token Mapbox",
"hideHeader": "Nascondi intestazione",
"showHeader": "Mostra intestazione"
}

View file

@ -46,5 +46,7 @@
"engineDefault": "O de cada mapa",
"engineMapboxNote": "precisa de um token",
"engineMaplibreNote": "não precisa de token",
"tokenNotNeeded": "MapLibre está selecionado: os seus mapas não precisam de token do Mapbox"
"tokenNotNeeded": "MapLibre está selecionado: os seus mapas não precisam de token do Mapbox",
"hideHeader": "Ocultar cabeçalho",
"showHeader": "Mostrar cabeçalho"
}

View file

@ -46,5 +46,7 @@
"engineDefault": "Al fiecărei hărți",
"engineMapboxNote": "necesită un token",
"engineMaplibreNote": "nu necesită token",
"tokenNotNeeded": "MapLibre este selectat: hărțile sale nu necesită token Mapbox"
"tokenNotNeeded": "MapLibre este selectat: hărțile sale nu necesită token Mapbox",
"hideHeader": "Ascunde antetul",
"showHeader": "Afișează antetul"
}

View file

@ -46,5 +46,7 @@
"engineDefault": "Свой у каждой карты",
"engineMapboxNote": "нужен токен",
"engineMaplibreNote": "токен не нужен",
"tokenNotNeeded": "Выбран MapLibre: его картам не нужен токен Mapbox"
"tokenNotNeeded": "Выбран MapLibre: его картам не нужен токен Mapbox",
"hideHeader": "Скрыть заголовок",
"showHeader": "Показать заголовок"
}

View file

@ -46,5 +46,7 @@
"engineDefault": "各地图自身的",
"engineMapboxNote": "需要令牌",
"engineMaplibreNote": "无需令牌",
"tokenNotNeeded": "已选择 MapLibre:其地图无需 Mapbox 令牌"
"tokenNotNeeded": "已选择 MapLibre:其地图无需 Mapbox 令牌",
"hideHeader": "隐藏标题栏",
"showHeader": "显示标题栏"
}

View file

@ -55,7 +55,7 @@
:dir="uiDir"
:nav-label="tr('navLabel')"
:labels="{ toggleList: tr('toggleList'), openViews: tr('openViews'), expandList: tr('expandList'), collapseList: tr('collapseList'), collapse: tr('collapse') }"
:header="true"
:header="!headerHidden"
:active="activeKey"
@update:active="select">
@ -142,6 +142,14 @@
<p class="ps-token-note">{{ tr('tokenStoredHere') }}</p>
</form>
</span>
<!-- COLLAPSE THE HEADER: the plugin takes the full height (as the Research Gallery does), and the
floating button below brings the header back — the header is the props panel, so it must be
one click away. The choice is remembered in this browser. -->
<button type="button" class="ps-locale-btn ps-hide-header" :title="tr('hideHeader')" :aria-label="tr('hideHeader')"
@click="setHeaderHidden(true)">
<span aria-hidden="true">⌃</span>
<span class="ps-locale-name">{{ tr('hideHeader') }}</span>
</button>
</template>
<!-- Toolbar strip: the active element's identity and copy-embed. Kept to ONE line on purpose —
@ -180,6 +188,11 @@
GPU context (measured: canvas count 1→0 and webglcontextlost fired). Without this,
every map ever visited would hold a live context until the browser silently dropped
the OLDEST one and an old tab went blank with no error. -->
<!-- The way back when the header is collapsed: always visible, top corner, above every plugin. -->
<button v-if="headerHidden" type="button" class="ps-show-header" :title="tr('showHeader')" :aria-label="tr('showHeader')"
@click="setHeaderHidden(false)">
<span aria-hidden="true">⌄</span> {{ tr('showHeader') }}
</button>
<div class="ps-stage">
<section
v-for="row in flatRows" :key="row.key" class="ps-pane"
@ -210,6 +223,7 @@ import { publishMapToken, clearMapToken } from '@map/composables/useSharedMapTok
// never sent anywhere but Mapbox (the token) or the URL the reader typed (the service).
const TOKEN_KEY = 'plugin-studio:mapbox-token'
const TOKEN_URL_KEY = 'plugin-studio:mapbox-token-url'
const HEADER_KEY = 'plugin-studio:header-hidden' // '1' = collapsed
const PROVIDER_KEY = 'plugin-studio:map-provider' // 'mapbox' | 'maplibre' | '' (each map's own default)
function readStored(k) { try { return (typeof window !== 'undefined' && window.localStorage.getItem(k)) || '' } catch (_) { return '' } }
function writeStored(k, v) { try { if (v) window.localStorage.setItem(k, v); else window.localStorage.removeItem(k) } catch (_) { /* storage unavailable */ } }
@ -231,6 +245,8 @@ const PS_SHADOW_CSS = `
.vtl-top { position:relative; z-index:3; }
.vtl-body { isolation:isolate; }
.ps-token { position:relative; display:inline-flex; }
.ps-show-header { position:fixed; top:8px; inset-inline-end:8px; z-index:2147483000; display:inline-flex; align-items:center; gap:6px; padding:6px 12px; border:1px solid #d1d4cd; border-radius:999px; background:rgba(255,255,255,.94); color:#3b463d; font:600 12px system-ui, sans-serif; cursor:pointer; box-shadow:0 4px 14px rgba(0,0,0,.18); }
.ps-show-header:hover, .ps-show-header:focus-visible { background:#ffffff; outline:2px solid #92b195; }
.ps-token-dot { width:8px; height:8px; border-radius:50%; background:#b8bdb4; }
.ps-token-dot.is-yours, .ps-token-dot.is-url { background:#92b195; }
.ps-token-dot.is-built { background:#c9b36a; }
@ -494,6 +510,7 @@ export default defineComponent({
localeOpen: false,
// Map token control — the reader's own entry, persisted in localStorage (never in the build).
settingsOpen: false,
headerHidden: readStored(HEADER_KEY) === '1',
engineOpen: false,
userProvider: readStored(PROVIDER_KEY),
tokenBusy: false,
@ -743,6 +760,12 @@ export default defineComponent({
this.settingsOpen = false
} finally { this.tokenBusy = false }
},
// Collapse / restore the header (the props panel). Remembered in this browser.
setHeaderHidden(v) {
this.headerHidden = !!v
this.settingsOpen = false; this.engineOpen = false; this.localeOpen = false
writeStored(HEADER_KEY, v ? '1' : '')
},
setProvider(id) {
this.engineOpen = false
if (id === this.userProvider) return
@ -1089,6 +1112,8 @@ export default defineComponent({
.vtl-top { position:relative; z-index:3; }
.vtl-body { isolation:isolate; }
.ps-token { position:relative; display:inline-flex; }
.ps-show-header { position:fixed; top:8px; inset-inline-end:8px; z-index:2147483000; display:inline-flex; align-items:center; gap:6px; padding:6px 12px; border:1px solid #d1d4cd; border-radius:999px; background:rgba(255,255,255,.94); color:#3b463d; font:600 12px system-ui, sans-serif; cursor:pointer; box-shadow:0 4px 14px rgba(0,0,0,.18); }
.ps-show-header:hover, .ps-show-header:focus-visible { background:#ffffff; outline:2px solid #92b195; }
.ps-token-dot { width:8px; height:8px; border-radius:50%; background:#b8bdb4; }
.ps-token-dot.is-yours, .ps-token-dot.is-url { background:#92b195; }
.ps-token-dot.is-built { background:#c9b36a; }