mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-04 20:05:38 +08:00
229 lines
7.8 KiB
TypeScript
229 lines
7.8 KiB
TypeScript
import { useRef } from 'react';
|
|
import type { ReactNode } from 'react';
|
|
import {
|
|
Download,
|
|
Eye,
|
|
FileText,
|
|
Maximize2,
|
|
Minimize2,
|
|
Moon,
|
|
Save,
|
|
Sparkles,
|
|
Sun,
|
|
X,
|
|
} from 'lucide-react';
|
|
import { cn } from '../../../lib/utils';
|
|
|
|
type PrdEditorHeaderProps = {
|
|
fileName: string;
|
|
onFileNameChange: (nextFileName: string) => void;
|
|
isNewFile: boolean;
|
|
previewMode: boolean;
|
|
onTogglePreview: () => void;
|
|
wordWrap: boolean;
|
|
onToggleWordWrap: () => void;
|
|
isDarkMode: boolean;
|
|
onToggleTheme: () => void;
|
|
onDownload: () => void;
|
|
onOpenGenerateTasks: () => void;
|
|
canGenerateTasks: boolean;
|
|
onSave: () => void;
|
|
saving: boolean;
|
|
saveSuccess: boolean;
|
|
isFullscreen: boolean;
|
|
onToggleFullscreen: () => void;
|
|
onClose: () => void;
|
|
};
|
|
|
|
type HeaderIconButtonProps = {
|
|
title: string;
|
|
onClick: () => void;
|
|
icon: ReactNode;
|
|
active?: boolean;
|
|
};
|
|
|
|
function HeaderIconButton({ title, onClick, icon, active = false }: HeaderIconButtonProps) {
|
|
return (
|
|
<button
|
|
onClick={onClick}
|
|
title={title}
|
|
className={cn(
|
|
'p-2 rounded-md min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center transition-colors',
|
|
active
|
|
? 'text-purple-600 dark:text-purple-400 bg-purple-50 dark:bg-purple-900/50'
|
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800',
|
|
)}
|
|
>
|
|
{icon}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export default function PrdEditorHeader({
|
|
fileName,
|
|
onFileNameChange,
|
|
isNewFile,
|
|
previewMode,
|
|
onTogglePreview,
|
|
wordWrap,
|
|
onToggleWordWrap,
|
|
isDarkMode,
|
|
onToggleTheme,
|
|
onDownload,
|
|
onOpenGenerateTasks,
|
|
canGenerateTasks,
|
|
onSave,
|
|
saving,
|
|
saveSuccess,
|
|
isFullscreen,
|
|
onToggleFullscreen,
|
|
onClose,
|
|
}: PrdEditorHeaderProps) {
|
|
const fileNameInputRef = useRef<HTMLInputElement | null>(null);
|
|
|
|
return (
|
|
<div className="flex min-w-0 flex-shrink-0 items-center justify-between border-b border-gray-200 p-4 dark:border-gray-700">
|
|
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded bg-purple-600">
|
|
<FileText className="h-4 w-4 text-white" />
|
|
</div>
|
|
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center">
|
|
<div className="flex min-w-0 flex-1 items-center gap-1">
|
|
<div className="flex min-w-0 flex-1 items-center rounded-md border border-gray-200 bg-gray-50 px-3 py-2 focus-within:border-purple-500 focus-within:ring-2 focus-within:ring-purple-500 dark:border-gray-600 dark:bg-gray-700 dark:focus-within:border-purple-400 dark:focus-within:ring-purple-400">
|
|
<input
|
|
ref={fileNameInputRef}
|
|
type="text"
|
|
value={fileName}
|
|
onChange={(event) => onFileNameChange(event.target.value)}
|
|
className="min-w-0 flex-1 border-none bg-transparent text-base font-medium text-gray-900 placeholder-gray-400 outline-none dark:text-white dark:placeholder-gray-500 sm:text-sm"
|
|
placeholder="Enter PRD filename"
|
|
maxLength={100}
|
|
/>
|
|
<span className="ml-1 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 sm:text-xs">
|
|
.txt
|
|
</span>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => fileNameInputRef.current?.focus()}
|
|
className="p-1 text-gray-400 transition-colors hover:text-purple-600 dark:hover:text-purple-400"
|
|
title="Focus filename input"
|
|
>
|
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex flex-shrink-0 items-center gap-2">
|
|
<span className="whitespace-nowrap rounded bg-purple-100 px-2 py-1 text-xs text-purple-600 dark:bg-purple-900 dark:text-purple-300">
|
|
PRD
|
|
</span>
|
|
{isNewFile && (
|
|
<span className="whitespace-nowrap rounded bg-green-100 px-2 py-1 text-xs text-green-600 dark:bg-green-900 dark:text-green-300">
|
|
New
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<p className="mt-1 truncate text-xs text-gray-500 dark:text-gray-400 sm:text-sm">
|
|
Product Requirements Document
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-shrink-0 items-center gap-1 md:gap-2">
|
|
<HeaderIconButton
|
|
title={previewMode ? 'Switch to edit mode' : 'Preview markdown'}
|
|
onClick={onTogglePreview}
|
|
icon={<Eye className="h-5 w-5 md:h-4 md:w-4" />}
|
|
active={previewMode}
|
|
/>
|
|
|
|
<HeaderIconButton
|
|
title={wordWrap ? 'Disable word wrap' : 'Enable word wrap'}
|
|
onClick={onToggleWordWrap}
|
|
icon={<span className="font-mono text-sm font-bold md:text-xs">WRAP</span>}
|
|
active={wordWrap}
|
|
/>
|
|
|
|
<HeaderIconButton
|
|
title="Toggle theme"
|
|
onClick={onToggleTheme}
|
|
icon={
|
|
isDarkMode ? (
|
|
<Sun className="h-5 w-5 md:h-4 md:w-4" />
|
|
) : (
|
|
<Moon className="h-5 w-5 md:h-4 md:w-4" />
|
|
)
|
|
}
|
|
/>
|
|
|
|
<HeaderIconButton
|
|
title="Download PRD"
|
|
onClick={onDownload}
|
|
icon={<Download className="h-5 w-5 md:h-4 md:w-4" />}
|
|
/>
|
|
|
|
<button
|
|
onClick={onOpenGenerateTasks}
|
|
disabled={!canGenerateTasks}
|
|
className={cn(
|
|
'px-3 py-2 rounded-md disabled:opacity-50 flex items-center gap-2 transition-colors text-sm font-medium text-white min-h-[44px] md:min-h-0',
|
|
'bg-purple-600 hover:bg-purple-700',
|
|
)}
|
|
title="Generate tasks from PRD content"
|
|
>
|
|
<Sparkles className="h-4 w-4" />
|
|
<span className="hidden md:inline">Generate Tasks</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={onSave}
|
|
disabled={saving}
|
|
className={cn(
|
|
'px-3 py-2 text-white rounded-md disabled:opacity-50 flex items-center gap-2 transition-colors min-h-[44px] md:min-h-0',
|
|
saveSuccess ? 'bg-green-600 hover:bg-green-700' : 'bg-purple-600 hover:bg-purple-700',
|
|
)}
|
|
>
|
|
{saveSuccess ? (
|
|
<>
|
|
<svg className="h-5 w-5 md:h-4 md:w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
<span className="hidden sm:inline">Saved!</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="h-5 w-5 md:h-4 md:w-4" />
|
|
<span className="hidden sm:inline">{saving ? 'Saving...' : 'Save PRD'}</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
<button
|
|
onClick={onToggleFullscreen}
|
|
className="hidden items-center justify-center rounded-md p-2 text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white md:flex"
|
|
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
|
|
>
|
|
{isFullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
|
|
</button>
|
|
|
|
<HeaderIconButton
|
|
title="Close"
|
|
onClick={onClose}
|
|
icon={<X className="h-6 w-6 md:h-4 md:w-4" />}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|