redesign command palette

- add collapsible search groups
- reduce modal padding
- reduce command item padding
- use breadcrumbs for pagination
- move page breadcrumbs to the bottom
This commit is contained in:
jeffvli
2026-03-17 22:28:07 -07:00
parent 3eafa73217
commit 3c562c1398
5 changed files with 146 additions and 44 deletions
@@ -0,0 +1,42 @@
.root {
display: flex;
flex-direction: column;
gap: var(--theme-spacing-xs);
&:not(:last-child) {
margin-bottom: var(--theme-spacing-xs);
}
}
.heading {
display: flex;
gap: var(--theme-spacing-xs);
align-items: center;
font-size: var(--theme-font-size-sm);
cursor: pointer;
user-select: none;
opacity: 0.8;
}
.heading:hover {
opacity: 1;
}
.heading:focus-visible {
outline: 2px solid var(--theme-colors-primary);
outline-offset: 2px;
}
.chevron {
flex-shrink: 0;
width: 1rem;
height: 1rem;
opacity: 0.9;
}
.items {
display: flex;
flex-direction: column;
gap: 0;
}
@@ -0,0 +1,52 @@
import { ReactNode, useCallback, useState } from 'react';
import styles from './collapsible-command-group.module.css';
import { Icon } from '/@/shared/components/icon/icon';
import { Paper } from '/@/shared/components/paper/paper';
interface CollapsibleCommandGroupProps {
children: ReactNode;
defaultExpanded?: boolean;
heading: string;
}
export function CollapsibleCommandGroup({
children,
defaultExpanded = true,
heading,
}: CollapsibleCommandGroupProps) {
const [expanded, setExpanded] = useState(defaultExpanded);
const toggle = useCallback(() => {
setExpanded((prev) => !prev);
}, []);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggle();
}
},
[toggle],
);
return (
<div className={styles.root}>
<Paper p="xs" radius="sm" withBorder>
<div
className={styles.heading}
onClick={toggle}
onKeyDown={handleKeyDown}
role="button"
tabIndex={0}
>
<Icon className={styles.chevron} icon={expanded ? 'dropdown' : 'arrowRightS'} />
<span>{heading}</span>
</div>
</Paper>
{expanded && <div className={styles.items}>{children}</div>}
</div>
);
}
@@ -1,9 +1,10 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Fragment, useCallback, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { generatePath, useNavigate } from 'react-router'; import { generatePath, useNavigate } from 'react-router';
import { searchQueries } from '/@/renderer/features/search/api/search-api'; import { searchQueries } from '/@/renderer/features/search/api/search-api';
import { CollapsibleCommandGroup } from '/@/renderer/features/search/components/collapsible-command-group';
import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command'; import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command';
import { CommandItemSelectable } from '/@/renderer/features/search/components/command-item-selectable'; import { CommandItemSelectable } from '/@/renderer/features/search/components/command-item-selectable';
import { GoToCommands } from '/@/renderer/features/search/components/go-to-commands'; import { GoToCommands } from '/@/renderer/features/search/components/go-to-commands';
@@ -13,8 +14,9 @@ import { ServerCommands } from '/@/renderer/features/search/components/server-co
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Box } from '/@/shared/components/box/box'; import { Breadcrumb } from '/@/shared/components/breadcrumb/breadcrumb';
import { Button } from '/@/shared/components/button/button'; import { Button } from '/@/shared/components/button/button';
import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
import { Kbd } from '/@/shared/components/kbd/kbd'; import { Kbd } from '/@/shared/components/kbd/kbd';
@@ -94,19 +96,10 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
}} }}
size="lg" size="lg"
styles={{ styles={{
body: { padding: '0' },
header: { display: 'none' }, header: { display: 'none' },
}} }}
> >
<Group gap="sm" mb="1rem">
{pages.map((page, index) => (
<Fragment key={page}>
{index > 0 && ' > '}
<Button disabled size="compact-md" variant="default">
{page?.toLocaleUpperCase()}
</Button>
</Fragment>
))}
</Group>
<Command <Command
filter={(value, search) => { filter={(value, search) => {
if (value.includes(search)) return 1; if (value.includes(search)) return 1;
@@ -130,26 +123,32 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
onChange={(e) => setQuery(e.currentTarget.value)} onChange={(e) => setQuery(e.currentTarget.value)}
ref={searchInputRef} ref={searchInputRef}
rightSection={ rightSection={
query && ( isLoading ? (
<ActionIcon <Spinner />
onClick={() => { ) : (
setQuery(''); query && (
searchInputRef.current?.focus(); <ActionIcon
}} onClick={() => {
variant="transparent" setQuery('');
> searchInputRef.current?.focus();
<Icon icon="x" /> }}
</ActionIcon> variant="transparent"
>
<Icon icon="x" />
</ActionIcon>
)
) )
} }
size="sm" size="sm"
value={query} value={query}
/> />
<Command.Separator /> <Divider my="sm" />
<Command.List> <Command.List>
<Command.Empty>No results found.</Command.Empty> <Command.Empty>
{t('common.noResultsFromQuery', { postProcess: 'sentenceCase' })}
</Command.Empty>
{showAlbumGroup && ( {showAlbumGroup && (
<Command.Group heading="Albums"> <CollapsibleCommandGroup heading="Albums">
{data?.albums?.map((album) => ( {data?.albums?.map((album) => (
<CommandItemSelectable <CommandItemSelectable
key={`search-album-${album.id}`} key={`search-album-${album.id}`}
@@ -180,10 +179,10 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
)} )}
</CommandItemSelectable> </CommandItemSelectable>
))} ))}
</Command.Group> </CollapsibleCommandGroup>
)} )}
{showArtistGroup && ( {showArtistGroup && (
<Command.Group heading="Artists"> <CollapsibleCommandGroup heading="Artists">
{data?.albumArtists.map((artist) => ( {data?.albumArtists.map((artist) => (
<CommandItemSelectable <CommandItemSelectable
key={`artist-${artist.id}`} key={`artist-${artist.id}`}
@@ -219,10 +218,10 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
)} )}
</CommandItemSelectable> </CommandItemSelectable>
))} ))}
</Command.Group> </CollapsibleCommandGroup>
)} )}
{showTrackGroup && ( {showTrackGroup && (
<Command.Group heading="Tracks"> <CollapsibleCommandGroup heading="Tracks">
{data?.songs.map((song) => ( {data?.songs.map((song) => (
<CommandItemSelectable <CommandItemSelectable
key={`artist-${song.id}`} key={`artist-${song.id}`}
@@ -254,7 +253,7 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
)} )}
</CommandItemSelectable> </CommandItemSelectable>
))} ))}
</Command.Group> </CollapsibleCommandGroup>
)} )}
{activePage === CommandPalettePages.HOME && ( {activePage === CommandPalettePages.HOME && (
<HomeCommands <HomeCommands
@@ -281,19 +280,28 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
)} )}
</Command.List> </Command.List>
</Command> </Command>
<Box mt="0.5rem" p="0.5rem"> <Divider my="sm" />
<Group justify="space-between"> <Group justify="space-between">
<Command.Loading> <Breadcrumb separator={<Icon icon="arrowRight" />}>
{isHome && isLoading && query !== '' && <Spinner />} {pages.map((page, index) => (
</Command.Loading> <Button
<Group gap="sm"> key={page}
<Kbd size="md">ESC</Kbd> onClick={() => setPages((prev) => prev.slice(0, index + 1))}
<Kbd size="md"></Kbd> size="compact-xs"
<Kbd size="md"></Kbd> variant="subtle"
<Kbd size="md"></Kbd> >
</Group> {page?.toLocaleUpperCase()}
</Button>
))}
</Breadcrumb>
<Group gap="sm">
<Kbd size="md">ESC</Kbd>
<Kbd size="md"></Kbd>
<Kbd size="md"></Kbd>
<Kbd size="md"></Kbd>
</Group> </Group>
</Box> </Group>
</Modal> </Modal>
); );
}; };
@@ -14,7 +14,7 @@ input[cmdk-input] {
[cmdk-group-items] { [cmdk-group-items] {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--theme-spacing-xs); gap: 0;
} }
[cmdk-item] { [cmdk-item] {
@@ -113,7 +113,7 @@ export const LibraryCommandItem = ({
</div> </div>
<div className={styles.metadataWrapper}> <div className={styles.metadataWrapper}>
<Text overflow="hidden">{title}</Text> <Text overflow="hidden">{title}</Text>
<Text isMuted overflow="hidden"> <Text isMuted overflow="hidden" size="sm">
{subtitle} {subtitle}
</Text> </Text>
</div> </div>