Migrate to Mantine v8 and Design Changes (#961)

* mantine v8 migration

* various design changes and improvements
This commit is contained in:
Jeff 2025-06-24 00:04:36 -07:00 committed by GitHub
parent bea55d48a8
commit c1330d92b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
473 changed files with 12469 additions and 11607 deletions

View file

@ -1,12 +1,8 @@
import { ActionIcon, Group, Kbd, ScrollArea } from '@mantine/core';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { Fragment, useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { RiCloseFill, RiSearchLine } from 'react-icons/ri';
import { generatePath, useNavigate } from 'react-router';
import styled from 'styled-components';
import { Button, Modal, Paper, Spinner, TextInput } from '/@/renderer/components';
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command';
import { GoToCommands } from '/@/renderer/features/search/components/go-to-commands';
@ -16,18 +12,21 @@ import { ServerCommands } from '/@/renderer/features/search/components/server-co
import { useSearch } from '/@/renderer/features/search/queries/search-query';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Box } from '/@/shared/components/box/box';
import { Button } from '/@/shared/components/button/button';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { Kbd } from '/@/shared/components/kbd/kbd';
import { Modal } from '/@/shared/components/modal/modal';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { TextInput } from '/@/shared/components/text-input/text-input';
import { LibraryItem } from '/@/shared/types/domain-types';
interface CommandPaletteProps {
modalProps: (typeof useDisclosure)['arguments'];
}
const CustomModal = styled(Modal)`
& .mantine-Modal-header {
display: none;
}
`;
export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
const navigate = useNavigate();
const server = useCurrentServer();
@ -69,7 +68,7 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
const handlePlayQueueAdd = usePlayQueueAdd();
return (
<CustomModal
<Modal
{...modalProps}
centered
handlers={{
@ -91,19 +90,21 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
}
},
}}
scrollAreaComponent={ScrollArea.Autosize}
size="lg"
styles={{
header: { display: 'none' },
}}
>
<Group
gap="sm"
mb="1rem"
spacing="sm"
>
{pages.map((page, index) => (
<Fragment key={page}>
{index > 0 && ' > '}
<Button
compact
disabled
size="compact-md"
variant="default"
>
{page?.toLocaleUpperCase()}
@ -123,20 +124,23 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
>
<TextInput
data-autofocus
icon={<RiSearchLine />}
leftSection={<Icon icon="search" />}
onChange={(e) => setQuery(e.currentTarget.value)}
ref={searchInputRef}
rightSection={
<ActionIcon
onClick={() => {
setQuery('');
searchInputRef.current?.focus();
}}
>
<RiCloseFill />
</ActionIcon>
query && (
<ActionIcon
onClick={() => {
setQuery('');
searchInputRef.current?.focus();
}}
variant="transparent"
>
<Icon icon="x" />
</ActionIcon>
)
}
size="lg"
size="sm"
value={query}
/>
<Command.Separator />
@ -263,22 +267,22 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
)}
</Command.List>
</Command>
<Paper
<Box
mt="0.5rem"
p="0.5rem"
>
<Group position="apart">
<Group justify="space-between">
<Command.Loading>
{isHome && isLoading && query !== '' && <Spinner />}
</Command.Loading>
<Group spacing="sm">
<Group gap="sm">
<Kbd size="md">ESC</Kbd>
<Kbd size="md"></Kbd>
<Kbd size="md"></Kbd>
<Kbd size="md"></Kbd>
</Group>
</Group>
</Paper>
</CustomModal>
</Box>
</Modal>
);
};

View file

@ -0,0 +1,44 @@
input[cmdk-input] {
width: 100%;
font-size: var(--theme-font-size-md);
border: none;
border-radius: var(--theme-radius-sm);
}
[cmdk-group-heading] {
margin: var(--theme-spacing-md) 0;
font-size: var(--theme-font-size-sm);
opacity: 0.8;
}
[cmdk-group-items] {
display: flex;
flex-direction: column;
gap: var(--theme-spacing-xs);
}
[cmdk-item] {
display: flex;
gap: var(--theme-spacing-sm);
align-items: center;
padding: var(--theme-spacing-sm);
font-size: var(--theme-font-size-md);
color: var(--theme-colors-foreground);
cursor: pointer;
border-radius: var(--theme-radius-sm);
svg {
width: 1.2rem;
height: 1.2rem;
}
&[data-selected] {
background: var(--theme-colors-surface);
}
}
[cmdk-separator] {
height: 1px;
margin: 0 0 var(--theme-spacing-sm);
background: var(--theme-colors-border);
}

