missions: collapsible header + scrollable tabs + wrap phase controls
- Mission header title/description now collapsible via chevron next to the title. Persisted in localStorage so it stays hidden across mission switches once the operator has read it — clears more room for phases/tasks/team panels below. - Tabs row: overflow-x auto + per-tab flex:none + whiteSpace:nowrap so 8+ tabs scroll horizontally instead of wrapping and cutting off. - Phase card action row (retry/security/benchmark buttons): flexWrap wrap so long button rows stack cleanly instead of overflowing. - Phase card status row wraps too, and the card itself gets overflow:hidden + minWidth:0 so long content stays inside the border and the parent tab-panel scroll handles vertical growth.
This commit is contained in:
@@ -9,7 +9,17 @@
|
||||
// This replaces ResearchCanvas + LoopsCanvas after Slice 9's cutover.
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { FileText, Pencil, Play, Plus, RefreshCw, Sparkles, Trash2 } from "lucide-react";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
FileText,
|
||||
Pencil,
|
||||
Play,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Sparkles,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
|
||||
import {
|
||||
deleteMission,
|
||||
@@ -120,6 +130,21 @@ export function MissionCanvas({
|
||||
const [deleteBusy, setDeleteBusy] = useState(false);
|
||||
const [runs, setRuns] = useState<MissionRunSummary[]>([]);
|
||||
const [lastLoadedAt, setLastLoadedAt] = useState<Date | null>(null);
|
||||
// Persist the collapsed state across mission switches so the operator
|
||||
// can keep the header hidden once they've read it.
|
||||
const [headerCollapsed, setHeaderCollapsed] = useState<boolean>(() => {
|
||||
if (typeof window === "undefined") return false;
|
||||
return window.localStorage.getItem("cm.mission.headerCollapsed") === "1";
|
||||
});
|
||||
const toggleHeader = useCallback(() => {
|
||||
setHeaderCollapsed((v) => {
|
||||
const next = !v;
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.setItem("cm.mission.headerCollapsed", next ? "1" : "0");
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!selectedId) {
|
||||
@@ -495,8 +520,26 @@ export function MissionCanvas({
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<h1 style={{ margin: 0, fontSize: 20, color: "#f3f3f5" }}>{mission.title}</h1>
|
||||
{mission.description && (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<h1 style={{ margin: 0, fontSize: 20, color: "#f3f3f5", flex: 1, minWidth: 0 }}>
|
||||
{mission.title}
|
||||
</h1>
|
||||
{mission.description && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleHeader}
|
||||
title={headerCollapsed ? "Expand description" : "Collapse description"}
|
||||
aria-label={headerCollapsed ? "Expand description" : "Collapse description"}
|
||||
style={{
|
||||
...iconBtn,
|
||||
flex: "none",
|
||||
}}
|
||||
>
|
||||
{headerCollapsed ? <ChevronDown size={13} /> : <ChevronUp size={13} />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{mission.description && !headerCollapsed && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 4,
|
||||
@@ -539,7 +582,16 @@ export function MissionCanvas({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div style={{ display: "flex", gap: 4, marginTop: 4 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 4,
|
||||
marginTop: 4,
|
||||
overflowX: "auto",
|
||||
paddingBottom: 2,
|
||||
scrollbarWidth: "thin",
|
||||
}}
|
||||
>
|
||||
{(
|
||||
[
|
||||
"overview",
|
||||
@@ -582,6 +634,8 @@ export function MissionCanvas({
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
flex: "none",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
@@ -641,15 +695,26 @@ export function MissionCanvas({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
minWidth: 0,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
flexWrap: "wrap",
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: "50%",
|
||||
background: PHASE_STATUS_COLOR[p.status],
|
||||
flex: "none",
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
@@ -686,7 +751,14 @@ export function MissionCanvas({
|
||||
<PhaseSummaryCard missionId={mission.id} phaseId={p.id} />
|
||||
)}
|
||||
{mission.status === "running" || mission.status === "completed" ? (
|
||||
<div style={{ display: "flex", gap: 6, marginTop: 6 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 6,
|
||||
marginTop: 6,
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
{p.status === "failed" && mission.status === "running" && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user