Files
openchamber/packages/ui/src/components/sections/shared/SettingsProjectSelector.tsx
T
Bohdan Triapitsyn d2358c2c03 feat: redesign settings pages to match canonical flat UI patterns (#493)
* refactor(settings): new IA shell + projects section + skills catalog discoverability

* chore(settings): split providers list by scope; show user before project

* fix: navigation flow in mobile Settings

* feat: redesign settings pages to use modern elevated surface patterns

* feat: replace helper text with tooltips in settings

* ui: redesign update dialog and fix external link routing

- Restructures UpdateDialog to focus on changelog readability with a wider max-w-4xl canvas
- Highlights @username contributor mentions with theme primary color
- Strips excessive vertical padding and right-aligns compact action buttons
- Disables streamdown's internal link safety dialog in favor of direct Tauri shell routing

* feat: refactor Git identities into dedicated Git settings page

* feat: unify sidebar background styling across VS Code and web/mobile

* fix: adjust button styling and layout for mobile settings pages

* feat: add MCP settings page and sidebar

* feat: hide models in provider view (thanks to @nguyenngothuong)

* feat: add "Add new provider" option to model selector dropdown

* fix: local evroc logo + provider dropdown icons

* fix: increase width of provider menu

* fix: dark theme background color for better contrast

* feat: update @opencode-ai/sdk dependency to v1.2.10

* fix: restore session sorting to only use updated time

* fix: added settings for sessions deletion dialog

* fix: adjust padding on settings pages for better layout

* fix: standardize select dropdown height across UI

* fix: agent selector UI and notification settings

* fix: remove redundant helper text from settings pages

* fix: update UI layout for description fields

* fix: remove border-none and shadow-none from textarea classes

* fix: enable context menu on sidebar items

* feat: refactor UI controls and layout patterns across settings pages

* fix: use headerless blocks when page title already provides context

* fix: remove subtask option from command settings

* fix: refactor mcp page settings

* fix: reduce spacing in skills configuration pages

* feat: refactor voice settings

* feat: refactor settings sidebar sections
2026-02-24 03:28:30 +02:00

90 lines
3.5 KiB
TypeScript

import React from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { RiArrowDownSLine, RiFolderLine } from '@remixicon/react';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { isVSCodeRuntime } from '@/lib/desktop';
import { cn } from '@/lib/utils';
const formatProjectLabel = (label: string): string => {
return label.replace(/[-_]/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase());
};
export const SettingsProjectSelector: React.FC<{ className?: string }> = ({ className }) => {
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
: (activeProject?.path.split('/').filter(Boolean).pop() || activeProject?.path || 'Project');
const label = formatProjectLabel(rawLabel);
return (
<div className={cn(className)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label="Switch project"
title="Switch project"
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'
)}
>
<RiFolderLine className="h-4 w-4 opacity-70" />
<span className="min-w-0 flex-1 truncate typography-ui-label font-medium">{label}</span>
<RiArrowDownSLine className="size-4 opacity-50" />
</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>
);
};