fix(spa): make embedded maps survive the sandboxed plugin iframe
Three fixes for the power-app SPA running as a go-base plugin (sandboxed iframe, origin 'null' — where localStorage throws and root-absolute asset paths misresolve under the /plugin-assets/<name>/ subpath): - localStorage: wrap every theme/preference read+write in try/catch across the library (ProfileLoader, useMapTheme, uiStore). `typeof localStorage` is not enough — in a sandbox the object exists but ACCESS throws SecurityError. Sandboxed maps now boot themeless instead of crashing. - assets: the SPA shell now injects an absolute assetsBase (derived from its own document.baseURI) into every mounted element's profile-config, so bundles fetch tables/vendor (Tabulator) from the correct subpath instead of a root-absolute /assets that 404s under /plugin-assets/doxa-maps-spa/. - nav: revert the Prayer Mobilization section icon to 'heart' (the prayer glyph read as a header it is not). Rebuilds every bundle (library-wide localStorage change).
This commit is contained in:
parent
30a516a6a3
commit
1cd2b46cd5
17 changed files with 46 additions and 32 deletions
|
|
@ -975,6 +975,10 @@ async function mount(id){
|
|||
// THE LOCALE HAND-OFF: ProfileLoader reads profile-config.locale, normalises it
|
||||
// and drives the bundle's i18n from it. This one field is the whole contract.
|
||||
cfg.locale = locale
|
||||
// ASSET BASE: bundles fetch tables/vendor (Tabulator) from `${assetsBase}/...`. Served under
|
||||
// a subpath (e.g. /plugin-assets/doxa-maps-spa/), a bare '/assets' or './assets' misresolves,
|
||||
// so hand each bundle an ABSOLUTE base derived from THIS shell's own document — correct at any depth.
|
||||
if (!cfg.assetsBase) cfg.assetsBase = new URL('assets', document.baseURI).href.replace(/\/+$/, '')
|
||||
el.setAttribute('profile-config', JSON.stringify(cfg))
|
||||
|
||||
// ── PANE COLUMN vs SINGLE MOUNT ─────────────────────────────────────────
|
||||
|
|
@ -997,6 +1001,7 @@ async function mount(id){
|
|||
const pcfg = { ...spec.config, ...pane.config }
|
||||
if (SHELL_TOKEN) pcfg.tk = SHELL_TOKEN
|
||||
pcfg.locale = locale
|
||||
if (!pcfg.assetsBase) pcfg.assetsBase = new URL('assets', document.baseURI).href.replace(/\/+$/, '')
|
||||
pel.setAttribute('profile-config', JSON.stringify(pcfg))
|
||||
column.mountEl(pane.id)?.appendChild(pel)
|
||||
extras.push(pel)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export const NAV = [
|
|||
// section sits at the TOP and its first child is the DEFAULT profile.
|
||||
// (2026-07-29: "so that the first map they see is the prayer map".)
|
||||
{
|
||||
kind: 'section', id: 'prayer-mobilization', label: 'Prayer Mobilization', icon: 'prayer', target: 'both',
|
||||
kind: 'section', id: 'prayer-mobilization', label: 'Prayer Mobilization', icon: 'heart', target: 'both',
|
||||
children: [
|
||||
// Prayer Progress = the research map opened on its PRAYER tab (colorStrategy
|
||||
// 'prayerProgress'). That tab is tabs[0] in the bundle, so it is already the
|
||||
|
|
|
|||
|
|
@ -144,10 +144,12 @@ const dataStore = useDataStore()
|
|||
// profiles that DO hydrate later in onMounted. init() (onMounted) still runs for
|
||||
// the ResizeObserver / matchMedia listeners; setTheme is idempotent so the re-apply
|
||||
// is harmless. (staging bug, coder 2026-06-25: dark selected + reload → map dark, UI light.)
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const savedTheme = localStorage.getItem('theme')
|
||||
// Sandboxed iframe (no allow-same-origin, origin 'null') → localStorage EXISTS but THROWS
|
||||
// SecurityError on access, so a bare typeof-guard isn't enough. try/catch → skip persisted theme.
|
||||
try {
|
||||
const savedTheme = (typeof localStorage !== 'undefined') ? localStorage.getItem('theme') : null
|
||||
if (savedTheme) uiStore.setTheme?.(savedTheme)
|
||||
}
|
||||
} catch (e) { /* sandboxed: no persisted theme */ }
|
||||
|
||||
provide('uiStore', uiStore)
|
||||
provide('mapStore', mapStore)
|
||||
|
|
|
|||
|
|
@ -53,8 +53,9 @@ export function useMapTheme(uiStore) {
|
|||
// session can never drag the UI back to dark.
|
||||
function isDarkTheme() {
|
||||
if (!THEME_TOGGLE) return false
|
||||
return uiStore?.theme === 'dark'
|
||||
|| (typeof localStorage !== 'undefined' && localStorage.getItem('theme') === 'dark')
|
||||
if (uiStore?.theme === 'dark') return true
|
||||
try { return typeof localStorage !== 'undefined' && localStorage.getItem('theme') === 'dark' }
|
||||
catch (e) { return false } // sandboxed iframe → localStorage throws
|
||||
}
|
||||
|
||||
// The Standard-style lightPreset for the current theme.
|
||||
|
|
|
|||
|
|
@ -412,8 +412,8 @@ export const useUIStore = defineStore('ui', {
|
|||
const isDark = this.isDarkTheme;
|
||||
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('theme', theme);
|
||||
// Save to localStorage (sandboxed iframe throws — persistence is best-effort)
|
||||
try { localStorage.setItem('theme', theme); } catch (e) { /* sandboxed: no persistence */ }
|
||||
|
||||
},
|
||||
|
||||
|
|
@ -978,8 +978,9 @@ export const useUIStore = defineStore('ui', {
|
|||
* Load saved preferences, set up listeners
|
||||
*/
|
||||
init() {
|
||||
// Load theme from localStorage
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
// Load theme from localStorage (sandboxed iframe throws — skip if unavailable)
|
||||
let savedTheme = null;
|
||||
try { savedTheme = localStorage.getItem('theme'); } catch (e) { /* sandboxed */ }
|
||||
if (savedTheme) {
|
||||
this.setTheme(savedTheme);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
public/doxa-maps/demo-map/demo-map.js
generated
4
public/doxa-maps/demo-map/demo-map.js
generated
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
public/doxa-maps/key-cities/key-cities.js
generated
4
public/doxa-maps/key-cities/key-cities.js
generated
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -496,7 +496,7 @@ body.rail .side .subgrp .row{padding-left:0;margin-left:8px;width:calc(100% - 16
|
|||
}
|
||||
// The pane-wrapper LAYOUT, nested inside this one: app-wrapper-material owns the
|
||||
// chrome (header, side menu, View fieldset); pane-wrapper owns the stage inside it.
|
||||
import { buildPaneColumn, PANE_GRID_STYLES } from '../pane-wrapper/pane-wrapper.js'
|
||||
import { buildPaneColumn, PANE_GRID_STYLES } from './pane-wrapper/pane-wrapper.js'
|
||||
|
||||
import { NAV, navForTarget } from './nav.js'
|
||||
import { icon } from './icons.js'
|
||||
|
|
@ -975,6 +975,10 @@ async function mount(id){
|
|||
// THE LOCALE HAND-OFF: ProfileLoader reads profile-config.locale, normalises it
|
||||
// and drives the bundle's i18n from it. This one field is the whole contract.
|
||||
cfg.locale = locale
|
||||
// ASSET BASE: bundles fetch tables/vendor (Tabulator) from `${assetsBase}/...`. Served under
|
||||
// a subpath (e.g. /plugin-assets/doxa-maps-spa/), a bare '/assets' or './assets' misresolves,
|
||||
// so hand each bundle an ABSOLUTE base derived from THIS shell's own document — correct at any depth.
|
||||
if (!cfg.assetsBase) cfg.assetsBase = new URL('assets', document.baseURI).href.replace(/\/+$/, '')
|
||||
el.setAttribute('profile-config', JSON.stringify(cfg))
|
||||
|
||||
// ── PANE COLUMN vs SINGLE MOUNT ─────────────────────────────────────────
|
||||
|
|
@ -997,6 +1001,7 @@ async function mount(id){
|
|||
const pcfg = { ...spec.config, ...pane.config }
|
||||
if (SHELL_TOKEN) pcfg.tk = SHELL_TOKEN
|
||||
pcfg.locale = locale
|
||||
if (!pcfg.assetsBase) pcfg.assetsBase = new URL('assets', document.baseURI).href.replace(/\/+$/, '')
|
||||
pel.setAttribute('profile-config', JSON.stringify(pcfg))
|
||||
column.mountEl(pane.id)?.appendChild(pel)
|
||||
extras.push(pel)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export const NAV = [
|
|||
// section sits at the TOP and its first child is the DEFAULT profile.
|
||||
// (2026-07-29: "so that the first map they see is the prayer map".)
|
||||
{
|
||||
kind: 'section', id: 'prayer-mobilization', label: 'Prayer Mobilization', icon: 'prayer', target: 'both',
|
||||
kind: 'section', id: 'prayer-mobilization', label: 'Prayer Mobilization', icon: 'heart', target: 'both',
|
||||
children: [
|
||||
// Prayer Progress = the research map opened on its PRAYER tab (colorStrategy
|
||||
// 'prayerProgress'). That tab is tabs[0] in the bundle, so it is already the
|
||||
|
|
|
|||
Loading…
Reference in a new issue