quantum-forms-dist/index.html
2026-09-24 02:21:39 +00:00

141 lines
8.6 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- interactive-widget=resizes-content: the BROWSER shrinks the layout viewport when ANY keyboard opens (incl. custom
keyboards like FUTO with a header). This is the platform mechanism — don't hand-roll visualViewport offset math,
which breaks on non-standard keyboards. With this, 100dvh + flexbox just works. -->
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
<title>Quantum Forms</title>
<style>
/* SimpleX-inspired: clean, minimal, dark. Sent bubbles right (accent), received left (surface). */
:root {
--bg:#111214; --header:#1b1d21; --surface:#26292e; --sent:#2b6cb0; --sent-2:#3182ce;
--fg:#e8eaed; --muted:#9aa0a6; --line:#33363b; --ok:#48bb78; --accent:#63b3ed;
}
* { box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
html,body { margin:0; padding:0; height:100%; overflow:hidden; }
/* With interactive-widget=resizes-content, the browser shrinks the viewport for the keyboard (FUTO incl.), so a
plain 100dvh flex column keeps the composer pinned to the bottom of the VISIBLE area — no JS offset math. */
#app { height:100dvh; background:var(--bg); color:var(--fg);
font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; display:flex; flex-direction:column; overflow:hidden; }
/* ── header: title + E2E banner + countdown ── (respects the Pixel notch/status bar) ── */
header { background:var(--header); border-bottom:1px solid var(--line);
padding:calc(10px + env(safe-area-inset-top)) 14px 10px; flex:0 0 auto; }
.h-row { display:flex; align-items:center; justify-content:space-between; gap:10px; }
.h-title { font-weight:600; font-size:1rem; }
.h-timer { font-size:.78rem; color:var(--muted); font-variant-numeric:tabular-nums; }
.e2e { margin-top:6px; font-size:.72rem; color:var(--ok); display:flex; align-items:center; gap:6px; }
.e2e .dot { width:7px; height:7px; border-radius:50%; background:var(--ok); box-shadow:0 0 6px var(--ok); }
/* ── join gate ── */
#gate { flex:1; display:flex; align-items:center; justify-content:center; padding:24px; }
#gate .box { width:100%; max-width:360px; text-align:center; }
#gate h2 { font-weight:600; margin:0 0 6px; }
#gate p { color:var(--muted); font-size:.85rem; margin:0 0 18px; }
#gate input { width:100%; padding:13px; border-radius:10px; border:1px solid var(--line); background:#0c0d0f;
color:var(--fg); font-size:1.1rem; text-align:center; letter-spacing:3px; text-transform:uppercase; }
#gate button { width:100%; margin-top:14px; padding:13px; border:0; border-radius:10px; background:var(--sent);
color:#fff; font-weight:600; font-size:1rem; cursor:pointer; }
/* ── chat ── */
#chat { flex:1; display:none; flex-direction:column; min-height:0; }
.msgs { flex:1; overflow-y:auto; -webkit-overflow-scrolling:touch; padding:12px 12px 4px;
display:flex; flex-direction:column; gap:6px; }
.bubble { max-width:80%; padding:8px 12px; border-radius:18px; font-size:.95rem; line-height:1.4; word-wrap:break-word; }
.bubble .meta { display:block; font-size:.64rem; color:rgba(255,255,255,.5); margin-top:3px; text-align:right; }
.recv { align-self:flex-start; background:var(--surface); border-bottom-left-radius:5px; }
.sent { align-self:flex-end; background:var(--sent); border-bottom-right-radius:5px; }
.sys { align-self:center; background:rgba(255,255,255,.06); color:var(--muted); font-size:.74rem; text-align:center;
max-width:90%; border-radius:12px; padding:5px 12px; }
.file-chip { display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:10px;
background:rgba(255,255,255,.12); font-size:.85rem; margin-top:2px; }
/* Signal-style file upload: a progress ring + name + cancel ✕ */
.filebub .fb-row { display:flex; align-items:center; gap:10px; }
.fb-ring { width:30px; height:30px; flex:0 0 auto; transform:rotate(-90deg); }
.fb-ring .fb-bg { fill:none; stroke:rgba(255,255,255,.25); stroke-width:3; }
.fb-ring .fb-fg { fill:none; stroke:#fff; stroke-width:3; stroke-linecap:round;
transition:stroke-dashoffset .2s linear; }
.fb-name { flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:.9rem; }
.fb-cancel { flex:0 0 auto; width:26px; height:26px; border-radius:50%; border:0; background:rgba(0,0,0,.25);
color:#fff; font-size:.85rem; cursor:pointer; }
/* ── staged attachment preview (SimpleX: file shows above the input; send transmits it) ── */
.preview { flex:0 0 auto; display:flex; align-items:center; gap:10px; padding:8px 12px; background:var(--header);
border-top:1px solid var(--line); }
.preview .pv-icon { font-size:1.1rem; }
.preview .pv-name { flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:.9rem; }
.preview .pv-size { color:var(--muted); font-size:.75rem; flex:0 0 auto; }
.preview .pv-x { flex:0 0 auto; width:28px; height:28px; border-radius:50%; border:0; background:var(--surface);
color:var(--fg); font-size:.9rem; cursor:pointer; }
/* ── composer: a real phone footer. safe-area padding clears the Pixel gesture bar; JS keeps it above the keyboard ── */
.composer { flex:0 0 auto; border-top:1px solid var(--line); background:var(--header);
padding:8px 8px calc(8px + env(safe-area-inset-bottom));
display:flex; align-items:flex-end; gap:8px; }
.composer .attach { width:42px; height:42px; border-radius:50%; border:1px solid var(--line); background:var(--surface);
color:var(--fg); font-size:1.3rem; cursor:pointer; flex:0 0 auto; line-height:1; }
.composer textarea { flex:1 1 auto; min-width:0; resize:none; max-height:120px; min-height:42px; padding:10px 14px;
border-radius:21px; border:1px solid var(--line); background:#0c0d0f; color:var(--fg);
font-size:1rem; font-family:inherit; line-height:1.3; }
.composer .send { width:42px; height:42px; border-radius:50%; border:0; background:var(--sent); color:#fff;
font-size:1.2rem; cursor:pointer; flex:0 0 auto; line-height:1; }
.composer .send:disabled { opacity:.4; }
#fileInput { display:none; }
.toast { position:fixed; bottom:90px; left:50%; transform:translateX(-50%); background:#000c; color:#fff;
padding:9px 16px; border-radius:20px; font-size:.85rem; display:none; z-index:10; }
</style>
</head>
<body>
<div id="app">
<header>
<div class="h-row">
<span class="h-title" id="title">Quantum Forms</span>
<span class="h-timer" id="timer"></span>
</div>
<div class="e2e"><span class="dot"></span> Quantum-resistant · end-to-end encrypted</div>
</header>
<!-- join gate -->
<div id="gate">
<div class="box">
<h2>Join session</h2>
<p>Enter the session code you were given.</p>
<input id="code" maxlength="12" placeholder="CODE" autocomplete="off" autocapitalize="characters" inputmode="text">
<button id="joinBtn">Join</button>
</div>
</div>
<!-- chat -->
<div id="chat">
<div class="msgs" id="msgs"></div>
<!-- SimpleX-style: a selected file STAGES here as a preview chip above the input; send transmits it -->
<div class="preview" id="preview" style="display:none">
<span class="pv-icon">📎</span>
<span class="pv-name" id="pvName"></span>
<span class="pv-size" id="pvSize"></span>
<button class="pv-x" id="pvCancel" title="Remove">✕</button>
</div>
<div class="composer">
<button class="attach" id="attachBtn" title="Attach a file">+</button>
<input type="file" id="fileInput">
<textarea id="input" rows="1" placeholder="Message…" enterkeyhint="send"></textarea>
<button class="send" id="sendBtn" title="Send">➤</button>
</div>
</div>
<div class="toast" id="toast"></div>
</div>
<script>
// The browser handles the keyboard (interactive-widget=resizes-content). We only keep the message list scrolled
// to the bottom when the layout changes (keyboard open/close) — no viewport offset math to fight the browser.
(function () {
function toBottom() { var m = document.getElementById('msgs'); if (m) m.scrollTop = m.scrollHeight; }
window.addEventListener('resize', function () { setTimeout(toBottom, 50); });
document.addEventListener('focusin', function (e) { if (e.target && e.target.id === 'input') setTimeout(toBottom, 300); });
})();
</script>
<script type="module" src="./chat.js?v=12"></script>
</body>
</html>