mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): style settings modal
This commit is contained in:
parent
fcf2006502
commit
bbca053b48
@ -16,6 +16,7 @@ export const baseStyle = definePartsStyle(() => ({
|
||||
header: {
|
||||
fontWeight: 'semibold',
|
||||
fontSize: 'lg',
|
||||
color: 'base.300'
|
||||
},
|
||||
closeButton: {
|
||||
opacity: 0.5,
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { InvHeading } from 'common/components/InvHeading/wrapper';
|
||||
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
|
||||
import { InvInput } from 'common/components/InvInput/InvInput';
|
||||
import {
|
||||
@ -22,6 +21,7 @@ import {
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import type { HotkeyGroup } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { useHotkeyData } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { StickyScrollable } from 'features/system/components/StickyScrollable';
|
||||
import type { ChangeEventHandler, ReactElement } from 'react';
|
||||
import {
|
||||
cloneElement,
|
||||
@ -121,26 +121,9 @@ const HotkeysModal = ({ children }: HotkeysModalProps) => {
|
||||
</InputGroup>
|
||||
|
||||
<ScrollableContent>
|
||||
{filteredHotkeyGroups.map((group) => (
|
||||
<Flex key={group.title} pb={4} flexDir="column">
|
||||
<Flex
|
||||
ps={2}
|
||||
pb={4}
|
||||
position="sticky"
|
||||
zIndex={1}
|
||||
top={0}
|
||||
bg="base.800"
|
||||
>
|
||||
<InvHeading size="sm">{group.title}</InvHeading>
|
||||
</Flex>
|
||||
<Flex
|
||||
key={group.title}
|
||||
p={4}
|
||||
borderRadius="base"
|
||||
bg="base.750"
|
||||
flexDir="column"
|
||||
gap={4}
|
||||
>
|
||||
<Flex flexDir="column" gap={4}>
|
||||
{filteredHotkeyGroups.map((group) => (
|
||||
<StickyScrollable key={group.title} title={group.title}>
|
||||
{group.hotkeyListItems.map((hotkey, i) => (
|
||||
<Fragment key={i}>
|
||||
<HotkeyListItem
|
||||
@ -151,15 +134,15 @@ const HotkeysModal = ({ children }: HotkeysModalProps) => {
|
||||
{i < group.hotkeyListItems.length - 1 && <Divider />}
|
||||
</Fragment>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
))}
|
||||
{!filteredHotkeyGroups.length && (
|
||||
<IAINoContentFallback
|
||||
label={t('hotkeys.noHotkeysFound')}
|
||||
icon={null}
|
||||
/>
|
||||
)}
|
||||
</StickyScrollable>
|
||||
))}
|
||||
{!filteredHotkeyGroups.length && (
|
||||
<IAINoContentFallback
|
||||
label={t('hotkeys.noHotkeysFound')}
|
||||
icon={null}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
</InvModalBody>
|
||||
<InvModalFooter />
|
||||
|
@ -1,93 +0,0 @@
|
||||
import { Heading } from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { InvButton } from 'common/components/InvButton/InvButton';
|
||||
import { InvText } from 'common/components/InvText/wrapper';
|
||||
import { resetCanvas } from 'features/canvas/store/canvasSlice';
|
||||
import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
useClearIntermediatesMutation,
|
||||
useGetIntermediatesCountQuery,
|
||||
} from 'services/api/endpoints/images';
|
||||
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
import StyledFlex from './StyledFlex';
|
||||
|
||||
const SettingsClearIntermediates = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { data: intermediatesCount } = useGetIntermediatesCountQuery(
|
||||
undefined,
|
||||
{
|
||||
refetchOnMountOrArgChange: true,
|
||||
}
|
||||
);
|
||||
|
||||
const [clearIntermediates, { isLoading: isLoadingClearIntermediates }] =
|
||||
useClearIntermediatesMutation();
|
||||
|
||||
const { data: queueStatus } = useGetQueueStatusQuery();
|
||||
const hasPendingItems =
|
||||
queueStatus &&
|
||||
(queueStatus.queue.in_progress > 0 || queueStatus.queue.pending > 0);
|
||||
|
||||
const handleClickClearIntermediates = useCallback(() => {
|
||||
if (hasPendingItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearIntermediates()
|
||||
.unwrap()
|
||||
.then((clearedCount) => {
|
||||
dispatch(controlAdaptersReset());
|
||||
dispatch(resetCanvas());
|
||||
dispatch(
|
||||
addToast({
|
||||
title: t('settings.intermediatesCleared', { count: clearedCount }),
|
||||
status: 'info',
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(
|
||||
addToast({
|
||||
title: t('settings.intermediatesClearedFailed'),
|
||||
status: 'error',
|
||||
})
|
||||
);
|
||||
});
|
||||
}, [t, clearIntermediates, dispatch, hasPendingItems]);
|
||||
|
||||
return (
|
||||
<StyledFlex>
|
||||
<Heading size="sm">{t('settings.clearIntermediates')}</Heading>
|
||||
<InvButton
|
||||
tooltip={
|
||||
hasPendingItems ? t('settings.clearIntermediatesDisabled') : undefined
|
||||
}
|
||||
colorScheme="warning"
|
||||
onClick={handleClickClearIntermediates}
|
||||
isLoading={isLoadingClearIntermediates}
|
||||
isDisabled={!intermediatesCount || hasPendingItems}
|
||||
>
|
||||
{t('settings.clearIntermediatesWithCount', {
|
||||
count: intermediatesCount ?? 0,
|
||||
})}
|
||||
</InvButton>
|
||||
<InvText fontWeight="bold">
|
||||
{t('settings.clearIntermediatesDesc1')}
|
||||
</InvText>
|
||||
<InvText variant="subtext">
|
||||
{t('settings.clearIntermediatesDesc2')}
|
||||
</InvText>
|
||||
<InvText variant="subtext">
|
||||
{t('settings.clearIntermediatesDesc3')}
|
||||
</InvText>
|
||||
</StyledFlex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SettingsClearIntermediates);
|
@ -4,7 +4,6 @@ import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvButton } from 'common/components/InvButton/InvButton';
|
||||
import { InvControl } from 'common/components/InvControl/InvControl';
|
||||
import { InvHeading } from 'common/components/InvHeading/wrapper';
|
||||
import {
|
||||
InvModal,
|
||||
InvModalBody,
|
||||
@ -16,8 +15,11 @@ import {
|
||||
} from 'common/components/InvModal/wrapper';
|
||||
import { InvSwitch } from 'common/components/InvSwitch/wrapper';
|
||||
import { InvText } from 'common/components/InvText/wrapper';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import { useClearStorage } from 'common/hooks/useClearStorage';
|
||||
import { shouldUseCpuNoiseChanged } from 'features/parameters/store/generationSlice';
|
||||
import { useClearIntermediates } from 'features/system/components/SettingsModal/useClearIntermediates';
|
||||
import { StickyScrollable } from 'features/system/components/StickyScrollable';
|
||||
import {
|
||||
setEnableImageDebugging,
|
||||
setShouldConfirmOnDelete,
|
||||
@ -33,10 +35,8 @@ import { cloneElement, memo, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetAppConfigQuery } from 'services/api/endpoints/appInfo';
|
||||
|
||||
import SettingsClearIntermediates from './SettingsClearIntermediates';
|
||||
import { SettingsLanguageSelect } from './SettingsLanguageSelect';
|
||||
import { SettingsLogLevelSelect } from './SettingsLogLevelSelect';
|
||||
import StyledFlex from './StyledFlex';
|
||||
|
||||
const selector = createMemoizedSelector(
|
||||
[stateSelector],
|
||||
@ -110,6 +110,13 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
||||
}),
|
||||
});
|
||||
|
||||
const {
|
||||
clearIntermediates,
|
||||
hasPendingItems,
|
||||
intermediatesCount,
|
||||
isLoading: isLoadingClearIntermediates,
|
||||
} = useClearIntermediates();
|
||||
|
||||
const {
|
||||
isOpen: isSettingsModalOpen,
|
||||
onOpen: onSettingsModalOpen,
|
||||
@ -218,120 +225,143 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
||||
isCentered
|
||||
>
|
||||
<InvModalOverlay />
|
||||
<InvModalContent>
|
||||
<InvModalContent maxH="80vh" h="80vh">
|
||||
<InvModalHeader bg="none">{t('common.settingsLabel')}</InvModalHeader>
|
||||
<InvModalCloseButton />
|
||||
<InvModalBody>
|
||||
<Flex gap={4} flexDir="column">
|
||||
<StyledFlex>
|
||||
<InvHeading size="sm">{t('settings.general')}</InvHeading>
|
||||
<InvControl label={t('settings.confirmOnDelete')}>
|
||||
<InvSwitch
|
||||
isChecked={shouldConfirmOnDelete}
|
||||
onChange={handleChangeShouldConfirmOnDelete}
|
||||
/>
|
||||
</InvControl>
|
||||
</StyledFlex>
|
||||
|
||||
<StyledFlex>
|
||||
<InvHeading size="sm">{t('settings.generation')}</InvHeading>
|
||||
<InvControl
|
||||
label={t('settings.enableNSFWChecker')}
|
||||
isDisabled={!isNSFWCheckerAvailable}
|
||||
>
|
||||
<InvSwitch
|
||||
isChecked={shouldUseNSFWChecker}
|
||||
onChange={handleChangeShouldUseNSFWChecker}
|
||||
/>
|
||||
</InvControl>
|
||||
<InvControl
|
||||
label={t('settings.enableInvisibleWatermark')}
|
||||
isDisabled={!isWatermarkerAvailable}
|
||||
>
|
||||
<InvSwitch
|
||||
isChecked={shouldUseWatermarker}
|
||||
onChange={handleChangeShouldUseWatermarker}
|
||||
/>
|
||||
</InvControl>
|
||||
</StyledFlex>
|
||||
|
||||
<StyledFlex>
|
||||
<InvHeading size="sm">{t('settings.ui')}</InvHeading>
|
||||
<InvControl label={t('settings.showProgressInViewer')}>
|
||||
<InvSwitch
|
||||
isChecked={shouldShowProgressInViewer}
|
||||
onChange={handleChangeShouldShowProgressInViewer}
|
||||
/>
|
||||
</InvControl>
|
||||
<InvControl label={t('settings.antialiasProgressImages')}>
|
||||
<InvSwitch
|
||||
isChecked={shouldAntialiasProgressImage}
|
||||
onChange={handleChangeShouldAntialiasProgressImage}
|
||||
/>
|
||||
</InvControl>
|
||||
<InvControl
|
||||
label={t('parameters.useCpuNoise')}
|
||||
feature="noiseUseCPU"
|
||||
>
|
||||
<InvSwitch
|
||||
isChecked={shouldUseCpuNoise}
|
||||
onChange={handleChangeShouldUseCpuNoise}
|
||||
/>
|
||||
</InvControl>
|
||||
{shouldShowLocalizationToggle && <SettingsLanguageSelect />}
|
||||
<InvControl label={t('settings.enableInformationalPopovers')}>
|
||||
<InvSwitch
|
||||
isChecked={shouldEnableInformationalPopovers}
|
||||
onChange={handleChangeShouldEnableInformationalPopovers}
|
||||
/>
|
||||
</InvControl>
|
||||
</StyledFlex>
|
||||
|
||||
{shouldShowDeveloperSettings && (
|
||||
<StyledFlex>
|
||||
<InvHeading size="sm">{t('settings.developer')}</InvHeading>
|
||||
<InvControl label={t('settings.shouldLogToConsole')}>
|
||||
<InvModalBody display="flex" flexDir="column" gap={4}>
|
||||
<ScrollableContent>
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<StickyScrollable title={t('settings.general')}>
|
||||
<InvControl label={t('settings.confirmOnDelete')}>
|
||||
<InvSwitch
|
||||
isChecked={shouldLogToConsole}
|
||||
onChange={handleLogToConsoleChanged}
|
||||
isChecked={shouldConfirmOnDelete}
|
||||
onChange={handleChangeShouldConfirmOnDelete}
|
||||
/>
|
||||
</InvControl>
|
||||
<SettingsLogLevelSelect />
|
||||
<InvControl label={t('settings.enableImageDebugging')}>
|
||||
</StickyScrollable>
|
||||
|
||||
<StickyScrollable title={t('settings.generation')}>
|
||||
<InvControl
|
||||
label={t('settings.enableNSFWChecker')}
|
||||
isDisabled={!isNSFWCheckerAvailable}
|
||||
>
|
||||
<InvSwitch
|
||||
isChecked={enableImageDebugging}
|
||||
onChange={handleChangeEnableImageDebugging}
|
||||
isChecked={shouldUseNSFWChecker}
|
||||
onChange={handleChangeShouldUseNSFWChecker}
|
||||
/>
|
||||
</InvControl>
|
||||
</StyledFlex>
|
||||
)}
|
||||
<InvControl
|
||||
label={t('settings.enableInvisibleWatermark')}
|
||||
isDisabled={!isWatermarkerAvailable}
|
||||
>
|
||||
<InvSwitch
|
||||
isChecked={shouldUseWatermarker}
|
||||
onChange={handleChangeShouldUseWatermarker}
|
||||
/>
|
||||
</InvControl>
|
||||
</StickyScrollable>
|
||||
|
||||
{shouldShowClearIntermediates && <SettingsClearIntermediates />}
|
||||
<StickyScrollable title={t('settings.ui')}>
|
||||
<InvControl label={t('settings.showProgressInViewer')}>
|
||||
<InvSwitch
|
||||
isChecked={shouldShowProgressInViewer}
|
||||
onChange={handleChangeShouldShowProgressInViewer}
|
||||
/>
|
||||
</InvControl>
|
||||
<InvControl label={t('settings.antialiasProgressImages')}>
|
||||
<InvSwitch
|
||||
isChecked={shouldAntialiasProgressImage}
|
||||
onChange={handleChangeShouldAntialiasProgressImage}
|
||||
/>
|
||||
</InvControl>
|
||||
<InvControl
|
||||
label={t('parameters.useCpuNoise')}
|
||||
feature="noiseUseCPU"
|
||||
>
|
||||
<InvSwitch
|
||||
isChecked={shouldUseCpuNoise}
|
||||
onChange={handleChangeShouldUseCpuNoise}
|
||||
/>
|
||||
</InvControl>
|
||||
{shouldShowLocalizationToggle && <SettingsLanguageSelect />}
|
||||
<InvControl label={t('settings.enableInformationalPopovers')}>
|
||||
<InvSwitch
|
||||
isChecked={shouldEnableInformationalPopovers}
|
||||
onChange={handleChangeShouldEnableInformationalPopovers}
|
||||
/>
|
||||
</InvControl>
|
||||
</StickyScrollable>
|
||||
|
||||
<StyledFlex>
|
||||
<InvHeading size="sm">{t('settings.resetWebUI')}</InvHeading>
|
||||
<InvButton colorScheme="error" onClick={handleClickResetWebUI}>
|
||||
{t('settings.resetWebUI')}
|
||||
</InvButton>
|
||||
{shouldShowResetWebUiText && (
|
||||
<>
|
||||
<InvText variant="subtext">
|
||||
{t('settings.resetWebUIDesc1')}
|
||||
</InvText>
|
||||
<InvText variant="subtext">
|
||||
{t('settings.resetWebUIDesc2')}
|
||||
</InvText>
|
||||
</>
|
||||
{shouldShowDeveloperSettings && (
|
||||
<StickyScrollable title={t('settings.developer')}>
|
||||
<InvControl label={t('settings.shouldLogToConsole')}>
|
||||
<InvSwitch
|
||||
isChecked={shouldLogToConsole}
|
||||
onChange={handleLogToConsoleChanged}
|
||||
/>
|
||||
</InvControl>
|
||||
<SettingsLogLevelSelect />
|
||||
<InvControl label={t('settings.enableImageDebugging')}>
|
||||
<InvSwitch
|
||||
isChecked={enableImageDebugging}
|
||||
onChange={handleChangeEnableImageDebugging}
|
||||
/>
|
||||
</InvControl>
|
||||
</StickyScrollable>
|
||||
)}
|
||||
</StyledFlex>
|
||||
</Flex>
|
||||
|
||||
{shouldShowClearIntermediates && (
|
||||
<StickyScrollable title={t('settings.clearIntermediates')}>
|
||||
<InvButton
|
||||
tooltip={
|
||||
hasPendingItems
|
||||
? t('settings.clearIntermediatesDisabled')
|
||||
: undefined
|
||||
}
|
||||
colorScheme="warning"
|
||||
onClick={clearIntermediates}
|
||||
isLoading={isLoadingClearIntermediates}
|
||||
isDisabled={!intermediatesCount || hasPendingItems}
|
||||
>
|
||||
{t('settings.clearIntermediatesWithCount', {
|
||||
count: intermediatesCount ?? 0,
|
||||
})}
|
||||
</InvButton>
|
||||
<InvText fontWeight="bold">
|
||||
{t('settings.clearIntermediatesDesc1')}
|
||||
</InvText>
|
||||
<InvText variant="subtext">
|
||||
{t('settings.clearIntermediatesDesc2')}
|
||||
</InvText>
|
||||
<InvText variant="subtext">
|
||||
{t('settings.clearIntermediatesDesc3')}
|
||||
</InvText>
|
||||
</StickyScrollable>
|
||||
)}
|
||||
|
||||
<StickyScrollable title={t('settings.resetWebUI')}>
|
||||
<InvButton
|
||||
colorScheme="error"
|
||||
onClick={handleClickResetWebUI}
|
||||
>
|
||||
{t('settings.resetWebUI')}
|
||||
</InvButton>
|
||||
{shouldShowResetWebUiText && (
|
||||
<>
|
||||
<InvText variant="subtext">
|
||||
{t('settings.resetWebUIDesc1')}
|
||||
</InvText>
|
||||
<InvText variant="subtext">
|
||||
{t('settings.resetWebUIDesc2')}
|
||||
</InvText>
|
||||
</>
|
||||
)}
|
||||
</StickyScrollable>
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
</InvModalBody>
|
||||
|
||||
<InvModalFooter>
|
||||
<InvButton onClick={onSettingsModalClose}>
|
||||
{t('common.close')}
|
||||
</InvButton>
|
||||
</InvModalFooter>
|
||||
<InvModalFooter />
|
||||
</InvModalContent>
|
||||
</InvModal>
|
||||
|
||||
|
@ -0,0 +1,71 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { resetCanvas } from 'features/canvas/store/canvasSlice';
|
||||
import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
useClearIntermediatesMutation,
|
||||
useGetIntermediatesCountQuery,
|
||||
} from 'services/api/endpoints/images';
|
||||
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
export type UseClearIntermediatesReturn = {
|
||||
intermediatesCount: number | undefined;
|
||||
clearIntermediates: () => void;
|
||||
isLoading: boolean;
|
||||
hasPendingItems: boolean;
|
||||
};
|
||||
|
||||
export const useClearIntermediates = (): UseClearIntermediatesReturn => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { data: intermediatesCount } = useGetIntermediatesCountQuery(
|
||||
undefined,
|
||||
{
|
||||
refetchOnMountOrArgChange: true,
|
||||
}
|
||||
);
|
||||
|
||||
const [_clearIntermediates, { isLoading }] = useClearIntermediatesMutation();
|
||||
|
||||
const { data: queueStatus } = useGetQueueStatusQuery();
|
||||
const hasPendingItems = useMemo(
|
||||
() =>
|
||||
Boolean(
|
||||
queueStatus &&
|
||||
(queueStatus.queue.in_progress > 0 || queueStatus.queue.pending > 0)
|
||||
),
|
||||
[queueStatus]
|
||||
);
|
||||
|
||||
const clearIntermediates = useCallback(() => {
|
||||
if (hasPendingItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
_clearIntermediates()
|
||||
.unwrap()
|
||||
.then((clearedCount) => {
|
||||
dispatch(controlAdaptersReset());
|
||||
dispatch(resetCanvas());
|
||||
dispatch(
|
||||
addToast({
|
||||
title: t('settings.intermediatesCleared', { count: clearedCount }),
|
||||
status: 'info',
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(
|
||||
addToast({
|
||||
title: t('settings.intermediatesClearedFailed'),
|
||||
status: 'error',
|
||||
})
|
||||
);
|
||||
});
|
||||
}, [t, _clearIntermediates, dispatch, hasPendingItems]);
|
||||
|
||||
return { intermediatesCount, clearIntermediates, isLoading, hasPendingItems };
|
||||
};
|
@ -0,0 +1,49 @@
|
||||
import { Flex } from '@chakra-ui/layout';
|
||||
import { InvHeading } from 'common/components/InvHeading/wrapper';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
export type StickyScrollableHeadingProps = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
export const StickyScrollableHeading = memo(
|
||||
(props: StickyScrollableHeadingProps) => {
|
||||
return (
|
||||
<Flex ps={2} pb={4} position="sticky" zIndex={1} top={0} bg="base.800">
|
||||
<InvHeading size="sm">{props.title}</InvHeading>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
StickyScrollableHeading.displayName = 'StickyScrollableHeading';
|
||||
|
||||
export type StickyScrollableContentProps = PropsWithChildren;
|
||||
|
||||
export const StickyScrollableContent = memo(
|
||||
(props: StickyScrollableContentProps) => {
|
||||
return (
|
||||
<Flex p={4} borderRadius="base" bg="base.750" flexDir="column" gap={4}>
|
||||
{props.children}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
StickyScrollableContent.displayName = 'StickyScrollableContent';
|
||||
|
||||
export type StickyScrollableProps = PropsWithChildren<{
|
||||
title: string;
|
||||
}>;
|
||||
|
||||
export const StickyScrollable = memo((props: StickyScrollableProps) => {
|
||||
return (
|
||||
<Flex key={props.title} flexDir="column">
|
||||
<StickyScrollableHeading title={props.title} />
|
||||
<StickyScrollableContent>{props.children}</StickyScrollableContent>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
StickyScrollable.displayName = 'StickyScrollable';
|
Loading…
Reference in New Issue
Block a user