import React, { useCallback, useEffect, useState } from 'react'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible'; import { ChevronDown, ChevronsUpDown, Map, FileText, Loader2, Users, FileCode, Clock } from 'lucide-react'; import { api } from '@/api'; import { APP_VERSION } from '@/lib/version'; /* ─── Priority lanes ────────────────────────────────────────────────────────── */ const PRIORITY_LANES = [ { key: 'critical', emoji: '🔴', label: 'CRITICAL', borderColor: 'border-t-red-500', textColor: 'text-red-500', badgeClass: 'bg-red-500/15 text-red-500 border-red-500/20' }, { key: 'high', emoji: '🟠', label: 'HIGH', borderColor: 'border-t-orange-500', textColor: 'text-orange-500', badgeClass: 'bg-orange-500/15 text-orange-500 border-orange-500/20' }, { key: 'medium', emoji: '🟡', label: 'MEDIUM', borderColor: 'border-t-yellow-500', textColor: 'text-yellow-500', badgeClass: 'bg-yellow-500/15 text-yellow-500 border-yellow-500/20' }, { key: 'low', emoji: '🔵', label: 'LOW', borderColor: 'border-t-blue-500', textColor: 'text-blue-500', badgeClass: 'bg-blue-500/15 text-blue-500 border-blue-500/20' }, { key: 'niceToHave', emoji: '💭', label: 'NICE TO HAVE', borderColor: 'border-t-border', textColor: 'text-muted-foreground', badgeClass: 'bg-muted/50 text-muted-foreground border-border/50' }, ]; // Normalise any priority string to a lane key. function laneForPriority(priority) { const normalised = String(priority || '').toLowerCase().replace(/[\s_-]+/g, ''); const map = { critical: 'critical', high: 'high', medium: 'medium', low: 'low', nicetohave: 'niceToHave', }; return map[normalised] ?? 'low'; } /* ─── Roadmap item card ─────────────────────────────────────────────────────── */ function RoadmapItemCard({ item, defaultOpen }) { const lane = PRIORITY_LANES.find(l => l.key === laneForPriority(item.priority)) ?? PRIORITY_LANES[3]; const [open, setOpen] = useState(defaultOpen); // Sync when parent toggles all cards via forceKey remount (no extra effect needed) return (
{/* Trigger — always visible header */} {/* Meta row — always visible */} {(item.added || item.addedBy || item.effort) && (
{item.added && ( {item.added} )} {item.addedBy && ( <> {item.addedBy} )} {item.effort && ( <> {item.effort} )}
)} {/* Expandable detail */}
{item.description && (

Description

{item.description}

)} {item.rationale && (

Rationale

{item.rationale}

)} {item.implementationNotes && (

Implementation Notes

{item.implementationNotes}
)}
); } /* ─── Priority lane column ──────────────────────────────────────────────────── */ function PriorityLane({ lane, items, defaultOpenCards, forceKey }) { if (items.length === 0) return null; return (

{lane.label}

{items.length}
{items.map(item => ( ))}
); } /* ─── Dev log entry ─────────────────────────────────────────────────────────── */ function DevLogEntry({ entry }) { const [open, setOpen] = useState(false); return (
{entry.agents?.length > 0 && (

Agents

{entry.agents.map((agent, idx) => ( {agent.status === 'COMPLETED' ? '✅' : agent.status === 'IN PROGRESS' ? '⏳' : '❓'}{' '} {agent.name}{agent.time ? ` · ${agent.time}` : ''} ))}
)} {entry.filesModified?.length > 0 && (

Files Modified

{entry.filesModified.map((file, idx) => ( {file} ))}
)} {entry.workCompleted?.length > 0 && (

Work Completed

    {entry.workCompleted.map((work, idx) => (
  • {work}
  • ))}
)}
); } /* ─── Main page ─────────────────────────────────────────────────────────────── */ export default function RoadmapPage() { const [roadmapData, setRoadmapData] = useState(null); const [devLogData, setDevLogData] = useState(null); const [roadmapLoading, setRoadmapLoading] = useState(true); const [devLogLoading, setDevLogLoading] = useState(false); const [roadmapError, setRoadmapError] = useState(null); const [devLogError, setDevLogError] = useState(null); // Expand/collapse all — forceKey causes cards to remount with the new default const [allExpanded, setAllExpanded] = useState(true); const [forceKey, setForceKey] = useState(0); const handleExpandToggle = () => { setAllExpanded(prev => !prev); setForceKey(prev => prev + 1); }; const getIsDesktop = () => ( typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(min-width: 1024px)').matches ); const [isDesktop, setIsDesktop] = useState(getIsDesktop); useEffect(() => { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return undefined; const mq = window.matchMedia('(min-width: 1024px)'); const handler = (e) => setIsDesktop(e.matches); setIsDesktop(mq.matches); if (typeof mq.addEventListener === 'function') { mq.addEventListener('change', handler); return () => mq.removeEventListener('change', handler); } mq.addListener(handler); return () => mq.removeListener(handler); }, []); // Fetch roadmap on mount useEffect(() => { let cancelled = false; setRoadmapLoading(true); api.roadmap() .then(data => { if (!cancelled) setRoadmapData(data); }) .catch(err => { if (!cancelled) setRoadmapError(err.message || 'Failed to load roadmap'); }) .finally(() => { if (!cancelled) setRoadmapLoading(false); }); return () => { cancelled = true; }; }, []); // Lazy-load dev log when the Activity tab is first opened const fetchDevLog = useCallback(() => { if (devLogData || devLogLoading) return; let cancelled = false; setDevLogLoading(true); api.devLog() .then(data => { if (!cancelled) setDevLogData(data); }) .catch(err => { if (!cancelled) setDevLogError(err.message || 'Failed to load activity log'); }) .finally(() => { if (!cancelled) setDevLogLoading(false); }); }, [devLogData, devLogLoading]); const version = roadmapData?.version || APP_VERSION; const items = roadmapData?.items || []; const grouped = PRIORITY_LANES.map(lane => ({ ...lane, items: items.filter(item => laneForPriority(item.priority) === lane.key), })); const defaultOpenCards = isDesktop && allExpanded; const laneProps = { defaultOpenCards, forceKey }; return (
{/* Header */}

Roadmap

Current and upcoming features by priority

v{version}
{/* Tabs */} { if (v === 'activity') fetchDevLog(); }} className="min-w-0"> Roadmap Activity Log {/* ── Roadmap tab ── */} {roadmapLoading ? (
Loading roadmap…
) : roadmapError ? (

Failed to load roadmap

{roadmapError}

) : items.length === 0 ? (
No roadmap items found.
) : ( <>
{/* Wide desktop: full five-lane view */}
{grouped.map(lane => )}
{/* Desktop: balanced three-column view for admin shell widths */}
{grouped.filter(l => l.key === 'critical' || l.key === 'high').map(lane => )}
{grouped.filter(l => l.key === 'medium' || l.key === 'low').map(lane => )}
{grouped.filter(l => l.key === 'niceToHave').map(lane => )}
{/* Tablet: 2 columns */}
{grouped.filter(l => l.key === 'critical' || l.key === 'high').map(lane => )}
{grouped.filter(l => l.key === 'medium' || l.key === 'low' || l.key === 'niceToHave').map(lane => )}
{/* Mobile: single column */}
{grouped.map(lane => )}
)}
{/* ── Activity log tab ── */} {devLogLoading ? (
Loading activity log…
) : devLogError ? (

Failed to load activity log

{devLogError}

) : devLogData && devLogData.entries?.length === 0 ? (
No activity log entries found.
) : devLogData ? (
{devLogData.entries.map((entry, idx) => ( ))}
) : null}
); }