/** * 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 stays in the DOM as the value/form source (kept in sync both ways). // Opt out per field with data-trek-native; multi/size selects are left alone. function dispatch(el, type) { var ev; try { ev = new Event(type, { bubbles: true }); } catch (e) { ev = document.createEvent('Event'); ev.initEvent(type, true, false); } el.dispatchEvent(ev); } function enhanceSelect(sel) { if (!sel || sel.__trekSelect || sel.hasAttribute('data-trek-native')) { return; } if (sel.multiple || sel.size > 1 || !sel.parentNode) { return; } sel.__trekSelect = true; var wrap = document.createElement('div'); wrap.className = 'trek-select-wrap'; sel.parentNode.insertBefore(wrap, sel); wrap.appendChild(sel); sel.classList.add('trek-select-native-hidden'); sel.setAttribute('tabindex', '-1'); sel.setAttribute('aria-hidden', 'true'); var trigger = document.createElement('button'); trigger.type = 'button'; trigger.className = 'trek-select trek-select-trigger'; trigger.setAttribute('aria-haspopup', 'listbox'); trigger.setAttribute('aria-expanded', 'false'); if (sel.disabled) { trigger.disabled = true; } var valueEl = document.createElement('span'); valueEl.className = 'trek-select-value'; var caret = document.createElement('span'); caret.className = 'trek-select-caret'; caret.innerHTML = ''; trigger.appendChild(valueEl); trigger.appendChild(caret); wrap.appendChild(trigger); var menu = document.createElement('div'); menu.className = 'trek-select-menu'; menu.setAttribute('role', 'listbox'); menu.hidden = true; wrap.appendChild(menu); var activeIdx = -1; function syncTrigger() { var o = sel.options[sel.selectedIndex]; valueEl.textContent = o ? o.text : ''; } function buildMenu() { menu.innerHTML = ''; for (var i = 0; i < sel.options.length; i++) { var o = sel.options[i]; var item = document.createElement('div'); item.className = 'trek-select-option'; item.setAttribute('role', 'option'); item.setAttribute('data-idx', String(i)); item.setAttribute('aria-selected', i === sel.selectedIndex ? 'true' : 'false'); if (o.disabled) { item.setAttribute('aria-disabled', 'true'); } item.textContent = o.text; menu.appendChild(item); } } function highlight(idx) { var opts = menu.children; for (var i = 0; i < opts.length; i++) { opts[i].className = i === idx ? 'trek-select-option trek-active' : 'trek-select-option'; } if (idx >= 0 && opts[idx] && opts[idx].scrollIntoView) { opts[idx].scrollIntoView({ block: 'nearest' }); } activeIdx = idx; } function onDocDown(e) { if (!wrap.contains(e.target)) { closeMenu(); } } function openMenu() { if (trigger.disabled || !menu.hidden) { return; } buildMenu(); menu.hidden = false; trigger.setAttribute('aria-expanded', 'true'); // Flip above the trigger when there isn't room below in the frame viewport. var r = trigger.getBoundingClientRect(); var below = window.innerHeight - r.bottom; if (below < Math.min(260, menu.scrollHeight + 12) && r.top > below) { menu.setAttribute('data-placement', 'top'); } else { menu.removeAttribute('data-placement'); } highlight(sel.selectedIndex); reportHeight(); document.addEventListener('mousedown', onDocDown, true); } function closeMenu() { if (menu.hidden) { return; } menu.hidden = true; trigger.setAttribute('aria-expanded', 'false'); document.removeEventListener('mousedown', onDocDown, true); reportHeight(); } function nextEnabled(from, dir) { var i = from; for (var n = 0; n < sel.options.length; n++) { i += dir; if (i < 0) { i = sel.options.length - 1; } if (i >= sel.options.length) { i = 0; } if (!sel.options[i].disabled) { return i; } } return from; } function commit(idx) { if (idx < 0 || idx >= sel.options.length || sel.options[idx].disabled) { return; } if (sel.selectedIndex !== idx) { sel.selectedIndex = idx; dispatch(sel, 'input'); dispatch(sel, 'change'); } syncTrigger(); closeMenu(); trigger.focus(); } trigger.addEventListener('click', function () { if (menu.hidden) { openMenu(); } else { closeMenu(); } }); trigger.addEventListener('keydown', function (e) { var k = e.key; if (menu.hidden) { if (k === 'ArrowDown' || k === 'ArrowUp' || k === 'Enter' || k === ' ' || k === 'Spacebar') { e.preventDefault(); openMenu(); } return; } if (k === 'Escape') { e.preventDefault(); closeMenu(); trigger.focus(); } else if (k === 'ArrowDown') { e.preventDefault(); highlight(nextEnabled(activeIdx, 1)); } else if (k === 'ArrowUp') { e.preventDefault(); highlight(nextEnabled(activeIdx, -1)); } else if (k === 'Home') { e.preventDefault(); highlight(nextEnabled(-1, 1)); } else if (k === 'End') { e.preventDefault(); highlight(nextEnabled(0, -1)); } else if (k === 'Enter' || k === ' ' || k === 'Spacebar') { e.preventDefault(); commit(activeIdx); } else if (k === 'Tab') { closeMenu(); } }); menu.addEventListener('mousedown', function (e) { e.preventDefault(); }); // keep focus on the trigger menu.addEventListener('click', function (e) { var t = e.target; while (t && t !== menu && !(t.getAttribute && t.hasAttribute('data-idx'))) { t = t.parentNode; } if (t && t.getAttribute && t.hasAttribute('data-idx')) { commit(parseInt(t.getAttribute('data-idx'), 10)); } }); menu.addEventListener('mousemove', function (e) { var t = e.target; while (t && t !== menu && !(t.getAttribute && t.hasAttribute('data-idx'))) { t = t.parentNode; } if (t && t.getAttribute && t.hasAttribute('data-idx')) { highlight(parseInt(t.getAttribute('data-idx'), 10)); } }); // The plugin may set select.value itself — mirror it back to the trigger. sel.addEventListener('change', syncTrigger); syncTrigger(); } function enhanceAllSelects(root) { var live = (root || document).getElementsByTagName('select'); var arr = []; for (var i = 0; i < live.length; i++) { arr.push(live[i]); } for (var j = 0; j < arr.length; j++) { enhanceSelect(arr[j]); } } function boot() { if (document.body) { document.body.classList.add('trek-ui'); } enhanceAllSelects(document); if (typeof MutationObserver !== 'undefined' && document.body) { new MutationObserver(function (muts) { for (var i = 0; i < muts.length; i++) { var added = muts[i].addedNodes; for (var j = 0; j < added.length; j++) { var n = added[j]; if (!n || n.nodeType !== 1) { continue; } if (n.tagName === 'SELECT') { enhanceSelect(n); } else if (n.getElementsByTagName) { enhanceAllSelects(n); } } } }).observe(document.body, { childList: true, subtree: true }); } api.ready(); reportHeight(); if (typeof ResizeObserver !== 'undefined' && document.body) { new ResizeObserver(reportHeight).observe(document.body); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();`; /** * Replace the `` marker in a plugin's HTML with the inlined kit * (style + bootstrap). A no-op when the marker is absent, so it is safe to run over * any HTML. The source file on disk is never touched — the expansion happens at * dev-serve / pack time, so an author's `client/index.html` stays a one-line opt-in. */ export function injectTrekUi(html: string): string { if (!html.includes(TREK_UI_MARKER)) return html; const block = `\n`; return html.split(TREK_UI_MARKER).join(block); }