Skip to content

Commit 1731f39

Browse files
Eric Migicovskyclaude
andcommitted
qemu-wasm: render Clay data: config pages in an in-page modal
Browsers block window.open on data: URLs, which is how Clay-based apps deliver their settings pages. Decode the data: URL, rewrite Clay's default pebblejs://close target to the same-origin return page, and render the page in a modal iframe; the return page now posts to parent as well as opener. Hosted config pages keep the popup + return_to path. Verified with a Clay-style page round-tripping its settings payload in Chromium. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
1 parent fe8f51a commit 1731f39

4 files changed

Lines changed: 132 additions & 18 deletions

File tree

tools/qemu-wasm/site-dist/config-return.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
// response payload in the fragment (Rebble convention). Hand it to the
99
// opener and close.
1010
var payload = location.hash ? location.hash.slice(1) : '';
11-
if (window.opener) {
12-
window.opener.postMessage({ type: 'pkjs-config-closed', response: payload }, location.origin);
11+
var target = window.opener || (window.parent !== window ? window.parent : null);
12+
if (target) {
13+
target.postMessage({ type: 'pkjs-config-closed', response: payload }, location.origin);
1314
document.body.textContent = 'Settings saved - you can close this tab.';
1415
setTimeout(function () { window.close(); }, 300);
1516
} else {

tools/qemu-wasm/site-dist/index.html

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -498,20 +498,75 @@ <h1>PebbleOS Emulator</h1>
498498
};
499499
}
500500

501-
// Config pages: open in a popup with return_to pointing at our
502-
// return page, which posts the response fragment back.
503-
function openConfigUrl(url, onClosed) {
504-
const ret = new URL('config-return.html', window.location.href).href;
505-
const sep = url.includes('?') ? '&' : '?';
506-
const full = url + sep + 'return_to=' + encodeURIComponent(ret + '#');
501+
// Config pages. Hosted (http/https) pages open in a popup with the
502+
// Rebble return_to convention. Clay-style data: pages cannot be
503+
// window.open()ed (browsers block top-level data: navigation), so
504+
// they render in an in-page modal iframe instead: the HTML is
505+
// decoded and Clay's default close target (pebblejs://close) is
506+
// rewritten to our same-origin return page, which posts the
507+
// response fragment back via postMessage.
508+
function listenForConfigClose(onClosed, cleanup) {
507509
const handler = (e) => {
508510
if (e.origin !== window.location.origin) return;
509511
if (e.data && e.data.type === 'pkjs-config-closed') {
510512
window.removeEventListener('message', handler);
513+
if (cleanup) cleanup();
511514
onClosed(e.data.response);
512515
}
513516
};
514517
window.addEventListener('message', handler);
518+
return handler;
519+
}
520+
521+
function decodeDataUrl(url) {
522+
const m = /^data:[^,]*?(;base64)?,(.*)$/s.exec(url);
523+
if (!m) return null;
524+
return m[1] ? atob(m[2]) : decodeURIComponent(m[2]);
525+
}
526+
527+
function openConfigUrl(url, onClosed) {
528+
const ret = new URL('config-return.html', window.location.href).href;
529+
530+
if (url.startsWith('data:')) {
531+
let html = decodeDataUrl(url);
532+
if (html === null) { say('Could not decode the settings page.'); return; }
533+
// Clay falls back to pebblejs://close when no return_to query
534+
// parameter reaches it (data: URLs have no query string).
535+
html = html.split('pebblejs://close').join(ret);
536+
537+
const overlay = document.createElement('div');
538+
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.7);' +
539+
'z-index:1000;display:flex;align-items:center;justify-content:center;';
540+
const box = document.createElement('div');
541+
box.style.cssText = 'position:relative;width:min(440px,94vw);height:min(720px,92vh);' +
542+
'background:#fff;border-radius:8px;overflow:hidden;display:flex;flex-direction:column;';
543+
const bar = document.createElement('div');
544+
bar.style.cssText = 'background:#1e1e30;color:#d8d8e0;font-size:12px;' +
545+
'padding:6px 10px;display:flex;justify-content:space-between;align-items:center;';
546+
bar.innerHTML = '<span>App settings</span>';
547+
const closeBtn = document.createElement('button');
548+
closeBtn.textContent = 'Cancel';
549+
closeBtn.style.cssText = 'font-size:12px;padding:2px 10px;';
550+
bar.appendChild(closeBtn);
551+
const frame = document.createElement('iframe');
552+
frame.style.cssText = 'border:0;flex:1;width:100%;background:#fff;';
553+
frame.srcdoc = html;
554+
box.appendChild(bar); box.appendChild(frame);
555+
overlay.appendChild(box);
556+
document.body.appendChild(overlay);
557+
558+
const handler = listenForConfigClose(onClosed, () => overlay.remove());
559+
closeBtn.addEventListener('click', () => {
560+
window.removeEventListener('message', handler);
561+
overlay.remove();
562+
onClosed(''); // empty response = user cancelled
563+
});
564+
return;
565+
}
566+
567+
const sep = url.includes('?') ? '&' : '?';
568+
const full = url + sep + 'return_to=' + encodeURIComponent(ret + '#');
569+
listenForConfigClose(onClosed);
515570
window.open(full, 'pkjs-config', 'width=420,height=680');
516571
}
517572

@@ -612,8 +667,9 @@ <h1>PebbleOS Emulator</h1>
612667
await installBytes(new Uint8Array(await f.arrayBuffer()), f.name);
613668
});
614669

