141 lines
8.6 KiB
HTML
141 lines
8.6 KiB
HTML
<!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>
|