feat(settings): host Claude config as a tab in Settings

Settings becomes a tabbed shell (SettingsHub) with one tab per AI tool:
Dashboard (existing settings), Claude Code (CcConfig), plus Codex and
Cursor placeholders. Active tab lives in the ?tool= query param, and the
old /cc-config route redirects to /settings?tool=claude-code. The
standalone sidebar entry is dropped.

Adding a tool is one entry in TOOL_TABS plus its i18n label.
This commit is contained in:
2026-08-26 09:01:00 +07:00
parent 64fc713440
commit 341197aac0
7 changed files with 127 additions and 15 deletions
+92
View File
@@ -0,0 +1,92 @@
/**
* @file SettingsHub.tsx
* @description Tabbed shell for /settings that hosts the dashboard's own settings plus per-AI-tool configuration panels (Claude Code today, Codex/Cursor reserved), with the active tab kept in the `?tool=` query param.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import type { ComponentType } from "react";
import { useSearchParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Boxes, Settings as SettingsIcon, SquareTerminal, MousePointerClick } from "lucide-react";
import { Settings } from "./Settings";
import { CcConfig } from "./CcConfig";
interface ToolTab {
id: string;
labelKey: string;
Icon: ComponentType<{ className?: string }>;
/** Omit for tools that are not integrated yet - the tab renders a placeholder. */
Panel?: ComponentType;
}
const DASHBOARD_TAB: ToolTab = {
id: "dashboard",
labelKey: "tools.dashboard",
Icon: SettingsIcon,
Panel: Settings,
};
/** Add a tool here to give it a tab; that is the whole extension point. */
const TOOL_TABS: ToolTab[] = [
DASHBOARD_TAB,
{ id: "claude-code", labelKey: "tools.claudeCode", Icon: Boxes, Panel: CcConfig },
{ id: "codex", labelKey: "tools.codex", Icon: SquareTerminal },
{ id: "cursor", labelKey: "tools.cursor", Icon: MousePointerClick },
];
/**
* Settings page shell with one tab per configuration surface.
* @returns The tab bar plus the active tab's panel.
*/
export function SettingsHub() {
const { t } = useTranslation("settings");
const [params, setParams] = useSearchParams();
const requested = params.get("tool");
const active = TOOL_TABS.find((tab) => tab.id === requested) ?? DASHBOARD_TAB;
return (
<div className="space-y-6">
<div
role="tablist"
aria-label={t("tools.label", "Tools")}
className="flex items-center gap-1.5 overflow-x-auto no-scrollbar border-b border-border/60 pb-2"
>
{TOOL_TABS.map(({ id, labelKey, Icon, Panel }) => {
const isActive = id === active.id;
return (
<button
key={id}
type="button"
role="tab"
aria-selected={isActive}
onClick={() =>
setParams(id === DASHBOARD_TAB.id ? {} : { tool: id }, { replace: true })
}
className={`inline-flex items-center gap-1.5 text-xs whitespace-nowrap px-3 py-1.5 rounded-lg border transition-colors flex-shrink-0 ${
isActive
? "bg-accent/15 border-accent/30 text-accent"
: "border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
}`}
>
<Icon className="w-3.5 h-3.5" />
{t(labelKey)}
{!Panel && (
<span className="text-[10px] text-fg-muted">{t("tools.soonBadge", "soon")}</span>
)}
</button>
);
})}
</div>
{active.Panel ? (
<active.Panel />
) : (
<div className="card p-8 text-center space-y-1">
<p className="text-sm font-medium text-fg-primary">
{t("tools.comingSoon", { tool: t(active.labelKey) })}
</p>
<p className="text-xs text-fg-muted">{t("tools.comingSoonDesc")}</p>
</div>
)}
</div>
);
}