Skip to content

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.

tsx
import { ProjectSwitcher, type ProjectSwitcherItem, type ProjectSwitcherProps } from '@basmilius/desktop-ui';
tsx
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 fieldWhat it draws
iconA React node before the name, in the row and in the trigger when this is current.
descriptionA tooltip beside the row, for a folder path or connection details.
hintQuiet text after the name, for a machine or workspace.
disabledAn unavailable project. Selection is disabled, but its actions stay reachable.
mutedA dimmed row that can still be selected, for a disconnected project the app can try to reconnect to.
actionsMenu.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.