feat: Enhance tab UX with drag & drop, middle-click close, context menu, and session management, and resolve tab flicker.

This commit is contained in:
2025-12-06 01:28:16 +01:00
parent 4db05100cf
commit 20e0f6e81c
7 changed files with 525 additions and 75 deletions

View File

@@ -38,3 +38,7 @@ File riassuntivo dello stato di sviluppo di Zentral.
- Implementazione `SchemaDiscoveryService` per allineamento automatico dataset report con strutture dati live.
- [2025-12-06 Sidebar Collapsible](./devlog/2025-12-06-010500_sidebar_collapsible.md) - **Completato**
- Reso il menu laterale collassabile (manuale e responsive) con visualizzazione a sole icone.
- [2025-12-06 Tab UX Improvements](./devlog/2025-12-06-011000_tab_ux_improvements.md) - **Completato**
- Miglioramento UX tab: chiusura con middle-click, drag & drop, gruppi di tab personalizzati.
- [2025-12-06 Tab Flicker Fix](./devlog/2025-12-06-011500_tab_flicker_fix.md) - **Completato**
- Risolto problema di flicker rimuovendo l'aggiornamento manuale dello stato attivo e affidandosi esclusivamente alla sincronizzazione con l'URL.

View File

@@ -0,0 +1,41 @@
# Tab UX Improvements
## Overview
Improve the user experience of the tab bar above the viewport. The goal is to make it more flexible and user-friendly.
## Features
1. **Middle-click to close**: Allow closing tabs by clicking with the middle mouse button.
2. **Drag and Drop**: Allow reordering tabs freely.
3. **Tab Groups (Sessions)**: Allow saving the current set of open tabs as a named group/session and restoring it later.
4. **Context Menu**: Add a right-click context menu to tabs with options like:
- Close
- Close Others
- Close to Right
- Pin Tab (optional, if time permits)
## Implementation Plan
### 1. Dependencies
- Install `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`.
### 2. Context Update (`TabContext.tsx`)
- Add `reorderTabs(newOrder: Tab[])` function.
- Add state for `tabGroups` (saved in `localStorage`).
- Add `saveTabGroup(name: string)` function.
- Add `loadTabGroup(name: string)` function.
- Add `deleteTabGroup(name: string)` function.
- Add `closeOtherTabs(path: string)` function.
- Add `closeTabsToRight(path: string)` function.
### 3. Component Update (`TabsBar.tsx`)
- Wrap tabs in `DndContext` and `SortableContext`.
- Create a `SortableTab` component.
- Implement `onAuxClick` for middle-click closing.
- Add a "Tab Groups" button/menu to the right of the tabs.
- Show saved groups.
- Option to save current session.
- Implement a custom Context Menu for tabs.
## Technical Details
- **Storage**: Use `localStorage` for now. Keys: `zentral_tabs`, `zentral_active_tab`, `zentral_tab_groups`.
- **Styling**: Use MUI components and system.

View File

@@ -0,0 +1,16 @@
# Tab Flicker Fix
## Issue
The user reports a flicker when clicking a tab before it becomes active.
## Diagnosis
The current implementation of the active tab style uses `borderBottom: isActive ? 2 : 0`. This causes a layout shift (height change or content displacement) of 2px whenever the active state changes. This visual jump is perceived as a flicker.
## Solution
Update the styling to maintain a constant border width but change the color.
- Change `borderBottom` to always be `2`.
- Change `borderColor` to be `'primary.main'` when active and `'transparent'` when inactive.
## Plan
1. Modify `src/frontend/src/components/TabsBar.tsx`.
2. Update `SortableTab` styles.

View File

@@ -8,6 +8,9 @@
"name": "frontend",
"version": "0.0.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fullcalendar/daygrid": "^6.1.19",
@@ -335,6 +338,59 @@
"node": ">=6.9.0"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
@@ -5587,6 +5643,12 @@
"typescript": ">=4.8.4"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@@ -10,6 +10,9 @@
"preview": "vite preview"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fullcalendar/daygrid": "^6.1.19",

View File

