mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +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 { 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user