From 4ec6a69fc4257061b3a06719a98f5bbde9d1f3cc Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Thu, 12 Nov 2026 18:23:07 -0300 Subject: [PATCH] docs: layout and presentation after the v0.7.1 screens, with its prototype (D254) Records D254 after a capture of every v0.7.1 screen at the three widths in both themes and an HTML prototype approved part by part: the preview badge, the storage warning on one line, the backup reminder at the top of the list, a neutral lock badge, pills for folders and types, the detail's header bar, the editor's fixed bar and the generator in a dialog or bottom sheet, Show and Hide on every password field, Settings inside the vault frame, wizards on a screen of their own, and the type scale within D231. Presentation only: no step, check, warning or security rule is removed. Adds the prototype as docs/prototypes/phase-7b-layout.html, the Phase 7b tasks, and moves the Essential UX Gate's hands-on test to v0.7.2. Co-Authored-By: Claude Opus 5.5 --- docs/overview.md | 21 +- docs/prototypes/phase-7b-layout.html | 2788 ++++++++++++++++++++++++++ docs/roadmap.md | 11 +- 3 files changed, 2809 insertions(+), 11 deletions(-) create mode 100644 docs/prototypes/phase-7b-layout.html diff --git a/docs/overview.md b/docs/overview.md index ac361f4..2229d19 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -526,15 +526,15 @@ BlindVault favors explicit security decisions, understandable warnings, safe def ### 13.6 Password generator (D128) -The item editor offers a password generator in two modes: random characters from the selected character sets, and a passphrase of words from the BIP39 English list that `@scure/bip39` already ships. Every choice comes from libsodium's CSPRNG through `randombytes_uniform`, which has no modulo bias. The generator shows the entropy of its settings in bits. Defaults: 20 characters from all four character sets (lowercase, uppercase, digits, symbols), or 6 words (about 66 bits). Minimums: 12 characters or 4 words; below them the generator offers no value. Every character is drawn uniformly from the union of the selected sets, with no guarantee of one character per set, which keeps the entropy display exact; the user generates again if a site requires each set (D138). A generated value exists only in the field being edited until the item is saved; the generator keeps no history and persists no generated value. Its settings (mode, length, character sets, word count) are non-sensitive UI preferences. +The item editor offers a password generator, opened from a button in the password field (D254), in two modes: random characters from the selected character sets, and a passphrase of words from the BIP39 English list that `@scure/bip39` already ships. Every choice comes from libsodium's CSPRNG through `randombytes_uniform`, which has no modulo bias. The generator shows the entropy of its settings in bits. Defaults: 20 characters from all four character sets (lowercase, uppercase, digits, symbols), or 6 words (about 66 bits). Minimums: 12 characters or 4 words; below them the generator offers no value. Every character is drawn uniformly from the union of the selected sets, with no guarantee of one character per set, which keeps the entropy display exact; the user generates again if a site requires each set (D138). A generated value exists only in the field being edited until the item is saved; the generator keeps no history and persists no generated value. Its settings (mode, length, character sets, word count) are non-sensitive UI preferences. ### 13.7 Visual design (D231, D232, D233) One sober identity: zinc neutrals, medium radii, a light shadow, and one accent color the user picks. Visual design never hides a security decision (section 13.5) and is the same in the three layouts. -- **Accent.** A closed list of six accents, each with values for the light and the dark theme: blue (the default), indigo, violet, teal, green and rose. There is no free color picker, since an arbitrary color guarantees neither contrast nor legible text on a button. Red, orange and amber are not accents: danger (red), warning (amber) and the Unlocked badge (amber) keep fixed colors whatever the accent, so an ordinary button never looks like an alert. +- **Accent.** A closed list of six accents, each with values for the light and the dark theme: blue (the default), indigo, violet, teal, green and rose. There is no free color picker, since an arbitrary color guarantees neither contrast nor legible text on a button. Red, orange and amber are not accents: danger (red) and warning (amber) keep fixed colors, and the Unlocked badge is neutral (D254) whatever the accent, so an ordinary button never looks like an alert. - **Theme.** System (the default), Light or Dark. The theme and the accent are UI preferences in `localStorage` (section 7), for the installation, not synchronized, applied before unlock and before the first render; an unknown or missing value falls back to the default. Both are chosen in Settings › Appearance, the accents as named swatches. The app icon does not change with the accent; `theme-color` follows the theme only. -- **Tokens (D232).** Components use semantic tokens whose values switch with the theme and the accent: neutrals (surface, raised, subtle, fg, muted, line, control, scrim), the accent with its hover, ink, soft and text variants, danger with the same five, warning and info with their soft and text variants, success, and focus, which is the accent. Tailwind's default palette is removed, so a palette class generates nothing, and a unit test refuses a palette color or a `dark:` variant in any component. The app resolves System to `data-theme` (light or dark) on the root element and follows the system while System is chosen; the accent is `data-accent`. Primary buttons and the focus ring take the accent, the selected list entry its soft variant; the Unlocked badge and the preview banner take warning. Two radii (`rounded-control`, `rounded-card`), two elevations (flat, and `shadow-raised`: a light shadow in the light theme, a 1 px edge in the dark one), the standard spacing scale. +- **Tokens (D232).** Components use semantic tokens whose values switch with the theme and the accent: neutrals (surface, raised, subtle, fg, muted, line, control, scrim), the accent with its hover, ink, soft and text variants, danger with the same five, warning and info with their soft and text variants, success, and focus, which is the accent. Tailwind's default palette is removed, so a palette class generates nothing, and a unit test refuses a palette color or a `dark:` variant in any component. The app resolves System to `data-theme` (light or dark) on the root element and follows the system while System is chosen; the accent is `data-accent`. Primary buttons and the focus ring take the accent, the selected list entry its soft variant; the preview badge takes warning (D254). Two radii (`rounded-control`, `rounded-card`), two elevations (flat, and `shadow-raised`: a light shadow in the light theme, a 1 px edge in the dark one), the standard spacing scale. - **Type.** The system font stack, so no web font and no change to the CSP (D193); the system monospace stack for secrets, TOTP codes, hashes and the Recovery Phrase. - **Icons (D233).** A small set copied from Lucide 1.61.0 (ISC; the icons derived from Feather, MIT) into `apps/web/src/ui/icons/`, with both notices, and no runtime dependency; an icon joins the set when a screen uses it. An icon alone carries an accessible name; a security action always has visible text. - **Dialogs (D233).** Destructive and security confirmations that ask a single yes or no open in the native `` as modal dialogs (focus kept inside, Escape cancels, the rest inert), with their text unchanged: retiring a device and dismissing a rejected peer from the backup warning. A flow whose warnings are a step of their own (erase, replace, re-found, restore, merge, regenerate the phrase) keeps them in the flow, its explicit acknowledgement included, so no dialog adds a step. @@ -937,7 +937,7 @@ This document is **normative**. | D174 | No vault content in the form history | Every text field the app renders sets `autocomplete="off"`, so the browser keeps no typed title, field value, tag, note or folder name in its form history, which it stores outside the vault's storage, in cleartext, and offers back as suggestions. The local password fields set `current-password` or `new-password` instead. Makes explicit what CRYPTO-INV-001 implies | | D175 | The browser's password manager | The local password fields keep `new-password` and `current-password`, and the app does not try to stop the browser from saving the password: browsers may ignore `autocomplete="off"` on password fields, and a text field masked by CSS to hide it from them would break accessibility. A password saved in the browser sits next to the locked storage, so, depending on how the browser protects saved passwords, a copy of the browser's data can be enough to open the vault. Wherever a local password is set (vault creation, password change), the UI states this | | D176 | No spellchecking of vault content | Every text field the app renders sets `spellcheck="false"`, notes included. Basic spellchecking runs locally, but an enhanced spellchecker the user can enable in the browser sends what is typed to a remote service, and no vault content leaves the device outside sync and backups. The cost is no typo marks in titles and notes | -| D177 | Preview deployment | Before Phase 3, the build MAY be served as a preview at `https://blindvault.allisson.dev`, a subdomain that serves nothing else and so a dedicated origin (D90), with the CSP and referrer policy by `` (D83, D91, D147) and the shared-origin warning. Under Trusted Types the app has one policy, which turns only the Argon2id worker's own URL into a script URL. There is no service worker yet, so a deploy changes the code at the next load without confirmation (D63 is not met): the workflow runs only when started by hand, never on a push. The preview's vaults are throwaway, as the app's banner states, and no migration is owed for them | *Amended by D190, D194.* +| D177 | Preview deployment | Before Phase 3, the build MAY be served as a preview at `https://blindvault.allisson.dev`, a subdomain that serves nothing else and so a dedicated origin (D90), with the CSP and referrer policy by `` (D83, D91, D147) and the shared-origin warning. Under Trusted Types the app has one policy, which turns only the Argon2id worker's own URL into a script URL. There is no service worker yet, so a deploy changes the code at the next load without confirmation (D63 is not met): the workflow runs only when started by hand, never on a push. The preview's vaults are throwaway, as the app's banner states, and no migration is owed for them | *Amended by D190, D194, D254.* | D178 | `.bvault` v1 layout | A fixed prefix (`\x89BVAULT\n` magic, `u16` format version, `u32` length) followed by a strict deterministic CBOR header holding only `phraseId`, phrase creation date, keyslots and the `secretstream` header; the header bytes are the additional data of the first stream chunk, which authenticates them whichever keyslot opened, since `crypto_box_seal` takes no AAD. The payload is cut into 64 KiB plaintext chunks with no framing, so the file reveals only the payload size, and holds a length-prefixed `meta` (`vaultId`, Master Key, record count, export date) followed by every stored envelope, read one by one. Export Argon2id is fixed at 64 MiB and ops 12, the first to reach 1 s on the slowest phone measured; a restore accepts the local unlock range (64–256 MiB, ops 2–64) before deriving. The header's `phraseId` and creation date must equal the payload's Recovery Envelope metadata. Format v1 rejects unknown header and `meta` keys and any other version. Amends D3 and D78 | | D179 | Recovery Phrase confirmation | Creation shows the 24 words numbered, with no copy or download, and a warning against screenshots and photos; confirmation asks for the words at three positions drawn at random, and a wrong answer shows the phrase again, since a checkbox proves nothing and typing all 24 words adds effort without adding proof. The order follows section 13.3: phrase, confirmation, then the local password, so nothing is written before the password step. A vault created before the Recovery Envelope existed (the D177 preview) still unlocks; export and Check Recovery Phrase say it has no Recovery Phrase and suggest creating a new vault, with no migration (D177) | | D180 | Check Recovery Phrase outcomes | Check Recovery Phrase reports one of: the phrase opens the envelope to this vault and its backups; a valid phrase that does not open it; an envelope that opens but whose recovery keyslot public key the phrase does not derive (D24); an envelope that opens to another Master Key, compared in constant time through a Master Key subkey since an unlocked session holds no Master Key; input that is not a phrase, naming the reason and, for a word not in the list, its position; or no envelope to check, a D177 preview vault (D179) or one that failed verification. Only a device holding the phrase and the Vault Key could write an envelope with another Master Key, but restoring a backup with that phrase would then fail (D43), so it is reported like a mismatched public key, with the advice to regenerate. The typed words fill one field, which is emptied once checked; the check writes nothing, keeps no derived key and has no attempt limit, since the vault is already unlocked. Only one `recordFormatVersion` exists, so the format migration of D38 has nothing to do yet. Amends D24 | @@ -948,7 +948,7 @@ This document is **normative**. | D185 | Recovery Phrase regeneration | Regeneration asks again for the local password, or a passkey assertion with user verification (D75), and opens the Master Key wrap with it, since the unlocked session holds no Master Key (D180) and the new phrase yields the Master Key as a backup does (D181): someone holding an unlocked device cannot take the Master Key away under a phrase of their own. The order follows D179: a warning that regeneration is for a lost phrase, not an exposed one (section 5.4), then the phrase, its confirmation and the password or passkey; the inactivity timer pauses from the phrase to the commit, and any lock before the commit discards the phrase and writes nothing (D104). One commit writes only the new Recovery Envelope (new `phraseId`, creation date and recovery keyslot public key, under an HLC above the held envelope's) and the new `phraseId` as the last one seen (D43), so no other record changes. Regeneration is blocked while the vault is read-only for a clock gone back (D73), while the held envelope is beyond the future-clock bound (D74) or its metadata holds a key beyond its three (D98), and for a vault without an envelope (D179); it is allowed over an envelope kept aside as corrupt at unlock, which it replaces, since a device that lost the phrase and holds a damaged envelope cannot export and would otherwise have no way out. A `phraseId` is shown as its first 8 hex digits next to the phrase's creation date wherever a phrase is named (Settings, a backup's header at restore and merge, the export, the D43 and D145 notices); it is a label, not a check, and the backup's file name is unchanged (D181). After the commit the UI states that the new phrase is not on the other devices until they sync, and that earlier backups open only with the previous phrase, and offers a backup at once, which asks again for the password as every export does; while the last backup made on this device is older than the current phrase's creation date, Settings says no backup made here opens with it. The superseded-phrase warning (D145) is shown, in place of the D43 notice, when an envelope of another `phraseId` replaces one this device wrote, whose `hlc` carries this device's `deviceId`; a backup merge shows it from now, and sync from Phase 5. LWW carries no causal information, so it also follows a later regeneration on another device, which its text still describes truly. Amends section 5.4, D2 and D145 | | D186 | Retirement and the restored device in the UI | Retiring a Device record asks for an explicit confirmation that names the device and states that retirement is irreversible, that it is not revocation (data the device already held stays exposed), and that the response to a device that may be compromised is re-founding (D28); it asks for neither the local password nor a passkey, since it exposes no secret and retiring a lost device is the urgent action (D85). One commit writes the retired Device record, keeping its display name, `registeredAt` and unknown keys (D59), and adds its `deviceId` to the seen-as-retired list in Local Config, so a later sync or backup merge carrying the same retirement shows no D47 notice. Its HLC is a normal one above the held record's while the vault is writable, a normal one not above it while the held record is beyond the future-clock bound (D74), and the D85 one while the vault is read-only for a clock gone back; a retired record wins whatever its `hlc` (D31). Retirement is refused for this device's own record (D41), a record already retired, and a record kept aside as corrupt, which the device does not hold as a Device. Settings lists the non-retired Device records, this device's marked as such, with the count of retired ones. While this device holds no Device record of its own and other non-retired ones exist, Settings states that it is not registered, that until re-admission it exchanges data only through backups, and that re-admission runs with one of those devices (D22, D113); right after a fresh restore the same recommendation also shows once, dismissible. When no other non-retired record exists, the device is the only one (D119) and Settings recommends a backup and pairing instead. Amends D30 and D41. *Amended by D198, D219.* | | D187 | Re-founding flow | Re-founding asks again for the local password, or a passkey assertion with user verification (D75), right after its warnings, as an export or a regeneration does (D181, D185), so someone holding an unlocked device cannot replace the vault on it by one under a password of their own; the KEK only proves the user, since re-founding uses no key of the old vault. The warnings are those of section 14 (D46, D72 with a way to the trash, D47 on a retired device, D116), plus the number of records kept aside as corrupt, which cannot be carried over, and that whoever already copied the old vault keeps it; one explicit confirmation is always required, and a second, naming the peers, while Local Config lists rejected peers (D116). Then the new phrase and its confirmation, then the new local password, checked as at creation (D71); the inactivity timer pauses from the phrase to the commit, and a lock before the commit aborts it (D93, D104). Every Item and Folder that is neither a tombstone nor in the trash is carried over (the trash is emptied, D72); a valid one has its `folderId` or `parentId` rewritten to the new ID, or removed when that folder is not carried over (D86, D99), while a read-only item keeps its content unchanged and shows at the root by the orphan rule (D60, D86); a record beyond the future-clock bound is carried over with its decoded content and a fresh HLC, and a vault read-only for a clock gone back can re-found, since the new vault starts its HLC state from now (D149). The new Device record takes the old own record's display name, or the default name on an unregistered device. The new Local Config keeps the security preferences and starts with no backup date, no seen or seen-as-retired devices, no rejected peers and no passkey (D107), with the new `phraseId` as the last one seen. One commit writes the whole new vault with `replacesVaultId`; the old database is then deleted and the old passkey handled as after a replacement (D96, D120). Amends D28 | -| D188 | Backup reminder | The backup interval of section 13.4 is a fixed 30 days in v1, with no setting, since a preference would add a Local Config key and a form field for no clear gain. After an unlock, the vault shows a reminder while a backup is due: none was made on this device (a new vault, a restored or re-founded device: a backup made elsewhere does not count, since the date is local), the last one is older than the current Recovery Phrase's creation date (D185), so none made here opens with it, or it is 30 days old or more by this device's clock. A last backup dated in the future, after the clock went back, counts as recent until the clock passes it: the reminder is a SHOULD, and guessing the right clock risks false reminders. A vault without a Recovery Envelope, or with one kept aside as corrupt, cannot export (D179, D181) and shows no reminder; Settings already says why. The reminder offers to make a backup, which opens Settings with the cursor in the export's password field, and to dismiss it until the next lock, so nothing new is stored and it shows again at the next unlock while still due. Amends section 13.4 | +| D188 | Backup reminder | The backup interval of section 13.4 is a fixed 30 days in v1, with no setting, since a preference would add a Local Config key and a form field for no clear gain. After an unlock, the vault shows a reminder while a backup is due: none was made on this device (a new vault, a restored or re-founded device: a backup made elsewhere does not count, since the date is local), the last one is older than the current Recovery Phrase's creation date (D185), so none made here opens with it, or it is 30 days old or more by this device's clock. A last backup dated in the future, after the clock went back, counts as recent until the clock passes it: the reminder is a SHOULD, and guessing the right clock risks false reminders. A vault without a Recovery Envelope, or with one kept aside as corrupt, cannot export (D179, D181) and shows no reminder; Settings already says why. The reminder offers to make a backup, which opens Settings with the cursor in the export's password field, and to dismiss it until the next lock, so nothing new is stored and it shows again at the next unlock while still due. Amends section 13.4. *Amended by D254.* | | D189 | Asset manifest and reproducible build | The asset manifest is `asset-manifest.txt` at the root of the build output: one line ` ` per file, the path relative to the output root with `/` separators, in byte order of the UTF-8 paths, each line ending in `\n`, so `shasum -a 256 -c asset-manifest.txt` checks a deployment with standard tools, the decisive check outside the app (section 11). It lists every file of the build except itself and `sw.js`, which the browser updates on its own (D63); a path holding a newline or a backslash, which `shasum` escapes, fails the build. The manifest hash is the SHA-256 of the manifest's bytes. The version gets no line of its own: the code that shows it is in a listed file. The build embeds no date; CI sets `SOURCE_DATE_EPOCH` to the commit time for tools that read it. `vite build` refuses to run under a Node version other than `.nvmrc`'s, since it is the build that publishes a hash, while `engines` keeps any 24.x for development and tests. A CI job builds the same commit twice, from checkouts at different paths and the second under another time zone, locale, umask and `SOURCE_DATE_EPOCH`, and fails unless both manifests are identical. The reference build is CI's (Ubuntu); a build on another OS is not yet known to match. Amends D5. *Amended by D191, D229.* | | D190 | Service worker and versioned cache | `sw.js` is written by hand in plain JavaScript, with no bundler and no dependency, so its bytes change only with its logic (D63), and is registered at `/` with `updateViaCache: 'none'`, in the production build only. At install it fetches `asset-manifest.txt` and every file it lists, bypassing the HTTP cache, checks each against its SHA-256 and the manifest's format (D189), and only then writes them, with the manifest, to the cache `blindvault-`; any mismatch, missing file or malformed line fails the install, leaving the app without a service worker. The check catches an inconsistent deployment, not a malicious host, which serves both the manifest and the files (section 11). A state cache records the cache of the installed worker, and activation makes it the active one and deletes every other versioned cache, so a worker still running never serves another version's files. The worker answers only same-origin GET requests: a navigation gets the cached `index.html`, a listed path its cached file, anything else the network, and nothing is cached at fetch time. Under Trusted Types the registration goes through a second policy, which turns only `/sw.js` into a script URL (amending D177's one policy). It calls neither `skipWaiting` nor `clients.claim`: the first load stays on the network and the next one is served from the cache. The web app manifest declares the app standalone at `/`, with an SVG icon, 192 and 512 px PNG icons, a maskable one and a 180 px Apple touch icon. Until the update flow (D63, D142, D152, D162) exists, a deploy reaches only new installations, so the preview is not redeployed (D177). Amends D5 and D177. *Amended by D191.* | | D191 | Update flow | **Asset updates.** The app asks its service worker to look for an update when it starts and whenever it becomes visible. The worker fetches `asset-manifest.txt` past the HTTP cache; when its hash differs from the active cache's, it fetches and checks the new bundle as an install does (D190), writes it to `blindvault-` and records it as staged, then answers with the new version and the manifest hash. A bundle that fails any check, or has no valid `version.txt`, is not offered, and the next look tries again. The build writes `version.txt`, the version of `apps/web/package.json` on one line, which the manifest lists like any other file (amending D189). The app compares no versions, since the host serves both; the hash is what is verified outside the app. The app offers the update, with the running and the new version and the full manifest hash, only in the `no-vault`, `locked` and `unlocked` states, and not while a restore and its follow-up steps, a backup merge, a re-founding, a shown Recovery Phrase, a pairing, a re-admission, a sync session or an import is in progress (D152), nor while a new `sw.js` is installing or waiting. Confirming locks the vault synchronously, then asks the worker to apply that hash. The worker refuses unless the staged cache holds a manifest of that hash and no other worker is installing or waiting; otherwise it makes that cache the active and the installed one, deletes every other versioned cache, reloads every other app window, and the app reloads itself. **Changed `sw.js`.** The app registers its service worker as soon as it starts, before the first render, instead of at the `load` event. An `updatefound` event while a worker is already active, or a worker installing or waiting next to an active one when the registration resolves, is a changed `sw.js`; the first install of a service worker is not. The handler locks the vault before anything else, with no `await` (D162), then the app shows, in place of every screen, that the host served a changed service worker the release notes must justify, and that the vault should not be unlocked until the release is verified outside the app; the user dismisses it with "I verified, continue". The warning comes back at every start while the new worker exists, and nothing records it. `registration.update()` runs whenever the app becomes visible, unlocked or not. **No code after the first unlock (D142).** The app starts one Argon2id worker at startup and keeps it until the window closes, serving one derivation at a time. A request released before its answer, by a lock, gets its key zeroed and discarded. A worker that fails is not restarted, so derivations fail until a reload. The password-strength dictionaries also load at startup. The app shows no start screen until both have loaded, and when either fails it asks for a reload and does not open the vault. A browser test records every script, worker and WebAssembly request after the first unlock, through a password change and an export, and requires none. Amends D63, D142, D189 and D190 | @@ -956,7 +956,7 @@ This document is **normative**. | D193 | Framed app | The app does not open inside a frame. Before anything else, the page compares `window.self` with `window.top`, which a framing page cannot forge; when they differ, it shows only a notice that BlindVault does not open inside another page, with a link that opens it in its own tab with `noopener` and `noreferrer` (D147). It creates no controller, so it opens no IndexedDB database and takes no Web Lock, and it starts no Argon2id worker, reads no asset manifest and registers no service worker. Refusing only unlock, as D83 said, left vault creation, restore and the forgotten-password reset, which also take a password or a Recovery Phrase, open to an overlay; and a page of the same site, such as another subdomain of the official domain, shares the app's storage partition, so a framed app would see the vault. The app does not navigate the top window to leave the frame, which a sandbox can block and the framing page can race. A host that can send headers also sets `frame-ancestors 'none'` (self-hosting instructions, D90). Amends D83 | | D194 | Releases | The official build is served at `https://blindvault.allisson.dev`, the dedicated origin of the preview (D90, D177), which becomes the official one. A release is a tag `v`, where the version is that of `apps/web/package.json` (and of the root `package.json`) in SemVer with no build metadata; every 0.x version and every pre-release version is published as a pre-release on GitHub, and the app keeps its preview banner until 1.0, since formats may still change with no migration owed (D177). Pushing the tag runs one workflow, which builds once with `SOURCE_DATE_EPOCH` and uses that build for both the GitHub Release and the GitHub Pages deployment: no push to `main` and no manual run deploys anything. The workflow refuses a tag that does not match the built `version.txt`, and requires release notes in `docs/releases/.md`. When `apps/web/public/sw.js` differs from the previous release's, the notes MUST hold a `## Service worker` section saying why (D191), and every release states whether `sw.js` changed. The release body ends with the version, the asset manifest hash, the SHA-256 of `sw.js` and how to verify them; `asset-manifest.txt`, `sw.js` and a reproducible `.tar.gz` of the build are attached, each with a GitHub build provenance attestation (`gh attestation verify`), which is the signing of section 12. After the deployment, the workflow downloads the served `asset-manifest.txt` and `sw.js`, requires them to equal the release's, and checks every listed file against the manifest; since the About screen shows the hash of the served manifest (D192), the hash shown is the one published. Amends D177 *Amended by D228.* | | D195 | Self-hosting | `docs/self-hosting.md` states what a self-hosted deployment needs: an origin that serves nothing else (D90), served at its root, over HTTPS, with no fallback to `index.html`; the files of a release checked with their attestations and the manifest, or rebuilt from the tag; and moving between origins by backup and restore. A host that can send headers sends the `` policy with `frame-ancestors 'none'` (D193), `Referrer-Policy: no-referrer`, `X-Content-Type-Options: nosniff`, `Strict-Transport-Security`, and `Cross-Origin-Opener-Policy: same-origin`, so a page of another origin that opens the app keeps no handle on its window and cannot navigate it to a fake unlock page; every file outside `assets/` is served `no-cache`, so an update is seen (D63, D191). Ready configurations for Caddy and nginx live in `deploy/`; a unit test requires their policy to be the build's, and a CI job serves the build with each, in Docker images pinned by digest, and runs browser tests against it: the headers and caching of each file, a frame of another origin stopped by `frame-ancestors`, no handle kept by a page of another origin that opened the app, and the app installed, offline and showing the hash of the served manifest. The policy has one source, `apps/web/build/csp.ts`. Amends D83 *Amended by D228.* | -| D196 | State UI | Settings opens with a "This device" section: whether the browser keeps this origin's storage (`navigator.storage.persisted()`: kept, may be cleared, or unknown), the storage used out of the quota (`navigator.storage.estimate()`, which counts IndexedDB and the service worker's caches together), and the last backup made on this device (section 13.4). The app reads them at unlock, whenever it becomes visible and after each persistence request, never by polling. While storage is not kept, the section offers to ask the browser again, on the user's click only: Firefox may prompt, and a request repeated at every unlock would repeat the prompt, so the automatic request stays at creation, pairing and restore (section 7). Three storage warnings exist, none dismissible, each gone once its condition ends: **Safari in a plain tab** (D55, D160), which recommends installing the app and says the installed app does not take this vault along (D103); **storage not kept on the only device** (D119), which recommends a backup and pairing; **storage nearly full**, at 80% of the quota or more, a chosen value, not a measured one. The vault shows only the first that applies, in that order, below its header in every layout, and Settings lists every one that applies. The Safari warning also shows on every screen before unlock, since it does not depend on the vault; the other two need the decrypted Device records or the vault open. Safari is detected by its user agent (`Safari/` without `Chrome`, `Chromium`, `Edg`, `CriOS`, `FxiOS` or `Firefox`), which D55 needs and section 13.1 forbids only for choosing the layout; a plain tab is one where `display-mode: standalone` does not match and `navigator.standalone` is not `true`. The synchronization state of section 13.2 waits for pairing (Phase 5). | +| D196 | State UI | Settings opens with a "This device" section: whether the browser keeps this origin's storage (`navigator.storage.persisted()`: kept, may be cleared, or unknown), the storage used out of the quota (`navigator.storage.estimate()`, which counts IndexedDB and the service worker's caches together), and the last backup made on this device (section 13.4). The app reads them at unlock, whenever it becomes visible and after each persistence request, never by polling. While storage is not kept, the section offers to ask the browser again, on the user's click only: Firefox may prompt, and a request repeated at every unlock would repeat the prompt, so the automatic request stays at creation, pairing and restore (section 7). Three storage warnings exist, none dismissible, each gone once its condition ends: **Safari in a plain tab** (D55, D160), which recommends installing the app and says the installed app does not take this vault along (D103); **storage not kept on the only device** (D119), which recommends a backup and pairing; **storage nearly full**, at 80% of the quota or more, a chosen value, not a measured one. The vault shows only the first that applies, in that order, below its header in every layout, and Settings lists every one that applies. The Safari warning also shows on every screen before unlock, since it does not depend on the vault; the other two need the decrypted Device records or the vault open. Safari is detected by its user agent (`Safari/` without `Chrome`, `Chromium`, `Edg`, `CriOS`, `FxiOS` or `Firefox`), which D55 needs and section 13.1 forbids only for choosing the layout; a plain tab is one where `display-mode: standalone` does not match and `navigator.standalone` is not `true`. The synchronization state of section 13.2 waits for pairing (Phase 5). *Amended by D254.* | | D197 | Layout polish and input adaptation | **State across layouts.** The three shells place the same panes (folder tree, item list, item detail) at the same places of one component tree and differ only by style, so changing width remounts nothing: an item draft, a revealed secret, the selection, the search and the open list survive every resize, as does a pane hidden by Settings. A secret stays revealed across a resize, since revealing it was the user's explicit action on the same item; lock and auto-lock clear everything as before. **Tablet drawer.** The folder tree opens over the item list, with a backdrop; choosing a list, opening an item, Escape or a click on the backdrop closes it. Opening it moves the focus to its first entry, and closing it returns the focus to its button. **System back.** While the navigation is deeper than the vault root, the history holds exactly one entry the app pushed, with no state (`null`) and the same URL; the browser's or the system's back takes the app one screen back, and pushes the entry again while the navigation is still deeper. Returning to the root from the app removes the entry by going back in the history, so the next system back leaves the app as before. The entry carries nothing of the vault, so D167 still holds. **Keyboard shortcuts**, in every layout, since a keyboard is an input, not a layout: Ctrl/⌘ + K focuses search, Ctrl/⌘ + Shift + L locks, and **N** alone starts a new item, only when the focus is not in a text field, the screen is a list or an item view (never the editor or Settings, which would lose a draft), the list is not the trash and the vault is writable; Ctrl/⌘ + N is not usable, since browsers keep it. The controls declare them in `aria-keyshortcuts` and their tooltips. Escape only closes the drawer and discards nothing. **Touch targets.** Every control, including the label row of each checkbox and radio button, is at least 44 px tall on a coarse pointer; hover styles apply only where hover exists (Tailwind's `hover:` variant is already limited to `(hover: hover)`), and nothing is reachable by hover alone. **Layout tests** are structural, not pixel screenshots, whose baselines change with each operating system and browser: at 390, 800 and 1280 px, no horizontal overflow, the expected panes visible, the lock state visible and secrets masked until revealed, and a flow resized across both breakpoints that keeps a draft, a revealed secret, the selection and the search. The 44 px check runs in Chromium only, the one test browser that emulates a coarse pointer. | | D198 | Onboarding | **Three starts.** The first screen states that BlindVault has no account and no server, that nobody, BlindVault included, can reset the local password or recover the vault, and that the Recovery Phrase and a backup are the only way back, then offers the three starts of section 13.3. Until pairing exists (Phase 4), the pair start opens a screen that says so and offers the path that works today: export a backup on the other device and restore it here; Phase 4 replaces that screen with the pairing flow. **Safari (D55, D103).** In a plain Safari tab with no vault, the first screen opens with the advice to install the app first, why (the installed app does not share this tab's storage, so a vault made here stays in this tab) and how (Share → Add to Home Screen on iPhone and iPad, File → Add to Dock on a Mac); "Continue in this tab" shows the starts. The choice is not stored: `localStorage` holds only UI preferences, so a reload shows the advice again. With no vault, the persistent Safari warning says that what is created here may be deleted and recommends starting in the installed app, since there is no vault yet to move; while the app is still starting, before it knows whether a vault exists, that warning waits. **Vault ready.** After a vault is created or restored (fresh or replacing the locked vault), the vault shows a "Vault ready" screen in place of its panes, once, with the recommendations of section 13.3 in order, each with its action: after creation, passkey unlock, a backup and pairing another device; after a restore, passkey unlock, Check Recovery Phrase (only when the backup opened with an export password, D43), re-admission while other non-retired devices exist or else pairing another device (D186, D119), and a backup. Passkey unlock is offered only where the browser supports it, and a backup only where the vault can export (D179). An action opens Settings at that section; going back returns to the screen, and "Open the vault" closes it. It replaces the one-time banners of D43 and D186 after a restore, which count as shown when it closes; the D43 banner after a backup merge and the Settings notices stay. The screen lives in the controller's memory only, and a lock clears it. **Passkey setup** is a recommendation on that screen rather than a step of the flow: setting it up needs the local password (D172), and keeping the password just typed in memory, or starting WebAuthn after the creation's KDF, when the click's user activation may have expired, would be worse. Until pairing exists, the pairing recommendation says that a backup restored on another device takes the vault there. Amends D55, D103 and D186. *Amended by D203, D234.* | | D199 | Pairing protocol | `docs/pairing-protocol.md` specifies pairing and re-admission. **Roles:** the initiator shows the first payload, makes the WebRTC offer and initiates `Noise_XX`, whatever the flow; in pairing the UI has the device holding the vault show first, so the joining device scans and sends a raw IP host candidate (D161). **Commitment in every pairing:** the initiator's payload carries `BLAKE2b-256` of a 32-byte nonce, in QR and copy/paste modes alike, so no attacker can strip it by claiming the other mode; after the handshake the responder sends its nonce, then the initiator reveals its own. **SAS:** `BLAKE2b-256(layout("bv.sas.v1", handshake hash, nonce_I, nonce_R))`, its first 8 bytes big-endian mod 10⁶, six digits; a man in the middle has one guess in 10⁶ per pairing. **Payload:** binary, version 1, with role, DTLS setup and `holdsVault` flags, a 16-byte session ID the responder repeats, the DTLS fingerprint, ICE credentials, 1 to 8 UDP candidates, the supported format versions and record types, and the commitment; decoded strictly. **Messages:** empty handshake payloads; afterwards deterministic CBOR messages `{v, t, …}` in frames of one Noise transport message each (`nonce`, `confirmed`, `master-key`, `received`, `key-confirmation`, `devices`, the full transfer, `committed`, `abort` with a closed list of reasons); both sides send `confirmed`. **Full transfer:** `sync-protocol.md` §2, every record once, with no summaries; the joining device of a pairing adds everything as a fresh restore does and aborts on any failure, re-admission merges by section 1 and rejects failing records one by one. **Timeouts:** a session expires 10 minutes after its payload is generated unless both confirmations happened, and 60 seconds without a frame after confirmation aborts; a payload serves one connection. **Versions:** payload `version`, prologue label `bv.pairing.v1`, message `v`. Amends D1, D4 and D148. *Amended by D201, D207.* | @@ -1005,12 +1005,13 @@ This document is **normative**. | D242 | Import of hidden fields, invalid values and documents | **Hidden.** A source field the source marks hidden (Bitwarden or Proton Pass hidden, 1Password concealed) becomes a `secret` field whatever its key: the source masked it, and showing it unmasked would expose what the user hid. **Invalid values.** A source value that fails the type of the role it would fill (an email, a date, a `YYYY-MM` card expiry, a seed phrase outside the BIP39 English list, a multi-line value under a single-line type) loses the role and is kept as a field under its source label: `secret` when the role's type is `secret`, `seed-phrase` or `totp` or its key looks secret, otherwise `text`, or `long-text` when it spans lines. The import drops no value for failing a type, and an item never fails to write for it. **Limits.** A blank title becomes `Untitled`; a title or label past its limit (`record-content.md` §1) is cut to it; tags past 64 code points or beyond the 32nd are named in the notes as `[More tags: …]`. **Documents.** A source category for a passport, a driver's license, a national identity card or a social security number matches the Document template, its kind in `documentType`, instead of becoming a Secure Note; any other category with no matching template still does. Amends D124 | | D243 | Threat-model review against the implementation | **Checked.** Every row of `threat-model.md` (A1 to A7 and the non-adversarial risks) was checked against the specs it points to and the code: each pointer resolves, and each mitigation is implemented, among them the Argon2id range checked before deriving (64 to 256 MiB) and the D78 range of a backup, zxcvbn score 4 and 10^14 guesses (D71, D134), the 24-hour future-clock bound (D76), the SAS commitment (D199), the CSP with Trusted Types in the `` tag and with `frame-ancestors 'none'` in the nginx and Caddy configs, the frame check (D193), `noopener noreferrer` and `no-referrer` (D147), `http:`/`https:` URLs only (D83), the changed service worker (D142), the export that opens the Master Key wrap again (D181) and the storage warnings (D196). **Added.** Three threats section 3 did not list: a hostile import file and a plaintext export file left on disk, which arrived with import (D123, D126, D130, D242), and a compromised dependency, which reproducible builds and published hashes cannot detect, since they prove only that the bundle matches the source. **Normative.** The rules `architecture.md` already applied become MUST in section 16: exact versions, a 14-day minimum release age and no install scripts. **Unchanged.** No mitigation was missing or weaker than specified, and no status changed | | D244 | Essential UX, after BlindPass | **Why.** A hands-on test of v0.6.0 found the UI too plain. Compared with BlindPass's web app, BlindVault is ahead on structure (three layouts, accessibility, search) and behind on information density and finish. Phase 7b brings the essentials as patterns rebuilt on the component set of section 13.7, not as copied code, and with no new dependency (no `sonner`, `vaul`, `framer-motion` or web font). **List row.** An avatar: the title's initial on one of six color pairs derived from the title, light and dark, added to the tokens and to the contrast test, with the template's icon; no favicon (D83, D91). A subtitle read only from non-secret fields, as search reads them (D132): username and host for a Login, the template's summary otherwise. A Copy button for the password on items that have one, through the clipboard clearing of D54 and D151. No TOTP code in the list. On a phone, items are grouped under their first letter. **Item detail.** A header with a large avatar, the title and chips for the template, host, folder and tags; fields in titled sections inside cards; an icon and visible text on every button of a secret (section 13.7); Edit visible, and Duplicate, History and Move to trash in a "…" menu; a shown seed phrase as a numbered grid. **TOTP.** A 30-second ring beside the code, amber from 10 s and red from 5 s, its seconds also in text; it steps once a second with no transition, so it is not motion under D236. **Strength.** zxcvbn on item secrets: a meter with its label and explanation in the editor, and a Weak badge on the detail; it informs and never blocks a save, unlike D71. **Feedback.** A toast region of the app's own (`role=status`) for copy, save, move and restore; on a phone a confirmation is a bottom sheet on the same native ``; an empty vault shows a first-steps checklist (first item, import, pairing); a top-level error boundary locks the vault, clearing plaintext state, before it shows its failure screen. **Navigation.** More Lucide icons, copied as in D232; the phone footer with icons and text: Items, Trash, Settings; on a phone, "+" as the header's primary action and Lock in an overflow menu; a type filter, and counts per folder and per type. **Keyboard.** ↑/↓ and j/k move through the list, ↓ moves from search into it, Esc clears the search and Ctrl/⌘+Enter saves the editor, each in `aria-keyshortcuts` and in `keyboard.spec.ts`. **Generator.** A length slider beside the numeric field. **Out.** Multi-select and bulk actions; recently viewed items (their IDs in `localStorage` would persist vault structure outside the vault's storage, D132, D167); Settings sub-screens; a density setting; a web font (D231); breach lookups (a network call derived from vault data, refused by the CSP, D83, D91); the password health page and a Reused badge; pasting an `otpauth://` URI; a slide between list and detail (D236 allows entrances only). **Rule.** CRYPTO-INV-024. **Delivery.** An HTML prototype, approved before any code; then one change per area, each with its own `Dn`; then v0.7.0 for another hands-on test. Phase 7b runs before the Security Gate of Phase 8 | -| D245 | Icons and navigation | The first area of Phase 7b (D244). **Icons.** `chevron-left`, `ellipsis`, `folder`, `list` and `trash-2`, copied from Lucide 1.61.0 as in D233. **Phone.** The bottom navigation shows Items, Trash and Settings, each an icon over its text, the current one marked `aria-current`, on the list screens as before; the folder pane leaves out its Trash entry, which the bottom navigation holds. The header's action is New item, an icon button named New item with its N shortcut, on a list other than the trash, when the vault is writable and not under "Vault ready" (D198); the list's own New item button is left out. Lock moves to a "…" menu, with its icon, visible text and Ctrl/⌘+Shift+L shortcut unchanged; the lock state stays in the header (section 13.2). Back takes a chevron beside its text. Tablet and desktop keep their header. **"…" menu.** A native popover (`popover=auto`) under its button: the browser closes it on Escape or a click outside, choosing an action closes it, and Tab moves through its buttons, a disclosure rather than an ARIA menu, so no arrow-key handling is written. The item detail's "…" menu reuses it. **Type filter.** Chips above the list, All then each template present in the list shown, in the templates' order, each with its count, `aria-pressed` on the chosen one, shown when the list holds two types or more; the filter applies to the list shown, search results and the trash included, only while its chips show, so no item is hidden by a filter out of sight; it is kept in memory across lists and falls back to All when the list shown lacks its type. An item from a newer app version is counted only in All. **Counts.** The folder pane shows an icon and a count beside All items (every item not in the trash), each folder (the items directly in it, as its list shows them) and the Trash (the items in it). **Accessibility.** Every count is `aria-hidden`: the list it describes is a `
    ` whose length a screen reader announces, and the entries keep their accessible names. | +| D245 | Icons and navigation | The first area of Phase 7b (D244). **Icons.** `chevron-left`, `ellipsis`, `folder`, `list` and `trash-2`, copied from Lucide 1.61.0 as in D233. **Phone.** The bottom navigation shows Items, Trash and Settings, each an icon over its text, the current one marked `aria-current`, on the list screens as before; the folder pane leaves out its Trash entry, which the bottom navigation holds. The header's action is New item, an icon button named New item with its N shortcut, on a list other than the trash, when the vault is writable and not under "Vault ready" (D198); the list's own New item button is left out. Lock moves to a "…" menu, with its icon, visible text and Ctrl/⌘+Shift+L shortcut unchanged; the lock state stays in the header (section 13.2). Back takes a chevron beside its text. Tablet and desktop keep their header. **"…" menu.** A native popover (`popover=auto`) under its button: the browser closes it on Escape or a click outside, choosing an action closes it, and Tab moves through its buttons, a disclosure rather than an ARIA menu, so no arrow-key handling is written. The item detail's "…" menu reuses it. **Type filter.** Chips above the list, All then each template present in the list shown, in the templates' order, each with its count, `aria-pressed` on the chosen one, shown when the list holds two types or more; the filter applies to the list shown, search results and the trash included, only while its chips show, so no item is hidden by a filter out of sight; it is kept in memory across lists and falls back to All when the list shown lacks its type. An item from a newer app version is counted only in All. **Counts.** The folder pane shows an icon and a count beside All items (every item not in the trash), each folder (the items directly in it, as its list shows them) and the Trash (the items in it). **Accessibility.** Every count is `aria-hidden`: the list it describes is a `
      ` whose length a screen reader announces, and the entries keep their accessible names. *Amended by D254.* | | D246 | List row | The second area of Phase 7b (D244). **Avatar.** The title's first grapheme, uppercased, on one of six color pairs chosen by a hash of the title; the pairs are `--bv-avatar-1` to `-6` with their `-ink`, light and dark, in the contrast test at 4.5:1; the template's icon (`key-round`, `sticky-note`, `credit-card`, `id-card`, `code-xml`, `wallet`, `file-text`, copied from Lucide 1.61.0 as in D233) sits in a corner, none for an unknown template; the avatar is decorative (`aria-hidden`). **Subtitle.** Two roles per template, joined by " · ": username and the URL's host for a Login, cardholder and expiry for a card, email and phone for an identity, provider and the base URL's host for a developer credential, wallet name and network for a crypto wallet, document type and issuer for a document; empty roles are skipped, and a field whose type search never reads (`secret`, `totp`, `seed-phrase`, D132) is skipped whatever role it carries; with nothing to show, the template's name. The row's accessible name stays the title, and the subtitle is its description. **Copy.** A Login with a non-empty password shows Copy beside its row, named "Copy password: ", through the clipboard clearing of D54 and D151 and its status line; the password is held by the button's handler and never rendered. **Phone.** Items fall under the first letter of their title, marks dropped, "#" for anything not a letter; the letters are seen only (`aria-hidden`), so the list announces its items alone. **Rule.** CRYPTO-INV-024, tested on the row's markup for every template with every field type. | -| D247 | Item detail | The third area of Phase 7b (D244). **Header.** The avatar of D246 at a larger size, the title, and chips in a list: the template with its icon, the host of the first non-empty URL field, the folder and each tag; the folder's and a tag's icon carry their name ("Folder", "Tags"), since the text alone does not say what the chip is. **Cards.** The fields whose role the template defines, in a card titled with the template's name; the user's own fields in "Other fields"; the notes in "Notes"; each card a `<section>` named by its heading. Folder and tags leave the field list for the chips. **Buttons.** Show and Hide carry `eye` and `eye-off`, Copy carries `copy` (the check of D231 in its place after a copy), Edit carries `pencil`; their visible text is unchanged (section 13.7). **Actions.** Edit stays visible, Restore in the trash; Duplicate (`copy-plus`), History (`history`) and Move to trash (`trash-2`) are under "More actions", the "…" popover of D245; the actions show under the same conditions as before. **Seed phrase.** Once shown, a numbered grid of its words, an ordered list of two columns, three from the small width; Copy copies the stored text. **Icons.** `copy-plus`, `eye`, `eye-off`, `globe`, `history`, `pencil` and `tag`, copied from Lucide 1.61.0 as in D233. | +| D247 | Item detail | The third area of Phase 7b (D244). **Header.** The avatar of D246 at a larger size, the title, and chips in a list: the template with its icon, the host of the first non-empty URL field, the folder and each tag; the folder's and a tag's icon carry their name ("Folder", "Tags"), since the text alone does not say what the chip is. **Cards.** The fields whose role the template defines, in a card titled with the template's name; the user's own fields in "Other fields"; the notes in "Notes"; each card a `<section>` named by its heading. Folder and tags leave the field list for the chips. **Buttons.** Show and Hide carry `eye` and `eye-off`, Copy carries `copy` (the check of D231 in its place after a copy), Edit carries `pencil`; their visible text is unchanged (section 13.7). **Actions.** Edit stays visible, Restore in the trash; Duplicate (`copy-plus`), History (`history`) and Move to trash (`trash-2`) are under "More actions", the "…" popover of D245; the actions show under the same conditions as before. **Seed phrase.** Once shown, a numbered grid of its words, an ordered list of two columns, three from the small width; Copy copies the stored text. **Icons.** `copy-plus`, `eye`, `eye-off`, `globe`, `history`, `pencil` and `tag`, copied from Lucide 1.61.0 as in D233. *Amended by D254.* | | D248 | TOTP ring | The fourth area of Phase 7b (D244). Beside a TOTP code, a ring shows the seconds left in its period: the share of the period left, in the accent color, amber (`warning`) from 10 s and red (`danger`) from 5 s, on a `line` track; each color is in the contrast test at 3:1 as a graphic. It steps once a second, when the code's clock does, with no transition or animation, so it is not motion under D236 and stays the same under `prefers-reduced-motion`. It is decorative (`aria-hidden`): the seconds stay in text beside the code ("Changes in N s"), so color is never the only signal. | | D249 | Item password strength | The fifth area of Phase 7b (D244). **What is rated.** A template's `password` field of type `secret`, the one secret whose use is known; a PIN, a card's security code, a TOTP secret, a seed phrase and the user's own secret fields are not rated, since a score for them would mislead. **How.** zxcvbn with the dictionaries of D71, already loaded before the first unlock (D191), run in the page on the value in memory; nothing is stored or sent. Scores 0 to 2 are Weak, 3 Fair, 4 Strong. **Editor.** Under the password, a meter of four bars, seen only (`aria-hidden`), and the field's hint, its accessible description: "Strength: <level>.", then zxcvbn's warning or a fixed advice below Strong. **Detail.** A Weak badge beside the password's name, only when weak; nothing for Fair or Strong. **Never blocks.** Unlike the local and export passwords (D71, D134), a weak item password is saved like any other: the site sets its rules, and the user may hold a password they cannot change. **Exposure.** The badge says a password is weak to someone who sees the screen, as the masked value does not; the detail is already shown only unlocked (section 13.2), and the list row shows no rating (CRYPTO-INV-024). | | D250 | Feedback | The sixth area of Phase 7b (D244). **Toast.** One region of the app's own, a `role=status` mounted with the vault shell, so each toast is announced and none outlives a lock; at the bottom, above the phone's navigation, never taking the pointer. A new toast replaces the last, and each goes after 5 s. It says that an item was saved, moved to the trash or restored, and what a copy said beside its button before: copied, with the clearing that is not guaranteed (D54, D127), or that the browser refused; the check of D231 stays on the button. A toast holds no secret. Its entrance is motion under D236, none under `prefers-reduced-motion`. **Bottom sheet.** On a phone the confirmation of D231 is a bottom sheet: the same native modal `<dialog>`, with its focus, Escape and inert page, placed at the bottom at full width, its buttons full width and 44 px tall, the action first; wider, the centered dialog as before. No drag handle: nothing is dragged. No confirmation is added: moving an item to the trash stays one action, since the trash restores it. **First steps.** All items, with no item outside the trash and no search, shows first steps in place of "No items yet.": the vault made, marked done; add an item, opening the template picker; import, opening Settings at the import; pair a device, opening the pairing as "Vault ready" does (D198). A vault that cannot be written leaves out the item and the import. **Error boundary.** Around every screen, a render error locks the vault (the session's keys and plaintext, as on any lock) and clears the clipboard (D54) in the commit, before the browser paints its failure screen; the screens that held plaintext are already unmounted. The screen shows nothing of the vault: what happened, that what was saved stays on the device, and Reload. Errors outside a render (an event handler, a promise) are handled where they occur, as before. | | D251 | Keyboard and length slider | The last area of Phase 7b (D244); keys, like D197, work in every layout. **List.** With the focus on an item of the list, ↓ or J moves it to the next item and ↑ or K to the one before, with no modifier; past the last item it stays, and above the first it goes to the search. They move the focus only: Enter opens the item, so a key never shows an item the user did not choose. A row's Copy is reached with Tab, as before. **Search.** ↓ moves the focus to the first item shown; Esc clears a non-empty search, the same in every browser (Firefox leaves `type=search` as typed), and keeps the focus in it; an empty search ignores Esc. The search's hint says both. **Editor.** Ctrl/⌘ + Enter submits the editor from any of its fields, the notes included, where Enter alone is a new line; never while the discard dialog is open or a save runs. The save goes through the same validation as Save. **Declared.** `aria-keyshortcuts` names the keys that activate or focus a control: Save carries Ctrl/⌘ + Enter and the search keeps Ctrl/⌘ + K. The movement keys are not declared on each row, where a screen reader would read them on every item; the search's hint says them instead. **Generator.** A range slider beside the length field, from 12 to 128 (D128), named as the field; either sets the same length, and the field keeps typed values the slider cannot show. | -| D252 | Settings pages | A Phase 7b area (D244), after the v0.7.0 hands-on test found Settings one long page of every section. **Index.** Settings opens on an index of rows in groups: Preferences (Appearance, Language, Locking), Security (Local password, Passkey unlock, Recovery Phrase), Devices (Devices), Data (Backup, Merge a backup, Import, This device), Danger zone (Re-found the vault, Erase from this device, in the danger color) and, untitled and last, About. Each row has its icon, copied from Lucide like the others, and a chevron; six rows show their current value where it helps: the theme, the language, the auto-lock minutes, passkey unlock on or off, the date of the last backup made on this device or Never, and the count of listed devices. **Pages.** A row opens its page over the index, with the page's name as its heading and only that section; Back, the header's or the system's, returns to the index, and from there to where Settings was opened. Every layout drills down the same way, the page filling the frame as Settings did. A page opened from a row puts the cursor on its heading. **Shortcuts.** What opened Settings at a section (the backup reminder, "Vault ready", First steps, the clock notice, a live sync) now opens that section's page over the index, with the cursor where it went before; "Make a backup now" after a merge, a regeneration or before an erase opens the Backup page. No setting, check, warning or step changes. Amends D192 and D196. | -| D253 | Wizards | A Phase 7b area (D244), after the v0.7.0 hands-on test found the pairing and sync screens unclear about where the user stands and what the other device expects. **Wizard.** A flow of several steps shows one step at a time under "Step N of M · name" and a bar of M segments, then the step's content, then, in a fixed box headed "On the other device", what to do there meanwhile, then Cancel while there is something to cancel. There is no Back between steps: a failure says why and offers "Start again", which returns to step 1. Waiting states (preparing, connecting, sending, merging) are not steps: each shows within the step it belongs to. **Device flows.** Pair, on the device that holds the vault: Confirm it is you, Exchange the codes, Compare the numbers, Done. Join, on the device that gets it: Create the local password, Read the other device's code, Show the answer, Compare the numbers, Done. Re-admission: Choose who shows first, Exchange the codes, Compare the numbers, Done. Sync: Choose who shows first, Exchange the codes, Syncing, Done. The choice of who shows first stays manual on both devices (D207, D216), and its step says that the other device chooses the other option. "Same network only" and the STUN list move into "Advanced options", closed on step 1 unless the stored list cannot be read, which forces the option (D209). Leaving the page still ends the flow, except a live sync (D225). **Other flows.** The same wizard runs create (Phrase, Confirm, Password), restore (File, Secret, Password), reset the password (Phrase, New password), regenerate the phrase (Warning, Phrase, Confirm, Authorize, Done), re-found (Warning, Authorize, Phrase, Confirm, New password), merge a backup (File, Secret, Confirm, Done) and import (File, the file's password when it has one, Preview, Done, the count set once the file is read). The start's choice between create, join and restore stays a menu, and Cancel returns to it. A destructive flow's warning is its step 1, with no summary added before it writes. No step, check, warning or confirmation is removed or reordered. Amends D203, D207, D216 and D234. | +| D252 | Settings pages | A Phase 7b area (D244), after the v0.7.0 hands-on test found Settings one long page of every section. **Index.** Settings opens on an index of rows in groups: Preferences (Appearance, Language, Locking), Security (Local password, Passkey unlock, Recovery Phrase), Devices (Devices), Data (Backup, Merge a backup, Import, This device), Danger zone (Re-found the vault, Erase from this device, in the danger color) and, untitled and last, About. Each row has its icon, copied from Lucide like the others, and a chevron; six rows show their current value where it helps: the theme, the language, the auto-lock minutes, passkey unlock on or off, the date of the last backup made on this device or Never, and the count of listed devices. **Pages.** A row opens its page over the index, with the page's name as its heading and only that section; Back, the header's or the system's, returns to the index, and from there to where Settings was opened. Every layout drills down the same way, the page filling the frame as Settings did. A page opened from a row puts the cursor on its heading. **Shortcuts.** What opened Settings at a section (the backup reminder, "Vault ready", First steps, the clock notice, a live sync) now opens that section's page over the index, with the cursor where it went before; "Make a backup now" after a merge, a regeneration or before an erase opens the Backup page. No setting, check, warning or step changes. Amends D192 and D196. *Amended by D254.* | +| D253 | Wizards | A Phase 7b area (D244), after the v0.7.0 hands-on test found the pairing and sync screens unclear about where the user stands and what the other device expects. **Wizard.** A flow of several steps shows one step at a time under "Step N of M · name" and a bar of M segments, then the step's content, then, in a fixed box headed "On the other device", what to do there meanwhile, then Cancel while there is something to cancel. There is no Back between steps: a failure says why and offers "Start again", which returns to step 1. Waiting states (preparing, connecting, sending, merging) are not steps: each shows within the step it belongs to. **Device flows.** Pair, on the device that holds the vault: Confirm it is you, Exchange the codes, Compare the numbers, Done. Join, on the device that gets it: Create the local password, Read the other device's code, Show the answer, Compare the numbers, Done. Re-admission: Choose who shows first, Exchange the codes, Compare the numbers, Done. Sync: Choose who shows first, Exchange the codes, Syncing, Done. The choice of who shows first stays manual on both devices (D207, D216), and its step says that the other device chooses the other option. "Same network only" and the STUN list move into "Advanced options", closed on step 1 unless the stored list cannot be read, which forces the option (D209). Leaving the page still ends the flow, except a live sync (D225). **Other flows.** The same wizard runs create (Phrase, Confirm, Password), restore (File, Secret, Password), reset the password (Phrase, New password), regenerate the phrase (Warning, Phrase, Confirm, Authorize, Done), re-found (Warning, Authorize, Phrase, Confirm, New password), merge a backup (File, Secret, Confirm, Done) and import (File, the file's password when it has one, Preview, Done, the count set once the file is read). The start's choice between create, join and restore stays a menu, and Cancel returns to it. A destructive flow's warning is its step 1, with no summary added before it writes. No step, check, warning or confirmation is removed or reordered. Amends D203, D207, D216 and D234. *Amended by D254.* | +| D254 | Layout and presentation after the v0.7.1 screens | A Phase 7b area (D244), after a capture of every v0.7.1 screen at the three widths in both themes and an HTML prototype the owner approved part by part. Presentation only: no step, check, warning, confirmation or security rule is removed, and every warning keeps its text. **Notices.** The preview banner becomes a "Preview" badge beside the app's name in the header, in the warning color, which opens the banner's text. The storage warning of D196 stays below the header, not dismissible, as one line with its icon, and "Details" shows its whole text. The backup reminder of D188 moves from the frame to the top of the item list, with its two actions unchanged. **Header.** The lock state stays visible (section 13.2) as a neutral badge with an open padlock beside Lock, no longer amber. Lock becomes a secondary button. The start screens show the app's name once, with no lock badge before a vault exists. **Phone list.** The folder pane leaves the list screen for a bottom sheet (D250), opened by an "All folders" pill. The type filter of D245 becomes an "All types" pill on every layout, opening a bottom sheet on a phone and a popover elsewhere, with the same counts and the same rule that no item is hidden by a filter out of sight. The toast sits above the phone's bottom navigation. **Item detail.** D247's anatomy stays, under a bar that holds Edit as the primary button and the "…" menu. Field rows are divided inside their card. Show and Copy on a secret keep their icon and visible text (section 13.7); Copy and Open on other fields become icon buttons with an accessible name and a tooltip. A History link sits at the detail's foot, beside the menu's entry. **Editor.** Cancel and Save sit in a bar fixed at the bottom of the pane. The generator (section 13.6) opens from a button in the password field, as a modal dialog on tablet and desktop and a bottom sheet on a phone, with the same settings, entropy display and "Use this password". "Add a field" stays. **Show password.** Every local password, export password and import passphrase field holds a toggle, an icon button named "Show password" or "Hide password" with `aria-pressed`. The field is masked when it appears and masked again on submit (a failed one included), when the screen changes and on lock, with no timer; autocomplete stays as D174 sets it. **Settings.** On tablet and desktop, Settings opens inside the vault frame: All items and Trash, then D252's index in the left column, and the open page in the rest. The phone drills down as before. **Wizards.** A flow of D253 opens on a screen of its own, about 35rem wide at most, never below a page's other content. Settings › Devices lists the devices and offers "Pair a device" and "Re-admit a device" as two cards, each opening its wizard. Below the bar, the wizard adds "Next:" and the next step's name, except on the last step. Cancel moves to the header on a phone and on the start screens, and beside the step's main action elsewhere. On a phone the main action is fixed at the bottom. A file step shows a drop area over the same file input, and a Continue button, enabled once a file is chosen, moves on. **Type and spacing.** Within D231: screen titles 22 px semibold with slight negative tracking, section labels 11 px in uppercase, field values 15 px, and spacing in multiples of 4 px. No token value changes. Amends D177, D188, D196, D245, D247, D252, D253 and sections 13.6 and 13.7. | diff --git a/docs/prototypes/phase-7b-layout.html b/docs/prototypes/phase-7b-layout.html new file mode 100644 index 0000000..f94cf09 --- /dev/null +++ b/docs/prototypes/phase-7b-layout.html @@ -0,0 +1,2788 @@ +<!doctype html> +<html lang="pt-BR"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>BlindVault UX prototype + + + +
      +
      +

      BlindVault · protótipo da D254

      +
      + Layout e apresentação após a v0.7.1, aprovado parte por parte antes do código. Tokens de + apps/web/src/index.css (D231). O "antes" é a v0.7.1: recapture a partir da + tag. +
      +
      +
      + +
      +
      + +
      +
      +
      + + + + diff --git a/docs/roadmap.md b/docs/roadmap.md index aa33f3a..d134e9b 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -432,6 +432,15 @@ and the password generator. Then `docs/record-content.md` vectors, `packages/vau - [x] A wizard for pairing, joining, re-admission and sync: one step at a time, what to do on the other device (D253) - [x] The same wizard on the other step flows: create, restore, reset the password, regenerate the phrase, re-found, merge, import (D253) - [x] Release v0.7.1 for another hands-on test +- [x] Capture every v0.7.1 screen and approve an HTML prototype of the layout and presentation changes (`docs/prototypes/phase-7b-layout.html`, D254) +- [ ] Notices: the Preview badge, the storage warning on one line with Details, the backup reminder at the top of the list, a neutral lock badge (D254) +- [ ] Lists: folders and types as pills opening a bottom sheet on a phone, the toast above the bottom navigation (D254) +- [ ] Item detail and editor: the header bar, divided field rows, icon buttons on non-secret fields, the fixed Cancel and Save bar, the generator in a dialog or bottom sheet (D254) +- [ ] Show and Hide on every password field, masked again on submit, on a screen change and on lock (D254) +- [ ] Settings inside the vault frame on tablet and desktop; Devices with the Pair and Re-admit cards (D254) +- [ ] Wizards on a screen of their own: "Next:", Cancel's place, the fixed action on a phone, the file drop area with Continue (D254) +- [ ] Type scale and spacing within D231 (D254) +- [ ] Release v0.7.2 for another hands-on test ### Essential UX Gate @@ -441,7 +450,7 @@ and the password generator. Then `docs/record-content.md` vectors, `packages/vau - [ ] Copying from the row clears the clipboard as copying from the detail does (D54, D151) - [ ] The error boundary locks the vault and clears plaintext state before its failure screen - [ ] Audit of every new screen in a real browser at the three widths and in both themes, as in D239 -- [ ] The owner approves v0.7.1 after a hands-on test +- [ ] The owner approves v0.7.2 after a hands-on test ---