@@ -1,15 +1,165 @@
import React from 'react';
import { Box, Tabs, Tab, IconButton } from '@mui/material';
import { Close as CloseIcon } from '@mui/icons-material';
import { useTabs } from '../contexts/TabContext';
import React, { useState } from 'react';
import { Box, Tab, IconButton, Menu, MenuItem, ListItemIcon, ListItemText, Divider, Button, TextField, Dialog, DialogTitle, DialogContent, DialogActions, Tooltip } from '@mui/material';
import { Close as CloseIcon, Save as SaveIcon, FolderOpen as FolderOpenIcon, Delete as DeleteIcon, ArrowForward as ArrowForwardIcon, ClearAll as ClearAllIcon } from '@mui/icons-material';
import { useTabs, Tab as TabType } from '../contexts/TabContext';
import { useTheme } from '@mui/material/styles';
import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core';
import { arrayMove, SortableContext, sortableKeyboardCoordinates, horizontalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
interface SortableTabProps {
tab: TabType;
activeTabPath: string;
onActivate: (path: string) => void;
onClose: (path: string) => void;
onContextMenu: (event: React.MouseEvent, tab: TabType) => void;
}
function SortableTab({ tab, activeTabPath, onActivate, onClose, onContextMenu }: SortableTabProps) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging
} = useSortable({ id: tab.path });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
zIndex: isDragging ? 1 : 'auto',
};
const isActive = activeTabPath === tab.path;
return (
<Box
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
sx={{ display: 'inline-flex', outline: 'none' }}
onAuxClick={(e) => {
if (e.button === 1 && tab.closable) {
e.preventDefault();
onClose(tab.path);
}
}}
onContextMenu={(e) => onContextMenu(e, tab)}
>
<Tab
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<span>{tab.label}</span>
{tab.closable && (
<IconButton
size="small"
component="span"
onClick={(e) => {
e.stopPropagation();
onClose(tab.path);
}}
sx={{
p: 0.5,
ml: 0.5,
width: 20,
height: 20,
'&:hover': {
bgcolor: 'action.hover',
color: 'error.main'
}
}}
>
<CloseIcon fontSize="small" sx={{ fontSize: 14 }} />
</IconButton>
)}
</Box>
}
value={tab.path}
onClick={() => onActivate(tab.path)}
sx={{
minHeight: 48,
textTransform: 'none',
fontWeight: 500,
color: isActive ? 'primary.main' : 'text.secondary',
borderBottom: 2,
borderColor: isActive ? 'primary.main' : 'transparent',
opacity: 1,
minWidth: 'auto',
px: 2,
'&:hover': {
bgcolor: 'action.hover',
color: isActive ? 'primary.main' : 'text.primary',
}
}}
/>
</Box>
);
}
export default function TabsBar() {
const { tabs, activeTabPath, setActiveTab, closeTab } = useTabs();
const {
tabs,
activeTabPath,
setActiveTab,
closeTab,
reorderTabs,
saveTabGroup,
loadTabGroup,
tabGroups,
deleteTabGroup,
closeOtherTabs,
closeTabsToRight
} = useTabs();
const theme = useTheme();
const handleChange = (_: React.SyntheticEvent, newValue: string) => {
setActiveTab(newValue);
const [contextMenu, setContextMenu] = useState<{ mouseX: number; mouseY: number; tab: TabType | null } | null>(null);
const [groupsMenuAnchor, setGroupsMenuAnchor] = useState<null | HTMLElement>(null);
const [saveGroupDialogOpen, setSaveGroupDialogOpen] = useState(false);
const [newGroupName, setNewGroupName] = useState('');
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
const oldIndex = tabs.findIndex((t) => t.path === active.id);
const newIndex = tabs.findIndex((t) => t.path === over.id);
reorderTabs(arrayMove(tabs, oldIndex, newIndex));
}
};
const handleContextMenu = (event: React.MouseEvent, tab: TabType) => {
event.preventDefault();
setContextMenu({
mouseX: event.clientX + 2,
mouseY: event.clientY - 6,
tab,
});
};
const handleCloseContextMenu = () => {
setContextMenu(null);
};
const handleSaveGroup = () => {
if (newGroupName.trim()) {
saveTabGroup(newGroupName.trim());
setNewGroupName('');
setSaveGroupDialogOpen(false);
}
};
return (
@@ -21,68 +171,144 @@ export default function TabsBar() {
minHeight: 48,
display: 'flex',
alignItems: 'center',
overflowX: 'auto',
'&::-webkit-scrollbar': {
height: 4,
},
'&::-webkit-scrollbar-track': {
backgroundColor: theme.palette.grey[100],
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: theme.palette.grey[400],
borderRadius: 2,
},
justifyContent: 'space-between',
}}
>
<Tabs
value={activeTabPath}
onChange={handleChange}
variant="scrollable"
scrollButtons="auto"
aria-label="app tabs"
sx={{
minHeight: 48,
'& .MuiTab-root': {
minHeight: 48,
textTransform: 'none',
minWidth: 'auto',
px: 2,
fontWeight: 500,
},
}}
<Box sx={{
flex: 1,
overflowX: 'auto',
display: 'flex',
'&::-webkit-scrollbar': { height: 4 },
'&::-webkit-scrollbar-track': { backgroundColor: theme.palette.grey[100] },
'&::-webkit-scrollbar-thumb': { backgroundColor: theme.palette.grey[400], borderRadius: 2 },
}}>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={tabs.map(t => t.path)}
strategy={horizontalListSortingStrategy}
>
{tabs.map((tab) => (
<SortableTab
key={tab.path}
tab={tab}
activeTabPath={activeTabPath}
onActivate={setActiveTab}
onClose={closeTab}
onContextMenu={handleContextMenu}
/>
))}
</SortableContext>
</DndContext>
</Box>
{/* Tab Groups & Actions */}
<Box sx={{ display: 'flex', alignItems: 'center', px: 1, borderLeft: 1, borderColor: 'divider' }}>
<Tooltip title="Tab Groups">
<IconButton
size="small"
onClick={(e) => setGroupsMenuAnchor(e.currentTarget)}
>
<FolderOpenIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
{/* Context Menu */}
<Menu
open={contextMenu !== null}
onClose={handleCloseContextMenu}
anchorReference="anchorPosition"
anchorPosition={
contextMenu !== null
? { top: contextMenu.mouseY, left: contextMenu.mouseX }
: undefined
}
>
{tabs.map((tab) => (
<Tab
key={tab.path}
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<span>{tab.label}</span>
{tab.closable && (
<IconButton
size="small"
component="span"
onClick={(e) => {
e.stopPropagation();
closeTab(tab.path);
}}
sx={{
p: 0.5,
ml: 1,
'&:hover': {
bgcolor: 'action.hover',
color: 'error.main',
},
}}
>
<CloseIcon fontSize="small" sx={{ fontSize: 14 }} />
</IconButton>
)}
</Box>
}
value={tab.path}
/>
<MenuItem onClick={() => {
if (contextMenu?.tab) closeTab(contextMenu.tab.path);
handleCloseContextMenu();
}} disabled={!contextMenu?.tab?.closable}>
<ListItemIcon><CloseIcon fontSize="small" /></ListItemIcon>
<ListItemText>Close</ListItemText>
</MenuItem>
<MenuItem onClick={() => {
if (contextMenu?.tab) closeOtherTabs(contextMenu.tab.path);
handleCloseContextMenu();
}}>
<ListItemIcon><ClearAllIcon fontSize="small" /></ListItemIcon>
<ListItemText>Close Others</ListItemText>
</MenuItem>
<MenuItem onClick={() => {
if (contextMenu?.tab) closeTabsToRight(contextMenu.tab.path);
handleCloseContextMenu();
}}>
<ListItemIcon><ArrowForwardIcon fontSize="small" /></ListItemIcon>
<ListItemText>Close to the Right</ListItemText>
</MenuItem>
</Menu>
{/* Groups Menu */}
<Menu
anchorEl={groupsMenuAnchor}
open={Boolean(groupsMenuAnchor)}
onClose={() => setGroupsMenuAnchor(null)}
>
<MenuItem onClick={() => {
setSaveGroupDialogOpen(true);
setGroupsMenuAnchor(null);
}}>
<ListItemIcon><SaveIcon fontSize="small" /></ListItemIcon>
<ListItemText>Save Current Session</ListItemText>
</MenuItem>
<Divider />
{tabGroups.length === 0 && (
<MenuItem disabled>
<ListItemText secondary="No saved groups" />
</MenuItem>
)}
{tabGroups.map((group) => (
<MenuItem key={group.name} onClick={() => {
loadTabGroup(group.name);
setGroupsMenuAnchor(null);
}}>
<ListItemIcon><FolderOpenIcon fontSize="small" /></ListItemIcon>
<ListItemText>{group.name}</ListItemText>
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
deleteTabGroup(group.name);
}}
>
<DeleteIcon fontSize="small" />
</IconButton>
</MenuItem>
))}
</Tabs>
</Menu>
{/* Save Group Dialog */}
<Dialog open={saveGroupDialogOpen} onClose={() => setSaveGroupDialogOpen(false)}>
<DialogTitle>Save Tab Group</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="Group Name"
fullWidth
variant="outlined"
value={newGroupName}
onChange={(e) => setNewGroupName(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setSaveGroupDialogOpen(false)}>Cancel</Button>
<Button onClick={handleSaveGroup} variant="contained">Save</Button>
</DialogActions>
</Dialog>
</Box>
);
}

