Release rolling / release (push) Successful in 7m25s
Place the Holesail page title in the middle of the chrome with ellipsis so long document titles no longer overflow off the right edge.
520 lines
16 KiB
HTML
520 lines
16 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); }
|
|
}
|
|
|
|
/* Centered page title (absolute under fixed #titlebar so long names ellipsize, not overflow) */
|
|
#titlebar .titlebar-center {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 0;
|
|
bottom: 0;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
max-width: min(48%, 26rem);
|
|
width: max-content;
|
|
padding: 0 12px;
|
|
pointer-events: none;
|
|
z-index: 1;
|
|
-webkit-app-region: drag;
|
|
}
|
|
|
|
#titlebar .pd-tunnel-title-label {
|
|
display: block;
|
|
max-width: 100%;
|
|
min-width: 0;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--text-secondary, #c8d1df);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
text-align: center;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
/* Left brand / right spacer sit above the centered title if they collide */
|
|
#titlebar .titlebar-left,
|
|
#titlebar .titlebar-right {
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
/* Empty right slot balances pear-ctrl + brand so the title stays optically centered */
|
|
#titlebar.pd-tunnel-titlebar .titlebar-right {
|
|
flex: 0 0 auto;
|
|
min-width: 120px;
|
|
min-height: 1px;
|
|
}
|
|
|
|
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" class="pd-tunnel-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-center" aria-live="polite">
|
|
<span class="pd-tunnel-title-label" id="tb-page-title">Holesail</span>
|
|
</div>
|
|
<div class="titlebar-right" aria-hidden="true"></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>
|