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:
9
.changeset/mission-view-sidebar-ux.md
Normal file
9
.changeset/mission-view-sidebar-ux.md
Normal 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 220–560px, 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.
|
||||
@@ -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
|
||||
================================================================ */
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user