WIP Gospel x Internet adopts the kit's useShareableView for its share links

Query mode, the published names as an alias map (links already shared
keep restoring), write only on our own leaves. The board's own writer now
keeps only view/group/panel/tree. NOT READY: an old multi-value link
(band=yes|partial) reads back as one value until the kit reads '|' on
parse; the model check pins that case and fails until it does.
This commit is contained in:
Doxa Maps 2026-09-27 18:57:26 +00:00
parent 9d7d630fd6
commit 9296aa9cc6
14 changed files with 68 additions and 40 deletions

View file

@ -23,6 +23,8 @@
*/ */
import { ref, computed, onMounted, onBeforeUnmount, watch, getCurrentInstance } from 'vue' import { ref, computed, onMounted, onBeforeUnmount, watch, getCurrentInstance } from 'vue'
import { useDashboardLayout } from '@map/composables/useDashboardLayout.js' import { useDashboardLayout } from '@map/composables/useDashboardLayout.js'
import { useShareableView } from '@map/composables/useShareableView.js'
import { SHARE_FACETS } from './src/model.js'
import { useShadowStyles } from '@map/composables/useShadowStyles.js' import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { listLabel } from '@map/lists/peopleGroupList.js' import { listLabel } from '@map/lists/peopleGroupList.js'
import { useBoard } from './src/useBoard.js' import { useBoard } from './src/useBoard.js'
@ -66,6 +68,11 @@ onBeforeUnmount(() => { if (selTimer) clearTimeout(selTimer) })
// board opens already in its state: no flash of the default view, and the first render mounts the panel // board opens already in its state: no flash of the default view, and the first render mounts the panel
// the preset asks for (the language tree included) where a headless mount check can see it. // the preset asks for (the language tree included) where a headless mount check can see it.
board.readUrl() board.readUrl()
// SHARE LINKS FROM THE KIT (useShareableView, query mode): the filters and the typed search live in the
// URL under the board's published names (?access= ?band= ?country= …, the alias map keeps links already
// shared working), restored on every load, written ONLY on our own leaves (urlState:'query') so a
// partner's address bar is never rewritten. facetKeys is explicit: its default reads nothing on a cold load.
useShareableView(bus, { mode: 'query', write: board.profileConfig.value?.urlState === 'query', facetKeys: SHARE_FACETS })
// ── responsive: one panel at a time under 760px (ResizeObserver on the shell, not @container: a // ── responsive: one panel at a time under 760px (ResizeObserver on the shell, not @container: a
// container query cannot target the container itself, which is unreliable inside a shadow root) ────── // container query cannot target the container itself, which is unreliable inside a shadow root) ──────

View file

