Release rolling / release (push) Successful in 7m37s
Open local tunnel URLs in a framed Electron window that matches main app chrome (hiddenInset/frameless, pear-ctrl, brand) instead of a bare popup.
484 lines
15 KiB
HTML
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>
|