feat(ui): use stable objects for animation/native element styles

This commit is contained in:
psychedelicious 2023-12-29 13:27:39 +11:00 committed by Kent Keirsey
parent f5194f9e2d
commit ca4b8e65c1
33 changed files with 292 additions and 183 deletions

View File

@ -1,5 +1,6 @@
import type { ChakraProps } from '@chakra-ui/react'; import type { ChakraProps } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import type { CSSProperties } from 'react';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { RgbaColorPicker } from 'react-colorful'; import { RgbaColorPicker } from 'react-colorful';
import type { import type {
@ -14,20 +15,22 @@ type IAIColorPickerProps = ColorPickerBaseProps<RgbaColor> & {
withNumberInput?: boolean; withNumberInput?: boolean;
}; };
const colorPickerStyles: NonNullable<ChakraProps['sx']> = { const colorPickerPointerStyles: NonNullable<ChakraProps['sx']> = {
width: 6, width: 6,
height: 6, height: 6,
borderColor: 'base.100', borderColor: 'base.100',
}; };
const sx: ChakraProps['sx'] = { const sx: ChakraProps['sx'] = {
'.react-colorful__hue-pointer': colorPickerStyles, '.react-colorful__hue-pointer': colorPickerPointerStyles,
'.react-colorful__saturation-pointer': colorPickerStyles, '.react-colorful__saturation-pointer': colorPickerPointerStyles,
'.react-colorful__alpha-pointer': colorPickerStyles, '.react-colorful__alpha-pointer': colorPickerPointerStyles,
gap: 2, gap: 2,
flexDir: 'column', flexDir: 'column',
}; };
const colorPickerStyles: CSSProperties = { width: '100%' };
const numberInputWidth: ChakraProps['w'] = '4.2rem'; const numberInputWidth: ChakraProps['w'] = '4.2rem';
const IAIColorPicker = (props: IAIColorPickerProps) => { const IAIColorPicker = (props: IAIColorPickerProps) => {
@ -53,7 +56,7 @@ const IAIColorPicker = (props: IAIColorPickerProps) => {
<RgbaColorPicker <RgbaColorPicker
color={color} color={color}
onChange={onChange} onChange={onChange}
style={{ width: '100%' }} style={colorPickerStyles}
{...rest} {...rest}
/> />
{withNumberInput && ( {withNumberInput && (

View File

@ -112,7 +112,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
isDisabled: isUploadDisabled, isDisabled: isUploadDisabled,
}); });
const uploadButtonStyles = useMemo(() => { const uploadButtonStyles = useMemo<SystemStyleObject>(() => {
const styles: SystemStyleObject = { const styles: SystemStyleObject = {
minH: minSize, minH: minSize,
w: 'full', w: 'full',
@ -134,6 +134,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
}, },
}); });
} }
return styles;
}, [isUploadDisabled, minSize]); }, [isUploadDisabled, minSize]);
return ( return (

View File

@ -1,4 +1,5 @@
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import type { AnimationProps } from 'framer-motion';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { memo, useRef } from 'react'; import { memo, useRef } from 'react';
@ -9,23 +10,27 @@ type Props = {
label?: ReactNode; label?: ReactNode;
}; };
const initial: AnimationProps['initial'] = {
opacity: 0,
};
const animate: AnimationProps['animate'] = {
opacity: 1,
transition: { duration: 0.1 },
};
const exit: AnimationProps['exit'] = {
opacity: 0,
transition: { duration: 0.1 },
};
export const IAIDropOverlay = (props: Props) => { export const IAIDropOverlay = (props: Props) => {
const { isOver, label = 'Drop' } = props; const { isOver, label = 'Drop' } = props;
const motionId = useRef(uuidv4()); const motionId = useRef(uuidv4());
return ( return (
<motion.div <motion.div
key={motionId.current} key={motionId.current}
initial={{ initial={initial}
opacity: 0, animate={animate}
}} exit={exit}
animate={{
opacity: 1,
transition: { duration: 0.1 },
}}
exit={{
opacity: 0,
transition: { duration: 0.1 },
}}
> >
<Flex position="absolute" top={0} insetInlineStart={0} w="full" h="full"> <Flex position="absolute" top={0} insetInlineStart={0} w="full" h="full">
<Flex <Flex

View File

@ -4,6 +4,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import type { KeyboardEvent, ReactNode } from 'react'; import type { KeyboardEvent, ReactNode } from 'react';
import { memo, useCallback, useEffect, useState } from 'react'; import { memo, useCallback, useEffect, useState } from 'react';
@ -155,17 +156,9 @@ const ImageUploader = (props: ImageUploaderProps) => {
{isDragActive && isHandlingUpload && ( {isDragActive && isHandlingUpload && (
<motion.div <motion.div
key="image-upload-overlay" key="image-upload-overlay"
initial={{ initial={initial}
opacity: 0, animate={animate}
}} exit={exit}
animate={{
opacity: 1,
transition: { duration: 0.1 },
}}
exit={{
opacity: 0,
transition: { duration: 0.1 },
}}
> >
<ImageUploadOverlay <ImageUploadOverlay
isDragAccept={isDragAccept} isDragAccept={isDragAccept}
@ -180,3 +173,15 @@ const ImageUploader = (props: ImageUploaderProps) => {
}; };
export default memo(ImageUploader); export default memo(ImageUploader);
const initial: AnimationProps['initial'] = {
opacity: 0,
};
const animate: AnimationProps['animate'] = {
opacity: 1,
transition: { duration: 0.1 },
};
const exit: AnimationProps['exit'] = {
opacity: 0,
transition: { duration: 0.1 },
};

View File

@ -31,8 +31,8 @@ import {
FaEyeDropper, FaEyeDropper,
FaFillDrip, FaFillDrip,
FaPaintBrush, FaPaintBrush,
FaPlus,
FaSlidersH, FaSlidersH,
FaTimes,
} from 'react-icons/fa'; } from 'react-icons/fa';
export const selector = createMemoizedSelector( export const selector = createMemoizedSelector(
@ -230,7 +230,7 @@ const IAICanvasToolChooserOptions = () => {
<InvIconButton <InvIconButton
aria-label={`${t('unifiedCanvas.eraseBoundingBox')} (Del/Backspace)`} aria-label={`${t('unifiedCanvas.eraseBoundingBox')} (Del/Backspace)`}
tooltip={`${t('unifiedCanvas.eraseBoundingBox')} (Del/Backspace)`} tooltip={`${t('unifiedCanvas.eraseBoundingBox')} (Del/Backspace)`}
icon={<FaPlus style={{ transform: 'rotate(45deg)' }} />} icon={<FaTimes />}
isDisabled={isStaging} isDisabled={isStaging}
onClick={handleEraseBoundingBox} onClick={handleEraseBoundingBox}
/> />

View File

@ -16,8 +16,9 @@ import type {
TypesafeDraggableData, TypesafeDraggableData,
} from 'features/dnd/types'; } from 'features/dnd/types';
import { customPointerWithin } from 'features/dnd/util/customPointerWithin'; import { customPointerWithin } from 'features/dnd/util/customPointerWithin';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import type { PropsWithChildren } from 'react'; import type { CSSProperties, PropsWithChildren } from 'react';
import { memo, useCallback, useState } from 'react'; import { memo, useCallback, useState } from 'react';
import { DndContextTypesafe } from './DndContextTypesafe'; import { DndContextTypesafe } from './DndContextTypesafe';
@ -90,29 +91,15 @@ const AppDndContext = (props: PropsWithChildren) => {
<DragOverlay <DragOverlay
dropAnimation={null} dropAnimation={null}
modifiers={[scaledModifier]} modifiers={[scaledModifier]}
style={{ style={dragOverlayStyles}
width: 'min-content',
height: 'min-content',
cursor: 'grabbing',
userSelect: 'none',
// expand overlay to prevent cursor from going outside it and displaying
padding: '10rem',
}}
> >
<AnimatePresence> <AnimatePresence>
{activeDragData && ( {activeDragData && (
<motion.div <motion.div
layout layout
key="overlay-drag-image" key="overlay-drag-image"
initial={{ initial={initial}
opacity: 0, animate={animate}
scale: 0.7,
}}
animate={{
opacity: 1,
scale: 1,
transition: { duration: 0.1 },
}}
> >
<DragPreview dragData={activeDragData} /> <DragPreview dragData={activeDragData} />
</motion.div> </motion.div>
@ -124,3 +111,22 @@ const AppDndContext = (props: PropsWithChildren) => {
}; };
export default memo(AppDndContext); export default memo(AppDndContext);
const dragOverlayStyles: CSSProperties = {
width: 'min-content',
height: 'min-content',
cursor: 'grabbing',
userSelect: 'none',
// expand overlay to prevent cursor from going outside it and displaying
padding: '10rem',
};
const initial: AnimationProps['initial'] = {
opacity: 0,
scale: 0.7,
};
const animate: AnimationProps['animate'] = {
opacity: 1,
scale: 1,
transition: { duration: 0.1 },
};

View File

@ -1,3 +1,4 @@
import type { ChakraProps } from '@chakra-ui/react';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { InvControl } from 'common/components/InvControl/InvControl'; import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSelect } from 'common/components/InvSelect/InvSelect'; import { InvSelect } from 'common/components/InvSelect/InvSelect';
@ -68,8 +69,12 @@ export const EmbeddingSelect = ({
onChange={onChange} onChange={onChange}
onMenuClose={onClose} onMenuClose={onClose}
data-testid="add-embedding" data-testid="add-embedding"
w="full" sx={selectStyles}
/> />
</InvControl> </InvControl>
); );
}; };
const selectStyles: ChakraProps['sx'] = {
w: 'full',
};

View File

@ -93,7 +93,7 @@ const BoardContextMenu = ({
const renderMenuFunc = useCallback( const renderMenuFunc = useCallback(
() => ( () => (
<InvMenuList visibility="visible !important" onContextMenu={skipEvent}> <InvMenuList visibility="visible" onContextMenu={skipEvent}>
<InvMenuGroup title={boardName}> <InvMenuGroup title={boardName}>
<InvMenuItem <InvMenuItem
icon={<FaPlus />} icon={<FaPlus />}

View File

@ -16,15 +16,8 @@ import { useNextPrevImage } from 'features/gallery/hooks/useNextPrevImage';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
import type { AnimationProps } from 'framer-motion'; import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import type { import type { CSSProperties } from 'react';
CSSProperties} from 'react'; import { memo, useCallback, useMemo, useRef, useState } from 'react';
import {
memo,
useCallback,
useMemo,
useRef,
useState,
} from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaImage } from 'react-icons/fa'; import { FaImage } from 'react-icons/fa';

View File

@ -43,7 +43,9 @@ export const LoRACard = memo((props: LoRACardProps) => {
return ( return (
<InvCard variant="lora"> <InvCard variant="lora">
<InvCardHeader> <InvCardHeader>
<InvText noOfLines={1} wordBreak='break-all'>{lora.model_name}</InvText> <InvText noOfLines={1} wordBreak="break-all">
{lora.model_name}
</InvText>
<InvIconButton <InvIconButton
aria-label="Remove LoRA" aria-label="Remove LoRA"
variant="ghost" variant="ghost"

View File

@ -1,3 +1,4 @@
import type { ChakraProps } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
@ -69,10 +70,14 @@ const LoRASelect = () => {
noOptionsMessage={noOptionsMessage} noOptionsMessage={noOptionsMessage}
onChange={onChange} onChange={onChange}
data-testid="add-lora" data-testid="add-lora"
w="full" sx={selectStyles}
/> />
</InvControl> </InvControl>
); );
}; };
export default memo(LoRASelect); export default memo(LoRASelect);
const selectStyles: ChakraProps['sx'] = {
w: 'full',
};

View File

@ -11,7 +11,7 @@ import CheckpointConfigsSelect from 'features/modelManager/subpanels/shared/Chec
import ModelVariantSelect from 'features/modelManager/subpanels/shared/ModelVariantSelect'; import ModelVariantSelect from 'features/modelManager/subpanels/shared/ModelVariantSelect';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast'; import { makeToast } from 'features/system/util/makeToast';
import type { FocusEventHandler } from 'react'; import type { CSSProperties, FocusEventHandler } from 'react';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAddMainModelsMutation } from 'services/api/endpoints/models'; import { useAddMainModelsMutation } from 'services/api/endpoints/models';
@ -112,7 +112,7 @@ export default function AdvancedAddCheckpoint(
onSubmit={advancedAddCheckpointForm.onSubmit((v) => onSubmit={advancedAddCheckpointForm.onSubmit((v) =>
advancedAddCheckpointFormHandler(v) advancedAddCheckpointFormHandler(v)
)} )}
style={{ width: '100%' }} style={formStyles}
> >
<Flex flexDirection="column" gap={2}> <Flex flexDirection="column" gap={2}>
<InvControl label={t('modelManager.model')} isRequired> <InvControl label={t('modelManager.model')} isRequired>
@ -148,7 +148,6 @@ export default function AdvancedAddCheckpoint(
{!useCustomConfig ? ( {!useCustomConfig ? (
<CheckpointConfigsSelect <CheckpointConfigsSelect
required required
w="full"
{...advancedAddCheckpointForm.getInputProps('config')} {...advancedAddCheckpointForm.getInputProps('config')}
/> />
) : ( ) : (
@ -175,3 +174,7 @@ export default function AdvancedAddCheckpoint(
</form> </form>
); );
} }
const formStyles: CSSProperties = {
width: '100%',
};

View File

@ -9,7 +9,7 @@ import BaseModelSelect from 'features/modelManager/subpanels/shared/BaseModelSel
import ModelVariantSelect from 'features/modelManager/subpanels/shared/ModelVariantSelect'; import ModelVariantSelect from 'features/modelManager/subpanels/shared/ModelVariantSelect';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast'; import { makeToast } from 'features/system/util/makeToast';
import type { FocusEventHandler } from 'react'; import type { CSSProperties, FocusEventHandler } from 'react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAddMainModelsMutation } from 'services/api/endpoints/models'; import { useAddMainModelsMutation } from 'services/api/endpoints/models';
@ -99,7 +99,7 @@ export default function AdvancedAddDiffusers(props: AdvancedAddDiffusersProps) {
onSubmit={advancedAddDiffusersForm.onSubmit((v) => onSubmit={advancedAddDiffusersForm.onSubmit((v) =>
advancedAddDiffusersFormHandler(v) advancedAddDiffusersFormHandler(v)
)} )}
style={{ width: '100%' }} style={formStyles}
> >
<Flex flexDirection="column" gap={2}> <Flex flexDirection="column" gap={2}>
<InvControl isRequired label={t('modelManager.model')}> <InvControl isRequired label={t('modelManager.model')}>
@ -138,3 +138,7 @@ export default function AdvancedAddDiffusers(props: AdvancedAddDiffusersProps) {
</form> </form>
); );
} }
const formStyles: CSSProperties = {
width: '100%',
};

View File

@ -9,6 +9,7 @@ import {
setAdvancedAddScanModel, setAdvancedAddScanModel,
setSearchFolder, setSearchFolder,
} from 'features/modelManager/store/modelManagerSlice'; } from 'features/modelManager/store/modelManagerSlice';
import type { CSSProperties } from 'react';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaSearch, FaSync, FaTrash } from 'react-icons/fa'; import { FaSearch, FaSync, FaTrash } from 'react-icons/fa';
@ -57,7 +58,7 @@ function SearchFolderForm() {
onSubmit={searchFolderForm.onSubmit((values) => onSubmit={searchFolderForm.onSubmit((values) =>
searchFolderFormSubmitHandler(values) searchFolderFormSubmitHandler(values)
)} )}
style={{ width: '100%' }} style={formStyles}
> >
<Flex w="100%" gap={2} borderRadius={4} alignItems="center"> <Flex w="100%" gap={2} borderRadius={4} alignItems="center">
<Flex w="100%" alignItems="center" gap={4} minH={12}> <Flex w="100%" alignItems="center" gap={4} minH={12}>
@ -127,3 +128,7 @@ function SearchFolderForm() {
} }
export default memo(SearchFolderForm); export default memo(SearchFolderForm);
const formStyles: CSSProperties = {
width: '100%',
};

View File

@ -8,6 +8,7 @@ import { InvSelect } from 'common/components/InvSelect/InvSelect';
import type { InvSelectOption } from 'common/components/InvSelect/types'; import type { InvSelectOption } from 'common/components/InvSelect/types';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast'; import { makeToast } from 'features/system/util/makeToast';
import type { CSSProperties } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useImportMainModelsMutation } from 'services/api/endpoints/models'; import { useImportMainModelsMutation } from 'services/api/endpoints/models';
@ -73,7 +74,7 @@ export default function SimpleAddModels() {
return ( return (
<form <form
onSubmit={addModelForm.onSubmit((v) => handleAddModelSubmit(v))} onSubmit={addModelForm.onSubmit((v) => handleAddModelSubmit(v))}
style={{ width: '100%' }} style={formStyles}
> >
<Flex flexDirection="column" width="100%" gap={4}> <Flex flexDirection="column" width="100%" gap={4}>
<InvControl label={t('modelManager.modelLocation')}> <InvControl label={t('modelManager.modelLocation')}>
@ -97,3 +98,7 @@ export default function SimpleAddModels() {
</form> </form>
); );
} }
const formStyles: CSSProperties = {
width: '100%',
};

View File

@ -123,8 +123,8 @@ export default function ModelListItem(props: ModelListItemProps) {
acceptButtonText={t('modelManager.delete')} acceptButtonText={t('modelManager.delete')}
> >
<Flex rowGap={4} flexDirection="column"> <Flex rowGap={4} flexDirection="column">
<p style={{ fontWeight: 'bold' }}>{t('modelManager.deleteMsg1')}</p> <InvText fontWeight="bold">{t('modelManager.deleteMsg1')}</InvText>
<p>{t('modelManager.deleteMsg2')}</p> <InvText>{t('modelManager.deleteMsg2')}</InvText>
</Flex> </Flex>
</InvConfirmationAlertDialog> </InvConfirmationAlertDialog>
</Flex> </Flex>

View File

@ -1,3 +1,4 @@
import type { ChakraProps } from '@chakra-ui/react';
import { InvControl } from 'common/components/InvControl/InvControl'; import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSelect } from 'common/components/InvSelect/InvSelect'; import { InvSelect } from 'common/components/InvSelect/InvSelect';
import type { import type {
@ -9,6 +10,8 @@ import { useGetCheckpointConfigsQuery } from 'services/api/endpoints/models';
type CheckpointConfigSelectProps = Omit<InvSelectProps, 'options'>; type CheckpointConfigSelectProps = Omit<InvSelectProps, 'options'>;
const sx: ChakraProps['sx'] = { w: 'full' };
export default function CheckpointConfigsSelect( export default function CheckpointConfigsSelect(
props: CheckpointConfigSelectProps props: CheckpointConfigSelectProps
) { ) {
@ -22,6 +25,7 @@ export default function CheckpointConfigsSelect(
<InvSelect <InvSelect
placeholder="Select A Config File" placeholder="Select A Config File"
options={options} options={options}
sx={sx}
{...props} {...props}
/> />
</InvControl> </InvControl>

View File

@ -4,7 +4,9 @@ import { Flex } from '@chakra-ui/react';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel'; import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import type { CSSProperties } from 'react';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { MdDeviceHub } from 'react-icons/md'; import { MdDeviceHub } from 'react-icons/md';
@ -14,6 +16,28 @@ import { Flow } from './flow/Flow';
import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel'; import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel';
import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel'; import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
const isReadyMotionStyles: CSSProperties = {
position: 'relative',
width: '100%',
height: '100%',
};
const notIsReadyMotionStyles: CSSProperties = {
position: 'absolute',
width: '100%',
height: '100%',
};
const initial: AnimationProps['initial'] = {
opacity: 0,
};
const animate: AnimationProps['animate'] = {
opacity: 1,
transition: { duration: 0.2 },
};
const exit: AnimationProps['exit'] = {
opacity: 0,
transition: { duration: 0.2 },
};
const NodeEditor = () => { const NodeEditor = () => {
const isReady = useAppSelector((state) => state.nodes.isReady); const isReady = useAppSelector((state) => state.nodes.isReady);
const { t } = useTranslation(); const { t } = useTranslation();
@ -30,18 +54,10 @@ const NodeEditor = () => {
<AnimatePresence> <AnimatePresence>
{isReady && ( {isReady && (
<motion.div <motion.div
initial={{ initial={initial}
opacity: 0, animate={animate}
}} exit={exit}
animate={{ style={isReadyMotionStyles}
opacity: 1,
transition: { duration: 0.2 },
}}
exit={{
opacity: 0,
transition: { duration: 0.2 },
}}
style={{ position: 'relative', width: '100%', height: '100%' }}
> >
<Flow /> <Flow />
<AddNodePopover /> <AddNodePopover />
@ -54,18 +70,10 @@ const NodeEditor = () => {
<AnimatePresence> <AnimatePresence>
{!isReady && ( {!isReady && (
<motion.div <motion.div
initial={{ initial={initial}
opacity: 0, animate={animate}
}} exit={exit}
animate={{ style={notIsReadyMotionStyles}
opacity: 1,
transition: { duration: 0.2 },
}}
exit={{
opacity: 0,
transition: { duration: 0.2 },
}}
style={{ position: 'absolute', width: '100%', height: '100%' }}
> >
<Flex <Flex
layerStyle="first" layerStyle="first"

View File

@ -23,8 +23,8 @@ import {
} from 'features/nodes/store/nodesSlice'; } from 'features/nodes/store/nodesSlice';
import { $flow } from 'features/nodes/store/reactFlowInstance'; import { $flow } from 'features/nodes/store/reactFlowInstance';
import { bumpGlobalMenuCloseTrigger } from 'features/ui/store/uiSlice'; import { bumpGlobalMenuCloseTrigger } from 'features/ui/store/uiSlice';
import type { MouseEvent } from 'react'; import type { CSSProperties, MouseEvent } from 'react';
import { useCallback, useRef } from 'react'; import { useCallback, useMemo, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import type { import type {
OnConnect, OnConnect,
@ -75,6 +75,8 @@ const selector = createMemoizedSelector(stateSelector, ({ nodes }) => {
}; };
}); });
const snapGrid: [number, number] = [25, 25];
export const Flow = () => { export const Flow = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const nodes = useAppSelector((state) => state.nodes.nodes); const nodes = useAppSelector((state) => state.nodes.nodes);
@ -87,6 +89,13 @@ export const Flow = () => {
const [borderRadius] = useToken('radii', ['base']); const [borderRadius] = useToken('radii', ['base']);
const flowStyles = useMemo<CSSProperties>(
() => ({
borderRadius,
}),
[borderRadius]
);
const onNodesChange: OnNodesChange = useCallback( const onNodesChange: OnNodesChange = useCallback(
(changes) => { (changes) => {
dispatch(nodesChanged(changes)); dispatch(nodesChanged(changes));
@ -266,10 +275,10 @@ export const Flow = () => {
isValidConnection={isValidConnection} isValidConnection={isValidConnection}
minZoom={0.1} minZoom={0.1}
snapToGrid={shouldSnapToGrid} snapToGrid={shouldSnapToGrid}
snapGrid={[25, 25]} snapGrid={snapGrid}
connectionRadius={30} connectionRadius={30}
proOptions={proOptions} proOptions={proOptions}
style={{ borderRadius }} style={flowStyles}
onPaneClick={handlePaneClick} onPaneClick={handlePaneClick}
deleteKeyCode={DELETE_KEYS} deleteKeyCode={DELETE_KEYS}
selectionMode={selectionMode} selectionMode={selectionMode}

View File

@ -3,6 +3,7 @@ import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor'; import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor';
import type { CSSProperties } from 'react';
import { memo } from 'react'; import { memo } from 'react';
import type { ConnectionLineComponentProps } from 'reactflow'; import type { ConnectionLineComponentProps } from 'reactflow';
import { getBezierPath } from 'reactflow'; import { getBezierPath } from 'reactflow';
@ -27,6 +28,8 @@ const selector = createMemoizedSelector(stateSelector, ({ nodes }) => {
}; };
}); });
const pathStyles: CSSProperties = { opacity: 0.8 };
const CustomConnectionLine = ({ const CustomConnectionLine = ({
fromX, fromX,
fromY, fromY,
@ -56,7 +59,7 @@ const CustomConnectionLine = ({
strokeWidth={2} strokeWidth={2}
className={className} className={className}
d={dAttr} d={dAttr}
style={{ opacity: 0.8 }} style={pathStyles}
/> />
</g> </g>
); );

View File

@ -47,21 +47,20 @@ const InvocationCollapsedEdge = ({
const { base500 } = useChakraThemeTokens(); const { base500 } = useChakraThemeTokens();
return ( const edgeStyles = useMemo(
<> () => ({
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{
strokeWidth: isSelected ? 3 : 2, strokeWidth: isSelected ? 3 : 2,
stroke: base500, stroke: base500,
opacity: isSelected ? 0.8 : 0.5, opacity: isSelected ? 0.8 : 0.5,
animation: shouldAnimate animation: shouldAnimate ? 'dashdraw 0.5s linear infinite' : undefined,
? 'dashdraw 0.5s linear infinite'
: undefined,
strokeDasharray: shouldAnimate ? 5 : 'none', strokeDasharray: shouldAnimate ? 5 : 'none',
}} }),
/> [base500, isSelected, shouldAnimate]
);
return (
<>
<BaseEdge path={edgePath} markerEnd={markerEnd} style={edgeStyles} />
{data?.count && data.count > 1 && ( {data?.count && data.count > 1 && (
<EdgeLabelRenderer> <EdgeLabelRenderer>
<Flex <Flex

View File

@ -1,4 +1,5 @@
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import type { CSSProperties } from 'react';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import type { EdgeProps } from 'reactflow'; import type { EdgeProps } from 'reactflow';
import { BaseEdge, getBezierPath } from 'reactflow'; import { BaseEdge, getBezierPath } from 'reactflow';
@ -42,19 +43,18 @@ const InvocationDefaultEdge = ({
targetPosition, targetPosition,
}); });
return ( const edgeStyles = useMemo<CSSProperties>(
<BaseEdge () => ({
path={edgePath}
markerEnd={markerEnd}
style={{
strokeWidth: isSelected ? 3 : 2, strokeWidth: isSelected ? 3 : 2,
stroke, stroke,
opacity: isSelected ? 0.8 : 0.5, opacity: isSelected ? 0.8 : 0.5,
animation: shouldAnimate ? 'dashdraw 0.5s linear infinite' : undefined, animation: shouldAnimate ? 'dashdraw 0.5s linear infinite' : undefined,
strokeDasharray: shouldAnimate ? 5 : 'none', strokeDasharray: shouldAnimate ? 5 : 'none',
}} }),
/> [isSelected, shouldAnimate, stroke]
); );
return <BaseEdge path={edgePath} markerEnd={markerEnd} style={edgeStyles} />;
}; };
export default memo(InvocationDefaultEdge); export default memo(InvocationDefaultEdge);

View File

@ -7,8 +7,9 @@ import { InvText } from 'common/components/InvText/wrapper';
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons'; import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper'; import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import type { AnimationProps } from 'framer-motion';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { PropsWithChildren } from 'react'; import type { CSSProperties, PropsWithChildren } from 'react';
import { memo, useCallback, useState } from 'react'; import { memo, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
@ -106,25 +107,10 @@ const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => {
{isHovering && ( {isHovering && (
<motion.div <motion.div
key="nextPrevButtons" key="nextPrevButtons"
initial={{ initial={initial}
opacity: 0, animate={animate}
}} exit={exit}
animate={{ style={styles}
opacity: 1,
transition: { duration: 0.1 },
}}
exit={{
opacity: 0,
transition: { duration: 0.1 },
}}
style={{
position: 'absolute',
top: 40,
left: -2,
right: -2,
bottom: 0,
pointerEvents: 'none',
}}
> >
<NextPrevImageButtons /> <NextPrevImageButtons />
</motion.div> </motion.div>
@ -134,3 +120,23 @@ const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => {
</NodeWrapper> </NodeWrapper>
); );
}; };
const initial: AnimationProps['initial'] = {
opacity: 0,
};
const animate: AnimationProps['animate'] = {
opacity: 1,
transition: { duration: 0.1 },
};
const exit: AnimationProps['exit'] = {
opacity: 0,
transition: { duration: 0.1 },
};
const styles: CSSProperties = {
position: 'absolute',
top: 40,
left: -2,
right: -2,
bottom: 0,
pointerEvents: 'none',
};

View File

@ -10,6 +10,8 @@ interface Props {
nodeId: string; nodeId: string;
} }
const hiddenHandleStyles: CSSProperties = { visibility: 'hidden' };
const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => { const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
const data = useNodeData(nodeId); const data = useNodeData(nodeId);
const { base600 } = useChakraThemeTokens(); const { base600 } = useChakraThemeTokens();
@ -26,6 +28,15 @@ const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
[base600] [base600]
); );
const collapsedTargetStyles: CSSProperties = useMemo(
() => ({ ...dummyHandleStyles, left: '-0.5rem' }),
[dummyHandleStyles]
);
const collapsedSourceStyles: CSSProperties = useMemo(
() => ({ ...dummyHandleStyles, right: '-0.5rem' }),
[dummyHandleStyles]
);
if (!isInvocationNodeData(data)) { if (!isInvocationNodeData(data)) {
return null; return null;
} }
@ -37,7 +48,7 @@ const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
id={`${data.id}-collapsed-target`} id={`${data.id}-collapsed-target`}
isConnectable={false} isConnectable={false}
position={Position.Left} position={Position.Left}
style={{ ...dummyHandleStyles, left: '-0.5rem' }} style={collapsedTargetStyles}
/> />
{map(data.inputs, (input) => ( {map(data.inputs, (input) => (
<Handle <Handle
@ -46,7 +57,7 @@ const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
id={input.name} id={input.name}
isConnectable={false} isConnectable={false}
position={Position.Left} position={Position.Left}
style={{ visibility: 'hidden' }} style={hiddenHandleStyles}
/> />
))} ))}
<Handle <Handle
@ -54,7 +65,7 @@ const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
id={`${data.id}-collapsed-source`} id={`${data.id}-collapsed-source`}
isConnectable={false} isConnectable={false}
position={Position.Right} position={Position.Right}
style={{ ...dummyHandleStyles, right: '-0.5rem' }} style={collapsedSourceStyles}
/> />
{map(data.outputs, (output) => ( {map(data.outputs, (output) => (
<Handle <Handle
@ -63,7 +74,7 @@ const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
id={output.name} id={output.name}
isConnectable={false} isConnectable={false}
position={Position.Right} position={Position.Right}
style={{ visibility: 'hidden' }} style={hiddenHandleStyles}
/> />
))} ))}
</> </>

View File

@ -4,6 +4,7 @@ import { Flex } from '@chakra-ui/react';
import QueueControls from 'features/queue/components/QueueControls'; import QueueControls from 'features/queue/components/QueueControls';
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle'; import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage'; import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
import type { CSSProperties } from 'react';
import { memo, useCallback, useRef, useState } from 'react'; import { memo, useCallback, useRef, useState } from 'react';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels'; import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels'; import { Panel, PanelGroup } from 'react-resizable-panels';
@ -11,6 +12,8 @@ import { Panel, PanelGroup } from 'react-resizable-panels';
import InspectorPanel from './inspector/InspectorPanel'; import InspectorPanel from './inspector/InspectorPanel';
import WorkflowPanel from './workflow/WorkflowPanel'; import WorkflowPanel from './workflow/WorkflowPanel';
const panelGroupStyles: CSSProperties = { height: '100%', width: '100%' };
const NodeEditorPanelGroup = () => { const NodeEditorPanelGroup = () => {
const [isTopPanelCollapsed, setIsTopPanelCollapsed] = useState(false); const [isTopPanelCollapsed, setIsTopPanelCollapsed] = useState(false);
const [isBottomPanelCollapsed, setIsBottomPanelCollapsed] = useState(false); const [isBottomPanelCollapsed, setIsBottomPanelCollapsed] = useState(false);
@ -31,7 +34,7 @@ const NodeEditorPanelGroup = () => {
id="workflow-panel-group" id="workflow-panel-group"
autoSaveId="workflow-panel-group" autoSaveId="workflow-panel-group"
direction="vertical" direction="vertical"
style={{ height: '100%', width: '100%' }} style={panelGroupStyles}
storage={panelStorage} storage={panelStorage}
> >
<Panel <Panel

View File

@ -2,6 +2,7 @@
import { Flex, Image } from '@chakra-ui/react'; import { Flex, Image } from '@chakra-ui/react';
import InvokeAILogoImage from 'assets/images/logo.png'; import InvokeAILogoImage from 'assets/images/logo.png';
import { InvText } from 'common/components/InvText/wrapper'; import { InvText } from 'common/components/InvText/wrapper';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { memo, useRef } from 'react'; import { memo, useRef } from 'react';
import { useHoverDirty } from 'react-use'; import { useHoverDirty } from 'react-use';
@ -35,17 +36,9 @@ const InvokeAILogoComponent = ({ showVersion = true }: Props) => {
{showVersion && isHovered && appVersion && ( {showVersion && isHovered && appVersion && (
<motion.div <motion.div
key="statusText" key="statusText"
initial={{ initial={initial}
opacity: 0, animate={animate}
}} exit={exit}
animate={{
opacity: 1,
transition: { duration: 0.15 },
}}
exit={{
opacity: 0,
transition: { delay: 0.8 },
}}
> >
<InvText <InvText
fontWeight="semibold" fontWeight="semibold"
@ -64,3 +57,15 @@ const InvokeAILogoComponent = ({ showVersion = true }: Props) => {
}; };
export default memo(InvokeAILogoComponent); export default memo(InvokeAILogoComponent);
const initial: AnimationProps['initial'] = {
opacity: 0,
};
const animate: AnimationProps['animate'] = {
opacity: 1,
transition: { duration: 0.1 },
};
const exit: AnimationProps['exit'] = {
opacity: 0,
transition: { delay: 0.8 },
};

View File

@ -4,6 +4,7 @@ import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { InvText } from 'common/components/InvText/wrapper'; import { InvText } from 'common/components/InvText/wrapper';
import { STATUS_TRANSLATION_KEYS } from 'features/system/store/types'; import { STATUS_TRANSLATION_KEYS } from 'features/system/store/types';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import type { ResourceKey } from 'i18next'; import type { ResourceKey } from 'i18next';
import { memo, useMemo, useRef } from 'react'; import { memo, useMemo, useRef } from 'react';
@ -58,17 +59,9 @@ const StatusIndicator = () => {
{isHovered && ( {isHovered && (
<motion.div <motion.div
key="statusText" key="statusText"
initial={{ initial={initial}
opacity: 0, animate={animate}
}} exit={exit}
animate={{
opacity: 1,
transition: { duration: 0.15 },
}}
exit={{
opacity: 0,
transition: { delay: 0.8 },
}}
> >
<InvText <InvText
fontSize="sm" fontSize="sm"
@ -88,3 +81,15 @@ const StatusIndicator = () => {
}; };
export default memo(StatusIndicator); export default memo(StatusIndicator);
const initial: AnimationProps['initial'] = {
opacity: 0,
};
const animate: AnimationProps['animate'] = {
opacity: 1,
transition: { duration: 0.1 },
};
const exit: AnimationProps['exit'] = {
opacity: 0,
transition: { delay: 0.8 },
};

View File

@ -21,7 +21,7 @@ import {
activeTabNameSelector, activeTabNameSelector,
} from 'features/ui/store/uiSelectors'; } from 'features/ui/store/uiSelectors';
import { setActiveTab } from 'features/ui/store/uiSlice'; import { setActiveTab } from 'features/ui/store/uiSlice';
import type { MouseEvent, ReactElement, ReactNode } from 'react'; import type { CSSProperties, MouseEvent, ReactElement, ReactNode } from 'react';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -101,6 +101,7 @@ const GALLERY_PANEL_MIN_SIZE_PX = 360;
export const NO_GALLERY_TABS: InvokeTabName[] = ['modelManager', 'queue']; export const NO_GALLERY_TABS: InvokeTabName[] = ['modelManager', 'queue'];
export const NO_SIDE_PANEL_TABS: InvokeTabName[] = ['modelManager', 'queue']; export const NO_SIDE_PANEL_TABS: InvokeTabName[] = ['modelManager', 'queue'];
const panelStyles: CSSProperties = { height: '100%', width: '100%' };
const InvokeTabs = () => { const InvokeTabs = () => {
const activeTabIndex = useAppSelector(activeTabIndexSelector); const activeTabIndex = useAppSelector(activeTabIndexSelector);
@ -231,7 +232,7 @@ const InvokeTabs = () => {
id="app" id="app"
autoSaveId="app" autoSaveId="app"
direction="horizontal" direction="horizontal"
style={{ height: '100%', width: '100%' }} style={panelStyles}
storage={panelStorage} storage={panelStorage}
units="pixels" units="pixels"
> >
@ -263,9 +264,7 @@ const InvokeTabs = () => {
</> </>
)} */} )} */}
<Panel id="main" order={1} minSize={MAIN_PANEL_MIN_SIZE_PX}> <Panel id="main" order={1} minSize={MAIN_PANEL_MIN_SIZE_PX}>
<InvTabPanels style={{ height: '100%', width: '100%' }}> <InvTabPanels style={panelStyles}>{tabPanels}</InvTabPanels>
{tabPanels}
</InvTabPanels>
</Panel> </Panel>
{!NO_GALLERY_TABS.includes(activeTabName) && ( {!NO_GALLERY_TABS.includes(activeTabName) && (
<> <>

View File

@ -13,8 +13,14 @@ import { ImageSettingsAccordion } from 'features/settingsAccordions/ImageSetting
import { RefinerSettingsAccordion } from 'features/settingsAccordions/RefinerSettingsAccordion/RefinerSettingsAccordion'; import { RefinerSettingsAccordion } from 'features/settingsAccordions/RefinerSettingsAccordion/RefinerSettingsAccordion';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import type { CSSProperties } from 'react';
import { memo } from 'react'; import { memo } from 'react';
const overlayScrollbarsStyles: CSSProperties = {
height: '100%',
width: '100%',
};
const ParametersPanel = () => { const ParametersPanel = () => {
const activeTabName = useAppSelector(activeTabNameSelector); const activeTabName = useAppSelector(activeTabNameSelector);
const isSDXL = useAppSelector( const isSDXL = useAppSelector(
@ -28,7 +34,7 @@ const ParametersPanel = () => {
<Box position="absolute" top={0} left={0} right={0} bottom={0}> <Box position="absolute" top={0} left={0} right={0} bottom={0}>
<OverlayScrollbarsComponent <OverlayScrollbarsComponent
defer defer
style={{ height: '100%', width: '100%' }} style={overlayScrollbarsStyles}
options={overlayScrollbarsParams.options} options={overlayScrollbarsParams.options}
> >
<Flex gap={2} flexDirection="column" h="full" w="full"> <Flex gap={2} flexDirection="column" h="full" w="full">

View File

@ -3,10 +3,19 @@ import InitialImageDisplay from 'features/parameters/components/ImageToImage/Ini
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle'; import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import TextToImageTabMain from 'features/ui/components/tabs/TextToImageTab'; import TextToImageTabMain from 'features/ui/components/tabs/TextToImageTab';
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage'; import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
import type { CSSProperties } from 'react';
import { memo, useCallback, useRef } from 'react'; import { memo, useCallback, useRef } from 'react';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels'; import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels'; import { Panel, PanelGroup } from 'react-resizable-panels';
const panelGroupStyles: CSSProperties = {
height: '100%',
width: '100%',
};
const panelStyles: CSSProperties = {
position: 'relative',
};
const ImageToImageTab = () => { const ImageToImageTab = () => {
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null); const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
@ -25,7 +34,7 @@ const ImageToImageTab = () => {
ref={panelGroupRef} ref={panelGroupRef}
autoSaveId="imageTab.content" autoSaveId="imageTab.content"
direction="horizontal" direction="horizontal"
style={{ height: '100%', width: '100%' }} style={panelGroupStyles}
storage={panelStorage} storage={panelStorage}
units="percentages" units="percentages"
> >
@ -34,7 +43,7 @@ const ImageToImageTab = () => {
order={0} order={0}
defaultSize={50} defaultSize={50}
minSize={25} minSize={25}
style={{ position: 'relative' }} style={panelStyles}
> >
<InitialImageDisplay /> <InitialImageDisplay />
</Panel> </Panel>