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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user