61 lines
3.2 KiB
TypeScript
61 lines
3.2 KiB
TypeScript
import { Activity, MessageSquare, PanelsTopLeft } from "lucide-react";
|
|
import { useEffect, useState, type KeyboardEvent, type ReactNode } from "react";
|
|
import { TopBar, type TopBarProps } from "../topbar/TopBar.js";
|
|
|
|
export type WorkspaceTab = "chat" | "skills" | "events";
|
|
|
|
export function AppShell({ topBar, skills, chat, timeline, activeTab, onTabChange: setActiveTab }: { topBar: TopBarProps; skills: ReactNode; chat: ReactNode; timeline: ReactNode; activeTab: WorkspaceTab; onTabChange: (tab: WorkspaceTab) => void }) {
|
|
const [mobile, setMobile] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (typeof window.matchMedia !== "function") return undefined;
|
|
const media = window.matchMedia("(max-width: 720px)");
|
|
const update = () => setMobile(media.matches);
|
|
update();
|
|
media.addEventListener("change", update);
|
|
return () => media.removeEventListener("change", update);
|
|
}, []);
|
|
|
|
const selectAdjacentTab = (current: WorkspaceTab, event: KeyboardEvent<HTMLButtonElement>) => {
|
|
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight" && event.key !== "Home" && event.key !== "End") return;
|
|
event.preventDefault();
|
|
const order: WorkspaceTab[] = ["chat", "skills", "events"];
|
|
const currentIndex = order.indexOf(current);
|
|
const targetIndex = event.key === "Home" ? 0 : event.key === "End" ? order.length - 1 : (currentIndex + (event.key === "ArrowRight" ? 1 : -1) + order.length) % order.length;
|
|
const target = order[targetIndex];
|
|
if (target === undefined) return;
|
|
setActiveTab(target);
|
|
document.getElementById(`${target}-tab`)?.focus();
|
|
};
|
|
|
|
return (
|
|
<div className="app-shell">
|
|
<TopBar {...topBar} />
|
|
<nav className="mobile-tabs" aria-label="工作区导航" role="tablist">
|
|
<TabButton id="chat" active={activeTab === "chat"} onSelect={setActiveTab} onKeyDown={selectAdjacentTab}><MessageSquare size={17} aria-hidden="true" />Chat</TabButton>
|
|
<TabButton id="skills" active={activeTab === "skills"} onSelect={setActiveTab} onKeyDown={selectAdjacentTab}><PanelsTopLeft size={17} aria-hidden="true" />Skills</TabButton>
|
|
<TabButton id="events" active={activeTab === "events"} onSelect={setActiveTab} onKeyDown={selectAdjacentTab}><Activity size={17} aria-hidden="true" />Events</TabButton>
|
|
</nav>
|
|
<div className="workspace-grid">
|
|
<div id="skills-panel" role={mobile ? "tabpanel" : undefined} hidden={mobile && activeTab !== "skills"}>{skills}</div>
|
|
<div id="chat-panel" role={mobile ? "tabpanel" : undefined} hidden={mobile && activeTab !== "chat"}>{chat}</div>
|
|
<div id="events-panel" role={mobile ? "tabpanel" : undefined} hidden={mobile && activeTab !== "events"}>{timeline}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function TabButton({ id, active, onSelect, onKeyDown, children }: {
|
|
id: WorkspaceTab;
|
|
active: boolean;
|
|
onSelect: (tab: WorkspaceTab) => void;
|
|
onKeyDown: (tab: WorkspaceTab, event: KeyboardEvent<HTMLButtonElement>) => void;
|
|
children: ReactNode;
|
|
}) {
|
|
return (
|
|
<button id={`${id}-tab`} type="button" role="tab" aria-selected={active} aria-controls={`${id}-panel`} tabIndex={active ? 0 : -1} onClick={() => onSelect(id)} onKeyDown={(event) => onKeyDown(id, event)}>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|