Add new search - cmd + k
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -75,6 +75,10 @@ export const ColorFamily = () => {
|
||||
the previous one, automatically incrementing the background depth.
|
||||
This creates clear visual hierarchy without manually picking colors.
|
||||
</p>
|
||||
<p className={styles.description}>
|
||||
Neutral 0 is the application background and should only be used once,
|
||||
at the root of your app. All other surfaces build on top of it.
|
||||
</p>
|
||||
<p className={styles.description}>
|
||||
Each level can be <strong>interactive</strong> or{' '}
|
||||
<strong>non-interactive</strong>. A Card, for example, can be flat
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 15vh;
|
||||
|
||||
&[data-entering] {
|
||||
animation: overlayFadeIn 200ms ease-out;
|
||||
}
|
||||
|
||||
&[data-exiting] {
|
||||
animation: overlayFadeOut 150ms ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes overlayFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes overlayFadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
width: min(90vw, 500px);
|
||||
max-height: min(70vh, 400px);
|
||||
background-color: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
|
||||
&[data-entering] {
|
||||
animation: modalScaleIn 200ms ease-out;
|
||||
}
|
||||
|
||||
&[data-exiting] {
|
||||
animation: modalScaleOut 150ms ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modalScaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modalScaleOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-8px);
|
||||
}
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-height: min(70vh, 400px);
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.searchField {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.srOnly {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 1rem;
|
||||
color: var(--primary);
|
||||
font-family: inherit;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
outline: none;
|
||||
|
||||
&[data-focused] {
|
||||
background-color: var(--action);
|
||||
}
|
||||
|
||||
&[data-pressed] {
|
||||
background-color: var(--action);
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 24px 12px;
|
||||
text-align: center;
|
||||
color: var(--secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Autocomplete,
|
||||
Dialog,
|
||||
Input,
|
||||
Label,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
SearchField,
|
||||
useFilter,
|
||||
} from 'react-aria-components';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { components } from '@/utils/data';
|
||||
import styles from './CommandPalette.module.css';
|
||||
|
||||
interface CommandPaletteProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
const items = components.map(c => ({ id: c.slug, ...c }));
|
||||
|
||||
export const CommandPalette = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
}: CommandPaletteProps) => {
|
||||
const router = useRouter();
|
||||
const { contains } = useFilter({ sensitivity: 'base' });
|
||||
|
||||
return (
|
||||
<ModalOverlay
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
isDismissable
|
||||
className={styles.overlay}
|
||||
>
|
||||
<Modal className={styles.modal}>
|
||||
<Dialog aria-label="Command palette" className={styles.dialog}>
|
||||
<Autocomplete filter={contains}>
|
||||
<SearchField
|
||||
autoFocus
|
||||
aria-label="Search components"
|
||||
className={styles.searchField}
|
||||
>
|
||||
<Label className={styles.srOnly}>Search</Label>
|
||||
<Input
|
||||
placeholder="Search components..."
|
||||
className={styles.input}
|
||||
/>
|
||||
</SearchField>
|
||||
<Menu
|
||||
items={items}
|
||||
className={styles.menu}
|
||||
onAction={key => {
|
||||
router.push(`/components/${key}`);
|
||||
onOpenChange(false);
|
||||
}}
|
||||
renderEmptyState={() => (
|
||||
<div className={styles.empty}>No components found.</div>
|
||||
)}
|
||||
>
|
||||
{item => (
|
||||
<MenuItem id={item.slug} className={styles.menuItem}>
|
||||
{item.title}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</Autocomplete>
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { CommandPalette } from './CommandPalette';
|
||||
@@ -10,12 +10,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.logoMobile {
|
||||
@@ -26,39 +24,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbDesktop {
|
||||
display: none;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbLink {
|
||||
color: var(--secondary);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
text-decoration: underline;
|
||||
transition: color 0.2s ease-in-out;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbSeparator {
|
||||
color: var(--secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.breadcrumbCurrent {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: none;
|
||||
|
||||
@@ -119,6 +84,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
.searchButton {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background-color: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 32px;
|
||||
padding-inline: 16px 12px;
|
||||
min-width: 180px;
|
||||
color: var(--secondary);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--action);
|
||||
color: var(--primary);
|
||||
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.searchLabel {
|
||||
display: none;
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.searchKbd {
|
||||
display: none;
|
||||
font-family: inherit;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
background-color: var(--action);
|
||||
border-radius: 4px;
|
||||
padding: 2px 5px;
|
||||
color: var(--secondary);
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
RiArrowRightSLine,
|
||||
RiGithubLine,
|
||||
RiMoonLine,
|
||||
RiSearchLine,
|
||||
RiSunLine,
|
||||
} from '@remixicon/react';
|
||||
import {
|
||||
@@ -19,10 +20,8 @@ import {
|
||||
} from 'react-aria-components';
|
||||
import styles from './Toolbar.module.css';
|
||||
import { usePlayground } from '@/utils/playground-context';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { components } from '@/utils/data';
|
||||
import { Logo } from '@/components/Sidebar/Logo';
|
||||
import { CommandPalette } from '@/components/CommandPalette';
|
||||
|
||||
interface ToolbarProps {
|
||||
version: string;
|
||||
@@ -42,70 +41,36 @@ export const Toolbar = ({ version }: ToolbarProps) => {
|
||||
setSelectedThemeName,
|
||||
} = usePlayground();
|
||||
|
||||
const pathname = usePathname();
|
||||
const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
|
||||
|
||||
// Determine breadcrumb content based on current path
|
||||
const getBreadcrumb = () => {
|
||||
const allComponents = components;
|
||||
useEffect(() => {
|
||||
const isMac = /mac(os|intosh)/i.test(navigator.userAgent);
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'k' && (isMac ? e.metaKey : e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
setIsCommandPaletteOpen(prev => !prev);
|
||||
}
|
||||
};
|
||||
|
||||
// Components index page
|
||||
if (pathname === '/components') {
|
||||
return { section: null, title: 'Components' };
|
||||
}
|
||||
|
||||
// Component detail pages
|
||||
if (pathname?.startsWith('/components/')) {
|
||||
const slug = pathname.split('/components/')[1];
|
||||
const component = allComponents.find(c => c.slug === slug);
|
||||
return {
|
||||
section: 'Components',
|
||||
sectionLink: '/components',
|
||||
title: component?.title || slug,
|
||||
};
|
||||
}
|
||||
|
||||
// Tokens page
|
||||
if (pathname === '/tokens') {
|
||||
return { section: null, title: 'Tokens' };
|
||||
}
|
||||
|
||||
// Changelog page
|
||||
if (pathname === '/changelog') {
|
||||
return { section: null, title: 'Changelog' };
|
||||
}
|
||||
|
||||
return { section: null, title: '' };
|
||||
};
|
||||
|
||||
const breadcrumb = getBreadcrumb();
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.toolbar}>
|
||||
<div className={styles.breadcrumb}>
|
||||
<div className={styles.left}>
|
||||
<div className={styles.logoMobile}>
|
||||
<Logo />
|
||||
</div>
|
||||
<div className={styles.breadcrumbDesktop}>
|
||||
{breadcrumb.section && breadcrumb.sectionLink ? (
|
||||
<>
|
||||
<Link
|
||||
href={breadcrumb.sectionLink}
|
||||
className={styles.breadcrumbLink}
|
||||
>
|
||||
{breadcrumb.section}
|
||||
</Link>
|
||||
<RiArrowRightSLine
|
||||
size={16}
|
||||
className={styles.breadcrumbSeparator}
|
||||
/>
|
||||
<span className={styles.breadcrumbCurrent}>
|
||||
{breadcrumb.title}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className={styles.breadcrumbCurrent}>{breadcrumb.title}</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.searchButton}
|
||||
onClick={() => setIsCommandPaletteOpen(true)}
|
||||
>
|
||||
<RiSearchLine size={14} />
|
||||
<span className={styles.searchLabel}>Search</span>
|
||||
<kbd className={styles.searchKbd}>⌘K</kbd>
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
<Select
|
||||
@@ -158,6 +123,10 @@ export const Toolbar = ({ version }: ToolbarProps) => {
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<CommandPalette
|
||||
isOpen={isCommandPaletteOpen}
|
||||
onOpenChange={setIsCommandPaletteOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user