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

@@ -1,4 +1,4 @@
import { Settings, Sparkles } from 'lucide-react';
import { Settings, Sparkles, PanelLeftOpen } from 'lucide-react';
import type { TFunction } from 'i18next';
type SidebarCollapsedProps = {
@@ -17,41 +17,39 @@ export default function SidebarCollapsed({
t,
}: SidebarCollapsedProps) {
return (
<div className="h-full flex flex-col items-center py-4 gap-4 bg-card">
<div className="h-full flex flex-col items-center py-3 gap-1 bg-background/80 backdrop-blur-sm w-12">
{/* Expand button with brand logo */}
<button
onClick={onExpand}
className="p-2 hover:bg-accent rounded-md transition-colors duration-200 group"
className="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-accent/80 transition-colors group"
aria-label={t('common:versionUpdate.ariaLabels.showSidebar')}
title={t('common:versionUpdate.ariaLabels.showSidebar')}
>
<svg
className="w-5 h-5 text-foreground group-hover:scale-110 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg>
<PanelLeftOpen className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors" />
</button>
<div className="nav-divider w-6 my-1" />
{/* Settings */}
<button
onClick={onShowSettings}
className="p-2 hover:bg-accent rounded-md transition-colors duration-200"
className="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-accent/80 transition-colors group"
aria-label={t('actions.settings')}
title={t('actions.settings')}
>
<Settings className="w-5 h-5 text-muted-foreground hover:text-foreground transition-colors" />
<Settings className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors" />
</button>
{/* Update indicator */}
{updateAvailable && (
<button
onClick={onShowVersionModal}
className="relative p-2 hover:bg-accent rounded-md transition-colors duration-200"
className="relative w-8 h-8 rounded-lg flex items-center justify-center hover:bg-accent/80 transition-colors"
aria-label={t('common:versionUpdate.ariaLabels.updateAvailable')}
title={t('common:versionUpdate.ariaLabels.updateAvailable')}
>
<Sparkles className="w-5 h-5 text-blue-500" />
<span className="absolute top-1 right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
<Sparkles className="w-4 h-4 text-blue-500" />
<span className="absolute top-1.5 right-1.5 w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse" />
</button>
)}
</div>

View File

@@ -49,7 +49,7 @@ export default function SidebarContent({
}: SidebarContentProps) {
return (
<div
className="h-full flex flex-col bg-card md:select-none md:w-80"
className="h-full flex flex-col bg-background/80 backdrop-blur-sm md:select-none md:w-72"
style={isPWA && isMobile ? { paddingTop: '44px' } : {}}
>
<SidebarHeader
@@ -67,7 +67,7 @@ export default function SidebarContent({
t={t}
/>
<ScrollArea className="flex-1 md:px-2 md:py-3 overflow-y-auto overscroll-contain">
<ScrollArea className="flex-1 md:px-1.5 md:py-2 overflow-y-auto overscroll-contain">
<SidebarProjectList {...projectListProps} />
</ScrollArea>

View File

@@ -1,7 +1,6 @@
import { Settings } from 'lucide-react';
import { Settings, ArrowUpCircle } from 'lucide-react';
import type { TFunction } from 'i18next';
import type { ReleaseInfo } from '../../../../types/sharedTypes';
import { Button } from '../../../ui/button';
type SidebarFooterProps = {
updateAvailable: boolean;
@@ -21,74 +20,81 @@ export default function SidebarFooter({
t,
}: SidebarFooterProps) {
return (
<>
<div className="flex-shrink-0">
{/* Update banner */}
{updateAvailable && (
<div className="md:p-2 border-t border-border/50 flex-shrink-0">
<div className="hidden md:block">
<Button
variant="ghost"
className="w-full justify-start gap-3 p-3 h-auto font-normal text-left hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors duration-200 border border-blue-200 dark:border-blue-700 rounded-lg mb-2"
<>
<div className="nav-divider" />
{/* Desktop update */}
<div className="hidden md:block px-2 py-1.5">
<button
className="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-left hover:bg-blue-50/80 dark:hover:bg-blue-900/15 transition-colors group"
onClick={onShowVersionModal}
>
<div className="relative">
<svg className="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
</svg>
<div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
<div className="relative flex-shrink-0">
<ArrowUpCircle className="w-4 h-4 text-blue-500 dark:text-blue-400" />
<span className="absolute -top-0.5 -right-0.5 w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse" />
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">
{releaseInfo?.title || `Version ${latestVersion}`}
</div>
<div className="text-xs text-blue-600 dark:text-blue-400">{t('version.updateAvailable')}</div>
</div>
</Button>
</div>
<div className="md:hidden p-3 pb-2">
<button
className="w-full h-12 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700 rounded-xl flex items-center justify-start gap-3 px-4 active:scale-[0.98] transition-all duration-150"
onClick={onShowVersionModal}
>
<div className="relative">
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
</svg>
<div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
</div>
<div className="min-w-0 flex-1 text-left">
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">
{releaseInfo?.title || `Version ${latestVersion}`}
</div>
<div className="text-xs text-blue-600 dark:text-blue-400">{t('version.updateAvailable')}</div>
<span className="text-xs font-medium text-blue-600 dark:text-blue-300 truncate block">
{releaseInfo?.title || `v${latestVersion}`}
</span>
<span className="text-[10px] text-blue-500/70 dark:text-blue-400/60">
{t('version.updateAvailable')}
</span>
</div>
</button>
</div>
</div>
{/* Mobile update */}
<div className="md:hidden px-3 py-2">
<button
className="w-full h-11 bg-blue-50/80 dark:bg-blue-900/15 border border-blue-200/60 dark:border-blue-700/40 rounded-xl flex items-center gap-3 px-3.5 active:scale-[0.98] transition-all"
onClick={onShowVersionModal}
>
<div className="relative flex-shrink-0">
<ArrowUpCircle className="w-4.5 h-4.5 text-blue-500 dark:text-blue-400" />
<span className="absolute -top-0.5 -right-0.5 w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse" />
</div>
<div className="min-w-0 flex-1 text-left">
<span className="text-sm font-medium text-blue-600 dark:text-blue-300 truncate block">
{releaseInfo?.title || `v${latestVersion}`}
</span>
<span className="text-xs text-blue-500/70 dark:text-blue-400/60">
{t('version.updateAvailable')}
</span>
</div>
</button>
</div>
</>
)}
<div className="md:p-2 md:border-t md:border-border flex-shrink-0">
<div className="md:hidden p-4 pb-20 border-t border-border/50">
<button
className="w-full h-14 bg-muted/50 hover:bg-muted/70 rounded-2xl flex items-center justify-start gap-4 px-4 active:scale-[0.98] transition-all duration-150"
onClick={onShowSettings}
>
<div className="w-10 h-10 rounded-2xl bg-background/80 flex items-center justify-center">
<Settings className="w-5 h-5 text-muted-foreground" />
</div>
<span className="text-lg font-medium text-foreground">{t('actions.settings')}</span>
</button>
</div>
{/* Settings */}
<div className="nav-divider" />
<Button
variant="ghost"
className="hidden md:flex w-full justify-start gap-2 p-2 h-auto font-normal text-muted-foreground hover:text-foreground hover:bg-accent transition-colors duration-200"
{/* Desktop settings */}
<div className="hidden md:block px-2 py-1.5">
<button
className="w-full flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent/60 transition-colors"
onClick={onShowSettings}
>
<Settings className="w-3 h-3" />
<Settings className="w-3.5 h-3.5" />
<span className="text-xs">{t('actions.settings')}</span>
</Button>
</button>
</div>
</>
{/* Mobile settings */}
<div className="md:hidden p-3 pb-20">
<button
className="w-full h-12 bg-muted/40 hover:bg-muted/60 rounded-xl flex items-center gap-3.5 px-4 active:scale-[0.98] transition-all"
onClick={onShowSettings}
>
<div className="w-8 h-8 rounded-xl bg-background/80 flex items-center justify-center">
<Settings className="w-4.5 h-4.5 text-muted-foreground" />
</div>
<span className="text-base font-medium text-foreground">{t('actions.settings')}</span>
</button>
</div>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { FolderPlus, MessageSquare, RefreshCw, Search, X } from 'lucide-react';
import { FolderPlus, Plus, RefreshCw, Search, X, PanelLeftClose } from 'lucide-react';
import type { TFunction } from 'i18next';
import { Button } from '../../../ui/button';
import { Input } from '../../../ui/input';
@@ -33,155 +33,159 @@ export default function SidebarHeader({
onCollapseSidebar,
t,
}: SidebarHeaderProps) {
const LogoBlock = () => (
<div className="flex items-center gap-2.5 min-w-0">
<div className="w-7 h-7 bg-primary/90 rounded-lg flex items-center justify-center shadow-sm flex-shrink-0">
<svg className="w-3.5 h-3.5 text-primary-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
</div>
<h1 className="text-sm font-semibold text-foreground tracking-tight truncate">{t('app.title')}</h1>
</div>
);
return (
<>
<div className="flex-shrink-0">
{/* Desktop header */}
<div
className="md:p-4 md:border-b md:border-border"
className="hidden md:block px-3 pt-3 pb-2"
style={isPWA && isMobile ? { paddingTop: '44px' } : {}}
>
<div className="hidden md:flex items-center justify-between">
<div className="flex items-center justify-between gap-2">
{IS_PLATFORM ? (
<a
href="https://cloudcli.ai/dashboard"
className="flex items-center gap-3 hover:opacity-80 transition-opacity group"
className="flex items-center gap-2.5 min-w-0 hover:opacity-80 transition-opacity"
title={t('tooltips.viewEnvironments')}
>
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center shadow-sm group-hover:shadow-md transition-shadow">
<MessageSquare className="w-4 h-4 text-primary-foreground" />
</div>
<div>
<h1 className="text-lg font-bold text-foreground">{t('app.title')}</h1>
<p className="text-sm text-muted-foreground">{t('app.subtitle')}</p>
</div>
<LogoBlock />
</a>
) : (
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center shadow-sm">
<MessageSquare className="w-4 h-4 text-primary-foreground" />
</div>
<div>
<h1 className="text-lg font-bold text-foreground">{t('app.title')}</h1>
<p className="text-sm text-muted-foreground">{t('app.subtitle')}</p>
</div>
</div>
<LogoBlock />
)}
<Button
variant="ghost"
size="sm"
className="h-8 w-8 px-0 hover:bg-accent transition-colors duration-200"
onClick={onCollapseSidebar}
title={t('tooltips.hideSidebar')}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</Button>
</div>
<div
className="md:hidden p-3 border-b border-border"
style={isPWA && isMobile ? { paddingTop: '16px' } : {}}
>
<div className="flex items-center justify-between">
{IS_PLATFORM ? (
<a
href="https://cloudcli.ai/dashboard"
className="flex items-center gap-3 active:opacity-70 transition-opacity"
title={t('tooltips.viewEnvironments')}
>
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
<MessageSquare className="w-4 h-4 text-primary-foreground" />
</div>
<div>
<h1 className="text-lg font-semibold text-foreground">{t('app.title')}</h1>
<p className="text-sm text-muted-foreground">{t('projects.title')}</p>
</div>
</a>
) : (
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
<MessageSquare className="w-4 h-4 text-primary-foreground" />
</div>
<div>
<h1 className="text-lg font-semibold text-foreground">{t('app.title')}</h1>
<p className="text-sm text-muted-foreground">{t('projects.title')}</p>
</div>
</div>
)}
<div className="flex gap-2">
<button
className="w-8 h-8 rounded-md bg-background border border-border flex items-center justify-center active:scale-95 transition-all duration-150"
onClick={onRefresh}
disabled={isRefreshing}
>
<RefreshCw className={`w-4 h-4 text-foreground ${isRefreshing ? 'animate-spin' : ''}`} />
</button>
<button
className="w-8 h-8 rounded-md bg-primary text-primary-foreground flex items-center justify-center active:scale-95 transition-all duration-150"
onClick={onCreateProject}
>
<FolderPlus className="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
{!isLoading && !isMobile && (
<div className="px-3 md:px-4 py-2 border-b border-border">
<div className="flex gap-2">
<div className="flex items-center gap-0.5 flex-shrink-0">
<Button
variant="default"
variant="ghost"
size="sm"
className="flex-1 h-8 text-xs bg-primary hover:bg-primary/90 transition-all duration-200"
onClick={onCreateProject}
title={t('tooltips.createProject')}
>
<FolderPlus className="w-3.5 h-3.5 mr-1.5" />
{t('projects.newProject')}
</Button>
<Button
variant="outline"
size="sm"
className="h-8 w-8 px-0 hover:bg-accent transition-colors duration-200 group"
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground hover:bg-accent/80 rounded-lg"
onClick={onRefresh}
disabled={isRefreshing}
title={t('tooltips.refresh')}
>
<RefreshCw
className={`w-3.5 h-3.5 ${
isRefreshing ? 'animate-spin' : 'group-hover:rotate-180 transition-transform duration-300'
isRefreshing ? 'animate-spin' : ''
}`}
/>
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground hover:bg-accent/80 rounded-lg"
onClick={onCreateProject}
title={t('tooltips.createProject')}
>
<Plus className="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground hover:bg-accent/80 rounded-lg"
onClick={onCollapseSidebar}
title={t('tooltips.hideSidebar')}
>
<PanelLeftClose className="w-3.5 h-3.5" />
</Button>
</div>
</div>
)}
{projectsCount > 0 && !isLoading && (
<div className="px-3 md:px-4 py-2 border-b border-border">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
{/* Search bar */}
{projectsCount > 0 && !isLoading && (
<div className="relative mt-2.5">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground/50 pointer-events-none" />
<Input
type="text"
placeholder={t('projects.searchPlaceholder')}
value={searchFilter}
onChange={(event) => onSearchFilterChange(event.target.value)}
className="pl-9 h-9 text-sm bg-muted/50 border-0 focus:bg-background focus:ring-1 focus:ring-primary/20"
className="nav-search-input pl-9 pr-8 h-9 text-xs rounded-xl border-0 placeholder:text-muted-foreground/40 focus-visible:ring-0 focus-visible:ring-offset-0 transition-all duration-200"
/>
{searchFilter && (
<button
onClick={onClearSearchFilter}
className="absolute right-2 top-1/2 transform -translate-y-1/2 p-1 hover:bg-accent rounded"
className="absolute right-2.5 top-1/2 -translate-y-1/2 p-0.5 hover:bg-accent rounded-md"
>
<X className="w-3 h-3 text-muted-foreground" />
</button>
)}
</div>
)}
</div>
{/* Desktop divider */}
<div className="hidden md:block nav-divider" />
{/* Mobile header */}
<div
className="md:hidden p-3 pb-2"
style={isPWA && isMobile ? { paddingTop: '16px' } : {}}
>
<div className="flex items-center justify-between">
{IS_PLATFORM ? (
<a
href="https://cloudcli.ai/dashboard"
className="flex items-center gap-2.5 active:opacity-70 transition-opacity min-w-0"
title={t('tooltips.viewEnvironments')}
>
<LogoBlock />
</a>
) : (
<LogoBlock />
)}
<div className="flex gap-1.5 flex-shrink-0">
<button
className="w-8 h-8 rounded-lg bg-muted/50 flex items-center justify-center active:scale-95 transition-all"
onClick={onRefresh}
disabled={isRefreshing}
>
<RefreshCw className={`w-4 h-4 text-muted-foreground ${isRefreshing ? 'animate-spin' : ''}`} />
</button>
<button
className="w-8 h-8 rounded-lg bg-primary/90 text-primary-foreground flex items-center justify-center active:scale-95 transition-all"
onClick={onCreateProject}
>
<FolderPlus className="w-4 h-4" />
</button>
</div>
</div>
)}
</>
{/* Mobile search */}
{projectsCount > 0 && !isLoading && (
<div className="relative mt-2.5">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground/50 pointer-events-none" />
<Input
type="text"
placeholder={t('projects.searchPlaceholder')}
value={searchFilter}
onChange={(event) => onSearchFilterChange(event.target.value)}
className="nav-search-input pl-10 pr-9 h-10 text-sm rounded-xl border-0 placeholder:text-muted-foreground/40 focus-visible:ring-0 focus-visible:ring-offset-0 transition-all duration-200"
/>
{searchFilter && (
<button
onClick={onClearSearchFilter}
className="absolute right-2.5 top-1/2 -translate-y-1/2 p-1 hover:bg-accent rounded-md"
>
<X className="w-3.5 h-3.5 text-muted-foreground" />
</button>
)}
</div>
)}
</div>
{/* Mobile divider */}
<div className="md:hidden nav-divider" />
</div>
);
}

View File

@@ -84,12 +84,16 @@ export default function SidebarModals({
document.body,
)}
<TypedSettings
isOpen={showSettings}
onClose={onCloseSettings}
projects={settingsProjects}
initialTab={settingsInitialTab}
/>
{showSettings &&
ReactDOM.createPortal(
<TypedSettings
isOpen={showSettings}
onClose={onCloseSettings}
projects={settingsProjects}
initialTab={settingsInitialTab}
/>,
document.body,
)}
{deleteConfirmation &&
ReactDOM.createPortal(