feat: enhance theme customization and UI components

- Implemented a new color generation module to create CSS variables based on a single hue value, supporting both light and dark modes.
- Added unit tests for the color generation logic to ensure accuracy and robustness.
- Replaced dropdown location filter with tab navigation in RulesManagerPage and SkillsManagerPage for improved UX.
- Updated app store to manage custom theme hues and states, allowing for dynamic theme adjustments.
- Sanitized notification content before persisting to localStorage to prevent sensitive data exposure.
- Refactored memory retrieval logic to handle archived status more flexibly.
- Improved Tailwind CSS configuration with new gradient utilities and animations.
- Minor adjustments to SettingsPage layout for better visual consistency.
This commit is contained in:
catlog22
2026-02-04 17:20:40 +08:00
parent 88616224e0
commit e260a3f77b
30 changed files with 1377 additions and 388 deletions

View File

@@ -296,19 +296,35 @@ export function CliViewerPage() {
}
}, [lastMessage, invalidateActive]);
// Auto-add new executions as tabs when they appear
// Auto-add new executions as tabs, distributing across available panes
// Uses round-robin distribution to spread executions across panes side-by-side
const addedExecutionsRef = useRef<Set<string>>(new Set());
useEffect(() => {
if (!focusedPaneId) return;
for (const executionId of Object.keys(executions)) {
if (!addedExecutionsRef.current.has(executionId)) {
addedExecutionsRef.current.add(executionId);
const exec = executions[executionId];
const toolShort = exec.tool.split('-')[0];
addTab(focusedPaneId, executionId, `${toolShort} (${exec.mode})`);
}
}
}, [executions, focusedPaneId, addTab]);
// Get all pane IDs from the current layout
const paneIds = Object.keys(panes);
if (paneIds.length === 0) return;
// Get addTab from store directly to avoid dependency on reactive function
// This prevents infinite loop when addTab updates store state
const storeAddTab = useViewerStore.getState().addTab;
// Get new executions that haven't been added yet
const newExecutionIds = Object.keys(executions).filter(
(id) => !addedExecutionsRef.current.has(id)
);
if (newExecutionIds.length === 0) return;
// Distribute new executions across panes round-robin
newExecutionIds.forEach((executionId, index) => {
addedExecutionsRef.current.add(executionId);
const exec = executions[executionId];
const toolShort = exec.tool.split('-')[0];
// Round-robin pane selection
const targetPaneId = paneIds[index % paneIds.length];
storeAddTab(targetPaneId, executionId, `${toolShort} (${exec.mode})`);
});
}, [executions, panes]);
// Initialize layout if empty
useEffect(() => {

View File

@@ -8,19 +8,21 @@ import { useIntl } from 'react-intl';
import {
Terminal,
Search,
Plus,
RefreshCw,
Eye,
EyeOff,
CheckCircle2,
XCircle,
Folder,
User,
} from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Badge } from '@/components/ui/Badge';
import { TabsNavigation } from '@/components/ui/TabsNavigation';
import { useCommands, useCommandMutations } from '@/hooks';
import { CommandGroupAccordion } from '@/components/commands/CommandGroupAccordion';
import { LocationSwitcher } from '@/components/commands/LocationSwitcher';
import { cn } from '@/lib/utils';
// ========== Main Page Component ==========
@@ -113,45 +115,52 @@ export function CommandsManagerPage() {
{formatMessage({ id: 'commands.description' })}
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => refetch()} disabled={isFetching}>
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
{formatMessage({ id: 'common.actions.refresh' })}
</Button>
<Button>
<Plus className="w-4 h-4 mr-2" />
{formatMessage({ id: 'commands.actions.create' })}
</Button>
</div>
<Button variant="outline" onClick={() => refetch()} disabled={isFetching}>
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
{formatMessage({ id: 'common.actions.refresh' })}
</Button>
</div>
{/* Location and Show Disabled Controls */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<LocationSwitcher
currentLocation={locationFilter}
onLocationChange={setLocationFilter}
projectCount={projectCount}
userCount={userCount}
{/* Location Tabs - styled like LiteTasksPage */}
<TabsNavigation
value={locationFilter}
onValueChange={(v) => setLocationFilter(v as 'project' | 'user')}
tabs={[
{
value: 'project',
label: formatMessage({ id: 'commands.location.project' }),
icon: <Folder className="h-4 w-4" />,
badge: <Badge variant="secondary" className="ml-2">{projectCount}</Badge>,
disabled: isToggling,
},
{
value: 'user',
label: formatMessage({ id: 'commands.location.user' }),
icon: <User className="h-4 w-4" />,
badge: <Badge variant="secondary" className="ml-2">{userCount}</Badge>,
disabled: isToggling,
},
]}
/>
{/* Show Disabled Controls */}
<div className="flex items-center justify-end gap-2">
<Button
variant={showDisabledCommands ? 'default' : 'outline'}
size="sm"
onClick={() => setShowDisabledCommands((prev) => !prev)}
disabled={isToggling}
/>
<div className="flex items-center gap-2">
<Button
variant={showDisabledCommands ? 'default' : 'outline'}
size="sm"
onClick={() => setShowDisabledCommands((prev) => !prev)}
disabled={isToggling}
>
{showDisabledCommands ? (
<Eye className="w-4 h-4 mr-2" />
) : (
<EyeOff className="w-4 h-4 mr-2" />
)}
{showDisabledCommands
? formatMessage({ id: 'commands.actions.hideDisabled' })
: formatMessage({ id: 'commands.actions.showDisabled' })}
<span className="ml-1 text-xs opacity-70">({disabledCount})</span>
</Button>
</div>
>
{showDisabledCommands ? (
<Eye className="w-4 h-4 mr-2" />
) : (
<EyeOff className="w-4 h-4 mr-2" />
)}
{showDisabledCommands
? formatMessage({ id: 'commands.actions.hideDisabled' })
: formatMessage({ id: 'commands.actions.showDisabled' })}
<span className="ml-1 text-xs opacity-70">({disabledCount})</span>
</Button>
</div>
</div>

View File

@@ -28,6 +28,7 @@ import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Badge } from '@/components/ui/Badge';
import { TabsNavigation } from '@/components/ui/TabsNavigation';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
import { Checkbox } from '@/components/ui/Checkbox';
import { useMemory, useMemoryMutations } from '@/hooks';
@@ -527,33 +528,28 @@ export function MemoryPage() {
</div>
</div>
{/* Tab Navigation */}
<div className="flex items-center gap-2 border-b border-border">
<Button
variant={currentTab === 'memories' ? 'default' : 'ghost'}
size="sm"
onClick={() => setCurrentTab('memories')}
>
<Brain className="w-4 h-4 mr-2" />
{formatMessage({ id: 'memory.tabs.memories' })}
</Button>
<Button
variant={currentTab === 'favorites' ? 'default' : 'ghost'}
size="sm"
onClick={() => setCurrentTab('favorites')}
>
<Star className="w-4 h-4 mr-2" />
{formatMessage({ id: 'memory.tabs.favorites' })}
</Button>
<Button
variant={currentTab === 'archived' ? 'default' : 'ghost'}
size="sm"
onClick={() => setCurrentTab('archived')}
>
<Archive className="w-4 h-4 mr-2" />
{formatMessage({ id: 'memory.tabs.archived' })}
</Button>
</div>
{/* Tab Navigation - styled like LiteTasksPage */}
<TabsNavigation
value={currentTab}
onValueChange={(v) => setCurrentTab(v as 'memories' | 'favorites' | 'archived')}
tabs={[
{
value: 'memories',
label: formatMessage({ id: 'memory.tabs.memories' }),
icon: <Brain className="h-4 w-4" />,
},
{
value: 'favorites',
label: formatMessage({ id: 'memory.tabs.favorites' }),
icon: <Star className="h-4 w-4" />,
},
{
value: 'archived',
label: formatMessage({ id: 'memory.tabs.archived' }),
icon: <Archive className="h-4 w-4" />,
},
]}
/>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">

View File

@@ -235,21 +235,21 @@ export function ProjectOverviewPage() {
{/* Header Row */}
<div className="flex items-start justify-between mb-4 pb-3 border-b border-border">
<div className="flex-1">
<h1 className="text-base font-semibold text-foreground mb-1">
<h1 className="text-lg font-semibold text-foreground mb-1">
{projectOverview.projectName}
</h1>
<p className="text-xs text-muted-foreground">
<p className="text-sm text-muted-foreground">
{projectOverview.description || formatMessage({ id: 'projectOverview.noDescription' })}
</p>
</div>
<div className="text-xs text-muted-foreground text-right">
<div className="text-sm text-muted-foreground text-right">
<div>
{formatMessage({ id: 'projectOverview.header.initialized' })}:{' '}
{formatDate(projectOverview.initializedAt)}
</div>
{metadata?.analysis_mode && (
<div className="mt-1">
<span className="font-mono text-[10px] px-1.5 py-0.5 bg-muted rounded">
<span className="font-mono text-xs px-1.5 py-0.5 bg-muted rounded">
{metadata.analysis_mode}
</span>
</div>
@@ -258,7 +258,7 @@ export function ProjectOverviewPage() {
</div>
{/* Technology Stack */}
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-1.5">
<h3 className="text-base font-semibold text-foreground mb-3 flex items-center gap-1.5">
<Code2 className="w-4 h-4" />
{formatMessage({ id: 'projectOverview.techStack.title' })}
</h3>
@@ -266,7 +266,7 @@ export function ProjectOverviewPage() {
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{/* Languages */}
<div>
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2">
{formatMessage({ id: 'projectOverview.techStack.languages' })}
</h4>
<div className="flex flex-wrap gap-1.5">
@@ -274,21 +274,21 @@ export function ProjectOverviewPage() {
technologyStack.languages.map((lang: { name: string; file_count: number; primary?: boolean }) => (
<div
key={lang.name}
className={`flex items-center gap-1.5 px-2 py-1 bg-background border border-border rounded text-xs ${
className={`flex items-center gap-1.5 px-2 py-1 bg-background border border-border rounded text-sm ${
lang.primary ? 'ring-1 ring-primary' : ''
}`}
>
<span className="font-medium text-foreground">{lang.name}</span>
<span className="text-[10px] text-muted-foreground">{lang.file_count}</span>
<span className="text-xs text-muted-foreground">{lang.file_count}</span>
{lang.primary && (
<span className="text-[9px] px-1 py-0.5 bg-primary text-primary-foreground rounded">
<span className="text-[10px] px-1 py-0.5 bg-primary text-primary-foreground rounded">
{formatMessage({ id: 'projectOverview.techStack.primary' })}
</span>
)}
</div>
))
) : (
<span className="text-muted-foreground text-xs">
<span className="text-muted-foreground text-sm">
{formatMessage({ id: 'projectOverview.techStack.noLanguages' })}
</span>
)}
@@ -297,18 +297,18 @@ export function ProjectOverviewPage() {
{/* Frameworks */}
<div>
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2">
{formatMessage({ id: 'projectOverview.techStack.frameworks' })}
</h4>
<div className="flex flex-wrap gap-1.5">
{technologyStack?.frameworks && technologyStack.frameworks.length > 0 ? (
technologyStack.frameworks.map((fw: string) => (
<Badge key={fw} variant="success" className="px-2 py-0.5 text-[10px]">
<Badge key={fw} variant="success" className="px-2 py-0.5 text-xs">
{fw}
</Badge>
))
) : (
<span className="text-muted-foreground text-xs">
<span className="text-muted-foreground text-sm">
{formatMessage({ id: 'projectOverview.techStack.noFrameworks' })}
</span>
)}
@@ -317,36 +317,36 @@ export function ProjectOverviewPage() {
{/* Build Tools */}
<div>
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2">
{formatMessage({ id: 'projectOverview.techStack.buildTools' })}
</h4>
<div className="flex flex-wrap gap-1.5">
{technologyStack?.build_tools && technologyStack.build_tools.length > 0 ? (
technologyStack.build_tools.map((tool: string) => (
<Badge key={tool} variant="warning" className="px-2 py-0.5 text-[10px]">
<Badge key={tool} variant="warning" className="px-2 py-0.5 text-xs">
{tool}
</Badge>
))
) : (
<span className="text-muted-foreground text-xs">-</span>
<span className="text-muted-foreground text-sm">-</span>
)}
</div>
</div>
{/* Test Frameworks */}
<div>
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2">
{formatMessage({ id: 'projectOverview.techStack.testFrameworks' })}
</h4>
<div className="flex flex-wrap gap-1.5">
{technologyStack?.test_frameworks && technologyStack.test_frameworks.length > 0 ? (
technologyStack.test_frameworks.map((fw: string) => (
<Badge key={fw} variant="default" className="px-2 py-0.5 text-[10px]">
<Badge key={fw} variant="default" className="px-2 py-0.5 text-xs">
{fw}
</Badge>
))
) : (
<span className="text-muted-foreground text-xs">-</span>
<span className="text-muted-foreground text-sm">-</span>
)}
</div>
</div>
@@ -354,120 +354,128 @@ export function ProjectOverviewPage() {
</CardContent>
</Card>
{/* Architecture */}
{architecture && (
{/* Architecture & Key Components - Merged */}
{(architecture || (keyComponents && keyComponents.length > 0)) && (
<Card>
<CardContent className="p-4">
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-1.5">
<Blocks className="w-4 h-4" />
{formatMessage({ id: 'projectOverview.architecture.title' })}
</h3>
{/* Architecture Section */}
{architecture && (
<>
<h3 className="text-base font-semibold text-foreground mb-3 flex items-center gap-1.5">
<Blocks className="w-4 h-4" />
{formatMessage({ id: 'projectOverview.architecture.title' })}
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Style */}
<div>
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
{formatMessage({ id: 'projectOverview.architecture.style' })}
</h4>
<div className="px-2 py-1.5 bg-background border border-border rounded">
<span className="text-foreground font-medium text-xs">{architecture.style}</span>
</div>
</div>
{/* Layers */}
{architecture.layers && architecture.layers.length > 0 && (
<div>
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
{formatMessage({ id: 'projectOverview.architecture.layers' })}
</h4>
<div className="flex flex-wrap gap-1.5">
{architecture.layers.map((layer: string) => (
<span key={layer} className="px-1.5 py-0.5 bg-muted text-foreground rounded text-[10px]">
{layer}
</span>
))}
</div>
</div>
)}
{/* Patterns */}
{architecture.patterns && architecture.patterns.length > 0 && (
<div>
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
{formatMessage({ id: 'projectOverview.architecture.patterns' })}
</h4>
<div className="flex flex-wrap gap-1.5">
{architecture.patterns.map((pattern: string) => (
<span key={pattern} className="px-1.5 py-0.5 bg-muted text-foreground rounded text-[10px]">
{pattern}
</span>
))}
</div>
</div>
)}
</div>
</CardContent>
</Card>
)}
{/* Key Components */}
{keyComponents && keyComponents.length > 0 && (
<Card>
<CardContent className="p-4">
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-1.5">
<Component className="w-4 h-4" />
{formatMessage({ id: 'projectOverview.components.title' })}
</h3>
<div className="space-y-2">
{keyComponents.map((comp: KeyComponent) => {
const importance = comp.importance || 'low';
const importanceColors: Record<string, string> = {
high: 'border-l-2 border-l-destructive bg-destructive/5',
medium: 'border-l-2 border-l-warning bg-warning/5',
low: 'border-l-2 border-l-muted-foreground bg-muted',
};
const importanceBadges: Record<string, React.ReactElement> = {
high: (
<Badge variant="destructive" className="text-[10px] px-1.5 py-0">
{formatMessage({ id: 'projectOverview.components.importance.high' })}
</Badge>
),
medium: (
<Badge variant="warning" className="text-[10px] px-1.5 py-0">
{formatMessage({ id: 'projectOverview.components.importance.medium' })}
</Badge>
),
low: (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
{formatMessage({ id: 'projectOverview.components.importance.low' })}
</Badge>
),
};
return (
<div
key={comp.name}
className={`p-2.5 rounded ${importanceColors[importance] || importanceColors.low}`}
>
<div className="flex items-start justify-between mb-1">
<h4 className="font-medium text-foreground text-xs">{comp.name}</h4>
{importanceBadges[importance]}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Style */}
<div>
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2">
{formatMessage({ id: 'projectOverview.architecture.style' })}
</h4>
<div className="px-2 py-1.5 bg-background border border-border rounded">
<span className="text-foreground font-medium text-sm">{architecture.style}</span>
</div>
{comp.description && (
<p className="text-[10px] text-muted-foreground mb-1">{comp.description}</p>
)}
{comp.responsibility && comp.responsibility.length > 0 && (
<ul className="text-[10px] text-muted-foreground list-disc list-inside">
{comp.responsibility.map((resp: string, i: number) => (
<li key={i}>{resp}</li>
))}
</ul>
)}
</div>
);
})}
</div>
{/* Layers */}
{architecture.layers && architecture.layers.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2">
{formatMessage({ id: 'projectOverview.architecture.layers' })}
</h4>
<div className="flex flex-wrap gap-1.5">
{architecture.layers.map((layer: string) => (
<span key={layer} className="px-1.5 py-0.5 bg-muted text-foreground rounded text-xs">
{layer}
</span>
))}
</div>
</div>
)}
{/* Patterns */}
{architecture.patterns && architecture.patterns.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2">
{formatMessage({ id: 'projectOverview.architecture.patterns' })}
</h4>
<div className="flex flex-wrap gap-1.5">
{architecture.patterns.map((pattern: string) => (
<span key={pattern} className="px-1.5 py-0.5 bg-muted text-foreground rounded text-xs">
{pattern}
</span>
))}
</div>
</div>
)}
</div>
</>
)}
{/* Divider between Architecture and Components */}
{architecture && keyComponents && keyComponents.length > 0 && (
<div className="border-t border-border my-4" />
)}
{/* Key Components Section */}
{keyComponents && keyComponents.length > 0 && (
<>
<h3 className="text-base font-semibold text-foreground mb-3 flex items-center gap-1.5">
<Component className="w-4 h-4" />
{formatMessage({ id: 'projectOverview.components.title' })}
</h3>
<div className="space-y-2">
{keyComponents.map((comp: KeyComponent) => {
const importance = comp.importance || 'low';
const importanceColors: Record<string, string> = {
high: 'border-l-2 border-l-destructive bg-destructive/5',
medium: 'border-l-2 border-l-warning bg-warning/5',
low: 'border-l-2 border-l-muted-foreground bg-muted',
};
const importanceBadges: Record<string, React.ReactElement> = {
high: (
<Badge variant="destructive" className="text-xs px-1.5 py-0">
{formatMessage({ id: 'projectOverview.components.importance.high' })}
</Badge>
),
medium: (
<Badge variant="warning" className="text-xs px-1.5 py-0">
{formatMessage({ id: 'projectOverview.components.importance.medium' })}
</Badge>
),
low: (
<Badge variant="secondary" className="text-xs px-1.5 py-0">
{formatMessage({ id: 'projectOverview.components.importance.low' })}
</Badge>
),
};
return (
<div
key={comp.name}
className={`p-2.5 rounded ${importanceColors[importance] || importanceColors.low}`}
>
<div className="flex items-start justify-between mb-1">
<h4 className="font-medium text-foreground text-sm">{comp.name}</h4>
{importanceBadges[importance]}
</div>
{comp.description && (
<p className="text-xs text-muted-foreground mb-1">{comp.description}</p>
)}
{comp.responsibility && comp.responsibility.length > 0 && (
<ul className="text-xs text-muted-foreground list-disc list-inside">
{comp.responsibility.map((resp: string, i: number) => (
<li key={i}>{resp}</li>
))}
</ul>
)}
</div>
);
})}
</div>
</>
)}
</CardContent>
</Card>
)}
@@ -477,7 +485,7 @@ export function ProjectOverviewPage() {
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-foreground flex items-center gap-1.5">
<h3 className="text-base font-semibold text-foreground flex items-center gap-1.5">
<GitBranch className="w-4 h-4" />
{formatMessage({ id: 'projectOverview.devIndex.title' })}
</h3>
@@ -487,8 +495,8 @@ export function ProjectOverviewPage() {
if (count === 0) return null;
const Icon = cat.icon;
return (
<Badge key={cat.key} variant={cat.color === 'primary' ? 'default' : 'secondary'} className="text-[10px] px-1.5 py-0">
<Icon className="w-2.5 h-2.5 mr-0.5" />
<Badge key={cat.key} variant={cat.color === 'primary' ? 'default' : 'secondary'} className="text-xs px-1.5 py-0">
<Icon className="w-3 h-3 mr-0.5" />
{count}
</Badge>
);
@@ -498,13 +506,13 @@ export function ProjectOverviewPage() {
<Tabs value={devIndexView} onValueChange={(v) => setDevIndexView(v as DevIndexView)}>
<div className="flex items-center justify-between mb-3">
<TabsList className="h-7">
<TabsTrigger value="category" className="text-xs px-2 py-1 h-6">
<LayoutGrid className="w-3 h-3 mr-1" />
<TabsList className="h-8">
<TabsTrigger value="category" className="text-sm px-3 py-1 h-7">
<LayoutGrid className="w-3.5 h-3.5 mr-1" />
{formatMessage({ id: 'projectOverview.devIndex.categories' })}
</TabsTrigger>
<TabsTrigger value="timeline" className="text-xs px-2 py-1 h-6">
<GitCommitHorizontal className="w-3 h-3 mr-1" />
<TabsTrigger value="timeline" className="text-sm px-3 py-1 h-7">
<GitCommitHorizontal className="w-3.5 h-3.5 mr-1" />
{formatMessage({ id: 'projectOverview.devIndex.timeline' })}
</TabsTrigger>
</TabsList>
@@ -519,10 +527,10 @@ export function ProjectOverviewPage() {
return (
<div key={cat.key}>
<h4 className="text-xs font-semibold text-foreground mb-2 flex items-center gap-1.5">
<h4 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-1.5">
<Icon className="w-3.5 h-3.5" />
<span>{formatMessage({ id: cat.i18nKey })}</span>
<Badge variant="secondary" className="text-[10px] px-1 py-0">{entries.length}</Badge>
<Badge variant="secondary" className="text-xs px-1 py-0">{entries.length}</Badge>
</h4>
<div className="space-y-1.5">
{entries.slice(0, 5).map((entry: DevelopmentIndexEntry & { type?: string; typeLabel?: string; typeIcon?: React.ElementType; typeColor?: string; date?: string }, i: number) => (
@@ -531,15 +539,15 @@ export function ProjectOverviewPage() {
className="p-2 bg-background border border-border rounded hover:shadow-sm transition-shadow"
>
<div className="flex items-start justify-between mb-0.5">
<h5 className="font-medium text-foreground text-xs">{entry.title}</h5>
<span className="text-[10px] text-muted-foreground">
<h5 className="font-medium text-foreground text-sm">{entry.title}</h5>
<span className="text-xs text-muted-foreground">
{formatDate(entry.archivedAt || entry.date || entry.implemented_at)}
</span>
</div>
{entry.description && (
<p className="text-[10px] text-muted-foreground mb-1">{entry.description}</p>
<p className="text-xs text-muted-foreground mb-1">{entry.description}</p>
)}
<div className="flex items-center gap-1.5 text-[10px] flex-wrap">
<div className="flex items-center gap-1.5 text-xs flex-wrap">
{entry.sessionId && (
<span className="px-1.5 py-0.5 bg-primary-light text-primary rounded font-mono">
{entry.sessionId}
@@ -563,7 +571,7 @@ export function ProjectOverviewPage() {
</div>
))}
{entries.length > 5 && (
<div className="text-xs text-muted-foreground text-center py-1">
<div className="text-sm text-muted-foreground text-center py-1">
... and {entries.length - 5} more
</div>
)}
@@ -601,20 +609,20 @@ export function ProjectOverviewPage() {
? 'destructive'
: 'secondary'
}
className="text-[10px] px-1.5 py-0"
className="text-xs px-1.5 py-0"
>
{entry.typeLabel}
</Badge>
<h5 className="font-medium text-foreground text-xs">{entry.title}</h5>
<h5 className="font-medium text-foreground text-sm">{entry.title}</h5>
</div>
<span className="text-[10px] text-muted-foreground whitespace-nowrap">
<span className="text-xs text-muted-foreground whitespace-nowrap">
{formatDate(entry.date)}
</span>
</div>
{entry.description && (
<p className="text-[10px] text-muted-foreground mb-1">{entry.description}</p>
<p className="text-xs text-muted-foreground mb-1">{entry.description}</p>
)}
<div className="flex items-center gap-1.5 text-[10px]">
<div className="flex items-center gap-1.5 text-xs">
{entry.sessionId && (
<span className="px-1.5 py-0.5 bg-muted rounded font-mono">
{entry.sessionId}
@@ -635,7 +643,7 @@ export function ProjectOverviewPage() {
);
})}
{allDevEntries.length > 20 && (
<div className="text-xs text-muted-foreground text-center py-2">
<div className="text-sm text-muted-foreground text-center py-2">
... and {allDevEntries.length - 20} more entries
</div>
)}

View File

@@ -13,6 +13,9 @@ import {
AlertCircle,
FileCode,
X,
Folder,
User,
Globe,
} from 'lucide-react';
import {
useRules,
@@ -25,6 +28,7 @@ import { RuleDialog } from '@/components/shared/RuleDialog';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Badge } from '@/components/ui/Badge';
import { TabsNavigation } from '@/components/ui/TabsNavigation';
import {
Dialog,
DialogContent,
@@ -130,6 +134,12 @@ export function RulesManagerPage() {
return Array.from(cats).sort();
}, [rules]);
// Count rules by location
const projectRulesCount = React.useMemo(() =>
rules.filter((r) => r.location === 'project').length, [rules]);
const userRulesCount = React.useMemo(() =>
rules.filter((r) => r.location === 'user').length, [rules]);
// Handlers
const handleEditClick = (rule: Rule) => {
setSelectedRule(rule);
@@ -223,6 +233,35 @@ export function RulesManagerPage() {
</div>
)}
{/* Location Tabs - styled like LiteTasksPage */}
<TabsNavigation
value={locationFilter}
onValueChange={(v) => setLocationFilter(v as LocationFilter)}
tabs={[
{
value: 'all',
label: formatMessage({ id: 'rules.filters.all' }),
icon: <Globe className="h-4 w-4" />,
badge: <Badge variant="secondary" className="ml-2">{rules.length}</Badge>,
disabled: isMutating,
},
{
value: 'project',
label: formatMessage({ id: 'rules.location.project' }),
icon: <Folder className="h-4 w-4" />,
badge: <Badge variant="secondary" className="ml-2">{projectRulesCount}</Badge>,
disabled: isMutating,
},
{
value: 'user',
label: formatMessage({ id: 'rules.location.user' }),
icon: <User className="h-4 w-4" />,
badge: <Badge variant="secondary" className="ml-2">{userRulesCount}</Badge>,
disabled: isMutating,
},
]}
/>
{/* Filters */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
{/* Status tabs */}
@@ -253,37 +292,6 @@ export function RulesManagerPage() {
)}
</div>
{/* Location filter dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-2">
<Filter className="h-4 w-4" />
{formatMessage({ id: 'rules.filters.location' })}
{locationFilter !== 'all' && (
<Badge variant="secondary" className="ml-1 h-5 min-w-5 px-1">
{locationFilter === 'project' ? formatMessage({ id: 'rules.location.project' }) : formatMessage({ id: 'rules.location.user' })}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuLabel>{formatMessage({ id: 'rules.filters.location' })}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setLocationFilter('all')}>
{formatMessage({ id: 'rules.filters.all' })}
{locationFilter === 'all' && <span className="ml-auto text-primary">&#10003;</span>}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLocationFilter('project')}>
{formatMessage({ id: 'rules.location.project' })}
{locationFilter === 'project' && <span className="ml-auto text-primary">&#10003;</span>}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLocationFilter('user')}>
{formatMessage({ id: 'rules.location.user' })}
{locationFilter === 'user' && <span className="ml-auto text-primary">&#10003;</span>}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* Category filter dropdown */}
{categories.length > 0 && (
<DropdownMenu>

View File

@@ -585,7 +585,7 @@ export function SettingsPage() {
</Card>
{/* Display Settings */}
<Card className="p-6">
<div className="py-4">
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2 mb-4">
<Settings className="w-5 h-5" />
{formatMessage({ id: 'settings.sections.display' })}
@@ -607,7 +607,7 @@ export function SettingsPage() {
</Button>
</div>
</div>
</Card>
</div>
{/* Reset Settings */}
<Card className="p-6 border-destructive/50">

View File

@@ -18,10 +18,14 @@ import {
EyeOff,
List,
Grid3x3,
Folder,
User,
} from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Badge } from '@/components/ui/Badge';
import { TabsNavigation } from '@/components/ui/TabsNavigation';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/Select';
import {
AlertDialog,
@@ -34,7 +38,6 @@ import {
AlertDialogCancel,
} from '@/components/ui';
import { SkillCard, SkillDetailPanel } from '@/components/shared';
import { LocationSwitcher } from '@/components/commands/LocationSwitcher';
import { useSkills, useSkillMutations } from '@/hooks';
import { fetchSkillDetail } from '@/lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
@@ -245,14 +248,26 @@ export function SkillsManagerPage() {
</div>
</div>
{/* Location Switcher */}
<LocationSwitcher
currentLocation={locationFilter}
onLocationChange={setLocationFilter}
projectCount={projectSkills.length}
userCount={userSkills.length}
disabled={isToggling}
translationPrefix="skills"
{/* Location Tabs - styled like LiteTasksPage */}
<TabsNavigation
value={locationFilter}
onValueChange={(v) => setLocationFilter(v as 'project' | 'user')}
tabs={[
{
value: 'project',
label: formatMessage({ id: 'skills.location.project' }),
icon: <Folder className="h-4 w-4" />,
badge: <Badge variant="secondary" className="ml-2">{projectSkills.length}</Badge>,
disabled: isToggling,
},
{
value: 'user',
label: formatMessage({ id: 'skills.location.user' }),
icon: <User className="h-4 w-4" />,
badge: <Badge variant="secondary" className="ml-2">{userSkills.length}</Badge>,
disabled: isToggling,
},
]}
/>
</div>