236 lines
12 KiB
Markdown
236 lines
12 KiB
Markdown
# Popup Visual Design
|
||
|
||
Layout, color, and asset specs for the popup UI. State transitions, data, and
|
||
which screen maps to which state are defined in `claude.md`; this document
|
||
defines what each screen looks like. The popup itself is the browser's native
|
||
extension popup window — no custom window chrome is drawn; everything below
|
||
starts at the popup's content area.
|
||
|
||
## Shared chrome
|
||
|
||
Every run-phase screen (`phase_1`–`phase_4`, `delivered`) is built from the
|
||
same structure:
|
||
|
||
- **Header** — brand mark (20×20px, rounded 6px, solid accent color) + `locqr`
|
||
wordmark (bold, 14.5px) on the left; site origin (e.g. `localhost`) on the
|
||
right, secondary text color. Bottom border divider.
|
||
- **Body** — 28px top / 22px bottom / 20px horizontal padding, content
|
||
centered.
|
||
- **Countdown bar** — 4px tall, full width, rounded-full track and fill.
|
||
Bound to whichever TTL governs the current phase (`qr_ttl` in phase_2,
|
||
`pin_ttl` in phase_3). Same component throughout the run; only the value
|
||
driving it changes.
|
||
- **Caption** — 12.5px, secondary text color, centered, below the primary
|
||
content.
|
||
|
||
### Color tokens
|
||
|
||
| Token | Light | Dark |
|
||
|---|---|---|
|
||
| `--bg` (page, outside popup) | `#eef1f6` | `#0a0d13` |
|
||
| `--card-bg` | `#ffffff` | `#12161f` |
|
||
| `--card-border` | `rgba(15,23,42,.08)` | `rgba(255,255,255,.08)` |
|
||
| `--text-primary` | `#0f172a` | `#f1f5f9` |
|
||
| `--text-secondary` | `#667085` | `#94a3b8` |
|
||
| `--text-tertiary` | `#94a3b8` | `#64748b` |
|
||
| `--accent` | `#2f6fed` | `#5b8bff` |
|
||
| `--accent-track` | `#e2e8f0` | `#232a38` |
|
||
| `--box-border` (PIN digit boxes) | `#d7dce4` | `#2a3140` |
|
||
| `--box-bg` (PIN digit boxes) | `#f8fafc` | `#171c26` |
|
||
| `--divider` | `rgba(15,23,42,.07)` | `rgba(255,255,255,.08)` |
|
||
|
||
Dark mode follows the user's OS/browser theme (`prefers-color-scheme`).
|
||
|
||
## Phase 2 — QR
|
||
|
||
- Square QR image, no `border-radius` — rounding clips the finder-pattern
|
||
squares, which sit flush in each corner.
|
||
- Two colorways, selected by theme:
|
||
|
||
**Light (default):** plain black (`#1a1a1a`-ish, i.e. the QR library's
|
||
standard module color) on white. Quiet zone is baked into the raster: ~4
|
||
modules per the QR spec's minimum, measured at the reference asset's actual
|
||
module pitch (~3.14px/module → ~13px margin at this asset's resolution).
|
||
This one needs a real margin because its background is plain white, not
|
||
guaranteed to match whatever it's rendered on.
|
||
|
||
**Dark (inverted):** modules white (`#ffffff`), background set to the exact
|
||
dark `--card-bg` value (`#12161f`) rather than plain black. Cropped tight
|
||
to the module grid with **no** baked-in margin — because the raster
|
||
background is identical to the card background, the card's own padding
|
||
serves as the quiet zone, and no raster pixels are spent on margin.
|
||
Confirmed scannable on an older Android device during design review;
|
||
treat companion-app support for inverted QR decoding as a real
|
||
requirement, not a nice-to-have, since older scanners are the ones most
|
||
likely to reject reversed polarity.
|
||
|
||
- Display size 260×260px — fills the card body's content width edge to
|
||
edge (300px popup − 20px padding × 2), so the QR's outer edge lines up
|
||
with the countdown bar below it. No margin beyond that shared 20px card
|
||
padding; a smaller fixed size (190px, tried initially) left extra
|
||
whitespace around the code and made it read as unnecessarily dense for
|
||
its size.
|
||
- Caption: "Scan with the companion app."
|
||
- Countdown bound to `qr_ttl`.
|
||
|
||
## Phase 3 — PIN confirmation
|
||
|
||
- 6-digit code, grouped 3+3 (wider gap between groups than within a group —
|
||
reads as one code, not two numbers).
|
||
- Each digit in its own box: 42×50px, 1.5px border, 10px corner radius,
|
||
monospace, 21px / weight 650.
|
||
- **Display-only.** The user does not type into these boxes — they compare
|
||
the shown code against the companion app's independently-derived value and
|
||
tap Confirm. (Confirmation semantics — what mismatch means, what the
|
||
underlying value is derived from — are specified in `../crypto.md` and
|
||
`claude.md`; this document only covers presentation.)
|
||
- Countdown bound to `pin_ttl`, same bar component as phase 2.
|
||
- Caption: "Confirm this code matches your companion app."
|
||
- Actions: **Confirm** (primary, filled, full width) above **Abort** (ghost /
|
||
text button, subdued). Confirm is the expected action and gets the visual
|
||
weight; Abort stays available without competing for attention.
|
||
|
||
## Toolbar icon set
|
||
|
||
Seven states, all built on the same rounded-square badge used for the header
|
||
brand mark (10px corner radius at 36px toolbar size) — the icon system reads
|
||
as one family rather than a separate alert-icon library bolted on. Severity
|
||
is never carried by color alone: color and glyph change together, so the
|
||
distinction holds under color blindness and under a glance too quick to read
|
||
text.
|
||
|
||
| State | Fill | Glyph |
|
||
|---|---|---|
|
||
| `neutral` (unverified) | `--neutral-badge` (`#94a3b8` / `#475569`) | small dot |
|
||
| `inactive` (not registered) | `--inactive-badge` (`#cbd5e1` / `#334155`) | dash |
|
||
| `ready` (idle) | `--accent` | checkmark |
|
||
| `animated` (phase_1–4) | `--accent` | rotating dashed ring |
|
||
| `success` (delivered) | `--green` (`#16a34a` / `#22c55e`) | bold checkmark |
|
||
| `error` (operational, retry-able) | `--amber` (`#f59e0b` / `#fbbf24`) | "!" |
|
||
| `security warning` (blocked, reported) | `--red` (`#dc2626` / `#ef4444`) | shield with "!" |
|
||
|
||
Color pairing is deliberate, not decorative: amber tracks every row in the
|
||
`claude.md` state table that offers Retry or needs no action; red tracks
|
||
every row that is Dismiss-only and already reported to the backend. The
|
||
shield glyph specifically for security warnings borrows the vocabulary
|
||
browsers already use for HTTPS/security indicators, so it doesn't depend on
|
||
the user reading anything.
|
||
|
||
## Error and security-warning popups
|
||
|
||
Three body variants, all using the shared chrome (header + card), differing
|
||
in the `card-body` content:
|
||
|
||
**Generic / operational error** — 48px amber badge (tinted background, not
|
||
solid), heading in normal text color, reason text, then full actions: primary
|
||
filled **Retry** above a plain-text **Dismiss**. Covers `cert_invalid`
|
||
(`insecure_origin`, operational reasons), `account_error`, and `run_error`
|
||
(timeout/user, network) — the icon is identical across these; only the body
|
||
copy and whether Retry is offered differ per `claude.md`'s table.
|
||
|
||
**Security warning — ambient** — 48px red badge (shield glyph), heading text
|
||
itself reads "Security warning" (explicit label, not just the reason —
|
||
required by `claude.md`: "user sees an explicit security warning, not a
|
||
generic error"), reason text, **no action buttons**. Used for `cert_invalid`
|
||
security-class reasons (`domain_mismatch`, `signature_invalid`, `malformed`),
|
||
which are ambient page state rather than part of an active run — nothing to
|
||
retry or dismiss; it persists until the cert or page changes.
|
||
|
||
**Security warning — mid-run** — same red badge and "Security warning"
|
||
heading, but with a single **Dismiss** button. Built for a `run_error`
|
||
security-class reason that interrupts something the user started, the same
|
||
way `cert_invalid`'s security variant interrupts before one starts. No
|
||
concrete extension-side reason uses this template today — `claude.md`'s
|
||
`run_error` note explains why `alpha_hash_mismatch`/`pin_mismatch` (this
|
||
template's original motivating examples) turned out to be companion-only
|
||
detections; `bundle_consumed` is the one real candidate, if the server is
|
||
ever extended to signal it. The template stays ready either way: the popup
|
||
code renders any `errorClass: 'security'` reason through it generically.
|
||
Dismiss uses the outlined/ghost button style, never the filled accent
|
||
button — nothing about a blocked state should visually read as "the
|
||
expected path," the way Confirm does in phase 3.
|
||
|
||
**Placeholder copy — needs a plain-language pass.** The reason strings used
|
||
while designing this ("relay timed out," "run has been stopped and
|
||
reported") are internal/protocol vocabulary — run, relay, bundle, etc. — and
|
||
were only good enough to validate the visual treatment. The end user has no
|
||
reason to know what a "run" or a "relay" is. Before this ships, every
|
||
user-facing string in this section needs to be rewritten in plain terms (what
|
||
happened, what it means for them, what to do), independent of the visual
|
||
design above, which is settled.
|
||
|
||
## Status screens — idle, phase_1, phase_4, delivered
|
||
|
||
Same chrome, no countdown bar (none of these four are TTL-governed). A 48px
|
||
badge + heading + short body, centered, shared across all four; only the
|
||
badge color/glyph, heading, body, and action differ.
|
||
|
||
- **`idle`** — two variants depending on the site's advisory
|
||
`sessionActive` hint (`claude.md`, `interfaces.md`'s `session_status`
|
||
message); same badge treatment either way, copy and action differ:
|
||
- No session reported (default): accent-blue tinted badge, checkmark
|
||
glyph (matches the `ready` toolbar icon). Heading "Ready to sign in,"
|
||
body naming the site (`"{origin} supports secure sign-in with
|
||
LOCQR."`). One primary action: **Sign in**.
|
||
- Session reported active: green tinted badge, checkmark glyph (same
|
||
glyph, badge color shifts to match `delivered`'s green — reads as "a
|
||
settled, good state," not an action item). Heading "Signed in," body
|
||
(`"You're already signed in to {origin}."`). One primary action:
|
||
**Sign in again** — same underlying action as plain `idle`'s Sign in
|
||
(starts a new run), just worded for a user who's already
|
||
authenticated. No extra confirmation before it: starting a run
|
||
doesn't affect the existing session at all, and the run's own
|
||
multi-step confirmation (QR scan, PIN compare) is already real friction
|
||
against an accidental click.
|
||
- **`phase_1`** and **`phase_4`** share one template: accent-blue tinted
|
||
badge containing the rotating-ring spinner (real CSS rotation, not a
|
||
static frame; honors `prefers-reduced-motion` by holding still instead of
|
||
spinning), heading, short body, single **Cancel** action (plain-text
|
||
button, not filled). Only copy differs between the two:
|
||
- `phase_1`: "Connecting…" / "Setting up a secure connection to your
|
||
phone."
|
||
- `phase_4`: "Almost done…" / "Waiting for your phone to finish sending
|
||
your sign-in details."
|
||
- **`delivered`** — green tinted badge, bold checkmark (matches the
|
||
`success` toolbar icon). Heading "Signed in," body confirming the site
|
||
(`"You're securely signed in to {origin}."`), no action buttons, small
|
||
"Closing automatically…" note instead — matches `claude.md`'s "brief
|
||
display then auto-transition to `idle`."
|
||
|
||
## Ambient screens — unverified, not_registered
|
||
|
||
Two low-key states, neither an error nor part of a run, and deliberately
|
||
distinct from each other: `unverified` is transient (resolves quickly on any
|
||
page that does have LOCQR); `not_registered` is a settled fact about the
|
||
page. Neither uses the run-phase spinner — that motion means "a multi-step
|
||
exchange is actively in progress," and reusing it here would blur that
|
||
meaning. Neither has an action button, matching `claude.md`'s table (both
|
||
rows currently show `—`); `not_registered`'s table row notes a possible
|
||
future "register site" action, but no button is speculated here since that
|
||
capability is undecided, not just uncopywritten.
|
||
|
||
- **`unverified`** — neutral-gray tinted badge, small dot glyph, with a soft
|
||
opacity pulse (1 → 0.35 → 1, ~1.6s) rather than the rotating spinner — reads
|
||
as "still looking" without borrowing the run-phase meaning. Heading
|
||
"Checking…," body "Looking for LOCQR sign-in support on this page."
|
||
Respects `prefers-reduced-motion` (pulse disabled).
|
||
- **`not_registered`** — inactive-gray tinted badge (lighter than neutral),
|
||
static dash glyph, no animation. Heading "Not available here," body naming
|
||
the site (`"{origin} hasn't set up sign-in with LOCQR."`).
|
||
- Even the header brand mark picks up the neutral/inactive tint on these two
|
||
screens instead of the usual accent blue, so the header itself signals
|
||
"nothing active" before any text is read.
|
||
|
||
## Open / not yet specified
|
||
|
||
- Icons: deliberately deferred until the rest of the popup design is
|
||
finalized — polish, not blocking.
|
||
- A full plain-language copy pass across every state, not just the error
|
||
family — see the caveat above. Most copy drafted in this document made a
|
||
real attempt at plain language already, but none of it has had a dedicated
|
||
review pass.
|
||
- `mobile/gui.md` — the companion app's screens (scan, key exchange, PIN
|
||
display) are entirely undesigned. Some of what's settled here — especially
|
||
the PIN/code display concept — should visually correspond to whatever the
|
||
companion shows, since the user compares the two side by side.
|