fix(dashboard): align view headers and pane spacing

This commit is contained in:
gsxdsm
2026-06-22 12:43:32 -07:00
parent 915d4b028a
commit ee9c8ab7d5
15 changed files with 228 additions and 100 deletions

View File

@@ -0,0 +1,3 @@
"@runfusion/fusion": patch
Align dashboard view chrome and inner-pane spacing across Chat, Mailbox, Workflows, Artifacts-adjacent controls, Goals, and Compound Engineering.

View File

@@ -2,12 +2,20 @@
.chat-view { .chat-view {
display: flex; display: flex;
flex: 1; flex-direction: column;
flex: 1 1 auto;
width: 100%; width: 100%;
height: 100%; height: 100%;
width: 100%; min-width: 0;
min-height: 0;
overflow: hidden;
}
.chat-view__body {
display: flex;
flex: 1 1 auto; flex: 1 1 auto;
min-width: 0; min-width: 0;
min-height: 0;
overflow: hidden; overflow: hidden;
} }
@@ -645,7 +653,7 @@ Mobile chat session switching needs a dedicated rename tap target beside each se
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.chat-thread-header-new-chat { .chat-view-header-new-chat {
flex-shrink: 0; flex-shrink: 0;
} }
@@ -1775,7 +1783,7 @@ Mobile chat session switching needs a dedicated rename tap target beside each se
thread takes the full viewport. The thread already renders a back thread takes the full viewport. The thread already renders a back
button (ChevronLeft) on mobile to flip back to the session list. */ button (ChevronLeft) on mobile to flip back to the session list. */
@media (max-width: 768px) { @media (max-width: 768px) {
.chat-view { .chat-view__body {
flex-direction: column; flex-direction: column;
} }
@@ -1889,10 +1897,6 @@ Mobile chat session switching needs a dedicated rename tap target beside each se
flex-shrink: 0; flex-shrink: 0;
} }
.chat-thread-header-new-chat {
display: none;
}
.chat-sidebar-scope-btn { .chat-sidebar-scope-btn {
min-height: calc(var(--space-lg) * 2.25); min-height: calc(var(--space-lg) * 2.25);
} }

View File

