diff --git a/packages/dashboard/app/components/Board.css b/packages/dashboard/app/components/Board.css index 364e63e008..aa8a71c027 100644 --- a/packages/dashboard/app/components/Board.css +++ b/packages/dashboard/app/components/Board.css @@ -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; diff --git a/packages/dashboard/app/components/Board.tsx b/packages/dashboard/app/components/Board.tsx index 0bc472b70c..aef57d3242 100644 --- a/packages/dashboard/app/components/Board.tsx +++ b/packages/dashboard/app/components/Board.tsx @@ -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>(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 = ( - - {t("dashboard.taskCount", "{{count}} task{{plural}}", { count: tasks.length, plural: tasks.length === 1 ? "" : "s" })} - - )} - /> - ); if (shouldGateLegacyBoard) { - return ( -
- {dashboardHeader} - -
- ); + return ; } if (workflowMode && selectedWorkflow) { @@ -573,22 +549,20 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask : null; return ( -
- {dashboardHeader} -
- {workflowControlsInHeader && headerWorkflowSlot ? relocatedWorkflowToolbar : workflowToolbar} -
{ - const id = (e.target as HTMLElement)?.closest?.("[data-id]")?.getAttribute("data-id"); - if (id) draggingTaskIdRef.current = id; - }} - onDragEnd={() => { - draggingTaskIdRef.current = null; - }} - > +
+ {workflowControlsInHeader && headerWorkflowSlot ? relocatedWorkflowToolbar : workflowToolbar} +
{ + 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} /> )} -
-
+
); } return ( -
- {dashboardHeader} + <>
{COLUMNS.map((col) => ( ))}
-
+ ); } diff --git a/packages/dashboard/app/components/ChatView.css b/packages/dashboard/app/components/ChatView.css index 414ae2c645..25b17c60ff 100644 --- a/packages/dashboard/app/components/ChatView.css +++ b/packages/dashboard/app/components/ChatView.css @@ -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; diff --git a/packages/dashboard/app/components/ProjectOverview.css b/packages/dashboard/app/components/ProjectOverview.css index b794507f59..931e7fc8d3 100644 --- a/packages/dashboard/app/components/ProjectOverview.css +++ b/packages/dashboard/app/components/ProjectOverview.css @@ -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 --- */ diff --git a/packages/dashboard/app/components/ProjectOverview.tsx b/packages/dashboard/app/components/ProjectOverview.tsx index 434a908d39..ecff3de35e 100644 --- a/packages/dashboard/app/components/ProjectOverview.tsx +++ b/packages/dashboard/app/components/ProjectOverview.tsx @@ -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 = ( + + + {t("projects.addProject", "Add Project")} + + )} + /> + ); // Show skeleton while loading if (needsInitialSkeleton) { - return ; + return ( +
+ {dashboardHeader} +
+ +
+
+ ); } // Empty state when no projects if (projects.length === 0) { return ( -
-
-
- +
+ {dashboardHeader} +
+
+
+ +
+

{t("projects.noProjectsFound", "No Projects Found")}

+

+ {t("projects.emptyStateDescription", "Get started by adding your first project. Projects allow you to organize and track tasks across multiple repositories.")} +

+
-

{t("projects.noProjectsFound", "No Projects Found")}

-

- {t("projects.emptyStateDescription", "Get started by adding your first project. Projects allow you to organize and track tasks across multiple repositories.")} -

-
); @@ -267,12 +297,10 @@ export function ProjectOverview({ return (
+ {dashboardHeader} +
{/* Header with stats */}
-

- - {t("projects.title", "Projects")} -

@@ -324,13 +352,6 @@ export function ProjectOverview({
)}
-
{/* Filter tabs */} @@ -450,6 +471,7 @@ export function ProjectOverview({
)} +
); } diff --git a/packages/dashboard/app/components/__tests__/ChatView.test.tsx b/packages/dashboard/app/components/__tests__/ChatView.test.tsx index 58f4fc0e75..a39a6c3c5f 100644 --- a/packages/dashboard/app/components/__tests__/ChatView.test.tsx +++ b/packages/dashboard/app/components/__tests__/ChatView.test.tsx @@ -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"); diff --git a/packages/dashboard/app/components/__tests__/ProjectOverview.test.tsx b/packages/dashboard/app/components/__tests__/ProjectOverview.test.tsx index 582882e953..62ae5f9b10 100644 --- a/packages/dashboard/app/components/__tests__/ProjectOverview.test.tsx +++ b/packages/dashboard/app/components/__tests__/ProjectOverview.test.tsx @@ -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", () => {