feat: pro: unify split panes under a single tab bar
This commit is contained in:
+123
-82
@@ -49,47 +49,30 @@ interface PaneProps {
|
|||||||
tabs: ProTab[];
|
tabs: ProTab[];
|
||||||
activeTabId: string | null;
|
activeTabId: string | null;
|
||||||
loadedTabIds: string[];
|
loadedTabIds: string[];
|
||||||
allTabs: ProTab[];
|
|
||||||
onActivate: (id: string) => void;
|
|
||||||
onClose: (id: string) => void;
|
|
||||||
onDragStateChange: (dragging: boolean) => void;
|
|
||||||
onPaneFocus: (paneId: ProPaneId) => void;
|
onPaneFocus: (paneId: ProPaneId) => void;
|
||||||
isFocused: boolean;
|
isFocused: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Pane({
|
function Pane({ paneId, tabs, activeTabId, loadedTabIds, onPaneFocus, isFocused }: PaneProps) {
|
||||||
paneId, tabs, activeTabId, loadedTabIds, allTabs,
|
|
||||||
onActivate, onClose, onDragStateChange, onPaneFocus, isFocused,
|
|
||||||
}: PaneProps) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-1 flex-col overflow-hidden min-w-0 min-h-0"
|
className="relative flex flex-1 flex-col overflow-hidden min-w-0 min-h-0"
|
||||||
onMouseDownCapture={() => { if (!isFocused) onPaneFocus(paneId); }}
|
onMouseDownCapture={() => { if (!isFocused) onPaneFocus(paneId); }}
|
||||||
>
|
>
|
||||||
<ProTabBar
|
{tabs
|
||||||
tabs={tabs}
|
.filter((tab) => loadedTabIds.includes(tab.id))
|
||||||
activeTabId={activeTabId}
|
.map((tab) => {
|
||||||
paneId={paneId}
|
const isActive = tab.id === activeTabId;
|
||||||
onActivate={onActivate}
|
return (
|
||||||
onClose={onClose}
|
<div
|
||||||
onDragStateChange={onDragStateChange}
|
key={tab.id}
|
||||||
/>
|
className={cn("absolute inset-0 overflow-hidden", !isActive && "hidden")}
|
||||||
<div className="relative flex-1 min-h-0">
|
aria-hidden={!isActive}
|
||||||
{allTabs
|
>
|
||||||
.filter((tab) => tab.paneId === paneId && loadedTabIds.includes(tab.id))
|
{renderTabBody(tab)}
|
||||||
.map((tab) => {
|
</div>
|
||||||
const isActive = tab.id === activeTabId;
|
);
|
||||||
return (
|
})}
|
||||||
<div
|
|
||||||
key={tab.id}
|
|
||||||
className={cn("absolute inset-0 overflow-hidden", !isActive && "hidden")}
|
|
||||||
aria-hidden={!isActive}
|
|
||||||
>
|
|
||||||
{renderTabBody(tab)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -134,6 +117,8 @@ export default function ProHome() {
|
|||||||
|
|
||||||
const [isTabDragging, setIsTabDragging] = useState(false);
|
const [isTabDragging, setIsTabDragging] = useState(false);
|
||||||
const [splitDropTarget, setSplitDropTarget] = useState<DropTarget>(null);
|
const [splitDropTarget, setSplitDropTarget] = useState<DropTarget>(null);
|
||||||
|
/** Whether the split pane visually renders before (true) or after (false) main. */
|
||||||
|
const [splitLeading, setSplitLeading] = useState(false);
|
||||||
|
|
||||||
// Auth bootstrap (mirrors standard page)
|
// Auth bootstrap (mirrors standard page)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -153,7 +138,6 @@ export default function ProHome() {
|
|||||||
}
|
}
|
||||||
}, [initialCheckDone, isAuthenticated, authLoading]);
|
}, [initialCheckDone, isAuthenticated, authLoading]);
|
||||||
|
|
||||||
// Pro is desktop-only — fall back to standard on mobile/tablet
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialCheckDone && (isMobile || isTablet) && typeof window !== "undefined") {
|
if (initialCheckDone && (isMobile || isTablet) && typeof window !== "undefined") {
|
||||||
window.location.replace("/");
|
window.location.replace("/");
|
||||||
@@ -180,7 +164,9 @@ export default function ProHome() {
|
|||||||
|| focusedActiveTab.kind === 'settings'
|
|| focusedActiveTab.kind === 'settings'
|
||||||
) ? focusedActiveTab.kind : null;
|
) ? focusedActiveTab.kind : null;
|
||||||
|
|
||||||
// ---- Split drop zones ----
|
const isSplit = splitOrientation !== null && splitTabs.length > 0;
|
||||||
|
|
||||||
|
// ---- Body-level drop targets ----
|
||||||
|
|
||||||
const isProTabDrag = (e: DragEvent) => e.dataTransfer.types.includes(PRO_TAB_DRAG_MIME);
|
const isProTabDrag = (e: DragEvent) => e.dataTransfer.types.includes(PRO_TAB_DRAG_MIME);
|
||||||
|
|
||||||
@@ -190,8 +176,15 @@ export default function ProHome() {
|
|||||||
const y = e.clientY - rect.top;
|
const y = e.clientY - rect.top;
|
||||||
const xFrac = x / rect.width;
|
const xFrac = x / rect.width;
|
||||||
const yFrac = y / rect.height;
|
const yFrac = y / rect.height;
|
||||||
// Edges: outer 22% of the body becomes a split drop target. The cursor's
|
|
||||||
// dominant axis decides which side activates.
|
if (isSplit) {
|
||||||
|
// When split: each pane gets half the body as its drop zone.
|
||||||
|
if (splitOrientation === 'vertical') {
|
||||||
|
return xFrac < 0.5 ? 'left' : 'right';
|
||||||
|
}
|
||||||
|
return yFrac < 0.5 ? 'top' : 'bottom';
|
||||||
|
}
|
||||||
|
// No split: only the outer 22% of each edge creates a split.
|
||||||
const fromLeft = xFrac;
|
const fromLeft = xFrac;
|
||||||
const fromRight = 1 - xFrac;
|
const fromRight = 1 - xFrac;
|
||||||
const fromTop = yFrac;
|
const fromTop = yFrac;
|
||||||
@@ -204,11 +197,25 @@ export default function ProHome() {
|
|||||||
return 'top';
|
return 'top';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const targetPaneFromDrop = (target: DropTarget): ProPaneId | null => {
|
||||||
|
if (!target || !isSplit) return null;
|
||||||
|
if (splitOrientation === 'vertical') {
|
||||||
|
const leftIsSplit = splitLeading;
|
||||||
|
if (target === 'left') return leftIsSplit ? 'split' : 'main';
|
||||||
|
if (target === 'right') return leftIsSplit ? 'main' : 'split';
|
||||||
|
}
|
||||||
|
if (splitOrientation === 'horizontal') {
|
||||||
|
const topIsSplit = splitLeading;
|
||||||
|
if (target === 'top') return topIsSplit ? 'split' : 'main';
|
||||||
|
if (target === 'bottom') return topIsSplit ? 'main' : 'split';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const handleBodyDragOver = (e: DragEvent<HTMLDivElement>) => {
|
const handleBodyDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||||
if (!isProTabDrag(e)) return;
|
if (!isProTabDrag(e)) return;
|
||||||
if (splitOrientation !== null) return; // already split — body drops disabled
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.dataTransfer.dropEffect = 'move';
|
e.dataTransfer.dropEffect = "move";
|
||||||
const next = computeDropTarget(e);
|
const next = computeDropTarget(e);
|
||||||
if (next !== splitDropTarget) setSplitDropTarget(next);
|
if (next !== splitDropTarget) setSplitDropTarget(next);
|
||||||
};
|
};
|
||||||
@@ -221,7 +228,6 @@ export default function ProHome() {
|
|||||||
|
|
||||||
const handleBodyDrop = (e: DragEvent<HTMLDivElement>) => {
|
const handleBodyDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||||
if (!isProTabDrag(e)) return;
|
if (!isProTabDrag(e)) return;
|
||||||
if (splitOrientation !== null) return;
|
|
||||||
const target = computeDropTarget(e);
|
const target = computeDropTarget(e);
|
||||||
setSplitDropTarget(null);
|
setSplitDropTarget(null);
|
||||||
setIsTabDragging(false);
|
setIsTabDragging(false);
|
||||||
@@ -229,19 +235,19 @@ export default function ProHome() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const draggedId = e.dataTransfer.getData(PRO_TAB_DRAG_MIME);
|
const draggedId = e.dataTransfer.getData(PRO_TAB_DRAG_MIME);
|
||||||
if (!draggedId) return;
|
if (!draggedId) return;
|
||||||
// The dragged tab must currently be in 'main' (the only pane right now).
|
|
||||||
// Moving it to 'split' creates the split.
|
if (isSplit) {
|
||||||
|
// Move tab to whichever pane occupies the dropped side.
|
||||||
|
const destPane = targetPaneFromDrop(target);
|
||||||
|
if (destPane) moveTabToPane(draggedId, destPane);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Create a new split.
|
||||||
const orientation = (target === 'left' || target === 'right') ? 'vertical' : 'horizontal';
|
const orientation = (target === 'left' || target === 'right') ? 'vertical' : 'horizontal';
|
||||||
moveTabToPane(draggedId, 'split', orientation);
|
moveTabToPane(draggedId, 'split', orientation);
|
||||||
// 'left'/'top' targets put the split pane on the leading edge — flipped
|
|
||||||
// visually by swapping the rendered order below. We track it via the
|
|
||||||
// splitLeading flag derived from the last drop.
|
|
||||||
setSplitLeading(target === 'left' || target === 'top');
|
setSplitLeading(target === 'left' || target === 'top');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Whether the split pane renders before (true) or after (false) the main pane.
|
|
||||||
const [splitLeading, setSplitLeading] = useState(false);
|
|
||||||
|
|
||||||
// Loading state (matches standard page exactly)
|
// Loading state (matches standard page exactly)
|
||||||
if (!initialCheckDone || authLoading || !isAuthenticated || !client) {
|
if (!initialCheckDone || authLoading || !isAuthenticated || !client) {
|
||||||
return (
|
return (
|
||||||
@@ -256,18 +262,12 @@ export default function ProHome() {
|
|||||||
|
|
||||||
if (!isDesktop) return null;
|
if (!isDesktop) return null;
|
||||||
|
|
||||||
const isSplit = splitOrientation !== null && splitTabs.length > 0;
|
|
||||||
|
|
||||||
const mainPane = (
|
const mainPane = (
|
||||||
<Pane
|
<Pane
|
||||||
paneId="main"
|
paneId="main"
|
||||||
tabs={mainTabs}
|
tabs={mainTabs}
|
||||||
activeTabId={activeMainTabId}
|
activeTabId={activeMainTabId}
|
||||||
loadedTabIds={loadedTabIds}
|
loadedTabIds={loadedTabIds}
|
||||||
allTabs={tabs}
|
|
||||||
onActivate={setActiveTab}
|
|
||||||
onClose={closeTab}
|
|
||||||
onDragStateChange={setIsTabDragging}
|
|
||||||
onPaneFocus={setFocusedPane}
|
onPaneFocus={setFocusedPane}
|
||||||
isFocused={focusedPaneId === 'main'}
|
isFocused={focusedPaneId === 'main'}
|
||||||
/>
|
/>
|
||||||
@@ -279,10 +279,6 @@ export default function ProHome() {
|
|||||||
tabs={splitTabs}
|
tabs={splitTabs}
|
||||||
activeTabId={activeSplitTabId}
|
activeTabId={activeSplitTabId}
|
||||||
loadedTabIds={loadedTabIds}
|
loadedTabIds={loadedTabIds}
|
||||||
allTabs={tabs}
|
|
||||||
onActivate={setActiveTab}
|
|
||||||
onClose={closeTab}
|
|
||||||
onDragStateChange={setIsTabDragging}
|
|
||||||
onPaneFocus={setFocusedPane}
|
onPaneFocus={setFocusedPane}
|
||||||
isFocused={focusedPaneId === 'split'}
|
isFocused={focusedPaneId === 'split'}
|
||||||
/>
|
/>
|
||||||
@@ -303,6 +299,32 @@ export default function ProHome() {
|
|||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
// Drop-zone overlays: 4 edges when not split, 2 panes when split.
|
||||||
|
const dropZones = isTabDragging ? (
|
||||||
|
<>
|
||||||
|
{!isSplit && (
|
||||||
|
<>
|
||||||
|
<DropZone active={splitDropTarget === 'left'} side="left" />
|
||||||
|
<DropZone active={splitDropTarget === 'right'} side="right" />
|
||||||
|
<DropZone active={splitDropTarget === 'top'} side="top" />
|
||||||
|
<DropZone active={splitDropTarget === 'bottom'} side="bottom" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isSplit && splitOrientation === 'vertical' && (
|
||||||
|
<>
|
||||||
|
<DropZoneHalf active={splitDropTarget === 'left'} axis="x" side="leading" />
|
||||||
|
<DropZoneHalf active={splitDropTarget === 'right'} axis="x" side="trailing" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isSplit && splitOrientation === 'horizontal' && (
|
||||||
|
<>
|
||||||
|
<DropZoneHalf active={splitDropTarget === 'top'} axis="y" side="leading" />
|
||||||
|
<DropZoneHalf active={splitDropTarget === 'bottom'} axis="y" side="trailing" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EmbeddedContext.Provider value={true}>
|
<EmbeddedContext.Provider value={true}>
|
||||||
<div className="flex flex-col h-dvh bg-background overflow-hidden pt-[env(safe-area-inset-top)]">
|
<div className="flex flex-col h-dvh bg-background overflow-hidden pt-[env(safe-area-inset-top)]">
|
||||||
@@ -337,31 +359,35 @@ export default function ProHome() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!inlineApp && (
|
{!inlineApp && (
|
||||||
<div
|
<div className="flex flex-1 flex-col overflow-hidden min-w-0">
|
||||||
className={cn(
|
{/* Single, unified tab bar above both panes. */}
|
||||||
"relative flex flex-1 overflow-hidden min-w-0",
|
<ProTabBar
|
||||||
isSplit && splitOrientation === 'horizontal' ? "flex-col" : "flex-row",
|
tabs={tabs}
|
||||||
)}
|
activeMainTabId={activeMainTabId}
|
||||||
onDragOver={handleBodyDragOver}
|
activeSplitTabId={activeSplitTabId}
|
||||||
onDragLeave={handleBodyDragLeave}
|
onActivate={setActiveTab}
|
||||||
onDrop={handleBodyDrop}
|
onClose={closeTab}
|
||||||
>
|
onDragStateChange={setIsTabDragging}
|
||||||
{isSplit
|
/>
|
||||||
? (splitLeading
|
|
||||||
? <>{splitPane}{splitDivider}{mainPane}</>
|
|
||||||
: <>{mainPane}{splitDivider}{splitPane}</>)
|
|
||||||
: mainPane}
|
|
||||||
|
|
||||||
{/* Split-creation drop zones — shown only while a tab is being
|
{/* Panes container — accepts body drops for split/move. */}
|
||||||
dragged and the body isn't already split. */}
|
<div
|
||||||
{isTabDragging && !isSplit && (
|
className={cn(
|
||||||
<>
|
"relative flex flex-1 overflow-hidden min-w-0",
|
||||||
<DropZone active={splitDropTarget === 'left'} side="left" />
|
isSplit && splitOrientation === 'horizontal' ? "flex-col" : "flex-row",
|
||||||
<DropZone active={splitDropTarget === 'right'} side="right" />
|
)}
|
||||||
<DropZone active={splitDropTarget === 'top'} side="top" />
|
onDragOver={handleBodyDragOver}
|
||||||
<DropZone active={splitDropTarget === 'bottom'} side="bottom" />
|
onDragLeave={handleBodyDragLeave}
|
||||||
</>
|
onDrop={handleBodyDrop}
|
||||||
)}
|
>
|
||||||
|
{isSplit
|
||||||
|
? (splitLeading
|
||||||
|
? <>{splitPane}{splitDivider}{mainPane}</>
|
||||||
|
: <>{mainPane}{splitDivider}{splitPane}</>)
|
||||||
|
: mainPane}
|
||||||
|
|
||||||
|
{dropZones}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -395,3 +421,18 @@ function DropZone({ active, side }: { active: boolean; side: 'left' | 'right' |
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DropZoneHalf({ active, axis, side }: { active: boolean; axis: 'x' | 'y'; side: 'leading' | 'trailing' }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none absolute z-10 transition-colors duration-100",
|
||||||
|
axis === 'x'
|
||||||
|
? cn("top-0 bottom-0 w-1/2", side === 'leading' ? "left-0" : "right-0")
|
||||||
|
: cn("left-0 right-0 h-1/2", side === 'leading' ? "top-0" : "bottom-0"),
|
||||||
|
active ? "bg-primary/10 ring-2 ring-primary/40 ring-inset" : "bg-transparent",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ import { useRef, useState, type DragEvent } from "react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Mail, Calendar, BookUser, HardDrive, Settings, PenSquare, MailOpen, X, type LucideIcon } from "lucide-react";
|
import { Mail, Calendar, BookUser, HardDrive, Settings, PenSquare, MailOpen, X, type LucideIcon } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useProTabStore, type ProTab, type ProTabKind, type ProPaneId } from "@/stores/pro-tab-store";
|
import { useProTabStore, type ProTab, type ProTabKind } from "@/stores/pro-tab-store";
|
||||||
|
|
||||||
/** Custom MIME type used to carry the dragged Pro tab id between handlers. */
|
/** Custom MIME type used to carry the dragged Pro tab id between handlers. */
|
||||||
export const PRO_TAB_DRAG_MIME = "application/x-pro-tab-id";
|
export const PRO_TAB_DRAG_MIME = "application/x-pro-tab-id";
|
||||||
|
|
||||||
interface ProTabBarProps {
|
interface ProTabBarProps {
|
||||||
/** Tabs belonging to this bar's pane, already filtered + ordered. */
|
/** All tabs (both panes). Order in the array is the order in the bar. */
|
||||||
tabs: ProTab[];
|
tabs: ProTab[];
|
||||||
activeTabId: string | null;
|
activeMainTabId: string | null;
|
||||||
paneId: ProPaneId;
|
activeSplitTabId: string | null;
|
||||||
onActivate: (id: string) => void;
|
onActivate: (id: string) => void;
|
||||||
onClose: (id: string) => void;
|
onClose: (id: string) => void;
|
||||||
onDragStateChange?: (dragging: boolean) => void;
|
onDragStateChange?: (dragging: boolean) => void;
|
||||||
@@ -34,8 +34,8 @@ type DropIndicator = { targetId: string; edge: "before" | "after" } | null;
|
|||||||
|
|
||||||
export function ProTabBar({
|
export function ProTabBar({
|
||||||
tabs,
|
tabs,
|
||||||
activeTabId,
|
activeMainTabId,
|
||||||
paneId,
|
activeSplitTabId,
|
||||||
onActivate,
|
onActivate,
|
||||||
onClose,
|
onClose,
|
||||||
onDragStateChange,
|
onDragStateChange,
|
||||||
@@ -43,8 +43,7 @@ export function ProTabBar({
|
|||||||
}: ProTabBarProps) {
|
}: ProTabBarProps) {
|
||||||
const tSidebar = useTranslations("sidebar");
|
const tSidebar = useTranslations("sidebar");
|
||||||
const reorderTab = useProTabStore((s) => s.reorderTab);
|
const reorderTab = useProTabStore((s) => s.reorderTab);
|
||||||
const moveTabToPane = useProTabStore((s) => s.moveTabToPane);
|
const focusedPaneId = useProTabStore((s) => s.focusedPaneId);
|
||||||
const splitOrientation = useProTabStore((s) => s.splitOrientation);
|
|
||||||
|
|
||||||
const [dropIndicator, setDropIndicator] = useState<DropIndicator>(null);
|
const [dropIndicator, setDropIndicator] = useState<DropIndicator>(null);
|
||||||
const dragLeaveTimer = useRef<number | null>(null);
|
const dragLeaveTimer = useRef<number | null>(null);
|
||||||
@@ -82,9 +81,6 @@ export function ProTabBar({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleStripDragLeave = (e: DragEvent<HTMLDivElement>) => {
|
const handleStripDragLeave = (e: DragEvent<HTMLDivElement>) => {
|
||||||
// Clear the indicator only when the cursor truly leaves the strip; the
|
|
||||||
// intermediate dragleave events that fire as the cursor crosses tab
|
|
||||||
// boundaries would otherwise flicker the indicator off.
|
|
||||||
const next = e.relatedTarget as Node | null;
|
const next = e.relatedTarget as Node | null;
|
||||||
if (next && e.currentTarget.contains(next)) return;
|
if (next && e.currentTarget.contains(next)) return;
|
||||||
if (dragLeaveTimer.current !== null) window.clearTimeout(dragLeaveTimer.current);
|
if (dragLeaveTimer.current !== null) window.clearTimeout(dragLeaveTimer.current);
|
||||||
@@ -107,8 +103,6 @@ export function ProTabBar({
|
|||||||
handleDragEnd();
|
handleDragEnd();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dropping on the empty trailing area moves the tab to the end of this bar's
|
|
||||||
// pane. If the dragged tab was in the other pane, that also moves it here.
|
|
||||||
const handleStripEndDrop = (e: DragEvent<HTMLDivElement>) => {
|
const handleStripEndDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||||
if (!isProTabDrag(e)) return;
|
if (!isProTabDrag(e)) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -118,10 +112,7 @@ export function ProTabBar({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const last = tabs[tabs.length - 1];
|
const last = tabs[tabs.length - 1];
|
||||||
if (!last) {
|
if (last && last.id !== draggedId) {
|
||||||
// Bar is empty — move into this pane.
|
|
||||||
moveTabToPane(draggedId, paneId, splitOrientation ?? "vertical");
|
|
||||||
} else if (last.id !== draggedId) {
|
|
||||||
reorderTab(draggedId, last.id, "after");
|
reorderTab(draggedId, last.id, "after");
|
||||||
}
|
}
|
||||||
handleDragEnd();
|
handleDragEnd();
|
||||||
@@ -149,7 +140,12 @@ export function ProTabBar({
|
|||||||
>
|
>
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const Icon = TAB_ICONS[tab.kind];
|
const Icon = TAB_ICONS[tab.kind];
|
||||||
const isActive = tab.id === activeTabId;
|
const isActiveMain = tab.id === activeMainTabId && tab.paneId === 'main';
|
||||||
|
const isActiveSplit = tab.id === activeSplitTabId && tab.paneId === 'split';
|
||||||
|
const isActive = isActiveMain || isActiveSplit;
|
||||||
|
const isFocusedActive =
|
||||||
|
(isActiveMain && focusedPaneId === 'main')
|
||||||
|
|| (isActiveSplit && focusedPaneId === 'split');
|
||||||
const label = tab.title ?? tSidebar(tab.labelKey);
|
const label = tab.title ?? tSidebar(tab.labelKey);
|
||||||
const showBefore = dropIndicator?.targetId === tab.id && dropIndicator.edge === "before";
|
const showBefore = dropIndicator?.targetId === tab.id && dropIndicator.edge === "before";
|
||||||
const showAfter = dropIndicator?.targetId === tab.id && dropIndicator.edge === "after";
|
const showAfter = dropIndicator?.targetId === tab.id && dropIndicator.edge === "after";
|
||||||
@@ -159,6 +155,7 @@ export function ProTabBar({
|
|||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={isActive}
|
aria-selected={isActive}
|
||||||
data-tab-id={tab.id}
|
data-tab-id={tab.id}
|
||||||
|
data-pane-id={tab.paneId}
|
||||||
draggable
|
draggable
|
||||||
onClick={() => onActivate(tab.id)}
|
onClick={() => onActivate(tab.id)}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
@@ -176,8 +173,9 @@ export function ProTabBar({
|
|||||||
"min-w-0 flex-1 basis-0 max-w-[200px] [min-width:80px]",
|
"min-w-0 flex-1 basis-0 max-w-[200px] [min-width:80px]",
|
||||||
"border-r border-border first:border-l",
|
"border-r border-border first:border-l",
|
||||||
isActive
|
isActive
|
||||||
? "bg-background text-foreground font-medium"
|
? "bg-background text-foreground"
|
||||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||||
|
isFocusedActive && "font-medium",
|
||||||
)}
|
)}
|
||||||
style={
|
style={
|
||||||
isActive
|
isActive
|
||||||
@@ -185,7 +183,7 @@ export function ProTabBar({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon className={cn("w-4 h-4 flex-shrink-0", isActive && "text-primary")} />
|
<Icon className={cn("w-4 h-4 flex-shrink-0", isFocusedActive && "text-primary")} />
|
||||||
<span className="truncate flex-1 min-w-0" title={label}>{label}</span>
|
<span className="truncate flex-1 min-w-0" title={label}>{label}</span>
|
||||||
|
|
||||||
{tab.closeable && (
|
{tab.closeable && (
|
||||||
@@ -229,8 +227,7 @@ export function ProTabBar({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Trailing area soaks up drops past the last tab and lets cross-pane
|
{/* Trailing area soaks up drops past the last tab. */}
|
||||||
moves drop onto an empty bar. */}
|
|
||||||
<div
|
<div
|
||||||
className="flex-1 min-w-[8px]"
|
className="flex-1 min-w-[8px]"
|
||||||
onDragOver={handleStripEndDragOver}
|
onDragOver={handleStripEndDragOver}
|
||||||
|
|||||||
Reference in New Issue
Block a user