style: cleanup

This commit is contained in:
zhom committed 2026-10-10 01:11:04 +04:00
1 parent 18a891f709
commit be78a6b722
5 files changed
+262 -89

No files matched your search

+45 -1
View File
@@ -194,6 +194,50 @@ test("profile creation stays compact and saves basic, protected, and ephemeral p
en.createProfile.proxy.noProxy,
]);
await app.capture("profile-create-compact-wide");
// The device's operating system is chosen on the dialog itself. Without
// a plan that includes cross-OS fingerprints, only this computer's
// system can be picked.
const hostOs =
{ darwin: "macOS", win32: "Windows" }[process.platform] ?? "Linux";
assert.equal(
await app.execute(
`return document.querySelector('#profile-os').textContent.trim();`,
),
hostOs,
);
await app.clickSelector("#profile-os");
await app.waitFor(
() =>
app.execute(
`return document.querySelectorAll('[cmdk-item]').length;`,
),
{ description: "operating system options" },
);
const osOptions = await app.execute(`
return [...document.querySelectorAll('[cmdk-item]')].map((item) => ({
text: item.textContent.trim(),
disabled: item.getAttribute('aria-disabled') === 'true',
}));
`);
assert.equal(osOptions.length, 5, JSON.stringify(osOptions));
assert.deepEqual(
osOptions.filter((option) => !option.disabled).map((o) => o.text),
[hostOs],
);
assert.ok(
osOptions
.filter((option) => option.disabled)
.every((option) => option.text.endsWith("PRO")),
JSON.stringify(osOptions),
);
await app.pressShortcut({ key: "Escape" });
await app.waitFor(
() =>
app.execute(
`return !document.querySelector('[cmdk-item]') && Boolean(document.querySelector('#profile-name'));`,
),
{ description: "the OS menu to close and the dialog to stay" },
);
// Adding a route is part of the route list, and stays there when the
// search finds nothing.
await app.clickSelector("#profile-proxy");
@@ -1350,7 +1394,7 @@ test("soft table and creation surfaces stay quiet and readable in every theme",
const colors = await paintedColors(app, {
off: ["#ephemeral"],
on: ["#enable-password"],
name: ['label[for="profile-name"]'],
name: ['[data-slot="profile-name-field"]'],
password: ["#profile-password"],
});
assertQuietTint(colors.off, colors.page, 1.35, `${theme.id} option`);
+135 -30
View File
@@ -52,6 +52,7 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ProBadge } from "@/components/ui/pro-badge";
import {
Select,
SelectContent,
@@ -69,6 +70,7 @@ import { useBrowserDownload } from "@/hooks/use-browser-download";
import { useInputModality } from "@/hooks/use-input-modality";
import { useProxyEvents } from "@/hooks/use-proxy-events";
import { useVpnEvents } from "@/hooks/use-vpn-events";
import { getOSDisplayName, getOSIcon } from "@/lib/browser-utils";
import { DNS_BLOCKLIST_LEVELS } from "@/lib/dns-blocklist-levels";
import { MOTION_EASE_OUT } from "@/lib/motion";
import { cn } from "@/lib/utils";
@@ -76,6 +78,8 @@ import type { BrowserReleaseTypes, WayfernConfig, WayfernOS } from "@/types";
const PASSWORD_MIN_LEN = 8;
const OS_OPTIONS: WayfernOS[] = ["windows", "macos", "linux", "android", "ios"];
const getCurrentOS = (): WayfernOS => {
if (typeof navigator === "undefined") return "linux";
const platform = navigator.platform.toLowerCase();
@@ -125,6 +129,9 @@ export function CreateProfileDialog({
const proxyListboxId = useId();
const proxyLabelId = useId();
const proxyValueId = useId();
const osListboxId = useId();
const osLabelId = useId();
const osValueId = useId();
const advancedOptionsId = useId();
const reduceMotion = useReducedMotion() ?? false;
const inputModality = useInputModality();
@@ -132,6 +139,7 @@ export function CreateProfileDialog({
const [advancedOpen, setAdvancedOpen] = useState(false);
const [selectedProxyId, setSelectedProxyId] = useState<string>();
const [proxyPopoverOpen, setProxyPopoverOpen] = useState(false);
const [osPopoverOpen, setOsPopoverOpen] = useState(false);
const [showProxyForm, setShowProxyForm] = useState(false);
const [dnsBlocklist, setDnsBlocklist] = useState("");
const [launchHook, setLaunchHook] = useState("");
@@ -271,6 +279,10 @@ export function CreateProfileDialog({
const proxyLabel = selectedVpn
? `WG — ${selectedVpn.name}`
: (selectedProxy?.name ?? t("createProfile.proxy.noProxy"));
const hostOs = getCurrentOS();
const selectedOs = wayfernConfig.os ?? hostOs;
const crossOs = selectedOs !== hostOs;
const SelectedOsIcon = getOSIcon(selectedOs);
const clearPassword = () => {
setEnablePassword(false);
@@ -285,6 +297,7 @@ export function CreateProfileDialog({
setAdvancedOpen(false);
setSelectedProxyId(undefined);
setProxyPopoverOpen(false);
setOsPopoverOpen(false);
setShowProxyForm(false);
setDnsBlocklist("");
setLaunchHook("");
@@ -417,38 +430,115 @@ export function CreateProfileDialog({
data-slot="profile-create-fields"
className="-mx-1 flex min-h-0 flex-col gap-3 overflow-y-auto px-1 pt-1 pb-2"
>
<motion.label
htmlFor="profile-name"
className="flex cursor-text items-center gap-2.5 rounded-xl bg-foreground/4 p-2 transition-[background-color,box-shadow] duration-150 focus-within:bg-foreground/6 focus-within:ring-2 focus-within:ring-foreground/10 hover:bg-foreground/5"
<motion.div
data-slot="profile-name-field"
className="flex items-center gap-2 rounded-xl bg-foreground/4 p-2 transition-[background-color,box-shadow] duration-150 focus-within:bg-foreground/6 focus-within:ring-2 focus-within:ring-foreground/10 hover:bg-foreground/5"
{...rise(0)}
>
<ProfileGlyph
seed={profileName}
routed={!!selectedProxyId}
locked={enablePassword}
ephemeral={ephemeral}
busy={isCreating}
/>
<span className="sr-only">{t("createProfile.profileName")}</span>
<Input
id="profile-name"
variant="bare"
autoFocus
autoComplete="off"
spellCheck={false}
value={profileName}
onChange={(event) => setProfileName(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter" && !event.nativeEvent.isComposing) {
event.preventDefault();
void handleCreate();
}
}}
placeholder={t("createProfile.profileNamePlaceholder")}
disabled={isCreating}
className="h-9 text-base font-medium md:text-base"
/>
</motion.label>
<label
htmlFor="profile-name"
className="flex min-w-0 flex-1 cursor-text items-center gap-2.5"
>
<ProfileGlyph
seed={profileName}
routed={!!selectedProxyId}
locked={enablePassword}
ephemeral={ephemeral}
busy={isCreating}
/>
<span className="sr-only">{t("createProfile.profileName")}</span>
<Input
id="profile-name"
variant="bare"
autoFocus
autoComplete="off"
spellCheck={false}
value={profileName}
onChange={(event) => setProfileName(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter" && !event.nativeEvent.isComposing) {
event.preventDefault();
void handleCreate();
}
}}
placeholder={t("createProfile.profileNamePlaceholder")}
disabled={isCreating}
className="h-9 text-base font-medium md:text-base"
/>
</label>
<span id={osLabelId} className="sr-only">
{t("fingerprint.osLabel")}
</span>
<Popover open={osPopoverOpen} onOpenChange={setOsPopoverOpen}>
<PopoverTrigger asChild>
<button
id="profile-os"
type="button"
role="combobox"
aria-expanded={osPopoverOpen}
aria-controls={osListboxId}
aria-labelledby={`${osLabelId} ${osValueId}`}
className={cn(optionPill(crossOs), "shrink-0")}
disabled={isCreating}
>
<PillIcon on={crossOs} animate={animateEntry}>
<SelectedOsIcon />
</PillIcon>
<PillLabel
id={osValueId}
label={getOSDisplayName(selectedOs)}
animate={animateEntry}
/>
<LuChevronDown
className={cn(
"-mr-0.5 opacity-60 transition-transform duration-200 motion-reduce:transition-none",
osPopoverOpen && "rotate-180",
)}
aria-hidden="true"
/>
</button>
</PopoverTrigger>
<PopoverContent className="w-52 p-0" align="end" sideOffset={6}>
<Command>
<CommandList id={osListboxId}>
<CommandGroup>
{OS_OPTIONS.map((os) => {
const locked = os !== hostOs && !crossOsUnlocked;
const Icon = getOSIcon(os);
return (
<CommandItem
key={os}
value={os}
disabled={locked}
onSelect={() => {
updateWayfernConfig("os", os);
setOsPopoverOpen(false);
}}
>
<LuCheck
className={cn(
"size-4 shrink-0",
selectedOs === os ? "opacity-100" : "opacity-0",
)}
aria-hidden="true"
/>
<Icon
className="size-3.5 shrink-0"
aria-hidden="true"
/>
<span className="truncate">
{getOSDisplayName(os)}
</span>
{locked && <ProBadge className="ml-auto" />}
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</motion.div>
<motion.div
className="flex flex-wrap items-center gap-1.5"
@@ -688,6 +778,20 @@ export function CreateProfileDialog({
</motion.div>
)}
{crossOs && (
<motion.p
role="status"
className={cn(STATUS_CLASS, "items-start")}
{...reveal}
>
<LuTriangleAlert
className="mt-0.5 size-3.5 shrink-0"
aria-hidden="true"
/>
{t("fingerprint.crossOsWarning")}
</motion.p>
)}
{downloading ? (
<motion.p role="status" className={STATUS_CLASS} {...reveal}>
<LuLoaderCircle
@@ -920,6 +1024,7 @@ export function CreateProfileDialog({
readOnly={isCreating}
crossOsUnlocked={crossOsUnlocked}
limitedMode={!crossOsUnlocked}
osPicker={false}
profileVersion={creatableVersion}
profileBrowser="wayfern"
/>
+2 -8
View File
@@ -19,7 +19,6 @@ import { emit, listen } from "@tauri-apps/api/event";
import type { Dispatch, SetStateAction } from "react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { FaApple, FaLinux, FaWindows } from "react-icons/fa";
import { FiWifi } from "react-icons/fi";
import {
LuArrowDown,
@@ -128,6 +127,7 @@ import { parseBackendError, translateBackendError } from "@/lib/backend-errors";
import {
getBrowserDisplayName,
getOSDisplayName,
getOSIcon,
getProfileIcon,
isCrossOsProfile,
} from "@/lib/browser-utils";
@@ -2543,13 +2543,7 @@ export function ProfilesDataTable({
const profile = row.original;
const crossOs = isCrossOsProfile(profile);
const os = profile.host_os || profile.wayfern_config?.os || "";
const Icon = crossOs
? os === "macos"
? FaApple
: os === "windows"
? FaWindows
: FaLinux
: getProfileIcon(profile);
const Icon = crossOs ? getOSIcon(os) : getProfileIcon(profile);
const message = crossOs
? t("crossOs.viewOnly", { os: getOSDisplayName(os) })
: meta.runningProfiles.has(profile.id)
+58 -50
View File
@@ -176,6 +176,8 @@ interface WayfernConfigFormProps {
readOnly?: boolean;
crossOsUnlocked?: boolean;
limitedMode?: boolean;
/** False when the caller shows its own operating system picker. */
osPicker?: boolean;
profileVersion?: string;
profileBrowser?: string;
/// The saved profile whose persona is edited here. Absent while a profile is
@@ -275,6 +277,7 @@ export function WayfernConfigForm({
readOnly = false,
crossOsUnlocked = false,
limitedMode = false,
osPicker = true,
profileVersion,
profileBrowser,
profileId,
@@ -428,60 +431,65 @@ export function WayfernConfigForm({
* whereas their absence is the truth. */
const isIdentityDerived = config.identity_id != null;
const generateButton = profileVersion && (!isCreating || crossOsUnlocked) && (
<LoadingButton
isLoading={isGeneratingFingerprint}
onClick={handleRegenerateClick}
disabled={readOnly}
variant="outline"
size="sm"
>
{isCreating
? t("fingerprint.generateFingerprint")
: t("fingerprint.regenerateFingerprint")}
</LoadingButton>
);
const renderAdvancedForm = () => (
<div className="space-y-6">
{/* Operating System Selection */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label>{t("fingerprint.osLabel")}</Label>
{profileVersion && (!isCreating || crossOsUnlocked) && (
<LoadingButton
isLoading={isGeneratingFingerprint}
onClick={handleRegenerateClick}
disabled={readOnly}
variant="outline"
size="sm"
>
{isCreating
? t("fingerprint.generateFingerprint")
: t("fingerprint.regenerateFingerprint")}
</LoadingButton>
{!osPicker ? (
generateButton && <div className="flex">{generateButton}</div>
) : (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label>{t("fingerprint.osLabel")}</Label>
{generateButton}
</div>
<Select
value={selectedOS}
onValueChange={(value: WayfernOS) => {
onConfigChange("os", value);
}}
disabled={readOnly}
>
<SelectTrigger>
<SelectValue placeholder={t("fingerprint.selectOSPlaceholder")} />
</SelectTrigger>
<SelectContent>
{(
["windows", "macos", "linux", "android", "ios"] as WayfernOS[]
).map((os) => {
const isDisabled = os !== currentOS && !crossOsUnlocked;
return (
<SelectItem key={os} value={os} disabled={isDisabled}>
<span className="flex items-center gap-2">
{osLabels[os]}
{isDisabled && <ProBadge />}
</span>
</SelectItem>
);
})}
</SelectContent>
</Select>
{selectedOS !== currentOS && crossOsUnlocked && (
<Alert className="mt-2">
<AlertDescription>
{t("fingerprint.crossOsWarning")}
</AlertDescription>
</Alert>
)}
</div>
<Select
value={selectedOS}
onValueChange={(value: WayfernOS) => {
onConfigChange("os", value);
}}
disabled={readOnly}
>
<SelectTrigger>
<SelectValue placeholder={t("fingerprint.selectOSPlaceholder")} />
</SelectTrigger>
<SelectContent>
{(
["windows", "macos", "linux", "android", "ios"] as WayfernOS[]
).map((os) => {
const isDisabled = os !== currentOS && !crossOsUnlocked;
return (
<SelectItem key={os} value={os} disabled={isDisabled}>
<span className="flex items-center gap-2">
{osLabels[os]}
{isDisabled && <ProBadge />}
</span>
</SelectItem>
);
})}
</SelectContent>
</Select>
{selectedOS !== currentOS && crossOsUnlocked && (
<Alert className="mt-2">
<AlertDescription>
{t("fingerprint.crossOsWarning")}
</AlertDescription>
</Alert>
)}
</div>
)}
{/* Randomize Fingerprint Option */}
<div className="space-y-3 rounded-lg bg-foreground/4 p-4">
+22
View File
@@ -6,12 +6,16 @@ import { SiBrave, SiOpera, SiVivaldi } from "react-icons/si";
*/
import {
FaAndroid,
FaApple,
FaChrome,
FaEdge,
FaExclamationTriangle,
FaFire,
FaFirefox,
FaLinux,
FaSafari,
FaWindows,
} from "react-icons/fa";
import { LuLock } from "react-icons/lu";
import { getCurrentOS } from "@/lib/platform";
@@ -103,7 +107,25 @@ export function getOSDisplayName(os: string): string {
return "Windows";
case "linux":
return "Linux";
case "android":
return "Android";
case "ios":
return "iOS";
default:
return os;
}
}
export function getOSIcon(os: string) {
switch (os) {
case "macos":
case "ios":
return FaApple;
case "windows":
return FaWindows;
case "android":
return FaAndroid;
default:
return FaLinux;
}
}