fix(dashboard): make workflow and memory headers breathe
This commit is contained in:
@@ -15,6 +15,9 @@ The header row now comes from the shared .view-header (which supplies the --spac
|
||||
|
||||
FNXC:ViewHeader 2026-06-23-03:45:
|
||||
The shared ViewHeader now owns the single border-bottom divider + surface background, so this wrapper drops its own border-bottom/background to avoid a doubled divider under the title row (the divider sits directly under the canonical title row, and the controls row reads as body content below it). The controls row keeps its own side/bottom padding so the tab bar/search stay aligned.
|
||||
|
||||
FNXC:ViewHeader 2026-06-22-12:00:
|
||||
Artifacts controls are the first page content below the shared header, so add a top inset there instead of on the header wrapper. This keeps the tab/search row from bumping against the title divider while preserving the existing body alignment.
|
||||
*/
|
||||
.documents-view-header {
|
||||
background: var(--surface);
|
||||
@@ -30,7 +33,7 @@ The shared ViewHeader now owns the single border-bottom divider + surface backgr
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
flex-wrap: nowrap;
|
||||
padding: 0 var(--space-lg) var(--space-lg);
|
||||
padding: var(--space-lg) var(--space-lg) var(--space-lg);
|
||||
}
|
||||
|
||||
.documents-tab-bar {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* === 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.
|
||||
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 tab bar and content area carry their own horizontal inset so they align under the header.
|
||||
*/
|
||||
.memory-view {
|
||||
display: flex;
|
||||
@@ -12,16 +12,8 @@ The title row now comes from the shared .view-header (which supplies the --space
|
||||
|
||||
/*
|
||||
FNXC:Navigation 2026-06-22-02:30:
|
||||
After the header migrated to the shared .view-header (which is flex-shrink:0), the sibling description and tab bar must also be flex-shrink:0. Without it they collapse under the flex column at constrained heights, letting .memory-view-content overlap the header/tabs. The scroll owner is the active tab pane (.memory-*-tab, flex:1 + min-height:0 + overflow-y:auto); the editor container keeps min-height:0 through the chain so CodeMirror bounds itself and never overruns the action bar.
|
||||
After the header migrated to the shared .view-header (which is flex-shrink:0), the sibling tab bar must also be flex-shrink:0. Without it the tabs collapse under the flex column at constrained heights, letting .memory-view-content overlap the header/tabs. The scroll owner is the active tab pane (.memory-*-tab, flex:1 + min-height:0 + overflow-y:auto); the editor container keeps min-height:0 through the chain so CodeMirror bounds itself and never overruns the action bar.
|
||||
*/
|
||||
.memory-view-description {
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
padding: 0 var(--space-lg);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.memory-view-tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -476,7 +468,6 @@ The memory editor box is CAPPED to about a page (max-height: 60vh) so a long mem
|
||||
/* Mobile responsive for memory view */
|
||||
@media (max-width: 768px) {
|
||||
/* 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);
|
||||
}
|
||||
@@ -524,4 +515,3 @@ The memory editor box is CAPPED to about a page (max-height: 60vh) so a long mem
|
||||
padding-top: var(--space-md);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -352,12 +352,12 @@ export function MemoryView({ projectId, addToast, onSendSelectionToTask }: Memor
|
||||
<div className="memory-view">
|
||||
{/*
|
||||
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.
|
||||
Memory adopts the shared ViewHeader (CC-modeled) for a consistent main-content title row.
|
||||
|
||||
FNXC:Memory 2026-06-22-12:00:
|
||||
The Memory view header should be title-only; remove the "Working memory, long-term insights, and engine status" subtitle so the tab bar becomes the first content under the header.
|
||||
*/}
|
||||
<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">
|
||||
|
||||
@@ -129,6 +129,76 @@ embedded view.
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:WorkflowSidebar 2026-06-22-12:00:
|
||||
Workflow authors need to reclaim horizontal graph-editing space without leaving
|
||||
the workflow view. Hide only the sidebar shell when collapsed and keep the
|
||||
restore control attached to the canvas so the state is always reversible.
|
||||
*/
|
||||
.wf-editor-body--sidebar-collapsed .wf-editor-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wf-editor-sidebar-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wf-editor-sidebar-head .wf-editor-new {
|
||||
flex: 1 1 auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wf-sidebar-shell-toggle,
|
||||
.wf-sidebar-shell-restore {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-xs);
|
||||
min-height: 30px;
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface);
|
||||
color: var(--text-muted);
|
||||
box-shadow: var(--shadow-sm);
|
||||
font: inherit;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
|
||||
.wf-sidebar-shell-toggle {
|
||||
flex: 0 0 auto;
|
||||
width: 30px;
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.wf-sidebar-shell-restore {
|
||||
position: absolute;
|
||||
top: var(--space-sm);
|
||||
left: var(--space-sm);
|
||||
z-index: 6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wf-sidebar-shell-toggle:hover,
|
||||
.wf-sidebar-shell-restore:hover {
|
||||
border-color: var(--accent);
|
||||
background: var(--surface-hover);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.wf-sidebar-shell-toggle:focus-visible,
|
||||
.wf-sidebar-shell-restore:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
/* U12: columns + fields authoring sections moved into the left sidebar, below
|
||||
the workflow list. Each is a collapsible disclosure whose toggle button is the
|
||||
section header; the panels' own internal <h3> is suppressed to avoid a double
|
||||
@@ -321,6 +391,7 @@ embedded view.
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wf-editor-mobile-back {
|
||||
@@ -430,6 +501,20 @@ embedded view.
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:WorkflowToolbar 2026-06-22-12:00:
|
||||
Toolbar commands such as Export should move as whole controls when space is
|
||||
tight; do not allow their short labels to split onto multiple lines.
|
||||
*/
|
||||
.wf-editor-toolbar .wf-editor-action,
|
||||
.wf-editor-toolbar .wf-editor-delete,
|
||||
.wf-editor-toolbar .wf-editor-save,
|
||||
.wf-editor-readonly-banner .wf-editor-action,
|
||||
.wf-editor-readonly-banner .wf-editor-save {
|
||||
white-space: nowrap;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
|
||||
.wf-editor-readonly-note {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-dim);
|
||||
|
||||
@@ -824,10 +824,25 @@ function InnerEditor({
|
||||
// U12: the columns/fields authoring panels live in the left sidebar (below the
|
||||
// workflow list) as collapsible disclosure sections. Each section's collapsed
|
||||
// state persists in localStorage; default expanded.
|
||||
/*
|
||||
FNXC:WorkflowSidebar 2026-06-22-12:00:
|
||||
The workflow view needs the entire left sidebar collapsible, not only its
|
||||
internal column/field/settings groups, so graph editing can use the full
|
||||
canvas width. Persist the shell state and keep a visible restore control in
|
||||
the canvas area when the sidebar is hidden.
|
||||
*/
|
||||
const sidebarCollapsedStorageKey = "fusion:wf-left-sidebar-collapsed";
|
||||
const columnsCollapsedStorageKey = "fusion:wf-sidebar-columns-collapsed";
|
||||
const fieldsCollapsedStorageKey = "fusion:wf-sidebar-fields-collapsed";
|
||||
const settingsCollapsedStorageKey = "fusion:wf-sidebar-settings-collapsed";
|
||||
const optionalStepsCollapsedStorageKey = "fusion:wf-sidebar-optional-steps-collapsed";
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState<boolean>(() => {
|
||||
try {
|
||||
return localStorage.getItem(sidebarCollapsedStorageKey) === "1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const [columnsCollapsed, setColumnsCollapsed] = useState<boolean>(() => {
|
||||
try {
|
||||
return localStorage.getItem(columnsCollapsedStorageKey) === "1";
|
||||
@@ -856,6 +871,13 @@ function InnerEditor({
|
||||
return false;
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(sidebarCollapsedStorageKey, sidebarCollapsed ? "1" : "0");
|
||||
} catch {
|
||||
// localStorage unavailable (private mode / SSR): non-fatal.
|
||||
}
|
||||
}, [sidebarCollapsed]);
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(columnsCollapsedStorageKey, columnsCollapsed ? "1" : "0");
|
||||
@@ -2540,17 +2562,32 @@ function InnerEditor({
|
||||
simpleLayoutEnabled ? " wf-editor-body--simple-layout" : ""
|
||||
}${mobileNodeDetailStage ? " wf-editor-body--mobile-node-detail" : ""}${
|
||||
mobileEdgeDetailStage ? " wf-editor-body--mobile-edge-detail" : ""
|
||||
}`}
|
||||
}${sidebarCollapsed ? " wf-editor-body--sidebar-collapsed" : ""}`}
|
||||
>
|
||||
<aside className="wf-editor-sidebar">
|
||||
<button
|
||||
className="wf-editor-new"
|
||||
ref={newWorkflowBtnRef}
|
||||
data-testid="wf-new-workflow"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<Plus size={14} /> {t("workflows.newWorkflow", "New workflow")}
|
||||
</button>
|
||||
<div className="wf-editor-sidebar-head">
|
||||
<button
|
||||
className="wf-editor-new"
|
||||
ref={newWorkflowBtnRef}
|
||||
data-testid="wf-new-workflow"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<Plus size={14} /> {t("workflows.newWorkflow", "New workflow")}
|
||||
</button>
|
||||
{!isMobileMode && (
|
||||
<button
|
||||
type="button"
|
||||
className="wf-sidebar-shell-toggle"
|
||||
data-testid="wf-sidebar-collapse"
|
||||
aria-expanded={!sidebarCollapsed}
|
||||
aria-label={t("workflows.collapseSidebar", "Collapse workflow sidebar")}
|
||||
title={t("workflows.collapseSidebar", "Collapse workflow sidebar")}
|
||||
onClick={() => setSidebarCollapsed(true)}
|
||||
>
|
||||
<ChevronLeft size={14} aria-hidden />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{/* U5/R10: keyboard-accessible import affordance triggering a hidden
|
||||
file input; validation failures render in the persistent inline
|
||||
region below (role="alert"), not a toast. */}
|
||||
@@ -2717,6 +2754,20 @@ function InnerEditor({
|
||||
</aside>
|
||||
|
||||
<section className="wf-editor-canvas-wrap">
|
||||
{sidebarCollapsed && !isMobileMode && (
|
||||
<button
|
||||
type="button"
|
||||
className="wf-sidebar-shell-restore"
|
||||
data-testid="wf-sidebar-restore"
|
||||
aria-expanded="false"
|
||||
aria-label={t("workflows.showSidebar", "Show workflow sidebar")}
|
||||
title={t("workflows.showSidebar", "Show workflow sidebar")}
|
||||
onClick={() => setSidebarCollapsed(false)}
|
||||
>
|
||||
<ChevronRight size={14} aria-hidden />
|
||||
<span>{t("workflows.sidebar", "Workflows")}</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="wf-editor-mobile-back"
|
||||
|
||||
@@ -141,6 +141,14 @@ describe("WorkflowNodeEditor sidebar overflow CSS contract", () => {
|
||||
expect(listStageSidebarRule).toMatch(/min-width\s*:\s*0\s*;/);
|
||||
expect(listStageSidebarRule).toMatch(/overflow-x\s*:\s*hidden\s*;/);
|
||||
expect(listStageSidebarRule).toMatch(/overflow-y\s*:\s*auto\s*;/);
|
||||
|
||||
const collapsedSidebarRule = findRule([editorCss], /\.wf-editor-body--sidebar-collapsed \.wf-editor-sidebar\s*\{[^}]*\}/);
|
||||
expect(collapsedSidebarRule).toMatch(/display\s*:\s*none\s*;/);
|
||||
|
||||
const restoreRule = findRule([editorCss], /\.wf-sidebar-shell-restore\s*\{[^}]*\}/);
|
||||
expect(restoreRule).toMatch(/position\s*:\s*absolute\s*;/);
|
||||
expect(restoreRule).toMatch(/left\s*:\s*var\(--space-sm\)\s*;/);
|
||||
expect(restoreRule).toMatch(/white-space\s*:\s*nowrap\s*;/);
|
||||
});
|
||||
|
||||
it("FN-6379 keeps sidebar children from forcing horizontal scroll", () => {
|
||||
@@ -165,6 +173,13 @@ describe("WorkflowNodeEditor sidebar overflow CSS contract", () => {
|
||||
expect(paletteButtonRule).toMatch(/min-width\s*:\s*0\s*;/);
|
||||
expect(paletteButtonRule).toMatch(/overflow-wrap\s*:\s*anywhere\s*;/);
|
||||
|
||||
const actionNoWrapRule = findRule(
|
||||
[editorCss],
|
||||
/\.wf-editor-toolbar \.wf-editor-action,\s*\.wf-editor-toolbar \.wf-editor-delete,\s*\.wf-editor-toolbar \.wf-editor-save,\s*\.wf-editor-readonly-banner \.wf-editor-action,\s*\.wf-editor-readonly-banner \.wf-editor-save\s*\{[^}]*\}/,
|
||||
);
|
||||
expect(actionNoWrapRule).toMatch(/white-space\s*:\s*nowrap\s*;/);
|
||||
expect(actionNoWrapRule).toMatch(/overflow-wrap\s*:\s*normal\s*;/);
|
||||
|
||||
const sidebarCodeRule = findRule([editorCss], /\.wf-editor-sidebar \.wf-code-source\s*\{[^}]*\}/);
|
||||
expect(sidebarCodeRule).toMatch(/overflow-x\s*:\s*hidden\s*;/);
|
||||
expect(sidebarCodeRule).toMatch(/overflow-wrap\s*:\s*anywhere\s*;/);
|
||||
|
||||
@@ -413,6 +413,7 @@ describe("WorkflowNodeEditor", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.removeItem("fusion:wf-left-sidebar-collapsed");
|
||||
localStorage.removeItem("fusion:wf-sidebar-settings-collapsed");
|
||||
localStorage.removeItem("fusion:wf-templates-collapsed");
|
||||
cleanup();
|
||||
@@ -438,6 +439,28 @@ describe("WorkflowNodeEditor", () => {
|
||||
expect(screen.getAllByRole("button", { name: "QA" })[0]).toHaveClass("active");
|
||||
});
|
||||
|
||||
it("lets desktop users collapse and restore the workflow sidebar", async () => {
|
||||
vi.mocked(fetchWorkflows).mockResolvedValue([def()]);
|
||||
|
||||
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
|
||||
|
||||
expect(await screen.findByTestId("wf-workflow-name")).toHaveTextContent("QA");
|
||||
const body = screen.getByTestId("wf-new-workflow").closest(".wf-editor-body");
|
||||
expect(body).not.toBeNull();
|
||||
expect(body!).not.toHaveClass("wf-editor-body--sidebar-collapsed");
|
||||
expect(screen.queryByTestId("wf-sidebar-restore")).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId("wf-sidebar-collapse"));
|
||||
|
||||
expect(body!).toHaveClass("wf-editor-body--sidebar-collapsed");
|
||||
expect(screen.getByTestId("wf-sidebar-restore")).toHaveTextContent("Workflows");
|
||||
|
||||
fireEvent.click(screen.getByTestId("wf-sidebar-restore"));
|
||||
|
||||
expect(body!).not.toHaveClass("wf-editor-body--sidebar-collapsed");
|
||||
expect(screen.queryByTestId("wf-sidebar-restore")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("lets users collapse and restore the workflow mini map", async () => {
|
||||
vi.mocked(fetchWorkflows).mockResolvedValue([def()]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user