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 { Fragment, useCallback, useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath, useNavigate } from 'react-router';
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 { CommandItemSelectable } from '/@/renderer/features/search/components/command-item-selectable';
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 { useCurrentServer } from '/@/renderer/store';
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 { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { Kbd } from '/@/shared/components/kbd/kbd';
@@ -94,19 +96,10 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
}}
size="lg"
styles={{
body: { padding: '0' },
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
filter={(value, search) => {
if (value.includes(search)) return 1;
@@ -130,26 +123,32 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
onChange={(e) => setQuery(e.currentTarget.value)}
ref={searchInputRef}
rightSection={
query && (
<ActionIcon
onClick={() => {
setQuery('');
searchInputRef.current?.focus();
}}
variant="transparent"
>
<Icon icon="x" />
</ActionIcon>
isLoading ? (
<Spinner />
) : (
query && (
<ActionIcon
onClick={() => {
setQuery('');
searchInputRef.current?.focus();
}}
variant="transparent"
>
<Icon icon="x" />
</ActionIcon>
)
)
}
size="sm"
value={query}
/>
<Command.Separator />
<Divider my="sm" />
<Command.List>
<Command.Empty>No results found.</Command.Empty>
<Command.Empty>
{t('common.noResultsFromQuery', { postProcess: 'sentenceCase' })}
</Command.Empty>
{showAlbumGroup && (
<Command.Group heading="Albums">
<CollapsibleCommandGroup heading="Albums">
{data?.albums?.map((album) => (
<CommandItemSelectable
key={`search-album-${album.id}`}
@@ -180,10 +179,10 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
)}
</CommandItemSelectable>
))}
</Command.Group>
</CollapsibleCommandGroup>
)}
{showArtistGroup && (
<Command.Group heading="Artists">
<CollapsibleCommandGroup heading="Artists">
{data?.albumArtists.map((artist) => (
<CommandItemSelectable
key={`artist-${artist.id}`}
@@ -219,10 +218,10 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
)}
</CommandItemSelectable>
))}
</Command.Group>
</CollapsibleCommandGroup>
)}
{showTrackGroup && (
<Command.Group heading="Tracks">
<CollapsibleCommandGroup heading="Tracks">
{data?.songs.map((song) => (
<CommandItemSelectable
key={`artist-${song.id}`}
@@ -254,7 +253,7 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
)}
</CommandItemSelectable>
))}
</Command.Group>
</CollapsibleCommandGroup>
)}
{activePage === CommandPalettePages.HOME && (
<HomeCommands
@@ -281,19 +280,28 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
)}
</Command.List>
</Command>
<Box mt="0.5rem" p="0.5rem">
<Group justify="space-between">
<Command.Loading>
{isHome && isLoading && query !== '' && <Spinner />}
</Command.Loading>
<Group gap="sm">
<Kbd size="md">ESC</Kbd>
<Kbd size="md"></Kbd>
<Kbd size="md"></Kbd>
<Kbd size="md"></Kbd>
</Group>
<Divider my="sm" />
<Group justify="space-between">
<Breadcrumb separator={<Icon icon="arrowRight" />}>
{pages.map((page, index) => (
<Button
key={page}
onClick={() => setPages((prev) => prev.slice(0, index + 1))}
size="compact-xs"
variant="subtle"
>
{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>
</Box>
</Group>
</Modal>
);
};
@@ -14,7 +14,7 @@ input[cmdk-input] {
[cmdk-group-items] {
display: flex;
flex-direction: column;
gap: var(--theme-spacing-xs);
gap: 0;
}
[cmdk-item] {
@@ -113,7 +113,7 @@ export const LibraryCommandItem = ({
</div>
<div className={styles.metadataWrapper}>
<Text overflow="hidden">{title}</Text>
<Text isMuted overflow="hidden">
<Text isMuted overflow="hidden" size="sm">
{subtitle}
</Text>
</div>