diff --git a/.claude/skills/ui-ux-pro-max/SKILL.md b/.claude/skills/ui-ux-pro-max/SKILL.md
index a937fb2..4b2ec8f 100644
--- a/.claude/skills/ui-ux-pro-max/SKILL.md
+++ b/.claude/skills/ui-ux-pro-max/SKILL.md
@@ -197,6 +197,7 @@ These are frequently overlooked issues that make UI look unprofessional:
Before delivering UI code, verify these items:
### Visual Quality
+
- [ ] No emojis used as icons (use SVG instead)
- [ ] All icons from consistent icon set (Heroicons/Lucide)
- [ ] Brand logos are correct (verified from Simple Icons)
@@ -204,24 +205,28 @@ Before delivering UI code, verify these items:
- [ ] Use theme colors directly (bg-primary) not var() wrapper
### Interaction
+
- [ ] All clickable elements have `cursor-pointer`
- [ ] Hover states provide clear visual feedback
- [ ] Transitions are smooth (150-300ms)
- [ ] Focus states visible for keyboard navigation
### Light/Dark Mode
+
- [ ] Light mode text has sufficient contrast (4.5:1 minimum)
- [ ] Glass/transparent elements visible in light mode
- [ ] Borders visible in both modes
- [ ] Test both modes before delivery
### Layout
+
- [ ] Floating elements have proper spacing from edges
- [ ] No content hidden behind fixed navbars
- [ ] Responsive at 320px, 768px, 1024px, 1440px
- [ ] No horizontal scroll on mobile
### Accessibility
+
- [ ] All images have alt text
- [ ] Form inputs have labels
- [ ] Color is not the only indicator
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ad05943..6659250 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -27,6 +27,7 @@ Or enter the dev shell: `nix develop`
### Manual Setup
Requirements:
+
- Node.js (see `.node-version`)
- pnpm
- Rust + Cargo (latest stable)
@@ -47,6 +48,7 @@ pnpm format && pnpm lint && pnpm test
```
This runs:
+
- **Biome** — JS/TS linting and formatting
- **Clippy + rustfmt** — Rust linting and formatting
- **typos** — Spellcheck (allowlist in `_typos.toml`)
diff --git a/donut-sync/README.md b/donut-sync/README.md
index d30c946..bb15dfc 100644
--- a/donut-sync/README.md
+++ b/donut-sync/README.md
@@ -2,8 +2,6 @@
-[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
-[circleci-url]: https://circleci.com/gh/nestjs/nest
A progressive Node.js framework for building efficient and scalable server-side applications.
@@ -28,33 +26,33 @@
## Project setup
```bash
-$ pnpm install
+pnpm install
```
## Compile and run the project
```bash
# development
-$ pnpm run start
+pnpm run start
# watch mode
-$ pnpm run start:dev
+pnpm run start:dev
# production mode
-$ pnpm run start:prod
+pnpm run start:prod
```
## Run tests
```bash
# unit tests
-$ pnpm run test
+pnpm run test
# e2e tests
-$ pnpm run test:e2e
+pnpm run test:e2e
# test coverage
-$ pnpm run test:cov
+pnpm run test:cov
```
## Deployment
@@ -64,8 +62,8 @@ When you're ready to deploy your NestJS application to production, there are som
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
-$ pnpm install -g @nestjs/mau
-$ mau deploy
+pnpm install -g @nestjs/mau
+mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
diff --git a/donut-sync/src/auth/auth.guard.ts b/donut-sync/src/auth/auth.guard.ts
index 3053527..e23088b 100644
--- a/donut-sync/src/auth/auth.guard.ts
+++ b/donut-sync/src/auth/auth.guard.ts
@@ -38,7 +38,7 @@ export class AuthGuard implements CanActivate {
// Try SYNC_TOKEN first (self-hosted mode)
const expectedToken = this.configService.get("SYNC_TOKEN");
if (expectedToken && token === expectedToken) {
- (request as any).user = {
+ (request as unknown as Record).user = {
mode: "self-hosted",
prefix: "",
teamPrefix: null,
@@ -55,7 +55,7 @@ export class AuthGuard implements CanActivate {
algorithms: ["RS256"],
}) as jwt.JwtPayload;
- (request as any).user = {
+ (request as unknown as Record).user = {
mode: "cloud",
prefix: decoded.prefix || `users/${decoded.sub}/`,
teamPrefix: decoded.teamPrefix || null,
diff --git a/donut-sync/src/sync/sync.controller.ts b/donut-sync/src/sync/sync.controller.ts
index 76df58c..1be8ad6 100644
--- a/donut-sync/src/sync/sync.controller.ts
+++ b/donut-sync/src/sync/sync.controller.ts
@@ -39,7 +39,7 @@ export class SyncController {
constructor(private readonly syncService: SyncService) {}
private getUserContext(req: Request): UserContext {
- return (req as any).user as UserContext;
+ return (req as unknown as Record).user as UserContext;
}
@Post("stat")
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 83a8875..5130abe 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -324,7 +324,7 @@ export default function Home() {
const currentUrl = await getCurrent();
if (currentUrl && currentUrl.length > 0) {
console.log("Startup URL detected:", currentUrl[0]);
- void handleUrlOpen(currentUrl[0]);
+ handleUrlOpen(currentUrl[0]);
}
} catch (error) {
console.error("Failed to check current URL:", error);
@@ -413,13 +413,13 @@ export default function Home() {
// Listen for URL open events from the deep link handler (when app is already running)
await listen("url-open-request", (event) => {
console.log("Received URL open request:", event.payload);
- void handleUrlOpen(event.payload);
+ handleUrlOpen(event.payload);
});
// Listen for show profile selector events
await listen("show-profile-selector", (event) => {
console.log("Received show profile selector request:", event.payload);
- void handleUrlOpen(event.payload);
+ handleUrlOpen(event.payload);
});
// Listen for show create profile dialog events
@@ -437,7 +437,7 @@ export default function Home() {
// Listen for custom logo click events
const handleLogoUrlEvent = (event: CustomEvent) => {
console.log("Received logo URL event:", event.detail);
- void handleUrlOpen(event.detail);
+ handleUrlOpen(event.detail);
};
window.addEventListener(
@@ -995,7 +995,7 @@ export default function Home() {
// Check permissions when they are initialized
useEffect(() => {
if (isInitialized) {
- void checkAllPermissions();
+ checkAllPermissions();
}
}, [isInitialized, checkAllPermissions]);
diff --git a/src/components/cookie-copy-dialog.tsx b/src/components/cookie-copy-dialog.tsx
index 0fa16fd..2532032 100644
--- a/src/components/cookie-copy-dialog.tsx
+++ b/src/components/cookie-copy-dialog.tsx
@@ -50,12 +50,13 @@ interface CookieCopyDialogProps {
onCopyComplete?: () => void;
}
-interface SelectionState {
- [domain: string]: {
+type SelectionState = Record<
+ string,
+ {
allSelected: boolean;
cookies: Set;
- };
-}
+ }
+>;
export function CookieCopyDialog({
isOpen,
@@ -148,7 +149,7 @@ export function CookieCopyDialog({
(domain: string, cookies: UnifiedCookie[]) => {
setSelection((prev) => {
const current = prev[domain];
- const allSelected = current.allSelected || false;
+ const allSelected = current.allSelected;
if (allSelected) {
const newSelection = { ...prev };
@@ -171,7 +172,7 @@ export function CookieCopyDialog({
const toggleCookie = useCallback(
(domain: string, cookieName: string, totalCookies: number) => {
setSelection((prev) => {
- const current = prev[domain] || {
+ const current = prev[domain] ?? {
allSelected: false,
cookies: new Set(),
};
@@ -503,8 +504,8 @@ function DomainRow({
onToggleExpand,
}: DomainRowProps) {
const domainSelection = selection[domain.domain];
- const isAllSelected = domainSelection.allSelected || false;
- const selectedCount = domainSelection.cookies.size || 0;
+ const isAllSelected = domainSelection.allSelected;
+ const selectedCount = domainSelection.cookies.size;
const isPartial =
selectedCount > 0 && selectedCount < domain.cookie_count && !isAllSelected;
@@ -539,8 +540,7 @@ function DomainRow({
{isExpanded && (
{domain.cookies.map((cookie) => {
- const isSelected =
- domainSelection.cookies.has(cookie.name) || false;
+ const isSelected = domainSelection.cookies.has(cookie.name);
return (
;
- };
-}
+ }
+>;
const countCookies = (content: string): number => {
const trimmed = content.trim();
@@ -329,7 +330,7 @@ export function CookieManagementDialog({
const toggleCookie = useCallback(
(domain: string, cookieName: string, totalCookies: number) => {
setExportSelection((prev) => {
- const current = prev[domain] || {
+ const current = prev[domain] ?? {
allSelected: false,
cookies: new Set
(),
};
@@ -591,8 +592,8 @@ function ExportDomainRow({
onToggleExpand,
}: ExportDomainRowProps) {
const domainSelection = selection[domain.domain];
- const isAllSelected = domainSelection.allSelected || false;
- const selectedCount = domainSelection.cookies.size || 0;
+ const isAllSelected = domainSelection.allSelected;
+ const selectedCount = domainSelection.cookies.size;
const isPartial =
selectedCount > 0 && selectedCount < domain.cookie_count && !isAllSelected;
@@ -627,8 +628,7 @@ function ExportDomainRow({
{isExpanded && (
{domain.cookies.map((cookie) => {
- const isSelected =
- domainSelection.cookies.has(cookie.name) || false;
+ const isSelected = domainSelection.cookies.has(cookie.name);
return (
handleDownload("wayfern")}
+ onClick={() => {
+ void handleDownload("wayfern");
+ }}
isLoading={isBrowserCurrentlyDownloading(
"wayfern",
)}
@@ -856,7 +858,9 @@ export function CreateProfileDialog({
})()}
handleDownload("camoufox")}
+ onClick={() => {
+ void handleDownload("camoufox");
+ }}
isLoading={isBrowserCurrentlyDownloading(
"camoufox",
)}
@@ -963,9 +967,9 @@ export function CreateProfileDialog({
})()}
- handleDownload(selectedBrowser)
- }
+ onClick={() => {
+ void handleDownload(selectedBrowser);
+ }}
isLoading={isBrowserCurrentlyDownloading(
selectedBrowser,
)}
@@ -1163,7 +1167,7 @@ export function CreateProfileDialog({
{t("extensions.extensionGroup")}
{
setSelectedExtensionGroupId(
val === "none" ? undefined : val,
@@ -1209,7 +1213,7 @@ export function CreateProfileDialog({
!isCreateDisabled &&
!isCreating
) {
- handleCreate();
+ void handleCreate();
}
}}
placeholder="Enter profile name"
@@ -1263,9 +1267,9 @@ export function CreateProfileDialog({
})()}
- handleDownload(selectedBrowser)
- }
+ onClick={() => {
+ void handleDownload(selectedBrowser);
+ }}
isLoading={isBrowserCurrentlyDownloading(
selectedBrowser,
)}
diff --git a/src/components/custom-toast.tsx b/src/components/custom-toast.tsx
index cafa7a2..eacc74f 100644
--- a/src/components/custom-toast.tsx
+++ b/src/components/custom-toast.tsx
@@ -363,15 +363,17 @@ export function UnifiedToast(props: ToastProps) {
>
)}
{action &&
- "onClick" in (action as any) &&
- "label" in (action as any) && (
+ "onClick" in (action as { onClick?: () => void; label?: string }) &&
+ "label" in (action as { onClick?: () => void; label?: string }) && (
void; label: string }).onClick
+ }
>
- {(action as any).label}
+ {(action as { onClick: () => void; label: string }).label}
)}
diff --git a/src/components/data-table-action-bar.tsx b/src/components/data-table-action-bar.tsx
index 3ccbc72..d675fb4 100644
--- a/src/components/data-table-action-bar.tsx
+++ b/src/components/data-table-action-bar.tsx
@@ -46,7 +46,7 @@ function DataTableActionBar({
}, [table]);
const portalContainer =
- portalContainerProp ?? (mounted ? globalThis.document?.body : null);
+ portalContainerProp ?? (mounted ? globalThis.document.body : null);
if (!portalContainer) return null;
diff --git a/src/components/group-assignment-dialog.tsx b/src/components/group-assignment-dialog.tsx
index 04f988e..9b9e30e 100644
--- a/src/components/group-assignment-dialog.tsx
+++ b/src/components/group-assignment-dialog.tsx
@@ -157,7 +157,7 @@ export function GroupAssignmentDialog({
) : (
{
setSelectedGroupId(value === "default" ? null : value);
}}
diff --git a/src/components/multiple-selector.tsx b/src/components/multiple-selector.tsx
index 541f13b..ede4f91 100644
--- a/src/components/multiple-selector.tsx
+++ b/src/components/multiple-selector.tsx
@@ -19,9 +19,7 @@ export interface Option {
/** Group the options by providing key. */
[key: string]: string | boolean | undefined;
}
-interface GroupOption {
- [key: string]: Option[];
-}
+type GroupOption = Record;
interface MultipleSelectorProps {
value?: Option[];
@@ -259,7 +257,7 @@ const MultipleSelector = React.forwardRef<
if (!arrayOptions || onSearch) {
return;
}
- const newOption = transToGroupOption(arrayOptions || [], groupBy);
+ const newOption = transToGroupOption(arrayOptions, groupBy);
if (JSON.stringify(newOption) !== JSON.stringify(options)) {
setOptions(newOption);
}
diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx
index 9c66e12..13679e1 100644
--- a/src/components/profile-data-table.tsx
+++ b/src/components/profile-data-table.tsx
@@ -218,12 +218,12 @@ interface TableMeta {
onLaunchWithSync: (profile: BrowserProfile) => void;
}
-type SyncStatusDot = {
+interface SyncStatusDot {
color: string;
tooltip: string;
animate: boolean;
encrypted: boolean;
-};
+}
function getProfileSyncStatusDot(
profile: BrowserProfile,
@@ -1215,7 +1215,7 @@ export function ProfilesDataTable({
React.useEffect(() => {
if (!browserState.isClient) return;
let unlisten: (() => void) | undefined;
- (async () => {
+ void (async () => {
try {
unlisten = await listen<{ id: string; is_running: boolean }>(
"profile-running-changed",
@@ -1540,7 +1540,11 @@ export function ProfilesDataTable({
// Overflow actions
onAssignProfilesToGroup,
- onCloneProfile,
+ onCloneProfile: onCloneProfile
+ ? (profile: BrowserProfile) => {
+ void onCloneProfile(profile);
+ }
+ : undefined,
onConfigureCamoufox,
onCopyCookiesToProfile,
onOpenCookieManagement,
@@ -1572,7 +1576,11 @@ export function ProfilesDataTable({
// Synchronizer
getProfileSyncInfo: getProfileSyncInfo ?? (() => undefined),
- onLaunchWithSync: onLaunchWithSync ?? (() => {}),
+ onLaunchWithSync:
+ onLaunchWithSync ??
+ (() => {
+ /* empty */
+ }),
}),
[
t,
diff --git a/src/components/profile-info-dialog.tsx b/src/components/profile-info-dialog.tsx
index 93e0579..119bfc2 100644
--- a/src/components/profile-info-dialog.tsx
+++ b/src/components/profile-info-dialog.tsx
@@ -147,7 +147,7 @@ export function ProfileInfoDialog({
setExtensionGroupName(null);
return;
}
- (async () => {
+ void (async () => {
try {
const group = await invoke<{ name: string } | null>(
"get_extension_group_for_profile",
diff --git a/src/components/settings-dialog.tsx b/src/components/settings-dialog.tsx
index 2bcbf76..02c7512 100644
--- a/src/components/settings-dialog.tsx
+++ b/src/components/settings-dialog.tsx
@@ -242,9 +242,9 @@ export function SettingsDialog({
const clearCustomTheme = useCallback(() => {
const root = document.documentElement;
- THEME_VARIABLES.forEach(({ key }) =>
- root.style.removeProperty(key as string),
- );
+ THEME_VARIABLES.forEach(({ key }) => {
+ root.style.removeProperty(key as string);
+ });
}, []);
const loadPermissions = useCallback(() => {
@@ -378,16 +378,13 @@ export function SettingsDialog({
// Apply or clear custom variables only on Save
if (settings.theme === "custom") {
- if (
- customThemeState.colors &&
- Object.keys(customThemeState.colors).length > 0
- ) {
+ if (Object.keys(customThemeState.colors).length > 0) {
try {
const root = document.documentElement;
// Clear any previous custom vars first
- THEME_VARIABLES.forEach(({ key }) =>
- root.style.removeProperty(key as string),
- );
+ THEME_VARIABLES.forEach(({ key }) => {
+ root.style.removeProperty(key as string);
+ });
Object.entries(customThemeState.colors).forEach(([k, v]) => {
root.style.setProperty(k, v, "important");
});
@@ -398,9 +395,9 @@ export function SettingsDialog({
} else {
try {
const root = document.documentElement;
- THEME_VARIABLES.forEach(({ key }) =>
- root.style.removeProperty(key as string),
- );
+ THEME_VARIABLES.forEach(({ key }) => {
+ root.style.removeProperty(key as string);
+ });
} catch {
/* empty */
}
diff --git a/src/components/shared-camoufox-config-form.tsx b/src/components/shared-camoufox-config-form.tsx
index b94d199..cc46f65 100644
--- a/src/components/shared-camoufox-config-form.tsx
+++ b/src/components/shared-camoufox-config-form.tsx
@@ -77,7 +77,7 @@ function ObjectEditor({
const [jsonString, setJsonString] = useState("");
useEffect(() => {
- setJsonString(JSON.stringify(value || {}, null, 2));
+ setJsonString(JSON.stringify(value ?? {}, null, 2));
}, [value]);
const handleChange = (newValue: string) => {
@@ -144,7 +144,7 @@ export function SharedCamoufoxConfigForm({
const handleGenerateFingerprint = async () => {
if (!profileVersion) return;
- const browser = profileBrowser || browserType || "camoufox";
+ const browser = profileBrowser ?? browserType ?? "camoufox";
setIsGeneratingFingerprint(true);
try {
const configJson = JSON.stringify(config);
@@ -917,7 +917,7 @@ export function SharedCamoufoxConfigForm({
(fingerprintConfig["webGl:parameters"] as Record<
string,
unknown
- >) || {}
+ >) ?? {}
}
onChange={(value) => {
updateFingerprintConfig("webGl:parameters", value);
@@ -934,7 +934,7 @@ export function SharedCamoufoxConfigForm({
(fingerprintConfig["webGl2:parameters"] as Record<
string,
unknown
- >) || {}
+ >) ?? {}
}
onChange={(value) => {
updateFingerprintConfig("webGl2:parameters", value);
@@ -951,7 +951,7 @@ export function SharedCamoufoxConfigForm({
(fingerprintConfig["webGl:shaderPrecisionFormats"] as Record<
string,
unknown
- >) || {}
+ >) ?? {}
}
onChange={(value) => {
updateFingerprintConfig("webGl:shaderPrecisionFormats", value);
@@ -968,7 +968,7 @@ export function SharedCamoufoxConfigForm({
(fingerprintConfig["webGl2:shaderPrecisionFormats"] as Record<
string,
unknown
- >) || {}
+ >) ?? {}
}
onChange={(value) => {
updateFingerprintConfig("webGl2:shaderPrecisionFormats", value);
diff --git a/src/components/sync-follower-dialog.tsx b/src/components/sync-follower-dialog.tsx
index aa2822b..e2edb87 100644
--- a/src/components/sync-follower-dialog.tsx
+++ b/src/components/sync-follower-dialog.tsx
@@ -162,7 +162,9 @@ export function SyncFollowerDialog({
!selectedIds.has(profile.id),
);
}}
- onKeyDown={() => {}}
+ onKeyDown={() => {
+ /* empty */
+ }}
role="button"
tabIndex={0}
>
diff --git a/src/components/traffic-details-dialog.tsx b/src/components/traffic-details-dialog.tsx
index 04a89f6..6a1dcb1 100644
--- a/src/components/traffic-details-dialog.tsx
+++ b/src/components/traffic-details-dialog.tsx
@@ -173,7 +173,9 @@ export function TrafficDetailsDialog({
};
void fetchStats();
- const interval = setInterval(fetchStats, 2000);
+ const interval = setInterval(() => {
+ void fetchStats();
+ }, 2000);
return () => {
clearInterval(interval);
diff --git a/src/components/ui/color-picker.tsx b/src/components/ui/color-picker.tsx
index b39bfc0..2e8dee5 100644
--- a/src/components/ui/color-picker.tsx
+++ b/src/components/ui/color-picker.tsx
@@ -363,7 +363,7 @@ export type ColorPickerOutputProps = ComponentProps;
const formats = ["hex", "rgb", "css", "hsl"];
export const ColorPickerOutput = ({
- className,
+ className: _className,
...props
}: ColorPickerOutputProps) => {
const { mode, setMode } = useColorPicker();
diff --git a/src/components/ui/highlight.tsx b/src/components/ui/highlight.tsx
index 2e81204..ed4389e 100644
--- a/src/components/ui/highlight.tsx
+++ b/src/components/ui/highlight.tsx
@@ -43,7 +43,7 @@ interface HighlightContextType {
}
const HighlightContext = React.createContext<
- HighlightContextType | undefined
+ HighlightContextType | undefined
>(undefined);
function useHighlight(): HighlightContextType {
@@ -419,7 +419,7 @@ function HighlightItem({
const Component = as ?? "div";
const element = children as React.ReactElement;
const childValue =
- id ?? value ?? element.props?.["data-value"] ?? element.props?.id ?? itemId;
+ id ?? value ?? element.props["data-value"] ?? element.props.id ?? itemId;
const isActive = activeValue === childValue;
const isDisabled = disabled === undefined ? contextDisabled : disabled;
const itemTransition = transition ?? contextTransition;
diff --git a/src/components/wayfern-config-form.tsx b/src/components/wayfern-config-form.tsx
index 328f5d7..9a52416 100644
--- a/src/components/wayfern-config-form.tsx
+++ b/src/components/wayfern-config-form.tsx
@@ -84,7 +84,7 @@ export function WayfernConfigForm({
try {
const configJson = JSON.stringify(config);
const result = await invoke("generate_sample_fingerprint", {
- browser: profileBrowser || "wayfern",
+ browser: profileBrowser ?? "wayfern",
version: profileVersion,
configJson,
});
diff --git a/src/hooks/use-auto-height.tsx b/src/hooks/use-auto-height.tsx
index 8a65a3e..2867e7e 100644
--- a/src/hooks/use-auto-height.tsx
+++ b/src/hooks/use-auto-height.tsx
@@ -22,18 +22,15 @@ export function useAutoHeight(
const el = ref.current;
if (!el) return 0;
- const base = el.getBoundingClientRect().height ?? 0;
+ const base = el.getBoundingClientRect().height;
let extra = 0;
if (options.includeParentBox && el.parentElement) {
const cs = getComputedStyle(el.parentElement);
- const paddingY =
- (parseFloat(cs.paddingTop ?? "0") ?? 0) +
- (parseFloat(cs.paddingBottom ?? "0") ?? 0);
+ const paddingY = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom);
const borderY =
- (parseFloat(cs.borderTopWidth ?? "0") ?? 0) +
- (parseFloat(cs.borderBottomWidth ?? "0") ?? 0);
+ parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth);
const isBorderBox = cs.boxSizing === "border-box";
if (isBorderBox) {
extra += paddingY + borderY;
@@ -42,20 +39,16 @@ export function useAutoHeight(
if (options.includeSelfBox) {
const cs = getComputedStyle(el);
- const paddingY =
- (parseFloat(cs.paddingTop ?? "0") ?? 0) +
- (parseFloat(cs.paddingBottom ?? "0") ?? 0);
+ const paddingY = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom);
const borderY =
- (parseFloat(cs.borderTopWidth ?? "0") ?? 0) +
- (parseFloat(cs.borderBottomWidth ?? "0") ?? 0);
+ parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth);
const isBorderBox = cs.boxSizing === "border-box";
if (isBorderBox) {
extra += paddingY + borderY;
}
}
- const dpr =
- typeof window !== "undefined" ? (window.devicePixelRatio ?? 1) : 1;
+ const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1;
const total = Math.ceil((base + extra) * dpr) / dpr;
return total;
diff --git a/src/hooks/use-browser-download.ts b/src/hooks/use-browser-download.ts
index fe1c972..455944a 100644
--- a/src/hooks/use-browser-download.ts
+++ b/src/hooks/use-browser-download.ts
@@ -367,7 +367,9 @@ export function useBrowserDownload() {
? loadDownloadedVersions("camoufox")
: Promise.resolve([]),
]);
- } catch {}
+ } catch {
+ /* empty */
+ }
showDownloadToast(browserName, progress.version, "completed");
setDownloadProgress(null);
}
diff --git a/src/hooks/use-cloud-auth.ts b/src/hooks/use-cloud-auth.ts
index aa3d281..a0deb76 100644
--- a/src/hooks/use-cloud-auth.ts
+++ b/src/hooks/use-cloud-auth.ts
@@ -50,7 +50,7 @@ export function useCloudAuth(): UseCloudAuthReturn {
};
}, [loadUser]);
- const requestOtp = useCallback(async (email: string): Promise => {
+ const requestOtp = useCallback((email: string): Promise => {
return invoke("cloud_request_otp", { email });
}, []);
diff --git a/src/hooks/use-controlled-state.tsx b/src/hooks/use-controlled-state.tsx
index bc9fe6b..6903781 100644
--- a/src/hooks/use-controlled-state.tsx
+++ b/src/hooks/use-controlled-state.tsx
@@ -5,7 +5,7 @@ interface CommonControlledStateProps {
defaultValue?: T;
}
-export function useControlledState(
+export function useControlledState(
props: CommonControlledStateProps & {
onChange?: (value: T, ...args: Rest) => void;
},
diff --git a/src/hooks/use-proxy-events.ts b/src/hooks/use-proxy-events.ts
index 43d06f2..14c5002 100644
--- a/src/hooks/use-proxy-events.ts
+++ b/src/hooks/use-proxy-events.ts
@@ -17,7 +17,7 @@ export function useProxyEvents() {
// Load proxy usage (how many profiles are using each proxy)
const loadProxyUsage = useCallback(async () => {
try {
- const profiles = await invoke>(
+ const profiles = await invoke<{ proxy_id?: string }[]>(
"list_browser_profiles",
);
const counts: Record = {};
diff --git a/src/hooks/use-vpn-events.ts b/src/hooks/use-vpn-events.ts
index 1b701c7..afc21e6 100644
--- a/src/hooks/use-vpn-events.ts
+++ b/src/hooks/use-vpn-events.ts
@@ -16,7 +16,7 @@ export function useVpnEvents() {
const loadVpnUsage = useCallback(async () => {
try {
- const profiles = await invoke>(
+ const profiles = await invoke<{ vpn_id?: string }[]>(
"list_browser_profiles",
);
const counts: Record = {};
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index ab8e3d1..246a58e 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -524,7 +524,29 @@
"selectedCount_plural": "{{count}} cookies selected"
},
"success": "Cookies copied successfully",
- "error": "Failed to copy cookies"
+ "error": "Failed to copy cookies",
+ "management": {
+ "title": "Cookie Management",
+ "menuItem": "Cookie Management"
+ },
+ "import": {
+ "title": "Import Cookies",
+ "description": "Import cookies from a Netscape or JSON format file.",
+ "selectFile": "Choose File",
+ "preview": "{{count}} cookies found",
+ "success": "Successfully imported {{imported}} cookies ({{replaced}} replaced)",
+ "error": "Failed to import cookies",
+ "proFeature": "Cookie import is a Pro feature"
+ },
+ "export": {
+ "title": "Export Cookies",
+ "description": "Export cookies from this profile.",
+ "formatLabel": "Format",
+ "netscape": "Netscape TXT",
+ "json": "JSON",
+ "success": "Cookies exported successfully",
+ "error": "Failed to export cookies"
+ }
},
"toasts": {
"success": {
@@ -724,30 +746,6 @@
"cannotLaunch": "This profile was created on {{os}} and is not supported on this system",
"cannotModify": "Cannot modify sync settings for a cross-OS profile"
},
- "cookies": {
- "management": {
- "title": "Cookie Management",
- "menuItem": "Cookie Management"
- },
- "import": {
- "title": "Import Cookies",
- "description": "Import cookies from a Netscape or JSON format file.",
- "selectFile": "Choose File",
- "preview": "{{count}} cookies found",
- "success": "Successfully imported {{imported}} cookies ({{replaced}} replaced)",
- "error": "Failed to import cookies",
- "proFeature": "Cookie import is a Pro feature"
- },
- "export": {
- "title": "Export Cookies",
- "description": "Export cookies from this profile.",
- "formatLabel": "Format",
- "netscape": "Netscape TXT",
- "json": "JSON",
- "success": "Cookies exported successfully",
- "error": "Failed to export cookies"
- }
- },
"profileInfo": {
"title": "Profile Details",
"tabs": {
diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json
index f158932..3cd58f8 100644
--- a/src/i18n/locales/es.json
+++ b/src/i18n/locales/es.json
@@ -524,7 +524,29 @@
"selectedCount_plural": "{{count}} cookies seleccionadas"
},
"success": "Cookies copiadas exitosamente",
- "error": "Error al copiar cookies"
+ "error": "Error al copiar cookies",
+ "management": {
+ "title": "Gestión de Cookies",
+ "menuItem": "Gestión de Cookies"
+ },
+ "import": {
+ "title": "Importar Cookies",
+ "description": "Importar cookies desde un archivo en formato Netscape o JSON.",
+ "selectFile": "Elegir Archivo",
+ "preview": "{{count}} cookies encontradas",
+ "success": "Se importaron {{imported}} cookies exitosamente ({{replaced}} reemplazadas)",
+ "error": "Error al importar cookies",
+ "proFeature": "La importación de cookies es una función Pro"
+ },
+ "export": {
+ "title": "Exportar Cookies",
+ "description": "Exportar cookies de este perfil.",
+ "formatLabel": "Formato",
+ "netscape": "Netscape TXT",
+ "json": "JSON",
+ "success": "Cookies exportadas exitosamente",
+ "error": "Error al exportar cookies"
+ }
},
"toasts": {
"success": {
@@ -724,30 +746,6 @@
"cannotLaunch": "Este perfil fue creado en {{os}} y no es compatible con este sistema",
"cannotModify": "No se pueden modificar los ajustes de sincronización de un perfil de otro sistema operativo"
},
- "cookies": {
- "management": {
- "title": "Gestión de Cookies",
- "menuItem": "Gestión de Cookies"
- },
- "import": {
- "title": "Importar Cookies",
- "description": "Importar cookies desde un archivo en formato Netscape o JSON.",
- "selectFile": "Elegir Archivo",
- "preview": "{{count}} cookies encontradas",
- "success": "Se importaron {{imported}} cookies exitosamente ({{replaced}} reemplazadas)",
- "error": "Error al importar cookies",
- "proFeature": "La importación de cookies es una función Pro"
- },
- "export": {
- "title": "Exportar Cookies",
- "description": "Exportar cookies de este perfil.",
- "formatLabel": "Formato",
- "netscape": "Netscape TXT",
- "json": "JSON",
- "success": "Cookies exportadas exitosamente",
- "error": "Error al exportar cookies"
- }
- },
"profileInfo": {
"title": "Detalles del Perfil",
"tabs": {
diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json
index 5ffd92b..34f58c2 100644
--- a/src/i18n/locales/fr.json
+++ b/src/i18n/locales/fr.json
@@ -524,7 +524,29 @@
"selectedCount_plural": "{{count}} cookies sélectionnés"
},
"success": "Cookies copiés avec succès",
- "error": "Échec de la copie des cookies"
+ "error": "Échec de la copie des cookies",
+ "management": {
+ "title": "Gestion des Cookies",
+ "menuItem": "Gestion des Cookies"
+ },
+ "import": {
+ "title": "Importer des Cookies",
+ "description": "Importer des cookies depuis un fichier au format Netscape ou JSON.",
+ "selectFile": "Choisir un Fichier",
+ "preview": "{{count}} cookies trouvés",
+ "success": "{{imported}} cookies importés avec succès ({{replaced}} remplacés)",
+ "error": "Échec de l'importation des cookies",
+ "proFeature": "L'importation de cookies est une fonctionnalité Pro"
+ },
+ "export": {
+ "title": "Exporter les Cookies",
+ "description": "Exporter les cookies de ce profil.",
+ "formatLabel": "Format",
+ "netscape": "Netscape TXT",
+ "json": "JSON",
+ "success": "Cookies exportés avec succès",
+ "error": "Échec de l'exportation des cookies"
+ }
},
"toasts": {
"success": {
@@ -724,30 +746,6 @@
"cannotLaunch": "Ce profil a été créé sur {{os}} et n'est pas pris en charge sur ce système",
"cannotModify": "Impossible de modifier les paramètres de synchronisation d'un profil d'un autre système d'exploitation"
},
- "cookies": {
- "management": {
- "title": "Gestion des Cookies",
- "menuItem": "Gestion des Cookies"
- },
- "import": {
- "title": "Importer des Cookies",
- "description": "Importer des cookies depuis un fichier au format Netscape ou JSON.",
- "selectFile": "Choisir un Fichier",
- "preview": "{{count}} cookies trouvés",
- "success": "{{imported}} cookies importés avec succès ({{replaced}} remplacés)",
- "error": "Échec de l'importation des cookies",
- "proFeature": "L'importation de cookies est une fonctionnalité Pro"
- },
- "export": {
- "title": "Exporter les Cookies",
- "description": "Exporter les cookies de ce profil.",
- "formatLabel": "Format",
- "netscape": "Netscape TXT",
- "json": "JSON",
- "success": "Cookies exportés avec succès",
- "error": "Échec de l'exportation des cookies"
- }
- },
"profileInfo": {
"title": "Détails du Profil",
"tabs": {
diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json
index 430d95b..ceacea3 100644
--- a/src/i18n/locales/ja.json
+++ b/src/i18n/locales/ja.json
@@ -524,7 +524,29 @@
"selectedCount_plural": "{{count}} 個のCookieを選択"
},
"success": "Cookieが正常にコピーされました",
- "error": "Cookieのコピーに失敗しました"
+ "error": "Cookieのコピーに失敗しました",
+ "management": {
+ "title": "Cookie管理",
+ "menuItem": "Cookie管理"
+ },
+ "import": {
+ "title": "Cookieのインポート",
+ "description": "NetscapeまたはJSON形式のファイルからCookieをインポートします。",
+ "selectFile": "ファイルを選択",
+ "preview": "{{count}}件のCookieが見つかりました",
+ "success": "{{imported}}件のCookieをインポートしました({{replaced}}件を置換)",
+ "error": "Cookieのインポートに失敗しました",
+ "proFeature": "Cookieのインポートはプロ機能です"
+ },
+ "export": {
+ "title": "Cookieのエクスポート",
+ "description": "このプロファイルからCookieをエクスポートします。",
+ "formatLabel": "形式",
+ "netscape": "Netscape TXT",
+ "json": "JSON",
+ "success": "Cookieのエクスポートに成功しました",
+ "error": "Cookieのエクスポートに失敗しました"
+ }
},
"toasts": {
"success": {
@@ -724,30 +746,6 @@
"cannotLaunch": "このプロファイルは{{os}}で作成されたもので、このシステムではサポートされていません",
"cannotModify": "他のOSのプロファイルの同期設定は変更できません"
},
- "cookies": {
- "management": {
- "title": "Cookie管理",
- "menuItem": "Cookie管理"
- },
- "import": {
- "title": "Cookieのインポート",
- "description": "NetscapeまたはJSON形式のファイルからCookieをインポートします。",
- "selectFile": "ファイルを選択",
- "preview": "{{count}}件のCookieが見つかりました",
- "success": "{{imported}}件のCookieをインポートしました({{replaced}}件を置換)",
- "error": "Cookieのインポートに失敗しました",
- "proFeature": "Cookieのインポートはプロ機能です"
- },
- "export": {
- "title": "Cookieのエクスポート",
- "description": "このプロファイルからCookieをエクスポートします。",
- "formatLabel": "形式",
- "netscape": "Netscape TXT",
- "json": "JSON",
- "success": "Cookieのエクスポートに成功しました",
- "error": "Cookieのエクスポートに失敗しました"
- }
- },
"profileInfo": {
"title": "プロフィール詳細",
"tabs": {
diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json
index 6ae0ff6..5ee1630 100644
--- a/src/i18n/locales/pt.json
+++ b/src/i18n/locales/pt.json
@@ -524,7 +524,29 @@
"selectedCount_plural": "{{count}} cookies selecionados"
},
"success": "Cookies copiados com sucesso",
- "error": "Falha ao copiar cookies"
+ "error": "Falha ao copiar cookies",
+ "management": {
+ "title": "Gerenciamento de Cookies",
+ "menuItem": "Gerenciamento de Cookies"
+ },
+ "import": {
+ "title": "Importar Cookies",
+ "description": "Importar cookies de um arquivo no formato Netscape ou JSON.",
+ "selectFile": "Escolher Arquivo",
+ "preview": "{{count}} cookies encontrados",
+ "success": "{{imported}} cookies importados com sucesso ({{replaced}} substituídos)",
+ "error": "Falha ao importar cookies",
+ "proFeature": "A importação de cookies é um recurso Pro"
+ },
+ "export": {
+ "title": "Exportar Cookies",
+ "description": "Exportar cookies deste perfil.",
+ "formatLabel": "Formato",
+ "netscape": "Netscape TXT",
+ "json": "JSON",
+ "success": "Cookies exportados com sucesso",
+ "error": "Falha ao exportar cookies"
+ }
},
"toasts": {
"success": {
@@ -724,30 +746,6 @@
"cannotLaunch": "Este perfil foi criado em {{os}} e não é compatível com este sistema",
"cannotModify": "Não é possível modificar as configurações de sincronização de um perfil de outro sistema operacional"
},
- "cookies": {
- "management": {
- "title": "Gerenciamento de Cookies",
- "menuItem": "Gerenciamento de Cookies"
- },
- "import": {
- "title": "Importar Cookies",
- "description": "Importar cookies de um arquivo no formato Netscape ou JSON.",
- "selectFile": "Escolher Arquivo",
- "preview": "{{count}} cookies encontrados",
- "success": "{{imported}} cookies importados com sucesso ({{replaced}} substituídos)",
- "error": "Falha ao importar cookies",
- "proFeature": "A importação de cookies é um recurso Pro"
- },
- "export": {
- "title": "Exportar Cookies",
- "description": "Exportar cookies deste perfil.",
- "formatLabel": "Formato",
- "netscape": "Netscape TXT",
- "json": "JSON",
- "success": "Cookies exportados com sucesso",
- "error": "Falha ao exportar cookies"
- }
- },
"profileInfo": {
"title": "Detalhes do Perfil",
"tabs": {
diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json
index 2f829a1..ca704e7 100644
--- a/src/i18n/locales/ru.json
+++ b/src/i18n/locales/ru.json
@@ -524,7 +524,29 @@
"selectedCount_plural": "Выбрано {{count}} cookie"
},
"success": "Cookie успешно скопированы",
- "error": "Ошибка копирования cookie"
+ "error": "Ошибка копирования cookie",
+ "management": {
+ "title": "Управление Cookies",
+ "menuItem": "Управление Cookies"
+ },
+ "import": {
+ "title": "Импорт Cookies",
+ "description": "Импорт cookies из файла в формате Netscape или JSON.",
+ "selectFile": "Выбрать файл",
+ "preview": "Найдено {{count}} cookies",
+ "success": "Успешно импортировано {{imported}} cookies ({{replaced}} заменено)",
+ "error": "Ошибка импорта cookies",
+ "proFeature": "Импорт cookies — функция Pro"
+ },
+ "export": {
+ "title": "Экспорт Cookies",
+ "description": "Экспорт cookies из этого профиля.",
+ "formatLabel": "Формат",
+ "netscape": "Netscape TXT",
+ "json": "JSON",
+ "success": "Cookies успешно экспортированы",
+ "error": "Ошибка экспорта cookies"
+ }
},
"toasts": {
"success": {
@@ -724,30 +746,6 @@
"cannotLaunch": "Этот профиль был создан на {{os}} и не поддерживается в этой системе",
"cannotModify": "Невозможно изменить настройки синхронизации профиля другой ОС"
},
- "cookies": {
- "management": {
- "title": "Управление Cookies",
- "menuItem": "Управление Cookies"
- },
- "import": {
- "title": "Импорт Cookies",
- "description": "Импорт cookies из файла в формате Netscape или JSON.",
- "selectFile": "Выбрать файл",
- "preview": "Найдено {{count}} cookies",
- "success": "Успешно импортировано {{imported}} cookies ({{replaced}} заменено)",
- "error": "Ошибка импорта cookies",
- "proFeature": "Импорт cookies — функция Pro"
- },
- "export": {
- "title": "Экспорт Cookies",
- "description": "Экспорт cookies из этого профиля.",
- "formatLabel": "Формат",
- "netscape": "Netscape TXT",
- "json": "JSON",
- "success": "Cookies успешно экспортированы",
- "error": "Ошибка экспорта cookies"
- }
- },
"profileInfo": {
"title": "Детали профиля",
"tabs": {
diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json
index 5bb4778..ca6ae2e 100644
--- a/src/i18n/locales/zh.json
+++ b/src/i18n/locales/zh.json
@@ -524,7 +524,29 @@
"selectedCount_plural": "已选择 {{count}} 个 cookies"
},
"success": "Cookies 复制成功",
- "error": "复制 cookies 失败"
+ "error": "复制 cookies 失败",
+ "management": {
+ "title": "Cookie 管理",
+ "menuItem": "Cookie 管理"
+ },
+ "import": {
+ "title": "导入 Cookies",
+ "description": "从 Netscape 或 JSON 格式文件导入 Cookies。",
+ "selectFile": "选择文件",
+ "preview": "找到 {{count}} 个 Cookies",
+ "success": "成功导入 {{imported}} 个 Cookies(替换了 {{replaced}} 个)",
+ "error": "导入 Cookies 失败",
+ "proFeature": "导入 Cookies 是 Pro 功能"
+ },
+ "export": {
+ "title": "导出 Cookies",
+ "description": "从此配置文件导出 Cookies。",
+ "formatLabel": "格式",
+ "netscape": "Netscape TXT",
+ "json": "JSON",
+ "success": "Cookies 导出成功",
+ "error": "导出 Cookies 失败"
+ }
},
"toasts": {
"success": {
@@ -724,30 +746,6 @@
"cannotLaunch": "此配置文件在 {{os}} 上创建,不受此系统支持",
"cannotModify": "无法修改跨操作系统配置文件的同步设置"
},
- "cookies": {
- "management": {
- "title": "Cookie 管理",
- "menuItem": "Cookie 管理"
- },
- "import": {
- "title": "导入 Cookies",
- "description": "从 Netscape 或 JSON 格式文件导入 Cookies。",
- "selectFile": "选择文件",
- "preview": "找到 {{count}} 个 Cookies",
- "success": "成功导入 {{imported}} 个 Cookies(替换了 {{replaced}} 个)",
- "error": "导入 Cookies 失败",
- "proFeature": "导入 Cookies 是 Pro 功能"
- },
- "export": {
- "title": "导出 Cookies",
- "description": "从此配置文件导出 Cookies。",
- "formatLabel": "格式",
- "netscape": "Netscape TXT",
- "json": "JSON",
- "success": "Cookies 导出成功",
- "error": "导出 Cookies 失败"
- }
- },
"profileInfo": {
"title": "配置文件详情",
"tabs": {
diff --git a/src/types.ts b/src/types.ts
index 3ee33cd..258c9af 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -341,13 +341,13 @@ export interface CamoufoxFingerprintConfig {
"canvas:aaCapOffset"?: boolean;
// Voices
- voices?: Array<{
+ voices?: {
isLocalService?: boolean;
isDefault?: boolean;
voiceURI?: string;
name?: string;
lang?: string;
- }>;
+ }[];
"voices:blockIfNotDefined"?: boolean;
"voices:fakeCompletion"?: boolean;
"voices:fakeCompletion:charsPerSecond"?: number;