fix(dashboard): resizable mission sidebar and cleaner card header

- Make the mission split sidebar drag-resizable (220–560px, persisted) so
  long mission titles aren't trapped behind a fixed-width column.
- Move card tags (autopilot/health/status) to a row below the title and
  drop the overflowing "Active: …" line.
- Collapse mission creation to a single AI-driven entry point: rename
  Sparkles to "Create New Mission" and remove the manual "+" button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-05 23:42:24 -07:00
parent c7da858ad9
commit f04ade0034
3 changed files with 149 additions and 36 deletions

View File

@@ -0,0 +1,9 @@
---
"@runfusion/fusion": patch
---
Mission view sidebar and list-card UX fixes.
- **Resizable mission sidebar**: the desktop split sidebar is now drag-resizable via a vertical handle (also keyboard-accessible with arrow keys). Width persists to `localStorage` (`fusion:mission-sidebar-width`), bounded 220560px, default 300px. Previously fixed at ~284px with `flex-shrink: 0`.
- **Mission card title no longer truncates aggressively**: tags (autopilot zap, health badge, status pill) moved to a second row below the title so the title can use the full card width. Removed the redundant overflow-prone `Active: …` line that was sometimes spilling outside the card.
- **Single AI-driven create flow**: removed the manual `+ New Mission` button from the sidebar header and bottom footer. The Sparkles button (now labeled "Create New Mission") is the only entry point — the dead `handleCreateMission` callback and unused `activeSliceLabel` were removed too.

View File

@@ -164,7 +164,8 @@
}
.mission-manager__sidebar {
width: calc((var(--space-xl) * 11) + var(--space-md) + var(--space-xs));
width: 300px;
min-width: 0;
flex-shrink: 0;
display: flex;
flex-direction: column;
@@ -173,6 +174,36 @@
overflow: hidden;
}
.mission-manager__sidebar-resize-handle {
position: relative;
width: var(--space-sm);
flex-shrink: 0;
cursor: col-resize;
background: transparent;
touch-action: none;
transition: background var(--transition-fast);
}
.mission-manager__sidebar-resize-handle::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: var(--space-xs);
transform: translateX(-50%);
}
.mission-manager__sidebar-resize-handle:hover::before,
.mission-manager__sidebar-resize-handle:active::before {
background: color-mix(in srgb, var(--todo) 30%, transparent);
}
.mission-manager__sidebar-resize-handle:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
/* Desktop split-only sidebar header layout for mission quick actions. */
.mission-manager__sidebar-header {
display: flex;
@@ -662,6 +693,15 @@
display: flex;
align-items: center;
gap: var(--space-sm);
min-width: 0;
}
.mission-list__item-tags {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-xs);
margin-top: var(--space-xs);
}
.mission-list__item-icon {
@@ -670,6 +710,8 @@
}
.mission-list__item-title {
flex: 1;
min-width: 0;
font-size: 14px;
font-weight: 500;
color: var(--text);
@@ -782,12 +824,6 @@
color: var(--text-dim);
}
.mission-list__item-active-slice {
margin: var(--space-xs) 0 0;
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
color: var(--text-muted);
}
/* ================================================================
Mission Detail View
================================================================ */

View File

