From 86f1c1a463c30456fd98ec2d9b69dcdde1a8d901 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sat, 3 Oct 2026 05:24:16 +0000 Subject: [PATCH 1/3] Posterverse: the open tab is in the page address The shell reads ?tab= through the shared view-address helper and writes the open tab back through it: a reader's choice is a push, first paint a replace, and Back / Forward re-apply the tab without writing. Only the address owner writes (urlOwner 'app' on our own pages, 'host' hands the change to a host's router, absent on a partner's page = read only), so a reload keeps the tab and a sent link opens it. --- plugin-designs/posterverse/posterverse.vue | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/plugin-designs/posterverse/posterverse.vue b/plugin-designs/posterverse/posterverse.vue index f80328b90..6bf39d9d1 100644 --- a/plugin-designs/posterverse/posterverse.vue +++ b/plugin-designs/posterverse/posterverse.vue @@ -74,6 +74,7 @@ import BackToTopButton from '@map/components/BackToTopButton.vue' import ShellLanguageMenu from '@map/components/ShellLanguageMenu.vue' import { saveLocale } from '@map/layouts/app-wrapper-material/languages.js' import { loadScriptOnce, whenTagDefined } from '@map/utils/embedScript.js' +import { readViewKeys, writeViewKeys, onAddressChange, urlOwnerOf } from '@map/composables/useViewAddress.js' import { translate, LOCALES } from './src/strings.js' import { SECTIONS, TABS, resolveTabs } from './src/tabs.js' @@ -226,11 +227,18 @@ export default defineComponent({ mounted() { this.watchHostLang() // Paint the rail at once; the manifest completes the rows in the background and re-drives the mount. - this.select(this.initialIndex()) + // First paint is a REPLACE, so a reload or a shared link keeps the tab without adding a Back step. + this.select(this.initialIndex(), { firstPaint: true }) + // Back / Forward re-apply the tab from the address and never write (writing would lose Forward). + this._offAddress = onAddressChange(({ tab }) => { + const i = this.tabs.findIndex((t) => t.id === tab) + this.select(i >= 0 ? i : 0, { fromHistory: true }) + }) this.loadManifest() }, beforeUnmount() { if (this._langObs) { this._langObs.disconnect(); this._langObs = null } + if (this._offAddress) { this._offAddress(); this._offAddress = null } }, methods: { tr(key, params) { return translate(this.uiLocale, key, params) }, @@ -250,7 +258,7 @@ export default defineComponent({ }, requestedTab() { let q = '' - try { q = new URLSearchParams(location.search).get('tab') || '' } catch { /* non-browser */ } + try { q = readViewKeys(location.search).tab } catch { /* non-browser */ } return String(this.tab || this.cfg.tab || q || '').trim() }, initialIndex() { @@ -306,7 +314,7 @@ export default defineComponent({ const w = !this._honouredTab && want ? this.tabs.findIndex((t) => t.id === want) : -1 const i = this.tabs.findIndex((t) => t.id === activeId) this._honouredTab = true - this.select(w >= 0 ? w : (i >= 0 ? i : 0)) + this.select(w >= 0 ? w : (i >= 0 ? i : 0), { firstPaint: true }) }, onSelect(key) { const i = this.tabs.findIndex((t) => t.id === key) @@ -322,7 +330,10 @@ export default defineComponent({ if (this.base) return this.base.replace(/\/+$/, '') + '/' + r return '../' + r }, - select(i) { + // The tab is written to the address by the ONE owner (urlOwner in profile-config: 'app' on our own pages, + // 'host' when a host's router writes, absent on a partner's page = read only). A reader's choice is a push; + // first paint is a replace; Back / Forward never write. + select(i, { fromHistory = false, firstPaint = false } = {}) { const prevIndex = this.activeIndex this.activeIndex = i // Release the outgoing heavy pane first, so at most one live map context exists at a time. @@ -334,6 +345,7 @@ export default defineComponent({ if (!t) return // Tell the host which plugin is showing (CustomEvent 'tab-change', detail[0] = { id, label }). if (prevIndex !== i || !this._announced) { this._announced = true; this.$emit('tab-change', { id: t.id, label: this.tabLabel(t) }) } + if (!fromHistory) writeViewKeys({ tab: t.id }, { owner: urlOwnerOf(this.cfg), push: !firstPaint, el: this.$el }) if (!t.scriptUrl) return // applyManifest selects again once the manifest lands // A heavy tab's slot exists only after its v-if subtree renders, so mount on the next tick. if (!t.mounted) this.$nextTick(() => { if (this.activeIndex === i) this.mountElement(i) }) From 32cba5f8fe6665bff39e988ba69c3f9c5e1ca5f7 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sat, 3 Oct 2026 05:25:36 +0000 Subject: [PATCH 2/3] Posterverse: on a page it owns, the address's tab beats a baked config tab When the shell owns the address, a reload or a shared ?tab= now wins over a profile-config tab, so the first-paint replace can never overwrite a sent link with the default. Elsewhere the order is unchanged (attribute, config, URL). --- plugin-designs/posterverse/posterverse.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugin-designs/posterverse/posterverse.vue b/plugin-designs/posterverse/posterverse.vue index 6bf39d9d1..1a0eac95f 100644 --- a/plugin-designs/posterverse/posterverse.vue +++ b/plugin-designs/posterverse/posterverse.vue @@ -259,7 +259,10 @@ export default defineComponent({ requestedTab() { let q = '' try { q = readViewKeys(location.search).tab } catch { /* non-browser */ } - return String(this.tab || this.cfg.tab || q || '').trim() + // On a page whose address this shell owns, the URL (a reload, a shared link) beats a baked cfg.tab, or the + // first-paint replace would overwrite the link with the default. Elsewhere the host's config leads. + const order = urlOwnerOf(this.cfg) ? [this.tab, q, this.cfg.tab] : [this.tab, this.cfg.tab, q] + return String(order.find((v) => v) || '').trim() }, initialIndex() { const i = this.tabs.findIndex((t) => t.id === this.requestedTab()) From 8c674c04478fb74c485cf72e7ac93d3c81645b6f Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sat, 3 Oct 2026 05:26:59 +0000 Subject: [PATCH 3/3] Posterverse: don't overwrite a link to a tab that arrives with the manifest The first-paint write is skipped when the address names a tab the shell doesn't know yet (a row that only resolves once the manifest is in). Writing tab 0 there would replace the link before applyManifest could honour it; applyManifest's own first-paint replace writes the honoured tab. --- plugin-designs/posterverse/posterverse.vue | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugin-designs/posterverse/posterverse.vue b/plugin-designs/posterverse/posterverse.vue index 1a0eac95f..39d7c95e7 100644 --- a/plugin-designs/posterverse/posterverse.vue +++ b/plugin-designs/posterverse/posterverse.vue @@ -227,8 +227,12 @@ export default defineComponent({ mounted() { this.watchHostLang() // Paint the rail at once; the manifest completes the rows in the background and re-drives the mount. - // First paint is a REPLACE, so a reload or a shared link keeps the tab without adding a Back step. - this.select(this.initialIndex(), { firstPaint: true }) + // First paint is a REPLACE, so a reload or a shared link keeps the tab without adding a Back step. If the + // address names a tab that only arrives with the manifest (a provisional row), write nothing yet: writing + // tab 0 here would overwrite the link, and applyManifest's own first-paint replace writes the honoured tab. + const want = this.requestedTab() + const known = !want || this.tabs.some((t) => t.id === want) + this.select(this.initialIndex(), { firstPaint: true, fromHistory: !known }) // Back / Forward re-apply the tab from the address and never write (writing would lose Forward). this._offAddress = onAddressChange(({ tab }) => { const i = this.tabs.findIndex((t) => t.id === tab)