mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-06 20:10:12 +02:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user