feat(ui): boards list layout & style tweaking

This commit is contained in:
psychedelicious 2024-07-09 21:58:48 +10:00
parent 907b257984
commit 81cf47dd99
6 changed files with 243 additions and 256 deletions

View File

@ -52,8 +52,8 @@ const IAIDropOverlay = (props: Props) => {
bottom={0.5} bottom={0.5}
opacity={1} opacity={1}
borderWidth={2} borderWidth={2}
borderColor={isOver ? 'base.50' : 'base.300'} borderColor={isOver ? 'base.300' : 'base.500'}
borderRadius="lg" borderRadius="base"
borderStyle="dashed" borderStyle="dashed"
transitionProperty="common" transitionProperty="common"
transitionDuration="0.1s" transitionDuration="0.1s"

View File

@ -1,4 +1,4 @@
import { Box, Collapse, Flex, Icon, Text, useDisclosure } from '@invoke-ai/ui-library'; import { Collapse, Flex, Icon, Text, useDisclosure } from '@invoke-ai/ui-library';
import { EMPTY_ARRAY } from 'app/store/constants'; import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
@ -45,80 +45,89 @@ const BoardsList = () => {
return ( return (
<> <>
<Flex layerStyle="first" flexDir="column" gap={2} p={2} mt={2} borderRadius="base"> <Flex layerStyle="first" flexDir="column" borderRadius="base">
<Flex gap={2} alignItems="center"> <Flex gap={2} alignItems="center" pb={2}>
<BoardsSearch /> <BoardsSearch />
<GallerySettingsPopover /> <GallerySettingsPopover />
</Flex> </Flex>
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}> {allowPrivateBoards && (
<Box maxH={346}> <>
{allowPrivateBoards && ( <Flex w="full" gap={2}>
<> <Flex
<Flex borderBottom="1px" borderColor="base.400" my="2" justifyContent="space-between"> flexGrow={1}
<Flex onClick={privateBoardsDisclosure.onToggle} gap={2} alignItems="center" cursor="pointer"> onClick={privateBoardsDisclosure.onToggle}
<Icon gap={2}
as={PiCaretUpBold} alignItems="center"
boxSize={6} cursor="pointer"
transform={privateBoardsDisclosure.isOpen ? 'rotate(0deg)' : 'rotate(180deg)'} >
transitionProperty="common"
transitionDuration="normal"
color="base.400"
/>
<Text fontSize="md" fontWeight="medium">
{t('boards.private')}
</Text>
</Flex>
<AddBoardButton isPrivateBoard={true} />
</Flex>
<Collapse in={privateBoardsDisclosure.isOpen} animateOpacity>
<Flex direction="column">
<NoBoardBoard isSelected={selectedBoardId === 'none'} />
{filteredPrivateBoards.map((board) => (
<GalleryBoard
board={board}
isSelected={selectedBoardId === board.board_id}
setBoardToDelete={setBoardToDelete}
key={board.board_id}
/>
))}
</Flex>
</Collapse>
</>
)}
<Flex borderBottom="1px" borderColor="base.400" my="2" justifyContent="space-between">
<Flex onClick={sharedBoardsDisclosure.onToggle} gap={2} alignItems="center" cursor="pointer">
<Icon <Icon
as={PiCaretUpBold} as={PiCaretUpBold}
boxSize={6} boxSize={4}
transform={sharedBoardsDisclosure.isOpen ? 'rotate(0deg)' : 'rotate(180deg)'} transform={privateBoardsDisclosure.isOpen ? 'rotate(0deg)' : 'rotate(180deg)'}
transitionProperty="common" transitionProperty="common"
transitionDuration="normal" transitionDuration="normal"
color="base.400" color="base.400"
/> />
<Text fontSize="md" fontWeight="medium"> <Text fontSize="md" fontWeight="medium" userSelect="none">
{allowPrivateBoards ? t('boards.shared') : t('boards.boards')} {t('boards.private')}
</Text> </Text>
</Flex> </Flex>
<AddBoardButton isPrivateBoard={false} /> <AddBoardButton isPrivateBoard={true} />
</Flex> </Flex>
<Collapse in={sharedBoardsDisclosure.isOpen} animateOpacity> <Collapse in={privateBoardsDisclosure.isOpen} animateOpacity>
<Flex direction="column"> <OverlayScrollbarsComponent
{filteredSharedBoards.map((board) => ( defer
<GalleryBoard style={overlayScrollbarsStyles}
board={board} options={overlayScrollbarsParams.options}
isSelected={selectedBoardId === board.board_id} >
setBoardToDelete={setBoardToDelete} <Flex direction="column" maxH={346} gap={1}>
key={board.board_id} <NoBoardBoard isSelected={selectedBoardId === 'none'} />
/> {filteredPrivateBoards.map((board) => (
))} <GalleryBoard
</Flex> board={board}
isSelected={selectedBoardId === board.board_id}
setBoardToDelete={setBoardToDelete}
key={board.board_id}
/>
))}
</Flex>
</OverlayScrollbarsComponent>
</Collapse> </Collapse>
</Box> </>
</OverlayScrollbarsComponent> )}
<Flex w="full" gap={2}>
<Flex onClick={sharedBoardsDisclosure.onToggle} gap={2} alignItems="center" cursor="pointer" flexGrow={1}>
<Icon
as={PiCaretUpBold}
boxSize={4}
transform={sharedBoardsDisclosure.isOpen ? 'rotate(0deg)' : 'rotate(180deg)'}
transitionProperty="common"
transitionDuration="normal"
color="base.400"
/>
<Text fontSize="md" fontWeight="medium" userSelect="none">
{allowPrivateBoards ? t('boards.shared') : t('boards.boards')}
</Text>
</Flex>
<AddBoardButton isPrivateBoard={false} />
</Flex>
<Collapse in={sharedBoardsDisclosure.isOpen} animateOpacity>
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex direction="column" maxH={346} gap={1}>
{filteredSharedBoards.map((board) => (
<GalleryBoard
board={board}
isSelected={selectedBoardId === board.board_id}
setBoardToDelete={setBoardToDelete}
key={board.board_id}
/>
))}
</Flex>
</OverlayScrollbarsComponent>
</Collapse>
</Flex> </Flex>
<DeleteBoardModal boardToDelete={boardToDelete} setBoardToDelete={setBoardToDelete} /> <DeleteBoardModal boardToDelete={boardToDelete} setBoardToDelete={setBoardToDelete} />
</> </>
); );
}; };
export default memo(BoardsList); export default memo(BoardsList);

