/** * The tab strip of one pane: tabs capped at TAB_MAX_WIDTH (ellipsized), * overflow scrolls horizontally, a close button per tab, a four-way split * button cluster, and the + menu that opens new tabs (explorer / git / * terminal). Tabs are draggable; dropping onto another tab inserts before it, * dropping on the strip background appends to this pane. */ import { useState, type ReactNode } from 'react' import clsx from 'clsx' import { IconCloseFill14, IconPlusOutline16, Menu, } from '@deepseek-ai/dsh-client-ui-primitives' import type { SidebarTab } from './state.ts' import { tabTypeIcon } from './icons.tsx' import { t } from './locales.ts' import css from './sidebar.module.css' /** One + menu option. */ export interface NewTabOption { id: string label: string disabled?: boolean /** Leading icon (Menu row). */ icon?: ReactNode } /** Drag payload for tab moves (HTML5 DnD dataTransfer). */ export const TAB_DRAG_TYPE = 'application/x-dsh-tab' export interface TabDragPayload { tabId: string paneId: string } export function serializeDrag(payload: TabDragPayload): string { return JSON.stringify(payload) } export function parseDrag(raw: string): TabDragPayload | null { try { const parsed = JSON.parse(raw) as TabDragPayload if (typeof parsed.tabId === 'string' && typeof parsed.paneId === 'string') return parsed return null } catch { return null } } export function TabBar(props: { paneId: string tabs: SidebarTab[] active: string | null onActivate: (tabId: string) => void onClose: (tabId: string) => void onNewTab: (optionId: string) => void newTabOptions: NewTabOption[] /** Drop of a tab from any pane: (payload, insertBeforeTabId | null). */ onDropTab: (payload: TabDragPayload, before: string | null) => void }) { const { paneId, tabs, active, onActivate, onClose, onNewTab, newTabOptions, onDropTab, } = props const [menuOpen, setMenuOpen] = useState(false) const [dragOver, setDragOver] = useState(false) return (