ProjectSwitcher
A project name in the toolbar that opens a menu of projects, recent projects and per-project actions. The app supplies the list and handles each selection; the component keeps no project state and opens no folders or windows itself.
import { ProjectSwitcher, type ProjectSwitcherItem, type ProjectSwitcherProps } from '@basmilius/desktop-ui';import { useState } from 'react';
import { Folder, FolderOpen, Settings2, X } from 'lucide-react';
import { Icon, Menu, ProjectSwitcher, type ProjectSwitcherItem } from '@basmilius/desktop-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}
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 a stable id and a name. IDs must distinguish projects on different machines or in different workspaces. The component draws the arrays in the order supplied and does not filter them.
| Item field | What it draws |
|---|---|
icon | A React 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 after the name, for a machine or workspace. |
disabled | An unavailable project. Selection is disabled, but its actions stay reachable. |
muted | A dimmed row that can still be selected, for a disconnected project the app can try to reconnect to. |
actions | Menu.Item children in an actions submenu beside the row. Omit it to draw a single row. |
projects appear in the main menu. recentProjects appear under Recent projects when the array is nonempty. Either list can contain rows with actions.
Selecting a project
onSelect(project, event) receives the original item and the click event. Add app-specific fields to your items; ProjectSwitcherProps<Item> preserves their type in the callback. The event carries modifier keys, so the app can handle Cmd-click or Ctrl-click by opening a separate window.
current supplies the trigger's name and icon. It may be absent from either list. With current={null}, the trigger reads Projects. leading adds a node before the current icon, such as a machine indicator with its own tooltip.
Actions and state
Put menu items in children for actions after the project lists, such as opening a folder or creating a project. The component adds the separator only when there are project rows above them.
The menu keeps its open state by default. defaultOpen sets its initial state; open and onOpenChange control it. disabled disables the trigger. label overrides its accessible name, which defaults to the current project name, or Projects without a current project. className and ref reach the trigger button.
The component uses Menu for keyboard navigation, typeahead, submenus and focus return. Typeahead matches the project name, without its hint. Labels use the library's English and Dutch ui namespace.