Drops the per-server pin list down to the one server your account is bound to, adds an enforced network timeout, and replaces the global status toast inside settings with inline feedback and a reusable confirm dialog for rotate/log out. Mail-fetching preferences move into the server tab; the now-superseded claim/recall forms are gone since onboarding covers both.
249 lines
13 KiB
HTML
249 lines
13 KiB
HTML
<!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" 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>
|
|
</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>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>
|
|
</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>
|
|
</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>
|
|
</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>
|
|
<footer>
|
|
<label class="anon"><input type="checkbox" name="anonymous"> anonymous — omit my address</label>
|
|
<span id="compose-error" class="error"></span>
|
|
<button type="button" id="compose-cancel">cancel</button>
|
|
<button id="send" class="filled" value="send">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>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>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>
|
|
</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>
|
|
<script type="module" src="js/app.js"></script>
|
|
</body>
|
|
</html>
|