optimize library headers (#1374)

This commit is contained in:
jeffvli
2025-12-14 02:33:19 -08:00
parent 4cc51c3700
commit b4b106222e
15 changed files with 247 additions and 155 deletions
+4 -4
View File
@@ -578,10 +578,10 @@
}
},
"player": {
"addLast": "add last",
"addNext": "add next",
"addLastShuffled": "add last (shuffled)",
"addNextShuffled": "add next (shuffled)",
"addLast": "last",
"addNext": "next",
"addLastShuffled": "last (shuffled)",
"addNextShuffled": "next (shuffled)",
"holdToShuffle": "hold to shuffle",
"favorite": "favorite",
"lyrics": "lyrics",
@@ -368,8 +368,10 @@ export const AlbumDetailContent = () => {
<div className={styles.contentContainer} ref={ref}>
<div className={styles.detailContainer}>
{comment && (
<Spoiler hideLabel={true} maxHeight={32} showLabel={true}>
{replaceURLWithHTMLLinks(comment)}
<Spoiler maxHeight={75}>
<Text
dangerouslySetInnerHTML={{ __html: replaceURLWithHTMLLinks(comment) }}
/>
</Spoiler>
)}
<div className={styles.contentLayout}>
@@ -89,7 +89,7 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
item={{ route: AppRoute.LIBRARY_ALBUMS, type: LibraryItem.ALBUM }}
title={detailQuery?.data?.name || ''}
>
<Stack gap="xl" w="100%">
<Stack gap="md" w="100%">
{(firstAlbumArtist || releaseYear) && (
<Group className={styles.metadataGroup}>
{firstAlbumArtist && (
@@ -121,7 +121,7 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
favorite={detailQuery?.data?.userFavorite}
onFavorite={handleFavorite}
onMore={handleMoreOptions}
onPlay={() => handlePlay(Play.NOW)}
onPlay={(type) => handlePlay(type)}
onRating={handleUpdateRating}
onShuffle={() => handlePlay(Play.SHUFFLE)}
rating={detailQuery?.data?.userRating || 0}
@@ -211,7 +211,13 @@ const DummyAlbumDetailRoute = () => {
)}
{comment && (
<section>
<Spoiler maxHeight={75}>{replaceURLWithHTMLLinks(comment)}</Spoiler>
<Spoiler maxHeight={75}>
<Text
dangerouslySetInnerHTML={{
__html: replaceURLWithHTMLLinks(comment),
}}
/>
</Spoiler>
</section>
)}
<section>
@@ -14,20 +14,14 @@ import { ItemControls } from '/@/renderer/components/item-list/types';
import { AlbumInfiniteCarousel } from '/@/renderer/features/albums/components/album-infinite-carousel';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { AlbumArtistGridCarousel } from '/@/renderer/features/artists/components/album-artist-grid-carousel';
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
import { DefaultPlayButton } from '/@/renderer/features/shared/components/play-button';
import { searchLibraryItems } from '/@/renderer/features/shared/utils';
import { useContainerQuery } from '/@/renderer/hooks';
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
import { AppRoute } from '/@/renderer/router/routes';
import { ArtistItem, useCurrentServer, usePlayerSong } from '/@/renderer/store';
import {
useGeneralSettings,
usePlayButtonBehavior,
useSettingsStore,
} from '/@/renderer/store/settings.store';
import { useGeneralSettings, useSettingsStore } from '/@/renderer/store/settings.store';
import { sanitize } from '/@/renderer/utils/sanitize';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Button } from '/@/shared/components/button/button';
@@ -52,58 +46,35 @@ import {
import { ItemListKey, ListDisplayType, Play } from '/@/shared/types/types';
interface AlbumArtistActionButtonsProps {
albumCount: null | number | undefined;
artistDiscographyLink: string;
artistSongsLink: string;
onFavorite: () => void;
onMoreOptions: (e: React.MouseEvent<HTMLButtonElement>) => void;
onPlay: () => void;
userFavorite?: boolean;
}
const AlbumArtistActionButtons = ({
albumCount,
artistDiscographyLink,
artistSongsLink,
onFavorite,
onMoreOptions,
onPlay,
userFavorite,
}: AlbumArtistActionButtonsProps) => {
const { t } = useTranslation();
return (
<>
<Group gap="md">
<DefaultPlayButton disabled={albumCount === 0} onClick={onPlay} />
<Group gap="xs">
<ActionIcon
icon="favorite"
iconProps={{
fill: userFavorite ? 'primary' : undefined,
}}
onClick={onFavorite}
size="lg"
variant="transparent"
/>
<ActionIcon
icon="ellipsisHorizontal"
onClick={onMoreOptions}
size="lg"
variant="transparent"
/>
</Group>
</Group>
<Group gap="md">
<Group gap="lg">
<Button
component={Link}
p={0}
size="compact-md"
to={artistDiscographyLink}
variant="subtle"
variant="transparent"
>
{String(t('page.albumArtistDetail.viewDiscography')).toUpperCase()}
</Button>
<Button component={Link} size="compact-md" to={artistSongsLink} variant="subtle">
<Button
component={Link}
p={0}
size="compact-md"
to={artistSongsLink}
variant="transparent"
>
{String(t('page.albumArtistDetail.viewAllTracks')).toUpperCase()}
</Button>
</Group>
@@ -175,8 +146,8 @@ const AlbumArtistMetadataBiography = ({
artist: artistName,
})}
</TextTitle>
<Spoiler maxHeight={50}>
<div dangerouslySetInnerHTML={{ __html: sanitizedBiography }}></div>
<Spoiler>
<Text dangerouslySetInnerHTML={{ __html: sanitizedBiography }} />
</Spoiler>
</section>
);
@@ -440,7 +411,6 @@ export const AlbumArtistDetailContent = () => {
};
const routeId = (artistId || albumArtistId) as string;
const { ref, ...cq } = useContainerQuery();
const { addToQueueByFetch, setFavorite } = usePlayer();
const server = useCurrentServer();
const [enabledItem, itemOrder] = useMemo(() => {
@@ -506,21 +476,11 @@ export const AlbumArtistDetailContent = () => {
sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: SortOrder.DESC,
title: (
<Group align="flex-end">
<TextTitle fw={700} order={2}>
{t('page.albumArtistDetail.recentReleases', {
postProcess: 'sentenceCase',
})}
</TextTitle>
<Button
component={Link}
size="compact-md"
to={artistDiscographyLink}
variant="subtle"
>
{String(t('page.albumArtistDetail.viewDiscography')).toUpperCase()}
</Button>
</Group>
<TextTitle fw={700} order={2}>
{t('page.albumArtistDetail.recentReleases', {
postProcess: 'sentenceCase',
})}
</TextTitle>
),
uniqueId: 'recentReleases',
},
@@ -560,7 +520,6 @@ export const AlbumArtistDetailContent = () => {
},
];
}, [
artistDiscographyLink,
detailQuery.data?.similarArtists,
enabledItem.compilations,
enabledItem.recentAlbums,
@@ -573,37 +532,6 @@ export const AlbumArtistDetailContent = () => {
t,
]);
const playButtonBehavior = usePlayButtonBehavior();
const handlePlay = async (playType?: Play) => {
if (!server?.id) return;
addToQueueByFetch(
server.id,
[routeId],
albumArtistId ? LibraryItem.ALBUM_ARTIST : LibraryItem.ARTIST,
playType || playButtonBehavior,
);
};
const handleFavorite = () => {
if (!detailQuery.data) return;
setFavorite(
detailQuery.data._serverId,
[detailQuery.data.id],
LibraryItem.ALBUM_ARTIST,
!detailQuery.data.userFavorite,
);
};
const handleMoreOptions = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!detailQuery.data) return;
ContextMenuController.call({
cmd: { items: [detailQuery.data], type: LibraryItem.ALBUM_ARTIST },
event: e,
});
};
const albumCount = detailQuery.data?.albumCount;
const biography =
detailQuery.data?.biography && enabledItem.biography ? detailQuery.data.biography : null;
const showGenres = detailQuery.data?.genres ? detailQuery.data.genres.length !== 0 : false;
@@ -618,13 +546,8 @@ export const AlbumArtistDetailContent = () => {
<div className={styles.contentContainer} ref={ref}>
<div className={styles.detailContainer}>
<AlbumArtistActionButtons
albumCount={albumCount}
artistDiscographyLink={artistDiscographyLink}
artistSongsLink={artistSongsLink}
onFavorite={handleFavorite}
onMoreOptions={handleMoreOptions}
onPlay={() => handlePlay(playButtonBehavior)}
userFavorite={detailQuery.data?.userFavorite}
/>
<Grid gutter="xl">
{showGenres && (
@@ -0,0 +1,8 @@
.metadata-group {
justify-content: center;
width: 100%;
@container (min-width: $mantine-breakpoint-sm) {
justify-content: flex-start;
}
}
@@ -3,17 +3,24 @@ import { forwardRef, Fragment, Ref } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
import styles from './album-artist-detail-header.module.css';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { LibraryHeader } from '/@/renderer/features/shared/components/library-header';
import {
LibraryHeader,
LibraryHeaderMenu,
} from '/@/renderer/features/shared/components/library-header';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { formatDurationString } from '/@/renderer/utils';
import { Group } from '/@/shared/components/group/group';
import { Rating } from '/@/shared/components/rating/rating';
import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text';
import { LibraryItem, ServerType } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivElement>) => {
const { albumArtistId, artistId } = useParams() as {
@@ -56,7 +63,28 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
},
];
const { setRating } = usePlayer();
const { addToQueueByFetch, setFavorite, setRating } = usePlayer();
const playButtonBehavior = usePlayButtonBehavior();
const handlePlay = (type?: Play) => {
if (!server?.id || !routeId) return;
addToQueueByFetch(
server.id,
[routeId],
LibraryItem.ALBUM_ARTIST,
type || playButtonBehavior,
);
};
const handleFavorite = () => {
if (!detailQuery?.data) return;
setFavorite(
detailQuery.data._serverId,
[detailQuery.data.id],
LibraryItem.ALBUM_ARTIST,
!detailQuery.data.userFavorite,
);
};
const handleUpdateRating = (rating: number) => {
if (!detailQuery?.data) return;
@@ -78,6 +106,14 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
);
};
const handleMoreOptions = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!detailQuery?.data) return;
ContextMenuController.call({
cmd: { items: [detailQuery.data], type: LibraryItem.ALBUM_ARTIST },
event: e,
});
};
const showRating = detailQuery?.data?._serverType === ServerType.NAVIDROME;
return (
@@ -87,8 +123,8 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
ref={ref}
title={detailQuery?.data?.name || ''}
>
<Stack>
<Group>
<Stack gap="md" w="100%">
<Group className={styles.metadataGroup}>
{metadataItems
.filter((i) => i.enabled)
.map((item, index) => (
@@ -97,17 +133,16 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
<Text isMuted={item.secondary}>{item.value}</Text>
</Fragment>
))}
{showRating && (
<>
<Text isNoSelect></Text>
<Rating
onChange={handleUpdateRating}
readOnly={detailQuery?.isFetching}
value={detailQuery?.data?.userRating || 0}
/>
</>
)}
</Group>
<LibraryHeaderMenu
favorite={detailQuery?.data?.userFavorite}
onFavorite={handleFavorite}
onMore={handleMoreOptions}
onPlay={(type) => handlePlay(type)}
onRating={showRating ? handleUpdateRating : undefined}
onShuffle={() => handlePlay(Play.SHUFFLE)}
rating={detailQuery?.data?.userRating || 0}
/>
</Stack>
</LibraryHeader>
);
@@ -228,8 +228,8 @@ const AlbumArtistPropertyMapping: ItemDetailRow<AlbumArtist>[] = [
label: 'common.biography',
render: (artist) =>
artist.biography ? (
<Spoiler maxHeight={50}>
<div dangerouslySetInnerHTML={{ __html: sanitize(artist.biography) }} />
<Spoiler>
<Text dangerouslySetInnerHTML={{ __html: sanitize(artist.biography) }} />
</Spoiler>
) : null,
},
@@ -91,7 +91,7 @@ export const PlaylistDetailSongListHeader = ({
title={detailQuery?.data?.name}
>
<LibraryHeaderMenu
onPlay={() => handlePlay(Play.NOW)}
onPlay={(type) => handlePlay(type)}
onShuffle={() => handlePlay(Play.SHUFFLE)}
/>
</LibraryHeader>
@@ -26,7 +26,7 @@
grid-template-areas: 'image info';
grid-template-rows: auto;
grid-template-columns: 225px minmax(0, 1fr);
align-items: center;
align-items: flex-end;
justify-items: start;
height: auto;
min-height: 340px;
@@ -98,7 +98,7 @@
.title {
display: flex;
margin: var(--theme-spacing-sm) 0;
font-size: clamp(2rem, 3.5dvw, 3.25rem);
font-size: clamp(1.75rem, 3dvw, 2.75rem);
line-height: 1.2;
}
@@ -7,9 +7,12 @@ import { Link } from 'react-router';
import styles from './library-header.module.css';
import {
PlayLastTextButton,
PlayNextTextButton,
PlayTextButton,
WideShuffleButton,
} from '/@/renderer/features/shared/components/play-button';
import { LONG_PRESS_PLAY_BEHAVIOR } from '/@/renderer/features/shared/components/play-button-group';
import { usePlayButtonClick } from '/@/renderer/features/shared/hooks/use-play-button-click';
import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
import { useIsMutatingRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
@@ -20,6 +23,7 @@ import { Image } from '/@/shared/components/image/image';
import { Rating } from '/@/shared/components/rating/rating';
import { Text } from '/@/shared/components/text/text';
import { LibraryItem } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
interface LibraryHeaderProps {
children?: ReactNode;
@@ -145,36 +149,36 @@ export const LibraryHeader = forwardRef(
const calculateTitleSize = (title: string) => {
const titleLength = title.length;
let baseSize = '3.5dvw';
let baseSize = '3dvw';
if (titleLength > 20) {
baseSize = '3dvw';
}
if (titleLength > 30) {
baseSize = '2.75dvw';
}
if (titleLength > 40) {
baseSize = '2.5dvw';
}
if (titleLength > 50) {
if (titleLength > 30) {
baseSize = '2.25dvw';
}
if (titleLength > 60) {
if (titleLength > 40) {
baseSize = '2dvw';
}
return `clamp(2rem, ${baseSize}, 3.25rem)`;
if (titleLength > 50) {
baseSize = '1.875dvw';
}
if (titleLength > 60) {
baseSize = '1.75dvw';
}
return `clamp(1.75rem, ${baseSize}, 2.75rem)`;
};
interface LibraryHeaderMenuProps {
favorite?: boolean;
onFavorite?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onMore?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onPlay?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onPlay?: (type: Play) => void;
onRating?: (rating: number) => void;
onShuffle?: (e: React.MouseEvent<HTMLButtonElement>) => void;
rating?: number;
@@ -186,7 +190,6 @@ export const LibraryHeaderMenu = ({
onMore,
onPlay,
onRating,
onShuffle,
rating,
}: LibraryHeaderMenuProps) => {
const isMutatingRating = useIsMutatingRating();
@@ -194,11 +197,43 @@ export const LibraryHeaderMenu = ({
const isMutatingDeleteFavorite = useIsMutatingDeleteFavorite();
const isMutatingFavorite = isMutatingCreateFavorite || isMutatingDeleteFavorite;
const handlePlayNow = usePlayButtonClick({
onClick: () => {
onPlay?.(Play.NOW);
},
onLongPress: () => {
onPlay?.(LONG_PRESS_PLAY_BEHAVIOR[Play.NOW]);
},
});
const handlePlayNext = usePlayButtonClick({
onClick: () => {
onPlay?.(Play.NEXT);
},
onLongPress: () => {
onPlay?.(LONG_PRESS_PLAY_BEHAVIOR[Play.NEXT]);
},
});
const handlePlayLast = usePlayButtonClick({
onClick: () => {
onPlay?.(Play.LAST);
},
onLongPress: () => {
onPlay?.(LONG_PRESS_PLAY_BEHAVIOR[Play.LAST]);
},
});
return (
<div className={styles.libraryHeaderMenu}>
<Group wrap="nowrap">
{onPlay && <PlayTextButton onClick={onPlay} />}
{onShuffle && <WideShuffleButton onClick={onShuffle} />}
{onPlay && <PlayTextButton {...handlePlayNow.handlers} {...handlePlayNow.props} />}
{onPlay && (
<PlayNextTextButton {...handlePlayNext.handlers} {...handlePlayNext.props} />
)}
{onPlay && (
<PlayLastTextButton {...handlePlayLast.handlers} {...handlePlayLast.props} />
)}
</Group>
<Group gap="sm" wrap="nowrap">
{onRating && (
@@ -50,10 +50,26 @@
padding-left: var(--theme-spacing-xl);
background: white;
border-radius: var(--theme-radius-xl);
transition: background-color 0.2s ease-in-out;
transition: background-color 0.2s ease-in-out !important;
&[data-variant='subtle'] {
transition: background-color 0.2s ease-in-out !important;
&:hover,
&:active,
&:focus-visible {
transition: background-color 0.2s ease-in-out !important;
}
}
}
.wide-text-button.unthemed {
transition: background-color 0.2s ease-in-out !important;
&[data-variant='subtle'] {
transition: background-color 0.2s ease-in-out !important;
}
@mixin light {
background: black;
@@ -62,8 +78,11 @@
fill: white;
}
&:hover {
background: lighten(black, 10%);
&[data-variant='subtle']:hover,
&[data-variant='subtle']:active,
&[data-variant='subtle']:focus-visible {
background: lighten(black, 10%) !important;
transition: background-color 0.2s ease-in-out !important;
}
}
@@ -75,8 +94,11 @@
fill: black;
}
&:hover {
background: darken(white, 20%);
&[data-variant='subtle']:hover,
&[data-variant='subtle']:active,
&[data-variant='subtle']:focus-visible {
background: darken(white, 20%) !important;
transition: background-color 0.2s ease-in-out !important;
}
}
}
@@ -90,14 +112,16 @@
color: white;
}
svg {
color: black;
fill: black;
}
}
.no-fill {
fill: none !important;
}
.play-button {
all: unset;
display: flex;
@@ -4,12 +4,14 @@ import { forwardRef, memo } from 'react';
import styles from './play-button.module.css';
import { PlayTooltip } from '/@/renderer/features/shared/components/play-button-group';
import { usePlayButtonClick } from '/@/renderer/features/shared/hooks/use-play-button-click';
import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon';
import { Button, ButtonProps } from '/@/shared/components/button/button';
import { Group } from '/@/shared/components/group/group';
import { AppIcon, Icon } from '/@/shared/components/icon/icon';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { Play } from '/@/shared/types/types';
export interface DefaultPlayButtonProps extends ActionIconProps {
size?: number | string;
@@ -36,14 +38,18 @@ export const DefaultPlayButton = forwardRef<HTMLButtonElement, DefaultPlayButton
DefaultPlayButton.displayName = 'DefaultPlayButton';
interface TextPlayButtonProps extends ButtonProps {}
interface TextPlayButtonProps extends ButtonProps {
onLongPress?: (e: React.MouseEvent<HTMLButtonElement>) => void;
showTooltip?: boolean;
}
export const PlayTextButton = ({
className,
showTooltip = true,
variant = 'default',
...props
}: TextPlayButtonProps) => {
return (
const button = (
<Button
className={clsx(styles.wideTextButton, className, {
[styles.unthemed]: variant !== 'filled',
@@ -57,12 +63,64 @@ export const PlayTextButton = ({
>
{props.children || (
<Group gap="sm" wrap="nowrap">
<Icon fill="default" icon="mediaPlay" size="lg" />
<Icon icon="mediaPlay" size="lg" />
{t('player.play', { postProcess: 'sentenceCase' })}
</Group>
)}
</Button>
);
const hasLongPress = Boolean(
props.onLongPress || (props as any).onMouseDown || (props as any).onTouchStart,
);
if (hasLongPress && showTooltip) {
return <PlayTooltip type={Play.NOW}>{button}</PlayTooltip>;
}
return button;
};
export const PlayNextTextButton = ({ ...props }: TextPlayButtonProps) => {
const button = (
<PlayTextButton {...props} showTooltip={false}>
<Group gap="sm" wrap="nowrap">
<Icon className={styles.noFill} icon="mediaPlayNext" size="lg" />
{t('player.addNext', { postProcess: 'sentenceCase' })}
</Group>
</PlayTextButton>
);
const hasLongPress = Boolean(
props.onLongPress || (props as any).onMouseDown || (props as any).onTouchStart,
);
if (hasLongPress) {
return <PlayTooltip type={Play.NEXT}>{button}</PlayTooltip>;
}
return button;
};
export const PlayLastTextButton = ({ ...props }: TextPlayButtonProps) => {
const button = (
<PlayTextButton {...props} showTooltip={false}>
<Group gap="sm" wrap="nowrap">
<Icon className={styles.noFill} icon="mediaPlayLast" size="lg" />
{t('player.addLast', { postProcess: 'sentenceCase' })}
</Group>
</PlayTextButton>
);
const hasLongPress = Boolean(
props.onLongPress || (props as any).onMouseDown || (props as any).onTouchStart,
);
if (hasLongPress) {
return <PlayTooltip type={Play.LAST}>{button}</PlayTooltip>;
}
return button;
};
export const WideShuffleButton = ({ ...props }: TextPlayButtonProps) => {
@@ -143,11 +143,11 @@
background-color: transparent;
@mixin dark {
color: lighten(var(--theme-colors-foreground), 10%);
color: darken(var(--theme-colors-foreground), 15%);
}
@mixin light {
color: darken(var(--theme-colors-foreground), 10%);
color: lighten(var(--theme-colors-foreground), 10%);
}
}
+3 -2
View File
@@ -5,17 +5,18 @@ import styles from './spoiler.module.css';
import { Icon } from '/@/shared/components/icon/icon';
interface SpoilerProps extends MantineSpoilerProps {
interface SpoilerProps extends Omit<MantineSpoilerProps, 'hideLabel' | 'showLabel'> {
children?: ReactNode;
}
export const Spoiler = ({ children, ...props }: SpoilerProps) => {
export const Spoiler = ({ children, maxHeight = 56, ...props }: SpoilerProps) => {
const [expanded, setExpanded] = useState(false);
return (
<MantineSpoiler
classNames={{ content: styles.spoiler, control: styles.control }}
expanded={expanded}
maxHeight={maxHeight}
{...props}
hideLabel={<Icon icon="arrowUpS" size="lg" />}
onClick={() => setExpanded(!expanded)}