feat(FN-2025): move secondary header views into overflow menu

- Add view-toggle overflow state and refs in Header with Escape and outside-click close behavior
- Replace inline Skills/Roadmaps/Insights buttons with a More views trigger and dropdown menu actions
- Style the view-toggle overflow menu and items for positioning, hover, and active states
- Update Header, tablet header controls, and App integration tests to validate overflow interactions and active view behavior
This commit is contained in:
Fusion
2026-04-17 20:57:26 -07:00
committed by gsxdsm
parent e4f43a8b43
commit f071fdcdfb
5 changed files with 289 additions and 222 deletions

View File

@@ -98,6 +98,41 @@ describe("tablet header controls", () => {
expect(screen.getByTitle("Board view")).toBeDefined();
expect(screen.getByTitle("List view")).toBeDefined();
expect(screen.getByTitle("Agents view")).toBeDefined();
// Skills, Roadmaps, Insights are NOT inline (they're in overflow)
expect(screen.queryByTitle("Skills view")).toBeNull();
expect(screen.queryByTitle("Roadmaps view")).toBeNull();
expect(screen.queryByTitle("Insights view")).toBeNull();
});
it("renders view toggle overflow trigger on tablet", () => {
renderTabletHeader({ onChangeView: noop });
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeDefined();
});
it("opens overflow menu with Insights, Roadmaps, Skills on tablet when trigger is clicked", () => {
renderTabletHeader({ onChangeView: noop, showSkillsTab: true, experimentalFeatures: { insights: true, roadmap: true } });
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.getByTestId("view-overflow-insights")).toBeDefined();
expect(screen.getByTestId("view-overflow-roadmaps")).toBeDefined();
expect(screen.getByTestId("view-overflow-skills")).toBeDefined();
});
it("calls onChangeView from overflow menu on tablet", () => {
const onChangeView = vi.fn();
renderTabletHeader({ onChangeView, experimentalFeatures: { insights: true } });
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
fireEvent.click(screen.getByTestId("view-overflow-insights"));
expect(onChangeView).toHaveBeenCalledWith("insights");
});
it("closes overflow menu on tablet after selecting an item", async () => {
renderTabletHeader({ onChangeView: noop, showSkillsTab: true, experimentalFeatures: { insights: true, roadmap: true } });
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.getByTestId("view-overflow-insights")).toBeDefined();
fireEvent.click(screen.getByTestId("view-overflow-skills"));
await waitFor(() => {
expect(screen.queryByTestId("view-overflow-insights")).toBeNull();
});
});
// ── Lower-priority actions move to overflow on tablet ──────────

View File

