Spotlight
Spotlight
애플리케이션의 명령 센터(command center)예요. @mantine/spotlight 패키지로 제공되며 MIT 라이선스로 배포돼요.
출처: 문서
본문
설치
yarn add @mantine/spotlight
설치 후 애플리케이션의 루트에서 패키지 스타일을 import 해요:
import '@mantine/core/styles.css';
// ‼️ spotlight 스타일은 core 패키지 스타일 다음에 import 하세요
import '@mantine/spotlight/styles.css';
사용법
Spotlight 컴포넌트는 애플리케이션의 검색 또는 명령 센터로 사용할 수 있어요. mantine.dev 웹사이트에서 검색으로 사용되며 Ctrl + K 단축키로 트리거할 수 있어요. Spotlight은 Modal 컴포넌트를 기반으로 하며 대부분의 props를 지원해요.
import { Button } from '@mantine/core';
import { Spotlight, SpotlightActionData, spotlight } from '@mantine/spotlight';
import { HouseIcon, GaugeIcon, FileTextIcon, MagnifyingGlassIcon } from '@phosphor-icons/react';
const actions: SpotlightActionData[] = [
{
id: 'home',
label: 'Home',
description: 'Get to home page',
onClick: () => console.log('Home'),
leftSection: <HouseIcon />,
},
{
id: 'dashboard',
label: 'Dashboard',
description: 'Get full information about current system status',
onClick: () => console.log('Dashboard'),
leftSection: <GaugeIcon />,
},
{
id: 'documentation',
label: 'Documentation',
description: 'Visit documentation to lean more about all features',
onClick: () => console.log('Documentation'),
leftSection: <FileTextIcon />,
},
];
function Demo() {
return (
<>
<Button onClick={spotlight.open}>Open spotlight</Button>
<Spotlight
actions={actions}
searchProps={{
leftSection: <MagnifyingGlassIcon />,
placeholder: 'Search...',
}}
/>
</>
);
}
Actions
@mantine/spotlight 패키지는 spotlight를 제어하는 데 사용할 수 있는 actions 객체를 내보내요:
import { spotlight } from '@mantine/spotlight';
spotlight.open(); // -> opens spotlight
spotlight.close(); // -> closes spotlight
spotlight.toggle(); // -> toggles spotlight opened state
이 actions들은 이벤트 리스너에 전달하거나 애플리케이션 어디서든(React 컴포넌트에 국한되지 않음) 사용할 수 있어요:
import { Button } from '@mantine/core';
import { spotlight } from '@mantine/spotlight';
function Demo() {
return <Button onClick={spotlight.open}>Open spotlight</Button>;
}
이 문법을 선호한다면 @mantine/spotlight 패키지에서 actions를 직접 import 할 수도 있어요:
import {
closeSpotlight,
openSpotlight,
toggleSpotlight,
} from '@mantine/spotlight';
openSpotlight(); // same as spotlight.open()
closeSpotlight(); // same as spotlight.close()
toggleSpotlight(); // same as spotlight.toggle()
Spotlight store
위에서 문서화한 spotlight 객체는 기본 store를 사용해요; 애플리케이션에 spotlight가 하나뿐이라면 잘 동작해요. 여러 spotlight가 필요하다면 각각에 대해 자신만의 store를 만들어야 해요:
import { Button } from '@mantine/core';
import { createSpotlight, Spotlight } from '@mantine/spotlight';
// You can import `firstSpotlight` and `secondSpotlight` anywhere
// in your application and use `open`, `close` and `toggle` actions
// to control spotlight the same way as with default `spotlight` object
export const [firstStore, firstSpotlight] = createSpotlight();
export const [secondStore, secondSpotlight] = createSpotlight();
function Demo() {
return (
<>
<Spotlight store={firstStore} />
<Spotlight store={secondStore} />
<Button onClick={firstSpotlight.open}>Open first spotlight</Button>
<Button onClick={secondSpotlight.open}>Open second spotlight</Button>
</>
);
}
키보드 단축키
Spotlight은 키보드 단축키를 처리하기 위해 use-hotkeys 훅을 사용해요. 기본적으로 Ctrl + K와 Cmd + K 단축키로 spotlight를 열어요; shortcut prop으로 바꿀 수 있어요:
import { Spotlight } from '@mantine/spotlight';
function SingleShortcut() {
return <Spotlight shortcut="mod + shift + P" />;
}
// Same as on mantine.dev
function MultipleShortcuts() {
return (
<Spotlight shortcut={['mod + K', 'mod + shift + K']} />
);
}
// Disable shortcut
function NoShortcut() {
return <Spotlight shortcut={null} />;
}
Limit prop
limit prop으로 한 번에 표시할 수 있는 최대 actions 수를 제한해요. 보통 5~7개가 좋은 숫자예요. limit prop은 actions가 많을 때 성능에 중요하며, spotlight가 모든 actions를 한 번에 렌더링하지 못하게 해요.
아래 예시는 3000개의 actions를 렌더링하지만 한 번에 7개만 표시돼요:
import { Button } from '@mantine/core';
import { Spotlight, SpotlightActionData, spotlight } from '@mantine/spotlight';
import { MagnifyingGlassIcon } from '@phosphor-icons/react';
const actions: SpotlightActionData[] = Array(3000)
.fill(0)
.map((_, index) => ({
id: `action-${index}`,
label: `Action ${index}`,
description: `Action ${index} description`,
}));
function Demo() {
return (
<>
<Button onClick={spotlight.open}>Open spotlight</Button>
<Spotlight
actions={actions}
limit={7}
searchProps={{
leftSection: <MagnifyingGlassIcon />,
placeholder: 'Search...',
}}
/>
</>
);
}
커스텀 필터 함수
기본적으로 Spotlight은 label, description, keywords로 actions를 매칭하는 간단한 필터를 사용해요. 커스텀 filter 함수를 제공해 필터링 로직을 커스터마이즈할 수 있어요. filter 함수는 검색 쿼리와 actions 배열을 받아 필터링된 actions를 반환해야 해요.
커스텀 filter 함수 시그니처:
type SpotlightFilterFunction = (
query: string,
actions: SpotlightActions[]
) => SpotlightActions[];
fuse.js로 퍼지 검색
fuse.js 라이브러리로 퍼지(fuzzy) 검색을 구현할 수 있어요. 오타가 있거나 부분 일치해도 actions를 매칭하고 싶을 때 유용해요:
import Fuse from 'fuse.js';
import { Button } from '@mantine/core';
import {
Spotlight,
SpotlightActionData,
SpotlightFilterFunction,
spotlight,
} from '@mantine/spotlight';
import { HouseIcon, GaugeIcon, FileTextIcon, MagnifyingGlassIcon } from '@phosphor-icons/react';
const actions: SpotlightActionData[] = [
{ id: 'home', label: 'Home', description: 'Get to home page', leftSection: <HouseIcon /> },
{ id: 'dashboard', label: 'Dashboard', description: 'Get full information about current system status', leftSection: <GaugeIcon /> },
{ id: 'documentation', label: 'Documentation', description: 'Visit documentation to learn more about all features', leftSection: <FileTextIcon /> },
{ id: 'settings', label: 'Settings', description: 'Manage application preferences and configurations', leftSection: <GaugeIcon /> },
];
const fuzzySearchFilter: SpotlightFilterFunction = (query, searchActions) => {
if (!query.trim()) {
return searchActions;
}
const flatActions = searchActions.reduce((acc, item) => {
if ('actions' in item) {
return [...acc, ...item.actions.map((action) => ({ ...action, group: item.group }))];
}
return [...acc, item];
}, []);
const fuse = new Fuse(flatActions, {
keys: ['label', 'description'],
threshold: 0.3,
minMatchCharLength: 1,
});
const results = fuse.search(query).map((result) => result.item);
const groups: Record = {};
const result: any[] = [];
results.forEach((action) => {
if (action.group) {
if (!groups[action.group]) {
groups[action.group] = { pushed: false, data: { group: action.group, actions: [] } };
}
groups[action.group].data.actions.push(action);
if (!groups[action.group].pushed) {
groups[action.group].pushed = true;
result.push(groups[action.group].data);
}
} else {
result.push(action);
}
});
return result;
};
function Demo() {
return (
<>
<Button onClick={spotlight.open}>Open spotlight</Button>
<Spotlight
actions={actions}
filter={fuzzySearchFilter}
searchProps={{
leftSection: <MagnifyingGlassIcon />,
placeholder: 'Search...',
}}
/>
</>
);
}
스크롤 가능한 actions 목록
기본적으로 Spotlight의 actions 목록은 스크롤할 수 없어요. 한 번에 표시해야 할 actions가 많다면 scrollable과 maxHeight props를 설정해요. 두 접근 방식 모두 주의사항이 있어요:
-
scrollableprop이 설정되지 않으면 actions 목록 높이는 제한되지 않고, spotlight 본문이 모든 actions를 담도록 커져요. 이렇게 하면 뷰포트를 넘칠 만큼 긴 spotlight 본문이 생길 수 있어요. 막으려면limitprop으로 한 번에 표시할 수 있는 최대 actions 수를 정의해요. 보통 5~7개가 좋은 숫자예요. -
scrollableprop이 설정되면 actions 목록 높이는 항상maxHeightprop 값과 같아요(공간을 채울 만큼 actions가 없으면 줄어들지 않아요). 목록에 담을 수 있는 것보다 actions가 더 많으면 스크롤 가능해져요. 스크롤링 로직은 ScrollArea 컴포넌트가 처리해요.
다시 말하면, actions 목록을 줄어들게 하고 싶다면 scrollable prop을 설정하지 말고 limit prop을 사용해요. actions 목록이 항상 고정 높이를 가지게 하고 싶다면 scrollable과 maxHeight props를 설정해요.
import { Button } from '@mantine/core';
import { Spotlight, SpotlightActionData, spotlight } from '@mantine/spotlight';
import { MagnifyingGlassIcon } from '@phosphor-icons/react';
const actions: SpotlightActionData[] = Array(100)
.fill(0)
.map((_, index) => ({
id: `action-${index}`,
label: `Action ${index}`,
description: `Action ${index} description`,
}));
function Demo() {
return (
<>
<Button onClick={spotlight.open}>Open spotlight</Button>
<Spotlight
actions={actions}
scrollable
maxHeight={350}
searchProps={{
leftSection: <MagnifyingGlassIcon />,
placeholder: 'Search...',
}}
/>
</>
);
}
Actions 그룹
Spotlight은 action 그룹을 지원해요; actions를 카테고리로 그룹화하는 데 사용할 수 있어요:
import { Button } from '@mantine/core';
import { Spotlight, SpotlightActionData, SpotlightActionGroupData, spotlight } from '@mantine/spotlight';
import { MagnifyingGlassIcon } from '@phosphor-icons/react';
const actions: (SpotlightActionGroupData | SpotlightActionData)[] = [
{
group: 'Pages',
actions: [
{ id: 'home', label: 'Home page', description: 'Where we present the product' },
{ id: 'careers', label: 'Careers page', description: 'Where we list open positions' },
{ id: 'about-us', label: 'About us page', description: 'Where we tell what we do' },
],
},
{
group: 'Apps',
actions: [
{ id: 'svg-compressor', label: 'SVG compressor', description: 'Compress SVG images' },
{ id: 'base64', label: 'Base 64 converter', description: 'Convert data to base 64 format' },
{ id: 'fake-data', label: 'Fake data generator', description: 'Lorem ipsum generator' },
],
},
];
function Demo() {
return (
<>
<Button onClick={spotlight.open}>Open spotlight</Button>
<Spotlight
actions={actions}
searchProps={{
leftSection: <MagnifyingGlassIcon />,
placeholder: 'Search...',
}}
/>
</>
);
}
복합 컴포넌트
spotlight 렌더링과 로직에 대한 더 많은 제어가 필요하다면 복합 컴포넌트를 사용해요. 사용 가능한 컴포넌트:
-
Spotlight.Root– 루트 컴포넌트, 다른 모든 컴포넌트의 래퍼로 사용해야 하며 로직을 커스터마이즈하는 모든 props를 받아요 -
Spotlight.Search– 검색 input -
Spotlight.ActionsList– actions 목록, 모든 actions와 actions 그룹을 감싸는 데 필요해요 -
Spotlight.Action– action 버튼 -
Spotlight.ActionsGroup– actions 그룹 -
Spotlight.Empty– 빈 상태(아무것도 찾지 못함)
import { useState } from 'react';
import { Spotlight, spotlight } from '@mantine/spotlight';
import { Button } from '@mantine/core';
import { MagnifyingGlassIcon } from '@phosphor-icons/react';
const data = ['Home', 'About us', 'Contacts', 'Blog', 'Careers', 'Terms of service'];
function Demo() {
const [query, setQuery] = useState('');
const items = data
.filter((item) => item.toLowerCase().includes(query.toLowerCase().trim()))
.map((item) => <Spotlight.Action key={item} label={item} />);
return (
<>
<Button onClick={spotlight.open}>Open spotlight</Button>
<Spotlight.Root query={query} onQueryChange={setQuery}>
<Spotlight.Search leftSection={<MagnifyingGlassIcon />} placeholder="Search..." />
<Spotlight.ActionsList>
{items.length > 0 ? items : <Spotlight.Empty>Nothing found...</Spotlight.Empty>}
</Spotlight.ActionsList>
</Spotlight.Root>
</>
);
}
예를 들어 복합 컴포넌트 패턴으로 action 콘텐츠를 커스터마이즈할 수 있어요:
import { useState } from 'react';
import { Spotlight, spotlight } from '@mantine/spotlight';
import { Badge, Button, Center, Group, Text } from '@mantine/core';
import { MagnifyingGlassIcon } from '@phosphor-icons/react';
const data = [
{
image: 'https://img.icons8.com/clouds/256/000000/futurama-bender.png',
title: 'Bender Bending Rodríguez',
description: 'Fascinated with cooking, though has no sense of taste',
new: true,
},
{
image: 'https://img.icons8.com/clouds/256/000000/futurama-mom.png',
title: 'Carol Miller',
description: 'One of the richest people on Earth',
new: false,
},
];
function Demo() {
const [query, setQuery] = useState('');
const items = data
.filter((item) => item.title.toLowerCase().includes(query.toLowerCase().trim()))
.map((item) => (
<Spotlight.Action key={item.title} label={item.title} onClick={() => console.log(item)}>
<Group wrap="nowrap">
{item.image && (
<img src={item.image} alt={item.title} width={40} height={40} />
)}
<div>
<Text>{item.title}</Text>
{item.description && (
<Text c="dimmed" size="xs">
{item.description}
</Text>
)}
</div>
{item.new && <Badge>new</Badge>}
</Group>
</Spotlight.Action>
));
return (
<>
<Button onClick={spotlight.open}>Open spotlight</Button>
<Spotlight.Root query={query} onQueryChange={setQuery}>
<Spotlight.Search leftSection={<MagnifyingGlassIcon />} placeholder="Search..." />
<Spotlight.ActionsList>
{items.length > 0 ? items : <Spotlight.Empty>Nothing found...</Spotlight.Empty>}
</Spotlight.ActionsList>
</Spotlight.Root>
</>
);
}
고정 요소 오프셋
Spotlight 컴포넌트는 스크롤을 잠그기 위해 react-remove-scroll 패키지를 사용해요. 이러한 position: fixed 요소들을 올바르게 크기 조절하려면(문서) 그 요소들에 className을 추가해요:
import { RemoveScroll } from '@mantine/core';
function Demo() {
return (
<>
<RemoveScroll className="foo">
width: 100%
</RemoveScroll>
<RemoveScroll className="bar">
right: 0
</RemoveScroll>
</>
);
}