2026-02-24 03:28:30 +02:00
|
|
|
import React from 'react';
|
|
|
|
|
import {
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
DropdownMenuRadioGroup,
|
|
|
|
|
DropdownMenuRadioItem,
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
} from '@/components/ui/dropdown-menu';
|
2026-05-13 13:26:15 +03:00
|
|
|
import { Icon } from "@/components/icon/Icon";
|
2026-02-24 03:28:30 +02:00
|
|
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
|
|
|
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
|
|
|
|
import { cn } from '@/lib/utils';
|
2026-04-26 14:03:39 +03:00
|
|
|
import { useI18n } from '@/lib/i18n';
|
2026-02-24 03:28:30 +02:00
|
|
|
|
2026-08-15 10:16:24 +03:00
|
|
|
const formatProjectLabel = (label: string): string => label.trim();
|
2026-02-24 03:28:30 +02:00
|
|
|
|
|
|
|
|
export const SettingsProjectSelector: React.FC<{ className?: string }> = ({ className }) => {
|
2026-04-26 14:03:39 +03:00
|
|
|
const { t } = useI18n();
|
2026-02-24 03:28:30 +02:00
|
|
|
const projects = useProjectsStore((state) => state.projects);
|
|
|
|
|
const activeProjectId = useProjectsStore((state) => state.activeProjectId);
|
|
|
|
|
const setActiveProject = useProjectsStore((state) => state.setActiveProject);
|
|
|
|
|
|
|
|
|
|
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
|
|
|
|
|
|
|
|
|
const sortedProjects = React.useMemo(() => {
|
|
|
|
|
return [...projects].sort((a, b) => (a.label || a.path).localeCompare(b.label || b.path));
|
|
|
|
|
}, [projects]);
|
|
|
|
|
|
|
|
|
|
const activeProject = React.useMemo(() => {
|
|
|
|
|
if (sortedProjects.length === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return sortedProjects.find((p) => p.id === activeProjectId) ?? sortedProjects[0];
|
|
|
|
|
}, [activeProjectId, sortedProjects]);
|
|
|
|
|
|
|
|
|
|
if (isVSCode || sortedProjects.length === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rawLabel = activeProject?.label && activeProject.label.trim().length > 0
|
|
|
|
|
? activeProject.label
|
2026-04-26 14:03:39 +03:00
|
|
|
: (activeProject?.path.split('/').filter(Boolean).pop() || activeProject?.path || t('settings.shared.projectSelector.fallbackProject'));
|
2026-02-24 03:28:30 +02:00
|
|
|
const label = formatProjectLabel(rawLabel);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={cn(className)}>
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
<DropdownMenuTrigger asChild>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-04-26 14:03:39 +03:00
|
|
|
aria-label={t('settings.shared.projectSelector.switchProjectAria')}
|
|
|
|
|
title={t('settings.shared.projectSelector.switchProjectTitle')}
|
2026-02-24 03:28:30 +02:00
|
|
|
className={cn(
|
|
|
|
|
// Mirror Input sizing so headers align visually.
|
|
|
|
|
'text-foreground border border-border/80 appearance-none flex h-8 w-full min-w-0 rounded-lg bg-transparent px-3 py-1 outline-none',
|
|
|
|
|
'hover:border-input focus-visible:ring-1 focus-visible:ring-primary/50 focus-visible:border-primary/70',
|
|
|
|
|
'flex items-center gap-1.5 text-left'
|
|
|
|
|
)}
|
|
|
|
|
>
|
2026-05-13 13:26:15 +03:00
|
|
|
<Icon name="folder" className="h-4 w-4 opacity-70" />
|
2026-02-24 03:28:30 +02:00
|
|
|
<span className="min-w-0 flex-1 truncate typography-ui-label font-medium">{label}</span>
|
2026-05-13 13:26:15 +03:00
|
|
|
<Icon name="arrow-down-s" className="size-4 opacity-50" />
|
2026-02-24 03:28:30 +02:00
|
|
|
</button>
|
|
|
|
|
</DropdownMenuTrigger>
|
|
|
|
|
<DropdownMenuContent align="start" className="w-auto">
|
|
|
|
|
<DropdownMenuRadioGroup
|
|
|
|
|
value={activeProject?.id ?? ''}
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
if (!value) return;
|
|
|
|
|
setActiveProject(value);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{sortedProjects.map((project) => {
|
|
|
|
|
const raw = project.label?.trim()
|
|
|
|
|
? project.label.trim()
|
|
|
|
|
: (project.path.split('/').filter(Boolean).pop() || project.path);
|
|
|
|
|
const itemLabel = formatProjectLabel(raw);
|
|
|
|
|
return (
|
|
|
|
|
<DropdownMenuRadioItem key={project.id} value={project.id}>
|
|
|
|
|
<span className="min-w-0 truncate typography-ui">{itemLabel}</span>
|
|
|
|
|
</DropdownMenuRadioItem>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</DropdownMenuRadioGroup>
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|