feat(dashboard): consistent ViewHeader on Artifacts/Research/Evals/Goals/Memory

Normalize these main-content views onto the shared ViewHeader (icon + CC-style title; existing header controls moved into the actions slot). Tests updated for the new header markup (95 pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-06-22 02:44:34 -07:00
parent efd659be0e
commit 0f98cf941f
12 changed files with 140 additions and 147 deletions

View File

@@ -9,33 +9,15 @@
min-height: 0;
}
/*
FNXC:Navigation 2026-06-22-01:10:
The header row now comes from the shared .view-header (which supplies the --space-lg top/side padding). The container only contributes the bottom border + surface; the controls row gets its own side/bottom padding so the tab bar/search stay aligned without doubling the ViewHeader inset.
*/
.documents-view-header {
padding: var(--space-lg);
border-bottom: 1px solid var(--border);
background: var(--surface);
}
.documents-view-title-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-md);
}
.documents-view-title {
display: flex;
align-items: center;
gap: var(--space-sm);
font-size: 18px;
font-weight: 600;
margin: 0;
color: var(--text);
}
.documents-view-title svg {
color: var(--todo);
}
.documents-view-count {
font-size: 14px;
color: var(--text-muted);
@@ -46,6 +28,7 @@
align-items: center;
gap: var(--space-md);
flex-wrap: nowrap;
padding: 0 var(--space-lg) var(--space-lg);
}
.documents-tab-bar {
@@ -847,25 +830,12 @@ The artifacts tab is a thumbnail-first responsive media gallery for agent-create
@media (max-width: 768px) {
/* Documents View mobile */
.documents-view-header {
padding: var(--space-md);
}
.documents-view-title-row {
flex-direction: column;
align-items: flex-start;
gap: var(--space-sm);
}
.documents-view-title {
font-size: 16px;
}
/* Documents View mobile: ViewHeader supplies its own responsive padding; only the controls row needs tightening here. */
.documents-controls-row {
flex-direction: column;
align-items: stretch;
gap: var(--space-sm);
padding: 0 var(--space-md) var(--space-md);
}
.documents-tab-bar {

View File

@@ -14,6 +14,7 @@ import { useSelectionComment } from "../hooks/useSelectionComment";
import { SelectionCommentPopover } from "./SelectionCommentPopover";
import { LoadingSpinner } from "./LoadingSpinner";
import { ArtifactMedia, getArtifactTypeLabel } from "./ArtifactMedia";
import { ViewHeader } from "./ViewHeader";
const MOBILE_BREAKPOINT = 768;
@@ -538,17 +539,21 @@ export function DocumentsView({ projectId, addToast, onOpenDetail, onSendSelecti
return (
<div className="documents-view">
{/*
FNXC:Navigation 2026-06-22-01:10:
Documents/Artifacts adopts the shared ViewHeader (CC-modeled) for a consistent main-content title row; the result count rides in the header actions while the tab bar, hidden-files toggle, and search stay in the controls row below.
FNXC:Navigation 2026-06-21-18:25: FN-6890 keeps the top-level title as Artifacts (renamed from Documents) without changing internal task-document tabs or artifact sub-tabs.
*/}
<div className="documents-view-header">
<div className="documents-view-title-row">
<h2 className="documents-view-title">
<FileText size={20} />
{/* FNXC:Navigation 2026-06-21-18:25: FN-6890 renames the top-level Documents view header to Artifacts without changing internal task-document tabs or artifact sub-tabs. */}
{t("documents.title", "Artifacts")}
</h2>
<span className="documents-view-count">
{t("documents.resultCount", "{{count}} result{{plural}}", { count: activeCount, plural: activeCount !== 1 ? "s" : "" })}
</span>
</div>
<ViewHeader
icon={FileText}
title={t("documents.title", "Artifacts")}
actions={(
<span className="documents-view-count">
{t("documents.resultCount", "{{count}} result{{plural}}", { count: activeCount, plural: activeCount !== 1 ? "s" : "" })}
</span>
)}
/>
<div className="documents-controls-row">
<div className="documents-tab-bar" role="tablist" aria-label={t("documents.sectionsLabel", "Documents sections")}>

View File

@@ -1,7 +1,17 @@
/*
FNXC:Navigation 2026-06-22-01:10:
The shared .view-header sits at the top of the Evals view; the two-column results/detail grid moves into .evals-view__body so the header keeps the standard --space-lg inset while the body retains its side/bottom padding.
*/
.evals-view {
display: flex;
flex-direction: column;
}
.evals-view__body {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: var(--space-lg);
padding: 0 var(--space-lg) var(--space-lg);
}
.evals-list,
@@ -80,8 +90,9 @@
}
@media (max-width: 768px) {
.evals-view {
.evals-view__body {
grid-template-columns: 1fr;
padding: 0 var(--space-md) var(--space-md);
}
.evals-toolbar {

View File

@@ -1,10 +1,11 @@
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { ExternalLink, RefreshCw, Settings } from "lucide-react";
import { ExternalLink, RefreshCw, Settings, Target } from "lucide-react";
import { fetchSettings } from "../api";
import { useEvals } from "../hooks/useEvals";
import type { SectionId } from "./SettingsModal";
import { LoadingSpinner } from "./LoadingSpinner";
import { ViewHeader } from "./ViewHeader";
import "./EvalsView.css";
interface EvalsViewProps {
@@ -51,7 +52,13 @@ export function EvalsView({ projectId, onOpenSettings, onOpenTaskDetail }: Evals
}
return (
/*
FNXC:Navigation 2026-06-22-01:10:
Evals adopts the shared ViewHeader (CC-modeled) so this main-content destination reads consistently with the others; the scored-results grid moves into a body wrapper beneath the header. The per-list Refresh control stays in the results toolbar.
*/
<section className="evals-view" data-testid="evals-view">
<ViewHeader icon={Target} title={t("evals.title", "Evals")} />
<div className="evals-view__body">
<div className="evals-list card">
<div className="evals-toolbar">
<input
@@ -140,6 +147,7 @@ export function EvalsView({ projectId, onOpenSettings, onOpenTaskDetail }: Evals
</>
)}
</div>
</div>
</section>
);
}

View File

@@ -2,31 +2,28 @@
FNXC:GoalsViewStyling 2026-06-20-01:33:
FN-6789 mounts Goals as a flex child of .project-content; grow, zero min-width, and use 100% width so the view fills the viewport instead of collapsing to intrinsic content width, mirroring the FN-6446 SecretsView fix.
*/
/*
FNXC:Navigation 2026-06-22-01:10:
The title row now comes from the shared .view-header (which supplies the --space-lg top/side padding). The root keeps scroll + sizing but drops its uniform padding; .goals-view__content carries the body's horizontal/bottom inset and the inter-block gap so cards stay aligned under the header.
*/
.goals-view {
display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: var(--space-lg);
height: 100%;
min-height: 0;
min-width: 0;
width: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: var(--space-lg);
}
.goals-header {
.goals-view__content {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-md);
}
.goals-title {
margin: 0;
color: var(--text);
font-size: calc(var(--space-lg) + var(--space-xs));
flex-direction: column;
gap: var(--space-lg);
min-width: 0;
padding: 0 var(--space-lg) var(--space-lg);
}
.goals-count {
@@ -286,11 +283,7 @@ FN-6828 keeps Goals action controls and linked-mission panels at intrinsic heigh
}
@media (max-width: 768px) {
.goals-header {
flex-direction: column;
align-items: stretch;
}
/* .goals-header removed: the shared .view-header wraps its actions cluster on narrow widths. */
.goals-card {
flex-direction: column;
align-items: stretch;

View File

@@ -1,10 +1,11 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import type { Goal } from "@fusion/core";
import { Link, Plus, Sparkles, X } from "lucide-react";
import { Link, Plus, Sparkles, Target, X } from "lucide-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { draftGoalDescription, getRefineErrorMessage } from "../api";
import { ViewHeader } from "./ViewHeader";
import "./GoalsView.css";
export interface GoalsViewProps {
@@ -430,18 +431,27 @@ export function GoalsView({ initialGoals, anchorGoalId, onNavigateToMission }: G
return (
<section className="goals-view" data-testid="goals-view">
<header className="goals-header">
<div>
<h2 className="goals-title">{t("goals.title", "Goals")}</h2>
<p className="goals-count" data-testid="goals-active-count">
{t("goals.activeCount", "{{count}} active goals", { count: activeCount })}
</p>
</div>
<button type="button" className="btn btn-primary goals-add-button" onClick={openAddForm} data-testid="goals-add-button">
<Plus aria-hidden="true" />
{t("goals.addGoal", "Add Goal")}
</button>
</header>
{/*
FNXC:Navigation 2026-06-22-01:10:
Goals adopts the shared ViewHeader (CC-modeled) for a consistent main-content title row; the Add Goal action and the active-goal count both ride in the header actions cluster so existing behavior and the goals-active-count test hook are preserved.
*/}
<ViewHeader
icon={Target}
title={t("goals.title", "Goals")}
actions={(
<>
<p className="goals-count" data-testid="goals-active-count">
{t("goals.activeCount", "{{count}} active goals", { count: activeCount })}
</p>
<button type="button" className="btn btn-primary goals-add-button" onClick={openAddForm} data-testid="goals-add-button">
<Plus aria-hidden="true" />
{t("goals.addGoal", "Add Goal")}
</button>
</>
)}
/>
{/* FNXC:Navigation 2026-06-22-01:12: Inner content keeps its own horizontal padding via .goals-view__content so it aligns with the ViewHeader inset after the root drops its uniform padding. */}
<div className="goals-view__content">
{isAddFormOpen ? (
<div className="card goals-form" data-testid="goals-form">
@@ -692,6 +702,7 @@ export function GoalsView({ initialGoals, anchorGoalId, onNavigateToMission }: G
))}
</div>
) : null}
</div>
</section>
);
}

View File

@@ -1,30 +1,20 @@
/* === MemoryView === */
/*
FNXC:Navigation 2026-06-22-01:10:
The title row now comes from the shared .view-header (which supplies the --space-lg top/side padding). The root drops its uniform padding; the description, tab bar, and content area carry their own horizontal inset so they align under the header.
*/
.memory-view {
display: flex;
flex-direction: column;
height: 100%;
padding: var(--space-lg);
overflow: hidden;
}
.memory-view-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--space-lg);
}
.memory-view-header h2 {
font-size: 18px;
color: var(--text);
margin: 0;
}
.memory-view-description {
color: var(--text-muted);
font-size: 13px;
margin: var(--space-xs) 0 0 0;
margin: 0;
padding: 0 var(--space-lg);
}
.memory-view-tabs {
@@ -32,7 +22,8 @@
flex-direction: row;
gap: var(--space-xs);
border-bottom: 1px solid var(--border);
margin-bottom: var(--space-lg);
margin: var(--space-md) 0 var(--space-lg);
padding: 0 var(--space-lg);
}
.memory-view-tab {
@@ -67,6 +58,7 @@
min-height: 0;
display: flex;
flex-direction: column;
padding: 0 var(--space-lg) var(--space-lg);
}
.memory-working-tab,
@@ -441,13 +433,14 @@
/* Mobile responsive for memory view */
@media (max-width: 768px) {
.memory-view {
padding: var(--space-md);
/* ViewHeader supplies its own responsive padding; the body blocks tighten their horizontal inset here. */
.memory-view-description,
.memory-view-tabs {
padding-inline: var(--space-md);
}
.memory-view-header {
flex-direction: column;
gap: var(--space-sm);
.memory-view-content {
padding: 0 var(--space-md) var(--space-md);
}
.memory-editor-container {

View File

@@ -1,10 +1,11 @@
import { useState, useMemo, useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Loader2 } from "lucide-react";
import { Brain, Loader2 } from "lucide-react";
import "./MemoryView.css";
import "./SettingsModal.css";
import type { MemoryFileInfo, MemoryRetrievalTestResult } from "../api";
import { FileEditor } from "./FileEditor";
import { ViewHeader } from "./ViewHeader";
import { useMemoryData } from "../hooks/useMemoryData";
interface MemoryViewProps {
@@ -349,15 +350,14 @@ export function MemoryView({ projectId, addToast, onSendSelectionToTask }: Memor
return (
<div className="memory-view">
{/* Header */}
<div className="memory-view-header">
<div>
<h2>{t("memory.title", "Memory")}</h2>
<p className="memory-view-description">
{t("memory.description", "Working memory, long-term insights, and engine status")}
</p>
</div>
</div>
{/*
FNXC:Navigation 2026-06-22-01:10:
Memory adopts the shared ViewHeader (CC-modeled) for a consistent main-content title row; the prior description renders just below the header, and the Working/Insights/Engines tab bar stays beneath it.
*/}
<ViewHeader icon={Brain} title={t("memory.title", "Memory")} />
<p className="memory-view-description">
{t("memory.description", "Working memory, long-term insights, and engine status")}
</p>
{/* Tab bar */}
<div className="memory-view-tabs" role="tablist">

View File

@@ -2,6 +2,10 @@
FNXC:ResearchViewStyling 2026-06-20-01:33:
FN-6789 mounts Research as a flex child of .project-content; grow, zero min-width, and use 100% width so the view fills the viewport instead of collapsing to intrinsic content width, mirroring the FN-6446 SecretsView fix.
*/
/*
FNXC:Navigation 2026-06-22-01:10:
The title row now comes from the shared .view-header, which supplies the --space-lg top/side padding. The root drops its top/side inset (keeping a bottom inset) and the remaining body blocks (subtitle, layout, setup state) carry their own horizontal padding so they stay aligned with the header.
*/
.research-view {
display: flex;
flex: 1 1 auto;
@@ -12,31 +16,19 @@ FN-6789 mounts Research as a flex child of .project-content; grow, zero min-widt
min-width: 0;
width: 100%;
overflow: hidden;
padding: var(--space-lg);
padding-bottom: var(--space-lg);
}
.research-view__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-md);
min-width: 0;
}
.research-view__header-actions {
flex-shrink: 0;
}
.research-view__title {
margin: 0;
padding: 0 0 var(--space-lg);
}
.research-view__subtitle {
margin: var(--space-xs) 0 0;
margin: 0;
padding-inline: var(--space-lg);
color: var(--text-muted);
}
.research-view__state {
margin-inline: var(--space-lg);
}
.research-view__layout {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
@@ -44,6 +36,7 @@ FN-6789 mounts Research as a flex child of .project-content; grow, zero min-widt
min-height: 0;
flex: 1;
overflow: hidden;
padding-inline: var(--space-lg);
}
.research-view__sidebar,
@@ -256,10 +249,18 @@ FN-6789 mounts Research as a flex child of .project-content; grow, zero min-widt
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
padding: var(--space-md);
padding: 0;
padding-bottom: calc(var(--space-md) + var(--mobile-nav-height) + env(safe-area-inset-bottom, 0px) + var(--standalone-bottom-gap));
}
.research-view__subtitle {
padding-inline: var(--space-md);
}
.research-view__state {
margin-inline: var(--space-md);
}
.research-view__layout {
display: flex;
flex-direction: column;
@@ -268,6 +269,7 @@ FN-6789 mounts Research as a flex child of .project-content; grow, zero min-widt
flex: initial;
min-height: auto;
overflow: visible;
padding-inline: var(--space-md);
}
.research-view__sidebar,
@@ -284,14 +286,6 @@ FN-6789 mounts Research as a flex child of .project-content; grow, zero min-widt
overflow: visible;
}
.research-view__header {
flex-direction: column;
}
.research-view__header-actions {
align-self: flex-start;
}
.research-view__stats {
grid-template-columns: minmax(0, 1fr);
}

View File

@@ -8,6 +8,7 @@ import { useResearch } from "../hooks/useResearch";
import type { ResearchProviderOption } from "../research-types";
import { ResearchTaskActionModal } from "./ResearchTaskActionModal";
import { LoadingSpinner } from "./LoadingSpinner";
import { ViewHeader } from "./ViewHeader";
import type { SectionId } from "./SettingsModal";
import "./ResearchView.css";
import { recordResumeEvent } from "../utils/resumeInstrumentation";
@@ -252,17 +253,20 @@ export function ResearchView({ projectId, addToast, onOpenSettings, readinessVer
return (
<section className="research-view" aria-label={t("research.viewLabel", "Research view")}>
<header className="research-view__header">
<div>
<h2 className="research-view__title">{t("research.title", "Research")}</h2>
<p className="research-view__subtitle">{t("research.subtitle", "Cited search and synthesis runs: gather sources, fetch content, and synthesize findings.")}</p>
</div>
<div className="research-view__header-actions">
{/*
FNXC:Navigation 2026-06-22-01:10:
Research adopts the shared ViewHeader (CC-modeled) for a consistent main-content title row; the Refresh action moves into the header actions cluster and the prior subtitle renders just below the header so the descriptive copy is preserved.
*/}
<ViewHeader
icon={Search}
title={t("research.title", "Research")}
actions={(
<button className="btn" type="button" onClick={() => void refresh()}>
{t("actions.refresh", "Refresh")}
</button>
</div>
</header>
)}
/>
<p className="research-view__subtitle">{t("research.subtitle", "Cited search and synthesis runs: gather sources, fetch content, and synthesize findings.")}</p>
{setupState ? (
<div className="research-view__state research-view__state--error card" data-testid="research-state-unavailable">

View File

@@ -14,6 +14,8 @@ vi.mock("lucide-react", () => ({
Link: () => <span data-testid="icon-link" />,
Plus: () => <span data-testid="icon-plus" />,
Sparkles: () => <span data-testid="icon-sparkles" />,
// Target backs the shared ViewHeader icon for the Goals view header (FNXC:Navigation 2026-06-22).
Target: () => <span data-testid="icon-target" />,
X: () => <span data-testid="icon-x" />,
}));

View File

@@ -23,6 +23,8 @@ vi.mock("../FileEditor", () => ({
vi.mock("lucide-react", () => ({
Loader2: () => <span data-testid="loader-icon" />,
// Brain backs the shared ViewHeader icon for the Memory view header (FNXC:Navigation 2026-06-22).
Brain: () => <span data-testid="icon-brain" />,
}));
function createMemoryData(overrides: Record<string, unknown> = {}) {