refactor: improve tab layout sizing and always show close buttons
This commit is contained in:
@@ -139,15 +139,11 @@ export function DraggableList<T>({
|
|||||||
nestable: _nestable = false,
|
nestable: _nestable = false,
|
||||||
}: DraggableListProps<T>) {
|
}: DraggableListProps<T>) {
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
const [activeId, setActiveId] = useState<string | null>(null);
|
||||||
const [items, setItems] = useState(data);
|
const [dragItems, setDragItems] = useState<T[] | null>(null);
|
||||||
|
const items = dragItems ?? data;
|
||||||
const scrollViewRef = useRef<ScrollView>(null);
|
const scrollViewRef = useRef<ScrollView>(null);
|
||||||
const scrollbarMetrics = useWebDesktopScrollbarMetrics();
|
const scrollbarMetrics = useWebDesktopScrollbarMetrics();
|
||||||
|
|
||||||
// Sync items with data prop
|
|
||||||
if (data !== items && !activeId) {
|
|
||||||
setItems(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
activationConstraint: {
|
activationConstraint: {
|
||||||
@@ -159,16 +155,21 @@ export function DraggableList<T>({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
const handleDragStart = useCallback(
|
||||||
setActiveId(String(event.active.id));
|
(event: DragStartEvent) => {
|
||||||
onDragBegin?.();
|
setDragItems(data);
|
||||||
}, [onDragBegin]);
|
setActiveId(String(event.active.id));
|
||||||
|
onDragBegin?.();
|
||||||
|
},
|
||||||
|
[data, onDragBegin]
|
||||||
|
);
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
const handleDragEnd = useCallback(
|
||||||
(event: DragEndEvent) => {
|
(event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
|
|
||||||
setActiveId(null);
|
setActiveId(null);
|
||||||
|
setDragItems(null);
|
||||||
|
|
||||||
if (over && active.id !== over.id) {
|
if (over && active.id !== over.id) {
|
||||||
const oldIndex = items.findIndex(
|
const oldIndex = items.findIndex(
|
||||||
@@ -178,9 +179,10 @@ export function DraggableList<T>({
|
|||||||
(item, i) => keyExtractor(item, i) === over.id
|
(item, i) => keyExtractor(item, i) === over.id
|
||||||
);
|
);
|
||||||
|
|
||||||
const newItems = arrayMove(items, oldIndex, newIndex);
|
if (oldIndex >= 0 && newIndex >= 0 && oldIndex !== newIndex) {
|
||||||
setItems(newItems);
|
const newItems = arrayMove(items, oldIndex, newIndex);
|
||||||
onDragEnd(newItems);
|
onDragEnd(newItems);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[items, keyExtractor, onDragEnd]
|
[items, keyExtractor, onDragEnd]
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
type ReactElement,
|
type ReactElement,
|
||||||
type MutableRefObject,
|
type MutableRefObject,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { router, usePathname, useSegments } from 'expo-router'
|
import { router, usePathname } from 'expo-router'
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
|
||||||
import { type GestureType } from 'react-native-gesture-handler'
|
import { type GestureType } from 'react-native-gesture-handler'
|
||||||
import { ChevronDown, ChevronRight } from 'lucide-react-native'
|
import { ChevronDown, ChevronRight } from 'lucide-react-native'
|
||||||
@@ -736,7 +736,6 @@ function ProjectBlock({
|
|||||||
iconDataUri,
|
iconDataUri,
|
||||||
serverId,
|
serverId,
|
||||||
activeWorkspaceSelection,
|
activeWorkspaceSelection,
|
||||||
shouldReplaceWorkspaceNavigation,
|
|
||||||
showShortcutBadges,
|
showShortcutBadges,
|
||||||
shortcutIndexByWorkspaceKey,
|
shortcutIndexByWorkspaceKey,
|
||||||
parentGestureRef,
|
parentGestureRef,
|
||||||
@@ -756,7 +755,6 @@ function ProjectBlock({
|
|||||||
iconDataUri: string | null
|
iconDataUri: string | null
|
||||||
serverId: string | null
|
serverId: string | null
|
||||||
activeWorkspaceSelection: { serverId: string; workspaceId: string } | null
|
activeWorkspaceSelection: { serverId: string; workspaceId: string } | null
|
||||||
shouldReplaceWorkspaceNavigation: boolean
|
|
||||||
showShortcutBadges: boolean
|
showShortcutBadges: boolean
|
||||||
shortcutIndexByWorkspaceKey: Map<string, number>
|
shortcutIndexByWorkspaceKey: Map<string, number>
|
||||||
parentGestureRef?: MutableRefObject<GestureType | undefined>
|
parentGestureRef?: MutableRefObject<GestureType | undefined>
|
||||||
@@ -778,7 +776,6 @@ function ProjectBlock({
|
|||||||
dragHandleProps: workspaceDragHandleProps,
|
dragHandleProps: workspaceDragHandleProps,
|
||||||
}: DraggableRenderItemInfo<SidebarWorkspaceEntry>) => {
|
}: DraggableRenderItemInfo<SidebarWorkspaceEntry>) => {
|
||||||
const workspaceRoute = buildHostWorkspaceRoute(serverId ?? '', item.workspaceId)
|
const workspaceRoute = buildHostWorkspaceRoute(serverId ?? '', item.workspaceId)
|
||||||
const navigate = shouldReplaceWorkspaceNavigation ? router.replace : router.push
|
|
||||||
const isSelected =
|
const isSelected =
|
||||||
Boolean(serverId) &&
|
Boolean(serverId) &&
|
||||||
activeWorkspaceSelection?.serverId === serverId &&
|
activeWorkspaceSelection?.serverId === serverId &&
|
||||||
@@ -795,7 +792,7 @@ function ProjectBlock({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
onWorkspacePress?.()
|
onWorkspacePress?.()
|
||||||
navigate(workspaceRoute as any)
|
router.push(workspaceRoute as any)
|
||||||
}}
|
}}
|
||||||
drag={workspaceDrag}
|
drag={workspaceDrag}
|
||||||
isDragging={isActive}
|
isDragging={isActive}
|
||||||
@@ -807,7 +804,6 @@ function ProjectBlock({
|
|||||||
activeWorkspaceSelection,
|
activeWorkspaceSelection,
|
||||||
onWorkspacePress,
|
onWorkspacePress,
|
||||||
serverId,
|
serverId,
|
||||||
shouldReplaceWorkspaceNavigation,
|
|
||||||
shortcutIndexByWorkspaceKey,
|
shortcutIndexByWorkspaceKey,
|
||||||
showShortcutBadges,
|
showShortcutBadges,
|
||||||
]
|
]
|
||||||
@@ -861,9 +857,7 @@ export function SidebarWorkspaceList({
|
|||||||
parentGestureRef,
|
parentGestureRef,
|
||||||
}: SidebarWorkspaceListProps) {
|
}: SidebarWorkspaceListProps) {
|
||||||
const isMobile = UnistylesRuntime.breakpoint === 'xs' || UnistylesRuntime.breakpoint === 'sm'
|
const isMobile = UnistylesRuntime.breakpoint === 'xs' || UnistylesRuntime.breakpoint === 'sm'
|
||||||
const segments = useSegments()
|
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const shouldReplaceWorkspaceNavigation = segments[0] === 'h'
|
|
||||||
const [collapsedProjectKeys, setCollapsedProjectKeys] = useState<Set<string>>(new Set())
|
const [collapsedProjectKeys, setCollapsedProjectKeys] = useState<Set<string>>(new Set())
|
||||||
const [outerScrollEnabled, setOuterScrollEnabled] = useState(true)
|
const [outerScrollEnabled, setOuterScrollEnabled] = useState(true)
|
||||||
const isTauri = getIsTauri()
|
const isTauri = getIsTauri()
|
||||||
@@ -1081,7 +1075,6 @@ export function SidebarWorkspaceList({
|
|||||||
iconDataUri={projectIconByProjectKey.get(item.projectKey) ?? null}
|
iconDataUri={projectIconByProjectKey.get(item.projectKey) ?? null}
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
activeWorkspaceSelection={activeWorkspaceSelection}
|
activeWorkspaceSelection={activeWorkspaceSelection}
|
||||||
shouldReplaceWorkspaceNavigation={shouldReplaceWorkspaceNavigation}
|
|
||||||
showShortcutBadges={showShortcutBadges}
|
showShortcutBadges={showShortcutBadges}
|
||||||
shortcutIndexByWorkspaceKey={shortcutModel.shortcutIndexByWorkspaceKey}
|
shortcutIndexByWorkspaceKey={shortcutModel.shortcutIndexByWorkspaceKey}
|
||||||
parentGestureRef={parentGestureRef}
|
parentGestureRef={parentGestureRef}
|
||||||
@@ -1106,7 +1099,6 @@ export function SidebarWorkspaceList({
|
|||||||
projectIconByProjectKey,
|
projectIconByProjectKey,
|
||||||
serverId,
|
serverId,
|
||||||
shortcutModel.shortcutIndexByWorkspaceKey,
|
shortcutModel.shortcutIndexByWorkspaceKey,
|
||||||
shouldReplaceWorkspaceNavigation,
|
|
||||||
showShortcutBadges,
|
showShortcutBadges,
|
||||||
toggleProjectCollapsed,
|
toggleProjectCollapsed,
|
||||||
lockOuterScrollForDrag,
|
lockOuterScrollForDrag,
|
||||||
|
|||||||
@@ -165,7 +165,6 @@ export function SortableInlineList<T>({
|
|||||||
|
|
||||||
if (oldIndex >= 0 && newIndex >= 0 && oldIndex !== newIndex) {
|
if (oldIndex >= 0 && newIndex >= 0 && oldIndex !== newIndex) {
|
||||||
const newItems = arrayMove(items, oldIndex, newIndex);
|
const newItems = arrayMove(items, oldIndex, newIndex);
|
||||||
setDragItems(newItems);
|
|
||||||
onDragEnd?.(newItems);
|
onDragEnd?.(newItems);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
|
|
||||||
type UseWorkspaceTabLayoutInput = {
|
type UseWorkspaceTabLayoutInput = {
|
||||||
tabLabels: string[];
|
tabLabels: string[];
|
||||||
|
viewportWidthOverride?: number | null;
|
||||||
metrics: {
|
metrics: {
|
||||||
rowHorizontalInset: number;
|
rowHorizontalInset: number;
|
||||||
actionsReservedWidth: number;
|
actionsReservedWidth: number;
|
||||||
@@ -29,15 +30,19 @@ type UseWorkspaceTabLayoutResult = {
|
|||||||
|
|
||||||
export function useWorkspaceTabLayout(input: UseWorkspaceTabLayoutInput): UseWorkspaceTabLayoutResult {
|
export function useWorkspaceTabLayout(input: UseWorkspaceTabLayoutInput): UseWorkspaceTabLayoutResult {
|
||||||
const { width: viewportWidth } = useWindowDimensions();
|
const { width: viewportWidth } = useWindowDimensions();
|
||||||
|
const resolvedViewportWidth =
|
||||||
|
typeof input.viewportWidthOverride === "number" && input.viewportWidthOverride > 0
|
||||||
|
? input.viewportWidthOverride
|
||||||
|
: viewportWidth;
|
||||||
|
|
||||||
const layout = useMemo(
|
const layout = useMemo(
|
||||||
() =>
|
() =>
|
||||||
computeWorkspaceTabLayout({
|
computeWorkspaceTabLayout({
|
||||||
viewportWidth,
|
viewportWidth: resolvedViewportWidth,
|
||||||
tabLabelLengths: input.tabLabels.map((label) => label.length),
|
tabLabelLengths: input.tabLabels.map((label) => label.length),
|
||||||
metrics: input.metrics,
|
metrics: input.metrics,
|
||||||
}),
|
}),
|
||||||
[input.metrics, input.tabLabels, viewportWidth]
|
[input.metrics, input.tabLabels, resolvedViewportWidth]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo, type Dispatch, type SetStateAction } from "react";
|
import { useCallback, useMemo, useState, type Dispatch, type SetStateAction } from "react";
|
||||||
import { ActivityIndicator, Pressable, ScrollView, Text, View } from "react-native";
|
import { ActivityIndicator, Pressable, ScrollView, Text, View, type LayoutChangeEvent } from "react-native";
|
||||||
import { Bot, FileText, Pencil, Plus, SquareTerminal, Terminal, X } from "lucide-react-native";
|
import { Bot, FileText, Pencil, Plus, SquareTerminal, Terminal, X } from "lucide-react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { SortableInlineList } from "@/components/sortable-inline-list";
|
import { SortableInlineList } from "@/components/sortable-inline-list";
|
||||||
@@ -73,11 +73,23 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
onReorderTabs,
|
onReorderTabs,
|
||||||
}: WorkspaceDesktopTabsRowProps) {
|
}: WorkspaceDesktopTabsRowProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
|
const [tabsContainerWidth, setTabsContainerWidth] = useState<number>(0);
|
||||||
|
const [tabsActionsWidth, setTabsActionsWidth] = useState<number>(0);
|
||||||
|
|
||||||
|
const handleTabsContainerLayout = useCallback((event: LayoutChangeEvent) => {
|
||||||
|
const nextWidth = Math.round(event.nativeEvent.layout.width);
|
||||||
|
setTabsContainerWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTabsActionsLayout = useCallback((event: LayoutChangeEvent) => {
|
||||||
|
const nextWidth = Math.round(event.nativeEvent.layout.width);
|
||||||
|
setTabsActionsWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const layoutMetrics = useMemo(
|
const layoutMetrics = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
rowHorizontalInset: 0,
|
rowHorizontalInset: 0,
|
||||||
actionsReservedWidth: 120,
|
actionsReservedWidth: Math.max(0, tabsActionsWidth),
|
||||||
rowPaddingHorizontal: theme.spacing[2],
|
rowPaddingHorizontal: theme.spacing[2],
|
||||||
tabGap: theme.spacing[1],
|
tabGap: theme.spacing[1],
|
||||||
minTabWidth: 60,
|
minTabWidth: 60,
|
||||||
@@ -86,19 +98,24 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
tabHorizontalPadding: theme.spacing[3],
|
tabHorizontalPadding: theme.spacing[3],
|
||||||
estimatedCharWidth: 7,
|
estimatedCharWidth: 7,
|
||||||
closeButtonWidth: 22,
|
closeButtonWidth: 22,
|
||||||
compactLabelCharCap: 10,
|
compactLabelCharCap: 9,
|
||||||
compactDenseLabelCharCap: 8,
|
compactDenseLabelCharCap: 7,
|
||||||
}),
|
}),
|
||||||
[theme.spacing]
|
[tabsActionsWidth, theme.spacing]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { layout } = useWorkspaceTabLayout({
|
const { layout } = useWorkspaceTabLayout({
|
||||||
tabLabels: tabs.map((tab) => tab.label),
|
tabLabels: tabs.map((tab) => tab.label),
|
||||||
|
viewportWidthOverride: tabsContainerWidth > 0 ? tabsContainerWidth : null,
|
||||||
metrics: layoutMetrics,
|
metrics: layoutMetrics,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.tabsContainer} testID="workspace-tabs-row">
|
<View
|
||||||
|
style={styles.tabsContainer}
|
||||||
|
testID="workspace-tabs-row"
|
||||||
|
onLayout={handleTabsContainerLayout}
|
||||||
|
>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
horizontal
|
horizontal
|
||||||
testID="workspace-tabs-scroll"
|
testID="workspace-tabs-scroll"
|
||||||
@@ -125,7 +142,7 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
const isClosingTerminal =
|
const isClosingTerminal =
|
||||||
tab.kind === "terminal" && killTerminalPending && killTerminalId === tab.terminalId;
|
tab.kind === "terminal" && killTerminalPending && killTerminalId === tab.terminalId;
|
||||||
const isClosingTab = isClosingAgent || isClosingTerminal;
|
const isClosingTab = isClosingAgent || isClosingTerminal;
|
||||||
const shouldShowCloseButton = layout.showCloseButtons;
|
const shouldShowCloseButton = layout.closeButtonPolicy === "all";
|
||||||
const iconColor = isActive ? theme.colors.foreground : theme.colors.foregroundMuted;
|
const iconColor = isActive ? theme.colors.foreground : theme.colors.foregroundMuted;
|
||||||
const tabAgentStatusBucket = tabAgent
|
const tabAgentStatusBucket = tabAgent
|
||||||
? deriveSidebarStateBucket({
|
? deriveSidebarStateBucket({
|
||||||
@@ -183,6 +200,13 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
style={({ hovered, pressed }) => [
|
style={({ hovered, pressed }) => [
|
||||||
styles.tab,
|
styles.tab,
|
||||||
layout.mode === "icon" && styles.tabIconOnly,
|
layout.mode === "icon" && styles.tabIconOnly,
|
||||||
|
layout.mode === "icon" && {
|
||||||
|
minWidth: layout.tabMaxWidth,
|
||||||
|
width: layout.tabMaxWidth,
|
||||||
|
maxWidth: layout.tabMaxWidth,
|
||||||
|
},
|
||||||
|
layout.mode === "compact" && styles.tabCompact,
|
||||||
|
layout.mode === "compact" && { maxWidth: layout.tabMaxWidth },
|
||||||
isActive && styles.tabActive,
|
isActive && styles.tabActive,
|
||||||
(hovered || pressed || isCloseHovered) && styles.tabHovered,
|
(hovered || pressed || isCloseHovered) && styles.tabHovered,
|
||||||
]}
|
]}
|
||||||
@@ -320,7 +344,7 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<View style={styles.tabsActions}>
|
<View style={styles.tabsActions} onLayout={handleTabsActionsLayout}>
|
||||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
testID="workspace-new-agent-tab"
|
testID="workspace-new-agent-tab"
|
||||||
@@ -415,6 +439,9 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
maxWidth: 40,
|
maxWidth: 40,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
|
tabCompact: {
|
||||||
|
paddingHorizontal: theme.spacing[2],
|
||||||
|
},
|
||||||
tabHandle: {
|
tabHandle: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ const metrics = {
|
|||||||
tabHorizontalPadding: 12,
|
tabHorizontalPadding: 12,
|
||||||
estimatedCharWidth: 7,
|
estimatedCharWidth: 7,
|
||||||
closeButtonWidth: 22,
|
closeButtonWidth: 22,
|
||||||
compactLabelCharCap: 10,
|
compactLabelCharCap: 9,
|
||||||
compactDenseLabelCharCap: 8,
|
compactDenseLabelCharCap: 7,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("computeWorkspaceTabLayout", () => {
|
describe("computeWorkspaceTabLayout", () => {
|
||||||
@@ -26,7 +26,7 @@ describe("computeWorkspaceTabLayout", () => {
|
|||||||
|
|
||||||
expect(result.mode).toBe("full");
|
expect(result.mode).toBe("full");
|
||||||
expect(result.showLabels).toBe(true);
|
expect(result.showLabels).toBe(true);
|
||||||
expect(result.showCloseButtons).toBe(true);
|
expect(result.closeButtonPolicy).toBe("all");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses compact mode before icon-only", () => {
|
it("uses compact mode before icon-only", () => {
|
||||||
@@ -38,7 +38,8 @@ describe("computeWorkspaceTabLayout", () => {
|
|||||||
|
|
||||||
expect(result.mode).toBe("compact");
|
expect(result.mode).toBe("compact");
|
||||||
expect(result.showLabels).toBe(true);
|
expect(result.showLabels).toBe(true);
|
||||||
expect(result.showCloseButtons).toBe(false);
|
expect(result.closeButtonPolicy).toBe("all");
|
||||||
|
expect(result.tabMaxWidth).toBeGreaterThan(108);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to icon mode when compact labels still cannot fit", () => {
|
it("falls back to icon mode when compact labels still cannot fit", () => {
|
||||||
@@ -50,7 +51,7 @@ describe("computeWorkspaceTabLayout", () => {
|
|||||||
|
|
||||||
expect(result.mode).toBe("icon");
|
expect(result.mode).toBe("icon");
|
||||||
expect(result.showLabels).toBe(false);
|
expect(result.showLabels).toBe(false);
|
||||||
expect(result.showCloseButtons).toBe(false);
|
expect(result.closeButtonPolicy).toBe("all");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps icon mode without scroll when icons can fit", () => {
|
it("keeps icon mode without scroll when icons can fit", () => {
|
||||||
@@ -72,18 +73,20 @@ describe("computeWorkspaceTabLayout", () => {
|
|||||||
|
|
||||||
expect(result.mode).toBe("compact");
|
expect(result.mode).toBe("compact");
|
||||||
expect(result.showLabels).toBe(true);
|
expect(result.showLabels).toBe(true);
|
||||||
expect(result.showCloseButtons).toBe(false);
|
expect(result.closeButtonPolicy).toBe("all");
|
||||||
|
expect(result.tabMaxWidth).toBeGreaterThan(94);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses dense compact labels for larger tab counts before icon-only", () => {
|
it("falls back to icon mode sooner at tighter widths when close buttons stay visible", () => {
|
||||||
const result = computeWorkspaceTabLayout({
|
const result = computeWorkspaceTabLayout({
|
||||||
viewportWidth: 1100,
|
viewportWidth: 1100,
|
||||||
tabLabelLengths: [18, 21, 14, 15, 19, 12, 17, 16, 20],
|
tabLabelLengths: [18, 21, 14, 15, 19, 12, 17, 16, 20],
|
||||||
metrics,
|
metrics,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.mode).toBe("compact");
|
expect(result.mode).toBe("icon");
|
||||||
expect(result.showLabels).toBe(true);
|
expect(result.showLabels).toBe(false);
|
||||||
expect(result.showCloseButtons).toBe(false);
|
expect(result.closeButtonPolicy).toBe("all");
|
||||||
|
expect(result.tabMaxWidth).toBe(58);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export type WorkspaceTabLayoutMode = "full" | "compact" | "icon";
|
export type WorkspaceTabLayoutMode = "full" | "compact" | "icon";
|
||||||
|
export type WorkspaceTabCloseButtonPolicy = "all";
|
||||||
|
|
||||||
export type WorkspaceTabLayoutInput = {
|
export type WorkspaceTabLayoutInput = {
|
||||||
viewportWidth: number;
|
viewportWidth: number;
|
||||||
@@ -22,7 +23,8 @@ export type WorkspaceTabLayoutInput = {
|
|||||||
export type WorkspaceTabLayoutResult = {
|
export type WorkspaceTabLayoutResult = {
|
||||||
mode: WorkspaceTabLayoutMode;
|
mode: WorkspaceTabLayoutMode;
|
||||||
showLabels: boolean;
|
showLabels: boolean;
|
||||||
showCloseButtons: boolean;
|
closeButtonPolicy: WorkspaceTabCloseButtonPolicy;
|
||||||
|
tabMaxWidth: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function clamp(value: number, min: number, max: number): number {
|
function clamp(value: number, min: number, max: number): number {
|
||||||
@@ -62,7 +64,8 @@ export function computeWorkspaceTabLayout(
|
|||||||
return {
|
return {
|
||||||
mode: "full",
|
mode: "full",
|
||||||
showLabels: true,
|
showLabels: true,
|
||||||
showCloseButtons: true,
|
closeButtonPolicy: "all",
|
||||||
|
tabMaxWidth: input.metrics.maxTabWidth,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,13 +93,20 @@ export function computeWorkspaceTabLayout(
|
|||||||
rowPaddingHorizontal: input.metrics.rowPaddingHorizontal,
|
rowPaddingHorizontal: input.metrics.rowPaddingHorizontal,
|
||||||
});
|
});
|
||||||
if (fullTotal <= availableWidth) {
|
if (fullTotal <= availableWidth) {
|
||||||
return { mode: "full", showLabels: true, showCloseButtons: true };
|
return {
|
||||||
|
mode: "full",
|
||||||
|
showLabels: true,
|
||||||
|
closeButtonPolicy: "all",
|
||||||
|
tabMaxWidth: input.metrics.maxTabWidth,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const compactTabWidths = input.tabLabelLengths.map((rawLength) => {
|
const compactTabWidths = input.tabLabelLengths.map((rawLength) => {
|
||||||
const labelLength = Math.max(rawLength, 1);
|
const labelLength = Math.max(rawLength, 1);
|
||||||
const estimatedWidth =
|
const estimatedWidth =
|
||||||
baseTabWidth + estimateLabelWidth(Math.min(labelLength, effectiveCompactLabelCharCap));
|
baseTabWidth +
|
||||||
|
estimateLabelWidth(Math.min(labelLength, effectiveCompactLabelCharCap)) +
|
||||||
|
input.metrics.closeButtonWidth;
|
||||||
return clamp(estimatedWidth, input.metrics.minTabWidth, input.metrics.maxTabWidth);
|
return clamp(estimatedWidth, input.metrics.minTabWidth, input.metrics.maxTabWidth);
|
||||||
});
|
});
|
||||||
const compactTotal = computeTotalRowWidth({
|
const compactTotal = computeTotalRowWidth({
|
||||||
@@ -105,8 +115,18 @@ export function computeWorkspaceTabLayout(
|
|||||||
rowPaddingHorizontal: input.metrics.rowPaddingHorizontal,
|
rowPaddingHorizontal: input.metrics.rowPaddingHorizontal,
|
||||||
});
|
});
|
||||||
if (compactTotal <= availableWidth) {
|
if (compactTotal <= availableWidth) {
|
||||||
return { mode: "compact", showLabels: true, showCloseButtons: false };
|
return {
|
||||||
|
mode: "compact",
|
||||||
|
showLabels: true,
|
||||||
|
closeButtonPolicy: "all",
|
||||||
|
tabMaxWidth: Math.max(...compactTabWidths),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { mode: "icon", showLabels: false, showCloseButtons: false };
|
return {
|
||||||
|
mode: "icon",
|
||||||
|
showLabels: false,
|
||||||
|
closeButtonPolicy: "all",
|
||||||
|
tabMaxWidth: 58,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user