/** * 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 { useEffect, 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 { 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 } } /** Global tab-drag flag: PDF iframes become non-interactive synchronously. */ function setTabDragging(active: boolean): void { if (active) document.body.setAttribute('data-dsh-tab-dragging', '') else document.body.removeAttribute('data-dsh-tab-dragging') } 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 /** Icon resolver for tab labels (reads from the tab descriptor registry). */ getTabIcon?: (tab: SidebarTab) => ReactNode }) { const { paneId, tabs, active, onActivate, onClose, onNewTab, newTabOptions, onDropTab, getTabIcon, } = props const [menuOpen, setMenuOpen] = useState(false) const [dragOver, setDragOver] = useState(false) useEffect(() => { const clear = (): void => { setTabDragging(false); setDragOver(false) } window.addEventListener('dragend', clear, true) window.addEventListener('drop', clear, true) window.addEventListener('blur', clear) return () => { window.removeEventListener('dragend', clear, true) window.removeEventListener('drop', clear, true) window.removeEventListener('blur', clear) } }, []) return (