feat(settings): one page per section with a grouped nav and search
Settings moves from a tab bar to /settings/<section>: a left nav grouped General / Agents / Routing / Git / Projects (a select on narrow screens), a search box over every section (Ctrl/Cmd+F, Enter opens the first match), and row primitives. Each AI tool has its page under agents/<id>, the AI Tools modal becomes the Agents page, Pricing shows every vendor, Git → Worktrees sets which untracked worktrees the sidebar lists, and each project gets a page with its own worktree override and Forget all. ?tool= and #ai-tools links redirect to the path form.
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* @file SettingsRow — the settings page's row primitives: a titled card
|
||||
* (`SettingsSection`), a label/description | control row (`SettingsRow`), a
|
||||
* switch row (`SettingsSwitchRow`) and a segmented radio group
|
||||
* (`SettingsSegmented`). Rows divide themselves with a hairline, none after the
|
||||
* last, so a section is just its rows in order.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function SettingsSection({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
}: {
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="card p-5">
|
||||
<h3 className="text-sm font-semibold text-fg-primary">{title}</h3>
|
||||
{description && <p className="mt-0.5 text-xs text-fg-muted">{description}</p>}
|
||||
<div className="mt-2">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsRow({
|
||||
label,
|
||||
description,
|
||||
control,
|
||||
alignTop = false,
|
||||
}: {
|
||||
label: ReactNode;
|
||||
description?: ReactNode;
|
||||
control: ReactNode;
|
||||
alignTop?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`flex justify-between gap-4 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>}
|
||||
</div>
|
||||
<div className="shrink-0">{control}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsSwitchRow({
|
||||
label,
|
||||
description,
|
||||
checked,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}: {
|
||||
label: string;
|
||||
description?: ReactNode;
|
||||
checked: boolean;
|
||||
onChange: (v: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<SettingsRow
|
||||
label={label}
|
||||
description={description}
|
||||
control={
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
aria-label={label}
|
||||
disabled={disabled}
|
||||
onClick={() => onChange(!checked)}
|
||||
className={`relative inline-flex h-5 w-9 rounded-full border-2 border-transparent transition-colors disabled:opacity-50 ${
|
||||
checked ? "bg-accent" : "bg-surface-4"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow transition-transform ${
|
||||
checked ? "translate-x-4" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsSegmented<T extends string>({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
ariaLabel,
|
||||
}: {
|
||||
options: { value: T; label: ReactNode }[];
|
||||
value: T;
|
||||
onChange: (v: T) => void;
|
||||
ariaLabel?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label={ariaLabel}
|
||||
className="inline-flex rounded-lg border border-border bg-surface-2 p-0.5"
|
||||
>
|
||||
{options.map((o) => (
|
||||
<button
|
||||
key={o.value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={value === o.value}
|
||||
onClick={() => onChange(o.value)}
|
||||
className={`flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
value === o.value
|
||||
? "bg-accent/20 text-accent"
|
||||
: "text-fg-secondary hover:bg-surface-3 hover:text-fg-primary"
|
||||
}`}
|
||||
>
|
||||
{o.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user