feat(FN-777): redesign Mission Manager interaction shell and update styles

- Redesign MissionManager component with improved interaction patterns and shell layout
- Refactor Board, Column, and QuickEntryBox components for consistency
- Add comprehensive CSS styles (742 lines) for mission manager and related components
- Add MissionManager test suite (324 lines) covering new interaction behavior
- Update Board, Column tests; remove obsolete ListView and QuickEntryBox tests
- Clean up unused code in App.tsx and ListView.tsx
- Update Missions section in README and dashboard README to reflect actual UI behavior
This commit is contained in:
gsxdsm
2026-04-03 08:37:33 -07:00
parent 6603844857
commit d67437a5a2
4 changed files with 1112 additions and 8 deletions

View File

@@ -81,7 +81,7 @@ fn mission delete M-001 [--force] # Delet
fn mission activate-slice SL-001 # Manually activate slice
```
**Dashboard:** Press `Cmd/Ctrl+Shift+M` to open the Mission List, then create missions, add milestones, slices, and features. Link features to tasks for automatic progress tracking.
**Dashboard:** Click the Target icon in the dashboard header to open the Mission Manager, then create missions, add milestones, slices, and features. Link features to tasks for automatic progress tracking. On mobile devices, the Mission Manager opens as a full-screen overlay with touch-friendly controls and stacked hierarchy cards.
## Quick Start

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback, useRef } from "react";
import {
X,
Plus,
@@ -597,29 +597,67 @@ export function MissionManager({ isOpen, onClose, addToast, projectId, onSelectT
}
}, [handleSaveFeature]);
// Ref for focus management
const modalRef = useRef<HTMLDivElement>(null);
// Escape key handling
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onClose();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="modal-overlay open" onClick={(e) => e.target === e.currentTarget && onClose()}>
<div className="modal-content mission-manager-modal">
<div
className="modal-overlay open"
onClick={(e) => e.target === e.currentTarget && onClose()}
data-testid="mission-manager-overlay"
>
<div
ref={modalRef}
className="mission-manager-modal"
role="dialog"
aria-modal="true"
aria-label="Mission Manager"
data-testid="mission-manager-dialog"
>
<div className="modal-header">
<div className="modal-title-row">
{selectedMission ? (
<button className="icon-btn" onClick={handleBackToList} title="Back to missions">
<button
className="icon-btn"
onClick={handleBackToList}
title="Back to missions"
aria-label="Back to missions list"
data-testid="mission-back-btn"
>
<ChevronLeft size={20} />
</button>
) : null}
<h2>
<Target size={20} />
Missions
{selectedMission ? selectedMission.title : "Missions"}
</h2>
</div>
<button className="icon-btn" onClick={onClose} title="Close">
<button
className="icon-btn"
onClick={onClose}
title="Close"
aria-label="Close Mission Manager"
data-testid="mission-close-btn"
>
<X size={20} />
</button>
</div>
<div className="modal-body mission-manager-body">
<div className="mission-manager-body">
{loading ? (
<div className="loading-state">
<Loader2 size={24} className="spinner" />

View File

@@ -0,0 +1,324 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { MissionManager } from "../MissionManager";
// Mock data
const mockMissions = [
{
id: "M-001",
title: "Build Auth System",
description: "Complete authentication flow",
status: "planning",
autoAdvance: false,
milestones: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
},
{
id: "M-002",
title: "API Redesign",
description: "Redesign the REST API",
status: "active",
autoAdvance: true,
milestones: [],
createdAt: "2026-01-02T00:00:00.000Z",
updatedAt: "2026-01-02T00:00:00.000Z",
},
];
const mockMissionDetail = {
id: "M-001",
title: "Build Auth System",
description: "Complete authentication flow",
status: "planning",
autoAdvance: false,
milestones: [
{
id: "MS-001",
title: "Database Schema",
description: "Set up auth tables",
status: "planning",
dependencies: [] as string[],
slices: [
{
id: "SL-001",
title: "User Tables",
description: "Create user tables",
status: "pending",
features: [
{
id: "F-001",
title: "User model",
description: "Create user model",
acceptanceCriteria: "Model exists with required fields",
status: "defined",
taskId: null,
sliceId: "SL-001",
missionId: "M-001",
},
],
milestoneId: "MS-001",
missionId: "M-001",
},
],
missionId: "M-001",
},
],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
/** Create a mock Response that matches the real api() function's expectations (text + content-type headers) */
function mockApiResponse(data: unknown) {
return {
ok: true,
headers: new Headers({ "content-type": "application/json" }),
text: () => Promise.resolve(JSON.stringify(data)),
};
}
/** Fetch mock that returns missions list for /missions and detail for /missions/M-001 */
function createFetchMock() {
return vi.fn().mockImplementation((_url: string) => {
return Promise.resolve(mockApiResponse(mockMissions));
});
}
/** Fetch mock for navigating into a mission detail */
function createDetailFetchMock() {
let callCount = 0;
return vi.fn().mockImplementation((_url: string) => {
callCount++;
// First call: list, subsequent: detail
if (callCount === 1) {
return Promise.resolve(mockApiResponse(mockMissions));
}
return Promise.resolve(mockApiResponse(mockMissionDetail));
});
}
describe("MissionManager", () => {
let originalFetch: typeof globalThis.fetch;
beforeEach(() => {
originalFetch = globalThis.fetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
vi.restoreAllMocks();
});
it("renders nothing when isOpen is false", () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={false} onClose={vi.fn()} addToast={vi.fn()} />);
expect(screen.queryByTestId("mission-manager-dialog")).toBeNull();
});
it("renders the dialog with accessible attributes when open", async () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
const dialog = screen.getByTestId("mission-manager-dialog");
expect(dialog).toBeDefined();
expect(dialog.getAttribute("role")).toBe("dialog");
expect(dialog.getAttribute("aria-modal")).toBe("true");
expect(dialog.getAttribute("aria-label")).toBe("Mission Manager");
});
});
it("renders the modal overlay with open class", async () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
const overlay = screen.getByTestId("mission-manager-overlay");
expect(overlay).toBeDefined();
expect(overlay.className).toContain("open");
});
});
it("shows the Missions title in list view", async () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Missions")).toBeDefined();
});
});
it("renders mission items in the list", async () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
expect(screen.getByText("API Redesign")).toBeDefined();
});
});
it("shows mission status badges", async () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("planning")).toBeDefined();
expect(screen.getByText("active")).toBeDefined();
});
});
it("shows empty state when no missions exist", async () => {
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse([]));
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("No missions yet. Create one to start planning.")).toBeDefined();
});
});
it("calls onClose when close button is clicked", async () => {
globalThis.fetch = createFetchMock();
const onClose = vi.fn();
render(<MissionManager isOpen={true} onClose={onClose} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByTestId("mission-close-btn")).toBeDefined();
});
fireEvent.click(screen.getByTestId("mission-close-btn"));
expect(onClose).toHaveBeenCalledOnce();
});
it("calls onClose when overlay background is clicked", async () => {
globalThis.fetch = createFetchMock();
const onClose = vi.fn();
render(<MissionManager isOpen={true} onClose={onClose} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByTestId("mission-manager-overlay")).toBeDefined();
});
const overlay = screen.getByTestId("mission-manager-overlay");
fireEvent.click(overlay);
expect(onClose).toHaveBeenCalledOnce();
});
it("navigates to detail view when a mission is clicked", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Wait for list to load
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
// Click on a mission to open detail
fireEvent.click(screen.getByText("Build Auth System"));
// Wait for detail view to render
await waitFor(() => {
// Back button should appear in detail view
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
// Milestone should be visible (auto-expanded)
expect(screen.getByText("Database Schema")).toBeDefined();
});
});
it("navigates back to list view when back button is clicked", async () => {
// call 1: list load, call 2: detail load, call 3: re-list load after back
let callCount = 0;
globalThis.fetch = vi.fn().mockImplementation(() => {
callCount++;
if (callCount === 2) {
return Promise.resolve(mockApiResponse(mockMissionDetail));
}
return Promise.resolve(mockApiResponse(mockMissions));
});
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
});
// Click back
fireEvent.click(screen.getByTestId("mission-back-btn"));
// Should return to list view
await waitFor(() => {
expect(screen.getByText("Missions")).toBeDefined();
expect(screen.getByText("API Redesign")).toBeDefined();
});
});
it("calls onClose on Escape key press", async () => {
globalThis.fetch = createFetchMock();
const onClose = vi.fn();
render(<MissionManager isOpen={true} onClose={onClose} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByTestId("mission-manager-dialog")).toBeDefined();
});
fireEvent.keyDown(document, { key: "Escape" });
expect(onClose).toHaveBeenCalled();
});
it("shows close button with accessible label", async () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByLabelText("Close Mission Manager")).toBeDefined();
});
});
it("shows back button with accessible label in detail view", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByLabelText("Back to missions list")).toBeDefined();
});
});
it("shows New Mission button in list view", async () => {
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse([]));
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("New Mission")).toBeDefined();
});
});
it("shows milestone hierarchy in detail view", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
// Milestone is auto-expanded, slice and feature visible
expect(screen.getByText("Database Schema")).toBeDefined();
expect(screen.getByText("User Tables")).toBeDefined();
expect(screen.getByText("User model")).toBeDefined();
});
});
});

View File

@@ -14310,3 +14310,745 @@ html .column.drag-over * {
opacity: 0.5;
cursor: not-allowed;
}
/* === Mission Manager Modal === */
.mission-manager-modal {
width: min(860px, 94vw);
max-height: min(88vh, 800px);
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
}
.mission-manager-modal .modal-header {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-md) var(--space-lg);
border-bottom: 1px solid var(--border);
background: rgba(0, 0, 0, 0.1);
}
.mission-manager-modal .modal-title-row {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.mission-manager-modal .modal-title-row h2 {
display: flex;
align-items: center;
gap: var(--space-sm);
font-size: 16px;
font-weight: 600;
margin: 0;
color: var(--text);
}
.mission-manager-modal .modal-title-row h2 svg {
color: var(--todo);
}
/* Shared icon-btn inside Mission Manager */
.mission-manager-modal .icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 6px;
border-radius: var(--radius-md);
transition:
color var(--transition-fast),
background var(--transition-fast);
}
.mission-manager-modal .icon-btn:hover {
color: var(--text);
background: var(--border);
}
.mission-manager-modal .icon-btn:focus-visible {
box-shadow: var(--focus-ring-strong);
outline: none;
}
.mission-manager-modal .icon-btn.small {
padding: 4px;
}
.mission-manager-modal .icon-btn.danger:hover {
color: var(--color-error);
background: rgba(248, 81, 73, 0.1);
}
.mission-manager-modal .icon-btn.success:hover {
color: var(--color-success);
background: rgba(63, 185, 80, 0.1);
}
/* Body: scrollable area */
.mission-manager-body {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: var(--space-lg);
-webkit-overflow-scrolling: touch;
}
.mission-manager-body::-webkit-scrollbar {
width: 6px;
}
.mission-manager-body::-webkit-scrollbar-track {
background: transparent;
}
.mission-manager-body::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: var(--radius-sm);
}
/* Loading state */
.mission-manager-modal .loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-md);
padding: var(--space-2xl);
color: var(--text-muted);
font-size: 14px;
}
/* Empty state */
.mission-manager-modal .empty-state {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-sm);
padding: var(--space-xl);
color: var(--text-muted);
font-size: 13px;
text-align: center;
}
.mission-manager-modal .empty-state svg {
opacity: 0.5;
}
/* Status badge */
.mission-manager-modal .status-badge {
display: inline-flex;
align-items: center;
font-size: 11px;
font-weight: 600;
text-transform: capitalize;
padding: 2px 8px;
border-radius: var(--radius-pill);
white-space: nowrap;
}
.mission-manager-modal .status-badge.small {
font-size: 10px;
padding: 1px 6px;
}
/* Add item button */
.mission-manager-modal .add-item-btn {
display: inline-flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
background: none;
border: 1px dashed var(--border);
border-radius: var(--radius-md);
color: var(--text-muted);
font-size: 13px;
cursor: pointer;
width: 100%;
justify-content: center;
transition:
color var(--transition-fast),
border-color var(--transition-fast),
background var(--transition-fast);
}
.mission-manager-modal .add-item-btn:hover {
color: var(--text);
border-color: var(--text-muted);
background: var(--card);
}
/* Inline form */
.mission-manager-modal .inline-form {
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding: var(--space-md);
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
margin-bottom: var(--space-sm);
}
.mission-manager-modal .inline-form input[type="text"],
.mission-manager-modal .inline-form textarea,
.mission-manager-modal .inline-form select {
width: 100%;
padding: 8px 10px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text);
font-size: 13px;
font-family: inherit;
transition: border-color var(--transition-fast);
}
.mission-manager-modal .inline-form input[type="text"]:focus,
.mission-manager-modal .inline-form textarea:focus,
.mission-manager-modal .inline-form select:focus {
outline: none;
border-color: var(--todo);
box-shadow: var(--focus-ring);
}
.mission-manager-modal .inline-form textarea {
resize: vertical;
min-height: 48px;
}
.mission-manager-modal .inline-form .form-row {
display: flex;
align-items: center;
gap: var(--space-md);
flex-wrap: wrap;
}
.mission-manager-modal .inline-form .form-actions {
display: flex;
gap: var(--space-sm);
align-items: center;
}
.mission-manager-modal .inline-form .btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: var(--radius-md);
font-size: 13px;
cursor: pointer;
border: 1px solid var(--border);
transition:
background var(--transition-fast),
border-color var(--transition-fast);
}
.mission-manager-modal .inline-form .btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.mission-manager-modal .inline-form .btn-primary {
background: #238636;
border-color: #2ea043;
color: #fff;
}
.mission-manager-modal .inline-form .btn-primary:hover:not(:disabled) {
background: #2ea043;
}
.mission-manager-modal .inline-form .btn-ghost {
background: none;
color: var(--text-muted);
border-color: var(--border);
}
.mission-manager-modal .inline-form .btn-ghost:hover:not(:disabled) {
background: var(--card-hover);
color: var(--text);
}
.mission-manager-modal .inline-form .btn-danger {
background: var(--color-error-dark);
border-color: var(--color-error);
color: #fff;
}
.mission-manager-modal .inline-form .btn-danger:hover:not(:disabled) {
background: var(--color-error);
}
.mission-manager-modal .checkbox-label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--text-muted);
cursor: pointer;
}
/* Confirm dialog */
.mission-manager-modal .confirm-dialog {
display: flex;
flex-direction: column;
gap: var(--space-md);
padding: var(--space-lg);
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
margin-bottom: var(--space-md);
}
.mission-manager-modal .confirm-dialog p {
font-size: 14px;
color: var(--text);
margin: 0;
}
.mission-manager-modal .confirm-dialog .form-actions {
display: flex;
gap: var(--space-sm);
}
/* Task suggestions */
.mission-manager-modal .task-suggestions {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.mission-manager-modal .task-suggestions small {
color: var(--text-dim);
font-size: 11px;
}
.mission-manager-modal .task-suggestion-list {
display: flex;
flex-direction: column;
gap: 2px;
}
.mission-manager-modal .task-suggestion-btn {
text-align: left;
padding: 6px 10px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-muted);
font-size: 12px;
cursor: pointer;
transition: background var(--transition-fast);
}
.mission-manager-modal .task-suggestion-btn:hover {
background: var(--card-hover);
color: var(--text);
}
/* === Mission List View === */
.mission-list-view {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.mission-item {
display: flex;
align-items: center;
gap: var(--space-md);
padding: var(--space-md) var(--space-lg);
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
cursor: pointer;
transition:
background var(--transition-fast),
border-color var(--transition-fast);
}
.mission-item:hover {
background: var(--card-hover);
border-color: var(--text-dim);
}
.mission-item.selected {
border-color: var(--todo);
box-shadow: inset 0 0 0 1px var(--todo);
}
.mission-item-content {
flex: 1;
min-width: 0;
}
.mission-item-header {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.mission-item-title {
font-size: 14px;
font-weight: 500;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mission-item-description {
font-size: 12px;
color: var(--text-muted);
margin: var(--space-xs) 0 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mission-item-actions {
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
/* === Mission Detail View === */
.mission-detail-view {
display: flex;
flex-direction: column;
gap: var(--space-lg);
}
.mission-detail-header {
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding-bottom: var(--space-lg);
border-bottom: 1px solid var(--border);
}
.mission-detail-title {
display: flex;
align-items: center;
gap: var(--space-sm);
flex-wrap: wrap;
}
.mission-detail-title h3 {
font-size: 18px;
font-weight: 600;
color: var(--text);
margin: 0;
}
.mission-description {
font-size: 13px;
color: var(--text-muted);
line-height: 1.5;
margin: 0;
}
.mission-meta {
display: flex;
align-items: center;
gap: var(--space-md);
flex-wrap: wrap;
}
.meta-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--color-success);
background: rgba(63, 185, 80, 0.1);
padding: 2px 8px;
border-radius: var(--radius-pill);
}
.meta-info {
font-size: 12px;
color: var(--text-muted);
}
/* === Milestone List === */
.milestone-list {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.milestone-item {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
}
.milestone-header {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-md) var(--space-lg);
cursor: pointer;
transition: background var(--transition-fast);
}
.milestone-header:hover {
background: var(--card-hover);
}
.milestone-title {
flex: 1;
font-size: 14px;
font-weight: 500;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.milestone-item .expand-btn {
display: inline-flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 2px;
border-radius: var(--radius-sm);
flex-shrink: 0;
}
.milestone-item .expand-btn:hover {
color: var(--text);
}
.milestone-item .item-count {
font-size: 11px;
color: var(--text-dim);
white-space: nowrap;
}
.milestone-content {
padding: 0 var(--space-lg) var(--space-md);
border-top: 1px solid var(--border);
}
/* === Slice Items === */
.slice-list {
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding-top: var(--space-sm);
}
.slice-item {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
}
.slice-header {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
cursor: pointer;
transition: background var(--transition-fast);
flex-wrap: wrap;
}
.slice-header:hover {
background: rgba(255, 255, 255, 0.02);
}
.slice-title {
flex: 1;
font-size: 13px;
font-weight: 500;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.slice-content {
padding: 0 var(--space-md) var(--space-sm);
border-top: 1px solid var(--border);
}
/* === Feature Items === */
.feature-list {
display: flex;
flex-direction: column;
gap: var(--space-xs);
padding-top: var(--space-sm);
}
.feature-item {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: var(--space-sm) var(--space-md);
}
.feature-header {
display: flex;
align-items: center;
gap: var(--space-sm);
flex-wrap: wrap;
}
.feature-title {
flex: 1;
font-size: 13px;
font-weight: 500;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.feature-description {
font-size: 12px;
color: var(--text-muted);
margin: var(--space-xs) 0 0;
line-height: 1.4;
}
.feature-criteria {
font-size: 12px;
color: var(--text-dim);
margin: var(--space-xs) 0 0;
}
.feature-criteria strong {
color: var(--text-muted);
}
.task-link-badge {
display: inline-flex;
align-items: center;
font-size: 11px;
font-family: var(--font-mono);
color: var(--todo);
background: rgba(88, 166, 255, 0.1);
padding: 1px 6px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background var(--transition-fast);
}
.task-link-badge:hover {
background: rgba(88, 166, 255, 0.2);
}
/* Item actions row */
.milestone-header .item-actions,
.slice-header .item-actions,
.feature-header .item-actions {
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
/* === Responsive: Mission Manager on mobile === */
@media (max-width: 768px) {
.mission-manager-modal {
width: 100%;
max-width: 100%;
max-height: 100dvh;
border-radius: 0;
border: none;
}
.mission-manager-modal .modal-header {
padding: var(--space-sm) var(--space-md);
}
.mission-manager-body {
padding: var(--space-md);
}
.mission-detail-title h3 {
font-size: 16px;
}
.milestone-header {
padding: var(--space-sm) var(--space-md);
flex-wrap: wrap;
gap: var(--space-xs);
}
.milestone-content {
padding: 0 var(--space-sm) var(--space-sm);
}
.slice-header {
padding: var(--space-sm);
gap: var(--space-xs);
}
.slice-content {
padding: 0 var(--space-sm) var(--space-sm);
}
.feature-item {
padding: var(--space-sm);
}
.feature-header {
gap: var(--space-xs);
}
.mission-item {
padding: var(--space-md);
}
/* Make action controls touch-friendly */
.mission-item-actions .icon-btn,
.milestone-header .item-actions .icon-btn,
.slice-header .item-actions .icon-btn,
.feature-header .item-actions .icon-btn {
min-width: 32px;
min-height: 32px;
}
/* Ensure inline forms don't overflow */
.mission-manager-modal .inline-form input[type="text"],
.mission-manager-modal .inline-form textarea,
.mission-manager-modal .inline-form select {
font-size: 16px; /* Prevent iOS zoom on focus */
}
/* Stacked form row on mobile */
.mission-manager-modal .inline-form .form-row {
flex-direction: column;
align-items: flex-start;
gap: var(--space-sm);
}
}
/* Light theme adjustments for Mission Manager */
[data-theme="light"] .mission-manager-modal {
background: var(--surface);
}