@@ -268,6 +268,7 @@ export function Header({
const [isTerminalSubmenuOpen, setIsTerminalSubmenuOpen] = useState(false);
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
const [isMobileProjectSwitchOpen, setIsMobileProjectSwitchOpen] = useState(false);
const [isViewOverflowOpen, setIsViewOverflowOpen] = useState(false);
const [overflowScripts, setOverflowScripts] = useState<Record<string, string>>({});
const [overflowScriptsLoading, setOverflowScriptsLoading] = useState(false);
const overflowButtonRef = useRef<HTMLButtonElement>(null);
@@ -277,6 +278,8 @@ export function Header({
const terminalSubmenuOpenRef = useRef(false);
const nodeSelectorRef = useRef<HTMLDivElement>(null);
const mobileProjectSwitchRef = useRef<HTMLDivElement>(null);
const viewOverflowRef = useRef<HTMLDivElement>(null);
const viewOverflowTriggerRef = useRef<HTMLButtonElement>(null);
// Get remote nodes only (exclude local node type)
const remoteNodes = useMemo(() =>
@@ -380,6 +383,7 @@ export function Header({
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setIsViewOverflowOpen(false);
if (terminalSubmenuOpenRef.current) {
setIsTerminalSubmenuOpen(false);
return;
@@ -412,6 +416,25 @@ export function Header({
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [isMobileProjectSwitchOpen]);
// Close view toggle overflow on outside click
useEffect(() => {
if (!isViewOverflowOpen) return;
const handleClickOutside = (e: MouseEvent) => {
if (
viewOverflowRef.current &&
!viewOverflowRef.current.contains(e.target as Node) &&
viewOverflowTriggerRef.current &&
!viewOverflowTriggerRef.current.contains(e.target as Node)
) {
setIsViewOverflowOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [isViewOverflowOpen]);
const handleMobileSearchToggle = useCallback(() => {
setIsMobileSearchOpen((prev) => !prev);
}, []);
@@ -744,38 +767,68 @@ export function Header({
>
<Mail size={16} />
</button>
{showSkillsTab && (
<button
className={`view-toggle-btn${view === "skills" ? " active" : ""}`}
onClick={() => onChangeView("skills")}
title="Skills view"
aria-label="Skills view"
aria-pressed={view === "skills"}
<button
ref={viewOverflowTriggerRef}
className={`view-toggle-btn${["skills", "roadmaps", "insights"].includes(view) ? " active" : ""}`}
onClick={() => setIsViewOverflowOpen((prev) => !prev)}
title="More views"
aria-label="More views"
aria-haspopup="menu"
aria-expanded={isViewOverflowOpen}
data-testid="view-toggle-overflow-trigger"
>
<ChevronDown size={12} />
</button>
{isViewOverflowOpen && (
<div
ref={viewOverflowRef}
className="view-toggle-overflow-menu"
role="menu"
aria-label="More views"
>
<Zap size={16} />
</button>
)}
{experimentalFeatures?.roadmap && (
<button
className={`view-toggle-btn${view === "roadmaps" ? " active" : ""}`}
onClick={() => onChangeView("roadmaps")}
title="Roadmaps view"
aria-label="Roadmaps view"
aria-pressed={view === "roadmaps"}
>
<Map size={16} />
</button>
)}
{experimentalFeatures?.insights && (
<button
className={`view-toggle-btn${view === "insights" ? " active" : ""}`}
onClick={() => onChangeView("insights")}
title="Insights view"
aria-label="Insights view"
aria-pressed={view === "insights"}
>
<Sparkles size={16} />
</button>
{experimentalFeatures?.insights && (
<button
className={`view-toggle-overflow-item${view === "insights" ? " active" : ""}`}
onClick={() => {
onChangeView("insights");
setIsViewOverflowOpen(false);
}}
role="menuitem"
data-testid="view-overflow-insights"
>
<Sparkles size={14} />
<span>Insights</span>
</button>
)}
{experimentalFeatures?.roadmap && (
<button
className={`view-toggle-overflow-item${view === "roadmaps" ? " active" : ""}`}
onClick={() => {
onChangeView("roadmaps");
setIsViewOverflowOpen(false);
}}
role="menuitem"
data-testid="view-overflow-roadmaps"
>
<Map size={14} />
<span>Roadmaps</span>
</button>
)}
{showSkillsTab && (
<button
className={`view-toggle-overflow-item${view === "skills" ? " active" : ""}`}
onClick={() => {
onChangeView("skills");
setIsViewOverflowOpen(false);
}}
role="menuitem"
data-testid="view-overflow-skills"
>
<Zap size={14} />
<span>Skills</span>
</button>
)}
</div>
)}
</div>
)}

View File

@@ -1254,11 +1254,12 @@ describe("App view switching", () => {
// Wait for the header to render
await waitFor(() => {
expect(screen.getByTitle("Insights view")).toBeTruthy();
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeTruthy();
});
// Click to switch to insights view
fireEvent.click(screen.getByTitle("Insights view"));
// Open the overflow menu and click Insights
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
fireEvent.click(screen.getByTestId("view-overflow-insights"));
// Insights view should be rendered (it has a insights-view container)
await waitFor(() => {
@@ -1277,10 +1278,11 @@ describe("App view switching", () => {
render(<App />);
await waitFor(() => {
expect(screen.getByTitle("Insights view")).toBeTruthy();
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeTruthy();
});
fireEvent.click(screen.getByTitle("Insights view"));
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
fireEvent.click(screen.getByTestId("view-overflow-insights"));
await waitFor(() => {
expect(localStorage.getItem(taskViewStorageKey())).toBe("insights");
@@ -1296,7 +1298,8 @@ describe("App view switching", () => {
expect(document.querySelector(".insights-view")).toBeTruthy();
});
expect(screen.getByTitle("Insights view").className).toContain("active");
// Overflow trigger should be active when view is insights
expect(screen.getByTestId("view-toggle-overflow-trigger").className).toContain("active");
localStorage.removeItem(taskViewStorageKey());
});
@@ -1319,8 +1322,8 @@ describe("App view switching", () => {
expect(document.querySelector(".insights-view")).toBeTruthy();
});
// Verify insights is active
expect(screen.getByTitle("Insights view").className).toContain("active");
// Verify overflow trigger is active
expect(screen.getByTestId("view-toggle-overflow-trigger").className).toContain("active");
// Cleanup
localStorage.removeItem("kb:proj_a:kb-dashboard-task-view");
@@ -1341,8 +1344,9 @@ describe("App view switching", () => {
expect(screen.getByTitle("Board view")).toBeTruthy();
});
// Insights button should not be rendered
expect(screen.queryByTitle("Insights view")).toBeNull();
// Open the overflow menu - Insights item should not be rendered
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.queryByTestId("view-overflow-insights")).toBeNull();
});
});

View File

@@ -358,205 +358,143 @@ describe("Header", () => {
expect(missionsBtn.getAttribute("aria-pressed")).toBe("false");
});
// ── Skills View Toggle ─────────────────────────────────────────
// ── View Toggle Overflow ─────────────────────────────────────────
it("renders skills view button in view toggle when onChangeView is provided", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} showSkillsTab={true} />);
const skillsBtn = screen.getByTitle("Skills view");
expect(skillsBtn).toBeDefined();
});
describe("View Toggle Overflow", () => {
it("renders overflow trigger button when onChangeView is provided", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} />);
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeDefined();
});
it("calls onChangeView with 'skills' when skills view button is clicked", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} showSkillsTab={true} />);
const skillsBtn = screen.getByTitle("Skills view");
fireEvent.click(skillsBtn);
expect(onChangeView).toHaveBeenCalledWith("skills");
});
it("does not render overflow trigger when onChangeView is not provided", () => {
render(<Header view="board" />);
expect(screen.queryByTestId("view-toggle-overflow-trigger")).toBeNull();
});
it("marks skills view button as active when view is 'skills'", () => {
const onChangeView = vi.fn();
render(<Header view="skills" onChangeView={onChangeView} showSkillsTab={true} />);
const skillsBtn = screen.getByTitle("Skills view");
expect(skillsBtn.className).toContain("active");
expect(skillsBtn.getAttribute("aria-pressed")).toBe("true");
});
it("opens overflow menu when trigger is clicked", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} showSkillsTab experimentalFeatures={{ insights: true, roadmap: true }} />);
const trigger = screen.getByTestId("view-toggle-overflow-trigger");
fireEvent.click(trigger);
expect(screen.getByTestId("view-overflow-insights")).toBeDefined();
expect(screen.getByTestId("view-overflow-roadmaps")).toBeDefined();
expect(screen.getByTestId("view-overflow-skills")).toBeDefined();
});
it("does not mark skills view button as active when view is 'board'", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} showSkillsTab={true} />);
const skillsBtn = screen.getByTitle("Skills view");
expect(skillsBtn.className).not.toContain("active");
expect(skillsBtn.getAttribute("aria-pressed")).toBe("false");
});
it("closes overflow menu when trigger is clicked again", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} showSkillsTab experimentalFeatures={{ insights: true, roadmap: true }} />);
const trigger = screen.getByTestId("view-toggle-overflow-trigger");
fireEvent.click(trigger);
expect(screen.getByTestId("view-overflow-insights")).toBeDefined();
fireEvent.click(trigger);
expect(screen.queryByTestId("view-overflow-insights")).toBeNull();
});
it("does not render skills view button when showSkillsTab is false", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} showSkillsTab={false} />);
expect(screen.queryByTitle("Skills view")).toBeNull();
});
it("calls onChangeView with 'insights' when Insights overflow item is clicked", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
fireEvent.click(screen.getByTestId("view-overflow-insights"));
expect(onChangeView).toHaveBeenCalledWith("insights");
});
it("does not render skills view button when showSkillsTab is omitted", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} />);
expect(screen.queryByTitle("Skills view")).toBeNull();
});
it("calls onChangeView with 'roadmaps' when Roadmaps overflow item is clicked", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ roadmap: true }} />);
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
fireEvent.click(screen.getByTestId("view-overflow-roadmaps"));
expect(onChangeView).toHaveBeenCalledWith("roadmaps");
});
// ── Chat View Toggle ─────────────────────────────────────────
it("calls onChangeView with 'skills' when Skills overflow item is clicked", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} showSkillsTab />);
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
fireEvent.click(screen.getByTestId("view-overflow-skills"));
expect(onChangeView).toHaveBeenCalledWith("skills");
});
it("renders chat view button in view toggle when onChangeView is provided", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} />);
const chatBtn = screen.getByTitle("Chat view");
expect(chatBtn).toBeDefined();
});
it("shows overflow trigger as active when view is 'insights'", () => {
const onChangeView = vi.fn();
render(<Header view="insights" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
expect(screen.getByTestId("view-toggle-overflow-trigger").className).toContain("active");
});
it("calls onChangeView with 'chat' when chat view button is clicked", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} />);
const chatBtn = screen.getByTitle("Chat view");
fireEvent.click(chatBtn);
expect(onChangeView).toHaveBeenCalledWith("chat");
});
it("shows overflow trigger as active when view is 'roadmaps'", () => {
const onChangeView = vi.fn();
render(<Header view="roadmaps" onChangeView={onChangeView} experimentalFeatures={{ roadmap: true }} />);
expect(screen.getByTestId("view-toggle-overflow-trigger").className).toContain("active");
});
it("marks chat view button as active when view is 'chat'", () => {
const onChangeView = vi.fn();
render(<Header view="chat" onChangeView={onChangeView} />);
const chatBtn = screen.getByTitle("Chat view");
expect(chatBtn.className).toContain("active");
expect(chatBtn.getAttribute("aria-pressed")).toBe("true");
});
it("shows overflow trigger as active when view is 'skills'", () => {
const onChangeView = vi.fn();
render(<Header view="skills" onChangeView={onChangeView} showSkillsTab />);
expect(screen.getByTestId("view-toggle-overflow-trigger").className).toContain("active");
});
it("does not mark chat view button as active when view is 'board'", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} />);
const chatBtn = screen.getByTitle("Chat view");
expect(chatBtn.className).not.toContain("active");
expect(chatBtn.getAttribute("aria-pressed")).toBe("false");
});
it("overflow trigger is not active when view is 'board'", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} />);
expect(screen.getByTestId("view-toggle-overflow-trigger").className).not.toContain("active");
});
// ── Mailbox View Toggle ─────────────────────────────────────
it("overflow trigger is not active when view is 'list'", () => {
const onChangeView = vi.fn();
render(<Header view="list" onChangeView={onChangeView} />);
expect(screen.getByTestId("view-toggle-overflow-trigger").className).not.toContain("active");
});
it("renders mailbox view button in view toggle when onChangeView is provided", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} />);
const mailboxBtn = screen.getByTitle("Mailbox view");
expect(mailboxBtn).toBeDefined();
});
it("closes overflow menu on Escape key", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} showSkillsTab experimentalFeatures={{ insights: true }} />);
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.getByTestId("view-overflow-insights")).toBeDefined();
fireEvent.keyDown(document, { key: "Escape" });
expect(screen.queryByTestId("view-overflow-insights")).toBeNull();
});
it("calls onChangeView with 'mailbox' when mailbox view button is clicked", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} />);
const mailboxBtn = screen.getByTitle("Mailbox view");
fireEvent.click(mailboxBtn);
expect(onChangeView).toHaveBeenCalledWith("mailbox");
});
it("closes overflow menu on outside click", async () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} showSkillsTab experimentalFeatures={{ insights: true }} />);
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.getByTestId("view-overflow-insights")).toBeDefined();
// Simulate click outside
fireEvent.mouseDown(document.body);
await waitFor(() => {
expect(screen.queryByTestId("view-overflow-insights")).toBeNull();
});
});
it("marks mailbox view button as active when view is 'mailbox'", () => {
const onChangeView = vi.fn();
render(<Header view="mailbox" onChangeView={onChangeView} />);
const mailboxBtn = screen.getByTitle("Mailbox view");
expect(mailboxBtn.className).toContain("active");
expect(mailboxBtn.getAttribute("aria-pressed")).toBe("true");
});
it("does not render skills, roadmaps, insights as inline toggle buttons", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} showSkillsTab experimentalFeatures={{ insights: true, roadmap: true }} />);
expect(screen.queryByTitle("Skills view")).toBeNull();
expect(screen.queryByTitle("Roadmaps view")).toBeNull();
expect(screen.queryByTitle("Insights view")).toBeNull();
});
it("does not mark mailbox view button as active when view is 'board'", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} />);
const mailboxBtn = screen.getByTitle("Mailbox view");
expect(mailboxBtn.className).not.toContain("active");
expect(mailboxBtn.getAttribute("aria-pressed")).toBe("false");
});
it("does not render Insights overflow item when experimentalFeatures.insights is false", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: false }} />);
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.queryByTestId("view-overflow-insights")).toBeNull();
});
// ── Roadmaps View Toggle ───────────────────────────────────────
it("does not render Roadmaps overflow item when experimentalFeatures.roadmap is false", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ roadmap: false }} />);
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.queryByTestId("view-overflow-roadmaps")).toBeNull();
});
it("renders roadmaps view button in view toggle when experimentalFeatures.roadmap is true", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ roadmap: true }} />);
const roadmapsBtn = screen.getByTitle("Roadmaps view");
expect(roadmapsBtn).toBeDefined();
});
it("calls onChangeView with 'roadmaps' when roadmaps view button is clicked", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ roadmap: true }} />);
const roadmapsBtn = screen.getByTitle("Roadmaps view");
fireEvent.click(roadmapsBtn);
expect(onChangeView).toHaveBeenCalledWith("roadmaps");
});
it("marks roadmaps view button as active when view is 'roadmaps'", () => {
const onChangeView = vi.fn();
render(<Header view="roadmaps" onChangeView={onChangeView} experimentalFeatures={{ roadmap: true }} />);
const roadmapsBtn = screen.getByTitle("Roadmaps view");
expect(roadmapsBtn.className).toContain("active");
expect(roadmapsBtn.getAttribute("aria-pressed")).toBe("true");
});
it("does not mark roadmaps view button as active when view is 'board'", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ roadmap: true }} />);
const roadmapsBtn = screen.getByTitle("Roadmaps view");
expect(roadmapsBtn.className).not.toContain("active");
expect(roadmapsBtn.getAttribute("aria-pressed")).toBe("false");
});
it("does not render roadmaps view button when experimentalFeatures.roadmap is false", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ roadmap: false }} />);
expect(screen.queryByTitle("Roadmaps view")).toBeNull();
});
it("does not render roadmaps view button when experimentalFeatures is not provided", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} />);
expect(screen.queryByTitle("Roadmaps view")).toBeNull();
});
// ── Insights View Toggle ─────────────────────────────────────────
it("renders insights view button in view toggle when experimentalFeatures.insights is true", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
const insightsBtn = screen.getByTitle("Insights view");
expect(insightsBtn).toBeDefined();
});
it("calls onChangeView with 'insights' when insights view button is clicked", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
const insightsBtn = screen.getByTitle("Insights view");
fireEvent.click(insightsBtn);
expect(onChangeView).toHaveBeenCalledWith("insights");
});
it("marks insights view button as active when view is 'insights'", () => {
const onChangeView = vi.fn();
render(<Header view="insights" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
const insightsBtn = screen.getByTitle("Insights view");
expect(insightsBtn.className).toContain("active");
expect(insightsBtn.getAttribute("aria-pressed")).toBe("true");
});
it("does not mark insights view button as active when view is 'board'", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
const insightsBtn = screen.getByTitle("Insights view");
expect(insightsBtn.className).not.toContain("active");
expect(insightsBtn.getAttribute("aria-pressed")).toBe("false");
});
it("does not render insights view button when experimentalFeatures.insights is false", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: false }} />);
expect(screen.queryByTitle("Insights view")).toBeNull();
});
it("does not render insights view button when experimentalFeatures is not provided", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} />);
expect(screen.queryByTitle("Insights view")).toBeNull();
it("does not render Skills overflow item when showSkillsTab is false", () => {
const onChangeView = vi.fn();
render(<Header view="board" onChangeView={onChangeView} showSkillsTab={false} />);
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.queryByTestId("view-overflow-skills")).toBeNull();
});
});
// ── Search Visibility by View ─────────────────────────────────────

View File

@@ -858,6 +858,7 @@ body {
border-radius: var(--radius);
padding: 2px;
box-sizing: border-box;
position: relative;
}
.view-toggle-btn {
@@ -893,6 +894,42 @@ body {
color: var(--bg);
}
/* === View Toggle Overflow Dropdown === */
.view-toggle-overflow-menu {
position: absolute;
top: 100%;
right: 0;
margin-top: var(--space-xs);
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
min-width: 140px;
z-index: 200;
padding: var(--space-xs) 0;
}
.view-toggle-overflow-item {
display: flex;
align-items: center;
gap: var(--space-sm);
width: 100%;
padding: var(--space-sm) var(--space-md);
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 13px;
text-align: left;
transition: all var(--transition-fast);
}
.view-toggle-overflow-item:hover {
background: var(--card-hover);
color: var(--text);
}
.view-toggle-overflow-item.active {
color: var(--todo);
}
/* Header Search */
.header-search {
display: flex;