@@ -97,6 +97,11 @@ import {
} from "../api";
import type { AutopilotState } from "./mission-types";
const MISSION_SIDEBAR_DEFAULT_WIDTH = 300;
const MISSION_SIDEBAR_MIN_WIDTH = 220;
const MISSION_SIDEBAR_MAX_WIDTH = 560;
const MISSION_SIDEBAR_STORAGE_KEY = "fusion:mission-sidebar-width";
interface MissionManagerProps {
isOpen: boolean;
isInline?: boolean;
@@ -439,6 +444,72 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
const [loading, setLoading] = useState(true);
const [detailLoading, setDetailLoading] = useState(false);
const isMobile = useViewportMode() === "mobile";
const [sidebarWidth, setSidebarWidth] = useState<number>(() => {
if (typeof window === "undefined") return MISSION_SIDEBAR_DEFAULT_WIDTH;
const stored = window.localStorage.getItem(MISSION_SIDEBAR_STORAGE_KEY);
const parsed = stored ? Number(stored) : NaN;
if (!Number.isFinite(parsed)) return MISSION_SIDEBAR_DEFAULT_WIDTH;
return Math.max(MISSION_SIDEBAR_MIN_WIDTH, Math.min(MISSION_SIDEBAR_MAX_WIDTH, parsed));
});
const persistSidebarWidth = useCallback((width: number) => {
try {
window.localStorage.setItem(MISSION_SIDEBAR_STORAGE_KEY, String(width));
} catch {
// Ignore storage errors.
}
}, []);
const handleSidebarResizeStart = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
if (isMobile) return;
event.preventDefault();
event.stopPropagation();
const handle = event.currentTarget;
if (typeof handle.setPointerCapture === "function") {
handle.setPointerCapture(event.pointerId);
}
const startX = event.clientX;
const startWidth = sidebarWidth;
let latestWidth = startWidth;
document.body.style.userSelect = "none";
const onPointerMove = (moveEvent: PointerEvent) => {
const deltaX = moveEvent.clientX - startX;
const nextWidth = Math.max(
MISSION_SIDEBAR_MIN_WIDTH,
Math.min(MISSION_SIDEBAR_MAX_WIDTH, startWidth + deltaX),
);
latestWidth = nextWidth;
setSidebarWidth(nextWidth);
};
const onPointerUp = (upEvent: PointerEvent) => {
if (typeof handle.releasePointerCapture === "function") {
handle.releasePointerCapture(upEvent.pointerId);
}
document.body.style.userSelect = "";
document.removeEventListener("pointermove", onPointerMove);
document.removeEventListener("pointerup", onPointerUp);
persistSidebarWidth(latestWidth);
};
document.addEventListener("pointermove", onPointerMove);
document.addEventListener("pointerup", onPointerUp);
}, [isMobile, persistSidebarWidth, sidebarWidth]);
const handleSidebarResizeKeyDown = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
if (isMobile) return;
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
event.preventDefault();
const step = event.shiftKey ? 50 : 10;
const delta = event.key === "ArrowLeft" ? -step : step;
const nextWidth = Math.max(
MISSION_SIDEBAR_MIN_WIDTH,
Math.min(MISSION_SIDEBAR_MAX_WIDTH, sidebarWidth + delta),
);
setSidebarWidth(nextWidth);
persistSidebarWidth(nextWidth);
}, [isMobile, persistSidebarWidth, sidebarWidth]);
// Form states
const [isCreatingMission, setIsCreatingMission] = useState(false);
@@ -1116,12 +1187,6 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
]);
// Mission handlers
const handleCreateMission = useCallback(() => {
setIsCreatingMission(true);
setEditingMissionId(null);
setMissionForm(EMPTY_MISSION_FORM);
}, []);
const handleEditMission = useCallback((mission: Mission) => {
setEditingMissionId(mission.id);
setIsCreatingMission(false);
@@ -3394,10 +3459,6 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
const progressPercent = health?.estimatedCompletionPercent ?? summary?.progressPercent ?? 0;
const showSummaryBlock = hasContent || totalTasks > 0 || tasksFailed > 0 || Boolean(health?.lastActivityAt);
const activeSliceLabel = m.status === "active" && (health?.currentMilestoneId || health?.currentSliceId)
? "Current milestone/slice in progress"
: null;
return (
<div
key={m.id}
@@ -3408,6 +3469,8 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
<div className="mission-list__item-header">
<Target size={16} className="mission-list__item-icon" />
<span className="mission-list__item-title">{m.title}</span>
</div>
<div className="mission-list__item-tags">
{mission.autopilotEnabled && (
<span title="Autopilot enabled"><Zap size={12} className="mission-list__item-autopilot-icon" /></span>
)}
@@ -3429,9 +3492,6 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
{m.description && (
<p className="mission-list__item-description">{m.description}</p>
)}
{activeSliceLabel && (
<p className="mission-list__item-active-slice">Active: {activeSliceLabel}</p>
)}
{showSummaryBlock && (
<div className="mission-list__item-summary">
{hasContent && (
@@ -3641,11 +3701,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
<div className="mission-list__footer-actions">
<button className="mission-add-btn" onClick={() => setShowInterviewModal(true)}>
<Sparkles size={16} />
Plan with AI
</button>
<button className="mission-add-btn" onClick={handleCreateMission}>
<Plus size={16} />
New Mission
Create New Mission
</button>
</div>
)}
@@ -3790,26 +3846,23 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
</div>
) : (
<div className="mission-manager__split">
<aside className="mission-manager__sidebar" data-testid="mission-sidebar" aria-label="Mission list">
<aside
className="mission-manager__sidebar"
data-testid="mission-sidebar"
aria-label="Mission list"
style={isMobile ? undefined : { width: `${sidebarWidth}px` }}
>
<div className="mission-manager__sidebar-header">
<span className="mission-manager__sidebar-title">Missions</span>
<div className="mission-manager__sidebar-actions">
<button
className="mission-add-btn mission-add-btn--sm"
onClick={() => setShowInterviewModal(true)}
title="Plan with AI"
aria-label="Plan with AI"
title="Create New Mission"
aria-label="Create New Mission"
>
<Sparkles size={14} />
</button>
<button
className="mission-add-btn mission-add-btn--sm"
onClick={handleCreateMission}
title="New Mission"
aria-label="New Mission"
>
<Plus size={14} />
</button>
</div>
</div>
<div className="mission-manager__sidebar-list">
@@ -3824,6 +3877,21 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
</div>
</aside>
{!isMobile && (
<div
className="mission-manager__sidebar-resize-handle"
role="separator"
aria-orientation="vertical"
aria-valuemin={MISSION_SIDEBAR_MIN_WIDTH}
aria-valuemax={MISSION_SIDEBAR_MAX_WIDTH}
aria-valuenow={sidebarWidth}
aria-label="Resize mission sidebar"
tabIndex={0}
onPointerDown={handleSidebarResizeStart}
onKeyDown={handleSidebarResizeKeyDown}
/>
)}
<div className="mission-manager__detail-pane">
{detailLoading ? (
<div className="mission-manager__loading">