merge main (studio landing 17 carried the Mark 5 sources)
This commit is contained in:
commit
082dbb8c18
108 changed files with 3317 additions and 485 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -145,3 +145,6 @@ data-private/
|
|||
/public/assets/
|
||||
/public/__gallery/
|
||||
/public/index.html
|
||||
|
||||
# Local plugin backends (backend/<name>/) — the compiled binaries are rebuilt from source, never committed.
|
||||
/backend/bin/
|
||||
|
|
|
|||
456
backend/rudder-engine/devices.go
Normal file
456
backend/rudder-engine/devices.go
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
// devices.go — microphone discovery, hot-plug, and the sound-server environment.
|
||||
//
|
||||
// The Jitsi Meet pattern (lib-jitsi-meet modules/RTC/RTCUtils.js), done natively because Rudder records
|
||||
// outside the browser:
|
||||
//
|
||||
// Jitsi (browser) Rudder (native, PipeWire/PulseAudio)
|
||||
// navigator.mediaDevices.enumerateDevices() → pactl -f json list sources
|
||||
// 'devicechange' event + polling fallback → pactl subscribe (+ re-list on every source/card event)
|
||||
// compareAvailableMediaDevices(new) → sameDevices(old, new) — only emit on a real change
|
||||
// getUserMedia({audio:{deviceId:{exact:id}}}) → parec --device=<id>
|
||||
//
|
||||
// Jitsi also reports the device list to analytics (sendDeviceListToAnalytics). Rudder never does:
|
||||
// the list goes to the local plugin over SSE and nowhere else.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// audioEnv gives child processes a path to the user's sound server even when the engine was launched
|
||||
// from a context without a desktop session (a detached shell, a service, an agent's terminal). Measured
|
||||
// 2026-09-27: started with no XDG_RUNTIME_DIR, parec exited instantly with "Connection refused" and every
|
||||
// recording captured nothing, although the socket sat at /run/user/<uid>/pulse/native all along.
|
||||
func audioEnv() []string {
|
||||
env := os.Environ()
|
||||
rt := os.Getenv("XDG_RUNTIME_DIR")
|
||||
if rt == "" {
|
||||
rt = fmt.Sprintf("/run/user/%d", os.Getuid())
|
||||
if st, err := os.Stat(rt); err == nil && st.IsDir() {
|
||||
env = append(env, "XDG_RUNTIME_DIR="+rt)
|
||||
}
|
||||
}
|
||||
// systemctl --user (a headset-reset command may restart PipeWire) needs the session bus.
|
||||
if os.Getenv("DBUS_SESSION_BUS_ADDRESS") == "" {
|
||||
if _, err := os.Stat(rt + "/bus"); err == nil {
|
||||
env = append(env, "DBUS_SESSION_BUS_ADDRESS=unix:path="+rt+"/bus")
|
||||
}
|
||||
}
|
||||
if os.Getenv("PULSE_SERVER") == "" {
|
||||
if _, err := os.Stat(rt + "/pulse/native"); err == nil {
|
||||
env = append(env, "PULSE_SERVER=unix:"+rt+"/pulse/native")
|
||||
}
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
// Device is one microphone as the plugin shows it.
|
||||
type Device struct {
|
||||
ID string `json:"id"` // the sound server's source name: what parec --device takes
|
||||
Label string `json:"label"` // human name ("My Headset", "Built-in Audio Analog Stereo")
|
||||
Bus string `json:"bus"` // bluetooth | usb | pci | ...
|
||||
Default bool `json:"default"` // the system's default input
|
||||
Muted bool `json:"muted"`
|
||||
Volume string `json:"volume"` // e.g. "52%"
|
||||
State string `json:"state"` // RUNNING | IDLE | SUSPENDED
|
||||
}
|
||||
|
||||
// DeviceList is the whole picture: the mics, plus sound cards that have a mic but are switched off
|
||||
// (the laptop's built-in card, profile "off", is the common case — its mic simply doesn't exist until
|
||||
// the card is turned on in Sound settings).
|
||||
type DeviceList struct {
|
||||
Devices []Device `json:"devices"`
|
||||
Default string `json:"default"`
|
||||
OffCards []string `json:"offCards"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func runPactl(args ...string) ([]byte, error) {
|
||||
cmd := exec.Command("pactl", args...)
|
||||
cmd.Env = audioEnv()
|
||||
return cmd.Output()
|
||||
}
|
||||
|
||||
func listDevices() DeviceList {
|
||||
out := DeviceList{Devices: []Device{}, OffCards: []string{}}
|
||||
if _, err := exec.LookPath("pactl"); err != nil {
|
||||
out.Error = "pactl not found: install pipewire-pulse or pulseaudio-utils to list microphones"
|
||||
return out
|
||||
}
|
||||
if b, err := runPactl("get-default-source"); err == nil {
|
||||
out.Default = strings.TrimSpace(string(b))
|
||||
}
|
||||
b, err := runPactl("-f", "json", "list", "sources")
|
||||
if err != nil {
|
||||
out.Error = "cannot reach the sound server (pactl list sources failed: " + err.Error() + ")"
|
||||
return out
|
||||
}
|
||||
var raw []struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
State string `json:"state"`
|
||||
Mute bool `json:"mute"`
|
||||
Volume map[string]any `json:"volume"`
|
||||
Properties map[string]any `json:"properties"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &raw); err != nil {
|
||||
out.Error = "unexpected pactl output: " + err.Error()
|
||||
return out
|
||||
}
|
||||
for _, s := range raw {
|
||||
// A ".monitor" source is the loopback of a SPEAKER, not a microphone: listing it would let the
|
||||
// user "record" their own music. Hidden, as Jitsi lists only audioinput kinds.
|
||||
if strings.HasSuffix(s.Name, ".monitor") || s.Properties["device.class"] == "monitor" {
|
||||
continue
|
||||
}
|
||||
d := Device{ID: s.Name, Label: s.Description, State: s.State, Muted: s.Mute, Default: s.Name == out.Default}
|
||||
if bus, ok := s.Properties["device.bus"].(string); ok {
|
||||
d.Bus = bus
|
||||
}
|
||||
for _, ch := range s.Volume { // first channel is representative
|
||||
if m, ok := ch.(map[string]any); ok {
|
||||
if p, ok := m["value_percent"].(string); ok {
|
||||
d.Volume = p
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
out.Devices = append(out.Devices, d)
|
||||
}
|
||||
// Cards whose active profile is "off" but which offer an input profile.
|
||||
if b, err := runPactl("-f", "json", "list", "cards"); err == nil {
|
||||
var cards []struct {
|
||||
Name string `json:"name"`
|
||||
ActiveProfile string `json:"active_profile"`
|
||||
Properties map[string]any `json:"properties"`
|
||||
Profiles map[string]struct {
|
||||
Sources int `json:"sources"`
|
||||
Available bool `json:"available"`
|
||||
} `json:"profiles"`
|
||||
}
|
||||
if json.Unmarshal(b, &cards) == nil {
|
||||
for _, c := range cards {
|
||||
if c.ActiveProfile != "off" {
|
||||
continue
|
||||
}
|
||||
for _, p := range c.Profiles {
|
||||
if p.Sources > 0 && p.Available {
|
||||
label, _ := c.Properties["device.description"].(string)
|
||||
if label == "" {
|
||||
label = c.Name
|
||||
}
|
||||
out.OffCards = append(out.OffCards, label)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// sameDevices mirrors Jitsi's compareAvailableMediaDevices: emit only when something a user would
|
||||
// notice changed (a mic came, went, was muted, or the default moved), not on every state flicker.
|
||||
func sameDevices(a, b DeviceList) bool {
|
||||
if a.Default != b.Default || len(a.Devices) != len(b.Devices) || a.Error != b.Error || len(a.OffCards) != len(b.OffCards) {
|
||||
return false
|
||||
}
|
||||
for i := range a.Devices {
|
||||
x, y := a.Devices[i], b.Devices[i]
|
||||
if x.ID != y.ID || x.Label != y.Label || x.Muted != y.Muted || x.Volume != y.Volume {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// deviceWatcher keeps the current list and pushes a "devices" event on every real change. It listens
|
||||
// to `pactl subscribe` (the native 'devicechange') and, like Jitsi, also polls as a fallback in case
|
||||
// the subscription dies.
|
||||
type deviceWatcher struct {
|
||||
mu sync.Mutex
|
||||
last DeviceList
|
||||
hub *hub
|
||||
logs *ring
|
||||
}
|
||||
|
||||
func (w *deviceWatcher) current() DeviceList {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.last
|
||||
}
|
||||
|
||||
func (w *deviceWatcher) refresh(reason string) {
|
||||
now := listDevices()
|
||||
w.mu.Lock()
|
||||
changed := !sameDevices(w.last, now)
|
||||
w.last = now
|
||||
w.mu.Unlock()
|
||||
if changed {
|
||||
w.logs.add("microphones (%s): %d found, default=%s %s", reason, len(now.Devices), now.Default, now.Error)
|
||||
w.hub.send(event{"type": "devices", "devices": now})
|
||||
}
|
||||
}
|
||||
|
||||
func (w *deviceWatcher) run() {
|
||||
w.refresh("startup")
|
||||
// Poll fallback (Jitsi polls enumerateDevices when devicechange is unavailable).
|
||||
go func() {
|
||||
for range time.Tick(5 * time.Second) {
|
||||
w.refresh("poll")
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
cmd := exec.Command("pactl", "subscribe")
|
||||
cmd.Env = audioEnv()
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL}
|
||||
out, err := cmd.StdoutPipe()
|
||||
if err == nil && cmd.Start() == nil {
|
||||
sc := bufio.NewScanner(out)
|
||||
var debounce *time.Timer
|
||||
for sc.Scan() {
|
||||
l := sc.Text()
|
||||
if strings.Contains(l, " card ") || (strings.Contains(l, " source ") && !strings.Contains(l, "source-output")) {
|
||||
w.logs.add("sound-server event: %s", l)
|
||||
}
|
||||
if strings.Contains(l, " source ") || strings.Contains(l, " card ") || strings.Contains(l, " server ") {
|
||||
if debounce != nil {
|
||||
debounce.Stop()
|
||||
}
|
||||
debounce = time.AfterFunc(300*time.Millisecond, func() { w.refresh("hot-plug") })
|
||||
}
|
||||
}
|
||||
_ = cmd.Wait()
|
||||
}
|
||||
time.Sleep(3 * time.Second) // sound server restarted or unreachable: retry
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// micTest records ~3 s from one device WITHOUT transcribing and says, in plain words, whether the mic
|
||||
// works — Rudder's version of Jitsi's pre-join audio preview. Level events stream to the plugin while
|
||||
// it runs so the meter moves.
|
||||
func (e *engine) micTest(device string) event {
|
||||
cmds := recorderCmds(device)
|
||||
var cmd *exec.Cmd
|
||||
for _, c := range cmds {
|
||||
if _, err := exec.LookPath(c[0]); err == nil {
|
||||
cmd = exec.Command(c[0], c[1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if cmd == nil {
|
||||
return event{"ok": false, "verdict": "No recorder found (need parec, pw-record or arecord)."}
|
||||
}
|
||||
cmd.Env = audioEnv()
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL}
|
||||
var errBuf strings.Builder
|
||||
cmd.Stderr = &errBuf
|
||||
out, err := cmd.StdoutPipe()
|
||||
if err != nil || cmd.Start() != nil {
|
||||
return event{"ok": false, "verdict": "Could not start the recorder."}
|
||||
}
|
||||
defer func() { _ = cmd.Process.Kill(); _ = cmd.Wait() }()
|
||||
|
||||
frame := make([]byte, 3200) // 100 ms
|
||||
peak, frames, zeroFrames := 0, 0, 0
|
||||
var sumSq float64
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
n, err := io.ReadFull(out, frame)
|
||||
if err != nil {
|
||||
why := strings.TrimSpace(errBuf.String())
|
||||
e.logs.add("mic test on %q failed after %d frames: %v | %s", device, frames, err, why)
|
||||
return event{"ok": false, "verdict": "The microphone produced no audio. " + why, "recorder": cmd.Path}
|
||||
}
|
||||
fp := 0
|
||||
for i := 0; i+1 < n; i += 2 {
|
||||
v := int(int16(uint16(frame[i]) | uint16(frame[i+1])<<8))
|
||||
sumSq += float64(v * v)
|
||||
if v < 0 {
|
||||
v = -v
|
||||
}
|
||||
if v > fp {
|
||||
fp = v
|
||||
}
|
||||
}
|
||||
if fp == 0 {
|
||||
zeroFrames++
|
||||
}
|
||||
if fp > peak {
|
||||
peak = fp
|
||||
}
|
||||
frames++
|
||||
e.hub.send(event{"type": "level", "peak": float64(fp) / 32768, "test": true})
|
||||
}
|
||||
rms := 0.0
|
||||
if frames > 0 {
|
||||
rms = math.Sqrt(sumSq/float64(frames*1600)) / 32768
|
||||
}
|
||||
res := event{"ok": true, "peak": float64(peak) / 32768, "rms": rms, "frames": frames}
|
||||
switch {
|
||||
case zeroFrames == frames:
|
||||
res["ok"] = false
|
||||
res["verdict"] = "Pure silence — the mic is connected but sends nothing. It may be muted, or a Bluetooth headset's mic link may have dropped (try reconnecting the headset or pick another mic)."
|
||||
case peak < 330: // below about -40 dBFS
|
||||
res["verdict"] = "Very quiet — Rudder can hear the mic, but barely. Speak closer or raise its input volume."
|
||||
default:
|
||||
res["verdict"] = "Working — Rudder can hear you."
|
||||
}
|
||||
e.logs.add("mic test on %q: peak=%d rms=%.4f frames=%d verdict=%s", device, peak, rms, frames, res["verdict"])
|
||||
return res
|
||||
}
|
||||
|
||||
// ─── Optional Bluetooth headset reset ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Some Bluetooth headsets drop the mic side of the HFP link (exact-zero audio while everything looks
|
||||
// connected; the kernel logs `hci0: corrupted SCO packet`). Re-opening the recorder does not revive
|
||||
// it; a full reset (disconnect → restart PipeWire/WirePlumber → reconnect) can. That is machine-specific,
|
||||
// so Rudder only runs one if RUDDER_BT_RESET names an executable that does it (exit 0 = headset works
|
||||
// again). Rate-limited: never more than once per 20 s.
|
||||
func btResetPath() string {
|
||||
p := os.Getenv("RUDDER_BT_RESET")
|
||||
if p == "" {
|
||||
return ""
|
||||
}
|
||||
if st, err := os.Stat(p); err == nil && !st.IsDir() && st.Mode()&0o111 != 0 {
|
||||
return p
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var (
|
||||
resetMu sync.Mutex
|
||||
lastReset time.Time
|
||||
)
|
||||
|
||||
// runHeadsetReset runs the RUDDER_BT_RESET command and reports whether it said the headset works again.
|
||||
func runHeadsetReset(logs *ring) (ok bool, ran bool) {
|
||||
path := btResetPath()
|
||||
if path == "" {
|
||||
return false, false
|
||||
}
|
||||
resetMu.Lock()
|
||||
if time.Since(lastReset) < 20*time.Second {
|
||||
resetMu.Unlock()
|
||||
logs.add("headset reset skipped: last one was %.0fs ago (rate limit 20s)", time.Since(lastReset).Seconds())
|
||||
return false, false
|
||||
}
|
||||
lastReset = time.Now()
|
||||
resetMu.Unlock()
|
||||
|
||||
logs.add("running %s (headset reset + CVSD)", path)
|
||||
cmd := exec.Command(path)
|
||||
cmd.Env = audioEnv()
|
||||
done := make(chan struct{})
|
||||
var out []byte
|
||||
var err error
|
||||
go func() { out, err = cmd.CombinedOutput(); close(done) }()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(60 * time.Second):
|
||||
_ = cmd.Process.Kill()
|
||||
logs.add("headset reset timed out after 60s")
|
||||
return false, true
|
||||
}
|
||||
for _, l := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
if l != "" {
|
||||
logs.add("[headset reset] %s", l)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
logs.add("headset reset failed: %v", err)
|
||||
return false, true
|
||||
}
|
||||
return true, true
|
||||
}
|
||||
|
||||
// ─── Bluetooth voice-profile helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// btCard maps a Bluetooth mic (bluez_input.<MAC>.<n>, or "" = the default input) to its card.
|
||||
func btCard(device string) string {
|
||||
if device == "" {
|
||||
if b, err := runPactl("get-default-source"); err == nil {
|
||||
device = strings.TrimSpace(string(b))
|
||||
}
|
||||
}
|
||||
if !strings.HasPrefix(device, "bluez_input.") {
|
||||
return ""
|
||||
}
|
||||
mac := strings.TrimPrefix(device, "bluez_input.")
|
||||
if i := strings.LastIndex(mac, "."); i > 0 {
|
||||
mac = mac[:i]
|
||||
}
|
||||
return "bluez_card." + mac
|
||||
}
|
||||
|
||||
func currentProfile(card string) string {
|
||||
if card == "" {
|
||||
return "(not bluetooth)"
|
||||
}
|
||||
b, err := runPactl("-f", "json", "list", "cards")
|
||||
if err != nil {
|
||||
return "?"
|
||||
}
|
||||
var cards []struct {
|
||||
Name string `json:"name"`
|
||||
ActiveProfile string `json:"active_profile"`
|
||||
}
|
||||
if json.Unmarshal(b, &cards) != nil {
|
||||
return "?"
|
||||
}
|
||||
for _, c := range cards {
|
||||
if c.Name == card {
|
||||
return c.ActiveProfile
|
||||
}
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
|
||||
// switchProfile forces a fresh voice link: CVSD if the card is on anything else, otherwise a bounce
|
||||
// through mSBC and back to CVSD (a same-profile set is a no-op and would not renegotiate the link).
|
||||
func switchProfile(card string, logs *ring) error {
|
||||
before := currentProfile(card)
|
||||
target := "headset-head-unit-cvsd"
|
||||
if before == target {
|
||||
if _, err := runPactl("set-card-profile", card, "headset-head-unit-msbc"); err != nil {
|
||||
logs.add("profile bounce via msbc failed: %v", err)
|
||||
}
|
||||
time.Sleep(700 * time.Millisecond)
|
||||
}
|
||||
if out, err := runPactl("set-card-profile", card, target); err != nil {
|
||||
return fmt.Errorf("pactl set-card-profile %s %s: %v %s", card, target, err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
time.Sleep(1500 * time.Millisecond) // let the new SCO link and the source come up
|
||||
logs.add("voice profile %s → %s", before, currentProfile(card))
|
||||
return nil
|
||||
}
|
||||
|
||||
// watchKernelBluetooth copies kernel Bluetooth messages into the log while a session runs
|
||||
// (`journalctl -k` is readable without root on this system).
|
||||
func watchKernelBluetooth(ctx context.Context, logs *ring) {
|
||||
cmd := exec.CommandContext(ctx, "journalctl", "-k", "-f", "-n", "0", "-o", "cat")
|
||||
out, err := cmd.StdoutPipe()
|
||||
if err != nil || cmd.Start() != nil {
|
||||
return
|
||||
}
|
||||
sc := bufio.NewScanner(out)
|
||||
for sc.Scan() {
|
||||
if l := sc.Text(); strings.Contains(strings.ToLower(l), "bluetooth") {
|
||||
logs.add("kernel: %s", l)
|
||||
}
|
||||
}
|
||||
_ = cmd.Wait()
|
||||
}
|
||||
5
backend/rudder-engine/go.mod
Normal file
5
backend/rudder-engine/go.mod
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module dx-maps/backend/rudder-engine
|
||||
|
||||
go 1.22.2
|
||||
|
||||
require github.com/gorilla/websocket v1.5.3
|
||||
2
backend/rudder-engine/go.sum
Normal file
2
backend/rudder-engine/go.sum
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
797
backend/rudder-engine/main.go
Normal file
797
backend/rudder-engine/main.go
Normal file
|
|
@ -0,0 +1,797 @@
|
|||
// rudder-engine — the local speech backend for the rudder-voice plugin.
|
||||
//
|
||||
// WHY THIS EXISTS: rudder-voice is a DESKTOP-target plugin. Speech-to-text runs on the user's own
|
||||
// machine, never in the browser and never on a server. In dev, `bun run dev` starts this process and
|
||||
// Vite forwards /api/rudder/* to it; in the desktop app, Tauri runs it as a sidecar. Same binary,
|
||||
// same API, so "works in the Plugin Studio" means "works on desktop".
|
||||
//
|
||||
// browser / webview ──HTTP+SSE──► rudder-engine (127.0.0.1:RUDDER_PORT)
|
||||
// ├─ recorder: parec → pw-record → arecord (mic open ONLY while recording)
|
||||
// └─ WebSocket ──► nemo-speech serve (127.0.0.1:RUDDER_NEMO_PORT)
|
||||
// NVIDIA NeMo-Speech.cpp + Nemotron 3.5 ASR GGUF
|
||||
//
|
||||
// NO TELEMETRY, NO NETWORK: the runtime is NVIDIA NeMo-Speech.cpp (ggml). ONNX-based runtimes were
|
||||
// REJECTED: measured 2026-09-26, the tested ONNX GenAI runtime looked up a vendor telemetry host even
|
||||
// with telemetry "disabled". NeMo-Speech.cpp was traced with strace: zero sockets, zero DNS.
|
||||
// Its only network path is an optional model download that shells out to `curl` when the model is not
|
||||
// a local file — so this engine REFUSES to start unless RUDDER_MODEL is an existing local file.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// ─── Config (env, with local defaults) ──────────────────────────────────────────────────────────
|
||||
|
||||
type config struct {
|
||||
Port string // this engine's HTTP port
|
||||
NemoPort string // the warm NeMo-Speech server's port (loopback only)
|
||||
NemoBin string // path to the nemo-speech executable
|
||||
Model string // local GGUF path — MUST exist (see the no-network note above)
|
||||
Language string // default language code
|
||||
EndpointMs int // silence that ends an utterance
|
||||
}
|
||||
|
||||
func envOr(k, d string) string {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
return v
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
home, _ := os.UserHomeDir()
|
||||
base := filepath.Join(home, ".local", "share", "rudder")
|
||||
return config{
|
||||
Port: envOr("RUDDER_PORT", "5193"),
|
||||
NemoPort: envOr("RUDDER_NEMO_PORT", "5194"),
|
||||
NemoBin: envOr("RUDDER_NEMO_BIN", filepath.Join(base, "bin", "nemo-speech-0.1.0-linux-x86_64-cpu", "bin", "nemo-speech")),
|
||||
Model: envOr("RUDDER_MODEL", filepath.Join(base, "models", "nemotron-3.5-asr-streaming-0.6b.q8_0.gguf")),
|
||||
Language: envOr("RUDDER_LANG", "en-US"),
|
||||
EndpointMs: 800,
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Log ring: every line is also served at /api/rudder/log so the Debug panel can copy it ──────
|
||||
|
||||
type ring struct {
|
||||
mu sync.Mutex
|
||||
lines []string
|
||||
}
|
||||
|
||||
// ★ PRIVACY: the log is what people paste to an agent (🐞 Debug → copy report), so it must never carry
|
||||
// anything that identifies them. Every line is redacted as it is written: device MAC addresses (inside
|
||||
// Bluetooth device names like bluez_input.AA_BB_…) and the home folder (which contains the username).
|
||||
// Transcribed words are never logged at all.
|
||||
var (
|
||||
macRe = regexp.MustCompile(`(?i)\b([0-9a-f]{2}[:_]){5}[0-9a-f]{2}\b`)
|
||||
homeDir = func() string { h, _ := os.UserHomeDir(); return h }()
|
||||
)
|
||||
|
||||
func redact(s string) string {
|
||||
s = macRe.ReplaceAllString(s, "XX:XX:XX:XX:XX:XX")
|
||||
if homeDir != "" && homeDir != "/" {
|
||||
s = strings.ReplaceAll(s, homeDir, "~")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (r *ring) add(format string, a ...any) {
|
||||
line := redact(time.Now().Format("15:04:05.000") + " " + fmt.Sprintf(format, a...))
|
||||
log.Println(line)
|
||||
r.mu.Lock()
|
||||
r.lines = append(r.lines, line)
|
||||
if len(r.lines) > 3000 {
|
||||
r.lines = r.lines[len(r.lines)-3000:]
|
||||
}
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *ring) text() string {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return strings.Join(r.lines, "\n")
|
||||
}
|
||||
|
||||
// ─── Event hub: Server-Sent Events to every open plugin ─────────────────────────────────────────
|
||||
|
||||
type event map[string]any
|
||||
|
||||
type hub struct {
|
||||
mu sync.Mutex
|
||||
clients map[chan []byte]struct{}
|
||||
}
|
||||
|
||||
func (h *hub) subscribe() chan []byte {
|
||||
c := make(chan []byte, 64)
|
||||
h.mu.Lock()
|
||||
h.clients[c] = struct{}{}
|
||||
h.mu.Unlock()
|
||||
return c
|
||||
}
|
||||
|
||||
func (h *hub) unsubscribe(c chan []byte) {
|
||||
h.mu.Lock()
|
||||
delete(h.clients, c)
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
func (h *hub) send(e event) {
|
||||
b, _ := json.Marshal(e)
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
for c := range h.clients {
|
||||
select {
|
||||
case c <- b:
|
||||
default: // a slow client drops an event rather than stalling the audio path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Engine: supervises nemo-speech serve and one recording session at a time ───────────────────
|
||||
|
||||
type engine struct {
|
||||
cfg config
|
||||
logs *ring
|
||||
hub *hub
|
||||
|
||||
mu sync.Mutex
|
||||
state string // starting | ready | error
|
||||
stateErr string
|
||||
nemo *exec.Cmd
|
||||
recording bool
|
||||
recorder string
|
||||
language string
|
||||
stopSession func()
|
||||
device string
|
||||
devs *deviceWatcher
|
||||
nemoKey string
|
||||
}
|
||||
|
||||
func (e *engine) setState(s, errText string) {
|
||||
e.mu.Lock()
|
||||
e.state, e.stateErr = s, errText
|
||||
e.mu.Unlock()
|
||||
e.hub.send(event{"type": "status", "engine": s, "error": errText})
|
||||
}
|
||||
|
||||
func (e *engine) status() event {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
return event{
|
||||
"engine": e.state, "error": e.stateErr, "recording": e.recording, "recorder": e.recorder,
|
||||
"language": e.language, "defaultLanguage": e.cfg.Language, "device": e.device,
|
||||
"runtime": "NVIDIA NeMo-Speech.cpp (ggml, CPU)", "model": filepath.Base(e.cfg.Model),
|
||||
}
|
||||
}
|
||||
|
||||
// startNemo launches the warm speech server. The model loads and warms up once (~20 s on CPU),
|
||||
// so starting and stopping a recording is instant afterwards.
|
||||
func (e *engine) startNemo() {
|
||||
if st, err := os.Stat(e.cfg.Model); err != nil || st.IsDir() {
|
||||
e.setState("error", "model file not found: "+e.cfg.Model+" (refusing to start: a non-local model would make the runtime download it)")
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(e.cfg.NemoBin); err != nil {
|
||||
e.setState("error", "nemo-speech runtime not found: "+e.cfg.NemoBin)
|
||||
return
|
||||
}
|
||||
e.setState("starting", "")
|
||||
// A fresh random key each start: the speech server answers only this engine, not any other local
|
||||
// program or web page that finds its port.
|
||||
cmd := exec.Command(e.cfg.NemoBin, "serve",
|
||||
"--host", "127.0.0.1", "--port", e.cfg.NemoPort,
|
||||
"--asr-model", e.cfg.Model, "--device", "cpu", "--no-ui", "--api-key", e.nemoKey)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL} // dies with us
|
||||
out, _ := cmd.StdoutPipe()
|
||||
cmd.Stderr = cmd.Stdout
|
||||
if err := cmd.Start(); err != nil {
|
||||
e.setState("error", "could not start nemo-speech: "+err.Error())
|
||||
return
|
||||
}
|
||||
e.mu.Lock()
|
||||
e.nemo = cmd
|
||||
e.mu.Unlock()
|
||||
e.logs.add("nemo-speech serve started (pid %d) on 127.0.0.1:%s", cmd.Process.Pid, e.cfg.NemoPort)
|
||||
go func() {
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := out.Read(buf)
|
||||
for _, l := range strings.Split(strings.TrimSpace(string(buf[:n])), "\n") {
|
||||
if l != "" {
|
||||
e.logs.add("[nemo] %s", l)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
e.setState("error", fmt.Sprintf("nemo-speech exited: %v", err))
|
||||
}()
|
||||
// Ready = the model list answers.
|
||||
go func() {
|
||||
url := "http://127.0.0.1:" + e.cfg.NemoPort + "/v1/models"
|
||||
for i := 0; i < 600; i++ {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+e.nemoKey)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == 200 {
|
||||
e.logs.add("nemo-speech ready after %.1fs", float64(i+1)*0.5)
|
||||
e.setState("ready", "")
|
||||
return
|
||||
}
|
||||
}
|
||||
e.mu.Lock()
|
||||
s := e.state
|
||||
e.mu.Unlock()
|
||||
if s == "error" {
|
||||
return
|
||||
}
|
||||
}
|
||||
e.setState("error", "nemo-speech did not become ready within 5 minutes")
|
||||
}()
|
||||
}
|
||||
|
||||
// recorderCmds: raw s16le 16 kHz mono PCM on stdout. parec first — on PipeWire/Bluetooth desktops it
|
||||
// talks to the sound server directly (the old Rudder's hardest-won lesson: PortAudio/ALSA-only
|
||||
// capture stalls on Bluetooth headsets).
|
||||
// RUDDER_RECORDER overrides the list with one space-separated command (e.g. a test that replays a
|
||||
// PCM file), so the engine can be tested end to end without a microphone.
|
||||
// device is a sound-server source name from /api/rudder/devices; empty = the system default input.
|
||||
func recorderCmds(device string) [][]string {
|
||||
if v := os.Getenv("RUDDER_RECORDER"); v != "" {
|
||||
return [][]string{strings.Fields(v)}
|
||||
}
|
||||
parec := []string{"parec", "--format=s16le", "--rate=16000", "--channels=1", "--raw", "--latency-msec=100"}
|
||||
pw := []string{"pw-record", "--format=s16", "--rate=16000", "--channels=1"}
|
||||
if device != "" {
|
||||
parec = append(parec, "--device="+device)
|
||||
pw = append(pw, "--target="+device)
|
||||
}
|
||||
pw = append(pw, "-")
|
||||
cmds := [][]string{parec, pw}
|
||||
if device == "" { // arecord can't address a sound-server source by name
|
||||
cmds = append(cmds, []string{"arecord", "-q", "-f", "S16_LE", "-r", "16000", "-c", "1", "-t", "raw"})
|
||||
}
|
||||
return cmds
|
||||
}
|
||||
|
||||
func (e *engine) start(lang, device string) error {
|
||||
e.mu.Lock()
|
||||
if e.state != "ready" {
|
||||
e.mu.Unlock()
|
||||
return fmt.Errorf("engine is %s — wait for it to be ready", e.state)
|
||||
}
|
||||
if e.recording {
|
||||
e.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
if lang == "" {
|
||||
lang = e.cfg.Language
|
||||
}
|
||||
e.mu.Unlock()
|
||||
|
||||
ws, _, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:"+e.cfg.NemoPort+"/v1/audio/transcriptions/realtime",
|
||||
http.Header{"Authorization": {"Bearer " + e.nemoKey}})
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to speech server: %w", err)
|
||||
}
|
||||
upd, _ := json.Marshal(event{"type": "session.update", "session": event{
|
||||
"sample_rate": 16000, "language": lang, "automatic_punctuation": true, "endpointing_ms": e.cfg.EndpointMs,
|
||||
}})
|
||||
if err := ws.WriteMessage(websocket.TextMessage, upd); err != nil {
|
||||
ws.Close()
|
||||
return fmt.Errorf("session.update: %w", err)
|
||||
}
|
||||
|
||||
// Bluetooth headsets start on CVSD, not mSBC. Measured 2026-09-27 on a Bluetooth headset: on mSBC the
|
||||
// mic link died 3-5 s into EVERY session that had speech (kernel: corrupted SCO packets); after
|
||||
// switching to CVSD it ran 9.5 minutes without a drop. CVSD is 8 kHz, and Nemotron's transcript of an
|
||||
// 8 kHz copy of the test clip differed by one word. RUDDER_KEEP_MSBC=1 opts out.
|
||||
if card := btCard(device); card != "" && os.Getenv("RUDDER_KEEP_MSBC") == "" {
|
||||
if p := currentProfile(card); p == "headset-head-unit-msbc" {
|
||||
e.logs.add("bluetooth headset on mSBC — switching to CVSD before recording (stable voice link)")
|
||||
e.hub.send(event{"type": "recovering", "message": "Switching your Bluetooth headset to its stable voice mode…"})
|
||||
if err := switchProfile(card, e.logs); err != nil {
|
||||
e.logs.add("pre-switch to CVSD failed: %v", err)
|
||||
} else {
|
||||
e.hub.send(event{"type": "recovered", "message": "Headset is in its stable voice mode — go ahead and talk."})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
first, err := spawnRecorder(device, e.logs)
|
||||
if err != nil {
|
||||
ws.Close()
|
||||
return err
|
||||
}
|
||||
recName := first.name
|
||||
var recMu sync.Mutex
|
||||
cur := first // the live recorder; replaced when a dead mic link is recovered
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var writeMu sync.Mutex
|
||||
done := make(chan struct{})
|
||||
|
||||
e.mu.Lock()
|
||||
e.recording, e.recorder, e.language, e.device = true, recName, lang, device
|
||||
e.stopSession = func() {
|
||||
cancel()
|
||||
recMu.Lock()
|
||||
cur.kill()
|
||||
recMu.Unlock()
|
||||
writeMu.Lock()
|
||||
_ = ws.WriteMessage(websocket.TextMessage, []byte(`{"type":"input_audio_buffer.commit"}`))
|
||||
writeMu.Unlock()
|
||||
select { // let the final words arrive
|
||||
case <-done:
|
||||
case <-time.After(4 * time.Second):
|
||||
}
|
||||
ws.Close()
|
||||
}
|
||||
e.mu.Unlock()
|
||||
e.logs.add("recording started: recorder=%s device=%q language=%s profile=%s", recName, device, lang, currentProfile(btCard(device)))
|
||||
go watchKernelBluetooth(ctx, e.logs)
|
||||
e.hub.send(event{"type": "recording", "recording": true, "recorder": recName, "language": lang, "device": device})
|
||||
for _, d := range e.devs.current().Devices {
|
||||
if (d.ID == device || (device == "" && d.Default)) && d.Muted {
|
||||
e.hub.send(event{"type": "warning", "message": "Your microphone \"" + d.Label + "\" is muted in the system sound settings — unmute it or nothing will be heard."})
|
||||
}
|
||||
}
|
||||
|
||||
// Audio pump: 100 ms frames → speech server, plus a level meter and dead-mic RECOVERY.
|
||||
go func() {
|
||||
frame := make([]byte, 3200)
|
||||
var silentSince time.Time
|
||||
recoveries := 0
|
||||
var recovering atomic.Bool
|
||||
var resetting atomic.Bool
|
||||
tracePeak, lastTrace := 0, time.Now()
|
||||
var healthySince time.Time // a mic that stays healthy 30 s earns back its 3 reconnects
|
||||
gaveUp := false
|
||||
lastLevel := time.Time{}
|
||||
for {
|
||||
recMu.Lock()
|
||||
r := cur
|
||||
recMu.Unlock()
|
||||
if _, err := io.ReadFull(r.out, frame); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
recMu.Lock()
|
||||
swapped := cur != r
|
||||
recMu.Unlock()
|
||||
if swapped { // we killed it ourselves during a recovery: read from the new one
|
||||
continue
|
||||
}
|
||||
// A headset reset restarts PipeWire, which kills parec mid-read. That is expected:
|
||||
// wait for the reset to hand over a fresh recorder instead of ending the session.
|
||||
if resetting.Load() {
|
||||
for resetting.Load() && ctx.Err() == nil {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// The recorder died on its own (bad device, sound server unreachable, device unplugged).
|
||||
// End the session so the UI stops showing "Listening", and say WHY in plain words.
|
||||
_ = r.cmd.Wait()
|
||||
why := strings.TrimSpace(r.errBuf.String())
|
||||
e.logs.add("recorder %s ended unexpectedly: %v | stderr: %s", r.name, err, why)
|
||||
msg := "The microphone stopped delivering audio."
|
||||
switch {
|
||||
case strings.Contains(why, "Connection refused") || strings.Contains(why, "pa_context_connect"):
|
||||
msg = "Can't reach the computer's sound system (PipeWire/PulseAudio). Is it running?"
|
||||
case strings.Contains(why, "No such entity") || strings.Contains(why, "not found"):
|
||||
msg = "That microphone isn't available any more — it may have been unplugged or disconnected. Pick another one."
|
||||
}
|
||||
if why != "" {
|
||||
msg += " (" + r.name + ": " + why + ")"
|
||||
}
|
||||
e.hub.send(event{"type": "error", "message": msg})
|
||||
go e.stop()
|
||||
return
|
||||
}
|
||||
peak := 0
|
||||
for i := 0; i+1 < len(frame); i += 2 {
|
||||
v := int(int16(binary.LittleEndian.Uint16(frame[i:])))
|
||||
if v < 0 {
|
||||
v = -v
|
||||
}
|
||||
if v > peak {
|
||||
peak = v
|
||||
}
|
||||
}
|
||||
// ★ DEAD MIC LINK. Exact-zero samples are not quiet speech (a live mic always has a noise
|
||||
// floor). Measured 2026-09-27 on a Bluetooth headset (HFP, mSBC): four words
|
||||
// transcribed, then literal zeros while parec stayed "healthy" — the old Rudder's BUG 1,
|
||||
// the headset losing the mic side of the link. Re-opening the source restarts the audio
|
||||
// stream; the speech session and the transcript carry on untouched.
|
||||
if peak == 0 {
|
||||
if silentSince.IsZero() {
|
||||
silentSince = time.Now()
|
||||
} else if !gaveUp && !resetting.Load() && time.Since(silentSince) > 1500*time.Millisecond {
|
||||
recoveries++
|
||||
switch {
|
||||
case recoveries == 1:
|
||||
// Step 1: reopen the recorder (cheap; fixes a stalled stream).
|
||||
e.logs.add("mic link dead (exact zeros 1.5s) — step 1: reopening the microphone")
|
||||
e.hub.send(event{"type": "recovering", "attempt": recoveries, "message": "Microphone went silent — reconnecting it…"})
|
||||
if next, err := spawnRecorder(device, e.logs); err == nil {
|
||||
recMu.Lock()
|
||||
old := cur
|
||||
cur = next
|
||||
recMu.Unlock()
|
||||
old.kill()
|
||||
recovering.Store(true)
|
||||
}
|
||||
case recoveries == 2 && btCard(device) != "":
|
||||
// Step 2: force a NEW Bluetooth voice link by switching the headset's voice profile to
|
||||
// CVSD (the old Rudder's proven-stabler choice). Unlike a detect-first reset script this does not
|
||||
// "detect first", so it can't decide a dead link is fine.
|
||||
card := btCard(device)
|
||||
e.logs.add("mic still dead — step 2: forcing a new voice link: %s → headset-head-unit-cvsd", card)
|
||||
e.hub.send(event{"type": "recovering", "attempt": recoveries, "message": "Microphone still silent — switching your Bluetooth headset to its more stable voice mode…"})
|
||||
resetting.Store(true)
|
||||
go func() {
|
||||
defer resetting.Store(false)
|
||||
if err := switchProfile(card, e.logs); err != nil {
|
||||
e.hub.send(event{"type": "warning", "message": "Couldn't switch the headset's voice mode: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if next, err := spawnRecorder(device, e.logs); err == nil {
|
||||
recMu.Lock()
|
||||
old := cur
|
||||
cur = next
|
||||
recMu.Unlock()
|
||||
old.kill()
|
||||
recovering.Store(true)
|
||||
}
|
||||
}()
|
||||
case recoveries == 3 && btResetPath() != "":
|
||||
// Step 3: full headset reset (disconnect → restart PipeWire → reconnect).
|
||||
e.logs.add("mic still dead — step 3: full headset reset via RUDDER_BT_RESET")
|
||||
e.hub.send(event{"type": "recovering", "attempt": recoveries, "message": "Still silent — fully resetting your Bluetooth headset (about 15 seconds)…"})
|
||||
resetting.Store(true)
|
||||
go func() {
|
||||
defer resetting.Store(false)
|
||||
ok, ran := runHeadsetReset(e.logs)
|
||||
if !ran {
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
e.hub.send(event{"type": "warning", "message": "The headset reset didn't bring the mic back — is the headset on and in range?"})
|
||||
}
|
||||
if next, err := spawnRecorder(device, e.logs); err == nil {
|
||||
recMu.Lock()
|
||||
old := cur
|
||||
cur = next
|
||||
recMu.Unlock()
|
||||
old.kill()
|
||||
recovering.Store(true)
|
||||
}
|
||||
}()
|
||||
default:
|
||||
gaveUp = true
|
||||
e.logs.add("mic still dead after reopen + headset resets — giving up for this session")
|
||||
msg := "Your microphone keeps going silent. This is the Bluetooth headset dropping its mic link (the kernel reports corrupted voice packets), not Rudder. Try a wired or built-in mic, or turn off the headset's noise cancellation / adaptive audio in its app."
|
||||
if btResetPath() == "" {
|
||||
msg += " (No headset-reset command is configured — see RUDDER_BT_RESET.)"
|
||||
}
|
||||
e.hub.send(event{"type": "warning", "message": msg})
|
||||
}
|
||||
silentSince = time.Now()
|
||||
}
|
||||
} else {
|
||||
if recovering.Load() {
|
||||
recovering.Store(false)
|
||||
healthySince = time.Now()
|
||||
e.logs.add("mic recovered after %d reconnect(s)", recoveries)
|
||||
e.hub.send(event{"type": "recovered", "message": "Microphone is back — keep talking."})
|
||||
}
|
||||
if recoveries > 0 && !healthySince.IsZero() && time.Since(healthySince) > 30*time.Second {
|
||||
recoveries, gaveUp, healthySince = 0, false, time.Time{}
|
||||
}
|
||||
silentSince = time.Time{}
|
||||
}
|
||||
// Flight recorder: the mic level twice a second, so a drop can be lined up against kernel and
|
||||
// sound-server events in the same log (/api/rudder/log).
|
||||
tracePeak = max(tracePeak, peak)
|
||||
if time.Since(lastTrace) >= 500*time.Millisecond {
|
||||
e.logs.add("trace level peak=%d", tracePeak)
|
||||
tracePeak, lastTrace = 0, time.Now()
|
||||
}
|
||||
if time.Since(lastLevel) > 150*time.Millisecond {
|
||||
lastLevel = time.Now()
|
||||
e.hub.send(event{"type": "level", "peak": float64(peak) / 32768})
|
||||
}
|
||||
writeMu.Lock()
|
||||
err := ws.WriteMessage(websocket.BinaryMessage, frame)
|
||||
writeMu.Unlock()
|
||||
if err != nil {
|
||||
if ctx.Err() == nil {
|
||||
e.hub.send(event{"type": "error", "message": "speech server connection lost: " + err.Error()})
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Result reader: map the server's events onto the plugin's small vocabulary.
|
||||
go func() {
|
||||
defer close(done)
|
||||
for {
|
||||
_, msg, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var m map[string]any
|
||||
if json.Unmarshal(msg, &m) != nil {
|
||||
continue
|
||||
}
|
||||
t, _ := m["type"].(string)
|
||||
text := firstString(m, "transcript", "delta", "text")
|
||||
switch {
|
||||
case strings.HasSuffix(t, "input_audio_transcription.delta"):
|
||||
// Deltas APPEND to the utterance in progress; the server sends many empty ones.
|
||||
if text != "" {
|
||||
e.hub.send(event{"type": "partial", "text": text})
|
||||
}
|
||||
case strings.HasSuffix(t, "input_audio_transcription.completed"):
|
||||
e.hub.send(event{"type": "final", "text": text, "at": time.Now().Format(time.RFC3339)})
|
||||
case t == "error":
|
||||
e.logs.add("speech server error: %s", string(msg))
|
||||
e.hub.send(event{"type": "error", "message": string(msg)})
|
||||
case t == "input_audio_buffer.committed":
|
||||
return
|
||||
default:
|
||||
e.logs.add("speech event: %s", t)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// recorderProc is one running recorder process.
|
||||
type recorderProc struct {
|
||||
cmd *exec.Cmd
|
||||
out io.ReadCloser
|
||||
name string
|
||||
errBuf *bytes.Buffer
|
||||
}
|
||||
|
||||
func (r *recorderProc) kill() {
|
||||
if r != nil && r.cmd != nil && r.cmd.Process != nil {
|
||||
_ = r.cmd.Process.Kill()
|
||||
go func() { _ = r.cmd.Wait() }()
|
||||
}
|
||||
}
|
||||
|
||||
// spawnRecorder starts the first recorder that works for this device.
|
||||
func spawnRecorder(device string, logs *ring) (*recorderProc, error) {
|
||||
for _, c := range recorderCmds(device) {
|
||||
if _, err := exec.LookPath(c[0]); err != nil {
|
||||
continue
|
||||
}
|
||||
cmd := exec.Command(c[0], c[1:]...)
|
||||
cmd.Env = audioEnv()
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL}
|
||||
errBuf := &bytes.Buffer{}
|
||||
cmd.Stderr = errBuf
|
||||
out, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
logs.add("recorder %s failed to start: %v", c[0], err)
|
||||
continue
|
||||
}
|
||||
return &recorderProc{cmd: cmd, out: out, name: c[0], errBuf: errBuf}, nil
|
||||
}
|
||||
return nil, errors.New("no microphone recorder found (need parec, pw-record or arecord)")
|
||||
}
|
||||
|
||||
func firstString(m map[string]any, keys ...string) string {
|
||||
for _, k := range keys {
|
||||
if s, ok := m[k].(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
if item, ok := m["item"].(map[string]any); ok {
|
||||
return firstString(item, keys...)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (e *engine) stop() {
|
||||
e.mu.Lock()
|
||||
stop := e.stopSession
|
||||
e.stopSession = nil
|
||||
was := e.recording
|
||||
e.recording = false
|
||||
e.mu.Unlock()
|
||||
if !was || stop == nil {
|
||||
return
|
||||
}
|
||||
stop()
|
||||
e.logs.add("recording stopped")
|
||||
e.hub.send(event{"type": "recording", "recording": false})
|
||||
}
|
||||
|
||||
func (e *engine) shutdown() {
|
||||
e.stop()
|
||||
e.mu.Lock()
|
||||
n := e.nemo
|
||||
e.mu.Unlock()
|
||||
if n != nil && n.Process != nil {
|
||||
_ = n.Process.Kill()
|
||||
}
|
||||
}
|
||||
|
||||
// ─── HTTP API ────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
func writeJSON(w http.ResponseWriter, code int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
// guard: two protections for a server that streams what you say.
|
||||
// - Host check (DNS rebinding): a web page on any domain can make the browser resolve its own hostname
|
||||
// to 127.0.0.1 and then read this API as "same-origin". Only requests addressed to 127.0.0.1 or
|
||||
// localhost are served.
|
||||
// - CORS: only the desktop webview's origin (tauri://localhost, or http://tauri.localhost on Windows)
|
||||
// may read responses cross-origin. The Plugin Studio needs none: it reaches /api/rudder through the
|
||||
// dev server's same-origin proxy. RUDDER_ALLOW_ORIGIN adds one more exact origin (tests).
|
||||
func guard(next http.Handler) http.Handler {
|
||||
extra := os.Getenv("RUDDER_ALLOW_ORIGIN")
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
host := r.Host
|
||||
if i := strings.LastIndex(host, ":"); i >= 0 && !strings.HasSuffix(host, "]") {
|
||||
host = host[:i]
|
||||
}
|
||||
if host != "127.0.0.1" && host != "localhost" && host != "[::1]" {
|
||||
http.Error(w, "forbidden host", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
o := r.Header.Get("Origin")
|
||||
if o == "tauri://localhost" || o == "http://tauri.localhost" || (extra != "" && o == extra) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", o)
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Vary", "Origin")
|
||||
}
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
logs := &ring{}
|
||||
h := &hub{clients: map[chan []byte]struct{}{}}
|
||||
key := make([]byte, 24)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
log.Fatal("no randomness for the speech-server key: ", err)
|
||||
}
|
||||
e := &engine{cfg: cfg, logs: logs, hub: h, state: "starting", devs: &deviceWatcher{hub: h, logs: logs}, nemoKey: hex.EncodeToString(key)}
|
||||
e.devs.run()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/api/rudder/status", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, 200, e.status())
|
||||
})
|
||||
mux.HandleFunc("/api/rudder/start", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeJSON(w, 405, event{"error": "POST only"})
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Language string `json:"language"`
|
||||
Device string `json:"device"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||
if err := e.start(body.Language, body.Device); err != nil {
|
||||
e.logs.add("start failed: %v", err)
|
||||
writeJSON(w, 409, event{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, e.status())
|
||||
})
|
||||
mux.HandleFunc("/api/rudder/stop", func(w http.ResponseWriter, r *http.Request) {
|
||||
e.stop()
|
||||
writeJSON(w, 200, e.status())
|
||||
})
|
||||
mux.HandleFunc("/api/rudder/devices", func(w http.ResponseWriter, r *http.Request) {
|
||||
e.devs.refresh("request")
|
||||
writeJSON(w, 200, e.devs.current())
|
||||
})
|
||||
mux.HandleFunc("/api/rudder/test", func(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Device string `json:"device"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||
writeJSON(w, 200, e.micTest(body.Device))
|
||||
})
|
||||
mux.HandleFunc("/api/rudder/log", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
_, _ = io.WriteString(w, logs.text())
|
||||
})
|
||||
mux.HandleFunc("/api/rudder/events", func(w http.ResponseWriter, r *http.Request) {
|
||||
fl, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "streaming unsupported", 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
c := e.hub.subscribe()
|
||||
defer e.hub.unsubscribe(c)
|
||||
first, _ := json.Marshal(event{"type": "status", "status": e.status()})
|
||||
fmt.Fprintf(w, "data: %s\n\n", first)
|
||||
devs, _ := json.Marshal(event{"type": "devices", "devices": e.devs.current()})
|
||||
fmt.Fprintf(w, "data: %s\n\n", devs)
|
||||
fl.Flush()
|
||||
ping := time.NewTicker(15 * time.Second)
|
||||
defer ping.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case b := <-c:
|
||||
fmt.Fprintf(w, "data: %s\n\n", b)
|
||||
fl.Flush()
|
||||
case <-ping.C:
|
||||
fmt.Fprint(w, ": ping\n\n")
|
||||
fl.Flush()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
srv := &http.Server{Addr: "127.0.0.1:" + cfg.Port, Handler: guard(mux)}
|
||||
logs.add("rudder-engine listening on http://127.0.0.1:%s (runtime=%s model=%s)", cfg.Port, cfg.NemoBin, cfg.Model)
|
||||
e.startNemo()
|
||||
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sig
|
||||
e.shutdown()
|
||||
os.Exit(0)
|
||||
}()
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
e.shutdown()
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
59
backend/rudder-engine/setup-runtime.sh
Executable file
59
backend/rudder-engine/setup-runtime.sh
Executable file
|
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env bash
|
||||
# setup-runtime.sh — fetch Rudder's speech runtime + model, verified by SHA-256, then build the engine.
|
||||
#
|
||||
# Rudder (plugin-designs/rudder-voice) transcribes on THIS computer. It needs two things that are too big
|
||||
# for git: NVIDIA's NeMo-Speech.cpp runtime (~4 MB, CPU build) and the Nemotron 3.5 ASR Streaming GGUF
|
||||
# (~742 MB). Both are pinned by version and checksum; a mismatch aborts instead of running an unverified
|
||||
# file. After this, `bun run dev` starts the engine and Rudder appears in the Plugin Studio.
|
||||
#
|
||||
# bash backend/rudder-engine/setup-runtime.sh # installs into ~/.local/share/rudder
|
||||
# RUDDER_HOME=/some/dir bash backend/rudder-engine/setup-runtime.sh
|
||||
#
|
||||
# Linux x86_64 only for now (the runtime ships other platforms; add them when a desktop build needs them).
|
||||
set -euo pipefail
|
||||
|
||||
RUDDER_HOME="${RUDDER_HOME:-$HOME/.local/share/rudder}"
|
||||
RT_VER="0.1.0"
|
||||
RT_NAME="nemo-speech-${RT_VER}-linux-x86_64-cpu"
|
||||
RT_URL="https://github.com/NVIDIA/NeMo-Speech.cpp/releases/download/v${RT_VER}/${RT_NAME}.tar.gz"
|
||||
RT_SHA="0f74131d631ad2c694cf0ec53490866bb6461147959589a69fb6fc231944065b"
|
||||
# NVIDIA re-converted the GGUF on 2026-09-10 (embedded tokenizer); this revision is the one Rudder is tested with.
|
||||
MODEL_REV="ea30d66debe3"
|
||||
MODEL_FILE="nemotron-3.5-asr-streaming-0.6b.q8_0.gguf"
|
||||
MODEL_URL="https://huggingface.co/nvidia/nemotron-3.5-asr-streaming-0.6b/resolve/${MODEL_REV}/${MODEL_FILE}"
|
||||
MODEL_SHA="3fc991d3badad7277c11030a7519832cddaf2057aafed6d4b25147e953a070b1"
|
||||
|
||||
say() { printf '\033[1;34m→\033[0m %s\n' "$*"; }
|
||||
die() { printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
[ "$(uname -s)-$(uname -m)" = "Linux-x86_64" ] || die "This script currently supports Linux x86_64 only."
|
||||
command -v curl >/dev/null || die "curl is required."
|
||||
command -v sha256sum >/dev/null || die "sha256sum is required."
|
||||
|
||||
fetch() { # url dest sha
|
||||
local url="$1" dest="$2" sha="$3"
|
||||
if [ -f "$dest" ] && [ "$(sha256sum "$dest" | cut -d' ' -f1)" = "$sha" ]; then
|
||||
say "already have $(basename "$dest") (checksum OK)"; return
|
||||
fi
|
||||
say "downloading $(basename "$dest")"
|
||||
curl -fL --retry 3 -C - -o "$dest.part" "$url"
|
||||
[ "$(sha256sum "$dest.part" | cut -d' ' -f1)" = "$sha" ] || { rm -f "$dest.part"; die "checksum mismatch for $(basename "$dest") — refusing to use it"; }
|
||||
mv "$dest.part" "$dest"
|
||||
}
|
||||
|
||||
mkdir -p "$RUDDER_HOME/bin" "$RUDDER_HOME/models"
|
||||
fetch "$RT_URL" "$RUDDER_HOME/bin/$RT_NAME.tar.gz" "$RT_SHA"
|
||||
tar -xzf "$RUDDER_HOME/bin/$RT_NAME.tar.gz" -C "$RUDDER_HOME/bin"
|
||||
fetch "$MODEL_URL" "$RUDDER_HOME/models/$MODEL_FILE" "$MODEL_SHA"
|
||||
|
||||
if command -v go >/dev/null; then
|
||||
say "building backend/bin/rudder-engine"
|
||||
(cd "$(dirname "$0")" && go build -o ../bin/rudder-engine .)
|
||||
else
|
||||
printf '! Go not found — install Go, then: cd backend/rudder-engine && go build -o ../bin/rudder-engine .\n'
|
||||
fi
|
||||
|
||||
for tool in parec pactl; do command -v "$tool" >/dev/null || printf '! %s not found — install pipewire-pulse or pulseaudio-utils for microphone capture.\n' "$tool"; done
|
||||
|
||||
say "done. Runtime: $RUDDER_HOME/bin/$RT_NAME Model: $RUDDER_HOME/models/$MODEL_FILE"
|
||||
say "run \`bun run dev\` and open Rudder (rudder-voice) in the Plugin Studio."
|
||||
61
internal/backend-dev-plugin.mjs
Normal file
61
internal/backend-dev-plugin.mjs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* internal/backend-dev-plugin.mjs — `bun run dev` starts the local plugin backends.
|
||||
*
|
||||
* WHY: some plugins ship to DESKTOP with a local backend (rudder-voice → backend/bin/rudder-engine,
|
||||
* run as a Tauri sidecar). In the Plugin Studio the same backend must be running, or the plugin can't
|
||||
* be tested in the browser. This plugin starts each built backend with the dev server, forwards its
|
||||
* /api/<name>/ routes, and stops it when the dev server stops. A backend that isn't built is skipped
|
||||
* with a hint: plugins without a backend never notice this file.
|
||||
*
|
||||
* DEV ONLY (`apply: 'serve'`). A production build never starts or bundles a backend.
|
||||
*/
|
||||
import { spawn } from 'node:child_process'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
// name → how to run it. `route` is proxied to `port` on 127.0.0.1.
|
||||
export const DEV_BACKENDS = [
|
||||
{
|
||||
name: 'rudder-engine',
|
||||
bin: 'backend/bin/rudder-engine',
|
||||
build: 'cd backend/rudder-engine && go build -o ../bin/rudder-engine .',
|
||||
route: '/api/rudder',
|
||||
port: Number(process.env.RUDDER_PORT || 5193),
|
||||
env: { RUDDER_PORT: String(process.env.RUDDER_PORT || 5193), RUDDER_NEMO_PORT: String(process.env.RUDDER_NEMO_PORT || 5194) },
|
||||
},
|
||||
]
|
||||
|
||||
// The proxy table for vite's `server.proxy`. SSE (`/events`) streams through unbuffered.
|
||||
export function devBackendProxy() {
|
||||
return Object.fromEntries(DEV_BACKENDS.map((b) => [b.route, { target: `http://127.0.0.1:${b.port}`, changeOrigin: false }]))
|
||||
}
|
||||
|
||||
export function backendDevPlugin(root) {
|
||||
const children = []
|
||||
return {
|
||||
name: 'dx-backend-dev',
|
||||
apply: 'serve',
|
||||
configureServer(server) {
|
||||
for (const b of DEV_BACKENDS) {
|
||||
const bin = resolve(root, b.bin)
|
||||
if (!existsSync(bin)) {
|
||||
server.config.logger.warn(`[backend] ${b.name} not built — its plugin will show "engine offline". Build: ${b.build}`)
|
||||
continue
|
||||
}
|
||||
const child = spawn(bin, [], { cwd: root, env: { ...process.env, ...b.env }, stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
const tag = `[${b.name}]`
|
||||
const out = (d) => String(d).trim().split('\n').forEach((l) => l && server.config.logger.info(`${tag} ${l}`))
|
||||
child.stdout.on('data', out)
|
||||
child.stderr.on('data', out)
|
||||
child.on('exit', (code) => server.config.logger.warn(`${tag} exited (${code})`))
|
||||
children.push(child)
|
||||
server.config.logger.info(`${tag} started → ${b.route} on 127.0.0.1:${b.port}`)
|
||||
}
|
||||
const stop = () => children.forEach((c) => { try { c.kill('SIGTERM') } catch {} })
|
||||
server.httpServer?.on('close', stop)
|
||||
process.once('exit', stop)
|
||||
process.once('SIGINT', () => { stop(); process.exit(0) })
|
||||
process.once('SIGTERM', () => { stop(); process.exit(0) })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -313,8 +313,11 @@ function profileReachesAMap(dir, file) {
|
|||
function metaStageHeavy(dir, profileName) {
|
||||
try {
|
||||
const meta = JSON.parse(readFileSync(join(dir, `${profileName}.meta.json`), 'utf8'))
|
||||
return { stage: meta?.stage === 'production' ? 'production' : 'experimental', heavy: meta?.heavy === true }
|
||||
} catch { return { stage: 'experimental', heavy: false } }
|
||||
const corner = meta?.widget?.corner
|
||||
return { stage: meta?.stage === 'production' ? 'production' : 'experimental', heavy: meta?.heavy === true,
|
||||
// "widget": { "corner": "bottom-right" } — a corner widget the Plugin Studio mounts once, fixed, not in a pane
|
||||
widget: ['bottom-right', 'bottom-left', 'top-right', 'top-left'].includes(corner) ? { corner } : null }
|
||||
} catch { return { stage: 'experimental', heavy: false, widget: null } }
|
||||
}
|
||||
|
||||
function metaIsMap(dir, profileName) {
|
||||
|
|
@ -1302,11 +1305,12 @@ function scanBundles() {
|
|||
? composesAMapLeaf(dir, profileName, file)
|
||||
: (metaIsMap(dir, profileName) ?? profileReachesAMap(dir, file))
|
||||
|
||||
const { stage: profileStage, heavy: profileHeavy } = metaStageHeavy(dir, profileName)
|
||||
const { stage: profileStage, heavy: profileHeavy, widget: profileWidget } = metaStageHeavy(dir, profileName)
|
||||
return {
|
||||
profile: profileName,
|
||||
stage: profileStage,
|
||||
...(profileHeavy ? { heavy: true } : {}),
|
||||
...(profileWidget ? { widget: profileWidget } : {}),
|
||||
file,
|
||||
description: description || fileTag || '',
|
||||
element: tag,
|
||||
|
|
|
|||
|
|
@ -95,8 +95,13 @@ const labelW = computed(() => {
|
|||
return Math.max(96, Math.min(260, Math.round((width.value || 640) * 0.34)))
|
||||
})
|
||||
// Room on the right for the printed value when a bar runs long.
|
||||
const VALUE_GUTTER = 74
|
||||
const plotW = computed(() => Math.max(40, (width.value || 640) - labelW.value - VALUE_GUTTER))
|
||||
// The value column fits the LONGEST value label (rega, row 82: a fixed 74px clipped "10,160,000" at the edge
|
||||
// of a 335px card). ~7px per character at the value font, plus the 8px gap and a margin; never under 74.
|
||||
const VALUE_GUTTER = computed(() => {
|
||||
const longest = Math.max(0, ...rows.value.filter((r) => !r.unknown).map((r) => String(props.format(total(r))).length))
|
||||
return Math.max(74, longest * 7 + 16)
|
||||
})
|
||||
const plotW = computed(() => Math.max(40, (width.value || 640) - labelW.value - VALUE_GUTTER.value))
|
||||
|
||||
const rowStep = computed(() => props.barHeight + props.gap)
|
||||
const svgH = computed(() => Math.max(rowStep.value, rows.value.length * rowStep.value + 6))
|
||||
|
|
@ -192,7 +197,10 @@ useShadowStyles(
|
|||
|
||||
<div v-if="!rows.length" class="bc-empty">{{ emptyText }}</div>
|
||||
|
||||
<svg v-else :width="width || 640" :height="svgH" role="img" :aria-label="ariaLabel">
|
||||
<!-- direction:ltr on the SVG: its geometry is left-to-right by construction (labels at x=0, values after
|
||||
the bar end). Under an inherited rtl, text-anchor start flips, so labels ran off the left edge and
|
||||
values back over their bars (rega, row 82, ar). Each label's own text still shapes as Arabic. -->
|
||||
<svg v-else :width="width || 640" :height="svgH" role="img" :aria-label="ariaLabel" direction="ltr" style="direction:ltr">
|
||||
<defs>
|
||||
<!-- Hatching means NOT MEASURED. It must not look like a small bar. -->
|
||||
<pattern :id="`${uid}-hatch`" width="7" height="7" patternUnits="userSpaceOnUse"
|
||||
|
|
|
|||
|
|
@ -469,7 +469,12 @@ export function useMapInstance({
|
|||
accessToken = window.MAP_TOKEN
|
||||
}
|
||||
if (!accessToken) {
|
||||
console.error('No Mapbox access token provided. Pass "tk" in profile-config JSON or set window.MAP_TOKEN.')
|
||||
// ONE warn per page, not an error per map: the UI already says "Map unavailable", and a page with
|
||||
// several maps logged this 7x as console.error, which breaks the 0-errors bar (dx-shepherd, TKWARN).
|
||||
if (typeof window !== 'undefined' && !window.__doxaNoTokenWarned) {
|
||||
window.__doxaNoTokenWarned = true
|
||||
console.warn('No Mapbox access token provided. Pass "tk" in profile-config JSON or set window.MAP_TOKEN.')
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!accessToken.startsWith('pk.')) {
|
||||
|
|
|
|||
|
|
@ -14,13 +14,25 @@
|
|||
"accessByBand": "الوصول بلغتهم، حسب استخدام الإنترنت في البلد",
|
||||
"aria": "رسوم بيانية",
|
||||
"bandCaveat": "استخدام الإنترنت رقم البلد (البنك الدولي)، وليس رقم المجموعة أبدًا.",
|
||||
"gapSub": "أكبر 8 من المعروضة حسب عدد السكان. انقر إحداها لفتحها.",
|
||||
"gapTake": "هذه الـ{k} وحدها تضم {people} شخص.",
|
||||
"gapTitle": "أكبر المجموعات العرقية التي لا شيء لها بلغتها",
|
||||
"groups": "المجموعات",
|
||||
"groupsN": "{n} مجموعة",
|
||||
"measure": "العدّ",
|
||||
"messagingTop": "تطبيق المراسلة الأول، حسب البلدان ضمن النطاق",
|
||||
"notMeasured": "غير مقيس",
|
||||
"ntSub": "لغات المجموعات المعروضة التي لم يُعثر لها على عهد جديد، حسب عدد متحدثيها. الأحمر: لا شيء؛ الأصفر: شيء ما لكن بلا عهد جديد.",
|
||||
"ntTake": "{n} لغة هنا لم يُعثر لها على عهد جديد، يتحدثها {people} شخص.",
|
||||
"ntTitle": "أي اللغات أحوج إلى العهد الجديد؟",
|
||||
"people": "الأشخاص",
|
||||
"peopleN": "{n} شخص",
|
||||
"q1sub": "عمود لكل مستوى وصول، للمجموعات العرقية المعروضة.",
|
||||
"q1take": "{n} من {total} مجموعة عرقية معروضة لا شيء لها بلغتها.",
|
||||
"q1title": "ماذا لديهم بلغتهم؟",
|
||||
"q2sub": "المجموعات العرقية التي لا شيء لها بلغتها، حسب نسبة الاتصال بالإنترنت في بلدها.",
|
||||
"q2take": "{online} من الـ{n} التي لا شيء لها تعيش في بلدان يستخدم فيها 60% أو أكثر الإنترنت.",
|
||||
"q2title": "هل يمكن للإنترنت إيصاله إلى من لا شيء لهم؟",
|
||||
"socialTop": "منصة التواصل الأولى، حسب البلدان ضمن النطاق",
|
||||
"unitCountries": "بلدان",
|
||||
"unitGroups": "مجموعة عرقية",
|
||||
|
|
@ -119,6 +131,8 @@
|
|||
"count": "{n} من {total} مجموعة عرقية",
|
||||
"empty": "لا توجد مجموعة عرقية تطابق عوامل التصفية هذه.",
|
||||
"exported": "تم تصدير {n} صف (نُسخت ونُزّلت).",
|
||||
"openCapped": "يفتح أول {n} من المحدد.",
|
||||
"openInGospel": "فتح في موارد الإنجيل",
|
||||
"selectRow": "اختيار {name}"
|
||||
},
|
||||
"headline": {
|
||||
|
|
|
|||
|
|
@ -14,13 +14,25 @@
|
|||
"accessByBand": "Zugang in eigener Sprache, nach Internetnutzung im Land",
|
||||
"aria": "Diagramme",
|
||||
"bandCaveat": "Die Internetnutzung ist die Zahl des Landes (Weltbank), nie die der Gruppe.",
|
||||
"gapSub": "Die 8 größten der angezeigten, nach Bevölkerung. Klicken Sie eine an, um sie zu öffnen.",
|
||||
"gapTake": "Allein diese {k} sind {people} Menschen.",
|
||||
"gapTitle": "Die größten Volksgruppen ohne etwas in ihrer eigenen Sprache",
|
||||
"groups": "Gruppen",
|
||||
"groupsN": "{n} Gruppen",
|
||||
"measure": "Zählen",
|
||||
"messagingTop": "Top-Messenger nach Land im Auswahlbereich",
|
||||
"notMeasured": "Nicht gemessen",
|
||||
"ntSub": "Sprachen der angezeigten Gruppen ohne gefundenes Neues Testament, nach Zahl der Sprecher. Rot: gar nichts; Gelb: etwas, aber kein Neues Testament.",
|
||||
"ntTake": "{n} Sprachen hier haben kein gefundenes Neues Testament, gesprochen von {people} Menschen.",
|
||||
"ntTitle": "Welche Sprachen brauchen das Neue Testament am meisten?",
|
||||
"people": "Menschen",
|
||||
"peopleN": "{n} Menschen",
|
||||
"q1sub": "Ein Balken je Zugangsstufe, für die angezeigten Volksgruppen.",
|
||||
"q1take": "{n} von {total} angezeigten Volksgruppen haben nichts in ihrer eigenen Sprache.",
|
||||
"q1title": "Was haben sie in ihrer eigenen Sprache?",
|
||||
"q2sub": "Volksgruppen ohne etwas in ihrer Sprache, danach, wie viel ihres Landes online ist.",
|
||||
"q2take": "{online} der {n} ohne etwas leben in Ländern, in denen 60 % oder mehr das Internet nutzen.",
|
||||
"q2title": "Könnte das Internet es zu denen ohne etwas bringen?",
|
||||
"socialTop": "Top-Social-Plattform nach Land im Auswahlbereich",
|
||||
"unitCountries": "Länder",
|
||||
"unitGroups": "Volksgruppen",
|
||||
|
|
@ -119,6 +131,8 @@
|
|||
"count": "{n} von {total} Volksgruppen",
|
||||
"empty": "Keine Volksgruppe entspricht diesen Filtern.",
|
||||
"exported": "{n} Zeilen exportiert (kopiert und heruntergeladen).",
|
||||
"openCapped": "Öffnet die ersten {n} ausgewählten.",
|
||||
"openInGospel": "In Evangeliumsressourcen öffnen",
|
||||
"selectRow": "{name} auswählen"
|
||||
},
|
||||
"headline": {
|
||||
|
|
|
|||
|
|
@ -14,13 +14,25 @@
|
|||
"accessByBand": "Own-language access, by country internet use",
|
||||
"aria": "Charts",
|
||||
"bandCaveat": "Internet use is the country's figure (World Bank), never the group's.",
|
||||
"gapSub": "The 8 biggest of those shown, by population. Click one to open it.",
|
||||
"gapTake": "These {k} alone are {people} people.",
|
||||
"gapTitle": "The largest people groups with nothing in their own language",
|
||||
"groups": "Groups",
|
||||
"groupsN": "{n} groups",
|
||||
"measure": "Count",
|
||||
"messagingTop": "Top messaging app, by country in scope",
|
||||
"notMeasured": "Not measured",
|
||||
"ntSub": "Languages of the groups shown with no New Testament found, by how many people speak them. Red: nothing at all; yellow: something, but no New Testament.",
|
||||
"ntTake": "{n} languages here have no New Testament found, spoken by {people} people.",
|
||||
"ntTitle": "Which languages most need the New Testament?",
|
||||
"people": "People",
|
||||
"peopleN": "{n} people",
|
||||
"q1sub": "One bar per level of access, for the people groups shown.",
|
||||
"q1take": "{n} of {total} people groups shown have nothing found in their own language.",
|
||||
"q1title": "What do they have in their own language?",
|
||||
"q2sub": "People groups with nothing in their own language, by how much of their country is online.",
|
||||
"q2take": "{online} of the {n} with nothing live in countries where 60% or more use the internet.",
|
||||
"q2title": "Could the internet carry it to those with nothing?",
|
||||
"socialTop": "Top social platform, by country in scope",
|
||||
"unitCountries": "countries",
|
||||
"unitGroups": "people groups",
|
||||
|
|
@ -119,6 +131,8 @@
|
|||
"count": "{n} of {total} people groups",
|
||||
"empty": "No people groups match these filters.",
|
||||
"exported": "{n} rows exported (copied and downloaded).",
|
||||
"openCapped": "Opens the first {n} selected.",
|
||||
"openInGospel": "Open in Gospel Resources",
|
||||
"selectRow": "Select {name}"
|
||||
},
|
||||
"headline": {
|
||||
|
|
|
|||
|
|
@ -14,13 +14,25 @@
|
|||
"accessByBand": "Acceso en su idioma, según el uso de internet del país",
|
||||
"aria": "Gráficos",
|
||||
"bandCaveat": "El uso de internet es la cifra del país (Banco Mundial), nunca la del grupo.",
|
||||
"gapSub": "Los 8 más grandes de los mostrados, por población. Haga clic en uno para abrirlo.",
|
||||
"gapTake": "Solo estos {k} son {people} personas.",
|
||||
"gapTitle": "Los grupos étnicos más grandes sin nada en su propio idioma",
|
||||
"groups": "Grupos",
|
||||
"groupsN": "{n} grupos",
|
||||
"measure": "Contar",
|
||||
"messagingTop": "App de mensajería principal, por país en la selección",
|
||||
"notMeasured": "No medido",
|
||||
"ntSub": "Idiomas de los grupos mostrados sin Nuevo Testamento encontrado, según cuántas personas los hablan. Rojo: nada; amarillo: algo, pero sin Nuevo Testamento.",
|
||||
"ntTake": "{n} idiomas aquí no tienen Nuevo Testamento encontrado, hablados por {people} personas.",
|
||||
"ntTitle": "¿Qué idiomas necesitan más el Nuevo Testamento?",
|
||||
"people": "Personas",
|
||||
"peopleN": "{n} personas",
|
||||
"q1sub": "Una barra por nivel de acceso, para los grupos étnicos mostrados.",
|
||||
"q1take": "{n} de {total} grupos étnicos mostrados no tienen nada en su propio idioma.",
|
||||
"q1title": "¿Qué tienen en su propio idioma?",
|
||||
"q2sub": "Grupos étnicos sin nada en su idioma, según cuánto de su país está en línea.",
|
||||
"q2take": "{online} de los {n} sin nada viven en países donde el 60 % o más usa internet.",
|
||||
"q2title": "¿Podría internet llevarlo a quienes no tienen nada?",
|
||||
"socialTop": "Red social principal, por país en la selección",
|
||||
"unitCountries": "países",
|
||||
"unitGroups": "grupos étnicos",
|
||||
|
|
@ -119,6 +131,8 @@
|
|||
"count": "{n} de {total} grupos étnicos",
|
||||
"empty": "Ningún grupo étnico coincide con estos filtros.",
|
||||
"exported": "{n} filas exportadas (copiadas y descargadas).",
|
||||
"openCapped": "Abre los primeros {n} seleccionados.",
|
||||
"openInGospel": "Abrir en Recursos del Evangelio",
|
||||
"selectRow": "Seleccionar {name}"
|
||||
},
|
||||
"headline": {
|
||||
|
|
|
|||
|
|
@ -14,13 +14,25 @@
|
|||
"accessByBand": "Accès dans leur langue, selon l’usage d’internet du pays",
|
||||
"aria": "Graphiques",
|
||||
"bandCaveat": "L’usage d’internet est le chiffre du pays (Banque mondiale), jamais celui du groupe.",
|
||||
"gapSub": "Les 8 plus grands de ceux affichés, par population. Cliquez pour en ouvrir un.",
|
||||
"gapTake": "Ces {k} seuls représentent {people} personnes.",
|
||||
"gapTitle": "Les plus grands groupes ethniques sans rien dans leur propre langue",
|
||||
"groups": "Groupes",
|
||||
"groupsN": "{n} groupes",
|
||||
"measure": "Compter",
|
||||
"messagingTop": "Messagerie principale, par pays sélectionné",
|
||||
"notMeasured": "Non mesuré",
|
||||
"ntSub": "Langues des groupes affichés sans Nouveau Testament trouvé, selon le nombre de locuteurs. Rouge : rien du tout ; jaune : quelque chose, mais pas de Nouveau Testament.",
|
||||
"ntTake": "{n} langues ici n’ont pas de Nouveau Testament trouvé, parlées par {people} personnes.",
|
||||
"ntTitle": "Quelles langues ont le plus besoin du Nouveau Testament ?",
|
||||
"people": "Personnes",
|
||||
"peopleN": "{n} personnes",
|
||||
"q1sub": "Une barre par niveau d’accès, pour les groupes ethniques affichés.",
|
||||
"q1take": "{n} des {total} groupes ethniques affichés n’ont rien dans leur propre langue.",
|
||||
"q1title": "Qu’ont-ils dans leur propre langue ?",
|
||||
"q2sub": "Groupes ethniques sans rien dans leur langue, selon la part de leur pays en ligne.",
|
||||
"q2take": "{online} des {n} sans rien vivent dans des pays où 60 % ou plus utilisent internet.",
|
||||
"q2title": "Internet pourrait-il le porter à ceux qui n’ont rien ?",
|
||||
"socialTop": "Réseau social principal, par pays sélectionné",
|
||||
"unitCountries": "pays",
|
||||
"unitGroups": "groupes ethniques",
|
||||
|
|
@ -119,6 +131,8 @@
|
|||
"count": "{n} sur {total} groupes ethniques",
|
||||
"empty": "Aucun groupe ethnique ne correspond à ces filtres.",
|
||||
"exported": "{n} lignes exportées (copiées et téléchargées).",
|
||||
"openCapped": "Ouvre les {n} premiers sélectionnés.",
|
||||
"openInGospel": "Ouvrir dans Ressources de l’Évangile",
|
||||
"selectRow": "Sélectionner {name}"
|
||||
},
|
||||
"headline": {
|
||||
|
|
|
|||
|
|
@ -14,13 +14,25 @@
|
|||
"accessByBand": "अपनी भाषा में पहुँच, देश के इंटरनेट उपयोग के अनुसार",
|
||||
"aria": "चार्ट",
|
||||
"bandCaveat": "इंटरनेट उपयोग देश का आंकड़ा है (विश्व बैंक), समूह का कभी नहीं।",
|
||||
"gapSub": "दिखाए गए में से जनसंख्या के अनुसार 8 सबसे बड़े। खोलने के लिए किसी पर क्लिक करें।",
|
||||
"gapTake": "केवल ये {k} ही {people} लोग हैं।",
|
||||
"gapTitle": "अपनी भाषा में कुछ भी न रखने वाले सबसे बड़े जन-समूह",
|
||||
"groups": "समूह",
|
||||
"groupsN": "{n} समूह",
|
||||
"measure": "गिनती",
|
||||
"messagingTop": "प्रमुख मैसेजिंग ऐप, दायरे के देशों के अनुसार",
|
||||
"notMeasured": "मापा नहीं गया",
|
||||
"ntSub": "दिखाए गए समूहों की वे भाषाएँ जिनमें नया नियम नहीं मिला, बोलने वालों की संख्या के अनुसार। लाल: कुछ नहीं; पीला: कुछ है, पर नया नियम नहीं।",
|
||||
"ntTake": "यहाँ {n} भाषाओं में नया नियम नहीं मिला, जिन्हें {people} लोग बोलते हैं।",
|
||||
"ntTitle": "किन भाषाओं को नए नियम की सबसे अधिक ज़रूरत है?",
|
||||
"people": "लोग",
|
||||
"peopleN": "{n} लोग",
|
||||
"q1sub": "दिखाए गए जन-समूहों के लिए, पहुँच के हर स्तर पर एक पट्टी।",
|
||||
"q1take": "दिखाए गए {total} जन-समूहों में से {n} की अपनी भाषा में कुछ नहीं मिला।",
|
||||
"q1title": "उनकी अपनी भाषा में क्या है?",
|
||||
"q2sub": "जिन जन-समूहों की भाषा में कुछ नहीं, उनके देश का कितना हिस्सा ऑनलाइन है उसके अनुसार।",
|
||||
"q2take": "जिन {n} के पास कुछ नहीं, उनमें से {online} ऐसे देशों में रहते हैं जहाँ 60% या अधिक लोग इंटरनेट का उपयोग करते हैं।",
|
||||
"q2title": "क्या इंटरनेट इसे उन तक पहुँचा सकता है जिनके पास कुछ नहीं है?",
|
||||
"socialTop": "प्रमुख सोशल प्लेटफ़ॉर्म, दायरे के देशों के अनुसार",
|
||||
"unitCountries": "देश",
|
||||
"unitGroups": "जन-समूह",
|
||||
|
|
@ -119,6 +131,8 @@
|
|||
"count": "{total} में से {n} जन-समूह",
|
||||
"empty": "इन फ़िल्टरों से कोई जन-समूह मेल नहीं खाता।",
|
||||
"exported": "{n} पंक्तियाँ निर्यात हुईं (कॉपी और डाउनलोड)।",
|
||||
"openCapped": "पहले {n} चयनित खोलता है।",
|
||||
"openInGospel": "सुसमाचार संसाधनों में खोलें",
|
||||
"selectRow": "{name} चुनें"
|
||||
},
|
||||
"headline": {
|
||||
|
|
|
|||
|
|
@ -14,13 +14,25 @@
|
|||
"accessByBand": "Accesso nella propria lingua, per uso di internet del paese",
|
||||
"aria": "Grafici",
|
||||
"bandCaveat": "L’uso di internet è il dato del paese (Banca Mondiale), mai quello del gruppo.",
|
||||
"gapSub": "Gli 8 più grandi tra quelli mostrati, per popolazione. Clicca per aprirne uno.",
|
||||
"gapTake": "Solo questi {k} sono {people} persone.",
|
||||
"gapTitle": "I gruppi etnici più grandi senza niente nella propria lingua",
|
||||
"groups": "Gruppi",
|
||||
"groupsN": "{n} gruppi",
|
||||
"measure": "Conta",
|
||||
"messagingTop": "App di messaggistica principale, per paese selezionato",
|
||||
"notMeasured": "Non misurato",
|
||||
"ntSub": "Lingue dei gruppi mostrati senza Nuovo Testamento trovato, per numero di parlanti. Rosso: niente; giallo: qualcosa, ma nessun Nuovo Testamento.",
|
||||
"ntTake": "{n} lingue qui non hanno un Nuovo Testamento trovato, parlate da {people} persone.",
|
||||
"ntTitle": "Quali lingue hanno più bisogno del Nuovo Testamento?",
|
||||
"people": "Persone",
|
||||
"peopleN": "{n} persone",
|
||||
"q1sub": "Una barra per livello di accesso, per i gruppi etnici mostrati.",
|
||||
"q1take": "{n} dei {total} gruppi etnici mostrati non hanno niente nella propria lingua.",
|
||||
"q1title": "Che cosa hanno nella propria lingua?",
|
||||
"q2sub": "Gruppi etnici senza niente nella loro lingua, secondo quanto del loro paese è online.",
|
||||
"q2take": "{online} dei {n} senza niente vivono in paesi dove il 60 % o più usa internet.",
|
||||
"q2title": "Internet potrebbe portarlo a chi non ha niente?",
|
||||
"socialTop": "Piattaforma social principale, per paese selezionato",
|
||||
"unitCountries": "paesi",
|
||||
"unitGroups": "gruppi etnici",
|
||||
|
|
@ -119,6 +131,8 @@
|
|||
"count": "{n} di {total} gruppi etnici",
|
||||
"empty": "Nessun gruppo etnico corrisponde a questi filtri.",
|
||||
"exported": "{n} righe esportate (copiate e scaricate).",
|
||||
"openCapped": "Apre i primi {n} selezionati.",
|
||||
"openInGospel": "Apri in Risorse del Vangelo",
|
||||
"selectRow": "Seleziona {name}"
|
||||
},
|
||||
"headline": {
|
||||
|
|
|
|||
|
|
@ -14,13 +14,25 @@
|
|||
"accessByBand": "Acesso na própria língua, por uso de internet do país",
|
||||
"aria": "Gráficos",
|
||||
"bandCaveat": "O uso de internet é o número do país (Banco Mundial), nunca o do grupo.",
|
||||
"gapSub": "Os 8 maiores dos exibidos, por população. Clique num para abri-lo.",
|
||||
"gapTake": "Só estes {k} somam {people} pessoas.",
|
||||
"gapTitle": "Os maiores povos sem nada na própria língua",
|
||||
"groups": "Grupos",
|
||||
"groupsN": "{n} grupos",
|
||||
"measure": "Contar",
|
||||
"messagingTop": "App de mensagens principal, por país no recorte",
|
||||
"notMeasured": "Não medido",
|
||||
"ntSub": "Línguas dos grupos exibidos sem Novo Testamento encontrado, por quantas pessoas as falam. Vermelho: nada; amarelo: algo, mas sem Novo Testamento.",
|
||||
"ntTake": "{n} línguas aqui não têm Novo Testamento encontrado, faladas por {people} pessoas.",
|
||||
"ntTitle": "Quais línguas mais precisam do Novo Testamento?",
|
||||
"people": "Pessoas",
|
||||
"peopleN": "{n} pessoas",
|
||||
"q1sub": "Uma barra por nível de acesso, para os povos exibidos.",
|
||||
"q1take": "{n} de {total} povos exibidos não têm nada na própria língua.",
|
||||
"q1title": "O que eles têm na própria língua?",
|
||||
"q2sub": "Povos sem nada na própria língua, pelo quanto do país está online.",
|
||||
"q2take": "{online} dos {n} sem nada vivem em países onde 60 % ou mais usam a internet.",
|
||||
"q2title": "A internet poderia levar isso a quem não tem nada?",
|
||||
"socialTop": "Rede social principal, por país no recorte",
|
||||
"unitCountries": "países",
|
||||
"unitGroups": "povos",
|
||||
|
|
@ -119,6 +131,8 @@
|
|||
"count": "{n} de {total} povos",
|
||||
"empty": "Nenhum povo corresponde a estes filtros.",
|
||||
"exported": "{n} linhas exportadas (copiadas e baixadas).",
|
||||
"openCapped": "Abre os primeiros {n} selecionados.",
|
||||
"openInGospel": "Abrir em Recursos do Evangelho",
|
||||
"selectRow": "Selecionar {name}"
|
||||
},
|
||||
"headline": {
|
||||
|
|
|
|||
|
|
@ -14,13 +14,25 @@
|
|||
"accessByBand": "Acces în propria limbă, după folosirea internetului în țară",
|
||||
"aria": "Grafice",
|
||||
"bandCaveat": "Folosirea internetului este cifra țării (Banca Mondială), niciodată a grupului.",
|
||||
"gapSub": "Cele mai mari 8 dintre cele afișate, după populație. Faceți clic pentru a deschide unul.",
|
||||
"gapTake": "Doar aceste {k} înseamnă {people} oameni.",
|
||||
"gapTitle": "Cele mai mari grupuri etnice fără nimic în propria limbă",
|
||||
"groups": "Grupuri",
|
||||
"groupsN": "{n} grupuri",
|
||||
"measure": "Numără",
|
||||
"messagingTop": "Aplicația de mesagerie principală, pe țări selectate",
|
||||
"notMeasured": "Nemăsurat",
|
||||
"ntSub": "Limbile grupurilor afișate fără Noul Testament găsit, după numărul vorbitorilor. Roșu: nimic; galben: ceva, dar fără Noul Testament.",
|
||||
"ntTake": "{n} limbi de aici nu au Noul Testament găsit, vorbite de {people} oameni.",
|
||||
"ntTitle": "Ce limbi au cea mai mare nevoie de Noul Testament?",
|
||||
"people": "Oameni",
|
||||
"peopleN": "{n} oameni",
|
||||
"q1sub": "O bară pentru fiecare nivel de acces, pentru grupurile etnice afișate.",
|
||||
"q1take": "{n} din {total} grupuri etnice afișate nu au nimic în propria limbă.",
|
||||
"q1title": "Ce au în propria lor limbă?",
|
||||
"q2sub": "Grupuri etnice fără nimic în limba lor, după cât din țara lor este online.",
|
||||
"q2take": "{online} din cele {n} fără nimic trăiesc în țări unde 60 % sau mai mult folosesc internetul.",
|
||||
"q2title": "Ar putea internetul să-l ducă celor care nu au nimic?",
|
||||
"socialTop": "Platforma socială principală, pe țări selectate",
|
||||
"unitCountries": "țări",
|
||||
"unitGroups": "grupuri etnice",
|
||||
|
|
@ -119,6 +131,8 @@
|
|||
"count": "{n} din {total} grupuri etnice",
|
||||
"empty": "Niciun grup etnic nu corespunde acestor filtre.",
|
||||
"exported": "{n} rânduri exportate (copiate și descărcate).",
|
||||
"openCapped": "Deschide primele {n} selectate.",
|
||||
"openInGospel": "Deschide în Resurse ale Evangheliei",
|
||||
"selectRow": "Selectează {name}"
|
||||
},
|
||||
"headline": {
|
||||
|
|
|
|||
|
|
@ -14,13 +14,25 @@
|
|||
"accessByBand": "Доступ на родном языке по уровню интернета в стране",
|
||||
"aria": "Диаграммы",
|
||||
"bandCaveat": "Использование интернета — показатель страны (Всемирный банк), а не группы.",
|
||||
"gapSub": "8 крупнейших из показанных, по численности. Нажмите, чтобы открыть.",
|
||||
"gapTake": "Только эти {k} — это {people} человек.",
|
||||
"gapTitle": "Крупнейшие народы, у которых нет ничего на родном языке",
|
||||
"groups": "Группы",
|
||||
"groupsN": "{n} групп",
|
||||
"measure": "Считать",
|
||||
"messagingTop": "Главный мессенджер по странам в выборке",
|
||||
"notMeasured": "Не измерено",
|
||||
"ntSub": "Языки показанных групп без найденного Нового Завета, по числу говорящих. Красный: ничего; жёлтый: что-то есть, но нет Нового Завета.",
|
||||
"ntTake": "Языков здесь без найденного Нового Завета: {n}; на них говорят {people} человек.",
|
||||
"ntTitle": "Каким языкам больше всего нужен Новый Завет?",
|
||||
"people": "Люди",
|
||||
"peopleN": "{n} чел.",
|
||||
"q1sub": "Одна полоса на уровень доступа, для показанных народов.",
|
||||
"q1take": "У {n} из {total} показанных народов нет ничего на родном языке.",
|
||||
"q1title": "Что у них есть на родном языке?",
|
||||
"q2sub": "Народы, у которых нет ничего на их языке, по доле их страны в сети.",
|
||||
"q2take": "{online} из {n} без ничего живут в странах, где интернетом пользуются 60 % и более.",
|
||||
"q2title": "Может ли интернет донести это до тех, у кого ничего нет?",
|
||||
"socialTop": "Главная соцсеть по странам в выборке",
|
||||
"unitCountries": "стран",
|
||||
"unitGroups": "народов",
|
||||
|
|
@ -119,6 +131,8 @@
|
|||
"count": "{n} из {total} народов",
|
||||
"empty": "Ни один народ не подходит под эти фильтры.",
|
||||
"exported": "Экспортировано строк: {n} (скопировано и скачано).",
|
||||
"openCapped": "Открывает первые {n} выбранных.",
|
||||
"openInGospel": "Открыть в «Евангельских ресурсах»",
|
||||
"selectRow": "Выбрать: {name}"
|
||||
},
|
||||
"headline": {
|
||||
|
|
|
|||
|
|
@ -14,13 +14,25 @@
|
|||
"accessByBand": "母语资源情况,按国家互联网使用率",
|
||||
"aria": "图表",
|
||||
"bandCaveat": "互联网使用率是国家的数字(世界银行),不是该族群的数字。",
|
||||
"gapSub": "所显示族群中人口最多的 8 个。点击即可打开。",
|
||||
"gapTake": "仅这 {k} 个就有 {people} 人。",
|
||||
"gapTitle": "母语中一无所有的最大族群",
|
||||
"groups": "族群",
|
||||
"groupsN": "{n} 个族群",
|
||||
"measure": "统计",
|
||||
"messagingTop": "各国(当前范围)最主要的通讯应用",
|
||||
"notMeasured": "未统计",
|
||||
"ntSub": "所显示族群中未找到新约的语言,按使用人数排列。红色:一无所有;黄色:有一些,但没有新约。",
|
||||
"ntTake": "这里有 {n} 种语言未找到新约,使用人数为 {people}。",
|
||||
"ntTitle": "哪些语言最需要新约?",
|
||||
"people": "人数",
|
||||
"peopleN": "{n} 人",
|
||||
"q1sub": "每个访问级别一条,针对所显示的族群。",
|
||||
"q1take": "所显示的 {total} 个族群中,有 {n} 个在母语中一无所有。",
|
||||
"q1title": "他们的母语中有什么?",
|
||||
"q2sub": "母语中一无所有的族群,按其国家的上网比例。",
|
||||
"q2take": "在 {n} 个一无所有的族群中,有 {online} 个生活在 60% 或以上人口使用互联网的国家。",
|
||||
"q2title": "互联网能把它带给一无所有的人吗?",
|
||||
"socialTop": "各国(当前范围)最主要的社交平台",
|
||||
"unitCountries": "个国家",
|
||||
"unitGroups": "个族群",
|
||||
|
|
@ -119,6 +131,8 @@
|
|||
"count": "{total} 个族群中的 {n} 个",
|
||||
"empty": "没有符合这些筛选条件的族群。",
|
||||
"exported": "已导出 {n} 行(已复制并下载)。",
|
||||
"openCapped": "打开前 {n} 个所选项。",
|
||||
"openInGospel": "在福音资源中打开",
|
||||
"selectRow": "选择 {name}"
|
||||
},
|
||||
"headline": {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
* internet figures are the country's (or state's); "found in the sources checked", never "reached"; no
|
||||
* engagement, no one-works-here. Sources are named from the table's own meta.
|
||||
*/
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, getCurrentInstance } from 'vue'
|
||||
import { useDashboardLayout } from '@map/composables/useDashboardLayout.js'
|
||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||
import { listLabel } from '@map/lists/peopleGroupList.js'
|
||||
|
|
@ -39,6 +39,29 @@ import GiCombos from './src/components/GiCombos.vue'
|
|||
const T = useGiT()
|
||||
const board = useBoard()
|
||||
const { bus } = board
|
||||
// ── INTEROP OUT: every committed selection leaves as a DOM event another element or the host page can
|
||||
// take (SPEC-DASHBOARD-KIT 5.3, level 2): bubbling + composed, so it crosses the shadow boundary; CODES,
|
||||
// never labels (slugs, ISO 639-3, ISO 3166 alpha-3); debounced so a range drag sends one event. The host
|
||||
// is found through the instance captured HERE, in setup: getCurrentInstance() in a watcher is null, and
|
||||
// that is exactly how Simple Research's map -> grid event silently never fired.
|
||||
const selfInstance = getCurrentInstance()
|
||||
let selTimer = null
|
||||
watch(() => bus.selectionVersion.value, () => {
|
||||
if (selTimer) clearTimeout(selTimer)
|
||||
selTimer = setTimeout(() => {
|
||||
selTimer = null
|
||||
const host = selfInstance?.proxy?.$el?.getRootNode?.()?.host
|
||||
if (!host) return
|
||||
const slugs = [...bus.selectionSet]
|
||||
const rows = slugs.map((s) => board.bySlug.value.get(s)).filter(Boolean)
|
||||
host.dispatchEvent(new CustomEvent('gospel-internet:selection', { bubbles: true, composed: true, detail: {
|
||||
slugs, languages: [...new Set(rows.map((r) => r.iso639_3))], countries: [...new Set(rows.map((r) => r.iso3))],
|
||||
list: board.listId.value,
|
||||
} }))
|
||||
}, 150)
|
||||
})
|
||||
onBeforeUnmount(() => { if (selTimer) clearTimeout(selTimer) })
|
||||
|
||||
// The URL / host preset (view, facets, centre panel, group) is applied BEFORE the first render, so the
|
||||
// board opens already in its state: no flash of the default view, and the first render mounts the panel
|
||||
// the preset asks for (the language tree included) where a headless mount check can see it.
|
||||
|
|
@ -62,7 +85,7 @@ onBeforeUnmount(() => { try { ro && ro.disconnect() } catch (_) {} })
|
|||
// The kit's layout engine, under THIS board's key (landing 2's namespacing), grid mode.
|
||||
const { startFacetResize, startRailResize, startTableResize, onResizeKeydown, facetW, railW, tableH } = useDashboardLayout({
|
||||
shellRef: shell, bus, isNarrow, storageKey: 'gi-board-layout-v1',
|
||||
defaults: { facetW: 264, railW: 300, tableH: '38%' },
|
||||
defaults: { facetW: 264, railW: 360, tableH: '38%' },
|
||||
})
|
||||
|
||||
// ── the headline: the board's question answered in one line, for the list in view ───────────────────
|
||||
|
|
@ -124,9 +147,11 @@ const sourcesLine = computed(() => (board.meta.value.sources || []).join(' · ')
|
|||
useShadowStyles(
|
||||
':host{display:block}' +
|
||||
'.gb{--gb-bg:#f3f3f1;position:relative;box-sizing:border-box;width:100%;height:100%;min-height:640px;display:grid;background:var(--gb-bg);color:#3b463d;' +
|
||||
'grid-template-columns:var(--facet-w,264px) minmax(0,1fr) var(--rail-w,300px);' +
|
||||
'grid-template-columns:var(--facet-w,264px) minmax(0,1fr) var(--rail-w,360px);' +
|
||||
'grid-template-rows:auto auto auto minmax(0,1fr) var(--table-h,38%);' +
|
||||
'grid-template-areas:"top top top" "combos combos combos" "note note note" "facets map rail" "facets grid grid";' +
|
||||
// The charts rail runs the full height beside the map AND the table: sized to the map row alone it was
|
||||
// ~300px and clipped its four charts (rega, the Driver: "the charts don't look good").
|
||||
'grid-template-areas:"top top top" "combos combos combos" "note note note" "facets map rail" "facets grid rail";' +
|
||||
'font:13px/1.35 Roboto,-apple-system,"Segoe UI",sans-serif;overflow:hidden}' +
|
||||
'.gb-top{grid-area:top;display:flex;align-items:center;flex-wrap:wrap;gap:10px 16px;padding:10px 14px;background:#fff;border-bottom:1px solid #d1d4cd}' +
|
||||
'.gb-title{margin:0;font-size:17px;font-weight:700;letter-spacing:.005em}' +
|
||||
|
|
|
|||
|
|
@ -82,6 +82,13 @@ const back = M.buildQuery({ view: 'zero', filterSet: { band: ['yes', 'partial'],
|
|||
ok(back.includes('lang=ar') && back.includes('list=700') && back.includes('band=yes%7Cpartial') && back.includes('view=zero'),
|
||||
`query builds and keeps lang/list: ${back}`)
|
||||
ok(M.parseQuery('?view=nonsense').view === 'board', 'an unknown view falls back to board')
|
||||
// embed-contract parity with Gospel Resources: ?groups= (peoples by slug) and ?codes= / ?code= (languages)
|
||||
const pg = M.parseQuery('?groups=abai-sungai,hadrami-arabs&codes=abf,ayh')
|
||||
ok(pg.groups.length === 2 && Array.isArray(pg.filters.language) && pg.filters.language.join('|') === 'abf|ayh', 'parses ?groups= (slugs) and ?codes= (languages), comma-separated as Gospel Resources writes them')
|
||||
ok(M.parseQuery('?code=ara').filters.language === 'ara' && M.parseQuery('?codes=ARAB,xx').filters.language === undefined, '?code= alone is a language; malformed codes are refused, never guessed')
|
||||
const link = M.gospelResourcesLink('https://x.test/dx-maps/gospel-resources/resources-map/index.html', ['a', 'b'], { list: '700', lang: 'ar' })
|
||||
ok(link.includes('groups=a%2Cb') && link.includes('list=700') && link.includes('lang=ar'), `out-link to Gospel Resources carries groups, list and lang: ${link.split('?')[1]}`)
|
||||
ok(new URLSearchParams(M.gospelResourcesLink('u', Array.from({ length: 500 }, (_, i) => 's' + i)).split('?')[1]).get('groups').split(',').length === M.GROUPS_LINK_CAP, 'the out-link is capped at GROUPS_LINK_CAP slugs')
|
||||
|
||||
// 7. CSV
|
||||
const csv = M.buildCsv(
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ try {
|
|||
b.tree.value = markRaw({ ...M.indexTree(treeDoc), meta: treeDoc.meta })
|
||||
b.resources.value = markRaw(res)
|
||||
b.loading.value = false
|
||||
if (open) b.openSlug.value = open.slug
|
||||
if (open) { b.openSlug.value = open.slug; b.bus.setSelection([open.slug]) }
|
||||
// narrow to the largest family through the tree, as a click would
|
||||
const fam = b.tree.value.nodes.filter((n) => !n.p).sort((x, y) => b.tree.value.isos.get(y.id).size - b.tree.value.isos.get(x.id).size)[0]
|
||||
b.setTreeNode(fam.id)
|
||||
|
|
@ -89,6 +89,10 @@ try {
|
|||
['GiTree', /data-tree-legend[\s\S]*data-tree-poster/], ['GiResources', /data-resources-total/], ['GiMap', /data-region="map"/]]) ok(re.test(en.html), `en: ${name} rendered its region`)
|
||||
ok(!!open && en.html.includes(open.people_group), `en: the drawer shows the opened group (${open ? open.people_group : 'no row'})`)
|
||||
ok(/data-notice="approximate-pin"/.test(en.html), 'en: the drawer carries the approximate-pin notice')
|
||||
// interop out: the selection opens in Gospel Resources by its ?groups= contract, carrying the slug
|
||||
const gl = (en.html.match(/href="([^"]*)"[^>]*data-open-gospel/) || [])[1] || ''
|
||||
ok(!!open && gl.includes('gospel-resources/resources-map/index.html') && decodeURIComponent(gl).includes('groups=' + open.slug) && !POISON,
|
||||
`en: the selection links out to Gospel Resources with ?groups= (${gl.split('?')[1] || 'no link'})`)
|
||||
ok(/data-tree-card/.test(en.html), 'en: the tree card shows the node the board is narrowed to')
|
||||
ok(/href="https?:\/\/[^"]+"/.test(en.html.slice(en.html.indexOf('data-region="resources"'))), 'en: the resources tab lists real links')
|
||||
ok(/\b\d[\d,]* of [\d,]+ people groups\b/.test(en.html), 'en: the grid prints its scope count')
|
||||
|
|
@ -97,6 +101,12 @@ try {
|
|||
const fam = treeDoc.nodes.filter((n) => !n.p).sort((x, y) => 0)[0]
|
||||
const famNode = (() => { const ix = M.indexTree(treeDoc); return ix.nodes.filter((n) => !n.p).sort((x, y) => ix.isos.get(y.id).size - ix.isos.get(x.id).size)[0] })()
|
||||
ok(/data-chip="language"/.test(en.html) && en.html.includes(famNode.n), `en: the kit chip names the tree node (${famNode.n})`)
|
||||
// the charts answer their question in words (the harness narrows to a family first, so assert the
|
||||
// takeaways' FORM and that the gap charts name real groups/languages, not the roster's totals)
|
||||
const plain = en.html.replace(/<!--[\s\S]*?-->/g, '')
|
||||
ok(/\d[\d,]* of the \d[\d,]* with nothing live in countries/.test(plain) && !POISON, 'en: chart 3 states its takeaway')
|
||||
ok(/data-chart="gaps"[\s\S]*?These \d+ alone are [\d,]+ people/.test(plain), 'en: the gap chart names its groups and totals them')
|
||||
ok(/data-chart="ntNeed"[\s\S]*?languages here have no New Testament found/.test(plain), 'en: the New Testament chart states its takeaway')
|
||||
ok(new RegExp('data-combo="digital-now"[^>]*>\\s*<span>[^<]+</span><span class="gic2-n">' + (POISON ? 'x' : '220')).test(en.html), 'en: a combination shows its live count (digital-now = 220)')
|
||||
void fam
|
||||
const ar = await render('ar')
|
||||
|
|
|
|||
|
|
@ -1,69 +1,98 @@
|
|||
<script setup>
|
||||
/**
|
||||
* GiCharts.vue — the right rail: the "bridge through the internet" picture in two charts, over the rows
|
||||
* in scope (they follow the facets, like every panel).
|
||||
* 1. In their own language x country internet band — stacked bars, groups or people.
|
||||
* 2. Nothing in their own language, by band — the zero-access front door.
|
||||
* The library's BarChart draws both (hand-rolled SVG, no chart CDN: a third-party request from a
|
||||
* persecuted reader's device is a safety leak). A bar is a filter: clicking one toggles that band.
|
||||
* GiCharts.vue — the right rail: FOUR charts, each answering one plain question about the groups shown,
|
||||
* chosen to show a newcomer the GAPS they would not otherwise see (Driver, 2026-09-27: "a reader can't
|
||||
* tell what they're saying"; "showcase the gaps ... the biggest, most helpful, clearest data"). Each
|
||||
* carries its question as the title, says what one bar is and what it counts, and ends in a one-line
|
||||
* takeaway computed from the same rows.
|
||||
* 1. What do they have in their own language? one bar per level of access
|
||||
* 2. The largest people groups with nothing the 8 biggest gaps, by name (click opens one)
|
||||
* 3. Could the internet carry it to those with nothing? by how much of their country is online
|
||||
* 4. Which languages most need the New Testament? languages with none found, by people who speak them
|
||||
* Dropped: a stacked access x band chart (five colours on four bars, no readable answer), a social
|
||||
* platform chart and a messaging-app chart (channels, not gaps: the facets and drawer carry them).
|
||||
* They follow the facets like every panel; a bar is a filter (click to narrow the board to it).
|
||||
*/
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import BarChart from '@map/components/charts/BarChart.vue'
|
||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||
import { useBoardState } from '../useBoard.js'
|
||||
import { useGiT } from '../useGiT.js'
|
||||
import { accessByBand, countriesBy, ACCESS_DETAILS, BANDS, DETAIL_FILL, label } from '../model.js'
|
||||
import { ACCESS_DETAILS, BANDS, DETAIL_FILL, BAND_COLOR, TREE_COLOR, label } from '../model.js'
|
||||
|
||||
const T = useGiT()
|
||||
const board = useBoardState()
|
||||
const { bus } = board
|
||||
const measure = ref('groups')
|
||||
const fmt = (v) => (Number(v) || 0).toLocaleString()
|
||||
|
||||
// The 'none' segment is drawn in the rim colour: a white segment on a white chart would be invisible.
|
||||
const SEG_COLOR = { ...DETAIL_FILL, none: '#7d3b30' }
|
||||
const stack = computed(() => {
|
||||
const m = accessByBand(board.filtered.value, measure.value)
|
||||
return BANDS.map((b) => {
|
||||
const segs = ACCESS_DETAILS.map((d) => ({ label: label(T, 'detail', d), value: m[b][d], color: SEG_COLOR[d] }))
|
||||
return { label: label(T, 'band', b), value: segs.reduce((s, x) => s + x.value, 0), segments: segs, meta: { band: b } }
|
||||
})
|
||||
})
|
||||
const legend = computed(() => ACCESS_DETAILS.map((d) => ({ label: label(T, 'detail', d), color: SEG_COLOR[d] })))
|
||||
|
||||
const zero = computed(() => {
|
||||
const rows = board.filtered.value.filter((r) => r.access_detail === 'none')
|
||||
return BANDS.map((b) => {
|
||||
const inBand = rows.filter((r) => r.internet_band === b)
|
||||
const people = inBand.reduce((s, r) => s + (Number(r.population) || 0), 0)
|
||||
return { label: label(T, 'band', b), value: measure.value === 'people' ? people : inBand.length,
|
||||
note: measure.value === 'people' ? T('gospelInternet.charts.groupsN', '{n} groups', { n: fmt(inBand.length) }) : T('gospelInternet.charts.peopleN', '{n} people', { n: fmt(people) }),
|
||||
color: '#7d3b30', meta: { band: b } }
|
||||
})
|
||||
})
|
||||
// Charts 3 and 4: which apps would carry a resource into the countries in scope. Counted by COUNTRY,
|
||||
// and each source's own caveat is printed under its chart (verbatim from the table's meta).
|
||||
const notInFeed = () => T('gospelInternet.charts.notMeasured', 'Not measured')
|
||||
const appSeries = computed(() => countriesBy(board.filtered.value, board.countries.value, (c) => c && c.top_messaging)
|
||||
.map((e) => ({ label: e.value || notInFeed(), value: e.n, color: e.value ? '#3b463d' : '#9aa2ad', meta: { app: e.value } })))
|
||||
const socialSeries = computed(() => countriesBy(board.filtered.value, board.countries.value, (c) => c && c.social && c.social[0] && c.social[0].platform)
|
||||
.map((e) => ({ label: e.value || notInFeed(), value: e.n, color: e.value ? '#92b195' : '#9aa2ad' })))
|
||||
const pmeta = computed(() => board.meta.value.platforms || {})
|
||||
const unitCountries = computed(() => T('gospelInternet.charts.unitCountries', 'countries'))
|
||||
function onApp(row) { const a = row && row.meta && row.meta.app; if (a) bus.toggleFilterValue('messaging', a) }
|
||||
const platformsFirst = computed(() => board.view.value === 'platforms')
|
||||
|
||||
const count = (rs) => measure.value === 'people' ? rs.reduce((s, r) => s + (Number(r.population) || 0), 0) : rs.length
|
||||
const unit = computed(() => measure.value === 'people' ? T('gospelInternet.charts.unitPeople', 'people') : T('gospelInternet.charts.unitGroups', 'people groups'))
|
||||
function onBar(row) { const b = row && row.meta && row.meta.band; if (b) bus.toggleFilterValue('band', b) }
|
||||
const rows = computed(() => board.filtered.value)
|
||||
|
||||
// 1 ─ what they have in their own language
|
||||
const SEG_COLOR = { ...DETAIL_FILL, none: '#7d3b30' } // a white bar would vanish on white
|
||||
const q1 = computed(() => ACCESS_DETAILS.map((d) => {
|
||||
const rs = rows.value.filter((r) => r.access_detail === d)
|
||||
return { label: label(T, 'detail', d), value: count(rs), color: SEG_COLOR[d], meta: { key: 'access_detail', value: d } }
|
||||
}))
|
||||
const q1take = computed(() => {
|
||||
const none = rows.value.filter((r) => r.access_detail === 'none').length
|
||||
return T('gospelInternet.charts.q1take', '{n} of {total} people groups shown have nothing found in their own language.', { n: fmt(none), total: fmt(rows.value.length) })
|
||||
})
|
||||
|
||||
// 2 ─ could the internet carry it to those with nothing?
|
||||
const zero = computed(() => rows.value.filter((r) => r.access_detail === 'none'))
|
||||
const q2 = computed(() => BANDS.map((b) => {
|
||||
const rs = zero.value.filter((r) => r.internet_band === b)
|
||||
return { label: label(T, 'band', b), value: count(rs), color: BAND_COLOR[b], meta: { key: 'band', value: b } }
|
||||
}))
|
||||
const q2take = computed(() => {
|
||||
const online = zero.value.filter((r) => r.internet_band === 'yes').length
|
||||
return T('gospelInternet.charts.q2take', '{online} of the {n} with nothing live in countries where 60% or more use the internet.', { online: fmt(online), n: fmt(zero.value.length) })
|
||||
})
|
||||
|
||||
// 2 ─ the largest people groups with nothing in their own language: the biggest gaps, by name
|
||||
const TOP = 8
|
||||
const gaps = computed(() => [...zero.value].sort((a, b) => (Number(b.population) || 0) - (Number(a.population) || 0)).slice(0, TOP))
|
||||
const gapSeries = computed(() => gaps.value.map((r) => ({ label: `${r.people_group} (${r.iso3})`, value: Number(r.population) || 0, color: SEG_COLOR.none, meta: { open: r.slug } })))
|
||||
const gapTake = computed(() => T('gospelInternet.charts.gapTake', 'These {k} alone are {people} people.', { k: fmt(gaps.value.length), people: fmt(gaps.value.reduce((s, r) => s + (Number(r.population) || 0), 0)) }))
|
||||
|
||||
// 4 ─ which languages most need the New Testament? (status per language comes from the language tree)
|
||||
onMounted(() => { board.loadTree().catch(() => {}) })
|
||||
const ntNeed = computed(() => {
|
||||
const ix = board.tree.value
|
||||
if (!ix) return []
|
||||
const by = new Map()
|
||||
for (const r of rows.value) {
|
||||
const node = ix.byId.get(ix.nodeOfIso.get(r.iso639_3))
|
||||
if (!node || node.st === 'g') continue
|
||||
const e = by.get(r.iso639_3) || { iso: r.iso639_3, name: r.language, st: node.st, people: 0 }
|
||||
e.people += Number(r.population) || 0
|
||||
by.set(r.iso639_3, e)
|
||||
}
|
||||
return [...by.values()].sort((a, b) => b.people - a.people)
|
||||
})
|
||||
const ntSeries = computed(() => ntNeed.value.slice(0, TOP).map((e) => ({ label: `${e.name} (${e.iso})`, value: e.people, color: TREE_COLOR[e.st], meta: { key: 'language', value: e.iso } })))
|
||||
const ntTake = computed(() => T('gospelInternet.charts.ntTake', '{n} languages here have no New Testament found, spoken by {people} people.', { n: fmt(ntNeed.value.length), people: fmt(ntNeed.value.reduce((s, e) => s + e.people, 0)) }))
|
||||
|
||||
function onBar(row) {
|
||||
const m = row && row.meta
|
||||
if (!m) return
|
||||
if (m.open) board.openGroup(m.open)
|
||||
else bus.toggleFilterValue(m.key, m.value)
|
||||
}
|
||||
|
||||
useShadowStyles(
|
||||
'.gic{display:flex;flex-direction:column;height:100%;overflow-y:auto;box-sizing:border-box;padding:10px 12px 16px;background:#fff;color:#3b463d;font:13px/1.35 Roboto,-apple-system,"Segoe UI",sans-serif;scrollbar-width:thin}' +
|
||||
'.gic-seg{display:inline-flex;border:1px solid #d1d4cd;border-radius:6px;overflow:hidden;margin-bottom:8px}' +
|
||||
'.gic{display:flex;flex-direction:column;gap:4px;height:100%;overflow-y:auto;box-sizing:border-box;padding:10px 12px 16px;background:#fff;color:#3b463d;font:13px/1.35 Roboto,-apple-system,"Segoe UI",sans-serif;scrollbar-width:thin}' +
|
||||
// A flex column's children SHRINK by default: inside this scroll box they were squeezed instead of
|
||||
// scrolled, so the measure toggle collapsed to a 2px line and a caveat card was cut in half (rega).
|
||||
'.gic > *{flex-shrink:0}' +
|
||||
'.gic-seg{align-self:flex-start;display:inline-flex;border:1px solid #d1d4cd;border-radius:6px;overflow:hidden;margin-bottom:4px}' +
|
||||
'.gic-seg button{font:inherit;font-size:12px;padding:4px 12px;min-height:26px;border:0;background:#fff;color:#3b463d;cursor:pointer}' +
|
||||
'.gic-seg button[aria-pressed="true"]{background:#3b463d;color:#fff}' +
|
||||
'.gic-seg button:focus-visible{outline:2px solid #92b195;outline-offset:-2px}' +
|
||||
'.gic-seg{align-self:flex-start;order:-1}' +
|
||||
'.gic-card{border-bottom:1px solid #e6e8e3;padding:6px 0 12px;margin-bottom:6px}',
|
||||
'.gic-card{border-bottom:1px solid #e6e8e3;padding:4px 0 12px}' +
|
||||
'.gic-take{margin:4px 0 0;font-size:12.5px;font-weight:600;color:#3b463d}',
|
||||
'gi-charts',
|
||||
)
|
||||
</script>
|
||||
|
|
@ -74,32 +103,34 @@ useShadowStyles(
|
|||
<button type="button" :aria-pressed="measure === 'groups'" @click="measure = 'groups'">{{ T('gospelInternet.charts.groups', 'Groups') }}</button>
|
||||
<button type="button" :aria-pressed="measure === 'people'" @click="measure = 'people'">{{ T('gospelInternet.charts.people', 'People') }}</button>
|
||||
</div>
|
||||
<div class="gic-card" :style="{ order: platformsFirst ? 0 : 3 }">
|
||||
<BarChart :series="appSeries" :unit="unitCountries" :bar-height="18" :gap="6"
|
||||
:title="T('gospelInternet.charts.messagingTop', 'Top messaging app, by country in scope')"
|
||||
:caveats="[pmeta.ios_caveat, pmeta.ranking_caveat, pmeta.classification_caveat].filter(Boolean)"
|
||||
:empty-text="T('gospelInternet.grid.empty', 'No people groups match these filters.')"
|
||||
@bar-click="onApp" />
|
||||
<div class="gic-card" data-chart="q1">
|
||||
<BarChart :series="q1" :unit="unit" :bar-height="18" :gap="6" :label-width="130"
|
||||
:title="T('gospelInternet.charts.q1title', 'What do they have in their own language?')"
|
||||
:subtitle="T('gospelInternet.charts.q1sub', 'One bar per level of access, for the people groups shown.')"
|
||||
:empty-text="T('gospelInternet.grid.empty', 'No people groups match these filters.')" @bar-click="onBar" />
|
||||
<p class="gic-take">{{ q1take }}</p>
|
||||
</div>
|
||||
<div class="gic-card" :style="{ order: platformsFirst ? 0 : 4 }">
|
||||
<BarChart :series="socialSeries" :unit="unitCountries" :bar-height="18" :gap="6"
|
||||
:title="T('gospelInternet.charts.socialTop', 'Top social platform, by country in scope')"
|
||||
:caveats="[pmeta.social_caveat].filter(Boolean)"
|
||||
:empty-text="T('gospelInternet.grid.empty', 'No people groups match these filters.')" />
|
||||
<div class="gic-card" data-chart="gaps">
|
||||
<BarChart :series="gapSeries" :unit="T('gospelInternet.charts.unitPeople', 'people')" :bar-height="18" :gap="6" :label-width="130"
|
||||
:title="T('gospelInternet.charts.gapTitle', 'The largest people groups with nothing in their own language')"
|
||||
:subtitle="T('gospelInternet.charts.gapSub', 'The 8 biggest of those shown, by population. Click one to open it.')"
|
||||
:empty-text="T('gospelInternet.grid.empty', 'No people groups match these filters.')" @bar-click="onBar" />
|
||||
<p v-if="gaps.length" class="gic-take">{{ gapTake }}</p>
|
||||
</div>
|
||||
<div class="gic-card" style="order:1">
|
||||
<BarChart :series="stack" :legend="legend" :unit="unit" :bar-height="20" :gap="8"
|
||||
:title="T('gospelInternet.charts.accessByBand', 'Own-language access, by country internet use')"
|
||||
<div class="gic-card" data-chart="q2">
|
||||
<BarChart :series="q2" :unit="unit" :bar-height="18" :gap="6" :label-width="130"
|
||||
:title="T('gospelInternet.charts.q2title', 'Could the internet carry it to those with nothing?')"
|
||||
:subtitle="T('gospelInternet.charts.q2sub', 'People groups with nothing in their own language, by how much of their country is online.')"
|
||||
:caveats="[T('gospelInternet.charts.bandCaveat', 'Internet use is the country\'s figure (World Bank), never the group\'s.')]"
|
||||
:empty-text="T('gospelInternet.grid.empty', 'No people groups match these filters.')"
|
||||
@bar-click="onBar" />
|
||||
:empty-text="T('gospelInternet.grid.empty', 'No people groups match these filters.')" @bar-click="onBar" />
|
||||
<p v-if="zero.length" class="gic-take">{{ q2take }}</p>
|
||||
</div>
|
||||
<div class="gic-card" style="order:2">
|
||||
<BarChart :series="zero" :unit="unit" :bar-height="20" :gap="8" color="#7d3b30"
|
||||
:title="T('gospelInternet.charts.zeroByBand', 'Nothing in their own language, by country internet use')"
|
||||
:caveats="[T('gospelInternet.charts.zeroCaveat', 'Every one of these has a way in: a bridge language, a candidate or a Deaf bridge.')]"
|
||||
:empty-text="T('gospelInternet.grid.empty', 'No people groups match these filters.')"
|
||||
@bar-click="onBar" />
|
||||
<div class="gic-card" data-chart="ntNeed">
|
||||
<BarChart :series="ntSeries" :unit="T('gospelInternet.charts.unitPeople', 'people')" :bar-height="18" :gap="6" :label-width="130"
|
||||
:title="T('gospelInternet.charts.ntTitle', 'Which languages most need the New Testament?')"
|
||||
:subtitle="T('gospelInternet.charts.ntSub', 'Languages of the groups shown with no New Testament found, by how many people speak them. Red: nothing at all; yellow: something, but no New Testament.')"
|
||||
:empty-text="T('grid.loading', 'Loading…')" @bar-click="onBar" />
|
||||
<p v-if="ntNeed.length" class="gic-take">{{ ntTake }}</p>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
|||
import { useBoardState } from '../useBoard.js'
|
||||
import { useGiT } from '../useGiT.js'
|
||||
import { buildColumns } from '../columns.js'
|
||||
import { buildCsv, exportScope, safeHref, DETAIL_FILL, DETAIL_RIM, BAND_COLOR } from '../model.js'
|
||||
import { buildCsv, exportScope, safeHref, gospelResourcesLink, GROUPS_LINK_CAP, DETAIL_FILL, DETAIL_RIM, BAND_COLOR } from '../model.js'
|
||||
import { resolveShareUrl } from '@map/utils/embedBase.js'
|
||||
|
||||
const T = useGiT()
|
||||
const board = useBoardState()
|
||||
|
|
@ -108,6 +109,15 @@ function exportCsv() {
|
|||
status.value = T('gospelInternet.grid.exported', '{n} rows exported (copied and downloaded).', { n: n.toLocaleString() })
|
||||
setTimeout(() => { status.value = '' }, 4000)
|
||||
}
|
||||
// INTEROP OUT, as a link: the selected peoples, opened in Gospel Resources' map (its ?groups= contract).
|
||||
const gospelLink = computed(() => {
|
||||
void bus.selectionVersion.value
|
||||
if (!bus.selectionSet.size) return ''
|
||||
const cfg = board.profileConfig.value || {}
|
||||
const base = resolveShareUrl({ embedPath: 'gospel-resources/resources-map/index.html', profileConfig: { ...cfg, shareUrl: '' } })
|
||||
return gospelResourcesLink(base, bus.selectionSet, { list: board.listIsDefault.value ? '' : board.listId.value, lang: cfg.lang || cfg.locale || '' })
|
||||
})
|
||||
const gospelCapped = computed(() => { void bus.selectionVersion.value; return bus.selectionSet.size > GROUPS_LINK_CAP })
|
||||
const exportLabel = computed(() => selCount.value ? T('dashboard.export_selection', 'Export this selection') : T('dashboard.export_csv', 'Export CSV'))
|
||||
const fmt = (n) => (Number(n) || 0).toLocaleString()
|
||||
|
||||
|
|
@ -118,6 +128,7 @@ useShadowStyles(
|
|||
'.gig-search{flex:1 1 180px;max-width:320px;border:1px solid #d1d4cd;border-radius:6px;padding:5px 9px;font:inherit;font-size:12.5px;background:#fff;color:#3b463d}' +
|
||||
'.gig-btn{border:1px solid #d1d4cd;background:#fff;color:#3b463d;border-radius:6px;padding:4px 10px;min-height:28px;font:inherit;font-size:12.5px;cursor:pointer}' +
|
||||
'.gig-btn:hover{border-color:#92b195}' +
|
||||
'a.gig-btn{text-decoration:none;display:inline-flex;align-items:center}' +
|
||||
'.gig-btn.primary{background:#3b463d;border-color:#3b463d;color:#fff}' +
|
||||
'.gig-btn:focus-visible,.gig-th button:focus-visible,.gig-tr:focus-visible{outline:2px solid #92b195;outline-offset:1px}' +
|
||||
'.gig-status{font-size:12px;color:#4f6b52}' +
|
||||
|
|
@ -157,6 +168,8 @@ useShadowStyles(
|
|||
:placeholder="T('grid.search_people_or_countries', 'Search people groups or countries…')"
|
||||
:aria-label="T('grid.search_people_or_countries', 'Search people groups or countries…')" />
|
||||
<button v-if="selCount" type="button" class="gig-btn" @click="bus.clearSelection()">{{ T('buttons.clearSelection', 'Clear selection') }} ({{ fmt(selCount) }})</button>
|
||||
<a v-if="gospelLink" class="gig-btn" :href="gospelLink" target="_blank" rel="noopener" data-open-gospel
|
||||
:title="gospelCapped ? T('gospelInternet.grid.openCapped', 'Opens the first {n} selected.', { n: fmt(GROUPS_LINK_CAP) }) : ''">{{ T('gospelInternet.grid.openInGospel', 'Open in Gospel Resources') }}</a>
|
||||
<span class="gig-status" role="status">{{ status }}</span>
|
||||
<span style="flex:1"></span>
|
||||
<div class="gig-picker">
|
||||
|
|
|
|||
|
|
@ -94,11 +94,18 @@ export function parseQuery(search) {
|
|||
if (vals.length) filters[key] = vals.length === 1 ? vals[0] : vals
|
||||
}
|
||||
const view = VIEWS.includes(q.get('view')) ? q.get('view') : 'board'
|
||||
// EMBED-CONTRACT PARITY with Gospel Resources (resources-map.vue:341/354): ?codes= (and ?code=) name
|
||||
// LANGUAGES by ISO 639-3, ?groups= names PEOPLES by DOXA slug. Comma-separated there, so accepted with
|
||||
// ',' as well as '|' here (neither can occur inside a slug or a code).
|
||||
const list = (name) => (q.get(name) || '').split(/[,|]/).map((s) => s.trim().toLowerCase()).filter(Boolean)
|
||||
const codes = [...list('codes'), ...list('code')].filter((c) => /^[a-z]{3}$/.test(c))
|
||||
if (codes.length && !filters.language) filters.language = codes.length === 1 ? codes[0] : codes
|
||||
const groups = list('groups').filter((s) => /^[a-z0-9-]+$/.test(s))
|
||||
const panel = q.get('panel') === 'tree' ? 'tree' : 'map'
|
||||
const tree = /^[a-z0-9-]{3,40}$/.test(q.get('tree') || '') ? q.get('tree') : ''
|
||||
// A tree node narrows by LANGUAGE (its ISO codes); the URL carries the node, not the code list.
|
||||
if (tree) delete filters.language
|
||||
return { view, filters, group: q.get('group') || '', panel, tree }
|
||||
return { view, filters, group: q.get('group') || '', panel, tree, groups }
|
||||
}
|
||||
|
||||
/** The inverse: filterSet + view + group -> a query string (no leading '?'), stable key order. */
|
||||
|
|
@ -300,6 +307,22 @@ export function exportScope(rows, filtered, selectionSet) {
|
|||
return { scope: filtered.length === rows.length ? 'all' : 'filter', rows: filtered }
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a selection goes next: Gospel Resources' map, narrowed to exactly these peoples (its ?groups=
|
||||
* contract, by DOXA slug). A URL is the interop link that works across bundles, pages and partner sites
|
||||
* alike. Capped: a link past a few hundred slugs outgrows what browsers and servers accept, so the first
|
||||
* GROUPS_LINK_CAP go and the caller says so. `base` is the tree's embed base (resolveShareUrl).
|
||||
*/
|
||||
export const GROUPS_LINK_CAP = 300
|
||||
export function gospelResourcesLink(base, slugs, { list = '', lang = '' } = {}) {
|
||||
const pick = [...slugs].slice(0, GROUPS_LINK_CAP)
|
||||
const q = new URLSearchParams()
|
||||
if (pick.length) q.set('groups', pick.join(','))
|
||||
if (list) q.set('list', list)
|
||||
if (lang && lang !== 'en') q.set('lang', lang)
|
||||
return `${base}${base.includes('?') ? '&' : '?'}${q.toString()}`
|
||||
}
|
||||
|
||||
/** A safe outbound URL: http(s) only (a javascript: or data: link in a table cell is never followed).
|
||||
* Not library/utils/html.js safeUrl: that one HTML-ESCAPES its result for string-built popups, and a
|
||||
* Vue :href binding escapes again, so '&' would reach the browser as '&amp;'. Every link in this
|
||||
|
|
|
|||
|
|
@ -153,12 +153,17 @@ export function useBoard() {
|
|||
if (group) pendingGroup = group
|
||||
const panel = q.panel === 'tree' || cfg().panel === 'tree' ? 'tree' : 'map'
|
||||
if (q.tree) pendingTree = q.tree
|
||||
if (q.groups && q.groups.length) pendingSelection = q.groups
|
||||
setCentre(panel)
|
||||
}
|
||||
let pendingGroup = ''
|
||||
let pendingSelection = null
|
||||
watch(all, () => {
|
||||
if (pendingGroup && bySlug.value.has(pendingGroup)) openGroup(pendingGroup)
|
||||
pendingGroup = ''
|
||||
// ?groups= arrives SELECTED (ringed, framed by the camera), the same peoples Gospel Resources would
|
||||
// draw for that link; slugs not on this list are dropped, never guessed
|
||||
if (pendingSelection) { bus.setSelection(pendingSelection.filter((s) => bySlug.value.has(s))); pendingSelection = null }
|
||||
})
|
||||
function syncUrl() {
|
||||
if (!writeUrl.value || typeof history === 'undefined') return
|
||||
|
|
|
|||
|
|
@ -915,6 +915,7 @@ export default defineComponent({
|
|||
const useModel = (m) => {
|
||||
this.manifestBundles = (m && m.bundles) || []
|
||||
this.rows = this.rowsFromManifest(m)
|
||||
this.setupWidgets()
|
||||
this.route()
|
||||
}
|
||||
const baked = (typeof window !== 'undefined' && window.__psBaked) || null
|
||||
|
|
@ -961,6 +962,9 @@ export default defineComponent({
|
|||
cap,
|
||||
icon: CAP_ICON[cap] || 'embed',
|
||||
description: p.description || '',
|
||||
// CORNER WIDGET (the Driver, BUILD-CORNER-WIDGET-CLASS): a profile whose .meta.json declares
|
||||
// "widget": { "corner": ... } is mounted once in a fixed corner of the Studio, not in a pane.
|
||||
widget: (p.widget && ['bottom-right', 'bottom-left', 'top-right', 'top-left'].includes(p.widget.corner)) ? p.widget.corner : '',
|
||||
heavy,
|
||||
bundleJs: b.scriptUrl || (b.bundle + '/' + b.bundle + '.js'),
|
||||
embed: (p.embed && p.embed.script) || '',
|
||||
|
|
@ -1017,6 +1021,10 @@ export default defineComponent({
|
|||
// guard would blank the content pane (flatRows has no matching entry to render) with the "Choose
|
||||
// an element" hint also gone — a real, if minor, regression a synthetic row must never cause.
|
||||
if (!this.rows.some(r => r.key === key)) return
|
||||
// A corner widget is never mounted in a pane (one live instance, in its corner): picking it in the
|
||||
// rail opens its corner instead.
|
||||
const wrow = this.rows.find(r => r.key === key)
|
||||
if (wrow && wrow.widget) { this.openWidget(wrow, true); return }
|
||||
const prevKey = this.activeKey
|
||||
this.activeKey = key
|
||||
this.embedOpen = false
|
||||
|
|
@ -1050,6 +1058,73 @@ export default defineComponent({
|
|||
row.state = 'idle'
|
||||
row.error = ''
|
||||
},
|
||||
// ── CORNER WIDGETS ────────────────────────────────────────────────────────────────────────────────
|
||||
// One fixed host per corner, created on document.body and kept in a PAGE-LEVEL singleton
|
||||
// (window.__doxaStudioWidgets), outside Vue's tree: a Studio hot update or remount finds the same host
|
||||
// and the same mounted element, so a widget like Rudder keeps its live state across HMR. Collapsed to a
|
||||
// button by default; the open state is remembered per widget.
|
||||
widgetStore() {
|
||||
if (!window.__doxaStudioWidgets) window.__doxaStudioWidgets = { hosts: {}, els: {} }
|
||||
return window.__doxaStudioWidgets
|
||||
},
|
||||
widgetHost(corner) {
|
||||
const W = this.widgetStore()
|
||||
if (W.hosts[corner] && W.hosts[corner].isConnected) return W.hosts[corner]
|
||||
const h = document.createElement('div')
|
||||
h.setAttribute('data-studio-widgets', corner)
|
||||
const [v, x] = corner.split('-')
|
||||
h.style.cssText = `position:fixed;${v}:12px;${x}:12px;z-index:2147483647;display:flex;flex-direction:column;align-items:${x === 'right' ? 'flex-end' : 'flex-start'};gap:8px;`
|
||||
document.body.appendChild(h)
|
||||
W.hosts[corner] = h
|
||||
return h
|
||||
},
|
||||
setupWidgets() {
|
||||
for (const row of this.rows.filter(r => r.widget)) {
|
||||
const W = this.widgetStore()
|
||||
const host = this.widgetHost(row.widget)
|
||||
let box = host.querySelector(`[data-widget="${row.key}"]`)
|
||||
if (!box) {
|
||||
box = document.createElement('div')
|
||||
box.setAttribute('data-widget', row.key)
|
||||
box.style.cssText = 'display:flex;flex-direction:column;align-items:inherit;gap:6px;'
|
||||
const panel = document.createElement('div')
|
||||
panel.setAttribute('data-widget-panel', '')
|
||||
panel.style.cssText = 'display:none;width:min(380px,calc(100vw - 24px));height:min(520px,calc(100vh - 80px));position:relative;background:#fff;border-radius:10px;box-shadow:0 8px 30px rgba(0,0,0,.25);overflow:hidden;'
|
||||
const btn = document.createElement('button')
|
||||
btn.type = 'button'
|
||||
btn.textContent = row.label
|
||||
btn.setAttribute('aria-expanded', 'false')
|
||||
btn.style.cssText = 'font:600 13px system-ui,sans-serif;padding:8px 14px;border-radius:999px;border:1px solid #30363d;background:#0d1117;color:#e6edf3;cursor:pointer;box-shadow:0 2px 10px rgba(0,0,0,.25);'
|
||||
btn.addEventListener('click', () => this.openWidget(row, panel.style.display === 'none'))
|
||||
box.appendChild(panel); box.appendChild(btn); host.appendChild(box)
|
||||
}
|
||||
if (W.els[row.key] && !box.querySelector(row.tag)) box.querySelector('[data-widget-panel]').appendChild(W.els[row.key])
|
||||
let open = false
|
||||
try { open = localStorage.getItem('plugin-studio:widget-open:' + row.key) === '1' } catch (_) {}
|
||||
if (open) this.openWidget(row, true)
|
||||
}
|
||||
},
|
||||
openWidget(row, open) {
|
||||
const host = this.widgetHost(row.widget)
|
||||
const box = host.querySelector(`[data-widget="${row.key}"]`)
|
||||
if (!box) { this.setupWidgets(); return }
|
||||
const panel = box.querySelector('[data-widget-panel]')
|
||||
const btn = box.querySelector('button')
|
||||
panel.style.display = open ? 'block' : 'none'
|
||||
btn.setAttribute('aria-expanded', open ? 'true' : 'false')
|
||||
try { localStorage.setItem('plugin-studio:widget-open:' + row.key, open ? '1' : '0') } catch (_) {}
|
||||
const W = this.widgetStore()
|
||||
if (!open || W.els[row.key]) return
|
||||
loadScriptOnce(this.resolve(row.bundleJs)).then(() => whenTagDefined(row.tag)).then(() => {
|
||||
if (W.els[row.key]) return
|
||||
const el = document.createElement(row.tag)
|
||||
el.setAttribute('profile-config', JSON.stringify(this.paneConfig(row, this.locale)))
|
||||
if (this.base) el.setAttribute('tree-base', this.base.replace(/\/+$/, ''))
|
||||
el.style.cssText = 'display:block;position:absolute;inset:0;width:100%;height:100%;'
|
||||
W.els[row.key] = el
|
||||
panel.appendChild(el)
|
||||
}).catch((e) => { panel.textContent = (e && e.message) || String(e) })
|
||||
},
|
||||
mountElement(row) {
|
||||
row.mounted = true
|
||||
row.state = 'loading'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup>
|
||||
import { dataUrl, glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
|
||||
import { usePplrT } from '../composables/usePplrT.js'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
/**
|
||||
* pplr-coverage.vue — Country choropleth: avg # of locales filled per PG in that country.
|
||||
*
|
||||
|
|
@ -56,7 +57,7 @@ onMounted(async () => {
|
|||
if (!map.value) {
|
||||
// The map did not construct (e.g. provider=mapbox with no token; useMapInstance logs why). Say so
|
||||
// and stop: everything below needs a map, and map.value.on() threw into the mounted hook.
|
||||
status.value = t('messages.mapUnavailable')
|
||||
status.value = tStatic('messages.mapUnavailable') // a shared key: usePplrT would prefix it with pplr.
|
||||
return
|
||||
}
|
||||
map.value.on('load', async () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup>
|
||||
import { dataUrl, glFor, exposeMap, whenGeojsonDrawn, setProjectionFor } from '../utils/pplrData.js'
|
||||
import { usePplrT } from '../composables/usePplrT.js'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
/**
|
||||
* pplr-language-families.vue — PG pins colored by PPLR ethnolinguistic family.
|
||||
*
|
||||
|
|
@ -432,7 +433,7 @@ onMounted(async () => {
|
|||
if (!map.value) {
|
||||
// The map did not construct (e.g. provider=mapbox with no token; useMapInstance logs why). Say so
|
||||
// and stop: everything below needs a map, and map.value.on() threw into the mounted hook.
|
||||
status.value = t('messages.mapUnavailable')
|
||||
status.value = tStatic('messages.mapUnavailable') // a shared key: usePplrT would prefix it with pplr.
|
||||
return
|
||||
}
|
||||
if (map.value) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { inject, onMounted, onBeforeUnmount, ref } from 'vue'
|
||||
import { glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
|
||||
import { usePplrT } from '../composables/usePplrT.js'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
import { useMapInstance } from '@map/composables/useMapInstance.js'
|
||||
import { addCountryPolygons, addPGPoints, popupForPG, POPUP_CSS } from '../utils/pplrMap.js'
|
||||
import LoadingGlobe from '../components/LoadingGlobe.vue'
|
||||
|
|
@ -40,7 +41,7 @@ onMounted(async () => {
|
|||
if (!map.value) {
|
||||
// The map did not construct (e.g. provider=mapbox with no token; useMapInstance logs why). Say so
|
||||
// and stop: everything below needs a map, and map.value.on() threw into the mounted hook.
|
||||
status.value = t('messages.mapUnavailable')
|
||||
status.value = tStatic('messages.mapUnavailable') // a shared key: usePplrT would prefix it with pplr.
|
||||
return
|
||||
}
|
||||
map.value.on('load', async () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<script setup>
|
||||
import { dataUrl, glFor, exposeMap, whenGeojsonDrawn, setProjectionFor } from '../utils/pplrData.js'
|
||||
import { usePplrT } from '../composables/usePplrT.js'
|
||||
import { religionFamilyLabel } from '@map/colors/religion.js'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
/**
|
||||
* pplr-religion.vue — PG pins colored by primary religion.
|
||||
*
|
||||
|
|
@ -137,6 +139,12 @@ const RELIGION_GROUPS = [
|
|||
{ key: 'other', label: 'Other / Unknown', color: '#4b5563' }, // dark gray
|
||||
]
|
||||
|
||||
// Shown in the reader's language through the shared religion catalogue; the key and the filter stay
|
||||
// English. "Other / Unknown" is one bucket here, so it is named from both catalogue entries.
|
||||
const groupLabel = (g) => (g.key === 'other'
|
||||
? `${religionFamilyLabel('Other')} / ${religionFamilyLabel('Unknown')}`
|
||||
: religionFamilyLabel(g.label))
|
||||
|
||||
const COLOR_EXPR = [
|
||||
'match', ['get', 'rg'],
|
||||
...RELIGION_GROUPS.flatMap(g => [g.key, g.color]),
|
||||
|
|
@ -147,7 +155,7 @@ const COLOR_EXPR = [
|
|||
const religionCounts = ref({})
|
||||
const religionLegendItems = computed(() => RELIGION_GROUPS.map(g => ({
|
||||
key: g.key,
|
||||
label: g.label,
|
||||
label: groupLabel(g),
|
||||
color: g.color,
|
||||
count: religionCounts.value[g.key] || 0,
|
||||
filter: ['==', ['get', 'rg'], g.key],
|
||||
|
|
@ -166,7 +174,7 @@ function onLegendSelect(node) {
|
|||
const religionLocalIndex = computed(() => {
|
||||
const out = RELIGION_GROUPS.map((g, i) => ({
|
||||
id: `rel:${g.key}`,
|
||||
label: g.label,
|
||||
label: groupLabel(g),
|
||||
color: g.color,
|
||||
kind: 'religion',
|
||||
depth: 0,
|
||||
|
|
@ -250,7 +258,7 @@ onMounted(async () => {
|
|||
if (!map.value) {
|
||||
// The map did not construct (e.g. provider=mapbox with no token; useMapInstance logs why). Say so
|
||||
// and stop: everything below needs a map, and map.value.on() threw into the mounted hook.
|
||||
status.value = t('messages.mapUnavailable')
|
||||
status.value = tStatic('messages.mapUnavailable') // a shared key: usePplrT would prefix it with pplr.
|
||||
return
|
||||
}
|
||||
if (map.value) {
|
||||
|
|
@ -279,7 +287,7 @@ onMounted(async () => {
|
|||
religionCounts.value = counts
|
||||
const dominant = RELIGION_GROUPS.reduce((a, b) => (counts[a.key] || 0) > (counts[b.key] || 0) ? a : b)
|
||||
// Per user 2026-05-02: drop the "Ready" prefix — it was unclear and chewed up width.
|
||||
status.value = t('pplr.status.readyReligion', { pins: pgData.value.features.length.toLocaleString(), religion: dominant.label, count: (counts[dominant.key]||0).toLocaleString() })
|
||||
status.value = t('pplr.status.readyReligion', { pins: pgData.value.features.length.toLocaleString(), religion: groupLabel(dominant), count: (counts[dominant.key]||0).toLocaleString() })
|
||||
// keep the LoadingGlobe up until the pins are actually drawn, not merely added
|
||||
await whenGeojsonDrawn(map.value)
|
||||
dataReady.value = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup>
|
||||
import { dataUrl, glFor, exposeMap, setProjectionFor } from '../utils/pplrData.js'
|
||||
import { usePplrT } from '../composables/usePplrT.js'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
/**
|
||||
* pplr-rop-tree.vue — ROP hierarchy: Affinity Bloc → Cluster → People Group → PGIC.
|
||||
*
|
||||
|
|
@ -399,7 +400,7 @@ onMounted(async () => {
|
|||
if (!map.value) {
|
||||
// The map did not construct (e.g. provider=mapbox with no token; useMapInstance logs why). Say so
|
||||
// and stop: everything below needs a map, and map.value.on() threw into the mounted hook.
|
||||
status.value = t('messages.mapUnavailable')
|
||||
status.value = tStatic('messages.mapUnavailable') // a shared key: usePplrT would prefix it with pplr.
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
import { inject, onMounted, onBeforeUnmount, ref } from 'vue'
|
||||
import { glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
|
||||
import { usePplrT } from '../composables/usePplrT.js'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
import { useMapInstance } from '@map/composables/useMapInstance.js'
|
||||
import { addCountryPolygons, loadOnce, popupForPG, POPUP_CSS, escHtml } from '../utils/pplrMap.js'
|
||||
import LoadingGlobe from '../components/LoadingGlobe.vue'
|
||||
|
|
@ -48,7 +49,7 @@ onMounted(async () => {
|
|||
if (!map.value) {
|
||||
// The map did not construct (e.g. provider=mapbox with no token; useMapInstance logs why). Say so
|
||||
// and stop: everything below needs a map, and map.value.on() threw into the mounted hook.
|
||||
status.value = t('messages.mapUnavailable')
|
||||
status.value = tStatic('messages.mapUnavailable') // a shared key: usePplrT would prefix it with pplr.
|
||||
return
|
||||
}
|
||||
map.value.on('load', async () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup>
|
||||
import { dataUrl, glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
|
||||
import { usePplrT } from '../composables/usePplrT.js'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
/**
|
||||
* pplr-translations.vue — PG name in all 17 locales.
|
||||
*
|
||||
|
|
@ -62,7 +63,7 @@ onMounted(async () => {
|
|||
if (!map.value) {
|
||||
// The map did not construct (e.g. provider=mapbox with no token; useMapInstance logs why). Say so
|
||||
// and stop: everything below needs a map, and map.value.on() threw into the mounted hook.
|
||||
status.value = t('messages.mapUnavailable')
|
||||
status.value = tStatic('messages.mapUnavailable') // a shared key: usePplrT would prefix it with pplr.
|
||||
return
|
||||
}
|
||||
if (map.value) {
|
||||
|
|
|
|||
55
plugin-designs/rudder-voice/README.md
Normal file
55
plugin-designs/rudder-voice/README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# rudder-voice — Rudder, private voice-to-text (desktop target)
|
||||
|
||||
**Rudder is a plugin that isn't a map.** It shows that the same dx-maps pattern — one folder in
|
||||
`plugin-designs/`, developed live in the Plugin Studio — can ship a **desktop app with its own local
|
||||
backend**, not only embeddable maps. It is also a genuinely useful tool for the people who build here:
|
||||
talk to your agents instead of typing, with nothing leaving your computer.
|
||||
|
||||
- **Streaming.** Words appear in the text box while you speak; talk for as long as you like.
|
||||
- **Type and talk at once.** Type, paste links or code anywhere in the box; everything you say is
|
||||
added to the end. While you're typing, Rudder holds its words and adds them when you pause.
|
||||
- **CPU only.** No graphics card. About 1 GB of memory; keeps up with normal speech on a laptop CPU.
|
||||
- **Offline, no telemetry.** NVIDIA Nemotron 3.5 ASR Streaming (0.6B) run by NVIDIA's open-source
|
||||
NeMo-Speech.cpp (ggml). Traced with `strace` while transcribing: zero sockets, zero DNS lookups.
|
||||
- **Microphones like a video-call app.** Pick the input, hot-plug updates, a 3-second mic test, and
|
||||
plain-language errors (muted, disconnected, dead Bluetooth mic link — which it tries to recover).
|
||||
|
||||
## Run it in the Plugin Studio
|
||||
|
||||
```bash
|
||||
bash backend/rudder-engine/setup-runtime.sh # once: runtime + model (verified by SHA-256) + engine build
|
||||
bun run dev # starts the engine too; open "rudder-voice" in the studio
|
||||
```
|
||||
|
||||
Linux with PipeWire or PulseAudio (`parec`, `pactl`). The engine listens on `127.0.0.1` only.
|
||||
|
||||
## How it fits together
|
||||
|
||||
```
|
||||
<rudder-voice> (this folder) ──HTTP + SSE /api/rudder──► backend/rudder-engine (Go)
|
||||
├─ parec / pw-record (mic, open only while recording)
|
||||
└─ WebSocket ► nemo-speech serve (127.0.0.1, model stays loaded)
|
||||
```
|
||||
|
||||
`bun run dev` starts `backend/bin/rudder-engine` through `internal/backend-dev-plugin.mjs` and forwards
|
||||
`/api/rudder` to it, so the plugin calls the same API in the browser as it will inside the desktop app,
|
||||
where Tauri runs the engine as a sidecar and passes its address in `profile-config` (`apiBase`).
|
||||
|
||||
`rudder-voice.meta.json` declares `"targets": ["desktop"]`: on a plain website there is no local engine,
|
||||
so this plugin is not meant for web embeds.
|
||||
|
||||
## Settings (environment variables)
|
||||
|
||||
| Variable | Default | What it does |
|
||||
|---|---|---|
|
||||
| `RUDDER_PORT` | `5193` | engine HTTP port (loopback) |
|
||||
| `RUDDER_NEMO_PORT` | `5194` | speech server port (loopback) |
|
||||
| `RUDDER_MODEL` / `RUDDER_NEMO_BIN` | `~/.local/share/rudder/...` | model and runtime paths; the engine refuses a model that isn't a local file |
|
||||
| `RUDDER_LANG` | `en-US` | default language (the model supports ~30) |
|
||||
| `RUDDER_KEEP_MSBC` | unset | keep Bluetooth headsets on mSBC instead of switching them to the more stable CVSD |
|
||||
| `RUDDER_BT_RESET` | unset | optional command that fully resets a Bluetooth headset, used when its mic link dies |
|
||||
|
||||
## Not yet
|
||||
|
||||
- Desktop build (`desktop:build`) with the engine as a Tauri sidecar.
|
||||
- A saved word-correction table (PocketBase) for technical terms the model mishears.
|
||||
32
plugin-designs/rudder-voice/index.html
Normal file
32
plugin-designs/rudder-voice/index.html
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>doxa-maps · rudder-voice staging</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { background: #0b0f14; color: #e5e7eb; min-height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; }
|
||||
.wrap { max-width: 960px; margin: 0 auto; padding: 28px 20px 48px; }
|
||||
h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
|
||||
.sub { color: #9ca3af; font-size: 13px; margin: 6px 0 24px; max-width: 820px; }
|
||||
.sub code { color: #c4b5fd; }
|
||||
.host { border: 1px solid #1f2937; border-radius: 12px; overflow: hidden; }
|
||||
rudder-voice { display: block; width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>rudder-voice · staging</h1>
|
||||
<p class="sub">Desktop-target plugin: private, offline, streaming voice-to-text. The speech model runs in the
|
||||
local backend <code>backend/rudder-engine</code> (NVIDIA NeMo-Speech.cpp + Nemotron 3.5 ASR), started by
|
||||
<code>bun run dev</code> and reached at <code>/api/rudder</code>.</p>
|
||||
<div class="host">
|
||||
<rudder-voice></rudder-voice>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
plugin-designs/rudder-voice/index.js
Normal file
23
plugin-designs/rudder-voice/index.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* rudder-voice / index.js — Bundle entry (Capability-1 single element, DESKTOP target).
|
||||
*
|
||||
* <rudder-voice> — private, offline, streaming voice-to-text. The UI is this bundle; the speech model
|
||||
* runs in the local backend (backend/rudder-engine), started by `bun run dev` in the Plugin Studio and
|
||||
* by Tauri as a sidecar in the desktop app. See rudder-voice.vue for the contract.
|
||||
*
|
||||
* Desktop-only: rudder-voice.meta.json declares "targets": ["desktop"]. On a plain website there is no
|
||||
* local engine, so the element shows "Engine offline" instead of pretending to work.
|
||||
*
|
||||
* Usage (desktop shell / Plugin Studio):
|
||||
* <rudder-voice profile-config='{"apiBase":"http://127.0.0.1:5193/api/rudder"}'></rudder-voice>
|
||||
*/
|
||||
import { defineCustomElement } from 'vue'
|
||||
import RudderVoice from './rudder-voice.vue'
|
||||
|
||||
const RudderVoiceElement = defineCustomElement(RudderVoice)
|
||||
|
||||
if (typeof customElements !== 'undefined') {
|
||||
if (!customElements.get('rudder-voice')) customElements.define('rudder-voice', RudderVoiceElement)
|
||||
}
|
||||
|
||||
export default RudderVoiceElement
|
||||
8
plugin-designs/rudder-voice/rudder-voice.meta.json
Normal file
8
plugin-designs/rudder-voice/rudder-voice.meta.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"_comment": "Rudder — private, offline, streaming voice-to-text. DESKTOP target: the speech model runs in the local backend (backend/rudder-engine: NVIDIA NeMo-Speech.cpp + Nemotron 3.5 ASR GGUF), started by `bun run dev` and run by Tauri as a sidecar. Not for public web embeds: a website has no local engine.",
|
||||
"type": "single",
|
||||
"widget": { "corner": "bottom-right" },
|
||||
"label": "Rudder — voice to text (desktop)",
|
||||
"targets": ["desktop"],
|
||||
"backend": "backend/rudder-engine"
|
||||
}
|
||||
656
plugin-designs/rudder-voice/rudder-voice.vue
Normal file
656
plugin-designs/rudder-voice/rudder-voice.vue
Normal file
|
|
@ -0,0 +1,656 @@
|
|||
<script setup>
|
||||
/**
|
||||
* rudder-voice.vue — Rudder: private, offline, streaming voice-to-text.
|
||||
*
|
||||
* @element rudder-voice
|
||||
* @description A DESKTOP-target plugin (rudder-voice.meta.json "targets": ["desktop"]). The speech
|
||||
* model runs in the local backend (backend/rudder-engine → NVIDIA NeMo-Speech.cpp + Nemotron 3.5
|
||||
* ASR), never in the browser and never on a server. In the Plugin Studio, `bun run dev` starts that
|
||||
* backend and forwards /api/rudder; in the desktop app, Tauri runs it as a sidecar and passes its
|
||||
* address in `profile-config` {"apiBase": "http://127.0.0.1:<port>/api/rudder"}.
|
||||
*
|
||||
* Everything is agent-interfaceable (the old Rudder's prime directive): the 🐞 panel copies the
|
||||
* engine log + status with a header telling an agent what to do with it.
|
||||
*/
|
||||
import { computed, onBeforeUnmount, onMounted, ref, nextTick } from 'vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = defineProps({ profileConfig: { type: String, default: '' } })
|
||||
|
||||
const cfg = computed(() => {
|
||||
try { return props.profileConfig ? JSON.parse(props.profileConfig) : {} } catch { return {} }
|
||||
})
|
||||
const apiBase = computed(() => (cfg.value.apiBase || '/api/rudder').replace(/\/$/, ''))
|
||||
|
||||
const LANGUAGES = [
|
||||
['en-US', 'English (US)'], ['en-GB', 'English (UK)'], ['es-US', 'Spanish (US)'], ['es-ES', 'Spanish (Spain)'],
|
||||
['fr-FR', 'French'], ['de-DE', 'German'], ['it-IT', 'Italian'], ['pt-BR', 'Portuguese (Brazil)'],
|
||||
['nl-NL', 'Dutch'], ['ru-RU', 'Russian'], ['uk-UA', 'Ukrainian'], ['tr-TR', 'Turkish'], ['ar-AR', 'Arabic'],
|
||||
['hi-IN', 'Hindi'], ['ja-JP', 'Japanese'], ['ko-KR', 'Korean'], ['vi-VN', 'Vietnamese'], ['zh-CN', 'Mandarin'],
|
||||
]
|
||||
|
||||
const STORE_KEY = 'rudder-voice:v1'
|
||||
function load() {
|
||||
try { return JSON.parse(localStorage.getItem(STORE_KEY) || '{}') } catch { return {} }
|
||||
}
|
||||
function save() {
|
||||
try { localStorage.setItem(STORE_KEY, JSON.stringify({ language: language.value, device: device.value, draft: draft.value })) } catch {}
|
||||
}
|
||||
const saved = load()
|
||||
|
||||
const engine = ref('connecting') // connecting | starting | ready | error | offline
|
||||
const engineError = ref('')
|
||||
const runtime = ref('')
|
||||
const model = ref('')
|
||||
const recording = ref(false)
|
||||
const busy = ref(false)
|
||||
const language = ref(saved.language || cfg.value.language || 'en-US')
|
||||
// The draft: a text area you type and paste into; every finished utterance is APPENDED to its end.
|
||||
// (v1 kept a list of lines — migrate it so nothing already dictated is lost.)
|
||||
const draft = ref(typeof saved.draft === 'string' ? saved.draft
|
||||
: Array.isArray(saved.lines) ? saved.lines.map((l) => l.text).join(' ') : '')
|
||||
const partial = ref('') // the utterance being heard right now (streamed into the box's live tail)
|
||||
const box = ref(null)
|
||||
const level = ref(0)
|
||||
const notice = ref('')
|
||||
const noticeKind = ref('info')
|
||||
const copied = ref('')
|
||||
const showDebug = ref(false)
|
||||
const showInfo = ref(false)
|
||||
const clientLog = ref([])
|
||||
|
||||
// ─── Microphones (the Jitsi device-selection pattern, served natively by the engine) ───
|
||||
const devices = ref([]) // [{id,label,bus,default,muted,volume,state}]
|
||||
const offCards = ref([]) // sound cards with a mic that are switched off (e.g. the laptop's own)
|
||||
const deviceError = ref('')
|
||||
const device = ref(saved.device || '') // '' = follow the system default input
|
||||
const testing = ref(false)
|
||||
const testResult = ref(null) // {ok, verdict, peak}
|
||||
|
||||
const BUS_ICON = { bluetooth: '🎧', usb: '🔌', pci: '💻' }
|
||||
function deviceLabel(d) {
|
||||
const bits = [(BUS_ICON[d.bus] || '🎙') + ' ' + d.label]
|
||||
if (d.muted) bits.push('(muted)')
|
||||
else if (d.volume) bits.push('· ' + d.volume)
|
||||
return bits.join(' ')
|
||||
}
|
||||
const defaultDevice = computed(() => devices.value.find((d) => d.default))
|
||||
const chosen = computed(() => devices.value.find((d) => d.id === device.value) || (device.value ? null : defaultDevice.value))
|
||||
|
||||
function applyDevices(list) {
|
||||
if (!list) return
|
||||
devices.value = list.devices || []
|
||||
offCards.value = list.offCards || []
|
||||
deviceError.value = list.error || ''
|
||||
// A saved choice that is gone (headset switched off) falls back to the default, and says so.
|
||||
if (device.value && !devices.value.some((d) => d.id === device.value)) {
|
||||
if (!recording.value) {
|
||||
say('Your chosen microphone is no longer connected — using the system default instead.', 'warn')
|
||||
device.value = ''
|
||||
save()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshDevices() {
|
||||
try { applyDevices(await (await fetch(apiBase.value + '/devices')).json()) } catch (err) { logc('devices fetch failed: ' + err) }
|
||||
}
|
||||
|
||||
async function testMic() {
|
||||
if (testing.value || recording.value) return
|
||||
testing.value = true
|
||||
testResult.value = null
|
||||
try {
|
||||
testResult.value = await post('/test', { device: device.value })
|
||||
} catch (err) {
|
||||
testResult.value = { ok: false, verdict: String(err.message || err) }
|
||||
} finally {
|
||||
testing.value = false
|
||||
level.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
let es = null
|
||||
let retry = null
|
||||
|
||||
function logc(msg) {
|
||||
clientLog.value.push(new Date().toISOString().slice(11, 23) + ' ' + msg)
|
||||
if (clientLog.value.length > 200) clientLog.value.splice(0, clientLog.value.length - 200)
|
||||
}
|
||||
|
||||
function say(msg, kind = 'info') { notice.value = msg; noticeKind.value = kind }
|
||||
|
||||
// ─── Writing into the box ────────────────────────────────────────────────────────────────────────
|
||||
// Every word streams straight into the END of the box while you keep typing, pasting and editing
|
||||
// anywhere. The sentence in progress lives in a "live tail" — the last `live.text.length` characters
|
||||
// of the draft — and each new word rewrites only that tail.
|
||||
//
|
||||
// ★ TYPING WHILE TALKING (bug fixed 2026-09-27): once you type into or after the tail it is yours, and
|
||||
// Rudder must continue with ONLY the words it hasn't written yet. v1 re-wrote the whole sentence-so-far
|
||||
// after every keystroke, so each key press duplicated the utterance. Now `written` counts the words of
|
||||
// this utterance already in the box, and the finished (punctuated) sentence is lined up word-by-word
|
||||
// against them, so nothing is ever written twice.
|
||||
let live = null // { start, text, shown } — the tail Rudder may still rewrite; shown = words in it
|
||||
let written = 0 // words of the current utterance committed to the box before the current tail
|
||||
|
||||
// The tail is still Rudder's while the box ENDS with it — wherever it now starts. Typing or pasting
|
||||
// anywhere above it shifts its position, and that must not count as touching it (bug 2026-09-27: a
|
||||
// position-based check released the tail on every keystroke above it, cutting "speaking" to "spe").
|
||||
function liveIntact() {
|
||||
if (!live || !draft.value.endsWith(live.text)) return false
|
||||
live.start = draft.value.length - live.text.length
|
||||
return true
|
||||
}
|
||||
|
||||
// Change only the end of the draft, keeping the person's caret/selection and scroll where they were.
|
||||
function writeEnd(next) {
|
||||
const el = box.value
|
||||
const focused = el && el.getRootNode().activeElement === el
|
||||
const selStart = el ? el.selectionStart : 0
|
||||
const selEnd = el ? el.selectionEnd : 0
|
||||
const atBottom = !el || el.scrollHeight - el.scrollTop - el.clientHeight < 32
|
||||
draft.value = next
|
||||
save()
|
||||
nextTick(() => {
|
||||
if (!el) return
|
||||
if (focused) el.setSelectionRange(Math.min(selStart, next.length), Math.min(selEnd, next.length))
|
||||
if (atBottom || !focused) el.scrollTop = el.scrollHeight
|
||||
})
|
||||
}
|
||||
|
||||
const sepFor = (text) => (!text || /\s$/.test(text) ? '' : ' ')
|
||||
const wordsOf = (text) => (text || '').trim().split(/\s+/).filter(Boolean)
|
||||
|
||||
// The person typed into or after the tail, so it's theirs now. Count its words as written — except a
|
||||
// HALF-FINISHED last word: the model streams words in pieces ("for" … "tnight"), and typing between the
|
||||
// pieces used to freeze "for" and lose the rest (measured 2026-09-27: "for a for [typed]"). If the word
|
||||
// has since grown, the fragment is removed (it sits just before the person's text; their caret is
|
||||
// shifted so it stays put relative to what they typed) and the whole word is written after it.
|
||||
const norm = (x) => (x || '').toLowerCase().replace(/[^\p{L}\p{N}']/gu, '')
|
||||
function releaseTail(utteranceWords) {
|
||||
const shownWords = wordsOf(live.text)
|
||||
const lastShown = shownWords[shownWords.length - 1] || ''
|
||||
const nowWord = utteranceWords[written + live.shown - 1]
|
||||
const grew = live.shown > 0 && nowWord !== undefined && norm(nowWord) !== norm(lastShown) && norm(nowWord).startsWith(norm(lastShown))
|
||||
// Find the tail by its text: typing above it may have moved it since it was written.
|
||||
const found = draft.value.lastIndexOf(live.text)
|
||||
if (found >= 0) live.start = found
|
||||
const tailEnd = live.start + live.text.length
|
||||
const prefixOk = found >= 0
|
||||
if (grew && prefixOk && lastShown) {
|
||||
let fragStart = tailEnd - lastShown.length
|
||||
if (fragStart > live.start && draft.value[fragStart - 1] === ' ') fragStart--
|
||||
const cut = tailEnd - fragStart
|
||||
const el = box.value
|
||||
const focused = el && el.getRootNode().activeElement === el
|
||||
const shift = (i) => (i >= tailEnd ? i - cut : i > fragStart ? fragStart : i)
|
||||
const a = el ? shift(el.selectionStart) : 0
|
||||
const b = el ? shift(el.selectionEnd) : 0
|
||||
draft.value = draft.value.slice(0, fragStart) + draft.value.slice(tailEnd)
|
||||
if (focused) nextTick(() => el.setSelectionRange(a, b))
|
||||
written += live.shown - 1
|
||||
} else {
|
||||
written += live.shown
|
||||
}
|
||||
live = null
|
||||
}
|
||||
|
||||
// Write `words` as the live tail: rewrite it if it's still ours, otherwise open a new one at the end.
|
||||
function writeTail(words, utterance) {
|
||||
const t = words.join(' ')
|
||||
if (liveIntact()) {
|
||||
const lead = live.text.match(/^\s*/)[0]
|
||||
live.text = t ? lead + t : ''
|
||||
live.shown = words.length
|
||||
writeEnd(draft.value.slice(0, live.start) + live.text)
|
||||
return
|
||||
}
|
||||
if (live) releaseTail(utterance) // the person took over the old tail: keep it
|
||||
if (!t) return
|
||||
live = { start: draft.value.length, text: sepFor(draft.value) + t, shown: words.length }
|
||||
writeEnd(draft.value + live.text)
|
||||
}
|
||||
|
||||
// ─── Hold the stream while the person is typing ─────────────────────────────────────────────────
|
||||
// Typing and streaming both write at the end of the box; a word landing between two keystrokes splices
|
||||
// into what is being typed (measured 2026-09-27: "he'[typed]ll"). So while keys are being pressed the
|
||||
// stream is buffered, and flushed — in order, nothing dropped — 700 ms after the last keystroke.
|
||||
const TYPING_HOLD_MS = 700
|
||||
let lastTypedAt = 0
|
||||
let pendingPartial = null
|
||||
const pendingFinals = []
|
||||
let flushTimer = null
|
||||
function markTyping() { lastTypedAt = Date.now() }
|
||||
function onUserInput() {
|
||||
markTyping()
|
||||
save()
|
||||
}
|
||||
function typingNow() { return Date.now() - lastTypedAt < TYPING_HOLD_MS }
|
||||
function scheduleFlush() {
|
||||
clearTimeout(flushTimer)
|
||||
flushTimer = setTimeout(() => {
|
||||
if (typingNow()) return scheduleFlush()
|
||||
const finals = pendingFinals.splice(0)
|
||||
const p = pendingPartial
|
||||
pendingPartial = null
|
||||
for (const f of finals) finishUtterance(f)
|
||||
if (p !== null) streamPartial(p)
|
||||
}, TYPING_HOLD_MS)
|
||||
}
|
||||
function onPartial(full) {
|
||||
if (typingNow() || pendingFinals.length) { pendingPartial = full; scheduleFlush(); return }
|
||||
streamPartial(full)
|
||||
}
|
||||
function onFinal(text) {
|
||||
if (typingNow() || pendingFinals.length) { pendingFinals.push(text); pendingPartial = null; scheduleFlush(); return }
|
||||
finishUtterance(text)
|
||||
}
|
||||
|
||||
// A word arrived: show the words of this utterance that aren't in the box yet.
|
||||
function streamPartial(full) {
|
||||
const w = wordsOf(full)
|
||||
if (live && !liveIntact()) releaseTail(w)
|
||||
writeTail(w.slice(written), w)
|
||||
}
|
||||
|
||||
// The sentence is finished: the same span of words, now punctuated and capitalised.
|
||||
function finishUtterance(text) {
|
||||
const w = wordsOf(text)
|
||||
if (live && !liveIntact()) releaseTail(w)
|
||||
writeTail(w.slice(written), w)
|
||||
live = null
|
||||
written = 0
|
||||
}
|
||||
|
||||
function applyStatus(s) {
|
||||
if (!s) return
|
||||
if (s.engine) engine.value = s.engine
|
||||
engineError.value = s.error || ''
|
||||
if (s.runtime) runtime.value = s.runtime
|
||||
if (s.model) model.value = s.model
|
||||
if (typeof s.recording === 'boolean') recording.value = s.recording
|
||||
}
|
||||
|
||||
function connect() {
|
||||
clearTimeout(retry)
|
||||
try { es?.close() } catch {}
|
||||
// No EventSource (a headless mount, an old WebView): show the engine-not-running state instead of throwing
|
||||
// in the mounted hook, which failed check:profiles-mount for every lane (go-int, END-DXS-ESRC).
|
||||
if (typeof window === 'undefined' || typeof window.EventSource !== 'function') {
|
||||
engineError.value = 'The desktop engine is not running.'
|
||||
return
|
||||
}
|
||||
es = new window.EventSource(apiBase.value + '/events')
|
||||
es.onopen = () => logc('events connected')
|
||||
es.onmessage = (m) => {
|
||||
let e
|
||||
try { e = JSON.parse(m.data) } catch { return }
|
||||
switch (e.type) {
|
||||
case 'status':
|
||||
applyStatus(e.status || e)
|
||||
if (e.engine === 'error') say(e.error || 'Speech engine error', 'error')
|
||||
if (e.engine === 'ready' && noticeKind.value !== 'error') say('')
|
||||
break
|
||||
case 'recording':
|
||||
recording.value = !!e.recording
|
||||
if (!e.recording) { level.value = 0; flushPartial() }
|
||||
break
|
||||
case 'partial':
|
||||
partial.value += e.text
|
||||
onPartial(partial.value)
|
||||
break
|
||||
case 'final':
|
||||
partial.value = ''
|
||||
onFinal(e.text)
|
||||
break
|
||||
case 'recovering':
|
||||
say(e.message, 'warn')
|
||||
break
|
||||
case 'recovered':
|
||||
say(e.message, 'ok')
|
||||
setTimeout(() => { if (noticeKind.value === 'ok') say('') }, 4000)
|
||||
break
|
||||
case 'devices':
|
||||
applyDevices(e.devices)
|
||||
break
|
||||
case 'level':
|
||||
level.value = Math.min(1, (e.peak || 0) * 3)
|
||||
break
|
||||
case 'warning':
|
||||
say(e.message, 'warn')
|
||||
break
|
||||
case 'error':
|
||||
say(e.message, 'error')
|
||||
logc('engine error: ' + e.message)
|
||||
break
|
||||
}
|
||||
}
|
||||
es.onerror = () => {
|
||||
engine.value = 'offline'
|
||||
recording.value = false
|
||||
logc('events disconnected; retrying in 3s')
|
||||
try { es.close() } catch {}
|
||||
retry = setTimeout(connect, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
// Recording stopped mid-sentence: the streamed words are already in the box, so just let go of them.
|
||||
function flushPartial() {
|
||||
clearTimeout(flushTimer)
|
||||
const finals = pendingFinals.splice(0)
|
||||
const p = pendingPartial
|
||||
pendingPartial = null
|
||||
for (const f of finals) finishUtterance(f)
|
||||
if (p !== null) streamPartial(p)
|
||||
partial.value = ''
|
||||
live = null
|
||||
written = 0
|
||||
written = 0
|
||||
}
|
||||
|
||||
async function post(path, body) {
|
||||
const r = await fetch(apiBase.value + path, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}),
|
||||
})
|
||||
const j = await r.json().catch(() => ({}))
|
||||
if (!r.ok) throw new Error(j.error || `HTTP ${r.status}`)
|
||||
return j
|
||||
}
|
||||
|
||||
async function toggle() {
|
||||
if (busy.value) return
|
||||
busy.value = true
|
||||
try {
|
||||
if (recording.value) {
|
||||
await post('/stop')
|
||||
} else {
|
||||
say('')
|
||||
testResult.value = null
|
||||
applyStatus(await post('/start', { language: language.value, device: device.value }))
|
||||
}
|
||||
} catch (err) {
|
||||
say(String(err.message || err), 'error')
|
||||
logc('toggle failed: ' + err)
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const words = computed(() => (draft.value.trim() ? draft.value.trim().split(/\s+/).length : 0))
|
||||
|
||||
async function copy(text, what) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} catch {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text; document.body.appendChild(ta); ta.select()
|
||||
try { document.execCommand('copy') } catch {}
|
||||
ta.remove()
|
||||
}
|
||||
copied.value = what
|
||||
setTimeout(() => { if (copied.value === what) copied.value = '' }, 1500)
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
if (draft.value.trim() && !window.confirm('Clear the whole text box? This cannot be undone.')) return
|
||||
draft.value = ''
|
||||
partial.value = ''
|
||||
live = null
|
||||
save()
|
||||
}
|
||||
|
||||
// ★ PRIVACY: the report is meant to be pasted to an agent, so it describes the setup without
|
||||
// identifying the person: no browser fingerprint (user-agent), no device names ("Anna's AirPods"), no
|
||||
// device IDs (they embed the Bluetooth MAC address), and never the text in the box — only its length.
|
||||
// The engine log it includes is already redacted at the source (MACs, home folder).
|
||||
const MAC_RE = /\b([0-9a-f]{2}[:_]){5}[0-9a-f]{2}\b/gi
|
||||
function privateDevices() {
|
||||
return devices.value.map((d, i) => ({ mic: 'mic ' + (i + 1), bus: d.bus, default: d.default, muted: d.muted, volume: d.volume, state: d.state }))
|
||||
}
|
||||
function browserFamily() {
|
||||
const ua = navigator.userAgent
|
||||
return /firefox/i.test(ua) ? 'Firefox' : /edg\//i.test(ua) ? 'Edge' : /chrom/i.test(ua) ? 'Chromium-based' : /safari/i.test(ua) ? 'Safari' : 'other'
|
||||
}
|
||||
|
||||
async function copyDebug() {
|
||||
let engineLog = '(unreachable)'
|
||||
let status = '(unreachable)'
|
||||
try { engineLog = await (await fetch(apiBase.value + '/log')).text() } catch {}
|
||||
try { status = JSON.stringify(await (await fetch(apiBase.value + '/status')).json(), null, 2) } catch {}
|
||||
const text = [
|
||||
'// RUDDER DEBUG REPORT — for a coding agent.',
|
||||
'// Read the engine status and log below, find why transcription failed or misbehaved, and propose a fix',
|
||||
'// in backend/rudder-engine/main.go or plugin-designs/rudder-voice/rudder-voice.vue. Do not guess:',
|
||||
'// quote the log lines that prove the cause.',
|
||||
'',
|
||||
'== status ==', status,
|
||||
'== plugin ==', JSON.stringify({ apiBase: apiBase.value, draftChars: draft.value.length, engine: engine.value, recording: recording.value, language: language.value, device: device.value ? 'chosen mic' : '(system default)', browser: browserFamily() }, null, 2),
|
||||
'== microphones ==', JSON.stringify({ devices: privateDevices(), offCards: offCards.value.length, error: deviceError.value, lastTest: testResult.value }, null, 2),
|
||||
'== client log ==', clientLog.value.join('\n'),
|
||||
'== engine log ==', engineLog,
|
||||
].join('\n').replace(MAC_RE, 'XX:XX:XX:XX:XX:XX')
|
||||
copy(text, 'debug')
|
||||
}
|
||||
|
||||
function onKey(ev) {
|
||||
// Ctrl+Shift+Space toggles recording without reaching for the mouse.
|
||||
if (ev.ctrlKey && ev.shiftKey && ev.code === 'Space') { ev.preventDefault(); toggle() }
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
connect()
|
||||
refreshDevices()
|
||||
window.addEventListener('keydown', onKey)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(retry)
|
||||
try { es?.close() } catch {}
|
||||
window.removeEventListener('keydown', onKey)
|
||||
})
|
||||
|
||||
const statusText = computed(() => ({
|
||||
connecting: 'Connecting…',
|
||||
starting: 'Loading speech model… (about 20 s the first time)',
|
||||
ready: recording.value ? 'Listening' : 'Ready',
|
||||
error: 'Engine error',
|
||||
offline: 'Engine offline',
|
||||
}[engine.value] || engine.value))
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rudder" :class="{ rec: recording }">
|
||||
<header class="top">
|
||||
<div class="brand">
|
||||
<span class="logo" aria-hidden="true">⎈</span>
|
||||
<div>
|
||||
<h1>Rudder</h1>
|
||||
<p class="sub">Voice-to-text for vibe coders · private · runs on this computer · no internet</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="info-btn" :aria-expanded="showInfo" title="What is Rudder?" aria-label="What is Rudder?" @click="showInfo = !showInfo">ⓘ</button>
|
||||
<div class="status" :data-state="engine" role="status">
|
||||
<span class="dot" aria-hidden="true"></span>{{ statusText }}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section v-if="showInfo" class="info">
|
||||
<h2>What is Rudder?</h2>
|
||||
<p>A voice-to-text tool for vibe coders: talk to your agents instead of typing to them. Paste links, code
|
||||
and notes into the box while you talk; everything you say is added to the end, ready to copy into a chat.</p>
|
||||
<ul>
|
||||
<li><b>Streaming.</b> Words appear while you speak, and you can keep talking for as long as you like —
|
||||
an hour is fine. There is no time limit and nothing to wait for at the end.</li>
|
||||
<li><b>Runs on your CPU.</b> No graphics card needed. The model uses about 1 GB of memory and keeps up with
|
||||
normal speech on an ordinary laptop processor.</li>
|
||||
<li><b>Private and offline.</b> Your voice never leaves this computer. No account, no cloud, no telemetry —
|
||||
checked with a network tracer: zero connections while transcribing.</li>
|
||||
<li><b>The model.</b> NVIDIA Nemotron 3.5 ASR Streaming (0.6B), run by NVIDIA's open-source
|
||||
NeMo-Speech.cpp. It adds punctuation and capital letters by itself.</li>
|
||||
<li><b>Why it lives in dx-maps.</b> Rudder is a plugin like the maps: built in the Plugin Studio, tested in the
|
||||
browser, and shipped as a desktop app with its own local backend — proof that one dx-maps codebase can
|
||||
build desktop apps, not only maps.</li>
|
||||
</ul>
|
||||
<p class="fine">Bluetooth headsets: Rudder switches them to their stable voice mode (CVSD), because the
|
||||
higher-quality mode (mSBC) kept dropping the mic after a few words.</p>
|
||||
</section>
|
||||
|
||||
<section class="controls">
|
||||
<button
|
||||
class="mic"
|
||||
:class="{ on: recording }"
|
||||
:disabled="busy || engine !== 'ready'"
|
||||
:aria-pressed="recording"
|
||||
:aria-label="recording ? 'Stop recording' : 'Start recording'"
|
||||
@click="toggle"
|
||||
>
|
||||
<span class="mic-icon" aria-hidden="true">{{ recording ? '■' : '●' }}</span>
|
||||
{{ recording ? 'Stop' : 'Record' }}
|
||||
</button>
|
||||
<div class="meter" aria-hidden="true"><div class="fill" :style="{ width: (level * 100).toFixed(0) + '%' }"></div></div>
|
||||
<label class="lang">
|
||||
<span>Language</span>
|
||||
<select v-model="language" :disabled="recording" @change="save">
|
||||
<option v-for="[code, name] in LANGUAGES" :key="code" :value="code">{{ name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="mics">
|
||||
<label class="mic-pick">
|
||||
<span>Microphone</span>
|
||||
<select v-model="device" :disabled="recording || testing" @change="save(); testResult = null">
|
||||
<option value="">System default{{ defaultDevice ? ' — ' + defaultDevice.label : '' }}</option>
|
||||
<option v-for="d in devices" :key="d.id" :value="d.id">{{ deviceLabel(d) }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="small" :disabled="testing || recording || engine === 'offline'" @click="testMic">
|
||||
{{ testing ? 'Testing… speak now' : 'Test mic' }}
|
||||
</button>
|
||||
<button class="small ghost" title="Look for microphones again" aria-label="Look for microphones again" :disabled="recording" @click="refreshDevices">↻</button>
|
||||
</section>
|
||||
<p v-if="testResult" class="notice" :data-kind="testResult.ok ? (testResult.peak < 0.01 ? 'warn' : 'ok') : 'error'">
|
||||
{{ testResult.ok ? '✓ ' : '✗ ' }}{{ testResult.verdict }}
|
||||
</p>
|
||||
<p v-if="chosen && chosen.muted" class="notice" data-kind="warn">“{{ chosen.label }}” is muted in your system sound settings — Rudder won't hear anything until it's unmuted.</p>
|
||||
<p v-if="deviceError" class="notice" data-kind="error">{{ deviceError }}</p>
|
||||
<p v-else-if="!devices.length && engine !== 'offline'" class="notice" data-kind="warn">No microphone found. Connect a headset or turn on your computer's microphone in Sound settings.</p>
|
||||
<p v-if="offCards.length" class="hint">Also available but switched off in Sound settings: {{ offCards.join(', ') }}. Turn it on there to use it here.</p>
|
||||
|
||||
<p v-if="notice" class="notice" :data-kind="noticeKind">{{ notice }}</p>
|
||||
<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
|
||||
<code>bun run dev</code> once built: <code>cd backend/rudder-engine && go build -o ../bin/rudder-engine .</code>
|
||||
</p>
|
||||
|
||||
<section class="compose">
|
||||
<textarea
|
||||
ref="box"
|
||||
v-model="draft"
|
||||
class="draft"
|
||||
spellcheck="true"
|
||||
aria-label="Your text. Type or paste here; everything Rudder hears is added to the end."
|
||||
placeholder="Type or paste here — and press Record (Ctrl+Shift+Space) to talk. Your words stream into the end of this box as you speak."
|
||||
@input="onUserInput"
|
||||
@keydown="markTyping"
|
||||
></textarea>
|
||||
<p class="hearing" aria-live="polite">
|
||||
<template v-if="recording"><span class="tag">listening</span> words appear at the end of the box as you speak — keep typing or pasting anywhere</template>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<footer class="bottom">
|
||||
<button :disabled="!draft.trim()" @click="copy(draft, 'all')">{{ copied === 'all' ? 'Copied ✓' : 'Copy all' }}</button>
|
||||
<button :disabled="!draft.trim() || recording" @click="clearAll">Clear</button>
|
||||
<span class="count">{{ words }} {{ words === 1 ? 'word' : 'words' }}</span>
|
||||
<span class="spacer"></span>
|
||||
<button class="ghost" :aria-expanded="showDebug" @click="showDebug = !showDebug">🐞 Debug</button>
|
||||
</footer>
|
||||
|
||||
<section v-if="showDebug" class="debug">
|
||||
<dl>
|
||||
<dt>API</dt><dd><code>{{ apiBase }}</code></dd>
|
||||
<dt>Runtime</dt><dd>{{ runtime || '—' }}</dd>
|
||||
<dt>Model</dt><dd>{{ model || '—' }}</dd>
|
||||
<dt>Engine</dt><dd>{{ engine }}<span v-if="engineError"> — {{ engineError }}</span></dd>
|
||||
</dl>
|
||||
<button @click="copyDebug">{{ copied === 'debug' ? 'Copied ✓' : 'Copy debug report for an agent' }}</button>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
:host { display: block; }
|
||||
.rudder {
|
||||
--bg: #0b0f14; --panel: #111823; --line: #1f2937; --text: #e5e7eb; --muted: #9ca3af;
|
||||
--accent: #38bdf8; --rec: #ef4444; --ok: #22c55e; --warn: #f59e0b;
|
||||
box-sizing: border-box; display: flex; flex-direction: column; gap: 14px; min-height: 480px; height: 100%;
|
||||
padding: 18px; background: var(--bg); color: var(--text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; font-size: 15px;
|
||||
}
|
||||
.rudder *, .rudder *::before, .rudder *::after { box-sizing: border-box; }
|
||||
.top { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
|
||||
.brand { display: flex; align-items: center; gap: 12px; }
|
||||
.logo { font-size: 30px; color: var(--accent); line-height: 1; }
|
||||
h1 { margin: 0; font-size: 20px; letter-spacing: -0.01em; }
|
||||
.sub { margin: 2px 0 0; color: var(--muted); font-size: 12.5px; }
|
||||
.status { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; border: 1px solid var(--line);
|
||||
border-radius: 999px; font-size: 13px; color: var(--muted); background: var(--panel); }
|
||||
.status .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
|
||||
.status[data-state='ready'] .dot { background: var(--ok); }
|
||||
.status[data-state='starting'] .dot, .status[data-state='connecting'] .dot { background: var(--warn); animation: pulse 1.2s infinite; }
|
||||
.status[data-state='error'] .dot, .status[data-state='offline'] .dot { background: var(--rec); }
|
||||
.rec .status .dot { background: var(--rec); animation: pulse 1s infinite; }
|
||||
@keyframes pulse { 50% { opacity: .35; } }
|
||||
.controls { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
|
||||
.mic { display: inline-flex; align-items: center; gap: 10px; padding: 12px 22px; border-radius: 999px;
|
||||
border: 1px solid var(--line); background: var(--accent); color: #04121c; font-weight: 700; font-size: 16px; cursor: pointer; }
|
||||
.mic.on { background: var(--rec); color: #fff; }
|
||||
.mic:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.mic:focus-visible, button:focus-visible, select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
.mic-icon { font-size: 14px; }
|
||||
.meter { flex: 1 1 140px; min-width: 100px; height: 8px; border-radius: 4px; background: var(--panel); border: 1px solid var(--line); overflow: hidden; }
|
||||
.meter .fill { height: 100%; background: linear-gradient(90deg, var(--ok), var(--warn), var(--rec)); transition: width .12s linear; }
|
||||
.lang { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); font-size: 13px; }
|
||||
select { background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 8px; padding: 7px 10px; font-size: 14px; }
|
||||
.notice { margin: 0; padding: 10px 12px; border-radius: 8px; border: 1px solid var(--line); background: var(--panel); font-size: 13.5px; line-height: 1.45; }
|
||||
.notice[data-kind='error'] { border-color: #7f1d1d; background: #1f0f12; }
|
||||
.notice[data-kind='warn'] { border-color: #78350f; background: #1c140a; }
|
||||
.notice[data-kind='ok'] { border-color: #14532d; background: #0b1a11; }
|
||||
.mics { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||
.mic-pick { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); font-size: 13px; flex: 1 1 260px; }
|
||||
.mic-pick select { flex: 1; min-width: 0; }
|
||||
.small { background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 8px; padding: 7px 12px; cursor: pointer; font-size: 13.5px; }
|
||||
.small:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.small.ghost { background: transparent; }
|
||||
.hint { margin: 0; color: var(--muted); font-size: 12.5px; }
|
||||
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; color: #c4b5fd; overflow-wrap: anywhere; }
|
||||
.compose { flex: 1 1 auto; display: flex; flex-direction: column; gap: 6px; min-height: 260px; }
|
||||
.draft { flex: 1 1 auto; width: 100%; min-height: 240px; resize: vertical; padding: 14px; border: 1px solid var(--line);
|
||||
border-radius: 12px; background: var(--panel); color: var(--text); font: inherit; font-size: 16px; line-height: 1.6; }
|
||||
.draft:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||
.draft::placeholder { color: #6b7280; }
|
||||
.hearing { margin: 0; min-height: 1.5em; color: var(--accent); font-size: 14px; overflow-wrap: anywhere; }
|
||||
.hearing .tag { display: inline-block; margin-right: 6px; padding: 1px 7px; border-radius: 999px; background: #0c2a3a;
|
||||
color: var(--accent); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
|
||||
.count { color: var(--muted); font-size: 12.5px; }
|
||||
.bottom { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||
.bottom button, .debug button { background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 8px; padding: 8px 14px; cursor: pointer; font-size: 14px; }
|
||||
.bottom button:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.bottom .ghost { background: transparent; }
|
||||
.spacer { flex: 1; }
|
||||
.info-btn { margin-left: auto; background: transparent; border: 1px solid var(--line); color: var(--muted); border-radius: 999px;
|
||||
width: 30px; height: 30px; font-size: 16px; cursor: pointer; }
|
||||
.info-btn:hover, .info-btn[aria-expanded='true'] { color: var(--accent); border-color: var(--accent); }
|
||||
.info { border: 1px solid var(--line); border-radius: 12px; background: var(--panel); padding: 14px 16px; font-size: 14px; line-height: 1.55; }
|
||||
.info h2 { margin: 0 0 6px; font-size: 16px; }
|
||||
.info p { margin: 0 0 8px; }
|
||||
.info ul { margin: 0 0 8px; padding-left: 18px; }
|
||||
.info li { margin-bottom: 6px; }
|
||||
.info .fine { color: var(--muted); font-size: 12.5px; margin: 0; }
|
||||
.debug { border: 1px dashed var(--line); border-radius: 10px; padding: 12px; font-size: 13px; }
|
||||
.debug dl { display: grid; grid-template-columns: 90px 1fr; gap: 6px 10px; margin: 0 0 10px; }
|
||||
.debug dt { color: var(--muted); }
|
||||
.debug dd { margin: 0; overflow-wrap: anywhere; }
|
||||
</style>
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
This folder is GENERATED by `bun run build`; do not edit it by hand. It holds exactly one thing:
|
||||
|
||||
- **`dx-maps/`** — the output folder: 24 plugin bundles and 36 pages, self-contained.
|
||||
- **`dx-maps/`** — the output folder: 25 plugin bundles and 37 pages, self-contained.
|
||||
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/<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
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <all-people-groups> custom element. -->
|
||||
<script src="../all-people-groups.js" integrity="sha384-xpQXT3X2W81IPWaSXifrP9kXGf/nbU8hwT5ZWMQjH0EwDWHMJeAiWj+yEuAi+KBx" crossorigin="anonymous"></script>
|
||||
<script src="../all-people-groups.js" integrity="sha384-ZDia7fZFwFRQlQ9EaeWXDOxKbMC7Uv9zMg+k43Ok4UhWgOjDbPxW9JJd+GEivhyr" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-kkdmZjOS8KJTTdcUmnMU5bpeVJZBMPFw4mb1rdZ3K2dacuRGxLSiCH4L7d6BN8dI" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-kkdmZjOS8KJTTdcUmnMU5bpeVJZBMPFw4mb1rdZ3K2dacuRGxLSiCH4L7d6BN8dI" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-kkdmZjOS8KJTTdcUmnMU5bpeVJZBMPFw4mb1rdZ3K2dacuRGxLSiCH4L7d6BN8dI" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
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>
|
||||
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
|
||||
<script src="../../dashboard-plus-grid.js" integrity="sha384-gLs4MQRznMyg9cKyDneIKEkjCDfd/Buvv+G8Xwwn+ggxaKshHtD1zKO658e8L6pw" crossorigin="anonymous"></script>
|
||||
<script src="../../dashboard-plus-grid.js" integrity="sha384-G2cfmNzjdjkx6ZLplzjiMJ/zUO5UzGNUprk+Uo409T5tqQkFk7qD4RkFsn4mzOZv" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
|
||||
<script src="../dashboard-plus-grid.js" integrity="sha384-gLs4MQRznMyg9cKyDneIKEkjCDfd/Buvv+G8Xwwn+ggxaKshHtD1zKO658e8L6pw" crossorigin="anonymous"></script>
|
||||
<script src="../dashboard-plus-grid.js" integrity="sha384-G2cfmNzjdjkx6ZLplzjiMJ/zUO5UzGNUprk+Uo409T5tqQkFk7qD4RkFsn4mzOZv" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
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>
|
||||
<!-- Bundle IIFE — registers the <demo-map> custom element. -->
|
||||
<script src="../demo-map.js" integrity="sha384-IVZC654ptY88sI56Xw0Mlmc58TNBXDzTR2+WrGRwv5LYPALSneviSCNrpg61Wzi6" crossorigin="anonymous"></script>
|
||||
<script src="../demo-map.js" integrity="sha384-Q3BuUcCOHvp7ko/LLlos7iF71x6bq4+TjfYQjbgCl2dynmwe7h7yCO94yNUdjoOG" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <demo-map> custom element. -->
|
||||
<script src="../../demo-map.js" integrity="sha384-IVZC654ptY88sI56Xw0Mlmc58TNBXDzTR2+WrGRwv5LYPALSneviSCNrpg61Wzi6" crossorigin="anonymous"></script>
|
||||
<script src="../../demo-map.js" integrity="sha384-Q3BuUcCOHvp7ko/LLlos7iF71x6bq4+TjfYQjbgCl2dynmwe7h7yCO94yNUdjoOG" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
"schema": "doxa-maps-manifest@1",
|
||||
"generatedFrom": "plugin-designs/*/*.vue",
|
||||
"embedBase": ".",
|
||||
"bundleCount": 24,
|
||||
"profileCount": 36,
|
||||
"bundleCount": 25,
|
||||
"profileCount": 37,
|
||||
"runtime": {
|
||||
"apiBaseUrl": "https://pray.doxa.life"
|
||||
},
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
"bundle": "all-people-groups",
|
||||
"scriptUrl": "./all-people-groups/all-people-groups.js",
|
||||
"output": "dx-maps/all-people-groups/all-people-groups.js",
|
||||
"integrity": "sha384-xpQXT3X2W81IPWaSXifrP9kXGf/nbU8hwT5ZWMQjH0EwDWHMJeAiWj+yEuAi+KBx",
|
||||
"integrity": "sha384-ZDia7fZFwFRQlQ9EaeWXDOxKbMC7Uv9zMg+k43Ok4UhWgOjDbPxW9JJd+GEivhyr",
|
||||
"elements": [
|
||||
"all-people-groups"
|
||||
],
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
"scriptUrl": "./all-people-groups/all-people-groups.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./all-people-groups/all-people-groups/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=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-xpQXT3X2W81IPWaSXifrP9kXGf/nbU8hwT5ZWMQjH0EwDWHMJeAiWj+yEuAi+KBx\" crossorigin=\"anonymous\"></script>\n<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>"
|
||||
"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=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-ZDia7fZFwFRQlQ9EaeWXDOxKbMC7Uv9zMg+k43Ok4UhWgOjDbPxW9JJd+GEivhyr\" crossorigin=\"anonymous\"></script>\n<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>"
|
||||
},
|
||||
"bundle": "all-people-groups"
|
||||
}
|
||||
|
|
@ -165,7 +165,7 @@
|
|||
"bundle": "country-posters",
|
||||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"output": "dx-maps/country-posters/country-posters.js",
|
||||
"integrity": "sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd",
|
||||
"integrity": "sha384-kkdmZjOS8KJTTdcUmnMU5bpeVJZBMPFw4mb1rdZ3K2dacuRGxLSiCH4L7d6BN8dI",
|
||||
"elements": [
|
||||
"country-posters-map"
|
||||
],
|
||||
|
|
@ -187,7 +187,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/countries-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=\"./country-posters/country-posters.js\" integrity=\"sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-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=\"./country-posters/country-posters.js\" integrity=\"sha384-kkdmZjOS8KJTTdcUmnMU5bpeVJZBMPFw4mb1rdZ3K2dacuRGxLSiCH4L7d6BN8dI\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -202,7 +202,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/country-poster-wall/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=\"./country-posters/country-posters.js\" integrity=\"sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-kkdmZjOS8KJTTdcUmnMU5bpeVJZBMPFw4mb1rdZ3K2dacuRGxLSiCH4L7d6BN8dI\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -217,7 +217,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/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=\"./country-posters/country-posters.js\" integrity=\"sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-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=\"./country-posters/country-posters.js\" integrity=\"sha384-kkdmZjOS8KJTTdcUmnMU5bpeVJZBMPFw4mb1rdZ3K2dacuRGxLSiCH4L7d6BN8dI\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -226,7 +226,7 @@
|
|||
"bundle": "dashboard-plus-grid",
|
||||
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
|
||||
"output": "dx-maps/dashboard-plus-grid/dashboard-plus-grid.js",
|
||||
"integrity": "sha384-gLs4MQRznMyg9cKyDneIKEkjCDfd/Buvv+G8Xwwn+ggxaKshHtD1zKO658e8L6pw",
|
||||
"integrity": "sha384-G2cfmNzjdjkx6ZLplzjiMJ/zUO5UzGNUprk+Uo409T5tqQkFk7qD4RkFsn4mzOZv",
|
||||
"elements": [
|
||||
"dashboard-plus-grid"
|
||||
],
|
||||
|
|
@ -248,7 +248,7 @@
|
|||
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
|
||||
"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>",
|
||||
"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-gLs4MQRznMyg9cKyDneIKEkjCDfd/Buvv+G8Xwwn+ggxaKshHtD1zKO658e8L6pw\" 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-G2cfmNzjdjkx6ZLplzjiMJ/zUO5UzGNUprk+Uo409T5tqQkFk7qD4RkFsn4mzOZv\" 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": [
|
||||
{
|
||||
|
|
@ -272,7 +272,7 @@
|
|||
"bundle": "demo-map",
|
||||
"scriptUrl": "./demo-map/demo-map.js",
|
||||
"output": "dx-maps/demo-map/demo-map.js",
|
||||
"integrity": "sha384-IVZC654ptY88sI56Xw0Mlmc58TNBXDzTR2+WrGRwv5LYPALSneviSCNrpg61Wzi6",
|
||||
"integrity": "sha384-Q3BuUcCOHvp7ko/LLlos7iF71x6bq4+TjfYQjbgCl2dynmwe7h7yCO94yNUdjoOG",
|
||||
"elements": [
|
||||
"demo-map"
|
||||
],
|
||||
|
|
@ -290,7 +290,7 @@
|
|||
"scriptUrl": "./demo-map/demo-map.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./demo-map/demo-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=\"./demo-map/demo-map.js\" integrity=\"sha384-IVZC654ptY88sI56Xw0Mlmc58TNBXDzTR2+WrGRwv5LYPALSneviSCNrpg61Wzi6\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-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=\"./demo-map/demo-map.js\" integrity=\"sha384-Q3BuUcCOHvp7ko/LLlos7iF71x6bq4+TjfYQjbgCl2dynmwe7h7yCO94yNUdjoOG\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-map>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -371,7 +371,7 @@
|
|||
"bundle": "doxa-research-map",
|
||||
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
|
||||
"output": "dx-maps/doxa-research-map/doxa-research-map.js",
|
||||
"integrity": "sha384-QF6F4GttfIpkev5vLjQn5SftilFmw7HksE+bvoYu5WccjETMjVxPoeqEB3wqmGh7",
|
||||
"integrity": "sha384-yfb9suM6BjJ8npI9/Ok9mmEUDxxuWUqB6gXCB33cCgLMfOeDtGwszF+fJiwAw//n",
|
||||
"elements": [
|
||||
"doxa-research-map"
|
||||
],
|
||||
|
|
@ -389,7 +389,7 @@
|
|||
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
|
||||
"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>",
|
||||
"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-QF6F4GttfIpkev5vLjQn5SftilFmw7HksE+bvoYu5WccjETMjVxPoeqEB3wqmGh7\" 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-yfb9suM6BjJ8npI9/Ok9mmEUDxxuWUqB6gXCB33cCgLMfOeDtGwszF+fJiwAw//n\" 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": [
|
||||
{
|
||||
|
|
@ -415,7 +415,7 @@
|
|||
"bundle": "doxa-simple-map",
|
||||
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
|
||||
"output": "dx-maps/doxa-simple-map/doxa-simple-map.js",
|
||||
"integrity": "sha384-qz7rw+kKZQs04E7Am98uyFYO8TfhD8uGy73NUFfbiR7Nag2rON9XEyjzgeg6c6Yn",
|
||||
"integrity": "sha384-bPFak/dvNd3d7uNUAJfMGiZudEOZlEM26mVv3IiEIN4Xi/y+mc9P36HhXV3r1O79",
|
||||
"elements": [
|
||||
"doxa-map",
|
||||
"doxa-simple-map"
|
||||
|
|
@ -434,7 +434,7 @@
|
|||
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-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-simple-map/doxa-simple-map.js\" integrity=\"sha384-qz7rw+kKZQs04E7Am98uyFYO8TfhD8uGy73NUFfbiR7Nag2rON9XEyjzgeg6c6Yn\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-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-simple-map/doxa-simple-map.js\" integrity=\"sha384-bPFak/dvNd3d7uNUAJfMGiZudEOZlEM26mVv3IiEIN4Xi/y+mc9P36HhXV3r1O79\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -604,7 +604,7 @@
|
|||
"bundle": "gospel-internet",
|
||||
"scriptUrl": "./gospel-internet/gospel-internet.js",
|
||||
"output": "dx-maps/gospel-internet/gospel-internet.js",
|
||||
"integrity": "sha384-oCqqOQ/nxQoV8hoX75YIVe3DBUOC+yWpy6TGCCsmoN6iNzctsiO8tR2FklINZLnq",
|
||||
"integrity": "sha384-FNeWKsULwwuFfsy7c6d5c7K9mi9MTGUDIJablvw48K8xMzkfcvQJUrjRvIta6klm",
|
||||
"elements": [
|
||||
"gospel-internet-board"
|
||||
],
|
||||
|
|
@ -623,7 +623,7 @@
|
|||
"scriptUrl": "./gospel-internet/gospel-internet.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-internet/board/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-oCqqOQ/nxQoV8hoX75YIVe3DBUOC+yWpy6TGCCsmoN6iNzctsiO8tR2FklINZLnq\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-FNeWKsULwwuFfsy7c6d5c7K9mi9MTGUDIJablvw48K8xMzkfcvQJUrjRvIta6klm\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -723,7 +723,7 @@
|
|||
"bundle": "gospel-resources",
|
||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"output": "dx-maps/gospel-resources/gospel-resources.js",
|
||||
"integrity": "sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq",
|
||||
"integrity": "sha384-2FCDJcK//gT/pWmCpB50HlBexpcO3SdbRlATTvtSQGWCEYohocvr9Hvt2U2hbCNR",
|
||||
"elements": [
|
||||
"gospel-resources-map"
|
||||
],
|
||||
|
|
@ -745,7 +745,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"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>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" 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-2FCDJcK//gT/pWmCpB50HlBexpcO3SdbRlATTvtSQGWCEYohocvr9Hvt2U2hbCNR\" 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>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -760,7 +760,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"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>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" 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-2FCDJcK//gT/pWmCpB50HlBexpcO3SdbRlATTvtSQGWCEYohocvr9Hvt2U2hbCNR\" 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>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -775,7 +775,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"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>",
|
||||
"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-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" 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-2FCDJcK//gT/pWmCpB50HlBexpcO3SdbRlATTvtSQGWCEYohocvr9Hvt2U2hbCNR\" 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>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -790,7 +790,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"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>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" 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-2FCDJcK//gT/pWmCpB50HlBexpcO3SdbRlATTvtSQGWCEYohocvr9Hvt2U2hbCNR\" 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>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -805,7 +805,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"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>",
|
||||
"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-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" 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-2FCDJcK//gT/pWmCpB50HlBexpcO3SdbRlATTvtSQGWCEYohocvr9Hvt2U2hbCNR\" 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>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -814,7 +814,7 @@
|
|||
"bundle": "my-upg-100-list",
|
||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"output": "dx-maps/my-upg-100-list/my-upg-100-list.js",
|
||||
"integrity": "sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR",
|
||||
"integrity": "sha384-meHgA2FfgqXz885IdY5fY9VgMQDHEBhl1nsot6tRszitV8aub/mWfBRoLnAmeFxU",
|
||||
"elements": [
|
||||
"my-upg-100-list-map"
|
||||
],
|
||||
|
|
@ -836,7 +836,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-meHgA2FfgqXz885IdY5fY9VgMQDHEBhl1nsot6tRszitV8aub/mWfBRoLnAmeFxU\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -851,7 +851,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-meHgA2FfgqXz885IdY5fY9VgMQDHEBhl1nsot6tRszitV8aub/mWfBRoLnAmeFxU\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -866,7 +866,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-meHgA2FfgqXz885IdY5fY9VgMQDHEBhl1nsot6tRszitV8aub/mWfBRoLnAmeFxU\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -938,7 +938,7 @@
|
|||
"bundle": "plugin-studio",
|
||||
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
||||
"output": "dx-maps/plugin-studio/plugin-studio.js",
|
||||
"integrity": "sha384-IQjG7XqoeppiQ+mxxtUxQApF7LoNWEc+LuQEftMZxWdDyht1X1u0mnB6LvDKAMJX",
|
||||
"integrity": "sha384-L2MCcGfR6X5/0VxHzMs9eDvMsdSYufZ66OhQdodC7vwcNgM7op8u8jNjayoJsySd",
|
||||
"elements": [
|
||||
"plugin-studio"
|
||||
],
|
||||
|
|
@ -968,7 +968,7 @@
|
|||
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./plugin-studio/plugin-studio/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-IQjG7XqoeppiQ+mxxtUxQApF7LoNWEc+LuQEftMZxWdDyht1X1u0mnB6LvDKAMJX\" 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-L2MCcGfR6X5/0VxHzMs9eDvMsdSYufZ66OhQdodC7vwcNgM7op8u8jNjayoJsySd\" 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": [
|
||||
{
|
||||
|
|
@ -995,7 +995,7 @@
|
|||
"bundle": "pplr-data-maps",
|
||||
"scriptUrl": "./pplr-data-maps/pplr-data-maps.js",
|
||||
"output": "dx-maps/pplr-data-maps/pplr-data-maps.js",
|
||||
"integrity": "sha384-idrM+onVQ8aeioB8d67pktceaA4tHSoJCH3OX1eHhr5YHoI+X6/JSePapupxL9Rc",
|
||||
"integrity": "sha384-ITBwAAzT9m4q3VvhBRMSwYGuJlTi/SFb5K3fyqqhPaa9FdbafyIPbI1Iu/YtWbae",
|
||||
"elements": [
|
||||
"pplr-data-maps"
|
||||
],
|
||||
|
|
@ -1013,7 +1013,7 @@
|
|||
"scriptUrl": "./pplr-data-maps/pplr-data-maps.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./pplr-data-maps/pplr-data-maps/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=\"./pplr-data-maps/pplr-data-maps.js\" integrity=\"sha384-idrM+onVQ8aeioB8d67pktceaA4tHSoJCH3OX1eHhr5YHoI+X6/JSePapupxL9Rc\" crossorigin=\"anonymous\"></script>\n<pplr-data-maps profile-config='{\"profile\":\"pplr-data-maps\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></pplr-data-maps>"
|
||||
"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=\"./pplr-data-maps/pplr-data-maps.js\" integrity=\"sha384-ITBwAAzT9m4q3VvhBRMSwYGuJlTi/SFb5K3fyqqhPaa9FdbafyIPbI1Iu/YtWbae\" crossorigin=\"anonymous\"></script>\n<pplr-data-maps profile-config='{\"profile\":\"pplr-data-maps\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></pplr-data-maps>"
|
||||
},
|
||||
"bundle": "pplr-data-maps"
|
||||
}
|
||||
|
|
@ -1023,7 +1023,7 @@
|
|||
"bundle": "prayer-walk-planner",
|
||||
"scriptUrl": "./prayer-walk-planner/prayer-walk-planner.js",
|
||||
"output": "dx-maps/prayer-walk-planner/prayer-walk-planner.js",
|
||||
"integrity": "sha384-zEGH2wLAtRReRGWJJcYWgfxVmjx/cmNBh1HWp9EoO77UdnIdaB+SGDQJ5P1cl7/N",
|
||||
"integrity": "sha384-vDE4FMYhrLgkHnj6K2nRGEbLyf8EyuV7OUo+F93bGd00+/F+MRpev49x/iXsllFq",
|
||||
"elements": [
|
||||
"prayer-walk-planner"
|
||||
],
|
||||
|
|
@ -1041,7 +1041,7 @@
|
|||
"scriptUrl": "./prayer-walk-planner/prayer-walk-planner.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./prayer-walk-planner/prayer-walk-planner/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=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-zEGH2wLAtRReRGWJJcYWgfxVmjx/cmNBh1HWp9EoO77UdnIdaB+SGDQJ5P1cl7/N\" crossorigin=\"anonymous\"></script>\n<prayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></prayer-walk-planner>"
|
||||
"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=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-vDE4FMYhrLgkHnj6K2nRGEbLyf8EyuV7OUo+F93bGd00+/F+MRpev49x/iXsllFq\" crossorigin=\"anonymous\"></script>\n<prayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></prayer-walk-planner>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -1050,7 +1050,7 @@
|
|||
"bundle": "regions-dashboard",
|
||||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"output": "dx-maps/regions-dashboard/regions-dashboard.js",
|
||||
"integrity": "sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q",
|
||||
"integrity": "sha384-aCE8BYISZb9Awa2lEWI0A3qgAc5ZFh1Q8EkSwnTyn2SizzOeyPwCRP8wz+oiVTmQ",
|
||||
"elements": [
|
||||
"regions-dashboard"
|
||||
],
|
||||
|
|
@ -1072,7 +1072,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-aCE8BYISZb9Awa2lEWI0A3qgAc5ZFh1Q8EkSwnTyn2SizzOeyPwCRP8wz+oiVTmQ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1087,7 +1087,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-aCE8BYISZb9Awa2lEWI0A3qgAc5ZFh1Q8EkSwnTyn2SizzOeyPwCRP8wz+oiVTmQ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1102,7 +1102,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-aCE8BYISZb9Awa2lEWI0A3qgAc5ZFh1Q8EkSwnTyn2SizzOeyPwCRP8wz+oiVTmQ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -1111,7 +1111,7 @@
|
|||
"bundle": "research-dashboard",
|
||||
"scriptUrl": "./research-dashboard/research-dashboard.js",
|
||||
"output": "dx-maps/research-dashboard/research-dashboard.js",
|
||||
"integrity": "sha384-GBlVmWSyzwQX0ty93zufhUFNTSJoDn8RF+7ywLJ0Q5sx5RLUoC2yt7W/XPtuX7LB",
|
||||
"integrity": "sha384-Oqb5Jww1i+G5kkTq8Bggw/DrP3F8T34jIRCgvR80AkiMJ3/WskZKVJl2M64iRwOe",
|
||||
"elements": [
|
||||
"research-dashboard-map"
|
||||
],
|
||||
|
|
@ -1129,7 +1129,7 @@
|
|||
"scriptUrl": "./research-dashboard/research-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./research-dashboard/research-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=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-GBlVmWSyzwQX0ty93zufhUFNTSJoDn8RF+7ywLJ0Q5sx5RLUoC2yt7W/XPtuX7LB\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-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=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-Oqb5Jww1i+G5kkTq8Bggw/DrP3F8T34jIRCgvR80AkiMJ3/WskZKVJl2M64iRwOe\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
|
||||
},
|
||||
"bundle": "research-dashboard"
|
||||
}
|
||||
|
|
@ -1181,6 +1181,36 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"bundle": "rudder-voice",
|
||||
"scriptUrl": "./rudder-voice/rudder-voice.js",
|
||||
"output": "dx-maps/rudder-voice/rudder-voice.js",
|
||||
"integrity": "sha384-Pb1pQJqoB3wMTum8vw0uUrm7e74j90ytj1hNdIl/1HbRlfy1YoT0hf6ggWQ3qDNO",
|
||||
"elements": [
|
||||
"rudder-voice"
|
||||
],
|
||||
"stage": "experimental",
|
||||
"profiles": [
|
||||
{
|
||||
"profile": "rudder-voice",
|
||||
"stage": "experimental",
|
||||
"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-Pb1pQJqoB3wMTum8vw0uUrm7e74j90ytj1hNdIl/1HbRlfy1YoT0hf6ggWQ3qDNO\" 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",
|
||||
"scriptUrl": "./stack-demo/stack-demo.js",
|
||||
|
|
@ -1231,7 +1261,7 @@
|
|||
"bundle": "upg-neighbor",
|
||||
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
|
||||
"output": "dx-maps/upg-neighbor/upg-neighbor.js",
|
||||
"integrity": "sha384-D2e4OB60VjRAhfnyVG192dx3hxC9tfP1WQMu1E+JcINQ2XRO7tCUhXXNZBMoQrD+",
|
||||
"integrity": "sha384-X6YaUyV+9g7wppNaQ+YWgER7pRrk8y4egSZypQF8+2lVYRith+nPvkpiI77Oh6p3",
|
||||
"elements": [
|
||||
"upg-neighbor"
|
||||
],
|
||||
|
|
@ -1249,7 +1279,7 @@
|
|||
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./upg-neighbor/upg-neighbor/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=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-D2e4OB60VjRAhfnyVG192dx3hxC9tfP1WQMu1E+JcINQ2XRO7tCUhXXNZBMoQrD+\" crossorigin=\"anonymous\"></script>\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
|
||||
"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=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-X6YaUyV+9g7wppNaQ+YWgER7pRrk8y4egSZypQF8+2lVYRith+nPvkpiI77Oh6p3\" crossorigin=\"anonymous\"></script>\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
|
||||
},
|
||||
"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>
|
||||
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
|
||||
<script src="../doxa-research-map.js" integrity="sha384-QF6F4GttfIpkev5vLjQn5SftilFmw7HksE+bvoYu5WccjETMjVxPoeqEB3wqmGh7" crossorigin="anonymous"></script>
|
||||
<script src="../doxa-research-map.js" integrity="sha384-yfb9suM6BjJ8npI9/Ok9mmEUDxxuWUqB6gXCB33cCgLMfOeDtGwszF+fJiwAw//n" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
|
||||
<script src="../../doxa-research-map.js" integrity="sha384-QF6F4GttfIpkev5vLjQn5SftilFmw7HksE+bvoYu5WccjETMjVxPoeqEB3wqmGh7" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-research-map.js" integrity="sha384-yfb9suM6BjJ8npI9/Ok9mmEUDxxuWUqB6gXCB33cCgLMfOeDtGwszF+fJiwAw//n" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
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>
|
||||
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-qz7rw+kKZQs04E7Am98uyFYO8TfhD8uGy73NUFfbiR7Nag2rON9XEyjzgeg6c6Yn" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-bPFak/dvNd3d7uNUAJfMGiZudEOZlEM26mVv3IiEIN4Xi/y+mc9P36HhXV3r1O79" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-qz7rw+kKZQs04E7Am98uyFYO8TfhD8uGy73NUFfbiR7Nag2rON9XEyjzgeg6c6Yn" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-bPFak/dvNd3d7uNUAJfMGiZudEOZlEM26mVv3IiEIN4Xi/y+mc9P36HhXV3r1O79" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
|
||||
<script src="../doxa-simple-map.js" integrity="sha384-qz7rw+kKZQs04E7Am98uyFYO8TfhD8uGy73NUFfbiR7Nag2rON9XEyjzgeg6c6Yn" crossorigin="anonymous"></script>
|
||||
<script src="../doxa-simple-map.js" integrity="sha384-bPFak/dvNd3d7uNUAJfMGiZudEOZlEM26mVv3IiEIN4Xi/y+mc9P36HhXV3r1O79" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-qz7rw+kKZQs04E7Am98uyFYO8TfhD8uGy73NUFfbiR7Nag2rON9XEyjzgeg6c6Yn" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-bPFak/dvNd3d7uNUAJfMGiZudEOZlEM26mVv3IiEIN4Xi/y+mc9P36HhXV3r1O79" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
|
||||
<script src="../../gospel-internet.js" integrity="sha384-oCqqOQ/nxQoV8hoX75YIVe3DBUOC+yWpy6TGCCsmoN6iNzctsiO8tR2FklINZLnq" crossorigin="anonymous"></script>
|
||||
<script src="../../gospel-internet.js" integrity="sha384-FNeWKsULwwuFfsy7c6d5c7K9mi9MTGUDIJablvw48K8xMzkfcvQJUrjRvIta6klm" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
|
||||
<script src="../gospel-internet.js" integrity="sha384-oCqqOQ/nxQoV8hoX75YIVe3DBUOC+yWpy6TGCCsmoN6iNzctsiO8tR2FklINZLnq" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-internet.js" integrity="sha384-FNeWKsULwwuFfsy7c6d5c7K9mi9MTGUDIJablvw48K8xMzkfcvQJUrjRvIta6klm" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
|
||||
<script src="../../gospel-internet.js" integrity="sha384-oCqqOQ/nxQoV8hoX75YIVe3DBUOC+yWpy6TGCCsmoN6iNzctsiO8tR2FklINZLnq" crossorigin="anonymous"></script>
|
||||
<script src="../../gospel-internet.js" integrity="sha384-FNeWKsULwwuFfsy7c6d5c7K9mi9MTGUDIJablvw48K8xMzkfcvQJUrjRvIta6klm" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
|
||||
<script src="../../gospel-internet.js" integrity="sha384-oCqqOQ/nxQoV8hoX75YIVe3DBUOC+yWpy6TGCCsmoN6iNzctsiO8tR2FklINZLnq" crossorigin="anonymous"></script>
|
||||
<script src="../../gospel-internet.js" integrity="sha384-FNeWKsULwwuFfsy7c6d5c7K9mi9MTGUDIJablvw48K8xMzkfcvQJUrjRvIta6klm" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
|
||||
<script src="../../gospel-internet.js" integrity="sha384-oCqqOQ/nxQoV8hoX75YIVe3DBUOC+yWpy6TGCCsmoN6iNzctsiO8tR2FklINZLnq" crossorigin="anonymous"></script>
|
||||
<script src="../../gospel-internet.js" integrity="sha384-FNeWKsULwwuFfsy7c6d5c7K9mi9MTGUDIJablvw48K8xMzkfcvQJUrjRvIta6klm" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
|
||||
<script src="../../gospel-internet.js" integrity="sha384-oCqqOQ/nxQoV8hoX75YIVe3DBUOC+yWpy6TGCCsmoN6iNzctsiO8tR2FklINZLnq" crossorigin="anonymous"></script>
|
||||
<script src="../../gospel-internet.js" integrity="sha384-FNeWKsULwwuFfsy7c6d5c7K9mi9MTGUDIJablvw48K8xMzkfcvQJUrjRvIta6klm" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
|
||||
<script src="../../gospel-internet.js" integrity="sha384-oCqqOQ/nxQoV8hoX75YIVe3DBUOC+yWpy6TGCCsmoN6iNzctsiO8tR2FklINZLnq" crossorigin="anonymous"></script>
|
||||
<script src="../../gospel-internet.js" integrity="sha384-FNeWKsULwwuFfsy7c6d5c7K9mi9MTGUDIJablvw48K8xMzkfcvQJUrjRvIta6klm" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// 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>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-2FCDJcK//gT/pWmCpB50HlBexpcO3SdbRlATTvtSQGWCEYohocvr9Hvt2U2hbCNR" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-2FCDJcK//gT/pWmCpB50HlBexpcO3SdbRlATTvtSQGWCEYohocvr9Hvt2U2hbCNR" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-2FCDJcK//gT/pWmCpB50HlBexpcO3SdbRlATTvtSQGWCEYohocvr9Hvt2U2hbCNR" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// 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>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-2FCDJcK//gT/pWmCpB50HlBexpcO3SdbRlATTvtSQGWCEYohocvr9Hvt2U2hbCNR" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-2FCDJcK//gT/pWmCpB50HlBexpcO3SdbRlATTvtSQGWCEYohocvr9Hvt2U2hbCNR" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,6 @@
|
|||
</head>
|
||||
<body>
|
||||
<plugin-studio tree-base="."></plugin-studio>
|
||||
<script src="./plugin-studio/plugin-studio.js" integrity="sha384-IQjG7XqoeppiQ+mxxtUxQApF7LoNWEc+LuQEftMZxWdDyht1X1u0mnB6LvDKAMJX" crossorigin="anonymous"></script>
|
||||
<script src="./plugin-studio/plugin-studio.js" integrity="sha384-L2MCcGfR6X5/0VxHzMs9eDvMsdSYufZ66OhQdodC7vwcNgM7op8u8jNjayoJsySd" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -2,8 +2,8 @@
|
|||
"schema": "doxa-maps-manifest@1",
|
||||
"generatedFrom": "plugin-designs/*/*.vue",
|
||||
"embedBase": ".",
|
||||
"bundleCount": 24,
|
||||
"profileCount": 36,
|
||||
"bundleCount": 25,
|
||||
"profileCount": 37,
|
||||
"runtime": {
|
||||
"apiBaseUrl": "https://pray.doxa.life"
|
||||
},
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
"bundle": "all-people-groups",
|
||||
"scriptUrl": "./all-people-groups/all-people-groups.js",
|
||||
"output": "dx-maps/all-people-groups/all-people-groups.js",
|
||||
"integrity": "sha384-xpQXT3X2W81IPWaSXifrP9kXGf/nbU8hwT5ZWMQjH0EwDWHMJeAiWj+yEuAi+KBx",
|
||||
"integrity": "sha384-ZDia7fZFwFRQlQ9EaeWXDOxKbMC7Uv9zMg+k43Ok4UhWgOjDbPxW9JJd+GEivhyr",
|
||||
"elements": [
|
||||
"all-people-groups"
|
||||
],
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
"scriptUrl": "./all-people-groups/all-people-groups.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./all-people-groups/all-people-groups/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=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-xpQXT3X2W81IPWaSXifrP9kXGf/nbU8hwT5ZWMQjH0EwDWHMJeAiWj+yEuAi+KBx\" crossorigin=\"anonymous\"></script>\n<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>"
|
||||
"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=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-ZDia7fZFwFRQlQ9EaeWXDOxKbMC7Uv9zMg+k43Ok4UhWgOjDbPxW9JJd+GEivhyr\" crossorigin=\"anonymous\"></script>\n<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>"
|
||||
},
|
||||
"bundle": "all-people-groups"
|
||||
}
|
||||
|
|
@ -165,7 +165,7 @@
|
|||
"bundle": "country-posters",
|
||||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"output": "dx-maps/country-posters/country-posters.js",
|
||||
"integrity": "sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd",
|
||||
"integrity": "sha384-kkdmZjOS8KJTTdcUmnMU5bpeVJZBMPFw4mb1rdZ3K2dacuRGxLSiCH4L7d6BN8dI",
|
||||
"elements": [
|
||||
"country-posters-map"
|
||||
],
|
||||
|
|
@ -187,7 +187,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/countries-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=\"./country-posters/country-posters.js\" integrity=\"sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-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=\"./country-posters/country-posters.js\" integrity=\"sha384-kkdmZjOS8KJTTdcUmnMU5bpeVJZBMPFw4mb1rdZ3K2dacuRGxLSiCH4L7d6BN8dI\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -202,7 +202,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/country-poster-wall/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=\"./country-posters/country-posters.js\" integrity=\"sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-kkdmZjOS8KJTTdcUmnMU5bpeVJZBMPFw4mb1rdZ3K2dacuRGxLSiCH4L7d6BN8dI\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -217,7 +217,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/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=\"./country-posters/country-posters.js\" integrity=\"sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-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=\"./country-posters/country-posters.js\" integrity=\"sha384-kkdmZjOS8KJTTdcUmnMU5bpeVJZBMPFw4mb1rdZ3K2dacuRGxLSiCH4L7d6BN8dI\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -226,7 +226,7 @@
|
|||
"bundle": "dashboard-plus-grid",
|
||||
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
|
||||
"output": "dx-maps/dashboard-plus-grid/dashboard-plus-grid.js",
|
||||
"integrity": "sha384-gLs4MQRznMyg9cKyDneIKEkjCDfd/Buvv+G8Xwwn+ggxaKshHtD1zKO658e8L6pw",
|
||||
"integrity": "sha384-G2cfmNzjdjkx6ZLplzjiMJ/zUO5UzGNUprk+Uo409T5tqQkFk7qD4RkFsn4mzOZv",
|
||||
"elements": [
|
||||
"dashboard-plus-grid"
|
||||
],
|
||||
|
|
@ -248,7 +248,7 @@
|
|||
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
|
||||
"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>",
|
||||
"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-gLs4MQRznMyg9cKyDneIKEkjCDfd/Buvv+G8Xwwn+ggxaKshHtD1zKO658e8L6pw\" 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-G2cfmNzjdjkx6ZLplzjiMJ/zUO5UzGNUprk+Uo409T5tqQkFk7qD4RkFsn4mzOZv\" 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": [
|
||||
{
|
||||
|
|
@ -272,7 +272,7 @@
|
|||
"bundle": "demo-map",
|
||||
"scriptUrl": "./demo-map/demo-map.js",
|
||||
"output": "dx-maps/demo-map/demo-map.js",
|
||||
"integrity": "sha384-IVZC654ptY88sI56Xw0Mlmc58TNBXDzTR2+WrGRwv5LYPALSneviSCNrpg61Wzi6",
|
||||
"integrity": "sha384-Q3BuUcCOHvp7ko/LLlos7iF71x6bq4+TjfYQjbgCl2dynmwe7h7yCO94yNUdjoOG",
|
||||
"elements": [
|
||||
"demo-map"
|
||||
],
|
||||
|
|
@ -290,7 +290,7 @@
|
|||
"scriptUrl": "./demo-map/demo-map.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./demo-map/demo-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=\"./demo-map/demo-map.js\" integrity=\"sha384-IVZC654ptY88sI56Xw0Mlmc58TNBXDzTR2+WrGRwv5LYPALSneviSCNrpg61Wzi6\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-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=\"./demo-map/demo-map.js\" integrity=\"sha384-Q3BuUcCOHvp7ko/LLlos7iF71x6bq4+TjfYQjbgCl2dynmwe7h7yCO94yNUdjoOG\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-map>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -371,7 +371,7 @@
|
|||
"bundle": "doxa-research-map",
|
||||
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
|
||||
"output": "dx-maps/doxa-research-map/doxa-research-map.js",
|
||||
"integrity": "sha384-QF6F4GttfIpkev5vLjQn5SftilFmw7HksE+bvoYu5WccjETMjVxPoeqEB3wqmGh7",
|
||||
"integrity": "sha384-yfb9suM6BjJ8npI9/Ok9mmEUDxxuWUqB6gXCB33cCgLMfOeDtGwszF+fJiwAw//n",
|
||||
"elements": [
|
||||
"doxa-research-map"
|
||||
],
|
||||
|
|
@ -389,7 +389,7 @@
|
|||
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
|
||||
"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>",
|
||||
"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-QF6F4GttfIpkev5vLjQn5SftilFmw7HksE+bvoYu5WccjETMjVxPoeqEB3wqmGh7\" 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-yfb9suM6BjJ8npI9/Ok9mmEUDxxuWUqB6gXCB33cCgLMfOeDtGwszF+fJiwAw//n\" 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": [
|
||||
{
|
||||
|
|
@ -415,7 +415,7 @@
|
|||
"bundle": "doxa-simple-map",
|
||||
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
|
||||
"output": "dx-maps/doxa-simple-map/doxa-simple-map.js",
|
||||
"integrity": "sha384-qz7rw+kKZQs04E7Am98uyFYO8TfhD8uGy73NUFfbiR7Nag2rON9XEyjzgeg6c6Yn",
|
||||
"integrity": "sha384-bPFak/dvNd3d7uNUAJfMGiZudEOZlEM26mVv3IiEIN4Xi/y+mc9P36HhXV3r1O79",
|
||||
"elements": [
|
||||
"doxa-map",
|
||||
"doxa-simple-map"
|
||||
|
|
@ -434,7 +434,7 @@
|
|||
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-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-simple-map/doxa-simple-map.js\" integrity=\"sha384-qz7rw+kKZQs04E7Am98uyFYO8TfhD8uGy73NUFfbiR7Nag2rON9XEyjzgeg6c6Yn\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-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-simple-map/doxa-simple-map.js\" integrity=\"sha384-bPFak/dvNd3d7uNUAJfMGiZudEOZlEM26mVv3IiEIN4Xi/y+mc9P36HhXV3r1O79\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -604,7 +604,7 @@
|
|||
"bundle": "gospel-internet",
|
||||
"scriptUrl": "./gospel-internet/gospel-internet.js",
|
||||
"output": "dx-maps/gospel-internet/gospel-internet.js",
|
||||
"integrity": "sha384-oCqqOQ/nxQoV8hoX75YIVe3DBUOC+yWpy6TGCCsmoN6iNzctsiO8tR2FklINZLnq",
|
||||
"integrity": "sha384-FNeWKsULwwuFfsy7c6d5c7K9mi9MTGUDIJablvw48K8xMzkfcvQJUrjRvIta6klm",
|
||||
"elements": [
|
||||
"gospel-internet-board"
|
||||
],
|
||||
|
|
@ -623,7 +623,7 @@
|
|||
"scriptUrl": "./gospel-internet/gospel-internet.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-internet/board/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-oCqqOQ/nxQoV8hoX75YIVe3DBUOC+yWpy6TGCCsmoN6iNzctsiO8tR2FklINZLnq\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-FNeWKsULwwuFfsy7c6d5c7K9mi9MTGUDIJablvw48K8xMzkfcvQJUrjRvIta6klm\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -723,7 +723,7 @@
|
|||
"bundle": "gospel-resources",
|
||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"output": "dx-maps/gospel-resources/gospel-resources.js",
|
||||
"integrity": "sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq",
|
||||
"integrity": "sha384-2FCDJcK//gT/pWmCpB50HlBexpcO3SdbRlATTvtSQGWCEYohocvr9Hvt2U2hbCNR",
|
||||
"elements": [
|
||||
"gospel-resources-map"
|
||||
],
|
||||
|
|
@ -745,7 +745,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"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>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" 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-2FCDJcK//gT/pWmCpB50HlBexpcO3SdbRlATTvtSQGWCEYohocvr9Hvt2U2hbCNR\" 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>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -760,7 +760,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"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>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" 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-2FCDJcK//gT/pWmCpB50HlBexpcO3SdbRlATTvtSQGWCEYohocvr9Hvt2U2hbCNR\" 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>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -775,7 +775,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"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>",
|
||||
"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-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" 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-2FCDJcK//gT/pWmCpB50HlBexpcO3SdbRlATTvtSQGWCEYohocvr9Hvt2U2hbCNR\" 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>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -790,7 +790,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"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>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" 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-2FCDJcK//gT/pWmCpB50HlBexpcO3SdbRlATTvtSQGWCEYohocvr9Hvt2U2hbCNR\" 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>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -805,7 +805,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"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>",
|
||||
"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-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" 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-2FCDJcK//gT/pWmCpB50HlBexpcO3SdbRlATTvtSQGWCEYohocvr9Hvt2U2hbCNR\" 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>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -814,7 +814,7 @@
|
|||
"bundle": "my-upg-100-list",
|
||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"output": "dx-maps/my-upg-100-list/my-upg-100-list.js",
|
||||
"integrity": "sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR",
|
||||
"integrity": "sha384-meHgA2FfgqXz885IdY5fY9VgMQDHEBhl1nsot6tRszitV8aub/mWfBRoLnAmeFxU",
|
||||
"elements": [
|
||||
"my-upg-100-list-map"
|
||||
],
|
||||
|
|
@ -836,7 +836,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-meHgA2FfgqXz885IdY5fY9VgMQDHEBhl1nsot6tRszitV8aub/mWfBRoLnAmeFxU\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -851,7 +851,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-meHgA2FfgqXz885IdY5fY9VgMQDHEBhl1nsot6tRszitV8aub/mWfBRoLnAmeFxU\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -866,7 +866,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-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=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-meHgA2FfgqXz885IdY5fY9VgMQDHEBhl1nsot6tRszitV8aub/mWfBRoLnAmeFxU\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -938,7 +938,7 @@
|
|||
"bundle": "plugin-studio",
|
||||
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
||||
"output": "dx-maps/plugin-studio/plugin-studio.js",
|
||||
"integrity": "sha384-IQjG7XqoeppiQ+mxxtUxQApF7LoNWEc+LuQEftMZxWdDyht1X1u0mnB6LvDKAMJX",
|
||||
"integrity": "sha384-L2MCcGfR6X5/0VxHzMs9eDvMsdSYufZ66OhQdodC7vwcNgM7op8u8jNjayoJsySd",
|
||||
"elements": [
|
||||
"plugin-studio"
|
||||
],
|
||||
|
|
@ -968,7 +968,7 @@
|
|||
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./plugin-studio/plugin-studio/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-IQjG7XqoeppiQ+mxxtUxQApF7LoNWEc+LuQEftMZxWdDyht1X1u0mnB6LvDKAMJX\" 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-L2MCcGfR6X5/0VxHzMs9eDvMsdSYufZ66OhQdodC7vwcNgM7op8u8jNjayoJsySd\" 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": [
|
||||
{
|
||||
|
|
@ -995,7 +995,7 @@
|
|||
"bundle": "pplr-data-maps",
|
||||
"scriptUrl": "./pplr-data-maps/pplr-data-maps.js",
|
||||
"output": "dx-maps/pplr-data-maps/pplr-data-maps.js",
|
||||
"integrity": "sha384-idrM+onVQ8aeioB8d67pktceaA4tHSoJCH3OX1eHhr5YHoI+X6/JSePapupxL9Rc",
|
||||
"integrity": "sha384-ITBwAAzT9m4q3VvhBRMSwYGuJlTi/SFb5K3fyqqhPaa9FdbafyIPbI1Iu/YtWbae",
|
||||
"elements": [
|
||||
"pplr-data-maps"
|
||||
],
|
||||
|
|
@ -1013,7 +1013,7 @@
|
|||
"scriptUrl": "./pplr-data-maps/pplr-data-maps.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./pplr-data-maps/pplr-data-maps/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=\"./pplr-data-maps/pplr-data-maps.js\" integrity=\"sha384-idrM+onVQ8aeioB8d67pktceaA4tHSoJCH3OX1eHhr5YHoI+X6/JSePapupxL9Rc\" crossorigin=\"anonymous\"></script>\n<pplr-data-maps profile-config='{\"profile\":\"pplr-data-maps\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></pplr-data-maps>"
|
||||
"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=\"./pplr-data-maps/pplr-data-maps.js\" integrity=\"sha384-ITBwAAzT9m4q3VvhBRMSwYGuJlTi/SFb5K3fyqqhPaa9FdbafyIPbI1Iu/YtWbae\" crossorigin=\"anonymous\"></script>\n<pplr-data-maps profile-config='{\"profile\":\"pplr-data-maps\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></pplr-data-maps>"
|
||||
},
|
||||
"bundle": "pplr-data-maps"
|
||||
}
|
||||
|
|
@ -1023,7 +1023,7 @@
|
|||
"bundle": "prayer-walk-planner",
|
||||
"scriptUrl": "./prayer-walk-planner/prayer-walk-planner.js",
|
||||
"output": "dx-maps/prayer-walk-planner/prayer-walk-planner.js",
|
||||
"integrity": "sha384-zEGH2wLAtRReRGWJJcYWgfxVmjx/cmNBh1HWp9EoO77UdnIdaB+SGDQJ5P1cl7/N",
|
||||
"integrity": "sha384-vDE4FMYhrLgkHnj6K2nRGEbLyf8EyuV7OUo+F93bGd00+/F+MRpev49x/iXsllFq",
|
||||
"elements": [
|
||||
"prayer-walk-planner"
|
||||
],
|
||||
|
|
@ -1041,7 +1041,7 @@
|
|||
"scriptUrl": "./prayer-walk-planner/prayer-walk-planner.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./prayer-walk-planner/prayer-walk-planner/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=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-zEGH2wLAtRReRGWJJcYWgfxVmjx/cmNBh1HWp9EoO77UdnIdaB+SGDQJ5P1cl7/N\" crossorigin=\"anonymous\"></script>\n<prayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></prayer-walk-planner>"
|
||||
"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=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-vDE4FMYhrLgkHnj6K2nRGEbLyf8EyuV7OUo+F93bGd00+/F+MRpev49x/iXsllFq\" crossorigin=\"anonymous\"></script>\n<prayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></prayer-walk-planner>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -1050,7 +1050,7 @@
|
|||
"bundle": "regions-dashboard",
|
||||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"output": "dx-maps/regions-dashboard/regions-dashboard.js",
|
||||
"integrity": "sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q",
|
||||
"integrity": "sha384-aCE8BYISZb9Awa2lEWI0A3qgAc5ZFh1Q8EkSwnTyn2SizzOeyPwCRP8wz+oiVTmQ",
|
||||
"elements": [
|
||||
"regions-dashboard"
|
||||
],
|
||||
|
|
@ -1072,7 +1072,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-aCE8BYISZb9Awa2lEWI0A3qgAc5ZFh1Q8EkSwnTyn2SizzOeyPwCRP8wz+oiVTmQ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1087,7 +1087,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-aCE8BYISZb9Awa2lEWI0A3qgAc5ZFh1Q8EkSwnTyn2SizzOeyPwCRP8wz+oiVTmQ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1102,7 +1102,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-aCE8BYISZb9Awa2lEWI0A3qgAc5ZFh1Q8EkSwnTyn2SizzOeyPwCRP8wz+oiVTmQ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -1111,7 +1111,7 @@
|
|||
"bundle": "research-dashboard",
|
||||
"scriptUrl": "./research-dashboard/research-dashboard.js",
|
||||
"output": "dx-maps/research-dashboard/research-dashboard.js",
|
||||
"integrity": "sha384-GBlVmWSyzwQX0ty93zufhUFNTSJoDn8RF+7ywLJ0Q5sx5RLUoC2yt7W/XPtuX7LB",
|
||||
"integrity": "sha384-Oqb5Jww1i+G5kkTq8Bggw/DrP3F8T34jIRCgvR80AkiMJ3/WskZKVJl2M64iRwOe",
|
||||
"elements": [
|
||||
"research-dashboard-map"
|
||||
],
|
||||
|
|
@ -1129,7 +1129,7 @@
|
|||
"scriptUrl": "./research-dashboard/research-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./research-dashboard/research-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=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-GBlVmWSyzwQX0ty93zufhUFNTSJoDn8RF+7ywLJ0Q5sx5RLUoC2yt7W/XPtuX7LB\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-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=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-Oqb5Jww1i+G5kkTq8Bggw/DrP3F8T34jIRCgvR80AkiMJ3/WskZKVJl2M64iRwOe\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
|
||||
},
|
||||
"bundle": "research-dashboard"
|
||||
}
|
||||
|
|
@ -1181,6 +1181,36 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"bundle": "rudder-voice",
|
||||
"scriptUrl": "./rudder-voice/rudder-voice.js",
|
||||
"output": "dx-maps/rudder-voice/rudder-voice.js",
|
||||
"integrity": "sha384-Pb1pQJqoB3wMTum8vw0uUrm7e74j90ytj1hNdIl/1HbRlfy1YoT0hf6ggWQ3qDNO",
|
||||
"elements": [
|
||||
"rudder-voice"
|
||||
],
|
||||
"stage": "experimental",
|
||||
"profiles": [
|
||||
{
|
||||
"profile": "rudder-voice",
|
||||
"stage": "experimental",
|
||||
"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-Pb1pQJqoB3wMTum8vw0uUrm7e74j90ytj1hNdIl/1HbRlfy1YoT0hf6ggWQ3qDNO\" 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",
|
||||
"scriptUrl": "./stack-demo/stack-demo.js",
|
||||
|
|
@ -1231,7 +1261,7 @@
|
|||
"bundle": "upg-neighbor",
|
||||
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
|
||||
"output": "dx-maps/upg-neighbor/upg-neighbor.js",
|
||||
"integrity": "sha384-D2e4OB60VjRAhfnyVG192dx3hxC9tfP1WQMu1E+JcINQ2XRO7tCUhXXNZBMoQrD+",
|
||||
"integrity": "sha384-X6YaUyV+9g7wppNaQ+YWgER7pRrk8y4egSZypQF8+2lVYRith+nPvkpiI77Oh6p3",
|
||||
"elements": [
|
||||
"upg-neighbor"
|
||||
],
|
||||
|
|
@ -1249,7 +1279,7 @@
|
|||
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./upg-neighbor/upg-neighbor/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=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-D2e4OB60VjRAhfnyVG192dx3hxC9tfP1WQMu1E+JcINQ2XRO7tCUhXXNZBMoQrD+\" crossorigin=\"anonymous\"></script>\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
|
||||
"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=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-X6YaUyV+9g7wppNaQ+YWgER7pRrk8y4egSZypQF8+2lVYRith+nPvkpiI77Oh6p3\" crossorigin=\"anonymous\"></script>\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
|
||||
},
|
||||
"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>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-meHgA2FfgqXz885IdY5fY9VgMQDHEBhl1nsot6tRszitV8aub/mWfBRoLnAmeFxU" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-meHgA2FfgqXz885IdY5fY9VgMQDHEBhl1nsot6tRszitV8aub/mWfBRoLnAmeFxU" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-meHgA2FfgqXz885IdY5fY9VgMQDHEBhl1nsot6tRszitV8aub/mWfBRoLnAmeFxU" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// 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>
|
||||
<!-- Bundle IIFE — registers the <plugin-studio> custom element. -->
|
||||
<script src="../plugin-studio.js" integrity="sha384-IQjG7XqoeppiQ+mxxtUxQApF7LoNWEc+LuQEftMZxWdDyht1X1u0mnB6LvDKAMJX" crossorigin="anonymous"></script>
|
||||
<script src="../plugin-studio.js" integrity="sha384-L2MCcGfR6X5/0VxHzMs9eDvMsdSYufZ66OhQdodC7vwcNgM7op8u8jNjayoJsySd" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <plugin-studio> custom element. -->
|
||||
<script src="../../plugin-studio.js" integrity="sha384-IQjG7XqoeppiQ+mxxtUxQApF7LoNWEc+LuQEftMZxWdDyht1X1u0mnB6LvDKAMJX" crossorigin="anonymous"></script>
|
||||
<script src="../../plugin-studio.js" integrity="sha384-L2MCcGfR6X5/0VxHzMs9eDvMsdSYufZ66OhQdodC7vwcNgM7op8u8jNjayoJsySd" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
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>
|
||||
<!-- Bundle IIFE — registers the <pplr-data-maps> custom element. -->
|
||||
<script src="../pplr-data-maps.js" integrity="sha384-idrM+onVQ8aeioB8d67pktceaA4tHSoJCH3OX1eHhr5YHoI+X6/JSePapupxL9Rc" crossorigin="anonymous"></script>
|
||||
<script src="../pplr-data-maps.js" integrity="sha384-ITBwAAzT9m4q3VvhBRMSwYGuJlTi/SFb5K3fyqqhPaa9FdbafyIPbI1Iu/YtWbae" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
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>
|
||||
<!-- Bundle IIFE — registers the <prayer-walk-planner> custom element. -->
|
||||
<script src="../prayer-walk-planner.js" integrity="sha384-zEGH2wLAtRReRGWJJcYWgfxVmjx/cmNBh1HWp9EoO77UdnIdaB+SGDQJ5P1cl7/N" crossorigin="anonymous"></script>
|
||||
<script src="../prayer-walk-planner.js" integrity="sha384-vDE4FMYhrLgkHnj6K2nRGEbLyf8EyuV7OUo+F93bGd00+/F+MRpev49x/iXsllFq" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-aCE8BYISZb9Awa2lEWI0A3qgAc5ZFh1Q8EkSwnTyn2SizzOeyPwCRP8wz+oiVTmQ" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// 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>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-aCE8BYISZb9Awa2lEWI0A3qgAc5ZFh1Q8EkSwnTyn2SizzOeyPwCRP8wz+oiVTmQ" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-aCE8BYISZb9Awa2lEWI0A3qgAc5ZFh1Q8EkSwnTyn2SizzOeyPwCRP8wz+oiVTmQ" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
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>
|
||||
<!-- Bundle IIFE — registers the <research-dashboard-map> custom element. -->
|
||||
<script src="../research-dashboard.js" integrity="sha384-GBlVmWSyzwQX0ty93zufhUFNTSJoDn8RF+7ywLJ0Q5sx5RLUoC2yt7W/XPtuX7LB" crossorigin="anonymous"></script>
|
||||
<script src="../research-dashboard.js" integrity="sha384-Oqb5Jww1i+G5kkTq8Bggw/DrP3F8T34jIRCgvR80AkiMJ3/WskZKVJl2M64iRwOe" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
23
public/dx-maps/rudder-voice/rudder-voice.js
Normal file
23
public/dx-maps/rudder-voice/rudder-voice.js
Normal file
File diff suppressed because one or more lines are too long
214
public/dx-maps/rudder-voice/rudder-voice/index.html
Normal file
214
public/dx-maps/rudder-voice/rudder-voice/index.html
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
<!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-Pb1pQJqoB3wMTum8vw0uUrm7e74j90ytj1hNdIl/1HbRlfy1YoT0hf6ggWQ3qDNO" 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
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue