feat(FN-1303): redesign mobile nav with 4-tab structure
- Split board and list into separate bottom nav tab buttons for direct access - Move activity from bottom nav into a more/action sheet for cleaner layout - Add usage and search icons to the mobile header - Remove unused mobile nav CSS for old view-toggle and badge patterns - Update mobile nav tests to cover the new 4-tab structure
This commit is contained in:
@@ -307,8 +307,8 @@ export function Header({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile Search Trigger - only show in board view on mobile */}
|
||||
{onSearchChange && view === "board" && isMobile && (
|
||||
{/* Mobile Search Trigger - show in board and list views when mobile nav is hidden (hideFullNav) or in board view when mobile nav is visible */}
|
||||
{onSearchChange && isMobile && (hideFullNav || view === "board") && (
|
||||
<>
|
||||
{!shouldShowMobileSearch ? (
|
||||
<button
|
||||
@@ -317,6 +317,7 @@ export function Header({
|
||||
title="Open search"
|
||||
aria-label="Open search"
|
||||
aria-expanded={false}
|
||||
data-testid="mobile-header-search-btn"
|
||||
>
|
||||
<Search size={16} />
|
||||
</button>
|
||||
@@ -347,6 +348,18 @@ export function Header({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Usage button on mobile when mobile bottom nav is active */}
|
||||
{isMobile && hideFullNav && onOpenUsage && (
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={onOpenUsage}
|
||||
title="View usage"
|
||||
data-testid="mobile-header-usage-btn"
|
||||
>
|
||||
<Activity size={16} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* View Toggle - always inline, even on mobile */}
|
||||
{!hideFullNav && onChangeView && (
|
||||
<div className="view-toggle">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
Activity,
|
||||
Bot,
|
||||
@@ -92,11 +92,6 @@ export function MobileNavBar({
|
||||
const mode = useViewportMode();
|
||||
const [isMoreOpen, setIsMoreOpen] = useState(false);
|
||||
|
||||
const combinedCount = useMemo(
|
||||
() => activePlanningSessionCount + mailboxUnreadCount,
|
||||
[activePlanningSessionCount, mailboxUnreadCount],
|
||||
);
|
||||
|
||||
const closeMore = useCallback(() => setIsMoreOpen(false), []);
|
||||
|
||||
const handleMoreAction = useCallback(
|
||||
@@ -133,30 +128,28 @@ export function MobileNavBar({
|
||||
role="tablist"
|
||||
aria-label="Primary navigation"
|
||||
>
|
||||
<div className="mobile-nav-view-toggle">
|
||||
<button
|
||||
type="button"
|
||||
className={`mobile-nav-view-toggle-btn${view === "board" ? " mobile-nav-view-toggle-btn--active" : ""}`}
|
||||
data-testid="mobile-nav-tab-board"
|
||||
role="tab"
|
||||
aria-selected={view === "board"}
|
||||
onClick={() => onChangeView("board")}
|
||||
>
|
||||
<LayoutGrid />
|
||||
<span>Board</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`mobile-nav-view-toggle-btn${view === "list" ? " mobile-nav-view-toggle-btn--active" : ""}`}
|
||||
data-testid="mobile-nav-tab-list"
|
||||
role="tab"
|
||||
aria-selected={view === "list"}
|
||||
onClick={() => onChangeView("list")}
|
||||
>
|
||||
<List />
|
||||
<span>List</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={`mobile-nav-tab${view === "board" ? " mobile-nav-tab--active" : ""}`}
|
||||
data-testid="mobile-nav-tab-board"
|
||||
role="tab"
|
||||
aria-selected={view === "board"}
|
||||
onClick={() => onChangeView("board")}
|
||||
>
|
||||
<LayoutGrid />
|
||||
<span className="mobile-nav-tab-label">Board</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`mobile-nav-tab${view === "list" ? " mobile-nav-tab--active" : ""}`}
|
||||
data-testid="mobile-nav-tab-list"
|
||||
role="tab"
|
||||
aria-selected={view === "list"}
|
||||
onClick={() => onChangeView("list")}
|
||||
>
|
||||
<List />
|
||||
<span className="mobile-nav-tab-label">List</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@@ -170,19 +163,6 @@ export function MobileNavBar({
|
||||
<span className="mobile-nav-tab-label">Agents</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-nav-tab"
|
||||
data-testid="mobile-nav-tab-activity"
|
||||
role="tab"
|
||||
aria-selected={false}
|
||||
onClick={() => onOpenActivityLog?.()}
|
||||
>
|
||||
<Activity />
|
||||
<span className="mobile-nav-tab-label">Activity</span>
|
||||
{combinedCount > 0 && <span className="mobile-nav-badge">{formatCount(combinedCount)}</span>}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-nav-tab"
|
||||
@@ -219,6 +199,16 @@ export function MobileNavBar({
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-more-item"
|
||||
data-testid="mobile-more-item-activity"
|
||||
onClick={() => handleMoreAction(onOpenActivityLog)}
|
||||
>
|
||||
<Activity />
|
||||
<span>Activity Log</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-more-item"
|
||||
|
||||
@@ -50,13 +50,12 @@ describe("MobileNavBar", () => {
|
||||
mockViewport("mobile");
|
||||
});
|
||||
|
||||
it("renders five tab buttons (board + list + agents + activity + more)", () => {
|
||||
it("renders four tab buttons (board + list + agents + more)", () => {
|
||||
render(<MobileNavBar {...createDefaultProps()} />);
|
||||
|
||||
expect(screen.getByTestId("mobile-nav-tab-board")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-nav-tab-list")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-nav-tab-agents")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-nav-tab-activity")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-nav-tab-more")).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -66,7 +65,7 @@ describe("MobileNavBar", () => {
|
||||
expect(screen.getByTestId("mobile-nav-tab-agents").className).toContain("mobile-nav-tab--active");
|
||||
});
|
||||
|
||||
it("board sub-button calls onChangeView with 'board'", () => {
|
||||
it("board tab calls onChangeView with 'board'", () => {
|
||||
const props = createDefaultProps();
|
||||
render(<MobileNavBar {...props} view="list" />);
|
||||
|
||||
@@ -74,7 +73,7 @@ describe("MobileNavBar", () => {
|
||||
expect(props.onChangeView).toHaveBeenCalledWith("board");
|
||||
});
|
||||
|
||||
it("list sub-button calls onChangeView with 'list'", () => {
|
||||
it("list tab calls onChangeView with 'list'", () => {
|
||||
const props = createDefaultProps();
|
||||
render(<MobileNavBar {...props} view="board" />);
|
||||
|
||||
@@ -82,48 +81,22 @@ describe("MobileNavBar", () => {
|
||||
expect(props.onChangeView).toHaveBeenCalledWith("list");
|
||||
});
|
||||
|
||||
it("board sub-button is active when view is 'board'", () => {
|
||||
it("board tab is active when view is 'board'", () => {
|
||||
render(<MobileNavBar {...createDefaultProps()} view="board" />);
|
||||
expect(screen.getByTestId("mobile-nav-tab-board").className).toContain("mobile-nav-view-toggle-btn--active");
|
||||
expect(screen.getByTestId("mobile-nav-tab-list").className).not.toContain("mobile-nav-view-toggle-btn--active");
|
||||
expect(screen.getByTestId("mobile-nav-tab-board").className).toContain("mobile-nav-tab--active");
|
||||
expect(screen.getByTestId("mobile-nav-tab-list").className).not.toContain("mobile-nav-tab--active");
|
||||
});
|
||||
|
||||
it("list sub-button is active when view is 'list'", () => {
|
||||
it("list tab is active when view is 'list'", () => {
|
||||
render(<MobileNavBar {...createDefaultProps()} view="list" />);
|
||||
expect(screen.getByTestId("mobile-nav-tab-list").className).toContain("mobile-nav-view-toggle-btn--active");
|
||||
expect(screen.getByTestId("mobile-nav-tab-board").className).not.toContain("mobile-nav-view-toggle-btn--active");
|
||||
expect(screen.getByTestId("mobile-nav-tab-list").className).toContain("mobile-nav-tab--active");
|
||||
expect(screen.getByTestId("mobile-nav-tab-board").className).not.toContain("mobile-nav-tab--active");
|
||||
});
|
||||
|
||||
it("board sub-button is not active when view is 'agents'", () => {
|
||||
it("board and list tabs are not active when view is 'agents'", () => {
|
||||
render(<MobileNavBar {...createDefaultProps()} view="agents" />);
|
||||
expect(screen.getByTestId("mobile-nav-tab-board").className).not.toContain("mobile-nav-view-toggle-btn--active");
|
||||
expect(screen.getByTestId("mobile-nav-tab-list").className).not.toContain("mobile-nav-view-toggle-btn--active");
|
||||
});
|
||||
|
||||
it("shows activity badge with combined planning + mailbox count", () => {
|
||||
const { container } = render(
|
||||
<MobileNavBar
|
||||
{...createDefaultProps()}
|
||||
activePlanningSessionCount={2}
|
||||
mailboxUnreadCount={3}
|
||||
/>,
|
||||
);
|
||||
|
||||
const activityTab = screen.getByTestId("mobile-nav-tab-activity");
|
||||
const badge = activityTab.querySelector(".mobile-nav-badge");
|
||||
expect(badge).not.toBeNull();
|
||||
expect(badge?.textContent).toBe("5");
|
||||
expect(container.querySelectorAll(".mobile-nav-badge")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("hides badge when combined count is zero", () => {
|
||||
const { container } = render(<MobileNavBar {...createDefaultProps()} mailboxUnreadCount={0} activePlanningSessionCount={0} />);
|
||||
expect(container.querySelector(".mobile-nav-badge")).toBeNull();
|
||||
});
|
||||
|
||||
it("caps badge at 99+", () => {
|
||||
render(<MobileNavBar {...createDefaultProps()} mailboxUnreadCount={75} activePlanningSessionCount={75} />);
|
||||
expect(screen.getByText("99+")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-nav-tab-board").className).not.toContain("mobile-nav-tab--active");
|
||||
expect(screen.getByTestId("mobile-nav-tab-list").className).not.toContain("mobile-nav-tab--active");
|
||||
});
|
||||
|
||||
it("opens and toggles the more sheet", () => {
|
||||
@@ -137,11 +110,12 @@ describe("MobileNavBar", () => {
|
||||
expect(container.querySelector(".mobile-more-sheet")).toBeNull();
|
||||
});
|
||||
|
||||
it("sheet contains expected navigation items", () => {
|
||||
it("sheet contains expected navigation items including activity log", () => {
|
||||
render(<MobileNavBar {...createDefaultProps()} />);
|
||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
|
||||
|
||||
expect(screen.getByTestId("mobile-more-item-mailbox")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-activity")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-missions")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-git")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-terminal")).toBeDefined();
|
||||
@@ -155,6 +129,17 @@ describe("MobileNavBar", () => {
|
||||
expect(screen.getByTestId("mobile-more-item-settings")).toBeDefined();
|
||||
});
|
||||
|
||||
it("activity log item in more sheet calls onOpenActivityLog", () => {
|
||||
const props = createDefaultProps();
|
||||
const { container } = render(<MobileNavBar {...props} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
|
||||
fireEvent.click(screen.getByTestId("mobile-more-item-activity"));
|
||||
|
||||
expect(container.querySelector(".mobile-more-sheet")).toBeNull();
|
||||
expect(props.onOpenActivityLog).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("closes sheet and calls handler when item is clicked", () => {
|
||||
const props = createDefaultProps();
|
||||
const { container } = render(<MobileNavBar {...props} />);
|
||||
|
||||
@@ -25918,52 +25918,6 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Board/List view toggle — two sub-buttons sharing first tab slot */
|
||||
.mobile-nav-view-toggle {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.mobile-nav-view-toggle-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
padding: 6px 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.mobile-nav-view-toggle-btn:active {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.mobile-nav-view-toggle-btn--active {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.mobile-nav-view-toggle-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.mobile-nav-view-toggle-btn span {
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-nav-tab:active {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
@@ -25983,26 +25937,6 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Badge on tab */
|
||||
.mobile-nav-badge {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 50%;
|
||||
transform: translateX(calc(50% + 10px));
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
background: var(--accent);
|
||||
color: var(--surface);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* === Mobile More Sheet (Bottom Drawer) === */
|
||||
|
||||
.mobile-more-sheet-backdrop {
|
||||
|
||||
Reference in New Issue
Block a user