Update from source 7c360016f
This commit is contained in:
parent
8d4866e4d0
commit
7bed09f2a1
55 changed files with 703 additions and 1047 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -150,3 +150,6 @@ data-private/
|
||||||
/backend/bin/
|
/backend/bin/
|
||||||
# The local PocketBase's data (`bun run dev`, internal/backend-dev-plugin.mjs): per laptop, never committed.
|
# The local PocketBase's data (`bun run dev`, internal/backend-dev-plugin.mjs): per laptop, never committed.
|
||||||
/.pb/
|
/.pb/
|
||||||
|
# bun run desktop:build <plugin>: the staged plugin and the built sidecar engines (never committed)
|
||||||
|
/.desktop/
|
||||||
|
/deployment/src-tauri/binaries/
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,46 @@
|
||||||
// main.rs — the whole desktop shell.
|
// main.rs — the whole desktop shell.
|
||||||
//
|
//
|
||||||
// A Tauri app around the SAME public/ tree Capacitor and the CDN serve. The window, its
|
// A Tauri app around a built web tree. The window, its size and the entry page all come from
|
||||||
// size and the entry page all come from ../tauri.conf.json (app.windows[0]) so there is
|
// ../tauri.conf.json (app.windows[0]) — or from the --config override `bun run desktop:build <plugin>`
|
||||||
// ONE place to change them and no value is duplicated between config and code.
|
// passes — so there is ONE place to change them and no value is duplicated between config and code.
|
||||||
//
|
//
|
||||||
// No commands, no IPC, no state. The maps are a compiled Vue custom element loaded by the
|
// No commands, no IPC. The ONE piece of logic: a desktop plugin's engine. When the app bundles a
|
||||||
// webview exactly as a browser loads them; giving this shell logic would fork behaviour
|
// `rudder-engine` sidecar (Tauri places it next to this executable), it is started with the webview's
|
||||||
// between desktop and web, which is the opposite of what one-build-four-consumers buys us.
|
// microphone mode (RUDDER_AUDIO=browser: the page captures the mic and streams it to the engine) and its
|
||||||
|
// runtime + model under the app's data directory, and it is stopped when the app exits. An app without the
|
||||||
|
// sidecar (the map gallery) starts nothing.
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
use std::process::{Child, Command};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use tauri::{Manager, RunEvent};
|
||||||
|
|
||||||
|
struct Sidecar(Mutex<Option<Child>>);
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
let app = tauri::Builder::default()
|
||||||
.run(tauri::generate_context!())
|
.manage(Sidecar(Mutex::new(None)))
|
||||||
.expect("error while running the Doxa Maps desktop shell");
|
.setup(|app| {
|
||||||
|
let exe_dir = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf()));
|
||||||
|
if let Some(dir) = exe_dir {
|
||||||
|
let bin = dir.join(if cfg!(windows) { "rudder-engine.exe" } else { "rudder-engine" });
|
||||||
|
if bin.exists() {
|
||||||
|
let home = app.path().app_data_dir().map(|d| d.join("rudder")).unwrap_or_default();
|
||||||
|
match Command::new(&bin).env("RUDDER_AUDIO", "browser").env("RUDDER_HOME", &home).spawn() {
|
||||||
|
Ok(child) => *app.state::<Sidecar>().0.lock().unwrap() = Some(child),
|
||||||
|
Err(e) => eprintln!("rudder-engine did not start: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.build(tauri::generate_context!())
|
||||||
|
.expect("error while building the Doxa Maps desktop shell");
|
||||||
|
app.run(|handle, event| {
|
||||||
|
if let RunEvent::Exit = event {
|
||||||
|
if let Some(mut child) = handle.state::<Sidecar>().0.lock().unwrap().take() {
|
||||||
|
let _ = child.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ desktop path has never been run by anyone, and the steps say so where that is th
|
||||||
| The engine **compiling** for macOS (arm64, x86_64) and Windows (x86_64) | Proven by cross-compiling. It has **never been run** on either. |
|
| The engine **compiling** for macOS (arm64, x86_64) and Windows (x86_64) | Proven by cross-compiling. It has **never been run** on either. |
|
||||||
| The engine accepting a WebSocket from the Tauri webview (`tauri://localhost`, `http://tauri.localhost`) | Proven against a running engine; another site's origin is refused. |
|
| The engine accepting a WebSocket from the Tauri webview (`tauri://localhost`, `http://tauri.localhost`) | Proven against a running engine; another site's origin is refused. |
|
||||||
| NeMo-Speech.cpp runtime for macOS / Windows | Published by NVIDIA (release v0.1.0: `macos-aarch64-cpu`, `macos-aarch64-metal`, `macos-x86_64-cpu`, `windows-x86_64-cpu` / `-cuda` / `-vulkan`). **Never run here.** |
|
| NeMo-Speech.cpp runtime for macOS / Windows | Published by NVIDIA (release v0.1.0: `macos-aarch64-cpu`, `macos-aarch64-metal`, `macos-x86_64-cpu`, `windows-x86_64-cpu` / `-cuda` / `-vulkan`). **Never run here.** |
|
||||||
| Rudder as a sidecar, first-run model download, installer | **Not built.** Steps 5–8 below are the work to do. |
|
| `bun run desktop:build rudder-voice` on **Linux** | Proven: produced `Rudder_0.1.0_amd64.deb` (8.8 MB) holding the app and the `rudder-engine` sidecar side by side. The app has **not been launched** from it yet (no display on the build box). |
|
||||||
|
| The shell starting and stopping the engine sidecar | Built (`deployment/src-tauri/src/main.rs`), compiled into that installer; not yet run. |
|
||||||
|
| First-run model download | **Not built.** Run `setup-runtime.sh` into the app's data folder by hand (step 7). |
|
||||||
|
|
||||||
## 1. Prerequisites
|
## 1. Prerequisites
|
||||||
|
|
||||||
|
|
@ -50,65 +52,35 @@ cd dx-maps
|
||||||
bun install
|
bun install
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. Build the web tree
|
## 3. No web build needed
|
||||||
|
|
||||||
|
`desktop:build` builds the one plugin it packages; `bun run build` (the web tree) is not a prerequisite.
|
||||||
|
|
||||||
|
## 4. Build the app: one command
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run build
|
bun run desktop:build rudder-voice
|
||||||
```
|
```
|
||||||
|
|
||||||
This writes `public/dx-maps/`, the tree every deployment serves, including
|
It packages ONE plugin whose `<profile>.meta.json` `"targets"` include `desktop` (see
|
||||||
`public/dx-maps/rudder-voice/`. The Tauri config points at that folder (`frontendDist`); it does **not**
|
`skills/plugin-designer/references/targets.md`), and nothing in `public/` or the committed tree changes:
|
||||||
build it for you (`beforeBuildCommand` is deliberately empty; see `deployment/src-tauri/README.md`).
|
|
||||||
**Build first, then package.**
|
|
||||||
|
|
||||||
## 4. Build the engine for this machine
|
1. the plugin is built into `.desktop/rudder-voice/`, with a one-element page that gives it the engine's
|
||||||
|
address (`apiBase: http://127.0.0.1:5193/api/rudder`);
|
||||||
|
2. its `"backend"` (`backend/rudder-engine`) is built for this machine as
|
||||||
|
`deployment/src-tauri/binaries/rudder-engine-<target triple>` (Tauri's sidecar naming);
|
||||||
|
3. `tauri build` runs with a `--config` override (the window opens that page, the app is named from the
|
||||||
|
meta label, the engine is bundled); `tauri.conf.json` is not edited.
|
||||||
|
|
||||||
Tauri expects a sidecar binary named with the target triple:
|
The first run compiles Tauri's dependencies (several minutes, about 2 GB under
|
||||||
|
`deployment/src-tauri/target/`); later runs reuse it.
|
||||||
|
|
||||||
```bash
|
The shell (`deployment/src-tauri/src/main.rs`) starts the bundled `rudder-engine` with
|
||||||
# macOS, Apple Silicon
|
`RUDDER_AUDIO=browser` (the webview captures the mic; the engine's own recorders are Linux-only) and
|
||||||
cd backend/rudder-engine && go build -o ../../deployment/src-tauri/binaries/rudder-engine-aarch64-apple-darwin . && cd ../..
|
`RUDDER_HOME=<the app's data folder>/rudder`, and stops it when the app quits. The engine accepts the
|
||||||
# macOS, Intel
|
webview's origins (`tauri://localhost`, `http://tauri.localhost`).
|
||||||
cd backend/rudder-engine && go build -o ../../deployment/src-tauri/binaries/rudder-engine-x86_64-apple-darwin . && cd ../..
|
|
||||||
# Windows (PowerShell)
|
|
||||||
cd backend/rudder-engine; go build -o ..\..\deployment\src-tauri\binaries\rudder-engine-x86_64-pc-windows-msvc.exe .; cd ..\..
|
|
||||||
```
|
|
||||||
|
|
||||||
`rustc -Vv` prints the exact triple (`host:` line) if unsure.
|
## 5. The speech runtime and model (first run, still by hand)
|
||||||
|
|
||||||
## 5. Make the Tauri shell start the engine (NOT BUILT: this is the work)
|
|
||||||
|
|
||||||
1. **Declare the sidecar** in `deployment/src-tauri/tauri.conf.json`:
|
|
||||||
`"bundle": { "externalBin": ["binaries/rudder-engine"], "targets": ["dmg"] }` on macOS, or
|
|
||||||
`["nsis"]` (or `["msi"]`) on Windows. Keep the file schema-pure: **no `//` comment keys**, Tauri
|
|
||||||
rejects unknown fields. Run `bun run check:tauri` after editing it.
|
|
||||||
2. **Add the shell plugin** (`tauri-plugin-shell`) to `deployment/src-tauri/Cargo.toml` and register it
|
|
||||||
in `src/main.rs`, then spawn the sidecar at startup with these environment variables:
|
|
||||||
- `RUDDER_AUDIO=browser`: the engine takes the **webview's** microphone. Its own recorders
|
|
||||||
(`parec` / `pw-record` / `arecord`) exist only on Linux, so on macOS and Windows this is required.
|
|
||||||
- `RUDDER_HOME=<the app's data directory>` (Tauri's `app_data_dir()`): where the runtime and model live.
|
|
||||||
- `RUDDER_PORT=5193` (the default) or a free port you pass on to the page (step 6).
|
|
||||||
Kill the sidecar when the window closes. On macOS and Windows the engine cannot ask the OS to kill
|
|
||||||
its own speech server if the engine crashes (Linux-only feature), so the shell should own that.
|
|
||||||
3. `src/main.rs` currently has no logic by design ("a window around the web tree"). Rudder is the first
|
|
||||||
desktop plugin with a backend, so this is the first logic it gains; keep it to starting and
|
|
||||||
stopping the sidecar.
|
|
||||||
|
|
||||||
## 6. Open Rudder, and tell it where the engine is (NOT BUILT)
|
|
||||||
|
|
||||||
The window opens `doxa-maps-spa/doxa-maps-spa/index.html` today. For a Rudder app, open Rudder's own
|
|
||||||
page, `rudder-voice/rudder-voice/index.html`, and give the element the engine's **absolute** address:
|
|
||||||
|
|
||||||
```
|
|
||||||
profile-config = {"apiBase": "http://127.0.0.1:5193/api/rudder"}
|
|
||||||
```
|
|
||||||
|
|
||||||
A relative `/api/rudder` would resolve against `tauri://localhost` and reach nothing. How the page
|
|
||||||
receives that config (a small wrapper page, a query parameter, or the shell injecting it) is not
|
|
||||||
decided and not built; pick the smallest option and say which in your report. The engine already
|
|
||||||
answers CORS for the Tauri origins and accepts their WebSocket.
|
|
||||||
|
|
||||||
## 7. The speech runtime and model on first run (NOT BUILT)
|
|
||||||
|
|
||||||
`backend/rudder-engine/setup-runtime.sh` is the reference: it pins the runtime (NeMo-Speech.cpp
|
`backend/rudder-engine/setup-runtime.sh` is the reference: it pins the runtime (NeMo-Speech.cpp
|
||||||
**0.1.0**) and the model (`nemotron-3.5-asr-streaming-0.6b.q8_0.gguf`, Hugging Face revision
|
**0.1.0**) and the model (`nemotron-3.5-asr-streaming-0.6b.q8_0.gguf`, Hugging Face revision
|
||||||
|
|
@ -128,16 +100,9 @@ macOS and Windows the app must do the same on first run:
|
||||||
real path of the `nemo-speech` binary.
|
real path of the `nemo-speech` binary.
|
||||||
- Show progress: the first start downloads ~750 MB and warms the model (~20 s on a laptop CPU).
|
- Show progress: the first start downloads ~750 MB and warms the model (~20 s on a laptop CPU).
|
||||||
|
|
||||||
## 8. Package
|
## 6. Where the installer lands
|
||||||
|
|
||||||
```bash
|
`desktop:build` prints the path. The installer lands under `deployment/src-tauri/target/release/bundle/`: `dmg/` on macOS, `nsis/` or
|
||||||
bun run tauri:build
|
|
||||||
```
|
|
||||||
|
|
||||||
Run it from the repo root. If the CLI cannot find the config, run `cargo tauri build` inside
|
|
||||||
`deployment/src-tauri/` (untested either way on macOS/Windows).
|
|
||||||
|
|
||||||
The installer lands under `deployment/src-tauri/target/release/bundle/`: `dmg/` on macOS, `nsis/` or
|
|
||||||
`msi/` on Windows.
|
`msi/` on Windows.
|
||||||
|
|
||||||
- **macOS:** the webview needs `NSMicrophoneUsageDescription` in the app's `Info.plist` (Tauri: an
|
- **macOS:** the webview needs `NSMicrophoneUsageDescription` in the app's `Info.plist` (Tauri: an
|
||||||
|
|
@ -146,7 +111,7 @@ The installer lands under `deployment/src-tauri/target/release/bundle/`: `dmg/`
|
||||||
to give it to others.
|
to give it to others.
|
||||||
- **Windows:** WebView2 asks for microphone permission on first use.
|
- **Windows:** WebView2 asks for microphone permission on first use.
|
||||||
|
|
||||||
## 9. Check it works
|
## 7. Check it works
|
||||||
|
|
||||||
1. Launch the app. Rudder's status reads "Loading speech model…" and then **Ready**.
|
1. Launch the app. Rudder's status reads "Loading speech model…" and then **Ready**.
|
||||||
2. Press **Record**, speak, and watch the words stream into the box. Press **Stop**: the last words
|
2. Press **Record**, speak, and watch the words stream into the box. Press **Stop**: the last words
|
||||||
|
|
@ -156,7 +121,7 @@ The installer lands under `deployment/src-tauri/target/release/bundle/`: `dmg/`
|
||||||
|
|
||||||
## Known gaps (untested; report what you find)
|
## Known gaps (untested; report what you find)
|
||||||
|
|
||||||
- Nothing in steps 5–8 has been run on macOS or Windows.
|
- `desktop:build` has run on Linux only; the app it produced has not been launched yet. Nothing has run on macOS or Windows.
|
||||||
- The macOS/Windows runtime archive layout, and whether the engine's derived path matches it.
|
- The macOS/Windows runtime archive layout, and whether the engine's derived path matches it.
|
||||||
- The first-run download, including resuming an interrupted 742 MB download.
|
- The first-run download, including resuming an interrupted 742 MB download.
|
||||||
- Mapbox basemaps in the Research Gallery need the token host to allow the Tauri origin
|
- Mapbox basemaps in the Research Gallery need the token host to allow the Tauri origin
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,6 @@ Generated by a per-file scan. The `@map/…` import path mirrors each `library/`
|
||||||
| `library/composables/useMapFly.js` | Camera navigation: fly/fitBounds helpers, panel-offset centering, antimeridian-safe legend-row zoom. | zoomToLegendRow reads getSource()._data and re-implements Mapbox filter evaluation in JS — extend _matchesFilter/_resolveExpr for new expression ops; wide spans ease to a world overview instead of fitBounds. |
|
| `library/composables/useMapFly.js` | Camera navigation: fly/fitBounds helpers, panel-offset centering, antimeridian-safe legend-row zoom. | zoomToLegendRow reads getSource()._data and re-implements Mapbox filter evaluation in JS — extend _matchesFilter/_resolveExpr for new expression ops; wide spans ease to a world overview instead of fitBounds. |
|
||||||
| `library/composables/useMapInstance.js` | One map's lifecycle: shadow-DOM CSS injection, aspect-aware dynamic minZoom floor, edge scroll strips, teardown. Returns map, isMapReady, initializeMap, destroy. | initializeMap() in onMounted with a real DOM element ref (string IDs break in shadow DOM); minZoom is a lower bound raised to a desktop-only no-duplicate-world floor; expects sibling `.map-scroll-edge` strips and global mapboxgl; always call destroy(). |
|
| `library/composables/useMapInstance.js` | One map's lifecycle: shadow-DOM CSS injection, aspect-aware dynamic minZoom floor, edge scroll strips, teardown. Returns map, isMapReady, initializeMap, destroy. | initializeMap() in onMounted with a real DOM element ref (string IDs break in shadow DOM); minZoom is a lower bound raised to a desktop-only no-duplicate-world floor; expects sibling `.map-scroll-edge` strips and global mapboxgl; always call destroy(). |
|
||||||
| `library/composables/useMapLayers.js` | Creates/manages all Mapbox layers (pin GeoJSON with family derivation + co-location fan-out, regions fill/border, connection lines, prayer-glow rAF ripple) and theme-aware pin ring widths. | Do NOT add a style.load listener here — the host profile owns post-setStyle reconciliation (flash race). addPrayerGlowLayers is idempotent; keep zoom 'case' inside interpolate OUTPUTS; dark-ring exceptions are config-only, keyed by strategy name. |
|
| `library/composables/useMapLayers.js` | Creates/manages all Mapbox layers (pin GeoJSON with family derivation + co-location fan-out, regions fill/border, connection lines, prayer-glow rAF ripple) and theme-aware pin ring widths. | Do NOT add a style.load listener here — the host profile owns post-setStyle reconciliation (flash race). addPrayerGlowLayers is idempotent; keep zoom 'case' inside interpolate OUTPUTS; dark-ring exceptions are config-only, keyed by strategy name. |
|
||||||
| `library/composables/useMapPacket.js` | Multi-page "prayer packet" PDF: page 1 = poster map raster, then one jsPDF vector card per people group. Returns renderPacket, progress, isRendering. | Page 1 reuses useMapPoster.render — keep that the single source of truth; jspdf lazy-imported; footer attribution on every page is a ToS requirement; field reads deliberately defensive across 3 object shapes. |
|
|
||||||
| `library/composables/useMapSelection.js` | Applies region/family selection to layer paint props (highlight/fade opacity, connection lines). | Region matching is on iso_3166_1_alpha_3 (tileset has no `region` prop); labels fuzzy-normalized to slugs; selection state lives in the Pinia mapStore. |
|
| `library/composables/useMapSelection.js` | Applies region/family selection to layer paint props (highlight/fade opacity, connection lines). | Region matching is on iso_3166_1_alpha_3 (tileset has no `region` prop); labels fuzzy-normalized to slugs; selection state lives in the Pinia mapStore. |
|
||||||
| `library/composables/useMapTheme.js` | Seamless light/dark toggle by flipping Mapbox Standard's lightPreset/theme config props. Returns bootStyle, applyTheme, swapTheme, isDarkTheme. | Never setStyle to switch themes (reloads map, wipes custom layers); applyTheme must run after style.load and re-asserts once on 'idle' (fast-toggle grey-film race); profiles should watch(uiStore.theme) → applyTheme. |
|
| `library/composables/useMapTheme.js` | Seamless light/dark toggle by flipping Mapbox Standard's lightPreset/theme config props. Returns bootStyle, applyTheme, swapTheme, isDarkTheme. | Never setStyle to switch themes (reloads map, wipes custom layers); applyTheme must run after style.load and re-asserts once on 'idle' (fast-toggle grey-film race); profiles should watch(uiStore.theme) → applyTheme. |
|
||||||
| `library/composables/usePplrInstance.js` | Per-map mediator "instance store" (provide/inject refs) so map, geocoder, legend in one cluster communicate. Exports createPplrInstance, provideInstance, useInstance, selectNode, clearAll. | Not Pinia — plain refs under key 'pplrInstance'; useInstance() returns null when standalone, so consumers must null-guard; profiles bridge instance.selection to their own store. |
|
| `library/composables/usePplrInstance.js` | Per-map mediator "instance store" (provide/inject refs) so map, geocoder, legend in one cluster communicate. Exports createPplrInstance, provideInstance, useInstance, selectNode, clearAll. | Not Pinia — plain refs under key 'pplrInstance'; useInstance() returns null when standalone, so consumers must null-guard; profiles bridge instance.selection to their own store. |
|
||||||
|
|
|
||||||
|
|
@ -163,8 +163,6 @@ and a change here can break other maps.
|
||||||
`import … from '@map/composables/useMapInstance.js'`
|
`import … from '@map/composables/useMapInstance.js'`
|
||||||
- **useMapLayers** — useMapLayers.js - Map Layers Composable
|
- **useMapLayers** — useMapLayers.js - Map Layers Composable
|
||||||
`import … from '@map/composables/useMapLayers.js'`
|
`import … from '@map/composables/useMapLayers.js'`
|
||||||
- **useMapPacket** — useMapPacket — render a multi-page "prayer packet" PDF for a map selection.
|
|
||||||
`import … from '@map/composables/useMapPacket.js'`
|
|
||||||
- **useMapProvider** — useMapProvider.js — resolve the active map engine ("provider").
|
- **useMapProvider** — useMapProvider.js — resolve the active map engine ("provider").
|
||||||
`import … from '@map/composables/useMapProvider.js'`
|
`import … from '@map/composables/useMapProvider.js'`
|
||||||
- **useMapSelection** — useMapSelection.js - Map Selection Composable
|
- **useMapSelection** — useMapSelection.js - Map Selection Composable
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
* a fresh tree — the mtime false alarm is gone.
|
* a fresh tree — the mtime false alarm is gone.
|
||||||
*/
|
*/
|
||||||
import { readFileSync, existsSync, readdirSync, mkdtempSync, rmSync } from 'node:fs'
|
import { readFileSync, existsSync, readdirSync, mkdtempSync, rmSync } from 'node:fs'
|
||||||
|
import { isWebBuilt } from './plugin-targets.mjs'
|
||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
import { spawnSync } from 'node:child_process'
|
import { spawnSync } from 'node:child_process'
|
||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
|
|
@ -64,7 +65,9 @@ const manifest = JSON.parse(readFileSync(join(TREE, 'manifest.json'), 'utf8'))
|
||||||
// tag -> owning bundle, so a composes entry naming an ELEMENT still resolves.
|
// tag -> owning bundle, so a composes entry naming an ELEMENT still resolves.
|
||||||
const byTag = new Map()
|
const byTag = new Map()
|
||||||
for (const b of manifest.bundles || []) for (const t of b.elements || []) byTag.set(t, b.bundle)
|
for (const b of manifest.bundles || []) for (const t of b.elements || []) byTag.set(t, b.bundle)
|
||||||
const bundles = new Set(readdirSync(DESIGNS).filter((d) => existsSync(join(DESIGNS, d, 'index.js'))))
|
// A plugin that is not built into the web tree (plugin-targets.mjs: targets without "web") has no committed
|
||||||
|
// bundle by design, so it is not held to one.
|
||||||
|
const bundles = new Set(readdirSync(DESIGNS).filter((d) => existsSync(join(DESIGNS, d, 'index.js')) && isWebBuilt(join(DESIGNS, d))))
|
||||||
// A bare PROFILE name of the same bundle (gospel-resources composes "resources-map", "data-tables" —
|
// A bare PROFILE name of the same bundle (gospel-resources composes "resources-map", "data-tables" —
|
||||||
// its own sibling profiles) resolves to that bundle, as the studio resolves it.
|
// its own sibling profiles) resolves to that bundle, as the studio resolves it.
|
||||||
const profilesOf = new Map((manifest.bundles || []).map((b) => [b.bundle, new Set((b.profiles || []).map((p) => p.profile))]))
|
const profilesOf = new Map((manifest.bundles || []).map((b) => [b.bundle, new Set((b.profiles || []).map((p) => p.profile))]))
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,9 @@
|
||||||
* NEGATIVE-TESTED: reintroducing a private `const CATEGORICAL = ['#...']` into the aggregations
|
* NEGATIVE-TESTED: reintroducing a private `const CATEGORICAL = ['#...']` into the aggregations
|
||||||
* composable fails assertion 2 by name; an `[i % CATEGORICAL.length]` lookup fails assertion 3.
|
* composable fails assertion 2 by name; an `[i % CATEGORICAL.length]` lookup fails assertion 3.
|
||||||
*/
|
*/
|
||||||
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
import { readFileSync, existsSync, readdirSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
||||||
import { resolve, dirname, join } from 'node:path'
|
import { resolve, dirname, join } from 'node:path'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { TREE_NAME } from './_tree-name.mjs'
|
import { TREE_NAME } from './_tree-name.mjs'
|
||||||
|
|
||||||
|
|
@ -96,7 +97,32 @@ export function paletteHexes(src) {
|
||||||
return [...new Set(strings.flatMap((t) => [...t.matchAll(/#[0-9a-fA-F]{6}\b/g)].map((m) => m[0].toLowerCase())))]
|
return [...new Set(strings.flatMap((t) => [...t.matchAll(/#[0-9a-fA-F]{6}\b/g)].map((m) => m[0].toLowerCase())))]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The strategy palettes in one colors folder: library/colors/, or a bundle's own
|
||||||
|
* plugin-designs/<bundle>/src/colors/ (bundle-private strategies merge OVER the library set for that
|
||||||
|
* bundle — see that folder's README). Filename → key as the registry names it is not needed here; the
|
||||||
|
* file stem is the key both places use.
|
||||||
|
*/
|
||||||
|
export function collectStrategies(dir) {
|
||||||
|
const out = {}
|
||||||
|
if (!existsSync(dir)) return out
|
||||||
|
for (const f of readdirSync(dir)) {
|
||||||
|
if (!f.endsWith('.js') || f.startsWith('_') || f.startsWith('.') || f === 'index.js') continue
|
||||||
|
const hexes = paletteHexes(readFileSync(join(dir, f), 'utf8'))
|
||||||
|
if (hexes && hexes.length) out[f.replace(/\.js$/, '')] = hexes
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
if (process.argv.includes('--self-test')) {
|
if (process.argv.includes('--self-test')) {
|
||||||
|
// A bundle's PRIVATE palette (src/colors/<mode>.js) must be seen, not only library/colors/ (the gate was
|
||||||
|
// blind to it: upg100's census, BUG-CHECK-ONE-PALETTE-BLIND-TO-PRIVATE-PALETTES).
|
||||||
|
const tmp = mkdtempSync(join(tmpdir(), 'one-palette-'))
|
||||||
|
writeFileSync(join(tmp, 'religion.js'), "export const PALETTE = { islam: '#010203', hinduism: '#040506' }\n")
|
||||||
|
writeFileSync(join(tmp, '_skip.js'), "export const PALETTE = { x: '#999999' }\n")
|
||||||
|
const seen = JSON.stringify(collectStrategies(tmp))
|
||||||
|
rmSync(tmp, { recursive: true, force: true })
|
||||||
|
if (seen !== JSON.stringify({ religion: ['#010203', '#040506'] })) { console.error(`ONE-PALETTE: SELF-TEST FAIL — a planted private palette read as ${seen}`); process.exit(1) }
|
||||||
const src = [
|
const src = [
|
||||||
"export const PALETTE = {",
|
"export const PALETTE = {",
|
||||||
" a: '#112233', // not #445566 (a comment inside the export)",
|
" a: '#112233', // not #445566 (a comment inside the export)",
|
||||||
|
|
@ -107,18 +133,14 @@ if (process.argv.includes('--self-test')) {
|
||||||
].join('\n')
|
].join('\n')
|
||||||
const got = paletteHexes(src).join(',')
|
const got = paletteHexes(src).join(',')
|
||||||
if (got !== '#112233,#aabbcc') { console.error(`ONE-PALETTE: SELF-TEST FAIL — read ${got}, want #112233,#aabbcc`); process.exit(1) }
|
if (got !== '#112233,#aabbcc') { console.error(`ONE-PALETTE: SELF-TEST FAIL — read ${got}, want #112233,#aabbcc`); process.exit(1) }
|
||||||
console.log('ONE-PALETTE: SELF-TEST PASS — only the PALETTE export\'s string values are read; comments and later code are not.')
|
console.log('ONE-PALETTE: SELF-TEST PASS — only the PALETTE export\'s string values are read; comments and later code are not; a bundle\'s private src/colors/ palette is seen.')
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── the expected palettes, read from the strategy modules the MAP uses ────────────────────────
|
// ── the expected palettes, read from the strategy modules the MAP uses ────────────────────────
|
||||||
const strategies = {}
|
// The library set, and per bundle its own src/colors/ strategies merged over it (same key overrides).
|
||||||
for (const f of readdirSync(COLORS)) {
|
const strategies = collectStrategies(COLORS)
|
||||||
if (!f.endsWith('.js') || f.startsWith('_') || f.startsWith('.')) continue
|
const privateOf = (bundle) => collectStrategies(join(ROOT, 'plugin-designs', bundle, 'src', 'colors'))
|
||||||
const src = readFileSync(join(COLORS, f), 'utf8')
|
|
||||||
const hexes = paletteHexes(src)
|
|
||||||
if (hexes && hexes.length) strategies[f.replace(/\.js$/, '')] = hexes
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── the consumers that must agree ─────────────────────────────────────────────────────────────
|
// ── the consumers that must agree ─────────────────────────────────────────────────────────────
|
||||||
const CONSUMERS = [
|
const CONSUMERS = [
|
||||||
|
|
@ -142,7 +164,7 @@ for (const c of CONSUMERS) {
|
||||||
const p = join(BUILT, c.bundle)
|
const p = join(BUILT, c.bundle)
|
||||||
if (!existsSync(p)) { errs.push(`bundle missing: ${c.bundle} — build is incomplete, so this gate cannot judge it.`); continue }
|
if (!existsSync(p)) { errs.push(`bundle missing: ${c.bundle} — build is incomplete, so this gate cannot judge it.`); continue }
|
||||||
const js = readFileSync(p, 'utf8').toLowerCase()
|
const js = readFileSync(p, 'utf8').toLowerCase()
|
||||||
for (const [strat, hexes] of Object.entries(strategies)) {
|
for (const [strat, hexes] of Object.entries({ ...strategies, ...privateOf(c.name) })) {
|
||||||
const missing = hexes.filter((h) => !js.includes(h))
|
const missing = hexes.filter((h) => !js.includes(h))
|
||||||
checkedPairs++
|
checkedPairs++
|
||||||
// A consumer need not use EVERY strategy, but if it uses one at all it must use the whole
|
// A consumer need not use EVERY strategy, but if it uses one at all it must use the whole
|
||||||
|
|
|
||||||
75
internal/check-targets.mjs
Normal file
75
internal/check-targets.mjs
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* check-targets — where each plugin runs, as its <profile>.meta.json "targets" says. Headless.
|
||||||
|
*
|
||||||
|
* Rules (plugin-targets.mjs holds the shared reading):
|
||||||
|
* - targets, when declared, is a non-empty list of known values: web, mobile, desktop, widget.
|
||||||
|
* Absent = web: a meta never has to declare it.
|
||||||
|
* - "widget" in targets and a meta "widget": { "corner" } go together (the Studio mounts a widget by its corner).
|
||||||
|
* - "backend": "backend/<name>" names an existing folder (desktop:build packages it as the app's sidecar).
|
||||||
|
* - A plugin that is not built for the web (targets without "web") is NOT in the built web tree: no
|
||||||
|
* public/<tree>/<bundle>/ folder and no manifest entry, so neither the /cdn export nor the Research
|
||||||
|
* Gallery can carry it. desktop:build packages it instead.
|
||||||
|
* --self-test plants each violation and confirms it is caught.
|
||||||
|
* EXIT 0 pass · 1 fail
|
||||||
|
*/
|
||||||
|
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'
|
||||||
|
import { join, resolve, dirname } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { KNOWN_TARGETS, profileTargets, isWebBuilt } from './plugin-targets.mjs'
|
||||||
|
import { TREE_NAME } from './_tree-name.mjs'
|
||||||
|
|
||||||
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||||
|
|
||||||
|
// One profile's meta against the rules; returns the problems.
|
||||||
|
export function metaProblems(where, meta, backendExists = (p) => existsSync(join(ROOT, p))) {
|
||||||
|
const out = []
|
||||||
|
const t = meta.targets
|
||||||
|
if (t !== undefined) {
|
||||||
|
if (!Array.isArray(t) || !t.length) out.push(`${where}: "targets" must be a non-empty list`)
|
||||||
|
else for (const v of t) if (!KNOWN_TARGETS.includes(v)) out.push(`${where}: unknown target "${v}" (known: ${KNOWN_TARGETS.join(', ')})`)
|
||||||
|
if (Array.isArray(t) && t.includes('widget') && !meta.widget?.corner) out.push(`${where}: targets include "widget" but there is no "widget": { "corner" }`)
|
||||||
|
if (Array.isArray(t) && meta.widget && !t.includes('widget')) out.push(`${where}: has a "widget" corner but its targets leave out "widget"`)
|
||||||
|
}
|
||||||
|
if (meta.backend !== undefined && !(typeof meta.backend === 'string' && backendExists(meta.backend))) out.push(`${where}: "backend" names no folder ${meta.backend}`)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv.includes('--self-test')) {
|
||||||
|
const planted = [
|
||||||
|
['unknown value', { targets: ['tv'] }],
|
||||||
|
['empty list', { targets: [] }],
|
||||||
|
['widget without corner', { targets: ['widget'] }],
|
||||||
|
['corner without widget target', { targets: ['desktop'], widget: { corner: 'bottom-right' } }],
|
||||||
|
['missing backend', { targets: ['desktop'], backend: 'backend/no-such-engine' }],
|
||||||
|
]
|
||||||
|
let missed = 0
|
||||||
|
for (const [name, meta] of planted) if (!metaProblems('planted', meta, () => false).length) { missed++; console.log(` MISSED ${name}`) }
|
||||||
|
if (metaProblems('ok', { targets: ['desktop', 'widget'], widget: { corner: 'bottom-right' }, backend: 'backend/x' }, () => true).length) { missed++; console.log(' FALSE ALARM on a valid meta') }
|
||||||
|
if (missed) { console.log(`TARGETS self-test: FAIL — ${missed}`); process.exit(1) }
|
||||||
|
console.log(`TARGETS self-test: PASS — ${planted.length} planted violations caught, a valid meta passes.`)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const problems = []
|
||||||
|
const DESIGNS = join(ROOT, 'plugin-designs')
|
||||||
|
const TREE = join(ROOT, 'public', TREE_NAME)
|
||||||
|
let manifest = null
|
||||||
|
try { manifest = JSON.parse(readFileSync(join(TREE, 'manifest.json'), 'utf8')) } catch { /* no built tree: tree rules skipped */ }
|
||||||
|
let declared = 0, notWeb = 0
|
||||||
|
for (const b of readdirSync(DESIGNS)) {
|
||||||
|
const dir = join(DESIGNS, b)
|
||||||
|
if (!statSync(dir).isDirectory()) continue
|
||||||
|
for (const p of profileTargets(dir)) {
|
||||||
|
if (p.targets) declared++
|
||||||
|
problems.push(...metaProblems(`${b}/${p.profile}.meta.json`, p.meta))
|
||||||
|
}
|
||||||
|
if (!isWebBuilt(dir)) {
|
||||||
|
notWeb++
|
||||||
|
if (existsSync(join(TREE, b))) problems.push(`${b} is not a web plugin but public/${TREE_NAME}/${b}/ exists (it would ship in the /cdn export)`)
|
||||||
|
if (manifest && (manifest.bundles || []).some((x) => x.bundle === b)) problems.push(`${b} is not a web plugin but the built manifest lists it (the gallery would offer it)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const p of problems) console.log(' FAIL ' + p)
|
||||||
|
if (problems.length) { console.log(`TARGETS: FAIL — ${problems.length} problem(s).`); process.exit(1) }
|
||||||
|
console.log(`TARGETS: PASS — ${declared} profile(s) declare targets (absent = web); ${notWeb} non-web plugin(s), none in the built web tree.`)
|
||||||
84
internal/desktop-build.mjs
Normal file
84
internal/desktop-build.mjs
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* desktop-build — `bun run desktop:build <plugin>`: package ONE desktop plugin as a desktop app (Tauri).
|
||||||
|
*
|
||||||
|
* Only a plugin whose <profile>.meta.json "targets" include "desktop" can be packaged (plugin-targets.mjs).
|
||||||
|
* Nothing here touches public/ or the committed tree:
|
||||||
|
* 1. the plugin's bundle is built into .desktop/<plugin>/ (DOXA_OUT_ROOT), with a one-element page that
|
||||||
|
* tells it where its engine listens;
|
||||||
|
* 2. the plugin's engine, when its meta names one ("backend": "backend/<name>", Go), is built for this
|
||||||
|
* machine as deployment/src-tauri/binaries/<name>-<target triple>, Tauri's sidecar layout;
|
||||||
|
* 3. `tauri build` runs with a --config override that points the window at that page, names the app,
|
||||||
|
* and bundles the sidecar; deployment/src-tauri/tauri.conf.json itself is not edited.
|
||||||
|
* The shell (deployment/src-tauri/src/main.rs) starts the sidecar with RUDDER_AUDIO=browser and
|
||||||
|
* RUDDER_HOME=<app data dir>/rudder, and stops it when the window closes.
|
||||||
|
* See documentation/HOWTO-build-the-desktop-app.md for prerequisites and what is still unproven.
|
||||||
|
*/
|
||||||
|
import { spawnSync } from 'node:child_process'
|
||||||
|
import { existsSync, mkdirSync, rmSync, writeFileSync, readdirSync } from 'node:fs'
|
||||||
|
import { join, resolve, dirname, relative } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { desktopProfile } from './plugin-targets.mjs'
|
||||||
|
|
||||||
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||||
|
const TAURI_DIR = join(ROOT, 'deployment', 'src-tauri')
|
||||||
|
const plugin = process.argv[2]
|
||||||
|
const die = (m) => { console.error('desktop:build: ' + m); process.exit(1) }
|
||||||
|
const run = (cmd, args, opts = {}) => {
|
||||||
|
console.log('→ ' + [cmd, ...args].join(' '))
|
||||||
|
const r = spawnSync(cmd, args, { stdio: 'inherit', cwd: ROOT, ...opts, env: { ...process.env, ...(opts.env || {}) } })
|
||||||
|
if (r.status !== 0) die(`${cmd} failed (exit ${r.status})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!plugin) die('usage: bun run desktop:build <plugin> (a plugin-designs/<plugin>/ whose meta targets include "desktop")')
|
||||||
|
const dir = join(ROOT, 'plugin-designs', plugin)
|
||||||
|
if (!existsSync(dir)) die(`no plugin-designs/${plugin}/`)
|
||||||
|
const dp = desktopProfile(dir)
|
||||||
|
if (!dp) die(`${plugin} does not declare "desktop" in any <profile>.meta.json "targets"; it is not a desktop plugin`)
|
||||||
|
|
||||||
|
// 1. The plugin, built outside public/.
|
||||||
|
const stage = join(ROOT, '.desktop', plugin)
|
||||||
|
rmSync(stage, { recursive: true, force: true })
|
||||||
|
mkdirSync(stage, { recursive: true })
|
||||||
|
run('bun', ['x', 'vite', 'build'], { env: { BUNDLE: plugin, DOXA_OUT_ROOT: stage } })
|
||||||
|
const tag = dp.meta.element || plugin
|
||||||
|
const port = Number(process.env.RUDDER_PORT || 5193)
|
||||||
|
const name = String(dp.meta.label || plugin).split(/\s+[—–-]\s+/)[0].trim() || plugin
|
||||||
|
const cfg = { apiBase: `http://127.0.0.1:${port}/api/rudder` }
|
||||||
|
writeFileSync(join(stage, 'index.html'), `<!doctype html>
|
||||||
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>${name}</title><style>html,body{margin:0;height:100%;background:#0b0f14}</style></head>
|
||||||
|
<body><${tag} profile-config='${JSON.stringify(cfg)}' style="display:block;height:100vh"></${tag}>
|
||||||
|
<script src="${plugin}/${plugin}.js"></script></body></html>
|
||||||
|
`)
|
||||||
|
|
||||||
|
// 2. The engine, when the plugin has one.
|
||||||
|
const externalBin = []
|
||||||
|
const backend = dp.meta.backend
|
||||||
|
if (backend) {
|
||||||
|
const src = join(ROOT, backend)
|
||||||
|
const sidecar = backend.split('/').filter(Boolean).pop()
|
||||||
|
if (!existsSync(src)) die(`meta names backend "${backend}" but that folder does not exist`)
|
||||||
|
const vv = spawnSync('rustc', ['-vV'], { encoding: 'utf8' })
|
||||||
|
const triple = ((vv.stdout || '').match(/^host:\s*(\S+)/m) || [])[1]
|
||||||
|
if (!triple) die('rustc not found: install Rust (https://rustup.rs) to build a desktop app')
|
||||||
|
const exe = process.platform === 'win32' ? '.exe' : ''
|
||||||
|
mkdirSync(join(TAURI_DIR, 'binaries'), { recursive: true })
|
||||||
|
run('go', ['build', '-o', join(TAURI_DIR, 'binaries', `${sidecar}-${triple}${exe}`), '.'], { cwd: src })
|
||||||
|
externalBin.push(`binaries/${sidecar}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. The app.
|
||||||
|
const targets = process.platform === 'darwin' ? ['dmg'] : process.platform === 'win32' ? ['nsis'] : ['deb']
|
||||||
|
const override = {
|
||||||
|
productName: name,
|
||||||
|
identifier: `life.doxa.${plugin.replace(/[^a-z0-9]+/gi, '')}`,
|
||||||
|
build: { frontendDist: relative(TAURI_DIR, stage) },
|
||||||
|
app: { windows: [{ title: name, width: 760, height: 820, minWidth: 360, minHeight: 480, resizable: true, url: 'index.html' }] },
|
||||||
|
bundle: { active: true, targets, ...(externalBin.length ? { externalBin } : {}) },
|
||||||
|
}
|
||||||
|
run(join(ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'tauri.cmd' : 'tauri'), ['build', '--config', JSON.stringify(override)])
|
||||||
|
|
||||||
|
const bundleDir = join(process.env.CARGO_TARGET_DIR ? resolve(process.env.CARGO_TARGET_DIR) : join(TAURI_DIR, 'target'), 'release', 'bundle')
|
||||||
|
const found = existsSync(bundleDir) ? readdirSync(bundleDir).flatMap((d) => readdirSync(join(bundleDir, d)).map((f) => join(bundleDir, d, f))) : []
|
||||||
|
console.log(`\ndesktop:build ${plugin}: done. Installer(s):\n` + (found.length ? found.map((f) => ' ' + f).join('\n') : ` (none found under ${bundleDir})`))
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
* Wired into: bun run build (runs after the per-bundle vite builds)
|
* Wired into: bun run build (runs after the per-bundle vite builds)
|
||||||
*/
|
*/
|
||||||
import { CDN } from '../library/constants/cdn.js'
|
import { CDN } from '../library/constants/cdn.js'
|
||||||
|
import { isWebBuilt } from './plugin-targets.mjs'
|
||||||
import {
|
import {
|
||||||
readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync, statSync,
|
readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync, statSync,
|
||||||
cpSync, rmSync, mkdtempSync,
|
cpSync, rmSync, mkdtempSync,
|
||||||
|
|
@ -1167,6 +1168,9 @@ function scanBundles() {
|
||||||
for (const entry of readdirSync(APP_PROFILES).sort()) {
|
for (const entry of readdirSync(APP_PROFILES).sort()) {
|
||||||
const dir = join(APP_PROFILES, entry)
|
const dir = join(APP_PROFILES, entry)
|
||||||
if (!isBundleDir(entry) || !statSync(dir).isDirectory()) continue
|
if (!isBundleDir(entry) || !statSync(dir).isDirectory()) continue
|
||||||
|
// Not a web plugin (plugin-targets.mjs: targets without "web", e.g. Rudder): no leaf pages, no manifest
|
||||||
|
// entry. desktop:build packages it.
|
||||||
|
if (!isWebBuilt(dir)) continue
|
||||||
|
|
||||||
const vueFiles = readdirSync(dir).filter((f) => f.endsWith('.vue')).sort()
|
const vueFiles = readdirSync(dir).filter((f) => f.endsWith('.vue')).sort()
|
||||||
if (vueFiles.length === 0) continue
|
if (vueFiles.length === 0) continue
|
||||||
|
|
|
||||||
37
internal/plugin-targets.mjs
Normal file
37
internal/plugin-targets.mjs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* plugin-targets.mjs — where a plugin runs, read from its own <profile>.meta.json "targets".
|
||||||
|
*
|
||||||
|
* Known values: web (a published map or tool), mobile, desktop (the Tauri app, `bun run desktop:build`),
|
||||||
|
* widget (the Plugin Studio's corner). ABSENT = web: most plugins never declare it and never need to.
|
||||||
|
* A plugin is built into the WEB tree (bun run build, the /cdn export, the Research Gallery) unless every
|
||||||
|
* profile declares targets and none of them is "web" — Rudder (["desktop","widget"]) is built only by
|
||||||
|
* desktop:build, and the dev server still discovers it from source for the Studio's corner.
|
||||||
|
* Read by run-parallel (--bundles), check-composite-freshness, check-shared-component-reached-bundles,
|
||||||
|
* desktop-build and check:targets, so there is ONE rule.
|
||||||
|
*/
|
||||||
|
import { readdirSync, readFileSync, existsSync } from 'node:fs'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
|
||||||
|
export const KNOWN_TARGETS = ['web', 'mobile', 'desktop', 'widget']
|
||||||
|
|
||||||
|
/** [{ profile, targets|null, meta }] for every <profile>.meta.json in a bundle folder. */
|
||||||
|
export function profileTargets(bundleDir) {
|
||||||
|
if (!existsSync(bundleDir)) return []
|
||||||
|
return readdirSync(bundleDir).filter((f) => f.endsWith('.meta.json')).map((f) => {
|
||||||
|
let meta = {}
|
||||||
|
try { meta = JSON.parse(readFileSync(join(bundleDir, f), 'utf8')) } catch { /* unreadable meta = no declaration */ }
|
||||||
|
return { profile: f.replace(/\.meta\.json$/, ''), targets: Array.isArray(meta.targets) ? meta.targets : null, meta }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True unless every profile declares targets and none of them is "web". No meta at all = web. */
|
||||||
|
export function isWebBuilt(bundleDir) {
|
||||||
|
const p = profileTargets(bundleDir)
|
||||||
|
if (!p.length) return true
|
||||||
|
return p.some((x) => !x.targets || x.targets.includes('web'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The profile a desktop app opens, or null: the first profile whose targets include "desktop". */
|
||||||
|
export function desktopProfile(bundleDir) {
|
||||||
|
return profileTargets(bundleDir).find((x) => x.targets && x.targets.includes('desktop')) || null
|
||||||
|
}
|
||||||
|
|
@ -45,6 +45,7 @@
|
||||||
*/
|
*/
|
||||||
import { spawn, spawnSync } from 'node:child_process'
|
import { spawn, spawnSync } from 'node:child_process'
|
||||||
import { readdirSync, existsSync, readFileSync } from 'node:fs'
|
import { readdirSync, existsSync, readFileSync } from 'node:fs'
|
||||||
|
import { isWebBuilt } from './plugin-targets.mjs'
|
||||||
import { cpus } from 'node:os'
|
import { cpus } from 'node:os'
|
||||||
import { resolve, dirname } from 'node:path'
|
import { resolve, dirname } from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
@ -195,6 +196,8 @@ function buildSteps() {
|
||||||
const dir = resolve(ROOT, 'plugin-designs')
|
const dir = resolve(ROOT, 'plugin-designs')
|
||||||
return readdirSync(dir, { withFileTypes: true })
|
return readdirSync(dir, { withFileTypes: true })
|
||||||
.filter((e) => e.isDirectory())
|
.filter((e) => e.isDirectory())
|
||||||
|
// Not a web plugin (targets without "web", e.g. Rudder: desktop + widget): built by desktop:build only.
|
||||||
|
.filter((e) => isWebBuilt(resolve(dir, e.name)))
|
||||||
.map((e) => e.name)
|
.map((e) => e.name)
|
||||||
.sort()
|
.sort()
|
||||||
.map((name) => ({ name, cmd: 'bun', args: ['x', 'vite', 'build'], env: { BUNDLE: name } }))
|
.map((name) => ({ name, cmd: 'bun', args: ['x', 'vite', 'build'], env: { BUNDLE: name } }))
|
||||||
|
|
|
||||||
|
|
@ -1,300 +0,0 @@
|
||||||
/**
|
|
||||||
* useMapPacket — render a multi-page "prayer packet" PDF for a map selection.
|
|
||||||
*
|
|
||||||
* A packet is one cohesive PDF whose:
|
|
||||||
* - Page 1 = the full-bleed map image (reuses useMapPoster.render(spec,'png')
|
|
||||||
* so the map raster, hidden-map spin-up, and Mapbox attribution path are
|
|
||||||
* identical to the poster export — single source of truth).
|
|
||||||
* - Pages 2..N = one clean "detail card" per people group, drawn with jsPDF
|
|
||||||
* vector primitives (setFontSize / text / rect / line) so the cards stay
|
|
||||||
* crisp at any zoom and add almost nothing to the file size.
|
|
||||||
*
|
|
||||||
* People-group field reads are DEFENSIVE: the same object may arrive as a
|
|
||||||
* normalized record from useMapData (top-level `name`, `country`, `population`,
|
|
||||||
* `peoplePraying`, …), as a backward-compat `peopleGroups[]` entry, or as a
|
|
||||||
* GeoJSON-feature `.properties` bag. We probe the known keys in priority order
|
|
||||||
* and fall back to an em-dash so a card never renders blank labels.
|
|
||||||
*
|
|
||||||
* Mapbox / OpenStreetMap attribution is rendered into every page footer —
|
|
||||||
* non-negotiable (Mapbox ToS), mirroring useMapPoster's footer slot.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const packet = useMapPacket(getMap);
|
|
||||||
* const blob = await packet.renderPacket(spec, peopleGroups, { source: 'Doxa Life' });
|
|
||||||
* download(blob, 'prayer-packet.pdf');
|
|
||||||
*
|
|
||||||
* // Deps (already in package.json): "jspdf": "^4.2.1"
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ref, readonly } from 'vue';
|
|
||||||
import { useMapPoster } from './poster/useMapPoster.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {() => any} getMap function returning the live Mapbox map
|
|
||||||
* @returns {{
|
|
||||||
* renderPacket: (spec: import('./poster/useMapPoster.js').PosterSpec, peopleGroups: Array<Object>, meta?: Object) => Promise<Blob>,
|
|
||||||
* progress: import('vue').Ref<{ phase: string, completed: number, total: number, message: string }>,
|
|
||||||
* isRendering: import('vue').Ref<boolean>
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
export function useMapPacket(getMap) {
|
|
||||||
const progress = ref({ phase: 'idle', completed: 0, total: 0, message: '' });
|
|
||||||
const isRendering = ref(false);
|
|
||||||
|
|
||||||
function _setProgress(phase, completed, total, message) {
|
|
||||||
progress.value = { phase, completed, total, message };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the multi-page packet PDF.
|
|
||||||
* @param {import('./poster/useMapPoster.js').PosterSpec} spec same shape useMapPoster uses
|
|
||||||
* @param {Array<Object>} peopleGroups normalized people-group objects
|
|
||||||
* @param {Object} [meta] { source?, date?, attribution? }
|
|
||||||
* @returns {Promise<Blob>}
|
|
||||||
*/
|
|
||||||
async function renderPacket(spec, peopleGroups, meta = {}) {
|
|
||||||
if (isRendering.value) throw new Error('useMapPacket: already rendering');
|
|
||||||
|
|
||||||
isRendering.value = true;
|
|
||||||
const groups = Array.isArray(peopleGroups) ? peopleGroups : [];
|
|
||||||
const total = 1 + groups.length; // page 1 = map, then one page per group
|
|
||||||
|
|
||||||
try {
|
|
||||||
_setProgress('map', 0, total, 'rendering map page');
|
|
||||||
|
|
||||||
// Page 1 raster — reuse the poster renderer so the map image, attribution,
|
|
||||||
// and hidden-map machinery are identical to the poster export path.
|
|
||||||
const mapBlob = await useMapPoster(getMap).render(spec, 'png');
|
|
||||||
const mapDataUrl = await _blobToDataUrl(mapBlob);
|
|
||||||
|
|
||||||
_setProgress('pdf-init', 1, total, 'building PDF');
|
|
||||||
|
|
||||||
// Lazy ESM import — keep jspdf out of the main bundle until a packet is
|
|
||||||
// actually requested. Mirrors useMapPoster._toPdfBlob exactly.
|
|
||||||
let jsPDFCtor;
|
|
||||||
try {
|
|
||||||
const mod = await import('jspdf');
|
|
||||||
jsPDFCtor = mod.jsPDF || mod.default;
|
|
||||||
} catch (_) {
|
|
||||||
throw new Error('useMapPacket: jspdf not installed (npm i jspdf)');
|
|
||||||
}
|
|
||||||
|
|
||||||
const doc = new jsPDFCtor({
|
|
||||||
orientation: spec.widthIn > spec.heightIn ? 'l' : 'p',
|
|
||||||
unit: 'in',
|
|
||||||
format: [spec.widthIn, spec.heightIn],
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageW = spec.widthIn;
|
|
||||||
const pageH = spec.heightIn;
|
|
||||||
|
|
||||||
const footer = _footerText(meta);
|
|
||||||
|
|
||||||
// ─── Page 1: full-page map image ──────────────────────────────────────
|
|
||||||
doc.addImage(mapDataUrl, 'PNG', 0, 0, pageW, pageH);
|
|
||||||
_drawFooter(doc, pageW, pageH, footer);
|
|
||||||
|
|
||||||
// ─── Pages 2..N: one detail card per people group ────────────────────
|
|
||||||
for (let i = 0; i < groups.length; i++) {
|
|
||||||
const pg = groups[i] || {};
|
|
||||||
doc.addPage();
|
|
||||||
_drawDetailCard(doc, pageW, pageH, pg, footer);
|
|
||||||
_setProgress('cards', 2 + i, total, `card ${i + 1}/${groups.length}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
_setProgress('done', total, total, 'packet complete');
|
|
||||||
return doc.output('blob');
|
|
||||||
} finally {
|
|
||||||
isRendering.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
renderPacket,
|
|
||||||
progress: readonly(progress),
|
|
||||||
isRendering: readonly(isRendering),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────
|
|
||||||
// Detail-card drawing (module-private)
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a single people-group detail card onto the current PDF page.
|
|
||||||
* Uses only jsPDF vector primitives so output is crisp and tiny.
|
|
||||||
*/
|
|
||||||
function _drawDetailCard(doc, pageW, pageH, pg, footer) {
|
|
||||||
const margin = Math.min(pageW, pageH) * 0.08;
|
|
||||||
const x = margin;
|
|
||||||
let y = margin;
|
|
||||||
|
|
||||||
// Heading: people-group name
|
|
||||||
const name = _pgName(pg);
|
|
||||||
doc.setFont('helvetica', 'bold');
|
|
||||||
doc.setFontSize(28);
|
|
||||||
doc.setTextColor(20, 20, 20);
|
|
||||||
doc.text(name, x, y + 0.32);
|
|
||||||
y += 0.7;
|
|
||||||
|
|
||||||
// Underline rule beneath the heading
|
|
||||||
doc.setDrawColor(146, 177, 149); // #92b195 — the Doxa accent green
|
|
||||||
doc.setLineWidth(0.02);
|
|
||||||
doc.line(x, y, pageW - margin, y);
|
|
||||||
y += 0.45;
|
|
||||||
|
|
||||||
// Field rows
|
|
||||||
const rows = [
|
|
||||||
['Country', _pgCountry(pg)],
|
|
||||||
['Population', _pgPopulation(pg)],
|
|
||||||
['% Evangelical', _pgPercent(pg, 'evangelical')],
|
|
||||||
['% Christian', _pgPercent(pg, 'christian')],
|
|
||||||
['Prayer count', _pgPrayerCount(pg)],
|
|
||||||
];
|
|
||||||
|
|
||||||
const labelX = x;
|
|
||||||
const valueX = x + 2.4;
|
|
||||||
const rowH = 0.5;
|
|
||||||
|
|
||||||
doc.setFontSize(14);
|
|
||||||
for (const [label, value] of rows) {
|
|
||||||
doc.setFont('helvetica', 'normal');
|
|
||||||
doc.setTextColor(110, 110, 110);
|
|
||||||
doc.text(String(label), labelX, y);
|
|
||||||
|
|
||||||
doc.setFont('helvetica', 'bold');
|
|
||||||
doc.setTextColor(40, 40, 40);
|
|
||||||
doc.text(String(value), valueX, y);
|
|
||||||
|
|
||||||
// hairline separator under each row
|
|
||||||
doc.setDrawColor(225, 225, 225);
|
|
||||||
doc.setLineWidth(0.008);
|
|
||||||
doc.line(labelX, y + 0.14, pageW - margin, y + 0.14);
|
|
||||||
|
|
||||||
y += rowH;
|
|
||||||
}
|
|
||||||
|
|
||||||
_drawFooter(doc, pageW, pageH, footer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Footer line drawn on every page: source • date • Mapbox/OSM attribution.
|
|
||||||
*/
|
|
||||||
function _drawFooter(doc, pageW, pageH, footer) {
|
|
||||||
const margin = Math.min(pageW, pageH) * 0.04;
|
|
||||||
doc.setFont('helvetica', 'normal');
|
|
||||||
doc.setFontSize(8);
|
|
||||||
doc.setTextColor(120, 120, 120);
|
|
||||||
doc.text(footer, margin, pageH - margin, { baseline: 'bottom' });
|
|
||||||
}
|
|
||||||
|
|
||||||
function _footerText(meta = {}) {
|
|
||||||
const date = meta.date || new Date().toISOString().slice(0, 10);
|
|
||||||
const attribution = meta.attribution || '© Mapbox © OpenStreetMap';
|
|
||||||
const parts = [];
|
|
||||||
if (meta.source) parts.push(String(meta.source));
|
|
||||||
parts.push(String(date));
|
|
||||||
parts.push(attribution);
|
|
||||||
return parts.join(' • ');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────
|
|
||||||
// Defensive people-group field reads
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────
|
|
||||||
// People-group objects reach us in three shapes (see useMapData.js +
|
|
||||||
// PeopleGroupDetail.vue): the normalized record (top-level keys), the
|
|
||||||
// backward-compat peopleGroups[] entry, and a GeoJSON feature `.properties`
|
|
||||||
// bag. `_props` flattens those so a single probe order works for all three.
|
|
||||||
|
|
||||||
function _props(pg) {
|
|
||||||
// Prefer an explicit GeoJSON `.properties`, fall back to the object itself.
|
|
||||||
return (pg && pg.properties) ? pg.properties : (pg || {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function _firstDefined(...vals) {
|
|
||||||
for (const v of vals) {
|
|
||||||
if (v !== undefined && v !== null && v !== '') return v;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _pgName(pg) {
|
|
||||||
const p = _props(pg);
|
|
||||||
return _firstDefined(
|
|
||||||
p.name,
|
|
||||||
p.peopleName,
|
|
||||||
p.PeopNameInCountry,
|
|
||||||
p._raw?.PeopNameInCountry,
|
|
||||||
p._raw?.peo_name,
|
|
||||||
pg.name,
|
|
||||||
) ?? '—';
|
|
||||||
}
|
|
||||||
|
|
||||||
function _pgCountry(pg) {
|
|
||||||
const p = _props(pg);
|
|
||||||
return _firstDefined(
|
|
||||||
p.countryName,
|
|
||||||
p.countryIsoLabel,
|
|
||||||
p.country,
|
|
||||||
p.countryIso,
|
|
||||||
p._raw?.Country,
|
|
||||||
pg.country,
|
|
||||||
) ?? '—';
|
|
||||||
}
|
|
||||||
|
|
||||||
function _pgPopulation(pg) {
|
|
||||||
const p = _props(pg);
|
|
||||||
const raw = _firstDefined(
|
|
||||||
p.population,
|
|
||||||
p._raw?.Population,
|
|
||||||
pg.population,
|
|
||||||
);
|
|
||||||
const n = Number(String(raw ?? '').replace(/,/g, ''));
|
|
||||||
if (raw === undefined || Number.isNaN(n)) return '—';
|
|
||||||
return n.toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Percent fields. The pray-tools / UUPG records expose evangelical &
|
|
||||||
* Christian percentages under several historical keys; probe them all and
|
|
||||||
* format as a percentage with one decimal.
|
|
||||||
*/
|
|
||||||
function _pgPercent(pg, kind) {
|
|
||||||
const p = _props(pg);
|
|
||||||
const candidates = kind === 'evangelical'
|
|
||||||
? [p.percentEvangelical, p.pctEvangelical, p.evangelical, p._raw?.PercentEvangelical, p._raw?.percent_evangelical]
|
|
||||||
: [p.percentChristian, p.pctChristian, p.percentAdherents, p.christian, p._raw?.PercentChristianPC, p._raw?.percent_christian, p._raw?.PercentAdherents];
|
|
||||||
const raw = _firstDefined(...candidates);
|
|
||||||
if (raw === undefined) return '—';
|
|
||||||
const n = Number(raw);
|
|
||||||
if (Number.isNaN(n)) return '—';
|
|
||||||
// Some sources store 0..1 fractions, others 0..100; normalize to percent.
|
|
||||||
const pct = n > 0 && n <= 1 ? n * 100 : n;
|
|
||||||
return `${pct.toFixed(1)}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _pgPrayerCount(pg) {
|
|
||||||
const p = _props(pg);
|
|
||||||
const raw = _firstDefined(
|
|
||||||
p.peoplePraying,
|
|
||||||
p._raw?.people_praying,
|
|
||||||
pg.peoplePraying,
|
|
||||||
);
|
|
||||||
if (raw === undefined) return '—';
|
|
||||||
const n = Number(raw);
|
|
||||||
if (Number.isNaN(n)) return '—';
|
|
||||||
return n.toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────
|
|
||||||
// Blob → dataURL
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function _blobToDataUrl(blob) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => resolve(reader.result);
|
|
||||||
reader.onerror = () => reject(new Error('useMapPacket: failed to read map blob'));
|
|
||||||
reader.readAsDataURL(blob);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -80,10 +80,8 @@
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||||
import { bundleTreeRoot } from '@map/utils/embedBase.js'
|
import { bundleTreeRoot } from '@map/utils/embedBase.js'
|
||||||
|
import { loadScriptOnce } from '@map/utils/embedScript.js'
|
||||||
|
|
||||||
// Inject a bundle script exactly once per URL (deduped across all layout instances). Same helper the
|
|
||||||
// tabs layout + the SPA use — resolves after the IIFE's customElements.define has run.
|
|
||||||
const _stlLoaded = {}
|
|
||||||
// ★ ANCHOR A SIBLING-BUNDLE src TO THE TREE ROOT, NOT TO A GUESSED DEPTH.
|
// ★ ANCHOR A SIBLING-BUNDLE src TO THE TREE ROOT, NOT TO A GUESSED DEPTH.
|
||||||
// THE BUG (the agent-reported 2026-09-23, verbatim console text):
|
// THE BUG (the agent-reported 2026-09-23, verbatim console text):
|
||||||
// Could not load <doxa-research-map> from ../../doxa-research-map/doxa-research-map.js
|
// Could not load <doxa-research-map> from ../../doxa-research-map/doxa-research-map.js
|
||||||
|
|
@ -134,30 +132,11 @@ function resolveBundleSrc(url) {
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject a bundle script exactly once per URL, through the ONE shared loader (library/utils/embedScript.js:
|
||||||
|
// the dedupe cache, the dev-server ES-module rule, the empty-url no-op). Only the src resolution is this
|
||||||
|
// layout's own: a sibling-bundle src is anchored to the tree root first (resolveBundleSrc above).
|
||||||
function injectScriptOnce(rawUrl) {
|
function injectScriptOnce(rawUrl) {
|
||||||
const url = resolveBundleSrc(rawUrl)
|
return loadScriptOnce(resolveBundleSrc(rawUrl))
|
||||||
if (!url) return Promise.resolve()
|
|
||||||
if (_stlLoaded[url]) return _stlLoaded[url]
|
|
||||||
_stlLoaded[url] = new Promise((ok, fail) => {
|
|
||||||
const s = document.createElement('script')
|
|
||||||
s.src = url
|
|
||||||
// ★ A DEV ENTRY IS AN ES MODULE, AND A CLASSIC <script> LIES ABOUT LOADING IT.
|
|
||||||
// Pointing a classic script at /plugin-designs/<bundle>/index.js fires onload SUCCESSFULLY and
|
|
||||||
// only THEN throws "Cannot use import statement outside a module", asynchronously. So this
|
|
||||||
// promise resolved, waitForDefine() below timed out, the tag never registered, and the section
|
|
||||||
// rendered empty — with NO failed request and NO console error at the injection site. onload is
|
|
||||||
// structurally incapable of detecting this.
|
|
||||||
// MEASURED on stack-demo's dev leaf 2026-09-22: 0 failed requests, and yet
|
|
||||||
// customElements.get('research-dashboard-map') === false with 0 rows rendered.
|
|
||||||
// Same rule as plugin-studio.vue:255, which hit this first. It belongs HERE because StackLayout
|
|
||||||
// is the SHARED layout — dashboard-plus-grid and research-map-plus-grid inject through this same
|
|
||||||
// function, so one fix covers all three consumers instead of becoming a fourth private copy.
|
|
||||||
if (/^\/plugin-designs\//.test(url) || /\/index\.js(\?|$)/.test(url)) s.type = 'module'
|
|
||||||
s.onload = () => ok()
|
|
||||||
s.onerror = () => { delete _stlLoaded[url]; fail(new Error('failed to load ' + url)) }
|
|
||||||
document.head.appendChild(s)
|
|
||||||
})
|
|
||||||
return _stlLoaded[url]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function waitForDefine(tag, timeoutMs = 5000) {
|
function waitForDefine(tag, timeoutMs = 5000) {
|
||||||
|
|
|
||||||
|
|
@ -246,22 +246,12 @@ import { tStatic } from '../i18n/index.js'
|
||||||
const tr = (key, en) => tStatic(key) || en
|
const tr = (key, en) => tStatic(key) || en
|
||||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||||
import { icon as localIcon } from '@map/layouts/app-wrapper-material/icons.js'
|
import { icon as localIcon } from '@map/layouts/app-wrapper-material/icons.js'
|
||||||
|
import { loadScriptOnce } from '@map/utils/embedScript.js'
|
||||||
|
|
||||||
// Inject a bundle script exactly once per URL (deduped across all layout instances). Resolves after
|
// Inject a bundle script exactly once per URL, through the ONE shared loader (library/utils/embedScript.js).
|
||||||
// the IIFE has executed (so its customElements.define has run). Mirrors the SPA/studio's once().
|
// This layout's private copy lacked the dev-server ES-module rule, so a bundle nested through it under
|
||||||
const _vtlLoaded = {}
|
// bun run dev loaded as a classic script, reported success, and never defined its tag.
|
||||||
function injectScriptOnce(url) {
|
const injectScriptOnce = (url) => loadScriptOnce(url)
|
||||||
if (!url) return Promise.resolve()
|
|
||||||
if (_vtlLoaded[url]) return _vtlLoaded[url]
|
|
||||||
_vtlLoaded[url] = new Promise((ok, fail) => {
|
|
||||||
const s = document.createElement('script')
|
|
||||||
s.src = url
|
|
||||||
s.onload = () => ok()
|
|
||||||
s.onerror = () => { delete _vtlLoaded[url]; fail(new Error('failed to load ' + url)) }
|
|
||||||
document.head.appendChild(s)
|
|
||||||
})
|
|
||||||
return _vtlLoaded[url]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait until a custom element tag is defined (up to ~5s). If a src bundle was injected, its
|
// Wait until a custom element tag is defined (up to ~5s). If a src bundle was injected, its
|
||||||
// define() has already run by onload; this covers elements registered async after script exec.
|
// define() has already run by onload; this covers elements registered async after script exec.
|
||||||
|
|
|
||||||
89
library/utils/embedScript.js
Normal file
89
library/utils/embedScript.js
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
/**
|
||||||
|
* embedScript — load another bundle's script and wait for its element to register.
|
||||||
|
*
|
||||||
|
* ★ WHAT AN APP SHELL DOES. A capability-5 plugin nests other plugins, so it needs three things:
|
||||||
|
* work out the tree root it was served from, load a bundle's `.js` exactly once, and wait for the
|
||||||
|
* custom element to actually exist before mounting it. Both shells in this repo — the Research
|
||||||
|
* Gallery (`doxa-maps-spa`) and the Plugin Studio (`plugin-studio`) — had their own copy of all
|
||||||
|
* three, identical in code (DEDUP-AUDIT.md, 2026-09-28: 33 lines, the only cross-plugin cluster
|
||||||
|
* that is identical, closes over nothing, and spans one owner's plugins).
|
||||||
|
*
|
||||||
|
* Moved verbatim; the comments below are the originals, because each records a measurement.
|
||||||
|
*
|
||||||
|
* ★ ONE BEHAVIOUR CHANGE, AND IT IS AN IMPROVEMENT: `_loaded` is now shared. Two shells on one page
|
||||||
|
* previously kept separate caches and would each load the same bundle. Now the second one reuses
|
||||||
|
* the first's promise. Same result, one network request instead of two.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a bundle exactly once per URL; resolves after its script has executed.
|
||||||
|
*
|
||||||
|
* ★ A CLASSIC SCRIPT FIRES onload AND THEN FAILS, for a dev-server bundle. Measured 2026-09-22:
|
||||||
|
* pointing a plain <script> at a dev entry (/plugin-designs/<bundle>/index.js, a real ES MODULE)
|
||||||
|
* fires onload — so a loader watching onload reports SUCCESS — and only THEN throws "Cannot use
|
||||||
|
* import statement outside a module" as an ASYNC page error. The tag is never defined,
|
||||||
|
* document.createElement(tag) yields an inert HTMLUnknownElement, and the pane sits empty while its
|
||||||
|
* own state reads 'ready'. onload is structurally incapable of seeing this, so it is not a usable
|
||||||
|
* success signal by itself — which is why every mount below also waits on whenTagDefined().
|
||||||
|
*
|
||||||
|
* The built tree ships IIFEs, where a classic tag is correct, so this only ever bites under the dev
|
||||||
|
* server — exactly the surface a developer works on. Rule: a rooted /plugin-designs/ entry (or any
|
||||||
|
* .../index.js) is a dev ES module; everything else is a built IIFE.
|
||||||
|
* MEASURED a second time, independently, on stack-demo's dev leaf 2026-09-22 (StackLayout, before it
|
||||||
|
* used this file): 0 failed requests, and yet customElements.get('research-dashboard-map') === false
|
||||||
|
* with 0 rows rendered. VerticalTabsLayout's private copy LACKED this rule until it moved here, so a
|
||||||
|
* bundle nested through it on the dev server never defined its tag.
|
||||||
|
*
|
||||||
|
* An empty url is a no-op: a <script src=""> would re-fetch the page itself.
|
||||||
|
*
|
||||||
|
* SRI is optional (attrs.integrity).
|
||||||
|
*/
|
||||||
|
const _loaded = {}
|
||||||
|
export function loadScriptOnce(url, attrs) {
|
||||||
|
if (!url) return Promise.resolve()
|
||||||
|
if (_loaded[url]) return _loaded[url]
|
||||||
|
_loaded[url] = new Promise((ok, fail) => {
|
||||||
|
const s = document.createElement('script')
|
||||||
|
s.src = url
|
||||||
|
if (/^\/plugin-designs\//.test(url) || /\/index\.js(\?|$)/.test(url)) s.type = 'module'
|
||||||
|
if (attrs) for (const k in attrs) if (attrs[k]) s.setAttribute(k, attrs[k])
|
||||||
|
s.onload = () => ok()
|
||||||
|
s.onerror = () => { delete _loaded[url]; fail(new Error('failed to load ' + url)) }
|
||||||
|
document.head.appendChild(s)
|
||||||
|
})
|
||||||
|
return _loaded[url]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a tag to ACTUALLY register. loadScriptOnce resolving is not proof the element exists (see
|
||||||
|
// above), and a module script's define() runs only after its import graph settles, so the tag can
|
||||||
|
// appear a tick or two after onload. customElements.whenDefined is the browser's own authoritative
|
||||||
|
// answer; the timeout turns a never-registered tag into THIS tab's error overlay rather than a silent
|
||||||
|
// blank pane that claims to be ready.
|
||||||
|
export function whenTagDefined(tag, ms) {
|
||||||
|
if (typeof customElements === 'undefined') return Promise.resolve()
|
||||||
|
if (customElements.get(tag)) return Promise.resolve()
|
||||||
|
return Promise.race([
|
||||||
|
customElements.whenDefined(tag),
|
||||||
|
new Promise((_, fail) => setTimeout(() => {
|
||||||
|
if (customElements.get(tag)) return
|
||||||
|
fail(new Error('<' + tag + '> never registered (bundle loaded but defined no element)'))
|
||||||
|
}, ms || 15000))
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tree root this shell was served from, with a trailing slash, or '' when it cannot be known.
|
||||||
|
*
|
||||||
|
* ⚠ READS document.currentScript, so it is only valid AT MODULE EVALUATION TIME. Call it once at the
|
||||||
|
* top of an entry file (`const TREE_BASE = deriveTreeBase()`), never later — `currentScript` is null
|
||||||
|
* once the script has finished executing, and a late call silently returns ''.
|
||||||
|
*/
|
||||||
|
export function deriveTreeBase() {
|
||||||
|
try {
|
||||||
|
const cur = (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) || ''
|
||||||
|
if (!cur) return ''
|
||||||
|
// Strip the trailing "<bundle>/<bundle>.js" → the tree root, with trailing slash.
|
||||||
|
const root = cur.replace(/[^/]+\/[^/]+$/, '')
|
||||||
|
return root.replace(/\/+$/, '') + '/'
|
||||||
|
} catch { return '' }
|
||||||
|
}
|
||||||
|
|
@ -234,6 +234,8 @@
|
||||||
"check:tab-radius:self-test": "node internal/check-tab-radius-token.mjs --self-test",
|
"check:tab-radius:self-test": "node internal/check-tab-radius-token.mjs --self-test",
|
||||||
"check:text-highlight": "node plugin-designs/regions-dashboard/scripts/check-text-highlight.mjs",
|
"check:text-highlight": "node plugin-designs/regions-dashboard/scripts/check-text-highlight.mjs",
|
||||||
"check:tabs-localized": "node internal/check-tabs-localized.mjs",
|
"check:tabs-localized": "node internal/check-tabs-localized.mjs",
|
||||||
|
"check:targets": "node internal/check-targets.mjs",
|
||||||
|
"check:targets:self-test": "node internal/check-targets.mjs --self-test",
|
||||||
"check:tauri": "node internal/check-tauri-config.mjs",
|
"check:tauri": "node internal/check-tauri-config.mjs",
|
||||||
"check:template-literal": "node internal/template-literal-check.mjs",
|
"check:template-literal": "node internal/template-literal-check.mjs",
|
||||||
"check:tree-weight": "node internal/check-published-tree-weight.mjs",
|
"check:tree-weight": "node internal/check-published-tree-weight.mjs",
|
||||||
|
|
@ -247,11 +249,12 @@
|
||||||
"check:zoning-declared:self-test": "node internal/check-zoning-declared.mjs --self-test",
|
"check:zoning-declared:self-test": "node internal/check-zoning-declared.mjs --self-test",
|
||||||
"clean": "node internal/clean.mjs",
|
"clean": "node internal/clean.mjs",
|
||||||
"deploy:dev": "bun run build && node internal/sync-dropin.mjs --from-config --prune",
|
"deploy:dev": "bun run build && node internal/sync-dropin.mjs --from-config --prune",
|
||||||
|
"desktop:build": "node internal/desktop-build.mjs",
|
||||||
"dev": "bun x vite",
|
"dev": "bun x vite",
|
||||||
"fix:sri": "node internal/fix-sri.mjs",
|
"fix:sri": "node internal/fix-sri.mjs",
|
||||||
"gallery": "node internal/generate-gallery.mjs",
|
"gallery": "node internal/generate-gallery.mjs",
|
||||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge",
|
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge",
|
||||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print",
|
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test",
|
||||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||||
"library-index": "node internal/generate-library-index.mjs",
|
"library-index": "node internal/generate-library-index.mjs",
|
||||||
|
|
@ -442,6 +445,8 @@
|
||||||
"check:stale-sweep-claim": "a caveat that has stopped being true is a wrong number. The gospel surfaces carry a paragraph saying which pools were searched; while the GRN sweep ran they truthfully said part of it was unread, the sweep finished, and the sentence stayed for four more commits. The DATA decides here: undecided gap codes mean the claim is allowed, all-decided means it must go, so it re-arms itself for the next slow pool. Does NOT flag a claim rendered under a v-if on the pending count — that form expires on its own. Self-test: `--poison`. Static, sub-second.",
|
"check:stale-sweep-claim": "a caveat that has stopped being true is a wrong number. The gospel surfaces carry a paragraph saying which pools were searched; while the GRN sweep ran they truthfully said part of it was unread, the sweep finished, and the sentence stayed for four more commits. The DATA decides here: undecided gap codes mean the claim is allowed, all-decided means it must go, so it re-arms itself for the next slow pool. Does NOT flag a claim rendered under a v-if on the pending count — that form expires on its own. Self-test: `--poison`. Static, sub-second.",
|
||||||
"check:surfaces-read-the-tier": "a surface may READ the tier the pin index computed, never re-derive 'this group has nothing' from the raw resource fields. That hand-rolled field list went stale every time a source was added: 7 codes drawn as having nothing while the map showed them a link, then 28 when Scripture Earth landed. Static, sub-second. Self-test: `bun run check:surfaces-read-the-tier:poison` re-injects the old formula into every surface and each must be caught.",
|
"check:surfaces-read-the-tier": "a surface may READ the tier the pin index computed, never re-derive 'this group has nothing' from the raw resource fields. That hand-rolled field list went stale every time a source was added: 7 codes drawn as having nothing while the map showed them a link, then 28 when Scripture Earth landed. Static, sub-second. Self-test: `bun run check:surfaces-read-the-tier:poison` re-injects the old formula into every surface and each must be caught.",
|
||||||
"check:tabs-localized": "a chosen locale must CHANGE the rendered tab text. check:i18n-parity compares catalogs to each OTHER, so it cannot tell a shipped translation from a dead one; tabs.json is reached only via a DYNAMIC key (SemanticTreeLegend.vue:40), invisible to any grep. Renders it and reads the result.",
|
"check:tabs-localized": "a chosen locale must CHANGE the rendered tab text. check:i18n-parity compares catalogs to each OTHER, so it cannot tell a shipped translation from a dead one; tabs.json is reached only via a DYNAMIC key (SemanticTreeLegend.vue:40), invisible to any grep. Renders it and reads the result.",
|
||||||
|
"check:targets": "where each plugin runs: a <profile>.meta.json \"targets\" is a list of known values (web, mobile, desktop, widget; absent = web), a widget target has a widget corner, a backend key names an existing folder, and a plugin that is not for the web (Rudder) is in neither the built tree nor its manifest, so the /cdn export and the gallery cannot carry it.",
|
||||||
|
"check:targets:self-test": "plants each check:targets violation and confirms it is caught, and that a valid meta passes.",
|
||||||
"check:text-highlight": "useTextHighlight (library/composables) matches the inline search-highlight construction it replaced in regions-dashboard UupgsList and people-groups-grid across six range shapes, and pins the two deliberate escaping deltas (backtick escaped; null returns '' instead of throwing) with controls proving each is real.",
|
"check:text-highlight": "useTextHighlight (library/composables) matches the inline search-highlight construction it replaced in regions-dashboard UupgsList and people-groups-grid across six range shapes, and pins the two deliberate escaping deltas (backtick escaped; null returns '' instead of throwing) with controls proving each is real.",
|
||||||
"check:tree-weight": "the tree we publish has a weight and nothing was watching it. A publish was ALREADY BLOCKED once at a 74MB asset tree (Forgejo rejected the pack); it has since grown to 132MB unnoticed. Reports the SPLIT, because a bare total invites deleting the wrong half: the 176 city posters are the heavy half AND the half that cannot be regenerated (they need OSM tiles). Raw bytes, not packed — see publish-html.mjs for why a raw saving is not a shipped one.",
|
"check:tree-weight": "the tree we publish has a weight and nothing was watching it. A publish was ALREADY BLOCKED once at a 74MB asset tree (Forgejo rejected the pack); it has since grown to 132MB unnoticed. Reports the SPLIT, because a bare total invites deleting the wrong half: the 176 city posters are the heavy half AND the half that cannot be regenerated (they need OSM tiles). Raw bytes, not packed — see publish-html.mjs for why a raw saving is not a shipped one.",
|
||||||
"check:upg-map-data": "the UPG-100 map must carry OUR polygons/zones/pins, not just a basemap — reads the counter the profile itself writes. Pass live URLs to audit what is published.",
|
"check:upg-map-data": "the UPG-100 map must carry OUR polygons/zones/pins, not just a basemap — reads the counter the profile itself writes. Pass live URLs to audit what is published.",
|
||||||
|
|
@ -450,6 +455,7 @@
|
||||||
"check:zone-census": "every zone a USER CAN SELECT must have a poster (asks the LEGEND, not clusters.json — two collapsed deaf lists are unreachable and not a gap). CONDITIONAL: SKIPs without out/posters/, which is gitignored — run `bun run posters` first. It cannot be fixtured like check:packets-all, because it asserts COVERAGE of real generated output and a fixture would only assert the fixture is complete.",
|
"check:zone-census": "every zone a USER CAN SELECT must have a poster (asks the LEGEND, not clusters.json — two collapsed deaf lists are unreachable and not a gap). CONDITIONAL: SKIPs without out/posters/, which is gitignored — run `bun run posters` first. It cannot be fixtured like check:packets-all, because it asserts COVERAGE of real generated output and a fixture would only assert the fixture is complete.",
|
||||||
"check:zoning-declared": "A project list's ZONING matches what its author DECLARED (data/zoning-declarations.json). Zoning is human judgement — geography first, balanced with language — so a gate cannot decide whether a zoning is GOOD; it can decide whether it still matches what was stated, which is what makes UPG-100 reusable as a PATTERN for any project list (7000/4700/3000/2000/700) zoned by an agent who did not write this code. ★ EVERY OBVIOUS RULE FAILED THE REFERENCE IMPLEMENTATION when measured on the live 28 zones / 2107 records: 'every group in exactly one zone' is wrong (112 double-claims, 110 of them the design-approved deaf bloc double-count, legend-tree.js:151) and 'no group unzoned' is wrong (11 JPN/CHN/MNG groups, plausibly deliberate). So it asserts A1 no zone resolves to zero groups, A2 at most one GEOGRAPHIC zone per group with bloc zones declared as a second partition, A3 the orphan set EQUALS the declared set (a RESOLVED orphan fails too — a stale declaration stops being evidence), A4 every selector type is one selector-evaluator supports, A5 no zone over the declared page budget. A1 and A4 would both have caught this cycle's real defects (deaf + 5 wagf zones resolving to nothing) before a browser did, and A2 caught a third overlap my own hand analysis had missed. Exit 75 without records or declarations. Run :self-test to prove the five still fire.",
|
"check:zoning-declared": "A project list's ZONING matches what its author DECLARED (data/zoning-declarations.json). Zoning is human judgement — geography first, balanced with language — so a gate cannot decide whether a zoning is GOOD; it can decide whether it still matches what was stated, which is what makes UPG-100 reusable as a PATTERN for any project list (7000/4700/3000/2000/700) zoned by an agent who did not write this code. ★ EVERY OBVIOUS RULE FAILED THE REFERENCE IMPLEMENTATION when measured on the live 28 zones / 2107 records: 'every group in exactly one zone' is wrong (112 double-claims, 110 of them the design-approved deaf bloc double-count, legend-tree.js:151) and 'no group unzoned' is wrong (11 JPN/CHN/MNG groups, plausibly deliberate). So it asserts A1 no zone resolves to zero groups, A2 at most one GEOGRAPHIC zone per group with bloc zones declared as a second partition, A3 the orphan set EQUALS the declared set (a RESOLVED orphan fails too — a stale declaration stops being evidence), A4 every selector type is one selector-evaluator supports, A5 no zone over the declared page budget. A1 and A4 would both have caught this cycle's real defects (deaf + 5 wagf zones resolving to nothing) before a browser did, and A2 caught a third overlap my own hand analysis had missed. Exit 75 without records or declarations. Run :self-test to prove the five still fire.",
|
||||||
"deploy:dev": "destination + allowlist come from vite.doxa-maps-build-config.json (dropin.dest / dropin.include); DOXA_DROPIN_DEST=<dir> overrides the destination",
|
"deploy:dev": "destination + allowlist come from vite.doxa-maps-build-config.json (dropin.dest / dropin.include); DOXA_DROPIN_DEST=<dir> overrides the destination",
|
||||||
|
"desktop:build": "bun run desktop:build <plugin>: packages ONE plugin whose meta targets include desktop as a desktop app (Tauri): builds it into .desktop/<plugin>/, builds its sidecar engine for this machine, and runs tauri build with an override pointing the window at it. See documentation/HOWTO-build-the-desktop-app.md.",
|
||||||
"gallery": "writes public/__gallery/index.html — the ARCHIVED bundle-index gallery, from public/doxa-maps/manifest.json. It is NO LONGER the root page: as of 2026-09-22 `/` mounts <plugin-studio> in BOTH trees (the built root page is written by `bun run tree`), and the gallery is the opt-in index at /__gallery. Archived, not deleted — the dev server imports renderGallery()/readBuildConfig() from this module.",
|
"gallery": "writes public/__gallery/index.html — the ARCHIVED bundle-index gallery, from public/doxa-maps/manifest.json. It is NO LONGER the root page: as of 2026-09-22 `/` mounts <plugin-studio> in BOTH trees (the built root page is written by `bun run tree`), and the gallery is the opt-in index at /__gallery. Archived, not deleted — the dev server imports renderGallery()/readBuildConfig() from this module.",
|
||||||
"gen:iso3-to-alpha2": "derive library/data/iso3-to-alpha2.json from vendored CLDR (48.2.2, Unicode-3.0) joined against the 242 codes in library/data/countries.json, so Intl.DisplayNames can produce a LOCALIZED country name — it takes alpha-2 and our data speaks alpha-3. Offline, no dependency. GRADES ITS OWN OUTPUT and writes nothing on failure: 242 coverage, injectivity, and KOS -> XK as the one documented exception (Kosovo has no ISO assignment). Cross-checks the 176-country research spine map and REPORTS disagreements without rewriting it — that file drives fetches and is not this generator's to edit.",
|
"gen:iso3-to-alpha2": "derive library/data/iso3-to-alpha2.json from vendored CLDR (48.2.2, Unicode-3.0) joined against the 242 codes in library/data/countries.json, so Intl.DisplayNames can produce a LOCALIZED country name — it takes alpha-2 and our data speaks alpha-3. Offline, no dependency. GRADES ITS OWN OUTPUT and writes nothing on failure: 242 coverage, injectivity, and KOS -> XK as the one documented exception (Kosovo has no ISO assignment). Cross-checks the 176-country research spine map and REPORTS disagreements without rewriting it — that file drives fetches and is not this generator's to edit.",
|
||||||
"internet:adm1": "regenerate data/tables/internet-by-adm1.json (internet use by first-level area; India from NFHS-5 2019-21, CC BY 4.0) from data/internet/ through a strict name crosswalk to the shipped gb-IND-ADM1 boundaries. Fails on an unmatched name, never guesses. Generic rows (iso3, adm1, measure, sex, area, value, source, year) so other plugins and go-int can reuse it.",
|
"internet:adm1": "regenerate data/tables/internet-by-adm1.json (internet use by first-level area; India from NFHS-5 2019-21, CC BY 4.0) from data/internet/ through a strict name crosswalk to the shipped gb-IND-ADM1 boundaries. Fails on an unmatched name, never guesses. Generic rows (iso3, adm1, measure, sex, area, value, source, year) so other plugins and go-int can reuse it.",
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,7 @@ import { defineComponent, inject } from 'vue'
|
||||||
import VerticalTabsLayout from '@map/layouts/VerticalTabsLayout.vue'
|
import VerticalTabsLayout from '@map/layouts/VerticalTabsLayout.vue'
|
||||||
import { SWITCHER_LANGUAGES, loadLocale, saveLocale, getLanguage } from '@map/layouts/app-wrapper-material/languages.js'
|
import { SWITCHER_LANGUAGES, loadLocale, saveLocale, getLanguage } from '@map/layouts/app-wrapper-material/languages.js'
|
||||||
import { translate, LOCALES, hasKey } from './src/strings.js'
|
import { translate, LOCALES, hasKey } from './src/strings.js'
|
||||||
|
import { loadScriptOnce, whenTagDefined } from '@map/utils/embedScript.js'
|
||||||
|
|
||||||
// THE HOST PAGE'S LANGUAGE, by the same ladder every nested map already uses (library/i18n
|
// THE HOST PAGE'S LANGUAGE, by the same ladder every nested map already uses (library/i18n
|
||||||
// detectLocale): ?lang= / ?locale= on the page URL, then <html lang>. '' when the page says nothing.
|
// detectLocale): ?lang= / ?locale= on the page URL, then <html lang>. '' when the page says nothing.
|
||||||
|
|
@ -680,54 +681,7 @@ function resolveTabsFromManifest(manifest, defs) {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Load a bundle exactly once per URL; resolves after its script has executed.
|
|
||||||
*
|
|
||||||
* ★ A CLASSIC SCRIPT FIRES onload AND THEN FAILS, for a dev-server bundle. Measured 2026-09-22:
|
|
||||||
* pointing a plain <script> at a dev entry (/plugin-designs/<bundle>/index.js, a real ES MODULE)
|
|
||||||
* fires onload — so a loader watching onload reports SUCCESS — and only THEN throws "Cannot use
|
|
||||||
* import statement outside a module" as an ASYNC page error. The tag is never defined,
|
|
||||||
* document.createElement(tag) yields an inert HTMLUnknownElement, and the pane sits empty while its
|
|
||||||
* own state reads 'ready'. onload is structurally incapable of seeing this, so it is not a usable
|
|
||||||
* success signal by itself — which is why every mount below also waits on whenTagDefined().
|
|
||||||
*
|
|
||||||
* The built tree ships IIFEs, where a classic tag is correct, so this only ever bites under the dev
|
|
||||||
* server — exactly the surface a developer works on. Rule: a rooted /plugin-designs/ entry (or any
|
|
||||||
* .../index.js) is a dev ES module; everything else is a built IIFE.
|
|
||||||
*
|
|
||||||
* SRI is optional (attrs.integrity).
|
|
||||||
*/
|
|
||||||
const _loaded = {}
|
|
||||||
function loadScriptOnce(url, attrs) {
|
|
||||||
if (_loaded[url]) return _loaded[url]
|
|
||||||
_loaded[url] = new Promise((ok, fail) => {
|
|
||||||
const s = document.createElement('script')
|
|
||||||
s.src = url
|
|
||||||
if (/^\/plugin-designs\//.test(url) || /\/index\.js(\?|$)/.test(url)) s.type = 'module'
|
|
||||||
if (attrs) for (const k in attrs) if (attrs[k]) s.setAttribute(k, attrs[k])
|
|
||||||
s.onload = () => ok()
|
|
||||||
s.onerror = () => { delete _loaded[url]; fail(new Error('failed to load ' + url)) }
|
|
||||||
document.head.appendChild(s)
|
|
||||||
})
|
|
||||||
return _loaded[url]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for a tag to ACTUALLY register. loadScriptOnce resolving is not proof the element exists (see
|
|
||||||
// above), and a module script's define() runs only after its import graph settles, so the tag can
|
|
||||||
// appear a tick or two after onload. customElements.whenDefined is the browser's own authoritative
|
|
||||||
// answer; the timeout turns a never-registered tag into THIS tab's error overlay rather than a silent
|
|
||||||
// blank pane that claims to be ready.
|
|
||||||
function whenTagDefined(tag, ms) {
|
|
||||||
if (typeof customElements === 'undefined') return Promise.resolve()
|
|
||||||
if (customElements.get(tag)) return Promise.resolve()
|
|
||||||
return Promise.race([
|
|
||||||
customElements.whenDefined(tag),
|
|
||||||
new Promise((_, fail) => setTimeout(() => {
|
|
||||||
if (customElements.get(tag)) return
|
|
||||||
fail(new Error('<' + tag + '> never registered (bundle loaded but defined no element)'))
|
|
||||||
}, ms || 15000))
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'DoxaMapsSpa',
|
name: 'DoxaMapsSpa',
|
||||||
|
|
|
||||||
|
|
@ -30,19 +30,11 @@
|
||||||
|
|
||||||
import { defineCustomElement } from 'vue'
|
import { defineCustomElement } from 'vue'
|
||||||
import DoxaMapsSpa from './doxa-maps-spa.vue'
|
import DoxaMapsSpa from './doxa-maps-spa.vue'
|
||||||
|
import { deriveTreeBase } from '@map/utils/embedScript.js'
|
||||||
|
|
||||||
// Capture the running <script>'s own URL synchronously at IIFE load, BEFORE any async runs, so
|
// Capture the running <script>'s own URL synchronously at IIFE load, BEFORE any async runs, so
|
||||||
// document.currentScript is still this bundle's tag. Derive the tree root = two levels up
|
// document.currentScript is still this bundle's tag. Derive the tree root = two levels up
|
||||||
// (.../doxa-maps/doxa-maps-spa/doxa-maps-spa.js → .../doxa-maps/). Guarded for non-browser.
|
// (.../doxa-maps/doxa-maps-spa/doxa-maps-spa.js → .../doxa-maps/). Guarded for non-browser.
|
||||||
function deriveTreeBase() {
|
|
||||||
try {
|
|
||||||
const cur = (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) || ''
|
|
||||||
if (!cur) return ''
|
|
||||||
// Strip the trailing "<bundle>/<bundle>.js" → the tree root, with trailing slash.
|
|
||||||
const root = cur.replace(/[^/]+\/[^/]+$/, '')
|
|
||||||
return root.replace(/\/+$/, '') + '/'
|
|
||||||
} catch { return '' }
|
|
||||||
}
|
|
||||||
const TREE_BASE = deriveTreeBase()
|
const TREE_BASE = deriveTreeBase()
|
||||||
|
|
||||||
const DoxaMapsSpaElement = defineCustomElement(DoxaMapsSpa, {
|
const DoxaMapsSpaElement = defineCustomElement(DoxaMapsSpa, {
|
||||||
|
|
|
||||||
|
|
@ -1111,9 +1111,52 @@ function _buildFlatLegendMatchExpr(node, legendType) {
|
||||||
const REGION_IDLE_FILL = ['case', ['has', 'iso_3166_1_alpha_3'], 0.7, 0.7]
|
const REGION_IDLE_FILL = ['case', ['has', 'iso_3166_1_alpha_3'], 0.7, 0.7]
|
||||||
const REGION_IDLE_COLOR = ['case', ['has', 'iso_3166_1_alpha_3'], 'rgba(60,60,80,0.35)', 'rgba(60,60,80,0.35)']
|
const REGION_IDLE_COLOR = ['case', ['has', 'iso_3166_1_alpha_3'], 'rgba(60,60,80,0.35)', 'rgba(60,60,80,0.35)']
|
||||||
const REGION_IDLE_WIDTH = ['case', ['has', 'iso_3166_1_alpha_3'], 0.6, 0.6]
|
const REGION_IDLE_WIDTH = ['case', ['has', 'iso_3166_1_alpha_3'], 0.6, 0.6]
|
||||||
|
// Make the region paints data-driven WITHOUT changing what they show: each property is read back and
|
||||||
|
// re-written wrapped in a case whose two branches are the value it already had. A property the layer
|
||||||
|
// does not define is skipped rather than invented, and one already wrapped is left alone.
|
||||||
|
function prewarmRegionPaint(m) {
|
||||||
|
if (!m || !m.getLayer('regions-fill')) return
|
||||||
|
const wrap = (v) => ['case', ['has', 'iso_3166_1_alpha_3'], v, v]
|
||||||
|
// ★ SKIP WHAT IS ALREADY DATA-DRIVEN, and note the test is not "is it an array". exp-shepherd wrote
|
||||||
|
// their copy to skip any array, reasoning that an expression is already data-driven so there is no
|
||||||
|
// variant to warm. That is right for this layer — line-color arrives as a match on a feature property
|
||||||
|
// — but it is right by accident: a ZOOM expression like ['interpolate',['linear'],['zoom'],…] is also
|
||||||
|
// an array and is NOT data-driven, so skipping it would leave that property to compile its variant on
|
||||||
|
// the first gesture and produce exactly the partial pre-warm this is trying to avoid.
|
||||||
|
// The property that matters is whether the expression reads the FEATURE. Wrapping one that already
|
||||||
|
// does is pointless anyway — it would rewrite a region-coloured match into a case around it, which is
|
||||||
|
// what my first version did.
|
||||||
|
const readsFeature = (e) => Array.isArray(e)
|
||||||
|
&& (e[0] === 'get' || e[0] === 'has' || e[0] === 'feature-state' || e.some(readsFeature))
|
||||||
|
const warm = (layer, prop) => {
|
||||||
|
if (!m.getLayer(layer)) return
|
||||||
|
const cur = m.getPaintProperty(layer, prop)
|
||||||
|
if (cur === undefined || cur === null) return
|
||||||
|
if (readsFeature(cur)) return
|
||||||
|
m.setPaintProperty(layer, prop, wrap(cur))
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
warm('regions-fill', 'fill-opacity')
|
||||||
|
warm('regions-border', 'line-color')
|
||||||
|
warm('regions-border', 'line-width')
|
||||||
|
} catch (_) { /* style still loading - the first real paint carries it */ }
|
||||||
|
}
|
||||||
|
|
||||||
function paintRegionScope(m, isoCodes) {
|
function paintRegionScope(m, isoCodes) {
|
||||||
if (!m || !m.getLayer('regions-fill')) return
|
if (!m || !m.getLayer('regions-fill')) return
|
||||||
const list = Array.isArray(isoCodes) ? isoCodes.filter(Boolean) : []
|
// ★ CANONICAL FIRST, OR THE COMPARE BELOW IS A LIE (rega, L43: still 9 writes per click, not 3).
|
||||||
|
// The two sources build the same region from different lists — node.isoCodes and
|
||||||
|
// mapLayers.regionIsoMap[slug] — and they do not agree on ORDER. rega had already told me that in
|
||||||
|
// the very first stack trace ("a case over ['IND',…]" then "a case over ['MYS',…], a different
|
||||||
|
// literal order") and I wrote a JSON-string compare anyway, which can never match two orderings of
|
||||||
|
// the same set. Sorting and de-duplicating makes the expression a function of the SET, which is what
|
||||||
|
// the two callers actually agree on.
|
||||||
|
//
|
||||||
|
// It is also a test: if the write count does not drop to 3, the two sources disagree about WHICH
|
||||||
|
// countries are in the region, which is a data defect this compare would otherwise have hidden.
|
||||||
|
const list = Array.isArray(isoCodes)
|
||||||
|
? [...new Set(isoCodes.filter(Boolean).map((c) => String(c).toUpperCase()))].sort()
|
||||||
|
: []
|
||||||
const isoArr = ['literal', list]
|
const isoArr = ['literal', list]
|
||||||
const fill = list.length ? ['case', ['in', ['get', 'iso_3166_1_alpha_3'], isoArr], 0.85, 0.15] : REGION_IDLE_FILL
|
const fill = list.length ? ['case', ['in', ['get', 'iso_3166_1_alpha_3'], isoArr], 0.85, 0.15] : REGION_IDLE_FILL
|
||||||
const color = list.length ? ['case', ['in', ['get', 'iso_3166_1_alpha_3'], isoArr], '#111827', 'rgba(60,60,80,0.15)'] : REGION_IDLE_COLOR
|
const color = list.length ? ['case', ['in', ['get', 'iso_3166_1_alpha_3'], isoArr], '#111827', 'rgba(60,60,80,0.15)'] : REGION_IDLE_COLOR
|
||||||
|
|
@ -1232,7 +1275,22 @@ function onSemanticTreeSelect(node) {
|
||||||
// Mirror region selection into mapStore (for geocoder / external sync).
|
// Mirror region selection into mapStore (for geocoder / external sync).
|
||||||
if (id.startsWith('region:')) mapStore.selectRegion?.(id.slice(7))
|
if (id.startsWith('region:')) mapStore.selectRegion?.(id.slice(7))
|
||||||
// Highlight matching polygons using isoCodes (alpha-3) on iso_3166_1_alpha_3.
|
// Highlight matching polygons using isoCodes (alpha-3) on iso_3166_1_alpha_3.
|
||||||
paintRegionScope(m, node.isoCodes || [])
|
// ★ ONE SOURCE, NOT JUST ONE WRITER. A region: click has TWO painters — this line, from
|
||||||
|
// node.isoCodes, and the canonical path it triggers one line above (selectRegion -> the store
|
||||||
|
// watcher -> applyDimFilter, which paints from mapLayers.regionIsoMap). Canonicalising the list
|
||||||
|
// made them comparable; it could not make them EQUAL, because the two sets are different by
|
||||||
|
// construction:
|
||||||
|
// · node.isoCodes comes from useWagfRegionsLegendData, which builds its regions by walking the
|
||||||
|
// PEOPLE-GROUP LIST — so it holds only countries that have pins.
|
||||||
|
// · regionIsoMap comes from regionsData.isoToRegion — every country assigned to the region.
|
||||||
|
// So a region highlight painted from the legend has HOLES wherever a country has no unreached
|
||||||
|
// group in the current list, and which version you saw depended on which writer ran last.
|
||||||
|
// The dataset is the right answer for "highlight the countries belonging to this region", so
|
||||||
|
// region: rows defer to it and stop painting here.
|
||||||
|
//
|
||||||
|
// block: and country: rows keep painting from node.isoCodes, because nothing else paints them —
|
||||||
|
// only region: reaches selectRegion above — and for those two the legend's own list IS the scope.
|
||||||
|
if (!id.startsWith('region:')) paintRegionScope(m, node.isoCodes || [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Theme bridge: keep the SemanticTreeLegend in sync with the app's dark-mode
|
// Theme bridge: keep the SemanticTreeLegend in sync with the app's dark-mode
|
||||||
|
|
@ -1402,9 +1460,16 @@ async function openPacket() {
|
||||||
//
|
//
|
||||||
// library/composables/usePosterPrints.js ALREADY has printPacket: the same pages, authored as HTML
|
// library/composables/usePosterPrints.js ALREADY has printPacket: the same pages, authored as HTML
|
||||||
// with @page rules, printed in place. This map was running a private second implementation of a
|
// with @page rules, printed in place. This map was running a private second implementation of a
|
||||||
// shared product feature. Routing to it deletes the duplicate, drops jspdf from this bundle, and
|
// shared product feature. Routing to it deletes the duplicate, and the packet gains the pages the
|
||||||
// the packet gains the pages the shared renderer has that the local one never did (the life
|
// shared renderer has that the local one never did (the life calendar, the funnel, the prayer
|
||||||
// calendar, the funnel, the prayer wheel — see packet-render.buildPacket's page order).
|
// wheel — see packet-render.buildPacket's page order).
|
||||||
|
//
|
||||||
|
// ⚠ THIS USED TO CLAIM IT DROPPED JSPDF FROM THE BUNDLE. It does not, and I checked the built
|
||||||
|
// artifact only after saying so: jspdf arrives through useMapPoster.toPdf, which this map still
|
||||||
|
// uses for the poster, so it was never this path's to remove. The bundle in fact GREW about 131 KB
|
||||||
|
// (983,264 -> 1,117,687 on L42) because the shared HTML renderer is inlined — each bundle is a
|
||||||
|
// single .js with no code splitting, so the dynamic imports above are build-time inlined, not lazy.
|
||||||
|
// Correct on the record here because the wrong sentence was copied verbatim into country-posters.
|
||||||
const [{ usePosterPrints }, { defaultPosterSpec }, { useMapPoster }, { selectedPinIds }] = await Promise.all([
|
const [{ usePosterPrints }, { defaultPosterSpec }, { useMapPoster }, { selectedPinIds }] = await Promise.all([
|
||||||
import('@map/composables/usePosterPrints.js'),
|
import('@map/composables/usePosterPrints.js'),
|
||||||
import('@map/constants/posterDefaults.js'),
|
import('@map/constants/posterDefaults.js'),
|
||||||
|
|
@ -1584,9 +1649,20 @@ async function ensureRegionsLoaded() {
|
||||||
// ★ PRE-WARM HERE, which is the only moment that helps. These layers are added on first visit
|
// ★ PRE-WARM HERE, which is the only moment that helps. These layers are added on first visit
|
||||||
// to the Regions tab, and rega measured the first ROW click paying 5.5 s (MapLibre) / 11.1 s
|
// to the Regions tab, and rega measured the first ROW click paying 5.5 s (MapLibre) / 11.1 s
|
||||||
// (Mapbox) of shader compile because that click was when their paint first became data-driven.
|
// (Mapbox) of shader compile because that click was when their paint first became data-driven.
|
||||||
// Painting the idle state through paintRegionScope makes it data-driven from the first draw, so
|
// Making the paint data-driven at layer-add moves that compile here, where a 6.4 MB GeoJSON is
|
||||||
// the variant compiles here — where a 6.4 MB GeoJSON is already loading — instead of mid-gesture.
|
// already loading, instead of into the middle of a gesture.
|
||||||
paintRegionScope(map.value, [])
|
//
|
||||||
|
// WARNING IT WRAPS WHAT THE LAYER ALREADY PAINTS, and does NOT paint the cleared state. The
|
||||||
|
// first version called paintRegionScope(map.value, []) — the CLEARED look, flat grey borders at
|
||||||
|
// width 0.6. addRegionsLayer does not start there: it sets line-color to the region-coloured
|
||||||
|
// match expression and line-width 1 (useMapLayers). So pre-warming with the cleared values
|
||||||
|
// silently repainted every border grey and thinner the moment the layer appeared — a visible
|
||||||
|
// change nobody asked for, and invisible to both things being measured at the time, a timing
|
||||||
|
// check and a write count.
|
||||||
|
// Reading each property back and re-writing it wrapped in an identical-branch case keeps the
|
||||||
|
// appearance exactly as the layer defined it while still making the property data-driven, which
|
||||||
|
// is all the shader variant depends on.
|
||||||
|
prewarmRegionPaint(map.value)
|
||||||
// Hide regions on non-Doxa-Regions tabs so they don't show through.
|
// Hide regions on non-Doxa-Regions tabs so they don't show through.
|
||||||
if (map.value.getLayer('regions-fill') && activeTab.value?.id !== 'doxa-regions') {
|
if (map.value.getLayer('regions-fill') && activeTab.value?.id !== 'doxa-regions') {
|
||||||
map.value.setLayoutProperty('regions-fill', 'visibility', 'none')
|
map.value.setLayoutProperty('regions-fill', 'visibility', 'none')
|
||||||
|
|
|
||||||
|
|
@ -33,18 +33,11 @@
|
||||||
|
|
||||||
import { defineCustomElement } from 'vue'
|
import { defineCustomElement } from 'vue'
|
||||||
import PluginStudio from './plugin-studio.vue'
|
import PluginStudio from './plugin-studio.vue'
|
||||||
|
import { deriveTreeBase } from '@map/utils/embedScript.js'
|
||||||
|
|
||||||
// Capture the running <script>'s own URL synchronously at IIFE load, BEFORE any async runs, so
|
// Capture the running <script>'s own URL synchronously at IIFE load, BEFORE any async runs, so
|
||||||
// document.currentScript is still this bundle's tag. Derive the tree root = two levels up
|
// document.currentScript is still this bundle's tag. Derive the tree root = two levels up
|
||||||
// (.../doxa-maps/plugin-studio/plugin-studio.js -> .../doxa-maps/). Guarded for non-browser.
|
// (.../doxa-maps/plugin-studio/plugin-studio.js -> .../doxa-maps/). Guarded for non-browser.
|
||||||
function deriveTreeBase() {
|
|
||||||
try {
|
|
||||||
const cur = (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) || ''
|
|
||||||
if (!cur) return ''
|
|
||||||
const root = cur.replace(/[^/]+\/[^/]+$/, '')
|
|
||||||
return root.replace(/\/+$/, '') + '/'
|
|
||||||
} catch { return '' }
|
|
||||||
}
|
|
||||||
const TREE_BASE = deriveTreeBase()
|
const TREE_BASE = deriveTreeBase()
|
||||||
|
|
||||||
const PluginStudioElement = defineCustomElement(PluginStudio, {
|
const PluginStudioElement = defineCustomElement(PluginStudio, {
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,7 @@ import VerticalTabsLayout from '@map/layouts/VerticalTabsLayout.vue'
|
||||||
import StudioTree from './src/StudioTree.vue'
|
import StudioTree from './src/StudioTree.vue'
|
||||||
import { LANGUAGES, loadLocale, saveLocale, getLanguage, normaliseLocale } from '@map/layouts/app-wrapper-material/languages.js'
|
import { LANGUAGES, loadLocale, saveLocale, getLanguage, normaliseLocale } from '@map/layouts/app-wrapper-material/languages.js'
|
||||||
import { publishMapToken, clearMapToken } from '@map/composables/useSharedMapToken.js'
|
import { publishMapToken, clearMapToken } from '@map/composables/useSharedMapToken.js'
|
||||||
|
import { loadScriptOnce, whenTagDefined } from '@map/utils/embedScript.js'
|
||||||
|
|
||||||
// The reader's own map token / token-service URL. localStorage only: never written into the build,
|
// 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).
|
// never sent anywhere but Mapbox (the token) or the URL the reader typed (the service).
|
||||||
|
|
@ -428,47 +429,7 @@ function rowLabel(bundle, p) {
|
||||||
return p.profile === bundle ? humanize(bundle) : humanize(bundle) + ' · ' + humanize(p.profile)
|
return p.profile === bundle ? humanize(bundle) : humanize(bundle) + ' · ' + humanize(p.profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load a bundle exactly once per URL; resolves after its define() has run.
|
|
||||||
//
|
|
||||||
// ★ A CLASSIC SCRIPT FIRES onload AND THEN FAILS, for a dev-server bundle. Measured 2026-09-22:
|
|
||||||
// pointing a plain <script> at the dev entry (/plugin-designs/<bundle>/index.js, a real ES
|
|
||||||
// module) fires onload — so the old loader resolved "success" — and only THEN throws
|
|
||||||
// "Cannot use import statement outside a module" as an async page error. The tag is never
|
|
||||||
// defined, createElement yields an inert HTMLUnknownElement, and the pane sits empty while its
|
|
||||||
// state reads 'ready'. onload is structurally incapable of seeing this failure, so it is not a
|
|
||||||
// usable success signal on its own. A built tree ships IIFEs, where a classic tag is correct, so
|
|
||||||
// this only ever bites under the dev server — exactly where the studio is developed.
|
|
||||||
// Rule: a rooted /plugin-designs/ entry is a dev ES module; everything else is a built IIFE.
|
|
||||||
const _loaded = {}
|
|
||||||
function loadScriptOnce(url, attrs) {
|
|
||||||
if (_loaded[url]) return _loaded[url]
|
|
||||||
_loaded[url] = new Promise((ok, fail) => {
|
|
||||||
const s = document.createElement('script')
|
|
||||||
s.src = url
|
|
||||||
if (/^\/plugin-designs\//.test(url) || /\/index\.js(\?|$)/.test(url)) s.type = 'module'
|
|
||||||
if (attrs) for (const k in attrs) if (attrs[k]) s.setAttribute(k, attrs[k])
|
|
||||||
s.onload = () => ok()
|
|
||||||
s.onerror = () => { delete _loaded[url]; fail(new Error('failed to load ' + url)) }
|
|
||||||
document.head.appendChild(s)
|
|
||||||
})
|
|
||||||
return _loaded[url]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for a tag to actually register. loadScriptOnce resolving is NOT proof the element exists
|
|
||||||
// (see above), and a module script's define() runs after its import graph settles, so the tag can
|
|
||||||
// appear a tick or two after onload. whenDefined is the browser's own authoritative answer; the
|
|
||||||
// timeout turns a never-registered tag into THIS row's error overlay instead of a silent blank pane.
|
|
||||||
function whenTagDefined(tag, ms) {
|
|
||||||
if (typeof customElements === 'undefined') return Promise.resolve()
|
|
||||||
if (customElements.get(tag)) return Promise.resolve()
|
|
||||||
return Promise.race([
|
|
||||||
customElements.whenDefined(tag),
|
|
||||||
new Promise((_, fail) => setTimeout(() => {
|
|
||||||
if (customElements.get(tag)) return
|
|
||||||
fail(new Error('<' + tag + '> never registered (bundle loaded but defined no element)'))
|
|
||||||
}, ms || 15000))
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Corner widgets: a round chat-style bubble; open, a small panel above it (a bottom sheet on a phone).
|
// Corner widgets: a round chat-style bubble; open, a small panel above it (a bottom sheet on a phone).
|
||||||
const WIDGET_CSS = `
|
const WIDGET_CSS = `
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,6 @@
|
||||||
"type": "single",
|
"type": "single",
|
||||||
"widget": { "corner": "bottom-right" },
|
"widget": { "corner": "bottom-right" },
|
||||||
"label": "Rudder — voice to text (desktop)",
|
"label": "Rudder — voice to text (desktop)",
|
||||||
"targets": ["desktop"],
|
"targets": ["desktop", "widget"],
|
||||||
"backend": "backend/rudder-engine"
|
"backend": "backend/rudder-engine"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -707,8 +707,10 @@ const statusText = computed(() => ({
|
||||||
the engine runs on. Open the Plugin Studio on your computer (<code>bun run dev</code>) or use the Rudder desktop app.
|
the engine runs on. Open the Plugin Studio on your computer (<code>bun run dev</code>) or use the Rudder desktop app.
|
||||||
</p>
|
</p>
|
||||||
<p v-else-if="engine === 'offline'" class="notice" data-kind="warn">
|
<p v-else-if="engine === 'offline'" class="notice" data-kind="warn">
|
||||||
The local speech engine isn't reachable at <code>{{ apiBase }}</code>. In the Plugin Studio it starts with
|
The local speech engine isn't reachable at <code>{{ apiBase }}</code>. It needs <b>Go 1.22 or newer</b> and a
|
||||||
<code>bun run dev</code> once built: <code>cd backend/rudder-engine && go build -o ../bin/rudder-engine .</code>
|
one-time setup that downloads the speech model (about 750 MB): <code>bash backend/rudder-engine/setup-runtime.sh</code>.
|
||||||
|
Then <code>bun run dev</code> starts it here. For the desktop app, <code>bun run desktop:build rudder-voice</code>;
|
||||||
|
both are described in <code>documentation/HOWTO-build-the-desktop-app.md</code>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<section class="compose">
|
<section class="compose">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
This folder is GENERATED by `bun run build`; do not edit it by hand. It holds exactly one thing:
|
This folder is GENERATED by `bun run build`; do not edit it by hand. It holds exactly one thing:
|
||||||
|
|
||||||
- **`dx-maps/`** — the output folder: 25 plugin bundles and 37 pages, self-contained.
|
- **`dx-maps/`** — the output folder: 24 plugin bundles and 36 pages, self-contained.
|
||||||
Copy this one folder to any web host (or open it from disk) and it works.
|
Copy this one folder to any web host (or open it from disk) and it works.
|
||||||
- `dx-maps/index.html` — the front page: the Plugin Studio, every plugin in one place.
|
- `dx-maps/index.html` — the front page: the Plugin Studio, every plugin in one place.
|
||||||
- `dx-maps/<plugin>/<plugin>.js` — a plugin's bundle (a custom element; load it with a `<script>` tag).
|
- `dx-maps/<plugin>/<plugin>.js` — a plugin's bundle (a custom element; load it with a `<script>` tag).
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -47,7 +47,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 <dashboard-plus-grid> custom element. -->
|
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
|
||||||
<script src="../../dashboard-plus-grid.js" integrity="sha384-P15DN0neJhlrI9+vzXDozhY8RZHDsJkVnV3Yxun8ewNUNYFEyCdSETY1vBVu+6ee" crossorigin="anonymous"></script>
|
<script src="../../dashboard-plus-grid.js" integrity="sha384-X3AEM/H4IPUfLH80IHRCRQHDmUwmuAAE+/ChGsH/3qdFV6KB7NhipFlWJtC3F/7t" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,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 <dashboard-plus-grid> custom element. -->
|
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
|
||||||
<script src="../dashboard-plus-grid.js" integrity="sha384-P15DN0neJhlrI9+vzXDozhY8RZHDsJkVnV3Yxun8ewNUNYFEyCdSETY1vBVu+6ee" crossorigin="anonymous"></script>
|
<script src="../dashboard-plus-grid.js" integrity="sha384-X3AEM/H4IPUfLH80IHRCRQHDmUwmuAAE+/ChGsH/3qdFV6KB7NhipFlWJtC3F/7t" 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
|
|
@ -32,7 +32,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 <doxa-maps-spa> custom element. -->
|
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
|
||||||
<script src="../../doxa-maps-spa.js" integrity="sha384-CKJT8BCWZubQKLenstpsz19Kd87akZiO1SGu5+Ma3byRUdyXz0gcjrtmmkQxzw7i" crossorigin="anonymous"></script>
|
<script src="../../doxa-maps-spa.js" integrity="sha384-AmW0CbWRRmUZJW0me3crwWEAS4XiX93Z7KM9Vpu5rBKYXgPOJYTf4LEVp8oDG4li" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,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 <doxa-maps-spa> custom element. -->
|
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
|
||||||
<script src="../doxa-maps-spa.js" integrity="sha384-CKJT8BCWZubQKLenstpsz19Kd87akZiO1SGu5+Ma3byRUdyXz0gcjrtmmkQxzw7i" crossorigin="anonymous"></script>
|
<script src="../doxa-maps-spa.js" integrity="sha384-AmW0CbWRRmUZJW0me3crwWEAS4XiX93Z7KM9Vpu5rBKYXgPOJYTf4LEVp8oDG4li" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
"schema": "doxa-maps-manifest@1",
|
"schema": "doxa-maps-manifest@1",
|
||||||
"generatedFrom": "plugin-designs/*/*.vue",
|
"generatedFrom": "plugin-designs/*/*.vue",
|
||||||
"embedBase": ".",
|
"embedBase": ".",
|
||||||
"bundleCount": 25,
|
"bundleCount": 24,
|
||||||
"profileCount": 37,
|
"profileCount": 36,
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"apiBaseUrl": "https://pray.doxa.life"
|
"apiBaseUrl": "https://pray.doxa.life"
|
||||||
},
|
},
|
||||||
|
|
@ -229,7 +229,7 @@
|
||||||
"bundle": "dashboard-plus-grid",
|
"bundle": "dashboard-plus-grid",
|
||||||
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
|
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
|
||||||
"output": "dx-maps/dashboard-plus-grid/dashboard-plus-grid.js",
|
"output": "dx-maps/dashboard-plus-grid/dashboard-plus-grid.js",
|
||||||
"integrity": "sha384-P15DN0neJhlrI9+vzXDozhY8RZHDsJkVnV3Yxun8ewNUNYFEyCdSETY1vBVu+6ee",
|
"integrity": "sha384-X3AEM/H4IPUfLH80IHRCRQHDmUwmuAAE+/ChGsH/3qdFV6KB7NhipFlWJtC3F/7t",
|
||||||
"elements": [
|
"elements": [
|
||||||
"dashboard-plus-grid"
|
"dashboard-plus-grid"
|
||||||
],
|
],
|
||||||
|
|
@ -252,7 +252,7 @@
|
||||||
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
|
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./dashboard-plus-grid/dashboard-plus-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./dashboard-plus-grid/dashboard-plus-grid/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=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-P15DN0neJhlrI9+vzXDozhY8RZHDsJkVnV3Yxun8ewNUNYFEyCdSETY1vBVu+6ee\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
|
"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=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-X3AEM/H4IPUfLH80IHRCRQHDmUwmuAAE+/ChGsH/3qdFV6KB7NhipFlWJtC3F/7t\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
|
||||||
},
|
},
|
||||||
"instances": [
|
"instances": [
|
||||||
{
|
{
|
||||||
|
|
@ -321,7 +321,7 @@
|
||||||
"bundle": "doxa-maps-spa",
|
"bundle": "doxa-maps-spa",
|
||||||
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
|
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
|
||||||
"output": "dx-maps/doxa-maps-spa/doxa-maps-spa.js",
|
"output": "dx-maps/doxa-maps-spa/doxa-maps-spa.js",
|
||||||
"integrity": "sha384-CKJT8BCWZubQKLenstpsz19Kd87akZiO1SGu5+Ma3byRUdyXz0gcjrtmmkQxzw7i",
|
"integrity": "sha384-AmW0CbWRRmUZJW0me3crwWEAS4XiX93Z7KM9Vpu5rBKYXgPOJYTf4LEVp8oDG4li",
|
||||||
"elements": [
|
"elements": [
|
||||||
"doxa-maps-spa"
|
"doxa-maps-spa"
|
||||||
],
|
],
|
||||||
|
|
@ -349,7 +349,7 @@
|
||||||
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
|
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./doxa-maps-spa/doxa-maps-spa/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./doxa-maps-spa/doxa-maps-spa/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=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-CKJT8BCWZubQKLenstpsz19Kd87akZiO1SGu5+Ma3byRUdyXz0gcjrtmmkQxzw7i\" crossorigin=\"anonymous\"></script>\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
|
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-AmW0CbWRRmUZJW0me3crwWEAS4XiX93Z7KM9Vpu5rBKYXgPOJYTf4LEVp8oDG4li\" crossorigin=\"anonymous\"></script>\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
|
||||||
},
|
},
|
||||||
"instances": [
|
"instances": [
|
||||||
{
|
{
|
||||||
|
|
@ -376,7 +376,7 @@
|
||||||
"bundle": "doxa-research-map",
|
"bundle": "doxa-research-map",
|
||||||
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
|
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
|
||||||
"output": "dx-maps/doxa-research-map/doxa-research-map.js",
|
"output": "dx-maps/doxa-research-map/doxa-research-map.js",
|
||||||
"integrity": "sha384-11TPej/kB1nQL0lXCw/gOjTmj/VznycM5fmIfsMNah74SMwH8i+NwFSgbr5zoDZs",
|
"integrity": "sha384-kC4KWUvDI3yN7HgjzUQVZTm2dYxTM0pRBlBxhFL9VQALYpLhmASkvjVthQ3dvbdC",
|
||||||
"elements": [
|
"elements": [
|
||||||
"doxa-research-map"
|
"doxa-research-map"
|
||||||
],
|
],
|
||||||
|
|
@ -395,7 +395,7 @@
|
||||||
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
|
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./doxa-research-map/research-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./doxa-research-map/research-map/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=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-11TPej/kB1nQL0lXCw/gOjTmj/VznycM5fmIfsMNah74SMwH8i+NwFSgbr5zoDZs\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
|
"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=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-kC4KWUvDI3yN7HgjzUQVZTm2dYxTM0pRBlBxhFL9VQALYpLhmASkvjVthQ3dvbdC\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
|
||||||
},
|
},
|
||||||
"instances": [
|
"instances": [
|
||||||
{
|
{
|
||||||
|
|
@ -737,7 +737,7 @@
|
||||||
"bundle": "gospel-resources",
|
"bundle": "gospel-resources",
|
||||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||||
"output": "dx-maps/gospel-resources/gospel-resources.js",
|
"output": "dx-maps/gospel-resources/gospel-resources.js",
|
||||||
"integrity": "sha384-lcwI2RPIjOWv3A0eY1Ej8qmWmdR0IYP3XKKFyZpwfE47av3KkP+f7D4ukhVw+qEe",
|
"integrity": "sha384-QVzftttPIYeK56Jo4vXx2mZ5lCxyapxR+YfayYABQ0ufom3AeWbw7cq8SpGSHeja",
|
||||||
"elements": [
|
"elements": [
|
||||||
"gospel-resources-map"
|
"gospel-resources-map"
|
||||||
],
|
],
|
||||||
|
|
@ -759,7 +759,7 @@
|
||||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./gospel-resources/charts/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./gospel-resources/charts/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-lcwI2RPIjOWv3A0eY1Ej8qmWmdR0IYP3XKKFyZpwfE47av3KkP+f7D4ukhVw+qEe\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-QVzftttPIYeK56Jo4vXx2mZ5lCxyapxR+YfayYABQ0ufom3AeWbw7cq8SpGSHeja\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -774,7 +774,7 @@
|
||||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./gospel-resources/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./gospel-resources/data-tables/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-lcwI2RPIjOWv3A0eY1Ej8qmWmdR0IYP3XKKFyZpwfE47av3KkP+f7D4ukhVw+qEe\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-QVzftttPIYeK56Jo4vXx2mZ5lCxyapxR+YfayYABQ0ufom3AeWbw7cq8SpGSHeja\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -790,7 +790,7 @@
|
||||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./gospel-resources/gospel-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./gospel-resources/gospel-dashboard/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-resources/gospel-resources.js\" integrity=\"sha384-lcwI2RPIjOWv3A0eY1Ej8qmWmdR0IYP3XKKFyZpwfE47av3KkP+f7D4ukhVw+qEe\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
"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-resources/gospel-resources.js\" integrity=\"sha384-QVzftttPIYeK56Jo4vXx2mZ5lCxyapxR+YfayYABQ0ufom3AeWbw7cq8SpGSHeja\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -805,7 +805,7 @@
|
||||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./gospel-resources/resources-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./gospel-resources/resources-list/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-lcwI2RPIjOWv3A0eY1Ej8qmWmdR0IYP3XKKFyZpwfE47av3KkP+f7D4ukhVw+qEe\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-QVzftttPIYeK56Jo4vXx2mZ5lCxyapxR+YfayYABQ0ufom3AeWbw7cq8SpGSHeja\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -821,7 +821,7 @@
|
||||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./gospel-resources/resources-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./gospel-resources/resources-map/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-resources/gospel-resources.js\" integrity=\"sha384-lcwI2RPIjOWv3A0eY1Ej8qmWmdR0IYP3XKKFyZpwfE47av3KkP+f7D4ukhVw+qEe\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
"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-resources/gospel-resources.js\" integrity=\"sha384-QVzftttPIYeK56Jo4vXx2mZ5lCxyapxR+YfayYABQ0ufom3AeWbw7cq8SpGSHeja\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -956,7 +956,7 @@
|
||||||
"bundle": "plugin-studio",
|
"bundle": "plugin-studio",
|
||||||
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
||||||
"output": "dx-maps/plugin-studio/plugin-studio.js",
|
"output": "dx-maps/plugin-studio/plugin-studio.js",
|
||||||
"integrity": "sha384-NsG6VNbCUAcOh9jEf5+8DeaKFm7xRySJO8yIUiE0O0AQ3FhuILru4eBHgaXCPQUD",
|
"integrity": "sha384-gC2dBaw7+rJERXYB7VaEtRlo1wIkVkA5CzKHyuid0QAmu0qPYmezGM9kdY9pPAOc",
|
||||||
"elements": [
|
"elements": [
|
||||||
"plugin-studio"
|
"plugin-studio"
|
||||||
],
|
],
|
||||||
|
|
@ -987,7 +987,7 @@
|
||||||
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
||||||
"embed": {
|
"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>",
|
"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-NsG6VNbCUAcOh9jEf5+8DeaKFm7xRySJO8yIUiE0O0AQ3FhuILru4eBHgaXCPQUD\" 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-gC2dBaw7+rJERXYB7VaEtRlo1wIkVkA5CzKHyuid0QAmu0qPYmezGM9kdY9pPAOc\" 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": [
|
"instances": [
|
||||||
{
|
{
|
||||||
|
|
@ -1163,7 +1163,7 @@
|
||||||
"bundle": "research-map-plus-grid",
|
"bundle": "research-map-plus-grid",
|
||||||
"scriptUrl": "./research-map-plus-grid/research-map-plus-grid.js",
|
"scriptUrl": "./research-map-plus-grid/research-map-plus-grid.js",
|
||||||
"output": "dx-maps/research-map-plus-grid/research-map-plus-grid.js",
|
"output": "dx-maps/research-map-plus-grid/research-map-plus-grid.js",
|
||||||
"integrity": "sha384-malo2pC/K0/1Xi9kyDubh+4tY9HC56zcO/VsgppNf5c4AXvXG9RMxqVHYoFAoOr5",
|
"integrity": "sha384-sIu4F0HzdbO4/0FF1bUGfe7xj/8bjMklygdB3/KmaG8bHHXa300y9nPDYg1bCouM",
|
||||||
"elements": [
|
"elements": [
|
||||||
"research-map-plus-grid"
|
"research-map-plus-grid"
|
||||||
],
|
],
|
||||||
|
|
@ -1186,7 +1186,7 @@
|
||||||
"scriptUrl": "./research-map-plus-grid/research-map-plus-grid.js",
|
"scriptUrl": "./research-map-plus-grid/research-map-plus-grid.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./research-map-plus-grid/research-map-plus-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./research-map-plus-grid/research-map-plus-grid/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=\"./research-map-plus-grid/research-map-plus-grid.js\" integrity=\"sha384-malo2pC/K0/1Xi9kyDubh+4tY9HC56zcO/VsgppNf5c4AXvXG9RMxqVHYoFAoOr5\" crossorigin=\"anonymous\"></script>\n<research-map-plus-grid profile-config='{\"profile\":\"research-map-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-map-plus-grid>"
|
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-map-plus-grid/research-map-plus-grid.js\" integrity=\"sha384-sIu4F0HzdbO4/0FF1bUGfe7xj/8bjMklygdB3/KmaG8bHHXa300y9nPDYg1bCouM\" crossorigin=\"anonymous\"></script>\n<research-map-plus-grid profile-config='{\"profile\":\"research-map-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-map-plus-grid>"
|
||||||
},
|
},
|
||||||
"instances": [
|
"instances": [
|
||||||
{
|
{
|
||||||
|
|
@ -1206,45 +1206,11 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"bundle": "rudder-voice",
|
|
||||||
"scriptUrl": "./rudder-voice/rudder-voice.js",
|
|
||||||
"output": "dx-maps/rudder-voice/rudder-voice.js",
|
|
||||||
"integrity": "sha384-a4oi+6HstA7vCZFrrX2qSEKq56mkDBJAkg485g8BRPeZb6C6QtJ+zxmjeBDg1/XT",
|
|
||||||
"elements": [
|
|
||||||
"rudder-voice"
|
|
||||||
],
|
|
||||||
"stage": "experimental",
|
|
||||||
"profiles": [
|
|
||||||
{
|
|
||||||
"profile": "rudder-voice",
|
|
||||||
"stage": "experimental",
|
|
||||||
"label": "Rudder — voice to text (desktop)",
|
|
||||||
"targets": [
|
|
||||||
"desktop"
|
|
||||||
],
|
|
||||||
"widget": {
|
|
||||||
"corner": "bottom-right"
|
|
||||||
},
|
|
||||||
"file": "rudder-voice.vue",
|
|
||||||
"description": "A DESKTOP-target plugin (rudder-voice.meta.json \"targets\": [\"desktop\"]). The speech",
|
|
||||||
"element": "rudder-voice",
|
|
||||||
"type": "single",
|
|
||||||
"isMap": false,
|
|
||||||
"iframeUrl": "./rudder-voice/rudder-voice/index.html",
|
|
||||||
"scriptUrl": "./rudder-voice/rudder-voice.js",
|
|
||||||
"embed": {
|
|
||||||
"iframe": "<iframe src=\"./rudder-voice/rudder-voice/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=\"./rudder-voice/rudder-voice.js\" integrity=\"sha384-a4oi+6HstA7vCZFrrX2qSEKq56mkDBJAkg485g8BRPeZb6C6QtJ+zxmjeBDg1/XT\" crossorigin=\"anonymous\"></script>\n<rudder-voice profile-config='{\"profile\":\"rudder-voice\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></rudder-voice>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"bundle": "stack-demo",
|
"bundle": "stack-demo",
|
||||||
"scriptUrl": "./stack-demo/stack-demo.js",
|
"scriptUrl": "./stack-demo/stack-demo.js",
|
||||||
"output": "dx-maps/stack-demo/stack-demo.js",
|
"output": "dx-maps/stack-demo/stack-demo.js",
|
||||||
"integrity": "sha384-Pg93Oyzao9WxHJMSJZPF798o1/Klk2ie7EDLTpVpQd0HaKCDDEHRDWUjt6OYwzqi",
|
"integrity": "sha384-zUkv1c/B+mQcflM2pwrczz+OpZxzl8JynKVV+5IyYiajtfQNU5RMJDJaF7nk/0oy",
|
||||||
"elements": [
|
"elements": [
|
||||||
"stack-demo"
|
"stack-demo"
|
||||||
],
|
],
|
||||||
|
|
@ -1267,7 +1233,7 @@
|
||||||
"scriptUrl": "./stack-demo/stack-demo.js",
|
"scriptUrl": "./stack-demo/stack-demo.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./stack-demo/stack-demo/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./stack-demo/stack-demo/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=\"./stack-demo/stack-demo.js\" integrity=\"sha384-Pg93Oyzao9WxHJMSJZPF798o1/Klk2ie7EDLTpVpQd0HaKCDDEHRDWUjt6OYwzqi\" crossorigin=\"anonymous\"></script>\n<stack-demo profile-config='{\"profile\":\"stack-demo\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></stack-demo>"
|
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./stack-demo/stack-demo.js\" integrity=\"sha384-zUkv1c/B+mQcflM2pwrczz+OpZxzl8JynKVV+5IyYiajtfQNU5RMJDJaF7nk/0oy\" crossorigin=\"anonymous\"></script>\n<stack-demo profile-config='{\"profile\":\"stack-demo\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></stack-demo>"
|
||||||
},
|
},
|
||||||
"instances": [
|
"instances": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -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 <doxa-research-map> custom element. -->
|
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
|
||||||
<script src="../doxa-research-map.js" integrity="sha384-11TPej/kB1nQL0lXCw/gOjTmj/VznycM5fmIfsMNah74SMwH8i+NwFSgbr5zoDZs" crossorigin="anonymous"></script>
|
<script src="../doxa-research-map.js" integrity="sha384-kC4KWUvDI3yN7HgjzUQVZTm2dYxTM0pRBlBxhFL9VQALYpLhmASkvjVthQ3dvbdC" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,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 <doxa-research-map> custom element. -->
|
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
|
||||||
<script src="../../doxa-research-map.js" integrity="sha384-11TPej/kB1nQL0lXCw/gOjTmj/VznycM5fmIfsMNah74SMwH8i+NwFSgbr5zoDZs" crossorigin="anonymous"></script>
|
<script src="../../doxa-research-map.js" integrity="sha384-kC4KWUvDI3yN7HgjzUQVZTm2dYxTM0pRBlBxhFL9VQALYpLhmASkvjVthQ3dvbdC" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,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-resources-map> custom element. -->
|
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||||
<script src="../gospel-resources.js" integrity="sha384-lcwI2RPIjOWv3A0eY1Ej8qmWmdR0IYP3XKKFyZpwfE47av3KkP+f7D4ukhVw+qEe" crossorigin="anonymous"></script>
|
<script src="../gospel-resources.js" integrity="sha384-QVzftttPIYeK56Jo4vXx2mZ5lCxyapxR+YfayYABQ0ufom3AeWbw7cq8SpGSHeja" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,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-resources-map> custom element. -->
|
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||||
<script src="../gospel-resources.js" integrity="sha384-lcwI2RPIjOWv3A0eY1Ej8qmWmdR0IYP3XKKFyZpwfE47av3KkP+f7D4ukhVw+qEe" crossorigin="anonymous"></script>
|
<script src="../gospel-resources.js" integrity="sha384-QVzftttPIYeK56Jo4vXx2mZ5lCxyapxR+YfayYABQ0ufom3AeWbw7cq8SpGSHeja" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,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-resources-map> custom element. -->
|
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||||
<script src="../gospel-resources.js" integrity="sha384-lcwI2RPIjOWv3A0eY1Ej8qmWmdR0IYP3XKKFyZpwfE47av3KkP+f7D4ukhVw+qEe" crossorigin="anonymous"></script>
|
<script src="../gospel-resources.js" integrity="sha384-QVzftttPIYeK56Jo4vXx2mZ5lCxyapxR+YfayYABQ0ufom3AeWbw7cq8SpGSHeja" 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
|
|
@ -32,7 +32,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-resources-map> custom element. -->
|
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||||
<script src="../gospel-resources.js" integrity="sha384-lcwI2RPIjOWv3A0eY1Ej8qmWmdR0IYP3XKKFyZpwfE47av3KkP+f7D4ukhVw+qEe" crossorigin="anonymous"></script>
|
<script src="../gospel-resources.js" integrity="sha384-QVzftttPIYeK56Jo4vXx2mZ5lCxyapxR+YfayYABQ0ufom3AeWbw7cq8SpGSHeja" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -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-resources-map> custom element. -->
|
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||||
<script src="../gospel-resources.js" integrity="sha384-lcwI2RPIjOWv3A0eY1Ej8qmWmdR0IYP3XKKFyZpwfE47av3KkP+f7D4ukhVw+qEe" crossorigin="anonymous"></script>
|
<script src="../gospel-resources.js" integrity="sha384-QVzftttPIYeK56Jo4vXx2mZ5lCxyapxR+YfayYABQ0ufom3AeWbw7cq8SpGSHeja" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,6 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<plugin-studio tree-base="."></plugin-studio>
|
<plugin-studio tree-base="."></plugin-studio>
|
||||||
<script src="./plugin-studio/plugin-studio.js" integrity="sha384-NsG6VNbCUAcOh9jEf5+8DeaKFm7xRySJO8yIUiE0O0AQ3FhuILru4eBHgaXCPQUD" crossorigin="anonymous"></script>
|
<script src="./plugin-studio/plugin-studio.js" integrity="sha384-gC2dBaw7+rJERXYB7VaEtRlo1wIkVkA5CzKHyuid0QAmu0qPYmezGM9kdY9pPAOc" crossorigin="anonymous"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
"schema": "doxa-maps-manifest@1",
|
"schema": "doxa-maps-manifest@1",
|
||||||
"generatedFrom": "plugin-designs/*/*.vue",
|
"generatedFrom": "plugin-designs/*/*.vue",
|
||||||
"embedBase": ".",
|
"embedBase": ".",
|
||||||
"bundleCount": 25,
|
"bundleCount": 24,
|
||||||
"profileCount": 37,
|
"profileCount": 36,
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"apiBaseUrl": "https://pray.doxa.life"
|
"apiBaseUrl": "https://pray.doxa.life"
|
||||||
},
|
},
|
||||||
|
|
@ -229,7 +229,7 @@
|
||||||
"bundle": "dashboard-plus-grid",
|
"bundle": "dashboard-plus-grid",
|
||||||
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
|
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
|
||||||
"output": "dx-maps/dashboard-plus-grid/dashboard-plus-grid.js",
|
"output": "dx-maps/dashboard-plus-grid/dashboard-plus-grid.js",
|
||||||
"integrity": "sha384-P15DN0neJhlrI9+vzXDozhY8RZHDsJkVnV3Yxun8ewNUNYFEyCdSETY1vBVu+6ee",
|
"integrity": "sha384-X3AEM/H4IPUfLH80IHRCRQHDmUwmuAAE+/ChGsH/3qdFV6KB7NhipFlWJtC3F/7t",
|
||||||
"elements": [
|
"elements": [
|
||||||
"dashboard-plus-grid"
|
"dashboard-plus-grid"
|
||||||
],
|
],
|
||||||
|
|
@ -252,7 +252,7 @@
|
||||||
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
|
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./dashboard-plus-grid/dashboard-plus-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./dashboard-plus-grid/dashboard-plus-grid/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=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-P15DN0neJhlrI9+vzXDozhY8RZHDsJkVnV3Yxun8ewNUNYFEyCdSETY1vBVu+6ee\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
|
"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=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-X3AEM/H4IPUfLH80IHRCRQHDmUwmuAAE+/ChGsH/3qdFV6KB7NhipFlWJtC3F/7t\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
|
||||||
},
|
},
|
||||||
"instances": [
|
"instances": [
|
||||||
{
|
{
|
||||||
|
|
@ -321,7 +321,7 @@
|
||||||
"bundle": "doxa-maps-spa",
|
"bundle": "doxa-maps-spa",
|
||||||
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
|
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
|
||||||
"output": "dx-maps/doxa-maps-spa/doxa-maps-spa.js",
|
"output": "dx-maps/doxa-maps-spa/doxa-maps-spa.js",
|
||||||
"integrity": "sha384-CKJT8BCWZubQKLenstpsz19Kd87akZiO1SGu5+Ma3byRUdyXz0gcjrtmmkQxzw7i",
|
"integrity": "sha384-AmW0CbWRRmUZJW0me3crwWEAS4XiX93Z7KM9Vpu5rBKYXgPOJYTf4LEVp8oDG4li",
|
||||||
"elements": [
|
"elements": [
|
||||||
"doxa-maps-spa"
|
"doxa-maps-spa"
|
||||||
],
|
],
|
||||||
|
|
@ -349,7 +349,7 @@
|
||||||
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
|
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./doxa-maps-spa/doxa-maps-spa/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./doxa-maps-spa/doxa-maps-spa/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=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-CKJT8BCWZubQKLenstpsz19Kd87akZiO1SGu5+Ma3byRUdyXz0gcjrtmmkQxzw7i\" crossorigin=\"anonymous\"></script>\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
|
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-AmW0CbWRRmUZJW0me3crwWEAS4XiX93Z7KM9Vpu5rBKYXgPOJYTf4LEVp8oDG4li\" crossorigin=\"anonymous\"></script>\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
|
||||||
},
|
},
|
||||||
"instances": [
|
"instances": [
|
||||||
{
|
{
|
||||||
|
|
@ -376,7 +376,7 @@
|
||||||
"bundle": "doxa-research-map",
|
"bundle": "doxa-research-map",
|
||||||
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
|
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
|
||||||
"output": "dx-maps/doxa-research-map/doxa-research-map.js",
|
"output": "dx-maps/doxa-research-map/doxa-research-map.js",
|
||||||
"integrity": "sha384-11TPej/kB1nQL0lXCw/gOjTmj/VznycM5fmIfsMNah74SMwH8i+NwFSgbr5zoDZs",
|
"integrity": "sha384-kC4KWUvDI3yN7HgjzUQVZTm2dYxTM0pRBlBxhFL9VQALYpLhmASkvjVthQ3dvbdC",
|
||||||
"elements": [
|
"elements": [
|
||||||
"doxa-research-map"
|
"doxa-research-map"
|
||||||
],
|
],
|
||||||
|
|
@ -395,7 +395,7 @@
|
||||||
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
|
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./doxa-research-map/research-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./doxa-research-map/research-map/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=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-11TPej/kB1nQL0lXCw/gOjTmj/VznycM5fmIfsMNah74SMwH8i+NwFSgbr5zoDZs\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
|
"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=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-kC4KWUvDI3yN7HgjzUQVZTm2dYxTM0pRBlBxhFL9VQALYpLhmASkvjVthQ3dvbdC\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
|
||||||
},
|
},
|
||||||
"instances": [
|
"instances": [
|
||||||
{
|
{
|
||||||
|
|
@ -737,7 +737,7 @@
|
||||||
"bundle": "gospel-resources",
|
"bundle": "gospel-resources",
|
||||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||||
"output": "dx-maps/gospel-resources/gospel-resources.js",
|
"output": "dx-maps/gospel-resources/gospel-resources.js",
|
||||||
"integrity": "sha384-lcwI2RPIjOWv3A0eY1Ej8qmWmdR0IYP3XKKFyZpwfE47av3KkP+f7D4ukhVw+qEe",
|
"integrity": "sha384-QVzftttPIYeK56Jo4vXx2mZ5lCxyapxR+YfayYABQ0ufom3AeWbw7cq8SpGSHeja",
|
||||||
"elements": [
|
"elements": [
|
||||||
"gospel-resources-map"
|
"gospel-resources-map"
|
||||||
],
|
],
|
||||||
|
|
@ -759,7 +759,7 @@
|
||||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./gospel-resources/charts/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./gospel-resources/charts/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-lcwI2RPIjOWv3A0eY1Ej8qmWmdR0IYP3XKKFyZpwfE47av3KkP+f7D4ukhVw+qEe\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-QVzftttPIYeK56Jo4vXx2mZ5lCxyapxR+YfayYABQ0ufom3AeWbw7cq8SpGSHeja\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -774,7 +774,7 @@
|
||||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./gospel-resources/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./gospel-resources/data-tables/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-lcwI2RPIjOWv3A0eY1Ej8qmWmdR0IYP3XKKFyZpwfE47av3KkP+f7D4ukhVw+qEe\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-QVzftttPIYeK56Jo4vXx2mZ5lCxyapxR+YfayYABQ0ufom3AeWbw7cq8SpGSHeja\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -790,7 +790,7 @@
|
||||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./gospel-resources/gospel-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./gospel-resources/gospel-dashboard/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-resources/gospel-resources.js\" integrity=\"sha384-lcwI2RPIjOWv3A0eY1Ej8qmWmdR0IYP3XKKFyZpwfE47av3KkP+f7D4ukhVw+qEe\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
"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-resources/gospel-resources.js\" integrity=\"sha384-QVzftttPIYeK56Jo4vXx2mZ5lCxyapxR+YfayYABQ0ufom3AeWbw7cq8SpGSHeja\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -805,7 +805,7 @@
|
||||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./gospel-resources/resources-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./gospel-resources/resources-list/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-lcwI2RPIjOWv3A0eY1Ej8qmWmdR0IYP3XKKFyZpwfE47av3KkP+f7D4ukhVw+qEe\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-QVzftttPIYeK56Jo4vXx2mZ5lCxyapxR+YfayYABQ0ufom3AeWbw7cq8SpGSHeja\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -821,7 +821,7 @@
|
||||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./gospel-resources/resources-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./gospel-resources/resources-map/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-resources/gospel-resources.js\" integrity=\"sha384-lcwI2RPIjOWv3A0eY1Ej8qmWmdR0IYP3XKKFyZpwfE47av3KkP+f7D4ukhVw+qEe\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
"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-resources/gospel-resources.js\" integrity=\"sha384-QVzftttPIYeK56Jo4vXx2mZ5lCxyapxR+YfayYABQ0ufom3AeWbw7cq8SpGSHeja\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -956,7 +956,7 @@
|
||||||
"bundle": "plugin-studio",
|
"bundle": "plugin-studio",
|
||||||
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
||||||
"output": "dx-maps/plugin-studio/plugin-studio.js",
|
"output": "dx-maps/plugin-studio/plugin-studio.js",
|
||||||
"integrity": "sha384-NsG6VNbCUAcOh9jEf5+8DeaKFm7xRySJO8yIUiE0O0AQ3FhuILru4eBHgaXCPQUD",
|
"integrity": "sha384-gC2dBaw7+rJERXYB7VaEtRlo1wIkVkA5CzKHyuid0QAmu0qPYmezGM9kdY9pPAOc",
|
||||||
"elements": [
|
"elements": [
|
||||||
"plugin-studio"
|
"plugin-studio"
|
||||||
],
|
],
|
||||||
|
|
@ -987,7 +987,7 @@
|
||||||
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
||||||
"embed": {
|
"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>",
|
"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-NsG6VNbCUAcOh9jEf5+8DeaKFm7xRySJO8yIUiE0O0AQ3FhuILru4eBHgaXCPQUD\" 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-gC2dBaw7+rJERXYB7VaEtRlo1wIkVkA5CzKHyuid0QAmu0qPYmezGM9kdY9pPAOc\" 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": [
|
"instances": [
|
||||||
{
|
{
|
||||||
|
|
@ -1163,7 +1163,7 @@
|
||||||
"bundle": "research-map-plus-grid",
|
"bundle": "research-map-plus-grid",
|
||||||
"scriptUrl": "./research-map-plus-grid/research-map-plus-grid.js",
|
"scriptUrl": "./research-map-plus-grid/research-map-plus-grid.js",
|
||||||
"output": "dx-maps/research-map-plus-grid/research-map-plus-grid.js",
|
"output": "dx-maps/research-map-plus-grid/research-map-plus-grid.js",
|
||||||
"integrity": "sha384-malo2pC/K0/1Xi9kyDubh+4tY9HC56zcO/VsgppNf5c4AXvXG9RMxqVHYoFAoOr5",
|
"integrity": "sha384-sIu4F0HzdbO4/0FF1bUGfe7xj/8bjMklygdB3/KmaG8bHHXa300y9nPDYg1bCouM",
|
||||||
"elements": [
|
"elements": [
|
||||||
"research-map-plus-grid"
|
"research-map-plus-grid"
|
||||||
],
|
],
|
||||||
|
|
@ -1186,7 +1186,7 @@
|
||||||
"scriptUrl": "./research-map-plus-grid/research-map-plus-grid.js",
|
"scriptUrl": "./research-map-plus-grid/research-map-plus-grid.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./research-map-plus-grid/research-map-plus-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./research-map-plus-grid/research-map-plus-grid/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=\"./research-map-plus-grid/research-map-plus-grid.js\" integrity=\"sha384-malo2pC/K0/1Xi9kyDubh+4tY9HC56zcO/VsgppNf5c4AXvXG9RMxqVHYoFAoOr5\" crossorigin=\"anonymous\"></script>\n<research-map-plus-grid profile-config='{\"profile\":\"research-map-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-map-plus-grid>"
|
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-map-plus-grid/research-map-plus-grid.js\" integrity=\"sha384-sIu4F0HzdbO4/0FF1bUGfe7xj/8bjMklygdB3/KmaG8bHHXa300y9nPDYg1bCouM\" crossorigin=\"anonymous\"></script>\n<research-map-plus-grid profile-config='{\"profile\":\"research-map-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-map-plus-grid>"
|
||||||
},
|
},
|
||||||
"instances": [
|
"instances": [
|
||||||
{
|
{
|
||||||
|
|
@ -1206,45 +1206,11 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"bundle": "rudder-voice",
|
|
||||||
"scriptUrl": "./rudder-voice/rudder-voice.js",
|
|
||||||
"output": "dx-maps/rudder-voice/rudder-voice.js",
|
|
||||||
"integrity": "sha384-a4oi+6HstA7vCZFrrX2qSEKq56mkDBJAkg485g8BRPeZb6C6QtJ+zxmjeBDg1/XT",
|
|
||||||
"elements": [
|
|
||||||
"rudder-voice"
|
|
||||||
],
|
|
||||||
"stage": "experimental",
|
|
||||||
"profiles": [
|
|
||||||
{
|
|
||||||
"profile": "rudder-voice",
|
|
||||||
"stage": "experimental",
|
|
||||||
"label": "Rudder — voice to text (desktop)",
|
|
||||||
"targets": [
|
|
||||||
"desktop"
|
|
||||||
],
|
|
||||||
"widget": {
|
|
||||||
"corner": "bottom-right"
|
|
||||||
},
|
|
||||||
"file": "rudder-voice.vue",
|
|
||||||
"description": "A DESKTOP-target plugin (rudder-voice.meta.json \"targets\": [\"desktop\"]). The speech",
|
|
||||||
"element": "rudder-voice",
|
|
||||||
"type": "single",
|
|
||||||
"isMap": false,
|
|
||||||
"iframeUrl": "./rudder-voice/rudder-voice/index.html",
|
|
||||||
"scriptUrl": "./rudder-voice/rudder-voice.js",
|
|
||||||
"embed": {
|
|
||||||
"iframe": "<iframe src=\"./rudder-voice/rudder-voice/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=\"./rudder-voice/rudder-voice.js\" integrity=\"sha384-a4oi+6HstA7vCZFrrX2qSEKq56mkDBJAkg485g8BRPeZb6C6QtJ+zxmjeBDg1/XT\" crossorigin=\"anonymous\"></script>\n<rudder-voice profile-config='{\"profile\":\"rudder-voice\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></rudder-voice>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"bundle": "stack-demo",
|
"bundle": "stack-demo",
|
||||||
"scriptUrl": "./stack-demo/stack-demo.js",
|
"scriptUrl": "./stack-demo/stack-demo.js",
|
||||||
"output": "dx-maps/stack-demo/stack-demo.js",
|
"output": "dx-maps/stack-demo/stack-demo.js",
|
||||||
"integrity": "sha384-Pg93Oyzao9WxHJMSJZPF798o1/Klk2ie7EDLTpVpQd0HaKCDDEHRDWUjt6OYwzqi",
|
"integrity": "sha384-zUkv1c/B+mQcflM2pwrczz+OpZxzl8JynKVV+5IyYiajtfQNU5RMJDJaF7nk/0oy",
|
||||||
"elements": [
|
"elements": [
|
||||||
"stack-demo"
|
"stack-demo"
|
||||||
],
|
],
|
||||||
|
|
@ -1267,7 +1233,7 @@
|
||||||
"scriptUrl": "./stack-demo/stack-demo.js",
|
"scriptUrl": "./stack-demo/stack-demo.js",
|
||||||
"embed": {
|
"embed": {
|
||||||
"iframe": "<iframe src=\"./stack-demo/stack-demo/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
"iframe": "<iframe src=\"./stack-demo/stack-demo/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=\"./stack-demo/stack-demo.js\" integrity=\"sha384-Pg93Oyzao9WxHJMSJZPF798o1/Klk2ie7EDLTpVpQd0HaKCDDEHRDWUjt6OYwzqi\" crossorigin=\"anonymous\"></script>\n<stack-demo profile-config='{\"profile\":\"stack-demo\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></stack-demo>"
|
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./stack-demo/stack-demo.js\" integrity=\"sha384-zUkv1c/B+mQcflM2pwrczz+OpZxzl8JynKVV+5IyYiajtfQNU5RMJDJaF7nk/0oy\" crossorigin=\"anonymous\"></script>\n<stack-demo profile-config='{\"profile\":\"stack-demo\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></stack-demo>"
|
||||||
},
|
},
|
||||||
"instances": [
|
"instances": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -32,7 +32,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 <plugin-studio> custom element. -->
|
<!-- Bundle IIFE — registers the <plugin-studio> custom element. -->
|
||||||
<script src="../plugin-studio.js" integrity="sha384-NsG6VNbCUAcOh9jEf5+8DeaKFm7xRySJO8yIUiE0O0AQ3FhuILru4eBHgaXCPQUD" crossorigin="anonymous"></script>
|
<script src="../plugin-studio.js" integrity="sha384-gC2dBaw7+rJERXYB7VaEtRlo1wIkVkA5CzKHyuid0QAmu0qPYmezGM9kdY9pPAOc" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,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 <plugin-studio> custom element. -->
|
<!-- Bundle IIFE — registers the <plugin-studio> custom element. -->
|
||||||
<script src="../../plugin-studio.js" integrity="sha384-NsG6VNbCUAcOh9jEf5+8DeaKFm7xRySJO8yIUiE0O0AQ3FhuILru4eBHgaXCPQUD" crossorigin="anonymous"></script>
|
<script src="../../plugin-studio.js" integrity="sha384-gC2dBaw7+rJERXYB7VaEtRlo1wIkVkA5CzKHyuid0QAmu0qPYmezGM9kdY9pPAOc" 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
|
|
@ -43,7 +43,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 <research-map-plus-grid> custom element. -->
|
<!-- Bundle IIFE — registers the <research-map-plus-grid> custom element. -->
|
||||||
<script src="../../research-map-plus-grid.js" integrity="sha384-malo2pC/K0/1Xi9kyDubh+4tY9HC56zcO/VsgppNf5c4AXvXG9RMxqVHYoFAoOr5" crossorigin="anonymous"></script>
|
<script src="../../research-map-plus-grid.js" integrity="sha384-sIu4F0HzdbO4/0FF1bUGfe7xj/8bjMklygdB3/KmaG8bHHXa300y9nPDYg1bCouM" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,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 <research-map-plus-grid> custom element. -->
|
<!-- Bundle IIFE — registers the <research-map-plus-grid> custom element. -->
|
||||||
<script src="../research-map-plus-grid.js" integrity="sha384-malo2pC/K0/1Xi9kyDubh+4tY9HC56zcO/VsgppNf5c4AXvXG9RMxqVHYoFAoOr5" crossorigin="anonymous"></script>
|
<script src="../research-map-plus-grid.js" integrity="sha384-sIu4F0HzdbO4/0FF1bUGfe7xj/8bjMklygdB3/KmaG8bHHXa300y9nPDYg1bCouM" 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
|
|
@ -1,214 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<!-- THIS PAGE IS OURS. The only thing that knows a document belongs to the plugin is the page
|
|
||||||
the generator writes, so it says so here. library/i18n reads it before reflecting an RTL
|
|
||||||
locale on <html dir>; nothing else sets it, so a PARTNER page defaults to "not ours" and
|
|
||||||
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
|
|
||||||
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
|
|
||||||
<script>window.MAP_OWNS_PAGE = true;</script>
|
|
||||||
<title>rudder-voice · rudder-voice</title>
|
|
||||||
<link rel="icon" href="data:,">
|
|
||||||
<!-- NON-MAP leaf (app-shell / composite / grid / card / chart-table): NO map peer.
|
|
||||||
This element draws no map tiles, so it needs no Mapbox/MapLibre engine and MUST NOT
|
|
||||||
gate on a token. Any nested MAP (in a composite/shell) renders in its own iframe leaf,
|
|
||||||
which loads its own map peer + token. A peer here would be dead weight + imply a gate. -->
|
|
||||||
<style>
|
|
||||||
/* BARE full-page map — no chrome. The custom element IS the page, so the
|
|
||||||
browser URL shares the map itself. */
|
|
||||||
html, body { height: 100%; margin: 0; background: #0a0e15; }
|
|
||||||
#slot, rudder-voice { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
|
|
||||||
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
|
|
||||||
padding: 24px; text-align: center; color: #9aa7b8;
|
|
||||||
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
||||||
#status.err { color: #f87171; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="slot"></div>
|
|
||||||
<div id="status">loading…</div>
|
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
|
||||||
<!-- Bundle IIFE — registers the <rudder-voice> custom element. -->
|
|
||||||
<script src="../rudder-voice.js" integrity="sha384-a4oi+6HstA7vCZFrrX2qSEKq56mkDBJAkg485g8BRPeZb6C6QtJ+zxmjeBDg1/XT" crossorigin="anonymous"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
|
||||||
// 1. prop — a token hardcoded into this page's profile-config (config.tk)
|
|
||||||
// 2. ?tk= URL param — token carried in the shared link
|
|
||||||
// 3. baked token — opt-in literal token inlined at build time (build-config.token)
|
|
||||||
// A token from rungs 1-3 is used DIRECTLY (no network). Otherwise a token
|
|
||||||
// *endpoint* is resolved and fetched:
|
|
||||||
// 4. window.MAP_TOKEN_URL override 5. configured TOKEN_SRC URL 6. same-origin /api/maps/token
|
|
||||||
// TOKEN_SRC is treated as DATA, never eval'd: the literal "window.MAP_TOKEN_URL"
|
|
||||||
// means "read that global"; anything else is used as a URL — so both a global
|
|
||||||
// name and a plain URL are safe (no SyntaxError footgun).
|
|
||||||
(function () {
|
|
||||||
var slot = document.getElementById('slot');
|
|
||||||
var status = document.getElementById('status');
|
|
||||||
var BASE_CONFIG = {"profile":"rudder-voice","dataSource":"pray-tools","instanceId":"rudder-voice-rudder-voice","assetsBase":"../../assets"};
|
|
||||||
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
|
|
||||||
// the ladder below is skipped entirely and the element mounts immediately.
|
|
||||||
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
|
|
||||||
var TOKEN_FREE = true;
|
|
||||||
// A token-free COMPOSITE still nests plugins, and a nested Mapbox map reads window.MAP_TOKEN
|
|
||||||
// rather than this page's profile-config. With no baked token (the public copy), it must get
|
|
||||||
// one from ?tk= or a pasted token, or be asked for one — see the rungs below.
|
|
||||||
var NESTS_MAPS = false;
|
|
||||||
var TOKEN_SRC = "window.MAP_TOKEN_URL";
|
|
||||||
var BAKED_TOKEN = "";
|
|
||||||
// Expose the baked token as window.MAP_TOKEN so a BAKED composite (a compiled element that mounts
|
|
||||||
// a map component directly, with no ProfileLoader/profile-config wrapper) still finds a token —
|
|
||||||
// useMapInstance falls back to window.MAP_TOKEN. No effect when BAKED_TOKEN is empty.
|
|
||||||
// A token the HOST set before this page ran outranks the baked one (measured: it used to lose).
|
|
||||||
var HOST_TOKEN = (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN) || '';
|
|
||||||
if (BAKED_TOKEN && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = BAKED_TOKEN; }
|
|
||||||
var DEFAULT_TOKEN_URL = '/api/maps/token';
|
|
||||||
|
|
||||||
function fail(msg) { if (status) { status.className = 'err'; status.textContent = '⚠ ' + msg; } }
|
|
||||||
// No token anywhere on the ladder → ask ONCE, store locally, remount. No dead ends.
|
|
||||||
function askToken() {
|
|
||||||
if (!status) return;
|
|
||||||
status.className = 'err';
|
|
||||||
status.innerHTML = 'map needs a Mapbox token — ' +
|
|
||||||
'<input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)" ' +
|
|
||||||
'style="font:12px ui-monospace,monospace;padding:6px 8px;border-radius:6px;border:1px solid #223047;background:#0e1623;color:#eef2f7;width:min(320px,70vw)"> ' +
|
|
||||||
'<button id="tkGo" style="font:700 11px ui-monospace,monospace;padding:7px 12px;border-radius:999px;border:0;cursor:pointer;background:linear-gradient(90deg,#46d4ff,#b9a6f5);color:#0a0e15">use it</button>';
|
|
||||||
var go = document.getElementById('tkGo');
|
|
||||||
if (go) go.addEventListener('click', function () {
|
|
||||||
var v = (document.getElementById('tkIn').value || '').trim();
|
|
||||||
if (!v) return;
|
|
||||||
try { localStorage.setItem('MAPS_STAGING_TK', v); } catch (e) {}
|
|
||||||
mount(v);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
/* ★ RELEASE THE LOAD BACKDROP WHEN THE ELEMENT MOUNTS (BUG-GOSPEL-MAP-BLACK-RECTANGLE).
|
|
||||||
html/body carry background:#0a0e15 so the light #status text (#9aa7b8) is readable while the
|
|
||||||
bundle loads. That backdrop is LOAD-STATE CHROME, but it outlived its moment: any gap the
|
|
||||||
mounted element does not cover shows it through, because every layer above body is
|
|
||||||
transparent. On gospel-resources/resources-map that produced a 352x126 DARK RECTANGLE between
|
|
||||||
the legend card and the detail panel — MEASURED rgb(8,12,18), i.e. the page background, NOT an
|
|
||||||
unpainted canvas as first supposed. Clearing it here (the one place we already know the
|
|
||||||
element is up) is a one-shot style write — no resize, no repaint loop. Keep it a one-shot
|
|
||||||
write: do NOT turn this into a per-frame or per-event handler (a resize loop strobes the screen). */
|
|
||||||
function done() {
|
|
||||||
if (status && status.parentNode) { status.parentNode.removeChild(status); }
|
|
||||||
document.documentElement.style.background = '#f3f3f1';
|
|
||||||
document.body.style.background = '#f3f3f1';
|
|
||||||
}
|
|
||||||
|
|
||||||
function mount(token) {
|
|
||||||
if (!token && !TOKEN_FREE) { return askToken(); }
|
|
||||||
// Nested maps (a composite's children) read window.MAP_TOKEN, not this element's config.
|
|
||||||
if (token && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = token; }
|
|
||||||
try {
|
|
||||||
var cfg = token ? Object.assign({}, BASE_CONFIG, { tk: token }) : BASE_CONFIG;
|
|
||||||
var el = document.createElement("rudder-voice");
|
|
||||||
el.setAttribute('profile-config', JSON.stringify(cfg));
|
|
||||||
slot.innerHTML = '';
|
|
||||||
slot.appendChild(el);
|
|
||||||
done();
|
|
||||||
} catch (e) { fail(e && e.message ? e.message : String(e)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
function qp(name) {
|
|
||||||
try { return new URLSearchParams(location.search).get(name) || ''; }
|
|
||||||
catch (e) { return ''; }
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractToken(data) {
|
|
||||||
if (typeof data === 'string') return data.trim();
|
|
||||||
if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || '';
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rungs 1-3b: an actual token → mount directly, no fetch.
|
|
||||||
// 2b = localStorage: the paste-once store shared with the staging pages
|
|
||||||
// (MAPS_STAGING_TK) — paste a token ONCE anywhere in the tree, every page uses it.
|
|
||||||
var stored = '';
|
|
||||||
try { stored = localStorage.getItem('MAPS_STAGING_TK') || ''; } catch (e) {}
|
|
||||||
var urlTk = qp('tk');
|
|
||||||
if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk); } catch (e) {} }
|
|
||||||
var directToken = (BASE_CONFIG && BASE_CONFIG.tk) || urlTk || stored || HOST_TOKEN || BAKED_TOKEN;
|
|
||||||
// PROVIDER — the same ladder as resolveMapProvider (library/composables/useMapProvider.js):
|
|
||||||
// profile-config.provider → ?provider= → window.MAP_PROVIDER → 'auto'. An explicit engine wins.
|
|
||||||
// 'auto' = Mapbox when a token is available (a rung above, a host-set window.MAP_TOKEN, or a token
|
|
||||||
// endpoint to ask), otherwise MapLibre, keyless: no token prompt, no request to Mapbox. The
|
|
||||||
// element decides the same way from the token this page hands it, so the two cannot disagree.
|
|
||||||
var ASKED = String((BASE_CONFIG && BASE_CONFIG.provider) || qp('provider') || window.MAP_PROVIDER || '').toLowerCase().trim();
|
|
||||||
var EXPLICIT = (ASKED === 'mapbox' || ASKED === 'maplibre') ? ASKED : '';
|
|
||||||
var HAS_TOKEN = !!(directToken || (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN));
|
|
||||||
var HAS_ENDPOINT = !!(window.MAP_TOKEN_URL || (TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL'));
|
|
||||||
if (EXPLICIT === 'maplibre' || (!EXPLICIT && !HAS_TOKEN && !HAS_ENDPOINT)) { TOKEN_FREE = true; }
|
|
||||||
if (TOKEN_FREE) {
|
|
||||||
if (directToken && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = directToken; }
|
|
||||||
mount('');
|
|
||||||
// A token-free composite with no token anywhere: its content renders, but its nested maps
|
|
||||||
// cannot. Ask once; the token is stored for the whole tree and the page reloads with it.
|
|
||||||
// Only when Mapbox was ASKED for: under 'auto' the nested maps draw with MapLibre instead.
|
|
||||||
if (NESTS_MAPS && !directToken && EXPLICIT === 'mapbox') {
|
|
||||||
var bar = document.createElement('div');
|
|
||||||
bar.id = 'status';
|
|
||||||
bar.setAttribute('style', 'position:fixed;left:12px;right:12px;bottom:12px;z-index:2147483000;padding:10px 12px;border-radius:10px;background:#0e1623;color:#eef2f7;font:13px system-ui,sans-serif;box-shadow:0 6px 24px rgba(0,0,0,.25)');
|
|
||||||
document.body.appendChild(bar);
|
|
||||||
status = bar;
|
|
||||||
askToken();
|
|
||||||
var goBtn = document.getElementById('tkGo');
|
|
||||||
if (goBtn) goBtn.addEventListener('click', function () {
|
|
||||||
var v = (document.getElementById('tkIn').value || '').trim();
|
|
||||||
if (v) { location.reload(); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (directToken) { return mount(directToken); }
|
|
||||||
|
|
||||||
// Rungs 4-6: resolve the token ENDPOINT candidates and try them IN ORDER — the first
|
|
||||||
// one that answers with a token wins. Order: window.MAP_TOKEN_URL override → the
|
|
||||||
// configured TOKEN_SRC (an ABSOLUTE url, so a tree hosted on a CDN away from the API
|
|
||||||
// still finds it) → same-origin /api/maps/token (dev, or the site hosting both).
|
|
||||||
// A single hard-coded pick used to kill the page when its one url 404'd or was
|
|
||||||
// blocked by CORS; a shared link must work wherever the tree is hosted.
|
|
||||||
var candidates = [];
|
|
||||||
function addCandidate(u) { if (u && candidates.indexOf(u) === -1) candidates.push(u); }
|
|
||||||
addCandidate(window.MAP_TOKEN_URL);
|
|
||||||
addCandidate(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '');
|
|
||||||
addCandidate(DEFAULT_TOKEN_URL);
|
|
||||||
|
|
||||||
function fetchToken(i, errors) {
|
|
||||||
if (i >= candidates.length) {
|
|
||||||
// Every endpoint failed. Under 'auto' that means no token: draw with MapLibre, keyless.
|
|
||||||
if (!EXPLICIT) { TOKEN_FREE = true; return mount(''); }
|
|
||||||
// Mapbox was asked for: say which endpoints failed, and still offer the paste-once box.
|
|
||||||
askToken();
|
|
||||||
if (status) {
|
|
||||||
var d = document.createElement('div');
|
|
||||||
d.textContent = '⚠ Token fetch failed from ' + candidates.join(', ') + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + errors.join(' | ') + ')';
|
|
||||||
status.insertBefore(d, status.firstChild);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var url = candidates[i];
|
|
||||||
return fetch(url)
|
|
||||||
.then(function (r) {
|
|
||||||
if (!r.ok) throw new Error(url + ' → HTTP ' + r.status);
|
|
||||||
var ct = r.headers.get('content-type') || '';
|
|
||||||
return ct.indexOf('json') !== -1 ? r.json() : r.text();
|
|
||||||
})
|
|
||||||
.then(function (data) {
|
|
||||||
var tk = extractToken(data);
|
|
||||||
if (!tk) throw new Error(url + ' → no token in response');
|
|
||||||
mount(tk);
|
|
||||||
})
|
|
||||||
.catch(function (e) {
|
|
||||||
errors.push(e && e.message ? e.message : String(e));
|
|
||||||
return fetchToken(i + 1, errors);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fetchToken(0, []);
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -43,7 +43,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 <stack-demo> custom element. -->
|
<!-- Bundle IIFE — registers the <stack-demo> custom element. -->
|
||||||
<script src="../stack-demo.js" integrity="sha384-Pg93Oyzao9WxHJMSJZPF798o1/Klk2ie7EDLTpVpQd0HaKCDDEHRDWUjt6OYwzqi" crossorigin="anonymous"></script>
|
<script src="../stack-demo.js" integrity="sha384-zUkv1c/B+mQcflM2pwrczz+OpZxzl8JynKVV+5IyYiajtfQNU5RMJDJaF7nk/0oy" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,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 <stack-demo> custom element. -->
|
<!-- Bundle IIFE — registers the <stack-demo> custom element. -->
|
||||||
<script src="../../stack-demo.js" integrity="sha384-Pg93Oyzao9WxHJMSJZPF798o1/Klk2ie7EDLTpVpQd0HaKCDDEHRDWUjt6OYwzqi" crossorigin="anonymous"></script>
|
<script src="../../stack-demo.js" integrity="sha384-zUkv1c/B+mQcflM2pwrczz+OpZxzl8JynKVV+5IyYiajtfQNU5RMJDJaF7nk/0oy" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Capabilities and targets
|
# Capabilities and targets
|
||||||
|
|
||||||
Two separate questions about a plugin: **how composed is it** (capability 1–5) and **where can it
|
Two separate questions about a plugin: **how composed is it** (capability 1–5) and **where can it
|
||||||
run** (`targets`). Only the first is enforced.
|
run** (`targets`). Both are enforced: `check:capability-derived` and `check:targets`.
|
||||||
|
|
||||||
## Capability 1–5
|
## Capability 1–5
|
||||||
|
|
||||||
|
|
@ -52,14 +52,24 @@ Worth knowing because they will bite you the same way:
|
||||||
A `<profile>.meta.json` may carry `"targets"`, naming where the plugin is meant to run:
|
A `<profile>.meta.json` may carry `"targets"`, naming where the plugin is meant to run:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "type": "single", "label": "Rudder", "targets": ["desktop"], "backend": "backend/rudder-engine" }
|
{ "type": "single", "label": "Rudder", "targets": ["desktop", "widget"], "widget": { "corner": "bottom-right" }, "backend": "backend/rudder-engine" }
|
||||||
```
|
```
|
||||||
|
|
||||||
**Omitting it means "everywhere", and that is the right default for almost every plugin.** The
|
**Omitting it means "everywhere", and that is the right default for almost every plugin.** The
|
||||||
whole point of the build is one folder that runs as a web embed, a mobile app and a desktop app.
|
whole point of the build is one folder that runs as a web embed, a mobile app and a desktop app.
|
||||||
|
|
||||||
|
Known values: **`web`** (a published map or tool), **`mobile`**, **`desktop`** (a desktop app, `bun run desktop:build`),
|
||||||
|
**`widget`** (the Plugin Studio's corner; needs a `"widget": { "corner" }`). `internal/plugin-targets.mjs`
|
||||||
|
holds the one reading every tool shares, and `bun run check:targets` rejects an unknown value, an empty list,
|
||||||
|
a widget target without a corner (or a corner without the target), and a `backend` naming a missing folder.
|
||||||
|
|
||||||
**Declaring it has a real consequence**, so declare it only when you mean it:
|
**Declaring it has a real consequence**, so declare it only when you mean it:
|
||||||
|
|
||||||
|
- **A plugin whose targets leave out `web` is not built for the web at all.** `bun run build` (and the
|
||||||
|
`/cdn` export) skip it, `generate-tree` writes no leaf page or manifest entry for it, and `check:targets`
|
||||||
|
fails if one appears in `public/`. The dev server still discovers it from source, so a `widget` plugin
|
||||||
|
keeps its Studio corner on `bun run dev`.
|
||||||
|
|
||||||
- `internal/profile-meta.mjs:28` reads the field (string entries only) and `:36` carries it into
|
- `internal/profile-meta.mjs:28` reads the field (string entries only) and `:36` carries it into
|
||||||
the manifest, so every consumer of the manifest sees it.
|
the manifest, so every consumer of the manifest sees it.
|
||||||
- **The Research Gallery skips a bundle whose every profile is a widget or lacks the `web`
|
- **The Research Gallery skips a bundle whose every profile is a widget or lacks the `web`
|
||||||
|
|
@ -90,7 +100,7 @@ The three outputs, all from the same built folder:
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Web folder | `public/dx-maps/`, embeddable by iframe or script tag from any host | copy the folder; snippets in `manifest.json` |
|
| Web folder | `public/dx-maps/`, embeddable by iframe or script tag from any host | copy the folder; snippets in `manifest.json` |
|
||||||
| Mobile app | the same folder wrapped by Capacitor (`capacitor.config.json` `webDir`) | `bun run phone` |
|
| Mobile app | the same folder wrapped by Capacitor (`capacitor.config.json` `webDir`) | `bun run phone` |
|
||||||
| Desktop app | the same folder in Tauri (`deployment/src-tauri/`), a proof of concept | `bun run tauri:build` |
|
| Desktop app | ONE plugin whose targets include `desktop`, in Tauri (`deployment/src-tauri/`), with its `backend` as a sidecar | `bun run desktop:build <plugin>` (see `documentation/HOWTO-build-the-desktop-app.md`) |
|
||||||
|
|
||||||
WordPress (`deployment/wordpress/`) embeds the web folder's snippets; it is experimental.
|
WordPress (`deployment/wordpress/`) embeds the web folder's snippets; it is experimental.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue