Files
ftb-project-management/apps/web/components/version/PlanTab.tsx
Script Generator 24fb392017 feat: 计划时间改为年月日时分 + 到期自动开始 + 耗时统计同步
1. 日期选择改为 datetime-local(年月日时分):
   - 计划开始时间/截止时间均精确到分钟
   - 展示时显示完整时间而非仅日期

2. 到期自动开始逻辑:
   - 当前时间 >= plan.startTime 且 status=pending → 自动视为开始
   - 自动触发 store 更新 status=in_progress + actualStartAt
   - 用户仍可在计划开始前手动点击"开始"按钮
   - 按钮改为"提前开始",已到期的不再显示按钮

3. 耗时计算统一用实际时间:
   - 耗时 = actualStartAt 到 completedAt(或当前时间)
   - 自动开始的 plan 使用 startTime 作为 effectiveStartAt
   - 概览总耗时/开始日期计算也同步纳入自动开始的 plan

4. checkbox 与自动开始联动:
   - 自动开始后 checkbox 可交互(不需要手动点开始)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-15 15:53:41 +08:00

443 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState } from 'react';
import { Plus, Pencil, Trash2, X, Check, ExternalLink, FileUp, Link2, Play } from 'lucide-react';
import type { VersionPlan, PlanTask } from '@/lib/version-plan';
import { calcPlanDuration, formatDuration, calcTotalDuration, calcPlanProgress, calcLinkedReqProgress } from '@/lib/version-plan';
interface Props {
plans: VersionPlan[];
versionId: string;
versionDeadline?: string;
currentUserName: string;
planType: 'research' | 'product' | 'ui';
linkedRequirements?: { id: string; title: string; code: string; productOwner?: string }[];
onCreate: (data: Omit<VersionPlan, 'id' | 'createdAt'>) => void;
onUpdate: (id: string, data: Partial<VersionPlan>) => void;
onComplete: (id: string, result: { resultType: 'link' | 'file'; resultUrl?: string; resultFileName?: string; resultFileData?: string }) => void;
onDelete: (id: string) => void;
}
const TYPE_LABEL = { research: '调研', product: '产品方案', ui: 'UI设计' };
const STATUS_STYLE = {
pending: 'bg-zinc-100 text-zinc-600',
in_progress: 'bg-blue-50 text-blue-600 border-blue-200',
completed: 'bg-green-50 text-green-700 border-green-200',
};
const STATUS_LABEL = { pending: '未开始', in_progress: '进行中', completed: '已完成' };
export function PlanTab({ plans, versionId, versionDeadline, currentUserName, planType, linkedRequirements, onCreate, onUpdate, onComplete, onDelete }: Props) {
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingPlan, setEditingPlan] = useState<VersionPlan | null>(null);
const [completingPlan, setCompletingPlan] = useState<VersionPlan | null>(null);
const typePlans = plans.filter((p) => p.versionId === versionId && p.type === planType);
const totalDuration = calcTotalDuration(typePlans);
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-[12px] text-[var(--ink-muted)]">{typePlans.length} {TYPE_LABEL[planType]}</span>
<span className="text-[12px] text-[var(--ink-soft)]"><span className="font-medium text-[var(--ink)]">{totalDuration}</span></span>
{versionDeadline && <span className="text-[12px] text-[var(--ink-muted)]"><span className="font-medium text-red-500">{versionDeadline}</span></span>}
</div>
<button onClick={() => setShowCreateModal(true)} className="flex h-8 items-center gap-1.5 rounded-lg bg-[var(--accent)] px-3 text-[13px] font-medium text-white shadow-[var(--shadow-sm)] hover:bg-[var(--accent-hover)] transition-colors">
<Plus className="h-3.5 w-3.5" strokeWidth={2} />
</button>
</div>
{typePlans.length === 0 ? (
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-12 text-center text-[13px] text-[var(--ink-muted)]">
{TYPE_LABEL[planType]}
</div>
) : (
<div className="space-y-3">
{typePlans.map((plan) => {
const now = new Date().toISOString();
// 自动开始:如果到了计划开始日期且状态还是 pending视为已开始
const autoStarted = plan.status === 'pending' && plan.startTime && new Date(plan.startTime) <= new Date();
const effectiveStatus = autoStarted ? 'in_progress' : plan.status;
const effectiveStartAt = plan.actualStartAt || (autoStarted ? plan.startTime : null);
// 耗时用实际时间戳计算
const dur = plan.status === 'completed' && plan.completedAt && plan.actualStartAt
? calcPlanDuration(plan.actualStartAt, plan.completedAt)
: effectiveStatus === 'in_progress' && effectiveStartAt
? calcPlanDuration(effectiveStartAt, now)
: { days: 0, hours: 0 };
const durText = dur.days > 0 || dur.hours > 0 ? formatDuration(dur.days, dur.hours) : '-';
// 如果自动开始了,触发 store 更新(副作用)
if (autoStarted && !plan.actualStartAt) {
onUpdate(plan.id, { status: 'in_progress' });
}
return (
<div key={plan.id} className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1.5">
<span className="text-[13px] font-medium text-[var(--ink)]">{plan.title}</span>
<span className={`inline-flex items-center rounded-md px-2 py-0.5 text-[10px] font-medium border ${STATUS_STYLE[effectiveStatus]}`}>
{STATUS_LABEL[effectiveStatus]}
</span>
</div>
<div className="flex items-center gap-4 text-[12px] text-[var(--ink-soft)]">
<span>{plan.startTime.slice(0, 16).replace('T', ' ')} {plan.endTime.slice(0, 16).replace('T', ' ')}</span>
{plan.actualStartAt && <span className="text-blue-600">{plan.actualStartAt.slice(0, 16).replace('T', ' ')}</span>}
{plan.completedAt && <span className="text-green-600">{plan.completedAt.slice(0, 16).replace('T', ' ')}</span>}
<span className="font-medium text-[var(--ink)]"> {durText}</span>
</div>
{plan.overdueReason && (
<div className="mt-1.5 text-[11px] text-red-600 bg-red-50 rounded px-2 py-1 inline-block">{plan.overdueReason}</div>
)}
{(plan.type === 'product' || plan.type === 'ui') && (
<div className="mt-1.5 text-[12px] text-[var(--ink-soft)]"><span className="font-medium text-[var(--ink)]">{plan.owner}</span></div>
)}
{plan.status === 'completed' && plan.resultUrl && (
<div className="flex items-center gap-1.5 mt-2">
{plan.resultType === 'link' ? <Link2 className="h-3 w-3 text-[var(--accent)]" /> : <FileUp className="h-3 w-3 text-[var(--accent)]" />}
<a href={plan.resultUrl} target="_blank" rel="noopener noreferrer" className="text-[12px] text-[var(--accent)] hover:underline flex items-center gap-1">
{plan.resultFileName || '查看成果'}<ExternalLink className="h-3 w-3" />
</a>
</div>
)}
{/* 调研:任务进度 */}
{plan.type === 'research' && plan.tasks && plan.tasks.length > 0 && (
<div className="mt-3 space-y-2">
<div className="flex items-center gap-2">
<div className="flex-1 h-1.5 rounded-full bg-[var(--bg-subtle)] overflow-hidden">
<div className="h-full rounded-full bg-[var(--accent)] transition-all" style={{ width: `${calcPlanProgress(plan.tasks)}%` }} />
</div>
<span className="text-[11px] font-medium tabular-nums text-[var(--ink-soft)]">{calcPlanProgress(plan.tasks)}%</span>
</div>
<div className="space-y-1">
{plan.tasks.map((task) => (
<div key={task.id} className="flex items-center gap-2">
<button
disabled={plan.status !== 'in_progress' && !autoStarted}
onClick={() => {
if (plan.status !== 'in_progress' && !autoStarted) return;
const nextStatus = task.status === 'completed' ? 'pending' : 'completed';
const updatedTasks = plan.tasks!.map((t) => t.id === task.id ? { ...t, status: nextStatus as PlanTask['status'] } : t);
onUpdate(plan.id, { tasks: updatedTasks });
}}
className={`h-4 w-4 rounded border flex items-center justify-center shrink-0 transition-colors ${plan.status !== 'in_progress' && !autoStarted ? 'opacity-40 cursor-not-allowed' : ''} ${task.status === 'completed' ? 'bg-[var(--accent)] border-[var(--accent)]' : 'border-[var(--line)]'}`}
>
{task.status === 'completed' && <Check className="h-2.5 w-2.5 text-white" strokeWidth={3} />}
</button>
<span className={`text-[12px] ${task.status === 'completed' ? 'line-through text-[var(--ink-muted)]' : 'text-[var(--ink)]'}`}>{task.title}</span>
<span className={`text-[10px] ${task.status === 'completed' ? 'text-green-600' : 'text-[var(--ink-muted)]'}`}>
{task.status === 'completed' ? '已完成' : '未完成'}
</span>
</div>
))}
</div>
</div>
)}
{/* 产品方案/UI关联需求进度 */}
{(plan.type === 'product' || plan.type === 'ui') && plan.linkedRequirementIds && plan.linkedRequirementIds.length > 0 && linkedRequirements && (
<div className="mt-3 space-y-2">
<div className="flex items-center gap-2">
<div className="flex-1 h-1.5 rounded-full bg-[var(--bg-subtle)] overflow-hidden">
<div className="h-full rounded-full bg-[var(--accent)] transition-all" style={{ width: `${calcLinkedReqProgress(plan.linkedRequirementIds, plan.completedRequirementIds)}%` }} />
</div>
<span className="text-[11px] font-medium tabular-nums text-[var(--ink-soft)]">{calcLinkedReqProgress(plan.linkedRequirementIds, plan.completedRequirementIds)}%</span>
</div>
<div className="space-y-1">
{plan.linkedRequirementIds.map((rid) => {
const req = linkedRequirements.find((r) => r.id === rid);
const isDone = (plan.completedRequirementIds || []).includes(rid);
return req ? (
<div key={rid} className="flex items-center gap-2">
<button
disabled={plan.status !== 'in_progress' && !autoStarted}
onClick={() => {
if (plan.status !== 'in_progress' && !autoStarted) return;
const current = plan.completedRequirementIds || [];
const next = isDone ? current.filter((id) => id !== rid) : [...current, rid];
onUpdate(plan.id, { completedRequirementIds: next });
}}
className={`h-4 w-4 rounded border flex items-center justify-center shrink-0 transition-colors ${plan.status !== 'in_progress' && !autoStarted ? 'opacity-40 cursor-not-allowed' : ''} ${isDone ? 'bg-[var(--accent)] border-[var(--accent)]' : 'border-[var(--line)]'}`}
>
{isDone && <Check className="h-2.5 w-2.5 text-white" strokeWidth={3} />}
</button>
<span className={`text-[11px] font-mono text-[var(--ink-muted)]`}>{req.code}</span>
<span className={`text-[12px] ${isDone ? 'line-through text-[var(--ink-muted)]' : 'text-[var(--ink)]'}`}>{req.title}</span>
</div>
) : null;
})}
</div>
{calcLinkedReqProgress(plan.linkedRequirementIds, plan.completedRequirementIds) === 100 && plan.status !== 'completed' && (
<div className="mt-2 rounded-lg bg-green-50 border border-green-200 px-3 py-2 flex items-center justify-between">
<span className="text-[12px] text-green-700"></span>
<button onClick={() => setCompletingPlan(plan)} className="text-[11px] font-medium text-green-700 hover:text-green-900 underline"></button>
</div>
)}
</div>
)}
</div>
<div className="flex items-center gap-1 ml-3">
{plan.status === 'pending' && !autoStarted && (
<button onClick={() => onUpdate(plan.id, { status: 'in_progress' })} className="h-7 px-2 flex items-center gap-1 rounded-md text-[11px] font-medium text-blue-600 hover:bg-blue-50 border border-blue-200" title="提前开始">
<Play className="h-3 w-3" />
</button>
)}
{plan.status !== 'completed' && plan.status !== 'pending' && (
<button onClick={() => setCompletingPlan(plan)} className="h-7 w-7 flex items-center justify-center rounded-md text-green-600 hover:bg-green-50" title="标记完成">
<Check className="h-3.5 w-3.5" />
</button>
)}
{plan.status !== 'completed' && (
<>
<button onClick={() => setEditingPlan(plan)} className="h-7 w-7 flex items-center justify-center rounded-md text-[var(--ink-muted)] hover:bg-[var(--bg-subtle)]" title="编辑">
<Pencil className="h-3.5 w-3.5" />
</button>
<button onClick={() => onDelete(plan.id)} className="h-7 w-7 flex items-center justify-center rounded-md text-red-500 hover:bg-red-50" title="删除">
<Trash2 className="h-3.5 w-3.5" />
</button>
</>
)}
</div>
</div>
</div>
);
})}
</div>
)}
{(showCreateModal || editingPlan) && (
<PlanFormModal
initial={editingPlan}
planType={planType}
versionId={versionId}
versionDeadline={versionDeadline}
currentUserName={currentUserName}
linkedRequirements={linkedRequirements}
onClose={() => { setShowCreateModal(false); setEditingPlan(null); }}
onSubmit={(data) => {
if (editingPlan) onUpdate(editingPlan.id, data);
else onCreate(data as any);
setShowCreateModal(false);
setEditingPlan(null);
}}
/>
)}
{completingPlan && (
<CompleteModal
onClose={() => setCompletingPlan(null)}
onSubmit={(result) => { onComplete(completingPlan.id, result); setCompletingPlan(null); }}
/>
)}
</div>
);
}
function PlanFormModal({ initial, planType, versionId, versionDeadline, currentUserName, linkedRequirements, onClose, onSubmit }: {
initial: VersionPlan | null;
planType: 'research' | 'product' | 'ui';
versionId: string;
versionDeadline?: string;
currentUserName: string;
linkedRequirements?: { id: string; title: string; code: string }[];
onClose: () => void;
onSubmit: (data: any) => void;
}) {
const now = new Date().toISOString().slice(0, 16);
const [title, setTitle] = useState(initial?.title ?? '');
const [owner] = useState(initial?.owner ?? currentUserName);
const [startTime, setStartTime] = useState(initial?.startTime?.slice(0, 16) ?? now);
const [endTime, setEndTime] = useState(initial?.endTime?.slice(0, 16) ?? '');
const [remark, setRemark] = useState(initial?.remark ?? '');
const [tasks, setTasks] = useState<PlanTask[]>(initial?.tasks ?? []);
const [newTaskTitle, setNewTaskTitle] = useState('');
const [overdueReason, setOverdueReason] = useState(initial?.overdueReason ?? '');
const [selectedReqs, setSelectedReqs] = useState<Set<string>>(new Set(initial?.linkedRequirementIds ?? []));
const showReqSelect = planType === 'product' || planType === 'ui';
const isOverdue = !!(versionDeadline && endTime && new Date(endTime) > new Date(versionDeadline));
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim() || !endTime) return;
if (isOverdue && !overdueReason.trim()) return;
if (planType === 'research' && tasks.length === 0) return;
onSubmit({
versionId,
type: planType,
title: title.trim(),
owner: owner.trim() || currentUserName,
startTime,
endTime,
status: initial?.status ?? 'pending',
linkedRequirementIds: showReqSelect ? Array.from(selectedReqs) : undefined,
tasks: tasks.length > 0 ? tasks : undefined,
remark: remark.trim() || undefined,
overdueReason: isOverdue ? overdueReason.trim() : undefined,
addedBy: currentUserName,
});
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={onClose}>
<div className="w-full max-w-md rounded-2xl bg-[var(--bg-card)] border border-[var(--line)] p-5 shadow-[var(--shadow-md)]" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-[13px] font-semibold text-[var(--ink)]">{initial ? '编辑' : '新建'}{TYPE_LABEL[planType]}</h3>
<button onClick={onClose} className="p-1 rounded hover:bg-[var(--bg-subtle)] text-[var(--ink-muted)]"><X className="h-4 w-4" /></button>
</div>
<form onSubmit={handleSubmit} className="space-y-3">
<div>
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block"></label>
<input value={title} onChange={(e) => setTitle(e.target.value)} required className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
</div>
<div>
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block"></label>
<input value={owner} readOnly className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-subtle)] px-3 text-[13px] text-[var(--ink-muted)] cursor-not-allowed" />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block"></label>
<input type="datetime-local" value={startTime} onChange={(e) => setStartTime(e.target.value)} className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
</div>
<div>
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block"></label>
<input type="datetime-local" value={endTime} onChange={(e) => setEndTime(e.target.value)} required className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
</div>
</div>
{versionDeadline && (
<div className="text-[11px] text-[var(--ink-muted)] -mt-1"><span className="text-red-500 font-medium">{versionDeadline}</span></div>
)}
{isOverdue && (
<div className="rounded-lg bg-red-50 border border-red-200 p-3 space-y-2">
<div className="text-[12px] text-red-700 font-medium"> </div>
<textarea
value={overdueReason}
onChange={(e) => setOverdueReason(e.target.value)}
rows={2}
placeholder="例如:技术难点超预期、上游交付延误..."
required
className="w-full rounded-lg border border-red-200 bg-white px-3 py-2 text-[13px] focus:border-red-400 focus:outline-none resize-none"
/>
</div>
)}
{showReqSelect && linkedRequirements && linkedRequirements.length > 0 && (
<div>
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1.5 block"></label>
<div className="max-h-[120px] overflow-y-auto rounded-lg border border-[var(--line)] p-2 space-y-1">
{linkedRequirements.map((req) => (
<label key={req.id} className="flex items-center gap-2 rounded px-2 py-1 hover:bg-[var(--bg-subtle)] cursor-pointer text-[12px]">
<input type="checkbox" checked={selectedReqs.has(req.id)} onChange={() => { const s = new Set(selectedReqs); if (s.has(req.id)) s.delete(req.id); else s.add(req.id); setSelectedReqs(s); }} className="h-3.5 w-3.5 rounded" />
<span className="text-[var(--ink-muted)] font-mono">{req.code}</span>
<span className="text-[var(--ink)] truncate">{req.title}</span>
</label>
))}
</div>
</div>
)}
{planType === 'research' && (
<div>
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1.5 block">
<span className="text-red-500">*</span>
<span className="text-[10px] text-[var(--ink-muted)] ml-1"></span>
</label>
{/* 预设选项 */}
{tasks.length === 0 && (
<div className="flex flex-wrap gap-1.5 mb-2">
{['竞品分析', '用户访谈', '数据调研', '技术可行性分析', '市场调研', '需求分析'].map((preset) => (
<button
key={preset}
type="button"
onClick={() => setTasks([...tasks, { id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 5)}`, title: preset, status: 'pending' }])}
className="h-6 px-2.5 rounded-md text-[11px] border border-dashed border-[var(--line)] text-[var(--ink-soft)] hover:border-[var(--accent)] hover:text-[var(--accent)] transition-colors"
>
+ {preset}
</button>
))}
</div>
)}
<div className="space-y-1.5 mb-2">
{tasks.map((task, i) => (
<div key={task.id} className="flex items-center gap-2 rounded-lg bg-[var(--bg-subtle)] px-3 py-1.5">
<span className="flex-1 text-[12px] text-[var(--ink)]">{task.title}</span>
<button type="button" onClick={() => setTasks(tasks.filter((_, idx) => idx !== i))} className="text-red-400 hover:text-red-600"><X className="h-3 w-3" /></button>
</div>
))}
</div>
<div className="flex gap-2">
<input
value={newTaskTitle}
onChange={(e) => setNewTaskTitle(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); if (newTaskTitle.trim()) { setTasks([...tasks, { id: `task-${Date.now()}`, title: newTaskTitle.trim(), status: 'pending' }]); setNewTaskTitle(''); } } }}
placeholder="自定义任务名称,回车添加"
className="flex-1 h-8 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[12px] focus:border-[var(--accent)] focus:outline-none"
/>
<button type="button" onClick={() => { if (newTaskTitle.trim()) { setTasks([...tasks, { id: `task-${Date.now()}`, title: newTaskTitle.trim(), status: 'pending' }]); setNewTaskTitle(''); } }} className="h-8 px-3 rounded-lg text-[12px] font-medium bg-[var(--bg-subtle)] text-[var(--ink-soft)] hover:bg-[var(--line)]"></button>
</div>
{tasks.length === 0 && (
<div className="text-[11px] text-red-500 mt-1"></div>
)}
</div>
)}
<div>
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block"></label>
<textarea value={remark} onChange={(e) => setRemark(e.target.value)} rows={2} className="w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 py-2 text-[13px] focus:border-[var(--accent)] focus:outline-none resize-none" />
</div>
<div className="flex justify-end gap-2 pt-2">
<button type="button" onClick={onClose} className="h-8 px-3 rounded-lg text-[12px] font-medium border border-[var(--line)] text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)]"></button>
<button type="submit" className="h-8 px-4 rounded-lg text-[12px] font-medium bg-[var(--accent)] text-white hover:bg-[var(--accent-hover)]">{initial ? '保存' : '创建'}</button>
</div>
</form>
</div>
</div>
);
}
function CompleteModal({ onClose, onSubmit }: {
onClose: () => void;
onSubmit: (result: { resultType: 'link' | 'file'; resultUrl?: string; resultFileName?: string; resultFileData?: string }) => void;
}) {
const [resultType, setResultType] = useState<'link' | 'file'>('link');
const [url, setUrl] = useState('');
const [fileName, setFileName] = useState('');
const [fileData, setFileData] = useState('');
const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setFileName(file.name);
const reader = new FileReader();
reader.onload = () => setFileData(reader.result as string);
reader.readAsDataURL(file);
};
const canSubmit = resultType === 'link' ? url.trim().length > 0 : fileData.length > 0;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={onClose}>
<div className="w-full max-w-sm rounded-2xl bg-[var(--bg-card)] border border-[var(--line)] p-5 shadow-[var(--shadow-md)]" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-[13px] font-semibold text-[var(--ink)]"></h3>
<button onClick={onClose} className="p-1 rounded hover:bg-[var(--bg-subtle)] text-[var(--ink-muted)]"><X className="h-4 w-4" /></button>
</div>
<div className="space-y-3">
<div className="flex gap-2">
<button type="button" onClick={() => setResultType('link')} className={`h-8 px-3 rounded-lg text-[12px] font-medium border transition-colors ${resultType === 'link' ? 'border-[var(--accent)] bg-[var(--accent-soft)] text-[var(--accent)]' : 'border-[var(--line)] text-[var(--ink-soft)]'}`}></button>
<button type="button" onClick={() => setResultType('file')} className={`h-8 px-3 rounded-lg text-[12px] font-medium border transition-colors ${resultType === 'file' ? 'border-[var(--accent)] bg-[var(--accent-soft)] text-[var(--accent)]' : 'border-[var(--line)] text-[var(--ink-soft)]'}`}></button>
</div>
{resultType === 'link' ? (
<input value={url} onChange={(e) => setUrl(e.target.value)} placeholder="https://..." className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
) : (
<div>
<input type="file" onChange={handleFile} className="text-[12px] text-[var(--ink-soft)]" />
{fileName && <p className="text-[11px] text-[var(--ink-muted)] mt-1">{fileName}</p>}
</div>
)}
<div className="flex justify-end gap-2 pt-2">
<button onClick={onClose} className="h-8 px-3 rounded-lg text-[12px] font-medium border border-[var(--line)] text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)]"></button>
<button onClick={() => onSubmit({ resultType, resultUrl: resultType === 'link' ? url.trim() : fileData, resultFileName: fileName || undefined, resultFileData: resultType === 'file' ? fileData : undefined })} disabled={!canSubmit} className="h-8 px-4 rounded-lg text-[12px] font-medium bg-[var(--accent)] text-white hover:bg-[var(--accent-hover)] disabled:opacity-50"></button>
</div>
</div>
</div>
</div>
);
}