uxui:CHANGE icons, peers indicator and responsive design - I fucking hate frontend with a passion

This commit is contained in:
Caffeine Fueled 2026-09-03 20:24:47 +02:00
parent aae1f5f924
commit dba602e698
Signed by: cf7
GPG key ID: CA295D643074C68C

132
app.py
View file

@ -188,6 +188,7 @@ def record_auth_failure(client_ip: str):
HTML = """<!doctype html> HTML = """<!doctype html>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>aukpad</title> <title>aukpad</title>
<style> <style>
:root { :root {
@ -232,8 +233,11 @@ HTML = """<!doctype html>
* { box-sizing: border-box; } * { box-sizing: border-box; }
html, body { height: 100%; margin: 0; padding: 0; background-color: var(--bg); color: var(--text); } html, body { height: 100%; margin: 0; padding: 0; background-color: var(--bg); color: var(--text); }
body { font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji","Segoe UI Emoji"; body { font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji","Segoe UI Emoji";
max-width: 1000px; margin: 0 auto; padding: 1rem; display: flex; flex-direction: column; height: 100vh; box-sizing: border-box; } max-width: 1000px; margin: 0 auto; padding: 1rem; display: flex; flex-direction: column;
header { display:flex; justify-content:space-between; align-items:center; margin-bottom: .5rem; flex-shrink: 0; } height: 100vh; height: 100dvh; /* dvh excludes iOS Safari's dynamic toolbar; 100vh is the fallback */
box-sizing: border-box; }
header { display:flex; justify-content:space-between; align-items:center; gap:.5rem; margin-bottom: .5rem; flex-shrink: 0; }
#padname { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
a,button { padding:.35rem 0; text-decoration:none; border:1px solid var(--border); border-radius:4px; background:var(--btn-bg); color:var(--text); cursor:pointer; font-size:.9rem; font-weight:bold; min-width:2rem; text-align:center; display:inline-block; } a,button { padding:.35rem 0; text-decoration:none; border:1px solid var(--border); border-radius:4px; background:var(--btn-bg); color:var(--text); cursor:pointer; font-size:.9rem; font-weight:bold; min-width:2rem; text-align:center; display:inline-block; }
a:hover,button:hover { background:var(--btn-hover); } a:hover,button:hover { background:var(--btn-hover); }
/* Icon buttons: inline SVG so glyphs render the same on every platform */ /* Icon buttons: inline SVG so glyphs render the same on every platform */
@ -241,13 +245,25 @@ HTML = """<!doctype html>
#lang { height:2rem; max-width:7.5rem; padding:0 .35rem; border:1px solid var(--border); border-radius:4px; #lang { height:2rem; max-width:7.5rem; padding:0 .35rem; border:1px solid var(--border); border-radius:4px;
background:var(--btn-bg); color:var(--text); font-size:.8rem; font-family:inherit; cursor:pointer; } background:var(--btn-bg); color:var(--text); font-size:.8rem; font-family:inherit; cursor:pointer; }
#lang:disabled { opacity:.5; cursor:default; } #lang:disabled { opacity:.5; cursor:default; }
/* Three groups: pad content | new pad | app UI. .25rem flex gap + .75rem = 1rem,
i.e. half of the 2rem icon box. */
#newpad, #theme-btn { margin-left:.75rem; }
.ic { width:1.05rem; height:1.05rem; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; pointer-events:none; } .ic { width:1.05rem; height:1.05rem; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; pointer-events:none; }
#status { font-size:.9rem; opacity:.7; margin-left:.5rem; } /* Flex row so pad name, status and peer badge share one centre line. Baseline
alignment cannot do this: #peers is an inline-flex box, whose baseline comes
from its own first item rather than from the text beside it. */
#meta { display:flex; align-items:center; gap:.5rem; min-width:0; }
#status { font-size:.9rem; opacity:.7; white-space:nowrap; }
#status::before { content:""; display:inline-block; width:.55em; height:.55em; border-radius:50%; margin-right:.35rem; background:#ef4444; } #status::before { content:""; display:inline-block; width:.55em; height:.55em; border-radius:50%; margin-right:.35rem; background:#ef4444; }
#status.connected::before { background:#22c55e; } #status.connected::before { background:#22c55e; }
#peers { font-size:.95rem; font-weight:bold; margin-left:.5rem; margin-right:.3rem; color:#22c55e; display:none; } /* Reads as a button to match the header, but it is a status badge: no hover, no pointer. */
#wrap { display:grid; grid-template-columns: max-content 1fr; border:1px solid var(--border); border-radius:4px; overflow:hidden; /* Bare status readout, no chrome. #meta's align-items:center keeps it on the
flex: 1; } header centre line, so no vertical-align is needed. */
#peers { display:none; align-items:center; gap:.3rem; flex-shrink:0;
color:#22c55e; font-size:.9rem; font-weight:700; line-height:1.4; }
#peers .ic-sm { width:1.05rem; height:1.05rem; } /* scoped: the notice icon stays .9rem */
#wrap { display:grid; grid-template-columns: max-content 1fr; grid-template-rows: 1fr auto;
border:1px solid var(--border); border-radius:4px; overflow:hidden; flex: 1; }
#gutter, #t, #hl { font: 14px/var(--line-h) ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } #gutter, #t, #hl { font: 14px/var(--line-h) ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
#gutter { padding:.5rem .75rem; text-align:right; color:var(--gutter-color); background:var(--gutter-bg); border-right:1px solid var(--gutter-border); #gutter { padding:.5rem .75rem; text-align:right; color:var(--gutter-color); background:var(--gutter-bg); border-right:1px solid var(--gutter-border);
user-select:none; min-width: 3ch; white-space: pre; height: 100%; overflow: hidden; } user-select:none; min-width: 3ch; white-space: pre; height: 100%; overflow: hidden; }
@ -265,17 +281,19 @@ HTML = """<!doctype html>
#wrap.hl #t::placeholder { color:var(--gutter-color); } #wrap.hl #t::placeholder { color:var(--gutter-color); }
#wrap.hl #t::selection { background:rgba(59,130,246,.35); } #wrap.hl #t::selection { background:rgba(59,130,246,.35); }
pre { margin: 0; } pre { margin: 0; }
/* Dismissed with the checkbox hack so it needs no JS. Trade-off: dismissal lasts /* Footer bar of the editor rather than loose text under it: it sits inside #wrap,
for the page view only, since there is no script to persist it. */ so it inherits the box's border and rounded corners. */
#notice-dismiss { position:absolute; width:0; height:0; opacity:0; } #notice { grid-column: 1 / -1; position:relative; display:flex; gap:.5rem;
#notice { flex-shrink:0; margin:.5rem 0 0; display:flex; gap:.75rem; align-items:flex-start; align-items:center; justify-content:center; text-align:center;
font-size:.75rem; line-height:1.45; color:var(--gutter-color); } margin:0; padding:.4rem 2rem; border-top:1px solid var(--border);
#notice strong { color:var(--hl-attr); font-weight:600; } background:var(--gutter-bg); font-size:.75rem; line-height:1.5;
#notice label { margin-left:auto; flex-shrink:0; cursor:pointer; padding:0 .2rem; color:var(--text); cursor:pointer; }
font-size:1rem; line-height:1.1; user-select:none; } .ic-sm { width:.9rem; height:.9rem; flex-shrink:0; }
#notice label:hover { color:var(--text); } /* Reset the global a,button rule: this is a bare glyph, not a control.
#notice-dismiss:focus-visible + #notice label { outline:2px solid var(--hl-attr); border-radius:3px; } Absolute so it stays at the right edge without offsetting the centred text. */
#notice-dismiss:checked + #notice { display:none; } #notice button { all:unset; position:absolute; right:.5rem; top:50%; transform:translateY(-50%);
cursor:pointer; padding:0 .2rem; font-size:1rem; line-height:1.1; }
#notice button:focus-visible { outline:2px solid currentColor; border-radius:3px; }
/* highlight.js tokens, mapped onto the theme variables above so dark/light just works */ /* highlight.js tokens, mapped onto the theme variables above so dark/light just works */
.hljs-comment, .hljs-quote { color:var(--hl-comment); font-style:italic; } .hljs-comment, .hljs-quote { color:var(--hl-comment); font-style:italic; }
.hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-type, .hljs-doctag { color:var(--hl-keyword); } .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-type, .hljs-doctag { color:var(--hl-keyword); }
@ -290,7 +308,8 @@ HTML = """<!doctype html>
#lock-btn.locked { background:#dbeafe; border-color:#3b82f6; } #lock-btn.locked { background:#dbeafe; border-color:#3b82f6; }
[data-theme="dark"] #lock-btn.locked { background:#1e3a5f; border-color:#3b82f6; } [data-theme="dark"] #lock-btn.locked { background:#1e3a5f; border-color:#3b82f6; }
#pw-panel { display:none; position:absolute; top:2.5rem; right:0; background:var(--panel-bg); border:1px solid var(--border); #pw-panel { display:none; position:absolute; top:2.5rem; right:0; background:var(--panel-bg); border:1px solid var(--border);
border-radius:6px; padding:.75rem; box-shadow:0 4px 12px rgba(0,0,0,.1); z-index:10; min-width:230px; } border-radius:6px; padding:.75rem; box-shadow:0 4px 12px rgba(0,0,0,.1); z-index:10;
min-width:230px; max-width:calc(100vw - 2rem); }
#pw-panel.open { display:block; } #pw-panel.open { display:block; }
#pw-panel label { font-size:.8rem; font-weight:bold; display:block; margin-bottom:.4rem; } #pw-panel label { font-size:.8rem; font-weight:bold; display:block; margin-bottom:.4rem; }
#pw-panel input { width:100%; padding:.35rem .5rem; border:1px solid var(--border); border-radius:4px; #pw-panel input { width:100%; padding:.35rem .5rem; border:1px solid var(--border); border-radius:4px;
@ -301,13 +320,41 @@ HTML = """<!doctype html>
#pw-overlay { display:none; position:fixed; inset:0; background:var(--overlay-bg); z-index:100; #pw-overlay { display:none; position:fixed; inset:0; background:var(--overlay-bg); z-index:100;
align-items:center; justify-content:center; } align-items:center; justify-content:center; }
#pw-overlay.open { display:flex; } #pw-overlay.open { display:flex; }
#pw-box { background:var(--panel-bg); border-radius:8px; padding:1.5rem; width:300px; } #pw-box { background:var(--panel-bg); border-radius:8px; padding:1.5rem; width:300px; max-width:calc(100vw - 2rem); }
#pw-box h2 { margin:0 0 .75rem; font-size:1rem; } #pw-box h2 { margin:0 0 .75rem; font-size:1rem; }
#auth-input { width:100%; padding:.45rem .6rem; border:1px solid var(--border); border-radius:4px; #auth-input { width:100%; padding:.45rem .6rem; border:1px solid var(--border); border-radius:4px;
font-size:1rem; margin-bottom:.5rem; font-family:inherit; display:block; background:var(--btn-bg); color:var(--text); } font-size:1rem; margin-bottom:.5rem; font-family:inherit; display:block; background:var(--btn-bg); color:var(--text); }
#auth-error { color:#ef4444; font-size:.85rem; margin-bottom:.5rem; display:none; } #auth-error { color:#ef4444; font-size:.85rem; margin-bottom:.5rem; display:none; }
#auth-submit { width:100%; padding:.45rem; background:#000; color:#fff; border:none; #auth-submit { width:100%; padding:.45rem; background:#000; color:#fff; border:none;
border-radius:4px; font-size:.95rem; cursor:pointer; } border-radius:4px; font-size:.95rem; cursor:pointer; }
/* --- Narrow screens: keep the header on one row by shedding text, not controls. --- */
@media (max-width: 640px) {
body { padding:.5rem; }
/* Hide the word but keep the dot. It is a ::before on #status itself, sized in em,
so it must be re-declared in rem to survive font-size:0. Doing it this way avoids
wrapping the label in a child element, which would break the four places where
JS assigns to $("#status").textContent. */
#status { font-size:0; }
#status::before { width:.55rem; height:.55rem; margin-right:0; }
#lang { max-width:5.5rem; }
#newpad, #theme-btn { margin-left:.4rem; }
#notice { padding:.4rem 1.75rem; }
}
/* Below ~450px the controls (5 buttons + dropdown = 281px) leave the pad name too
little room to be legible - it degrades to "/..". Hide it rather than show a stub:
it is not interactive and the phone's URL bar already displays it. */
@media (max-width: 450px) {
#padname { display:none; }
}
/* --- Touch devices: 16px fields, because iOS Safari force-zooms into anything
smaller on focus and never zooms back out. Button sizes are unchanged. --- */
@media (pointer: coarse) {
#gutter, #t, #hl { font-size:16px; } /* all three, or the overlay desyncs */
#pw-panel input { font-size:1rem; }
}
</style> </style>
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false"><defs> <svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false"><defs>
<symbol id="i-copy" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></symbol> <symbol id="i-copy" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></symbol>
@ -316,17 +363,21 @@ HTML = """<!doctype html>
<symbol id="i-lock" viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></symbol> <symbol id="i-lock" viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></symbol>
<symbol id="i-moon" viewBox="0 0 24 24"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></symbol> <symbol id="i-moon" viewBox="0 0 24 24"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></symbol>
<symbol id="i-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M6.3 17.7l-1.4 1.4M19.1 4.9l-1.4 1.4"/></symbol> <symbol id="i-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M6.3 17.7l-1.4 1.4M19.1 4.9l-1.4 1.4"/></symbol>
<symbol id="i-user" viewBox="0 0 24 24"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol>
<symbol id="i-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></symbol> <symbol id="i-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></symbol>
</defs></svg> </defs></svg>
<header> <header>
<div> <div id="meta">
<strong id="padname"></strong><span id="peers"></span><span id="status">disconnected</span> <strong id="padname"></strong><span id="status">disconnected</span><span id="peers"><svg class="ic ic-sm" aria-hidden="true"><use href="#i-user"/></svg><span id="peer-count"></span></span>
</div> </div>
<div style="position:relative; display:flex; align-items:center; gap:.25rem;"> <div style="position:relative; display:flex; align-items:center; gap:.25rem;">
<!-- Acts on this pad's content -->
<select id="lang" title="Syntax highlighting for this pad" aria-label="Syntax highlighting" disabled></select> <select id="lang" title="Syntax highlighting for this pad" aria-label="Syntax highlighting" disabled></select>
<button id="copy" class="ibtn" onclick="copyToClipboard()" title="Copy to clipboard" aria-label="Copy to clipboard"><svg class="ic"><use href="#i-copy"/></svg></button> <button id="copy" class="ibtn" onclick="copyToClipboard()" title="Copy to clipboard" aria-label="Copy to clipboard"><svg class="ic"><use href="#i-copy"/></svg></button>
<a id="newpad" class="ibtn" href="/" target="_blank" title="Opens a new pad in a new tab" aria-label="New pad"><svg class="ic"><use href="#i-plus"/></svg></a>
<button id="lock-btn" class="ibtn" onclick="togglePwPanel()" title="No password click to set one"><svg class="ic"><use href="#i-lock"/></svg></button> <button id="lock-btn" class="ibtn" onclick="togglePwPanel()" title="No password click to set one"><svg class="ic"><use href="#i-lock"/></svg></button>
<!-- Leaves this pad -->
<a id="newpad" class="ibtn" href="/" target="_blank" title="Opens a new pad in a new tab" aria-label="New pad"><svg class="ic"><use href="#i-plus"/></svg></a>
<!-- App-level UI -->
<button id="theme-btn" class="ibtn" onclick="toggleTheme()" title="Toggle dark/light mode" aria-label="Toggle dark/light mode"><svg class="ic"><use href="#i-moon"/></svg></button> <button id="theme-btn" class="ibtn" onclick="toggleTheme()" title="Toggle dark/light mode" aria-label="Toggle dark/light mode"><svg class="ic"><use href="#i-moon"/></svg></button>
<a id="info" class="ibtn" href="/system/info" title="System info" aria-label="System info"><svg class="ic"><use href="#i-info"/></svg></a> <a id="info" class="ibtn" href="/system/info" title="System info" aria-label="System info"><svg class="ic"><use href="#i-info"/></svg></a>
<div id="pw-panel"> <div id="pw-panel">
@ -348,14 +399,13 @@ HTML = """<!doctype html>
<textarea id="t" spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="off" <textarea id="t" spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="off"
placeholder="Start typing…"></textarea> placeholder="Start typing…"></textarea>
</div> </div>
</div> <p id="notice" onclick="hideNotice()" title="Click to dismiss">
<input type="checkbox" id="notice-dismiss"> <svg class="ic ic-sm" aria-hidden="true"><use href="#i-info"/></svg>
<p id="notice"> <span>Content is deleted after __RETENTION_HOURS__ hours of inactivity and can be accessed
<span><strong>Please Note:</strong> no client-side encryption the server and anyone with the by the server and anyone with the link and optional password.</span>
link and password can access the content. Content is deleted after __RETENTION_HOURS__ hours <button type="button" aria-label="Dismiss notice">&times;</button>
of inactivity.</span>
<label for="notice-dismiss" title="Dismiss" aria-label="Dismiss notice">&times;</label>
</p> </p>
</div>
<div id="pw-overlay"> <div id="pw-overlay">
<div id="pw-box"> <div id="pw-box">
@ -400,6 +450,16 @@ function toggleTheme() {
}); });
})(); })();
// Bottom notice: click anywhere on it to dismiss, remembered across pads and reloads.
function hideNotice() {
$("#notice").style.display = "none";
try { localStorage.setItem("notice", "off"); } catch (e) {}
}
(function() {
try { if (localStorage.getItem("notice") === "off") $("#notice").style.display = "none"; }
catch (e) {} // private mode / storage disabled: just leave the notice up
})();
// Derive docId from path; redirect root to random // Derive docId from path; redirect root to random
let docId = decodeURIComponent(location.pathname.replace(/(^\\/|\\/$)/g, "")); let docId = decodeURIComponent(location.pathname.replace(/(^\\/|\\/$)/g, ""));
if (!docId) { location.replace("/" + rand() + "/"); } if (!docId) { location.replace("/" + rand() + "/"); }
@ -587,6 +647,13 @@ function submitAuth() {
} }
} }
function setPeers(n) {
const el = $("#peers");
$("#peer-count").textContent = n; // never el.textContent: it would wipe the <svg>
el.title = n + (n === 1 ? " peer" : " peers") + " connected";
el.style.display = "inline-flex";
}
// --- WS connect + sync --- // --- WS connect + sync ---
function connect(){ function connect(){
isAuthed = false; isAuthed = false;
@ -603,7 +670,7 @@ function connect(){
if (msg.type === "init") { if (msg.type === "init") {
isProtected = !!msg.protected; isProtected = !!msg.protected;
updateLockBtn(); updateLockBtn();
if (msg.peers !== undefined) { const el = $("#peers"); el.textContent = msg.peers; el.style.display = "inline"; } if (msg.peers !== undefined) setPeers(msg.peers);
if (isProtected && !isAuthed) { if (isProtected && !isAuthed) {
if (urlPw) { if (urlPw) {
ws.send(JSON.stringify({type: "auth", password: urlPw})); ws.send(JSON.stringify({type: "auth", password: urlPw}));
@ -648,9 +715,7 @@ function connect(){
ta.selectionStart = adjustCursor(oldText, msg.text, s); ta.selectionStart = adjustCursor(oldText, msg.text, s);
ta.selectionEnd = adjustCursor(oldText, msg.text, e); ta.selectionEnd = adjustCursor(oldText, msg.text, e);
} else if (msg.type === "peers_changed") { } else if (msg.type === "peers_changed") {
const el = $("#peers"); setPeers(msg.count);
el.textContent = msg.count;
el.style.display = "inline";
} else if (msg.type === "lang_changed") { } else if (msg.type === "lang_changed") {
applyLang(msg.lang || ""); applyLang(msg.lang || "");
} else if (msg.type === "protected_changed") { } else if (msg.type === "protected_changed") {
@ -803,6 +868,7 @@ def get_system_info():
<html> <html>
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>aukpad - System Info</title> <title>aukpad - System Info</title>
<style> <style>
:root {{ :root {{