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.
|
// This replaces ResearchCanvas + LoopsCanvas after Slice 9's cutover.
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
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 {
|
import {
|
||||||
deleteMission,
|
deleteMission,
|
||||||
@@ -120,6 +130,21 @@ export function MissionCanvas({
|
|||||||
const [deleteBusy, setDeleteBusy] = useState(false);
|
const [deleteBusy, setDeleteBusy] = useState(false);
|
||||||
const [runs, setRuns] = useState<MissionRunSummary[]>([]);
|
const [runs, setRuns] = useState<MissionRunSummary[]>([]);
|
||||||
const [lastLoadedAt, setLastLoadedAt] = useState<Date | null>(null);
|
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 () => {
|
const load = useCallback(async () => {
|
||||||
if (!selectedId) {
|
if (!selectedId) {
|
||||||
@@ -495,8 +520,26 @@ export function MissionCanvas({
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 style={{ margin: 0, fontSize: 20, color: "#f3f3f5" }}>{mission.title}</h1>
|
<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 && (
|
{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
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: 4,
|
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",
|
"overview",
|
||||||
@@ -582,6 +634,8 @@ export function MissionCanvas({
|
|||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 6,
|
gap: 6,
|
||||||
|
flex: "none",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t}
|
{t}
|
||||||
@@ -641,15 +695,26 @@ export function MissionCanvas({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: 4,
|
gap: 4,
|
||||||
|
minWidth: 0,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
minWidth: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
width: 7,
|
width: 7,
|
||||||
height: 7,
|
height: 7,
|
||||||
borderRadius: "50%",
|
borderRadius: "50%",
|
||||||
background: PHASE_STATUS_COLOR[p.status],
|
background: PHASE_STATUS_COLOR[p.status],
|
||||||
|
flex: "none",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -686,7 +751,14 @@ export function MissionCanvas({
|
|||||||
<PhaseSummaryCard missionId={mission.id} phaseId={p.id} />
|
<PhaseSummaryCard missionId={mission.id} phaseId={p.id} />
|
||||||
)}
|
)}
|
||||||
{mission.status === "running" || mission.status === "completed" ? (
|
{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" && (
|
{p.status === "failed" && mission.status === "running" && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user