615-
// Debug/testing hook: install from raw bytes without the store.
670+
// Debug/testing hooks.
616671
window.__installPbw = installBytes;
672+
window.__openConfig = openConfigUrl;
617673

618674
// Drag-drop a .pbw anywhere on the page.
619675
document.addEventListener('dragover', (e) => {

tools/qemu-wasm/web/config-return.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
// response payload in the fragment (Rebble convention). Hand it to the
99
// opener and close.
1010
var payload = location.hash ? location.hash.slice(1) : '';
11-
if (window.opener) {
12-
window.opener.postMessage({ type: 'pkjs-config-closed', response: payload }, location.origin);
11+
var target = window.opener || (window.parent !== window ? window.parent : null);
12+
if (target) {
13+
target.postMessage({ type: 'pkjs-config-closed', response: payload }, location.origin);
1314
document.body.textContent = 'Settings saved - you can close this tab.';
1415
setTimeout(function () { window.close(); }, 300);
1516
} else {

tools/qemu-wasm/web/index.html

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -498,20 +498,75 @@ <h1>PebbleOS Emulator</h1>
498498
};
499499
}
500500

501-
// Config pages: open in a popup with return_to pointing at our
502-
// return page, which posts the response fragment back.
503-
function openConfigUrl(url, onClosed) {
504-
const ret = new URL('config-return.html', window.location.href).href;
505-
const sep = url.includes('?') ? '&' : '?';
506-
const full = url + sep + 'return_to=' + encodeURIComponent(ret + '#');
501+
// Config pages. Hosted (http/https) pages open in a popup with the
502+
// Rebble return_to convention. Clay-style data: pages cannot be
503+
// window.open()ed (browsers block top-level data: navigation), so
504+
// they render in an in-page modal iframe instead: the HTML is
505+
// decoded and Clay's default close target (pebblejs://close) is
506+
// rewritten to our same-origin return page, which posts the
507+
// response fragment back via postMessage.
508+
function listenForConfigClose(onClosed, cleanup) {
507509
const handler = (e) => {
508510
if (e.origin !== window.location.origin) return;
509511
if (e.data && e.data.type === 'pkjs-config-closed') {
510512
window.removeEventListener('message', handler);
513+
if (cleanup) cleanup();
511514
onClosed(e.data.response);
512515
}
513516
};
514517
window.addEventListener('message', handler);
518+
return handler;
519+
}
520+
521+
function decodeDataUrl(url) {
522+
const m = /^data:[^,]*?(;base64)?,(.*)$/s.exec(url);
523+
if (!m) return null;
524+
return m[1] ? atob(m[2]) : decodeURIComponent(m[2]);
525+
}
526+
527+
function openConfigUrl(url, onClosed) {
528+
const ret = new URL('config-return.html', window.location.href).href;
529+
530+
if (url.startsWith('data:')) {
531+
let html = decodeDataUrl(url);
532+
if (html === null) { say('Could not decode the settings page.'); return; }
533+
// Clay falls back to pebblejs://close when no return_to query
534+
// parameter reaches it (data: URLs have no query string).
535+
html = html.split('pebblejs://close').join(ret);
536+
537+
const overlay = document.createElement('div');
538+
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.7);' +
539+
'z-index:1000;display:flex;align-items:center;justify-content:center;';
540+
const box = document.createElement('div');
541+
box.style.cssText = 'position:relative;width:min(440px,94vw);height:min(720px,92vh);' +
542+
'background:#fff;border-radius:8px;overflow:hidden;display:flex;flex-direction:column;';
543+
const bar = document.createElement('div');
544+
bar.style.cssText = 'background:#1e1e30;color:#d8d8e0;font-size:12px;' +
545+
'padding:6px 10px;display:flex;justify-content:space-between;align-items:center;';
546+
bar.innerHTML = '<span>App settings</span>';
547+
const closeBtn = document.createElement('button');
548+
closeBtn.textContent = 'Cancel';
549+
closeBtn.style.cssText = 'font-size:12px;padding:2px 10px;';
550+
bar.appendChild(closeBtn);
551+
const frame = document.createElement('iframe');
552+
frame.style.cssText = 'border:0;flex:1;width:100%;background:#fff;';
553+
frame.srcdoc = html;
554+
box.appendChild(bar); box.appendChild(frame);
555+
overlay.appendChild(box);
556+
document.body.appendChild(overlay);
557+
558+
const handler = listenForConfigClose(onClosed, () => overlay.remove());
559+
closeBtn.addEventListener('click', () => {
560+
window.removeEventListener('message', handler);
561+
overlay.remove();
562+
onClosed(''); // empty response = user cancelled
563+
});
564+
return;
565+
}
566+
567+
const sep = url.includes('?') ? '&' : '?';
568+
const full = url + sep + 'return_to=' + encodeURIComponent(ret + '#');
569+
listenForConfigClose(onClosed);
515570
window.open(full, 'pkjs-config', 'width=420,height=680');
516571
}
517572

@@ -612,8 +667,9 @@ <h1>PebbleOS Emulator</h1>
612667
await installBytes(new Uint8Array(await f.arrayBuffer()), f.name);
613668
});
614669

615-
// Debug/testing hook: install from raw bytes without the store.
670+
// Debug/testing hooks.
616671
window.__installPbw = installBytes;
672+
window.__openConfig = openConfigUrl;
617673

618674
// Drag-drop a .pbw anywhere on the page.
619675
document.addEventListener('dragover', (e) => {

0 commit comments

Comments
 (0)