View File

@ -1,16 +1,25 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library'; import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Editable, EditableInput, EditablePreview, Flex, Icon, Image, Text, Tooltip } from '@invoke-ai/ui-library'; import {
Editable,
EditableInput,
EditablePreview,
Flex,
Icon,
Image,
Text,
Tooltip,
useDisclosure,
} from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query'; import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDroppable from 'common/components/IAIDroppable'; import IAIDroppable from 'common/components/IAIDroppable';
import SelectionOverlay from 'common/components/SelectionOverlay';
import type { AddToBoardDropData } from 'features/dnd/types'; import type { AddToBoardDropData } from 'features/dnd/types';
import BoardContextMenu from 'features/gallery/components/Boards/BoardContextMenu'; import BoardContextMenu from 'features/gallery/components/Boards/BoardContextMenu';
import { BoardTotalsTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTotalsTooltip'; import { BoardTotalsTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTotalsTooltip';
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice'; import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo, useState } from 'react'; import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiArchiveBold, PiImagesSquare } from 'react-icons/pi'; import { PiArchiveBold, PiImageSquare } from 'react-icons/pi';
import { useUpdateBoardMutation } from 'services/api/endpoints/boards'; import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import type { BoardDTO } from 'services/api/types'; import type { BoardDTO } from 'services/api/types';
@ -19,14 +28,13 @@ const editableInputStyles: SystemStyleObject = {
p: 0, p: 0,
fontSize: 'md', fontSize: 'md',
w: '100%', w: '100%',
_focusVisible: {
p: 0,
},
}; };
const ArchivedIcon = () => { const _hover: SystemStyleObject = {
return ( bg: 'base.800',
<Box position="absolute" top={1} insetInlineEnd={2} p={0} minW={0}>
<Icon as={PiArchiveBold} fill="base.300" filter="drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-800))" />
</Box>
);
}; };
interface GalleryBoardProps { interface GalleryBoardProps {
@ -39,65 +47,51 @@ const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick); const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
const editingDisclosure = useDisclosure();
const [isHovered, setIsHovered] = useState(false); const [localBoardName, setLocalBoardName] = useState(board.board_name);
const handleMouseOver = useCallback(() => {
setIsHovered(true);
}, []);
const handleMouseOut = useCallback(() => {
setIsHovered(false);
}, []);
const { currentData: coverImage } = useGetImageDTOQuery(board.cover_image_name ?? skipToken);
const { board_name, board_id } = board;
const [localBoardName, setLocalBoardName] = useState(board_name);
const handleSelectBoard = useCallback(() => { const handleSelectBoard = useCallback(() => {
dispatch(boardIdSelected({ boardId: board_id })); dispatch(boardIdSelected({ boardId: board.board_id }));
if (autoAssignBoardOnClick) { if (autoAssignBoardOnClick) {
dispatch(autoAddBoardIdChanged(board_id)); dispatch(autoAddBoardIdChanged(board.board_id));
} }
}, [board_id, autoAssignBoardOnClick, dispatch]); }, [dispatch, board.board_id, autoAssignBoardOnClick]);
const [updateBoard, { isLoading: isUpdateBoardLoading }] = useUpdateBoardMutation(); const [updateBoard, { isLoading: isUpdateBoardLoading }] = useUpdateBoardMutation();
const droppableData: AddToBoardDropData = useMemo( const droppableData: AddToBoardDropData = useMemo(
() => ({ () => ({
id: board_id, id: board.board_id,
actionType: 'ADD_TO_BOARD', actionType: 'ADD_TO_BOARD',
context: { boardId: board_id }, context: { boardId: board.board_id },
}), }),
[board_id] [board.board_id]
); );
const handleSubmit = useCallback( const handleSubmit = useCallback(
async (newBoardName: string) => { async (newBoardName: string) => {
// empty strings are not allowed
if (!newBoardName.trim()) { if (!newBoardName.trim()) {
setLocalBoardName(board_name); // empty strings are not allowed
return; setLocalBoardName(board.board_name);
} } else if (newBoardName === board.board_name) {
// don't updated the board name if it hasn't changed
} else {
try {
const { board_name } = await updateBoard({
board_id: board.board_id,
changes: { board_name: newBoardName },
}).unwrap();
// don't updated the board name if it hasn't changed // update local state
if (newBoardName === board_name) { setLocalBoardName(board_name);
return; } catch {
} // revert on error
setLocalBoardName(board.board_name);
try { }
const { board_name } = await updateBoard({
board_id,
changes: { board_name: newBoardName },
}).unwrap();
// update local state
setLocalBoardName(board_name);
} catch {
// revert on error
setLocalBoardName(board_name);
} }
editingDisclosure.onClose();
}, },
[board_id, board_name, updateBoard] [board.board_id, board.board_name, editingDisclosure, updateBoard]
); );
const handleChange = useCallback((newBoardName: string) => { const handleChange = useCallback((newBoardName: string) => {
@ -105,92 +99,88 @@ const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps
}, []); }, []);
return ( return (
<Box w="full" userSelect="none" px="1"> <BoardContextMenu board={board} setBoardToDelete={setBoardToDelete}>
<Flex {(ref) => (
onMouseOver={handleMouseOver} <Tooltip
onMouseOut={handleMouseOut} label={<BoardTotalsTooltip board_id={board.board_id} isArchived={Boolean(board.archived)} />}
position="relative" openDelay={1000}
alignItems="center" >
borderRadius="base" <Flex
w="full" position="relative"
my="2" ref={ref}
userSelect="none" onClick={handleSelectBoard}
> w="full"
<BoardContextMenu board={board} setBoardToDelete={setBoardToDelete}> alignItems="center"
{(ref) => ( borderRadius="base"
<Tooltip cursor="pointer"
label={<BoardTotalsTooltip board_id={board.board_id} isArchived={Boolean(board.archived)} />} py={1}
openDelay={1000} px={2}
gap={2}
bg={isSelected ? 'base.800' : undefined}
_hover={_hover}
>
<CoverImage board={board} />
<Editable
as={Flex}
alignItems="center"
gap={4}
flexGrow={1}
onEdit={editingDisclosure.onOpen}
value={localBoardName}
isDisabled={isUpdateBoardLoading}
submitOnBlur={true}
onChange={handleChange}
onSubmit={handleSubmit}
> >
<Flex <EditablePreview
ref={ref} p={0}
onClick={handleSelectBoard} fontSize="md"
w="full" textOverflow="ellipsis"
alignItems="center" noOfLines={1}
justifyContent="space-between" w="fit-content"
borderRadius="base" wordBreak="break-all"
cursor="pointer" color={isSelected ? 'base.100' : 'base.400'}
gap="6" fontWeight={isSelected ? 'semibold' : 'normal'}
p="1" />
> <EditableInput sx={editableInputStyles} />
<Flex gap="6"> </Editable>
{board.archived && <ArchivedIcon />} {board.archived && !editingDisclosure.isOpen && (
{coverImage?.thumbnail_url ? ( <Icon
<Image as={PiArchiveBold}
src={coverImage?.thumbnail_url} fill="base.300"
draggable={false} filter="drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-800))"
objectFit="cover" />
w="8" )}
h="8" <IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
borderRadius="base" </Flex>
borderBottomRadius="lg" </Tooltip>
/> )}
) : ( </BoardContextMenu>
<Flex w="8" h="8" justifyContent="center" alignItems="center">
<Icon boxSize={8} as={PiImagesSquare} opacity={0.7} color="base.500" />
</Flex>
)}
<SelectionOverlay isSelected={isSelected} isSelectedForCompare={false} isHovered={isHovered} />
<Flex
p={1}
justifyContent="center"
alignItems="center"
color={isSelected ? 'base.100' : 'base.400'}
lineHeight="short"
fontSize="md"
>
<Editable
value={localBoardName}
isDisabled={isUpdateBoardLoading}
submitOnBlur={true}
onChange={handleChange}
onSubmit={handleSubmit}
>
<EditablePreview
p={0}
fontSize="md"
textOverflow="ellipsis"
noOfLines={1}
color="inherit"
w="fit-content"
/>
<EditableInput sx={editableInputStyles} />
</Editable>
</Flex>
</Flex>
<Text justifySelf="end" color="base.600">
{`${t('boards.imagesWithCount', { count: board.image_count })}`}
</Text>
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
</Flex>
</Tooltip>
)}
</BoardContextMenu>
</Flex>
</Box>
); );
}; };
export default memo(GalleryBoard); export default memo(GalleryBoard);
const CoverImage = ({ board }: { board: BoardDTO }) => {
const { currentData: coverImage } = useGetImageDTOQuery(board.cover_image_name ?? skipToken);
if (coverImage) {
return (
<Image
src={coverImage.thumbnail_url}
draggable={false}
objectFit="cover"
w={8}
h={8}
borderRadius="base"
borderBottomRadius="lg"
/>
);
}
return (
<Flex w={8} h={8} justifyContent="center" alignItems="center">
<Icon boxSize={8} as={PiImageSquare} opacity={0.7} color="base.500" />
</Flex>
);
};