@@ -53,6 +53,7 @@ import { recordResumeEvent } from "../utils/resumeInstrumentation";
import { parseQuestionToolCall } from "../utils/parseQuestionToolCall"; import { parseQuestionToolCall } from "../utils/parseQuestionToolCall";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next"; import type { TFunction } from "i18next";
import { ViewHeader } from "./ViewHeader";
export interface ChatViewProps { export interface ChatViewProps {
projectId?: string; projectId?: string;
@@ -3183,10 +3184,27 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
return ( return (
/* /*
FNXC:Navigation 2026-06-22-01:10: FNXC:Chat 2026-06-22-12:55:
Chat deliberately does NOT adopt the shared ViewHeader. Unlike the other main-content views, .chat-view has no view-level title row to swap: its root is a height-sensitive two-pane flex-row (sidebar | thread) and each pane already owns its own header (chat-thread-header / chat-sidebar). The thread height is driven by flex:1 + the mobile-keyboard drift compensation applied directly to .chat-thread, so prepending a full-width column header would change the flex/height contract that those keyboard/scroll/resize paths depend on. Stacking a redundant "Chat" title above the existing thread header would also be visually duplicative. Consistency is therefore intentionally scoped to the views that have a single title row. Chat uses the shared ViewHeader so its page chrome matches the other main-content views. The height-sensitive two-pane chat layout remains isolated in .chat-view__body beneath that header, preserving sidebar resize, thread scrolling, and mobile keyboard compensation while moving the desktop New Chat action into the canonical header actions cluster.
*/ */
<div className="chat-view"> <div className="chat-view">
<ViewHeader
icon={MessageSquare}
title={t("chat.title", "Chat")}
actions={
!isMobile ? (
<button
className="btn btn-sm btn-primary chat-view-header-new-chat"
onClick={() => setShowNewDialog(true)}
data-testid="chat-new-btn"
>
<Plus size={14} />
{t("chat.newChat", "New Chat")}
</button>
) : null
}
/>
<div className="chat-view__body">
{/* Sidebar */} {/* Sidebar */}
<div <div
className={`chat-sidebar${!sidebarVisible ? " chat-sidebar--hidden" : ""}`} className={`chat-sidebar${!sidebarVisible ? " chat-sidebar--hidden" : ""}`}
@@ -3400,16 +3418,18 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
</div> </div>
) : null ) : null
) : ( ) : (
<div className="chat-sidebar-footer"> isMobile ? (
<button <div className="chat-sidebar-footer">
className="btn btn-sm btn-primary chat-sidebar-footer-btn" <button
onClick={() => setShowNewDialog(true)} className="btn btn-sm btn-primary chat-sidebar-footer-btn"
data-testid="chat-new-btn" onClick={() => setShowNewDialog(true)}
> data-testid="chat-new-btn"
<Plus size={14} /> >
{t("chat.newChat", "New Chat")} <Plus size={14} />
</button> {t("chat.newChat", "New Chat")}
</div> </button>
</div>
) : null
)} )}
</div> </div>
@@ -3890,17 +3910,6 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
{showAllAsPlain ? <EyeOff size={14} /> : <Eye size={14} />} {showAllAsPlain ? <EyeOff size={14} /> : <Eye size={14} />}
</button> </button>
)} )}
{!isMobile && (
<button
className="btn btn-sm btn-primary chat-thread-header-new-chat"
onClick={() => setShowNewDialog(true)}
data-testid="chat-thread-new-chat-btn"
>
<Plus size={14} />
{t("chat.newChat", "New Chat")}
</button>
)}
</div> </div>
)} )}
@@ -3953,6 +3962,7 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
}} }}
/> />
)} )}
</div>
{/* New Chat Dialog (rendered at root level) */} {/* New Chat Dialog (rendered at root level) */}
{showNewDialog && ( {showNewDialog && (

View File

@@ -313,23 +313,27 @@ Transient inline toast confirming issue close. Sits directly under the preview h
align-items: center; align-items: center;
} }
/* Tab styles for GitHub Import Modal */ /*
FNXC:ImportTasks 2026-06-22-12:45:
The Import Tasks sub-header tab bar should match the Artifacts view's button bar: plain body row, tokenized border/surface buttons, todo-accent active state, and no card-like filled strip under the main header.
*/
.github-import-tabs { .github-import-tabs {
display: flex; display: flex;
gap: var(--space-xs); align-items: center;
padding: var(--space-sm) var(--space-md); gap: var(--space-sm);
border-bottom: 1px solid var(--border); padding: 0;
background: var(--surface); border-bottom: none;
background: transparent;
} }
.github-import-tab { .github-import-tab {
display: flex; display: inline-flex;
align-items: center; align-items: center;
gap: var(--space-xs); gap: var(--space-sm);
padding: var(--space-sm) var(--space-md); padding: var(--space-xs) var(--space-sm);
border: 1px solid transparent; border: 1px solid var(--border);
border-radius: var(--radius-md); border-radius: var(--radius-md);
background: transparent; background: var(--surface);
color: var(--text-muted); color: var(--text-muted);
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 500;
@@ -338,14 +342,14 @@ Transient inline toast confirming issue close. Sits directly under the preview h
} }
.github-import-tab:hover:not(:disabled) { .github-import-tab:hover:not(:disabled) {
background: var(--surface-hover); background: var(--card-hover);
color: var(--text); color: var(--text);
} }
.github-import-tab.active { .github-import-tab.active {
background: var(--card); color: var(--todo);
border-color: var(--border); border-color: var(--todo);
color: var(--text); background: color-mix(in srgb, var(--todo) 12%, transparent);
} }
.github-import-tab:disabled { .github-import-tab:disabled {
@@ -1598,4 +1602,3 @@ body. Let the content take its full intrinsic height and hand vertical scrolling
} }
} }

View File

