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. */}