2942ced3ec
The Codex tab strip wrapped onto a second row while Claude Code's stayed one scrollable row, so two panels that are meant to be twins read as different components. Both now render the same ConfigTabBar — the scroller lifted out of CcConfig, with the fade edges and chevrons that only appear when there is somewhere to scroll. The deferral note in CodexConfig that predicted this is gone with it. The bar also carries role="tablist"/"tab" and aria-selected now: Codex had them, Claude Code did not, and one of the two had to win. Cursor's placeholder tab goes away with the machinery that served only it — the optional Panel, the "soon" badge, the coming-soon card, and their four i18n keys. Element.scrollBy and friends move to test-setup: jsdom has none of them, and the tab bar now calls them from both pages.
58 lines
1.7 KiB
TypeScript
58 lines
1.7 KiB
TypeScript
/**
|
|
* @file PageTabs.tsx
|
|
* @description Shared top-of-page tab bar used by the pages that merged several
|
|
* former routes into one (Settings hub, Sessions/Events history). Rendering only -
|
|
* the owning page keeps the active tab in its own query param.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
import type { ComponentType } from "react";
|
|
|
|
export interface PageTab {
|
|
id: string;
|
|
label: string;
|
|
Icon: ComponentType<{ className?: string }>;
|
|
}
|
|
|
|
interface PageTabsProps {
|
|
tabs: PageTab[];
|
|
activeId: string;
|
|
onSelect: (id: string) => void;
|
|
ariaLabel: string;
|
|
}
|
|
|
|
/**
|
|
* Horizontal tab strip for a page that hosts several panels.
|
|
* @param props - Tabs to render, the active tab id, and the selection callback.
|
|
* @returns The tab bar element.
|
|
*/
|
|
export function PageTabs({ tabs, activeId, onSelect, ariaLabel }: PageTabsProps) {
|
|
return (
|
|
<div
|
|
role="tablist"
|
|
aria-label={ariaLabel}
|
|
className="flex items-center gap-1.5 overflow-x-auto no-scrollbar border-b border-border/60 pb-2"
|
|
>
|
|
{tabs.map(({ id, label, Icon }) => {
|
|
const isActive = id === activeId;
|
|
return (
|
|
<button
|
|
key={id}
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={isActive}
|
|
onClick={() => onSelect(id)}
|
|
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" />
|
|
{label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|