@@ -681,13 +681,16 @@ than overflow the message column. Theme tokens only — no hard-coded colors.
padding-top: var(--space-sm); padding-top: var(--space-sm);
} }
/* FNXC:MailboxView 2026-06-22-01:00: ViewHeader supplies the top padding, so the standalone-view scroll body drops its top inset to avoid doubling the gap under the header (keeps horizontal + bottom padding). */ /*
FNXC:MailboxView 2026-06-22-12:58:
The full-page Mailbox inner view should match Chat's page body: the shared ViewHeader owns the top chrome, and the content area is a flush split row with no extra outer padding. Keep padding on the Mailbox modal, but remove it from the page-scoped .mailbox-content so the left message-list pane aligns like Chat's sidebar.
*/
.mailbox-view .mailbox-content { .mailbox-view .mailbox-content {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
padding: 0 var(--space-xl) var(--space-xl); padding: 0;
max-height: none; max-height: none;
} }
@@ -751,10 +754,7 @@ Tighten the mailbox divider's static visible line to a single 1px var(--border)
.mailbox-view .mailbox-split-detail-pane { .mailbox-view .mailbox-split-detail-pane {
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
border: var(--btn-border-width) solid var(--border);
border-radius: var(--radius-md);
background: var(--surface); background: var(--surface);
padding: var(--space-md);
} }
/* /*
@@ -763,6 +763,10 @@ The list pane is fixed to its inline `width` (`flex: 0 0 auto`) so the divider d
*/ */
.mailbox-view .mailbox-split-list-pane { .mailbox-view .mailbox-split-list-pane {
flex: 0 0 auto; flex: 0 0 auto;
min-width: 0;
max-width: 500px;
border-right: var(--btn-border-width) solid var(--border);
background: var(--bg-secondary);
} }
.mailbox-view .mailbox-split-detail-pane { .mailbox-view .mailbox-split-detail-pane {
@@ -771,6 +775,7 @@ The list pane is fixed to its inline `width` (`flex: 0 0 auto`) so the divider d
min-width: 0; min-width: 0;
flex-direction: column; flex-direction: column;
gap: var(--space-md); gap: var(--space-md);
padding: var(--space-lg);
} }
.mailbox-view .mailbox-split-empty { .mailbox-view .mailbox-split-empty {

View File

@@ -114,27 +114,30 @@ Non-Command-Center dashboard CSS must use the canonical --text token. The legacy
min-height: 0; min-height: 0;
} }
/* Scheduling scope selector */ /*
FNXC:Automations 2026-06-22-12:45:
The Automations sub-header scope button bar should match the Artifacts tab bar style: each scope is a standalone bordered surface button with the same todo-accent active state, not a segmented control inside a filled capsule.
*/
.scheduling-scope-selector { .scheduling-scope-selector {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-xs); gap: var(--space-sm);
background: var(--surface); background: transparent;
border: 1px solid var(--border); border: none;
border-radius: var(--radius-md); border-radius: 0;
padding: var(--space-xs); padding: 0;
} }
.scope-btn { .scope-btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: var(--space-xs); gap: var(--space-sm);
padding: var(--space-xs) var(--space-md); padding: var(--space-xs) var(--space-md);
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 500; font-weight: 500;
border: none; border: 1px solid var(--border);
border-radius: var(--radius-sm); border-radius: var(--radius-md);
background: transparent; background: var(--surface);
color: var(--text-muted); color: var(--text-muted);
cursor: pointer; cursor: pointer;
transition: background-color var(--transition-fast), color var(--transition-fast); transition: background-color var(--transition-fast), color var(--transition-fast);
@@ -146,8 +149,9 @@ Non-Command-Center dashboard CSS must use the canonical --text token. The legacy
} }
.scope-btn.active { .scope-btn.active {
background: var(--todo); color: var(--todo);
color: var(--cta-text); border-color: var(--todo);
background: color-mix(in srgb, var(--todo) 12%, transparent);
} }
.scope-btn:focus-visible { .scope-btn:focus-visible {

View File

@@ -47,12 +47,20 @@ embedded view.
--wf-editor-touch-target: calc(var(--space-xl) + var(--space-lg) + var(--space-xs)); --wf-editor-touch-target: calc(var(--space-xl) + var(--space-lg) + var(--space-xs));
} }
/* FNXC:WorkflowEditorEmbedding 2026-06-22-01:00: Align the embedded workflows header to the shared ViewHeader metric — --space-lg padding, an icon + 1.125rem title — for cross-view consistency. */ /*
FNXC:WorkflowEditorEmbedding 2026-06-22-01:00:
Align the embedded workflows header to the shared ViewHeader/Insights metric — var(--surface) background, --view-header-min-height, --space-lg/--space-xl padding, a single divider, and a 20px todo-tinted icon + 1.125rem title — so Workflows reads the same size and color as Insights.
*/
.wf-editor-header { .wf-editor-header {
box-sizing: border-box;
display: flex; display: flex;
flex-shrink: 0;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: var(--space-lg); gap: var(--space-sm);
min-height: var(--view-header-min-height);
padding: var(--space-lg) var(--space-xl);
background: var(--surface);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
@@ -60,12 +68,25 @@ embedded view.
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-sm); gap: var(--space-sm);
min-width: 0;
margin: 0; margin: 0;
font-size: 1.125rem; font-size: 1.125rem;
font-weight: 600; font-weight: 600;
color: var(--text); color: var(--text);
} }
.wf-editor-header h2 svg {
flex-shrink: 0;
color: var(--todo);
}
.wf-editor-header h2 span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wf-editor-close { .wf-editor-close {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -134,6 +155,9 @@ FNXC:WorkflowSidebar 2026-06-22-12:00:
Workflow authors need to reclaim horizontal graph-editing space without leaving Workflow authors need to reclaim horizontal graph-editing space without leaving
the workflow view. Hide only the sidebar shell when collapsed and keep the 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. restore control attached to the canvas so the state is always reversible.
FNXC:WorkflowSidebar 2026-06-22-12:35:
When collapsed, the show-sidebar control belongs inline in the workflow name strip before the workflow name. It must be icon-only and in normal document flow so it cannot overlap the header title or workflow name.
*/ */
.wf-editor-body--sidebar-collapsed .wf-editor-sidebar { .wf-editor-body--sidebar-collapsed .wf-editor-sidebar {
display: none; display: none;
@@ -179,10 +203,9 @@ restore control attached to the canvas so the state is always reversible.
} }
.wf-sidebar-shell-restore { .wf-sidebar-shell-restore {
position: absolute; flex: 0 0 auto;
top: var(--space-sm); width: 30px;
left: var(--space-sm); padding-inline: 0;
z-index: 6;
white-space: nowrap; white-space: nowrap;
} }
@@ -1416,7 +1439,7 @@ Built-in workflow prompts need visible override state and a reset action without
/* Inline name + description strip (KTD-10). */ /* Inline name + description strip (KTD-10). */
.wf-name-strip { .wf-name-strip {
display: flex; display: flex;
align-items: baseline; align-items: center;
gap: var(--space-sm); gap: var(--space-sm);
padding: var(--space-xs) var(--space-sm); padding: var(--space-xs) var(--space-sm);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);

View File

@@ -2754,20 +2754,6 @@ function InnerEditor({
</aside> </aside>
<section className="wf-editor-canvas-wrap"> <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 <button
type="button" type="button"
className="wf-editor-mobile-back" className="wf-editor-mobile-back"
@@ -2783,6 +2769,19 @@ function InnerEditor({
plain text (no click affordance); user-owned workflows are plain text (no click affordance); user-owned workflows are
click-to-edit (Enter commits, Escape cancels, blur commits). */} click-to-edit (Enter commits, Escape cancels, blur commits). */}
<div className="wf-name-strip"> <div className="wf-name-strip">
{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 />
</button>
)}
{isBuiltin ? ( {isBuiltin ? (
<span className="wf-workflow-name wf-workflow-name--readonly" data-testid="wf-workflow-name"> <span className="wf-workflow-name wf-workflow-name--readonly" data-testid="wf-workflow-name">
{activeWorkflow.name} {activeWorkflow.name}

View File

@@ -2800,16 +2800,16 @@ describe("ChatView", () => {
const toggle = screen.getByTestId("chat-thread-render-toggle"); const toggle = screen.getByTestId("chat-thread-render-toggle");
const providerIcon = identity.querySelector(".provider-icon"); const providerIcon = identity.querySelector(".provider-icon");
const modelTag = identity.querySelector(".chat-model-tag"); const modelTag = identity.querySelector(".chat-model-tag");
const newChatButton = screen.getByTestId("chat-thread-new-chat-btn"); const newChatButton = screen.getByTestId("chat-new-btn");
expect(header).toBeInTheDocument(); expect(header).toBeInTheDocument();
expect(newChatButton.closest(".view-header")).toBeInTheDocument();
expect(providerIcon).toBeInTheDocument(); expect(providerIcon).toBeInTheDocument();
expect(within(identity).getByText("Agent Chat")).toBeInTheDocument(); expect(within(identity).getByText("Agent Chat")).toBeInTheDocument();
expect(modelTag).toBeInTheDocument(); expect(modelTag).toBeInTheDocument();
expect(modelTag).toHaveTextContent("Claude Sonnet 4.5"); expect(modelTag).toHaveTextContent("Claude Sonnet 4.5");
expect(toggle).toBeInTheDocument(); expect(toggle).toBeInTheDocument();
expect(header?.children[header.children.length - 2]).toBe(toggle); expect(header?.children[header.children.length - 1]).toBe(toggle);
expect(header?.children[header.children.length - 1]).toBe(newChatButton);
expect(document.querySelectorAll(".chat-thread-header .chat-model-tag")).toHaveLength(1); expect(document.querySelectorAll(".chat-thread-header .chat-model-tag")).toHaveLength(1);
}); });
@@ -3445,7 +3445,8 @@ describe("ChatView sidebar structure", () => {
expect(document.querySelector(".chat-sidebar")).toBeInTheDocument(); expect(document.querySelector(".chat-sidebar")).toBeInTheDocument();
expect(document.querySelector(".chat-sidebar-search")).toBeInTheDocument(); expect(document.querySelector(".chat-sidebar-search")).toBeInTheDocument();
expect(document.querySelector(".chat-sidebar-list")).toBeInTheDocument(); expect(document.querySelector(".chat-sidebar-list")).toBeInTheDocument();
expect(document.querySelector(".chat-sidebar-footer")).toBeInTheDocument(); expect(document.querySelector(".chat-sidebar-footer")).not.toBeInTheDocument();
expect(screen.getByTestId("chat-new-btn").closest(".view-header")).toBeInTheDocument();
expect(document.querySelector(".chat-sidebar-header")).not.toBeInTheDocument(); expect(document.querySelector(".chat-sidebar-header")).not.toBeInTheDocument();
}); });
@@ -3994,30 +3995,31 @@ describe("resizable sidebar", () => {
}); });
}); });
describe("thread header New Chat button", () => { describe("Chat header New Chat button", () => {
const activeSession = { id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" }; const activeSession = { id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" };
it("renders New Chat button in thread header on desktop when session is active", async () => { it("renders New Chat button in the shared header on desktop when session is active", async () => {
const viewportSpy = mockViewportMode("desktop"); const viewportSpy = mockViewportMode("desktop");
setupMockChat({ activeSession }); setupMockChat({ activeSession });
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />); await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
const btn = screen.getByTestId("chat-thread-new-chat-btn"); const btn = screen.getByTestId("chat-new-btn");
expect(btn).toBeInTheDocument(); expect(btn).toBeInTheDocument();
expect(btn.closest(".view-header")).toBeInTheDocument();
expect(btn).toHaveTextContent("New Chat"); expect(btn).toHaveTextContent("New Chat");
expect(btn).toHaveClass("btn", "btn-sm", "btn-primary"); expect(btn).toHaveClass("btn", "btn-sm", "btn-primary");
viewportSpy.mockRestore(); viewportSpy.mockRestore();
}); });
it("clicking thread header New Chat button opens the NewChatDialog", async () => { it("clicking shared header New Chat button opens the NewChatDialog", async () => {
const viewportSpy = mockViewportMode("desktop"); const viewportSpy = mockViewportMode("desktop");
setupMockChat({ activeSession }); setupMockChat({ activeSession });
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />); await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
const btn = screen.getByTestId("chat-thread-new-chat-btn"); const btn = screen.getByTestId("chat-new-btn");
await act(async () => { await act(async () => {
fireEvent.click(btn); fireEvent.click(btn);
}); });
@@ -4027,13 +4029,14 @@ describe("thread header New Chat button", () => {
viewportSpy.mockRestore(); viewportSpy.mockRestore();
}); });
it("does not render New Chat button in thread header on mobile", async () => { it("does not render New Chat button in the shared header on mobile", async () => {
const viewportSpy = mockViewportMode("mobile"); const viewportSpy = mockViewportMode("mobile");
setupMockChat({ activeSession }); setupMockChat({ activeSession });
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />); await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
expect(screen.queryByTestId("chat-thread-new-chat-btn")).toBeNull(); expect(screen.queryByTestId("chat-thread-new-chat-btn")).toBeNull();
expect(document.querySelector(".view-header [data-testid='chat-new-btn']")).toBeNull();
viewportSpy.mockRestore(); viewportSpy.mockRestore();
}); });
@@ -5568,6 +5571,18 @@ describe("ChatView mobile CSS contract", () => {
expect(mobileRuleNotContains(".chat-sidebar", "max-height: 40vh")).toBe(true); expect(mobileRuleNotContains(".chat-sidebar", "max-height: 40vh")).toBe(true);
}); });
it("keeps the shared header outside the bounded chat body row", async () => {
const viewRule = css.match(/\.chat-view\s*\{([^}]*)\}/)?.[1] ?? "";
const bodyRule = css.match(/\.chat-view__body\s*\{([^}]*)\}/)?.[1] ?? "";
expect(viewRule).toContain("flex-direction: column;");
expect(viewRule).toContain("min-height: 0;");
expect(bodyRule).toContain("display: flex;");
expect(bodyRule).toContain("flex: 1 1 auto;");
expect(bodyRule).toContain("min-height: 0;");
expect(bodyRule).toContain("overflow: hidden;");
});
it("mobile .chat-sidebar-header is hidden", async () => { it("mobile .chat-sidebar-header is hidden", async () => {
expect(mobileRuleContains(".chat-sidebar-header", "display: none")).toBe(true); expect(mobileRuleContains(".chat-sidebar-header", "display: none")).toBe(true);
}); });

View File

@@ -95,6 +95,21 @@ describe("GitHubImportModal", () => {
expect(source).toContain(".github-import-preview-pane.mobile.active .github-import-pane-content {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n overscroll-behavior: contain;"); expect(source).toContain(".github-import-preview-pane.mobile.active .github-import-pane-content {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n overscroll-behavior: contain;");
}); });
it("styles import type tabs like the Artifacts button bar", () => {
const source = readFileSync(resolve(__dirname, "../GitHubImportModal.css"), "utf8");
const tabsRule = source.match(/\.github-import-tabs\s*\{[^}]*\}/)?.[0] ?? "";
const tabRule = source.match(/\.github-import-tab\s*\{[^}]*\}/)?.[0] ?? "";
const activeRule = source.match(/\.github-import-tab\.active\s*\{[^}]*\}/)?.[0] ?? "";
expect(tabsRule).toContain("background: transparent;");
expect(tabsRule).toContain("border-bottom: none;");
expect(tabRule).toContain("border: 1px solid var(--border);");
expect(tabRule).toContain("background: var(--surface);");
expect(activeRule).toContain("color: var(--todo);");
expect(activeRule).toContain("border-color: var(--todo);");
expect(activeRule).toContain("background: color-mix(in srgb, var(--todo) 12%, transparent);");
});
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
vi.mocked(fetchGitRemotes).mockReset(); vi.mocked(fetchGitRemotes).mockReset();

