From 84a94581dc16509722f4aa3817374e3e6b525677 Mon Sep 17 00:00:00 2001 From: Martin Dvorak Date: Fri, 16 Oct 2026 10:12:20 +0000 Subject: [PATCH] Key fields: keep the browser's password UI away, fix the keyboard layout "Trying to use a different provider on the web app glitches out completely" - an iPhone photo of the first-run gate with iOS's saved-password sheet laid over it ("password for proxmox...") and the provider field stuck under the keyboard. - The key fields (gate, Settings, provider rows) were type=password with autocomplete=off, which iOS/Safari and 1Password/LastPass read as a login. They now carry autoComplete="new-password", data-form-type="other", data-lpignore="true", autoCorrect/autoCapitalize=off and inputMode=text: the mask stays, the password overlay goes. The custom base URL gets inputMode=url and the same hints. - The gate's keyboard handling scrolled to the Continue button instead of the field being edited, so with the provider rows open the keyboard covered the input and the locked wrapper could not be scrolled to it. The wrapper is user-scrollable again, alignment anchors on the focused element (Continue is the fallback), and the corner controls hide while the keyboard is up rather than landing on the card. providers-test drives a fake visualViewport (340px, installed before mount) and pins the three layout contracts plus the hardening attributes. All 16 suites green, contrast clean. Version 0.7.2, SW v94. --- README.md | 16 +++++++ components/ApiKeyGate.tsx | 40 ++++++++++------ components/ProvidersSection.tsx | 13 +++++- components/SettingsDialog.tsx | 18 +++++++- package.json | 2 +- public/sw.js | 2 +- tests/providers-test.mjs | 82 +++++++++++++++++++++++++++++++++ 7 files changed, 154 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 629d2c5..a9a8b61 100644 --- a/README.md +++ b/README.md @@ -1492,6 +1492,22 @@ Things I chose while building, with the reasoning: into view as you type. No strings, so no i18n; the additive change sits on top of the merged scene-heat rework unchanged. +102. **An API key is not a login.** "When trying to use a different provider on the web app it + glitches out completely" — a photo of the first-run gate with iOS's saved-password sheet laid + over it ("password for proxmox…"). Two causes, both fixed. (1) The key fields were + `type="password"` with `autocomplete="off"`, which iOS/Safari (and 1Password/LastPass) read as + a login and answer with their own overlay; the gate, Settings and the provider rows now carry + `autoComplete="new-password"`, `data-form-type="other"`, `data-lpignore="true"`, + `autoCorrect`/`autoCapitalize="off"` and `inputMode="text"` — the mask stays, the password UI + goes. (2) The gate's keyboard handling scrolled to the *Continue* button rather than the field + being edited, so with the provider rows open the keyboard covered the input and the locked + wrapper gave no way to reach it; the wrapper stays user-scrollable, alignment now anchors on + the focused element (Continue is only the fallback), and the corner controls hide while the + keyboard is up instead of landing on the card. The providers suite drives a fake + `visualViewport` (shrunk to 340px, installed before the app mounts) and pins all of it: the + field stays in view, the card stays scrollable, the corners step aside — plus the + autofill-hardening attributes on the field itself. + 101. **Frutiger Aero has its soundtrack.** Picking the theme starts a random track from the Frutiger Aero playlist (Bigblamboii's set on SoundCloud) and draws every following track at random too, so the set shuffles itself. The player is the credit bar from the user's mock: a diff --git a/components/ApiKeyGate.tsx b/components/ApiKeyGate.tsx index 3be8a8d..270b487 100644 --- a/components/ApiKeyGate.tsx +++ b/components/ApiKeyGate.tsx @@ -115,17 +115,22 @@ function GateScreen() { if (window.matchMedia("(pointer: fine)").matches) inputRef.current?.focus(); }, []); - // Keep the field and the Continue button in view while the on-screen keyboard - // is up (the card itself is locked, so this is done by scrolling the wrapper - // programmatically rather than by the user). + // Keep what the listener is actually editing in view while the on-screen + // keyboard is up: the focused field when there is one (the provider rows sit + // far below the Continue button), the button otherwise. The wrapper stays + // user-scrollable underneath, this just does the first move. const alignKeyboard = useCallback(() => { const wrap = wrapRef.current; - const submit = submitRef.current; - if (!wrap || !submit) return; + if (!wrap) return; + const active = document.activeElement; + const focused = + active instanceof HTMLElement && active !== wrap && wrap.contains(active) ? active : null; + const target = focused ?? submitRef.current; + if (!target) return; const wrapBox = wrap.getBoundingClientRect(); - const btnBox = submit.getBoundingClientRect(); - const below = btnBox.bottom + 12 - wrapBox.bottom; - const above = wrapBox.top + 12 - btnBox.top; + const box = target.getBoundingClientRect(); + const below = box.bottom + 12 - wrapBox.bottom; + const above = wrapBox.top + 12 - box.top; if (below > 0) wrap.scrollTop += below; else if (above > 0) wrap.scrollTop -= above; }, []); @@ -205,7 +210,7 @@ function GateScreen() { > {/* corner controls: theme + help ---------------------------------- */}
) : null} @@ -116,8 +119,16 @@ export function ProvidersSection() { }} placeholder={saved ? t("providers.keySaved") : provider.keyHint} aria-label={`${provider.label} ${t("providers.key")}`} - autoComplete="off" + /* See the gate: an API key must not raise the browser's + saved-password UI ("new-password" + the data hints). */ + autoComplete="new-password" + autoCorrect="off" + autoCapitalize="off" spellCheck={false} + inputMode="text" + enterKeyHint="done" + data-form-type="other" + data-lpignore="true" className="min-w-0 flex-1 rounded-xl border border-[var(--hairline)] bg-[var(--surface-strong)] px-3 py-2 text-[13px] text-[var(--text)] outline-none placeholder:text-[var(--text-subtle)] focus:border-accent-400" />