View File

@ -1,13 +1,12 @@
import { Box, Flex, Image, Text, Tooltip } from '@invoke-ai/ui-library'; import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, Icon, Text, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDroppable from 'common/components/IAIDroppable'; import IAIDroppable from 'common/components/IAIDroppable';
import SelectionOverlay from 'common/components/SelectionOverlay';
import type { RemoveFromBoardDropData } from 'features/dnd/types'; import type { RemoveFromBoardDropData } from 'features/dnd/types';
import { BoardTotalsTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTotalsTooltip'; import { BoardTotalsTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTotalsTooltip';
import NoBoardBoardContextMenu from 'features/gallery/components/Boards/NoBoardBoardContextMenu'; import NoBoardBoardContextMenu from 'features/gallery/components/Boards/NoBoardBoardContextMenu';
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice'; import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
import InvokeLogoSVG from 'public/assets/images/invoke-symbol-wht-lrg.svg'; import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useBoardName } from 'services/api/hooks/useBoardName'; import { useBoardName } from 'services/api/hooks/useBoardName';
@ -15,6 +14,10 @@ interface Props {
isSelected: boolean; isSelected: boolean;
} }
const _hover: SystemStyleObject = {
bg: 'base.800',
};
const NoBoardBoard = memo(({ isSelected }: Props) => { const NoBoardBoard = memo(({ isSelected }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick); const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
@ -25,15 +28,6 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
dispatch(autoAddBoardIdChanged('none')); dispatch(autoAddBoardIdChanged('none'));
} }
}, [dispatch, autoAssignBoardOnClick]); }, [dispatch, autoAssignBoardOnClick]);
const [isHovered, setIsHovered] = useState(false);
const handleMouseOver = useCallback(() => {
setIsHovered(true);
}, []);
const handleMouseOut = useCallback(() => {
setIsHovered(false);
}, []);
const droppableData: RemoveFromBoardDropData = useMemo( const droppableData: RemoveFromBoardDropData = useMemo(
() => ({ () => ({
@ -44,50 +38,47 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
); );
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Box w="full" userSelect="none" px="1"> <NoBoardBoardContextMenu>
<Flex {(ref) => (
onMouseOver={handleMouseOver} <Tooltip label={<BoardTotalsTooltip board_id="none" isArchived={false} />} openDelay={1000}>
onMouseOut={handleMouseOut} <Flex
position="relative" position="relative"
alignItems="center" ref={ref}
borderRadius="base" onClick={handleSelectBoard}
w="full" w="full"
my="2" alignItems="center"
userSelect="none" borderRadius="base"
> cursor="pointer"
<NoBoardBoardContextMenu> px={2}
{(ref) => ( py={1}
<Tooltip label={<BoardTotalsTooltip board_id="none" isArchived={false} />} openDelay={1000}> gap={2}
<Flex bg={isSelected ? 'base.800' : undefined}
ref={ref} _hover={_hover}
onClick={handleSelectBoard} >
w="full" <Flex w={8} h={8} justifyContent="center" alignItems="center">
alignItems="center" {/* iconified from public/assets/images/invoke-symbol-wht-lrg.svg */}
borderRadius="base" <Icon boxSize={6} opacity={1} stroke="base.500" viewBox="0 0 66 66" fill="none">
cursor="pointer" <path
gap="6" d="M43.9137 16H63.1211V3H3.12109V16H22.3285L43.9137 50H63.1211V63H3.12109V50H22.3285"
p="1" strokeWidth="5"
>
<Image
src={InvokeLogoSVG}
alt="invoke-ai-logo"
opacity={0.7}
mixBlendMode="overlay"
userSelect="none"
height="6"
width="6"
/> />
<Text fontSize="md" color={isSelected ? 'base.100' : 'base.400'}> </Icon>
{boardName} </Flex>
</Text>
<SelectionOverlay isSelected={isSelected} isSelectedForCompare={false} isHovered={isHovered} /> <Text
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} /> fontSize="md"
</Flex> color={isSelected ? 'base.100' : 'base.400'}
</Tooltip> fontWeight={isSelected ? 'semibold' : 'normal'}
)} noOfLines={1}
</NoBoardBoardContextMenu> flexShrink={0}
</Flex> >
</Box> {boardName}
</Text>
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
</Flex>
</Tooltip>
)}
</NoBoardBoardContextMenu>
); );
}); });

