sync: update from private repo (ddadfd71)
CI / build-and-test (push) Waiting to run

This commit is contained in:
oss-sync
2026-07-08 23:35:00 +00:00
parent b1292e34b2
commit 77ee3bc426
187 changed files with 19918 additions and 10938 deletions
@@ -0,0 +1,96 @@
/**
* SpaceChatTabBar — 会話画面の単一タブバー(2ゾーン構造)。
*
* 左: タブ群(overflow-x-auto のスクロールゾーン)
* 右: actionsshrink-0 の固定ゾーン。継続 / 共有 / 削除ボタンが入る)
*
* タブが溢れて横スクロールになっても、actions は右端に固定されたまま
* スクロールで流れない。これが2ゾーンに分ける理由(旧: アクション専用行
* space-chat-actions を廃止して縦約40pxを回収した)。
*/
import { useRef } from 'react';
export interface SpaceChatTabDef {
id: string;
labelKey: string;
}
interface SpaceChatTabBarProps {
tabs: SpaceChatTabDef[];
activeTab: string;
onSelect: (id: string) => void;
ariaLabel: string;
renderLabel: (tab: SpaceChatTabDef) => string;
/** browser/ssh タブの出現アニメ用クラス(detailTabs.tabAppearClass を渡す) */
appearClass: (id: string) => string;
/** 右端固定ゾーンに置くアクション群。無ければゾーンごと描画しない。 */
actions?: React.ReactNode;
}
export function SpaceChatTabBar({
tabs,
activeTab,
onSelect,
ariaLabel,
renderLabel,
appearClass,
actions,
}: SpaceChatTabBarProps) {
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
// 矢印キーでタブ間移動(端で wrap)+ Home/End。選択とフォーカスを同時に動かす
// role=tablist の標準操作)。SpaceDetail から移設。
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
let next: number;
if (e.key === 'ArrowRight') next = (index + 1) % tabs.length;
else if (e.key === 'ArrowLeft') next = (index - 1 + tabs.length) % tabs.length;
else if (e.key === 'Home') next = 0;
else if (e.key === 'End') next = tabs.length - 1;
else return;
e.preventDefault();
onSelect(tabs[next].id);
tabRefs.current[next]?.focus();
};
return (
<div className="flex items-center border-b border-hairline pl-3 pr-2">
<div
role="tablist"
aria-label={ariaLabel}
data-testid="space-chat-tabs"
className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto"
>
{tabs.map((tb, i) => {
const active = tb.id === activeTab;
return (
<button
key={tb.id}
ref={(el) => { tabRefs.current[i] = el; }}
type="button"
role="tab"
id={`space-chat-tab-${tb.id}`}
data-testid={`space-chat-tab-${tb.id}`}
aria-selected={active}
aria-controls="space-chat-tabpanel"
tabIndex={active ? 0 : -1}
onClick={() => onSelect(tb.id)}
onKeyDown={(e) => handleKeyDown(e, i)}
className={`-mb-px shrink-0 whitespace-nowrap border-b-2 px-3 py-2 text-sm font-semibold transition-colors ${appearClass(tb.id)} ${
active
? 'border-[var(--brand-primary)] text-slate-800'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
{renderLabel(tb)}
</button>
);
})}
</div>
{actions && (
<div data-testid="space-chat-actions" className="ml-2 flex shrink-0 items-center gap-1.5">
{actions}
</div>
)}
</div>
);
}