mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
optimize various base components
This commit is contained in:
+10
-5
@@ -72,16 +72,21 @@ export const App = () => {
|
|||||||
}
|
}
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
|
const notificationStyles = useMemo(
|
||||||
|
() => ({
|
||||||
|
root: {
|
||||||
|
marginBottom: 90,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineProvider forceColorScheme={mode} theme={theme}>
|
<MantineProvider forceColorScheme={mode} theme={theme}>
|
||||||
<Notifications
|
<Notifications
|
||||||
containerWidth="300px"
|
containerWidth="300px"
|
||||||
position="bottom-center"
|
position="bottom-center"
|
||||||
styles={{
|
styles={notificationStyles}
|
||||||
root: {
|
|
||||||
marginBottom: 90,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
zIndex={50000}
|
zIndex={50000}
|
||||||
/>
|
/>
|
||||||
<WebAudioContext.Provider value={webAudioProvider}>
|
<WebAudioContext.Provider value={webAudioProvider}>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { AnimatePresence } from 'motion/react';
|
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 { generatePath, Link } from 'react-router';
|
||||||
|
|
||||||
import styles from './item-card.module.css';
|
import styles from './item-card.module.css';
|
||||||
@@ -84,7 +84,7 @@ export const ItemCard = ({
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case 'compact':
|
case 'compact':
|
||||||
return (
|
return (
|
||||||
<CompactItemCard
|
<MemoizedCompactItemCard
|
||||||
controls={controls}
|
controls={controls}
|
||||||
data={data}
|
data={data}
|
||||||
enableDrag={enableDrag}
|
enableDrag={enableDrag}
|
||||||
@@ -101,7 +101,7 @@ export const ItemCard = ({
|
|||||||
);
|
);
|
||||||
case 'poster':
|
case 'poster':
|
||||||
return (
|
return (
|
||||||
<PosterItemCard
|
<MemoizedPosterItemCard
|
||||||
controls={controls}
|
controls={controls}
|
||||||
data={data}
|
data={data}
|
||||||
enableDrag={enableDrag}
|
enableDrag={enableDrag}
|
||||||
@@ -119,7 +119,7 @@ export const ItemCard = ({
|
|||||||
case 'default':
|
case 'default':
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<DefaultItemCard
|
<MemoizedDefaultItemCard
|
||||||
controls={controls}
|
controls={controls}
|
||||||
data={data}
|
data={data}
|
||||||
enableDrag={enableDrag}
|
enableDrag={enableDrag}
|
||||||
@@ -167,46 +167,64 @@ const CompactItemCard = ({
|
|||||||
: undefined;
|
: undefined;
|
||||||
const isSelected = useItemSelectionState(internalState, itemRowId || undefined);
|
const isSelected = useItemSelectionState(internalState, itemRowId || undefined);
|
||||||
|
|
||||||
const { isDragging: isDraggingLocal, ref } = useDragDrop<HTMLDivElement>({
|
const getId = useCallback(() => {
|
||||||
drag: {
|
if (!data) {
|
||||||
getId: () => {
|
return [];
|
||||||
if (!data) {
|
}
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const draggedItems = getDraggedItems(data, internalState);
|
const draggedItems = getDraggedItems(data, internalState);
|
||||||
return draggedItems.map((item) => item.id);
|
return draggedItems.map((item) => item.id);
|
||||||
},
|
}, [data, internalState]);
|
||||||
getItem: () => {
|
|
||||||
if (!data) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const draggedItems = getDraggedItems(data, internalState);
|
const getItem = useCallback(() => {
|
||||||
return draggedItems;
|
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,
|
itemType,
|
||||||
onDragStart: () => {
|
onDragStart,
|
||||||
if (!data) {
|
onDrop,
|
||||||
return;
|
operation: dragOperation,
|
||||||
}
|
|
||||||
|
|
||||||
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],
|
|
||||||
target: DragTarget.ALBUM,
|
target: DragTarget.ALBUM,
|
||||||
},
|
}),
|
||||||
|
[getId, getItem, itemType, onDragStart, onDrop, dragOperation],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { isDragging: isDraggingLocal, ref } = useDragDrop<HTMLDivElement>({
|
||||||
|
drag,
|
||||||
isEnabled: !!enableDrag && !!data,
|
isEnabled: !!enableDrag && !!data,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -649,46 +667,64 @@ const PosterItemCard = ({
|
|||||||
: undefined;
|
: undefined;
|
||||||
const isSelected = useItemSelectionState(internalState, itemRowId || undefined);
|
const isSelected = useItemSelectionState(internalState, itemRowId || undefined);
|
||||||
|
|
||||||
const { isDragging: isDraggingLocal, ref } = useDragDrop<HTMLDivElement>({
|
const getId = useCallback(() => {
|
||||||
drag: {
|
if (!data) {
|
||||||
getId: () => {
|
return [];
|
||||||
if (!data) {
|
}
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const draggedItems = getDraggedItems(data, internalState);
|
const draggedItems = getDraggedItems(data, internalState);
|
||||||
return draggedItems.map((item) => item.id);
|
return draggedItems.map((item) => item.id);
|
||||||
},
|
}, [data, internalState]);
|
||||||
getItem: () => {
|
|
||||||
if (!data) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const draggedItems = getDraggedItems(data, internalState);
|
const getItem = useCallback(() => {
|
||||||
return draggedItems;
|
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,
|
itemType,
|
||||||
onDragStart: () => {
|
onDragStart,
|
||||||
if (!data) {
|
onDrop,
|
||||||
return;
|
operation: dragOperation,
|
||||||
}
|
|
||||||
|
|
||||||
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],
|
|
||||||
target: DragTarget.ALBUM,
|
target: DragTarget.ALBUM,
|
||||||
},
|
}),
|
||||||
|
[getId, getItem, itemType, onDragStart, onDrop, dragOperation],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { isDragging: isDraggingLocal, ref } = useDragDrop<HTMLDivElement>({
|
||||||
|
drag,
|
||||||
isEnabled: !!enableDrag && !!data,
|
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[] => {
|
export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -1160,56 +1205,67 @@ const getItemNavigationPath = (
|
|||||||
return getTitlePath(effectiveItemType, data.id);
|
return getTitlePath(effectiveItemType, data.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ItemCardRow = ({
|
const ItemCardRow = memo(
|
||||||
data,
|
({
|
||||||
index,
|
data,
|
||||||
row,
|
index,
|
||||||
type,
|
row,
|
||||||
}: {
|
type,
|
||||||
data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
|
}: {
|
||||||
index: number;
|
data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
|
||||||
row: DataRow;
|
index: number;
|
||||||
type?: 'compact' | 'default' | 'poster';
|
row: DataRow;
|
||||||
}) => {
|
type?: 'compact' | 'default' | 'poster';
|
||||||
const alignmentClass =
|
}) => {
|
||||||
row.align === 'center'
|
const alignmentClass =
|
||||||
? styles['align-center']
|
row.align === 'center'
|
||||||
: row.align === 'end'
|
? styles['align-center']
|
||||||
? styles['align-end']
|
: row.align === 'end'
|
||||||
: styles['align-start'];
|
? styles['align-end']
|
||||||
|
: styles['align-start'];
|
||||||
|
|
||||||
// All rows except the first one (index 0) should be muted
|
// All rows except the first one (index 0) should be muted
|
||||||
const isMuted = index > 0 || row.isMuted;
|
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',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Text
|
||||||
className={clsx(styles.row, alignmentClass, {
|
className={clsx(styles.row, alignmentClass, {
|
||||||
|
[styles.bold]: index === 0,
|
||||||
[styles.compact]: type === 'compact',
|
[styles.compact]: type === 'compact',
|
||||||
[styles.default]: type === 'default',
|
[styles.default]: type === 'default',
|
||||||
[styles.muted]: isMuted,
|
[styles.muted]: isMuted,
|
||||||
[styles.poster]: type === 'poster',
|
[styles.poster]: type === 'poster',
|
||||||
})}
|
})}
|
||||||
|
size={index > 0 ? 'sm' : 'md'}
|
||||||
>
|
>
|
||||||
|
{formattedContent}
|
||||||
</div>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
ItemCardRow.displayName = 'ItemCardRow';
|
||||||
<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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MemoizedItemCard = memo(ItemCard);
|
export const MemoizedItemCard = memo(ItemCard);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { QueryBuilderOption } from '/@/renderer/components/query-builder/query-builder-option';
|
import { QueryBuilderOption } from '/@/renderer/components/query-builder/query-builder-option';
|
||||||
@@ -107,15 +108,17 @@ export const QueryBuilder = ({
|
|||||||
onChangeType({ groupIndex, level, value });
|
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 (
|
return (
|
||||||
<Box
|
<Box p="md" style={boxStyle}>
|
||||||
p="md"
|
|
||||||
style={{
|
|
||||||
border: '1px solid var(--theme-colors-border)',
|
|
||||||
borderRadius: 'var(--theme-radius-md)',
|
|
||||||
marginLeft: level > 0 ? '20px' : '0px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Group gap="sm" justify="space-between" wrap="nowrap">
|
<Group gap="sm" justify="space-between" wrap="nowrap">
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
|
|||||||
@@ -67,16 +67,13 @@ export const MultiSelectWithInvalidData = ({ data, defaultValue, ...props }: Mul
|
|||||||
return [data, []];
|
return [data, []];
|
||||||
}, [data, defaultValue]);
|
}, [data, defaultValue]);
|
||||||
|
|
||||||
return (
|
const error = useMemo(
|
||||||
<MultiSelect
|
() =>
|
||||||
data={fullData}
|
missing.length
|
||||||
defaultValue={defaultValue}
|
? t('error.badValue', { postProcess: 'sentenceCase', value: missing })
|
||||||
error={
|
: undefined,
|
||||||
missing.length
|
[missing, t],
|
||||||
? t('error.badValue', { postProcess: 'sentenceCase', value: missing })
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return <MultiSelect data={fullData} defaultValue={defaultValue} error={error} {...props} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||||
@@ -147,8 +147,8 @@ export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFilte
|
|||||||
[setMaxYear],
|
[setMaxYear],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleGenresFilter = useMemo(
|
const handleGenresFilter = useCallback(
|
||||||
() => (e: string[] | undefined) => {
|
(e: null | string[]) => {
|
||||||
setGenreId(e && e.length > 0 ? e : null);
|
setGenreId(e && e.length > 0 ? e : null);
|
||||||
},
|
},
|
||||||
[setGenreId],
|
[setGenreId],
|
||||||
@@ -178,13 +178,16 @@ export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFilte
|
|||||||
}));
|
}));
|
||||||
}, [albumArtistListQuery.data?.items]);
|
}, [albumArtistListQuery.data?.items]);
|
||||||
|
|
||||||
const handleAlbumArtistFilter = (e: null | string[]) => {
|
const handleAlbumArtistFilter = useCallback(
|
||||||
setAlbumArtist(e ?? null);
|
(e: null | string[]) => {
|
||||||
};
|
setAlbumArtist(e ?? null);
|
||||||
|
},
|
||||||
|
[setAlbumArtist],
|
||||||
|
);
|
||||||
|
|
||||||
const handleTagFilter = useMemo(
|
const handleTagFilter = useCallback(
|
||||||
() => (e: string[] | undefined) => {
|
(e: null | string[]) => {
|
||||||
setCustom({ Tags: e?.join('|') ?? null });
|
setCustom({ Tags: e && e.length > 0 ? e.join('|') : null });
|
||||||
},
|
},
|
||||||
[setCustom],
|
[setCustom],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { ChangeEvent, useMemo } from 'react';
|
import { ChangeEvent, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||||
@@ -148,6 +148,28 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
|
|||||||
|
|
||||||
const debouncedHandleYearFilter = useDebouncedCallback(handleYearFilter, 300);
|
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 (
|
return (
|
||||||
<Stack px="md" py="md">
|
<Stack px="md" py="md">
|
||||||
{yesNoUndefinedFilters.map((filter) => (
|
{yesNoUndefinedFilters.map((filter) => (
|
||||||
@@ -180,7 +202,7 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
|
|||||||
data={genreList}
|
data={genreList}
|
||||||
defaultValue={query.genreIds || []}
|
defaultValue={query.genreIds || []}
|
||||||
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
|
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
|
||||||
onChange={(e) => (e && e.length > 0 ? setGenreId(e) : setGenreId(null))}
|
onChange={handleGenreChange}
|
||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -191,7 +213,7 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
|
|||||||
disabled={disableArtistFilter}
|
disabled={disableArtistFilter}
|
||||||
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
|
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
|
||||||
limit={300}
|
limit={300}
|
||||||
onChange={(e) => (e && e.length > 0 ? setAlbumArtist(e) : setAlbumArtist(null))}
|
onChange={handleAlbumArtistChange}
|
||||||
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
|
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
|
||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
@@ -224,6 +246,17 @@ const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterI
|
|||||||
return Array.isArray(value) ? value : [value];
|
return Array.isArray(value) ? value : [value];
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e: null | string[]) => {
|
||||||
|
if (e && e.length > 0) {
|
||||||
|
onChange(e);
|
||||||
|
} else {
|
||||||
|
onChange(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiSelectWithInvalidData
|
<MultiSelectWithInvalidData
|
||||||
clearable
|
clearable
|
||||||
@@ -232,7 +265,7 @@ const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterI
|
|||||||
key={tagValue}
|
key={tagValue}
|
||||||
label={label}
|
label={label}
|
||||||
limit={100}
|
limit={100}
|
||||||
onChange={(e) => (e && e.length > 0 ? onChange(e) : onChange(null))}
|
onChange={handleChange}
|
||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
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 { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||||
@@ -63,8 +63,8 @@ export const SubsonicAlbumFilters = ({ disableArtistFilter }: SubsonicAlbumFilte
|
|||||||
}));
|
}));
|
||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
const handleAlbumArtistFilter = useMemo(
|
const handleAlbumArtistFilter = useCallback(
|
||||||
() => (e: null | string[]) => {
|
(e: null | string[]) => {
|
||||||
setAlbumArtist(e ?? null);
|
setAlbumArtist(e ?? null);
|
||||||
},
|
},
|
||||||
[setAlbumArtist],
|
[setAlbumArtist],
|
||||||
@@ -80,8 +80,8 @@ export const SubsonicAlbumFilters = ({ disableArtistFilter }: SubsonicAlbumFilte
|
|||||||
}));
|
}));
|
||||||
}, [genreListQuery.data]);
|
}, [genreListQuery.data]);
|
||||||
|
|
||||||
const handleGenresFilter = useMemo(
|
const handleGenresFilter = useCallback(
|
||||||
() => (e: null | string) => {
|
(e: null | string) => {
|
||||||
setGenreId(e ? [e] : null);
|
setGenreId(e ? [e] : null);
|
||||||
},
|
},
|
||||||
[setGenreId],
|
[setGenreId],
|
||||||
@@ -178,7 +178,7 @@ export const SubsonicAlbumFilters = ({ disableArtistFilter }: SubsonicAlbumFilte
|
|||||||
defaultValue={query.genreIds?.[0] ?? undefined}
|
defaultValue={query.genreIds?.[0] ?? undefined}
|
||||||
disabled={Boolean(query.minYear || query.maxYear)}
|
disabled={Boolean(query.minYear || query.maxYear)}
|
||||||
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
|
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
|
||||||
onChange={(e) => handleGenresFilter(e)}
|
onChange={handleGenresFilter}
|
||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { ComponentPropsWithoutRef } from 'react';
|
import { ComponentPropsWithoutRef, memo, useMemo } from 'react';
|
||||||
|
|
||||||
import styles from './lyric-line.module.css';
|
import styles from './lyric-line.module.css';
|
||||||
|
|
||||||
@@ -12,23 +12,28 @@ interface LyricLineProps extends ComponentPropsWithoutRef<'div'> {
|
|||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LyricLine = ({ alignment, className, fontSize, text, ...props }: LyricLineProps) => {
|
export const LyricLine = memo(
|
||||||
const lines = text.split('_BREAK_');
|
({ alignment, className, fontSize, text, ...props }: LyricLineProps) => {
|
||||||
|
const lines = useMemo(() => text.split('_BREAK_'), [text]);
|
||||||
|
|
||||||
return (
|
const style = useMemo(
|
||||||
<Box
|
() => ({
|
||||||
className={clsx(styles.lyricLine, className)}
|
|
||||||
style={{
|
|
||||||
fontSize,
|
fontSize,
|
||||||
textAlign: alignment,
|
textAlign: alignment,
|
||||||
}}
|
}),
|
||||||
{...props}
|
[fontSize, alignment],
|
||||||
>
|
);
|
||||||
<Stack gap={0}>
|
|
||||||
{lines.map((line, index) => (
|
return (
|
||||||
<span key={index}>{line}</span>
|
<Box className={clsx(styles.lyricLine, className)} style={style} {...props}>
|
||||||
))}
|
<Stack gap={0}>
|
||||||
</Stack>
|
{lines.map((line, index) => (
|
||||||
</Box>
|
<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 { shallow } from 'zustand/shallow';
|
||||||
|
|
||||||
import { useSettingsStore } from '/@/renderer/store';
|
import { useSettingsStore } from '/@/renderer/store';
|
||||||
@@ -90,29 +98,38 @@ export const SearchInput = ({
|
|||||||
const shouldShowInput = isInputMode || hasValue;
|
const shouldShowInput = isInputMode || hasValue;
|
||||||
const shouldExpand = isInputMode || hasValue;
|
const shouldExpand = isInputMode || hasValue;
|
||||||
|
|
||||||
const containerStyle: CSSProperties = {
|
const containerStyle: CSSProperties = useMemo(
|
||||||
display: 'inline-flex',
|
() => ({
|
||||||
overflow: 'hidden',
|
display: 'inline-flex',
|
||||||
position: 'relative',
|
overflow: 'hidden',
|
||||||
transition: 'width 0.3s ease-in-out',
|
position: 'relative',
|
||||||
width: shouldExpand ? '200px' : '36px',
|
transition: 'width 0.3s ease-in-out',
|
||||||
};
|
width: shouldExpand ? '200px' : '36px',
|
||||||
|
}),
|
||||||
|
[shouldExpand],
|
||||||
|
);
|
||||||
|
|
||||||
const buttonStyle: CSSProperties = {
|
const buttonStyle: CSSProperties = useMemo(
|
||||||
left: 0,
|
() => ({
|
||||||
opacity: shouldShowInput ? 0 : 1,
|
left: 0,
|
||||||
pointerEvents: shouldShowInput ? 'none' : 'auto',
|
opacity: shouldShowInput ? 0 : 1,
|
||||||
position: 'absolute',
|
pointerEvents: shouldShowInput ? 'none' : 'auto',
|
||||||
top: 0,
|
position: 'absolute',
|
||||||
transition: 'opacity 0.2s ease-in-out',
|
top: 0,
|
||||||
zIndex: 10,
|
transition: 'opacity 0.2s ease-in-out',
|
||||||
};
|
zIndex: 10,
|
||||||
|
}),
|
||||||
|
[shouldShowInput],
|
||||||
|
);
|
||||||
|
|
||||||
const inputStyle: CSSProperties = {
|
const inputStyle: CSSProperties = useMemo(
|
||||||
opacity: shouldShowInput ? 1 : 0,
|
() => ({
|
||||||
transition: 'opacity 0.2s ease-in-out',
|
opacity: shouldShowInput ? 1 : 0,
|
||||||
width: '100%',
|
transition: 'opacity 0.2s ease-in-out',
|
||||||
};
|
width: '100%',
|
||||||
|
}),
|
||||||
|
[shouldShowInput],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box style={containerStyle}>
|
<Box style={containerStyle}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||||
@@ -105,8 +105,8 @@ export const JellyfinSongFilters = () => {
|
|||||||
const debouncedHandleMinYearFilter = useDebouncedCallback(handleMinYearFilter, 300);
|
const debouncedHandleMinYearFilter = useDebouncedCallback(handleMinYearFilter, 300);
|
||||||
const debouncedHandleMaxYearFilter = useDebouncedCallback(handleMaxYearFilter, 300);
|
const debouncedHandleMaxYearFilter = useDebouncedCallback(handleMaxYearFilter, 300);
|
||||||
|
|
||||||
const handleGenresFilter = useMemo(
|
const handleGenresFilter = useCallback(
|
||||||
() => (e: string[] | undefined) => {
|
(e: null | string[]) => {
|
||||||
setCustom((prev) => {
|
setCustom((prev) => {
|
||||||
const current = prev ?? {};
|
const current = prev ?? {};
|
||||||
|
|
||||||
@@ -129,9 +129,9 @@ export const JellyfinSongFilters = () => {
|
|||||||
[setCustom],
|
[setCustom],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTagFilter = useMemo(
|
const handleTagFilter = useCallback(
|
||||||
() => (e: string[] | undefined) => {
|
(e: null | string[]) => {
|
||||||
setCustom({ Tags: e?.join('|') ?? null });
|
setCustom({ Tags: e && e.length > 0 ? e.join('|') : null });
|
||||||
},
|
},
|
||||||
[setCustom],
|
[setCustom],
|
||||||
);
|
);
|
||||||
@@ -173,7 +173,7 @@ export const JellyfinSongFilters = () => {
|
|||||||
data={genreList}
|
data={genreList}
|
||||||
defaultValue={selectedGenres}
|
defaultValue={selectedGenres}
|
||||||
label={t('entity.genre', { count: 1, postProcess: 'sentenceCase' })}
|
label={t('entity.genre', { count: 1, postProcess: 'sentenceCase' })}
|
||||||
onChange={(e) => handleGenresFilter(e)}
|
onChange={handleGenresFilter}
|
||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -183,7 +183,7 @@ export const JellyfinSongFilters = () => {
|
|||||||
data={tagsQuery.data.boolTags}
|
data={tagsQuery.data.boolTags}
|
||||||
defaultValue={selectedTags}
|
defaultValue={selectedTags}
|
||||||
label={t('common.tags', { postProcess: 'sentenceCase' })}
|
label={t('common.tags', { postProcess: 'sentenceCase' })}
|
||||||
onChange={(e) => handleTagFilter(e)}
|
onChange={handleTagFilter}
|
||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||||
@@ -73,6 +73,17 @@ export const NavidromeSongFilters = () => {
|
|||||||
|
|
||||||
const debouncedHandleYearFilter = useDebouncedCallback(handleYearFilter, 300);
|
const debouncedHandleYearFilter = useDebouncedCallback(handleYearFilter, 300);
|
||||||
|
|
||||||
|
const handleGenreChange = useCallback(
|
||||||
|
(e: null | string[]) => {
|
||||||
|
if (e && e.length > 0) {
|
||||||
|
setGenreId(e);
|
||||||
|
} else {
|
||||||
|
setGenreId(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setGenreId],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack px="md" py="md">
|
<Stack px="md" py="md">
|
||||||
{yesNoUndefinedFilters.map((filter) => (
|
{yesNoUndefinedFilters.map((filter) => (
|
||||||
@@ -99,7 +110,7 @@ export const NavidromeSongFilters = () => {
|
|||||||
data={genreList}
|
data={genreList}
|
||||||
defaultValue={query.genreIds || []}
|
defaultValue={query.genreIds || []}
|
||||||
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
|
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
|
||||||
onChange={(e) => (e && e.length > 0 ? setGenreId(e) : setGenreId(null))}
|
onChange={handleGenreChange}
|
||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -132,6 +143,17 @@ const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterI
|
|||||||
return Array.isArray(value) ? value : [value];
|
return Array.isArray(value) ? value : [value];
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e: null | string[]) => {
|
||||||
|
if (e && e.length > 0) {
|
||||||
|
onChange(e);
|
||||||
|
} else {
|
||||||
|
onChange(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiSelectWithInvalidData
|
<MultiSelectWithInvalidData
|
||||||
clearable
|
clearable
|
||||||
@@ -140,7 +162,7 @@ const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterI
|
|||||||
key={tagValue}
|
key={tagValue}
|
||||||
label={label}
|
label={label}
|
||||||
limit={100}
|
limit={100}
|
||||||
onChange={(e) => (e && e.length > 0 ? onChange(e) : onChange(null))}
|
onChange={handleChange}
|
||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChangeEvent, useMemo } from 'react';
|
import { ChangeEvent, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||||
@@ -29,8 +29,8 @@ export const SubsonicSongFilters = () => {
|
|||||||
}));
|
}));
|
||||||
}, [genreListQuery.data]);
|
}, [genreListQuery.data]);
|
||||||
|
|
||||||
const handleGenresFilter = useMemo(
|
const handleGenresFilter = useCallback(
|
||||||
() => (e: null | string) => {
|
(e: null | string) => {
|
||||||
setGenreId(e ? [e] : null);
|
setGenreId(e ? [e] : null);
|
||||||
},
|
},
|
||||||
[setGenreId],
|
[setGenreId],
|
||||||
|
|||||||
@@ -32,12 +32,12 @@ const ResponsiveLayoutBase = ({ shell }: ResponsiveLayoutProps) => {
|
|||||||
|
|
||||||
export const ResponsiveLayout = ({ shell }: ResponsiveLayoutProps) => {
|
export const ResponsiveLayout = ({ shell }: ResponsiveLayoutProps) => {
|
||||||
useAppTracker();
|
useAppTracker();
|
||||||
useGarbageCollection();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ResponsiveLayoutBase shell={shell} />
|
<ResponsiveLayoutBase shell={shell} />
|
||||||
<LayoutHotkeys />
|
<LayoutHotkeys />
|
||||||
|
<GarbageCollection />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -77,3 +77,8 @@ const LayoutHotkeys = () => {
|
|||||||
|
|
||||||
return <CommandPalette modalProps={{ handlers, opened }} />;
|
return <CommandPalette modalProps={{ handlers, opened }} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const GarbageCollection = () => {
|
||||||
|
useGarbageCollection();
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|||||||
@@ -249,9 +249,14 @@ export const useAppTheme = (overrideTheme?: AppTheme) => {
|
|||||||
}
|
}
|
||||||
}, [colorVars, selectedTheme, themeVars]);
|
}, [colorVars, selectedTheme, themeVars]);
|
||||||
|
|
||||||
|
const mantineTheme = useMemo(
|
||||||
|
() => createMantineTheme(appTheme as AppThemeConfiguration),
|
||||||
|
[appTheme],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mode: appTheme?.mode || 'dark',
|
mode: appTheme?.mode || 'dark',
|
||||||
theme: createMantineTheme(appTheme as AppThemeConfiguration),
|
theme: mantineTheme,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
ActionIcon as MantineActionIcon,
|
ActionIcon as MantineActionIcon,
|
||||||
ActionIconProps as MantineActionIconProps,
|
ActionIconProps as MantineActionIconProps,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef, useMemo } from 'react';
|
||||||
|
|
||||||
import styles from './action-icon.module.css';
|
import styles from './action-icon.module.css';
|
||||||
|
|
||||||
@@ -41,11 +41,16 @@ const _ActionIcon = forwardRef<HTMLButtonElement, ActionIconProps>(
|
|||||||
if (onClick) onClick(e);
|
if (onClick) onClick(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionIconProps: ActionIconProps = {
|
const memoizedClassNames = useMemo(
|
||||||
classNames: {
|
() => ({
|
||||||
root: styles.root,
|
root: styles.root,
|
||||||
...classNames,
|
...classNames,
|
||||||
},
|
}),
|
||||||
|
[classNames],
|
||||||
|
);
|
||||||
|
|
||||||
|
const actionIconProps: ActionIconProps = {
|
||||||
|
classNames: memoizedClassNames,
|
||||||
size,
|
size,
|
||||||
variant,
|
variant,
|
||||||
...props,
|
...props,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Badge as MantineBadge,
|
Badge as MantineBadge,
|
||||||
BadgeProps as MantineBadgeProps,
|
BadgeProps as MantineBadgeProps,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import styles from './badge.module.css';
|
import styles from './badge.module.css';
|
||||||
|
|
||||||
@@ -12,17 +13,20 @@ export interface BadgeProps
|
|||||||
extends ElementProps<'div', keyof MantineBadgeProps>,
|
extends ElementProps<'div', keyof MantineBadgeProps>,
|
||||||
MantineBadgeProps {}
|
MantineBadgeProps {}
|
||||||
|
|
||||||
const _Badge = ({ children, classNames, variant = 'default', ...props }: BadgeProps) => {
|
const BaseBadge = ({ children, classNames, variant = 'default', ...props }: BadgeProps) => {
|
||||||
|
const memoizedClassNames = useMemo(
|
||||||
|
() => ({
|
||||||
|
root: styles.root,
|
||||||
|
...classNames,
|
||||||
|
}),
|
||||||
|
[classNames],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineBadge
|
<MantineBadge classNames={memoizedClassNames} radius="md" variant={variant} {...props}>
|
||||||
classNames={{ root: styles.root, ...classNames }}
|
|
||||||
radius="md"
|
|
||||||
variant={variant}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</MantineBadge>
|
</MantineBadge>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Badge = createPolymorphicComponent<'button', BadgeProps>(_Badge);
|
export const Badge = createPolymorphicComponent<'button', BadgeProps>(BaseBadge);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { ElementProps, Box as MantineBox, BoxProps as MantineBoxProps } from '@mantine/core';
|
import { ElementProps, Box as MantineBox, BoxProps as MantineBoxProps } from '@mantine/core';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
export interface BoxProps extends ElementProps<'div', keyof MantineBoxProps>, MantineBoxProps {}
|
export interface BoxProps extends ElementProps<'div', keyof MantineBoxProps>, MantineBoxProps {}
|
||||||
|
|
||||||
export const Box = ({ children, ...props }: BoxProps) => {
|
export const Box = memo(({ children, ...props }: BoxProps) => {
|
||||||
return <MantineBox {...props}>{children}</MantineBox>;
|
return <MantineBox {...props}>{children}</MantineBox>;
|
||||||
};
|
});
|
||||||
|
|
||||||
|
Box.displayName = 'Box';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { ButtonVariant, ButtonProps as MantineButtonProps } from '@mantine/
|
|||||||
|
|
||||||
import { ElementProps, Button as MantineButton } from '@mantine/core';
|
import { ElementProps, Button as MantineButton } from '@mantine/core';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import styles from './button.module.css';
|
import styles from './button.module.css';
|
||||||
|
|
||||||
@@ -41,21 +41,26 @@ export const _Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
}: ButtonProps,
|
}: ButtonProps,
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
|
const memoizedClassNames = useMemo(
|
||||||
|
() => ({
|
||||||
|
inner: styles.inner,
|
||||||
|
label: clsx(styles.label, {
|
||||||
|
[styles.uppercase]: uppercase,
|
||||||
|
}),
|
||||||
|
loader: styles.loader,
|
||||||
|
root: styles.root,
|
||||||
|
section: styles.section,
|
||||||
|
...classNames,
|
||||||
|
}),
|
||||||
|
[classNames, uppercase],
|
||||||
|
);
|
||||||
|
|
||||||
if (tooltip) {
|
if (tooltip) {
|
||||||
return (
|
return (
|
||||||
<Tooltip withinPortal {...tooltip}>
|
<Tooltip withinPortal {...tooltip}>
|
||||||
<MantineButton
|
<MantineButton
|
||||||
autoContrast
|
autoContrast
|
||||||
classNames={{
|
classNames={memoizedClassNames}
|
||||||
inner: styles.inner,
|
|
||||||
label: clsx(styles.label, {
|
|
||||||
[styles.uppercase]: uppercase,
|
|
||||||
}),
|
|
||||||
loader: styles.loader,
|
|
||||||
root: styles.root,
|
|
||||||
section: styles.section,
|
|
||||||
...classNames,
|
|
||||||
}}
|
|
||||||
loading={loading}
|
loading={loading}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
size={size}
|
size={size}
|
||||||
@@ -71,16 +76,7 @@ export const _Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineButton
|
<MantineButton
|
||||||
classNames={{
|
classNames={memoizedClassNames}
|
||||||
inner: styles.inner,
|
|
||||||
label: clsx(styles.label, {
|
|
||||||
[styles.uppercase]: uppercase,
|
|
||||||
}),
|
|
||||||
loader: styles.loader,
|
|
||||||
root: styles.root,
|
|
||||||
section: styles.section,
|
|
||||||
...classNames,
|
|
||||||
}}
|
|
||||||
loading={loading}
|
loading={loading}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
size={size}
|
size={size}
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
import { Center as MantineCenter, CenterProps as MantineCenterProps } from '@mantine/core';
|
import { Center as MantineCenter, CenterProps as MantineCenterProps } from '@mantine/core';
|
||||||
import { forwardRef, MouseEvent } from 'react';
|
import { forwardRef, memo, MouseEvent, useMemo } from 'react';
|
||||||
|
|
||||||
export interface CenterProps extends MantineCenterProps {
|
export interface CenterProps extends MantineCenterProps {
|
||||||
onClick?: (e: MouseEvent<HTMLDivElement>) => void;
|
onClick?: (e: MouseEvent<HTMLDivElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Center = forwardRef<HTMLDivElement, CenterProps>(
|
const _Center = forwardRef<HTMLDivElement, CenterProps>(
|
||||||
({ children, classNames, onClick, style, ...props }, ref) => {
|
({ children, classNames, onClick, style, ...props }, ref) => {
|
||||||
|
const memoizedClassNames = useMemo(() => ({ ...classNames }), [classNames]);
|
||||||
|
const memoizedStyle = useMemo(() => ({ ...style }), [style]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineCenter
|
<MantineCenter
|
||||||
classNames={{ ...classNames }}
|
classNames={memoizedClassNames}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={{ ...style }}
|
style={memoizedStyle}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -20,3 +23,7 @@ export const Center = forwardRef<HTMLDivElement, CenterProps>(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_Center.displayName = 'Center';
|
||||||
|
|
||||||
|
export const Center = memo(_Center);
|
||||||
|
|||||||
@@ -1,19 +1,33 @@
|
|||||||
import { Divider as MantineDivider, DividerProps as MantineDividerProps } from '@mantine/core';
|
import { Divider as MantineDivider, DividerProps as MantineDividerProps } from '@mantine/core';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef, memo, useMemo } from 'react';
|
||||||
|
|
||||||
import styles from './divider.module.css';
|
import styles from './divider.module.css';
|
||||||
|
|
||||||
export interface DividerProps extends MantineDividerProps {}
|
export interface DividerProps extends MantineDividerProps {}
|
||||||
|
|
||||||
export const Divider = forwardRef<HTMLDivElement, DividerProps>(
|
const _Divider = forwardRef<HTMLDivElement, DividerProps>(
|
||||||
({ classNames, style, ...props }, ref) => {
|
({ classNames, style, ...props }, ref) => {
|
||||||
|
const memoizedClassNames = useMemo(
|
||||||
|
() => ({
|
||||||
|
root: styles.root,
|
||||||
|
...classNames,
|
||||||
|
}),
|
||||||
|
[classNames],
|
||||||
|
);
|
||||||
|
|
||||||
|
const memoizedStyle = useMemo(() => ({ ...style }), [style]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineDivider
|
<MantineDivider
|
||||||
classNames={{ root: styles.root, ...classNames }}
|
classNames={memoizedClassNames}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={{ ...style }}
|
style={memoizedStyle}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_Divider.displayName = 'Divider';
|
||||||
|
|
||||||
|
export const Divider = memo(_Divider);
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import { Flex as MantineFlex, FlexProps as MantineFlexProps } from '@mantine/core';
|
import { Flex as MantineFlex, FlexProps as MantineFlexProps } from '@mantine/core';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef, memo, useMemo } from 'react';
|
||||||
|
|
||||||
export interface FlexProps extends MantineFlexProps {}
|
export interface FlexProps extends MantineFlexProps {}
|
||||||
|
|
||||||
export const Flex = forwardRef<HTMLDivElement, FlexProps>(({ children, ...props }, ref) => {
|
const _Flex = forwardRef<HTMLDivElement, FlexProps>(
|
||||||
return (
|
({ children, classNames, style, ...props }, ref) => {
|
||||||
<MantineFlex
|
const memoizedClassNames = useMemo(() => ({ ...classNames }), [classNames]);
|
||||||
classNames={{ ...props.classNames }}
|
const memoizedStyle = useMemo(() => ({ ...style }), [style]);
|
||||||
ref={ref}
|
|
||||||
style={{ ...props.style }}
|
return (
|
||||||
{...props}
|
<MantineFlex classNames={memoizedClassNames} ref={ref} style={memoizedStyle} {...props}>
|
||||||
>
|
{children}
|
||||||
{children}
|
</MantineFlex>
|
||||||
</MantineFlex>
|
);
|
||||||
);
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
|
_Flex.displayName = 'Flex';
|
||||||
|
|
||||||
|
export const Flex = memo(_Flex);
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import { Grid as MantineGrid, GridProps as MantineGridProps } from '@mantine/core';
|
import { Grid as MantineGrid, GridProps as MantineGridProps } from '@mantine/core';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
|
||||||
export interface GridProps extends MantineGridProps {}
|
export interface GridProps extends MantineGridProps {}
|
||||||
|
|
||||||
export const Grid = ({ classNames, style, ...props }: GridProps) => {
|
const BaseGrid = ({ classNames, style, ...props }: GridProps) => {
|
||||||
return <MantineGrid classNames={{ ...classNames }} style={{ ...style }} {...props} />;
|
const memoizedClassNames = useMemo(() => ({ ...classNames }), [classNames]);
|
||||||
|
const memoizedStyle = useMemo(() => ({ ...style }), [style]);
|
||||||
|
|
||||||
|
return <MantineGrid classNames={memoizedClassNames} style={memoizedStyle} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
Grid.Col = MantineGrid.Col;
|
BaseGrid.displayName = 'Grid';
|
||||||
|
|
||||||
|
export const Grid = memo(BaseGrid);
|
||||||
|
|
||||||
|
(Grid as typeof Grid & { Col: typeof MantineGrid.Col }).Col = MantineGrid.Col;
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
import { Group as MantineGroup, GroupProps as MantineGroupProps } from '@mantine/core';
|
import { Group as MantineGroup, GroupProps as MantineGroupProps } from '@mantine/core';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef, memo, useMemo } from 'react';
|
||||||
|
|
||||||
export interface GroupProps extends MantineGroupProps {}
|
export interface GroupProps extends MantineGroupProps {}
|
||||||
|
|
||||||
export const Group = forwardRef<HTMLDivElement, GroupProps>(({ children, ...props }, ref) => {
|
const _Group = forwardRef<HTMLDivElement, GroupProps>(
|
||||||
return (
|
({ children, classNames, style, ...props }, ref) => {
|
||||||
<MantineGroup
|
const memoizedClassNames = useMemo(() => ({ ...classNames }), [classNames]);
|
||||||
classNames={{ ...props.classNames }}
|
const memoizedStyle = useMemo(() => ({ ...style }), [style]);
|
||||||
ref={ref}
|
|
||||||
style={{ ...props.style }}
|
return (
|
||||||
{...props}
|
<MantineGroup
|
||||||
>
|
classNames={memoizedClassNames}
|
||||||
{children}
|
ref={ref}
|
||||||
</MantineGroup>
|
style={memoizedStyle}
|
||||||
);
|
{...props}
|
||||||
});
|
>
|
||||||
|
{children}
|
||||||
|
</MantineGroup>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
_Group.displayName = 'Group';
|
||||||
|
|
||||||
|
export const Group = memo(_Group);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { motion } from 'motion/react';
|
import { motion } from 'motion/react';
|
||||||
import { type ComponentType, forwardRef } from 'react';
|
import { type ComponentType, forwardRef, memo, useMemo } from 'react';
|
||||||
import { IconBaseProps } from 'react-icons';
|
import { IconBaseProps } from 'react-icons';
|
||||||
import { FaLastfmSquare } from 'react-icons/fa';
|
import { FaLastfmSquare } from 'react-icons/fa';
|
||||||
import {
|
import {
|
||||||
@@ -278,19 +278,23 @@ type IconColor =
|
|||||||
| 'success'
|
| 'success'
|
||||||
| 'warn';
|
| 'warn';
|
||||||
|
|
||||||
export const Icon = forwardRef<HTMLDivElement, IconProps>((props, ref) => {
|
const _Icon = forwardRef<HTMLDivElement, IconProps>((props, ref) => {
|
||||||
const { animate, className, color, fill, icon, size = 'md' } = props;
|
const { animate, className, color, fill, icon, size = 'md' } = props;
|
||||||
|
|
||||||
const IconComponent: ComponentType<any> = AppIcon[icon];
|
const IconComponent: ComponentType<any> = AppIcon[icon];
|
||||||
|
|
||||||
const classNames = clsx(className, {
|
const classNames = useMemo(
|
||||||
[styles.fill]: true,
|
() =>
|
||||||
[styles.pulse]: animate === 'pulse',
|
clsx(className, {
|
||||||
[styles.spin]: animate === 'spin',
|
[styles.fill]: true,
|
||||||
[styles[`color-${color || fill}`]]: color || fill,
|
[styles.pulse]: animate === 'pulse',
|
||||||
[styles[`fill-${fill}`]]: fill,
|
[styles.spin]: animate === 'spin',
|
||||||
[styles[`size-${size}`]]: true,
|
[styles[`color-${color || fill}`]]: color || fill,
|
||||||
});
|
[styles[`fill-${fill}`]]: fill,
|
||||||
|
[styles[`size-${size}`]]: true,
|
||||||
|
}),
|
||||||
|
[animate, className, color, fill, size],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconComponent
|
<IconComponent
|
||||||
@@ -302,6 +306,10 @@ export const Icon = forwardRef<HTMLDivElement, IconProps>((props, ref) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_Icon.displayName = 'Icon';
|
||||||
|
|
||||||
|
export const Icon = memo(_Icon);
|
||||||
|
|
||||||
Icon.displayName = 'Icon';
|
Icon.displayName = 'Icon';
|
||||||
|
|
||||||
export const MotionIcon: ComponentType = motion.create(Icon);
|
export const MotionIcon: ComponentType = motion.create(Icon);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
MultiSelect as MantineMultiSelect,
|
MultiSelect as MantineMultiSelect,
|
||||||
MultiSelectProps as MantineMultiSelectProps,
|
MultiSelectProps as MantineMultiSelectProps,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { CSSProperties } from 'react';
|
import { CSSProperties, useMemo } from 'react';
|
||||||
|
|
||||||
import styles from './multi-select.module.css';
|
import styles from './multi-select.module.css';
|
||||||
|
|
||||||
@@ -11,6 +11,23 @@ export interface MultiSelectProps extends MantineMultiSelectProps {
|
|||||||
width?: CSSProperties['width'];
|
width?: CSSProperties['width'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultClassNames = {
|
||||||
|
dropdown: styles.dropdown,
|
||||||
|
input: styles.input,
|
||||||
|
label: styles.label,
|
||||||
|
option: styles.option,
|
||||||
|
pill: styles.pill,
|
||||||
|
pillsList: styles.pillsList,
|
||||||
|
root: styles.root,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultClearButtonProps = {
|
||||||
|
classNames: {
|
||||||
|
root: styles.clearButton,
|
||||||
|
},
|
||||||
|
variant: 'transparent' as const,
|
||||||
|
};
|
||||||
|
|
||||||
export const MultiSelect = ({
|
export const MultiSelect = ({
|
||||||
classNames,
|
classNames,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
@@ -18,25 +35,21 @@ export const MultiSelect = ({
|
|||||||
width,
|
width,
|
||||||
...props
|
...props
|
||||||
}: MultiSelectProps) => {
|
}: MultiSelectProps) => {
|
||||||
|
const mergedClassNames = useMemo(
|
||||||
|
() => (classNames ? { ...defaultClassNames, ...classNames } : defaultClassNames),
|
||||||
|
[classNames],
|
||||||
|
);
|
||||||
|
|
||||||
|
const style = useMemo(
|
||||||
|
() => (maxWidth || width ? { maxWidth, width } : undefined),
|
||||||
|
[maxWidth, width],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineMultiSelect
|
<MantineMultiSelect
|
||||||
classNames={{
|
classNames={mergedClassNames}
|
||||||
dropdown: styles.dropdown,
|
clearButtonProps={defaultClearButtonProps}
|
||||||
input: styles.input,
|
style={style}
|
||||||
label: styles.label,
|
|
||||||
option: styles.option,
|
|
||||||
pill: styles.pill,
|
|
||||||
pillsList: styles.pillsList,
|
|
||||||
root: styles.root,
|
|
||||||
...classNames,
|
|
||||||
}}
|
|
||||||
clearButtonProps={{
|
|
||||||
classNames: {
|
|
||||||
root: styles.clearButton,
|
|
||||||
},
|
|
||||||
variant: 'transparent',
|
|
||||||
}}
|
|
||||||
style={{ maxWidth, width }}
|
|
||||||
variant={variant}
|
variant={variant}
|
||||||
withCheckIcon={false}
|
withCheckIcon={false}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ReactNode } from 'react';
|
import { memo, ReactNode, useMemo } from 'react';
|
||||||
|
|
||||||
import styles from './option.module.css';
|
import styles from './option.module.css';
|
||||||
|
|
||||||
@@ -10,13 +10,22 @@ interface OptionProps extends GroupProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Option = ({ children, ...props }: OptionProps) => {
|
const defaultClassNames = { root: styles.root };
|
||||||
|
|
||||||
|
export const Option = memo(({ children, classNames, ...props }: OptionProps) => {
|
||||||
|
const mergedClassNames = useMemo(
|
||||||
|
() => (classNames ? { ...defaultClassNames, ...classNames } : defaultClassNames),
|
||||||
|
[classNames],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group classNames={{ root: styles.root }} grow {...props}>
|
<Group classNames={mergedClassNames} grow {...props}>
|
||||||
{children}
|
{children}
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
Option.displayName = 'Option';
|
||||||
|
|
||||||
interface LabelProps {
|
interface LabelProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -34,5 +43,5 @@ const Control = ({ children }: ControlProps) => {
|
|||||||
return <Flex justify="flex-end">{children}</Flex>;
|
return <Flex justify="flex-end">{children}</Flex>;
|
||||||
};
|
};
|
||||||
|
|
||||||
Option.Label = Label;
|
(Option as typeof Option & { Label: typeof Label }).Label = Label;
|
||||||
Option.Control = Control;
|
(Option as typeof Option & { Control: typeof Control }).Control = Control;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { PaperProps as MantinePaperProps } from '@mantine/core';
|
import type { PaperProps as MantinePaperProps } from '@mantine/core';
|
||||||
|
|
||||||
import { Paper as MantinePaper } from '@mantine/core';
|
import { Paper as MantinePaper } from '@mantine/core';
|
||||||
import { ReactNode } from 'react';
|
import { memo, ReactNode, useMemo } from 'react';
|
||||||
|
|
||||||
import styles from './paper.module.css';
|
import styles from './paper.module.css';
|
||||||
|
|
||||||
@@ -9,19 +9,24 @@ export interface PaperProps extends MantinePaperProps {
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Paper = ({ children, classNames, style, ...props }: PaperProps) => {
|
const BasePaper = ({ children, classNames, style, ...props }: PaperProps) => {
|
||||||
|
const memoizedClassNames = useMemo(
|
||||||
|
() => ({
|
||||||
|
root: styles.root,
|
||||||
|
...classNames,
|
||||||
|
}),
|
||||||
|
[classNames],
|
||||||
|
);
|
||||||
|
|
||||||
|
const memoizedStyle = useMemo(() => ({ ...style }), [style]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantinePaper
|
<MantinePaper classNames={memoizedClassNames} style={memoizedStyle} {...props}>
|
||||||
classNames={{
|
|
||||||
root: styles.root,
|
|
||||||
...classNames,
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</MantinePaper>
|
</MantinePaper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
BasePaper.displayName = 'Paper';
|
||||||
|
|
||||||
|
export const Paper = memo(BasePaper);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Center } from '@mantine/core';
|
import { Center } from '@mantine/core';
|
||||||
|
import { memo } from 'react';
|
||||||
import { IconBaseProps } from 'react-icons';
|
import { IconBaseProps } from 'react-icons';
|
||||||
import { CgSpinnerTwo } from 'react-icons/cg';
|
import { CgSpinnerTwo } from 'react-icons/cg';
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ interface SpinnerProps extends IconBaseProps {
|
|||||||
|
|
||||||
export const SpinnerIcon = CgSpinnerTwo;
|
export const SpinnerIcon = CgSpinnerTwo;
|
||||||
|
|
||||||
export const Spinner = ({ ...props }: SpinnerProps) => {
|
const _Spinner = ({ ...props }: SpinnerProps) => {
|
||||||
if (props.container) {
|
if (props.container) {
|
||||||
return (
|
return (
|
||||||
<Center className={styles.container}>
|
<Center className={styles.container}>
|
||||||
@@ -23,3 +24,7 @@ export const Spinner = ({ ...props }: SpinnerProps) => {
|
|||||||
|
|
||||||
return <SpinnerIcon className={styles.icon} color={props.color} size={props.size} />;
|
return <SpinnerIcon className={styles.icon} color={props.color} size={props.size} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_Spinner.displayName = 'Spinner';
|
||||||
|
|
||||||
|
export const Spinner = memo(_Spinner);
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
import { Stack as MantineStack, StackProps as MantineStackProps } from '@mantine/core';
|
import { Stack as MantineStack, StackProps as MantineStackProps } from '@mantine/core';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef, memo, useMemo } from 'react';
|
||||||
|
|
||||||
export interface StackProps extends MantineStackProps {}
|
export interface StackProps extends MantineStackProps {}
|
||||||
|
|
||||||
export const Stack = forwardRef<HTMLDivElement, StackProps>(({ children, ...props }, ref) => {
|
const _Stack = forwardRef<HTMLDivElement, StackProps>(
|
||||||
return (
|
({ children, classNames, style, ...props }, ref) => {
|
||||||
<MantineStack
|
const memoizedClassNames = useMemo(() => ({ ...classNames }), [classNames]);
|
||||||
classNames={{ ...props.classNames }}
|
const memoizedStyle = useMemo(() => ({ ...style }), [style]);
|
||||||
ref={ref}
|
|
||||||
style={{ ...props.style }}
|
return (
|
||||||
{...props}
|
<MantineStack
|
||||||
>
|
classNames={memoizedClassNames}
|
||||||
{children}
|
ref={ref}
|
||||||
</MantineStack>
|
style={memoizedStyle}
|
||||||
);
|
{...props}
|
||||||
});
|
>
|
||||||
|
{children}
|
||||||
|
</MantineStack>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
_Stack.displayName = 'Stack';
|
||||||
|
|
||||||
|
export const Stack = memo(_Stack);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Text as MantineText, TextProps as MantineTextProps } from '@mantine/core';
|
import { Text as MantineText, TextProps as MantineTextProps } from '@mantine/core';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
import { ComponentPropsWithoutRef, ReactNode, useMemo } from 'react';
|
||||||
|
|
||||||
import styles from './text.module.css';
|
import styles from './text.module.css';
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ type Font = 'Epilogue' | 'Gotham' | 'Inter' | 'Poppins';
|
|||||||
|
|
||||||
type MantineTextDivProps = ComponentPropsWithoutRef<'div'> & MantineTextProps;
|
type MantineTextDivProps = ComponentPropsWithoutRef<'div'> & MantineTextProps;
|
||||||
|
|
||||||
export const _Text = ({
|
export const BaseText = ({
|
||||||
children,
|
children,
|
||||||
font,
|
font,
|
||||||
isLink,
|
isLink,
|
||||||
@@ -31,28 +31,31 @@ export const _Text = ({
|
|||||||
weight,
|
weight,
|
||||||
...rest
|
...rest
|
||||||
}: TextProps) => {
|
}: TextProps) => {
|
||||||
|
const classNames = useMemo(
|
||||||
|
() => ({
|
||||||
|
root: clsx(styles.root, {
|
||||||
|
[styles.link]: isLink,
|
||||||
|
[styles.muted]: isMuted,
|
||||||
|
[styles.noSelect]: isNoSelect,
|
||||||
|
[styles.overflowHidden]: overflow === 'hidden',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
[isLink, isMuted, isNoSelect, overflow],
|
||||||
|
);
|
||||||
|
|
||||||
|
const style = useMemo(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
'--font-family': font,
|
||||||
|
}) as React.CSSProperties,
|
||||||
|
[font],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineText
|
<MantineText classNames={classNames} component="div" fw={weight} style={style} {...rest}>
|
||||||
classNames={{
|
|
||||||
root: clsx(styles.root, {
|
|
||||||
[styles.link]: isLink,
|
|
||||||
[styles.muted]: isMuted,
|
|
||||||
[styles.noSelect]: isNoSelect,
|
|
||||||
[styles.overflowHidden]: overflow === 'hidden',
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
component="div"
|
|
||||||
fw={weight}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
'--font-family': font,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</MantineText>
|
</MantineText>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Text = createPolymorphicComponent<'div', TextProps>(_Text);
|
export const Text = createPolymorphicComponent<'div', TextProps>(BaseText);
|
||||||
|
|||||||
@@ -1,38 +1,61 @@
|
|||||||
import { Tooltip as MantineTooltip, TooltipProps as MantineTooltipProps } from '@mantine/core';
|
import { Tooltip as MantineTooltip, TooltipProps as MantineTooltipProps } from '@mantine/core';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
|
||||||
import styles from './tooltip.module.css';
|
import styles from './tooltip.module.css';
|
||||||
|
|
||||||
export interface TooltipProps extends MantineTooltipProps {}
|
export interface TooltipProps extends MantineTooltipProps {}
|
||||||
|
|
||||||
export const Tooltip = ({
|
const DEFAULT_TRANSITION_PROPS = {
|
||||||
children,
|
duration: 250,
|
||||||
classNames,
|
transition: 'fade',
|
||||||
openDelay = 500,
|
} as const;
|
||||||
transitionProps = {
|
|
||||||
duration: 250,
|
const TooltipComponent = memo(
|
||||||
transition: 'fade',
|
({
|
||||||
},
|
children,
|
||||||
withinPortal = true,
|
classNames,
|
||||||
...props
|
openDelay = 500,
|
||||||
}: TooltipProps) => {
|
transitionProps = DEFAULT_TRANSITION_PROPS,
|
||||||
return (
|
withinPortal = true,
|
||||||
<MantineTooltip
|
...props
|
||||||
arrowSize={10}
|
}: TooltipProps) => {
|
||||||
classNames={{
|
const memoizedClassNames = useMemo(
|
||||||
|
() => ({
|
||||||
...classNames,
|
...classNames,
|
||||||
tooltip: clsx(styles.tooltip, classNames?.['tooltip']),
|
tooltip: clsx(styles.tooltip, classNames?.['tooltip']),
|
||||||
}}
|
}),
|
||||||
multiline
|
[classNames],
|
||||||
openDelay={openDelay}
|
);
|
||||||
transitionProps={transitionProps}
|
|
||||||
withArrow
|
const memoizedTransitionProps = useMemo(
|
||||||
withinPortal={withinPortal}
|
() => transitionProps ?? DEFAULT_TRANSITION_PROPS,
|
||||||
{...props}
|
[transitionProps],
|
||||||
>
|
);
|
||||||
{children}
|
|
||||||
</MantineTooltip>
|
return (
|
||||||
);
|
<MantineTooltip
|
||||||
|
arrowSize={10}
|
||||||
|
classNames={memoizedClassNames}
|
||||||
|
multiline
|
||||||
|
openDelay={openDelay}
|
||||||
|
transitionProps={memoizedTransitionProps}
|
||||||
|
withArrow
|
||||||
|
withinPortal={withinPortal}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MantineTooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
TooltipComponent.displayName = 'Tooltip';
|
||||||
|
|
||||||
|
export const Tooltip = TooltipComponent as typeof TooltipComponent & {
|
||||||
|
Group: typeof MantineTooltip.Group;
|
||||||
};
|
};
|
||||||
|
|
||||||
Tooltip.Group = MantineTooltip.Group;
|
Tooltip.Group = MantineTooltip.Group;
|
||||||
|
|
||||||
|
Tooltip.Group = MantineTooltip.Group;
|
||||||
|
|||||||
Reference in New Issue
Block a user