From 3135780c0339b2ec4f661d579e4a8ac61198d91d Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Fri, 25 Sep 2026 11:16:28 +0100 Subject: [PATCH 1/2] Unify list rows, page headers and the sidebar Status, task, device and profile lists share one grouped-row card: icon, text and trailing actions, dropping actions under the text only when the list is narrow. Every row subject gets the same icon tile. Page headers put the subtitle under the title and drop decorative corner icons, and the nested Mobile sub-views use section headers instead of a second h1. The sidebar marks the active page with a raised surface and brand bar and exposes it with aria-current. Closes #883 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/App.tsx | 49 ++++++++++---------- src/settings/SettingsView.tsx | 3 +- src/ui/app.css | 86 ++++++++++++++++++++--------------- 3 files changed, 75 insertions(+), 63 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 103ca7e5..ce8f7a37 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -44,10 +44,11 @@ const bluetoothDescriptions: Record = { }; function NavButton({ active, icon, children, onClick }: { active: boolean; icon: ReactNode; children: ReactNode; onClick: () => void }) { - return ; + return ; } -function StatusIcon({ ok, children }: { ok: boolean; children: ReactNode }) { +// `ok` omitted marks a row that has no state to report, only a subject. +function StatusIcon({ ok, children }: { ok?: boolean; children: ReactNode }) { return {children}; } @@ -76,9 +77,9 @@ function HomeView({ state, switches, scanning, navigate, onDisconnect, onAccessi

Input access

{state.accessibility === "required" && }

Saved switches

{saved.length} saved · {hasSelect ? "Select assigned" : "Select action needed"}

-

Scanning

Select starts {scanning.config.controlMode === "mouse" ? "Mouse scanning" : "Point scanning"} · {areaOptions(scanning.config, scanning.config.controlMode).automatic ? "Automatic" : "Manual"}

+

Scanning

Select starts {scanning.config.controlMode === "mouse" ? "Mouse scanning" : "Point scanning"} · {areaOptions(scanning.config, scanning.config.controlMode).automatic ? "Automatic" : "Manual"}

-

Mobile connection

{state.bluetooth === "connected" ? state.connectedDeviceName ?? bluetoothLabels.connected : bluetoothLabels[state.bluetooth]}

{state.bluetooth === "connected" && }
+

Mobile connection

{state.bluetooth === "connected" ? state.connectedDeviceName ?? bluetoothLabels.connected : bluetoothLabels[state.bluetooth]}