View file

@ -1,5 +1,6 @@
import { Command as Cmdk } from 'cmdk';
import styled from 'styled-components';
import './command.css';
export enum CommandPalettePages {
GO_TO = 'go',
@ -7,64 +8,4 @@ export enum CommandPalettePages {
MANAGE_SERVERS = 'servers',
}
export const Command = styled(Cmdk)`
[cmdk-root] {
background-color: var(--background-color);
}
input[cmdk-input] {
width: 100%;
height: 1.5rem;
padding: 1.3rem 0.5rem;
margin-bottom: 1rem;
font-family: var(--content-font-family);
color: var(--input-fg);
background: var(--input-bg);
border: none;
border-radius: 5px;
&::placeholder {
color: var(--input-placeholder-fg);
}
}
[cmdk-group-heading] {
margin: 1rem 0;
font-size: 0.9rem;
opacity: 0.8;
}
[cmdk-group-items] {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
[cmdk-item] {
display: flex;
gap: 0.5rem;
align-items: center;
padding: 0.5rem;
font-family: var(--content-font-family);
color: var(--btn-default-fg);
cursor: pointer;
background: var(--btn-default-bg);
border-radius: 5px;
svg {
width: 1.2rem;
height: 1.2rem;
}
&[data-selected] {
color: var(--btn-default-fg-hover);
background: var(--btn-default-bg-hover);
}
}
[cmdk-separator] {
height: 1px;
margin: 0 0 0.5rem;
background: var(--generic-border-color);
}
`;
export const Command = Cmdk as typeof Cmdk;

View file

@ -35,7 +35,7 @@ export const HomeCommands = ({
openModal({
children: <CreatePlaylistForm onCancel={() => closeAllModals()} />,
size: server?.type === ServerType?.NAVIDROME ? 'xl' : 'sm',
size: server?.type === ServerType?.NAVIDROME ? 'lg' : 'sm',
title: t('form.createPlaylist.title', { postProcess: 'sentenceCase' }),
});
}, [handleClose, server?.type, t]);

View file

@ -0,0 +1,34 @@
.item-grid {
display: grid;
grid-template-areas: 'image info';
grid-template-rows: 1fr;
grid-template-columns: var(--item-height) minmax(0, 1fr);
grid-auto-columns: 1fr;
gap: 0.5rem;
width: 100%;
max-width: 100%;
height: 100%;
letter-spacing: 0.5px;
}
.image-wrapper {
display: flex;
grid-area: image;
align-items: center;
justify-content: center;
height: 100%;
}
.metadata-wrapper {
display: flex;
flex-direction: column;
grid-area: info;
justify-content: center;
width: 100%;
}
.image {
object-fit: var(--theme-image-fit);
background: alpha(var(--theme-colors-foreground-muted), 0.3);
border-radius: 4px;
}

View file

@ -1,59 +1,16 @@
import { Center, Flex } from '@mantine/core';
import { MouseEvent, useCallback } from 'react';
import { CSSProperties, MouseEvent, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
RiAddBoxFill,
RiAddCircleFill,
RiAlbumFill,
RiPlayFill,
RiPlayListFill,
RiShuffleFill,
RiUserVoiceFill,
} from 'react-icons/ri';
import styled from 'styled-components';
import { Button, Text } from '/@/renderer/components';
import styles from './library-command-item.module.css';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group';
import { Image } from '/@/shared/components/image/image';
import { Text } from '/@/shared/components/text/text';
import { LibraryItem } from '/@/shared/types/domain-types';
import { Play, PlayQueueAddOptions } from '/@/shared/types/types';
const Item = styled(Flex)``;
const ItemGrid = styled.div<{ height: number }>`
display: grid;
grid-template-areas: 'image info';
grid-template-rows: 1fr;
grid-template-columns: ${(props) => props.height}px minmax(0, 1fr);
grid-auto-columns: 1fr;
gap: 0.5rem;
width: 100%;
max-width: 100%;
height: 100%;
letter-spacing: 0.5px;
`;
const ImageWrapper = styled.div`
display: flex;
grid-area: image;
align-items: center;
justify-content: center;
height: 100%;
`;
const MetadataWrapper = styled.div`
display: flex;
flex-direction: column;
grid-area: info;
justify-content: center;
width: 100%;
`;
const StyledImage = styled.img<{ placeholder?: string }>`
object-fit: var(--image-fit);
border-radius: 4px;
`;
const ActionsContainer = styled(Flex)``;
interface LibraryCommandItemProps {
disabled?: boolean;
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
@ -74,25 +31,6 @@ export const LibraryCommandItem = ({
title,
}: LibraryCommandItemProps) => {
const { t } = useTranslation();
let Placeholder = RiAlbumFill;
switch (itemType) {
case LibraryItem.ALBUM:
Placeholder = RiAlbumFill;
break;
case LibraryItem.ALBUM_ARTIST:
Placeholder = RiUserVoiceFill;
break;
case LibraryItem.ARTIST:
Placeholder = RiUserVoiceFill;
break;
case LibraryItem.PLAYLIST:
Placeholder = RiPlayListFill;
break;
default:
Placeholder = RiAlbumFill;
break;
}
const handlePlay = useCallback(
(e: MouseEvent, id: string, playType: Play) => {
@ -108,110 +46,95 @@ export const LibraryCommandItem = ({
[handlePlayQueueAdd, itemType],
);
const [isHovered, setIsHovered] = useState(false);
return (
<Item
<Flex
gap="xl"
justify="space-between"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{ height: '40px', width: '100%' }}
>
<ItemGrid height={40}>
<ImageWrapper>
{imageUrl ? (
<StyledImage
alt="cover"
height={40}
placeholder="var(--placeholder-bg)"
src={imageUrl}
style={{}}
width={40}
/>
) : (
<Center
style={{
background: 'var(--placeholder-bg)',
borderRadius: 'var(--card-default-radius)',
height: `${40}px`,
width: `${40}px`,
}}
>
<Placeholder
color="var(--placeholder-fg)"
size={35}
/>
</Center>
)}
</ImageWrapper>
<MetadataWrapper>
<div
className={styles.itemGrid}
style={{ '--item-height': '40px' } as CSSProperties}
>
<div className={styles.imageWrapper}>
<Image
alt="cover"
className={styles.image}
height={40}
src={imageUrl || ''}
width={40}
/>
</div>
<div className={styles.metadataWrapper}>
<Text overflow="hidden">{title}</Text>
<Text
$secondary
isMuted
overflow="hidden"
>
{subtitle}
</Text>
</MetadataWrapper>
</ItemGrid>
<ActionsContainer
align="center"
gap="sm"
justify="flex-end"
>
<Button
compact
disabled={disabled}
onClick={(e) => handlePlay(e, id, Play.NOW)}
size="md"
tooltip={{
label: t('player.play', { postProcess: 'sentenceCase' }),
openDelay: 500,
}}
variant="default"
</div>
</div>
{isHovered && (
<Group
align="center"
gap="sm"
justify="flex-end"
wrap="nowrap"
>
<RiPlayFill />
</Button>
{itemType !== LibraryItem.SONG && (
<Button
compact
<ActionIcon
disabled={disabled}
onClick={(e) => handlePlay(e, id, Play.SHUFFLE)}
size="md"
icon="mediaPlay"
onClick={(e) => handlePlay(e, id, Play.NOW)}
size="xs"
tooltip={{
label: t('player.shuffle', { postProcess: 'sentenceCase' }),
label: t('player.play', { postProcess: 'sentenceCase' }),
openDelay: 500,
}}
variant="default"
>
<RiShuffleFill />
</Button>
)}
<Button
compact
disabled={disabled}
onClick={(e) => handlePlay(e, id, Play.LAST)}
size="md"
tooltip={{
label: t('player.addLast', { postProcess: 'sentenceCase' }),
variant="subtle"
/>
{itemType !== LibraryItem.SONG && (
<ActionIcon
disabled={disabled}
icon="mediaShuffle"
onClick={(e) => handlePlay(e, id, Play.SHUFFLE)}
size="xs"
tooltip={{
label: t('player.shuffle', { postProcess: 'sentenceCase' }),
openDelay: 500,
}}
variant="subtle"
/>
)}
<ActionIcon
disabled={disabled}
icon="mediaPlayLast"
onClick={(e) => handlePlay(e, id, Play.LAST)}
size="xs"
tooltip={{
label: t('player.addLast', { postProcess: 'sentenceCase' }),
openDelay: 500,
}}
variant="default"
>
<RiAddBoxFill />
</Button>
<Button
compact
disabled={disabled}
onClick={(e) => handlePlay(e, id, Play.NEXT)}
size="md"
tooltip={{
label: t('player.addNext', { postProcess: 'sentenceCase' }),
openDelay: 500,
}}
variant="default"
>
<RiAddCircleFill />
</Button>
</ActionsContainer>
</Item>
openDelay: 500,
}}
variant="subtle"
/>
<ActionIcon
disabled={disabled}
icon="mediaPlayNext"
onClick={(e) => handlePlay(e, id, Play.NEXT)}
size="xs"
tooltip={{
label: t('player.addNext', { postProcess: 'sentenceCase' }),
openDelay: 500,
}}
variant="subtle"
/>
</Group>
)}
</Flex>
);
};

View file

@ -1,17 +1,21 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Flex, Group, Stack } from '@mantine/core';
import debounce from 'lodash/debounce';
import { ChangeEvent, MutableRefObject } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath, Link, useParams, useSearchParams } from 'react-router-dom';
import { Button, PageHeader, SearchInput } from '/@/renderer/components';
import { PageHeader } from '/@/renderer/components/page-header/page-header';
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
import { SearchInput } from '/@/renderer/features/shared/components/search-input';
import { useContainerQuery } from '/@/renderer/hooks';
import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useListStoreByKey } from '/@/renderer/store';
import { Button } from '/@/shared/components/button/button';
import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group';
import { Stack } from '/@/shared/components/stack/stack';
import {
AlbumArtistListQuery,
AlbumListQuery,
@ -46,8 +50,8 @@ export const SearchHeader = ({ navigationId, tableRef }: SearchHeaderProps) => {
return (
<Stack
gap={0}
ref={cq.ref}
spacing={0}
>
<PageHeader>
<Flex
@ -61,7 +65,6 @@ export const SearchHeader = ({ navigationId, tableRef }: SearchHeaderProps) => {
<SearchInput
defaultValue={searchParams.get('query') || ''}
onChange={handleSearch}
openedWidth={cq.isMd ? 250 : cq.isSm ? 200 : 150}
/>
</Group>
</Flex>
@ -69,11 +72,10 @@ export const SearchHeader = ({ navigationId, tableRef }: SearchHeaderProps) => {
<FilterBar>
<Group>
<Button
compact
component={Link}
fw={600}
replace
size="md"
size="compact-md"
state={{ navigationId }}
to={{
pathname: generatePath(AppRoute.SEARCH, { itemType: LibraryItem.SONG }),
@ -84,11 +86,10 @@ export const SearchHeader = ({ navigationId, tableRef }: SearchHeaderProps) => {
{t('entity.track_other', { postProcess: 'sentenceCase' })}
</Button>
<Button
compact
component={Link}
fw={600}
replace
size="md"
size="compact-md"
state={{ navigationId }}
to={{
pathname: generatePath(AppRoute.SEARCH, {
@ -101,11 +102,10 @@ export const SearchHeader = ({ navigationId, tableRef }: SearchHeaderProps) => {
{t('entity.album_other', { postProcess: 'sentenceCase' })}
</Button>
<Button
compact
component={Link}
fw={600}
replace
size="md"
size="compact-md"
state={{ navigationId }}
to={{
pathname: generatePath(AppRoute.SEARCH, {