gsmol/web/index.html

275 lines
16 KiB
HTML
Raw Permalink Normal View History

2026-10-09 13:47:42 +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">
<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" data-l10n-aria-label="settings-nav-label"
data-l10n-title="settings-nav-label"><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>
</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. -->
<main id="setup" class="setup" hidden>
<section id="setup-intro" class="setup-card">
<h2 data-l10n-id="onboard-welcome-title">welcome</h2>
<p class="lead" data-l10n-id="onboard-welcome-description">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" data-l10n-id="onboard-create-button">create a new identity</button>
<button type="button" id="show-restore" class="link" data-l10n-id="onboard-restore-button">restore existing identity from seed</button>
</section>
<section id="setup-seed" class="setup-card" hidden>
<h2 data-l10n-id="onboard-created-title">write this seed down</h2>
<p class="lead" data-l10n-id="onboard-created-description">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" data-l10n-id="onboard-saved-button">I saved it</button>
<button type="button" id="seed-back" class="link" data-l10n-id="action-back">back</button>
</section>
<section id="setup-restore" class="setup-card" hidden>
<h2 data-l10n-id="onboard-restore-title">restore from a seed</h2>
<p class="lead" data-l10n-id="onboard-restore-description">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><span data-l10n-id="onboard-seed-entry-title">your secret</span>
<input id="restore-master" spellcheck="false" required>
</label>
<span id="restore-error" class="error"></span>
<button class="filled large" data-l10n-id="onboard-restore-go-button">restore</button>
</form>
<button type="button" id="restore-back" class="link" data-l10n-id="action-back">back</button>
</section>
<section id="setup-server" class="setup-card" hidden>
<h2 data-l10n-id="signup-server-title">set up the server</h2>
<form id="server-form" class="stack">
<label><span data-l10n-id="signup-host-label">host</span>
<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" data-l10n-id="signup-fetch-key-button">fetch the server's key</button>
</form>
<button type="button" id="server-back" class="link" data-l10n-id="signup-start-over-button">start over</button>
</section>
<section id="setup-account" class="setup-card" hidden>
<h2 data-l10n-id="signup-account-title">choose your address</h2>
<div id="account-mode" class="mode-toggle" role="group">
<button type="button" data-mode="new" class="active" aria-current="true"
data-l10n-id="signup-mode-new-label">create a new address</button>
<button type="button" data-mode="existing" data-l10n-id="signup-mode-existing-label">I already have this address</button>
</div>
<form id="account-form" class="stack">
<label><span data-l10n-id="signup-user-label">user</span>
<input id="account-user" placeholder="alice" spellcheck="false" required>
</label>
<label id="account-token-label"><span data-l10n-id="signup-invite-label">invite token (if the server requires one)</span>
<input id="account-token">
</label>
<span id="account-error" class="error"></span>
<button id="account-action" class="filled large" data-l10n-id="signup-register-button">register</button>
</form>
<button type="button" id="account-back" class="link" data-l10n-id="action-back">back</button>
</section>
</main>
<main id="app" hidden>
<nav class="rail" aria-label="folders">
<button id="write" class="compose"><svg class="icon"><use href="#i-compose"/></svg><span data-l10n-id="mail-write-button">compose</span></button>
<div id="folders">
<button data-folder="inbox" class="active" aria-current="true" data-l10n-aria-label="folder-inbox">
<svg class="icon"><use href="#i-inbox"/></svg><span class="label" data-l10n-id="folder-inbox">inbox</span>
<span id="count-inbox" class="count"></span>
</button>
<button data-folder="requests" data-l10n-aria-label="folder-requests">
<svg class="icon"><use href="#i-inbox"/></svg><span class="label" data-l10n-id="folder-requests">requests</span>
<span id="count-requests" class="count"></span>
</button>
<button data-folder="sent" data-l10n-aria-label="folder-sent"><svg class="icon"><use href="#i-sent"/></svg><span class="label" data-l10n-id="folder-sent">sent</span></button>
<button data-folder="contacts" data-l10n-aria-label="contacts-caption"><svg class="icon"><use href="#i-contacts"/></svg><span class="label" data-l10n-id="contacts-caption">contacts</span></button>
</div>
</nav>
<div class="listpane">
<div class="toolbar">
<div id="mail-tools" class="tools">
<button id="fetch" class="primary"><svg class="icon"><use href="#i-refresh"/></svg><span class="label" data-l10n-id="mail-fetch-button">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/…" data-l10n-placeholder="contacts-import-placeholder"
data-l10n-aria-label="contacts-import-placeholder" spellcheck="false" required>
<button class="primary" data-l10n-id="contacts-import-button">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" data-l10n-id="mail-empty-title">select a message</p></article>
</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" data-l10n-placeholder="compose-subject-label" placeholder="subject">
<textarea name="body" rows="12" data-l10n-placeholder="compose-body-placeholder" placeholder="message"></textarea>
<div id="compose-unpinned" class="seed-card" hidden>
<h4 id="compose-unpinned-title"></h4>
<p id="compose-unpinned-body" class="hint"></p>
<div id="compose-unpinned-key-area" hidden></div>
<div class="actions">
<button type="button" id="compose-unpinned-fetch" data-l10n-id="compose-pin-button-fetch">Fetch their server's key</button>
<button type="button" id="compose-unpinned-pin" class="filled" hidden data-l10n-id="compose-pin-button-send">Pin and send</button>
</div>
<span id="compose-unpinned-error" class="error"></span>
</div>
<footer>
<label class="anon"><input type="checkbox" name="anonymous"> <span data-l10n-id="compose-anonymous-label">anonymous — omit my address</span></label>
<span id="compose-error" class="error"></span>
<button type="button" id="compose-cancel" data-l10n-id="action-cancel">cancel</button>
<button id="send" class="filled" value="send" data-l10n-id="compose-send-button">send</button>
</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. -->
<dialog id="settings">
<header class="dialog-head">
<h2 data-l10n-id="settings-nav-label">settings</h2>
<button type="button" id="settings-close" class="icon-only" data-l10n-aria-label="settings-close-aria-label"
data-l10n-title="settings-close-tooltip"><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"
data-l10n-id="settings-tab-identity">identity</button>
<button type="button" data-tab="server" data-l10n-id="settings-tab-server">server</button>
<button type="button" data-tab="backup" data-l10n-id="settings-tab-backup">backup</button>
<button type="button" data-tab="advanced" data-l10n-id="settings-tab-advanced">advanced</button>
</nav>
<div id="settings-panels" class="settings-panels">
<section class="panel" data-panel="identity">
<div id="identity-fields"></div>
<h3 data-l10n-id="settings-recovery-secret-title">recovery secret</h3>
<div id="identity-secret"></div>
</section>
<section class="panel" data-panel="server" hidden>
<h3 data-l10n-id="server-connection-title">connection</h3>
<div id="server-connection"></div>
<h3 data-l10n-id="server-fetching-title">fetching</h3>
<label class="check-row">
<input type="checkbox" id="leave-on-server">
<span>
<span class="check-title" data-l10n-id="server-leave-mail-title">Leave mail on server</span>
<span class="hint" data-l10n-id="server-leave-mail-subtitle">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" data-l10n-id="backup-page-description">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" data-l10n-id="backup-export-title">Export</button>
<span class="hint" data-l10n-id="backup-export-subtitle">Write a backup file</span>
</div>
<div class="action-row">
<button type="button" id="import-data-trigger" class="outline" data-l10n-id="backup-import-title">Import</button>
<span class="hint" data-l10n-id="backup-import-subtitle">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 data-l10n-id="advanced-trust-section-title">trust</h3>
<label class="check-row">
<input type="checkbox" id="require-pinned-server">
<span>
<span class="check-title" data-l10n-id="advanced-strict-pinning-title">Require a pinned server</span>
<span class="hint" data-l10n-id="advanced-strict-pinning-subtitle">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 data-l10n-id="advanced-rotations-title">rotations</h3>
<p id="rotations-description" class="hint" data-l10n-id="advanced-rotations-description">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" data-l10n-id="advanced-rotate-button">Rotate identity</button>
<span id="rotate-error" class="error"></span>
<h3 class="danger-head" data-l10n-id="advanced-logout-title">log out</h3>
<p id="logout-description" class="hint" data-l10n-id="advanced-logout-description">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" data-l10n-id="advanced-logout-button">Log out and delete all data</button>
<span id="logout-error" class="error"></span>
</section>
</div>
</div>
</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 data-l10n-id="action-cancel">Cancel</button>
<button type="button" id="confirm-ok"></button>
</footer>
</dialog>
<div id="status" class="toast" role="status" aria-live="polite" hidden></div>
<script type="module" src="js/app.js"></script>
</body>
</html>