feat: port the demo redesign with Manage and Settings tabs

This commit is contained in:
LukeGus committed 2026-10-05 16:23:21 -05:00
1 parent f8151cb885
commit 35c954b79a
128 files changed
+13262 -8143

No files matched your search

+239 -212
View File
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { InlineView, useSurfaceKind } from "@/components/surface/surface-scope";
import { toast } from "sonner";
import { AlertCircle, Loader2, X } from "lucide-react";
import { Button } from "@/components/button";
@@ -222,6 +223,241 @@ export function LinkServerDialog({
0,
);
const wide = step === "signin";
const inSurface = useSurfaceKind() !== null;
const title = relogin
? t("sync.wizard.reloginTitle")
: t("sync.wizard.title");
const description =
step === "server"
? t("sync.wizard.serverDescription")
: step === "signin"
? t("sync.wizard.signinDescription", { url: target.serverUrl })
: step === "choose"
? t("sync.wizard.chooseDescription")
: t("sync.wizard.finishingDescription");
const content = (
<>
{step === "server" && (
<form
className="flex flex-col gap-4"
onSubmit={(event) => {
event.preventDefault();
void handleCheck();
}}
>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-semibold" htmlFor="sync-server-url">
{t("sync.wizard.serverUrl")}
</label>
<Input
id="sync-server-url"
autoFocus
className="rounded-none"
placeholder="https://termix.example.com"
value={target.serverUrl}
onChange={(event) =>
setTarget({ ...target, serverUrl: event.target.value })
}
/>
<p className="text-[10px] text-muted-foreground">
{t("sync.wizard.serverUrlHint")}
</p>
{!relogin && savedUrls.length > 0 && (
<div className="flex flex-col gap-1 pt-1">
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
{t("sync.wizard.savedServers")}
</span>
{savedUrls.map((url) => (
<div
key={url}
className="flex items-center border border-border hover:bg-muted"
>
<button
type="button"
className="flex-1 min-w-0 truncate px-2 py-1.5 text-left font-mono text-xs"
onClick={() => setTarget({ ...target, serverUrl: url })}
>
{url}
</button>
<button
type="button"
title={t("sync.wizard.removeServer")}
className="px-2 py-1.5 text-muted-foreground hover:text-destructive"
onClick={() => setSavedUrls(forgetServerUrl(url))}
>
<X className="size-3" />
</button>
</div>
))}
</div>
)}
</div>
{problem && (
<div className="flex gap-2 border border-destructive/40 bg-destructive/10 p-2.5 text-xs">
<AlertCircle className="size-4 shrink-0 text-destructive" />
<span>{problem}</span>
</div>
)}
{(problemKind === "tls_untrusted" ||
target.allowInvalidCertificate) && (
<div className="flex items-center justify-between gap-3 border border-border p-2.5">
<div className="flex flex-col gap-0.5">
<span className="text-xs font-medium">
{t("sync.proxy.allowInvalidCertificate")}
</span>
<span className="text-[10px] text-muted-foreground">
{t("sync.proxy.allowInvalidCertificateHint")}
</span>
</div>
<Switch
checked={!!target.allowInvalidCertificate}
onCheckedChange={(checked) =>
setTarget({ ...target, allowInvalidCertificate: checked })
}
/>
</div>
)}
<button
type="button"
className="self-start text-[10px] font-bold uppercase tracking-widest text-muted-foreground hover:text-foreground"
onClick={() => setShowAdvanced((value) => !value)}
>
{showAdvanced
? t("sync.wizard.hideProxySettings")
: t("sync.wizard.showProxySettings")}
</button>
{showAdvanced && (
<ProxySettingsFields
customHeaders={target.customHeaders ?? []}
basicAuth={target.basicAuth ?? null}
onChange={(next) => setTarget({ ...target, ...next })}
/>
)}
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
className="rounded-none"
onClick={() => onOpenChange(false)}
>
{t("common.cancel")}
</Button>
<Button
type="submit"
className="rounded-none"
disabled={checking || !target.serverUrl.trim()}
>
{checking && <Loader2 className="size-4 animate-spin" />}
{t("sync.wizard.continue")}
</Button>
</div>
</form>
)}
{step === "signin" && (
<div className="flex-1 min-h-0">
<ElectronLoginForm
serverUrl={target.serverUrl}
targetPurpose="link"
onAuthSuccess={handleSignedIn}
onChangeServer={() =>
relogin ? onOpenChange(false) : setStep("server")
}
/>
</div>
)}
{step === "choose" && (
<div className="flex flex-col gap-3">
{finishError && (
<div className="flex gap-2 border border-destructive/40 bg-destructive/10 p-2.5 text-xs">
<AlertCircle className="size-4 shrink-0 text-destructive" />
<span>{finishError}</span>
</div>
)}
{(
[
{
value: "merge",
title: t("sync.wizard.mergeTitle"),
body: t("sync.wizard.mergeBody", { count: localTotal }),
},
{
value: "replace",
title: t("sync.wizard.replaceTitle"),
body: t("sync.wizard.replaceBody", { count: localTotal }),
},
] as const
).map((option) => (
<button
key={option.value}
type="button"
onClick={() => setMode(option.value)}
className={`text-left border p-3 flex flex-col gap-1 transition-colors ${
mode === option.value
? "border-accent-brand bg-accent-brand/10"
: "border-border hover:bg-muted/40"
}`}
>
<span className="text-sm font-semibold">{option.title}</span>
<span className="text-xs text-muted-foreground">
{option.body}
</span>
</button>
))}
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
className="rounded-none"
onClick={() => onOpenChange(false)}
>
{t("common.cancel")}
</Button>
<Button
type="button"
className="rounded-none"
disabled={!token}
onClick={() => token && void finish(token, mode)}
>
{t("sync.wizard.link")}
</Button>
</div>
</div>
)}
{step === "finishing" && (
<div className="flex items-center gap-3 py-6 text-sm text-muted-foreground">
<Loader2 className="size-5 animate-spin text-accent-brand" />
{t("sync.wizard.linking")}
</div>
)}
</>
);
// From a sidebar panel the wizard takes over the panel. Outside one (the
// first-run setup) it stays a dialog.
if (inSurface) {
return (
<InlineView
open={open}
onOpenChange={onOpenChange}
title={title}
width="wide"
scroll={!wide}
>
{!wide && (
<p className="text-xs text-muted-foreground">{description}</p>
)}
{content}
</InlineView>
);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -235,220 +471,11 @@ export function LinkServerDialog({
onInteractOutside={(event) => wide && event.preventDefault()}
>
<DialogHeader className={wide ? "sr-only" : undefined}>
<DialogTitle>
{relogin ? t("sync.wizard.reloginTitle") : t("sync.wizard.title")}
</DialogTitle>
<DialogDescription>
{step === "server" && t("sync.wizard.serverDescription")}
{step === "signin" &&
t("sync.wizard.signinDescription", { url: target.serverUrl })}
{step === "choose" && t("sync.wizard.chooseDescription")}
{step === "finishing" && t("sync.wizard.finishingDescription")}
</DialogDescription>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
{step === "server" && (
<form
className="flex flex-col gap-4"
onSubmit={(event) => {
event.preventDefault();
void handleCheck();
}}
>
<div className="flex flex-col gap-1.5">
<label
className="text-xs font-semibold"
htmlFor="sync-server-url"
>
{t("sync.wizard.serverUrl")}
</label>
<Input
id="sync-server-url"
autoFocus
className="rounded-none"
placeholder="https://termix.example.com"
value={target.serverUrl}
onChange={(event) =>
setTarget({ ...target, serverUrl: event.target.value })
}
/>
<p className="text-[10px] text-muted-foreground">
{t("sync.wizard.serverUrlHint")}
</p>
{!relogin && savedUrls.length > 0 && (
<div className="flex flex-col gap-1 pt-1">
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
{t("sync.wizard.savedServers")}
</span>
{savedUrls.map((url) => (
<div
key={url}
className="flex items-center border border-border hover:bg-muted"
>
<button
type="button"
className="flex-1 min-w-0 truncate px-2 py-1.5 text-left font-mono text-xs"
onClick={() => setTarget({ ...target, serverUrl: url })}
>
{url}
</button>
<button
type="button"
title={t("sync.wizard.removeServer")}
className="px-2 py-1.5 text-muted-foreground hover:text-destructive"
onClick={() => setSavedUrls(forgetServerUrl(url))}
>
<X className="size-3" />
</button>
</div>
))}
</div>
)}
</div>
{problem && (
<div className="flex gap-2 border border-destructive/40 bg-destructive/10 p-2.5 text-xs">
<AlertCircle className="size-4 shrink-0 text-destructive" />
<span>{problem}</span>
</div>
)}
{(problemKind === "tls_untrusted" ||
target.allowInvalidCertificate) && (
<div className="flex items-center justify-between gap-3 border border-border p-2.5">
<div className="flex flex-col gap-0.5">
<span className="text-xs font-medium">
{t("sync.proxy.allowInvalidCertificate")}
</span>
<span className="text-[10px] text-muted-foreground">
{t("sync.proxy.allowInvalidCertificateHint")}
</span>
</div>
<Switch
checked={!!target.allowInvalidCertificate}
onCheckedChange={(checked) =>
setTarget({ ...target, allowInvalidCertificate: checked })
}
/>
</div>
)}
<button
type="button"
className="self-start text-[10px] font-bold uppercase tracking-widest text-muted-foreground hover:text-foreground"
onClick={() => setShowAdvanced((value) => !value)}
>
{showAdvanced
? t("sync.wizard.hideProxySettings")
: t("sync.wizard.showProxySettings")}
</button>
{showAdvanced && (
<ProxySettingsFields
customHeaders={target.customHeaders ?? []}
basicAuth={target.basicAuth ?? null}
onChange={(next) => setTarget({ ...target, ...next })}
/>
)}
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
className="rounded-none"
onClick={() => onOpenChange(false)}
>
{t("common.cancel")}
</Button>
<Button
type="submit"
className="rounded-none"
disabled={checking || !target.serverUrl.trim()}
>
{checking && <Loader2 className="size-4 animate-spin" />}
{t("sync.wizard.continue")}
</Button>
</div>
</form>
)}
{step === "signin" && (
<div className="flex-1 min-h-0">
<ElectronLoginForm
serverUrl={target.serverUrl}
targetPurpose="link"
onAuthSuccess={handleSignedIn}
onChangeServer={() =>
relogin ? onOpenChange(false) : setStep("server")
}
/>
</div>
)}
{step === "choose" && (
<div className="flex flex-col gap-3">
{finishError && (
<div className="flex gap-2 border border-destructive/40 bg-destructive/10 p-2.5 text-xs">
<AlertCircle className="size-4 shrink-0 text-destructive" />
<span>{finishError}</span>
</div>
)}
{(
[
{
value: "merge",
title: t("sync.wizard.mergeTitle"),
body: t("sync.wizard.mergeBody", { count: localTotal }),
},
{
value: "replace",
title: t("sync.wizard.replaceTitle"),
body: t("sync.wizard.replaceBody", { count: localTotal }),
},
] as const
).map((option) => (
<button
key={option.value}
type="button"
onClick={() => setMode(option.value)}
className={`text-left border p-3 flex flex-col gap-1 transition-colors ${
mode === option.value
? "border-accent-brand bg-accent-brand/10"
: "border-border hover:bg-muted/40"
}`}
>
<span className="text-sm font-semibold">{option.title}</span>
<span className="text-xs text-muted-foreground">
{option.body}
</span>
</button>
))}
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
className="rounded-none"
onClick={() => onOpenChange(false)}
>
{t("common.cancel")}
</Button>
<Button
type="button"
className="rounded-none"
disabled={!token}
onClick={() => token && void finish(token, mode)}
>
{t("sync.wizard.link")}
</Button>
</div>
</div>
)}
{step === "finishing" && (
<div className="flex items-center gap-3 py-6 text-sm text-muted-foreground">
<Loader2 className="size-5 animate-spin text-accent-brand" />
{t("sync.wizard.linking")}
</div>
)}
{content}
</DialogContent>
</Dialog>
);
+47 -50
View File
@@ -1,3 +1,4 @@
import { InlineView } from "@/components/surface/surface-scope";
import {
useCallback,
useEffect,
@@ -17,14 +18,6 @@ import {
} from "lucide-react";
import { Button } from "@/components/button";
import { Switch } from "@/components/switch";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/dialog";
import { useSyncStatus } from "@/hooks/use-sync-status";
import { refreshSyncStatus, setSyncStatus } from "@/lib/sync-status";
import { notifySyncChanged } from "@/lib/linked-server";
@@ -565,45 +558,12 @@ export function SyncPanel() {
relogin={status.serverUrl ? { serverUrl: status.serverUrl } : undefined}
/>
<Dialog open={unlinkOpen} onOpenChange={setUnlinkOpen}>
<DialogContent className="bg-card border border-border rounded-none sm:max-w-md">
<DialogHeader>
<DialogTitle>{t("sync.unlinkTitle")}</DialogTitle>
<DialogDescription>{t("sync.unlinkDescription")}</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2">
{(
[
{
value: true,
title: t("sync.unlinkKeepTitle"),
body: t("sync.unlinkKeepBody"),
},
{
value: false,
title: t("sync.unlinkRemoveTitle"),
body: t("sync.unlinkRemoveBody"),
},
] as const
).map((option) => (
<button
key={String(option.value)}
type="button"
onClick={() => setKeepData(option.value)}
className={`text-left border p-3 flex flex-col gap-1 transition-colors ${
keepData === option.value
? "border-accent-brand bg-accent-brand/10"
: "border-border hover:bg-muted/40"
}`}
>
<span className="text-sm font-semibold">{option.title}</span>
<span className="text-xs text-muted-foreground">
{option.body}
</span>
</button>
))}
</div>
<DialogFooter>
<InlineView
open={unlinkOpen}
onOpenChange={setUnlinkOpen}
title={t("sync.unlinkTitle")}
footer={
<div className="ml-auto flex flex-wrap items-center justify-end gap-2">
<Button
variant="outline"
className="rounded-none"
@@ -626,9 +586,46 @@ export function SyncPanel() {
{busy === "unlink" && <Loader2 className="size-4 animate-spin" />}
{t("sync.unlink")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
}
>
<p className="text-xs text-muted-foreground">
{t("sync.unlinkDescription")}
</p>
<div className="flex flex-col gap-2">
{(
[
{
value: true,
title: t("sync.unlinkKeepTitle"),
body: t("sync.unlinkKeepBody"),
},
{
value: false,
title: t("sync.unlinkRemoveTitle"),
body: t("sync.unlinkRemoveBody"),
},
] as const
).map((option) => (
<button
key={String(option.value)}
type="button"
onClick={() => setKeepData(option.value)}
className={`text-left border p-3 flex flex-col gap-1 transition-colors ${
keepData === option.value
? "border-accent-brand bg-accent-brand/10"
: "border-border hover:bg-muted/40"
}`}
>
<span className="text-sm font-semibold">{option.title}</span>
<span className="text-xs text-muted-foreground">
{option.body}
</span>
</button>
))}
</div>
</InlineView>
{linkDialog}
</div>
);