mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-18 00:47:19 +02:00
feat: xray support
This commit is contained in:
+232
-62
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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),
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" ||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 }}
|
||||
/>
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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),
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user