gsmol/web/index.html

276 lines
14 KiB
HTML
Raw Normal View History

2026-09-26 15:59:28 +03:00
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>smol mail</title>
<link rel="icon" href="favicon.svg" type="image/svg+xml">
2026-09-26 15:59:28 +03:00
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- One sprite, referenced by <use>; same-document so the CSP needs no exception. -->
<svg class="sprite" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
<symbol id="i-compose" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></symbol>
<symbol id="i-inbox" viewBox="0 0 24 24"><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></symbol>
<symbol id="i-sent" viewBox="0 0 24 24"><path d="M22 2 11 13"/><path d="M22 2l-7 20-4-9-9-4 20-7z"/></symbol>
<symbol id="i-contacts" viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol>
<symbol id="i-refresh" viewBox="0 0 24 24"><path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M3 22v-6h6"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/></symbol>
<symbol id="i-settings" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></symbol>
<symbol id="i-auto" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 1 0 18z" fill="currentColor" stroke="none"/></symbol>
<symbol id="i-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></symbol>
<symbol id="i-moon" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol>
<symbol id="i-reply" viewBox="0 0 24 24"><path d="M9 17 4 12l5-5"/><path d="M4 12h11a5 5 0 0 1 5 5v2"/></symbol>
<symbol id="i-back" viewBox="0 0 24 24"><path d="M15 18l-6-6 6-6"/></symbol>
<symbol id="i-close" viewBox="0 0 24 24"><path d="M18 6 6 18M6 6l12 12"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></symbol>
</svg>
<header class="topbar">
<div class="brand">smol mail</div>
<input id="search" class="search" placeholder="search mail" aria-label="search mail"
spellcheck="false" hidden>
<div id="top-actions" class="top-actions" hidden>
<button id="theme" class="icon-only" aria-label="theme"><svg class="icon"><use href="#i-auto"/></svg></button>
<button id="settings-open" class="icon-only" aria-label="settings" title="settings"><svg class="icon"><use href="#i-settings"/></svg></button>
<div id="account" class="account"></div>
<div id="avatar" class="avatar" aria-hidden="true"></div>
</div>
2026-09-26 15:59:28 +03:00
</header>
<!-- Five screens, one concern each: generate-or-recover an identity, establish
trust in a server, then bind a name to it. No screen asks about the next
screen's concern — e.g. the server screen never asks for a username. -->
2026-09-26 15:59:28 +03:00
<main id="setup" class="setup" hidden>
<section id="setup-intro" class="setup-card">
<h2>welcome</h2>
<p class="lead">Encrypted mail with no account and no password. Your identity is one secret,
kept in this browser and nowhere else.</p>
<button type="button" id="create-identity" class="filled large">create a new identity</button>
<button type="button" id="show-restore" class="link">restore existing identity from seed</button>
</section>
<section id="setup-seed" class="setup-card" hidden>
<h2>write this seed down</h2>
<p class="lead">It is the only secret, and it is shown once.</p>
<div class="seed-card">
<div id="setup-seed-value" class="seed"></div>
</div>
<button type="button" id="seed-saved" class="filled large">I saved it</button>
<button type="button" id="seed-back" class="link">back</button>
</section>
<section id="setup-restore" class="setup-card" hidden>
<h2>restore from a seed</h2>
<p class="lead">Paste the secret you saved. Your keys come back from it — it is all this client
needs.</p>
<form id="restore-form" class="stack">
<label>your secret
<input id="restore-master" placeholder="hex or base32" spellcheck="false" required>
</label>
<span id="restore-error" class="error"></span>
<button class="filled large">restore</button>
</form>
<button type="button" id="restore-back" class="link">back</button>
</section>
<section id="setup-server" class="setup-card" hidden>
<h2>set up the server</h2>
<form id="server-form" class="stack">
<label>host
<input id="server-host" placeholder="example.org" spellcheck="false" required>
</label>
<div id="server-key-area" class="seed-card" hidden></div>
<span id="server-error" class="error"></span>
<button id="server-action" class="filled large">fetch the server's key</button>
</form>
<button type="button" id="server-back" class="link">start over</button>
</section>
<section id="setup-account" class="setup-card" hidden>
<h2>choose your address</h2>
<div id="account-mode" class="mode-toggle" role="group">
<button type="button" data-mode="new" class="active" aria-current="true">create a new address</button>
<button type="button" data-mode="existing">I already have this address</button>
</div>
<form id="account-form" class="stack">
<label>user
<input id="account-user" placeholder="alice" spellcheck="false" required>
</label>
<label id="account-token-label"><span>invite token <span class="hint">(optional)</span></span>
<input id="account-token" placeholder="only if your server asks">
</label>
<span id="account-error" class="error"></span>
<button id="account-action" class="filled large">register</button>
</form>
<button type="button" id="account-back" class="link">back</button>
</section>
2026-09-26 15:59:28 +03:00
</main>
<main id="app" hidden>
<nav class="rail" aria-label="folders">
<button id="write" class="compose"><svg class="icon"><use href="#i-compose"/></svg>compose</button>
<div id="folders">
<button data-folder="inbox" aria-label="inbox" class="active" aria-current="true">
<svg class="icon"><use href="#i-inbox"/></svg><span class="label">inbox</span>
<span id="count-inbox" class="count"></span>
</button>
<button data-folder="requests" aria-label="requests">
<svg class="icon"><use href="#i-inbox"/></svg><span class="label">requests</span>
<span id="count-requests" class="count"></span>
</button>
<button data-folder="sent" aria-label="sent"><svg class="icon"><use href="#i-sent"/></svg><span class="label">sent</span></button>
<button data-folder="contacts" aria-label="contacts"><svg class="icon"><use href="#i-contacts"/></svg><span class="label">contacts</span></button>
</div>
2026-09-26 15:59:28 +03:00
</nav>
<div class="listpane">
<div id="list-toolbar" class="toolbar">
<div id="mail-tools" class="tools">
<button id="fetch" class="primary"><svg class="icon"><use href="#i-refresh"/></svg><span class="label">fetch</span></button>
<span id="list-status" class="list-status"></span>
</div>
<form id="import-form" class="tools" hidden>
<input name="uri" placeholder="smol://alice@example.org/…" aria-label="smol:// address to import"
spellcheck="false" required>
<button class="primary">import</button>
</form>
</div>
<section id="list" class="list" aria-label="messages"></section>
</div>
<article id="view" class="view" aria-label="message"><p class="placeholder">select a message</p></article>
2026-09-26 15:59:28 +03:00
</main>
<dialog id="compose">
<form id="compose-form" method="dialog">
<input name="to" required spellcheck="false" placeholder="to: alice@example.org or smol://…">
<input name="subject" spellcheck="false" placeholder="subject">
<textarea name="body" rows="12" placeholder="message"></textarea>
<div id="compose-unpinned" class="seed-card" hidden>
<h4>unpinned server</h4>
<p class="hint">
<strong id="compose-unpinned-host"></strong> is not pinned, and "require a pinned
server" refuses to trust a key fetched from it for a new recipient. Fetch its key,
compare the fingerprint against what the operator published through a trusted
channel, then pin it to send.
</p>
<div id="compose-unpinned-key-area" hidden></div>
<div class="actions">
<button type="button" id="compose-unpinned-fetch">Fetch their server's key</button>
<button type="button" id="compose-unpinned-pin" class="filled" hidden>Pin and send</button>
</div>
<span id="compose-unpinned-error" class="error"></span>
</div>
2026-09-26 15:59:28 +03:00
<footer>
<label class="anon"><input type="checkbox" name="anonymous"> anonymous — omit my address</label>
2026-09-26 15:59:28 +03:00
<span id="compose-error" class="error"></span>
<button type="button" id="compose-cancel">cancel</button>
<button id="send" class="filled" value="send">send</button>
2026-09-26 15:59:28 +03:00
</footer>
</form>
</dialog>
<!-- Tabs, not one scroll: identity, server, the backup file and the
irreversible operations have nothing to say to each other, and reading
all four at once was the overwhelming part. -->
2026-09-26 15:59:28 +03:00
<dialog id="settings">
<header class="dialog-head">
<h2>settings</h2>
<button type="button" id="settings-close" class="icon-only" aria-label="close settings"
title="close"><svg class="icon"><use href="#i-close"/></svg></button>
</header>
<div class="settings-body">
<nav id="settings-tabs" class="settings-tabs" aria-label="settings sections">
<button type="button" data-tab="identity" class="active" aria-current="true">identity</button>
<button type="button" data-tab="server">server</button>
<button type="button" data-tab="backup">backup</button>
<button type="button" data-tab="advanced">advanced</button>
</nav>
<div id="settings-panels" class="settings-panels">
<section class="panel" data-panel="identity">
<div id="identity-fields"></div>
<h3>recovery secret</h3>
<div id="identity-secret"></div>
</section>
<section class="panel" data-panel="server" hidden>
<h3>connection</h3>
<div id="server-connection"></div>
<h3>fetching</h3>
<label class="check-row">
<input type="checkbox" id="leave-on-server">
<span>
<span class="check-title">Leave mail on server</span>
<span class="hint">Keep a copy on the server after fetching, so another client can fetch
it too. When this is enabled, deleting a message removes it from this client and the
server.</span>
</span>
</label>
</section>
<section class="panel" data-panel="backup" hidden>
<p class="hint">One file holding your mail, contacts and server info for moving to another
client, or for keeping what the server no longer has. It is sealed to this identity and
deliberately leaves the secret itself out.</p>
<div class="action-row">
<button type="button" id="export-data" class="filled">Export</button>
<span class="hint">Write a backup file</span>
</div>
<div class="action-row">
<button type="button" id="import-data-trigger" class="outline">Import</button>
<span class="hint">Restore from a backup file</span>
<input id="import-data" type="file" accept="application/json" hidden>
</div>
<span id="backup-error" class="error"></span>
</section>
<section class="panel" data-panel="advanced" hidden>
<h3>trust</h3>
<label class="check-row">
<input type="checkbox" id="require-pinned-server">
<span>
<span class="check-title">Require a pinned server</span>
<span class="hint">Refuse to look up a new recipient's key from a server you haven't
pinned. An address that already carries a key, or a recipient you already know, is
unaffected — this only applies to a new, hand-typed address.</span>
</span>
</label>
<h3>rotations</h3>
<p id="rotations-description" class="hint">Issues a fresh key from your secret with a signed
certificate proving it replaces this one. Contacts accept the change from that chain, and
the old key stays readable. Rotation is not revocation.</p>
<div id="rotations-count"></div>
<button type="button" id="rotate-identity" class="outline">Rotate identity</button>
<span id="rotate-error" class="error"></span>
<h3 class="danger-head">log out</h3>
<p id="logout-description" class="hint">Removes this identity from this client entirely: the
secret, pins, contacts and every cached message. Without a backup of the secret there is no
way back in. Mail already fetched here may have no other copy anywhere, unless "leave mail on
server" was on. Whatever is still on the server comes back on the first fetch after logging
back in.</p>
<button type="button" id="logout" class="danger">Log out and delete all data</button>
<span id="logout-error" class="error"></span>
</section>
</div>
</div>
2026-09-26 15:59:28 +03:00
</dialog>
<dialog id="confirm">
<div class="confirm-body">
<h2 id="confirm-title"></h2>
<p id="confirm-text"></p>
</div>
<footer>
<button type="button" id="confirm-cancel" autofocus>Cancel</button>
<button type="button" id="confirm-ok"></button>
</footer>
</dialog>
<div id="status" class="toast" role="status" aria-live="polite" hidden></div>
2026-09-26 15:59:28 +03:00
<script type="module" src="js/app.js"></script>
</body>
</html>