@ -141,6 +141,19 @@ for (const n of ix.nodes) if (!n.p) { const x = scZero.get(n.id); gy += x.g + x.
ok(gy === 0, `with only zero-access groups in scope, no family counts a green or yellow language (${gy})`) ok(gy === 0, `with only zero-access groups in scope, no family counts a green or yellow language (${gy})`)
bus.clearFilters() bus.clearFilters()
// 12. share links through the kit (useShareableView): the board's published names map to its bus keys,
// and a link already shared restores (single values, and multi-values written with '|')
const SV = await import(resolve(REPO, 'library/composables/useShareableView.js'))
const r1 = SV.parseQuery('?access=bridge&country=IND&wayin=deaf&app=Telegram%20Messenger', M.SHARE_FACETS)
ok(r1.filters.access_level === 'bridge' && r1.filters.country === 'IND' && r1.filters.way_in === 'deaf' && r1.filters.messaging === 'Telegram Messenger',
'kit parser maps ?access= ?wayin= ?app= to the bus keys (old single-value links restore)')
const r2 = SV.parseQuery('?band=yes|partial', M.SHARE_FACETS)
ok(Array.isArray(r2.filters.band) && r2.filters.band.join(',') === 'yes,partial', `an old multi-value link (band=yes|partial) restores as two values (got ${JSON.stringify(r2.filters.band)})`)
const w = SV.serializeQuery({ access_level: 'bridge', band: ['yes', 'partial'] }, '', '?view=zero&utm_source=x', { facetKeys: M.SHARE_FACETS })
ok(w.includes('access=bridge') && w.includes('view=zero') && w.includes('utm_source=x') && !w.includes('access_level'), `the kit writes the published names and keeps the board's own params (${w})`)
const mine = M.buildQuery({ view: 'zero', filterSet: { band: 'yes' }, keep: '?band=yes,partial', filters: false })
ok(mine.includes('band=yes%2Cpartial') && mine.includes('view=zero'), 'the board\'s own writer leaves the filter params to the kit (filters:false)')
// 11. translation readiness: current against its snapshot, every roster language present, the no-NT set is // 11. translation readiness: current against its snapshot, every roster language present, the no-NT set is
// the tree's, the 31 codes Glottolog lacks read as UNKNOWN (null) not "none", and the signal count is a // the tree's, the 31 codes Glottolog lacks read as UNKNOWN (null) not "none", and the signal count is a
// recount of the five named facts (no hidden weighting) // recount of the five named facts (no hidden weighting)

View file

@ -82,6 +82,9 @@ export const URL_KEYS = {
app: 'messaging', app: 'messaging',
} }
export const VIEWS = ['board', 'zero', 'province', 'platforms'] export const VIEWS = ['board', 'zero', 'province', 'platforms']
/** The board's URL filter names -> bus keys, for the kit's useShareableView (query mode, alias map).
* The published names are kept so links already shared keep restoring. */
export const SHARE_FACETS = Object.fromEntries(Object.entries(URL_KEYS))
/** Parse `?view=&access=…&group=` into { view, filters, group }. Unknown views fall back to board. */ /** Parse `?view=&access=…&group=` into { view, filters, group }. Unknown views fall back to board. */
export function parseQuery(search) { export function parseQuery(search) {
@ -109,13 +112,15 @@ export function parseQuery(search) {
} }
/** The inverse: filterSet + view + group -> a query string (no leading '?'), stable key order. */ /** The inverse: filterSet + view + group -> a query string (no leading '?'), stable key order. */
export function buildQuery({ view, filterSet, group, keep, panel, tree } = {}) { export function buildQuery({ view, filterSet, group, keep, panel, tree, filters = true } = {}) {
const q = new URLSearchParams(keep || '') const q = new URLSearchParams(keep || '')
for (const name of [...Object.keys(URL_KEYS), 'view', 'group', 'panel', 'tree']) q.delete(name) // filters:false = the FILTER params belong to another writer (the kit's useShareableView): neither
// delete nor write them, so the two writers never fight over one address bar
for (const name of [...(filters ? Object.keys(URL_KEYS) : []), 'view', 'group', 'panel', 'tree']) q.delete(name)
if (view && view !== 'board') q.set('view', view) if (view && view !== 'board') q.set('view', view)
if (panel === 'tree') q.set('panel', 'tree') if (panel === 'tree') q.set('panel', 'tree')
if (tree) q.set('tree', tree) if (tree) q.set('tree', tree)
for (const [name, key] of Object.entries(URL_KEYS)) { if (filters) for (const [name, key] of Object.entries(URL_KEYS)) {
if (tree && key === 'language') continue if (tree && key === 'language') continue
const v = filterSet?.[key] const v = filterSet?.[key]
if (v === undefined || v === null || v === '') continue if (v === undefined || v === null || v === '') continue

View file

@ -164,9 +164,12 @@ export function useBoard() {
const q = M.parseQuery(location.search) const q = M.parseQuery(location.search)
const hostView = cfg().view const hostView = cfg().view
setView(q.view !== 'board' ? q.view : (M.VIEWS.includes(hostView) ? hostView : 'board')) setView(q.view !== 'board' ? q.view : (M.VIEWS.includes(hostView) ? hostView : 'board'))
// Explicit facets in the URL (or the host's profile-config `filters`) narrow the preset further. // The host's profile-config `filters` narrow the preset; URL FILTER params are read by the kit's
// useShareableView (board.vue), except Gospel Resources' ?codes= / ?code=, which are this board's.
const hostFilters = cfg().filters && typeof cfg().filters === 'object' ? cfg().filters : {} const hostFilters = cfg().filters && typeof cfg().filters === 'object' ? cfg().filters : {}
for (const [k, v] of Object.entries({ ...hostFilters, ...q.filters })) if (k in M.FACET_ACC) bus.setFilter(k, v) for (const [k, v] of Object.entries(hostFilters)) if (k in M.FACET_ACC) bus.setFilter(k, v)
const codes = [...new URLSearchParams(location.search).getAll('codes'), ...new URLSearchParams(location.search).getAll('code')].length ? q.filters.language : null
if (codes) bus.setFilter('language', codes)
const group = q.group || cfg().group || '' const group = q.group || cfg().group || ''
if (group) pendingGroup = group if (group) pendingGroup = group
const panel = q.panel === 'tree' || cfg().panel === 'tree' ? 'tree' : 'map' const panel = q.panel === 'tree' || cfg().panel === 'tree' ? 'tree' : 'map'
@ -186,7 +189,7 @@ export function useBoard() {
function syncUrl() { function syncUrl() {
if (!writeUrl.value || typeof history === 'undefined') return if (!writeUrl.value || typeof history === 'undefined') return
const qs = M.buildQuery({ view: view.value, filterSet: bus.filterSet, group: openSlug.value, keep: location.search, const qs = M.buildQuery({ view: view.value, filterSet: bus.filterSet, group: openSlug.value, keep: location.search,
panel: centre.value, tree: treeNode.value }) panel: centre.value, tree: treeNode.value, filters: false })
const next = location.pathname + (qs ? '?' + qs : '') + location.hash const next = location.pathname + (qs ? '?' + qs : '') + location.hash
if (next !== location.pathname + location.search + location.hash) { if (next !== location.pathname + location.search + location.hash) {
try { history.replaceState(history.state, '', next) } catch (_) { /* sandboxed */ } try { history.replaceState(history.state, '', next) } catch (_) { /* sandboxed */ }

View file

@ -604,7 +604,7 @@
"bundle": "gospel-internet", "bundle": "gospel-internet",
"scriptUrl": "./gospel-internet/gospel-internet.js", "scriptUrl": "./gospel-internet/gospel-internet.js",
"output": "dx-maps/gospel-internet/gospel-internet.js", "output": "dx-maps/gospel-internet/gospel-internet.js",
"integrity": "sha384-wRWCJhXf3QrvCUO5i22k46dpJFvQWnI8dmA1jLl2l4YkiomWTVYPjTfXlKz3HRez", "integrity": "sha384-3be8zy4914UlmNZka99aGQPzEbp80c2Zr4ES+kvPMwW7SEzWNfxYEJgqgpbHr18d",
"elements": [ "elements": [
"gospel-internet-board" "gospel-internet-board"
], ],
@ -623,7 +623,7 @@
"scriptUrl": "./gospel-internet/gospel-internet.js", "scriptUrl": "./gospel-internet/gospel-internet.js",
"embed": { "embed": {
"iframe": "<iframe src=\"./gospel-internet/board/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>", "iframe": "<iframe src=\"./gospel-internet/board/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-wRWCJhXf3QrvCUO5i22k46dpJFvQWnI8dmA1jLl2l4YkiomWTVYPjTfXlKz3HRez\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>" "script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-3be8zy4914UlmNZka99aGQPzEbp80c2Zr4ES+kvPMwW7SEzWNfxYEJgqgpbHr18d\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
}, },
"instances": [ "instances": [
{ {

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. --> <!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-wRWCJhXf3QrvCUO5i22k46dpJFvQWnI8dmA1jLl2l4YkiomWTVYPjTfXlKz3HRez" crossorigin="anonymous"></script> <script src="../../gospel-internet.js" integrity="sha384-3be8zy4914UlmNZka99aGQPzEbp80c2Zr4ES+kvPMwW7SEzWNfxYEJgqgpbHr18d" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. --> <!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../gospel-internet.js" integrity="sha384-wRWCJhXf3QrvCUO5i22k46dpJFvQWnI8dmA1jLl2l4YkiomWTVYPjTfXlKz3HRez" crossorigin="anonymous"></script> <script src="../gospel-internet.js" integrity="sha384-3be8zy4914UlmNZka99aGQPzEbp80c2Zr4ES+kvPMwW7SEzWNfxYEJgqgpbHr18d" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. --> <!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-wRWCJhXf3QrvCUO5i22k46dpJFvQWnI8dmA1jLl2l4YkiomWTVYPjTfXlKz3HRez" crossorigin="anonymous"></script> <script src="../../gospel-internet.js" integrity="sha384-3be8zy4914UlmNZka99aGQPzEbp80c2Zr4ES+kvPMwW7SEzWNfxYEJgqgpbHr18d" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. --> <!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-wRWCJhXf3QrvCUO5i22k46dpJFvQWnI8dmA1jLl2l4YkiomWTVYPjTfXlKz3HRez" crossorigin="anonymous"></script> <script src="../../gospel-internet.js" integrity="sha384-3be8zy4914UlmNZka99aGQPzEbp80c2Zr4ES+kvPMwW7SEzWNfxYEJgqgpbHr18d" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. --> <!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-wRWCJhXf3QrvCUO5i22k46dpJFvQWnI8dmA1jLl2l4YkiomWTVYPjTfXlKz3HRez" crossorigin="anonymous"></script> <script src="../../gospel-internet.js" integrity="sha384-3be8zy4914UlmNZka99aGQPzEbp80c2Zr4ES+kvPMwW7SEzWNfxYEJgqgpbHr18d" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. --> <!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-wRWCJhXf3QrvCUO5i22k46dpJFvQWnI8dmA1jLl2l4YkiomWTVYPjTfXlKz3HRez" crossorigin="anonymous"></script> <script src="../../gospel-internet.js" integrity="sha384-3be8zy4914UlmNZka99aGQPzEbp80c2Zr4ES+kvPMwW7SEzWNfxYEJgqgpbHr18d" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. --> <!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-wRWCJhXf3QrvCUO5i22k46dpJFvQWnI8dmA1jLl2l4YkiomWTVYPjTfXlKz3HRez" crossorigin="anonymous"></script> <script src="../../gospel-internet.js" integrity="sha384-3be8zy4914UlmNZka99aGQPzEbp80c2Zr4ES+kvPMwW7SEzWNfxYEJgqgpbHr18d" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -604,7 +604,7 @@
"bundle": "gospel-internet", "bundle": "gospel-internet",
"scriptUrl": "./gospel-internet/gospel-internet.js", "scriptUrl": "./gospel-internet/gospel-internet.js",
"output": "dx-maps/gospel-internet/gospel-internet.js", "output": "dx-maps/gospel-internet/gospel-internet.js",
"integrity": "sha384-wRWCJhXf3QrvCUO5i22k46dpJFvQWnI8dmA1jLl2l4YkiomWTVYPjTfXlKz3HRez", "integrity": "sha384-3be8zy4914UlmNZka99aGQPzEbp80c2Zr4ES+kvPMwW7SEzWNfxYEJgqgpbHr18d",
"elements": [ "elements": [
"gospel-internet-board" "gospel-internet-board"
], ],
@ -623,7 +623,7 @@
"scriptUrl": "./gospel-internet/gospel-internet.js", "scriptUrl": "./gospel-internet/gospel-internet.js",
"embed": { "embed": {
"iframe": "<iframe src=\"./gospel-internet/board/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>", "iframe": "<iframe src=\"./gospel-internet/board/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-wRWCJhXf3QrvCUO5i22k46dpJFvQWnI8dmA1jLl2l4YkiomWTVYPjTfXlKz3HRez\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>" "script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-3be8zy4914UlmNZka99aGQPzEbp80c2Zr4ES+kvPMwW7SEzWNfxYEJgqgpbHr18d\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
}, },
"instances": [ "instances": [
{ {