feat(FN-1880): add mobile roadmap controls - list, create, navigate

- Add mobile-friendly roadmap view with list, create, and navigation controls
- Implement RoadmapCard and inline creation form for quick roadmap entry
- Add mobile-optimized navigation between roadmap views
- Include mobile touch targets and responsive layout using design tokens
- Add comprehensive tests for RoadmapsView mobile components
This commit is contained in:
Fusion
2026-04-16 03:56:09 -07:00
committed by gsxdsm
parent 8e1d43ecc6
commit ea02e146c5
3 changed files with 676 additions and 40 deletions

View File

@@ -1,7 +1,8 @@
import { useState, useCallback } from "react";
import { Plus, Pencil, Trash2, Check, X, GripVertical, Sparkles, Download, Copy, Loader } from "lucide-react";
import { Plus, Pencil, Trash2, Check, X, GripVertical, Sparkles, Download, Copy, Loader, ChevronLeft, ArrowLeft } from "lucide-react";
import type { ToastType } from "../hooks/useToast";
import { useRoadmaps, type FeatureSuggestion, type MilestoneSuggestion, type SuggestionDraftPatch } from "../hooks/useRoadmaps";
import { useViewportMode } from "../hooks/useViewportMode";
import type {
Roadmap,
RoadmapMilestone,
@@ -266,6 +267,190 @@ function RoadmapItem({
);
}
// ── Mobile Roadmap List ──────────────────────────────────────────────
function MobileRoadmapList({
roadmaps,
selectedRoadmapId,
onSelect,
onCreate,
onEdit,
onDelete,
onExport,
showCreateForm,
onCancelCreate,
onSaveCreate,
}: {
roadmaps: Roadmap[];
selectedRoadmapId: string | null;
onSelect: (id: string) => void;
onCreate: () => void;
onEdit: (roadmap: Roadmap) => void;
onDelete: (roadmapId: string) => void;
onExport: (roadmap: Roadmap) => void;
showCreateForm: boolean;
onCancelCreate: () => void;
onSaveCreate: (input: RoadmapCreateInput) => void;
}) {
return (
<div className="roadmaps-view__mobile-list" data-testid="roadmaps-view__mobile-list">
<div className="roadmaps-view__mobile-list-header">
<h2 className="roadmaps-view__mobile-list-title">Roadmaps</h2>
{!showCreateForm && (
<button
className="roadmaps-view__mobile-add-btn"
onClick={onCreate}
title="Create roadmap"
aria-label="Create roadmap"
data-testid="mobile-create-roadmap-btn"
>
<Plus size={18} />
</button>
)}
</div>
{showCreateForm && (
<div className="roadmaps-view__mobile-create-form">
<CreateRoadmapForm onSave={onSaveCreate} onCancel={onCancelCreate} />
</div>
)}
{roadmaps.length === 0 && !showCreateForm ? (
<div className="roadmaps-view__mobile-empty">
<p>No roadmaps yet.</p>
<button className="btn btn-primary btn-sm" onClick={onCreate}>
<Plus size={14} />
<span>Create Roadmap</span>
</button>
</div>
) : (
<div className="roadmaps-view__mobile-list-items">
{roadmaps.map((roadmap) => (
<div
key={roadmap.id}
className={`roadmaps-view__mobile-item${roadmap.id === selectedRoadmapId ? " roadmaps-view__mobile-item--active" : ""}`}
onClick={() => onSelect(roadmap.id)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter") {
onSelect(roadmap.id);
}
}}
data-testid={`mobile-roadmap-item-${roadmap.id}`}
>
<div className="roadmaps-view__mobile-item-content">
<span className="roadmaps-view__mobile-item-title">{roadmap.title}</span>
{roadmap.description && (
<span className="roadmaps-view__mobile-item-desc">{roadmap.description}</span>
)}
</div>
<div className="roadmaps-view__mobile-item-actions">
<button
className="roadmaps-view__mobile-action-btn"
onClick={(e) => {
e.stopPropagation();
onExport(roadmap);
}}
title="Export roadmap"
aria-label="Export roadmap"
data-testid={`mobile-roadmap-export-${roadmap.id}`}
>
<Download size={16} />
</button>
<button
className="roadmaps-view__mobile-action-btn"
onClick={(e) => {
e.stopPropagation();
onEdit(roadmap);
}}
title="Edit roadmap"
aria-label="Edit roadmap"
data-testid={`mobile-roadmap-edit-${roadmap.id}`}
>
<Pencil size={16} />
</button>
<button
className="roadmaps-view__mobile-action-btn roadmaps-view__mobile-action-btn--danger"
onClick={(e) => {
e.stopPropagation();
onDelete(roadmap.id);
}}
title="Delete roadmap"
aria-label="Delete roadmap"
data-testid={`mobile-roadmap-delete-${roadmap.id}`}
>
<Trash2 size={16} />
</button>
</div>
</div>
))}
</div>
)}
</div>
);
}
// ── Mobile Roadmap Header (shown when roadmap is selected) ────────────
function MobileRoadmapHeader({
roadmapTitle,
onBack,
onEdit,
onDelete,
onCreate,
}: {
roadmapTitle: string;
onBack: () => void;
onEdit: () => void;
onDelete: () => void;
onCreate: () => void;
}) {
return (
<div className="roadmaps-view__mobile-header" data-testid="roadmaps-view__mobile-header">
<button
className="roadmaps-view__mobile-back-btn"
onClick={onBack}
title="Back to roadmap list"
aria-label="Back to roadmap list"
data-testid="mobile-back-btn"
>
<ArrowLeft size={20} />
</button>
<h2 className="roadmaps-view__mobile-header-title">{roadmapTitle}</h2>
<div className="roadmaps-view__mobile-header-actions">
<button
className="roadmaps-view__mobile-action-btn"
onClick={onCreate}
title="Create roadmap"
aria-label="Create roadmap"
data-testid="mobile-header-create-btn"
>
<Plus size={18} />
</button>
<button
className="roadmaps-view__mobile-action-btn"
onClick={onEdit}
title="Edit roadmap"
aria-label="Edit roadmap"
data-testid="mobile-header-edit-btn"
>
<Pencil size={18} />
</button>
<button
className="roadmaps-view__mobile-action-btn roadmaps-view__mobile-action-btn--danger"
onClick={onDelete}
title="Delete roadmap"
aria-label="Delete roadmap"
data-testid="mobile-header-delete-btn"
>
<Trash2 size={18} />
</button>
</div>
</div>
);
}
// ── Milestone Card ───────────────────────────────────────────────────
function MilestoneCard({
@@ -1219,6 +1404,8 @@ function CreateFeatureForm({
// ── Main Component ────────────────────────────────────────────────────
export function RoadmapsView({ projectId, addToast }: RoadmapsViewProps) {
const isMobile = useViewportMode() === "mobile";
const {
roadmaps,
selectedRoadmapId,
@@ -1294,8 +1481,8 @@ export function RoadmapsView({ projectId, addToast }: RoadmapsViewProps) {
description: "",
});
// Mobile sidebar state
const [mobileSelectedRoadmapId, setMobileSelectedRoadmapId] = useState<string | null>(null);
// Mobile roadmap list create form state
const [mobileShowCreateForm, setMobileShowCreateForm] = useState(false);
// Milestone drag-and-drop state
const [milestoneDrag, setMilestoneDrag] = useState<MilestoneDragState>({
@@ -1896,49 +2083,83 @@ export function RoadmapsView({ projectId, addToast }: RoadmapsViewProps) {
return (
<div className="roadmaps-view">
{/* Desktop sidebar */}
<aside className="roadmaps-view__sidebar" aria-label="Roadmaps">
<div className="roadmaps-view__sidebar-header">
<h2 className="roadmaps-view__sidebar-title">Roadmaps</h2>
<button
className="roadmaps-view__add-btn"
onClick={() => setCreateForm({ type: "roadmap", title: "", description: "" })}
title="Create roadmap"
aria-label="Create roadmap"
data-testid="create-roadmap-btn"
>
<Plus size={16} />
</button>
</div>
{/* Mobile Roadmap List (shown when mobile and no roadmap selected) */}
{isMobile && !effectiveSelectedRoadmapId && (
<MobileRoadmapList
roadmaps={roadmaps}
selectedRoadmapId={effectiveSelectedRoadmapId}
onSelect={(id) => selectRoadmap(id)}
onCreate={() => setMobileShowCreateForm(true)}
onEdit={handleStartRoadmapEdit}
onDelete={handleDeleteRoadmap}
onExport={(roadmap) => handleOpenHandoffModal(roadmap.id, roadmap.title)}
showCreateForm={mobileShowCreateForm}
onCancelCreate={() => setMobileShowCreateForm(false)}
onSaveCreate={async (input) => {
await handleCreateRoadmap(input);
setMobileShowCreateForm(false);
}}
/>
)}
{createForm.type === "roadmap" && (
<CreateRoadmapForm
onSave={handleCreateRoadmap}
onCancel={() => setCreateForm({ type: null, parentId: undefined, title: "", description: "" })}
/>
)}
{/* Desktop sidebar (hidden on mobile) */}
{!isMobile && (
<aside className="roadmaps-view__sidebar" aria-label="Roadmaps">
<div className="roadmaps-view__sidebar-header">
<h2 className="roadmaps-view__sidebar-title">Roadmaps</h2>
<button
className="roadmaps-view__add-btn"
onClick={() => setCreateForm({ type: "roadmap", title: "", description: "" })}
title="Create roadmap"
aria-label="Create roadmap"
data-testid="create-roadmap-btn"
>
<Plus size={16} />
</button>
</div>
<div className="roadmaps-view__sidebar-list">
{roadmaps.length === 0 ? (
<p className="roadmaps-view__empty-sidebar">No roadmaps yet. Click + to create one.</p>
) : (
roadmaps.map((roadmap) => (
<RoadmapItem
key={roadmap.id}
roadmap={roadmap}
isSelected={roadmap.id === effectiveSelectedRoadmapId}
onSelect={() => selectRoadmap(roadmap.id)}
onEdit={() => handleStartRoadmapEdit(roadmap)}
onDelete={() => handleDeleteRoadmap(roadmap.id)}
onExport={() => handleOpenHandoffModal(roadmap.id, roadmap.title)}
/>
))
{createForm.type === "roadmap" && (
<CreateRoadmapForm
onSave={handleCreateRoadmap}
onCancel={() => setCreateForm({ type: null, parentId: undefined, title: "", description: "" })}
/>
)}
</div>
</aside>
<div className="roadmaps-view__sidebar-list">
{roadmaps.length === 0 ? (
<p className="roadmaps-view__empty-sidebar">No roadmaps yet. Click + to create one.</p>
) : (
roadmaps.map((roadmap) => (
<RoadmapItem
key={roadmap.id}
roadmap={roadmap}
isSelected={roadmap.id === effectiveSelectedRoadmapId}
onSelect={() => selectRoadmap(roadmap.id)}
onEdit={() => handleStartRoadmapEdit(roadmap)}
onDelete={() => handleDeleteRoadmap(roadmap.id)}
onExport={() => handleOpenHandoffModal(roadmap.id, roadmap.title)}
/>
))
)}
</div>
</aside>
)}
{/* Main content */}
<main className="roadmaps-view__main" aria-label="Roadmap content">
{/* Mobile header when roadmap is selected */}
{isMobile && effectiveSelectedRoadmapId && (
<MobileRoadmapHeader
roadmapTitle={selectedRoadmap?.title || "Untitled Roadmap"}
onBack={() => selectRoadmap(null)}
onEdit={() => {
if (selectedRoadmap) handleStartRoadmapEdit(selectedRoadmap);
}}
onDelete={() => handleDeleteRoadmap(effectiveSelectedRoadmapId)}
onCreate={() => setMobileShowCreateForm(true)}
/>
)}
{!effectiveSelectedRoadmapId ? (
<div className="roadmaps-view__empty-main">
<p>Select a roadmap from the sidebar to view its milestones.</p>

View File

@@ -44,8 +44,28 @@ vi.mock("lucide-react", () => ({
Download: (props: unknown) => <span data-testid="download-icon" {...props}>Download</span>,
Copy: (props: unknown) => <span data-testid="copy-icon" {...props}>Copy</span>,
Loader: (props: unknown) => <span data-testid="loader-icon" {...props}>Loader</span>,
ArrowLeft: (props: unknown) => <span data-testid="arrow-left-icon" {...props}>ArrowLeft</span>,
ChevronLeft: (props: unknown) => <span data-testid="chevron-left-icon" {...props}>ChevronLeft</span>,
}));
// Viewport mode mock helper
function mockViewport(mode: "mobile" | "desktop") {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => {
const isMobileQuery = query === "(max-width: 768px)";
const isTabletQuery = query === "(min-width: 769px) and (max-width: 1024px)";
return {
matches: mode === "mobile" ? isMobileQuery : false,
media: query,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
};
}),
});
}
const mockRoadmaps: Roadmap[] = [
{
id: "RM-001",
@@ -108,6 +128,7 @@ const mockAddToast = vi.fn();
describe("RoadmapsView", () => {
beforeEach(() => {
vi.clearAllMocks();
mockViewport("desktop");
(api.fetchRoadmaps as ReturnType<typeof vi.fn>).mockResolvedValue(mockRoadmaps);
(api.fetchRoadmap as ReturnType<typeof vi.fn>).mockResolvedValue(mockRoadmapHierarchy);
vi.spyOn(window, "confirm").mockReturnValue(true);
@@ -646,4 +667,181 @@ describe("RoadmapsView", () => {
expect(editButtons.length).toBeGreaterThan(0);
});
});
describe("Mobile roadmap controls", () => {
beforeEach(() => {
mockViewport("mobile");
});
afterEach(() => {
mockViewport("desktop");
});
it("shows mobile roadmap list when no roadmap selected on mobile", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("roadmaps-view__mobile-list")).toBeInTheDocument();
});
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
expect(screen.getByText("Q3 Roadmap")).toBeInTheDocument();
expect(screen.getByTestId("mobile-create-roadmap-btn")).toBeInTheDocument();
});
it("shows mobile roadmap items when roadmaps exist on mobile", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
});
expect(screen.getByTestId("mobile-roadmap-item-RM-002")).toBeInTheDocument();
});
it("can select a roadmap from mobile list", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
});
// Click on a roadmap item
fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
// Should show the mobile header with the roadmap title
await waitFor(() => {
expect(screen.getByTestId("roadmaps-view__mobile-header")).toBeInTheDocument();
expect(screen.getByText("Q2 Roadmap", { selector: ".roadmaps-view__mobile-header-title" })).toBeInTheDocument();
});
});
it("mobile back button deselects roadmap", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
});
// Select a roadmap
fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
await waitFor(() => {
expect(screen.getByTestId("roadmaps-view__mobile-header")).toBeInTheDocument();
});
// Click back button
fireEvent.click(screen.getByTestId("mobile-back-btn"));
// Should show mobile list again
await waitFor(() => {
expect(screen.getByTestId("roadmaps-view__mobile-list")).toBeInTheDocument();
});
});
it("mobile create button shows create form", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("mobile-create-roadmap-btn")).toBeInTheDocument();
});
// Click create button
fireEvent.click(screen.getByTestId("mobile-create-roadmap-btn"));
// Should show create form
await waitFor(() => {
expect(screen.getByTestId("create-roadmap-form")).toBeInTheDocument();
expect(screen.getByTestId("create-roadmap-title")).toBeInTheDocument();
});
});
it("mobile can create roadmap via form", async () => {
const newRoadmap = {
id: "RM-003",
title: "New Roadmap",
createdAt: "2026-01-03T00:00:00.000Z",
updatedAt: "2026-01-03T00:00:00.000Z",
};
(api.createRoadmap as ReturnType<typeof vi.fn>).mockResolvedValue(newRoadmap);
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("mobile-create-roadmap-btn")).toBeInTheDocument();
});
// Click create button
fireEvent.click(screen.getByTestId("mobile-create-roadmap-btn"));
// Fill in the form using userEvent for better React integration
await waitFor(() => {
const titleInput = screen.getByTestId("create-roadmap-title");
expect(titleInput).toBeInTheDocument();
});
const titleInput = screen.getByTestId("create-roadmap-title");
await userEvent.type(titleInput, "New Roadmap");
// Submit the form using fireEvent.submit
const form = screen.getByTestId("create-roadmap-form").querySelector("form");
expect(form).toBeTruthy();
fireEvent.submit(form!);
await waitFor(() => {
expect(api.createRoadmap).toHaveBeenCalledWith(
{ title: "New Roadmap" },
undefined
);
expect(mockAddToast).toHaveBeenCalledWith("Roadmap created", "success");
});
});
it("mobile edit and delete buttons are visible on roadmap items", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
});
// Edit and delete buttons should be visible (not hidden behind hover on mobile)
expect(screen.getByTestId("mobile-roadmap-edit-RM-001")).toBeInTheDocument();
expect(screen.getByTestId("mobile-roadmap-delete-RM-001")).toBeInTheDocument();
expect(screen.getByTestId("mobile-roadmap-export-RM-001")).toBeInTheDocument();
});
it("shows empty state on mobile when no roadmaps", async () => {
(api.fetchRoadmaps as ReturnType<typeof vi.fn>).mockResolvedValue([]);
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("roadmaps-view__mobile-list")).toBeInTheDocument();
});
expect(screen.getByText("No roadmaps yet.")).toBeInTheDocument();
expect(screen.getByTestId("roadmaps-view__mobile-list").textContent).toContain("Create Roadmap");
});
it("mobile header shows action buttons when roadmap selected", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
});
// Select a roadmap
fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
await waitFor(() => {
expect(screen.getByTestId("roadmaps-view__mobile-header")).toBeInTheDocument();
});
// Action buttons should be visible in header
expect(screen.getByTestId("mobile-header-create-btn")).toBeInTheDocument();
expect(screen.getByTestId("mobile-header-edit-btn")).toBeInTheDocument();
expect(screen.getByTestId("mobile-header-delete-btn")).toBeInTheDocument();
expect(screen.getByTestId("mobile-back-btn")).toBeInTheDocument();
});
});
});

View File

@@ -32134,10 +32134,217 @@ html .column.drag-over * {
display: none;
}
/* Mobile Roadmap List */
.roadmaps-view__mobile-list {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background: var(--surface);
}
.roadmaps-view__mobile-list-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-md) var(--space-lg);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.roadmaps-view__mobile-list-title {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--text);
}
.roadmaps-view__mobile-add-btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border: none;
border-radius: var(--radius-md);
background: var(--accent);
color: white;
cursor: pointer;
transition: opacity var(--transition-fast);
}
.roadmaps-view__mobile-add-btn:hover {
opacity: 0.85;
}
.roadmaps-view__mobile-add-btn:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.roadmaps-view__mobile-create-form {
padding: var(--space-md);
border-bottom: 1px solid var(--border);
background: var(--bg);
}
.roadmaps-view__mobile-list-items {
flex: 1;
overflow-y: auto;
}
.roadmaps-view__mobile-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-md) var(--space-lg);
border-bottom: 1px solid var(--border);
min-height: 44px;
cursor: pointer;
transition: background var(--transition-fast);
}
.roadmaps-view__mobile-item:hover {
background: var(--surface-hover, rgba(0, 0, 0, 0.03));
}
.roadmaps-view__mobile-item--active {
background: color-mix(in srgb, var(--accent) 10%, transparent);
border-left: 3px solid var(--accent);
}
.roadmaps-view__mobile-item-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.roadmaps-view__mobile-item-title {
font-weight: 500;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.roadmaps-view__mobile-item-desc {
font-size: 0.8rem;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.roadmaps-view__mobile-item-actions {
display: flex;
gap: var(--space-xs);
flex-shrink: 0;
}
.roadmaps-view__mobile-action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border: none;
border-radius: var(--radius-md);
background: transparent;
color: var(--text-muted);
cursor: pointer;
transition: background var(--transition-fast), color var(--transition-fast);
}
.roadmaps-view__mobile-action-btn:hover {
background: var(--surface-hover, rgba(0, 0, 0, 0.05));
color: var(--text);
}
.roadmaps-view__mobile-action-btn:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.roadmaps-view__mobile-action-btn--danger:hover {
background: color-mix(in srgb, var(--color-error) 10%, transparent);
color: var(--color-error);
}
.roadmaps-view__mobile-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-md);
padding: var(--space-2xl);
color: var(--text-muted);
text-align: center;
}
/* Mobile Roadmap Header (shown when roadmap selected) */
.roadmaps-view__mobile-header {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
border-bottom: 1px solid var(--border);
background: var(--surface);
flex-shrink: 0;
position: sticky;
top: 0;
z-index: 10;
}
.roadmaps-view__mobile-back-btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border: none;
border-radius: var(--radius-md);
background: transparent;
color: var(--text-muted);
cursor: pointer;
transition: background var(--transition-fast), color var(--transition-fast);
flex-shrink: 0;
}
.roadmaps-view__mobile-back-btn:hover {
background: var(--surface-hover, rgba(0, 0, 0, 0.05));
color: var(--text);
}
.roadmaps-view__mobile-back-btn:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.roadmaps-view__mobile-header-title {
flex: 1;
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.roadmaps-view__mobile-header-actions {
display: flex;
gap: var(--space-xs);
flex-shrink: 0;
}
/* Mobile milestone lanes */
.roadmaps-view__milestone-lanes {
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
padding-bottom: calc(var(--mobile-nav-height) + env(safe-area-inset-bottom, 0px));
}
.roadmaps-view__milestone {
@@ -32166,6 +32373,16 @@ html .column.drag-over * {
.roadmap-suggestion-card {
padding: var(--space-sm);
}
/* Mobile roadmap header section */
.roadmaps-view__roadmap-header {
padding: var(--space-md) var(--space-lg);
}
/* Mobile create form on desktop sidebar for roadmaps - ensure proper sizing */
.roadmaps-view__create-form {
margin: var(--space-sm);
}
}