feat: xray support

This commit is contained in:
zhom
2026-07-31 01:04:58 +04:00
parent 064bf297dd
commit 0a7d7803f2
112 changed files with 10291 additions and 772 deletions
+232 -62
View File
@@ -3,8 +3,9 @@
import { invoke } from "@tauri-apps/api/core";
import { openUrl } from "@tauri-apps/plugin-opener";
import { useReducedMotion } from "motion/react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { LuArrowLeft, LuExternalLink, LuSearch } from "react-icons/lu";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -12,6 +13,11 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { FadingScrollArea } from "@/components/ui/fading-scroll-area";
import { Input } from "@/components/ui/input";
import { StepTransition } from "@/components/ui/step-transition";
import licenses from "@/generated/licenses.json";
import xraySource from "@/generated/xray-source.json";
import { launchDonutClone } from "@/lib/donut-physics";
import { Logo } from "./icons/logo";
import { RippleButton } from "./ui/ripple";
@@ -28,6 +34,8 @@ interface SystemInfo {
portable: boolean;
}
type AboutView = "about" | "licenses";
// Flywheel: each click adds spin; past this speed the donut escapes the
// dialog and bounces around the window (shared physics with the rail egg).
const SPIN_PER_CLICK = 540; // deg/s
@@ -39,6 +47,8 @@ export function AboutDialog({ isOpen, onClose }: AboutDialogProps) {
const reducedMotion = useReducedMotion();
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
const [logoFlown, setLogoFlown] = useState(false);
const [view, setView] = useState<AboutView>("about");
const [licenseQuery, setLicenseQuery] = useState("");
const logoRef = useRef<HTMLButtonElement>(null);
const rotationRef = useRef(0);
@@ -46,6 +56,7 @@ export function AboutDialog({ isOpen, onClose }: AboutDialogProps) {
const rafRef = useRef(0);
const lastTimeRef = useRef(0);
const cancelLaunchRef = useRef<(() => void) | null>(null);
const restoreLicensesButtonFocusRef = useRef(false);
useEffect(() => {
if (!isOpen) return;
@@ -111,7 +122,7 @@ export function AboutDialog({ isOpen, onClose }: AboutDialogProps) {
}
}, [reducedMotion, logoFlown, spinFrame]);
const handleClose = useCallback(() => {
const resetLogo = useCallback(() => {
stopSpin();
cancelLaunchRef.current?.();
cancelLaunchRef.current = null;
@@ -121,8 +132,47 @@ export function AboutDialog({ isOpen, onClose }: AboutDialogProps) {
logoRef.current.style.visibility = "";
}
setLogoFlown(false);
}, [stopSpin]);
const handleClose = useCallback(() => {
resetLogo();
setView("about");
setLicenseQuery("");
restoreLicensesButtonFocusRef.current = false;
onClose();
}, [stopSpin, onClose]);
}, [onClose, resetLogo]);
const handleLicensesButtonRef = useCallback(
(node: HTMLButtonElement | null) => {
if (node && restoreLicensesButtonFocusRef.current) {
restoreLicensesButtonFocusRef.current = false;
node.focus();
}
},
[],
);
const handleOpenLicenses = useCallback(() => {
resetLogo();
setLicenseQuery("");
setView("licenses");
}, [resetLogo]);
const handleBackToAbout = useCallback(() => {
restoreLicensesButtonFocusRef.current = true;
setLicenseQuery("");
setView("about");
}, []);
const filteredLicenses = useMemo(() => {
const query = licenseQuery.trim().toLocaleLowerCase();
if (!query) return licenses;
return licenses.filter(
({ name, license }) =>
name.toLocaleLowerCase().includes(query) ||
license.toLocaleLowerCase().includes(query),
);
}, [licenseQuery]);
useEffect(() => {
return () => {
@@ -132,72 +182,192 @@ export function AboutDialog({ isOpen, onClose }: AboutDialogProps) {
}, []);
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle>{t("about.title")}</DialogTitle>
</DialogHeader>
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open) handleClose();
}}
>
<DialogContent
className={
view === "licenses"
? "flex h-[min(80dvh,40rem)] max-w-lg flex-col overflow-hidden"
: "max-w-sm"
}
>
<StepTransition
transitionKey={view}
direction={view === "licenses" ? 1 : -1}
className={
view === "licenses"
? "flex min-h-0 flex-1 flex-col gap-4"
: "grid gap-4"
}
>
{view === "about" ? (
<>
<DialogHeader>
<DialogTitle>{t("about.title")}</DialogTitle>
</DialogHeader>
<div className="flex flex-col items-center gap-3 py-4">
<button
ref={logoRef}
type="button"
aria-label={t("header.donutLogo")}
onClick={handleLogoClick}
className="grid size-16 cursor-pointer place-items-center rounded-full bg-transparent text-foreground select-none will-change-transform"
style={logoFlown ? { visibility: "hidden" } : undefined}
>
<Logo className="size-14" />
</button>
<div className="flex flex-col items-center gap-3 py-4">
<button
ref={logoRef}
type="button"
aria-label={t("header.donutLogo")}
onClick={handleLogoClick}
className="grid size-16 cursor-pointer place-items-center rounded-full bg-transparent text-foreground select-none will-change-transform"
style={logoFlown ? { visibility: "hidden" } : undefined}
>
<Logo className="size-14" />
</button>
<div className="text-center">
<p className="text-lg font-semibold">Donut Browser</p>
{systemInfo && (
<>
<p className="text-sm text-muted-foreground">
{t("about.version", { version: systemInfo.app_version })}
{systemInfo.portable && (
<span className="ml-1.5 rounded border border-border bg-muted px-1 py-px text-[10px] align-middle">
{t("about.portableBadge")}
</span>
<div className="text-center">
<p className="text-lg font-semibold">Donut Browser</p>
{systemInfo && (
<>
<p className="text-sm text-muted-foreground">
{t("about.version", {
version: systemInfo.app_version,
})}
{systemInfo.portable && (
<span className="ml-1.5 rounded border border-border bg-muted px-1 py-px text-[10px] align-middle">
{t("about.portableBadge")}
</span>
)}
</p>
<p className="text-xs text-muted-foreground">
{systemInfo.os} {systemInfo.arch}
</p>
</>
)}
</div>
<p className="text-center text-xs text-muted-foreground">
{t("about.licenseNotice")}
</p>
<p className="text-xs text-muted-foreground">
{systemInfo.os} {systemInfo.arch}
</p>
</>
)}
</div>
<p className="text-center text-xs text-muted-foreground">
{t("about.licenseNotice")}
</p>
<div className="flex flex-wrap justify-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => void openUrl("https://donutbrowser.com")}
>
{t("about.website")}
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
void openUrl("https://github.com/zhom/donutbrowser")
}
>
{t("about.github")}
</Button>
<Button
ref={handleLicensesButtonRef}
variant="outline"
size="sm"
onClick={handleOpenLicenses}
>
{t("about.licenses")}
</Button>
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => void openUrl("https://donutbrowser.com")}
>
{t("about.website")}
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
void openUrl("https://github.com/zhom/donutbrowser")
}
>
GitHub
</Button>
</div>
</div>
<div className="flex justify-end">
<RippleButton variant="outline" onClick={handleClose}>
{t("common.buttons.close")}
</RippleButton>
</div>
</>
) : (
<>
<DialogHeader className="flex-row items-center gap-2">
<Button
type="button"
variant="ghost"
size="icon"
className="-ml-2 size-8"
aria-label={t("common.buttons.back")}
onClick={handleBackToAbout}
>
<LuArrowLeft aria-hidden="true" />
</Button>
<DialogTitle>{t("about.licenses")}</DialogTitle>
</DialogHeader>
<div className="flex justify-end">
<RippleButton variant="outline" onClick={handleClose}>
{t("common.buttons.close")}
</RippleButton>
</div>
<div className="relative shrink-0">
<LuSearch
aria-hidden="true"
className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground"
/>
<Input
autoFocus
type="search"
value={licenseQuery}
onChange={(event) => {
setLicenseQuery(event.target.value);
}}
aria-label={t("about.searchLicenses")}
placeholder={t("about.searchLicenses")}
className="pl-9"
/>
</div>
<FadingScrollArea
role="region"
tabIndex={0}
aria-label={t("about.licenses")}
className="-mx-2 min-h-0 flex-1 rounded-md px-2 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
{filteredLicenses.length > 0 ? (
<ul>
{filteredLicenses.map(({ name, license }) => (
<li
key={`${name}-${license}`}
className="grid grid-cols-[minmax(0,1fr)_minmax(7rem,45%)] items-baseline gap-3 border-b border-border/60 py-2.5 last:border-b-0"
>
{name === "Xray-core" ? (
<button
type="button"
onClick={() => void openUrl(xraySource.sourceUrl)}
aria-label={t("about.openSource", { name })}
title={t("about.openSource", { name })}
className="flex min-w-0 items-center gap-1.5 rounded-sm text-left text-sm font-medium text-foreground underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 active:scale-[0.98] motion-reduce:transform-none"
>
<span className="truncate">{name}</span>
<LuExternalLink
aria-hidden="true"
className="size-3.5 shrink-0 text-muted-foreground"
/>
</button>
) : (
<span
className="truncate text-sm font-medium text-foreground"
title={name}
>
{name}
</span>
)}
<code className="break-words text-right text-xs text-muted-foreground">
{license}
</code>
</li>
))}
</ul>
) : (
<p
role="status"
className="py-12 text-center text-sm text-muted-foreground"
>
{t("about.noMatchingLicenses")}
</p>
)}
</FadingScrollArea>
</>
)}
</StepTransition>
</DialogContent>
</Dialog>
);
+3 -3
View File
@@ -222,7 +222,7 @@ export function AccountPage({
<AnimatedTabsContent value="account" className="mt-4">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-3">
<div className="grid size-12 shrink-0 place-items-center rounded-full bg-accent text-foreground">
<div className="grid size-12 shrink-0 place-items-center rounded-full bg-accent text-accent-foreground">
<LuUser className="size-6" />
</div>
<div className="min-w-0 flex-1">
@@ -305,7 +305,7 @@ export function AccountPage({
user &&
getEntitlements(user).browserAutomation &&
user.isPrimaryDevice === false && (
<p className="text-xs text-warning">
<p className="text-xs text-warning-text">
{t("account.automationPrimaryOnly")}
</p>
)}
@@ -314,7 +314,7 @@ export function AccountPage({
getEntitlements(user).browserAutomation &&
user.isPrimaryDevice === true &&
(user.deviceCount ?? 1) > 1 && (
<p className="text-xs text-success">
<p className="text-xs text-success-text">
{t("account.automationActiveHere")}
</p>
)}
+1 -1
View File
@@ -63,7 +63,7 @@ export function BandwidthMiniChart({
type="button"
onClick={onClick}
className={cn(
"relative flex w-full min-w-0 cursor-pointer items-center gap-1.5 rounded border-none bg-transparent px-2 transition-colors hover:bg-accent/50",
"relative flex w-full min-w-0 cursor-pointer items-center gap-1.5 rounded border-none bg-transparent px-2 transition-colors hover:bg-muted",
className,
)}
>
@@ -98,7 +98,7 @@ export function ConsistencyWarningDialog({
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<LuTriangleAlert className="size-5 text-warning" />
<LuTriangleAlert className="size-5 text-warning-text" />
{t("consistencyWarning.title")}
</DialogTitle>
</DialogHeader>
+3 -3
View File
@@ -407,7 +407,7 @@ export function CookieCopyDialog({
>
{p.name}
{runningProfiles.has(p.id) && (
<span className="text-xs text-destructive">
<span className="text-xs text-destructive-text">
{t("cookies.copy.running")}
</span>
)}
@@ -451,7 +451,7 @@ export function CookieCopyDialog({
<div className="size-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
</div>
) : error ? (
<div className="rounded-md bg-destructive/10 p-4 text-center text-destructive">
<div className="rounded-md bg-destructive/10 p-4 text-center text-destructive-text">
{error}
</div>
) : filteredDomains.length === 0 ? (
@@ -547,7 +547,7 @@ function DomainRow({
return (
<AnimatedDisclosureItem>
<div className="flex items-center gap-2 rounded p-2 hover:bg-accent/50">
<div className="flex items-center gap-2 rounded p-2 hover:bg-muted">
<Checkbox
checked={isAllSelected || isPartial}
onCheckedChange={() => {
+2 -2
View File
@@ -487,7 +487,7 @@ export function CookieManagementDialog({
{importResult && (
<div className="space-y-4">
<div className="rounded-lg bg-success/10 p-4">
<div className="font-medium text-success">
<div className="font-medium text-success-text">
{t("cookies.management.importedSuccess", {
imported: importResult.cookies_imported,
replaced: importResult.cookies_replaced,
@@ -634,7 +634,7 @@ function ExportDomainRow({
return (
<AnimatedDisclosureItem>
<div className="flex items-center gap-2 rounded p-1.5 hover:bg-accent/50">
<div className="flex items-center gap-2 rounded p-1.5 hover:bg-muted">
<Checkbox
checked={isAllSelected || isPartial}
onCheckedChange={() => {
+1 -1
View File
@@ -93,7 +93,7 @@ export function CreateGroupDialog({
</div>
{error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive-text">
{error}
</div>
)}
+5 -5
View File
@@ -741,7 +741,7 @@ export function CreateProfileDialog({
autoComplete="new-password"
/>
{passwordError && (
<p className="text-sm text-destructive">
<p className="text-sm text-destructive-text">
{passwordError}
</p>
)}
@@ -764,7 +764,7 @@ export function CreateProfileDialog({
)}
{!isLoadingReleaseTypes && releaseTypesError && (
<div className="flex items-center gap-3 rounded-md border border-destructive/50 bg-destructive/10 p-3">
<p className="flex-1 text-sm text-destructive">
<p className="flex-1 text-sm text-destructive-text">
{releaseTypesError}
</p>
<RippleButton
@@ -783,7 +783,7 @@ export function CreateProfileDialog({
!releaseTypesError &&
!getBestAvailableVersion("wayfern") && (
<div className="flex items-center gap-3 rounded-md border border-warning/50 bg-warning/10 p-3">
<p className="text-sm text-warning">
<p className="text-sm text-warning-text">
{t("createProfile.platformUnavailable", {
browser: "Wayfern",
})}
@@ -910,7 +910,7 @@ export function CreateProfileDialog({
{!isLoadingReleaseTypes &&
releaseTypesError && (
<div className="flex items-center gap-3 rounded-md border border-destructive/50 bg-destructive/10 p-3">
<p className="flex-1 text-sm text-destructive">
<p className="flex-1 text-sm text-destructive-text">
{releaseTypesError}
</p>
<RippleButton
@@ -1272,7 +1272,7 @@ export function CreateProfileDialog({
)}
{!isLoadingReleaseTypes && releaseTypesError && (
<div className="flex items-center gap-3 rounded-md border border-destructive/50 bg-destructive/10 p-3">
<p className="flex-1 text-sm text-destructive">
<p className="flex-1 text-sm text-destructive-text">
{releaseTypesError}
</p>
<RippleButton
+1 -1
View File
@@ -330,7 +330,7 @@ export function UnifiedToast(props: ToastProps) {
})}`}
</p>
{progress.failed_count > 0 && (
<p className="mt-0.5 text-xs text-destructive">
<p className="mt-0.5 text-xs text-destructive-text">
{t("toasts.progress.filesFailed", {
count: progress.failed_count,
})}
+3 -3
View File
@@ -98,7 +98,7 @@ function DataTableActionBarAction({
variant="secondary"
size={size}
className={cn(
"gap-1.5 border border-secondary bg-secondary/50 hover:bg-secondary/70 [&>svg]:size-3.5",
"gap-1.5 border border-secondary bg-secondary text-secondary-foreground hover:bg-secondary hover:text-secondary-foreground [&>svg]:size-3.5",
size === "icon" ? "size-7" : "h-7",
className,
)}
@@ -120,7 +120,7 @@ function DataTableActionBarAction({
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
<TooltipContent
sideOffset={6}
className="border bg-accent font-semibold text-foreground dark:bg-card [&>span]:hidden"
className="border bg-accent font-semibold text-accent-foreground [&>span]:hidden"
>
<p>{tooltip}</p>
</TooltipContent>
@@ -161,7 +161,7 @@ function DataTableActionBarSelection<TData>({
</TooltipTrigger>
<TooltipContent
sideOffset={10}
className="flex items-center gap-2 border bg-accent px-2 py-1 font-semibold text-foreground dark:bg-card [&>span]:hidden"
className="flex items-center gap-2 border bg-accent px-2 py-1 font-semibold text-accent-foreground [&>span]:hidden"
>
<p>{t("dataTableActionBar.clearSelection")}</p>
<kbd className="rounded border bg-background px-1.5 py-px font-mono text-[0.7rem] font-normal text-foreground shadow-xs select-none">
+2 -2
View File
@@ -165,7 +165,7 @@ export function DeleteGroupDialog({
<RadioGroupItem value="delete" id="delete" />
<Label
htmlFor="delete"
className="text-sm text-destructive"
className="text-sm text-destructive-text"
>
{t("groups.deleteAlongWithGroup")}
</Label>
@@ -184,7 +184,7 @@ export function DeleteGroupDialog({
)}
{error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive-text">
{error}
</div>
)}
+1 -1
View File
@@ -103,7 +103,7 @@ export function EditGroupDialog({
</div>
{error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive-text">
{error}
</div>
)}
@@ -160,7 +160,7 @@ export function ExtensionGroupAssignmentDialog({
</div>
{error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive-text">
{error}
</div>
)}
@@ -1147,13 +1147,13 @@ export function ExtensionManagementDialog({
disabled={limitedMode}
>
<span>{t("extensions.extensionsTab")}</span>
<span className="text-xs text-muted-foreground tabular-nums">
<span className="text-xs tabular-nums">
{extensions.length}
</span>
</AnimatedTabsTrigger>
<AnimatedTabsTrigger value="groups" disabled={limitedMode}>
<span>{t("extensions.groupsTab")}</span>
<span className="text-xs text-muted-foreground tabular-nums">
<span className="text-xs tabular-nums">
{extensionGroups.length}
</span>
</AnimatedTabsTrigger>
@@ -1696,7 +1696,7 @@ export function ExtensionManagementDialog({
href={editingExtension.homepage_url}
target="_blank"
rel="noopener noreferrer"
className="flex min-w-0 items-center gap-1 text-primary hover:underline"
className="flex min-w-0 items-center gap-1 text-primary-text hover:underline"
>
<span className="truncate">
{editingExtension.homepage_url}
@@ -1849,7 +1849,7 @@ export function ExtensionManagementDialog({
tooltip={t("common.buttons.delete")}
variant="destructive"
size="icon"
className="border-destructive bg-destructive/50 hover:bg-destructive/70"
className="border-destructive bg-destructive hover:bg-destructive"
onClick={() => {
setBulkExtDeleteOpen(true);
}}
@@ -1875,7 +1875,7 @@ export function ExtensionManagementDialog({
tooltip={t("common.buttons.delete")}
variant="destructive"
size="icon"
className="border-destructive bg-destructive/50 hover:bg-destructive/70"
className="border-destructive bg-destructive hover:bg-destructive"
onClick={() => {
setBulkGroupDeleteOpen(true);
}}
+1 -1
View File
@@ -198,7 +198,7 @@ export function GroupAssignmentDialog({
</div>
{error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive-text">
{error}
</div>
)}
+10 -4
View File
@@ -569,9 +569,15 @@ export function GroupManagementDialog({
<div className="@container flex min-h-0 w-full flex-1 flex-col">
<div className="flex shrink-0 flex-wrap items-center justify-between gap-2">
<div className="inline-flex h-7 items-center justify-center gap-1.5 rounded-md bg-accent px-3 text-sm font-medium whitespace-nowrap text-foreground">
<div
data-slot="group-summary-pill"
className="inline-flex h-7 items-center justify-center gap-1.5 rounded-md bg-accent px-3 text-sm font-medium whitespace-nowrap text-accent-foreground"
>
<span>{t("groups.pageTitle")}</span>
<span className="text-xs text-muted-foreground tabular-nums">
<span
data-slot="group-summary-count"
className="text-xs tabular-nums"
>
{groups.length}
</span>
</div>
@@ -591,7 +597,7 @@ export function GroupManagementDialog({
</div>
{error && (
<div className="mt-4 rounded-md bg-destructive/10 p-3 text-sm text-destructive">
<div className="mt-4 rounded-md bg-destructive/10 p-3 text-sm text-destructive-text">
{error}
</div>
)}
@@ -708,7 +714,7 @@ export function GroupManagementDialog({
onClick={() => setBulkDeleteOpen(true)}
size="icon"
variant="destructive"
className="border-destructive bg-destructive/50 hover:bg-destructive/70"
className="border-destructive bg-destructive hover:bg-destructive"
>
<LuTrash2 />
</DataTableActionBarAction>
+3 -3
View File
@@ -221,7 +221,7 @@ const HomeHeader = ({
behavior: "smooth",
});
}}
className="absolute top-1/2 left-0 z-10 grid size-5 -translate-y-1/2 place-items-center rounded-full bg-card/90 text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-foreground"
className="absolute top-1/2 left-0 z-10 grid size-5 -translate-y-1/2 place-items-center rounded-full bg-card/90 text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
>
<LuChevronLeft className="size-3" />
</button>
@@ -295,7 +295,7 @@ const HomeHeader = ({
behavior: "smooth",
});
}}
className="absolute top-1/2 right-0 z-10 grid size-5 -translate-y-1/2 place-items-center rounded-full bg-card/90 text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-foreground"
className="absolute top-1/2 right-0 z-10 grid size-5 -translate-y-1/2 place-items-center rounded-full bg-card/90 text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
>
<LuChevronRight className="size-3" />
</button>
@@ -323,7 +323,7 @@ const HomeHeader = ({
onClick={() => {
onSearchQueryChange("");
}}
className="absolute top-1/2 right-1.5 -translate-y-1/2 transform rounded-sm p-0.5 transition-colors hover:bg-accent"
className="absolute top-1/2 right-1.5 -translate-y-1/2 transform rounded-sm p-0.5 transition-colors hover:bg-accent hover:text-accent-foreground"
aria-label={t("header.clearSearch")}
>
<LuX className="size-3.5 text-muted-foreground hover:text-foreground" />
+4 -3
View File
@@ -847,10 +847,11 @@ export function ImportProfileDialog({
<span
className={cn(
"shrink-0 text-xs font-medium",
item.status === "imported" && "text-success",
item.status === "imported" && "text-success-text",
item.status === "skipped" &&
"text-muted-foreground",
item.status === "failed" && "text-destructive",
item.status === "failed" &&
"text-destructive-text",
)}
>
{item.status === "imported" &&
@@ -864,7 +865,7 @@ export function ImportProfileDialog({
{item.name || item.source_path}
</span>
{item.error && (
<span className="min-w-0 flex-1 truncate text-xs text-destructive">
<span className="min-w-0 flex-1 truncate text-xs text-destructive-text">
{translateBackendError(t, new Error(item.error))}
</span>
)}
+2 -2
View File
@@ -535,7 +535,7 @@ export function IntegrationsDialog({
<p className="text-xs text-muted-foreground">
{t("integrations.mcpEnableDescription")}
{!termsAccepted && (
<span className="ml-1 text-warning">
<span className="ml-1 text-warning-text">
{t("integrations.mcpAcceptTermsFirst")}
</span>
)}
@@ -622,7 +622,7 @@ export function IntegrationsDialog({
type="button"
variant="ghost"
size="icon"
className="size-8 text-muted-foreground hover:text-destructive"
className="size-8 text-muted-foreground hover:text-destructive-text"
disabled={busy}
onClick={() =>
void handleRemoveAgent(agent)
+1 -1
View File
@@ -205,7 +205,7 @@ export function PermissionDialog({
<div className="space-y-4">
{!isCurrentPermissionGranted && (
<div className="rounded-lg bg-warning/10 p-3">
<p className="text-sm text-warning">
<p className="text-sm text-warning-text">
{permissionType === "microphone"
? t("permissionDialog.notGrantedMicrophone")
: t("permissionDialog.notGrantedCamera")}
+10 -10
View File
@@ -370,7 +370,7 @@ function ExtCell({
<button
type="button"
disabled={isSaving}
className="flex h-7 w-full items-center gap-1.5 rounded px-1.5 text-left text-xs text-muted-foreground transition-colors duration-100 hover:bg-accent/50 hover:text-foreground disabled:opacity-50"
className="flex h-7 w-full items-center gap-1.5 rounded px-1.5 text-left text-xs text-muted-foreground transition-colors duration-100 hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
>
<LuPuzzle className="size-3 shrink-0" />
<span className="flex-1 truncate" title={label}>
@@ -457,7 +457,7 @@ function DnsCell({
type="button"
data-onborda="dns-blocklist"
disabled={isSaving}
className="flex h-7 w-full items-center gap-1.5 rounded px-1.5 text-left text-xs text-muted-foreground transition-colors duration-100 hover:bg-accent/50 hover:text-foreground disabled:opacity-50"
className="flex h-7 w-full items-center gap-1.5 rounded px-1.5 text-left text-xs text-muted-foreground transition-colors duration-100 hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
title={
level
? meta.t("profiles.table.dnsLevel", { level })
@@ -681,7 +681,7 @@ const TagsCell = React.memo<{
"flex h-6 w-full cursor-pointer items-center gap-1 overflow-hidden rounded border-none bg-transparent px-2 py-1",
isDisabled
? "cursor-not-allowed opacity-60"
: "cursor-pointer hover:bg-accent/50",
: "cursor-pointer hover:bg-muted",
)}
onClick={() => {
if (!isDisabled) setOpenTagsEditorFor(profile.id);
@@ -894,7 +894,7 @@ const ProxyCellTrigger = React.memo<{
"flex max-w-full min-w-0 items-center gap-2 rounded px-2 py-1",
isDisabled
? "pointer-events-none cursor-not-allowed opacity-60"
: "cursor-pointer hover:bg-accent/50",
: "cursor-pointer hover:bg-muted",
)}
>
{vpnBadge && (
@@ -1043,7 +1043,7 @@ const NoteCell = React.memo<{
"flex min-h-6 w-full min-w-0 items-center rounded border-none bg-transparent px-2 py-1 text-left",
isDisabled
? "cursor-not-allowed opacity-60"
: "cursor-pointer hover:bg-accent/50",
: "cursor-pointer hover:bg-muted",
)}
onClick={() => {
if (!isDisabled) {
@@ -2362,7 +2362,7 @@ export function ProfilesDataTable({
<Tooltip>
<TooltipTrigger asChild>
<span>
<LuTriangleAlert className="size-4 text-warning" />
<LuTriangleAlert className="size-4 text-warning-text" />
</span>
</TooltipTrigger>
<TooltipContent>
@@ -2390,7 +2390,7 @@ export function ProfilesDataTable({
canLaunch && "cursor-pointer",
isFollower && "border-accent",
isRunning &&
"bg-destructive/10 text-destructive hover:bg-destructive/20",
"bg-destructive/10 text-destructive-text hover:bg-destructive/20",
)}
onClick={() =>
isRunning
@@ -2611,7 +2611,7 @@ export function ProfilesDataTable({
"mr-auto h-6 max-w-full min-w-0 overflow-hidden rounded border-none bg-transparent px-2 py-1 text-left",
isCrossOsBlocked
? "cursor-not-allowed opacity-60"
: "cursor-pointer hover:bg-accent/50",
: "cursor-pointer hover:bg-muted",
)}
onClick={() => {
if (isCrossOsBlocked) return;
@@ -3325,7 +3325,7 @@ export function ProfilesDataTable({
title={crossOsTitle}
style={{ height: `${ROW_HEIGHT}px` }}
className={cn(
"overflow-visible border-0! hover:bg-accent/50",
"overflow-visible border-0! hover:bg-muted",
rowIsCrossOs && "opacity-60",
)}
>
@@ -3514,7 +3514,7 @@ export function ProfilesDataTable({
onClick={onBulkDelete}
size="icon"
variant="destructive"
className="border-destructive bg-destructive/50 hover:bg-destructive/70"
className="border-destructive bg-destructive hover:bg-destructive"
>
<LuTrash2 />
</DataTableActionBarAction>
+30 -24
View File
@@ -870,7 +870,7 @@ function ProfileInfoLayout({
type="button"
aria-label={t("common.buttons.close")}
onClick={onClose}
className="grid size-7 place-items-center rounded-md text-muted-foreground transition-colors duration-100 hover:bg-accent/50 hover:text-foreground"
className="grid size-7 place-items-center rounded-md text-muted-foreground transition-colors duration-100 hover:bg-accent hover:text-accent-foreground"
>
<LuX className="size-3.5" />
</button>
@@ -893,7 +893,7 @@ function ProfileInfoLayout({
"flex h-7 items-center gap-2 rounded-md px-2 text-left text-xs transition-colors duration-100",
active
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
)}
>
<span className="shrink-0">{it.icon}</span>
@@ -913,7 +913,7 @@ function ProfileInfoLayout({
type="button"
onClick={deleteAction.onClick}
disabled={deleteAction.disabled}
className="flex h-7 items-center gap-2 rounded-md px-2 text-xs text-destructive transition-colors duration-100 hover:bg-destructive/10 disabled:pointer-events-none disabled:opacity-50"
className="flex h-7 items-center gap-2 rounded-md px-2 text-xs text-destructive-text transition-colors duration-100 hover:bg-destructive/10 disabled:pointer-events-none disabled:opacity-50"
>
<LuTrash2 className="size-3.5 shrink-0" />
<span className="flex-1 text-left">
@@ -1181,8 +1181,8 @@ function _SectionAction({
className={cn(
"flex h-9 items-center gap-2 rounded-md px-3 text-left text-xs transition-colors",
destructive
? "text-destructive hover:bg-destructive/10"
: "hover:bg-accent",
? "text-destructive-text hover:bg-destructive/10"
: "hover:bg-accent hover:text-accent-foreground",
"disabled:pointer-events-none disabled:opacity-50",
)}
>
@@ -1254,11 +1254,11 @@ function LaunchHookEditor({
className="font-mono text-xs"
/>
{showInvalidHint && (
<p className="text-xs text-warning">
<p className="text-xs text-warning-text">
{t("profileInfo.launchHook.invalidUrlHint")}
</p>
)}
{error && <p className="text-xs text-destructive">{error}</p>}
{error && <p className="text-xs text-destructive-text">{error}</p>}
<div className="flex items-center gap-2">
<Button
size="sm"
@@ -1360,11 +1360,13 @@ function SyncSectionInline({
{t(`profileInfo.syncStatusValue.${syncStatus.status}`)}
</p>
{syncStatus.error && (
<p className="mt-1 text-xs text-destructive">{syncStatus.error}</p>
<p className="mt-1 text-xs text-destructive-text">
{syncStatus.error}
</p>
)}
</div>
)}
{error && <p className="text-xs text-destructive">{error}</p>}
{error && <p className="text-xs text-destructive-text">{error}</p>}
</div>
);
}
@@ -1500,7 +1502,7 @@ function NetworkSectionInline({
</Select>
</div>
{error && <p className="text-xs text-destructive">{error}</p>}
{error && <p className="text-xs text-destructive-text">{error}</p>}
</div>
);
}
@@ -1604,7 +1606,7 @@ function ExtensionsSectionInline({
</SelectContent>
</Select>
</div>
{error && <p className="text-xs text-destructive">{error}</p>}
{error && <p className="text-xs text-destructive-text">{error}</p>}
</div>
);
}
@@ -1812,7 +1814,7 @@ function CookiesSectionInline({
</ul>
</div>
)}
{error && <p className="text-xs text-destructive">{error}</p>}
{error && <p className="text-xs text-destructive-text">{error}</p>}
</>
)}
</div>
@@ -1928,8 +1930,10 @@ function FingerprintSectionInline({
profileBrowser={profile.browser}
/>
{error && <p className="text-xs text-destructive">{error}</p>}
{success && !error && <p className="text-xs text-success">{success}</p>}
{error && <p className="text-xs text-destructive-text">{error}</p>}
{success && !error && (
<p className="text-xs text-success-text">{success}</p>
)}
<div className="mt-3 flex items-center gap-2 border-t border-border pt-3">
<Button
@@ -2102,7 +2106,7 @@ function SecuritySectionInline({
}}
className={cn(
"h-7 flex-1 rounded-md border px-2 text-xs transition-colors",
"border-border text-muted-foreground hover:bg-accent/50 hover:text-foreground",
"border-border text-muted-foreground hover:bg-accent hover:text-accent-foreground",
)}
>
{t("profilePassword.modes.validate")}
@@ -2117,7 +2121,7 @@ function SecuritySectionInline({
"h-7 flex-1 rounded-md border px-2 text-xs transition-colors",
mode === "change"
? "border-transparent bg-accent text-accent-foreground"
: "border-border text-muted-foreground hover:bg-accent/50 hover:text-foreground",
: "border-border text-muted-foreground hover:bg-accent hover:text-accent-foreground",
)}
>
{t("profilePassword.modes.change")}
@@ -2131,8 +2135,8 @@ function SecuritySectionInline({
className={cn(
"h-7 flex-1 rounded-md border px-2 text-xs transition-colors",
mode === "remove"
? "border-transparent bg-destructive/10 text-destructive"
: "border-border text-muted-foreground hover:bg-accent/50 hover:text-foreground",
? "border-transparent bg-destructive/10 text-destructive-text"
: "border-border text-muted-foreground hover:bg-accent hover:text-accent-foreground",
)}
>
{t("profilePassword.modes.remove")}
@@ -2182,8 +2186,10 @@ function SecuritySectionInline({
)}
</div>
{error && <p className="text-xs text-destructive">{error}</p>}
{success && !error && <p className="text-xs text-success">{success}</p>}
{error && <p className="text-xs text-destructive-text">{error}</p>}
{success && !error && (
<p className="text-xs text-success-text">{success}</p>
)}
{isRunning && (
<p className="text-xs text-muted-foreground">
@@ -2395,7 +2401,7 @@ export function ProfileDnsBlocklistDialog({
href="https://github.com/hagezi/dns-blocklists"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
className="text-primary-text hover:underline"
>
{t("common.buttons.moreInfo")}
</a>
@@ -2408,8 +2414,8 @@ export function ProfileDnsBlocklistDialog({
onClick={() => setLevel(option.value)}
className={`w-full text-left px-3 py-2 rounded-md text-sm transition-colors ${
level === option.value
? "bg-primary/10 text-primary border border-primary/30"
: "hover:bg-accent border border-transparent"
? "border border-primary/30 bg-primary/10 text-primary-text"
: "border border-transparent hover:bg-accent hover:text-accent-foreground"
}`}
>
{option.label}
@@ -2531,7 +2537,7 @@ export function ProfileBypassRulesDialog({
onClick={() => {
handleRemoveRule(rule);
}}
className="shrink-0 text-muted-foreground transition-colors hover:text-destructive"
className="shrink-0 text-muted-foreground transition-colors hover:text-destructive-text"
>
<LuX className="size-3.5" />
</button>
+2 -2
View File
@@ -203,7 +203,7 @@ export function ProfilePasswordDialog({
<div className="flex flex-col gap-3">
{(mode === "set" || mode === "change") && (
<div className="rounded-md border border-warning/50 bg-warning/10 p-3 text-sm">
<p className="font-medium text-warning">
<p className="font-medium text-warning-text">
{t("profilePassword.warnings.forgetWarningTitle")}
</p>
<p className="mt-1 text-xs text-muted-foreground">
@@ -212,7 +212,7 @@ export function ProfilePasswordDialog({
</div>
)}
{lockoutSecondsRemaining != null && (
<div className="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
<div className="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive-text">
{t("backendErrors.lockedOut", {
duration: formatLockoutDuration(t, lockoutSecondsRemaining),
})}
+1 -1
View File
@@ -267,7 +267,7 @@ export function ProfileSyncDialog({
{syncMode === "Encrypted" &&
!hasE2ePassword &&
userChangedMode && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive-text">
{t("sync.mode.noPasswordWarning")}
</div>
)}
+1 -1
View File
@@ -299,7 +299,7 @@ export function ProxyAssignmentDialog({
</div>
{error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive-text">
{error}
</div>
)}
+4 -4
View File
@@ -165,7 +165,7 @@ export function ProxyCheckButton({
<FiCheck className="absolute right-[-4px] bottom-[-6px]" />
</span>
) : result && !result.is_valid ? (
<span className="text-sm text-destructive"></span>
<span className="text-sm text-destructive-text"></span>
) : (
<FiCheck className="size-3" />
)}
@@ -185,10 +185,10 @@ export function ProxyCheckButton({
{[result.city, result.country].filter(Boolean).join(", ") ||
t("proxyCheck.unknownLocation")}
</p>
<p className="text-xs text-primary-foreground/70">
<p className="text-xs text-primary-foreground">
{t("proxyCheck.tooltipIp", { ip: result.ip })}
</p>
<p className="text-xs text-primary-foreground/70">
<p className="text-xs text-primary-foreground">
{t("proxyCheck.tooltipChecked", {
time: formatRelativeTime(result.timestamp),
})}
@@ -197,7 +197,7 @@ export function ProxyCheckButton({
) : result && !result.is_valid ? (
<div>
<p>{t("proxyCheck.tooltipFailedTitle")}</p>
<p className="text-xs text-primary-foreground/70">
<p className="text-xs text-primary-foreground">
{t("proxyCheck.tooltipFailed", {
time: formatRelativeTime(result.timestamp),
})}
+179 -82
View File
@@ -21,6 +21,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { translateBackendError } from "@/lib/backend-errors";
import type { StoredProxy } from "@/types";
import { RippleButton } from "./ui/ripple";
@@ -32,6 +33,7 @@ interface ProxyFormData {
port: number;
username: string;
password: string;
vless_uri: string;
}
interface ProxyFormDialogProps {
@@ -47,8 +49,38 @@ const DEFAULT_FORM: ProxyFormData = {
port: 8080,
username: "",
password: "",
vless_uri: "",
};
interface VlessEndpoint {
host: string;
port: number;
}
function parseVlessEndpoint(uri: string): VlessEndpoint | null {
try {
const parsed = new URL(uri.trim());
const port = Number.parseInt(parsed.port, 10);
if (
parsed.protocol !== "vless:" ||
!parsed.hostname ||
!Number.isInteger(port) ||
port < 1 ||
port > 65535
) {
return null;
}
const host =
parsed.hostname.startsWith("[") && parsed.hostname.endsWith("]")
? parsed.hostname.slice(1, -1)
: parsed.hostname;
return { host, port };
} catch {
return null;
}
}
export function ProxyFormDialog({
isOpen,
onClose,
@@ -79,6 +111,7 @@ export function ProxyFormDialog({
port: editingProxy.proxy_settings.port,
username: editingProxy.proxy_settings.username ?? "",
password: editingProxy.proxy_settings.password ?? "",
vless_uri: editingProxy.proxy_settings.vless_uri ?? "",
});
}, [editingProxy, isOpen, resetForm]);
@@ -88,7 +121,20 @@ export function ProxyFormDialog({
return;
}
if (!form.host.trim() || !form.port) {
const isVless = form.proxy_type === "vless";
const vlessEndpoint = isVless ? parseVlessEndpoint(form.vless_uri) : null;
if (isVless && !form.vless_uri.trim()) {
toast.error(t("proxies.form.vlessUriRequired"));
return;
}
if (isVless && !vlessEndpoint) {
toast.error(t("proxies.form.vlessUriInvalid"));
return;
}
if (!isVless && (!form.host.trim() || !form.port)) {
toast.error(t("proxies.form.hostPortRequired"));
return;
}
@@ -107,10 +153,11 @@ export function ProxyFormDialog({
name: form.name.trim(),
proxySettings: {
proxy_type: form.proxy_type,
host: form.host.trim(),
port: form.port,
username: form.username.trim() || undefined,
password: form.password.trim() || undefined,
host: vlessEndpoint?.host ?? form.host.trim(),
port: vlessEndpoint?.port ?? form.port,
username: isVless ? undefined : form.username.trim() || undefined,
password: isVless ? undefined : form.password.trim() || undefined,
vless_uri: isVless ? form.vless_uri.trim() : undefined,
},
};
@@ -144,13 +191,19 @@ export function ProxyFormDialog({
}
}, [isSubmitting, onClose]);
const isVless = form.proxy_type === "vless";
const vlessEndpoint = isVless ? parseVlessEndpoint(form.vless_uri) : null;
const hasInvalidVlessUri =
isVless && form.vless_uri.trim().length > 0 && !vlessEndpoint;
const isFormValid =
form.name.trim() &&
form.host.trim() &&
form.port > 0 &&
form.port <= 65535 &&
(form.proxy_type !== "ss" ||
(form.username.trim() && form.password.trim()));
(isVless
? vlessEndpoint !== null
: form.host.trim() &&
form.port > 0 &&
form.port <= 65535 &&
(form.proxy_type !== "ss" ||
(form.username.trim() && form.password.trim())));
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
@@ -176,7 +229,7 @@ export function ProxyFormDialog({
</div>
<div className="grid gap-2">
<Label>{t("proxies.form.type")}</Label>
<Label htmlFor="proxy-type">{t("proxies.form.type")}</Label>
<Select
value={form.proxy_type}
onValueChange={(value) => {
@@ -184,91 +237,135 @@ export function ProxyFormDialog({
}}
disabled={isSubmitting}
>
<SelectTrigger>
<SelectTrigger id="proxy-type">
<SelectValue placeholder={t("proxies.form.selectType")} />
</SelectTrigger>
<SelectContent>
{["http", "https", "socks4", "socks5", "ss"].map((type) => (
<SelectItem key={type} value={type}>
{type === "ss" ? "Shadowsocks" : type.toUpperCase()}
</SelectItem>
))}
{["http", "https", "socks4", "socks5", "ss", "vless"].map(
(type) => (
<SelectItem key={type} value={type}>
{type === "ss"
? "Shadowsocks"
: type === "vless"
? t("proxies.form.vlessType")
: type.toUpperCase()}
</SelectItem>
),
)}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
{isVless ? (
<div className="grid gap-2">
<Label htmlFor="proxy-host">{t("proxies.form.host")}</Label>
<Input
id="proxy-host"
value={form.host}
onChange={(e) => {
setForm({ ...form, host: e.target.value });
}}
placeholder={t("proxies.form.hostPlaceholder")}
disabled={isSubmitting}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="proxy-port">{t("proxies.form.port")}</Label>
<Input
id="proxy-port"
type="number"
value={form.port}
onChange={(e) => {
setForm({
...form,
port: Number.parseInt(e.target.value, 10) || 0,
});
}}
placeholder={t("proxies.form.portPlaceholder")}
min="1"
max="65535"
disabled={isSubmitting}
/>
</div>
</div>
<div className="grid grid-cols-1 gap-4 @sm:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="proxy-username">
{form.proxy_type === "ss"
? t("proxies.form.cipher")
: t("proxies.form.username")}
<Label htmlFor="proxy-vless-uri">
{t("proxies.form.vlessUri")}
</Label>
<Input
id="proxy-username"
value={form.username}
<Textarea
id="proxy-vless-uri"
value={form.vless_uri}
onChange={(e) => {
setForm({ ...form, username: e.target.value });
setForm({ ...form, vless_uri: e.target.value });
}}
placeholder={
form.proxy_type === "ss"
? t("proxies.form.cipherPlaceholder")
: t("proxies.form.usernamePlaceholder")
placeholder={t("proxies.form.vlessUriPlaceholder")}
disabled={isSubmitting}
aria-invalid={hasInvalidVlessUri}
aria-describedby="proxy-vless-uri-help"
autoCapitalize="none"
autoComplete="off"
spellCheck={false}
className="min-h-24 resize-y font-mono text-xs leading-relaxed"
/>
<p
id="proxy-vless-uri-help"
className={
hasInvalidVlessUri
? "text-xs text-destructive-text"
: "text-xs text-muted-foreground"
}
disabled={isSubmitting}
/>
role={hasInvalidVlessUri ? "alert" : undefined}
>
{hasInvalidVlessUri
? t("proxies.form.vlessUriInvalid")
: t("proxies.form.vlessUriHint")}
</p>
</div>
) : (
<>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="proxy-host">{t("proxies.form.host")}</Label>
<Input
id="proxy-host"
value={form.host}
onChange={(e) => {
setForm({ ...form, host: e.target.value });
}}
placeholder={t("proxies.form.hostPlaceholder")}
disabled={isSubmitting}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="proxy-password">
{t("proxies.form.password")}
</Label>
<Input
id="proxy-password"
type="password"
value={form.password}
onChange={(e) => {
setForm({ ...form, password: e.target.value });
}}
placeholder={t("proxies.form.passwordPlaceholder")}
disabled={isSubmitting}
/>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="proxy-port">{t("proxies.form.port")}</Label>
<Input
id="proxy-port"
type="number"
value={form.port}
onChange={(e) => {
setForm({
...form,
port: Number.parseInt(e.target.value, 10) || 0,
});
}}
placeholder={t("proxies.form.portPlaceholder")}
min="1"
max="65535"
disabled={isSubmitting}
/>
</div>
</div>
<div className="grid grid-cols-1 gap-4 @sm:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="proxy-username">
{form.proxy_type === "ss"
? t("proxies.form.cipher")
: t("proxies.form.username")}
</Label>
<Input
id="proxy-username"
value={form.username}
onChange={(e) => {
setForm({ ...form, username: e.target.value });
}}
placeholder={
form.proxy_type === "ss"
? t("proxies.form.cipherPlaceholder")
: t("proxies.form.usernamePlaceholder")
}
disabled={isSubmitting}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="proxy-password">
{t("proxies.form.password")}
</Label>
<Input
id="proxy-password"
type="password"
value={form.password}
onChange={(e) => {
setForm({ ...form, password: e.target.value });
}}
placeholder={t("proxies.form.passwordPlaceholder")}
disabled={isSubmitting}
/>
</div>
</div>
</>
)}
</div>
<DialogFooter>
+5 -5
View File
@@ -420,7 +420,7 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
key={`${proxy.original_line}-${i}`}
className="rounded bg-muted/30 p-2 font-mono text-xs break-all"
>
<span className="text-primary">
<span className="text-primary-text">
{proxy.proxy_type}://
</span>
{proxy.username && (
@@ -487,7 +487,7 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
<span className="text-sm">
{t("proxies.importDialog.imported")}
</span>
<span className="text-sm font-medium text-success">
<span className="text-sm font-medium text-success-text">
{importResult.imported_count}
</span>
</div>
@@ -496,7 +496,7 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
<span className="text-sm">
{t("proxies.importDialog.skippedDuplicates")}
</span>
<span className="text-sm font-medium text-warning">
<span className="text-sm font-medium text-warning-text">
{importResult.skipped_count}
</span>
</div>
@@ -506,7 +506,7 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
<span className="text-sm">
{t("proxies.importDialog.errors")}
</span>
<span className="text-sm font-medium text-destructive">
<span className="text-sm font-medium text-destructive-text">
{importResult.errors.length}
</span>
</div>
@@ -521,7 +521,7 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
{importResult.errors.map((error, i) => (
<div
key={`error-${i}`}
className="text-xs text-destructive"
className="text-xs text-destructive-text"
>
{error}
</div>
+4 -4
View File
@@ -1111,13 +1111,13 @@ export function ProxyManagementDialog({
<AnimatedTabsList>
<AnimatedTabsTrigger value="proxies">
<span>{t("proxies.management.tabProxies")}</span>
<span className="text-xs text-muted-foreground tabular-nums">
<span className="text-xs tabular-nums">
{storedProxies.length}
</span>
</AnimatedTabsTrigger>
<AnimatedTabsTrigger value="vpns">
<span>{t("proxies.management.tabVpns")}</span>
<span className="text-xs text-muted-foreground tabular-nums">
<span className="text-xs tabular-nums">
{vpnConfigs.length}
</span>
</AnimatedTabsTrigger>
@@ -1531,7 +1531,7 @@ export function ProxyManagementDialog({
}}
size="icon"
variant="destructive"
className="border-destructive bg-destructive/50 hover:bg-destructive/70"
className="border-destructive bg-destructive hover:bg-destructive"
>
<LuTrash2 />
</DataTableActionBarAction>
@@ -1554,7 +1554,7 @@ export function ProxyManagementDialog({
}}
size="icon"
variant="destructive"
className="border-destructive bg-destructive/50 hover:bg-destructive/70"
className="border-destructive bg-destructive hover:bg-destructive"
>
<LuTrash2 />
</DataTableActionBarAction>
+8 -8
View File
@@ -319,8 +319,8 @@ export function RailNav({
className={cn(
"relative grid size-7 shrink-0 cursor-pointer place-items-center rounded-md transition-colors duration-100",
active
? "bg-accent text-foreground"
: "text-muted-foreground hover:bg-accent/50 hover:text-card-foreground",
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
)}
>
{active && <ActiveIndicator />}
@@ -347,8 +347,8 @@ export function RailNav({
className={cn(
"grid size-7 shrink-0 cursor-pointer place-items-center rounded-md transition-colors duration-100",
moreOpen
? "bg-accent text-foreground"
: "text-muted-foreground hover:bg-accent/50 hover:text-card-foreground",
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
)}
>
<GoKebabHorizontal className="size-3.5" />
@@ -369,8 +369,8 @@ export function RailNav({
className={cn(
"relative grid size-7 shrink-0 cursor-pointer place-items-center rounded-md transition-colors duration-100",
currentPage === "settings"
? "bg-accent text-foreground"
: "text-muted-foreground hover:bg-accent/50 hover:text-card-foreground",
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
)}
>
{currentPage === "settings" && <ActiveIndicator />}
@@ -404,7 +404,7 @@ export function RailNav({
setMoreOpen(false);
onNavigate(page);
}}
className="flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-100 hover:bg-accent"
className="flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-100 hover:bg-accent hover:text-accent-foreground"
>
<span className="grid size-5 shrink-0 place-items-center rounded bg-muted text-muted-foreground">
<Icon className="size-3" />
@@ -426,7 +426,7 @@ export function RailNav({
setMoreOpen(false);
onOpenAbout();
}}
className="flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-100 hover:bg-accent"
className="flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-100 hover:bg-accent hover:text-accent-foreground"
>
<span className="grid size-5 shrink-0 place-items-center rounded bg-muted text-muted-foreground">
<LuInfo className="size-3" />
+22 -33
View File
@@ -51,8 +51,11 @@ import { useLanguage } from "@/hooks/use-language";
import type { PermissionType } from "@/hooks/use-permissions";
import { usePermissions } from "@/hooks/use-permissions";
import {
applyThemeColors,
clearThemeColors,
getThemeByColors,
getThemeById,
normalizeThemeColors,
THEME_VARIABLES,
THEMES,
withThemeTransition,
@@ -238,19 +241,13 @@ export function SettingsDialog({
const applyCustomTheme = useCallback((vars: Record<string, string>) => {
withThemeTransition(() => {
const root = document.documentElement;
Object.entries(vars).forEach(([k, v]) => {
root.style.setProperty(k, v, "important");
});
applyThemeColors(vars);
});
}, []);
const clearCustomTheme = useCallback(() => {
withThemeTransition(() => {
const root = document.documentElement;
THEME_VARIABLES.forEach(({ key }) => {
root.style.removeProperty(key as string);
});
clearThemeColors();
});
}, []);
@@ -267,7 +264,7 @@ export function SettingsDialog({
custom_theme:
appSettings.custom_theme &&
Object.keys(appSettings.custom_theme).length > 0
? appSettings.custom_theme
? normalizeThemeColors(appSettings.custom_theme)
: tokyoNightTheme.colors,
};
setSettings(merged);
@@ -490,24 +487,15 @@ export function SettingsDialog({
if (settings.theme === "custom") {
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);
});
Object.entries(customThemeState.colors).forEach(([k, v]) => {
root.style.setProperty(k, v, "important");
});
clearThemeColors();
applyThemeColors(customThemeState.colors);
} catch {
/* empty */
}
}
} else {
try {
const root = document.documentElement;
THEME_VARIABLES.forEach(({ key }) => {
root.style.removeProperty(key as string);
});
clearThemeColors();
} catch {
/* empty */
}
@@ -569,6 +557,7 @@ export function SettingsDialog({
applyCustomTheme(originalSettings.custom_theme);
} else {
clearCustomTheme();
setTheme(originalSettings.theme);
}
// Reset custom theme state to original
@@ -587,6 +576,7 @@ export function SettingsDialog({
applyCustomTheme,
clearCustomTheme,
onClose,
setTheme,
]);
// Only clear custom theme when switching away from custom, don't apply live
@@ -596,8 +586,9 @@ export function SettingsDialog({
useEffect(() => {
if (hasLoadedSettings && settings.theme !== "custom") {
clearCustomTheme();
setTheme(settings.theme);
}
}, [hasLoadedSettings, settings.theme, clearCustomTheme]);
}, [hasLoadedSettings, settings.theme, clearCustomTheme, setTheme]);
// Unmount-safe theme restore: rail navigation unmounts this dialog without
// calling handleClose, which used to leave the theme vars wiped for the
@@ -606,21 +597,17 @@ export function SettingsDialog({
return () => {
const s = originalSettingsRef.current;
if (!hasLoadedSettingsRef.current || !s) return;
const root = document.documentElement;
if (s.theme === "custom" && s.custom_theme) {
Object.entries(s.custom_theme).forEach(([k, v]) => {
root.style.setProperty(k, v, "important");
});
applyThemeColors(s.custom_theme);
} else {
// A non-custom persisted theme (light/dark/system) uses the
// stylesheet palette — strip any leftover inline custom vars so a
// just-saved switch away from custom isn't reverted on unmount.
THEME_VARIABLES.forEach(({ key }) => {
root.style.removeProperty(key as string);
});
clearThemeColors();
setTheme(s.theme);
}
};
}, []);
}, [setTheme]);
useEffect(() => {
if (isOpen) {
@@ -1177,7 +1164,9 @@ export function SettingsDialog({
}}
/>
{e2eError && (
<p className="text-sm text-destructive">{e2eError}</p>
<p className="text-sm text-destructive-text">
{e2eError}
</p>
)}
<LoadingButton
variant="default"
@@ -1249,7 +1238,7 @@ export function SettingsDialog({
// customer reads like a billing error, so swap in a
// subscription-active badge instead.
<div className="space-y-1">
<p className="text-sm font-medium text-success">
<p className="text-sm font-medium text-success-text">
{t("settings.commercial.subscriptionActive", {
plan: cloudUser.plan,
})}
@@ -1272,7 +1261,7 @@ export function SettingsDialog({
</div>
) : (
<div className="space-y-1">
<p className="text-sm font-medium text-warning">
<p className="text-sm font-medium text-warning-text">
{t("settings.commercial.trialExpired")}
</p>
<p className="text-xs text-muted-foreground">
+1 -1
View File
@@ -168,7 +168,7 @@ export function SyncAllDialog({ isOpen, onClose }: SyncAllDialogProps) {
}}
className="flex items-center gap-3 rounded-lg border border-border/60 bg-card/50 p-3 transition-colors hover:bg-card"
>
<div className="flex size-9 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
<div className="flex size-9 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary-text">
<Icon className="size-4" />
</div>
<div className="min-w-0 flex-1 truncate text-sm font-medium">
+1 -1
View File
@@ -412,7 +412,7 @@ export function SyncConfigDialog({
onClick={() => {
setShowToken(!showToken);
}}
className="absolute top-1/2 right-3 -translate-y-1/2 transform rounded-sm p-1 transition-colors hover:bg-accent"
className="absolute top-1/2 right-3 -translate-y-1/2 transform rounded-sm p-1 transition-colors hover:bg-accent hover:text-accent-foreground"
aria-label={
showToken
? t("common.aria.hideToken")
+2 -2
View File
@@ -155,7 +155,7 @@ export function SyncFollowerDialog({
return (
<div
key={profile.id}
className="flex cursor-pointer items-center gap-3 rounded-md p-2 hover:bg-accent"
className="flex cursor-pointer items-center gap-3 rounded-md p-2 hover:bg-accent hover:text-accent-foreground"
onClick={() => {
handleToggle(
profile.id,
@@ -182,7 +182,7 @@ export function SyncFollowerDialog({
<TooltipTrigger asChild>
<Badge
variant="outline"
className="shrink-0 border-warning/50 px-1.5 py-0 text-[10px] text-warning"
className="shrink-0 border-warning/50 px-1.5 py-0 text-[10px] text-warning-text"
>
{t("profiles.synchronizer.flakyBadge")}
</Badge>
+3 -4
View File
@@ -59,9 +59,7 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
const setTheme = useCallback((newTheme: string) => {
setThemeState(newTheme);
withThemeTransition(() => {
if (newTheme === "custom") {
applyClassToHtml("dark");
} else {
if (newTheme !== "custom") {
applyClassToHtml(newTheme);
}
});
@@ -77,7 +75,6 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
if (themeValue === "custom") {
setThemeState("custom");
applyClassToHtml("dark");
if (
settings.custom_theme &&
Object.keys(settings.custom_theme).length > 0
@@ -87,6 +84,8 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
} catch (error) {
console.warn("Failed to apply custom theme variables:", error);
}
} else {
applyClassToHtml("dark");
}
} else if (
themeValue === "light" ||
+1 -1
View File
@@ -10,7 +10,7 @@ const alertVariants = cva(
variant: {
default: "bg-card text-card-foreground",
destructive:
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
"bg-card text-destructive-text *:data-[slot=alert-description]:text-destructive-text [&>svg]:text-current",
},
},
defaultVariants: {
+3 -1
View File
@@ -122,7 +122,8 @@ function AnimatedTabsTrigger({
className={cn(
"relative inline-flex h-7 cursor-pointer items-center justify-center gap-1.5 rounded-md px-3 text-sm font-medium whitespace-nowrap transition-colors duration-150",
"text-muted-foreground hover:text-foreground",
isActive && "text-foreground",
isActive && !showIndicator && "text-foreground",
showIndicator && "text-accent-foreground hover:text-accent-foreground",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none",
"disabled:pointer-events-none disabled:opacity-50",
className,
@@ -132,6 +133,7 @@ function AnimatedTabsTrigger({
{showIndicator && (
<motion.span
layoutId={`animated-tabs-indicator-${indicatorId}`}
data-slot="animated-tabs-indicator"
className="pointer-events-none absolute inset-0 -z-10 rounded-md bg-accent"
transition={{ type: "spring", stiffness: 360, damping: 32 }}
/>
+3 -3
View File
@@ -10,11 +10,11 @@ const badgeVariants = cva(
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
"border-transparent bg-primary text-primary-foreground [a&]:hover:shadow-sm",
secondary:
"border-transparent bg-secondary text-secondary-foreground dark:bg-secondary/60 [a&]:hover:bg-secondary/90",
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:shadow-sm",
destructive:
"border-transparent bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
"border-transparent bg-destructive text-destructive-foreground focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 [a&]:hover:shadow-sm",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
+6 -8
View File
@@ -9,17 +9,15 @@ const buttonVariants = cva(
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
default: "bg-primary text-primary-foreground shadow-xs hover:shadow-md",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
"bg-destructive text-destructive-foreground shadow-xs hover:shadow-md focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
"border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
"bg-secondary text-secondary-foreground shadow-xs hover:shadow-md",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary-text underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
+1 -1
View File
@@ -153,7 +153,7 @@ function CommandItem({
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
"relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[selected=true]:[&_svg:not([class*='text-'])]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
className,
)}
{...props}
+1 -1
View File
@@ -270,7 +270,7 @@ function DialogContent({
>
{children}
{!hideClose && dismissible && (
<DialogPrimitive.Close className="absolute top-4 right-4 cursor-pointer rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<DialogPrimitive.Close className="absolute top-4 right-4 cursor-pointer rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<RxCross2 />
<span className="sr-only">{t("common.buttons.close")}</span>
</DialogPrimitive.Close>
+1 -1
View File
@@ -74,7 +74,7 @@ function DropdownMenuItem({
data-inset={inset}
data-variant={variant}
className={cn(
"relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
"relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-8 data-[variant=destructive]:text-destructive-text data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive-text focus:[&_svg:not([class*='text-'])]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive-text!",
className,
)}
{...props}
+1 -1
View File
@@ -28,7 +28,7 @@ const RadioGroupItem = React.forwardRef<
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square size-4 cursor-pointer rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"aspect-square size-4 cursor-pointer rounded-full border border-primary-text text-primary-text ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
+5 -7
View File
@@ -11,15 +11,13 @@ const buttonVariants = cva(
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
default: "bg-primary text-primary-foreground hover:shadow-sm",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
"bg-destructive text-destructive-foreground hover:shadow-sm focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
outline:
"border bg-background hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
"border border-input bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30",
secondary: "bg-secondary text-secondary-foreground hover:shadow-sm",
ghost: "hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-4 py-2 has-[>svg]:px-3",
+1 -1
View File
@@ -107,7 +107,7 @@ function SelectItem({
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
"relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 focus:[&_svg:not([class*='text-'])]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
+4 -4
View File
@@ -109,9 +109,9 @@ export function VpnCheckButton({
{isCurrentlyChecking ? (
<span className="size-3 animate-spin rounded-full border border-current border-t-transparent" />
) : result?.is_valid ? (
<FiCheck className="size-3 text-success" />
<FiCheck className="size-3 text-success-text" />
) : result && !result.is_valid ? (
<span className="text-sm text-destructive"></span>
<span className="text-sm text-destructive-text"></span>
) : (
<FiCheck className="size-3" />
)}
@@ -125,7 +125,7 @@ export function VpnCheckButton({
) : result?.is_valid ? (
<div className="space-y-1">
<p>{t("vpnCheck.tooltipValid")}</p>
<p className="text-xs text-primary-foreground/70">
<p className="text-xs text-primary-foreground">
{t("vpnCheck.tooltipChecked", {
time: formatRelativeTime(result.timestamp),
})}
@@ -134,7 +134,7 @@ export function VpnCheckButton({
) : result && !result.is_valid ? (
<div>
<p>{t("vpnCheck.tooltipInvalid")}</p>
<p className="text-xs text-primary-foreground/70">
<p className="text-xs text-primary-foreground">
{t("vpnCheck.tooltipChecked", {
time: formatRelativeTime(result.timestamp),
})}
+5 -5
View File
@@ -282,7 +282,7 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
{step === "vpn-preview" && vpnPreview && (
<div className="space-y-4">
<div className="flex items-center gap-3 rounded-lg bg-muted/30 p-4">
<LuShield className="size-8 text-primary" />
<LuShield className="size-8 text-primary-text" />
<div>
<div className="font-medium">
{t("vpns.import.configurationLabel", {
@@ -332,9 +332,9 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
>
{vpnImportResult.success ? (
<div className="flex items-center gap-3">
<LuShield className="size-8 text-success" />
<LuShield className="size-8 text-success-text" />
<div>
<div className="font-medium text-success">
<div className="font-medium text-success-text">
{t("vpns.import.importedSuccess")}
</div>
<div className="text-sm text-muted-foreground">
@@ -344,10 +344,10 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
</div>
) : (
<div className="space-y-2">
<div className="font-medium text-destructive">
<div className="font-medium text-destructive-text">
{t("vpns.import.importFailed")}
</div>
<div className="text-sm text-destructive">
<div className="text-sm text-destructive-text">
{vpnImportResult.error}
</div>
</div>
+1 -1
View File
@@ -70,7 +70,7 @@ export function WayfernTermsDialog({
href="https://wayfern.com/tos"
target="_blank"
rel="noopener noreferrer"
className="block text-sm font-medium text-primary hover:underline"
className="block text-sm font-medium text-primary-text hover:underline"
>
https://wayfern.com/tos
</a>
+4 -4
View File
@@ -316,7 +316,7 @@ export function WelcomeDialog({
<dl className="flex flex-col gap-3">
<div className="flex items-start gap-3 rounded-lg border p-4">
<LuHeart className="mt-0.5 size-4 shrink-0 text-success" />
<LuHeart className="mt-0.5 size-4 shrink-0 text-success-text" />
<div className="flex flex-col gap-0.5 text-left">
<dt className="text-base/7 font-medium text-foreground sm:text-sm/6">
{t("welcome.license.personalTitle")}
@@ -331,7 +331,7 @@ export function WelcomeDialog({
<div className="flex flex-col gap-0.5 text-left">
<dt className="flex flex-wrap items-center gap-2 text-base/7 font-medium text-foreground sm:text-sm/6">
{t("welcome.license.commercialTitle")}
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary-text">
{t("welcome.license.trialBadge")}
</span>
</dt>
@@ -389,7 +389,7 @@ export function WelcomeDialog({
initial={{ opacity: 0, scale: 0.85, rotate: -8 }}
animate={{ opacity: 1, scale: 1, rotate: 0 }}
transition={panelTransition}
className="flex size-12 items-center justify-center gap-1.5 rounded-full bg-primary/10 text-primary"
className="flex size-12 items-center justify-center gap-1.5 rounded-full bg-primary/10 text-primary-text"
aria-hidden="true"
>
<LuMic className="size-4 shrink-0" />
@@ -445,7 +445,7 @@ export function WelcomeDialog({
{setup.phase === "error" ? (
<>
<div className="flex flex-col items-center gap-2">
<h2 className="flex items-center justify-center gap-2 text-2xl font-semibold tracking-tight text-balance text-destructive">
<h2 className="flex items-center justify-center gap-2 text-2xl font-semibold tracking-tight text-balance text-destructive-text">
<LuTriangleAlert className="size-5 shrink-0" />
{t("welcome.ready.errorTitle")}
</h2>
+1 -1
View File
@@ -167,7 +167,7 @@ export function WindowDragArea() {
onClick={() => {
void handleClose();
}}
className="flex h-full w-11 items-center justify-center text-muted-foreground transition-colors hover:bg-destructive/90 hover:text-destructive-foreground"
className="flex h-full w-11 items-center justify-center text-muted-foreground transition-colors hover:bg-destructive hover:text-destructive-foreground"
aria-label={t("common.buttons.close")}
>
<svg