Skip to content
File

Blob: src/client/components/tab-strip.tsx

typescript80 lines
1import { X, Plus } from "lucide-react";
2import { useWorkspaceStore } from "../workspace/store";
3import type { SocketState } from "../terminal/socket";
4 
5const STATE_DOT_CLASS: Record<SocketState, string> = {
6 disconnected: "bg-zinc-500",
7 connecting: "bg-amber-500",
8 connected: "bg-emerald-500",
9 reconnecting: "bg-amber-500",
10 ended: "bg-red-500",
11 error: "bg-red-500",
12};
13 
14export function TabStrip() {
15 const tabs = useWorkspaceStore((s) => s.tabs);
16 const activeTabId = useWorkspaceStore((s) => s.activeTabId);
17 const connectionStates = useWorkspaceStore((s) => s.connectionStates);
18 const switchTab = useWorkspaceStore((s) => s.switchTab);
19 const closeTab = useWorkspaceStore((s) => s.closeTab);
20 const newTab = useWorkspaceStore((s) => s.newTab);
21 
22 return (
23 <nav
24 className="flex items-center bg-zinc-900 border-b border-zinc-800/60 min-h-8 px-2 shrink-0"
25 aria-label="Terminal tabs"
26 >
27 <div className="flex overflow-x-auto gap-0.5 scrollbar-none" role="tablist">
28 {tabs.map((tab) => {
29 const isActive = tab.id === activeTabId;
30 const state: SocketState = connectionStates[tab.id] ?? "disconnected";
31
32 return (
33 <div
34 key={tab.id}
35 role="presentation"
36 className={`group flex items-center rounded-t max-w-[180px] min-w-[80px] transition-colors ${
37 isActive ? "bg-canvas" : "hover:bg-zinc-800/50"
38 }`}
39 >
40 <button
41 type="button"
42 role="tab"
43 aria-selected={isActive}
44 tabIndex={isActive ? 0 : -1}
45 className={`flex min-w-0 flex-1 items-center gap-1.5 px-2.5 py-1 rounded-t text-xs whitespace-nowrap transition-colors ${
46 isActive ? "text-zinc-300" : "text-zinc-500 hover:text-zinc-400"
47 }`}
48 onClick={() => switchTab(tab.id)}
49 >
50 <span className={`size-1.5 rounded-full shrink-0 ${STATE_DOT_CLASS[state]}`} />
51 <span className="truncate">{tab.title}</span>
52 </button>
53 <button
54 type="button"
55 className="shrink-0 size-4 mr-1 flex items-center justify-center rounded opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 text-zinc-500 hover:text-red-400 hover:bg-red-500/15 transition-opacity"
56 aria-label={`Close ${tab.title}`}
57 title="Detach tab (session stays alive)"
58 onClick={(e) => {
59 e.stopPropagation();
60 closeTab(tab.id);
61 }}
62 >
63 <X size={12} aria-hidden="true" />
64 </button>
65 </div>
66 );
67 })}
68 </div>
69 <button
70 type="button"
71 className="shrink-0 size-7 flex items-center justify-center rounded border border-zinc-800/60 text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800/50 ml-1 transition-colors"
72 onClick={newTab}
73 aria-label="New terminal tab"
74 >
75 <Plus size={14} aria-hidden="true" />
76 </button>
77 </nav>
78 );
79}