optimize various base components

This commit is contained in:
jeffvli
2026-01-02 12:46:35 -08:00
parent a66c67e86d
commit d06d1674d1
31 changed files with 669 additions and 393 deletions
+10 -5
View File
@@ -72,16 +72,21 @@ export const App = () => {
}
}, [language]);
const notificationStyles = useMemo(
() => ({
root: {
marginBottom: 90,
},
}),
[],
);
return (
<MantineProvider forceColorScheme={mode} theme={theme}>
<Notifications
containerWidth="300px"
position="bottom-center"
styles={{
root: {
marginBottom: 90,
},
}}
styles={notificationStyles}
zIndex={50000}
/>
<WebAudioContext.Provider value={webAudioProvider}>
+171 -115
View File
@@ -1,6 +1,6 @@
import clsx from 'clsx';
import { AnimatePresence } from 'motion/react';
import { Fragment, memo, ReactNode, useState } from 'react';
import { Fragment, memo, ReactNode, useCallback, useMemo, useState } from 'react';
import { generatePath, Link } from 'react-router';
import styles from './item-card.module.css';
@@ -84,7 +84,7 @@ export const ItemCard = ({
switch (type) {
case 'compact':
return (
<CompactItemCard
<MemoizedCompactItemCard
controls={controls}
data={data}
enableDrag={enableDrag}
@@ -101,7 +101,7 @@ export const ItemCard = ({
);
case 'poster':
return (
<PosterItemCard
<MemoizedPosterItemCard
controls={controls}
data={data}
enableDrag={enableDrag}
@@ -119,7 +119,7 @@ export const ItemCard = ({
case 'default':
default:
return (
<DefaultItemCard
<MemoizedDefaultItemCard
controls={controls}
data={data}
enableDrag={enableDrag}
@@ -167,46 +167,64 @@ const CompactItemCard = ({
: undefined;
const isSelected = useItemSelectionState(internalState, itemRowId || undefined);
const { isDragging: isDraggingLocal, ref } = useDragDrop<HTMLDivElement>({
drag: {
getId: () => {
if (!data) {
return [];
}
const getId = useCallback(() => {
if (!data) {
return [];
}
const draggedItems = getDraggedItems(data, internalState);
return draggedItems.map((item) => item.id);
},
getItem: () => {
if (!data) {
return [];
}
const draggedItems = getDraggedItems(data, internalState);
return draggedItems.map((item) => item.id);
}, [data, internalState]);
const draggedItems = getDraggedItems(data, internalState);
return draggedItems;
},
const getItem = useCallback(() => {
if (!data) {
return [];
}
const draggedItems = getDraggedItems(data, internalState);
return draggedItems;
}, [data, internalState]);
const onDragStart = useCallback(() => {
if (!data) {
return;
}
const draggedItems = getDraggedItems(data, internalState);
if (internalState) {
internalState.setDragging(draggedItems);
}
}, [data, internalState]);
const onDrop = useCallback(() => {
if (internalState) {
internalState.setDragging([]);
}
}, [internalState]);
const dragOperation = useMemo(
() =>
itemType === LibraryItem.QUEUE_SONG
? [DragOperation.REORDER, DragOperation.ADD]
: [DragOperation.ADD],
[itemType],
);
const drag = useMemo(
() => ({
getId,
getItem,
itemType,
onDragStart: () => {
if (!data) {
return;
}
const draggedItems = getDraggedItems(data, internalState);
if (internalState) {
internalState.setDragging(draggedItems);
}
},
onDrop: () => {
if (internalState) {
internalState.setDragging([]);
}
},
operation:
itemType === LibraryItem.QUEUE_SONG
? [DragOperation.REORDER, DragOperation.ADD]
: [DragOperation.ADD],
onDragStart,
onDrop,
operation: dragOperation,
target: DragTarget.ALBUM,
},
}),
[getId, getItem, itemType, onDragStart, onDrop, dragOperation],
);
const { isDragging: isDraggingLocal, ref } = useDragDrop<HTMLDivElement>({
drag,
isEnabled: !!enableDrag && !!data,
});
@@ -649,46 +667,64 @@ const PosterItemCard = ({
: undefined;
const isSelected = useItemSelectionState(internalState, itemRowId || undefined);
const { isDragging: isDraggingLocal, ref } = useDragDrop<HTMLDivElement>({
drag: {
getId: () => {
if (!data) {
return [];
}
const getId = useCallback(() => {
if (!data) {
return [];
}
const draggedItems = getDraggedItems(data, internalState);
return draggedItems.map((item) => item.id);
},
getItem: () => {
if (!data) {
return [];
}
const draggedItems = getDraggedItems(data, internalState);
return draggedItems.map((item) => item.id);
}, [data, internalState]);
const draggedItems = getDraggedItems(data, internalState);
return draggedItems;
},
const getItem = useCallback(() => {
if (!data) {
return [];
}
const draggedItems = getDraggedItems(data, internalState);
return draggedItems;
}, [data, internalState]);
const onDragStart = useCallback(() => {
if (!data) {
return;
}
const draggedItems = getDraggedItems(data, internalState);
if (internalState) {
internalState.setDragging(draggedItems);
}
}, [data, internalState]);
const onDrop = useCallback(() => {
if (internalState) {
internalState.setDragging([]);
}
}, [internalState]);
const dragOperation = useMemo(
() =>
itemType === LibraryItem.QUEUE_SONG
? [DragOperation.REORDER, DragOperation.ADD]
: [DragOperation.ADD],
[itemType],
);
const drag = useMemo(
() => ({
getId,
getItem,
itemType,
onDragStart: () => {
if (!data) {
return;
}
const draggedItems = getDraggedItems(data, internalState);
if (internalState) {
internalState.setDragging(draggedItems);
}
},
onDrop: () => {
if (internalState) {
internalState.setDragging([]);
}
},
operation:
itemType === LibraryItem.QUEUE_SONG
? [DragOperation.REORDER, DragOperation.ADD]
: [DragOperation.ADD],
onDragStart,
onDrop,
operation: dragOperation,
target: DragTarget.ALBUM,
},
}),
[getId, getItem, itemType, onDragStart, onDrop, dragOperation],
);
const { isDragging: isDraggingLocal, ref } = useDragDrop<HTMLDivElement>({
drag,
isEnabled: !!enableDrag && !!data,
});
@@ -896,6 +932,15 @@ const PosterItemCard = ({
);
};
const MemoizedPosterItemCard = memo(PosterItemCard);
MemoizedPosterItemCard.displayName = 'MemoizedPosterItemCard';
const MemoizedCompactItemCard = memo(CompactItemCard);
MemoizedCompactItemCard.displayName = 'MemoizedCompactItemCard';
const MemoizedDefaultItemCard = memo(DefaultItemCard);
MemoizedDefaultItemCard.displayName = 'MemoizedDefaultItemCard';
export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[] => {
return [
{
@@ -1160,56 +1205,67 @@ const getItemNavigationPath = (
return getTitlePath(effectiveItemType, data.id);
};
const ItemCardRow = ({
data,
index,
row,
type,
}: {
data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
index: number;
row: DataRow;
type?: 'compact' | 'default' | 'poster';
}) => {
const alignmentClass =
row.align === 'center'
? styles['align-center']
: row.align === 'end'
? styles['align-end']
: styles['align-start'];
const ItemCardRow = memo(
({
data,
index,
row,
type,
}: {
data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
index: number;
row: DataRow;
type?: 'compact' | 'default' | 'poster';
}) => {
const alignmentClass =
row.align === 'center'
? styles['align-center']
: row.align === 'end'
? styles['align-end']
: styles['align-start'];
// All rows except the first one (index 0) should be muted
const isMuted = index > 0 || row.isMuted;
// All rows except the first one (index 0) should be muted
const isMuted = index > 0 || row.isMuted;
const formattedContent = useMemo(() => {
if (!data) {
return null;
}
return row.format(data);
}, [data, row]);
if (!data) {
return (
<div
className={clsx(styles.row, alignmentClass, {
[styles.compact]: type === 'compact',
[styles.default]: type === 'default',
[styles.muted]: isMuted,
[styles.poster]: type === 'poster',
})}
>
&nbsp;
</div>
);
}
if (!data) {
return (
<div
<Text
className={clsx(styles.row, alignmentClass, {
[styles.bold]: index === 0,
[styles.compact]: type === 'compact',
[styles.default]: type === 'default',
[styles.muted]: isMuted,
[styles.poster]: type === 'poster',
})}
size={index > 0 ? 'sm' : 'md'}
>
&nbsp;
</div>
{formattedContent}
</Text>
);
}
},
);
return (
<Text
className={clsx(styles.row, alignmentClass, {
[styles.bold]: index === 0,
[styles.compact]: type === 'compact',
[styles.default]: type === 'default',
[styles.muted]: isMuted,
[styles.poster]: type === 'poster',
})}
size={index > 0 ? 'sm' : 'md'}
>
{row.format(data)}
</Text>
);
};
ItemCardRow.displayName = 'ItemCardRow';
export const MemoizedItemCard = memo(ItemCard);
@@ -1,3 +1,4 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { QueryBuilderOption } from '/@/renderer/components/query-builder/query-builder-option';
@@ -107,15 +108,17 @@ export const QueryBuilder = ({
onChangeType({ groupIndex, level, value });
};
const boxStyle = useMemo(
() => ({
border: '1px solid var(--theme-colors-border)',
borderRadius: 'var(--theme-radius-md)',
marginLeft: level > 0 ? '20px' : '0px',
}),
[level],
);
return (
<Box
p="md"
style={{
border: '1px solid var(--theme-colors-border)',
borderRadius: 'var(--theme-radius-md)',
marginLeft: level > 0 ? '20px' : '0px',
}}
>
<Box p="md" style={boxStyle}>
<Stack gap="sm">
<Group gap="sm" justify="space-between" wrap="nowrap">
<Group gap="sm" wrap="nowrap">
@@ -67,16 +67,13 @@ export const MultiSelectWithInvalidData = ({ data, defaultValue, ...props }: Mul
return [data, []];
}, [data, defaultValue]);
return (
<MultiSelect
data={fullData}
defaultValue={defaultValue}
error={
missing.length
? t('error.badValue', { postProcess: 'sentenceCase', value: missing })
: undefined
}
{...props}
/>
const error = useMemo(
() =>
missing.length
? t('error.badValue', { postProcess: 'sentenceCase', value: missing })
: undefined,
[missing, t],
);
return <MultiSelect data={fullData} defaultValue={defaultValue} error={error} {...props} />;
};
@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
@@ -147,8 +147,8 @@ export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFilte
[setMaxYear],
);
const handleGenresFilter = useMemo(
() => (e: string[] | undefined) => {
const handleGenresFilter = useCallback(
(e: null | string[]) => {
setGenreId(e && e.length > 0 ? e : null);
},
[setGenreId],
@@ -178,13 +178,16 @@ export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFilte
}));
}, [albumArtistListQuery.data?.items]);
const handleAlbumArtistFilter = (e: null | string[]) => {
setAlbumArtist(e ?? null);
};
const handleAlbumArtistFilter = useCallback(
(e: null | string[]) => {
setAlbumArtist(e ?? null);
},
[setAlbumArtist],
);
const handleTagFilter = useMemo(
() => (e: string[] | undefined) => {
setCustom({ Tags: e?.join('|') ?? null });
const handleTagFilter = useCallback(
(e: null | string[]) => {
setCustom({ Tags: e && e.length > 0 ? e.join('|') : null });
},
[setCustom],
);
@@ -1,5 +1,5 @@
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { ChangeEvent, useMemo } from 'react';
import { ChangeEvent, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
@@ -148,6 +148,28 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
const debouncedHandleYearFilter = useDebouncedCallback(handleYearFilter, 300);
const handleGenreChange = useCallback(
(e: null | string[]) => {
if (e && e.length > 0) {
setGenreId(e);
} else {
setGenreId(null);
}
},
[setGenreId],
);
const handleAlbumArtistChange = useCallback(
(e: null | string[]) => {
if (e && e.length > 0) {
setAlbumArtist(e);
} else {
setAlbumArtist(null);
}
},
[setAlbumArtist],
);
return (
<Stack px="md" py="md">
{yesNoUndefinedFilters.map((filter) => (
@@ -180,7 +202,7 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
data={genreList}
defaultValue={query.genreIds || []}
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
onChange={(e) => (e && e.length > 0 ? setGenreId(e) : setGenreId(null))}
onChange={handleGenreChange}
searchable
/>
)}
@@ -191,7 +213,7 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
disabled={disableArtistFilter}
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
limit={300}
onChange={(e) => (e && e.length > 0 ? setAlbumArtist(e) : setAlbumArtist(null))}
onChange={handleAlbumArtistChange}
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
searchable
/>
@@ -224,6 +246,17 @@ const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterI
return Array.isArray(value) ? value : [value];
}, [value]);
const handleChange = useCallback(
(e: null | string[]) => {
if (e && e.length > 0) {
onChange(e);
} else {
onChange(null);
}
},
[onChange],
);
return (
<MultiSelectWithInvalidData
clearable
@@ -232,7 +265,7 @@ const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterI
key={tagValue}
label={label}
limit={100}
onChange={(e) => (e && e.length > 0 ? onChange(e) : onChange(null))}
onChange={handleChange}
searchable
/>
);
@@ -1,5 +1,5 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { ChangeEvent, useMemo, useState } from 'react';
import { ChangeEvent, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
@@ -63,8 +63,8 @@ export const SubsonicAlbumFilters = ({ disableArtistFilter }: SubsonicAlbumFilte
}));
}, [items]);
const handleAlbumArtistFilter = useMemo(
() => (e: null | string[]) => {
const handleAlbumArtistFilter = useCallback(
(e: null | string[]) => {
setAlbumArtist(e ?? null);
},
[setAlbumArtist],
@@ -80,8 +80,8 @@ export const SubsonicAlbumFilters = ({ disableArtistFilter }: SubsonicAlbumFilte
}));
}, [genreListQuery.data]);
const handleGenresFilter = useMemo(
() => (e: null | string) => {
const handleGenresFilter = useCallback(
(e: null | string) => {
setGenreId(e ? [e] : null);
},
[setGenreId],
@@ -178,7 +178,7 @@ export const SubsonicAlbumFilters = ({ disableArtistFilter }: SubsonicAlbumFilte
defaultValue={query.genreIds?.[0] ?? undefined}
disabled={Boolean(query.minYear || query.maxYear)}
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
onChange={(e) => handleGenresFilter(e)}
onChange={handleGenresFilter}
searchable
/>
)}
+23 -18
View File
@@ -1,5 +1,5 @@
import clsx from 'clsx';
import { ComponentPropsWithoutRef } from 'react';
import { ComponentPropsWithoutRef, memo, useMemo } from 'react';
import styles from './lyric-line.module.css';
@@ -12,23 +12,28 @@ interface LyricLineProps extends ComponentPropsWithoutRef<'div'> {
text: string;
}
export const LyricLine = ({ alignment, className, fontSize, text, ...props }: LyricLineProps) => {
const lines = text.split('_BREAK_');
export const LyricLine = memo(
({ alignment, className, fontSize, text, ...props }: LyricLineProps) => {
const lines = useMemo(() => text.split('_BREAK_'), [text]);
return (
<Box
className={clsx(styles.lyricLine, className)}
style={{
const style = useMemo(
() => ({
fontSize,
textAlign: alignment,
}}
{...props}
>
<Stack gap={0}>
{lines.map((line, index) => (
<span key={index}>{line}</span>
))}
</Stack>
</Box>
);
};
}),
[fontSize, alignment],
);
return (
<Box className={clsx(styles.lyricLine, className)} style={style} {...props}>
<Stack gap={0}>
{lines.map((line, index) => (
<span key={index}>{line}</span>
))}
</Stack>
</Box>
);
},
);
LyricLine.displayName = 'LyricLine';
@@ -1,4 +1,12 @@
import { ChangeEvent, CSSProperties, KeyboardEvent, useEffect, useRef, useState } from 'react';
import {
ChangeEvent,
CSSProperties,
KeyboardEvent,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { shallow } from 'zustand/shallow';
import { useSettingsStore } from '/@/renderer/store';
@@ -90,29 +98,38 @@ export const SearchInput = ({
const shouldShowInput = isInputMode || hasValue;
const shouldExpand = isInputMode || hasValue;
const containerStyle: CSSProperties = {
display: 'inline-flex',
overflow: 'hidden',
position: 'relative',
transition: 'width 0.3s ease-in-out',
width: shouldExpand ? '200px' : '36px',
};
const containerStyle: CSSProperties = useMemo(
() => ({
display: 'inline-flex',
overflow: 'hidden',
position: 'relative',
transition: 'width 0.3s ease-in-out',
width: shouldExpand ? '200px' : '36px',
}),
[shouldExpand],
);
const buttonStyle: CSSProperties = {
left: 0,
opacity: shouldShowInput ? 0 : 1,
pointerEvents: shouldShowInput ? 'none' : 'auto',
position: 'absolute',
top: 0,
transition: 'opacity 0.2s ease-in-out',
zIndex: 10,
};
const buttonStyle: CSSProperties = useMemo(
() => ({
left: 0,
opacity: shouldShowInput ? 0 : 1,
pointerEvents: shouldShowInput ? 'none' : 'auto',
position: 'absolute',
top: 0,
transition: 'opacity 0.2s ease-in-out',
zIndex: 10,
}),
[shouldShowInput],
);
const inputStyle: CSSProperties = {
opacity: shouldShowInput ? 1 : 0,
transition: 'opacity 0.2s ease-in-out',
width: '100%',
};
const inputStyle: CSSProperties = useMemo(
() => ({
opacity: shouldShowInput ? 1 : 0,
transition: 'opacity 0.2s ease-in-out',
width: '100%',
}),
[shouldShowInput],
);
return (
<Box style={containerStyle}>
@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
@@ -105,8 +105,8 @@ export const JellyfinSongFilters = () => {
const debouncedHandleMinYearFilter = useDebouncedCallback(handleMinYearFilter, 300);
const debouncedHandleMaxYearFilter = useDebouncedCallback(handleMaxYearFilter, 300);
const handleGenresFilter = useMemo(
() => (e: string[] | undefined) => {
const handleGenresFilter = useCallback(
(e: null | string[]) => {
setCustom((prev) => {
const current = prev ?? {};
@@ -129,9 +129,9 @@ export const JellyfinSongFilters = () => {
[setCustom],
);
const handleTagFilter = useMemo(
() => (e: string[] | undefined) => {
setCustom({ Tags: e?.join('|') ?? null });
const handleTagFilter = useCallback(
(e: null | string[]) => {
setCustom({ Tags: e && e.length > 0 ? e.join('|') : null });
},
[setCustom],
);
@@ -173,7 +173,7 @@ export const JellyfinSongFilters = () => {
data={genreList}
defaultValue={selectedGenres}
label={t('entity.genre', { count: 1, postProcess: 'sentenceCase' })}
onChange={(e) => handleGenresFilter(e)}
onChange={handleGenresFilter}
searchable
/>
)}
@@ -183,7 +183,7 @@ export const JellyfinSongFilters = () => {
data={tagsQuery.data.boolTags}
defaultValue={selectedTags}
label={t('common.tags', { postProcess: 'sentenceCase' })}
onChange={(e) => handleTagFilter(e)}
onChange={handleTagFilter}
searchable
/>
)}
@@ -1,5 +1,5 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
@@ -73,6 +73,17 @@ export const NavidromeSongFilters = () => {
const debouncedHandleYearFilter = useDebouncedCallback(handleYearFilter, 300);
const handleGenreChange = useCallback(
(e: null | string[]) => {
if (e && e.length > 0) {
setGenreId(e);
} else {
setGenreId(null);
}
},
[setGenreId],
);
return (
<Stack px="md" py="md">
{yesNoUndefinedFilters.map((filter) => (
@@ -99,7 +110,7 @@ export const NavidromeSongFilters = () => {
data={genreList}
defaultValue={query.genreIds || []}
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
onChange={(e) => (e && e.length > 0 ? setGenreId(e) : setGenreId(null))}
onChange={handleGenreChange}
searchable
/>
)}
@@ -132,6 +143,17 @@ const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterI
return Array.isArray(value) ? value : [value];
}, [value]);
const handleChange = useCallback(
(e: null | string[]) => {
if (e && e.length > 0) {
onChange(e);
} else {
onChange(null);
}
},
[onChange],
);
return (
<MultiSelectWithInvalidData
clearable
@@ -140,7 +162,7 @@ const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterI
key={tagValue}
label={label}
limit={100}
onChange={(e) => (e && e.length > 0 ? onChange(e) : onChange(null))}
onChange={handleChange}
searchable
/>
);
@@ -1,4 +1,4 @@
import { ChangeEvent, useMemo } from 'react';
import { ChangeEvent, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
@@ -29,8 +29,8 @@ export const SubsonicSongFilters = () => {
}));
}, [genreListQuery.data]);
const handleGenresFilter = useMemo(
() => (e: null | string) => {
const handleGenresFilter = useCallback(
(e: null | string) => {
setGenreId(e ? [e] : null);
},
[setGenreId],
+6 -1
View File
@@ -32,12 +32,12 @@ const ResponsiveLayoutBase = ({ shell }: ResponsiveLayoutProps) => {
export const ResponsiveLayout = ({ shell }: ResponsiveLayoutProps) => {
useAppTracker();
useGarbageCollection();
return (
<>
<ResponsiveLayoutBase shell={shell} />
<LayoutHotkeys />
<GarbageCollection />
</>
);
};
@@ -77,3 +77,8 @@ const LayoutHotkeys = () => {
return <CommandPalette modalProps={{ handlers, opened }} />;
};
const GarbageCollection = () => {
useGarbageCollection();
return null;
};
+6 -1
View File
@@ -249,9 +249,14 @@ export const useAppTheme = (overrideTheme?: AppTheme) => {
}
}, [colorVars, selectedTheme, themeVars]);
const mantineTheme = useMemo(
() => createMantineTheme(appTheme as AppThemeConfiguration),
[appTheme],
);
return {
mode: appTheme?.mode || 'dark',
theme: createMantineTheme(appTheme as AppThemeConfiguration),
theme: mantineTheme,
};
};