/**
* The TREK plugin design kit (#plugins).
*
* A plugin's UI runs in a sandboxed, opaque-origin iframe — it can't load TREK's
* stylesheet, only postMessage. So instead of forcing every author to re-derive the
* look, we ship it: a token-driven stylesheet (`TREK_UI_CSS`) plus a tiny bootstrap
* (`TREK_THEME_JS`) that wires the frame to the host. Both are plain strings, meant
* to be INLINED into the plugin's own `client/index.html` (the CSP forbids external
* /` (typically via
* the `` marker). It: announces readiness; applies the host's theme
* tokens, theme name and appearance flags to the document; auto-reports its height so
* a widget/page self-sizes; and installs a small `window.trek` helper over the same
* bridge messages the host already understands — it adds no new capability.
*/
export const TREK_THEME_JS = `(function () {
'use strict';
var docEl = document.documentElement;
var ctxHandlers = [];
var evtHandlers = [];
var lastCtx = null;
var pending = {};
var pendingConfirms = {};
var pendingGeo = {};
var geoWatchers = [];
var seq = 0;
var lastH = -1;
function send(msg) { try { window.parent.postMessage(msg, '*'); } catch (e) {} }
function setFlag(name, on) { if (on) { docEl.setAttribute(name, ''); } else { docEl.removeAttribute(name); } }
function applyContext(m) {
if (m.theme) { docEl.setAttribute('data-theme', m.theme); }
if (m.locale) { docEl.setAttribute('lang', m.locale); }
docEl.setAttribute('dir', m.dir === 'rtl' ? 'rtl' : 'ltr');
var t = m.tokens || {};
for (var k in t) {
if (Object.prototype.hasOwnProperty.call(t, k) && t[k]) { docEl.style.setProperty(k, t[k]); }
}
var a = m.appearance || {};
setFlag('data-reduce-motion', a.reducedMotion);
setFlag('data-no-transparency', a.noTransparency);
if (a.density) { docEl.setAttribute('data-density', a.density); }
if (a.scheme) { docEl.setAttribute('data-scheme', a.scheme); }
if (document.body) { document.body.classList.add('trek-ui'); }
}
function reportHeight() {
var h = Math.ceil(document.documentElement.scrollHeight);
if (h > 0 && h !== lastH) { lastH = h; send({ type: 'trek:resize', height: h }); }
}
window.addEventListener('message', function (ev) {
// Opaque frame: origin serialises to 'null', so trust the SENDER — only our real
// parent window. Never act on a claimed id or on origin.
if (ev.source !== window.parent) { return; }
var m = ev.data;
if (!m || typeof m !== 'object') { return; }
if (m.type === 'trek:context') {
lastCtx = m; api.context = m;
applyContext(m);
for (var i = 0; i < ctxHandlers.length; i++) { try { ctxHandlers[i](m); } catch (e) {} }
reportHeight();
} else if (m.type === 'trek:response') {
var p = pending[m.requestId];
if (p) { delete pending[m.requestId]; p.resolve(m.data); }
} else if (m.type === 'trek:error') {
var q = pending[m.requestId];
if (q) { delete pending[m.requestId]; var err = new Error(m.message || 'invoke failed'); err.code = m.code; q.reject(err); }
} else if (m.type === 'trek:confirm:result') {
var c = pendingConfirms[m.requestId];
if (c) { delete pendingConfirms[m.requestId]; c(!!m.confirmed); }
} else if (m.type === 'trek:geolocation:result') {
var g = pendingGeo[m.requestId];
if (g) {
delete pendingGeo[m.requestId];
if (m.error) { var gerr = new Error('geolocation: ' + m.error); gerr.code = m.error; g.reject(gerr); }
else { g.resolve(m.position || m); }
}
} else if (m.type === 'trek:geolocation:update') {
for (var w = 0; w < geoWatchers.length; w++) { try { geoWatchers[w](m.position || null, m.error || null); } catch (e) {} }
} else if (m.type === 'trek:event') {
for (var j = 0; j < evtHandlers.length; j++) { try { evtHandlers[j](m.event, m.tripId); } catch (e) {} }
}
});
// Native DOM helpers so a widget can build kit-styled UI with no bundler and no
// hand-written CSS — every element carries the same trek-* classes the kit ships.
function mkEl(tag, props, children) {
var node = document.createElement(tag);
props = props || {};
for (var k in props) {
if (!Object.prototype.hasOwnProperty.call(props, k)) { continue; }
var v = props[k];
if (v == null) { continue; }
if (k === 'class' || k === 'className') { node.className = v; }
else if (k === 'text') { node.textContent = v; }
else if (k === 'html') { node.innerHTML = v; }
else if (k === 'on') { for (var ev in v) { if (Object.prototype.hasOwnProperty.call(v, ev)) { node.addEventListener(ev, v[ev]); } } }
else { node.setAttribute(k, v); }
}
var kids = children == null ? [] : (typeof children === 'string' || children.nodeType ? [children] : children);
for (var i = 0; i < kids.length; i++) {
var c = kids[i];
if (c == null) { continue; }
node.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
}
return node;
}
var ui = {
el: mkEl,
button: function (label, opts) {
opts = opts || {};
return mkEl('button', { class: 'trek-btn' + (opts.variant ? ' trek-btn--' + opts.variant : ''), type: 'button', text: label, on: opts.onClick ? { click: opts.onClick } : null }, null);
},
card: function (children) { return mkEl('div', { class: 'trek-card' }, children); },
chip: function (text, variant) { return mkEl('span', { class: 'trek-chip' + (variant ? ' trek-chip--' + variant : ''), text: text }, null); },
input: function (opts) { opts = opts || {}; return mkEl('input', { class: 'trek-input', type: opts.type || 'text', placeholder: opts.placeholder || '', value: opts.value || '' }, null); },
mount: function (node, target) { (target || document.body).appendChild(node); return node; }
};
var session = {
get: function (key, opts) {
var id = 's' + (++seq);
return new Promise(function (resolve, reject) {
pending[id] = { resolve: resolve, reject: reject };
send({ type: 'trek:session:get', requestId: id, key: key, scope: opts && opts.scope });
});
},
set: function (key, value, opts) {
var id = 's' + (++seq);
return new Promise(function (resolve, reject) {
pending[id] = { resolve: resolve, reject: reject };
send({ type: 'trek:session:set', requestId: id, key: key, value: value, scope: opts && opts.scope });
});
},
remove: function (key, opts) {
var id = 's' + (++seq);
return new Promise(function (resolve, reject) {
pending[id] = { resolve: resolve, reject: reject };
send({ type: 'trek:session:remove', requestId: id, key: key, scope: opts && opts.scope });
});
},
clear: function (opts) {
var id = 's' + (++seq);
return new Promise(function (resolve, reject) {
pending[id] = { resolve: resolve, reject: reject };
send({ type: 'trek:session:clear', requestId: id, scope: opts && opts.scope });
});
}
};
var api = {
context: null,
ui: ui,
session: session,
ready: function () { send({ type: 'trek:ready' }); },
requestContext: function () { send({ type: 'trek:context:request' }); },
onContext: function (cb) {
ctxHandlers.push(cb);
if (lastCtx) { try { cb(lastCtx); } catch (e) {} }
return function () { var i = ctxHandlers.indexOf(cb); if (i >= 0) { ctxHandlers.splice(i, 1); } };
},
notify: function (level, message, duration) { send({ type: 'trek:notify', level: level, message: message, duration: duration }); },
navigate: function (to) { send({ type: 'trek:navigate', to: to }); },
openExternal: function (url) { send({ type: 'trek:openExternal', url: url }); },
resize: function (px) { var h = px | 0; if (h > 0) { lastH = h; send({ type: 'trek:resize', height: h }); } },
// Host-rendered native confirm dialog; resolves true/false. The host shows one
// at a time — a second concurrent request resolves false immediately.
confirm: function (opts) {
opts = typeof opts === 'string' ? { message: opts } : (opts || {});
var id = 'c' + (++seq);
return new Promise(function (resolve) {
pendingConfirms[id] = resolve;
send({ type: 'trek:confirm', requestId: id, title: opts.title, message: opts.message, confirmLabel: opts.confirmLabel, cancelLabel: opts.cancelLabel, danger: opts.danger });
});
},
// Core-event names for the trip in view ({ event, tripId } only, no payloads) —
// refetch via invoke() when something relevant fires instead of polling.
onEvent: function (cb) {
evtHandlers.push(cb);
return function () { var i = evtHandlers.indexOf(cb); if (i >= 0) { evtHandlers.splice(i, 1); } };
},
invoke: function (sub, opts) {
opts = opts || {};
var id = 'r' + (++seq);
return new Promise(function (resolve, reject) {
pending[id] = { resolve: resolve, reject: reject };
send({ type: 'trek:invoke', requestId: id, sub: sub, method: opts.method, body: opts.body });
});
},
// Host-brokered browser position (needs the geolocation:read grant; the
// browser's own site prompt still applies). get() resolves one position;
// watch(cb) streams updates — cb(position, error) — until every returned
// unsubscribe ran, which also releases the host's GPS watch.
geolocation: {
get: function () {
var id = 'g' + (++seq);
return new Promise(function (resolve, reject) {
pendingGeo[id] = { resolve: resolve, reject: reject };
send({ type: 'trek:geolocation', requestId: id, action: 'get' });
});
},
watch: function (cb) {
geoWatchers.push(cb);
if (geoWatchers.length === 1) {
var id = 'g' + (++seq);
// The start ack resolves silently; a start error (e.g. forbidden) is
// surfaced through the callback so watch() itself never throws.
pendingGeo[id] = { resolve: function () {}, reject: function (e) { try { cb(null, e && e.code ? e.code : 'unavailable'); } catch (x) {} } };
send({ type: 'trek:geolocation', requestId: id, action: 'watch' });
}
return function () {
var i = geoWatchers.indexOf(cb);
if (i >= 0) { geoWatchers.splice(i, 1); }
if (geoWatchers.length === 0) {
var cid = 'g' + (++seq);
pendingGeo[cid] = { resolve: function () {}, reject: function () {} };
send({ type: 'trek:geolocation', requestId: cid, action: 'clear' });
}
};
}
}
};
window.trek = api;
// --- Native