View File

@@ -1855,6 +1855,7 @@ describe("MailboxView", () => {
expect(contentBlock).toContain("min-height: 0;"); expect(contentBlock).toContain("min-height: 0;");
expect(contentBlock).toContain("overflow-y: auto;"); expect(contentBlock).toContain("overflow-y: auto;");
expect(contentBlock).toContain("max-height: none;"); expect(contentBlock).toContain("max-height: none;");
expect(contentBlock).toContain("padding: 0;");
}); });
it("defines desktop/tablet split-pane selectors under .mailbox-view scope", async () => { it("defines desktop/tablet split-pane selectors under .mailbox-view scope", async () => {
@@ -1867,19 +1868,25 @@ describe("MailboxView", () => {
expect(splitPaneBlockMatch).toBeTruthy(); expect(splitPaneBlockMatch).toBeTruthy();
const splitPaneBlock = splitPaneBlockMatch![1]; const splitPaneBlock = splitPaneBlockMatch![1];
expect(splitPaneBlock).toContain("overflow-y: auto;"); expect(splitPaneBlock).toContain("overflow-y: auto;");
expect(splitPaneBlock).toContain("border: var(--btn-border-width) solid var(--border);");
expect(splitPaneBlock).toContain("background: var(--surface);"); expect(splitPaneBlock).toContain("background: var(--surface);");
expect(splitPaneBlock).not.toContain("border: var(--btn-border-width) solid var(--border);");
expect(splitPaneBlock).not.toContain("border-radius: var(--radius-md);");
// FNXC:Mailbox 2026-06-22-18:05: list pane fixed to inline width; detail pane fills remainder and may shrink below content. // FNXC:MailboxView 2026-06-22-12:58: full-page Mailbox list pane mirrors Chat's left sidebar surface and spacing.
const listPaneBlockMatch = css.match(/\.mailbox-view\s+\.mailbox-split-list-pane\s*\{([^}]*)\}/); const listPaneBlockMatch = css.match(/\.mailbox-view\s+\.mailbox-split-list-pane\s*\{([^}]*)\}/);
expect(listPaneBlockMatch).toBeTruthy(); expect(listPaneBlockMatch).toBeTruthy();
expect(listPaneBlockMatch![1]).toContain("flex: 0 0 auto;"); expect(listPaneBlockMatch![1]).toContain("flex: 0 0 auto;");
expect(listPaneBlockMatch![1]).toContain("min-width: 0;");
expect(listPaneBlockMatch![1]).toContain("max-width: 500px;");
expect(listPaneBlockMatch![1]).toContain("border-right: var(--btn-border-width) solid var(--border);");
expect(listPaneBlockMatch![1]).toContain("background: var(--bg-secondary);");
// Match the standalone detail-pane rule (the one declaring `display: flex;`), not the shared border/background block. // Match the standalone detail-pane rule (the one declaring `display: flex;`), not the shared border/background block.
const detailPaneBlockMatch = css.match(/\.mailbox-view\s+\.mailbox-split-detail-pane\s*\{([^}]*display:\s*flex;[^}]*)\}/); const detailPaneBlockMatch = css.match(/\.mailbox-view\s+\.mailbox-split-detail-pane\s*\{([^}]*display:\s*flex;[^}]*)\}/);
expect(detailPaneBlockMatch).toBeTruthy(); expect(detailPaneBlockMatch).toBeTruthy();
expect(detailPaneBlockMatch![1]).toContain("flex: 1 1 auto;"); expect(detailPaneBlockMatch![1]).toContain("flex: 1 1 auto;");
expect(detailPaneBlockMatch![1]).toContain("min-width: 0;"); expect(detailPaneBlockMatch![1]).toContain("min-width: 0;");
expect(detailPaneBlockMatch![1]).toContain("padding: var(--space-lg);");
const resizeHandleBlockMatch = css.match(/\.mailbox-view\s+\.mailbox-split-resize-handle\s*\{([^}]*)\}/); const resizeHandleBlockMatch = css.match(/\.mailbox-view\s+\.mailbox-split-resize-handle\s*\{([^}]*)\}/);
expect(resizeHandleBlockMatch).toBeTruthy(); expect(resizeHandleBlockMatch).toBeTruthy();

