diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index f7c2513edb..c5e580c049 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -67,6 +67,7 @@ "chakra-react-select": "^4.9.2", "cmdk": "^1.1.1", "compare-versions": "^6.1.1", + "dockview": "^4.3.1", "filesize": "^10.1.6", "fracturedjsonjs": "^4.1.0", "framer-motion": "^11.10.0", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 79de0c48c1..61fba0669c 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -50,6 +50,9 @@ dependencies: compare-versions: specifier: ^6.1.1 version: 6.1.1 + dockview: + specifier: ^4.3.1 + version: 4.3.1(react@18.3.1) filesize: specifier: ^10.1.6 version: 10.1.6 @@ -4492,6 +4495,19 @@ packages: resolution: {integrity: sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==} dev: false + /dockview-core@4.3.1: + resolution: {integrity: sha512-cjGIXKc1wtHHkeKisuDLNt3HSHCVzvabxm1K9Auna27A9T3QR7ISOiTJyEUKUPllkcztFYBut0vwnnvwLnPAuQ==} + dev: false + + /dockview@4.3.1(react@18.3.1): + resolution: {integrity: sha512-D4SvZPs1GJxGUBPkrehlKNGsWlSDaBiPuSYI+IEXnZ7b2bCUs1/h954sVs7xyykqEW3r6TkPKLWdTR/47Q7/QQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + dependencies: + dockview-core: 4.3.1 + react: 18.3.1 + dev: false + /doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} diff --git a/invokeai/frontend/web/src/common/components/Loading/Loading.tsx b/invokeai/frontend/web/src/common/components/Loading/Loading.tsx index 7caf12952a..b8bbcf668a 100644 --- a/invokeai/frontend/web/src/common/components/Loading/Loading.tsx +++ b/invokeai/frontend/web/src/common/components/Loading/Loading.tsx @@ -17,6 +17,7 @@ const Loading = () => { right={0} bottom={0} left={0} + zIndex={99999} > { }, []); return ( - - - - - - - renderMenu={renderMenu} withLongPress={false}> - {(ref) => ( - - + + + Welcome + Workspace + Viewer + + + + + + + + - - {showHUD && } - - - - - - - } colorScheme="base" /> - - - + + + + renderMenu={renderMenu} withLongPress={false}> + {(ref) => ( + + + + + {showHUD && } + + + + + + + } colorScheme="base" /> + + + + + + )} + + {id !== null && ( + + + + + + + + + + + + + )} + + + + + + + + + - )} - - {id !== null && ( - - - - - - - - - - - - - )} - - - - - - - - - - - - + + + + + + + + + + ); }); AdvancedSession.displayName = 'AdvancedSession'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx index dc968e5a51..73d867cff2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx @@ -1,4 +1,4 @@ -import { Alert, Button, Divider, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library'; +import { Alert, Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { InitialStateAddAStyleReference } from 'features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference'; import { InitialStateGenerateFromText } from 'features/controlLayers/components/SimpleSession/InitialStateGenerateFromText'; @@ -14,11 +14,7 @@ export const InitialState = memo(() => { return ( - - Get Started - - - + Get started with Invoke. diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx index 750066526e..54b0be8e1a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx @@ -1,16 +1,45 @@ -import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context'; +import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState'; -import { StagingArea } from 'features/controlLayers/components/SimpleSession/StagingArea'; -import { memo } from 'react'; +import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2'; +import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2'; +import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2'; +import { selectShowGenerateTabSplashScreen } from 'features/ui/store/uiSelectors'; +import { showGenerateTabSplashScreenChanged } from 'features/ui/store/uiSlice'; +import { memo, useCallback } from 'react'; + +export const SimpleSession = memo(() => { + const showGenerateTabSplashScreen = useAppSelector(selectShowGenerateTabSplashScreen); + const dispatch = useAppDispatch(); + + const showSplashScreen = useCallback(() => { + dispatch(showGenerateTabSplashScreenChanged(true)); + }, [dispatch]); -export const SimpleSession = memo(({ id }: { id: string | null }) => { - if (id === null) { - return ; - } return ( - - - + + + Launchpad + Viewer + Generation Progress + + + + + + + + + + + + + + + + + + ); }); SimpleSession.displayName = 'SimpleSession'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSessionNoId.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSessionNoId.tsx new file mode 100644 index 0000000000..e7b3657a95 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSessionNoId.tsx @@ -0,0 +1,15 @@ +import { Divider, Flex } from '@invoke-ai/ui-library'; +import { StagingAreaHeader } from 'features/controlLayers/components/SimpleSession/StagingAreaHeader'; +import { StagingAreaNoItems } from 'features/controlLayers/components/SimpleSession/StagingAreaNoItems'; +import { memo } from 'react'; + +export const SimpleSessionNoId = memo(() => { + return ( + + + + + + ); +}); +SimpleSessionNoId.displayName = 'StSimpleSessionNoIdagingArea'; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx index 5c578a5539..3b66a41c63 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx @@ -1,5 +1,6 @@ import { IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { buildUseDisclosure } from 'common/hooks/useBoolean'; import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors'; import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice'; import type { ChangeEvent, KeyboardEvent } from 'react'; @@ -7,6 +8,8 @@ import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiXBold } from 'react-icons/pi'; +export const [useBoardSearchDisclosure, $boardSearchIsOpen] = buildUseDisclosure(false); + export const BoardsSearch = memo(() => { const dispatch = useAppDispatch(); const boardSearchText = useAppSelector(selectBoardSearchText); diff --git a/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx b/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx index b456cbaaa7..bb80d8e160 100644 --- a/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx @@ -1,25 +1,26 @@ -import type { UseDisclosureReturn } from '@invoke-ai/ui-library'; import { Box, Collapse, Divider, Flex } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { BoardsListWrapper } from 'features/gallery/components/Boards/BoardsList/BoardsListWrapper'; -import { BoardsSearch } from 'features/gallery/components/Boards/BoardsList/BoardsSearch'; +import { $boardSearchIsOpen, BoardsSearch } from 'features/gallery/components/Boards/BoardsList/BoardsSearch'; +import { GalleryTopBar } from 'features/gallery/components/GalleryTopBar'; import type { CSSProperties } from 'react'; import { memo } from 'react'; const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 }; -export const BoardsListPanelContent = memo( - ({ boardSearchDisclosure }: { boardSearchDisclosure: UseDisclosureReturn }) => { - return ( - - - - - - - - - - ); - } -); +export const BoardsListPanelContent = memo(() => { + const boardSearchDisclosure = useStore($boardSearchIsOpen); + return ( + + + + + + + + + + + ); +}); BoardsListPanelContent.displayName = 'BoardsListPanelContent'; diff --git a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx index 785bc53af8..0fcfa7dd82 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx @@ -70,7 +70,7 @@ export const Gallery = memo(() => { const boardName = useBoardName(selectedBoardId); return ( - + diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx index e84e733c58..a5ff519384 100644 --- a/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx @@ -1,67 +1,67 @@ -import type { UseDisclosureReturn } from '@invoke-ai/ui-library'; import { Button, Flex, IconButton } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useBoardSearchDisclosure } from 'features/gallery/components/Boards/BoardsList/BoardsSearch'; import { BoardsSettingsPopover } from 'features/gallery/components/Boards/BoardsSettingsPopover'; import { GalleryHeader } from 'features/gallery/components/GalleryHeader'; import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors'; import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice'; -import type { UsePanelReturn } from 'features/ui/hooks/usePanel'; +import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context'; +import { useCollapsibleGridviewPanel } from 'features/ui/layouts/use-collapsible-gridview-panel'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiCaretDownBold, PiCaretUpBold, PiMagnifyingGlassBold } from 'react-icons/pi'; -export const GalleryTopBar = memo( - ({ - boardsListPanel, - boardSearchDisclosure, - }: { - boardsListPanel: UsePanelReturn; - boardSearchDisclosure: UseDisclosureReturn; - }) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const boardSearchText = useAppSelector(selectBoardSearchText); +export const GalleryTopBar = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const boardSearchText = useAppSelector(selectBoardSearchText); + const boardSearchDisclosure = useBoardSearchDisclosure(); + const api = useAutoLayoutContext(); + const boardsPanel = useCollapsibleGridviewPanel(api, 'boards', 'vertical', 256); + const isBoardsPanelCollapsed = useStore(boardsPanel.$isCollapsed); - const onClickBoardSearch = useCallback(() => { - if (boardSearchText.length) { - dispatch(boardSearchTextChanged('')); - } - boardSearchDisclosure.onToggle(); - boardsListPanel.expand(); - }, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]); + const onClickBoardSearch = useCallback(() => { + if (boardSearchText.length) { + dispatch(boardSearchTextChanged('')); + } + if (!boardSearchDisclosure.isOpen && boardsPanel.$isCollapsed.get()) { + boardsPanel.expand(); + } + boardSearchDisclosure.toggle(); + }, [boardSearchText.length, boardSearchDisclosure, dispatch, boardsPanel]); - return ( - - - - - - - - - - } - colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'} - /> - + return ( + + + - ); - } -); + + + + + + } + colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'} + /> + + + ); +}); GalleryTopBar.displayName = 'GalleryTopBar'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/useGallerySearchTerm.ts b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/useGallerySearchTerm.ts index b9651e3d8e..6071d8a8f8 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/useGallerySearchTerm.ts +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/useGallerySearchTerm.ts @@ -1,5 +1,4 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { selectSearchTerm } from 'features/gallery/store/gallerySelectors'; import { searchTermChanged } from 'features/gallery/store/gallerySlice'; import { debounce } from 'lodash-es'; @@ -7,7 +6,7 @@ import { useCallback, useMemo, useState } from 'react'; export const useGallerySearchTerm = () => { // Highlander! - useAssertSingleton('gallery-search-state'); + // useAssertSingleton('gallery-search-state'); const dispatch = useAppDispatch(); const searchTerm = useAppSelector(selectSearchTerm); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview2.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview2.tsx new file mode 100644 index 0000000000..9e323bd920 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview2.tsx @@ -0,0 +1,110 @@ +import { Box, Flex } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { useAppSelector } from 'app/store/storeHooks'; +import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress'; +import { DndImage } from 'features/dnd/DndImage'; +import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer'; +import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons'; +import { selectShouldShowImageDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors'; +import type { AnimationProps } from 'framer-motion'; +import { AnimatePresence, motion } from 'framer-motion'; +import { memo, useCallback, useRef, useState } from 'react'; +import type { ImageDTO } from 'services/api/types'; +import { $hasLastProgressImage } from 'services/events/stores'; + +import { NoContentForViewer } from './NoContentForViewer'; + +export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => { + const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails); + + // Show and hide the next/prev buttons on mouse move + const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState(false); + const timeoutId = useRef(0); + const onMouseOver = useCallback(() => { + setShouldShowNextPrevButtons(true); + window.clearTimeout(timeoutId.current); + }, []); + const onMouseOut = useCallback(() => { + timeoutId.current = window.setTimeout(() => { + setShouldShowNextPrevButtons(false); + }, 500); + }, []); + + return ( + + + + + + {shouldShowImageDetails && imageDTO && ( + + + + )} + + {shouldShowNextPrevButtons && imageDTO && ( + + + + )} + + + ); +}); +CurrentImagePreview.displayName = 'CurrentImagePreview'; + +const ImageContent = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => { + const hasProgressImage = useStore($hasLastProgressImage); + const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer); + + if (!imageDTO) { + return ; + } + + return ( + + + + ); +}); +ImageContent.displayName = 'ImageContent'; + +const initial: AnimationProps['initial'] = { + opacity: 0, +}; +const animateArrows: AnimationProps['animate'] = { + opacity: 1, + transition: { duration: 0.07 }, +}; +const exit: AnimationProps['exit'] = { + opacity: 0, + transition: { duration: 0.07 }, +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer2.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer2.tsx new file mode 100644 index 0000000000..07cab441af --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer2.tsx @@ -0,0 +1,125 @@ +import { Box, Flex, IconButton, type SystemStyleObject, useOutsideClick } from '@invoke-ai/ui-library'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { selectImageToCompare } from 'features/gallery/components/ImageViewer/common'; +import { CurrentImagePreview } from 'features/gallery/components/ImageViewer/CurrentImagePreview2'; +import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison'; +import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2'; +import { selectLastSelectedImageName } from 'features/gallery/store/gallerySelectors'; +import { memo, useRef } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiXBold } from 'react-icons/pi'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; + +import { useImageViewer } from './useImageViewer'; + +// type Props = { +// closeButton?: ReactNode; +// }; + +// const useFocusRegionOptions = { +// focusOnMount: true, +// }; + +// const FOCUS_REGION_STYLES: SystemStyleObject = { +// display: 'flex', +// width: 'full', +// height: 'full', +// position: 'absolute', +// flexDirection: 'column', +// inset: 0, +// alignItems: 'center', +// justifyContent: 'center', +// overflow: 'hidden', +// }; + +export const ImageViewer = memo(() => { + const lastSelectedImageName = useAppSelector(selectLastSelectedImageName); + const { data: lastSelectedImageDTO } = useGetImageDTOQuery(lastSelectedImageName ?? skipToken); + const comparisonImageDTO = useAppSelector(selectImageToCompare); + + if (lastSelectedImageDTO && comparisonImageDTO) { + return ; + } + + return ; +}); + +ImageViewer.displayName = 'ImageViewer'; + +const imageViewerContainerSx: SystemStyleObject = { + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, + transition: 'opacity 0.15s ease', + opacity: 1, + pointerEvents: 'auto', + '&[data-hidden="true"]': { + opacity: 0, + pointerEvents: 'none', + }, + backdropFilter: 'blur(10px) brightness(70%)', +}; + +export const ImageViewerModal = memo(() => { + const ref = useRef(null); + const imageViewer = useImageViewer(); + useOutsideClick({ + ref, + handler: imageViewer.close, + }); + + useHotkeys( + 'esc', + imageViewer.close, + { + preventDefault: true, + enabled: imageViewer.isOpen, + }, + [imageViewer.isOpen] + ); + + return ( + + + + + + + ); +}); + +ImageViewerModal.displayName = 'GatedImageViewer'; + +const ImageViewerCloseButton = memo(() => { + const { t } = useTranslation(); + const imageViewer = useImageViewer(); + useAssertSingleton('ImageViewerCloseButton'); + useHotkeys('esc', imageViewer.close); + return ( + } + variant="link" + alignSelf="stretch" + onClick={imageViewer.close} + /> + ); +}); + +ImageViewerCloseButton.displayName = 'ImageViewerCloseButton'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage2.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage2.tsx new file mode 100644 index 0000000000..850ebc63e1 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage2.tsx @@ -0,0 +1,56 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { Flex, Image } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import { selectSystemSlice } from 'features/system/store/systemSlice'; +import { memo, useMemo } from 'react'; +import { PiPulseBold } from 'react-icons/pi'; +import { $lastProgressImage } from 'services/events/stores'; + +const selectShouldAntialiasProgressImage = createSelector( + selectSystemSlice, + (system) => system.shouldAntialiasProgressImage +); + +export const ProgressImage = memo(() => { + const progressImage = useStore($lastProgressImage); + const shouldAntialiasProgressImage = useAppSelector(selectShouldAntialiasProgressImage); + + const sx = useMemo( + () => ({ + imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated', + }), + [shouldAntialiasProgressImage] + ); + + if (!progressImage) { + return ( + + + + ); + } + + return ( + + + + ); +}); + +ProgressImage.displayName = 'ProgressImage'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar2.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar2.tsx new file mode 100644 index 0000000000..f8dc34d654 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar2.tsx @@ -0,0 +1,18 @@ +import { ButtonGroup, Flex } from '@invoke-ai/ui-library'; +import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton'; +import { memo } from 'react'; + +import CurrentImageButtons from './CurrentImageButtons'; + +export const ViewerToolbar = memo(() => { + return ( + + + + + + + ); +}); + +ViewerToolbar.displayName = 'ViewerToolbar'; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts index ab6da024fd..0f2a175c1c 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts @@ -1,6 +1,5 @@ import { useAppSelector } from 'app/store/storeHooks'; import { useIsRegionFocused } from 'common/hooks/focus'; -import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state'; import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation'; import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination'; @@ -14,7 +13,7 @@ import { useListImagesQuery } from 'services/api/endpoints/images'; * Registers gallery hotkeys. This hook is a singleton. */ export const useGalleryHotkeys = () => { - useAssertSingleton('useGalleryHotkeys'); + // useAssertSingleton('useGalleryHotkeys'); const { goNext, goPrev, isNextEnabled, isPrevEnabled } = useGalleryPagination(); const selection = useAppSelector((s) => s.gallery.selection); const queryArgs = useAppSelector(selectListImagesQueryArgs); diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts index 87f0b6aba2..cb37c2c11c 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts @@ -1,6 +1,7 @@ import { useStore } from '@nanostores/react'; import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppStore } from 'app/store/nanostores/store'; +import { useAppSelector } from 'app/store/storeHooks'; import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state'; import { @@ -24,11 +25,10 @@ import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata'; import type { ImageDTO } from 'services/api/types'; export const useImageActions = (imageDTO: ImageDTO) => { - const dispatch = useAppDispatch(); + const { dispatch, getState } = useAppStore(); const { t } = useTranslation(); const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId); const isStaging = useAppSelector(selectIsStaging); - const activeTabName = useAppSelector(selectActiveTab); const { metadata } = useDebouncedMetadata(imageDTO.image_name); const [hasMetadata, setHasMetadata] = useState(false); const [hasSeed, setHasSeed] = useState(false); @@ -82,18 +82,20 @@ export const useImageActions = (imageDTO: ImageDTO) => { if (!metadata) { return; } + const activeTabName = selectActiveTab(getState()); parseAndRecallAllMetadata(metadata, activeTabName === 'canvas', isStaging ? ['width', 'height'] : []); clearStylePreset(); - }, [metadata, activeTabName, isStaging, clearStylePreset]); + }, [metadata, getState, isStaging, clearStylePreset]); const remix = useCallback(() => { if (!metadata) { return; } + const activeTabName = selectActiveTab(getState()); // Recalls all metadata parameters except seed parseAndRecallAllMetadata(metadata, activeTabName === 'canvas', ['seed']); clearStylePreset(); - }, [activeTabName, metadata, clearStylePreset]); + }, [metadata, getState, clearStylePreset]); const recallSeed = useCallback(() => { if (!metadata) { diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx index 00dc4754d6..37475ad6aa 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx @@ -12,12 +12,10 @@ import ParamClipSkip from 'features/parameters/components/Advanced/ParamClipSkip import ParamT5EncoderModelSelect from 'features/parameters/components/Advanced/ParamT5EncoderModelSelect'; import ParamSeamlessXAxis from 'features/parameters/components/Seamless/ParamSeamlessXAxis'; import ParamSeamlessYAxis from 'features/parameters/components/Seamless/ParamSeamlessYAxis'; -import { ParamSeed } from 'features/parameters/components/Seed/ParamSeed'; import ParamFLUXVAEModelSelect from 'features/parameters/components/VAEModel/ParamFLUXVAEModelSelect'; import ParamVAEModelSelect from 'features/parameters/components/VAEModel/ParamVAEModelSelect'; import ParamVAEPrecision from 'features/parameters/components/VAEModel/ParamVAEPrecision'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetModelConfigQuery } from 'services/api/endpoints/models'; @@ -33,7 +31,6 @@ const formLabelProps2: FormLabelProps = { export const AdvancedSettingsAccordion = memo(() => { const vaeKey = useAppSelector(selectVAEKey); const { currentData: vaeConfig } = useGetModelConfigQuery(vaeKey ?? skipToken); - const activeTabName = useAppSelector(selectActiveTab); const isFLUX = useAppSelector(selectIsFLUX); const isSD3 = useAppSelector(selectIsSD3); @@ -68,19 +65,16 @@ export const AdvancedSettingsAccordion = memo(() => { if (params.seamlessXAxis || params.seamlessYAxis) { badges.push('seamless'); } - if (activeTabName === 'upscaling' && !params.shouldRandomizeSeed) { - badges.push('Manual Seed'); - } } return badges; }), - [vaeConfig, activeTabName] + [vaeConfig] ); const badges = useAppSelector(selectBadges); const { t } = useTranslation(); const { isOpen, onToggle } = useStandaloneAccordionToggle({ - id: `'advanced-settings-${activeTabName}`, + id: `'advanced-settings-generate`, defaultIsOpen: false, }); @@ -91,39 +85,33 @@ export const AdvancedSettingsAccordion = memo(() => { {isFLUX ? : } {!isFLUX && !isSD3 && } - {activeTabName === 'upscaling' ? ( - - ) : ( + {!isFLUX && !isSD3 && ( <> - {!isFLUX && !isSD3 && ( - <> - - - - - - - - - - - - )} - {isFLUX && ( - - - + + + + + + + + - )} - {isSD3 && ( - - - - - - )} + )} + {isFLUX && ( + + + + + )} + {isSD3 && ( + + + + + + )} ); diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/UpscaleTabAdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/UpscaleTabAdvancedSettingsAccordion.tsx new file mode 100644 index 0000000000..04b55e80c6 --- /dev/null +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/UpscaleTabAdvancedSettingsAccordion.tsx @@ -0,0 +1,90 @@ +import type { FormLabelProps } from '@invoke-ai/ui-library'; +import { Flex, StandaloneAccordion } from '@invoke-ai/ui-library'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectIsFLUX, selectIsSD3, selectParamsSlice, selectVAEKey } from 'features/controlLayers/store/paramsSlice'; +import { ParamSeed } from 'features/parameters/components/Seed/ParamSeed'; +import ParamFLUXVAEModelSelect from 'features/parameters/components/VAEModel/ParamFLUXVAEModelSelect'; +import ParamVAEModelSelect from 'features/parameters/components/VAEModel/ParamVAEModelSelect'; +import ParamVAEPrecision from 'features/parameters/components/VAEModel/ParamVAEPrecision'; +import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useGetModelConfigQuery } from 'services/api/endpoints/models'; + +const formLabelProps: FormLabelProps = { + minW: '9.2rem', +}; + +const formLabelProps2: FormLabelProps = { + flexGrow: 1, +}; + +export const AdvancedSettingsAccordion = memo(() => { + const vaeKey = useAppSelector(selectVAEKey); + const { currentData: vaeConfig } = useGetModelConfigQuery(vaeKey ?? skipToken); + const isFLUX = useAppSelector(selectIsFLUX); + const isSD3 = useAppSelector(selectIsSD3); + + const selectBadges = useMemo( + () => + createMemoizedSelector([selectParamsSlice, selectIsFLUX], (params, isFLUX) => { + const badges: (string | number)[] = []; + if (isFLUX) { + if (vaeConfig) { + let vaeBadge = vaeConfig.name; + if (params.vaePrecision === 'fp16') { + vaeBadge += ` ${params.vaePrecision}`; + } + badges.push(vaeBadge); + } + } else { + if (vaeConfig) { + let vaeBadge = vaeConfig.name; + if (params.vaePrecision === 'fp16') { + vaeBadge += ` ${params.vaePrecision}`; + } + badges.push(vaeBadge); + } else if (params.vaePrecision === 'fp16') { + badges.push(`VAE ${params.vaePrecision}`); + } + if (params.clipSkip) { + badges.push(`Skip ${params.clipSkip}`); + } + if (params.cfgRescaleMultiplier) { + badges.push(`Rescale ${params.cfgRescaleMultiplier}`); + } + if (params.seamlessXAxis || params.seamlessYAxis) { + badges.push('seamless'); + } + if (!params.shouldRandomizeSeed) { + badges.push('Manual Seed'); + } + } + + return badges; + }), + [vaeConfig] + ); + const badges = useAppSelector(selectBadges); + const { t } = useTranslation(); + const { isOpen, onToggle } = useStandaloneAccordionToggle({ + id: `'advanced-settings-upscaling`, + defaultIsOpen: false, + }); + + return ( + + + + {isFLUX ? : } + {!isFLUX && !isSD3 && } + + + + + ); +}); + +AdvancedSettingsAccordion.displayName = 'AdvancedSettingsAccordion'; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx index 0be0a0c602..1e5e29884b 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx @@ -12,14 +12,11 @@ import ParamGuidance from 'features/parameters/components/Core/ParamGuidance'; import ParamScheduler from 'features/parameters/components/Core/ParamScheduler'; import ParamSteps from 'features/parameters/components/Core/ParamSteps'; import { DisabledModelWarning } from 'features/parameters/components/MainModel/DisabledModelWarning'; -import ParamUpscaleCFGScale from 'features/parameters/components/Upscale/ParamUpscaleCFGScale'; -import ParamUpscaleScheduler from 'features/parameters/components/Upscale/ParamUpscaleScheduler'; import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel'; import { API_BASE_MODELS } from 'features/parameters/types/constants'; import { MainModelPicker } from 'features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker'; import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig'; @@ -32,16 +29,12 @@ const formLabelProps: FormLabelProps = { export const GenerationSettingsAccordion = memo(() => { const { t } = useTranslation(); const modelConfig = useSelectedModelConfig(); - const activeTabName = useAppSelector(selectActiveTab); const isFLUX = useAppSelector(selectIsFLUX); const isSD3 = useAppSelector(selectIsSD3); const isCogView4 = useAppSelector(selectIsCogView4); const isApiModel = useIsApiModel(); - const isUpscaling = useMemo(() => { - return activeTabName === 'upscaling'; - }, [activeTabName]); const selectBadges = useMemo( () => createMemoizedSelector(selectLoRAsSlice, (loras) => { @@ -63,8 +56,8 @@ export const GenerationSettingsAccordion = memo(() => { defaultIsOpen: false, }); const { isOpen: isOpenAccordion, onToggle: onToggleAccordion } = useStandaloneAccordionToggle({ - id: `generation-settings-${activeTabName}`, - defaultIsOpen: activeTabName !== 'upscaling', + id: `generation-settings-generate`, + defaultIsOpen: true, }); return ( @@ -85,12 +78,10 @@ export const GenerationSettingsAccordion = memo(() => { - {!isFLUX && !isSD3 && !isCogView4 && !isUpscaling && } - {isUpscaling && } + {!isFLUX && !isSD3 && !isCogView4 && } {isFLUX && modelConfig && !isFluxFillMainModelModelConfig(modelConfig) && } - {isUpscaling && } - {!isFLUX && !isUpscaling && } + {!isFLUX && } diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/UpscaleTabGenerationSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/UpscaleTabGenerationSettingsAccordion.tsx new file mode 100644 index 0000000000..31221105e1 --- /dev/null +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/UpscaleTabGenerationSettingsAccordion.tsx @@ -0,0 +1,92 @@ +import type { FormLabelProps } from '@invoke-ai/ui-library'; +import { Box, Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-library'; +import { EMPTY_ARRAY } from 'app/store/constants'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectLoRAsSlice } from 'features/controlLayers/store/lorasSlice'; +import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice'; +import { LoRAList } from 'features/lora/components/LoRAList'; +import LoRASelect from 'features/lora/components/LoRASelect'; +import ParamGuidance from 'features/parameters/components/Core/ParamGuidance'; +import ParamSteps from 'features/parameters/components/Core/ParamSteps'; +import { DisabledModelWarning } from 'features/parameters/components/MainModel/DisabledModelWarning'; +import ParamUpscaleCFGScale from 'features/parameters/components/Upscale/ParamUpscaleCFGScale'; +import ParamUpscaleScheduler from 'features/parameters/components/Upscale/ParamUpscaleScheduler'; +import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel'; +import { API_BASE_MODELS } from 'features/parameters/types/constants'; +import { MainModelPicker } from 'features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker'; +import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle'; +import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig'; +import { isFluxFillMainModelModelConfig } from 'services/api/types'; + +const formLabelProps: FormLabelProps = { + minW: '4rem', +}; + +export const UpscaleTabGenerationSettingsAccordion = memo(() => { + const { t } = useTranslation(); + const modelConfig = useSelectedModelConfig(); + const isFLUX = useAppSelector(selectIsFLUX); + + const isApiModel = useIsApiModel(); + + const selectBadges = useMemo( + () => + createMemoizedSelector(selectLoRAsSlice, (loras) => { + const enabledLoRAsCount = loras.loras.filter((l) => l.isEnabled).length; + const loraTabBadges = enabledLoRAsCount ? [`${enabledLoRAsCount} ${t('models.concepts')}`] : EMPTY_ARRAY; + const accordionBadges = + modelConfig && API_BASE_MODELS.includes(modelConfig.base) + ? [modelConfig.name] + : modelConfig + ? [modelConfig.name, modelConfig.base] + : EMPTY_ARRAY; + return { loraTabBadges, accordionBadges }; + }), + [modelConfig, t] + ); + const { loraTabBadges, accordionBadges } = useAppSelector(selectBadges); + const { isOpen: isOpenExpander, onToggle: onToggleExpander } = useExpanderToggle({ + id: 'generation-settings-advanced', + defaultIsOpen: false, + }); + const { isOpen: isOpenAccordion, onToggle: onToggleAccordion } = useStandaloneAccordionToggle({ + id: `generation-settings-upscaling`, + defaultIsOpen: false, + }); + + return ( + + + + + + {!isApiModel && } + {!isApiModel && } + + {!isApiModel && ( + + + + + + {isFLUX && modelConfig && !isFluxFillMainModelModelConfig(modelConfig) && } + + + + + )} + + + ); +}); + +UpscaleTabGenerationSettingsAccordion.displayName = 'UpscaleTabGenerationSettingsAccordion'; diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx index 199e5d8c43..74bcdc6119 100644 --- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx +++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx @@ -1,15 +1,16 @@ -import { Flex } from '@invoke-ai/ui-library'; +import 'dockview/dist/styles/dockview.css'; +import 'features/ui/styles/dockview-theme-invoke.css'; + +import { TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { useDndMonitor } from 'features/dnd/useDndMonitor'; import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; -import { FloatingLeftPanelButtons } from 'features/ui/components/FloatingLeftPanelButtons'; -import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightPanelButtons'; -import { LeftPanelContent } from 'features/ui/components/LeftPanelContent'; -import { MainPanelContent } from 'features/ui/components/MainPanelContent'; -import { RightPanelContent } from 'features/ui/components/RightPanelContent'; import { VerticalNavBar } from 'features/ui/components/VerticalNavBar'; import type { UsePanelOptions } from 'features/ui/hooks/usePanel'; import { usePanel } from 'features/ui/hooks/usePanel'; +import { CanvasTabAutoLayout } from 'features/ui/layouts/canvas-tab-auto-layout'; +import { GenerateTabAutoLayout } from 'features/ui/layouts/generate-tab-auto-layout'; +import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { $isLeftPanelOpen, $isRightPanelOpen, @@ -21,9 +22,6 @@ import { import type { CSSProperties } from 'react'; import { memo, useMemo, useRef } from 'react'; import type { ImperativePanelGroupHandle } from 'react-resizable-panels'; -import { Panel, PanelGroup } from 'react-resizable-panels'; - -import { VerticalResizeHandle } from './tabs/ResizeHandle'; const panelStyles: CSSProperties = { position: 'relative', height: '100%', width: '100%', minWidth: 0 }; @@ -31,6 +29,7 @@ const onLeftPanelCollapse = (isCollapsed: boolean) => $isLeftPanelOpen.set(!isCo const onRightPanelCollapse = (isCollapsed: boolean) => $isRightPanelOpen.set(!isCollapsed); export const AppContent = memo(() => { + const tab = useAppSelector(selectActiveTab); const imperativePanelGroupRef = useRef(null); useDndMonitor(); @@ -108,38 +107,19 @@ export const AppContent = memo(() => { }); return ( - - - - {withLeftPanel && ( - <> - - - - - - )} - - - {withLeftPanel && } - {withRightPanel && } - - {withRightPanel && ( - <> - - - - - - )} - - + + + + + + + + + + + + + ); }); AppContent.displayName = 'AppContent'; diff --git a/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx index a603d540e7..e30de8bf17 100644 --- a/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx +++ b/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx @@ -12,7 +12,7 @@ export const LeftPanelContent = memo(() => { const tab = useAppSelector(selectActiveTab); return ( - + {tab === 'generate' && } diff --git a/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx index 58010ba503..ec5e41deff 100644 --- a/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx +++ b/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx @@ -1,27 +1,22 @@ import { useAppSelector } from 'app/store/storeHooks'; import { AdvancedSession } from 'features/controlLayers/components/AdvancedSession/AdvancedSession'; import { SimpleSession } from 'features/controlLayers/components/SimpleSession/SimpleSession'; -import { selectCanvasSessionId, selectGenerateSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer'; import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab'; import QueueTab from 'features/ui/components/tabs/QueueTab'; import { WorkflowsMainPanel } from 'features/ui/components/tabs/WorkflowsTabContent'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; -import { atom } from 'nanostores'; import { memo } from 'react'; import type { Equals } from 'tsafe'; import { assert } from 'tsafe'; -export const $simpleId = atom(null); -export const $advancedId = atom(null); - export const MainPanelContent = memo(() => { const tab = useAppSelector(selectActiveTab); - const generateId = useAppSelector(selectGenerateSessionId); const canvasId = useAppSelector(selectCanvasSessionId); if (tab === 'generate') { - return ; + return ; } if (tab === 'canvas') { return ; diff --git a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx index 8974a9c326..1a8850f309 100644 --- a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx +++ b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx @@ -1,5 +1,5 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; -import { IconButton, Tooltip } from '@invoke-ai/ui-library'; +import { IconButton, Tab, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; @@ -25,7 +25,8 @@ export const TabButton = memo(({ tab, icon, label }: { tab: TabName; icon: React return ( - { const customNavComponent = useStore($customNavComponent); return ( - + diff --git a/invokeai/frontend/web/src/features/ui/layouts/AutoLayout.tsx b/invokeai/frontend/web/src/features/ui/layouts/AutoLayout.tsx new file mode 100644 index 0000000000..a1feebb13f --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/AutoLayout.tsx @@ -0,0 +1,45 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import type { GridviewApi, IGridviewReactProps } from 'dockview'; +import { GridviewReact, Orientation } from 'dockview'; +import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context'; +import { canvasTabComponents, initializeCanvasTabLayout } from 'features/ui/layouts/canvas-tab-auto-layout'; +import { generateTabComponents, initializeGenerateTabLayout } from 'features/ui/layouts/generate-tab-auto-layout'; +import { selectActiveTab } from 'features/ui/store/uiSelectors'; +import type { TabName } from 'features/ui/store/uiTypes'; +import { memo, useCallback, useEffect, useState } from 'react'; + +const components: IGridviewReactProps['components'] = { + ...generateTabComponents, + ...canvasTabComponents, +}; + +export const AutoLayout = memo(() => { + const tab = useAppSelector(selectActiveTab); + const [api, setApi] = useState(null); + const syncLayout = useCallback((tab: TabName, api: GridviewApi) => { + if (tab === 'generate') { + initializeGenerateTabLayout(api); + } else if (tab === 'canvas') { + initializeCanvasTabLayout(api); + } + }, []); + const onReady = useCallback((event) => { + setApi(event.api); + }, []); + useEffect(() => { + if (api) { + syncLayout(tab, api); + } + }, [api, syncLayout, tab]); + return ( + + + + ); +}); +AutoLayout.displayName = 'AutoLayout'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx b/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx new file mode 100644 index 0000000000..77e469a462 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx @@ -0,0 +1,35 @@ +import { Flex, Text } from '@invoke-ai/ui-library'; +import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter'; +import type { IDockviewPanelHeaderProps } from 'dockview'; +import { useCallback, useEffect, useId, useRef } from 'react'; + +export const TabWithoutCloseButton = (props: IDockviewPanelHeaderProps) => { + const id = useId(); + const ref = useRef(null); + const setActive = useCallback(() => { + if (!props.api.isActive) { + props.api.setActive(); + } + }, [props.api]); + + useCallbackOnDragEnter(setActive, ref, 300); + + useEffect(() => { + const el = document.querySelector(`[data-id="${id}"]`); + if (!el) { + return; + } + const parentTab = el.closest('.dv-tab'); + if (!parentTab) { + return; + } + parentTab.setAttribute('draggable', 'false'); + }, [id]); + + return ( + + {props.api.title ?? props.api.id} + + ); +}; +TabWithoutCloseButton.displayName = 'TabWithoutCloseButton'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx new file mode 100644 index 0000000000..d86ae4bee1 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx @@ -0,0 +1,14 @@ +import type { GridviewApi } from 'dockview'; +import type { PropsWithChildren } from 'react'; +import { createContext, useContext } from 'react'; + +const AutoLayoutContext = createContext(null); + +export const AutoLayoutProvider = (props: PropsWithChildren<{ api: GridviewApi | null }>) => { + return {props.children}; +}; + +export const useAutoLayoutContext = () => { + const api = useContext(AutoLayoutContext); + return api; +}; diff --git a/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx new file mode 100644 index 0000000000..43ea4dc4e1 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx @@ -0,0 +1,339 @@ +import { Box, ContextMenu, Divider, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview'; +import { DockviewReact, GridviewReact, Orientation } from 'dockview'; +import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress'; +import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask'; +import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus'; +import { CanvasContextMenuGlobalMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems'; +import { CanvasContextMenuSelectedEntityMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems'; +import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea'; +import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent'; +import { Filter } from 'features/controlLayers/components/Filters/Filter'; +import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD'; +import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent'; +import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject'; +import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context'; +import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState'; +import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList'; +import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar'; +import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar'; +import { Transform } from 'features/controlLayers/components/Transform/Transform'; +import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent'; +import { Gallery } from 'features/gallery/components/Gallery'; +import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2'; +import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2'; +import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2'; +import QueueControls from 'features/queue/components/QueueControls'; +import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage'; +import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context'; +import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton'; +import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice'; +import { dockviewTheme } from 'features/ui/styles/theme'; +import { memo, useCallback, useState } from 'react'; +import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi'; + +const MenuContent = memo(() => { + return ( + + + + + + + ); +}); +MenuContent.displayName = 'MenuContent'; + +const canvasBgSx = { + position: 'relative', + w: 'full', + h: 'full', + borderRadius: 'base', + overflow: 'hidden', + bg: 'base.900', + '&[data-dynamic-grid="true"]': { + bg: 'base.850', + }, +}; + +export const CanvasPanel = memo(() => { + const dynamicGrid = useAppSelector(selectDynamicGrid); + const showHUD = useAppSelector(selectShowHUD); + const canvasId = useAppSelector(selectCanvasSessionId); + + const renderMenu = useCallback(() => { + return ; + }, []); + + return ( + + + + + + renderMenu={renderMenu} withLongPress={false}> + {(ref) => ( + + + + + {showHUD && } + + + + + + + } colorScheme="base" /> + + + + + + )} + + {canvasId !== null && ( + + + + + + + + + + + + + )} + + + + + + + + + + + + ); +}); +CanvasPanel.displayName = 'CanvasPanel'; + +const LayersPanelContent = memo(() => ( + + + +)); +LayersPanelContent.displayName = 'LayersPanelContent'; + +const ViewerPanelContent = memo(() => ( + + + + + +)); +ViewerPanelContent.displayName = 'ViewerPanelContent'; + +const ProgressPanelContent = memo(() => ( + + + +)); +ProgressPanelContent.displayName = 'ProgressPanelContent'; + +const mainPanelComponents: IDockviewReactProps['components'] = { + welcome: InitialState, + canvas: CanvasPanel, + viewer: ViewerPanelContent, + progress: ProgressPanelContent, +}; + +const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => { + const { api } = event; + api.addPanel({ + id: 'welcome', + component: 'welcome', + title: 'Launchpad', + }); + api.addPanel({ + id: 'canvas', + component: 'canvas', + title: 'Canvas', + position: { + direction: 'within', + referencePanel: 'welcome', + }, + }); + api.addPanel({ + id: 'viewer', + component: 'viewer', + title: 'Image Viewer', + position: { + direction: 'within', + referencePanel: 'welcome', + }, + }); + api.addPanel({ + id: 'progress', + component: 'progress', + title: 'Generation Progress', + position: { + direction: 'within', + referencePanel: 'welcome', + }, + }); + + const disposables = [ + api.onWillShowOverlay((e) => { + if (e.kind === 'header_space' || e.kind === 'tab') { + return; + } + e.preventDefault(); + }), + ]; + + return () => { + disposables.forEach((disposable) => { + disposable.dispose(); + }); + }; +}; + +const MainPanel = memo(() => { + return ( + + + + ); +}); +MainPanel.displayName = 'MainPanel'; + +const Left = memo(() => { + return ( + + + + + + + ); +}); +Left.displayName = 'Left'; + +export const canvasTabComponents: IGridviewReactProps['components'] = { + left: Left, + main: MainPanel, + boards: BoardsListPanelContent, + gallery: Gallery, + layers: LayersPanelContent, +}; + +export const initializeCanvasTabLayout = (api: GridviewApi) => { + const main = api.addPanel({ + id: 'main', + component: 'main', + minimumWidth: 256, + }); + const left = api.addPanel({ + id: 'left', + component: 'left', + minimumWidth: LEFT_PANEL_MIN_SIZE_PX, + position: { + direction: 'left', + referencePanel: 'main', + }, + }); + api.addPanel({ + id: 'gallery', + component: 'gallery', + minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, + minimumHeight: 232, + position: { + direction: 'right', + referencePanel: 'main', + }, + }); + api.addPanel({ + id: 'layers', + component: 'layers', + minimumHeight: 256, + position: { + direction: 'below', + referencePanel: 'gallery', + }, + }); + const boards = api.addPanel({ + id: 'boards', + component: 'boards', + minimumHeight: 36, + position: { + direction: 'above', + referencePanel: 'gallery', + }, + }); + left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX }); + boards.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX }); +}; + +export const CanvasTabAutoLayout = memo(() => { + const [api, setApi] = useState(null); + const onReady = useCallback((event) => { + setApi(event.api); + initializeCanvasTabLayout(event.api); + }, []); + return ( + + + + ); +}); +CanvasTabAutoLayout.displayName = 'CanvasTabAutoLayout'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx new file mode 100644 index 0000000000..f97a404652 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx @@ -0,0 +1,175 @@ +import { Box, Divider, Flex } from '@invoke-ai/ui-library'; +import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview'; +import { DockviewReact, GridviewReact, Orientation } from 'dockview'; +import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState'; +import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent'; +import { Gallery } from 'features/gallery/components/Gallery'; +import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2'; +import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2'; +import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2'; +import QueueControls from 'features/queue/components/QueueControls'; +import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage'; +import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context'; +import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton'; +import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice'; +import { dockviewTheme } from 'features/ui/styles/theme'; +import { memo, useCallback, useState } from 'react'; + +const ViewerPanelContent = memo(() => ( + + + + + +)); +ViewerPanelContent.displayName = 'ViewerPanelContent'; + +const ProgressPanelContent = memo(() => ( + + + +)); +ProgressPanelContent.displayName = 'ProgressPanelContent'; + +const mainPanelComponents: IDockviewReactProps['components'] = { + welcome: InitialState, + viewer: ViewerPanelContent, + progress: ProgressPanelContent, +}; + +const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => { + const { api } = event; + api.addPanel({ + id: 'welcome', + component: 'welcome', + title: 'Launchpad', + }); + api.addPanel({ + id: 'viewer', + component: 'viewer', + title: 'Image Viewer', + position: { + direction: 'within', + referencePanel: 'welcome', + }, + }); + api.addPanel({ + id: 'progress', + component: 'progress', + title: 'Generation Progress', + position: { + direction: 'within', + referencePanel: 'welcome', + }, + }); + + const disposables = [ + api.onWillShowOverlay((e) => { + if (e.kind === 'header_space' || e.kind === 'tab') { + return; + } + e.preventDefault(); + }), + ]; + + return () => { + disposables.forEach((disposable) => { + disposable.dispose(); + }); + }; +}; + +const MainPanel = memo(() => { + return ( + + + + ); +}); +MainPanel.displayName = 'MainPanel'; + +const Left = memo(() => { + return ( + + + + + + + ); +}); +Left.displayName = 'Left'; + +export const generateTabComponents: IGridviewReactProps['components'] = { + left: Left, + main: MainPanel, + boards: BoardsListPanelContent, + gallery: Gallery, +}; + +export const initializeGenerateTabLayout = (api: GridviewApi) => { + const main = api.addPanel({ + id: 'main', + component: 'main', + minimumWidth: 256, + }); + const left = api.addPanel({ + id: 'left', + component: 'left', + minimumWidth: LEFT_PANEL_MIN_SIZE_PX, + position: { + direction: 'left', + referencePanel: 'main', + }, + }); + api.addPanel({ + id: 'gallery', + component: 'gallery', + minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, + minimumHeight: 232, + position: { + direction: 'right', + referencePanel: 'main', + }, + }); + const boards = api.addPanel({ + id: 'boards', + component: 'boards', + minimumHeight: 36, + position: { + direction: 'above', + referencePanel: 'gallery', + }, + }); + left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX }); + boards.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX }); +}; + +export const GenerateTabAutoLayout = memo(() => { + const [api, setApi] = useState(null); + const onReady = useCallback((event) => { + console.log('GenerateTabAutoLayout onReady'); + setApi(event.api); + initializeGenerateTabLayout(event.api); + }, []); + return ( + + + + ); +}); +GenerateTabAutoLayout.displayName = 'GenerateTabAutoLayout'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts b/invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts new file mode 100644 index 0000000000..7d6bd82989 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts @@ -0,0 +1,98 @@ +import type { GridviewApi, GridviewPanelApi, IGridviewPanel } from 'dockview'; +import { atom } from 'nanostores'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +const getIsCollapsed = ( + panel: IGridviewPanel, + orientation: 'vertical' | 'horizontal', + collapsedSize?: number +) => { + if (orientation === 'vertical') { + return panel.height <= (collapsedSize ?? panel.minimumHeight); + } + return panel.width <= (collapsedSize ?? panel.minimumWidth); +}; + +export const useCollapsibleGridviewPanel = ( + api: GridviewApi | null, + panelId: string, + orientation: 'horizontal' | 'vertical', + defaultSize: number, + collapsedSize?: number +) => { + const $isCollapsed = useState(() => atom(false))[0]; + const collapse = useCallback(() => { + if (!api) { + return; + } + const panel = api.getPanel(panelId); + if (!panel) { + return; + } + if (orientation === 'vertical') { + panel.api.setSize({ height: collapsedSize ?? panel.minimumHeight }); + } else { + panel.api.setSize({ width: collapsedSize ?? panel.minimumWidth }); + } + }, [api, collapsedSize, orientation, panelId]); + + const expand = useCallback(() => { + if (!api) { + return; + } + const panel = api.getPanel(panelId); + if (!panel) { + return; + } + if (orientation === 'vertical') { + panel.api.setSize({ height: defaultSize }); + } else { + panel.api.setSize({ width: defaultSize }); + } + }, [api, defaultSize, orientation, panelId]); + + const toggle = useCallback(() => { + if (!api) { + return; + } + const panel = api.getPanel(panelId); + if (!panel) { + return; + } + const isCollapsed = getIsCollapsed(panel, orientation, collapsedSize); + if (isCollapsed) { + expand(); + } else { + collapse(); + } + }, [api, panelId, orientation, collapsedSize, expand, collapse]); + + useEffect(() => { + if (!api) { + return; + } + const panel = api.getPanel(panelId); + if (!panel) { + return; + } + + const disposable = panel.api.onDidDimensionsChange(() => { + const isCollapsed = getIsCollapsed(panel, orientation, collapsedSize); + $isCollapsed.set(isCollapsed); + }); + + return () => { + disposable.dispose(); + }; + }, [$isCollapsed, api, collapsedSize, orientation, panelId]); + + return useMemo( + () => ({ + $isCollapsed, + expand, + collapse, + toggle, + }), + [$isCollapsed, collapse, expand, toggle] + ); +}; diff --git a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts index 3990c28aa2..4b11ca8d6a 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts @@ -5,3 +5,5 @@ export const selectActiveTab = createSelector(selectUiSlice, (ui) => ui.activeTa export const selectShouldShowImageDetails = createSelector(selectUiSlice, (ui) => ui.shouldShowImageDetails); export const selectShouldShowProgressInViewer = createSelector(selectUiSlice, (ui) => ui.shouldShowProgressInViewer); export const selectActiveTabCanvasRightPanel = createSelector(selectUiSlice, (ui) => ui.activeTabCanvasRightPanel); +export const selectShowGenerateTabSplashScreen = createSelector(selectUiSlice, (ui) => ui.showGenerateTabSplashScreen); +export const selectShowCanvasTabSplashScreen = createSelector(selectUiSlice, (ui) => ui.showCanvasTabSplashScreen); diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index c96be420bb..2637b58d89 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -123,11 +123,11 @@ export const uiPersistConfig: PersistConfig = { }; const TABS_WITH_LEFT_PANEL: TabName[] = ['canvas', 'upscaling', 'workflows', 'generate'] as const; -export const LEFT_PANEL_MIN_SIZE_PX = 400; +export const LEFT_PANEL_MIN_SIZE_PX = 420; export const $isLeftPanelOpen = atom(true); export const selectWithLeftPanel = createSelector(selectUiSlice, (ui) => TABS_WITH_LEFT_PANEL.includes(ui.activeTab)); const TABS_WITH_RIGHT_PANEL: TabName[] = ['canvas', 'upscaling', 'workflows', 'generate'] as const; -export const RIGHT_PANEL_MIN_SIZE_PX = 390; +export const RIGHT_PANEL_MIN_SIZE_PX = 420; export const $isRightPanelOpen = atom(true); export const selectWithRightPanel = createSelector(selectUiSlice, (ui) => TABS_WITH_RIGHT_PANEL.includes(ui.activeTab)); diff --git a/invokeai/frontend/web/src/features/ui/styles/dockview-theme-invoke.css b/invokeai/frontend/web/src/features/ui/styles/dockview-theme-invoke.css new file mode 100644 index 0000000000..06a1e99c58 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/styles/dockview-theme-invoke.css @@ -0,0 +1,65 @@ +.dockview-theme-invoke { + --dv-paneview-active-outline-color: var(--invoke-colors-invokeBlue-300); + --dv-tabs-and-actions-container-font-size: var(--invoke-fontSizes-sm); + --dv-tabs-and-actions-container-height: var(--invoke-sizes-8); + --dv-drag-over-background-color: var(--invoke-colors-baseAlpha-400); + --dv-drag-over-border-color: var(--invoke-colors-base-300); + --dv-tabs-container-scrollbar-color: #888; + --dv-icon-hover-background-color: rgba(90, 93, 94, 0.31); + --dv-floating-box-shadow: none; + --dv-overlay-z-index: 999; + + --dv-tab-font-size: inherit; + --dv-border-radius: 0; + --dv-tab-margin: 0; + --dv-sash-color: transparent; + --dv-active-sash-color: var(--invoke-colors-base-700); + --dv-active-sash-transition-duration: 0.15s; + --dv-active-sash-transition-delay: 0.1s; + + --dv-group-view-background-color: var(--invoke-colors-base-900); + + --dv-tabs-and-actions-container-background-color: var(--invoke-colors-base-850); + + --dv-activegroup-visiblepanel-tab-color: var(--invoke-colors-base-50); + --dv-activegroup-visiblepanel-tab-background-color: var(--invoke-colors-base-700); + + --dv-activegroup-hiddenpanel-tab-color: var(--invoke-colors-base-300); + --dv-activegroup-hiddenpanel-tab-background-color: var(--invoke-colors-base-850); + + --dv-inactivegroup-visiblepanel-tab-color: var(--invoke-colors-base-500); + --dv-inactivegroup-visiblepanel-tab-background-color: var(--invoke-colors-base-800); + + --dv-inactivegroup-hiddenpanel-tab-color: var(--invoke-colors-base-600); + --dv-inactivegroup-hiddenpanel-tab-background-color: var(--invoke-colors-base-850); + + --dv-tab-divider-color: var(--invoke-colors-base-700); + --dv-inactivegroup-tab-divider-color: var(--invoke-colors-base-800); + + --dv-separator-border: var(--invoke-colors-base-750); + --dv-paneview-header-border-color: rgba(204, 204, 204, 0.2); +} + +.dv-default-tab-content { + margin-right: 0px !important; +} + +.dv-groupview-floating { + border-radius: var(--invoke-space-2); + border-width: 1px; + border-color: var(--invoke-colors-base-800); + filter: drop-shadow(0px 0px 3px rgba(0, 0, 0, 0.4)) drop-shadow(5px 5px 10px rgba(0, 0, 0, 0.6)); +} + +.dv-resize-container { + border: none; +} + +.dv-tab { + /* margin-right: 2px; */ +} + +.dv-inactive-group .dv-tabs-container.dv-horizontal .dv-tab:not(:first-child)::before { + /* this is the tab divider */ + background-color: var(--dv-inactivegroup-tab-divider-color); +} diff --git a/invokeai/frontend/web/src/features/ui/styles/theme.ts b/invokeai/frontend/web/src/features/ui/styles/theme.ts new file mode 100644 index 0000000000..032bc48e34 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/styles/theme.ts @@ -0,0 +1,6 @@ +import type { DockviewTheme } from 'dockview'; + +export const dockviewTheme: DockviewTheme = { + name: 'invoke', + className: 'dockview-theme-invoke', +};