Feat: Refine design language and use theme tokens across most pages.

This commit is contained in:
simosmik
2026-02-16 13:17:47 +00:00
parent 42f13e151c
commit afe1be7fca
21 changed files with 1209 additions and 880 deletions

View File

@@ -13,9 +13,9 @@ export default function MainContentHeader({
onMenuClick,
}: MainContentHeaderProps) {
return (
<div className="bg-background border-b border-border p-2 sm:p-3 pwa-header-safe flex-shrink-0">
<div className="flex items-center justify-between relative">
<div className="flex items-center space-x-2 min-w-0 flex-1">
<div className="bg-background border-b border-border/60 px-3 py-1.5 sm:px-4 sm:py-2 pwa-header-safe flex-shrink-0">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2 min-w-0 flex-1">
{isMobile && <MobileMenuButton onMenuClick={onMenuClick} />}
<MainContentTitle
activeTab={activeTab}

View File

@@ -1,3 +1,4 @@
import { Folder } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import MobileMenuButton from './MobileMenuButton';
import type { MainContentStateViewProps } from '../../types/types';
@@ -10,17 +11,17 @@ export default function MainContentStateView({ mode, isMobile, onMenuClick }: Ma
return (
<div className="h-full flex flex-col">
{isMobile && (
<div className="bg-background border-b border-border p-2 sm:p-3 pwa-header-safe flex-shrink-0">
<div className="bg-background/80 backdrop-blur-sm border-b border-border/50 p-2 sm:p-3 pwa-header-safe flex-shrink-0">
<MobileMenuButton onMenuClick={onMenuClick} compact />
</div>
)}
{isLoading ? (
<div className="flex-1 flex items-center justify-center">
<div className="text-center text-gray-500 dark:text-gray-400">
<div className="w-12 h-12 mx-auto mb-4">
<div className="text-center text-muted-foreground">
<div className="w-10 h-10 mx-auto mb-4">
<div
className="w-full h-full rounded-full border-4 border-gray-200 border-t-blue-500"
className="w-full h-full rounded-full border-[3px] border-muted border-t-primary"
style={{
animation: 'spin 1s linear infinite',
WebkitAnimation: 'spin 1s linear infinite',
@@ -28,22 +29,20 @@ export default function MainContentStateView({ mode, isMobile, onMenuClick }: Ma
}}
/>
</div>
<h2 className="text-xl font-semibold mb-2">{t('mainContent.loading')}</h2>
<p>{t('mainContent.settingUpWorkspace')}</p>
<h2 className="text-lg font-semibold text-foreground mb-1">{t('mainContent.loading')}</h2>
<p className="text-sm">{t('mainContent.settingUpWorkspace')}</p>
</div>
</div>
) : (
<div className="flex-1 flex items-center justify-center">
<div className="text-center text-gray-500 dark:text-gray-400 max-w-md mx-auto px-6">
<div className="w-16 h-16 mx-auto mb-6 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-5l-2-2H5a2 2 0 00-2 2z" />
</svg>
<div className="text-center max-w-md mx-auto px-6">
<div className="w-14 h-14 mx-auto mb-5 bg-muted/50 rounded-2xl flex items-center justify-center">
<Folder className="w-7 h-7 text-muted-foreground" />
</div>
<h2 className="text-2xl font-semibold mb-3 text-gray-900 dark:text-white">{t('mainContent.chooseProject')}</h2>
<p className="text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">{t('mainContent.selectProjectDescription')}</p>
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
<p className="text-sm text-blue-700 dark:text-blue-300">
<h2 className="text-xl font-semibold mb-2 text-foreground">{t('mainContent.chooseProject')}</h2>
<p className="text-sm text-muted-foreground mb-5 leading-relaxed">{t('mainContent.selectProjectDescription')}</p>
<div className="bg-primary/5 rounded-xl p-3.5 border border-primary/10">
<p className="text-sm text-primary">
<strong>{t('mainContent.tip')}:</strong> {isMobile ? t('mainContent.createProjectMobile') : t('mainContent.createProjectDesktop')}
</p>
</div>

View File

@@ -1,3 +1,4 @@
import { MessageSquare, Terminal, Folder, GitBranch, ClipboardCheck, type LucideIcon } from 'lucide-react';
import Tooltip from '../../../Tooltip';
import type { AppTab } from '../../../../types/app';
import type { Dispatch, SetStateAction } from 'react';
@@ -12,50 +13,22 @@ type MainContentTabSwitcherProps = {
type TabDefinition = {
id: AppTab;
labelKey: string;
iconPath: string;
icon: LucideIcon;
};
const BASE_TABS: TabDefinition[] = [
{
id: 'chat',
labelKey: 'tabs.chat',
iconPath:
'M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z',
},
{
id: 'shell',
labelKey: 'tabs.shell',
iconPath: 'M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z',
},
{
id: 'files',
labelKey: 'tabs.files',
iconPath: 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-5l-2-2H5a2 2 0 00-2 2z',
},
{
id: 'git',
labelKey: 'tabs.git',
iconPath: 'M13 10V3L4 14h7v7l9-11h-7z',
},
{ id: 'chat', labelKey: 'tabs.chat', icon: MessageSquare },
{ id: 'shell', labelKey: 'tabs.shell', icon: Terminal },
{ id: 'files', labelKey: 'tabs.files', icon: Folder },
{ id: 'git', labelKey: 'tabs.git', icon: GitBranch },
];
const TASKS_TAB: TabDefinition = {
id: 'tasks',
labelKey: 'tabs.tasks',
iconPath:
'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4',
icon: ClipboardCheck,
};
function getButtonClasses(tabId: AppTab, activeTab: AppTab) {
const base = 'relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200';
if (tabId === activeTab) {
return `${base} bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm`;
}
return `${base} text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700`;
}
export default function MainContentTabSwitcher({
activeTab,
setActiveTab,
@@ -66,19 +39,27 @@ export default function MainContentTabSwitcher({
const tabs = shouldShowTasksTab ? [...BASE_TABS, TASKS_TAB] : BASE_TABS;
return (
<div className="relative flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1">
{tabs.map((tab) => (
<Tooltip key={tab.id} content={t(tab.labelKey)} position="bottom">
<button onClick={() => setActiveTab(tab.id)} className={getButtonClasses(tab.id, activeTab)}>
<span className="flex items-center gap-1 sm:gap-1.5">
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={tab.iconPath} />
</svg>
<span className="hidden md:hidden lg:inline">{t(tab.labelKey)}</span>
</span>
</button>
</Tooltip>
))}
<div className="inline-flex items-center bg-muted/60 rounded-lg p-[3px] gap-[2px]">
{tabs.map((tab) => {
const Icon = tab.icon;
const isActive = tab.id === activeTab;
return (
<Tooltip key={tab.id} content={t(tab.labelKey)} position="bottom">
<button
onClick={() => setActiveTab(tab.id)}
className={`relative flex items-center gap-1.5 px-2.5 py-[5px] text-xs font-medium rounded-md transition-all duration-150 ${
isActive
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
<Icon className="w-3.5 h-3.5" strokeWidth={isActive ? 2.2 : 1.8} />
<span className="hidden lg:inline">{t(tab.labelKey)}</span>
</button>
</Tooltip>
);
})}
</div>
);
}

View File

@@ -55,22 +55,22 @@ export default function MainContentTitle({
<div className="min-w-0 flex-1">
{activeTab === 'chat' && selectedSession ? (
<div className="min-w-0">
<h2 className="text-sm sm:text-base font-semibold text-gray-900 dark:text-white whitespace-nowrap overflow-x-auto scrollbar-hide">
<h2 className="text-sm font-semibold text-foreground whitespace-nowrap overflow-x-auto scrollbar-hide leading-tight">
{getSessionTitle(selectedSession)}
</h2>
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">{selectedProject.displayName}</div>
<div className="text-[11px] text-muted-foreground truncate leading-tight">{selectedProject.displayName}</div>
</div>
) : showChatNewSession ? (
<div className="min-w-0">
<h2 className="text-sm sm:text-base font-semibold text-gray-900 dark:text-white">{t('mainContent.newSession')}</h2>
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">{selectedProject.displayName}</div>
<h2 className="text-sm font-semibold text-foreground leading-tight">{t('mainContent.newSession')}</h2>
<div className="text-[11px] text-muted-foreground truncate leading-tight">{selectedProject.displayName}</div>
</div>
) : (
<div className="min-w-0">
<h2 className="text-sm sm:text-base font-semibold text-gray-900 dark:text-white">
<h2 className="text-sm font-semibold text-foreground leading-tight">
{getTabTitle(activeTab, shouldShowTasksTab, t)}
</h2>
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">{selectedProject.displayName}</div>
<div className="text-[11px] text-muted-foreground truncate leading-tight">{selectedProject.displayName}</div>
</div>
)}
</div>

View File

@@ -5,8 +5,8 @@ export default function MobileMenuButton({ onMenuClick, compact = false }: Mobil
const { handleMobileMenuClick, handleMobileMenuTouchEnd } = useMobileMenuHandlers(onMenuClick);
const buttonClasses = compact
? 'p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 pwa-menu-button'
: 'p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 touch-manipulation active:scale-95 pwa-menu-button flex-shrink-0';
? 'p-1.5 text-muted-foreground hover:text-foreground rounded-lg hover:bg-accent/60 pwa-menu-button'
: 'p-1.5 text-muted-foreground hover:text-foreground rounded-lg hover:bg-accent/60 touch-manipulation active:scale-95 pwa-menu-button flex-shrink-0';
return (
<button