Plugin Studio: a map-token control (paste a Mapbox public token, or point it at a token service)
Driver request, 2026-09-25: "add a spot on the studio for placing a Mapbox token, or setting the URL for where the Mapbox token API is from, like doxa.life has an API token". A 🔑 Map token button in the studio header opens a small panel: - paste a Mapbox PUBLIC token (pk.…) — a secret sk.… token is refused, the page is public; - or a token-service URL: the studio fetches it (JSON {token|tk|accessToken} or a bare string, the leaf pages' contract) and uses the pk.… it returns. The maps never read a token URL themselves, so the studio resolves it and hands them the token; - Clear returns to the token built into the page. A dot and a status line say which is in use. Precedence: your token > a token from your URL > the host's tk > the page's built-in token. Kept in this browser's localStorage only; applying re-mounts the open map and publishes window.MAP_TOKEN for maps nested inside composites. Labels in all 11 locales (i18n parity PASS). Tested in a browser on the built studio: shows the built-in source; sk.… refused and not stored; a pasted pk.… reaches the pane's profile-config and window.MAP_TOKEN and survives a reload; Clear restores the built-in token; a token URL is fetched and applied. https://doxa.life/api/maps/token is refused cross-origin from another site — the panel says so and keeps the current token.
This commit is contained in:
parent
816deb56a8
commit
966c44fcc0
18 changed files with 342 additions and 30 deletions
|
|
@ -26,5 +26,18 @@
|
|||
"expandList": "توسيع القائمة",
|
||||
"collapseList": "طي القائمة",
|
||||
"collapse": "طي",
|
||||
"partOf": "جزء من {name}، غير منشور بمفرده"
|
||||
"partOf": "جزء من {name}، غير منشور بمفرده",
|
||||
"mapToken": "رمز الخريطة",
|
||||
"tokenPaste": "رمز Mapbox العام (pk.…)",
|
||||
"tokenUrlLabel": "…أو رابط خدمة الرموز",
|
||||
"tokenApply": "استخدام",
|
||||
"tokenClear": "مسح",
|
||||
"tokenStoredHere": "يُحفظ في هذا المتصفح فقط.",
|
||||
"tokenSourceYours": "يُستخدم رمزك",
|
||||
"tokenSourceUrl": "يُستخدم رمز من {url}",
|
||||
"tokenSourceBuilt": "يُستخدم الرمز المضمَّن في هذه الصفحة",
|
||||
"tokenSourceNone": "لا يوجد رمز Mapbox: لا يمكن تحميل خرائط Mapbox (خرائط MapLibre لا تحتاج رمزًا)",
|
||||
"tokenSecret": "هذا رمز سري (sk.…). الصق رمزًا عامًا (pk.…): هذه الصفحة عامة.",
|
||||
"tokenNotPk": "يبدأ رمز Mapbox العام بـ pk.",
|
||||
"tokenUrlFailed": "تعذّر الحصول على رمز من هذا الرابط: {why}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,5 +26,18 @@
|
|||
"expandList": "Liste ausklappen",
|
||||
"collapseList": "Liste einklappen",
|
||||
"collapse": "Einklappen",
|
||||
"partOf": "Teil von {name}, nicht eigenständig veröffentlicht"
|
||||
"partOf": "Teil von {name}, nicht eigenständig veröffentlicht",
|
||||
"mapToken": "Karten-Token",
|
||||
"tokenPaste": "Öffentlicher Mapbox-Token (pk.…)",
|
||||
"tokenUrlLabel": "…oder die URL eines Token-Dienstes",
|
||||
"tokenApply": "Verwenden",
|
||||
"tokenClear": "Löschen",
|
||||
"tokenStoredHere": "Nur in diesem Browser gespeichert.",
|
||||
"tokenSourceYours": "Ihr Token wird verwendet",
|
||||
"tokenSourceUrl": "Token von {url} wird verwendet",
|
||||
"tokenSourceBuilt": "Der in diese Seite eingebaute Token wird verwendet",
|
||||
"tokenSourceNone": "Kein Mapbox-Token: Mapbox-Karten können nicht laden (MapLibre-Karten brauchen keinen)",
|
||||
"tokenSecret": "Das ist ein geheimer Token (sk.…). Fügen Sie einen öffentlichen ein (pk.…): Diese Seite ist öffentlich.",
|
||||
"tokenNotPk": "Ein öffentlicher Mapbox-Token beginnt mit pk.",
|
||||
"tokenUrlFailed": "Von dieser URL kam kein Token: {why}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,5 +26,18 @@
|
|||
"expandList": "Expand list",
|
||||
"collapseList": "Collapse list",
|
||||
"collapse": "Collapse",
|
||||
"partOf": "part of {name}, not published on its own"
|
||||
"partOf": "part of {name}, not published on its own",
|
||||
"mapToken": "Map token",
|
||||
"tokenPaste": "Mapbox public token (pk.…)",
|
||||
"tokenUrlLabel": "…or the URL of a token service",
|
||||
"tokenApply": "Use",
|
||||
"tokenClear": "Clear",
|
||||
"tokenStoredHere": "Kept in this browser only.",
|
||||
"tokenSourceYours": "Using your token",
|
||||
"tokenSourceUrl": "Using a token from {url}",
|
||||
"tokenSourceBuilt": "Using the token built into this page",
|
||||
"tokenSourceNone": "No Mapbox token: Mapbox maps cannot load (MapLibre maps need none)",
|
||||
"tokenSecret": "That is a secret token (sk.…). Paste a public one (pk.…): this page is public.",
|
||||
"tokenNotPk": "A Mapbox public token starts with pk.",
|
||||
"tokenUrlFailed": "Could not get a token from that URL: {why}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,5 +26,18 @@
|
|||
"expandList": "Expandir la lista",
|
||||
"collapseList": "Contraer la lista",
|
||||
"collapse": "Contraer",
|
||||
"partOf": "parte de {name}, no publicada por separado"
|
||||
"partOf": "parte de {name}, no publicada por separado",
|
||||
"mapToken": "Token del mapa",
|
||||
"tokenPaste": "Token público de Mapbox (pk.…)",
|
||||
"tokenUrlLabel": "…o la URL de un servicio de tokens",
|
||||
"tokenApply": "Usar",
|
||||
"tokenClear": "Borrar",
|
||||
"tokenStoredHere": "Se guarda solo en este navegador.",
|
||||
"tokenSourceYours": "Usando tu token",
|
||||
"tokenSourceUrl": "Usando un token de {url}",
|
||||
"tokenSourceBuilt": "Usando el token integrado en esta página",
|
||||
"tokenSourceNone": "Sin token de Mapbox: los mapas de Mapbox no cargan (los de MapLibre no lo necesitan)",
|
||||
"tokenSecret": "Ese es un token secreto (sk.…). Pega uno público (pk.…): esta página es pública.",
|
||||
"tokenNotPk": "Un token público de Mapbox empieza por pk.",
|
||||
"tokenUrlFailed": "No se pudo obtener un token de esa URL: {why}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,5 +26,18 @@
|
|||
"expandList": "Déplier la liste",
|
||||
"collapseList": "Replier la liste",
|
||||
"collapse": "Replier",
|
||||
"partOf": "partie de {name}, non publiée seule"
|
||||
"partOf": "partie de {name}, non publiée seule",
|
||||
"mapToken": "Jeton de carte",
|
||||
"tokenPaste": "Jeton public Mapbox (pk.…)",
|
||||
"tokenUrlLabel": "…ou l’URL d’un service de jetons",
|
||||
"tokenApply": "Utiliser",
|
||||
"tokenClear": "Effacer",
|
||||
"tokenStoredHere": "Conservé dans ce navigateur uniquement.",
|
||||
"tokenSourceYours": "Votre jeton est utilisé",
|
||||
"tokenSourceUrl": "Jeton obtenu de {url}",
|
||||
"tokenSourceBuilt": "Jeton intégré à cette page",
|
||||
"tokenSourceNone": "Aucun jeton Mapbox : les cartes Mapbox ne peuvent pas charger (MapLibre n’en a pas besoin)",
|
||||
"tokenSecret": "C’est un jeton secret (sk.…). Collez un jeton public (pk.…) : cette page est publique.",
|
||||
"tokenNotPk": "Un jeton public Mapbox commence par pk.",
|
||||
"tokenUrlFailed": "Impossible d’obtenir un jeton depuis cette URL : {why}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,5 +26,18 @@
|
|||
"expandList": "सूची फैलाएँ",
|
||||
"collapseList": "सूची संक्षिप्त करें",
|
||||
"collapse": "संक्षिप्त करें",
|
||||
"partOf": "{name} का भाग, अलग से प्रकाशित नहीं"
|
||||
"partOf": "{name} का भाग, अलग से प्रकाशित नहीं",
|
||||
"mapToken": "मानचित्र टोकन",
|
||||
"tokenPaste": "Mapbox सार्वजनिक टोकन (pk.…)",
|
||||
"tokenUrlLabel": "…या किसी टोकन सेवा का URL",
|
||||
"tokenApply": "उपयोग करें",
|
||||
"tokenClear": "साफ़ करें",
|
||||
"tokenStoredHere": "केवल इसी ब्राउज़र में रखा जाता है।",
|
||||
"tokenSourceYours": "आपका टोकन उपयोग हो रहा है",
|
||||
"tokenSourceUrl": "{url} से टोकन उपयोग हो रहा है",
|
||||
"tokenSourceBuilt": "इस पेज में बना टोकन उपयोग हो रहा है",
|
||||
"tokenSourceNone": "कोई Mapbox टोकन नहीं: Mapbox मानचित्र लोड नहीं हो सकते (MapLibre को इसकी ज़रूरत नहीं)",
|
||||
"tokenSecret": "यह एक गुप्त टोकन है (sk.…)। सार्वजनिक टोकन (pk.…) चिपकाएँ: यह पेज सार्वजनिक है।",
|
||||
"tokenNotPk": "Mapbox सार्वजनिक टोकन pk. से शुरू होता है",
|
||||
"tokenUrlFailed": "उस URL से टोकन नहीं मिल सका: {why}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,5 +26,18 @@
|
|||
"expandList": "Espandi l’elenco",
|
||||
"collapseList": "Comprimi l’elenco",
|
||||
"collapse": "Comprimi",
|
||||
"partOf": "parte di {name}, non pubblicata da sola"
|
||||
"partOf": "parte di {name}, non pubblicata da sola",
|
||||
"mapToken": "Token mappa",
|
||||
"tokenPaste": "Token pubblico Mapbox (pk.…)",
|
||||
"tokenUrlLabel": "…o l’URL di un servizio di token",
|
||||
"tokenApply": "Usa",
|
||||
"tokenClear": "Cancella",
|
||||
"tokenStoredHere": "Conservato solo in questo browser.",
|
||||
"tokenSourceYours": "Uso il tuo token",
|
||||
"tokenSourceUrl": "Uso un token da {url}",
|
||||
"tokenSourceBuilt": "Uso il token integrato in questa pagina",
|
||||
"tokenSourceNone": "Nessun token Mapbox: le mappe Mapbox non si caricano (MapLibre non ne ha bisogno)",
|
||||
"tokenSecret": "Questo è un token segreto (sk.…). Incolla un token pubblico (pk.…): questa pagina è pubblica.",
|
||||
"tokenNotPk": "Un token pubblico Mapbox inizia con pk.",
|
||||
"tokenUrlFailed": "Impossibile ottenere un token da quell’URL: {why}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,5 +26,18 @@
|
|||
"expandList": "Expandir a lista",
|
||||
"collapseList": "Recolher a lista",
|
||||
"collapse": "Recolher",
|
||||
"partOf": "parte de {name}, não publicada separadamente"
|
||||
"partOf": "parte de {name}, não publicada separadamente",
|
||||
"mapToken": "Token do mapa",
|
||||
"tokenPaste": "Token público do Mapbox (pk.…)",
|
||||
"tokenUrlLabel": "…ou o URL de um serviço de tokens",
|
||||
"tokenApply": "Usar",
|
||||
"tokenClear": "Limpar",
|
||||
"tokenStoredHere": "Guardado apenas neste navegador.",
|
||||
"tokenSourceYours": "A usar o seu token",
|
||||
"tokenSourceUrl": "A usar um token de {url}",
|
||||
"tokenSourceBuilt": "A usar o token incorporado nesta página",
|
||||
"tokenSourceNone": "Sem token do Mapbox: os mapas Mapbox não carregam (os MapLibre não precisam)",
|
||||
"tokenSecret": "Esse é um token secreto (sk.…). Cole um público (pk.…): esta página é pública.",
|
||||
"tokenNotPk": "Um token público do Mapbox começa por pk.",
|
||||
"tokenUrlFailed": "Não foi possível obter um token desse URL: {why}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,5 +26,18 @@
|
|||
"expandList": "Extinde lista",
|
||||
"collapseList": "Restrânge lista",
|
||||
"collapse": "Restrânge",
|
||||
"partOf": "parte din {name}, nepublicată separat"
|
||||
"partOf": "parte din {name}, nepublicată separat",
|
||||
"mapToken": "Token hartă",
|
||||
"tokenPaste": "Token public Mapbox (pk.…)",
|
||||
"tokenUrlLabel": "…sau adresa URL a unui serviciu de tokenuri",
|
||||
"tokenApply": "Folosește",
|
||||
"tokenClear": "Șterge",
|
||||
"tokenStoredHere": "Păstrat doar în acest browser.",
|
||||
"tokenSourceYours": "Se folosește tokenul tău",
|
||||
"tokenSourceUrl": "Se folosește un token de la {url}",
|
||||
"tokenSourceBuilt": "Se folosește tokenul inclus în această pagină",
|
||||
"tokenSourceNone": "Niciun token Mapbox: hărțile Mapbox nu se pot încărca (MapLibre nu are nevoie)",
|
||||
"tokenSecret": "Acesta este un token secret (sk.…). Lipește unul public (pk.…): pagina este publică.",
|
||||
"tokenNotPk": "Un token public Mapbox începe cu pk.",
|
||||
"tokenUrlFailed": "Nu s-a putut obține un token de la acea adresă: {why}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,5 +26,18 @@
|
|||
"expandList": "Развернуть список",
|
||||
"collapseList": "Свернуть список",
|
||||
"collapse": "Свернуть",
|
||||
"partOf": "часть {name}, не публикуется отдельно"
|
||||
"partOf": "часть {name}, не публикуется отдельно",
|
||||
"mapToken": "Токен карты",
|
||||
"tokenPaste": "Публичный токен Mapbox (pk.…)",
|
||||
"tokenUrlLabel": "…или адрес службы токенов",
|
||||
"tokenApply": "Применить",
|
||||
"tokenClear": "Очистить",
|
||||
"tokenStoredHere": "Хранится только в этом браузере.",
|
||||
"tokenSourceYours": "Используется ваш токен",
|
||||
"tokenSourceUrl": "Используется токен с {url}",
|
||||
"tokenSourceBuilt": "Используется токен, встроенный в страницу",
|
||||
"tokenSourceNone": "Нет токена Mapbox: карты Mapbox не загрузятся (MapLibre он не нужен)",
|
||||
"tokenSecret": "Это секретный токен (sk.…). Вставьте публичный (pk.…): эта страница общедоступна.",
|
||||
"tokenNotPk": "Публичный токен Mapbox начинается с pk.",
|
||||
"tokenUrlFailed": "Не удалось получить токен по этому адресу: {why}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,5 +26,18 @@
|
|||
"expandList": "展开列表",
|
||||
"collapseList": "折叠列表",
|
||||
"collapse": "折叠",
|
||||
"partOf": "{name} 的一部分,不单独发布"
|
||||
"partOf": "{name} 的一部分,不单独发布",
|
||||
"mapToken": "地图令牌",
|
||||
"tokenPaste": "Mapbox 公开令牌 (pk.…)",
|
||||
"tokenUrlLabel": "…或令牌服务的网址",
|
||||
"tokenApply": "使用",
|
||||
"tokenClear": "清除",
|
||||
"tokenStoredHere": "仅保存在此浏览器中。",
|
||||
"tokenSourceYours": "正在使用您的令牌",
|
||||
"tokenSourceUrl": "正在使用来自 {url} 的令牌",
|
||||
"tokenSourceBuilt": "正在使用此页面内置的令牌",
|
||||
"tokenSourceNone": "没有 Mapbox 令牌:Mapbox 地图无法加载(MapLibre 地图不需要)",
|
||||
"tokenSecret": "这是秘密令牌 (sk.…)。请粘贴公开令牌 (pk.…):此页面是公开的。",
|
||||
"tokenNotPk": "Mapbox 公开令牌以 pk. 开头",
|
||||
"tokenUrlFailed": "无法从该网址获取令牌:{why}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,31 @@
|
|||
app-wrapper-material/languages.js (persisted, offline, no dep) — same switcher as the SPA. -->
|
||||
<template #meta>
|
||||
<span class="ps-count">{{ countLabel }}</span>
|
||||
<!-- MAP TOKEN (Driver request 2026-09-25): paste a Mapbox public token, or point the studio at a
|
||||
token service (e.g. https://doxa.life/api/maps/token). Kept in THIS browser only; every map
|
||||
pane (and every map nested inside a composite) is re-mounted with it. -->
|
||||
<span class="ps-token">
|
||||
<button type="button" class="ps-locale-btn ps-token-btn" :aria-expanded="String(tokenOpen)"
|
||||
:title="tokenStatus" @click="tokenOpen = !tokenOpen">
|
||||
<span aria-hidden="true">🔑</span>
|
||||
<span class="ps-locale-name">{{ tr('mapToken') }}</span>
|
||||
<span class="ps-token-dot" :class="'is-' + tokenSource" aria-hidden="true"></span>
|
||||
</button>
|
||||
<form v-if="tokenOpen" class="ps-token-panel" @submit.prevent="applyToken">
|
||||
<label class="ps-token-label">{{ tr('tokenPaste') }}
|
||||
<input class="ps-token-input" v-model.trim="tokenDraft" placeholder="pk.eyJ…" autocomplete="off" spellcheck="false" />
|
||||
</label>
|
||||
<label class="ps-token-label">{{ tr('tokenUrlLabel') }}
|
||||
<input class="ps-token-input" v-model.trim="urlDraft" type="url" placeholder="https://doxa.life/api/maps/token" autocomplete="off" spellcheck="false" />
|
||||
</label>
|
||||
<div class="ps-token-row">
|
||||
<button type="submit" class="ps-token-apply" :disabled="tokenBusy">{{ tr('tokenApply') }}</button>
|
||||
<button type="button" class="ps-token-clear" @click="clearToken">{{ tr('tokenClear') }}</button>
|
||||
</div>
|
||||
<p class="ps-token-status" :class="{ err: !!tokenErr }" role="status">{{ tokenErr || tokenStatus }}</p>
|
||||
<p class="ps-token-note">{{ tr('tokenStoredHere') }}</p>
|
||||
</form>
|
||||
</span>
|
||||
<span class="ps-locale">
|
||||
<button type="button" class="ps-locale-btn" :aria-expanded="String(localeOpen)"
|
||||
:aria-label="tr('changeLanguage')" @click="localeOpen = !localeOpen">
|
||||
|
|
@ -163,6 +188,13 @@ import VerticalTabsLayout from '@map/layouts/VerticalTabsLayout.vue'
|
|||
import StudioTree from './src/StudioTree.vue'
|
||||
import { SWITCHER_LANGUAGES, loadLocale, saveLocale, getLanguage, normaliseLocale } from '@map/layouts/app-wrapper-material/languages.js'
|
||||
|
||||
// The reader's own map token / token-service URL. localStorage only: never written into the build,
|
||||
// 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'
|
||||
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 */ } }
|
||||
|
||||
// Only the studio's CONTENT-AREA styles remain here; the shell (top bar + rail + body) is now the
|
||||
// shared VerticalTabsLayout (which self-injects its own CSS). Kept as an injected string so the studio
|
||||
// content is styled even when this component is nested as a child element (see useShadowStyles).
|
||||
|
|
@ -172,6 +204,21 @@ const PS_SHADOW_CSS = `
|
|||
.ps-logo { display:inline-flex; align-items:center; }
|
||||
.ps-logo svg { display:block; }
|
||||
.ps-count { color:#6d766e; }
|
||||
.ps-token { position:relative; display:inline-flex; }
|
||||
.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; }
|
||||
.ps-token-dot.is-none { background:#d27a6a; }
|
||||
.ps-token-panel { position:absolute; top:100%; inset-inline-end:0; margin-top:4px; z-index:30; width:min(340px, 90vw); display:flex; flex-direction:column; gap:8px; padding:12px; border:1px solid #d1d4cd; border-radius:10px; background:#ffffff; box-shadow:0 12px 32px rgba(0,0,0,.25); color:#3b463d; font-size:12px; }
|
||||
.ps-token-label { display:flex; flex-direction:column; gap:4px; }
|
||||
.ps-token-input { font:inherit; font-size:12px; padding:6px 8px; border:1px solid #d1d4cd; border-radius:7px; background:#f8f8f6; color:#3b463d; font-family:ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||
.ps-token-row { display:flex; gap:8px; }
|
||||
.ps-token-apply, .ps-token-clear { font:inherit; font-size:12px; padding:5px 12px; border-radius:7px; border:1px solid #d1d4cd; background:#d1d4cd; color:#3b463d; cursor:pointer; }
|
||||
.ps-token-apply { background:#92b195; border-color:#92b195; color:#fff; }
|
||||
.ps-token-apply:disabled { opacity:.6; cursor:progress; }
|
||||
.ps-token-status { margin:0; }
|
||||
.ps-token-status.err { color:#b3412f; }
|
||||
.ps-token-note { margin:0; color:#6d766e; }
|
||||
.ps-locale { position:relative; display:inline-flex; }
|
||||
.ps-locale-btn { display:inline-flex; align-items:center; gap:6px; font:inherit; font-size:12px; padding:4px 9px; border:1px solid #d1d4cd; border-radius:8px; background:#d1d4cd; color:#3b463d; cursor:pointer; }
|
||||
.ps-locale-btn:hover { background:#d1d4cd; }
|
||||
|
|
@ -417,6 +464,16 @@ export default defineComponent({
|
|||
// A host- or URL-given locale is NOT persisted: it is the host's choice, not the reader's.
|
||||
locale: this.initialLocale(),
|
||||
localeOpen: false,
|
||||
// Map token control — the reader's own entry, persisted in localStorage (never in the build).
|
||||
tokenOpen: false,
|
||||
tokenBusy: false,
|
||||
tokenErr: '',
|
||||
tokenDraft: readStored(TOKEN_KEY),
|
||||
urlDraft: readStored(TOKEN_URL_KEY),
|
||||
userTk: readStored(TOKEN_KEY),
|
||||
userTokenUrl: readStored(TOKEN_URL_KEY),
|
||||
fetchedTk: '',
|
||||
bakedTk: '',
|
||||
switcherLangs: SWITCHER_LANGUAGES,
|
||||
// rows keyed by "<bundle>/<profile>"; each carries its resolved nesting state.
|
||||
rows: [], // flat list built from the manifest (grouped for display via `groups`)
|
||||
|
|
@ -555,6 +612,24 @@ export default defineComponent({
|
|||
// disagreeing by one, with nothing on either saying why, is how a reader stops trusting both.
|
||||
// "listed here" is the whole fix: it names the population, so the difference from the catalog
|
||||
// total reads as a deliberate exclusion rather than an off-by-one.
|
||||
// The token every pane gets, in precedence order: the reader's pasted token, a token fetched from
|
||||
// the reader's token URL, a token the HOST passed (tk attribute / profile-config), and last the
|
||||
// token built into the page (window.MAP_TOKEN, baked at build time).
|
||||
effectiveTk() {
|
||||
return this.userTk || this.fetchedTk || this.tk || this.cfg.tk || this.bakedTk || ''
|
||||
},
|
||||
tokenSource() {
|
||||
if (this.userTk) return 'yours'
|
||||
if (this.fetchedTk) return 'url'
|
||||
if (this.tk || this.cfg.tk || this.bakedTk) return 'built'
|
||||
return 'none'
|
||||
},
|
||||
tokenStatus() {
|
||||
if (this.tokenSource === 'yours') return this.tr('tokenSourceYours')
|
||||
if (this.tokenSource === 'url') return this.tr('tokenSourceUrl', { url: this.userTokenUrl })
|
||||
if (this.tokenSource === 'built') return this.tr('tokenSourceBuilt')
|
||||
return this.tr('tokenSourceNone')
|
||||
},
|
||||
countLabel() {
|
||||
const nb = new Set(this.rows.map(r => r.bundle)).size
|
||||
return this.tr('countLabel', { bundles: nb, elements: this.rows.length })
|
||||
|
|
@ -566,9 +641,73 @@ export default defineComponent({
|
|||
uiDir() { return (this.activeLang && this.activeLang.dir) === 'rtl' ? 'rtl' : 'ltr' }
|
||||
},
|
||||
mounted() {
|
||||
// Read once: after applyToken sets window.MAP_TOKEN it would no longer be "the page's own".
|
||||
this.bakedTk = (typeof window !== 'undefined' && window.MAP_TOKEN) || ''
|
||||
if (this.userTk) this.publishToken()
|
||||
else if (this.userTokenUrl) this.fetchToken(this.userTokenUrl).then((t) => { if (t) { this.fetchedTk = t; this.publishToken() } }).catch(() => {})
|
||||
this.loadManifest()
|
||||
},
|
||||
methods: {
|
||||
// ── MAP TOKEN ────────────────────────────────────────────────────────────────────────────────
|
||||
// A token service answers with JSON ({token|tk|accessToken}) or a bare string — the same contract
|
||||
// the leaf pages' token ladder accepts. Only a PUBLIC token (pk.…) is ever used: this page is public.
|
||||
async fetchToken(url) {
|
||||
if (!/^https?:\/\//i.test(url)) throw new Error('not an http(s) URL')
|
||||
let res
|
||||
try { res = await fetch(url, { cache: 'no-store', credentials: 'omit' }) } catch (e) {
|
||||
throw new Error('the request was blocked or the server did not answer (cross-origin rules can do this)')
|
||||
}
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status)
|
||||
const text = (await res.text()).trim()
|
||||
let tok = text
|
||||
try { const j = JSON.parse(text); tok = (j && (j.token || j.tk || j.accessToken)) || (typeof j === 'string' ? j : '') } catch { /* a bare token string */ }
|
||||
tok = String(tok || '').trim()
|
||||
if (tok.startsWith('sk.')) throw new Error(this.tr('tokenSecret'))
|
||||
if (!tok.startsWith('pk.')) throw new Error(this.tr('tokenNotPk'))
|
||||
return tok
|
||||
},
|
||||
async applyToken() {
|
||||
this.tokenErr = ''
|
||||
const tk = this.tokenDraft, url = this.urlDraft
|
||||
if (tk.startsWith('sk.')) { this.tokenErr = this.tr('tokenSecret'); return }
|
||||
if (tk && !tk.startsWith('pk.')) { this.tokenErr = this.tr('tokenNotPk'); return }
|
||||
this.tokenBusy = true
|
||||
try {
|
||||
let fetched = ''
|
||||
if (!tk && url) {
|
||||
try { fetched = await this.fetchToken(url) } catch (e) { this.tokenErr = this.tr('tokenUrlFailed', { why: e.message }); return }
|
||||
}
|
||||
this.userTk = tk
|
||||
this.userTokenUrl = tk ? '' : url
|
||||
this.fetchedTk = fetched
|
||||
writeStored(TOKEN_KEY, this.userTk)
|
||||
writeStored(TOKEN_URL_KEY, this.userTokenUrl)
|
||||
this.publishToken()
|
||||
this.remountPanes()
|
||||
this.tokenOpen = false
|
||||
} finally { this.tokenBusy = false }
|
||||
},
|
||||
clearToken() {
|
||||
this.tokenDraft = ''; this.urlDraft = ''; this.tokenErr = ''
|
||||
this.userTk = ''; this.userTokenUrl = ''; this.fetchedTk = ''
|
||||
writeStored(TOKEN_KEY, ''); writeStored(TOKEN_URL_KEY, '')
|
||||
this.publishToken()
|
||||
this.remountPanes()
|
||||
},
|
||||
// Maps nested INSIDE a composite read window.MAP_TOKEN rather than their parent's profile-config,
|
||||
// so the effective token is published there too (and the page's own token restored on clear).
|
||||
publishToken() {
|
||||
if (typeof window === 'undefined') return
|
||||
if (this.effectiveTk) window.MAP_TOKEN = this.effectiveTk
|
||||
else if ('MAP_TOKEN' in window) delete window.MAP_TOKEN
|
||||
},
|
||||
// Release every mounted pane (frees its map) and re-mount the one on screen with the new token;
|
||||
// the others pick it up when they are next selected.
|
||||
remountPanes() {
|
||||
for (const row of this.rows) if (row.mounted) this.releaseRow(row)
|
||||
const active = this.active
|
||||
if (active) this.$nextTick(() => { if (this.activeKey === active.key) this.mountElement(active) })
|
||||
},
|
||||
// Localised studio string: current locale → English → the key itself; {name}-style placeholders.
|
||||
tr(key, vars) {
|
||||
const base = (this.locale || 'en').split('-')[0]
|
||||
|
|
@ -603,7 +742,7 @@ export default defineComponent({
|
|||
// and treeBase, so switching language flipped a maplibre pane back to mapbox.
|
||||
paneConfig(row, locale) {
|
||||
const pc = { profile: row.profile, instanceId: 'ps-' + row.key.replace(/\//g, '-'), locale }
|
||||
if (this.tk || this.cfg.tk) pc.tk = this.tk || this.cfg.tk
|
||||
if (this.effectiveTk) pc.tk = this.effectiveTk
|
||||
if (this.tokenUrl || this.cfg.tokenUrl) pc.tokenUrl = this.tokenUrl || this.cfg.tokenUrl
|
||||
const wantProvider = this.provider || this.cfg.provider
|
||||
if (wantProvider === 'maplibre' || wantProvider === 'mapbox') pc.provider = wantProvider
|
||||
|
|
@ -867,6 +1006,21 @@ export default defineComponent({
|
|||
.ps-logo { display: inline-flex; align-items: center; }
|
||||
.ps-logo svg { display: block; }
|
||||
.ps-count { color: #6d766e; }
|
||||
.ps-token { position:relative; display:inline-flex; }
|
||||
.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; }
|
||||
.ps-token-dot.is-none { background:#d27a6a; }
|
||||
.ps-token-panel { position:absolute; top:100%; inset-inline-end:0; margin-top:4px; z-index:30; width:min(340px, 90vw); display:flex; flex-direction:column; gap:8px; padding:12px; border:1px solid #d1d4cd; border-radius:10px; background:#ffffff; box-shadow:0 12px 32px rgba(0,0,0,.25); color:#3b463d; font-size:12px; }
|
||||
.ps-token-label { display:flex; flex-direction:column; gap:4px; }
|
||||
.ps-token-input { font:inherit; font-size:12px; padding:6px 8px; border:1px solid #d1d4cd; border-radius:7px; background:#f8f8f6; color:#3b463d; font-family:ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||
.ps-token-row { display:flex; gap:8px; }
|
||||
.ps-token-apply, .ps-token-clear { font:inherit; font-size:12px; padding:5px 12px; border-radius:7px; border:1px solid #d1d4cd; background:#d1d4cd; color:#3b463d; cursor:pointer; }
|
||||
.ps-token-apply { background:#92b195; border-color:#92b195; color:#fff; }
|
||||
.ps-token-apply:disabled { opacity:.6; cursor:progress; }
|
||||
.ps-token-status { margin:0; }
|
||||
.ps-token-status.err { color:#b3412f; }
|
||||
.ps-token-note { margin:0; color:#6d766e; }
|
||||
.ps-locale { position: relative; display: inline-flex; }
|
||||
.ps-locale-btn { display: inline-flex; align-items: center; gap: 6px; font: inherit; font-size: 12px; padding: 4px 9px; border: 1px solid #d1d4cd; border-radius: 8px; background: #d1d4cd; color: #3b463d; cursor: pointer; }
|
||||
.ps-locale-btn:hover { background: #d1d4cd; }
|
||||
|
|
|
|||
4
public/doxa-maps/doxa-maps-spa/manifest.json
generated
4
public/doxa-maps/doxa-maps-spa/manifest.json
generated
|
|
@ -782,7 +782,7 @@
|
|||
"bundle": "plugin-studio",
|
||||
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
||||
"output": "doxa-maps/plugin-studio/plugin-studio.js",
|
||||
"integrity": "sha384-JkHKL07MX7U5S8CLVSqL5FoDxuS25rpJygq75ozwNJuq8w9TVeZCw5DCZw82CTTA",
|
||||
"integrity": "sha384-iwq2RhhEc/Rx5dfEVoxYtxYjBd+1w47smMC8IDs0VglbZBt9RqwzrFu0SOuPGrBd",
|
||||
"elements": [
|
||||
"plugin-studio"
|
||||
],
|
||||
|
|
@ -810,7 +810,7 @@
|
|||
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./plugin-studio/plugin-studio/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-JkHKL07MX7U5S8CLVSqL5FoDxuS25rpJygq75ozwNJuq8w9TVeZCw5DCZw82CTTA\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-iwq2RhhEc/Rx5dfEVoxYtxYjBd+1w47smMC8IDs0VglbZBt9RqwzrFu0SOuPGrBd\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
|
|||
2
public/doxa-maps/index.html
generated
2
public/doxa-maps/index.html
generated
|
|
@ -14,6 +14,6 @@
|
|||
</head>
|
||||
<body>
|
||||
<plugin-studio tree-base="."></plugin-studio>
|
||||
<script src="./plugin-studio/plugin-studio.js" integrity="sha384-JkHKL07MX7U5S8CLVSqL5FoDxuS25rpJygq75ozwNJuq8w9TVeZCw5DCZw82CTTA" crossorigin="anonymous"></script>
|
||||
<script src="./plugin-studio/plugin-studio.js" integrity="sha384-iwq2RhhEc/Rx5dfEVoxYtxYjBd+1w47smMC8IDs0VglbZBt9RqwzrFu0SOuPGrBd" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
4
public/doxa-maps/manifest.json
generated
4
public/doxa-maps/manifest.json
generated
|
|
@ -782,7 +782,7 @@
|
|||
"bundle": "plugin-studio",
|
||||
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
||||
"output": "doxa-maps/plugin-studio/plugin-studio.js",
|
||||
"integrity": "sha384-JkHKL07MX7U5S8CLVSqL5FoDxuS25rpJygq75ozwNJuq8w9TVeZCw5DCZw82CTTA",
|
||||
"integrity": "sha384-iwq2RhhEc/Rx5dfEVoxYtxYjBd+1w47smMC8IDs0VglbZBt9RqwzrFu0SOuPGrBd",
|
||||
"elements": [
|
||||
"plugin-studio"
|
||||
],
|
||||
|
|
@ -810,7 +810,7 @@
|
|||
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./plugin-studio/plugin-studio/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-JkHKL07MX7U5S8CLVSqL5FoDxuS25rpJygq75ozwNJuq8w9TVeZCw5DCZw82CTTA\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-iwq2RhhEc/Rx5dfEVoxYtxYjBd+1w47smMC8IDs0VglbZBt9RqwzrFu0SOuPGrBd\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
|
|||
37
public/doxa-maps/plugin-studio/plugin-studio.js
generated
37
public/doxa-maps/plugin-studio/plugin-studio.js
generated
File diff suppressed because one or more lines are too long
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <plugin-studio> custom element. -->
|
||||
<script src="../plugin-studio.js" integrity="sha384-JkHKL07MX7U5S8CLVSqL5FoDxuS25rpJygq75ozwNJuq8w9TVeZCw5DCZw82CTTA" crossorigin="anonymous"></script>
|
||||
<script src="../plugin-studio.js" integrity="sha384-iwq2RhhEc/Rx5dfEVoxYtxYjBd+1w47smMC8IDs0VglbZBt9RqwzrFu0SOuPGrBd" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <plugin-studio> custom element. -->
|
||||
<script src="../../plugin-studio.js" integrity="sha384-JkHKL07MX7U5S8CLVSqL5FoDxuS25rpJygq75ozwNJuq8w9TVeZCw5DCZw82CTTA" crossorigin="anonymous"></script>
|
||||
<script src="../../plugin-studio.js" integrity="sha384-iwq2RhhEc/Rx5dfEVoxYtxYjBd+1w47smMC8IDs0VglbZBt9RqwzrFu0SOuPGrBd" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue