From 56b0da990ba78fb70c161d5dbabb76513df12c1a Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Thu, 14 May 2026 20:04:19 +0400 Subject: [PATCH] refactor: cleanup --- AGENTS.md | 62 ++++++++++++ src-tauri/src/browser_runner.rs | 10 +- src-tauri/src/lib.rs | 20 ++-- src-tauri/src/profile/password.rs | 29 ++++-- src/components/account-page.tsx | 14 +-- src/components/app-update-toast.tsx | 10 +- src/components/clone-profile-dialog.tsx | 18 ++-- src/components/cookie-copy-dialog.tsx | 12 +-- src/components/cookie-management-dialog.tsx | 8 +- src/components/create-profile-dialog.tsx | 59 +++++++----- src/components/custom-toast.tsx | 24 ++--- src/components/data-table-action-bar.tsx | 2 +- src/components/delete-group-dialog.tsx | 4 +- src/components/device-code-verify-dialog.tsx | 2 +- src/components/dns-blocklist-dialog.tsx | 2 +- .../extension-management-dialog.tsx | 36 +++---- src/components/group-assignment-dialog.tsx | 2 +- src/components/group-management-dialog.tsx | 8 +- src/components/home-header.tsx | 14 +-- src/components/import-profile-dialog.tsx | 6 +- src/components/integrations-dialog.tsx | 18 ++-- src/components/loading-button.tsx | 2 +- src/components/location-proxy-dialog.tsx | 8 +- src/components/multiple-selector.tsx | 2 +- src/components/permission-dialog.tsx | 6 +- src/components/profile-data-table.tsx | 78 ++++++++------- src/components/profile-info-dialog.tsx | 96 +++++++++---------- src/components/profile-password-dialog.tsx | 18 ++-- src/components/profile-selector-dialog.tsx | 2 +- src/components/profile-sync-dialog.tsx | 8 +- src/components/proxy-assignment-dialog.tsx | 18 ++-- src/components/proxy-check-button.tsx | 6 +- src/components/proxy-export-dialog.tsx | 10 +- src/components/proxy-import-dialog.tsx | 2 +- src/components/proxy-management-dialog.tsx | 22 ++--- src/components/rail-nav.tsx | 22 ++--- src/components/release-type-selector.tsx | 12 ++- src/components/settings-dialog.tsx | 14 +-- .../shared-camoufox-config-form.tsx | 16 ++-- src/components/sync-all-dialog.tsx | 2 +- src/components/sync-config-dialog.tsx | 16 ++-- src/components/theme-provider.tsx | 36 +++---- src/components/traffic-details-dialog.tsx | 4 +- src/components/ui/chart.tsx | 4 +- src/components/ui/color-picker.tsx | 6 +- src/components/ui/combobox.tsx | 8 +- src/components/ui/copy-to-clipboard.tsx | 4 +- src/components/ui/radio-group.tsx | 4 +- src/components/vpn-check-button.tsx | 8 +- src/components/vpn-import-dialog.tsx | 6 +- src/components/wayfern-config-form.tsx | 10 +- .../window-resize-warning-dialog.tsx | 2 +- src/styles/globals.css | 11 +++ 53 files changed, 473 insertions(+), 350 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8409816..a9840e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,6 +69,58 @@ donutbrowser/ - Strings excluded from this rule: `console.log/warn/error`, dev-only debug labels, internal IDs, CSS class names, type names. If unsure whether a string renders to the user, assume it does and translate it. - **Never use `t(key, "fallback")` with a default-value second argument.** The 2-arg form is forbidden — every key must exist in every locale file before the call site lands. Fallbacks mask missing translations: a key missing from `ru.json` will silently render the English fallback to Russian users, so the bug never surfaces in CI or review. Only call `t("namespace.key")`. If a translation is missing for any locale, that's a bug to fix at the JSON, not a hole to paper over at the call site. - Empty-string values in non-English locales are also forbidden — a locale either has the right translation or it has the same content as English; never `""`. If a particular language doesn't need a particular phrase (e.g. a suffix that doesn't grammatically apply), refactor the JSX to use a single interpolated key (`t("foo.bar", { name })` with `"...{{name}}..."` in each locale) instead of splitting prefix/suffix. +- When adding or removing keys across all seven locales, use a one-shot Python script in `/tmp/` that loads each `*.json`, mutates it, and writes it back. Seven sequential `Edit` calls drift (typos, ordering differences) and burn tokens; a single script keeps the locales in lockstep and is easy to throw away. + +## Backend error codes (mandatory) + +User-facing errors returned from a Tauri command MUST be JSON `{ "code": "FOO_BAR", "params": { … } }` strings — never raw English (`format!("Failed to …")`). The frontend resolves the code via `translateBackendError(t, err)` from `src/lib/backend-errors.ts`. Adding a new code requires four parallel edits: + +1. Emit the JSON from Rust: + ```rust + return Err(serde_json::json!({ "code": "FOO_BAR" }).to_string()); + // or with params: + return Err(serde_json::json!({ "code": "FOO_BAR", "params": { "n": "5" } }).to_string()); + ``` +2. Add `"FOO_BAR"` to the `BackendErrorCode` union in `src/lib/backend-errors.ts`. +3. Add a `case "FOO_BAR":` in the switch that returns `t("backendErrors.fooBar", …)`. +4. Add `backendErrors.fooBar` to all seven locale files. + +Raw error strings reach the user untranslated; that's the bug pattern this rule blocks. + +## Sub-page Dialog mode + +A `` becomes a first-class app sub-page (no modal overlay, no center positioning) when `subPage` is passed. Pages like Account, Settings, Proxy Management, and Extension Management use this. The pattern for a sub-page with tabs: + +```tsx + + + + + + Account + + … + + + + + +``` + +Reference implementations: `src/components/account-page.tsx`, `src/components/proxy-management-dialog.tsx`. Reuse the exact class strings — the overrides are tuned to match the rest of the sub-page chrome. ## Singletons @@ -93,6 +145,16 @@ donutbrowser/ - Use these as Tailwind classes: `bg-success`, `text-destructive`, `border-warning`, etc. - For lighter variants use opacity: `bg-destructive/10`, `bg-success/10`, `border-warning/50` +## App data directory naming + +`src-tauri/src/app_dirs.rs::app_name()` returns `"DonutBrowserDev"` when `cfg!(debug_assertions)` is true, `"DonutBrowser"` otherwise. So release builds (anything built via `tauri build` / `cargo build --release`) write to: + +- macOS — `~/Library/Application Support/DonutBrowser/` +- Linux — `~/.local/share/DonutBrowser/` +- Windows — `%LOCALAPPDATA%\DonutBrowser\` + +Debug builds (`cargo build`, `pnpm tauri dev`) write to the `DonutBrowserDev` sibling at the same root, and a `dev-{version}` `BUILD_VERSION` is injected via `build.rs`. Logs / screenshots referencing `DonutBrowserDev` therefore mean a local dev build is in play, not a release; useful when a bug report seems to disagree with what production users see. + ## Publishing Linux Repositories The `scripts/publish-repo.sh` script publishes DEB and RPM packages to Cloudflare R2 (served at `repo.donutbrowser.com`). It requires Linux tools, so run it in Docker on macOS: diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 1ba9616..bc67069 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -1582,7 +1582,10 @@ impl BrowserRunner { } if profile.password_protected { - crate::profile::password::complete_after_quit(profile); + // Await the re-encryption so the queued sync (released later by + // `mark_profile_stopped` in `kill_browser`) sees fresh ciphertext on + // disk instead of the previous snapshot. + crate::profile::password::complete_after_quit_and_wait(profile).await; } else if profile.ephemeral { crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string()); } @@ -1924,7 +1927,10 @@ impl BrowserRunner { } if profile.password_protected { - crate::profile::password::complete_after_quit(profile); + // Await the re-encryption so the queued sync (released later by + // `mark_profile_stopped` in `kill_browser`) sees fresh ciphertext on + // disk instead of the previous snapshot. + crate::profile::password::complete_after_quit_and_wait(profile).await; } else if profile.ephemeral { crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string()); } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4804c85..b77ea5c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1822,6 +1822,19 @@ pub fn run() { ); } + // Re-encrypt password-protected profiles when the browser + // exits naturally (user closing the window) — the explicit + // kill path in browser_runner.rs handles app-driven stops. + // Must run BEFORE `mark_profile_stopped` because that + // releases any queued sync run, and a sync that picks up + // the on-disk dir before re-encryption finishes uploads + // the previous snapshot (issue: encrypted profiles not + // syncing fresh data). + if !is_running && profile.password_protected { + crate::profile::password::complete_after_quit_and_wait(&profile) + .await; + } + // Notify sync scheduler of running state changes if let Some(scheduler) = sync::get_global_scheduler() { if is_running { @@ -1832,13 +1845,6 @@ pub fn run() { } } - // Re-encrypt password-protected profiles when the browser - // exits naturally (user closing the window) — the explicit - // kill path in browser_runner.rs handles app-driven stops. - if !is_running && profile.password_protected { - crate::profile::password::complete_after_quit(&profile); - } - last_running_states.insert(profile_id, is_running); } else { // Update the state even if unchanged to ensure we have it tracked diff --git a/src-tauri/src/profile/password.rs b/src-tauri/src/profile/password.rs index 1c309ee..fb96e57 100644 --- a/src-tauri/src/profile/password.rs +++ b/src-tauri/src/profile/password.rs @@ -637,22 +637,31 @@ pub fn complete_after_quit_blocking( result } -/// Async re-encrypt of a password-protected profile's ephemeral dir back to -/// disk, called after the browser process exits. Optionally purges the -/// ephemeral dir + cached key based on the global setting. -pub fn complete_after_quit(profile: &crate::profile::BrowserProfile) { +/// Re-encrypt a password-protected profile's ephemeral dir back to the +/// on-disk encrypted dir after the browser process exits. Optionally purges +/// the ephemeral dir + cached key based on the global setting. Returns the +/// number of files re-encrypted (`None` when nothing to do or the profile +/// isn't protected). +/// +/// Callers that release a queued sync run after a browser quit MUST await +/// this future — releasing sync while re-encryption is still in-flight +/// uploads the stale on-disk snapshot and leaves the fresh ciphertext +/// orphaned until the next scheduler tick. +pub async fn complete_after_quit_and_wait( + profile: &crate::profile::BrowserProfile, +) -> Option { if !profile.password_protected { - return; + return None; } let keep_decrypted = read_keep_decrypted_setting(); let profile = profile.clone(); - tauri::async_runtime::spawn(async move { - let _ = tokio::task::spawn_blocking(move || { - complete_after_quit_blocking(&profile, keep_decrypted); + tokio::task::spawn_blocking(move || complete_after_quit_blocking(&profile, keep_decrypted)) + .await + .unwrap_or_else(|e| { + log::error!("complete_after_quit_and_wait join error: {e}"); + None }) - .await; - }); } #[cfg(test)] diff --git a/src/components/account-page.tsx b/src/components/account-page.tsx index 267c475..a4d0f77 100644 --- a/src/components/account-page.tsx +++ b/src/components/account-page.tsx @@ -233,8 +233,8 @@ export function AccountPage({
-
- +
+
{isLoggedIn && user ? ( @@ -309,7 +309,7 @@ export function AccountPage({ disabled={isRefreshing} className="h-8 text-xs gap-1.5" > - + {t("account.refresh")} - + {t("account.logout")} @@ -332,7 +332,7 @@ export function AccountPage({ onClick={onOpenSignIn} className="h-8 text-xs gap-1.5" > - + {t("account.signIn")} )} @@ -410,9 +410,9 @@ export function AccountPage({ className="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-muted-foreground hover:text-foreground" > {showToken ? ( - + ) : ( - + )}
diff --git a/src/components/app-update-toast.tsx b/src/components/app-update-toast.tsx index 332a665..381fb64 100644 --- a/src/components/app-update-toast.tsx +++ b/src/components/app-update-toast.tsx @@ -37,7 +37,7 @@ export function AppUpdateToast({ return (
- +
@@ -59,9 +59,9 @@ export function AppUpdateToast({ variant="ghost" size="sm" onClick={onDismiss} - className="p-0 w-6 h-6 shrink-0" + className="p-0 size-6 shrink-0" > - +
@@ -72,7 +72,7 @@ export function AppUpdateToast({ size="sm" className="flex gap-2 items-center text-xs" > - + {t("appUpdate.toast.restartNow")} ) : ( @@ -83,7 +83,7 @@ export function AppUpdateToast({ size="sm" className="flex gap-2 items-center text-xs" > - + {t("appUpdate.toast.viewRelease")} ) diff --git a/src/components/clone-profile-dialog.tsx b/src/components/clone-profile-dialog.tsx index cdb39c0..f0ace55 100644 --- a/src/components/clone-profile-dialog.tsx +++ b/src/components/clone-profile-dialog.tsx @@ -36,16 +36,18 @@ export function CloneProfileDialog({ const inputRef = React.useRef(null); React.useEffect(() => { - if (isOpen && profile) { - const defaultName = `${profile.name} (Copy)`; - setName(defaultName); - setTimeout(() => { - inputRef.current?.focus(); - inputRef.current?.select(); - }, 0); - } else { + if (!(isOpen && profile)) { setIsLoading(false); + return; } + setName(`${profile.name} (Copy)`); + const handle = window.setTimeout(() => { + inputRef.current?.focus(); + inputRef.current?.select(); + }, 0); + return () => { + window.clearTimeout(handle); + }; }, [isOpen, profile]); if (!profile) return null; diff --git a/src/components/cookie-copy-dialog.tsx b/src/components/cookie-copy-dialog.tsx index d7b3a7c..6eb0f68 100644 --- a/src/components/cookie-copy-dialog.tsx +++ b/src/components/cookie-copy-dialog.tsx @@ -335,7 +335,7 @@ export function CookieCopyDialog({ - + {t("cookies.copy.title")} @@ -372,7 +372,7 @@ export function CookieCopyDialog({ disabled={isRunning} >
- {IconComponent && } + {IconComponent && } {profile.name} {isRunning && ( @@ -437,7 +437,7 @@ export function CookieCopyDialog({
- + -
+
) : error ? (
@@ -565,9 +565,9 @@ function DomainRow({ }} > {isExpanded ? ( - + ) : ( - + )} {domain.domain} diff --git a/src/components/cookie-management-dialog.tsx b/src/components/cookie-management-dialog.tsx index bc14219..a354b5c 100644 --- a/src/components/cookie-management-dialog.tsx +++ b/src/components/cookie-management-dialog.tsx @@ -429,7 +429,7 @@ export function CookieManagementDialog({ } }} > - +

{t("cookies.management.dropPrompt")}
@@ -556,7 +556,7 @@ export function CookieManagementDialog({ {isLoadingExportCookies ? (

-
+
) : !exportCookieData || exportCookieData.domains.length === 0 ? (
@@ -645,9 +645,9 @@ function ExportDomainRow({ }} > {isExpanded ? ( - + ) : ( - + )} {domain.domain} diff --git a/src/components/create-profile-dialog.tsx b/src/components/create-profile-dialog.tsx index eca51a7..436d231 100644 --- a/src/components/create-profile-dialog.tsx +++ b/src/components/create-profile-dialog.tsx @@ -1,7 +1,14 @@ "use client"; import { invoke } from "@tauri-apps/api/core"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + useCallback, + useEffect, + useId, + useMemo, + useRef, + useState, +} from "react"; import { useTranslation } from "react-i18next"; import { GoPlus } from "react-icons/go"; import { LuCheck, LuChevronsUpDown } from "react-icons/lu"; @@ -116,6 +123,8 @@ export function CreateProfileDialog({ crossOsUnlocked = false, }: CreateProfileDialogProps) { const { t } = useTranslation(); + const proxyListboxIdAntiDetect = useId(); + const proxyListboxIdRegular = useId(); const [profileName, setProfileName] = useState(""); const [currentStep, setCurrentStep] = useState< "browser-selection" | "browser-config" @@ -605,11 +614,11 @@ export function CreateProfileDialog({ className="flex gap-3 justify-start items-center p-4 w-full h-16 border-2 transition-colors hover:border-primary/50" variant="outline" > -
+
{(() => { const IconComponent = getBrowserIcon("wayfern"); return IconComponent ? ( - + ) : null; })()}
@@ -631,11 +640,11 @@ export function CreateProfileDialog({ className="flex gap-3 justify-start items-center p-4 w-full h-16 border-2 transition-colors hover:border-primary/50" variant="outline" > -
+
{(() => { const IconComponent = getBrowserIcon("camoufox"); return IconComponent ? ( - + ) : null; })()}
@@ -676,9 +685,9 @@ export function CreateProfileDialog({ className="flex gap-3 justify-start items-center p-4 w-full h-16 border-2 transition-colors hover:border-primary/50" variant="outline" > -
+
{IconComponent && ( - + )}
@@ -729,7 +738,7 @@ export function CreateProfileDialog({ {/* Ephemeral Option */}
-
+
-
+
-
+

{t("createProfile.version.fetching")}

@@ -922,7 +931,7 @@ export function CreateProfileDialog({ {/* Camoufox Download Status */} {isLoadingReleaseTypes && (
-
+

{t("createProfile.version.fetching")}

@@ -1041,7 +1050,7 @@ export function CreateProfileDialog({
{isLoadingReleaseTypes && (
-
+

{t("createProfile.version.fetching")}

@@ -1154,7 +1163,7 @@ export function CreateProfileDialog({ }} className="px-2 h-7 text-xs" > - {" "} + {" "} {t("createProfile.proxy.addProxy")}
@@ -1168,6 +1177,7 @@ export function CreateProfileDialog({ variant="outline" role="combobox" aria-expanded={proxyPopoverOpen} + aria-controls={proxyListboxIdAntiDetect} className="w-full justify-between font-normal" > {(() => { @@ -1190,10 +1200,11 @@ export function CreateProfileDialog({ t("createProfile.proxy.noProxy") ); })()} - + @@ -1217,7 +1228,7 @@ export function CreateProfileDialog({ > {isLoadingReleaseTypes && (
-
+

Fetching available versions...

@@ -1520,7 +1531,7 @@ export function CreateProfileDialog({ }} className="px-2 h-7 text-xs" > - {" "} + {" "} {t("createProfile.proxy.addProxy")}
@@ -1534,6 +1545,7 @@ export function CreateProfileDialog({ variant="outline" role="combobox" aria-expanded={proxyPopoverOpen} + aria-controls={proxyListboxIdRegular} className="w-full justify-between font-normal" > {(() => { @@ -1556,10 +1568,11 @@ export function CreateProfileDialog({ t("createProfile.proxy.noProxy") ); })()} - + @@ -1583,7 +1596,7 @@ export function CreateProfileDialog({ > ; + return ; case "error": return ( - + ); case "download": if (stage === "completed") { return ( - + ); } - return ; + return ; case "version-update": return ( - + ); case "fetching": return ( - + ); case "twilight-update": return ( - + ); case "sync-progress": return ( - + ); case "loading": return ( -
+
); default: return ( -
+
); } } @@ -235,7 +235,7 @@ export function UnifiedToast(props: ToastProps) { className="ml-2 p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors flex-shrink-0" aria-label={t("common.buttons.cancel")} > - + )}
@@ -272,7 +272,7 @@ export function UnifiedToast(props: ToastProps) { <>Looking for updates for {progress.current_browser} )}

-
+
{isPending ? ( -
+
) : ( children )} diff --git a/src/components/delete-group-dialog.tsx b/src/components/delete-group-dialog.tsx index 528e8f4..f7352d2 100644 --- a/src/components/delete-group-dialog.tsx +++ b/src/components/delete-group-dialog.tsx @@ -155,13 +155,13 @@ export function DeleteGroupDialog({ setDeleteAction(value as "move" | "delete"); }} > -
+
-
+
@@ -326,7 +326,7 @@ export function GroupManagementDialog({
@@ -383,7 +383,7 @@ export function GroupManagementDialog({ handleEditGroup(group); }} > - + @@ -401,7 +401,7 @@ export function GroupManagementDialog({ handleDeleteGroup(group); }} > - + diff --git a/src/components/home-header.tsx b/src/components/home-header.tsx index 52359e1..4c53184 100644 --- a/src/components/home-header.tsx +++ b/src/components/home-header.tsx @@ -208,9 +208,9 @@ const HomeHeader = ({ behavior: "smooth", }); }} - className="absolute left-0 top-1/2 -translate-y-1/2 z-10 grid place-items-center w-5 h-5 rounded-full bg-card/90 hover:bg-accent text-muted-foreground hover:text-foreground transition-colors shadow-sm" + className="absolute left-0 top-1/2 -translate-y-1/2 z-10 grid place-items-center size-5 rounded-full bg-card/90 hover:bg-accent text-muted-foreground hover:text-foreground transition-colors shadow-sm" > - + )}
- + )}
@@ -304,7 +304,7 @@ const HomeHeader = ({ }} className="pr-7 pl-8 w-52 h-7 text-xs" /> - + {searchQuery ? ( ) : null}
@@ -331,7 +331,7 @@ const HomeHeader = ({ }} className="flex gap-1.5 items-center h-7 px-2.5 text-xs" > - + {t("header.newProfile")} diff --git a/src/components/import-profile-dialog.tsx b/src/components/import-profile-dialog.tsx index 32ff445..98fef32 100644 --- a/src/components/import-profile-dialog.tsx +++ b/src/components/import-profile-dialog.tsx @@ -383,7 +383,7 @@ export function ImportProfileDialog({ >
{IconComponent && ( - + )}
@@ -475,7 +475,7 @@ export function ImportProfileDialog({
{IconComponent && ( - + )} {getBrowserDisplayName(browser)}
@@ -507,7 +507,7 @@ export function ImportProfileDialog({ onClick={() => void handleBrowseFolder()} title={t("importProfile.browseFolderTitle")} > - +

diff --git a/src/components/integrations-dialog.tsx b/src/components/integrations-dialog.tsx index 44c96cb..7ed5731 100644 --- a/src/components/integrations-dialog.tsx +++ b/src/components/integrations-dialog.tsx @@ -225,7 +225,7 @@ export function IntegrationsDialog({ -

+
{t("integrations.apiPortLabel")} -
+
@@ -376,7 +376,7 @@ export function IntegrationsDialog({ -
+
{t("integrations.mcp.url")} -
+
{showMcpToken ? ( - + ) : ( - + )}
diff --git a/src/components/loading-button.tsx b/src/components/loading-button.tsx index b4e0025..99e7aa7 100644 --- a/src/components/loading-button.tsx +++ b/src/components/loading-button.tsx @@ -17,7 +17,7 @@ export const LoadingButton = ({ isLoading, className, ...props }: Props) => { disabled={props.disabled || isLoading} > {isLoading ? ( - + ) : ( props.children )} diff --git a/src/components/location-proxy-dialog.tsx b/src/components/location-proxy-dialog.tsx index ddf9391..1c559c2 100644 --- a/src/components/location-proxy-dialog.tsx +++ b/src/components/location-proxy-dialog.tsx @@ -26,6 +26,10 @@ interface LocationProxyDialogProps { onClose: () => void; } +function LoadingSpinner() { + return ; +} + export function LocationProxyDialog({ isOpen, onClose, @@ -219,10 +223,6 @@ export function LocationProxyDialog({ const cityOptions = cities.map((c) => ({ value: c.code, label: c.name })); const ispOptions = isps.map((i) => ({ value: i.code, label: i.name })); - const LoadingSpinner = () => ( - - ); - return ( diff --git a/src/components/multiple-selector.tsx b/src/components/multiple-selector.tsx index 0da6a29..cbe06bf 100644 --- a/src/components/multiple-selector.tsx +++ b/src/components/multiple-selector.tsx @@ -434,7 +434,7 @@ const MultipleSelector = React.forwardRef< handleUnselect(option); }} > - + ); diff --git a/src/components/permission-dialog.tsx b/src/components/permission-dialog.tsx index 6184168..73e6732 100644 --- a/src/components/permission-dialog.tsx +++ b/src/components/permission-dialog.tsx @@ -131,9 +131,9 @@ export function PermissionDialog({ const getPermissionIcon = (type: PermissionType) => { switch (type) { case "microphone": - return ; + return ; case "camera": - return ; + return ; } }; @@ -195,7 +195,7 @@ export function PermissionDialog({ -
+
{getPermissionIcon(permissionType)}
diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index 25c6d8e..4206263 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -350,11 +350,11 @@ function ExtCell({ disabled={isSaving} className="flex items-center gap-1.5 h-7 px-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-accent/50 rounded transition-colors duration-100 w-full text-left disabled:opacity-50" > - + {label} - + @@ -369,7 +369,7 @@ function ExtCell({ void onPick(null); }} > - {groupId === null && } + {groupId === null && } {meta.t("profiles.table.extDefault")} @@ -382,7 +382,7 @@ function ExtCell({ void onPick(g.id); }} > - {groupId === g.id && } + {groupId === g.id && } {g.name} @@ -445,11 +445,11 @@ function DnsCell({ : meta.t("dnsBlocklist.none") } > - + {level ?? "—"} - + @@ -462,7 +462,7 @@ function DnsCell({ void onPick(null); }} > - {level === null && } + {level === null && } {meta.t("dnsBlocklist.none")} @@ -475,9 +475,7 @@ function DnsCell({ void onPick(l.value); }} > - {level === l.value && ( - - )} + {level === l.value && } {meta.t(l.labelKey)} @@ -1960,7 +1958,7 @@ export function ProfilesDataTable({ return ( - + @@ -1999,14 +1997,14 @@ export function ProfilesDataTable({ sideOffset={4} horizontalOffset={8} > - + { meta.handleCheckboxChange(profile.id, !!value); }} aria-label={t("common.aria.selectRow")} - className="w-4 h-4" + className="size-4" /> @@ -2025,9 +2023,9 @@ export function ProfilesDataTable({ return ( - + {IconComponent && ( - + )} @@ -2047,14 +2045,14 @@ export function ProfilesDataTable({ sideOffset={4} horizontalOffset={8} > - + { meta.handleCheckboxChange(profile.id, !!value); }} aria-label={t("common.aria.selectRow")} - className="w-4 h-4" + className="size-4" /> @@ -2067,7 +2065,7 @@ export function ProfilesDataTable({ sideOffset={4} horizontalOffset={8} > - + @@ -2194,7 +2192,7 @@ export function ProfilesDataTable({ - + @@ -2217,7 +2215,7 @@ export function ProfilesDataTable({ : meta.t("profiles.actions.launch") } className={cn( - "h-7 w-7 p-0 grid place-items-center", + "size-7 p-0 grid place-items-center", !canLaunch && "opacity-50 cursor-not-allowed", canLaunch && "cursor-pointer", isFollower && "border-accent", @@ -2231,11 +2229,11 @@ export function ProfilesDataTable({ } > {isLaunching || isStopping ? ( -
+
) : isRunning ? ( - + ) : ( - + )} @@ -2265,9 +2263,9 @@ export function ProfilesDataTable({ > {meta.t("common.labels.name")} {column.getIsSorted() === "asc" ? ( - + ) : column.getIsSorted() === "desc" ? ( - + ) : null} ); @@ -2382,7 +2380,7 @@ export function ProfilesDataTable({ - + @@ -2606,7 +2604,7 @@ export function ProfilesDataTable({ > - +{" "} + +{" "} {country.name} ))} @@ -2793,11 +2791,11 @@ export function ProfilesDataTable({ {dot.encrypted ? ( ) : ( )} @@ -2818,7 +2816,7 @@ export function ProfilesDataTable({
); diff --git a/src/components/profile-info-dialog.tsx b/src/components/profile-info-dialog.tsx index 38e7389..1cbadf5 100644 --- a/src/components/profile-info-dialog.tsx +++ b/src/components/profile-info-dialog.tsx @@ -97,11 +97,11 @@ interface ProfileInfoDialogProps { function OSIcon({ os }: { os: string }) { switch (os) { case "macos": - return ; + return ; case "windows": - return ; + return ; case "linux": - return ; + return ; default: return null; } @@ -317,7 +317,7 @@ export function ProfileInfoDialog({ const actions: ActionItem[] = [ { - icon: , + icon: , label: t("profiles.actions.viewNetwork"), onClick: () => { handleAction(() => onOpenTrafficDialog?.(profile.id)); @@ -325,7 +325,7 @@ export function ProfileInfoDialog({ disabled: isCrossOs, }, { - icon: , + icon: , label: t("profiles.actions.syncSettings"), onClick: () => { handleAction(() => onOpenProfileSyncDialog?.(profile)); @@ -334,7 +334,7 @@ export function ProfileInfoDialog({ hidden: profile.ephemeral === true, }, { - icon: , + icon: , label: t("profiles.actions.assignToGroup"), onClick: () => { handleAction(() => onAssignProfilesToGroup?.([profile.id])); @@ -343,7 +343,7 @@ export function ProfileInfoDialog({ runningBadge: isRunning, }, { - icon: , + icon: , label: t("profiles.actions.changeFingerprint"), onClick: () => { handleAction(() => onConfigureCamoufox?.(profile)); @@ -353,7 +353,7 @@ export function ProfileInfoDialog({ hidden: !isCamoufoxOrWayfern || !onConfigureCamoufox, }, { - icon: , + icon: , label: t("profiles.synchronizer.launchWithSync"), onClick: () => { handleAction(() => onLaunchWithSync?.(profile)); @@ -363,7 +363,7 @@ export function ProfileInfoDialog({ hidden: profile.browser !== "wayfern" || !onLaunchWithSync, }, { - icon: , + icon: , label: t("profiles.actions.copyCookiesToProfile"), onClick: () => { handleAction(() => onCopyCookiesToProfile?.(profile)); @@ -376,7 +376,7 @@ export function ProfileInfoDialog({ !onCopyCookiesToProfile, }, { - icon: , + icon: , label: t("profileInfo.actions.manageCookies"), onClick: () => { handleAction(() => onOpenCookieManagement?.(profile)); @@ -389,7 +389,7 @@ export function ProfileInfoDialog({ !onOpenCookieManagement, }, { - icon: , + icon: , label: t("profiles.actions.clone"), onClick: () => { handleAction(() => onCloneProfile?.(profile)); @@ -399,7 +399,7 @@ export function ProfileInfoDialog({ hidden: profile.ephemeral === true, }, { - icon: , + icon: , label: t("profileInfo.actions.assignExtensionGroup"), onClick: () => { handleAction(() => onAssignExtensionGroup?.([profile.id])); @@ -409,21 +409,21 @@ export function ProfileInfoDialog({ hidden: profile.ephemeral === true, }, { - icon: , + icon: , label: t("profileInfo.network.bypassRulesTitle"), onClick: () => { handleAction(() => onOpenBypassRules?.(profile)); }, }, { - icon: , + icon: , label: t("dnsBlocklist.title"), onClick: () => { handleAction(() => onOpenDnsBlocklist?.(profile)); }, }, { - icon: , + icon: , label: t("profiles.actions.launchHook"), onClick: () => { handleAction(() => onOpenLaunchHook?.(profile)); @@ -431,7 +431,7 @@ export function ProfileInfoDialog({ hidden: !onOpenLaunchHook, }, { - icon: , + icon: , label: t("profiles.actions.setPassword"), onClick: () => { handleAction(() => onSetPassword?.(profile)); @@ -444,7 +444,7 @@ export function ProfileInfoDialog({ !onSetPassword, }, { - icon: , + icon: , label: t("profiles.actions.changePassword"), onClick: () => { handleAction(() => onChangePassword?.(profile)); @@ -454,7 +454,7 @@ export function ProfileInfoDialog({ hidden: profile.password_protected !== true || !onChangePassword, }, { - icon: , + icon: , label: t("profiles.actions.removePassword"), onClick: () => { handleAction(() => onRemovePassword?.(profile)); @@ -465,7 +465,7 @@ export function ProfileInfoDialog({ destructive: true, }, { - icon: , + icon: , label: t("profiles.actions.delete"), onClick: () => { handleAction(() => onDeleteProfile?.(profile)); @@ -646,12 +646,12 @@ function ProfileInfoLayout({ }[] = [ { id: "overview", - icon: , + icon: , label: t("profileInfo.sections.overview"), }, { id: "fingerprint", - icon: , + icon: , label: t("profileInfo.sections.fingerprint"), badge: profile.password_protected ? t("profileInfo.badges.locked") @@ -660,13 +660,13 @@ function ProfileInfoLayout({ }, { id: "network", - icon: , + icon: , label: t("profileInfo.sections.network"), badge: profile.proxy_id || profile.vpn_id ? networkLabel : undefined, }, { id: "cookies", - icon: , + icon: , label: t("profileInfo.sections.cookies"), badge: cookieCount !== null && cookieCount > 0 @@ -676,26 +676,26 @@ function ProfileInfoLayout({ }, { id: "extensions", - icon: , + icon: , label: t("profileInfo.sections.extensions"), badge: extensionGroupName ?? undefined, hidden: !extensionAction, }, { id: "sync", - icon: , + icon: , label: t("profileInfo.sections.sync"), hidden: !syncAction, }, { id: "automation", - icon: , + icon: , label: t("profileInfo.sections.launchHook"), badge: profile.launch_hook ? t("profileInfo.badges.active") : undefined, }, { id: "security", - icon: , + icon: , label: t("profileInfo.sections.security"), }, ]; @@ -704,7 +704,7 @@ function ProfileInfoLayout({ <> {/* Top bar */}
- +
{t("profileInfo.breadcrumbRoot")} @@ -720,7 +720,7 @@ function ProfileInfoLayout({ disabled={isDisabled} onClick={() => onCloneProfile(profile)} > - + {t("profileInfo.duplicate")} )} @@ -728,9 +728,9 @@ function ProfileInfoLayout({ type="button" aria-label={t("common.buttons.close")} onClick={onClose} - className="grid place-items-center w-7 h-7 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors duration-100" + className="grid place-items-center size-7 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors duration-100" > - +
@@ -773,7 +773,7 @@ function ProfileInfoLayout({ disabled={deleteAction.disabled} className="flex items-center gap-2 h-7 px-2 rounded-md text-xs transition-colors duration-100 text-destructive hover:bg-destructive/10 disabled:opacity-50 disabled:pointer-events-none" > - + {t("profileInfo.sections.delete")} @@ -789,7 +789,7 @@ function ProfileInfoLayout({ {/* Hero */}
- +
@@ -809,7 +809,7 @@ function ProfileInfoLayout({ <> · - + {t("common.status.running")} @@ -826,7 +826,7 @@ function ProfileInfoLayout({ <> · - + {t("profiles.passwordProtectedBadge")} @@ -875,9 +875,9 @@ function ProfileInfoLayout({ aria-label={t("common.buttons.copy")} > {copied ? ( - + ) : ( - + )}
@@ -1082,7 +1082,7 @@ function _SectionAction({ > {icon} {label} - + ); } @@ -1132,7 +1132,7 @@ function LaunchHookEditor({ return (
- + {t("profileInfo.sections.launchHook")}

@@ -1216,7 +1216,7 @@ function SyncSectionInline({ return (

- + {t("profileInfo.sections.sync")}

@@ -1331,7 +1331,7 @@ function NetworkSectionInline({ return (

- + {t("profileInfo.sections.network")}

@@ -1464,7 +1464,7 @@ function ExtensionsSectionInline({ return (

- + {t("profileInfo.sections.extensions")}

@@ -1553,7 +1553,7 @@ function CookiesSectionInline({ return (

- + {t("profileInfo.sections.cookies")}

@@ -1651,7 +1651,7 @@ function FingerprintSectionInline({ return (

- + {t("profileInfo.sections.fingerprint")}

@@ -1705,7 +1705,7 @@ function FingerprintSectionInline({ return (

- + {t("profileInfo.sections.fingerprint")}

@@ -1863,7 +1863,7 @@ function SecuritySectionInline({ return (

- + {t("profileInfo.sections.security")}

@@ -2226,7 +2226,7 @@ export function ProfileBypassRulesDialog({ onClick={handleAddRule} disabled={!newRule.trim()} > - + {t("profileInfo.network.addRule")}

@@ -2249,7 +2249,7 @@ export function ProfileBypassRulesDialog({ }} className="text-muted-foreground hover:text-destructive transition-colors shrink-0" > - +
))} diff --git a/src/components/profile-password-dialog.tsx b/src/components/profile-password-dialog.tsx index fa0d9da..81743c6 100644 --- a/src/components/profile-password-dialog.tsx +++ b/src/components/profile-password-dialog.tsx @@ -53,14 +53,16 @@ export function ProfilePasswordDialog({ const firstInputRef = React.useRef(null); React.useEffect(() => { - if (isOpen) { - setOldPassword(""); - setPassword(""); - setConfirm(""); - setIsSubmitting(false); - setLockoutSecondsRemaining(null); - setTimeout(() => firstInputRef.current?.focus(), 0); - } + if (!isOpen) return; + setOldPassword(""); + setPassword(""); + setConfirm(""); + setIsSubmitting(false); + setLockoutSecondsRemaining(null); + const handle = window.setTimeout(() => firstInputRef.current?.focus(), 0); + return () => { + window.clearTimeout(handle); + }; }, [isOpen]); // Tick down the lockout timer diff --git a/src/components/profile-selector-dialog.tsx b/src/components/profile-selector-dialog.tsx index 6d10f3d..762af84 100644 --- a/src/components/profile-selector-dialog.tsx +++ b/src/components/profile-selector-dialog.tsx @@ -237,7 +237,7 @@ export function ProfileSelectorDialog({ profile.browser, ); return IconComponent ? ( - + ) : null; })()}
diff --git a/src/components/profile-sync-dialog.tsx b/src/components/profile-sync-dialog.tsx index 541e2aa..984a666 100644 --- a/src/components/profile-sync-dialog.tsx +++ b/src/components/profile-sync-dialog.tsx @@ -188,7 +188,7 @@ export function ProfileSyncDialog({ {isCheckingConfig ? (
-
+
) : (
@@ -216,7 +216,7 @@ export function ProfileSyncDialog({ disabled={isSaving} className="grid gap-3" > -
+
-
+
-
+
(null); const [selectionType, setSelectionType] = useState<"none" | "proxy" | "vpn">( "none", @@ -183,6 +184,7 @@ export function ProxyAssignmentDialog({ variant="outline" role="combobox" aria-expanded={proxyPopoverOpen} + aria-controls={proxyListboxId} className="w-full justify-between font-normal" > {(() => { @@ -199,10 +201,14 @@ export function ProxyAssignmentDialog({ ); return proxy ? proxy.name : t("proxyAssignment.noneOption"); })()} - + - + {isCurrentlyChecking ? ( -
+
) : result?.is_valid && result.country_code ? ( @@ -132,7 +132,7 @@ export function ProxyCheckButton({ ) : result && !result.is_valid ? ( ) : ( - + )} diff --git a/src/components/proxy-export-dialog.tsx b/src/components/proxy-export-dialog.tsx index bed672d..a378d98 100644 --- a/src/components/proxy-export-dialog.tsx +++ b/src/components/proxy-export-dialog.tsx @@ -108,13 +108,13 @@ export function ProxyExportDialog({ isOpen, onClose }: ProxyExportDialogProps) { }} className="flex gap-4" > -
+
-
+
@@ -473,7 +473,7 @@ export function ProxyManagementDialog({ onClick={handleCreateProxy} className="flex gap-2 items-center" > - + {t("proxies.management.create")}
@@ -519,7 +519,7 @@ export function ProxyManagementDialog({
- + @@ -627,7 +627,7 @@ export function ProxyManagementDialog({ (proxyUsage[proxy.id] ?? 0) > 0 } > - + @@ -681,7 +681,7 @@ export function ProxyManagementDialog({ }} className="flex gap-2 items-center" > - + {t("common.buttons.import")}
@@ -690,7 +690,7 @@ export function ProxyManagementDialog({ onClick={handleCreateVpn} className="flex gap-2 items-center" > - + {t("proxies.management.create")}
@@ -738,7 +738,7 @@ export function ProxyManagementDialog({
- + @@ -834,7 +834,7 @@ export function ProxyManagementDialog({ (vpnUsage[vpn.id] ?? 0) > 0 } > - + diff --git a/src/components/rail-nav.tsx b/src/components/rail-nav.tsx index 0a12543..fa9896c 100644 --- a/src/components/rail-nav.tsx +++ b/src/components/rail-nav.tsx @@ -290,7 +290,7 @@ export function RailNav({ currentPage, onNavigate }: RailNavProps) { ref={logoRef} type="button" aria-label={t("header.donutLogo")} - className="grid place-items-center w-7 h-7 rounded-md cursor-pointer select-none text-foreground bg-transparent" + className="grid place-items-center size-7 rounded-md cursor-pointer select-none text-foreground bg-transparent" onClick={handleClick} onPointerDown={() => { setIsPressed(true); @@ -322,12 +322,12 @@ export function RailNav({ currentPage, onNavigate }: RailNavProps) { "animate-[wiggle_0.3s_ease-in-out]", )} > - + ) : ( -
+
)}
@@ -345,7 +345,7 @@ export function RailNav({ currentPage, onNavigate }: RailNavProps) { aria-label={t(labelKey)} aria-current={active ? "page" : undefined} className={cn( - "relative grid place-items-center w-7 h-7 rounded-md transition-colors duration-100", + "relative grid place-items-center size-7 rounded-md transition-colors duration-100", active ? "text-foreground bg-accent" : "text-muted-foreground hover:text-card-foreground hover:bg-accent/50", @@ -357,7 +357,7 @@ export function RailNav({ currentPage, onNavigate }: RailNavProps) { className="absolute left-[-7px] top-1.5 bottom-1.5 w-[2px] rounded-full bg-foreground" /> )} - + {t(labelKey)} @@ -377,13 +377,13 @@ export function RailNav({ currentPage, onNavigate }: RailNavProps) { aria-label={t("rail.more.label")} aria-expanded={moreOpen} className={cn( - "grid place-items-center w-7 h-7 rounded-md transition-colors duration-100", + "grid place-items-center size-7 rounded-md transition-colors duration-100", moreOpen ? "text-foreground bg-accent" : "text-muted-foreground hover:text-card-foreground hover:bg-accent/50", )} > - + {t("rail.more.label")} @@ -399,7 +399,7 @@ export function RailNav({ currentPage, onNavigate }: RailNavProps) { aria-label={t("rail.settings")} aria-current={currentPage === "settings" ? "page" : undefined} className={cn( - "relative grid place-items-center w-7 h-7 rounded-md transition-colors duration-100", + "relative grid place-items-center size-7 rounded-md transition-colors duration-100", currentPage === "settings" ? "text-foreground bg-accent" : "text-muted-foreground hover:text-card-foreground hover:bg-accent/50", @@ -411,7 +411,7 @@ export function RailNav({ currentPage, onNavigate }: RailNavProps) { className="absolute left-[-7px] top-1.5 bottom-1.5 w-[2px] rounded-full bg-foreground" /> )} - + {t("rail.settings")} @@ -438,8 +438,8 @@ export function RailNav({ currentPage, onNavigate }: RailNavProps) { }} className="flex items-center gap-2 w-full px-2 py-1.5 rounded-md hover:bg-accent transition-colors duration-100 text-left" > - - + + diff --git a/src/components/release-type-selector.tsx b/src/components/release-type-selector.tsx index 7b7d29b..b7d0541 100644 --- a/src/components/release-type-selector.tsx +++ b/src/components/release-type-selector.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useId, useState } from "react"; import { useTranslation } from "react-i18next"; import { LuCheck, LuChevronsUpDown, LuDownload } from "react-icons/lu"; import { LoadingButton } from "@/components/loading-button"; @@ -44,6 +44,7 @@ export function ReleaseTypeSelector({ }: ReleaseTypeSelectorProps) { const { t } = useTranslation(); const [popoverOpen, setPopoverOpen] = useState(false); + const listboxId = useId(); const effectivePlaceholder = placeholder ?? t("releaseTypeSelector.placeholder"); @@ -91,13 +92,14 @@ export function ReleaseTypeSelector({ variant="outline" role="combobox" aria-expanded={popoverOpen} + aria-controls={listboxId} className="justify-between w-full" > {selectedDisplayText} - + - + {t("releaseTypeSelector.noReleaseTypes")} @@ -126,7 +128,7 @@ export function ReleaseTypeSelector({ > - + {isDownloading ? t("releaseTypeSelector.downloading") : t("releaseTypeSelector.downloadBrowser")} diff --git a/src/components/settings-dialog.tsx b/src/components/settings-dialog.tsx index e61a9ac..f402932 100644 --- a/src/components/settings-dialog.tsx +++ b/src/components/settings-dialog.tsx @@ -165,9 +165,9 @@ export function SettingsDialog({ const getPermissionIcon = useCallback((type: PermissionType) => { switch (type) { case "microphone": - return ; + return ; case "camera": - return ; + return ; } }, []); @@ -738,7 +738,7 @@ export function SettingsDialog({ @@ -434,19 +434,19 @@ export function SyncConfigDialog({ {connectionStatus === "testing" && (
-
+
{t("sync.status.syncing")}
)} {connectionStatus === "connected" && (
-
+
{t("sync.status.connected")}
)} {connectionStatus === "error" && (
-
+
{t("sync.status.disconnected")}
)} diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx index e1b7543..d3c1daa 100644 --- a/src/components/theme-provider.tsx +++ b/src/components/theme-provider.tsx @@ -108,24 +108,28 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) { // Re-apply custom theme after mount useEffect(() => { - if (!isLoading && theme === "custom") { - const reapplyCustomTheme = async () => { - try { - const { invoke } = await import("@tauri-apps/api/core"); - const settings = await invoke("get_app_settings"); - if (settings?.theme === "custom" && settings.custom_theme) { - applyThemeColors(settings.custom_theme); - } - } catch (error) { - console.warn("Failed to reapply custom theme:", error); - } - }; - setTimeout(() => { - void reapplyCustomTheme(); - }, 100); - } else if (!isLoading) { + if (isLoading) return; + if (theme !== "custom") { clearThemeColors(); + return; } + const reapplyCustomTheme = async () => { + try { + const { invoke } = await import("@tauri-apps/api/core"); + const settings = await invoke("get_app_settings"); + if (settings?.theme === "custom" && settings.custom_theme) { + applyThemeColors(settings.custom_theme); + } + } catch (error) { + console.warn("Failed to reapply custom theme:", error); + } + }; + const handle = window.setTimeout(() => { + void reapplyCustomTheme(); + }, 100); + return () => { + window.clearTimeout(handle); + }; }, [isLoading, theme]); // Listen for system theme changes when in "system" mode diff --git a/src/components/traffic-details-dialog.tsx b/src/components/traffic-details-dialog.tsx index 70b63eb..9e2603e 100644 --- a/src/components/traffic-details-dialog.tsx +++ b/src/components/traffic-details-dialog.tsx @@ -398,7 +398,7 @@ export function TrafficDetailsDialog({
@@ -407,7 +407,7 @@ export function TrafficDetailsDialog({
diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx index c7f8bde..3ebae87 100644 --- a/src/components/ui/chart.tsx +++ b/src/components/ui/chart.tsx @@ -229,7 +229,7 @@ const ChartTooltipContent = React.forwardRef< className={cn( "shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", { - "h-2.5 w-2.5": indicator === "dot", + "size-2.5": indicator === "dot", "w-1": indicator === "line", "w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed", @@ -321,7 +321,7 @@ const ChartLegendContent = React.forwardRef< ) : (
- + ); }; @@ -315,7 +315,7 @@ export const ColorPickerAlpha = ({
- + ); }; diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx index 52ffecc..a4963d7 100644 --- a/src/components/ui/combobox.tsx +++ b/src/components/ui/combobox.tsx @@ -47,6 +47,7 @@ export function Combobox({ }: ComboboxProps) { const { t } = useTranslation(); const [open, setOpen] = React.useState(false); + const listboxId = React.useId(); const resolvedPlaceholder = placeholder ?? t("common.buttons.select"); const resolvedSearchPlaceholder = @@ -59,16 +60,17 @@ export function Combobox({ variant="outline" role="combobox" aria-expanded={open} + aria-controls={listboxId} disabled={disabled} className={cn("w-full justify-between", className)} > {value ? options.find((option) => option.value === value)?.label : resolvedPlaceholder} - + - + @@ -85,7 +87,7 @@ export function Combobox({ > diff --git a/src/components/ui/copy-to-clipboard.tsx b/src/components/ui/copy-to-clipboard.tsx index 9f58e33..73a9b30 100644 --- a/src/components/ui/copy-to-clipboard.tsx +++ b/src/components/ui/copy-to-clipboard.tsx @@ -55,12 +55,12 @@ export function CopyToClipboard({ {copied ? t("common.srOnly.copied") : t("common.srOnly.copy")} diff --git a/src/components/ui/radio-group.tsx b/src/components/ui/radio-group.tsx index e7f2fcb..e2d957c 100644 --- a/src/components/ui/radio-group.tsx +++ b/src/components/ui/radio-group.tsx @@ -28,13 +28,13 @@ const RadioGroupItem = React.forwardRef< - + ); diff --git a/src/components/vpn-check-button.tsx b/src/components/vpn-check-button.tsx index 7ab3c49..490b9da 100644 --- a/src/components/vpn-check-button.tsx +++ b/src/components/vpn-check-button.tsx @@ -70,18 +70,18 @@ export function VpnCheckButton({ diff --git a/src/components/vpn-import-dialog.tsx b/src/components/vpn-import-dialog.tsx index 4a6a214..781accf 100644 --- a/src/components/vpn-import-dialog.tsx +++ b/src/components/vpn-import-dialog.tsx @@ -219,7 +219,7 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) { } }} > - +

{t("vpns.import.dropzonePrompt")}

@@ -244,7 +244,7 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) { {step === "vpn-preview" && vpnPreview && (
- +
{t("vpns.import.configurationLabel", { @@ -292,7 +292,7 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) { > {vpnImportResult.success ? (
- +
{t("vpns.import.importedSuccess")} diff --git a/src/components/wayfern-config-form.tsx b/src/components/wayfern-config-form.tsx index cf91a73..d4addd4 100644 --- a/src/components/wayfern-config-form.tsx +++ b/src/components/wayfern-config-form.tsx @@ -228,7 +228,7 @@ export function WayfernConfigForm({ {/* Randomize Fingerprint Option */}
-
+
-
+
{t("fingerprint.battery")}
-
+
-
+
-
+
{description}

-
+