feat: rework first run as a setup checklist and split settings into tabs

This commit is contained in:
randogoth 2026-10-06 11:57:04 +03:00
parent 6f82a84b69
commit 1710d7b144
5 changed files with 604 additions and 232 deletions

View file

@ -21,6 +21,8 @@
<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>
@ -34,21 +36,43 @@
</div>
</header>
<!-- Two cards, one at a time: deciding between "create" and "restore" is the
only question this screen asks. Everything the new identity still needs is
deferred to the checklist in the app, so neither card is a wall of fields. -->
<main id="setup" class="setup" hidden>
<h2>welcome</h2>
<p>Identity is one 32-byte master secret. Back it up: it is the only secret, and losing it means losing the address.</p>
<div class="row">
<button id="create-identity">create a new identity</button>
</div>
<p class="hint">or restore from a saved master:</p>
<div class="row restore-row">
<label>master<input id="restore-master" placeholder="64 hex characters" spellcheck="false"></label>
<label>address (optional)<input id="restore-address" placeholder="alice@example.org, if already registered"
spellcheck="false"></label>
<label id="restore-key-label">server's public key
<input id="restore-key" placeholder="base32" spellcheck="false"></label>
<button id="restore-identity">restore</button>
</div>
<section id="setup-create" 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>
<p class="hint centered">The next screen walks you through the three things left to set up.</p>
<button type="button" id="show-restore" class="link">I already have a secret to restore</button>
</section>
<section id="setup-restore" class="setup-card" hidden>
<h2>restore</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 master secret
<input id="restore-master" placeholder="64 hex characters" spellcheck="false" required>
</label>
<details id="restore-extra">
<summary>I also know my address</summary>
<p class="hint">Optional, and easy to add later from settings. A secret does not record which
address it was registered under, and this browser was never told — so filling these in is
what brings the address back now.</p>
<label>address
<input id="restore-address" placeholder="alice@example.org" spellcheck="false">
</label>
<label id="restore-key-label">your server's public key
<input id="restore-key" placeholder="base32, from the server's operator" spellcheck="false">
</label>
</details>
<button class="filled large">restore</button>
</form>
<button type="button" id="show-create" class="link">back</button>
</section>
</main>
<main id="app" hidden>
@ -68,6 +92,17 @@
</div>
</nav>
<div class="listpane">
<!-- What setup still owes, over the mailbox it is holding up rather than
inside the settings dialog the user would have to think to open. -->
<aside id="setup-banner" class="setup-banner" hidden>
<div class="setup-banner-head">
<h2>finish setting up</h2>
<span id="setup-banner-count" class="setup-banner-count"></span>
<button type="button" id="setup-dismiss" class="icon-only" aria-label="hide this checklist"
title="hide this checklist"><svg class="icon"><use href="#i-close"/></svg></button>
</div>
<ol id="setup-steps" class="setup-steps"></ol>
</aside>
<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>
@ -98,72 +133,107 @@
</form>
</dialog>
<!-- Tabs, not one scroll: the account, the server, a mailbox preference, the
backup file and the irreversible operations have nothing to say to each
other, and reading all five at once was the overwhelming part. -->
<dialog id="settings">
<h2>settings</h2>
<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>
<section>
<h3><span id="step-server" class="step" data-n="1">1</span> your server</h3>
<p class="hint">Pin the key of the server that will hold your mail. Get it from its operator over a
channel you already trust — registering, recalling and fetching all refuse an unpinned server.</p>
<form id="pin-form">
<label>host<input name="host" placeholder="example.org" spellcheck="false" required></label>
<label>public key<input name="key" placeholder="base32" spellcheck="false" required></label>
<button class="filled">pin</button>
</form>
<ul id="pins" class="kv"></ul>
</section>
<div class="settings-body">
<nav id="settings-tabs" class="settings-tabs" aria-label="settings sections">
<button type="button" data-tab="account" class="active" aria-current="true">account</button>
<button type="button" data-tab="server">server</button>
<button type="button" data-tab="mail">mail</button>
<button type="button" data-tab="data">backup</button>
<button type="button" data-tab="advanced">advanced</button>
</nav>
<section>
<h3><span id="step-address" class="step" data-n="2">2</span> your address</h3>
<p class="hint">Claim a username on a server you have pinned. This is what people write to.</p>
<form id="register-form">
<label>address<input name="address" placeholder="alice@example.org" spellcheck="false" required></label>
<label>invite token<input name="token" placeholder="if required"></label>
<button class="filled">register</button>
</form>
<details>
<summary>already registered elsewhere?</summary>
<p class="hint">Restoring a master brings back the identity, not the memory of what address it was
registered under in a different browser — the server was never asked. Recall it here: this
looks the address up and checks it resolves to this identity's key, without registering anything.</p>
<form id="recall-form">
<label>address<input name="address" placeholder="alice@example.org" spellcheck="false" required></label>
<button>recall</button>
</form>
</details>
</section>
<div id="settings-panels" class="settings-panels">
<section class="panel" data-panel="account">
<div id="account-card"></div>
<section>
<h3>your identity</h3>
<div id="identity-card"></div>
</section>
<details id="account-claim">
<summary id="account-claim-summary">claim your address</summary>
<p class="hint">Pick a username on a server you have pinned. This is what people write to.</p>
<form id="register-form">
<label>address<input name="address" placeholder="alice@example.org" spellcheck="false" required></label>
<label>invite token<input name="token" placeholder="only if your server asks"></label>
<button class="filled">register</button>
</form>
<p class="hint">Already registered this identity in another browser? Look the address up
instead — this checks it resolves to this identity and claims nothing.</p>
<form id="recall-form">
<label>address<input name="address" placeholder="alice@example.org" spellcheck="false" required></label>
<button>look it up</button>
</form>
</details>
<section>
<h3>fetch</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">Fetch keeps a copy on the server instead of deleting it; delete a message
here to remove it from both.</span>
</span>
</label>
</section>
<h3>recovery secret</h3>
<div id="identity-card"></div>
</section>
<section>
<h3>backup</h3>
<p class="hint">Mail, once fetched, exists only here unless "leave mail on server" is on above.
This backs up the local inbox, requests, sent mail, contacts and pinned servers into one file.
It never includes the master: that has its own reveal-and-copy flow above, meant for a
password manager, not a downloadable file.</p>
<div class="backup-row">
<button type="button" id="export-data">export</button>
<button type="button" id="import-data-trigger">import</button>
<input id="import-data" type="file" accept="application/json" hidden>
<section class="panel" data-panel="server" hidden>
<h3>your server</h3>
<p class="hint">Pinning a server's public key is what lets this client tell your real server
from anything pretending to be it. Ask its operator for the key over a channel you already
trust.</p>
<details>
<summary>why does this matter?</summary>
<p class="hint">Nothing else authenticates the server, so an unpinned host could be anyone —
registering, looking up an address and fetching all refuse to run against one.</p>
</details>
<ul id="pins" class="kv"></ul>
<form id="pin-form">
<label>host<input name="host" placeholder="example.org" spellcheck="false" required></label>
<label>public key<input name="key" placeholder="base32" spellcheck="false" required></label>
<button class="filled">pin</button>
</form>
</section>
<section class="panel" data-panel="mail" hidden>
<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 browser can
fetch it too. Deleting a message here removes it from both.</span>
</span>
</label>
<details>
<summary>what happens by default?</summary>
<p class="hint">Fetching acknowledges each message once it has been verified, and the server
drops its copy — so mail you have read lives only in this browser. Back it up under
"backup", or turn the option above on.</p>
</details>
</section>
<section class="panel" data-panel="data" hidden>
<h3>backup &amp; transfer</h3>
<p class="hint">One file holding your mail, contacts and pinned servers — for moving to
another browser, or for keeping what the server no longer has.</p>
<div class="backup-row">
<button type="button" id="export-data" class="filled">export</button>
<button type="button" id="import-data-trigger" class="outline">import</button>
<input id="import-data" type="file" accept="application/json" hidden>
</div>
<details>
<summary>what is in the file, and is it safe?</summary>
<p class="hint">It is sealed to this identity: only a browser with this secret restored can
open it. It deliberately leaves the secret itself out — that belongs in a password
manager, not in a file in your downloads folder.</p>
</details>
</section>
<section class="panel" data-panel="advanced" hidden>
<div id="advanced-card"></div>
</section>
</div>
</section>
<footer><button id="settings-close">close</button></footer>
</div>
</dialog>
<div id="status" class="toast" role="status" aria-live="polite" hidden></div>