redesign browser chrome UI: unified surface, real icons, polished tabs and omnibox

- single design-token block (colors/spacing/radii/type) drives every chrome surface
- tab strip and toolbar merged into one cohesive surface instead of stacked bars
- inline SVG icons replace text glyphs (back/forward/reload, ads, downloads, properties, new tab, KIR pin, close) with tooltips and aria labels
- tabs: rounded shapes, favicon letter badges, masked title truncation, hover close/pin, clear active/hover/suspended states
- URL bar: pill with site-info indicator (HTTPS lock / HTTP warning / internal page), focus glow, select-all on focus
- right-side controls grouped into one capsule; adblock and downloads show clear on/off states
- downloads panel and permission prompt restyled; properties page matches the same tokens
- behavior unchanged: all existing buttons, IPC commands and shortcuts (Ctrl+L/T/W, F5, Ctrl+R) work as before
This commit is contained in:
Martin Dvořák 2026-10-11 18:05:26 +02:00
commit 5c7f28b656

View file

@ -143,92 +143,293 @@ const CHROME_HTML: &str = r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="color-scheme" content="dark">
<style>
:root {
--bg-top: #1d1e23;
--bg-bottom: #191a1e;
--surface: rgba(255,255,255,.05);
--surface-hover: rgba(255,255,255,.08);
--surface-active: rgba(255,255,255,.12);
--well: rgba(0,0,0,.30);
--border: rgba(255,255,255,.09);
--border-strong: rgba(255,255,255,.15);
--text: #eef0f4;
--text-dim: #9aa1af;
--accent: #4c7dff;
--accent-strong: #5d8bff;
--accent-soft: rgba(76,125,255,.25);
--accent-text: #aac2ff;
--warn: #e0b25c;
--ok: #46c07a;
--danger: #e06a6a;
--radius-xs: 6px;
--radius-sm: 8px;
--radius-md: 10px;
--radius-lg: 12px;
--radius-pill: 999px;
--speed: 120ms;
--ease: cubic-bezier(.2,.8,.3,1);
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, sans-serif;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; overflow: hidden; background: #101012;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
-webkit-user-select: none; user-select: none; }
#bar { display: flex; flex-direction: column; height: 100%;
padding: 6px 8px; gap: 6px;
border-bottom: 1px solid rgba(255,255,255,0.15); }
#tabs-top { display: flex; align-items: center; gap: 6px; height: 30px; }
#tabs-row { flex: 1; display: flex; align-items: center; gap: 4px;
height: 30px; overflow-x: auto; overflow-y: hidden; }
#nav-row { display: flex; align-items: center; gap: 6px; height: 36px; }
#downloads-row { display: none; flex-direction: column; align-items: stretch; gap: 6px; overflow-y: auto;
border: 1px solid rgba(255,255,255,0.18); border-radius: 0; padding: 8px;
background: rgba(255,255,255,0.07); min-height: 92px; }
.dl-empty { font-size: 12px; opacity: .75; }
.dl-item { display: flex; flex-direction: column; gap: 4px; border: 1px solid rgba(255,255,255,0.16);
border-radius: 0; padding: 6px 8px; background: rgba(0,0,0,0.2); color: #e8e8ed; }
.dl-title { font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dl-meta { font-size: 11px; opacity: .82; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dl-track { width: 100%; height: 6px; background: rgba(255,255,255,0.18); border-radius: 0; overflow: hidden; }
.dl-fill { height: 100%; width: 0%; border-radius: 0; background: linear-gradient(90deg, #4f8bff, #72d8ff);
transform-origin: left center; }
html, body { height: 100%; overflow: hidden; }
body {
background: var(--bg-bottom);
color: var(--text);
font-family: var(--font);
font-size: 13px;
-webkit-user-select: none; user-select: none;
}
#chrome {
display: flex; flex-direction: column; gap: 4px;
height: 100%; padding: 6px 8px 5px;
background: linear-gradient(180deg, var(--bg-top), var(--bg-bottom));
border-bottom: 1px solid var(--border);
}
/* ---------- Icon buttons ---------- */
.icon-btn {
flex: 0 0 auto;
display: inline-flex; align-items: center; justify-content: center;
width: 30px; height: 30px; padding: 0;
border: 1px solid transparent; border-radius: var(--radius-sm);
background: transparent; color: var(--text-dim);
cursor: pointer; outline: none;
transition: background-color var(--speed) var(--ease), color var(--speed) var(--ease),
border-color var(--speed) var(--ease), transform var(--speed) var(--ease);
}
.icon-btn svg { display: block; width: 16px; height: 16px; }
.icon-btn:hover { background: var(--surface-hover); color: var(--text); }
.icon-btn:active { background: var(--surface-active); transform: scale(.94); }
.icon-btn:focus-visible { box-shadow: 0 0 0 3px var(--accent-soft); color: var(--text); }
.icon-btn.active { background: var(--accent-soft); color: var(--accent-text); }
/* ---------- Tab strip ---------- */
#tabs-row {
display: flex; align-items: center; gap: 6px;
flex: 0 0 34px; height: 34px; min-width: 0;
}
#tabs {
display: flex; align-items: center; gap: 4px;
flex: 1 1 auto; min-width: 0; height: 100%;
overflow-x: auto; overflow-y: hidden; scrollbar-width: none;
}
#tabs::-webkit-scrollbar { display: none; }
#add-btn { width: 28px; height: 28px; }
#add-btn svg { width: 15px; height: 15px; }
.tab {
position: relative;
display: flex; align-items: center; gap: 4px;
flex: 1 1 auto; min-width: 112px; max-width: 220px; height: 30px;
padding: 0 6px 0 8px;
border: 1px solid transparent; border-radius: var(--radius-sm);
color: var(--text-dim);
cursor: default; outline: none;
transition: background-color var(--speed) var(--ease), color var(--speed) var(--ease),
border-color var(--speed) var(--ease);
}
.tab:hover { background: var(--surface-hover); color: var(--text); }
.tab.active { background: var(--surface-active); border-color: var(--border); color: var(--text); }
.tab:focus-visible { box-shadow: 0 0 0 2px var(--accent-soft); }
.tab-favicon {
flex: 0 0 auto; width: 16px; height: 16px; border-radius: 5px;
display: inline-flex; align-items: center; justify-content: center;
font-size: 9px; font-weight: 700; line-height: 1; color: #fff;
text-transform: uppercase;
box-shadow: inset 0 0 0 1px rgba(255,255,255,.14);
}
.tab-title {
flex: 1 1 auto; min-width: 0; font-size: 12.5px; line-height: 1;
white-space: nowrap; overflow: hidden;
-webkit-mask-image: linear-gradient(90deg, black calc(100% - 14px), transparent);
mask-image: linear-gradient(90deg, black calc(100% - 14px), transparent);
}
.tab.sleeping .tab-title { opacity: .6; }
.tab.sleeping .tab-favicon { opacity: .55; }
.tab-sleep {
flex: 0 0 auto; display: none; align-items: center; justify-content: center;
color: var(--warn); opacity: .85;
}
.tab-sleep svg { display: block; width: 12px; height: 12px; }
.tab.sleeping .tab-sleep { display: inline-flex; }
.tab-pin, .tab-close {
flex: 0 0 auto;
display: inline-flex; align-items: center; justify-content: center;
width: 20px; height: 20px; padding: 0;
border: 0; border-radius: var(--radius-xs);
background: transparent; color: var(--text-dim);
cursor: pointer; outline: none; opacity: 0; pointer-events: none;
transition: background-color var(--speed) var(--ease), color var(--speed) var(--ease),
opacity var(--speed) var(--ease);
}
.tab-pin svg, .tab-close svg { display: block; width: 12px; height: 12px; }
.tab:hover .tab-pin, .tab:hover .tab-close,
.tab.active .tab-pin, .tab.active .tab-close,
.tab:focus-within .tab-pin, .tab:focus-within .tab-close {
opacity: .85; pointer-events: auto;
}
.tab-pin:hover, .tab-close:hover { background: var(--surface-active); color: var(--text); opacity: 1; }
.tab-pin.on { opacity: 1; pointer-events: auto; color: var(--accent-text); background: var(--accent-soft); }
.tab-pin.on:hover { color: #c9d8ff; background: rgba(76,125,255,.35); }
/* ---------- Toolbar ---------- */
#nav-row {
display: flex; align-items: center; gap: 4px;
flex: 0 0 34px; height: 34px; min-width: 0;
}
#url-pill {
flex: 1 1 auto; min-width: 0; margin: 0 4px;
display: flex; align-items: center; gap: 7px;
height: 32px; padding: 0 12px;
background: var(--well);
border: 1px solid var(--border);
border-radius: var(--radius-pill);
transition: border-color var(--speed) var(--ease), background-color var(--speed) var(--ease),
box-shadow var(--speed) var(--ease);
}
#url-pill:hover { border-color: var(--border-strong); }
#url-pill:focus-within {
border-color: var(--accent);
background: rgba(0,0,0,.42);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.site-icon {
flex: 0 0 auto;
display: inline-flex; align-items: center; justify-content: center;
width: 16px; height: 16px; color: var(--text-dim);
transition: color var(--speed) var(--ease);
}
.site-icon svg { display: none; width: 14px; height: 14px; }
.site-icon[data-state='search'] .i-search,
.site-icon[data-state='secure'] .i-secure,
.site-icon[data-state='insecure'] .i-insecure,
.site-icon[data-state='internal'] .i-internal { display: block; }
.site-icon[data-state='secure'] { color: #8fd6a8; }
.site-icon[data-state='insecure'] { color: var(--warn); }
.site-icon[data-state='internal'] { color: var(--accent-text); }
#url-input {
flex: 1 1 auto; min-width: 0; height: 100%; padding: 0;
border: 0; outline: none; background: transparent;
color: var(--text); font: inherit; font-size: 13px;
-webkit-user-select: text; user-select: text;
}
#url-input::placeholder { color: var(--text-dim); opacity: .85; }
#actions {
flex: 0 0 auto; margin-left: 4px;
display: inline-flex; align-items: center; gap: 2px; padding: 2px;
border: 1px solid var(--border); border-radius: var(--radius-md);
background: var(--surface);
}
#actions .icon-btn { width: 28px; height: 28px; border-radius: 7px; }
#adblock-btn .icon-on { display: none; }
#adblock-btn.active .icon-on { display: block; }
#adblock-btn.active .icon-off { display: none; }
/* ---------- Downloads panel ---------- */
#downloads-row {
display: none; flex: 1 1 auto; min-height: 0;
flex-direction: column; gap: 6px; overflow-y: auto; padding: 8px;
border: 1px solid var(--border); border-radius: var(--radius-lg);
background: var(--surface);
}
#downloads-row::-webkit-scrollbar { width: 8px; }
#downloads-row::-webkit-scrollbar-thumb { background: rgba(255,255,255,.16); border-radius: var(--radius-pill); }
.dl-empty {
display: flex; flex: 1 1 auto; flex-direction: column;
align-items: center; justify-content: center; gap: 6px;
color: var(--text-dim); font-size: 12px;
}
.dl-empty svg { width: 20px; height: 20px; opacity: .7; }
.dl-item {
flex: 0 0 auto;
display: flex; flex-direction: column; gap: 5px;
padding: 8px 10px;
border: 1px solid var(--border); border-radius: var(--radius-sm);
background: rgba(0,0,0,.22);
}
.dl-title {
font-size: 12px; font-weight: 600; color: var(--text);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dl-meta {
font-size: 11px; color: var(--text-dim);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dl-track { width: 100%; height: 5px; border-radius: var(--radius-pill); background: rgba(255,255,255,.10); overflow: hidden; }
.dl-fill { height: 100%; width: 0%; border-radius: var(--radius-pill); background: linear-gradient(90deg, #4f8bff, #72d8ff); transform-origin: left center; }
.dl-fill.indeterminate { width: 35%; animation: glide 1.05s ease-in-out infinite; }
@keyframes glide { 0% { transform: translateX(-120%); } 100% { transform: translateX(320%); } }
button { flex-shrink: 0; height: 30px; min-width: 30px; padding: 0 8px;
border: 1px solid rgba(255,255,255,0.2); border-radius: 0;
background: rgba(255,255,255,0.1); color: #e8e8ed;
font-size: 14px; cursor: pointer; outline: none; }
button:active { background: rgba(255,255,255,0.2); }
#url-input { flex: 1; height: 30px; border: 1px solid rgba(255,255,255,0.2);
border-radius: 0; padding: 4px 10px; font-size: 13px; outline: none;
color: #f5f5f7; background: rgba(255,255,255,0.07);
-webkit-user-select: text; user-select: text; }
#url-input:focus { border-color: rgba(90,140,255,0.7); background: rgba(255,255,255,0.1); }
.tab-btn { position: relative; display: inline-flex; align-items: center;
max-width: 170px; height: 26px; font-size: 12px;
padding-right: 42px; }
.tab-btn.unloaded:not(.active) { opacity: .72; }
.tab-title { display: block; width: 100%;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tab-btn.active { background: rgba(90,140,255,0.75); border-color: rgba(90,140,255,0.5); }
.kir-btn { position: absolute; right: 22px; top: 2px;
width: 18px; height: 18px; min-width: 18px; border-radius: 0;
border: 1px solid rgba(255,255,255,0.2); background: rgba(0,0,0,0.25);
color: #fff; font-size: 10px; line-height: 16px; padding: 0; }
.kir-btn.active { background: rgba(90,140,255,0.75); border-color: rgba(90,140,255,0.5); }
.close-btn { position: absolute; right: 3px; top: 2px;
width: 18px; height: 18px; min-width: 18px; border-radius: 0;
border: 1px solid rgba(255,255,255,0.2); background: rgba(0,0,0,0.25);
color: #fff; font-size: 11px; line-height: 16px; padding: 0; }
#add-btn { height: 26px; min-width: 28px; font-size: 16px; }
#adblock-btn { height: 26px; min-width: 40px; font-size: 12px; font-weight: 700; }
#adblock-btn.active { background: rgba(90,140,255,0.75); border-color: rgba(90,140,255,0.5); }
#props-btn { height: 26px; min-width: 88px; font-size: 12px; }
#downloads-btn { height: 26px; min-width: 28px; font-size: 12px; font-weight: 700; }
#downloads-btn.active { background: rgba(90,140,255,0.75); border-color: rgba(90,140,255,0.5); }
#popup-row { display: none; align-items: center; justify-content: space-between; gap: 8px;
min-height: 34px; padding: 6px 8px; border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06); color: #e8e8ed; font-size: 12px; }
#popup-message { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#popup-actions { display: inline-flex; align-items: center; gap: 6px; }
#popup-yes, #popup-no { height: 24px; min-width: 44px; font-size: 12px; }
/* ---------- Popup permission prompt ---------- */
#popup-row {
display: none; align-items: center; gap: 10px;
flex: 0 0 40px; height: 40px; padding: 0 10px 0 12px;
border: 1px solid rgba(76,125,255,.35); border-radius: var(--radius-md);
background: rgba(76,125,255,.12); font-size: 12.5px; color: var(--text);
}
.popup-glyph { flex: 0 0 auto; width: 15px; height: 15px; color: var(--accent-text); }
#popup-message { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#popup-actions { flex: 0 0 auto; display: inline-flex; gap: 6px; }
.btn {
height: 26px; padding: 0 13px;
border: 1px solid var(--border-strong); border-radius: var(--radius-pill);
background: transparent; color: var(--text);
font: inherit; font-size: 12px; font-weight: 600;
cursor: pointer; outline: none;
transition: background-color var(--speed) var(--ease), color var(--speed) var(--ease),
border-color var(--speed) var(--ease), transform var(--speed) var(--ease);
}
.btn:hover { background: var(--surface-hover); }
.btn:active { transform: scale(.96); }
.btn:focus-visible { box-shadow: 0 0 0 3px var(--accent-soft); }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.primary:hover { background: var(--accent-strong); }
</style>
</head>
<body>
<div id="bar">
<div id="tabs-top">
<div id="tabs-row"></div>
<button id="adblock-btn" title="Toggle adblock">ADS</button>
<button id="downloads-btn" title="Downloads">D</button>
<button id="props-btn" title="History and downloads">Properties</button>
<div id="chrome">
<div id="tabs-row">
<div id="tabs" role="tablist" aria-label="Tabs"></div>
<button id="add-btn" class="icon-btn" title="New tab (Ctrl+T)" aria-label="New tab">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>
</button>
</div>
<div id="nav-row">
<button id="back" title="Back">←</button>
<button id="forward" title="Forward">→</button>
<button id="reload" title="Reload">⟳</button>
<button id="back" class="icon-btn" title="Back" aria-label="Back">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5"/><path d="m12 19-7-7 7-7"/></svg>
</button>
<button id="forward" class="icon-btn" title="Forward" aria-label="Forward">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
</button>
<button id="reload" class="icon-btn" title="Reload (F5 / Ctrl+R)" aria-label="Reload">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
</button>
<div id="url-pill">
<span id="site-icon" class="site-icon" data-state="search" title="Search or enter address">
<svg class="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<svg class="i-secure" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
<svg class="i-insecure" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
<svg class="i-internal" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
</span>
<input id="url-input" type="text" placeholder="Search or enter address"
autocomplete="off" autocapitalize="off" spellcheck="false">
</div>
<div id="downloads-row"></div>
<div id="popup-row">
<div id="actions" role="group" aria-label="Browser tools">
<button id="adblock-btn" class="icon-btn" title="Ad blocking: Off" aria-label="Toggle ad blocking" aria-pressed="false">
<svg class="icon-on" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="m9 12 2 2 4-4"/></svg>
<svg class="icon-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="m9.5 14.5 5-5"/><path d="m14.5 14.5-5-5"/></svg>
</button>
<button id="downloads-btn" class="icon-btn" title="Downloads" aria-label="Downloads" aria-pressed="false">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/></svg>
</button>
<button id="props-btn" class="icon-btn" title="Properties - history and downloads" aria-label="Properties">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
</button>
</div>
</div>
<div id="downloads-row" aria-label="Downloads"></div>
<div id="popup-row" role="alert">
<svg class="popup-glyph" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
<div id="popup-message"></div>
<div id="popup-actions">
<button id="popup-yes">Yes</button>
<button id="popup-no">No</button>
<button id="popup-no" class="btn">Not now</button>
<button id="popup-yes" class="btn primary">Allow</button>
</div>
</div>
</div>
@ -248,6 +449,9 @@ const normalize = (raw) => {
};
const input = document.getElementById('url-input');
const tabStrip = document.getElementById('tabs');
const siteIcon = document.getElementById('site-icon');
let currentUrl = '';
const isEditableTarget = (target) => {
if (!target || !(target instanceof Element)) return false;
@ -262,70 +466,164 @@ document.getElementById('reload').addEventListener('click', () => post('reload')
document.getElementById('props-btn').addEventListener('click', () => post('open_properties'));
document.getElementById('adblock-btn').addEventListener('click', () => post('toggle_adblock'));
document.getElementById('downloads-btn').addEventListener('click', () => post('toggle_downloads'));
document.getElementById('add-btn').addEventListener('click', () => post('new_tab'));
document.getElementById('popup-yes').addEventListener('click', () => post('popup_allow_yes'));
document.getElementById('popup-no').addEventListener('click', () => post('popup_allow_no'));
input.addEventListener('focus', () => input.select());
input.addEventListener('keydown', (ev) => {
if (ev.key === 'Enter') {
ev.preventDefault();
const next = normalize(input.value);
if (next) post('navigate:' + next);
if (next) {
post('navigate:' + next);
input.blur();
}
return;
}
if (ev.key === 'Escape') {
ev.preventDefault();
input.value = currentUrl;
input.blur();
}
});
const shortTitle = (tab, i) => {
if (tab.title && tab.title.trim()) return tab.title.trim().slice(0, 22);
try { return new URL(tab.url).hostname || 'Tab ' + (i + 1); }
catch (_) { return 'Tab ' + (i + 1); }
// Plain mouse wheel scrolls the tab strip horizontally.
tabStrip.addEventListener('wheel', (ev) => {
if (ev.deltaY === 0) return;
ev.preventDefault();
tabStrip.scrollLeft += ev.deltaY;
}, { passive: false });
const setSiteIcon = (url) => {
let state = 'search';
let hint = 'Search or enter address';
try {
const parsed = new URL(url);
if (parsed.protocol === 'https:') { state = 'secure'; hint = 'Connection is secure (HTTPS)'; }
else if (parsed.protocol === 'http:') { state = 'insecure'; hint = 'Not secure (HTTP)'; }
else if (parsed.protocol === 'data:' || parsed.protocol === 'file:' || parsed.protocol === 'about:') {
state = 'internal'; hint = 'RustyWolf internal page';
}
} catch (_) {}
siteIcon.dataset.state = state;
siteIcon.title = hint;
};
window.__rustywolfSetTabs = (state) => {
const row = document.getElementById('tabs-row');
row.innerHTML = '';
state.tabs.forEach((tab, i) => {
const btn = document.createElement('button');
btn.className =
'tab-btn' +
(tab.id === state.activeId ? ' active' : '') +
(tab.loaded ? '' : ' unloaded');
btn.title = tab.url;
btn.addEventListener('click', () => post('switch_tab:' + tab.id));
const hostOf = (url) => {
try { return new URL(url).hostname.replace(/^www\./, ''); } catch (_) { return ''; }
};
const hashHue = (text) => {
let hash = 0;
for (let i = 0; i < text.length; i += 1) hash = (hash * 31 + text.charCodeAt(i)) % 360;
return hash;
};
const ICON_PIN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></svg>';
const ICON_CLOSE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>';
const ICON_MOON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
const shortTitle = (tab, i) => {
if (tab.title && tab.title.trim()) return tab.title.trim();
const host = hostOf(tab.url);
return host || 'Tab ' + (i + 1);
};
const makeTab = (tab, i, activeId) => {
const isActive = tab.id === activeId;
const el = document.createElement('div');
el.className = 'tab' + (isActive ? ' active' : '') + (tab.loaded ? '' : ' sleeping');
el.title = tab.url;
el.setAttribute('role', 'tab');
el.setAttribute('aria-selected', isActive ? 'true' : 'false');
el.tabIndex = 0;
el.addEventListener('click', () => post('switch_tab:' + tab.id));
el.addEventListener('keydown', (ev) => {
if (ev.target !== el) return;
if (ev.key === 'Enter' || ev.key === ' ') {
ev.preventDefault();
post('switch_tab:' + tab.id);
}
});
el.addEventListener('auxclick', (ev) => {
if (ev.button === 1) {
ev.preventDefault();
post('close_tab:' + tab.id);
}
});
const host = hostOf(tab.url);
const favicon = document.createElement('span');
favicon.className = 'tab-favicon';
if (host) {
const hue = hashHue(host);
favicon.textContent = host.charAt(0).toUpperCase();
favicon.style.background = 'linear-gradient(135deg, hsl(' + hue + ', 62%, 50%), hsl(' + hue + ', 68%, 36%))';
favicon.title = host;
} else if ((tab.url || '').startsWith('data:')) {
favicon.textContent = 'R';
favicon.style.background = 'linear-gradient(135deg, hsl(218, 62%, 52%), hsl(218, 68%, 38%))';
favicon.title = 'RustyWolf internal page';
} else {
favicon.textContent = '?';
favicon.style.background = 'linear-gradient(135deg, hsl(220, 10%, 38%), hsl(220, 12%, 28%))';
favicon.title = 'New tab';
}
el.appendChild(favicon);
const title = document.createElement('span');
title.className = 'tab-title';
title.textContent = shortTitle(tab, i) + (tab.loaded ? '' : ' [S]');
btn.appendChild(title);
title.textContent = shortTitle(tab, i);
el.appendChild(title);
const kir = document.createElement('button');
kir.className = 'kir-btn' + (tab.kirEnabled ? ' active' : '');
kir.textContent = 'K';
kir.title = tab.kirEnabled ? 'KIR On' : 'KIR Off';
kir.addEventListener('click', (ev) => {
if (!tab.loaded) {
const sleep = document.createElement('span');
sleep.className = 'tab-sleep';
sleep.title = 'Suspended to save memory - click the tab to reload it';
sleep.innerHTML = ICON_MOON;
el.appendChild(sleep);
}
const pin = document.createElement('button');
pin.className = 'tab-pin' + (tab.kirEnabled ? ' on' : '');
pin.title = tab.kirEnabled
? 'Keeping in RAM (KIR): on - click to let it suspend when inactive'
: 'Keeping in RAM (KIR): off - click to keep this tab loaded in the background';
pin.setAttribute('aria-label', 'Toggle keeping in RAM');
pin.setAttribute('aria-pressed', tab.kirEnabled ? 'true' : 'false');
pin.innerHTML = ICON_PIN;
pin.addEventListener('click', (ev) => {
ev.stopPropagation();
ev.preventDefault();
post('toggle_kir:' + tab.id);
});
btn.appendChild(kir);
el.appendChild(pin);
const x = document.createElement('button');
x.className = 'close-btn';
x.textContent = '-'; x.title = 'Close tab';
x.addEventListener('click', (ev) => {
const close = document.createElement('button');
close.className = 'tab-close';
close.title = 'Close tab (Ctrl+W)';
close.setAttribute('aria-label', 'Close tab');
close.innerHTML = ICON_CLOSE;
close.addEventListener('click', (ev) => {
ev.stopPropagation();
ev.preventDefault();
post('close_tab:' + tab.id);
});
btn.appendChild(x);
row.appendChild(btn);
});
el.appendChild(close);
const add = document.createElement('button');
add.id = 'add-btn'; add.textContent = '+'; add.title = 'New tab';
add.addEventListener('click', () => post('new_tab'));
row.appendChild(add);
return el;
};
const active = state.tabs.find(t => t.id === state.activeId);
if (active) input.value = (active.url && active.url !== 'about:blank') ? active.url : '';
window.__rustywolfSetTabs = (state) => {
tabStrip.innerHTML = '';
state.tabs.forEach((tab, i) => tabStrip.appendChild(makeTab(tab, i, state.activeId)));
const active = state.tabs.find((t) => t.id === state.activeId);
currentUrl = (active && active.url && active.url !== 'about:blank') ? active.url : '';
setSiteIcon(currentUrl);
if (document.activeElement !== input) input.value = currentUrl;
};
const shortPath = (value) => {
@ -340,18 +638,23 @@ window.__rustywolfSetDownloads = (state) => {
const items = (state && Array.isArray(state.downloads)) ? state.downloads : [];
const panelOpen = !!(state && state.panelOpen);
row.innerHTML = '';
button.classList.toggle('active', panelOpen);
button.setAttribute('aria-pressed', panelOpen ? 'true' : 'false');
if (!panelOpen) {
row.style.display = 'none';
button.classList.remove('active');
return;
}
button.classList.add('active');
row.style.display = 'flex';
if (items.length === 0) {
const empty = document.createElement('div');
empty.className = 'dl-empty';
empty.textContent = 'No active downloads';
const glyph = document.createElement('span');
glyph.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/></svg>';
const label = document.createElement('span');
label.textContent = 'No active downloads';
empty.appendChild(glyph);
empty.appendChild(label);
row.appendChild(empty);
return;
}
@ -424,13 +727,11 @@ window.__rustywolfSetDownloads = (state) => {
window.__rustywolfSetAdblock = (enabled) => {
const button = document.getElementById('adblock-btn');
if (!button) return;
if (enabled) {
button.classList.add('active');
button.title = 'Adblock: On';
} else {
button.classList.remove('active');
button.title = 'Adblock: Off';
}
button.classList.toggle('active', !!enabled);
button.setAttribute('aria-pressed', enabled ? 'true' : 'false');
button.title = enabled
? 'Ad blocking: on - click to turn off'
: 'Ad blocking: off - click to turn on';
};
window.__rustywolfSetPopupPrompt = (state) => {
@ -2000,16 +2301,22 @@ fn render_properties_html(state: &BrowserState) -> String {
format!(
"<!doctype html><html><head><meta charset=\"utf-8\"><title>RustyWolf Properties</title>\
<style>body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#141418;color:#e9e9ee;margin:0;padding:20px}}\
h1{{font-size:24px;margin:0 0 14px}} h2{{font-size:17px;margin:20px 0 8px}}\
table{{width:100%;border-collapse:collapse;background:#1c1c22;border:1px solid rgba(255,255,255,.12);border-radius:0;overflow:hidden}}\
th,td{{padding:8px 10px;border-bottom:1px solid rgba(255,255,255,.08);font-size:13px;text-align:left;vertical-align:top}}\
th{{background:#23232b;color:#f4f4f8}} tr:last-child td{{border-bottom:none}}\
a{{color:#8ab4ff;text-decoration:none}} a:hover{{text-decoration:underline}} .muted{{opacity:.75;font-size:12px}}</style></head>\
<body><h1>Browser Properties</h1><p class=\"muted\">Session history and download activity.</p>\
<h2>Browsing History</h2><p class=\"muted\">{}</p><table><thead><tr><th>Visited</th><th>Title</th><th>URL</th></tr></thead><tbody>{history_rows}</tbody></table>\
<h2>Download History</h2><p class=\"muted\">{}</p><table><thead><tr><th>Updated</th><th>Status</th><th>Saved To</th><th>Source URL</th></tr></thead><tbody>{download_rows}</tbody></table>\
</body></html>",
<style>*{{box-sizing:border-box}}:root{{color-scheme:dark}}\
body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Inter,Roboto,sans-serif;background:#15161a;color:#eef0f4;margin:0;padding:28px 20px}}\
.wrap{{max-width:1080px;margin:0 auto}}\
h1{{font-size:22px;font-weight:700;margin:0 0 4px}} .sub{{color:#9aa1af;font-size:13px;margin:0 0 4px}}\
h2{{font-size:15px;font-weight:600;margin:26px 0 10px}}\
.muted{{color:#9aa1af;font-size:12px;margin:0 0 8px}}\
.card{{background:#1c1d22;border:1px solid rgba(255,255,255,.09);border-radius:12px;overflow:hidden}}\
table{{width:100%;border-collapse:collapse}}\
th,td{{padding:9px 12px;border-bottom:1px solid rgba(255,255,255,.07);font-size:13px;text-align:left;vertical-align:top;overflow-wrap:anywhere}}\
th{{background:rgba(255,255,255,.04);color:#c6cbd6;font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.05em}}\
tr:last-child td{{border-bottom:none}} tr:hover td{{background:rgba(255,255,255,.03)}}\
a{{color:#8ab4ff;text-decoration:none}} a:hover{{text-decoration:underline}}</style></head>\
<body><div class=\"wrap\"><h1>Browser Properties</h1><p class=\"sub\">Session browsing history and download activity.</p>\
<h2>Browsing History</h2><p class=\"muted\">{}</p><div class=\"card\"><table><thead><tr><th>Visited</th><th>Title</th><th>URL</th></tr></thead><tbody>{history_rows}</tbody></table></div>\
<h2>Download History</h2><p class=\"muted\">{}</p><div class=\"card\"><table><thead><tr><th>Updated</th><th>Status</th><th>Saved To</th><th>Source URL</th></tr></thead><tbody>{download_rows}</tbody></table></div>\
</div></body></html>",
if history_is_truncated {
format!("Showing latest {PROPERTIES_MAX_ROWS} entries.")
} else {