From ec7759821df70605b293e8b35bbcc8daf82dd8e2 Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Mon, 11 May 2026 22:00:13 -0500 Subject: [PATCH 1/5] fix(ui): suppress password-manager autofill icons on form inputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Password managers (LastPass, 1Password) were attaching their autofill UI to ham-radio data fields like callsign, RST, datetime, etc. — fields that have nothing to do with credentials. Set autoComplete="off" and the per-manager opt-out data attributes (data-lpignore, data-1p-ignore, data-form-type="other") as defaults on the shared Input component and the combobox search input. Patched the five raw elements that bypass the shared component too (new-contact callsign + RST sent/received, dashboard table search, combobox). Auth forms (login/register, LoTW password, station password) already pass explicit autoComplete values, which still override. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/dashboard/page.tsx | 4 ++++ src/app/new-contact/page.tsx | 11 +++++++++++ src/components/ui/combobox.tsx | 4 ++++ src/components/ui/input.tsx | 8 ++++++++ 4 files changed, 27 insertions(+) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index b903c7f..cd46ef0 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -480,6 +480,10 @@ export default function DashboardPage() { value={tableSearch} onChange={(e) => setTableSearch(e.target.value)} placeholder="Search callsign, name, grid, frequency…" + autoComplete="off" + data-lpignore="true" + data-1p-ignore="" + data-form-type="other" className="flex-1 min-w-0 bg-transparent border-0 outline-none text-fg text-[15px] placeholder:text-fg-3" /> / diff --git a/src/app/new-contact/page.tsx b/src/app/new-contact/page.tsx index 6290753..3855936 100644 --- a/src/app/new-contact/page.tsx +++ b/src/app/new-contact/page.tsx @@ -479,6 +479,9 @@ export default function NewContactPage() { onChange={handleChange} placeholder="W1AW" autoComplete="off" + data-lpignore="true" + data-1p-ignore="" + data-form-type="other" className={cn( 'w-full bg-transparent border-0 border-b-2 border-line-hi text-fg font-mono text-[32px] sm:text-[56px] font-semibold tracking-[0.04em] py-3 sm:py-4 outline-none transition-colors uppercase placeholder:text-fg-3', 'focus:border-accent', @@ -652,6 +655,10 @@ export default function NewContactPage() { name="rst_sent" value={formData.rst_sent} onChange={handleChange} + autoComplete="off" + data-lpignore="true" + data-1p-ignore="" + data-form-type="other" className="w-full bg-transparent border-0 outline-none text-center text-fg font-mono text-[28px] font-semibold mt-1" />
@@ -675,6 +682,10 @@ export default function NewContactPage() { name="rst_received" value={formData.rst_received} onChange={handleChange} + autoComplete="off" + data-lpignore="true" + data-1p-ignore="" + data-form-type="other" className="w-full bg-transparent border-0 outline-none text-center text-fg font-mono text-[28px] font-semibold mt-1" />
diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx index 4bcaa4f..586a1c2 100644 --- a/src/components/ui/combobox.tsx +++ b/src/components/ui/combobox.tsx @@ -86,6 +86,10 @@ export function Combobox({ placeholder={searchPlaceholder} value={search} onChange={(e) => setSearch(e.target.value)} + autoComplete="off" + data-lpignore="true" + data-1p-ignore="" + data-form-type="other" />
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index f7d760c..06ce4fe 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -36,6 +36,14 @@ const Input = React.forwardRef( type={type} className={cn(inputVariants({ size, mono, className }))} ref={ref} + // Suppress password-manager autofill UI by default — this app is + // ham-radio data, not credentials. Auth forms pass an explicit + // autoComplete value (e.g. "email", "current-password") which + // overrides via prop spread below. + autoComplete="off" + data-lpignore="true" + data-1p-ignore="" + data-form-type="other" {...props} /> ) From f8a9138f2e5cce8cea2c07bf292b8e0c9965a27e Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Mon, 11 May 2026 22:08:02 -0500 Subject: [PATCH 2/5] fix(ui): wrap search filters in
so LastPass respects data-lpignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LastPass ignores data-lpignore on the search page callsign input because the filter fields aren't inside a — LP treats standalone inputs more aggressively than form-bound ones. Wrapping the filters in a form with autoComplete="off" gives LP a form context to bind to, and it then honors the per-input opt-out attrs. Search is debounced on filter change, so the form has no real submit action — onSubmit calls preventDefault to keep Enter from triggering a page navigation. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/search/page.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index 851d672..540d8e7 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -768,6 +768,16 @@ export default function SearchPage() { + {/* Wrapping the filter fields in a gives LastPass a form + context to bind to, so it respects data-lpignore on the + individual inputs and stops attaching its autofill icon. + Search is debounced on filter change, so the form has no + real submit action — Enter is suppressed via preventDefault. */} + e.preventDefault()} + className="space-y-6" + > {/* Quick Filters */}
@@ -931,6 +941,7 @@ export default function SearchPage() {
)} +
From 89ae762b54022c908c65e1810562aaaab014f3f0 Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Mon, 11 May 2026 22:12:27 -0500 Subject: [PATCH 3/5] fix(ui): set type=search on search-page text filters to dodge LastPass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LastPass keeps attaching its autofill icon to the search-page callsign input despite data-lpignore being set and the fields being wrapped in a
. Switching the text filters to type="search" tells LP these are search inputs, which it skips entirely. Applied to callsign, name, qth, and gridLocator — the four free-text filter fields. Semantically these are search inputs anyway, so the type change matches their actual role. (Browser-default styling of search inputs is suppressed by the shared Input component's own CSS.) Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/search/page.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index 540d8e7..3c01da7 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -803,6 +803,7 @@ export default function SearchPage() { handleFilterChange('callsign', e.target.value)} @@ -812,6 +813,7 @@ export default function SearchPage() { handleFilterChange('name', e.target.value)} @@ -821,6 +823,7 @@ export default function SearchPage() { handleFilterChange('qth', e.target.value)} @@ -895,6 +898,7 @@ export default function SearchPage() { handleFilterChange('gridLocator', e.target.value)} From 8aaa5652dc4f32a34325a4845a0b69ad4e82290d Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Mon, 11 May 2026 22:17:59 -0500 Subject: [PATCH 4/5] fix(ui): CSS-hide LastPass/1Password icon overlays on opted-out inputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The data-lpignore / data-1p-ignore attrs we set as defaults on the shared Input component aren't honored on every field — LP keeps attaching its icon to fields like profile name, station-edit city / operator-name / club-callsign, and the new-contact datetime picker. Adding a global CSS rule that: - Hides the LP/1P icon overlay sibling elements that get injected next to inputs already opted out via the data-* attrs. - Resets background-image on opted-out inputs (covers the inline- injection variant some LP versions use). - Hides the WebKit credentials-auto-fill pseudo-element as a backup. This is the catch-all for fields where type="search" isn't a fit (date/datetime, plain text inputs that need normal browser behavior). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/globals.css | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/app/globals.css b/src/app/globals.css index c4f97ff..73b9c6b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -237,3 +237,33 @@ background: var(--line-hi); filter: brightness(1.4); } + +/* ────────────────────────────────────────────────────────────── + Suppress password-manager autofill icon overlays + ────────────────────────────────────────────────────────────── + LastPass / 1Password / Dashlane inject icon overlays next to + text inputs they think are credentials. We set data-lpignore / + data-1p-ignore as defaults on the shared Input component, but + in practice LP still attaches its icon on some fields. These + rules nuke the injected overlay elements for any input that + has already opted out via those attributes, plus background- + image overrides for the inline-injection variant. */ +input[data-lpignore="true"] + div[data-lastpass-icon-root], +input[data-lpignore="true"] + div[data-lastpass-root], +input[data-1p-ignore] + com-1password-button, +input[data-1p-ignore] + [data-dashlane-rid] { + display: none !important; +} +input[data-lpignore="true"], +input[data-1p-ignore] { + background-image: none !important; + background-attachment: initial !important; +} +input[data-lpignore="true"]::-webkit-credentials-auto-fill-button, +input[data-1p-ignore]::-webkit-credentials-auto-fill-button { + visibility: hidden !important; + display: none !important; + pointer-events: none !important; + position: absolute !important; + right: 0 !important; +} From ee631c0301f6a85357910ae3efd3a5d288fe7f6b Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Mon, 11 May 2026 22:22:46 -0500 Subject: [PATCH 5/5] fix(ui): hide LastPass overlay globally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CSS selectors in the previous attempt were guesses for an older LP variant — the actual injection is positioned absolutely over the input, ignoring data-lpignore. Replacing the speculative selectors with the real one. Also adding parity selectors for 1Password / Dashlane / Bitwarden so any future manager that injects its standard marker is also suppressed. Tradeoff: hides LP's in-field icon on the auth forms too. The forms still autofill correctly — users trigger LP via the extension's keyboard shortcut or the dropdown that appears when focused. Only the in-field icon overlay is hidden. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/globals.css | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 73b9c6b..886fe44 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -241,29 +241,16 @@ /* ────────────────────────────────────────────────────────────── Suppress password-manager autofill icon overlays ────────────────────────────────────────────────────────────── - LastPass / 1Password / Dashlane inject icon overlays next to - text inputs they think are credentials. We set data-lpignore / - data-1p-ignore as defaults on the shared Input component, but - in practice LP still attaches its icon on some fields. These - rules nuke the injected overlay elements for any input that - has already opted out via those attributes, plus background- - image overrides for the inline-injection variant. */ -input[data-lpignore="true"] + div[data-lastpass-icon-root], -input[data-lpignore="true"] + div[data-lastpass-root], -input[data-1p-ignore] + com-1password-button, -input[data-1p-ignore] + [data-dashlane-rid] { + LastPass injects overlays + positioned absolutely over inputs it thinks are credentials, + regardless of data-lpignore. Since this app is ham-radio data + (callsign, RST, datetime, city, etc.) and not credentials, we + hide the in-field icon globally. LP/1P autofill still works on + the auth forms via the extension's keyboard shortcut and + dropdown — only the in-field icon overlay is hidden. */ +svg[data-lastpass-icon="true"], +com-1password-button, +[data-dashlane-rid], +[data-bitwarden-watching] { display: none !important; } -input[data-lpignore="true"], -input[data-1p-ignore] { - background-image: none !important; - background-attachment: initial !important; -} -input[data-lpignore="true"]::-webkit-credentials-auto-fill-button, -input[data-1p-ignore]::-webkit-credentials-auto-fill-button { - visibility: hidden !important; - display: none !important; - pointer-events: none !important; - position: absolute !important; - right: 0 !important; -}