View File

@@ -7,12 +7,24 @@ export interface Tab {
closable?: boolean;
}
export interface TabGroup {
name: string;
tabs: Tab[];
}
interface TabContextType {
tabs: Tab[];
activeTabPath: string;
tabGroups: TabGroup[];
openTab: (path: string, label: string, closable?: boolean) => void;
closeTab: (path: string) => void;
setActiveTab: (path: string) => void;
reorderTabs: (newTabs: Tab[]) => void;
saveTabGroup: (name: string) => void;
loadTabGroup: (name: string) => void;
deleteTabGroup: (name: string) => void;
closeOtherTabs: (path: string) => void;
closeTabsToRight: (path: string) => void;
}
const TabContext = createContext<TabContextType | undefined>(undefined);
@@ -20,13 +32,15 @@ const TabContext = createContext<TabContextType | undefined>(undefined);
export function TabProvider({ children }: { children: ReactNode }) {
const [tabs, setTabs] = useState<Tab[]>([]);
const [activeTabPath, setActiveTabPath] = useState<string>('/');
const [tabGroups, setTabGroups] = useState<TabGroup[]>([]);
const navigate = useNavigate();
const location = useLocation();
// Load tabs from local storage on mount
// Load tabs and groups from local storage on mount
useEffect(() => {
const savedTabs = localStorage.getItem('zentral_tabs');
const savedActiveTab = localStorage.getItem('zentral_active_tab');
const savedTabGroups = localStorage.getItem('zentral_tab_groups');
if (savedTabs) {
try {
@@ -34,7 +48,6 @@ export function TabProvider({ children }: { children: ReactNode }) {
if (Array.isArray(parsedTabs) && parsedTabs.length > 0) {
setTabs(parsedTabs);
} else {
// Default tab
setTabs([{ path: '/', label: 'Dashboard', closable: false }]);
}
} catch (e) {
@@ -48,6 +61,17 @@ export function TabProvider({ children }: { children: ReactNode }) {
if (savedActiveTab) {
setActiveTabPath(savedActiveTab);
}
if (savedTabGroups) {
try {
const parsedGroups = JSON.parse(savedTabGroups);
if (Array.isArray(parsedGroups)) {
setTabGroups(parsedGroups);
}
} catch (e) {
console.error("Failed to parse tab groups", e);
}
}
}, []);
// Save tabs to local storage whenever they change
@@ -58,22 +82,24 @@ export function TabProvider({ children }: { children: ReactNode }) {
localStorage.setItem('zentral_active_tab', activeTabPath);
}, [tabs, activeTabPath]);
// Save groups to local storage
useEffect(() => {
localStorage.setItem('zentral_tab_groups', JSON.stringify(tabGroups));
}, [tabGroups]);
// Sync active tab with location
useEffect(() => {
// If the current location is not the active tab, update active tab if it exists in tabs
// This handles browser back/forward buttons
const currentPath = location.pathname;
const tabExists = tabs.find(t => t.path === currentPath);
if (tabExists && activeTabPath !== currentPath) {
setActiveTabPath(currentPath);
}
}, [location.pathname, tabs, activeTabPath]);
}, [location.pathname, tabs]);
const openTab = (path: string, label: string, closable: boolean = true) => {
if (!tabs.find((t) => t.path === path)) {
setTabs((prev) => [...prev, { path, label, closable }]);
}
setActiveTabPath(path);
navigate(path);
};
@@ -81,14 +107,15 @@ export function TabProvider({ children }: { children: ReactNode }) {
const tabIndex = tabs.findIndex((t) => t.path === path);
if (tabIndex === -1) return;
// Prevent closing the last tab if it's the dashboard (or make sure dashboard is always there)
// But logic here says if we close a tab, we switch to another.
const newTabs = tabs.filter((t) => t.path !== path);
setTabs(newTabs);
if (activeTabPath === path) {
// Switch to the nearest tab
const newActiveTab = newTabs[tabIndex] || newTabs[tabIndex - 1] || newTabs[0];
if (newActiveTab) {
setActiveTabPath(newActiveTab.path);
navigate(newActiveTab.path);
} else {
navigate('/');
@@ -97,12 +124,83 @@ export function TabProvider({ children }: { children: ReactNode }) {
};
const setActiveTab = (path: string) => {
setActiveTabPath(path);
navigate(path);
};
const reorderTabs = (newTabs: Tab[]) => {
setTabs(newTabs);
};
const saveTabGroup = (name: string) => {
const newGroup: TabGroup = { name, tabs: [...tabs] };
setTabGroups(prev => {
const existingIndex = prev.findIndex(g => g.name === name);
if (existingIndex >= 0) {
const updated = [...prev];
updated[existingIndex] = newGroup;
return updated;
}
return [...prev, newGroup];
});
};
const loadTabGroup = (name: string) => {
const group = tabGroups.find(g => g.name === name);
if (group) {
setTabs(group.tabs);
if (group.tabs.length > 0) {
setActiveTabPath(group.tabs[0].path);
navigate(group.tabs[0].path);
}
}
};
const deleteTabGroup = (name: string) => {
setTabGroups(prev => prev.filter(g => g.name !== name));
};
const closeOtherTabs = (path: string) => {
const tabToKeep = tabs.find(t => t.path === path);
if (tabToKeep) {
// Keep dashboard if it exists and is not the current tab?
// Usually "Close others" means close everything else that is closable.
// Assuming Dashboard is not closable, it should be kept?
// The current logic has 'closable' property.
const newTabs = tabs.filter(t => t.path === path || !t.closable);
setTabs(newTabs);
navigate(path);
}
};
const closeTabsToRight = (path: string) => {
const index = tabs.findIndex(t => t.path === path);
if (index !== -1) {
const tabsToKeep = tabs.slice(0, index + 1);
// Also keep non-closable tabs that might be to the right (unlikely but good to be safe)
const tabsToRight = tabs.slice(index + 1);
const nonClosableToRight = tabsToRight.filter(t => !t.closable);
setTabs([...tabsToKeep, ...nonClosableToRight]);
navigate(path);
}
};
return (
<TabContext.Provider value={{ tabs, activeTabPath, openTab, closeTab, setActiveTab }}>
<TabContext.Provider value={{
tabs,
activeTabPath,
tabGroups,
openTab,
closeTab,
setActiveTab,
reorderTabs,
saveTabGroup,
loadTabGroup,
deleteTabGroup,
closeOtherTabs,
closeTabsToRight
}}>
{children}
</TabContext.Provider>
);