mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
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:
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">✓</span>}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setLocationFilter('project')}>
|
||||
{formatMessage({ id: 'rules.location.project' })}
|
||||
{locationFilter === 'project' && <span className="ml-auto text-primary">✓</span>}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setLocationFilter('user')}>
|
||||
{formatMessage({ id: 'rules.location.user' })}
|
||||
{locationFilter === 'user' && <span className="ml-auto text-primary">✓</span>}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Category filter dropdown */}
|
||||
{categories.length > 0 && (
|
||||
<DropdownMenu>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user