ui(settings): Quit/Uninstall as real buttons with icons and a busy spinner; code in row descriptions; dimmed unavailable rows; smoother switch
This commit is contained in:
@@ -33,13 +33,16 @@ export function Switch({
|
||||
// On: the solid accent (white knob clears 3:1 in every theme). Off: a
|
||||
// border-light track with an inset fg-muted/80 ring (≥3:1 in every theme), so the control's edge
|
||||
// is visible on any surface (surface-4 alone was ~1:1 on a light card).
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-surface-0 disabled:cursor-not-allowed disabled:opacity-50 ${
|
||||
checked ? "bg-accent-solid" : "bg-border-light ring-1 ring-inset ring-fg-muted/80"
|
||||
// Hover darkens the track (enabled only), so it reads as clickable.
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-surface-0 disabled:cursor-not-allowed disabled:opacity-40 ${
|
||||
checked
|
||||
? "bg-accent-solid enabled:hover:bg-accent-solid-hover"
|
||||
: "bg-border-light ring-1 ring-inset ring-fg-muted/80 enabled:hover:ring-fg-secondary"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={`inline-block h-4 w-4 rounded-full bg-white shadow transition-transform ${
|
||||
className={`inline-block h-4 w-4 rounded-full bg-white shadow-sm ring-1 ring-black/10 transition-transform duration-200 ease-out ${
|
||||
checked ? "translate-x-[18px]" : "translate-x-0.5"
|
||||
}`}
|
||||
/>
|
||||
|
||||
@@ -9,17 +9,24 @@
|
||||
* boot/login while the switch is on. Between them, the status-area icon switch
|
||||
* (`GET/PUT /api/settings/tray`, on by default — server/lib/tray.js). Last,
|
||||
* **Uninstall AgentHub**: `POST /api/settings/uninstall` removes AgentHub from
|
||||
* the machine, database included (scripts/uninstall.js).
|
||||
* the machine, database included (scripts/uninstall.js). Quit is a plain
|
||||
* button and Uninstall a danger one, both the same width with an icon, and
|
||||
* each spins while its request runs.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Loader2, Power, Trash2 } from "lucide-react";
|
||||
import { api, type AutostartStatus, type TrayStatus } from "../../lib/api";
|
||||
import { toast } from "../../lib/toast";
|
||||
import { confirmDialog } from "../../lib/confirm";
|
||||
import { SettingsRow, SettingsSection, SettingsSwitchRow } from "./SettingsRow";
|
||||
|
||||
// The two action buttons share one width, so they line up under the switches.
|
||||
const ACTION = "min-w-[6.5rem] justify-center text-xs";
|
||||
const ICON = "h-3.5 w-3.5 shrink-0";
|
||||
|
||||
export function AutostartSettings() {
|
||||
const { t } = useTranslation("settings");
|
||||
const [status, setStatus] = useState<AutostartStatus | null>(null);
|
||||
@@ -126,9 +133,11 @@ export function AutostartSettings() {
|
||||
type="button"
|
||||
data-testid="quit-agenthub"
|
||||
disabled={quitting}
|
||||
aria-busy={quitting}
|
||||
onClick={() => void quit()}
|
||||
className="btn-ghost text-xs text-status-danger disabled:opacity-40"
|
||||
className={`btn-secondary ${ACTION}`}
|
||||
>
|
||||
{quitting ? <Loader2 className={ICON + " animate-spin"} /> : <Power className={ICON} />}
|
||||
{t("quit.button")}
|
||||
</button>
|
||||
}
|
||||
@@ -141,9 +150,15 @@ export function AutostartSettings() {
|
||||
type="button"
|
||||
data-testid="uninstall-agenthub"
|
||||
disabled={uninstalling}
|
||||
aria-busy={uninstalling}
|
||||
onClick={() => void uninstall()}
|
||||
className="btn-ghost text-xs text-status-danger disabled:opacity-40"
|
||||
className={`btn-danger ${ACTION}`}
|
||||
>
|
||||
{uninstalling ? (
|
||||
<Loader2 className={ICON + " animate-spin"} />
|
||||
) : (
|
||||
<Trash2 className={ICON} />
|
||||
)}
|
||||
{t("uninstall.button")}
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* switch row (`SettingsSwitchRow`) and a segmented radio group
|
||||
* (`SettingsSegmented`, arrow keys + one roving tab stop). Rows divide
|
||||
* themselves with a hairline, none after the last, so a section is just its
|
||||
* rows in order.
|
||||
* rows in order. A string description shows its `backticked` words as code,
|
||||
* and a `disabled` row dims its label, so an unavailable control reads as one.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -32,30 +33,55 @@ export function SettingsSection({
|
||||
);
|
||||
}
|
||||
|
||||
/** `text` with every `backticked` run drawn as inline code (a command to type). */
|
||||
export function withCode(text: string): ReactNode {
|
||||
const parts = text.split(/`([^`]+)`/);
|
||||
if (parts.length === 1) return text;
|
||||
return parts.map((p, i) =>
|
||||
i % 2 ? (
|
||||
<code
|
||||
key={i}
|
||||
className="whitespace-nowrap rounded border border-border bg-surface-2 px-1 py-px font-mono text-[11px] text-fg-secondary"
|
||||
>
|
||||
{p}
|
||||
</code>
|
||||
) : (
|
||||
p
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsRow({
|
||||
label,
|
||||
description,
|
||||
control,
|
||||
alignTop = false,
|
||||
disabled = false,
|
||||
id,
|
||||
}: {
|
||||
label: ReactNode;
|
||||
description?: ReactNode;
|
||||
control: ReactNode;
|
||||
alignTop?: boolean;
|
||||
/** The control cannot be used here: the label is dimmed with it. */
|
||||
disabled?: boolean;
|
||||
/** A deep-link anchor (`#id`) for one row inside a grouped section. */
|
||||
id?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
className={`${id ? "scroll-mt-24 " : ""}flex justify-between gap-4 border-b border-border/60 last:border-b-0 ${
|
||||
className={`${id ? "scroll-mt-24 " : ""}flex justify-between gap-6 border-b border-border/60 last:border-b-0 ${
|
||||
description ? "py-3" : "py-2"
|
||||
} ${alignTop ? "items-start" : "items-center"}`}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-fg-secondary">{label}</p>
|
||||
{description && <p className="mt-0.5 text-xs text-fg-muted">{description}</p>}
|
||||
<p className={`text-sm ${disabled ? "text-fg-muted" : "text-fg-secondary"}`}>{label}</p>
|
||||
{description && (
|
||||
<p className="mt-0.5 text-xs leading-relaxed text-fg-muted">
|
||||
{typeof description === "string" ? withCode(description) : description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="shrink-0">{control}</div>
|
||||
</div>
|
||||
@@ -82,6 +108,7 @@ export function SettingsSwitchRow({
|
||||
id={id}
|
||||
label={label}
|
||||
description={description}
|
||||
disabled={disabled}
|
||||
control={<Switch checked={checked} onChange={onChange} label={label} disabled={disabled} />}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user