feat(ui): wip resizable pinnable drawer

fix(ui): remove old scrollbar css

fix(ui): make guidepopover lazy

feat(ui): wip resizable drawer

feat(ui): wip resizable drawer

feat(ui): add scroll-linked shadow

feat(ui): organize files

Align Scrollbar next to content

Move resizable drawer underneath the progress bar

Add InvokeLogo to unpinned & align

Adds Invoke Logo to Unpinned Parameters panel and aligns to make it feel seamless.
This commit is contained in:
psychedelicious 2023-03-07 00:23:16 +11:00
parent 1aaad9336f
commit d81088dff7
67 changed files with 904 additions and 495 deletions

View File

@ -35,6 +35,7 @@ module.exports = {
{ varsIgnorePattern: '^_', argsIgnorePattern: '^_' },
],
'prettier/prettier': ['error', { endOfLine: 'auto' }],
'@typescript-eslint/ban-ts-comment': 'warn',
},
settings: {
react: {

View File

@ -52,6 +52,8 @@
"i18next-http-backend": "^2.1.1",
"konva": "^8.4.2",
"lodash": "^4.17.21",
"overlayscrollbars": "^2.1.0",
"overlayscrollbars-react": "^0.5.0",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
"react-colorful": "^5.6.1",

View File

@ -9,7 +9,7 @@ import useToastWatcher from 'features/system/hooks/useToastWatcher';
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
import { Box, Grid } from '@chakra-ui/react';
import { Box, Grid, Portal } from '@chakra-ui/react';
import { APP_HEIGHT, APP_PADDING, APP_WIDTH } from 'theme/util/constants';
keepGUIAlive();
@ -18,26 +18,32 @@ const App = () => {
useToastWatcher();
return (
<Grid w="100vw" h="100vh">
<ImageUploader>
<ProgressBar />
<Grid
gap={4}
p={APP_PADDING}
gridAutoRows="min-content auto"
w={APP_WIDTH}
h={APP_HEIGHT}
>
<SiteHeader />
<InvokeTabs />
</Grid>
<Box>
<Console />
</Box>
</ImageUploader>
<FloatingParametersPanelButtons />
<FloatingGalleryButton />
</Grid>
<>
<Grid w="100vw" h="100vh">
<ImageUploader>
<ProgressBar />
<Grid
gap={4}
p={APP_PADDING}
gridAutoRows="min-content auto"
w={APP_WIDTH}
h={APP_HEIGHT}
>
<SiteHeader />
<InvokeTabs />
</Grid>
<Box>
<Console />
</Box>
</ImageUploader>
<Portal>
<FloatingParametersPanelButtons />
</Portal>
<Portal>
<FloatingGalleryButton />
</Portal>
</Grid>
</>
);
};

View File

@ -9,6 +9,17 @@ import { greenTeaThemeColors } from 'theme/colors/greenTea';
import { invokeAIThemeColors } from 'theme/colors/invokeAI';
import { lightThemeColors } from 'theme/colors/lightTheme';
import { oceanBlueColors } from 'theme/colors/oceanBlue';
import '@fontsource/inter/100.css';
import '@fontsource/inter/200.css';
import '@fontsource/inter/300.css';
import '@fontsource/inter/400.css';
import '@fontsource/inter/500.css';
import '@fontsource/inter/600.css';
import '@fontsource/inter/700.css';
import '@fontsource/inter/800.css';
import '@fontsource/inter/900.css';
import 'overlayscrollbars/overlayscrollbars.css';
import 'theme/overlayscrollbar.css';
type ThemeLocaleProviderProps = {
children: ReactNode;

View File

@ -30,7 +30,7 @@ const GuidePopover = ({ children, feature }: GuideProps) => {
if (!shouldDisplayGuides) return null;
return (
<Popover trigger="hover">
<Popover trigger="hover" isLazy>
<PopoverTrigger>
<Box>{children}</Box>
</PopoverTrigger>

View File

@ -59,6 +59,11 @@ const ParametersAccordion = (props: ParametersAccordionsType) => {
allowMultiple
reduceMotion
onChange={handleChangeAccordionState}
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
}}
>
{renderAccordions()}
</Accordion>

View File

@ -28,7 +28,6 @@ export const floatingSelector = createSelector(
const {
shouldPinParametersPanel,
shouldShowParametersPanel,
shouldHoldParametersPanelOpen,
shouldUseCanvasBetaLayout,
} = ui;
@ -40,10 +39,7 @@ export const floatingSelector = createSelector(
const shouldShowParametersPanelButton =
!canvasBetaLayoutCheck &&
!(
shouldShowParametersPanel ||
(shouldHoldParametersPanelOpen && !shouldPinParametersPanel)
) &&
!shouldPinParametersPanel &&
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName);
const shouldShowGalleryButton =
@ -51,8 +47,7 @@ export const floatingSelector = createSelector(
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName);
const shouldShowProcessButtons =
!canvasBetaLayoutCheck &&
(!shouldPinParametersPanel || !shouldShowParametersPanel);
!canvasBetaLayoutCheck && !shouldPinParametersPanel;
return {
shouldPinParametersPanel,

View File

@ -1,18 +0,0 @@
import { Image } from '@chakra-ui/react';
import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks';
export default function InitialImageOverlay() {
const initialImage = useAppSelector(
(state: RootState) => state.generation.initialImage
);
return initialImage ? (
<Image
fit="contain"
src={typeof initialImage === 'string' ? initialImage : initialImage.url}
rounded="md"
className="checkerboard"
/>
) : null;
}

View File

@ -1,11 +0,0 @@
import InvokeWorkarea from 'features/ui/components/InvokeWorkarea';
import ImageToImageDisplay from './ImageToImageDisplay';
import ImageToImagePanel from './ImageToImagePanel';
export default function ImageToImageWorkarea() {
return (
<InvokeWorkarea optionsPanel={<ImageToImagePanel />}>
<ImageToImageDisplay />
</InvokeWorkarea>
);
}

View File

@ -1,35 +0,0 @@
.ltr-parameters-panel-transition-enter {
transform: translateX(-150%);
}
.ltr-parameters-panel-transition-enter-active {
transform: translateX(0);
transition: all 120ms ease-out;
}
.ltr-parameters-panel-transition-exit {
transform: translateX(0);
}
.ltr-parameters-panel-transition-exit-active {
transform: translateX(-150%);
transition: all 120ms ease-out;
}
.rtl-parameters-panel-transition-enter {
transform: translateX(150%);
}
.rtl-parameters-panel-transition-enter-active {
transform: translateX(0);
transition: all 120ms ease-out;
}
.rtl-parameters-panel-transition-exit {
transform: translateX(0);
}
.rtl-parameters-panel-transition-exit-active {
transform: translateX(150%);
transition: all 120ms ease-out;
}

View File

@ -1,249 +0,0 @@
import { Box, Flex, Tooltip, Icon, useTheme } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import {
setShouldHoldParametersPanelOpen,
setShouldPinParametersPanel,
setShouldShowParametersPanel,
} from 'features/ui/store/uiSlice';
import React, { ReactNode, useCallback, useEffect, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
import { CSSTransition } from 'react-transition-group';
import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice';
import { setParametersPanelScrollPosition } from 'features/ui/store/uiSlice';
import { isEqual } from 'lodash';
import { uiSelector } from '../store/uiSelectors';
import { useTranslation } from 'react-i18next';
import {
APP_CONTENT_HEIGHT,
OPTIONS_BAR_MAX_WIDTH,
PROGRESS_BAR_THICKNESS,
} from 'theme/util/constants';
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
import './InvokeParametersPanel.css';
import { no_scrollbar } from 'theme/components/scrollbar';
type Props = { children: ReactNode };
const optionsPanelSelector = createSelector(
uiSelector,
(ui) => {
const {
shouldShowParametersPanel,
shouldHoldParametersPanelOpen,
shouldPinParametersPanel,
parametersPanelScrollPosition,
} = ui;
return {
shouldShowParametersPanel,
shouldHoldParametersPanelOpen,
shouldPinParametersPanel,
parametersPanelScrollPosition,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
const InvokeOptionsPanel = (props: Props) => {
const dispatch = useAppDispatch();
const { direction } = useTheme();
const {
shouldShowParametersPanel,
shouldHoldParametersPanelOpen,
shouldPinParametersPanel,
} = useAppSelector(optionsPanelSelector);
const optionsPanelRef = useRef<HTMLDivElement>(null);
const optionsPanelContainerRef = useRef<HTMLDivElement>(null);
const timeoutIdRef = useRef<number | null>(null);
const { children } = props;
const { t } = useTranslation();
// Hotkeys
useHotkeys(
'o',
() => {
dispatch(setShouldShowParametersPanel(!shouldShowParametersPanel));
shouldPinParametersPanel &&
setTimeout(() => dispatch(setDoesCanvasNeedScaling(true)), 400);
},
[shouldShowParametersPanel, shouldPinParametersPanel]
);
useHotkeys(
'esc',
() => {
dispatch(setShouldShowParametersPanel(false));
},
{
enabled: () => !shouldPinParametersPanel,
preventDefault: true,
},
[shouldPinParametersPanel]
);
useHotkeys(
'shift+o',
() => {
handleClickPinOptionsPanel();
dispatch(setDoesCanvasNeedScaling(true));
},
[shouldPinParametersPanel]
);
const handleCloseOptionsPanel = useCallback(() => {
if (shouldPinParametersPanel) return;
dispatch(
setParametersPanelScrollPosition(
optionsPanelContainerRef.current
? optionsPanelContainerRef.current.scrollTop
: 0
)
);
dispatch(setShouldShowParametersPanel(false));
dispatch(setShouldHoldParametersPanelOpen(false));
}, [dispatch, shouldPinParametersPanel]);
const setCloseOptionsPanelTimer = () => {
timeoutIdRef.current = window.setTimeout(
() => handleCloseOptionsPanel(),
500
);
};
const cancelCloseOptionsPanelTimer = () => {
timeoutIdRef.current && window.clearTimeout(timeoutIdRef.current);
};
const handleClickPinOptionsPanel = () => {
dispatch(setShouldPinParametersPanel(!shouldPinParametersPanel));
dispatch(setDoesCanvasNeedScaling(true));
};
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (
optionsPanelRef.current &&
!optionsPanelRef.current.contains(e.target as Node)
) {
handleCloseOptionsPanel();
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [handleCloseOptionsPanel]);
return (
<CSSTransition
nodeRef={optionsPanelRef}
in={
shouldShowParametersPanel ||
(shouldHoldParametersPanelOpen && !shouldPinParametersPanel)
}
unmountOnExit
timeout={200}
classNames={`${direction}-parameters-panel-transition`}
>
<Box
className={`${direction}-parameters-panel-transition`}
tabIndex={1}
ref={optionsPanelRef}
onMouseEnter={
!shouldPinParametersPanel ? cancelCloseOptionsPanelTimer : undefined
}
onMouseOver={
!shouldPinParametersPanel ? cancelCloseOptionsPanelTimer : undefined
}
sx={{
borderInlineEndWidth: !shouldPinParametersPanel ? 5 : 0,
borderInlineEndStyle: 'solid',
bg: 'base.900',
borderColor: 'base.700',
height: APP_CONTENT_HEIGHT,
width: OPTIONS_BAR_MAX_WIDTH,
maxWidth: OPTIONS_BAR_MAX_WIDTH,
flexShrink: 0,
position: 'relative',
overflowY: 'scroll',
overflowX: 'hidden',
...no_scrollbar,
...(!shouldPinParametersPanel && {
zIndex: 20,
position: 'fixed',
top: 0,
insetInlineStart: 0,
width: `calc(${OPTIONS_BAR_MAX_WIDTH} + 2rem)`,
maxWidth: `calc(${OPTIONS_BAR_MAX_WIDTH} + 2rem)`,
height: '100%',
}),
}}
>
<Box sx={{ margin: !shouldPinParametersPanel && 4 }}>
<Flex
ref={optionsPanelContainerRef}
onMouseLeave={(e: React.MouseEvent<HTMLDivElement>) => {
if (e.target !== optionsPanelContainerRef.current) {
cancelCloseOptionsPanelTimer();
} else {
!shouldPinParametersPanel && setCloseOptionsPanelTimer();
}
}}
sx={{
display: 'flex',
flexDirection: 'column',
rowGap: 2,
height: '100%',
}}
>
<Tooltip label={t('common.pinOptionsPanel')}>
<Box
onClick={handleClickPinOptionsPanel}
sx={{
position: 'absolute',
cursor: 'pointer',
padding: 2,
top: 4,
insetInlineEnd: 4,
zIndex: 20,
...(shouldPinParametersPanel && {
top: 0,
insetInlineEnd: 0,
}),
}}
>
<Icon
sx={{ opacity: 0.2 }}
as={shouldPinParametersPanel ? BsPinAngleFill : BsPinAngle}
/>
</Box>
</Tooltip>
{!shouldPinParametersPanel && (
<Box sx={{ pt: PROGRESS_BAR_THICKNESS, pb: 2 }}>
<InvokeAILogoComponent />
</Box>
)}
{children}
</Flex>
</Box>
</Box>
</CSSTransition>
);
};
export default InvokeOptionsPanel;

View File

@ -36,9 +36,9 @@ import {
} from 'react-icons/md';
import { activeTabIndexSelector } from '../store/uiSelectors';
import { floatingSelector } from './FloatingParametersPanelButtons';
import ImageToImageWorkarea from './ImageToImage';
import TextToImageWorkarea from './TextToImage';
import UnifiedCanvasWorkarea from './UnifiedCanvas/UnifiedCanvasWorkarea';
import ImageToImageWorkarea from 'features/ui/components/tabs/ImageToImage/ImageToImageWorkarea';
import TextToImageWorkarea from 'features/ui/components/tabs/TextToImage/TextToImageWorkarea';
import UnifiedCanvasWorkarea from 'features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasWorkarea';
export interface InvokeTabInfo {
title: ReactElement;

View File

@ -1,71 +0,0 @@
import { Box, BoxProps, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import ImageGallery from 'features/gallery/components/ImageGallery';
import { setInitialImage } from 'features/parameters/store/generationSlice';
import {
activeTabNameSelector,
uiSelector,
} from 'features/ui/store/uiSelectors';
import { DragEvent, ReactNode } from 'react';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import useGetImageByUuid from 'features/gallery/hooks/useGetImageByUuid';
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
import { isEqual } from 'lodash';
const workareaSelector = createSelector(
[uiSelector, lightboxSelector, activeTabNameSelector],
(ui, lightbox, activeTabName) => {
const { shouldPinParametersPanel } = ui;
const { isLightboxOpen } = lightbox;
return {
shouldPinParametersPanel,
isLightboxOpen,
activeTabName,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
type InvokeWorkareaProps = BoxProps & {
optionsPanel: ReactNode;
children: ReactNode;
};
const InvokeWorkarea = (props: InvokeWorkareaProps) => {
const dispatch = useAppDispatch();
const { optionsPanel, children, ...rest } = props;
const { activeTabName, isLightboxOpen } = useAppSelector(workareaSelector);
const getImageByUuid = useGetImageByUuid();
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
const uuid = e.dataTransfer.getData('invokeai/imageUuid');
const image = getImageByUuid(uuid);
if (!image) return;
if (activeTabName === 'img2img') {
dispatch(setInitialImage(image));
} else if (activeTabName === 'unifiedCanvas') {
dispatch(setInitialCanvasImage(image));
}
};
return (
<Box {...rest} pos="relative" w="100%" h="100%">
<Flex gap={4} h="100%">
{optionsPanel}
<Box pos="relative" w="100%" h="100%" onDrop={handleDrop}>
{children}
</Box>
{!isLightboxOpen && <ImageGallery />}
</Flex>
</Box>
);
};
export default InvokeWorkarea;

View File

@ -1,11 +0,0 @@
import InvokeWorkarea from 'features/ui/components/InvokeWorkarea';
import TextToImageDisplay from './TextToImageDisplay';
import TextToImagePanel from './TextToImagePanel';
export default function TextToImageWorkarea() {
return (
<InvokeWorkarea optionsPanel={<TextToImagePanel />}>
<TextToImageDisplay />
</InvokeWorkarea>
);
}

View File

@ -1,21 +0,0 @@
import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks';
import InvokeWorkarea from 'features/ui/components/InvokeWorkarea';
import UnifiedCanvasDisplayBeta from './UnifiedCanvasBeta/UnifiedCanvasDisplayBeta';
import UnifiedCanvasDisplay from './UnifiedCanvasDisplay';
import UnifiedCanvasPanel from './UnifiedCanvasPanel';
export default function UnifiedCanvasWorkarea() {
const shouldUseCanvasBetaLayout = useAppSelector(
(state: RootState) => state.ui.shouldUseCanvasBetaLayout
);
return (
<InvokeWorkarea optionsPanel={<UnifiedCanvasPanel />}>
{shouldUseCanvasBetaLayout ? (
<UnifiedCanvasDisplayBeta />
) : (
<UnifiedCanvasDisplay />
)}
</InvokeWorkarea>
);
}

View File

@ -0,0 +1,147 @@
import { Box, BoxProps, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import ImageGallery from 'features/gallery/components/ImageGallery';
import { setInitialImage } from 'features/parameters/store/generationSlice';
import {
activeTabNameSelector,
uiSelector,
} from 'features/ui/store/uiSelectors';
import { DragEvent, ReactNode } from 'react';
import {
setDoesCanvasNeedScaling,
setInitialCanvasImage,
} from 'features/canvas/store/canvasSlice';
import useGetImageByUuid from 'features/gallery/hooks/useGetImageByUuid';
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
import { isEqual } from 'lodash';
import {
APP_CONTENT_HEIGHT,
PARAMETERS_PANEL_WIDTH,
} from 'theme/util/constants';
import ResizableDrawer from 'features/ui/components/common/ResizableDrawer/ResizableDrawer';
import {
setShouldPinParametersPanel,
setShouldShowParametersPanel,
} from 'features/ui/store/uiSlice';
import { useHotkeys } from 'react-hotkeys-hook';
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
const workareaSelector = createSelector(
[uiSelector, lightboxSelector, activeTabNameSelector],
(ui, lightbox, activeTabName) => {
const { shouldPinParametersPanel } = ui;
const { isLightboxOpen } = lightbox;
return {
shouldPinParametersPanel,
isLightboxOpen,
activeTabName,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
type InvokeWorkareaProps = BoxProps & {
parametersPanel: ReactNode;
children: ReactNode;
};
const InvokeWorkarea = (props: InvokeWorkareaProps) => {
const { parametersPanel, children, ...rest } = props;
const dispatch = useAppDispatch();
const { activeTabName, isLightboxOpen } = useAppSelector(workareaSelector);
const { shouldPinParametersPanel, shouldShowParametersPanel } =
useAppSelector(uiSelector);
const getImageByUuid = useGetImageByUuid();
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
const uuid = e.dataTransfer.getData('invokeai/imageUuid');
const image = getImageByUuid(uuid);
if (!image) return;
if (activeTabName === 'img2img') {
dispatch(setInitialImage(image));
} else if (activeTabName === 'unifiedCanvas') {
dispatch(setInitialCanvasImage(image));
}
};
const closeParametersPanel = () => {
dispatch(setShouldShowParametersPanel(false));
};
useHotkeys(
'o',
() => {
dispatch(setShouldShowParametersPanel(!shouldShowParametersPanel));
shouldPinParametersPanel &&
setTimeout(() => dispatch(setDoesCanvasNeedScaling(true)), 400);
},
[shouldShowParametersPanel, shouldPinParametersPanel]
);
useHotkeys(
'esc',
() => {
dispatch(setShouldShowParametersPanel(false));
},
{
enabled: () => !shouldPinParametersPanel,
preventDefault: true,
},
[shouldPinParametersPanel]
);
useHotkeys(
'shift+o',
() => {
dispatch(setShouldPinParametersPanel(!shouldPinParametersPanel));
dispatch(setDoesCanvasNeedScaling(true));
},
[shouldPinParametersPanel]
);
return (
<Flex {...rest} pos="relative" h={APP_CONTENT_HEIGHT} gap={4}>
<ResizableDrawer
direction="left"
isResizable={true}
shouldAllowResize={!shouldPinParametersPanel}
isOpen={shouldShowParametersPanel || shouldPinParametersPanel}
onClose={closeParametersPanel}
isPinned={shouldPinParametersPanel}
handleWidth={5}
handleInteractWidth={'15px'}
sx={{
borderColor: 'base.700',
p: shouldPinParametersPanel ? 0 : 4,
bg: 'base.900',
}}
initialWidth={PARAMETERS_PANEL_WIDTH}
minWidth={PARAMETERS_PANEL_WIDTH}
pinnedWidth={PARAMETERS_PANEL_WIDTH}
pinnedHeight={APP_CONTENT_HEIGHT}
>
<Flex
flexDir="column"
rowGap={4}
paddingTop={!shouldPinParametersPanel ? 1.5 : 0}
>
{!shouldPinParametersPanel && <InvokeAILogoComponent />}
{parametersPanel}
</Flex>
</ResizableDrawer>
<Box pos="relative" w="100%" h="100%" onDrop={handleDrop}>
{children}
</Box>
{!isLightboxOpen && <ImageGallery />}
</Flex>
);
};
export default InvokeWorkarea;

View File

@ -0,0 +1,44 @@
import { Box, Icon, Tooltip } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIIconButton, {
IAIIconButtonProps,
} from 'common/components/IAIIconButton';
import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice';
import { useTranslation } from 'react-i18next';
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
import { setShouldPinParametersPanel } from '../../store/uiSlice';
const PinParametersPanelButton = () => {
const dispatch = useAppDispatch();
const shouldPinParametersPanel = useAppSelector(
(state) => state.ui.shouldPinParametersPanel
);
const { t } = useTranslation();
const handleClickPinOptionsPanel = () => {
dispatch(setShouldPinParametersPanel(!shouldPinParametersPanel));
dispatch(setDoesCanvasNeedScaling(true));
};
return (
<Tooltip label={t('common.pinOptionsPanel')}>
<IAIIconButton
aria-label={t('common.pinOptionsPanel')}
opacity={0.2}
onClick={handleClickPinOptionsPanel}
icon={shouldPinParametersPanel ? <BsPinAngleFill /> : <BsPinAngle />}
variant="unstyled"
size="sm"
padding={2}
sx={{
position: 'absolute',
top: 1,
insetInlineEnd: 1,
}}
/>
</Tooltip>
);
};
export default PinParametersPanelButton;

View File

@ -0,0 +1,217 @@
import {
Box,
chakra,
ChakraProps,
Slide,
useOutsideClick,
useTheme,
SlideDirection,
} from '@chakra-ui/react';
import {
Resizable,
ResizableProps,
ResizeCallback,
ResizeStartCallback,
} from 're-resizable';
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { LangDirection } from './types';
import {
getDefaultSize,
getHandleEnables,
getHandleStyles,
getMinMaxDimensions,
getResizableStyles,
} from './util';
import Scrollable from '../Scrollable';
type ResizableDrawerProps = ResizableProps & {
children: ReactNode;
isResizable: boolean;
isPinned: boolean;
isOpen: boolean;
onClose: () => void;
direction?: SlideDirection;
initialWidth?: string | number;
minWidth?: string | number;
maxWidth?: string | number;
initialHeight?: string | number;
minHeight?: string | number;
maxHeight?: string | number;
shouldAllowResize?: boolean;
onResizeStart?: ResizeStartCallback;
onResizeStop?: ResizeCallback;
onResize?: ResizeCallback;
handleWidth?: number;
handleInteractWidth?: string | number;
sx?: ChakraProps['sx'];
pinnedWidth: number;
pinnedHeight: string | number;
};
const ChakraResizeable = chakra(Resizable, {
shouldForwardProp: (prop) => !['sx'].includes(prop),
});
const ResizableDrawer = ({
direction = 'left',
isResizable,
isPinned,
isOpen,
onClose,
children,
initialWidth = undefined,
minWidth = undefined,
maxWidth = undefined,
initialHeight = undefined,
minHeight = undefined,
maxHeight = undefined,
shouldAllowResize,
onResizeStart,
onResizeStop,
onResize,
handleWidth = 5,
handleInteractWidth = '15px',
pinnedWidth,
pinnedHeight,
sx = {},
}: ResizableDrawerProps) => {
const langDirection = useTheme().direction as LangDirection;
const outsideClickRef = useRef<HTMLDivElement>(null);
useOutsideClick({
ref: outsideClickRef,
handler: () => {
if (isPinned) {
return;
}
onClose();
},
});
const [width, setWidth] = useState<number | string>(0);
const [height, setHeight] = useState<number | string>(0);
const handleEnables = useMemo(
() =>
isResizable && shouldAllowResize
? getHandleEnables({ direction, langDirection })
: {},
[isResizable, shouldAllowResize, langDirection, direction]
);
const handleStyles = useMemo(
() =>
getHandleStyles({
handleEnables,
handleStyle: {
width: handleInteractWidth,
},
}),
[handleEnables, handleInteractWidth]
);
const minMaxDimensions = useMemo(
() =>
getMinMaxDimensions({
direction,
minWidth,
maxWidth,
minHeight,
maxHeight,
}),
[minWidth, maxWidth, minHeight, maxHeight, direction]
);
const resizableStyles = useMemo(
() => getResizableStyles({ isPinned, direction, sx, handleWidth }),
[sx, handleWidth, direction, isPinned]
);
useEffect(() => {
const { width, height } = getDefaultSize({
initialWidth,
initialHeight,
direction,
});
setWidth(width);
setHeight(height);
}, [initialWidth, initialHeight, direction, langDirection]);
useEffect(() => {
if (['left', 'right'].includes(direction)) {
setHeight(isPinned ? '100%' : '100vh');
}
if (['top', 'bottom'].includes(direction)) {
setWidth(isPinned ? '100%' : '100vw');
}
}, [isPinned, direction]);
return (
<Slide
direction={direction}
in={isOpen}
motionProps={{ initial: false }}
{...(isPinned
? {
style: {
position: undefined,
left: undefined,
top: undefined,
bottom: undefined,
width: undefined,
},
}
: {
transition: { enter: { duration: 0.2 }, exit: { duration: 0.2 } },
style: { zIndex: 98 },
})}
>
<Box
ref={outsideClickRef}
sx={{
width: ['left', 'right'].includes(direction) ? 'min-content' : 'full',
height: ['left', 'right'].includes(direction)
? '100%'
: 'min-content',
position: 'relative',
}}
>
<ChakraResizeable
size={{
width: isPinned ? '100%' : width,
height: isPinned ? '100%' : height,
}}
enable={handleEnables}
handleStyles={handleStyles}
{...minMaxDimensions}
sx={{ ...resizableStyles, height: 'full' }}
onResizeStart={(event, direction, elementRef) => {
onResizeStart && onResizeStart(event, direction, elementRef);
}}
onResize={(event, direction, elementRef, delta) => {
onResize && onResize(event, direction, elementRef, delta);
}}
onResizeStop={(event, direction, elementRef, delta) => {
event.stopPropagation();
event.stopImmediatePropagation();
event.preventDefault();
if (direction === 'left' || direction === 'right') {
setWidth(Number(width) + delta.width);
}
if (direction === 'top' || direction === 'bottom') {
setHeight(Number(height) + delta.height);
}
onResizeStop && onResizeStop(event, direction, elementRef, delta);
}}
>
<Scrollable>{children}</Scrollable>
</ChakraResizeable>
</Box>
</Slide>
);
};
export default ResizableDrawer;

View File

@ -0,0 +1,2 @@
export type Placement = 'top' | 'right' | 'bottom' | 'left';
export type LangDirection = 'ltr' | 'rtl' | undefined;

View File

@ -0,0 +1,211 @@
import { ChakraProps, SlideDirection } from '@chakra-ui/react';
import { AnimationProps } from 'framer-motion';
import { Enable } from 're-resizable';
import React from 'react';
import { LangDirection } from './types';
export type GetHandleEnablesOptions = {
direction: SlideDirection;
langDirection: LangDirection;
};
// Determine handles to enable, taking into account language direction
export const getHandleEnables = ({
direction,
langDirection,
}: GetHandleEnablesOptions) => {
const top = direction === 'bottom';
const right =
(langDirection !== 'rtl' && direction === 'left') ||
(langDirection === 'rtl' && direction === 'right');
const bottom = direction === 'top';
const left =
(langDirection !== 'rtl' && direction === 'right') ||
(langDirection === 'rtl' && direction === 'left');
return { top, right, bottom, left };
};
export type GetDefaultSizeOptions = {
initialWidth?: string | number;
initialHeight?: string | number;
direction: SlideDirection;
};
// Get default sizes based on direction and initial values
export const getDefaultSize = ({
initialWidth,
initialHeight,
direction,
}: GetDefaultSizeOptions) => {
const width =
initialWidth ?? (['left', 'right'].includes(direction) ? 500 : '100vw');
const height =
initialHeight ?? (['top', 'bottom'].includes(direction) ? 500 : '100vh');
return { width, height };
};
export type GetMinMaxDimensionsOptions = {
direction: SlideDirection;
minWidth?: string | number;
maxWidth?: string | number;
minHeight?: string | number;
maxHeight?: string | number;
};
// Get the min/max width/height based on direction and provided values
export const getMinMaxDimensions = ({
direction,
minWidth,
maxWidth,
minHeight,
maxHeight,
}: GetMinMaxDimensionsOptions) => {
const minW =
minWidth ?? (['left', 'right'].includes(direction) ? 10 : undefined);
const maxW =
maxWidth ?? (['left', 'right'].includes(direction) ? '95vw' : undefined);
const minH =
minHeight ?? (['top', 'bottom'].includes(direction) ? 10 : undefined);
const maxH =
maxHeight ?? (['top', 'bottom'].includes(direction) ? '95vh' : undefined);
return { minWidth: minW, maxWidth: maxW, minHeight: minH, maxHeight: maxH };
};
export type GetHandleStylesOptions = {
handleEnables: Enable;
handleStyle?: React.CSSProperties;
};
// Get handle styles, the enables already have language direction factored in so
// that does not need to be handled here
export const getHandleStyles = ({
handleEnables,
handleStyle,
}: GetHandleStylesOptions) => {
if (!handleStyle) {
return {};
}
const top = handleEnables.top ? handleStyle : {};
const right = handleEnables.right ? handleStyle : {};
const bottom = handleEnables.bottom ? handleStyle : {};
const left = handleEnables.left ? handleStyle : {};
return {
top,
right,
bottom,
left,
};
};
export type GetAnimationsOptions = {
direction: SlideDirection;
langDirection: LangDirection;
};
// Get the framer-motion animation props, taking into account language direction
export const getAnimations = ({
direction,
langDirection,
}: GetAnimationsOptions): AnimationProps => {
const baseAnimation = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
// chakra consumes the transition prop, which, for it, is a string.
// however we know the transition prop will make it to framer motion,
// which wants it as an object. cast as string to satisfy TS.
transition: { duration: 0.2, ease: 'easeInOut' },
};
const langDirectionFactor = langDirection === 'rtl' ? -1 : 1;
if (direction === 'top') {
return {
...baseAnimation,
initial: { y: -999 },
animate: { y: 0 },
exit: { y: -999 },
};
}
if (direction === 'right') {
return {
...baseAnimation,
initial: { x: 999 * langDirectionFactor },
animate: { x: 0 },
exit: { x: 999 * langDirectionFactor },
};
}
if (direction === 'bottom') {
return {
...baseAnimation,
initial: { y: 999 },
animate: { y: 0 },
exit: { y: 999 },
};
}
if (direction === 'left') {
return {
...baseAnimation,
initial: { x: -999 * langDirectionFactor },
animate: { x: 0 },
exit: { x: -999 * langDirectionFactor },
};
}
return {};
};
export type GetResizableStylesProps = {
sx: ChakraProps['sx'];
direction: SlideDirection;
handleWidth: number;
isPinned: boolean;
};
export const getResizableStyles = ({
isPinned, // TODO add borderRadius for pinned?
sx,
direction,
handleWidth,
}: GetResizableStylesProps): ChakraProps['sx'] => {
if (isPinned) {
return sx;
}
if (direction === 'top') {
return {
borderBottomWidth: handleWidth,
...sx,
};
}
if (direction === 'right') {
return { borderInlineStartWidth: handleWidth, ...sx };
}
if (direction === 'bottom') {
return {
borderTopWidth: handleWidth,
...sx,
};
}
if (direction === 'left') {
return { borderInlineEndWidth: handleWidth, ...sx };
}
};

View File

@ -0,0 +1,117 @@
import { Box, ChakraProps } from '@chakra-ui/react';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
import { ReactNode, useEffect, useRef } from 'react';
type ScrollableProps = {
children: ReactNode;
containerProps?: ChakraProps;
};
const Scrollable = ({
children,
containerProps = {
width: 'full',
height: 'full',
flexShrink: 0,
},
}: ScrollableProps) => {
const scrollableRef = useRef<HTMLDivElement>(null);
const topShadowRef = useRef<HTMLDivElement>(null);
const bottomShadowRef = useRef<HTMLDivElement>(null);
const [initialize, _instance] = useOverlayScrollbars({
defer: true,
events: {
initialized(instance) {
if (!topShadowRef.current || !bottomShadowRef.current) {
return;
}
const { scrollTop, scrollHeight, offsetHeight } =
instance.elements().content;
const scrollPercentage = scrollTop / (scrollHeight - offsetHeight);
topShadowRef.current.style.opacity = String(scrollPercentage * 5);
bottomShadowRef.current.style.opacity = String(
(1 - scrollPercentage) * 5
);
},
scroll: (_instance, event) => {
if (
!topShadowRef.current ||
!bottomShadowRef.current ||
!scrollableRef.current
) {
return;
}
const { scrollTop, scrollHeight, offsetHeight } =
event.target as HTMLDivElement;
const scrollPercentage = scrollTop / (scrollHeight - offsetHeight);
topShadowRef.current.style.opacity = String(scrollPercentage * 5);
bottomShadowRef.current.style.opacity = String(
(1 - scrollPercentage) * 5
);
},
},
});
useEffect(() => {
if (
!scrollableRef.current ||
!topShadowRef.current ||
!bottomShadowRef.current
) {
return;
}
topShadowRef.current.style.opacity = '0';
bottomShadowRef.current.style.opacity = '0';
initialize(scrollableRef.current);
}, [initialize]);
return (
<Box position="relative" w="full" h="full">
<Box ref={scrollableRef} {...containerProps} overflowY="scroll">
<Box paddingInlineEnd={5}>{children}</Box>
</Box>
<Box
ref={bottomShadowRef}
sx={{
position: 'absolute',
boxShadow:
'inset 0 -3.5rem 2rem -2rem var(--invokeai-colors-base-900)',
width: 'full',
height: 24,
bottom: 0,
left: 0,
pointerEvents: 'none',
transition: 'opacity 0.2s',
}}
></Box>
<Box
ref={topShadowRef}
sx={{
position: 'absolute',
boxShadow:
'inset 0 3.5rem 2rem -2rem var(--invokeai-colors-base-900)',
width: 'full',
height: 24,
top: 0,
left: 0,
pointerEvents: 'none',
transition: 'opacity 0.2s',
}}
></Box>
</Box>
);
};
export default Scrollable;

View File

@ -14,7 +14,7 @@ const workareaSplitViewStyle: ChakraProps['sx'] = {
padding: 4,
};
const ImageToImageDisplay = () => {
const ImageToImageContent = () => {
const initialImage = useAppSelector(
(state: RootState) => state.generation.initialImage
);
@ -47,4 +47,4 @@ const ImageToImageDisplay = () => {
);
};
export default ImageToImageDisplay;
export default ImageToImageContent;

View File

@ -15,11 +15,11 @@ import ParametersAccordion from 'features/parameters/components/ParametersAccord
import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput';
import PromptInput from 'features/parameters/components/PromptInput/PromptInput';
import InvokeOptionsPanel from 'features/ui/components/InvokeParametersPanel';
import { useTranslation } from 'react-i18next';
import ImageToImageSettings from './ImageToImageSettings';
import PinParametersPanelButton from 'features/ui/components/common/PinParametersPanelButton';
import ImageToImageSettings from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings';
export default function ImageToImagePanel() {
export default function ImageToImageParameters() {
const { t } = useTranslation();
const imageToImageAccordions = {
@ -69,13 +69,12 @@ export default function ImageToImagePanel() {
};
return (
<InvokeOptionsPanel>
<Flex flexDir="column" rowGap={2}>
<PromptInput />
<NegativePromptInput />
</Flex>
<Flex flexDir="column" gap={2} position="relative">
<PromptInput />
<NegativePromptInput />
<ProcessButtons />
<ParametersAccordion accordionInfo={imageToImageAccordions} />
</InvokeOptionsPanel>
<PinParametersPanelButton />
</Flex>
);
}

View File

@ -0,0 +1,11 @@
import InvokeWorkarea from 'features/ui/components/common/InvokeWorkarea';
import ImageToImageContent from './ImageToImageContent';
import ImageToImageParameters from './ImageToImageParameters';
export default function ImageToImageWorkarea() {
return (
<InvokeWorkarea parametersPanel={<ImageToImageParameters />}>
<ImageToImageContent />
</InvokeWorkarea>
);
}

View File

@ -1,7 +1,7 @@
import { Box, Flex } from '@chakra-ui/react';
import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay';
const TextToImageDisplay = () => {
const TextToImageContent = () => {
return (
<Box
sx={{
@ -18,4 +18,4 @@ const TextToImageDisplay = () => {
);
};
export default TextToImageDisplay;
export default TextToImageContent;

View File

@ -15,10 +15,10 @@ import ParametersAccordion from 'features/parameters/components/ParametersAccord
import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput';
import PromptInput from 'features/parameters/components/PromptInput/PromptInput';
import InvokeOptionsPanel from 'features/ui/components/InvokeParametersPanel';
import { useTranslation } from 'react-i18next';
import PinParametersPanelButton from 'features/ui/components/common/PinParametersPanelButton';
export default function TextToImagePanel() {
export default function TextToImageParameters() {
const { t } = useTranslation();
const textToImageAccordions = {
@ -63,13 +63,12 @@ export default function TextToImagePanel() {
};
return (
<InvokeOptionsPanel>
<Flex flexDir="column" rowGap={2}>
<PromptInput />
<NegativePromptInput />
</Flex>
<Flex flexDir="column" gap={2} position="relative">
<PromptInput />
<NegativePromptInput />
<ProcessButtons />
<ParametersAccordion accordionInfo={textToImageAccordions} />
</InvokeOptionsPanel>
<PinParametersPanelButton />
</Flex>
);
}

View File

@ -0,0 +1,11 @@
import InvokeWorkarea from 'features/ui/components/common/InvokeWorkarea';
import TextToImageContent from './TextToImageContent';
import TextToImageParameters from './TextToImageParameters';
export default function TextToImageWorkarea() {
return (
<InvokeWorkarea parametersPanel={<TextToImageParameters />}>
<TextToImageContent />
</InvokeWorkarea>
);
}

View File

@ -27,7 +27,7 @@ const selector = createSelector(
}
);
const UnifiedCanvasDisplayBeta = () => {
const UnifiedCanvasContentBeta = () => {
const dispatch = useAppDispatch();
const { doesCanvasNeedScaling } = useAppSelector(selector);
@ -70,4 +70,4 @@ const UnifiedCanvasDisplayBeta = () => {
);
};
export default UnifiedCanvasDisplayBeta;
export default UnifiedCanvasContentBeta;

View File

@ -26,7 +26,7 @@ const selector = createSelector(
}
);
const UnifiedCanvasDisplay = () => {
const UnifiedCanvasContent = () => {
const dispatch = useAppDispatch();
const { doesCanvasNeedScaling } = useAppSelector(selector);
@ -80,4 +80,4 @@ const UnifiedCanvasDisplay = () => {
);
};
export default UnifiedCanvasDisplay;
export default UnifiedCanvasContent;

View File

@ -15,10 +15,10 @@ import ParametersAccordion from 'features/parameters/components/ParametersAccord
import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput';
import PromptInput from 'features/parameters/components/PromptInput/PromptInput';
import InvokeOptionsPanel from 'features/ui/components/InvokeParametersPanel';
import { useTranslation } from 'react-i18next';
import PinParametersPanelButton from 'features/ui/components/common/PinParametersPanelButton';
export default function UnifiedCanvasPanel() {
export default function UnifiedCanvasParameters() {
const { t } = useTranslation();
const unifiedCanvasAccordions = {
@ -66,14 +66,12 @@ export default function UnifiedCanvasPanel() {
};
return (
<InvokeOptionsPanel>
<Flex flexDir="column" rowGap={2}>
<PromptInput />
<NegativePromptInput />
</Flex>
<Flex flexDir="column" gap={2} position="relative">
<PromptInput />
<NegativePromptInput />
<ProcessButtons />
{/* <ParametersAccordion accordionInfo={unifiedCanvasImg2ImgAccordion} /> */}
<ParametersAccordion accordionInfo={unifiedCanvasAccordions} />
</InvokeOptionsPanel>
<PinParametersPanelButton />
</Flex>
);
}

View File

@ -0,0 +1,21 @@
import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks';
import InvokeWorkarea from 'features/ui/components/common/InvokeWorkarea';
import UnifiedCanvasContentBeta from './UnifiedCanvasBeta/UnifiedCanvasContentBeta';
import UnifiedCanvasContent from './UnifiedCanvasContent';
import UnifiedCanvasParameters from './UnifiedCanvasParameters';
export default function UnifiedCanvasWorkarea() {
const shouldUseCanvasBetaLayout = useAppSelector(
(state: RootState) => state.ui.shouldUseCanvasBetaLayout
);
return (
<InvokeWorkarea parametersPanel={<UnifiedCanvasParameters />}>
{shouldUseCanvasBetaLayout ? (
<UnifiedCanvasContentBeta />
) : (
<UnifiedCanvasContent />
)}
</InvokeWorkarea>
);
}

View File

@ -7,7 +7,6 @@ const initialtabsState: UIState = {
activeTab: 0,
currentTheme: 'dark',
parametersPanelScrollPosition: 0,
shouldHoldParametersPanelOpen: false,
shouldPinParametersPanel: true,
shouldShowParametersPanel: true,
shouldShowImageDetails: false,
@ -41,16 +40,11 @@ export const uiSlice = createSlice({
},
setShouldPinParametersPanel: (state, action: PayloadAction<boolean>) => {
state.shouldPinParametersPanel = action.payload;
state.shouldShowParametersPanel = true;
},
setShouldShowParametersPanel: (state, action: PayloadAction<boolean>) => {
state.shouldShowParametersPanel = action.payload;
},
setShouldHoldParametersPanelOpen: (
state,
action: PayloadAction<boolean>
) => {
state.shouldHoldParametersPanelOpen = action.payload;
},
setShouldShowImageDetails: (state, action: PayloadAction<boolean>) => {
state.shouldShowImageDetails = action.payload;
},
@ -76,7 +70,6 @@ export const {
setActiveTab,
setCurrentTheme,
setParametersPanelScrollPosition,
setShouldHoldParametersPanelOpen,
setShouldPinParametersPanel,
setShouldShowParametersPanel,
setShouldShowImageDetails,

View File

@ -4,7 +4,6 @@ export interface UIState {
activeTab: number;
currentTheme: string;
parametersPanelScrollPosition: number;
shouldHoldParametersPanelOpen: boolean;
shouldPinParametersPanel: boolean;
shouldShowParametersPanel: boolean;
shouldShowImageDetails: boolean;

View File

@ -9,7 +9,6 @@ const { definePartsStyle, defineMultiStyleConfig } =
const invokeAIContainer = defineStyle({
border: 'none',
pt: 2,
});
const invokeAIButton = defineStyle((props) => {
@ -40,7 +39,6 @@ const invokeAIPanel = defineStyle((props) => {
bg: `${c}.800`,
borderRadius: 'base',
borderTopRadius: 'none',
p: 4,
};
});

View File

@ -0,0 +1,25 @@
.os-scrollbar {
/* --os-size: 0; */
/* --os-padding-perpendicular: 0; */
/* --os-padding-axis: 0; */
/* --os-track-border-radius: 0; */
/* --os-track-bg: none; */
/* --os-track-bg-hover: none; */
/* --os-track-bg-active: none; */
/* --os-track-border: none; */
/* --os-track-border-hover: none; */
/* --os-track-border-active: none; */
/* --os-handle-border-radius: 0; */
--os-handle-bg: var(--invokeai-colors-accent-600);
--os-handle-bg-hover: var(--invokeai-colors-accent-550);
--os-handle-bg-active: var(--invokeai-colors-accent-500);
/* --os-handle-border: none; */
/* --os-handle-border-hover: none; */
/* --os-handle-border-active: none; */
/* --os-handle-min-size: 33px; */
/* --os-handle-max-size: none; */
/* --os-handle-perpendicular-size: 100%; */
/* --os-handle-perpendicular-size-hover: 100%; */
/* --os-handle-perpendicular-size-active: 100%; */
/* --os-handle-interactive-area-offset: 0; */
}

View File

@ -12,7 +12,7 @@ import { modalTheme } from './components/modal';
import { numberInputTheme } from './components/numberInput';
import { popoverTheme } from './components/popover';
import { progressTheme } from './components/progress';
import { scrollbar } from './components/scrollbar';
import { no_scrollbar, scrollbar } from './components/scrollbar';
import { selectTheme } from './components/select';
import { sliderTheme } from './components/slider';
import { switchTheme } from './components/switch';
@ -31,7 +31,7 @@ export const theme: ThemeOverride = {
color: 'base.50',
overflow: 'hidden',
},
...scrollbar,
...no_scrollbar,
}),
},
direction: 'ltr',

View File

@ -11,6 +11,9 @@ export const APP_GALLERY_HEIGHT = 'calc(100vw - 0.3rem + 5rem)';
export const APP_GALLERY_POPOVER_HEIGHT = `calc(100vh - (${APP_CONTENT_HEIGHT_CUTOFF} + 6rem))`;
export const APP_METADATA_HEIGHT = `calc(100vh - (${APP_CONTENT_HEIGHT_CUTOFF} + 4.4rem))`;
// this is in pixels
export const PARAMETERS_PANEL_WIDTH = 384;
// do not touch ffs
export const APP_TEXT_TO_IMAGE_HEIGHT =
'calc(100vh - 9.4375rem - 1.925rem - 1.15rem)';

View File

@ -4204,6 +4204,16 @@ os-tmpdir@~1.0.2:
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
overlayscrollbars-react@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/overlayscrollbars-react/-/overlayscrollbars-react-0.5.0.tgz#0272bdc6304c7228a58d30e5b678e97fd5c5d8dd"
integrity sha512-uCNTnkfWW74veoiEv3kSwoLelKt4e8gTNv65D771X3il0x5g5Yo0fUbro7SpQzR9yNgi23cvB2mQHTTdQH96pA==
overlayscrollbars@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/overlayscrollbars/-/overlayscrollbars-2.1.0.tgz#d647034ef388980e0e5e092f7429c501215330a1"
integrity sha512-L6p4o4aWse5pDstRnJjZaos+al+bkuAgzGIlWwlsxRSgW6+7Kvrp+kAzlWoTZ1bgB4CJj+8u5bjdq8XHEhWjrw==
p-cancelable@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"