View File

@ -16,7 +16,6 @@ const GalleryBoardName = () => {
return ( return (
<Flex <Flex
my="1"
justifyContent="center" justifyContent="center"
fontSize="md" fontSize="md"
fontWeight="bold" fontWeight="bold"

View File

@ -1,4 +1,4 @@
import { Box, Button, ButtonGroup, Flex, Tab, TabList, Tabs } from '@invoke-ai/ui-library'; import { Button, ButtonGroup, Flex, Tab, TabList, Tabs } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { $galleryHeader } from 'app/store/nanostores/galleryHeader'; import { $galleryHeader } from 'app/store/nanostores/galleryHeader';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
@ -40,10 +40,8 @@ const ImageGalleryContent = () => {
gap={2} gap={2}
> >
{galleryHeader} {galleryHeader}
<Box> <BoardsList />
<BoardsList /> <GalleryBoardName />
<GalleryBoardName />
</Box>
<Flex alignItems="center" justifyContent="space-between" gap={2}> <Flex alignItems="center" justifyContent="space-between" gap={2}>
<Tabs index={galleryView === 'images' ? 0 : 1} variant="unstyled" size="sm" w="full"> <Tabs index={galleryView === 'images' ? 0 : 1} variant="unstyled" size="sm" w="full">
<TabList> <TabList>