mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-14 18:37:22 +00:00
refactor: new settings page design and new pill component
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useCallback, useRef, useState, useEffect } from 'react';
|
||||
import type { MainContentHeaderProps } from '../../types/types';
|
||||
import MobileMenuButton from './MobileMenuButton';
|
||||
import MainContentTabSwitcher from './MainContentTabSwitcher';
|
||||
@@ -12,6 +13,26 @@ export default function MainContentHeader({
|
||||
isMobile,
|
||||
onMenuClick,
|
||||
}: MainContentHeaderProps) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
||||
const [canScrollRight, setCanScrollRight] = useState(false);
|
||||
|
||||
const updateScrollState = useCallback(() => {
|
||||
const el = scrollRef.current;
|
||||
if (!el) return;
|
||||
setCanScrollLeft(el.scrollLeft > 2);
|
||||
setCanScrollRight(el.scrollLeft < el.scrollWidth - el.clientWidth - 2);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
if (!el) return;
|
||||
updateScrollState();
|
||||
const observer = new ResizeObserver(updateScrollState);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [updateScrollState]);
|
||||
|
||||
return (
|
||||
<div className="pwa-header-safe flex-shrink-0 border-b border-border/60 bg-background px-3 py-1.5 sm:px-4 sm:py-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
@@ -25,12 +46,24 @@ export default function MainContentHeader({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="hidden flex-shrink-0 sm:block">
|
||||
<MainContentTabSwitcher
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
shouldShowTasksTab={shouldShowTasksTab}
|
||||
/>
|
||||
<div className="relative min-w-0 flex-shrink overflow-hidden sm:flex-shrink-0">
|
||||
{canScrollLeft && (
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 z-10 w-6 bg-gradient-to-r from-background to-transparent" />
|
||||
)}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={updateScrollState}
|
||||
className="scrollbar-hide overflow-x-auto"
|
||||
>
|
||||
<MainContentTabSwitcher
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
shouldShowTasksTab={shouldShowTasksTab}
|
||||
/>
|
||||
</div>
|
||||
{canScrollRight && (
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 z-10 w-6 bg-gradient-to-l from-background to-transparent" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MessageSquare, Terminal, Folder, GitBranch, ClipboardCheck, type LucideIcon } from 'lucide-react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tooltip } from '../../../../shared/view/ui';
|
||||
import { Tooltip, PillBar, Pill } from '../../../../shared/view/ui';
|
||||
import type { AppTab } from '../../../../types/app';
|
||||
import { usePlugins } from '../../../../contexts/PluginsContext';
|
||||
import PluginIcon from '../../../plugins/view/PluginIcon';
|
||||
@@ -66,20 +66,17 @@ export default function MainContentTabSwitcher({
|
||||
const tabs: TabDefinition[] = [...builtInTabs, ...pluginTabs];
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center gap-[2px] rounded-lg bg-muted/60 p-[3px]">
|
||||
<PillBar>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = tab.id === activeTab;
|
||||
const displayLabel = tab.kind === 'builtin' ? t(tab.labelKey) : tab.label;
|
||||
|
||||
return (
|
||||
<Tooltip key={tab.id} content={displayLabel} position="bottom">
|
||||
<button
|
||||
<Pill
|
||||
isActive={isActive}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`relative flex items-center gap-1.5 rounded-md px-2.5 py-[5px] text-sm font-medium transition-all duration-150 ${
|
||||
isActive
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
className="px-2.5 py-[5px]"
|
||||
>
|
||||
{tab.kind === 'builtin' ? (
|
||||
<tab.icon className="h-3.5 w-3.5" strokeWidth={isActive ? 2.2 : 1.8} />
|
||||
@@ -91,10 +88,10 @@ export default function MainContentTabSwitcher({
|
||||
/>
|
||||
)}
|
||||
<span className="hidden lg:inline">{displayLabel}</span>
|
||||
</button>
|
||||
</Pill>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PillBar>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -191,7 +191,6 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
const closeTimerRef = useRef<number | null>(null);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<SettingsMainTab>(() => normalizeMainTab(initialTab));
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveStatus, setSaveStatus] = useState<'success' | 'error' | null>(null);
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
const [projectSortOrder, setProjectSortOrder] = useState<ProjectSortOrder>('name');
|
||||
@@ -701,9 +700,6 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
}, [checkAuthStatus, loginProvider]);
|
||||
|
||||
const saveSettings = useCallback(() => {
|
||||
setIsSaving(true);
|
||||
setSaveStatus(null);
|
||||
|
||||
try {
|
||||
const now = new Date().toISOString();
|
||||
localStorage.setItem('claude-settings', JSON.stringify({
|
||||
@@ -732,16 +728,9 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
}));
|
||||
|
||||
setSaveStatus('success');
|
||||
if (closeTimerRef.current !== null) {
|
||||
window.clearTimeout(closeTimerRef.current);
|
||||
closeTimerRef.current = null;
|
||||
}
|
||||
closeTimerRef.current = window.setTimeout(() => onClose(), 1000);
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
setSaveStatus('error');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [
|
||||
claudePermissions.allowedTools,
|
||||
@@ -751,7 +740,7 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
cursorPermissions.allowedCommands,
|
||||
cursorPermissions.disallowedCommands,
|
||||
cursorPermissions.skipPermissions,
|
||||
onClose,
|
||||
geminiPermissionMode,
|
||||
projectSortOrder,
|
||||
]);
|
||||
|
||||
@@ -804,11 +793,58 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
|
||||
}, [codeEditorSettings]);
|
||||
|
||||
// Auto-save permissions and sort order with debounce
|
||||
const autoSaveTimerRef = useRef<number | null>(null);
|
||||
const isInitialLoadRef = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Skip auto-save on initial load (settings are being loaded from localStorage)
|
||||
if (isInitialLoadRef.current) {
|
||||
isInitialLoadRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoSaveTimerRef.current !== null) {
|
||||
window.clearTimeout(autoSaveTimerRef.current);
|
||||
}
|
||||
|
||||
autoSaveTimerRef.current = window.setTimeout(() => {
|
||||
saveSettings();
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
if (autoSaveTimerRef.current !== null) {
|
||||
window.clearTimeout(autoSaveTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, [saveSettings]);
|
||||
|
||||
// Clear save status after 2 seconds
|
||||
useEffect(() => {
|
||||
if (saveStatus === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = window.setTimeout(() => setSaveStatus(null), 2000);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [saveStatus]);
|
||||
|
||||
// Reset initial load flag when settings dialog opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
isInitialLoadRef.current = true;
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (closeTimerRef.current !== null) {
|
||||
window.clearTimeout(closeTimerRef.current);
|
||||
closeTimerRef.current = null;
|
||||
}
|
||||
if (autoSaveTimerRef.current !== null) {
|
||||
window.clearTimeout(autoSaveTimerRef.current);
|
||||
autoSaveTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
@@ -816,7 +852,6 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
setActiveTab,
|
||||
isDarkMode,
|
||||
toggleDarkMode,
|
||||
isSaving,
|
||||
saveStatus,
|
||||
deleteError,
|
||||
projectSortOrder,
|
||||
@@ -861,6 +896,5 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
loginProvider,
|
||||
selectedProject,
|
||||
handleLoginComplete,
|
||||
saveSettings,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Settings as SettingsIcon, X } from 'lucide-react';
|
||||
import { X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ProviderLoginModal from '../../provider-auth/view/ProviderLoginModal';
|
||||
import { Button } from '../../../shared/view/ui';
|
||||
import ClaudeMcpFormModal from '../view/modals/ClaudeMcpFormModal';
|
||||
import CodexMcpFormModal from '../view/modals/CodexMcpFormModal';
|
||||
import SettingsMainTabs from '../view/SettingsMainTabs';
|
||||
import SettingsSidebar from '../view/SettingsSidebar';
|
||||
import AgentsSettingsTab from '../view/tabs/agents-settings/AgentsSettingsTab';
|
||||
import AppearanceSettingsTab from '../view/tabs/AppearanceSettingsTab';
|
||||
import CredentialsSettingsTab from '../view/tabs/api-settings/CredentialsSettingsTab';
|
||||
@@ -19,7 +19,6 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
||||
const {
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
isSaving,
|
||||
saveStatus,
|
||||
deleteError,
|
||||
projectSortOrder,
|
||||
@@ -64,7 +63,6 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
||||
loginProvider,
|
||||
selectedProject,
|
||||
handleLoginComplete,
|
||||
saveSettings,
|
||||
} = useSettingsController({
|
||||
isOpen,
|
||||
initialTab,
|
||||
@@ -85,140 +83,90 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
||||
: false;
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop fixed inset-0 z-[9999] flex items-center justify-center bg-background/95 md:p-4">
|
||||
<div className="flex h-full w-full flex-col border border-border bg-background shadow-xl md:h-[90vh] md:max-w-4xl md:rounded-lg">
|
||||
<div className="flex flex-shrink-0 items-center justify-between border-b border-border p-4 md:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<SettingsIcon className="h-5 w-5 text-blue-600 md:h-6 md:w-6" />
|
||||
<h2 className="text-lg font-semibold text-foreground md:text-xl">{t('title')}</h2>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
className="touch-manipulation text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<SettingsMainTabs activeTab={activeTab} onChange={setActiveTab} />
|
||||
|
||||
<div className="space-y-6 p-4 pb-safe-area-inset-bottom md:space-y-8 md:p-6">
|
||||
{activeTab === 'appearance' && (
|
||||
<AppearanceSettingsTab
|
||||
projectSortOrder={projectSortOrder}
|
||||
onProjectSortOrderChange={setProjectSortOrder}
|
||||
codeEditorSettings={codeEditorSettings}
|
||||
onCodeEditorThemeChange={(value) => updateCodeEditorSetting('theme', value)}
|
||||
onCodeEditorWordWrapChange={(value) => updateCodeEditorSetting('wordWrap', value)}
|
||||
onCodeEditorShowMinimapChange={(value) => updateCodeEditorSetting('showMinimap', value)}
|
||||
onCodeEditorLineNumbersChange={(value) => updateCodeEditorSetting('lineNumbers', value)}
|
||||
onCodeEditorFontSizeChange={(value) => updateCodeEditorSetting('fontSize', value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'git' && <GitSettingsTab />}
|
||||
|
||||
{activeTab === 'agents' && (
|
||||
<AgentsSettingsTab
|
||||
claudeAuthStatus={claudeAuthStatus}
|
||||
cursorAuthStatus={cursorAuthStatus}
|
||||
codexAuthStatus={codexAuthStatus}
|
||||
geminiAuthStatus={geminiAuthStatus}
|
||||
onClaudeLogin={() => openLoginForProvider('claude')}
|
||||
onCursorLogin={() => openLoginForProvider('cursor')}
|
||||
onCodexLogin={() => openLoginForProvider('codex')}
|
||||
onGeminiLogin={() => openLoginForProvider('gemini')}
|
||||
claudePermissions={claudePermissions}
|
||||
onClaudePermissionsChange={setClaudePermissions}
|
||||
cursorPermissions={cursorPermissions}
|
||||
onCursorPermissionsChange={setCursorPermissions}
|
||||
codexPermissionMode={codexPermissionMode}
|
||||
onCodexPermissionModeChange={setCodexPermissionMode}
|
||||
geminiPermissionMode={geminiPermissionMode}
|
||||
onGeminiPermissionModeChange={setGeminiPermissionMode}
|
||||
mcpServers={mcpServers}
|
||||
cursorMcpServers={cursorMcpServers}
|
||||
codexMcpServers={codexMcpServers}
|
||||
mcpTestResults={mcpTestResults}
|
||||
mcpServerTools={mcpServerTools}
|
||||
mcpToolsLoading={mcpToolsLoading}
|
||||
onOpenMcpForm={openMcpForm}
|
||||
onDeleteMcpServer={handleMcpDelete}
|
||||
onTestMcpServer={handleMcpTest}
|
||||
onDiscoverMcpTools={handleMcpToolsDiscovery}
|
||||
onOpenCodexMcpForm={openCodexMcpForm}
|
||||
onDeleteCodexMcpServer={handleCodexMcpDelete}
|
||||
deleteError={deleteError}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'tasks' && (
|
||||
<div className="space-y-6 md:space-y-8">
|
||||
<TasksSettingsTab />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'api' && (
|
||||
<div className="space-y-6 md:space-y-8">
|
||||
<CredentialsSettingsTab />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'plugins' && (
|
||||
<div className="space-y-6 md:space-y-8">
|
||||
<PluginSettingsTab />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-shrink-0 flex-col gap-3 border-t border-border p-4 pb-safe-area-inset-bottom sm:flex-row sm:items-center sm:justify-between md:p-6">
|
||||
<div className="order-2 flex items-center justify-center gap-2 sm:order-1 sm:justify-start">
|
||||
<div className="modal-backdrop fixed inset-0 z-[9999] flex items-center justify-center bg-background/80 backdrop-blur-sm md:p-4">
|
||||
<div className="flex h-full w-full flex-col overflow-hidden border border-border bg-background shadow-2xl md:h-[90vh] md:max-w-4xl md:rounded-xl">
|
||||
{/* Header */}
|
||||
<div className="flex flex-shrink-0 items-center justify-between border-b border-border px-4 py-3 md:px-5">
|
||||
<h2 className="text-base font-semibold text-foreground">{t('title')}</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{saveStatus === 'success' && (
|
||||
<div className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
|
||||
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{t('saveStatus.success')}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground animate-in fade-in">{t('saveStatus.success')}</span>
|
||||
)}
|
||||
{saveStatus === 'error' && (
|
||||
<div className="flex items-center gap-1 text-sm text-red-600 dark:text-red-400">
|
||||
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{t('saveStatus.error')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="order-1 flex items-center gap-3 sm:order-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
disabled={isSaving}
|
||||
className="h-10 flex-1 touch-manipulation sm:flex-none"
|
||||
className="h-10 w-10 touch-manipulation p-0 text-muted-foreground hover:text-foreground active:bg-accent/50"
|
||||
>
|
||||
{t('footerActions.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={saveSettings}
|
||||
disabled={isSaving}
|
||||
className="h-10 flex-1 touch-manipulation bg-blue-600 hover:bg-blue-700 disabled:opacity-50 sm:flex-none"
|
||||
>
|
||||
{isSaving ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||
{t('saveStatus.saving')}
|
||||
</div>
|
||||
) : (
|
||||
t('footerActions.save')
|
||||
)}
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body: sidebar + content */}
|
||||
<div className="flex min-h-0 flex-1 flex-col md:flex-row">
|
||||
<SettingsSidebar activeTab={activeTab} onChange={setActiveTab} />
|
||||
|
||||
{/* Content */}
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<div key={activeTab} className="settings-content-enter space-y-6 p-4 pb-safe-area-inset-bottom md:space-y-8 md:p-6">
|
||||
{activeTab === 'appearance' && (
|
||||
<AppearanceSettingsTab
|
||||
projectSortOrder={projectSortOrder}
|
||||
onProjectSortOrderChange={setProjectSortOrder}
|
||||
codeEditorSettings={codeEditorSettings}
|
||||
onCodeEditorThemeChange={(value) => updateCodeEditorSetting('theme', value)}
|
||||
onCodeEditorWordWrapChange={(value) => updateCodeEditorSetting('wordWrap', value)}
|
||||
onCodeEditorShowMinimapChange={(value) => updateCodeEditorSetting('showMinimap', value)}
|
||||
onCodeEditorLineNumbersChange={(value) => updateCodeEditorSetting('lineNumbers', value)}
|
||||
onCodeEditorFontSizeChange={(value) => updateCodeEditorSetting('fontSize', value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'git' && <GitSettingsTab />}
|
||||
|
||||
{activeTab === 'agents' && (
|
||||
<AgentsSettingsTab
|
||||
claudeAuthStatus={claudeAuthStatus}
|
||||
cursorAuthStatus={cursorAuthStatus}
|
||||
codexAuthStatus={codexAuthStatus}
|
||||
geminiAuthStatus={geminiAuthStatus}
|
||||
onClaudeLogin={() => openLoginForProvider('claude')}
|
||||
onCursorLogin={() => openLoginForProvider('cursor')}
|
||||
onCodexLogin={() => openLoginForProvider('codex')}
|
||||
onGeminiLogin={() => openLoginForProvider('gemini')}
|
||||
claudePermissions={claudePermissions}
|
||||
onClaudePermissionsChange={setClaudePermissions}
|
||||
cursorPermissions={cursorPermissions}
|
||||
onCursorPermissionsChange={setCursorPermissions}
|
||||
codexPermissionMode={codexPermissionMode}
|
||||
onCodexPermissionModeChange={setCodexPermissionMode}
|
||||
geminiPermissionMode={geminiPermissionMode}
|
||||
onGeminiPermissionModeChange={setGeminiPermissionMode}
|
||||
mcpServers={mcpServers}
|
||||
cursorMcpServers={cursorMcpServers}
|
||||
codexMcpServers={codexMcpServers}
|
||||
mcpTestResults={mcpTestResults}
|
||||
mcpServerTools={mcpServerTools}
|
||||
mcpToolsLoading={mcpToolsLoading}
|
||||
onOpenMcpForm={openMcpForm}
|
||||
onDeleteMcpServer={handleMcpDelete}
|
||||
onTestMcpServer={handleMcpTest}
|
||||
onDiscoverMcpTools={handleMcpToolsDiscovery}
|
||||
onOpenCodexMcpForm={openCodexMcpForm}
|
||||
onDeleteCodexMcpServer={handleCodexMcpDelete}
|
||||
deleteError={deleteError}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'tasks' && <TasksSettingsTab />}
|
||||
|
||||
{activeTab === 'api' && <CredentialsSettingsTab />}
|
||||
|
||||
{activeTab === 'plugins' && <PluginSettingsTab />}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProviderLoginModal
|
||||
|
||||
22
src/components/settings/view/SettingsCard.tsx
Normal file
22
src/components/settings/view/SettingsCard.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { cn } from '../../../lib/utils';
|
||||
|
||||
type SettingsCardProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
divided?: boolean;
|
||||
};
|
||||
|
||||
export default function SettingsCard({ children, className, divided }: SettingsCardProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-xl border border-border bg-card/50',
|
||||
divided && 'divide-y divide-border',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
src/components/settings/view/SettingsRow.tsx
Normal file
23
src/components/settings/view/SettingsRow.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { cn } from '../../../lib/utils';
|
||||
|
||||
type SettingsRowProps = {
|
||||
label: string;
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function SettingsRow({ label, description, children, className }: SettingsRowProps) {
|
||||
return (
|
||||
<div className={cn('flex items-center justify-between gap-4 px-4 py-4', className)}>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium text-foreground">{label}</div>
|
||||
{description && (
|
||||
<div className="mt-0.5 text-sm text-muted-foreground">{description}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-shrink-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
src/components/settings/view/SettingsSection.tsx
Normal file
25
src/components/settings/view/SettingsSection.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { cn } from '../../../lib/utils';
|
||||
|
||||
type SettingsSectionProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function SettingsSection({ title, description, children, className }: SettingsSectionProps) {
|
||||
return (
|
||||
<div className={cn('space-y-3', className)}>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{title}
|
||||
</h3>
|
||||
{description && (
|
||||
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
80
src/components/settings/view/SettingsSidebar.tsx
Normal file
80
src/components/settings/view/SettingsSidebar.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Bot, GitBranch, Key, ListChecks, Palette, Puzzle } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '../../../lib/utils';
|
||||
import { PillBar, Pill } from '../../../shared/view/ui';
|
||||
import type { SettingsMainTab } from '../types/types';
|
||||
|
||||
type SettingsSidebarProps = {
|
||||
activeTab: SettingsMainTab;
|
||||
onChange: (tab: SettingsMainTab) => void;
|
||||
};
|
||||
|
||||
type NavItem = {
|
||||
id: SettingsMainTab;
|
||||
labelKey: string;
|
||||
icon: typeof Bot;
|
||||
};
|
||||
|
||||
const NAV_ITEMS: NavItem[] = [
|
||||
{ id: 'agents', labelKey: 'mainTabs.agents', icon: Bot },
|
||||
{ id: 'appearance', labelKey: 'mainTabs.appearance', icon: Palette },
|
||||
{ id: 'git', labelKey: 'mainTabs.git', icon: GitBranch },
|
||||
{ id: 'api', labelKey: 'mainTabs.apiTokens', icon: Key },
|
||||
{ id: 'tasks', labelKey: 'mainTabs.tasks', icon: ListChecks },
|
||||
{ id: 'plugins', labelKey: 'mainTabs.plugins', icon: Puzzle },
|
||||
];
|
||||
|
||||
export default function SettingsSidebar({ activeTab, onChange }: SettingsSidebarProps) {
|
||||
const { t } = useTranslation('settings');
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Desktop sidebar */}
|
||||
<aside className="hidden w-56 flex-shrink-0 border-r border-border bg-muted/30 md:flex md:flex-col">
|
||||
<nav className="flex flex-col gap-1 p-3">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = activeTab === item.id;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => onChange(item.id)}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm font-medium transition-colors duration-150',
|
||||
isActive
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground active:bg-accent/50',
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4 flex-shrink-0" />
|
||||
{t(item.labelKey)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* Mobile horizontal nav — pill bar */}
|
||||
<div className="flex-shrink-0 border-b border-border px-3 py-2 md:hidden">
|
||||
<PillBar className="scrollbar-hide w-full overflow-x-auto">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<Pill
|
||||
key={item.id}
|
||||
isActive={activeTab === item.id}
|
||||
onClick={() => onChange(item.id)}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{t(item.labelKey)}
|
||||
</Pill>
|
||||
);
|
||||
})}
|
||||
</PillBar>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
34
src/components/settings/view/SettingsToggle.tsx
Normal file
34
src/components/settings/view/SettingsToggle.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { cn } from '../../../lib/utils';
|
||||
|
||||
type SettingsToggleProps = {
|
||||
checked: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
ariaLabel: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export default function SettingsToggle({ checked, onChange, ariaLabel, disabled }: SettingsToggleProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
aria-label={ariaLabel}
|
||||
disabled={disabled}
|
||||
onClick={() => onChange(!checked)}
|
||||
className={cn(
|
||||
'relative inline-flex h-7 w-12 flex-shrink-0 touch-manipulation cursor-pointer items-center rounded-full border-2 transition-colors duration-200',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
|
||||
checked ? 'border-primary bg-primary' : 'border-border bg-muted',
|
||||
disabled && 'cursor-not-allowed opacity-50',
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'pointer-events-none inline-block h-5 w-5 rounded-full shadow-sm transition-transform duration-200',
|
||||
checked ? 'translate-x-[22px] bg-white' : 'translate-x-[2px] bg-foreground/60 dark:bg-foreground/80',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DarkModeToggle } from '../../../../shared/view/ui';
|
||||
import type { CodeEditorSettingsState, ProjectSortOrder } from '../../types/types';
|
||||
import LanguageSelector from '../../../../shared/view/ui/LanguageSelector';
|
||||
import SettingsCard from '../SettingsCard';
|
||||
import SettingsRow from '../SettingsRow';
|
||||
import SettingsSection from '../SettingsSection';
|
||||
import SettingsToggle from '../SettingsToggle';
|
||||
|
||||
type AppearanceSettingsTabProps = {
|
||||
projectSortOrder: ProjectSortOrder;
|
||||
@@ -15,52 +18,6 @@ type AppearanceSettingsTabProps = {
|
||||
onCodeEditorFontSizeChange: (value: string) => void;
|
||||
};
|
||||
|
||||
type ToggleCardProps = {
|
||||
label: string;
|
||||
description: string;
|
||||
checked: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
onIcon?: ReactNode;
|
||||
offIcon?: ReactNode;
|
||||
ariaLabel: string;
|
||||
};
|
||||
|
||||
function ToggleCard({
|
||||
label,
|
||||
description,
|
||||
checked,
|
||||
onChange,
|
||||
onIcon,
|
||||
offIcon,
|
||||
ariaLabel,
|
||||
}: ToggleCardProps) {
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{label}</div>
|
||||
<div className="text-sm text-muted-foreground">{description}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onChange(!checked)}
|
||||
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:focus:ring-offset-gray-900"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<span className="sr-only">{ariaLabel}</span>
|
||||
<span
|
||||
className={`${checked ? 'translate-x-7' : 'translate-x-1'
|
||||
} flex h-6 w-6 transform items-center justify-center rounded-full bg-white shadow-lg transition-transform duration-200`}
|
||||
>
|
||||
{checked ? onIcon : offIcon}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AppearanceSettingsTab({
|
||||
projectSortOrder,
|
||||
onProjectSortOrderChange,
|
||||
@@ -72,108 +29,98 @@ export default function AppearanceSettingsTab({
|
||||
onCodeEditorFontSizeChange,
|
||||
}: AppearanceSettingsTabProps) {
|
||||
const { t } = useTranslation('settings');
|
||||
const codeEditorThemeLabel = t('appearanceSettings.codeEditor.theme.label');
|
||||
|
||||
return (
|
||||
<div className="space-y-6 md:space-y-8">
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{t('appearanceSettings.darkMode.label')}</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('appearanceSettings.darkMode.description')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<SettingsSection title={t('appearanceSettings.darkMode.label')}>
|
||||
<SettingsCard>
|
||||
<SettingsRow
|
||||
label={t('appearanceSettings.darkMode.label')}
|
||||
description={t('appearanceSettings.darkMode.description')}
|
||||
>
|
||||
<DarkModeToggle ariaLabel={t('appearanceSettings.darkMode.label')} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
|
||||
<div className="space-y-4">
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
<SettingsSection title={t('mainTabs.appearance')}>
|
||||
<SettingsCard>
|
||||
<LanguageSelector />
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">
|
||||
{t('appearanceSettings.projectSorting.label')}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('appearanceSettings.projectSorting.description')}
|
||||
</div>
|
||||
</div>
|
||||
<SettingsSection title={t('appearanceSettings.projectSorting.label')}>
|
||||
<SettingsCard>
|
||||
<SettingsRow
|
||||
label={t('appearanceSettings.projectSorting.label')}
|
||||
description={t('appearanceSettings.projectSorting.description')}
|
||||
>
|
||||
<select
|
||||
value={projectSortOrder}
|
||||
onChange={(event) => onProjectSortOrderChange(event.target.value as ProjectSortOrder)}
|
||||
className="w-32 rounded-lg border border-gray-300 bg-gray-50 p-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
className="w-full rounded-lg border border-input bg-card p-2.5 text-sm text-foreground touch-manipulation focus:border-primary focus:ring-1 focus:ring-primary sm:w-36"
|
||||
>
|
||||
<option value="name">{t('appearanceSettings.projectSorting.alphabetical')}</option>
|
||||
<option value="date">{t('appearanceSettings.projectSorting.recentActivity')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">{t('appearanceSettings.codeEditor.title')}</h3>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{codeEditorThemeLabel}</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('appearanceSettings.codeEditor.theme.description')}
|
||||
</div>
|
||||
</div>
|
||||
<SettingsSection title={t('appearanceSettings.codeEditor.title')}>
|
||||
<SettingsCard divided>
|
||||
<SettingsRow
|
||||
label={t('appearanceSettings.codeEditor.theme.label')}
|
||||
description={t('appearanceSettings.codeEditor.theme.description')}
|
||||
>
|
||||
<DarkModeToggle
|
||||
checked={codeEditorSettings.theme === 'dark'}
|
||||
onToggle={(enabled) => onCodeEditorThemeChange(enabled ? 'dark' : 'light')}
|
||||
ariaLabel={codeEditorThemeLabel}
|
||||
ariaLabel={t('appearanceSettings.codeEditor.theme.label')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
<ToggleCard
|
||||
label={t('appearanceSettings.codeEditor.wordWrap.label')}
|
||||
description={t('appearanceSettings.codeEditor.wordWrap.description')}
|
||||
checked={codeEditorSettings.wordWrap}
|
||||
onChange={onCodeEditorWordWrapChange}
|
||||
ariaLabel={t('appearanceSettings.codeEditor.wordWrap.label')}
|
||||
/>
|
||||
<SettingsRow
|
||||
label={t('appearanceSettings.codeEditor.wordWrap.label')}
|
||||
description={t('appearanceSettings.codeEditor.wordWrap.description')}
|
||||
>
|
||||
<SettingsToggle
|
||||
checked={codeEditorSettings.wordWrap}
|
||||
onChange={onCodeEditorWordWrapChange}
|
||||
ariaLabel={t('appearanceSettings.codeEditor.wordWrap.label')}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<ToggleCard
|
||||
label={t('appearanceSettings.codeEditor.showMinimap.label')}
|
||||
description={t('appearanceSettings.codeEditor.showMinimap.description')}
|
||||
checked={codeEditorSettings.showMinimap}
|
||||
onChange={onCodeEditorShowMinimapChange}
|
||||
ariaLabel={t('appearanceSettings.codeEditor.showMinimap.label')}
|
||||
/>
|
||||
<SettingsRow
|
||||
label={t('appearanceSettings.codeEditor.showMinimap.label')}
|
||||
description={t('appearanceSettings.codeEditor.showMinimap.description')}
|
||||
>
|
||||
<SettingsToggle
|
||||
checked={codeEditorSettings.showMinimap}
|
||||
onChange={onCodeEditorShowMinimapChange}
|
||||
ariaLabel={t('appearanceSettings.codeEditor.showMinimap.label')}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<ToggleCard
|
||||
label={t('appearanceSettings.codeEditor.lineNumbers.label')}
|
||||
description={t('appearanceSettings.codeEditor.lineNumbers.description')}
|
||||
checked={codeEditorSettings.lineNumbers}
|
||||
onChange={onCodeEditorLineNumbersChange}
|
||||
ariaLabel={t('appearanceSettings.codeEditor.lineNumbers.label')}
|
||||
/>
|
||||
<SettingsRow
|
||||
label={t('appearanceSettings.codeEditor.lineNumbers.label')}
|
||||
description={t('appearanceSettings.codeEditor.lineNumbers.description')}
|
||||
>
|
||||
<SettingsToggle
|
||||
checked={codeEditorSettings.lineNumbers}
|
||||
onChange={onCodeEditorLineNumbersChange}
|
||||
ariaLabel={t('appearanceSettings.codeEditor.lineNumbers.label')}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">
|
||||
{t('appearanceSettings.codeEditor.fontSize.label')}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('appearanceSettings.codeEditor.fontSize.description')}
|
||||
</div>
|
||||
</div>
|
||||
<SettingsRow
|
||||
label={t('appearanceSettings.codeEditor.fontSize.label')}
|
||||
description={t('appearanceSettings.codeEditor.fontSize.description')}
|
||||
>
|
||||
<select
|
||||
value={codeEditorSettings.fontSize}
|
||||
onChange={(event) => onCodeEditorFontSizeChange(event.target.value)}
|
||||
className="w-24 rounded-lg border border-gray-300 bg-gray-50 p-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
className="w-full rounded-lg border border-input bg-card p-2.5 text-sm text-foreground touch-manipulation focus:border-primary focus:ring-1 focus:ring-primary sm:w-28"
|
||||
>
|
||||
<option value="10">10px</option>
|
||||
<option value="11">11px</option>
|
||||
@@ -185,9 +132,9 @@ export default function AppearanceSettingsTab({
|
||||
<option value="18">18px</option>
|
||||
<option value="20">20px</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '../../../../../lib/utils';
|
||||
import SessionProviderLogo from '../../../../llm-logo-provider/SessionProviderLogo';
|
||||
import type { AgentProvider, AuthStatus } from '../../../types/types';
|
||||
|
||||
@@ -36,27 +36,15 @@ const agentConfig: Record<AgentProvider, AgentConfig> = {
|
||||
|
||||
const colorClasses = {
|
||||
blue: {
|
||||
border: 'border-l-blue-500 md:border-l-blue-500',
|
||||
borderBottom: 'border-b-blue-500',
|
||||
bg: 'bg-blue-50 dark:bg-blue-900/20',
|
||||
dot: 'bg-blue-500',
|
||||
},
|
||||
purple: {
|
||||
border: 'border-l-purple-500 md:border-l-purple-500',
|
||||
borderBottom: 'border-b-purple-500',
|
||||
bg: 'bg-purple-50 dark:bg-purple-900/20',
|
||||
dot: 'bg-purple-500',
|
||||
},
|
||||
gray: {
|
||||
border: 'border-l-gray-700 dark:border-l-gray-300',
|
||||
borderBottom: 'border-b-gray-700 dark:border-b-gray-300',
|
||||
bg: 'bg-gray-100 dark:bg-gray-800/50',
|
||||
dot: 'bg-gray-700 dark:bg-gray-300',
|
||||
dot: 'bg-foreground/60',
|
||||
},
|
||||
indigo: {
|
||||
border: 'border-l-indigo-500 md:border-l-indigo-500',
|
||||
borderBottom: 'border-b-indigo-500',
|
||||
bg: 'bg-indigo-50 dark:bg-indigo-900/20',
|
||||
dot: 'bg-indigo-500',
|
||||
},
|
||||
} as const;
|
||||
@@ -68,7 +56,6 @@ export default function AgentListItem({
|
||||
onClick,
|
||||
isMobile = false,
|
||||
}: AgentListItemProps) {
|
||||
const { t } = useTranslation('settings');
|
||||
const config = agentConfig[agentId];
|
||||
const colors = colorClasses[config.color];
|
||||
|
||||
@@ -76,16 +63,18 @@ export default function AgentListItem({
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`flex-1 border-b-2 px-2 py-3 text-center transition-colors ${isSelected
|
||||
? `${colors.borderBottom} ${colors.bg}`
|
||||
: 'border-transparent hover:bg-gray-50 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
className={cn(
|
||||
'min-w-0 flex-1 touch-manipulation rounded-md px-2 py-2 text-center transition-all duration-150',
|
||||
isSelected
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground active:bg-background/50',
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<SessionProviderLogo provider={agentId} className="h-5 w-5" />
|
||||
<span className="text-xs font-medium text-foreground">{config.name}</span>
|
||||
<div className="flex items-center justify-center gap-1.5">
|
||||
<SessionProviderLogo provider={agentId} className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="truncate text-xs font-medium">{config.name}</span>
|
||||
{authStatus.authenticated && (
|
||||
<span className={`h-1.5 w-1.5 rounded-full ${colors.dot}`} />
|
||||
<span className={`h-1.5 w-1.5 flex-shrink-0 rounded-full ${colors.dot}`} />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
@@ -95,32 +84,20 @@ export default function AgentListItem({
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`w-full border-l-4 p-3 text-left transition-colors ${isSelected
|
||||
? `${colors.border} ${colors.bg}`
|
||||
: 'border-transparent hover:bg-gray-50 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
className={cn(
|
||||
'flex touch-manipulation items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-all duration-150',
|
||||
isSelected
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground active:bg-background/50',
|
||||
)}
|
||||
>
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<SessionProviderLogo provider={agentId} className="h-4 w-4" />
|
||||
<span className="font-medium text-foreground">{config.name}</span>
|
||||
</div>
|
||||
<div className="pl-6 text-xs text-muted-foreground">
|
||||
{authStatus.loading ? (
|
||||
<span className="text-gray-400">{t('agents.authStatus.checking')}</span>
|
||||
) : authStatus.authenticated ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={`h-1.5 w-1.5 rounded-full ${colors.dot}`} />
|
||||
<span className="max-w-[120px] truncate" title={authStatus.email ?? undefined}>
|
||||
{authStatus.email || t('agents.authStatus.connected')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-gray-400" />
|
||||
<span>{t('agents.authStatus.notConnected')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SessionProviderLogo provider={agentId} className="h-4 w-4 flex-shrink-0" />
|
||||
<span>{config.name}</span>
|
||||
{authStatus.authenticated ? (
|
||||
<span className={`h-1.5 w-1.5 flex-shrink-0 rounded-full ${colors.dot}`} />
|
||||
) : authStatus.loading ? (
|
||||
<span className="h-1.5 w-1.5 flex-shrink-0 rounded-full bg-muted-foreground/30 animate-pulse" />
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export default function AgentsSettingsTab({
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-[400px] flex-col md:min-h-[500px] md:flex-row">
|
||||
<div className="-mx-4 -mb-4 -mt-2 flex min-h-[300px] flex-col overflow-hidden md:-mx-6 md:-mb-6 md:-mt-2 md:min-h-[500px]">
|
||||
<AgentSelectorSection
|
||||
selectedAgent={selectedAgent}
|
||||
onSelectAgent={setSelectedAgent}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '../../../../../../lib/utils';
|
||||
import type { AgentCategory } from '../../../../types/types';
|
||||
import type { AgentCategoryTabsSectionProps } from '../types';
|
||||
|
||||
@@ -11,7 +12,7 @@ export default function AgentCategoryTabsSection({
|
||||
const { t } = useTranslation('settings');
|
||||
|
||||
return (
|
||||
<div className="flex-shrink-0 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex-shrink-0 border-b border-border">
|
||||
<div role="tablist" className="flex overflow-x-auto px-2 md:px-4">
|
||||
{AGENT_CATEGORIES.map((category) => (
|
||||
<button
|
||||
@@ -19,11 +20,12 @@ export default function AgentCategoryTabsSection({
|
||||
role="tab"
|
||||
aria-selected={selectedCategory === category}
|
||||
onClick={() => onSelectCategory(category)}
|
||||
className={`whitespace-nowrap border-b-2 px-3 py-2 text-xs font-medium transition-colors md:px-4 md:py-3 md:text-sm ${
|
||||
className={cn(
|
||||
'whitespace-nowrap border-b-2 px-4 py-3 text-sm font-medium touch-manipulation transition-colors duration-150',
|
||||
selectedCategory === category
|
||||
? 'border-blue-600 text-blue-600 dark:text-blue-400'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
? 'border-primary text-primary'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
{category === 'account' && t('tabs.account')}
|
||||
{category === 'permissions' && t('tabs.permissions')}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { PillBar, Pill } from '../../../../../../shared/view/ui';
|
||||
import SessionProviderLogo from '../../../../../llm-logo-provider/SessionProviderLogo';
|
||||
import type { AgentProvider } from '../../../../types/types';
|
||||
import AgentListItem from '../AgentListItem';
|
||||
import type { AgentSelectorSectionProps } from '../types';
|
||||
|
||||
const AGENT_PROVIDERS: AgentProvider[] = ['claude', 'cursor', 'codex'];
|
||||
const AGENT_PROVIDERS: AgentProvider[] = ['claude', 'cursor', 'codex', 'gemini'];
|
||||
|
||||
const AGENT_NAMES: Record<AgentProvider, string> = {
|
||||
claude: 'Claude',
|
||||
cursor: 'Cursor',
|
||||
codex: 'Codex',
|
||||
gemini: 'Gemini',
|
||||
};
|
||||
|
||||
export default function AgentSelectorSection({
|
||||
selectedAgent,
|
||||
@@ -10,35 +18,30 @@ export default function AgentSelectorSection({
|
||||
agentContextById,
|
||||
}: AgentSelectorSectionProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex-shrink-0 border-b border-gray-200 dark:border-gray-700 md:hidden">
|
||||
<div className="flex">
|
||||
{AGENT_PROVIDERS.map((agent) => (
|
||||
<AgentListItem
|
||||
key={`mobile-${agent}`}
|
||||
agentId={agent}
|
||||
authStatus={agentContextById[agent].authStatus}
|
||||
isSelected={selectedAgent === agent}
|
||||
onClick={() => onSelectAgent(agent)}
|
||||
isMobile
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 border-b border-border px-3 py-2 md:px-4 md:py-3">
|
||||
<PillBar className="w-full md:w-auto">
|
||||
{AGENT_PROVIDERS.map((agent) => {
|
||||
const dotColor =
|
||||
agent === 'claude' ? 'bg-blue-500' :
|
||||
agent === 'cursor' ? 'bg-purple-500' :
|
||||
agent === 'gemini' ? 'bg-indigo-500' : 'bg-foreground/60';
|
||||
|
||||
<div className="hidden w-48 flex-shrink-0 border-r border-gray-200 dark:border-gray-700 md:block">
|
||||
<div className="p-2">
|
||||
{AGENT_PROVIDERS.map((agent) => (
|
||||
<AgentListItem
|
||||
key={`desktop-${agent}`}
|
||||
agentId={agent}
|
||||
authStatus={agentContextById[agent].authStatus}
|
||||
isSelected={selectedAgent === agent}
|
||||
return (
|
||||
<Pill
|
||||
key={agent}
|
||||
isActive={selectedAgent === agent}
|
||||
onClick={() => onSelectAgent(agent)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
className="min-w-0 flex-1 justify-center md:flex-initial"
|
||||
>
|
||||
<SessionProviderLogo provider={agent} className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="truncate">{AGENT_NAMES[agent]}</span>
|
||||
{agentContextById[agent].authStatus.authenticated && (
|
||||
<span className={`h-1.5 w-1.5 flex-shrink-0 rounded-full ${dotColor}`} />
|
||||
)}
|
||||
</Pill>
|
||||
);
|
||||
})}
|
||||
</PillBar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ const agentConfig: Record<AgentProvider, AgentVisualConfig> = {
|
||||
borderClass: 'border-blue-200 dark:border-blue-800',
|
||||
textClass: 'text-blue-900 dark:text-blue-100',
|
||||
subtextClass: 'text-blue-700 dark:text-blue-300',
|
||||
buttonClass: 'bg-blue-600 hover:bg-blue-700',
|
||||
buttonClass: 'bg-blue-600 hover:bg-blue-700 active:bg-blue-800',
|
||||
},
|
||||
cursor: {
|
||||
name: 'Cursor',
|
||||
@@ -35,15 +35,15 @@ const agentConfig: Record<AgentProvider, AgentVisualConfig> = {
|
||||
borderClass: 'border-purple-200 dark:border-purple-800',
|
||||
textClass: 'text-purple-900 dark:text-purple-100',
|
||||
subtextClass: 'text-purple-700 dark:text-purple-300',
|
||||
buttonClass: 'bg-purple-600 hover:bg-purple-700',
|
||||
buttonClass: 'bg-purple-600 hover:bg-purple-700 active:bg-purple-800',
|
||||
},
|
||||
codex: {
|
||||
name: 'Codex',
|
||||
bgClass: 'bg-gray-100 dark:bg-gray-800/50',
|
||||
bgClass: 'bg-muted/50',
|
||||
borderClass: 'border-gray-300 dark:border-gray-600',
|
||||
textClass: 'text-gray-900 dark:text-gray-100',
|
||||
subtextClass: 'text-gray-700 dark:text-gray-300',
|
||||
buttonClass: 'bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-600',
|
||||
buttonClass: 'bg-gray-800 hover:bg-gray-900 active:bg-gray-950 dark:bg-gray-700 dark:hover:bg-gray-600 dark:active:bg-gray-500',
|
||||
},
|
||||
gemini: {
|
||||
name: 'Gemini',
|
||||
@@ -52,7 +52,7 @@ const agentConfig: Record<AgentProvider, AgentVisualConfig> = {
|
||||
borderClass: 'border-indigo-200 dark:border-indigo-800',
|
||||
textClass: 'text-indigo-900 dark:text-indigo-100',
|
||||
subtextClass: 'text-indigo-700 dark:text-indigo-300',
|
||||
buttonClass: 'bg-indigo-600 hover:bg-indigo-700',
|
||||
buttonClass: 'bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo
|
||||
</div>
|
||||
<div>
|
||||
{authStatus.loading ? (
|
||||
<Badge variant="secondary" className="bg-gray-100 dark:bg-gray-800">
|
||||
<Badge variant="secondary" className="bg-muted">
|
||||
{t('agents.authStatus.checking')}
|
||||
</Badge>
|
||||
) : authStatus.authenticated ? (
|
||||
@@ -107,7 +107,7 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo
|
||||
</div>
|
||||
|
||||
{authStatus.method !== 'api_key' && (
|
||||
<div className="border-t border-gray-200 pt-4 dark:border-gray-700">
|
||||
<div className="border-t border-border/50 pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className={`font-medium ${config.textClass}`}>
|
||||
@@ -132,7 +132,7 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo
|
||||
)}
|
||||
|
||||
{authStatus.error && (
|
||||
<div className="border-t border-gray-200 pt-4 dark:border-gray-700">
|
||||
<div className="border-t border-border/50 pt-4">
|
||||
<div className="text-sm text-red-600 dark:text-red-400">
|
||||
{t('agents.error', { error: authStatus.error })}
|
||||
</div>
|
||||
|
||||
@@ -80,7 +80,7 @@ function ClaudeMcpServers({
|
||||
const toolsResult = serverTools[serverId];
|
||||
|
||||
return (
|
||||
<div key={serverId} className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
||||
<div key={serverId} className="rounded-lg border border-border bg-card/50 p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
@@ -102,19 +102,19 @@ function ClaudeMcpServers({
|
||||
{server.type === 'stdio' && server.config?.command && (
|
||||
<div>
|
||||
{t('mcpServers.config.command')}:{' '}
|
||||
<code className="rounded bg-gray-100 px-1 text-xs dark:bg-gray-800">{server.config.command}</code>
|
||||
<code className="rounded bg-muted px-1 text-xs">{server.config.command}</code>
|
||||
</div>
|
||||
)}
|
||||
{(server.type === 'sse' || server.type === 'http') && server.config?.url && (
|
||||
<div>
|
||||
{t('mcpServers.config.url')}:{' '}
|
||||
<code className="rounded bg-gray-100 px-1 text-xs dark:bg-gray-800">{server.config.url}</code>
|
||||
<code className="rounded bg-muted px-1 text-xs">{server.config.url}</code>
|
||||
</div>
|
||||
)}
|
||||
{server.config?.args && server.config.args.length > 0 && (
|
||||
<div>
|
||||
{t('mcpServers.config.args')}:{' '}
|
||||
<code className="rounded bg-gray-100 px-1 text-xs dark:bg-gray-800">{server.config.args.join(' ')}</code>
|
||||
<code className="rounded bg-muted px-1 text-xs">{server.config.args.join(' ')}</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -156,7 +156,7 @@ function ClaudeMcpServers({
|
||||
onClick={() => onEdit(server)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-gray-600 hover:text-gray-700"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
title={t('mcpServers.actions.edit')}
|
||||
>
|
||||
<Edit3 className="h-4 w-4" />
|
||||
@@ -176,7 +176,7 @@ function ClaudeMcpServers({
|
||||
);
|
||||
})}
|
||||
{servers.length === 0 && (
|
||||
<div className="py-8 text-center text-gray-500 dark:text-gray-400">{t('mcpServers.empty')}</div>
|
||||
<div className="py-8 text-center text-muted-foreground">{t('mcpServers.empty')}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -214,7 +214,7 @@ function CursorMcpServers({ servers, onAdd, onEdit, onDelete }: Omit<CursorMcpSe
|
||||
const serverId = server.id || server.name;
|
||||
|
||||
return (
|
||||
<div key={serverId} className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
||||
<div key={serverId} className="rounded-lg border border-border bg-card/50 p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
@@ -226,7 +226,7 @@ function CursorMcpServers({ servers, onAdd, onEdit, onDelete }: Omit<CursorMcpSe
|
||||
{server.config?.command && (
|
||||
<div>
|
||||
{t('mcpServers.config.command')}:{' '}
|
||||
<code className="rounded bg-gray-100 px-1 text-xs dark:bg-gray-800">{server.config.command}</code>
|
||||
<code className="rounded bg-muted px-1 text-xs">{server.config.command}</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -236,7 +236,7 @@ function CursorMcpServers({ servers, onAdd, onEdit, onDelete }: Omit<CursorMcpSe
|
||||
onClick={() => onEdit(server)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-gray-600 hover:text-gray-700"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
title={t('mcpServers.actions.edit')}
|
||||
>
|
||||
<Edit3 className="h-4 w-4" />
|
||||
@@ -256,7 +256,7 @@ function CursorMcpServers({ servers, onAdd, onEdit, onDelete }: Omit<CursorMcpSe
|
||||
);
|
||||
})}
|
||||
{servers.length === 0 && (
|
||||
<div className="py-8 text-center text-gray-500 dark:text-gray-400">{t('mcpServers.empty')}</div>
|
||||
<div className="py-8 text-center text-muted-foreground">{t('mcpServers.empty')}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -278,7 +278,7 @@ function CodexMcpServers({ servers, onAdd, onEdit, onDelete, deleteError }: Omit
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="h-5 w-5 text-gray-700 dark:text-gray-300" />
|
||||
<Server className="h-5 w-5 text-muted-foreground" />
|
||||
<h3 className="text-lg font-medium text-foreground">{t('mcpServers.title')}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{t('mcpServers.description.codex')}</p>
|
||||
@@ -297,7 +297,7 @@ function CodexMcpServers({ servers, onAdd, onEdit, onDelete, deleteError }: Omit
|
||||
|
||||
<div className="space-y-2">
|
||||
{servers.map((server) => (
|
||||
<div key={server.name} className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
||||
<div key={server.name} className="rounded-lg border border-border bg-card/50 p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
@@ -310,19 +310,19 @@ function CodexMcpServers({ servers, onAdd, onEdit, onDelete, deleteError }: Omit
|
||||
{server.config?.command && (
|
||||
<div>
|
||||
{t('mcpServers.config.command')}:{' '}
|
||||
<code className="rounded bg-gray-100 px-1 text-xs dark:bg-gray-800">{server.config.command}</code>
|
||||
<code className="rounded bg-muted px-1 text-xs">{server.config.command}</code>
|
||||
</div>
|
||||
)}
|
||||
{server.config?.args && server.config.args.length > 0 && (
|
||||
<div>
|
||||
{t('mcpServers.config.args')}:{' '}
|
||||
<code className="rounded bg-gray-100 px-1 text-xs dark:bg-gray-800">{server.config.args.join(' ')}</code>
|
||||
<code className="rounded bg-muted px-1 text-xs">{server.config.args.join(' ')}</code>
|
||||
</div>
|
||||
)}
|
||||
{server.config?.env && Object.keys(server.config.env).length > 0 && (
|
||||
<div>
|
||||
{t('mcpServers.config.environment')}:{' '}
|
||||
<code className="rounded bg-gray-100 px-1 text-xs dark:bg-gray-800">
|
||||
<code className="rounded bg-muted px-1 text-xs">
|
||||
{Object.entries(server.config.env).map(([key, value]) => `${key}=${maskSecret(value)}`).join(', ')}
|
||||
</code>
|
||||
</div>
|
||||
@@ -335,7 +335,7 @@ function CodexMcpServers({ servers, onAdd, onEdit, onDelete, deleteError }: Omit
|
||||
onClick={() => onEdit(server)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-gray-600 hover:text-gray-700"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
title={t('mcpServers.actions.edit')}
|
||||
>
|
||||
<Edit3 className="h-4 w-4" />
|
||||
@@ -354,13 +354,13 @@ function CodexMcpServers({ servers, onAdd, onEdit, onDelete, deleteError }: Omit
|
||||
</div>
|
||||
))}
|
||||
{servers.length === 0 && (
|
||||
<div className="py-8 text-center text-gray-500 dark:text-gray-400">{t('mcpServers.empty')}</div>
|
||||
<div className="py-8 text-center text-muted-foreground">{t('mcpServers.empty')}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-300 bg-gray-100 p-4 dark:border-gray-600 dark:bg-gray-800/50">
|
||||
<h4 className="mb-2 font-medium text-gray-900 dark:text-gray-100">{t('mcpServers.help.title')}</h4>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">{t('mcpServers.help.description')}</p>
|
||||
<div className="rounded-lg border border-border bg-muted/50 p-4">
|
||||
<h4 className="mb-2 font-medium text-foreground">{t('mcpServers.help.title')}</h4>
|
||||
<p className="text-sm text-muted-foreground">{t('mcpServers.help.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -104,7 +104,7 @@ function ClaudePermissions({
|
||||
type="checkbox"
|
||||
checked={skipPermissions}
|
||||
onChange={(event) => onSkipPermissionsChange(event.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="h-4 w-4 rounded border-input bg-card text-primary focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-orange-900 dark:text-orange-100">
|
||||
@@ -150,7 +150,7 @@ function ClaudePermissions({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
{t('permissions.allowedTools.quickAdd')}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -184,7 +184,7 @@ function ClaudePermissions({
|
||||
</div>
|
||||
))}
|
||||
{allowedTools.length === 0 && (
|
||||
<div className="py-6 text-center text-gray-500 dark:text-gray-400">
|
||||
<div className="py-6 text-center text-muted-foreground">
|
||||
{t('permissions.allowedTools.empty')}
|
||||
</div>
|
||||
)}
|
||||
@@ -237,7 +237,7 @@ function ClaudePermissions({
|
||||
</div>
|
||||
))}
|
||||
{disallowedTools.length === 0 && (
|
||||
<div className="py-6 text-center text-gray-500 dark:text-gray-400">
|
||||
<div className="py-6 text-center text-muted-foreground">
|
||||
{t('permissions.blockedTools.empty')}
|
||||
</div>
|
||||
)}
|
||||
@@ -314,7 +314,7 @@ function CursorPermissions({
|
||||
type="checkbox"
|
||||
checked={skipPermissions}
|
||||
onChange={(event) => onSkipPermissionsChange(event.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 bg-gray-100 text-purple-600 focus:ring-2 focus:ring-purple-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="h-4 w-4 rounded border-input bg-card text-primary focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-orange-900 dark:text-orange-100">
|
||||
@@ -360,7 +360,7 @@ function CursorPermissions({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
{t('permissions.allowedCommands.quickAdd')}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -394,7 +394,7 @@ function CursorPermissions({
|
||||
</div>
|
||||
))}
|
||||
{allowedCommands.length === 0 && (
|
||||
<div className="py-6 text-center text-gray-500 dark:text-gray-400">
|
||||
<div className="py-6 text-center text-muted-foreground">
|
||||
{t('permissions.allowedCommands.empty')}
|
||||
</div>
|
||||
)}
|
||||
@@ -447,7 +447,7 @@ function CursorPermissions({
|
||||
</div>
|
||||
))}
|
||||
{disallowedCommands.length === 0 && (
|
||||
<div className="py-6 text-center text-gray-500 dark:text-gray-400">
|
||||
<div className="py-6 text-center text-muted-foreground">
|
||||
{t('permissions.blockedCommands.empty')}
|
||||
</div>
|
||||
)}
|
||||
@@ -489,8 +489,8 @@ function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit<Codex
|
||||
|
||||
<div
|
||||
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'default'
|
||||
? 'border-gray-400 bg-gray-100 dark:border-gray-500 dark:bg-gray-800'
|
||||
: 'border-gray-200 bg-gray-50 hover:border-gray-300 dark:border-gray-700 dark:bg-gray-900/50 dark:hover:border-gray-600'
|
||||
? 'border-border bg-accent'
|
||||
: 'border-border bg-card/50 active:border-border active:bg-accent/50'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('default')}
|
||||
>
|
||||
@@ -514,7 +514,7 @@ function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit<Codex
|
||||
<div
|
||||
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'acceptEdits'
|
||||
? 'border-green-400 bg-green-50 dark:border-green-600 dark:bg-green-900/20'
|
||||
: 'border-gray-200 bg-gray-50 hover:border-gray-300 dark:border-gray-700 dark:bg-gray-900/50 dark:hover:border-gray-600'
|
||||
: 'border-border bg-card/50 active:border-border active:bg-accent/50'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('acceptEdits')}
|
||||
>
|
||||
@@ -538,7 +538,7 @@ function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit<Codex
|
||||
<div
|
||||
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'bypassPermissions'
|
||||
? 'border-orange-400 bg-orange-50 dark:border-orange-600 dark:bg-orange-900/20'
|
||||
: 'border-gray-200 bg-gray-50 hover:border-gray-300 dark:border-gray-700 dark:bg-gray-900/50 dark:hover:border-gray-600'
|
||||
: 'border-border bg-card/50 active:border-border active:bg-accent/50'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('bypassPermissions')}
|
||||
>
|
||||
@@ -566,7 +566,7 @@ function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit<Codex
|
||||
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
|
||||
{t('permissions.codex.technicalDetails')}
|
||||
</summary>
|
||||
<div className="mt-2 space-y-2 rounded-lg bg-gray-50 p-3 text-xs text-muted-foreground dark:bg-gray-900/50">
|
||||
<div className="mt-2 space-y-2 rounded-lg bg-muted/50 p-3 text-xs text-muted-foreground">
|
||||
<p><strong>{t('permissions.codex.modes.default.title')}:</strong> {t('permissions.codex.technicalInfo.default')}</p>
|
||||
<p><strong>{t('permissions.codex.modes.acceptEdits.title')}:</strong> {t('permissions.codex.technicalInfo.acceptEdits')}</p>
|
||||
<p><strong>{t('permissions.codex.modes.bypassPermissions.title')}:</strong> {t('permissions.codex.technicalInfo.bypassPermissions')}</p>
|
||||
@@ -603,8 +603,8 @@ function GeminiPermissions({ permissionMode, onPermissionModeChange }: Omit<Gemi
|
||||
{/* Default Mode */}
|
||||
<div
|
||||
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'default'
|
||||
? 'border-gray-400 bg-gray-100 dark:border-gray-500 dark:bg-gray-800'
|
||||
: 'border-gray-200 bg-gray-50 hover:border-gray-300 dark:border-gray-700 dark:bg-gray-900/50 dark:hover:border-gray-600'
|
||||
? 'border-border bg-accent'
|
||||
: 'border-border bg-card/50 active:border-border active:bg-accent/50'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('default')}
|
||||
>
|
||||
@@ -629,7 +629,7 @@ function GeminiPermissions({ permissionMode, onPermissionModeChange }: Omit<Gemi
|
||||
<div
|
||||
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'auto_edit'
|
||||
? 'border-green-400 bg-green-50 dark:border-green-600 dark:bg-green-900/20'
|
||||
: 'border-gray-200 bg-gray-50 hover:border-gray-300 dark:border-gray-700 dark:bg-gray-900/50 dark:hover:border-gray-600'
|
||||
: 'border-border bg-card/50 active:border-border active:bg-accent/50'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('auto_edit')}
|
||||
>
|
||||
@@ -654,7 +654,7 @@ function GeminiPermissions({ permissionMode, onPermissionModeChange }: Omit<Gemi
|
||||
<div
|
||||
className={`cursor-pointer rounded-lg border p-4 transition-all ${permissionMode === 'yolo'
|
||||
? 'border-orange-400 bg-orange-50 dark:border-orange-600 dark:bg-orange-900/20'
|
||||
: 'border-gray-200 bg-gray-50 hover:border-gray-300 dark:border-gray-700 dark:bg-gray-900/50 dark:hover:border-gray-600'
|
||||
: 'border-border bg-card/50 active:border-border active:bg-accent/50'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('yolo')}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Check, GitBranch } from 'lucide-react';
|
||||
import { Check } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGitSettings } from '../../../hooks/useGitSettings';
|
||||
import { Button, Input } from '../../../../../shared/view/ui';
|
||||
import SettingsCard from '../../SettingsCard';
|
||||
import SettingsSection from '../../SettingsSection';
|
||||
|
||||
export default function GitSettingsTab() {
|
||||
const { t } = useTranslation('settings');
|
||||
@@ -18,64 +20,62 @@ export default function GitSettingsTab() {
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<GitBranch className="h-5 w-5" />
|
||||
<h3 className="text-lg font-semibold">{t('git.title')}</h3>
|
||||
</div>
|
||||
<SettingsSection
|
||||
title={t('git.title')}
|
||||
description={t('git.description')}
|
||||
>
|
||||
<SettingsCard className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="settings-git-name" className="mb-2 block text-sm font-medium text-foreground">
|
||||
{t('git.name.label')}
|
||||
</label>
|
||||
<Input
|
||||
id="settings-git-name"
|
||||
type="text"
|
||||
value={gitName}
|
||||
onChange={(event) => setGitName(event.target.value)}
|
||||
placeholder="John Doe"
|
||||
disabled={isLoading}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{t('git.name.help')}</p>
|
||||
</div>
|
||||
|
||||
<p className="mb-4 text-sm text-muted-foreground">{t('git.description')}</p>
|
||||
<div>
|
||||
<label htmlFor="settings-git-email" className="mb-2 block text-sm font-medium text-foreground">
|
||||
{t('git.email.label')}
|
||||
</label>
|
||||
<Input
|
||||
id="settings-git-email"
|
||||
type="email"
|
||||
value={gitEmail}
|
||||
onChange={(event) => setGitEmail(event.target.value)}
|
||||
placeholder="john@example.com"
|
||||
disabled={isLoading}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{t('git.email.help')}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 rounded-lg border bg-card p-4">
|
||||
<div>
|
||||
<label htmlFor="settings-git-name" className="mb-2 block text-sm font-medium text-foreground">
|
||||
{t('git.name.label')}
|
||||
</label>
|
||||
<Input
|
||||
id="settings-git-name"
|
||||
type="text"
|
||||
value={gitName}
|
||||
onChange={(event) => setGitName(event.target.value)}
|
||||
placeholder="John Doe"
|
||||
disabled={isLoading}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{t('git.name.help')}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={saveGitConfig}
|
||||
disabled={isSaving || !gitName.trim() || !gitEmail.trim()}
|
||||
>
|
||||
{isSaving ? t('git.actions.saving') : t('git.actions.save')}
|
||||
</Button>
|
||||
|
||||
{saveStatus === 'success' && (
|
||||
<div className="flex items-center gap-2 text-sm text-green-600 dark:text-green-400">
|
||||
<Check className="h-4 w-4" />
|
||||
{t('git.status.success')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="settings-git-email" className="mb-2 block text-sm font-medium text-foreground">
|
||||
{t('git.email.label')}
|
||||
</label>
|
||||
<Input
|
||||
id="settings-git-email"
|
||||
type="email"
|
||||
value={gitEmail}
|
||||
onChange={(event) => setGitEmail(event.target.value)}
|
||||
placeholder="john@example.com"
|
||||
disabled={isLoading}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{t('git.email.help')}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={saveGitConfig}
|
||||
disabled={isSaving || !gitName.trim() || !gitEmail.trim()}
|
||||
>
|
||||
{isSaving ? t('git.actions.saving') : t('git.actions.save')}
|
||||
</Button>
|
||||
|
||||
{saveStatus === 'success' && (
|
||||
<div className="flex items-center gap-2 text-sm text-green-600 dark:text-green-400">
|
||||
<Check className="h-4 w-4" />
|
||||
{t('git.status.success')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTasksSettings } from '../../../../../contexts/TasksSettingsContext';
|
||||
import SettingsCard from '../../SettingsCard';
|
||||
import SettingsRow from '../../SettingsRow';
|
||||
import SettingsSection from '../../SettingsSection';
|
||||
import SettingsToggle from '../../SettingsToggle';
|
||||
|
||||
type TasksSettingsContextValue = {
|
||||
tasksEnabled: boolean;
|
||||
@@ -19,88 +23,83 @@ export default function TasksSettingsTab() {
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{isCheckingInstallation ? (
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-blue-600 border-t-transparent" />
|
||||
<span className="text-sm text-muted-foreground">{t('tasks.checking')}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!isTaskMasterInstalled && (
|
||||
<div className="rounded-lg border border-orange-200 bg-orange-50 p-4 dark:border-orange-800 dark:bg-orange-950/50">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-orange-100 dark:bg-orange-900">
|
||||
<svg className="h-4 w-4 text-orange-600 dark:text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 font-medium text-orange-900 dark:text-orange-100">
|
||||
{t('tasks.notInstalled.title')}
|
||||
<SettingsSection title={t('mainTabs.tasks')}>
|
||||
{isCheckingInstallation ? (
|
||||
<SettingsCard className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
<span className="text-sm text-muted-foreground">{t('tasks.checking')}</span>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
) : (
|
||||
<>
|
||||
{!isTaskMasterInstalled && (
|
||||
<div className="rounded-xl border border-orange-200 bg-orange-50 p-4 dark:border-orange-800/50 dark:bg-orange-950/30">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-orange-100 dark:bg-orange-900/50">
|
||||
<svg className="h-4 w-4 text-orange-600 dark:text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="space-y-3 text-sm text-orange-800 dark:text-orange-200">
|
||||
<p>{t('tasks.notInstalled.description')}</p>
|
||||
|
||||
<div className="rounded-lg bg-orange-100 p-3 font-mono text-sm dark:bg-orange-900/50">
|
||||
<code>{t('tasks.notInstalled.installCommand')}</code>
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 font-medium text-orange-900 dark:text-orange-100">
|
||||
{t('tasks.notInstalled.title')}
|
||||
</div>
|
||||
<div className="space-y-3 text-sm text-orange-800 dark:text-orange-200">
|
||||
<p>{t('tasks.notInstalled.description')}</p>
|
||||
|
||||
<div>
|
||||
<a
|
||||
href="https://github.com/eyaltoledano/claude-task-master"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{t('tasks.notInstalled.viewOnGitHub')}
|
||||
<svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div className="rounded-lg bg-orange-100 p-3 font-mono text-sm dark:bg-orange-900/40">
|
||||
<code>{t('tasks.notInstalled.installCommand')}</code>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="font-medium">{t('tasks.notInstalled.afterInstallation')}</p>
|
||||
<ol className="list-inside list-decimal space-y-1 text-xs">
|
||||
<li>{t('tasks.notInstalled.steps.restart')}</li>
|
||||
<li>{t('tasks.notInstalled.steps.autoAvailable')}</li>
|
||||
<li>{t('tasks.notInstalled.steps.initCommand')}</li>
|
||||
</ol>
|
||||
<div>
|
||||
<a
|
||||
href="https://github.com/eyaltoledano/claude-task-master"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{t('tasks.notInstalled.viewOnGitHub')}
|
||||
<svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="font-medium">{t('tasks.notInstalled.afterInstallation')}</p>
|
||||
<ol className="list-inside list-decimal space-y-1 text-xs">
|
||||
<li>{t('tasks.notInstalled.steps.restart')}</li>
|
||||
<li>{t('tasks.notInstalled.steps.autoAvailable')}</li>
|
||||
<li>{t('tasks.notInstalled.steps.initCommand')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{isTaskMasterInstalled && (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{t('tasks.settings.enableLabel')}</div>
|
||||
<div className="mt-1 text-sm text-muted-foreground">{t('tasks.settings.enableDescription')}</div>
|
||||
</div>
|
||||
<label className="relative inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tasksEnabled}
|
||||
onChange={(event) => setTasksEnabled(event.target.checked)}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div className="peer h-6 w-11 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isTaskMasterInstalled && (
|
||||
<SettingsCard>
|
||||
<SettingsRow
|
||||
label={t('tasks.settings.enableLabel')}
|
||||
description={t('tasks.settings.enableDescription')}
|
||||
>
|
||||
<SettingsToggle
|
||||
checked={tasksEnabled}
|
||||
onChange={setTasksEnabled}
|
||||
ariaLabel={t('tasks.settings.enableLabel')}
|
||||
/>
|
||||
</SettingsRow>
|
||||
</SettingsCard>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SettingsSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -309,6 +309,9 @@
|
||||
},
|
||||
"codex": {
|
||||
"description": "OpenAI Codex AI assistant"
|
||||
},
|
||||
"gemini": {
|
||||
"description": "Google Gemini AI assistant"
|
||||
}
|
||||
},
|
||||
"connectionStatus": "Connection Status",
|
||||
|
||||
@@ -309,6 +309,9 @@
|
||||
},
|
||||
"codex": {
|
||||
"description": "OpenAI Codex AIアシスタント"
|
||||
},
|
||||
"gemini": {
|
||||
"description": "Google Gemini AIアシスタント"
|
||||
}
|
||||
},
|
||||
"connectionStatus": "接続状態",
|
||||
|
||||
@@ -309,6 +309,9 @@
|
||||
},
|
||||
"codex": {
|
||||
"description": "OpenAI Codex AI 어시스턴트"
|
||||
},
|
||||
"gemini": {
|
||||
"description": "Google Gemini AI 어시스턴트"
|
||||
}
|
||||
},
|
||||
"connectionStatus": "연결 상태",
|
||||
|
||||
@@ -308,6 +308,9 @@
|
||||
},
|
||||
"codex": {
|
||||
"description": "AI-ассистент OpenAI Codex"
|
||||
},
|
||||
"gemini": {
|
||||
"description": "AI-ассистент Google Gemini"
|
||||
}
|
||||
},
|
||||
"connectionStatus": "Статус подключения",
|
||||
|
||||
@@ -309,6 +309,9 @@
|
||||
},
|
||||
"codex": {
|
||||
"description": "OpenAI Codex AI 助手"
|
||||
},
|
||||
"gemini": {
|
||||
"description": "Google Gemini AI 助手"
|
||||
}
|
||||
},
|
||||
"connectionStatus": "连接状态",
|
||||
|
||||
@@ -905,6 +905,16 @@
|
||||
transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Settings content fade-in transition */
|
||||
@keyframes settings-fade-in {
|
||||
from { opacity: 0; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.settings-content-enter {
|
||||
animation: settings-fade-in 150ms ease-out;
|
||||
}
|
||||
|
||||
/* Search result highlight flash */
|
||||
.search-highlight-flash {
|
||||
animation: search-flash 4s ease-out;
|
||||
|
||||
@@ -4,24 +4,24 @@ import { cn } from '../../../lib/utils';
|
||||
|
||||
// Keep visual variants centralized so all button usages stay consistent.
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium touch-manipulation transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90 active:bg-primary/80',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 active:bg-destructive/80',
|
||||
outline:
|
||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground active:bg-accent/80',
|
||||
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80 active:bg-secondary/70',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground active:bg-accent/80',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3 text-sm',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
import { useTheme } from '../../../contexts/ThemeContext';
|
||||
import { cn } from '../../../lib/utils';
|
||||
|
||||
type DarkModeToggleProps = {
|
||||
checked?: boolean;
|
||||
@@ -13,7 +14,6 @@ function DarkModeToggle({
|
||||
ariaLabel = 'Toggle dark mode',
|
||||
}: DarkModeToggleProps) {
|
||||
const { isDarkMode, toggleDarkMode } = useTheme();
|
||||
// Support controlled usage while keeping ThemeContext as the default source of truth.
|
||||
const isControlled = typeof checked === 'boolean' && typeof onToggle === 'function';
|
||||
const isEnabled = isControlled ? checked : isDarkMode;
|
||||
|
||||
@@ -29,21 +29,26 @@ function DarkModeToggle({
|
||||
return (
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:focus:ring-offset-gray-900"
|
||||
className={cn(
|
||||
'relative inline-flex h-7 w-12 flex-shrink-0 touch-manipulation cursor-pointer items-center rounded-full border-2 transition-colors duration-200',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
|
||||
isEnabled ? 'border-primary bg-primary' : 'border-border bg-muted',
|
||||
)}
|
||||
role="switch"
|
||||
aria-checked={isEnabled}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<span className="sr-only">{ariaLabel}</span>
|
||||
<span
|
||||
className={`${
|
||||
isEnabled ? 'translate-x-7' : 'translate-x-1'
|
||||
} flex h-6 w-6 transform items-center justify-center rounded-full bg-white shadow-lg transition-transform duration-200`}
|
||||
className={cn(
|
||||
'flex h-5 w-5 transform items-center justify-center rounded-full shadow-sm transition-transform duration-200',
|
||||
isEnabled ? 'translate-x-[22px] bg-white' : 'translate-x-[2px] bg-foreground/60 dark:bg-foreground/80',
|
||||
)}
|
||||
>
|
||||
{isEnabled ? (
|
||||
<Moon className="h-3.5 w-3.5 text-gray-700" />
|
||||
<Moon className="h-3 w-3 text-primary" />
|
||||
) : (
|
||||
<Sun className="h-3.5 w-3.5 text-yellow-500" />
|
||||
<Sun className="h-3 w-3 text-white dark:text-background" />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -28,15 +28,15 @@ export default function LanguageSelector({ compact = false }: LanguageSelectorPr
|
||||
// Compact style for QuickSettingsPanel
|
||||
if (compact) {
|
||||
return (
|
||||
<div className="flex items-center justify-between rounded-lg border border-transparent bg-gray-50 p-3 transition-colors hover:border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:hover:border-gray-600 dark:hover:bg-gray-700">
|
||||
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
|
||||
<Languages className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
<div className="flex items-center justify-between rounded-lg border border-transparent bg-muted/50 p-3 transition-colors hover:border-border hover:bg-accent">
|
||||
<span className="flex items-center gap-2 text-sm text-foreground">
|
||||
<Languages className="h-4 w-4 text-muted-foreground" />
|
||||
{t('account.language')}
|
||||
</span>
|
||||
<select
|
||||
value={i18n.language}
|
||||
onChange={handleLanguageChange}
|
||||
className="w-[100px] rounded-lg border border-gray-300 bg-gray-50 p-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:focus:ring-blue-400"
|
||||
className="w-[100px] rounded-lg border border-input bg-card p-2 text-sm text-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
{languages.map((lang) => (
|
||||
<option key={lang.value} value={lang.value}>
|
||||
@@ -50,28 +50,26 @@ export default function LanguageSelector({ compact = false }: LanguageSelectorPr
|
||||
|
||||
// Full style for Settings page
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="mb-1 font-medium text-gray-900 dark:text-gray-100">
|
||||
{t('account.languageLabel')}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t('account.languageDescription')}
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4 py-3.5">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{t('account.languageLabel')}
|
||||
</div>
|
||||
<div className="mt-0.5 text-xs text-muted-foreground">
|
||||
{t('account.languageDescription')}
|
||||
</div>
|
||||
<select
|
||||
value={i18n.language}
|
||||
onChange={handleLanguageChange}
|
||||
className="w-36 rounded-lg border border-gray-300 bg-gray-50 p-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
>
|
||||
{languages.map((lang) => (
|
||||
<option key={lang.value} value={lang.value}>
|
||||
{lang.nativeName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<select
|
||||
value={i18n.language}
|
||||
onChange={handleLanguageChange}
|
||||
className="w-36 rounded-lg border border-input bg-card p-2 text-sm text-foreground focus:border-primary focus:ring-1 focus:ring-primary"
|
||||
>
|
||||
{languages.map((lang) => (
|
||||
<option key={lang.value} value={lang.value}>
|
||||
{lang.nativeName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
41
src/shared/view/ui/PillBar.tsx
Normal file
41
src/shared/view/ui/PillBar.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { cn } from '../../../lib/utils';
|
||||
|
||||
/* ── Container ─────────────────────────────────────────────────── */
|
||||
type PillBarProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function PillBar({ children, className }: PillBarProps) {
|
||||
return (
|
||||
<div className={cn('inline-flex items-center gap-[2px] rounded-lg bg-muted/60 p-[3px]', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Individual pill button ────────────────────────────────────── */
|
||||
type PillProps = {
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function Pill({ isActive, onClick, children, className }: PillProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'flex touch-manipulation items-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium transition-all duration-150',
|
||||
isActive
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground active:bg-background/50',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export { default as DarkModeToggle } from './DarkModeToggle';
|
||||
export { Input } from './Input';
|
||||
export { ScrollArea } from './ScrollArea';
|
||||
export { default as Tooltip } from './Tooltip';
|
||||
export { PillBar, Pill } from './PillBar';
|
||||
|
||||
Reference in New Issue
Block a user