fix(dashboard): move header to overview

This commit is contained in:
gsxdsm
2026-06-22 16:21:03 -07:00
parent ce6c0fb0b0
commit 45842a3318
7 changed files with 107 additions and 109 deletions

View File

@@ -2,35 +2,6 @@
FNXC:BoardWorkflows 2026-06-20-09:02:
The board needs a neutral first-paint shell whenever workflow lanes are enabled but metadata is not ready, so users never see the legacy single-lane board flash before workflow lanes load.
*/
.dashboard-board-view {
display: flex;
flex: 1 1 auto;
flex-direction: column;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
background: var(--bg);
}
/*
FNXC:DashboardHeader 2026-06-22-16:05:
The Dashboard board now sits under the shared ViewHeader like Artifacts/Skills. The board remains the scroll container below the header, so reset the old full-height assumption within this shell and let flex allocate the remaining height.
*/
.dashboard-board-view > .board,
.dashboard-board-view .board-workflow-view {
flex: 1 1 auto;
height: auto;
min-height: 0;
}
.dashboard-board-view__count {
font-size: 14px;
color: var(--text-muted);
white-space: nowrap;
}
.board.board-workflows-skeleton {
display: flex;
align-items: stretch;

View File

@@ -16,9 +16,6 @@ import { WorkflowSwitcher } from "./WorkflowSwitcher";
import { computeWorkflowStatusCounts } from "./workflowStatusCounts";
import { writeBoardWorkflowsCache } from "../utils/boardWorkflowsCache";
import { useBoardWorkflows } from "../hooks/useBoardWorkflows";
import { useTranslation } from "react-i18next";
import { LayoutDashboard } from "lucide-react";
import { ViewHeader } from "./ViewHeader";
interface BoardProps {
tasks: Task[];
@@ -149,7 +146,6 @@ function BoardWorkflowSkeleton({ empty = false }: { empty?: boolean }) {
}
export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask, onOpenDetail, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onDeleteTask, onArchiveAllDone, onLoadArchivedTasks, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, taskStuckTimeoutMs, onOpenMission, staleHighFanoutBlockerAgeThresholdMs, lastFetchTimeMs, prAuthAvailable, onOpenWorkflowEditor, onCreateWorkflow, workflowColumnsEnabled, settingsLoaded, workflowControlsInHeader = false }: BoardProps) {
const { t } = useTranslation("app");
const [archivedCollapsed, setArchivedCollapsed] = useState(true);
const archivedLoadedRef = useRef(false);
const [workflowStepNameLookup, setWorkflowStepNameLookup] = useState<ReadonlyMap<string, string>>(EMPTY_WORKFLOW_STEP_NAME_LOOKUP);
@@ -519,29 +515,9 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
const shouldGateLegacyBoard = boardWorkflows === null
? (workflowColumnsEnabled === true || settingsLoaded === false)
: boardWorkflows.flagEnabled === true && boardWorkflows.workflows.length === 0;
/*
FNXC:DashboardHeader 2026-06-22-16:05:
Dashboard/Board is a first-class left-sidebar view, so it needs the same canonical ViewHeader chrome as Artifacts, Skills, and Goals instead of letting the board columns touch the top app chrome. Keep the board itself as the scroll owner below the header so horizontal lane scrolling and mobile snap behavior are unchanged.
*/
const dashboardHeader = (
<ViewHeader
icon={LayoutDashboard}
title={t("dashboard.title", "Dashboard")}
actions={(
<span className="dashboard-board-view__count">
{t("dashboard.taskCount", "{{count}} task{{plural}}", { count: tasks.length, plural: tasks.length === 1 ? "" : "s" })}
</span>
)}
/>
);
if (shouldGateLegacyBoard) {
return (
<div className="dashboard-board-view">
{dashboardHeader}
<BoardWorkflowSkeleton empty={boardWorkflows?.flagEnabled === true} />
</div>
);
return <BoardWorkflowSkeleton empty={boardWorkflows?.flagEnabled === true} />;
}
if (workflowMode && selectedWorkflow) {
@@ -573,22 +549,20 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
: null;
return (
<div className="dashboard-board-view">
{dashboardHeader}
<div className="board-workflow-view">
{workflowControlsInHeader && headerWorkflowSlot ? relocatedWorkflowToolbar : workflowToolbar}
<main
className="board board-workflow-columns"
id="board"
ref={boardRef}
onDragStart={(e) => {
const id = (e.target as HTMLElement)?.closest?.("[data-id]")?.getAttribute("data-id");
if (id) draggingTaskIdRef.current = id;
}}
onDragEnd={() => {
draggingTaskIdRef.current = null;
}}
>
<div className="board-workflow-view">
{workflowControlsInHeader && headerWorkflowSlot ? relocatedWorkflowToolbar : workflowToolbar}
<main
className="board board-workflow-columns"
id="board"
ref={boardRef}
onDragStart={(e) => {
const id = (e.target as HTMLElement)?.closest?.("[data-id]")?.getAttribute("data-id");
if (id) draggingTaskIdRef.current = id;
}}
onDragEnd={() => {
draggingTaskIdRef.current = null;
}}
>
{selectedWorkflowColumns.map((columnDef) => {
const isCreateColumn = columnDef.id === selectedWorkflowCreateColumnId;
return (
@@ -683,15 +657,13 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
onToggleCollapse={handleToggleArchivedCollapse}
/>
)}
</main>
</div>
</main>
</div>
);
}
return (
<div className="dashboard-board-view">
{dashboardHeader}
<>
<main className="board" id="board" ref={boardRef}>
{COLUMNS.map((col) => (
<Column
@@ -734,6 +706,6 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
/>
))}
</main>
</div>
</>
);
}

View File

@@ -81,8 +81,10 @@ Direct/Rooms now lives in the Chat ViewHeader immediately before New Chat. The c
width: clamp(128px, 24vw, 220px);
min-width: min(128px, 100%);
max-width: 220px;
padding: 0;
border-bottom: none;
padding: 2px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface);
}
.chat-sidebar-scope-btn {
@@ -101,6 +103,8 @@ Direct/Rooms now lives in the Chat ViewHeader immediately before New Chat. The c
min-height: 0;
height: var(--view-header-content-row, 28px);
padding: 0 clamp(var(--space-xs), 1.2vw, var(--space-sm));
border: 1px solid transparent;
border-radius: calc(var(--radius-md) - 2px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -122,6 +126,10 @@ Direct/Rooms now lives in the Chat ViewHeader immediately before New Chat. The c
color: var(--text);
}
.chat-view-header-scope-toggle .chat-sidebar-scope-btn--active {
border-color: var(--todo);
}
.chat-sidebar-rooms {
flex: 1;
min-height: 0;

View File

@@ -4,17 +4,31 @@
flex-direction: column;
flex: 1;
min-height: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: var(--bg);
}
/*
FNXC:DashboardHeader 2026-06-22-16:42:
Dashboard overview uses the canonical ViewHeader at the top; the overview body owns padding, max-width, and vertical scrolling below that header so the header aligns with Artifacts/Skills while the project content keeps its readable width.
*/
.project-overview__body {
display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: var(--space-lg);
padding: var(--space-xl);
min-height: 0;
width: 100%;
max-width: 1400px;
margin: 0 auto;
width: 100%;
padding: var(--space-xl);
overflow-y: auto;
height: 100%;
-webkit-overflow-scrolling: touch;
}
.project-overview--empty {
.project-overview__body--empty {
display: flex;
align-items: center;
justify-content: center;
@@ -124,6 +138,7 @@
display: inline-flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-xs) var(--space-sm);
}
/* --- Filter Tabs --- */

View File

@@ -8,6 +8,7 @@ import { ProjectCard } from "./ProjectCard";
import { getNodeMappingsForProject, resolveNodeDisplayName } from "../utils/nodeProjectAssignment";
import { ProjectGridSkeleton } from "./ProjectGridSkeleton";
import { useProjectHealth } from "../hooks/useProjectHealth";
import { ViewHeader } from "./ViewHeader";
export interface ProjectOverviewProps {
projects: ProjectInfoWithSource[];
@@ -235,31 +236,60 @@ export function ProjectOverview({
// 2. Projects exist but we haven't fetched health data yet (healthLoading with no data)
// Don't show skeleton during background health polling when health data already exists
const needsInitialSkeleton = loading || (healthLoading && projects.length > 0 && Object.keys(healthMap).length === 0);
/*
FNXC:DashboardHeader 2026-06-22-16:42:
The Dashboard overview (projects, stats, filters, and charts/overview content) owns the shared top header. The Board view must stay headerless because its columns already consume the full board surface.
*/
const dashboardHeader = (
<ViewHeader
icon={LayoutGrid}
title={t("dashboard.title", "Dashboard")}
actions={(
<button
className="btn btn-primary btn-sm project-overview__add-btn"
onClick={onAddProject}
>
<Plus size={14} />
{t("projects.addProject", "Add Project")}
</button>
)}
/>
);
// Show skeleton while loading
if (needsInitialSkeleton) {
return <ProjectGridSkeleton />;
return (
<div className="project-overview">
{dashboardHeader}
<div className="project-overview__body">
<ProjectGridSkeleton />
</div>
</div>
);
}
// Empty state when no projects
if (projects.length === 0) {
return (
<div className="project-overview project-overview--empty">
<div className="project-empty-state">
<div className="project-empty-state__icon">
<Inbox size={48} />
<div className="project-overview">
{dashboardHeader}
<div className="project-overview__body project-overview__body--empty">
<div className="project-empty-state">
<div className="project-empty-state__icon">
<Inbox size={48} />
</div>
<h2 className="project-empty-state__title">{t("projects.noProjectsFound", "No Projects Found")}</h2>
<p className="project-empty-state__description">
{t("projects.emptyStateDescription", "Get started by adding your first project. Projects allow you to organize and track tasks across multiple repositories.")}
</p>
<button
className="btn btn-primary project-empty-state__cta"
onClick={onAddProject}
>
<Plus size={16} />
{t("projects.addFirstProject", "Add Your First Project")}
</button>
</div>
<h2 className="project-empty-state__title">{t("projects.noProjectsFound", "No Projects Found")}</h2>
<p className="project-empty-state__description">
{t("projects.emptyStateDescription", "Get started by adding your first project. Projects allow you to organize and track tasks across multiple repositories.")}
</p>
<button
className="btn btn-primary project-empty-state__cta"
onClick={onAddProject}
>
<Plus size={16} />
{t("projects.addFirstProject", "Add Your First Project")}
</button>
</div>
</div>
);
@@ -267,12 +297,10 @@ export function ProjectOverview({
return (
<div className="project-overview">
{dashboardHeader}
<div className="project-overview__body">
{/* Header with stats */}
<div className="project-overview__header">
<h2 className="project-overview__title">
<LayoutGrid size={20} />
{t("projects.title", "Projects")}
</h2>
<div className="project-overview__stats">
<div className="project-stat">
<div className="project-stat__icon">
@@ -324,13 +352,6 @@ export function ProjectOverview({
</div>
)}
</div>
<button
className="btn btn-primary project-overview__add-btn"
onClick={onAddProject}
>
<Plus size={16} />
{t("projects.addProject", "Add Project")}
</button>
</div>
{/* Filter tabs */}
@@ -450,6 +471,7 @@ export function ProjectOverview({
</button>
</div>
)}
</div>
</div>
);
}

View File

@@ -3226,6 +3226,16 @@ describe("ChatView CSS — active state edge highlights", () => {
expect(activeScopeRule).not.toContain("inset");
});
it("renders the header Direct/Rooms toggle with visible borders", async () => {
const headerScopeRule = findRule(".chat-view-header-scope-toggle");
const headerScopeButtonRule = findRule(".chat-view-header-scope-toggle .chat-sidebar-scope-btn");
const headerActiveScopeRule = findRule(".chat-view-header-scope-toggle .chat-sidebar-scope-btn--active");
expect(headerScopeRule).toContain("border: 1px solid var(--border)");
expect(headerScopeButtonRule).toContain("border: 1px solid transparent");
expect(headerActiveScopeRule).toContain("border-color: var(--todo)");
});
it("keeps active chat-row background without the removed left edge or offset", async () => {
const activeSessionRule = findRule(".chat-session-item--active");

View File

@@ -131,7 +131,7 @@ describe("ProjectOverview", () => {
/>
);
expect(screen.getByText("Projects")).toBeDefined();
expect(screen.getByRole("heading", { name: /Dashboard/ })).toBeDefined();
});
it("displays project cards when projects provided", () => {