View File

@@ -1,5 +1,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { ScheduledTasksModal } from "../ScheduledTasksModal"; import { ScheduledTasksModal } from "../ScheduledTasksModal";
import type { Routine } from "@fusion/core"; import type { Routine } from "@fusion/core";
@@ -167,6 +169,21 @@ describe("ScheduledTasksModal", () => {
expect(toolbarRight?.contains(newAutomationButton)).toBe(true); expect(toolbarRight?.contains(newAutomationButton)).toBe(true);
}); });
it("styles scope controls like the Artifacts button bar", () => {
const source = readFileSync(resolve(__dirname, "../ScriptsModal.css"), "utf8");
const selectorRule = source.match(/\.scheduling-scope-selector\s*\{[^}]*\}/)?.[0] ?? "";
const scopeRule = source.match(/\.scope-btn\s*\{[^}]*\}/)?.[0] ?? "";
const activeRule = source.match(/\.scope-btn\.active\s*\{[^}]*\}/)?.[0] ?? "";
expect(selectorRule).toContain("background: transparent;");
expect(selectorRule).toContain("border: none;");
expect(scopeRule).toContain("border: 1px solid var(--border);");
expect(scopeRule).toContain("background: var(--surface);");
expect(activeRule).toContain("color: var(--todo);");
expect(activeRule).toContain("border-color: var(--todo);");
expect(activeRule).toContain("background: color-mix(in srgb, var(--todo) 12%, transparent);");
});
it("uses routine APIs with global scope by default", async () => { it("uses routine APIs with global scope by default", async () => {
render(<ScheduledTasksModal onClose={onClose} addToast={addToast} />); render(<ScheduledTasksModal onClose={onClose} addToast={addToast} />);

View File

@@ -42,6 +42,21 @@ function expectNoHardcodedWhiteBackground(rule: string): void {
} }
describe("WorkflowNodeEditor themed React Flow CSS contract", () => { describe("WorkflowNodeEditor themed React Flow CSS contract", () => {
it("matches the shared Insights/ViewHeader chrome", () => {
const editorCss = readComponentCss("WorkflowNodeEditor.css");
const headerRule = findRule([editorCss], /\.wf-editor-header\s*\{[^}]*\}/);
const titleRule = findRule([editorCss], /\.wf-editor-header h2\s*\{[^}]*\}/);
const iconRule = findRule([editorCss], /\.wf-editor-header h2 svg\s*\{[^}]*\}/);
expect(headerRule).toMatch(/min-height\s*:\s*var\(--view-header-min-height\)\s*;/);
expect(headerRule).toMatch(/padding\s*:\s*var\(--space-lg\) var\(--space-xl\)\s*;/);
expect(headerRule).toMatch(/background\s*:\s*var\(--surface\)\s*;/);
expect(headerRule).toMatch(/border-bottom\s*:\s*1px solid var\(--border\)\s*;/);
expect(titleRule).toMatch(/font-size\s*:\s*1\.125rem\s*;/);
expect(titleRule).toMatch(/font-weight\s*:\s*600\s*;/);
expect(iconRule).toMatch(/color\s*:\s*var\(--todo\)\s*;/);
});
it("FN-6701 themes zoom controls, mini-map, and sidebar checkboxes with tokens", () => { it("FN-6701 themes zoom controls, mini-map, and sidebar checkboxes with tokens", () => {
const baseCss = loadAllAppCssBaseOnly(); const baseCss = loadAllAppCssBaseOnly();
@@ -146,8 +161,10 @@ describe("WorkflowNodeEditor sidebar overflow CSS contract", () => {
expect(collapsedSidebarRule).toMatch(/display\s*:\s*none\s*;/); expect(collapsedSidebarRule).toMatch(/display\s*:\s*none\s*;/);
const restoreRule = findRule([editorCss], /\.wf-sidebar-shell-restore\s*\{[^}]*\}/); const restoreRule = findRule([editorCss], /\.wf-sidebar-shell-restore\s*\{[^}]*\}/);
expect(restoreRule).toMatch(/position\s*:\s*absolute\s*;/); expect(restoreRule).not.toMatch(/position\s*:\s*absolute\s*;/);
expect(restoreRule).toMatch(/left\s*:\s*var\(--space-sm\)\s*;/); expect(restoreRule).toMatch(/flex\s*:\s*0 0 auto\s*;/);
expect(restoreRule).toMatch(/width\s*:\s*30px\s*;/);
expect(restoreRule).toMatch(/padding-inline\s*:\s*0\s*;/);
expect(restoreRule).toMatch(/white-space\s*:\s*nowrap\s*;/); expect(restoreRule).toMatch(/white-space\s*:\s*nowrap\s*;/);
}); });

View File

@@ -453,7 +453,10 @@ describe("WorkflowNodeEditor", () => {
fireEvent.click(screen.getByTestId("wf-sidebar-collapse")); fireEvent.click(screen.getByTestId("wf-sidebar-collapse"));
expect(body!).toHaveClass("wf-editor-body--sidebar-collapsed"); expect(body!).toHaveClass("wf-editor-body--sidebar-collapsed");
expect(screen.getByTestId("wf-sidebar-restore")).toHaveTextContent("Workflows"); const restoreButton = screen.getByTestId("wf-sidebar-restore");
expect(restoreButton).toHaveAccessibleName("Show workflow sidebar");
expect(restoreButton).toHaveTextContent("");
expect(screen.getByTestId("wf-workflow-name").previousElementSibling).toBe(restoreButton);
fireEvent.click(screen.getByTestId("wf-sidebar-restore")); fireEvent.click(screen.getByTestId("wf-sidebar-restore"));

View File

@@ -6,6 +6,9 @@ The Compound Engineering dashboard view must follow the dashboard design scale s
/* /*
FNXC:CompoundEngineeringUI 2026-06-22-09:40: FNXC:CompoundEngineeringUI 2026-06-22-09:40:
The root .ce-view is a flex column hosting the flex-shrink:0 shared ViewHeader on top and a single scrolling .ce-view-body below. ViewHeader owns the --space-lg top/side padding (so the root no longer pads its own header row); the body re-adds matching side/bottom padding so content keeps the same horizontal rhythm and there is no double top padding. The root .ce-view is a flex column hosting the flex-shrink:0 shared ViewHeader on top and a single scrolling .ce-view-body below. ViewHeader owns the --space-lg top/side padding (so the root no longer pads its own header row); the body re-adds matching side/bottom padding so content keeps the same horizontal rhythm and there is no double top padding.
FNXC:CompoundEngineeringUI 2026-06-22-12:35:
The shared ViewHeader sits directly above plugin content, so the body needs top padding too; otherwise the first Compound Engineering card/flow row bumps against the header divider. Use the same tokenized vertical rhythm as the dashboard's native artifact and goals views.
*/ */
.ce-view { .ce-view {
display: flex; display: flex;
@@ -27,7 +30,7 @@ The root .ce-view is a flex column hosting the flex-shrink:0 shared ViewHeader o
gap: var(--space-lg); gap: var(--space-lg);
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
padding: 0 calc(var(--space-lg) + var(--space-xs)) var(--space-lg); padding: var(--space-lg) calc(var(--space-lg) + var(--space-xs)) var(--space-lg);
box-sizing: border-box; box-sizing: border-box;
overflow: auto; overflow: auto;
} }