From db2479a31080b9cef6d48e1bb4e327a2adda124f Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 00:57:55 +0000 Subject: [PATCH 1/6] Gospel x Internet: the selection strip label is never clipped; only its chips clip --- plugin-designs/gospel-internet/src/components/GiCombos.vue | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugin-designs/gospel-internet/src/components/GiCombos.vue b/plugin-designs/gospel-internet/src/components/GiCombos.vue index ef83f84f3..fa6acb739 100644 --- a/plugin-designs/gospel-internet/src/components/GiCombos.vue +++ b/plugin-designs/gospel-internet/src/components/GiCombos.vue @@ -103,9 +103,10 @@ useShadowStyles( '.gic2{grid-area:combos;display:flex;align-items:center;gap:10px;padding:6px 14px;background:#fff;border-bottom:1px solid #d1d4cd;' + 'min-width:0;font:12.5px/1.3 Roboto,-apple-system,"Segoe UI",sans-serif;color:#3b463d}' + '.gic2-h{flex:none;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:#6d766e}' + - // The selection side is BOUNDED and clips: its nowrap placeholder ran under "Try a combination" at 1280 - // and 1440 (rega r82). It now ellipsizes; the full text stays in the title tooltip. - '.gic2-sel{display:flex;align-items:center;gap:6px;min-width:0;flex:0 1 auto;max-width:40%;overflow:hidden}' + + // The selection side is BOUNDED at 40% (rega r82: the placeholder ran under "Try a combination"), but only + // its chips and placeholder clip: they scroll or ellipsize. No overflow here and no min-width:0, so the + // flex:none label sets the minimum width and is never cut ('YOUR S…' at 390, the zh/ru/hi/ar labels at 1280). + '.gic2-sel{display:flex;align-items:center;gap:6px;flex:0 1 auto;max-width:40%}' + '.gic2-none{color:#9aa19b;font-style:italic;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}' + '.gic2-try{display:flex;align-items:center;gap:6px;min-width:0;flex:1 1 auto;overflow-x:auto;scrollbar-width:thin}' + '.gic2-c{flex:none;display:inline-flex;align-items:center;gap:6px;border:1px solid #d1d4cd;background:#f3f3f1;color:#3b463d;border-radius:999px;' + From f0a31411332c68c21b5da4939706a6d37ba9d25c Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 01:36:32 +0000 Subject: [PATCH 2/6] Gospel x Internet opens on the language tree and its legends start collapsed --- .../gospel-internet/gospel-internet.instances.json | 1 + .../scripts/check-gospel-internet-model.mjs | 3 +++ .../scripts/check-gospel-internet-render.mjs | 8 ++++++-- plugin-designs/gospel-internet/src/components/GiMap.vue | 5 ++--- plugin-designs/gospel-internet/src/components/GiTree.vue | 4 ++-- plugin-designs/gospel-internet/src/model.js | 5 +++-- plugin-designs/gospel-internet/src/useBoard.js | 5 +++-- 7 files changed, 20 insertions(+), 11 deletions(-) diff --git a/plugin-designs/gospel-internet/gospel-internet.instances.json b/plugin-designs/gospel-internet/gospel-internet.instances.json index 7e55be06f..ca843b01e 100644 --- a/plugin-designs/gospel-internet/gospel-internet.instances.json +++ b/plugin-designs/gospel-internet/gospel-internet.instances.json @@ -24,6 +24,7 @@ "props": { "profile": "board", "view": "province", + "panel": "map", "urlState": "query" } }, diff --git a/plugin-designs/gospel-internet/scripts/check-gospel-internet-model.mjs b/plugin-designs/gospel-internet/scripts/check-gospel-internet-model.mjs index e0abc2019..ba2a831cf 100644 --- a/plugin-designs/gospel-internet/scripts/check-gospel-internet-model.mjs +++ b/plugin-designs/gospel-internet/scripts/check-gospel-internet-model.mjs @@ -85,6 +85,9 @@ const back = M.buildQuery({ view: 'zero', filterSet: { band: ['yes', 'partial'], ok(back.includes('lang=ar') && back.includes('list=700') && back.includes('band=yes%7Cpartial') && back.includes('view=zero'), `query builds and keeps lang/list: ${back}`) ok(M.parseQuery('?view=nonsense').view === 'board', 'an unknown view falls back to board') +// the tree is the default panel: a bare URL names no panel, only the map is written, both are read back +ok(M.parseQuery('').panel === '' && M.parseQuery('?panel=map').panel === 'map' && M.parseQuery('?panel=tree').panel === 'tree' + && M.buildQuery({ panel: 'map' }) === 'panel=map' && M.buildQuery({ panel: 'tree' }) === '', 'the tree is the default panel; ?panel=map restores the map') // embed-contract parity with Gospel Resources: ?groups= (peoples by slug) and ?codes= / ?code= (languages) const pg = M.parseQuery('?groups=abai-sungai,hadrami-arabs&codes=abf,ayh') ok(pg.groups.length === 2 && Array.isArray(pg.filters.language) && pg.filters.language.join('|') === 'abf|ayh', 'parses ?groups= (slugs) and ?codes= (languages), comma-separated as Gospel Resources writes them') diff --git a/plugin-designs/gospel-internet/scripts/check-gospel-internet-render.mjs b/plugin-designs/gospel-internet/scripts/check-gospel-internet-render.mjs index ef298845a..5a8ae0ef9 100644 --- a/plugin-designs/gospel-internet/scripts/check-gospel-internet-render.mjs +++ b/plugin-designs/gospel-internet/scripts/check-gospel-internet-render.mjs @@ -50,7 +50,7 @@ try { const res = T('gospel-internet-resources.json') const open = rows.find((r) => r.access_detail === 'none' && r.province) || rows[0] - async function render(lang) { + async function render(lang, extra = { legend: 'open' }) { const app = createSSRApp({ setup() { const b = useBoard() @@ -75,7 +75,7 @@ try { const appI18n = i18n.createAppI18n() app.use(appI18n) appI18n.global.locale.value = lang - app.provide('profileConfig', ref({ profile: 'board', lang })) + app.provide('profileConfig', ref({ profile: 'board', lang, ...extra })) // legends start collapsed; the content checks open them const errors = [] app.config.errorHandler = (e) => { errors.push(String(e && e.stack || e)) } app.config.warnHandler = (m) => { if (/Unhandled error|Property .* was accessed during render but is not defined/.test(m)) errors.push(m) } @@ -84,6 +84,10 @@ try { } const en = await render('en') + // the Driver's default (2026-09-28): with no knob both legends start collapsed behind their "Legend" button + const shut = await render('en', {}) + ok(!/data-tree-legend|class="gim-legend"/.test(shut.html) && /class="gtr-min"/.test(shut.html) && /class="gim-legend-min"/.test(shut.html), + 'en: with no knob, the tree and map legends start collapsed') ok(en.errors.length === 0, `en: every panel rendered with no error (${en.errors.length}${en.errors[0] ? ': ' + en.errors[0].slice(0, 160) : ''})`) for (const [name, re] of [['GiFacetRail', /data-facet="access"/], ['GiGrid', /data-grid-total/], ['GiCharts', /data-region="charts"/], ['GiTree', /data-tree-legend[\s\S]*data-tree-poster/], ['GiResources', /data-resources-total/], ['GiMap', /data-region="map"/]]) ok(re.test(en.html), `en: ${name} rendered its region`) diff --git a/plugin-designs/gospel-internet/src/components/GiMap.vue b/plugin-designs/gospel-internet/src/components/GiMap.vue index 59587c42f..dc0cc75c1 100644 --- a/plugin-designs/gospel-internet/src/components/GiMap.vue +++ b/plugin-designs/gospel-internet/src/components/GiMap.vue @@ -44,7 +44,8 @@ const ready = ref(false) // Province shading: 'zero' = groups with nothing found in their own language (from the rows in scope), // 'women' / 'men' = NFHS-5 ever used the internet (India only; China has no verified provincial source). const provinceMode = ref('zero') -const legendOpen = ref(true) +// the Driver: legends start collapsed, one tap on "Legend" opens them; the embed knob legend:"open" opens them +const legendOpen = ref(board.profileConfig.value?.legend === 'open') let unmounted = false let adm1Table = null // { [shapeName]: { women, men } } India, NFHS-5 const nfhsAttribution = ref('') @@ -313,8 +314,6 @@ onMounted(async () => { // resize the canvas too, or it stretches the old bitmap. let ro = null onMounted(() => { - // A phone opens on the map, not on its legend: at 390px the card covered most of the map (rega r75). - if (mapEl.value && mapEl.value.clientWidth && mapEl.value.clientWidth < 520) legendOpen.value = false if (mapEl.value && typeof ResizeObserver !== 'undefined') { ro = new ResizeObserver(() => { try { mapRef.value && mapRef.value.resize() } catch (_) {} }) ro.observe(mapEl.value) diff --git a/plugin-designs/gospel-internet/src/components/GiTree.vue b/plugin-designs/gospel-internet/src/components/GiTree.vue index 300690018..936b93bf7 100644 --- a/plugin-designs/gospel-internet/src/components/GiTree.vue +++ b/plugin-designs/gospel-internet/src/components/GiTree.vue @@ -43,7 +43,8 @@ const mapRef = shallowRef(null) const err = ref('') const ready = ref(false) const hoverId = ref('') -const legendOpen = ref(true) +// the Driver: legends start collapsed, one tap on "Legend" opens them; the embed knob legend:"open" opens them +const legendOpen = ref(board.profileConfig.value?.legend === 'open') let unmounted = false let ro = null @@ -242,7 +243,6 @@ async function build() { } catch (_) {} } onMounted(() => { - if (el.value && el.value.clientWidth && el.value.clientWidth < 520) legendOpen.value = false // a phone opens on the tree if (el.value && typeof ResizeObserver !== 'undefined') { // the engines only follow WINDOW resizes; a dragged rail or an un-hidden panel must resize the canvas ro = new ResizeObserver(() => { try { mapRef.value && mapRef.value.resize() } catch (_) {} }) diff --git a/plugin-designs/gospel-internet/src/model.js b/plugin-designs/gospel-internet/src/model.js index 122fbde53..0d3ee52c7 100644 --- a/plugin-designs/gospel-internet/src/model.js +++ b/plugin-designs/gospel-internet/src/model.js @@ -104,7 +104,8 @@ export function parseQuery(search) { const codes = [...list('codes'), ...list('code')].filter((c) => /^[a-z]{3}$/.test(c)) if (codes.length && !filters.language) filters.language = codes.length === 1 ? codes[0] : codes const groups = list('groups').filter((s) => /^[a-z0-9-]+$/.test(s)) - const panel = q.get('panel') === 'tree' ? 'tree' : 'map' + // '' = not in the URL: the board opens on its default (the language tree, the Driver 2026-09-28) + const panel = ['tree', 'map'].includes(q.get('panel')) ? q.get('panel') : '' const tree = /^[a-z0-9-]{3,40}$/.test(q.get('tree') || '') ? q.get('tree') : '' // A tree node narrows by LANGUAGE (its ISO codes); the URL carries the node, not the code list. if (tree) delete filters.language @@ -118,7 +119,7 @@ export function buildQuery({ view, filterSet, group, keep, panel, tree, filters // 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 (panel === 'tree') q.set('panel', 'tree') + if (panel === 'map') q.set('panel', 'map') // the tree is the default, so only the map is written if (tree) q.set('tree', tree) if (filters) for (const [name, key] of Object.entries(URL_KEYS)) { if (tree && key === 'language') continue diff --git a/plugin-designs/gospel-internet/src/useBoard.js b/plugin-designs/gospel-internet/src/useBoard.js index d528bf8fe..285429ad2 100644 --- a/plugin-designs/gospel-internet/src/useBoard.js +++ b/plugin-designs/gospel-internet/src/useBoard.js @@ -37,7 +37,7 @@ export function useBoard() { const sort = ref({ key: 'population', dir: -1 }) const openSlug = ref('') // the group in the detail drawer ('' = closed) // ── the centre panel: the geographic map or the language tree (loaded on first use) ─────────────── - const centre = ref('map') + const centre = ref('tree') // the Driver: people come for the language tree; the map is one tap away const tree = shallowRef(null) // indexTree(doc) + { meta } const resources = shallowRef(null) // { meta, rows } — gospel links for the board's languages const treeNode = ref('') // the tree node the board is narrowed to ('' = none) @@ -180,7 +180,8 @@ export function useBoard() { if (codes) bus.setFilter('language', codes) const group = q.group || cfg().group || '' if (group) pendingGroup = group - const panel = q.panel === 'tree' || cfg().panel === 'tree' ? 'tree' : 'map' + // the URL wins, then the embed knob (profile-config panel: 'map'), then the default: the tree + const panel = q.panel || (cfg().panel === 'map' ? 'map' : 'tree') if (q.tree) pendingTree = q.tree if (q.groups && q.groups.length) pendingSelection = q.groups setCentre(panel) From c1fbb8f50f132f27f09cbf7247066fba1e49086f Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 01:37:11 +0000 Subject: [PATCH 3/6] Rebuild gospel-internet: the tree-first default, collapsed legends and the unclipped selection label --- public/dx-maps/doxa-maps-spa/manifest.json | 5 ++- .../gospel-internet/board/board/index.html | 2 +- .../dx-maps/gospel-internet/board/index.html | 2 +- .../board/india-china/index.html | 4 +- .../board/language-tree/index.html | 2 +- .../board/platforms/index.html | 2 +- .../board/project-700/index.html | 2 +- .../gospel-internet/board/zero/index.html | 2 +- .../gospel-internet/gospel-internet.js | 38 +++++++++---------- public/dx-maps/manifest.json | 5 ++- 10 files changed, 33 insertions(+), 31 deletions(-) diff --git a/public/dx-maps/doxa-maps-spa/manifest.json b/public/dx-maps/doxa-maps-spa/manifest.json index 8d975f3c7..ee8f2be8c 100644 --- a/public/dx-maps/doxa-maps-spa/manifest.json +++ b/public/dx-maps/doxa-maps-spa/manifest.json @@ -604,7 +604,7 @@ "bundle": "gospel-internet", "scriptUrl": "./gospel-internet/gospel-internet.js", "output": "dx-maps/gospel-internet/gospel-internet.js", - "integrity": "sha384-1QKATO2zc5eFQVqHLIMkyHx2RcXSpvb2U/+1i2wyQFAy9WQPCR449qwFFu1+8S/Y", + "integrity": "sha384-y/ti76gIc9CO7C0Q74z8teaxCKsB5xbb5sk+aDVBgZLecERoSaFhc+brdokaOTIb", "elements": [ "gospel-internet-board" ], @@ -623,7 +623,7 @@ "scriptUrl": "./gospel-internet/gospel-internet.js", "embed": { "iframe": "", - "script": "\n\n\n\n" + "script": "\n\n\n\n" }, "instances": [ { @@ -662,6 +662,7 @@ "props": { "profile": "board", "view": "province", + "panel": "map", "urlState": "query" }, "element": "gospel-internet-board", diff --git a/public/dx-maps/gospel-internet/board/board/index.html b/public/dx-maps/gospel-internet/board/board/index.html index 2b69d4b47..a071cfa76 100644 --- a/public/dx-maps/gospel-internet/board/board/index.html +++ b/public/dx-maps/gospel-internet/board/board/index.html @@ -36,7 +36,7 @@ - + - + - + - + - + - + - + \n\n" + "script": "\n\n\n\n" }, "instances": [ { @@ -662,6 +662,7 @@ "props": { "profile": "board", "view": "province", + "panel": "map", "urlState": "query" }, "element": "gospel-internet-board", From 86471947fc08a3887e728fd42cdfd1882a9ae162 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 00:57:09 +0000 Subject: [PATCH 4/6] README quick start: a shallow clone, and Go for Rudder's voice engine (everything else works without it) --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b228653e7..011b58452 100644 --- a/README.md +++ b/README.md @@ -646,10 +646,11 @@ a higher-capability plugin from them — that's how the Research Gallery was bui You need **git**, **[Bun](https://bun.sh)** and **Node ≥ 18**, plus a free **[Mapbox](https://account.mapbox.com) public token** (it starts with `pk.`) so the maps have a -background. +background. For Rudder's voice engine, also **[Go](https://go.dev/dl/) 1.22+**, then run +`bash backend/rudder-engine/setup-runtime.sh` once; without Go, everything except voice works. ```bash -git clone https://git.nuxtbase.app/doxa-open/doxa-maps.git +git clone --depth 1 https://git.nuxtbase.app/doxa-open/doxa-maps.git # latest code only: seconds, not hundreds of MB of history cd doxa-maps bun install --frozen-lockfile cp .env.example .env # then paste your pk. token after VITE_MAPBOX_TOKEN= From 8c529c0313db28ffec24e2722a8428aa835036e6 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 01:13:57 +0000 Subject: [PATCH 5/6] Zero-config local setup: .env.example spells out every local default; README love line A fresh clone needs no .env. .env.example lists each local setting with its default, and an empty value means the default: DX_SYNC_REMOTE empty = hot reload of local edits only; DX_PB_URL empty = a local PocketBase. Rudder's settings are marked as shell-environment only, since the dev server reads them there. --- .env.example | 51 ++++++++++++++++++++++++++++++++++++++------------- README.md | 4 +++- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/.env.example b/.env.example index a31e2208b..300456003 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1,41 @@ -# Mapbox token used in dev. Production hosts pass the token via profile-config.tk -# on the host page; never bake into the IIFE bundle. -VITE_MAPBOX_TOKEN=pk.your_dev_token_here +# .env.example — every local setting, each shown with its DEFAULT. +# A fresh clone needs NONE of this: `bun run dev` works with no .env at all. To change something, copy +# this file to .env (or .env.local, which wins; both are gitignored) and edit only the lines you need. +# An empty value means "use the default". + +# ── Maps ───────────────────────────────────────────────────────────────────────────────────────── +# A Mapbox public token (starts with pk.) for the map backgrounds. Empty = no Mapbox background; set +# "provider": "maplibre" on a plugin (or ?provider=maplibre on a page) to draw with MapLibre, no key. +# Production hosts pass the token through profile-config.tk; never bake it into a bundle. +VITE_MAPBOX_TOKEN= +# The data API the maps read when a plugin's profile does not name one (profile apiBaseUrl and a host +# page's window.MAP_APP_API_URL come first). This is the value the maps are built against. VITE_API_BASE_URL=https://pray.doxa.life -# Follow a shared branch while `bun run dev` runs (internal/dev-sync-plugin.mjs): fetch every N seconds and -# fast-forward when this checkout is on that branch with no local edits; otherwise only report it at -# /api/dev-sync. DX_SYNC_REMOTE is a remote name (origin) or a repo URL; leave it unset to turn this off. -# DX_SYNC_REMOTE=origin -# DX_SYNC_BRANCH=main -# DX_SYNC_INTERVAL=5 +# ── Hot reload from a shared branch (internal/dev-sync-plugin.mjs) ─────────────────────────────── +# Empty = OFF: the dev server hot-reloads YOUR local edits only (the default). +# Set to a remote name (origin) or a repo URL (https://…/repo.git) and `bun run dev` fetches every few +# seconds and fast-forwards when this checkout is on that branch with no local edits or commits; otherwise +# it only reports "main is N commits ahead" (in the log and at /api/dev-sync). It never merges over your work. +DX_SYNC_REMOTE= +# The branch to follow. Empty = main. +DX_SYNC_BRANCH= +# Seconds between checks. Empty = 5 (minimum 2). +DX_SYNC_INTERVAL= -# PocketBase for plugins (internal/backend-dev-plugin.mjs): `bun run dev` downloads the pinned binary once -# and serves it at /api/pb, data in .pb/dev. DX_PB_URL uses a shared PocketBase instead of a local one. -# DX_PB_PORT=8090 -# DX_PB_URL= +# ── PocketBase, the backend for plugins (internal/backend-dev-plugin.mjs) ──────────────────────── +# Empty = a LOCAL PocketBase: `bun run dev` downloads the pinned binary once (checksum-verified) and +# serves it at /api/pb, admin UI /api/pb/_/, data in .pb/dev (gitignored). Nothing leaves this computer. +# Set to a PocketBase root (e.g. https:///pb) to use a shared team one instead. +DX_PB_URL= +# The local PocketBase's port. Empty = 8090 (change it only if the log says the port is in use). +DX_PB_PORT= + +# ── Rudder, voice to text (backend/rudder-engine) ──────────────────────────────────────────────── +# These are read from the SHELL ENVIRONMENT, not from this file (e.g. `RUDDER_PORT=6193 bun run dev`). +# Rudder runs when its engine is built: install Go 1.22+ and run `bash backend/rudder-engine/setup-runtime.sh`. +# RUDDER_ENGINE 0 = never start the engine here (default: start it if it is built) +# RUDDER_PORT the engine's port (default 5193) +# RUDDER_NEMO_PORT the speech server's port (default 5194) +# RUDDER_HOME where the runtime and model live (default ~/.local/share/rudder) +# RUDDER_LANG the default language (default en-US) diff --git a/README.md b/README.md index 011b58452..7b58cf4fe 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Made by Vibe Coders for Vibe Coders to prepare the way of the Lord and make His **Isaiah 40:3 “...Prepare the way for the LORD in the wilderness; make a straight highway for our God in the desert."** +Built with love, with the prayer that these maps bless the nations. + # dx-maps — one build, maps that embed everywhere **Write a map once in `plugin-designs//`, run one build, and drop it on any site** — @@ -653,7 +655,7 @@ background. For Rudder's voice engine, also **[Go](https://go.dev/dl/) 1.22+**, git clone --depth 1 https://git.nuxtbase.app/doxa-open/doxa-maps.git # latest code only: seconds, not hundreds of MB of history cd doxa-maps bun install --frozen-lockfile -cp .env.example .env # then paste your pk. token after VITE_MAPBOX_TOKEN= +cp .env.example .env # optional: paste your pk. token after VITE_MAPBOX_TOKEN= bun run dev # → http://localhost:5173/ is the Plugin Studio ``` From 7138385774a3c2c9f3b936e85a70d24902bd33cf Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 01:26:47 +0000 Subject: [PATCH 6/6] README: link the cloud development guide next to the join-the-table guide --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7b58cf4fe..c0104f5c8 100644 --- a/README.md +++ b/README.md @@ -708,8 +708,9 @@ The studio is a plugin like any other: `plugin-designs/plugin-studio/plugin-stud their browser within seconds, with no rebuild. Add OIDC settings and it puts a "Sign in with *your provider*" page in front of everything, open only to the groups you allow. Recipe and settings: [deployment/cloud/](./deployment/cloud/README.md). -Working from your own laptop alongside the team (following the shared branch, the team's PocketBase): -[documentation/HOWTO-join-the-table.md](./documentation/HOWTO-join-the-table.md). +Working from your own laptop alongside the team (zero config, everything local by default): +[documentation/HOWTO-join-the-table.md](./documentation/HOWTO-join-the-table.md). When your agent runs on a +server instead: [documentation/CLOUD-DEVELOPMENT-ENVIRONMENT.md](./documentation/CLOUD-DEVELOPMENT-ENVIRONMENT.md). ---