{state.bluetooth === "connected" && }
; } @@ -99,10 +100,10 @@ function MobileConnection({ state, onDisconnect }: { state: AppState; onDisconne } function DevicesView({ state, forget }: { state: AppState; forget: (id: string) => void }) { - return

Paired devices

Mobile devices trusted by this computer

+ return

Paired devices

Mobile devices trusted by this computer

{state.pairedDevices.length === 0 ?

No paired devices

Open Switchify on your mobile device to pair while this computer is advertising.

: -
{state.pairedDevices.map((device) =>

{device.deviceName}

{device.lastSeenAt !== null ? `Last connected ${new Date(device.lastSeenAt).toLocaleString()}` : "Not connected yet"}

)}
} -
; +
{state.pairedDevices.map((device) =>

{device.deviceName}

{device.lastSeenAt !== null ? `Last connected ${new Date(device.lastSeenAt).toLocaleString()}` : "Not connected yet"}

)}
} + ; } const newProfile = (): SwitchProfile => ({ @@ -304,13 +305,13 @@ function ProfileEditor({ profile, profiles, onClose, onSave, onDelete, onDuplica function ProfilesView({ profiles, platform, saveProfile, deleteProfile, onDirtyChange, nativeExitRequest, onConfirmNativeExit, onCancelNativeExit, busy }: { profiles: SwitchProfile[]; platform: AppState["capabilities"]["platform"]; saveProfile: (profile: SwitchProfile) => Promise; deleteProfile: (id: string) => Promise; onDirtyChange: (dirty: boolean) => void; nativeExitRequest: ProfileExitAction | null; onConfirmNativeExit: () => void; onCancelNativeExit: () => void; busy: boolean }) { const [editing, setEditing] = useState(null); const openerRef = useRef(null); - const closeEditor = () => { setEditing(null); requestAnimationFrame(() => (openerRef.current?.isConnected ? openerRef.current : document.querySelector(".page-header button"))?.focus()); }; + const closeEditor = () => { setEditing(null); requestAnimationFrame(() => (openerRef.current?.isConnected ? openerRef.current : document.querySelector(".section-header button"))?.focus()); }; const openEditor = (profile: SwitchProfile, opener: HTMLButtonElement) => { openerRef.current = opener; setEditing(profile); }; - return

Switch Forwarding

Profiles available to physical switch sessions

-
{profiles.map((profile) => )}
+ return

Switch Forwarding

Profiles available to physical switch sessions

+
{profiles.map((profile) => )}
{platform === "macos" &&

Grid 3 profiles are available on Windows only.

} {editing && { closeEditor(); onConfirmNativeExit(); }} onCancelNativeExit={onCancelNativeExit} onClose={closeEditor} onDuplicate={() => setEditing(duplicateProfile(editing, profiles))} onSave={async (profile) => { await saveProfile(profile); closeEditor(); }} onDelete={editing.builtIn || !profiles.some((profile) => profile.id === editing.id) ? null : async () => { await deleteProfile(editing.id); closeEditor(); }} />} -
; + ; } function UpdateBanner({ update, openUpdates }: { update: UpdateState; openUpdates: () => void }) { @@ -331,20 +332,20 @@ const supportTabs = [{ id: "setup", label: "Setup" }, { id: "troubleshooting", l function SupportView({ state, switches, busy, perform, openSetup, openUpdates }: { state: AppState; switches: SwitchController; busy: boolean; perform: (operation: () => Promise) => void; openSetup: () => void; openUpdates: () => void }) { const [tab, setTab] = useState<(typeof supportTabs)[number]["id"]>("setup"); - return

Help

+ return

Help

Set up Switchify PC and fix common problems.

{tab === "setup" ? <>

Input access

{state.accessibility === "required" && }
-

Local switches

{switches.state?.settings.bindings.length ?? 0} configured. Open Switches to assign Select, then open Scanning to adjust movement.

-

Optional mobile connection

{bluetoothLabels[state.bluetooth]}. Open Mobile connection for pairing and forwarding profiles.

+

Local switches

{switches.state?.settings.bindings.length ?? 0} configured. Open Switches to assign Select, then open Scanning to adjust movement.

+

Optional mobile connection

{bluetoothLabels[state.bluetooth]}. Open Mobile connection for pairing and forwarding profiles.

:
-

Bluetooth connection

{bluetoothLabels[state.bluetooth]}

-

Input access

{state.accessibility === "required" ? : }
-

Application update

Switchify PC {state.version}

-

Diagnostics

Export sanitized health, capability, and recent event data

-

Recent Bluetooth changes

{state.diagnostics.recentBluetooth.length > 0 ? state.diagnostics.recentBluetooth.map((event) => event.status).join(" → ") : "No Bluetooth changes recorded yet"}

-

Last disconnect

{state.diagnostics.lastDisconnect ? `${state.diagnostics.lastDisconnect.detail ?? state.diagnostics.lastDisconnect.status}` : "No disconnect recorded yet"}

-

Recent errors

{state.diagnostics.recentErrors.length > 0 ? state.diagnostics.recentErrors.map((event) => event.detail ?? event.status).join(" · ") : "No recent errors"}

+

Bluetooth connection

{bluetoothLabels[state.bluetooth]}

+

Input access

{state.accessibility === "required" ? : }
+

Application update

Switchify PC {state.version}

+

Diagnostics

Export sanitized health, capability, and recent event data

+

Recent Bluetooth changes

{state.diagnostics.recentBluetooth.length > 0 ? state.diagnostics.recentBluetooth.map((event) => event.status).join(" → ") : "No Bluetooth changes recorded yet"}

+

Last disconnect

{state.diagnostics.lastDisconnect ? `${state.diagnostics.lastDisconnect.detail ?? state.diagnostics.lastDisconnect.status}` : "No disconnect recorded yet"}

+

Recent errors

{state.diagnostics.recentErrors.length > 0 ? state.diagnostics.recentErrors.map((event) => event.detail ?? event.status).join(" · ") : "No recent errors"}

}
; } @@ -778,10 +779,10 @@ export function App() {

{updateNotice}

{view === "home" && void perform(api.disconnectAll)} onAccessibility={() => void perform(() => api.checkAccessibility(true))} />} - {view === "switches" &&

Switches

} - {view === "scanning" &&

Scanning

Choose how your switches control the pointer.

} + {view === "switches" &&

Switches

The switches you use to control this computer.

} + {view === "scanning" &&

Scanning

Choose how your switches control the pointer.

} {view === "mouse" && } - {(view === "mobile" || view === "devices" || view === "profiles") &&

Mobile

+ {(view === "mobile" || view === "devices" || view === "profiles") &&

Mobile

Control this computer from Switchify on a phone or tablet.

{view === "mobile" && void perform(api.disconnectAll)} />} {view === "devices" && void perform(() => api.forgetDevice(id))} />} {view === "profiles" && { profileEditorDirty.current = dirty; }} nativeExitRequest={profileExitRequest} onConfirmNativeExit={confirmProfileExit} onCancelNativeExit={cancelProfileExit} />} diff --git a/src/settings/SettingsView.tsx b/src/settings/SettingsView.tsx index 5961bb82..f990d425 100644 --- a/src/settings/SettingsView.tsx +++ b/src/settings/SettingsView.tsx @@ -1,5 +1,4 @@ import { useEffect, useMemo, useRef, useState } from "react"; -import { Settings } from "lucide-react"; import type { AppSettings, AppState } from "../types"; import { Tabs, TabPanel, type TabDefinition } from "../Tabs"; import { GeneralSection } from "./GeneralSection"; @@ -51,7 +50,7 @@ export function SettingsView({ state, settings, onChange, chooseTelemetry, updat const update = (key: K, value: AppSettings[K]) => onChange({ ...settings, [key]: value }); - return

Settings

How Switchify PC behaves on this computer

+ return

Settings

How Switchify PC behaves on this computer

{active === "general" && } diff --git a/src/ui/app.css b/src/ui/app.css index 1858e7cc..564c163e 100644 --- a/src/ui/app.css +++ b/src/ui/app.css @@ -168,26 +168,31 @@ h1, h2, h3, p { margin: 0; letter-spacing: 0; } .field { display: grid; gap: 6px; min-width: 0; margin-top: 20px; color: var(--muted); } .field-error { color: var(--status-error); line-height: 1.35; } -/* --- Shell ------------------------------------------------------------------ */ +/* --- Shell ------------------------------------------------------------------ + The sidebar is quiet: the active place is a raised surface with a brand bar + and brand-coloured icon, so the page's own primary action stays the only + solid red on screen. */ .app-shell { display: grid; grid-template-columns: 240px minmax(0, 1fr); min-height: 100vh; border-top: 3px solid var(--chrome); background: var(--app-background); } -.app-shell > aside { position: sticky; top: 0; display: flex; flex-direction: column; gap: 12px; height: 100vh; overflow-y: auto; padding: 20px 12px 14px; border-right: 1px solid var(--border); background: var(--surface-muted); } -.brand { display: flex; align-items: center; gap: 11px; min-height: 44px; padding: 0 8px 18px; } +.app-shell > aside { position: sticky; top: 0; display: flex; flex-direction: column; gap: 8px; height: 100vh; overflow-y: auto; padding: 20px 12px 16px; border-right: 1px solid var(--border); background: var(--surface-muted); } +.brand { display: flex; align-items: center; gap: 12px; min-height: 44px; padding: 0 8px 16px; } .brand-mark { display: block; width: 36px; height: 36px; border-radius: 8px; object-fit: contain; } .brand strong, .brand small { display: block; } -.brand strong { color: var(--brand-label); } -.brand small { margin-top: 2px; color: var(--muted); } -.app-shell nav { display: grid; gap: 12px; } -.btn.nav-button { justify-content: flex-start; width: 100%; border-color: transparent; color: var(--muted); background: transparent; text-align: left; } -.btn.nav-button:hover { background: var(--surface-hover); } -.btn.nav-button[data-active="true"] { color: #ffffff; background: var(--chrome); } -.sidebar-footer { margin-top: auto; padding: 12px 9px 0; color: var(--muted); } +.brand strong { color: var(--brand-label); line-height: 1.2; } +.brand small { margin-top: 2px; color: var(--muted); line-height: 1.2; } +.app-shell nav { display: grid; gap: 4px; } +.btn.nav-button { justify-content: flex-start; width: 100%; border-color: transparent; padding: 12px 16px; color: var(--muted); background: transparent; text-align: left; } +.btn.nav-button:hover { color: var(--text); background: var(--surface-hover); } +.btn.nav-button[data-active="true"] { color: var(--text); background: var(--surface); box-shadow: inset 4px 0 0 var(--brand), 0 1px 2px rgba(28, 27, 31, 0.08); } +.btn.nav-button[data-active="true"] svg { color: var(--brand); } +.sidebar-footer { margin-top: auto; padding: 12px 16px 0; color: var(--muted); } main { min-width: 0; } -.view { width: min(100%, 1100px); margin: 0 auto; padding: 32px; } -.page-header { display: flex; align-items: center; justify-content: space-between; gap: 20px; margin-bottom: 24px; } +.view { width: min(100%, 1100px); margin: 0 auto; padding: 32px; container-type: inline-size; } +.page-header, .section-header { display: flex; align-items: center; justify-content: space-between; gap: 16px 24px; } +.page-header { margin-bottom: 24px; } .page-header h1 { font-size: 30px; line-height: 1.2; font-weight: 700; } -.page-header p { margin-top: 5px; color: var(--muted); } -.page-header > svg { color: var(--muted); } +.page-header p, .section-header p { max-width: 64ch; margin-top: 6px; color: var(--muted); } +.section-header { flex-wrap: wrap; margin-bottom: 16px; } .loading { display: flex; align-items: center; justify-content: center; gap: 10px; min-height: 100vh; color: var(--muted); } .spin { animation: spin 1s linear infinite; } @@ -205,31 +210,37 @@ main { min-width: 0; } .connection-band[data-connected="true"] { border-left-color: var(--status-ok); } .connection-band p { margin-top: 4px; color: var(--muted); } .connection-band > .btn { grid-column: 1 / -1; } -.status-icon { display: grid; place-items: center; width: 38px; height: 38px; border-radius: 50%; color: var(--muted); background: var(--surface-muted); } +.status-icon { display: grid; flex: none; place-items: center; width: 40px; height: 40px; border-radius: 50%; color: var(--muted); background: var(--surface-muted); } .status-icon[data-ok="true"] { color: var(--status-ok); background: var(--status-ok-soft); } -.status-list { margin-top: 22px; border-top: 1px solid var(--border); } -.status-list .status-icon { width: 34px; height: 34px; } -.status-list p { margin-top: 3px; color: var(--muted); } -.task-list { border-top: 1px solid var(--border); } -.task-list article > svg { margin: 0 9px; color: var(--muted); } -.task-list p { margin-top: 4px; color: var(--muted); line-height: 1.4; } +/* Grouped rows: one card per list, hairlines between rows. A row is an icon, + its text, then any actions on the right; when the list is narrow the + actions drop under the text rather than squeezing it. */ +.status-list, .task-list, .device-list, .profile-list, .setup-statuses { border: 1px solid var(--border); border-radius: var(--radius-card); overflow: hidden; background: var(--surface); container-type: inline-size; } +.status-list + .status-list { margin-top: 16px; } +.connection-band + .status-list { margin-top: 24px; } +:is(.status-list, .task-list, .device-list, .setup-statuses) > article { display: grid; grid-template-columns: auto minmax(0, 1fr); grid-auto-columns: auto; grid-auto-flow: column; align-items: center; gap: 12px 16px; min-height: 80px; border-bottom: 1px solid var(--border); padding: 16px 20px; } +:is(.status-list, .task-list, .device-list, .setup-statuses) > article:last-child { border-bottom: 0; } +:is(.status-list, .task-list, .device-list, .setup-statuses) > article > .btn { justify-self: end; } +:is(.status-list, .task-list, .device-list, .setup-statuses) p { margin-top: 2px; color: var(--muted); } .task-list .permission-recovery { margin-top: 8px; color: var(--text); } -.status-list article, .task-list article { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 16px; min-height: 76px; border-bottom: 1px solid var(--border); padding: 16px 4px; } -.status-list article > .btn, .task-list article > .btn { grid-column: 1 / -1; justify-self: start; } -.setup-launch { margin-bottom: 18px; } +@container (max-width: 560px) { + :is(.status-list, .task-list, .device-list, .setup-statuses) > article { grid-auto-flow: row; padding: 16px; } + :is(.status-list, .task-list, .device-list, .setup-statuses) > article > .btn { grid-column: 2; justify-self: start; } +} +.setup-launch { margin-bottom: 24px; } -.empty-state { display: grid; justify-items: center; border: 1px dashed var(--border); padding: 52px 24px; color: var(--muted); text-align: center; } +.empty-state { display: grid; justify-items: center; border: 1px dashed var(--border-strong); border-radius: var(--radius-card); padding: 48px 24px; color: var(--muted); text-align: center; } .empty-state h2 { margin-top: 14px; color: var(--text); } -.empty-state p { margin-top: 6px; } -.device-list, .profile-list { border-top: 1px solid var(--border); } -.device-list article, .btn.profile-row { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 12px; min-height: 72px; border-bottom: 1px solid var(--border); padding: 16px 4px; } -.device-list p, .profile-list p { margin-top: 4px; color: var(--muted); } -.btn.profile-row { justify-content: stretch; width: 100%; grid-template-columns: auto 1fr auto auto; border-width: 0 0 1px; border-radius: 0; color: inherit; background: transparent; text-align: left; } +.empty-state p { max-width: 56ch; margin-top: 6px; } +.btn.profile-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; align-items: center; justify-content: stretch; gap: 16px; width: 100%; min-height: 80px; border-width: 0 0 1px; border-color: var(--border); border-radius: 0; padding: 16px 20px; color: inherit; background: transparent; font-weight: 400; text-align: left; } +.btn.profile-row:last-child { border-bottom: 0; } .btn.profile-row:hover { background: var(--surface-hover); } -.profile-row > span { color: var(--muted); } -.profile-icon { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 6px; color: var(--muted); background: var(--surface-muted); } +.btn.profile-row:focus-visible { outline-offset: -3px; } +.profile-row p { margin-top: 2px; color: var(--muted); } +.profile-row > span, .profile-row > svg { color: var(--muted); } .capability-note { margin-top: 16px; color: var(--muted); } +.subview { display: grid; } /* --- Settings controls ------------------------------------------------------ */ .setting-group { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; border-top: 1px solid var(--border); padding: 24px 0; } @@ -436,8 +447,6 @@ main { min-width: 0; } .setup-progress span[data-active="true"] { background: var(--brand); } .setup-content { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 16px 4px; } .setup-content p { margin-top: 8px; color: var(--muted); } -.setup-statuses { border-top: 1px solid var(--border); } -.setup-statuses article { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 14px; min-height: 92px; border-bottom: 1px solid var(--border); padding: 14px 4px; } .setup-choices { margin-top: 22px; } .setup-choices .btn[aria-pressed="true"] { border-color: var(--brand); color: var(--on-brand-container); background: var(--segment-selected); box-shadow: inset 0 0 0 1px var(--brand-border); } .setup-privacy { margin-top: 18px; color: var(--brand-label); } @@ -476,13 +485,18 @@ main { min-width: 0; } @keyframes capture-pulse { 50% { border-color: var(--brand); box-shadow: var(--shadow-dialog), 0 0 0 4px var(--brand-container); } } @media (prefers-reduced-motion: reduce) { .capture-dialog, .btn { animation: none; transition: none; } } +@media (forced-colors: active) { + .btn.nav-button[data-active="true"] { outline: 2px solid Highlight; outline-offset: -2px; } +} + /* --- Responsive --------------------------------------------------------------- */ @media (max-width: 800px) { .app-shell { grid-template-columns: 1fr; } .app-shell > aside { position: static; height: auto; padding: 12px; } .app-shell > aside .brand, .app-shell .sidebar-footer { display: none; } .app-shell nav { grid-template-columns: repeat(3, minmax(0, 1fr)); } - .btn.nav-button { flex-direction: column; justify-content: center; text-align: center; } + .btn.nav-button { flex-direction: column; justify-content: center; gap: 4px; padding: 8px; text-align: center; } + .btn.nav-button[data-active="true"] { box-shadow: inset 0 -4px 0 var(--brand), 0 1px 2px rgba(28, 27, 31, 0.08); } .view { padding: 16px; } .switch-row-summary { grid-template-columns: auto minmax(0, 1fr); } .switch-row-summary > .btn:first-of-type, .switch-row-summary > .btn:last-of-type { width: 100%; grid-row: auto; grid-column: auto; } @@ -500,8 +514,6 @@ main { min-width: 0; } .profile-dialog > footer > span { flex-basis: 100%; height: 0; } .update-banner { grid-template-columns: auto 1fr; } .update-banner .btn.text-button { grid-column: 2; justify-self: start; padding-left: 0; } - .setup-statuses article { grid-template-columns: auto 1fr; } - .setup-statuses article > .btn { grid-column: 1 / -1; } .mobile-download img { width: 150px; height: 150px; } } @media (max-width: 480px) { From ff5c0f15cca3fe37ed4dc2b806daa3596ba0c49d Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Fri, 25 Sep 2026 11:47:51 +0100 Subject: [PATCH 2/2] Keep focus visible on the active nav item in high contrast The forced-colours outline marking the current page replaced the focus ring on that same button. Focus now draws its own CanvasText ring there. The Mobile sub-views become plain containers so screen readers do not announce each tab's name twice as a region, and the empty paired-devices heading drops to h3 beneath its section heading. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/App.tsx | 10 +++++----- src/ui/app.css | 4 +++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index ce8f7a37..119ffbfd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -100,10 +100,10 @@ function MobileConnection({ state, onDisconnect }: { state: AppState; onDisconne } function DevicesView({ state, forget }: { state: AppState; forget: (id: string) => void }) { - return

Paired devices

Mobile devices trusted by this computer

- {state.pairedDevices.length === 0 ?

No paired devices

Open Switchify on your mobile device to pair while this computer is advertising.

: + return

Paired devices

Mobile devices trusted by this computer

+ {state.pairedDevices.length === 0 ?

No paired devices

Open Switchify on your mobile device to pair while this computer is advertising.

:
{state.pairedDevices.map((device) =>

{device.deviceName}

{device.lastSeenAt !== null ? `Last connected ${new Date(device.lastSeenAt).toLocaleString()}` : "Not connected yet"}

)}
} -
; +
; } const newProfile = (): SwitchProfile => ({ @@ -307,11 +307,11 @@ function ProfilesView({ profiles, platform, saveProfile, deleteProfile, onDirtyC const openerRef = useRef(null); const closeEditor = () => { setEditing(null); requestAnimationFrame(() => (openerRef.current?.isConnected ? openerRef.current : document.querySelector(".section-header button"))?.focus()); }; const openEditor = (profile: SwitchProfile, opener: HTMLButtonElement) => { openerRef.current = opener; setEditing(profile); }; - return

Switch Forwarding

Profiles available to physical switch sessions

+ return

Switch Forwarding

Profiles available to physical switch sessions

{profiles.map((profile) => )}
{platform === "macos" &&

Grid 3 profiles are available on Windows only.

} {editing && { closeEditor(); onConfirmNativeExit(); }} onCancelNativeExit={onCancelNativeExit} onClose={closeEditor} onDuplicate={() => setEditing(duplicateProfile(editing, profiles))} onSave={async (profile) => { await saveProfile(profile); closeEditor(); }} onDelete={editing.builtIn || !profiles.some((profile) => profile.id === editing.id) ? null : async () => { await deleteProfile(editing.id); closeEditor(); }} />} -
; +
; } function UpdateBanner({ update, openUpdates }: { update: UpdateState; openUpdates: () => void }) { diff --git a/src/ui/app.css b/src/ui/app.css index 564c163e..fb5b5cfa 100644 --- a/src/ui/app.css +++ b/src/ui/app.css @@ -231,7 +231,7 @@ main { min-width: 0; } .setup-launch { margin-bottom: 24px; } .empty-state { display: grid; justify-items: center; border: 1px dashed var(--border-strong); border-radius: var(--radius-card); padding: 48px 24px; color: var(--muted); text-align: center; } -.empty-state h2 { margin-top: 14px; color: var(--text); } +.empty-state :is(h2, h3) { margin-top: 14px; color: var(--text); } .empty-state p { max-width: 56ch; margin-top: 6px; } .btn.profile-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; align-items: center; justify-content: stretch; gap: 16px; width: 100%; min-height: 80px; border-width: 0 0 1px; border-color: var(--border); border-radius: 0; padding: 16px 20px; color: inherit; background: transparent; font-weight: 400; text-align: left; } .btn.profile-row:last-child { border-bottom: 0; } @@ -487,6 +487,8 @@ main { min-width: 0; } @media (forced-colors: active) { .btn.nav-button[data-active="true"] { outline: 2px solid Highlight; outline-offset: -2px; } + /* Focus must still read as focus on the current page's own button. */ + .btn.nav-button[data-active="true"]:focus-visible { outline: 3px solid CanvasText; outline-offset: 3px; } } /* --- Responsive --------------------------------------------------------------- */