Files
peardock/tunnel-browser.html
T
Raven Scott e20ae7b41f
Release rolling / release (push) Successful in 7m37s
Style Holesail tunnel browser with peardock titlebar and pear-ctrl
Open local tunnel URLs in a framed Electron window that matches main app
chrome (hiddenInset/frameless, pear-ctrl, brand) instead of a bare popup.
2026-07-17 20:26:51 -04:00

484 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Holesail tunnel — peardock</title>
<meta name="theme-color" content="#0a0c10">
<link rel="icon" href="assets/favicons/favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicons/favicon-32x32.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="ui/modern.css">
<link rel="stylesheet" href="ui/theme-light.css">
<style>
/* Tunnel shell: reuse #titlebar tokens from modern.css; layout is shell-specific */
html, body {
margin: 0;
height: 100%;
overflow: hidden;
background: var(--bg-primary, #0a0c10);
color: var(--text-primary, #f4f7fb);
font-family: var(--font-sans, 'Inter', system-ui, sans-serif);
-webkit-font-smoothing: antialiased;
}
body.pd-tunnel-shell {
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* Titlebar already fixed in modern.css — reserve space for content */
.pd-tunnel-main {
flex: 1 1 auto;
display: flex;
flex-direction: column;
margin-top: var(--titlebar-h, 42px);
height: calc(100vh - var(--titlebar-h, 42px));
min-height: 0;
}
.pd-tunnel-toolbar {
display: flex;
align-items: center;
gap: 8px;
flex: 0 0 auto;
padding: 6px 12px;
background: var(--bg-secondary, #0f131a);
border-bottom: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
-webkit-app-region: no-drag;
}
.pd-tunnel-nav-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 28px;
margin: 0;
padding: 0;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--text-secondary, #94a3b8);
cursor: pointer;
transition: background 0.12s ease, color 0.12s ease, opacity 0.12s ease;
}
.pd-tunnel-nav-btn:hover:not(:disabled) {
background: var(--bg-hover, rgba(148, 163, 184, 0.16));
color: var(--text-primary, #e2e8f0);
}
.pd-tunnel-nav-btn:disabled {
opacity: 0.35;
cursor: default;
}
.pd-tunnel-nav-btn:focus-visible {
outline: 2px solid var(--accent-secondary, #2dd4bf);
outline-offset: 1px;
}
.pd-tunnel-url {
flex: 1 1 auto;
min-width: 0;
height: 28px;
padding: 0 12px;
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
border-radius: 8px;
background: var(--bg-tertiary, #151b24);
color: var(--text-secondary, #c8d1df);
font-family: var(--font-mono, ui-monospace, Menlo, monospace);
font-size: 12px;
line-height: 28px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
user-select: text;
}
.pd-tunnel-badge {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0 10px;
height: 28px;
border-radius: 999px;
border: 1px solid rgba(52, 211, 153, 0.28);
background: rgba(52, 211, 153, 0.1);
color: var(--accent-primary, #34d399);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.pd-tunnel-badge .dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent-primary, #34d399);
box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.18);
}
.pd-tunnel-frame-wrap {
flex: 1 1 auto;
min-height: 0;
position: relative;
background: var(--bg-primary, #0a0c10);
}
/* BrowserView mode: main process paints content below chrome; keep region clear */
body.pd-tunnel-view .pd-tunnel-frame-wrap {
background: transparent;
}
body.pd-tunnel-view .pd-tunnel-frame {
display: none;
}
/* BrowserView covers the content area — status lives in the chrome, not under the view */
body.pd-tunnel-view .pd-tunnel-status {
display: none !important;
}
.pd-tunnel-url.is-loading {
color: var(--accent-secondary, #2dd4bf);
}
.pd-tunnel-url.is-error {
color: var(--accent-danger, #f87171);
border-color: rgba(248, 113, 113, 0.45);
}
.pd-tunnel-frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: 0;
background: #fff;
}
.pd-tunnel-status {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
padding: 2rem;
background: var(--bg-primary, #0a0c10);
color: var(--text-secondary, #c8d1df);
text-align: center;
z-index: 2;
}
.pd-tunnel-status.hidden {
display: none;
}
.pd-tunnel-status h2 {
margin: 0;
font-size: 15px;
font-weight: 650;
color: var(--text-primary, #f4f7fb);
}
.pd-tunnel-status p {
margin: 0;
max-width: 36rem;
font-size: 13px;
line-height: 1.45;
color: var(--text-muted, #9aa8bc);
word-break: break-all;
}
.pd-tunnel-spinner {
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid rgba(52, 211, 153, 0.2);
border-top-color: var(--accent-primary, #34d399);
animation: pd-tunnel-spin 0.7s linear infinite;
}
@keyframes pd-tunnel-spin {
to { transform: rotate(360deg); }
}
/* Titlebar right: tunnel label */
#titlebar .titlebar-right .pd-tunnel-title-label {
font-size: 12px;
font-weight: 500;
color: var(--text-secondary, #c8d1df);
max-width: 42vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
html[data-theme='light'] .pd-tunnel-toolbar {
background: #ffffff;
}
html[data-theme='light'] .pd-tunnel-url {
background: #f8fafc;
color: #334155;
}
html[data-theme='light'] .pd-tunnel-status {
background: #f3f5f8;
}
html[data-theme='light'] .pd-tunnel-frame-wrap {
background: #fff;
}
</style>
<script>
(function () {
try {
var raw = JSON.parse(localStorage.getItem('peardock.settings.v1') || '{}');
var pref = String(raw.theme || 'dark').toLowerCase();
if (pref !== 'light' && pref !== 'system') pref = 'dark';
var resolved = pref;
if (pref === 'system') {
resolved =
window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches
? 'light'
: 'dark';
}
document.documentElement.setAttribute('data-theme', resolved === 'light' ? 'light' : 'dark');
document.documentElement.setAttribute('data-theme-pref', pref);
if (resolved === 'light') {
var meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute('content', '#f3f5f8');
}
} catch (e) {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
</head>
<body class="pd-tunnel-shell">
<div id="titlebar">
<div class="titlebar-left">
<pear-ctrl></pear-ctrl>
<div class="app-brand">
<div class="app-brand-mark" aria-hidden="true">
<img src="assets/logo/peardock-icon-64.png" alt="" width="22" height="22">
</div>
<div class="app-brand-text">peardock <span>tunnel</span></div>
</div>
</div>
<div class="titlebar-right">
<span class="pd-tunnel-title-label" id="tb-page-title">Holesail</span>
</div>
</div>
<div class="pd-tunnel-main">
<div class="pd-tunnel-toolbar" role="toolbar" aria-label="Tunnel browser">
<button type="button" class="pd-tunnel-nav-btn" id="btn-back" title="Back" aria-label="Back" disabled>
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path fill="currentColor" d="M8.8 2.2 4 7l4.8 4.8.9-.9L5.8 7l3.9-3.9-.9-.9z"/></svg>
</button>
<button type="button" class="pd-tunnel-nav-btn" id="btn-forward" title="Forward" aria-label="Forward" disabled>
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path fill="currentColor" d="M5.2 2.2 9.98 7 5.2 11.8l-.9-.9L8.2 7 4.3 3.1l.9-.9z"/></svg>
</button>
<button type="button" class="pd-tunnel-nav-btn" id="btn-reload" title="Reload" aria-label="Reload">
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path fill="currentColor" d="M12 7a5 5 0 1 1-1.3-3.4l.7.7A4 4 0 1 0 11 7h-2l2.5 2.5L14 7h-2zm-5-4v1.1A4 4 0 0 0 3 7H2a5 5 0 0 1 5-4z"/></svg>
</button>
<div class="pd-tunnel-url" id="tb-url" title=""></div>
<div class="pd-tunnel-badge" title="Local Holesail client">
<span class="dot" aria-hidden="true"></span>
Holesail
</div>
</div>
<div class="pd-tunnel-frame-wrap">
<div class="pd-tunnel-status" id="tb-status" role="status">
<div class="pd-tunnel-spinner" aria-hidden="true"></div>
<h2>Opening tunnel…</h2>
<p id="tb-status-detail">Connecting to local Holesail endpoint</p>
</div>
<iframe class="pd-tunnel-frame" id="tb-frame" title="Tunnel content"></iframe>
</div>
</div>
<script>
(function () {
var params = new URLSearchParams(window.location.search);
var target = (params.get('url') || '').trim();
var initialTitle = (params.get('title') || '').trim();
var mode = (params.get('mode') || '').trim(); // "view" = Electron BrowserView
var frame = document.getElementById('tb-frame');
var statusEl = document.getElementById('tb-status');
var statusDetail = document.getElementById('tb-status-detail');
var urlEl = document.getElementById('tb-url');
var titleEl = document.getElementById('tb-page-title');
var btnBack = document.getElementById('btn-back');
var btnForward = document.getElementById('btn-forward');
var btnReload = document.getElementById('btn-reload');
var ipcRenderer = null;
try {
if (typeof require === 'function') {
ipcRenderer = require('electron').ipcRenderer;
}
} catch (e) {
ipcRenderer = null;
}
var useView = mode === 'view' && ipcRenderer;
function isAllowedLocalUrl(href) {
try {
var u = new URL(href);
if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
var host = (u.hostname || '').toLowerCase();
return host === '127.0.0.1' || host === 'localhost' || host === '[::1]' || host === '::1';
} catch {
return false;
}
}
function setUrlBar(href) {
urlEl.textContent = href || '—';
urlEl.title = href || '';
}
function setPageTitle(text) {
var t = String(text || 'Holesail').trim() || 'Holesail';
titleEl.textContent = t;
document.title = t + ' — peardock tunnel';
}
function hideStatus() {
statusEl.classList.add('hidden');
}
function showStatus(heading, detail) {
statusEl.classList.remove('hidden');
var h = statusEl.querySelector('h2');
if (h) h.textContent = heading || 'Tunnel';
statusDetail.textContent = detail || '';
}
function applyNavState(state) {
if (!state) return;
if (state.url) setUrlBar(state.url);
if (state.title) setPageTitle(state.title);
btnBack.disabled = !state.canGoBack;
btnForward.disabled = !state.canGoForward;
urlEl.classList.toggle('is-loading', Boolean(state.loading) && !state.error);
urlEl.classList.toggle('is-error', Boolean(state.error));
if (state.error) {
urlEl.title = state.error + (state.url ? ' — ' + state.url : '');
setPageTitle('Failed to load');
} else if (state.loading) {
urlEl.title = 'Loading… ' + (state.url || target);
} else {
urlEl.title = state.url || '';
}
}
if (!target || !isAllowedLocalUrl(target)) {
showStatus(
'Invalid tunnel URL',
target
? 'Only local http(s) endpoints (127.0.0.1 / localhost) can be opened here.'
: 'No tunnel URL was provided.'
);
setUrlBar(target || '');
frame.removeAttribute('src');
return;
}
setUrlBar(target);
try {
var u0 = new URL(target);
setPageTitle(initialTitle || u0.host + (u0.port ? ':' + u0.port : ''));
} catch (e) {
setPageTitle(initialTitle || 'Holesail');
}
statusDetail.textContent = target;
if (useView) {
document.body.classList.add('pd-tunnel-view');
frame.removeAttribute('src');
ipcRenderer.on('peardock:tunnel-nav-state', function (_evt, state) {
applyNavState(state);
});
btnReload.addEventListener('click', function () {
showStatus('Reloading…', urlEl.textContent || target);
ipcRenderer.invoke('peardock:tunnel-nav', 'reload');
});
btnBack.addEventListener('click', function () {
ipcRenderer.invoke('peardock:tunnel-nav', 'back');
});
btnForward.addEventListener('click', function () {
ipcRenderer.invoke('peardock:tunnel-nav', 'forward');
});
return;
}
// Fallback: iframe (browser / window.open path)
function updateNav() {
try {
var hist = frame.contentWindow.history;
btnBack.disabled = !hist || hist.length <= 1;
btnForward.disabled = false;
} catch (e) {
btnBack.disabled = true;
btnForward.disabled = true;
}
}
frame.addEventListener('load', function () {
hideStatus();
try {
var loc = frame.contentWindow && frame.contentWindow.location && frame.contentWindow.location.href;
if (loc && loc !== 'about:blank') setUrlBar(loc);
var docTitle =
frame.contentDocument && frame.contentDocument.title
? frame.contentDocument.title
: '';
if (docTitle) setPageTitle(docTitle);
} catch (e) {
// keep prior URL
}
updateNav();
});
frame.addEventListener('error', function () {
showStatus('Failed to load', target);
});
btnReload.addEventListener('click', function () {
showStatus('Reloading…', urlEl.textContent || target);
try {
frame.contentWindow.location.reload();
} catch (e) {
frame.src = urlEl.textContent || target;
}
});
btnBack.addEventListener('click', function () {
try { frame.contentWindow.history.back(); } catch (e) { /* ignore */ }
});
btnForward.addEventListener('click', function () {
try { frame.contentWindow.history.forward(); } catch (e) { /* ignore */ }
});
frame.src = target;
})();
</script>
</body>
</html>