import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { DetailTabId } from '../../lib/urlState'; import { shareTask, unshareTask } from '../../api'; interface Tab { id: DetailTabId; labelKey: string; } interface DetailHeaderProps { title: string; subtitle: string; tabs: Tab[]; activeTab: DetailTabId; /** True while the deferred content tab is still catching up to activeTab. * Used to render a subtle pulse on the active tab so the user knows the * click was registered even if the content takes a frame or two to paint. */ tabTransitionPending?: boolean; onTabChange: (tab: DetailTabId) => void; onClose: () => void; detailWidth?: 'normal' | 'focused'; onWidthToggle?: () => void; // 共有機能 taskId?: number; shareToken?: string | null; onShareChange?: () => void; /** Status of the latest job for this task. The Continue button is shown * when latestJobStatus is provided and enabled only on terminal states. */ latestJobStatus?: string | null; /** Click handler for the Continue button. When undefined, the button is * hidden entirely (e.g., shared/read-only views). */ onContinue?: () => void; } function ShareButton({ taskId, shareToken, onShareChange }: { taskId: number; shareToken: string | null; onShareChange?: () => void }) { const { t } = useTranslation('detail'); const [copied, setCopied] = useState(false); const qc = useQueryClient(); const shareMutation = useMutation({ mutationFn: () => shareTask(taskId), onSuccess: (data) => { const url = `${window.location.origin}${data.shareUrl}`; navigator.clipboard.writeText(url); setCopied(true); setTimeout(() => setCopied(false), 2000); qc.invalidateQueries({ queryKey: ['localTaskDetail', taskId] }); qc.invalidateQueries({ queryKey: ['localTasks'] }); onShareChange?.(); }, }); const unshareMutation = useMutation({ mutationFn: () => unshareTask(taskId), onSuccess: () => { qc.invalidateQueries({ queryKey: ['localTaskDetail', taskId] }); qc.invalidateQueries({ queryKey: ['localTasks'] }); onShareChange?.(); }, }); // Refero refresh: collapse share UI from text-button rows into compact // icon-only buttons. The title row was getting eaten by long Japanese // labels ("リンクコピー" / "共有停止") on narrow viewports; with icons // we get the same affordance in ~32px instead of ~80px each. const iconBtnBase = 'inline-flex items-center justify-center w-7 h-7 border rounded-md transition-colors disabled:opacity-50'; if (!shareToken) { return ( ); } const handleCopy = () => { const url = `${window.location.origin}/ui/shared/${shareToken}`; navigator.clipboard.writeText(url); setCopied(true); setTimeout(() => setCopied(false), 2000); }; return (
); } function ContinueButton({ latestJobStatus, onClick }: { latestJobStatus: string | null; onClick: () => void }) { const { t } = useTranslation('detail'); // Mirror the spec/backend TERMINAL list (worker maps abort outcomes to // 'failed', so 'aborted' is intentionally absent). const TERMINAL = ['succeeded', 'failed', 'waiting_human', 'cancelled']; const enabled = latestJobStatus != null && TERMINAL.includes(latestJobStatus); const iconBtnBase = 'inline-flex items-center justify-center w-7 h-7 border rounded-md transition-colors disabled:opacity-40 disabled:cursor-not-allowed'; return ( ); } export function DetailHeader({ title, subtitle, tabs, activeTab, tabTransitionPending, onTabChange, onClose, detailWidth, onWidthToggle, taskId, shareToken, onShareChange, latestJobStatus, onContinue }: DetailHeaderProps) { const { t } = useTranslation('detail'); // Mobile (< sm) hides the close button and tab bar because App.tsx // renders its own mobile-level top tab bar with the same controls. // Two close buttons / two tab bars on iPhone was visually redundant. return (
{subtitle}
{title}
{/* Inline action cluster: width toggle + share + close. Share is now icon-only (32px) so it fits next to the title instead of occupying its own row. */}
{onContinue && taskId != null && ( )} {taskId != null && ( )} {onWidthToggle && detailWidth && ( )}
{tabs.map(tab => { const active = activeTab === tab.id; const pending = active && tabTransitionPending; return ( ); })}
); }