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" />