ProjectSwitcher
A project name in a toolbar that opens a menu of projects, recent projects and their actions. The app passes the lists and handles each pick; the component holds no project state.
import { ProjectSwitcher } from '@adecore/ui';import { useState } from 'react';
import { Folder, FolderOpen, NotebookPen, Settings2, X } from 'lucide-react';
import { Icon, Menu, ProjectSwitcher, type ProjectSwitcherItem } from '@adecore/ui';
const PROJECTS: ProjectSwitcherItem[] = [
{ id: 'studio', name: 'Studio', icon: <Icon icon={Folder} size={16} />, description: '~/Projects/studio', hint: 'This Mac' },
{ id: 'website', name: 'Website', icon: <Icon icon={Folder} size={16} />, description: '~/Projects/website', hint: 'This Mac' },
{ id: 'archive', name: 'Archive', icon: <Icon icon={Folder} size={16} />, description: '/work/archive', hint: 'Laptop', muted: true }
];
const RECENT: ProjectSwitcherItem[] = [{ id: 'notes', name: 'Notes', icon: <Icon icon={Folder} size={16} />, description: '~/Projects/notes' }];
export default function ProjectSwitcherDemo() {
const [current, setCurrent] = useState(PROJECTS[0]!);
const [projects, setProjects] = useState(PROJECTS);
const [recent, setRecent] = useState(RECENT);
const [message, setMessage] = useState('');
return (
<div className="flex flex-col items-start gap-3">
<ProjectSwitcher
current={current}
projects={projects.map((project): ProjectSwitcherItem => ({
...project,
actions: (
<>
<Menu.Item onClick={() => setMessage(`Settings for ${project.name}`)}>
<Icon icon={Settings2} size={14} /> Project settings…
</Menu.Item>
<Menu.Separator />
<Menu.Item
onClick={() => {
setProjects((rows) => rows.filter((row) => row.id !== project.id));
setRecent((rows) => [project, ...rows]);
setMessage(`Closed ${project.name}`);
}}
>
<Icon icon={X} size={14} /> Close project
</Menu.Item>
</>
)
}))}
recentProjects={recent}
before={
<Menu.Item onClick={() => setMessage('Open the scratchpad')}>
<Icon icon={NotebookPen} size={14} /> Scratchpad
</Menu.Item>
}
onSelect={(project, event) => {
if (event.metaKey || event.ctrlKey) {
setMessage(`Open ${project.name} in another window`);
return;
}
setCurrent(project);
setProjects((rows) => (rows.some((row) => row.id === project.id) ? rows : [...rows, project]));
setRecent((rows) => rows.filter((row) => row.id !== project.id));
setMessage(`Selected ${project.name}`);
}}
>
<Menu.Item onClick={() => setMessage('Choose a project folder')}>
<Icon icon={FolderOpen} size={14} /> Open folder
</Menu.Item>
</ProjectSwitcher>
<span role="status" className="text-xs text-text-muted">
{message || 'Choose a project or open its actions.'}
</span>
</div>
);
}Projects
Each ProjectSwitcherItem has an id, unique within its list, and a name. The lists are drawn as given, in order and unfiltered.
| Item field | What it draws |
|---|---|
icon | A node before the name, in the row and in the trigger when this is current. |
description | A tooltip beside the row, for a folder path or connection details. |
hint | Quiet text at the end of the row, for a machine or workspace. |
disabled | A project that cannot be picked. Its actions stay reachable. |
muted | A dimmed row that can still be picked, for a disconnected project the app can try to reopen. |
actions | Menu.Items in a submenu beside the row. |
projects fill the top of the menu. A nonempty recentProjects adds a Recent projects submenu.
onSelect(project, event) receives the original item and the click event, whose modifier keys let the app open a project in a new window on Cmd-click. ProjectSwitcherProps<Item> keeps your own item type.
current gives the trigger its name and icon, and need not be in either list. leading adds a node before the current icon, such as a machine indicator.
Without a current project
With current={null} the trigger shows label, or "Projects" (projectSwitcher.label) without one. With a current project, the accessible name joins both, such as "Product: Studio".
import { useState } from 'react';
import { Box, Plus } from 'lucide-react';
import { Button, Icon, Menu, ProjectSwitcher, type ProjectSwitcherItem } from '@adecore/ui';
const PRODUCTS: ProjectSwitcherItem[] = [
{ id: 'website', name: 'Website', icon: <Icon icon={Box} size={16} /> },
{ id: 'mobile', name: 'Mobile app', icon: <Icon icon={Box} size={16} /> }
];
export default function ProjectSwitcherEmptyDemo() {
const [current, setCurrent] = useState<ProjectSwitcherItem | null>(null);
return (
<div className="flex items-center gap-3">
<ProjectSwitcher label="Product" current={current} projects={PRODUCTS} onSelect={setCurrent}>
<Menu.Item>
<Icon icon={Plus} size={14} /> New product
</Menu.Item>
</ProjectSwitcher>
<Button size="sm" variant="secondary" disabled={current === null} onClick={() => setCurrent(null)}>
Leave the product
</Button>
</div>
);
}Groups
The menu has three groups: the projects, the items in before, and Recent projects with the items in children. Put ways to open a project (open a folder, create one) in children. Empty groups draw no separator.
ProjectSwitcherGroups draws the same groups inside a Menu.Popup of your own, such as behind a plus button. It takes projects, recentProjects, before, children and onSelect.
<Menu.Root>
<IconButton render={<Menu.Trigger />} icon={Plus} label="Open project" />
<Menu.Popup className="min-w-60">
<ProjectSwitcherGroups projects={projects} onSelect={open} />
</Menu.Popup>
</Menu.Root>Keyboard, typeahead and submenus come from Menu. Typeahead matches the project name, not its hint.
Props
| Prop | Type | Default | |
|---|---|---|---|
current | ProjectSwitcherItem | null | Required. | |
projects | readonly Item[] | Required. | |
onSelect | (project: Item, event: MouseEvent<HTMLElement>) => void | Required. | |
recentProjects | readonly Item[] | [] | |
leading | ReactNode | ||
before | ReactNode | Menu items between the projects and Recent projects. | |
children | ReactNode | Menu items in the group of Recent projects. | |
label | string | projectSwitcher.label | The trigger's text while current is null; prefixes the accessible name otherwise. |
disabled | boolean | ||
tabIndex | number | On the trigger. | |
open, defaultOpen, onOpenChange | As on Menu.Root. | ||
className | string | On the trigger. | |
ref | Ref<HTMLButtonElement> | The trigger. |
ProjectSwitcherProps and ProjectSwitcherItem are exported types.