import { useState, useCallback, useEffect, useRef } from "react"; import { Plus, Pencil, Trash2, Check, X, GripVertical, Sparkles, Download, Copy, Loader, ArrowLeft, ChevronUp } from "lucide-react"; import "./RoadmapsView.css"; import type { ToastType } from "../hooks/useToast"; import { useRoadmaps, type FeatureSuggestion, type MilestoneSuggestion, type SuggestionDraftPatch } from "../hooks/useRoadmaps"; import { useViewportMode } from "../hooks/useViewportMode"; import { useConfirm } from "../hooks/useConfirm"; import type { Roadmap, RoadmapMilestone, RoadmapFeature, RoadmapCreateInput, RoadmapUpdateInput, RoadmapMilestoneCreateInput, RoadmapMilestoneUpdateInput, RoadmapFeatureCreateInput, RoadmapFeatureUpdateInput, RoadmapMissionPlanningHandoff, RoadmapFeatureTaskPlanningHandoff, } from "@fusion/core"; export interface RoadmapsViewProps { projectId?: string; addToast: (message: string, type?: ToastType) => void; } // ── Drag State Types ──────────────────────────────────────────────── interface MilestoneDragState { draggingId: string | null; dropTargetId: string | null; dropPosition: "before" | "after" | null; } interface FeatureDragState { draggingId: string | null; draggingMilestoneId: string | null; dropTargetMilestoneId: string | null; dropTargetIndex: number | null; dropPosition: "before" | "after" | null; } // ── Inline Edit State Types ───────────────────────────────────────── interface InlineEditState { roadmapId: string | null; field: "title" | "description" | null; value: string; } interface MilestoneInlineEditState { milestoneId: string | null; field: "title" | "description" | null; value: string; } interface FeatureInlineEditState { featureId: string | null; field: "title" | "description" | null; value: string; } // ── Create Form State ─────────────────────────────────────────────── interface CreateFormState { type: "roadmap" | "milestone" | "feature" | null; parentId?: string; title: string; description: string; } // ── Handoff Modal Types ───────────────────────────────────────────── interface HandoffModalProps { isOpen: boolean; onClose: () => void; roadmapId: string; roadmapTitle: string; handoffPayload: { mission: RoadmapMissionPlanningHandoff; features: RoadmapFeatureTaskPlanningHandoff[] } | null; isLoading: boolean; error: Error | null; onFetchHandoff: () => void; onCopyToClipboard: () => void; } // ── Handoff Modal Component ───────────────────────────────────────── function HandoffModal({ isOpen, onClose, roadmapTitle, handoffPayload, isLoading, error, onFetchHandoff, onCopyToClipboard, }: HandoffModalProps) { if (!isOpen) return null; return (
e.stopPropagation()} role="dialog" aria-modal="true" aria-labelledby="handoff-modal-title">

Export Roadmap: {roadmapTitle}

Export roadmap data for use in mission and task planning flows. This is a read-only export — no missions or tasks will be created.

{error && (
Error loading handoff data: {error.message}
)} {!handoffPayload && !isLoading && (
)} {isLoading && (

Loading handoff data...

)} {handoffPayload && ( <>

Mission Planning Handoff

                    {JSON.stringify(handoffPayload.mission, null, 2)}
                  

Feature Task Planning Handoffs ({handoffPayload.features.length})

                    {JSON.stringify(handoffPayload.features, null, 2)}
                  
)}
{handoffPayload && ( )}
); } // ── Roadmap Item ───────────────────────────────────────────────────── function RoadmapItem({ roadmap, isSelected, onSelect, onEdit, onDelete, onExport, }: { roadmap: Roadmap; isSelected: boolean; onSelect: () => void; onEdit: () => void; onDelete: () => void; onExport: () => void; }) { const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { onSelect(); } }; const handleEditClick = (e: React.MouseEvent) => { e.stopPropagation(); onEdit(); }; const handleDeleteClick = (e: React.MouseEvent) => { e.stopPropagation(); onDelete(); }; const handleExportClick = (e: React.MouseEvent) => { e.stopPropagation(); onExport(); }; return (
{roadmap.title}
{roadmap.description && (
{roadmap.description}
)}
); } // ── Mobile Roadmap List ────────────────────────────────────────────── function MobileRoadmapList({ roadmaps, selectedRoadmapId, onSelect, onCreate, onEdit, onDelete, onExport, showCreateForm, onCancelCreate, onSaveCreate, }: { roadmaps: Roadmap[]; selectedRoadmapId: string | null; onSelect: (id: string) => void; onCreate: () => void; onEdit: (roadmap: Roadmap) => void; onDelete: (roadmapId: string) => void; onExport: (roadmap: Roadmap) => void; showCreateForm: boolean; onCancelCreate: () => void; onSaveCreate: (input: RoadmapCreateInput) => void; }) { return (

Roadmaps

{!showCreateForm && ( )}
{showCreateForm && (
)} {roadmaps.length === 0 && !showCreateForm ? (

No roadmaps yet.

) : (
{roadmaps.map((roadmap) => (
onSelect(roadmap.id)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter") { onSelect(roadmap.id); } }} data-testid={`mobile-roadmap-item-${roadmap.id}`} >
{roadmap.title} {roadmap.description && ( {roadmap.description} )}
))}
)}
); } // ── Mobile Roadmap Header (shown when roadmap is selected) ──────────── function MobileRoadmapHeader({ roadmapTitle, onBack, onEdit, onDelete, onCreate, }: { roadmapTitle: string; onBack: () => void; onEdit: () => void; onDelete: () => void; onCreate: () => void; }) { return (

{roadmapTitle}

); } // ── Milestone Card ─────────────────────────────────────────────────── function MilestoneCard({ milestone, features, onEditMilestone, onDeleteMilestone, onAddFeature, onEditFeature, onDeleteFeature, milestoneEdit, onMilestoneEditChange, onMilestoneEditFieldChange, onCancelMilestoneEdit, onSaveMilestoneEdit, featureEdit, onFeatureEditChange, onStartFeatureEdit: _onStartFeatureEdit, onCancelFeatureEdit, onSaveFeatureEdit, projectId: _projectId, addToast: _addToast, // Milestone drag-and-drop props isMilestoneDragging, isMilestoneDropTarget, milestoneDropPosition, onMilestoneDragStart, onMilestoneDragEnd, onMilestoneDragOver, onMilestoneDrop, onMilestoneDragLeave, // Feature drag-and-drop props isFeatureDragging, isFeatureDropTarget, featureDropIndex, onFeatureDragStart, onFeatureDragEnd, onFeatureDragOver, onFeatureDrop, onFeatureDragLeave, onFeatureDropOnMilestone, // Feature suggestion props featureSuggestions, isGeneratingFeatureSuggestions, onGenerateFeatureSuggestions, onAcceptFeatureSuggestion, onAcceptAllFeatureSuggestions, onUpdateFeatureSuggestionDraft, onClearFeatureSuggestions, }: { milestone: RoadmapMilestone; features: RoadmapFeature[]; onEditMilestone: () => void; onDeleteMilestone: () => void; onAddFeature: () => void; onEditFeature: (featureId: string) => void; onDeleteFeature: (featureId: string) => void; milestoneEdit: MilestoneInlineEditState | null; onMilestoneEditChange: (value: string) => void; onMilestoneEditFieldChange: (field: "title" | "description") => void; onCancelMilestoneEdit: () => void; onSaveMilestoneEdit: (updates: RoadmapMilestoneUpdateInput) => void; featureEdit: FeatureInlineEditState | null; onFeatureEditChange: (value: string) => void; onStartFeatureEdit: (featureId: string, currentTitle: string, currentDescription?: string) => void; onCancelFeatureEdit: () => void; onSaveFeatureEdit: (updates: RoadmapFeatureUpdateInput) => void; projectId?: string; addToast: (message: string, type?: ToastType) => void; // Milestone drag-and-drop props isMilestoneDragging: boolean; isMilestoneDropTarget: boolean; milestoneDropPosition: "before" | "after" | null; onMilestoneDragStart: (milestoneId: string) => void; onMilestoneDragEnd: () => void; onMilestoneDragOver: (milestoneId: string) => void; onMilestoneDrop: (milestoneId: string) => void; onMilestoneDragLeave: (e: React.DragEvent) => void; // Feature drag-and-drop props isFeatureDragging: (featureId: string) => boolean; isFeatureDropTarget: boolean; featureDropIndex: number | null; onFeatureDragStart: (featureId: string, milestoneId: string) => void; onFeatureDragEnd: () => void; onFeatureDragOver: (featureId: string, position: "before" | "after") => void; onFeatureDrop: (featureId: string, targetIndex: number) => void; onFeatureDragLeave: (e: React.DragEvent) => void; onFeatureDropOnMilestone: () => void; // Feature suggestion props featureSuggestions?: FeatureSuggestion[]; isGeneratingFeatureSuggestions?: boolean; onGenerateFeatureSuggestions?: () => void; onUpdateFeatureSuggestionDraft?: (milestoneId: string, draftId: string, patch: SuggestionDraftPatch) => void; onAcceptFeatureSuggestion?: (milestoneId: string, draftId: string) => void; onAcceptAllFeatureSuggestions?: () => void; onClearFeatureSuggestions?: () => void; }) { const isEditingMilestone = milestoneEdit?.milestoneId === milestone.id; const handleMilestoneTitleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); if (milestoneEdit) { onSaveMilestoneEdit({ title: milestoneEdit.value }); } } else if (e.key === "Escape") { onCancelMilestoneEdit(); } }; const handleMilestoneDescKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Escape") { onCancelMilestoneEdit(); } }; // Build class names for drag states const milestoneClasses = [ "roadmaps-view__milestone", isMilestoneDragging ? "roadmaps-view__milestone--dragging" : "", isMilestoneDropTarget ? "roadmaps-view__milestone--drop-target" : "", isMilestoneDropTarget && milestoneDropPosition === "before" ? "roadmaps-view__milestone--drop-before" : "", isMilestoneDropTarget && milestoneDropPosition === "after" ? "roadmaps-view__milestone--drop-after" : "", ].filter(Boolean).join(" "); // Build class names for feature list drop state const featureListClasses = [ "roadmaps-view__feature-list", isFeatureDropTarget ? "roadmaps-view__feature-list--drop-target" : "", ].filter(Boolean).join(" "); return (
{ if (!isEditingMilestone) { onMilestoneDragStart(milestone.id); e.dataTransfer.setData("text/plain", `milestone:${milestone.id}`); e.dataTransfer.effectAllowed = "move"; } }} onDragEnd={onMilestoneDragEnd} onDragOver={(e) => { // Only prevent default for milestone drops, not feature drops if (e.dataTransfer.types.includes("text/plain")) { const data = e.dataTransfer.types.includes("text/plain"); if (data) { // This is a milestone drag e.preventDefault(); e.dataTransfer.dropEffect = "move"; onMilestoneDragOver(milestone.id); } } }} onDrop={(e) => { e.preventDefault(); // Check if this is a feature drop or milestone drop const data = e.dataTransfer.getData("text/plain"); if (data?.startsWith("feature:")) { // Feature drop - handled by child element } else { onMilestoneDrop(milestone.id); } }} onDragLeave={onMilestoneDragLeave} data-testid={`milestone-card-${milestone.id}`} >
{isEditingMilestone ? (
{ onMilestoneEditFieldChange("title"); onMilestoneEditChange(e.target.value); }} onKeyDown={handleMilestoneTitleKeyDown} placeholder="Milestone title" autoFocus data-testid={`milestone-title-input-${milestone.id}`} />