FN-6959: align footer launcher typography
Align the Quick Chat and Terminal footer launchers so they read as matching peer controls. - Inherit the footer font and color contract for the Terminal footer launcher. - Preserve usable hover, focus, padding, and scripts chevron behavior in the footer variant. - Document the footer Quick Chat launcher placement and add release notes. - Expand status bar tests to lock the shared footer launcher styling. Files changed: .changeset/fn-6959-footer-launcher-style.md | 7 ++ docs/dashboard-guide.md | 2 +- .../dashboard/app/components/ExecutorStatusBar.css | 2 +- .../dashboard/app/components/TerminalLauncher.css | 37 +++++++- .../__tests__/ExecutorStatusBar.test.tsx | 105 +++++++++++++++++++-- 5 files changed, 138 insertions(+), 15 deletions(-) Fusion-Task-Id: FN-6959 Fusion-Task-Lineage: 539307cb-dc81-441f-8baf-8823826e2195
This commit is contained in:
7
.changeset/fn-6959-footer-launcher-style.md
Normal file
7
.changeset/fn-6959-footer-launcher-style.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Match Quick Chat and Terminal typography in the dashboard footer.
|
||||
category: fix
|
||||
dev: Footer launcher CSS now shares inherited font and color contracts between Quick Chat and Terminal.
|
||||
@@ -374,7 +374,7 @@ Chat Rooms are project-scoped group conversations for multiple agents. They are
|
||||
|
||||
## Quick Chat
|
||||
|
||||
Quick Chat is an optional floating panel for fast, project-scoped assistant conversations without leaving your current view.
|
||||
Quick Chat is an optional fast, project-scoped assistant surface for conversations without leaving your current view. Depending on the project launcher setting, desktop and tablet can open it from the footer status bar beside Terminal, while mobile continues to use the compact Quick Chat panel behavior.
|
||||
|
||||
- Controlled by the project setting `showQuickChatFAB`
|
||||
- Supports agent mentions (`@agent`) and shared `#` task/file mentions
|
||||
|
||||
@@ -72,7 +72,7 @@ FN-6887 makes the footer status bar the canonical desktop/tablet terminal launch
|
||||
border: none;
|
||||
background: transparent;
|
||||
/*
|
||||
* FNXC:FooterChrome 2026-06-23-00:20:
|
||||
* FNXC:FooterLaunchers 2026-06-24-00:00:
|
||||
* Quick Chat and Terminal are peer footer launchers. Pin both to the footer's compact UI font and color token so switching the launcher location does not make one control read heavier or dimmer than the other.
|
||||
*/
|
||||
color: inherit;
|
||||
|
||||
@@ -17,36 +17,67 @@
|
||||
.terminal-launcher--footer {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: inherit;
|
||||
font-family: var(--font-primary);
|
||||
font-size: inherit;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:Terminal 2026-06-22-00:00:
|
||||
In the footer status bar the Terminal launcher must read as plain clickable text, matching the executor "running" state-trigger: no border, no card background, no chunky button padding. The label underlines on hover like the state trigger. The scripts chevron is flattened to match and the split divider is hidden so the footer affordance is text-first. Only the footer variant is flattened; the header variant keeps its grouped split-button chrome.
|
||||
|
||||
FNXC:FooterLaunchers 2026-06-24-00:00:
|
||||
Quick Chat and Terminal are peer footer launchers. Keep the Terminal footer variant on the same inherited footer font family, size, weight, color, hover color, and focus ring as .executor-status-bar__footer-launcher without changing header, mobile-nav, modal, or non-footer Terminal surfaces.
|
||||
*/
|
||||
.terminal-launcher--footer .terminal-launcher__main,
|
||||
.terminal-launcher--footer .terminal-launcher__chevron {
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
padding: 0 var(--space-xs);
|
||||
min-height: calc(var(--space-lg) + var(--space-xs));
|
||||
border: none;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.terminal-launcher--footer .terminal-launcher__chevron {
|
||||
width: auto;
|
||||
padding-left: var(--space-xxs);
|
||||
padding-inline: var(--space-xxs);
|
||||
}
|
||||
|
||||
.terminal-launcher--footer .terminal-launcher__label {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.terminal-launcher--footer .terminal-launcher__divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.terminal-launcher--footer .terminal-launcher__main:hover,
|
||||
.terminal-launcher--footer .terminal-launcher__chevron:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.terminal-launcher--footer .terminal-launcher__main:hover .terminal-launcher__label {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: calc(var(--space-xs) / 2);
|
||||
}
|
||||
|
||||
.terminal-launcher--footer .terminal-launcher__main:focus-visible,
|
||||
.terminal-launcher--footer .terminal-launcher__chevron:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.terminal-launcher__main {
|
||||
min-height: var(--control-height-sm);
|
||||
gap: var(--space-xs);
|
||||
|
||||
@@ -46,13 +46,23 @@ import type { ExecutorStats } from "../../api";
|
||||
|
||||
const mockUseExecutorStats = useExecutorStats as ReturnType<typeof vi.fn>;
|
||||
const executorStatusBarCss = fs.readFileSync(path.join(__dirname, "../ExecutorStatusBar.css"), "utf-8");
|
||||
const terminalLauncherCss = fs.readFileSync(path.join(__dirname, "../TerminalLauncher.css"), "utf-8");
|
||||
|
||||
function getCssRuleBlock(selector: string): string {
|
||||
function getCssRuleBlock(css: string, selector: string): string {
|
||||
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const match = executorStatusBarCss.match(new RegExp(`${escapedSelector}\\s*\\{([^}]*)\\}`));
|
||||
const match = css.match(new RegExp(`${escapedSelector}\\s*\\{([^}]*)\\}`));
|
||||
return match?.[1] ?? "";
|
||||
}
|
||||
|
||||
function getCssRuleBlockByPattern(css: string, pattern: RegExp): string {
|
||||
const match = css.match(pattern);
|
||||
return match?.[1] ?? "";
|
||||
}
|
||||
|
||||
function expectNoHardcodedColors(cssBlock: string): void {
|
||||
expect(cssBlock).not.toMatch(/#[0-9a-f]{3,8}\b|rgba?\(/i);
|
||||
}
|
||||
|
||||
/** Minimal empty task list used by tests that mock the hook. */
|
||||
const emptyTasks: any[] = [];
|
||||
|
||||
@@ -205,11 +215,32 @@ describe("ExecutorStatusBar", () => {
|
||||
await waitFor(() => expect(mockFetchScripts).toHaveBeenCalledWith(undefined));
|
||||
});
|
||||
|
||||
it("renders the terminal launcher in the footer on tablet", () => {
|
||||
viewportModeMock.value = "tablet";
|
||||
it("keeps the footer terminal scripts chevron usable when scripts are empty", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockFetchScripts.mockResolvedValueOnce({});
|
||||
|
||||
render(<ExecutorStatusBar tasks={emptyTasks} onToggleTerminal={vi.fn()} onOpenScripts={vi.fn()} onRunScript={vi.fn()} />);
|
||||
|
||||
await user.click(screen.getByTestId("scripts-btn"));
|
||||
|
||||
expect(await screen.findByTestId("quick-scripts-empty")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the peer Quick Chat and Terminal footer launchers on tablet", () => {
|
||||
viewportModeMock.value = "tablet";
|
||||
|
||||
render(
|
||||
<ExecutorStatusBar
|
||||
tasks={emptyTasks}
|
||||
onToggleTerminal={vi.fn()}
|
||||
onOpenScripts={vi.fn()}
|
||||
onRunScript={vi.fn()}
|
||||
quickChatButtonMode="footer"
|
||||
onOpenQuickChat={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("executor-quick-chat-launcher-segment")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("executor-terminal-launcher-segment")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("terminal-toggle-btn")).toBeInTheDocument();
|
||||
});
|
||||
@@ -237,13 +268,67 @@ describe("ExecutorStatusBar", () => {
|
||||
});
|
||||
|
||||
it("keeps Quick Chat and Terminal footer launchers on the same font and color tokens", () => {
|
||||
const launcherRule = getCssRuleBlock(".executor-status-bar__footer-launcher");
|
||||
render(
|
||||
<ExecutorStatusBar
|
||||
tasks={emptyTasks}
|
||||
onToggleTerminal={vi.fn()}
|
||||
onOpenScripts={vi.fn()}
|
||||
onRunScript={vi.fn()}
|
||||
quickChatButtonMode="footer"
|
||||
onOpenQuickChat={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(launcherRule).toContain("color: inherit");
|
||||
expect(launcherRule).toContain("font-family: var(--font-primary)");
|
||||
expect(launcherRule).toContain("font-size: inherit");
|
||||
expect(launcherRule).toContain("font-weight: 500");
|
||||
expect(launcherRule).not.toMatch(/#|rgb\(/i);
|
||||
const quickChatLauncher = screen.getByTestId("executor-quick-chat-launcher");
|
||||
const terminalLauncher = screen.getByTestId("terminal-toggle-btn");
|
||||
expect(quickChatLauncher).toHaveClass("executor-status-bar__footer-launcher");
|
||||
expect(terminalLauncher).toHaveClass("terminal-launcher__main");
|
||||
expect(screen.getByTestId("executor-quick-chat-launcher-segment")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("executor-terminal-launcher-segment")).toBeInTheDocument();
|
||||
|
||||
const quickChatRule = getCssRuleBlock(executorStatusBarCss, ".executor-status-bar__footer-launcher");
|
||||
const quickChatHoverRule = getCssRuleBlock(executorStatusBarCss, ".executor-status-bar__footer-launcher:hover");
|
||||
const quickChatFocusRule = getCssRuleBlock(executorStatusBarCss, ".executor-status-bar__footer-launcher:focus-visible");
|
||||
const terminalFooterRule = getCssRuleBlock(terminalLauncherCss, ".terminal-launcher--footer");
|
||||
const terminalControlRule = getCssRuleBlockByPattern(
|
||||
terminalLauncherCss,
|
||||
/\.terminal-launcher--footer \.terminal-launcher__main,\s*\.terminal-launcher--footer \.terminal-launcher__chevron\s*\{([^}]*)\}/,
|
||||
);
|
||||
const terminalLabelRule = getCssRuleBlock(terminalLauncherCss, ".terminal-launcher--footer .terminal-launcher__label");
|
||||
const terminalHoverRule = getCssRuleBlockByPattern(
|
||||
terminalLauncherCss,
|
||||
/\.terminal-launcher--footer \.terminal-launcher__main:hover,\s*\.terminal-launcher--footer \.terminal-launcher__chevron:hover\s*\{([^}]*)\}/,
|
||||
);
|
||||
const terminalFocusRule = getCssRuleBlockByPattern(
|
||||
terminalLauncherCss,
|
||||
/\.terminal-launcher--footer \.terminal-launcher__main:focus-visible,\s*\.terminal-launcher--footer \.terminal-launcher__chevron:focus-visible\s*\{([^}]*)\}/,
|
||||
);
|
||||
|
||||
expect(quickChatRule).toContain("color: inherit");
|
||||
expect(quickChatRule).toContain("font-family: var(--font-primary)");
|
||||
expect(quickChatRule).toContain("font-size: inherit");
|
||||
expect(quickChatRule).toContain("font-weight: 500");
|
||||
expect(quickChatRule).toContain("line-height: 1");
|
||||
expect(quickChatHoverRule).toContain("color: var(--text)");
|
||||
expect(quickChatFocusRule).toContain("box-shadow: var(--focus-ring-strong)");
|
||||
|
||||
expect(terminalFooterRule).toContain("color: inherit");
|
||||
expect(terminalFooterRule).toContain("font-family: var(--font-primary)");
|
||||
expect(terminalFooterRule).toContain("font-size: inherit");
|
||||
expect(terminalFooterRule).toContain("font-weight: 500");
|
||||
expect(terminalFooterRule).toContain("line-height: 1");
|
||||
expect(terminalControlRule).toContain("color: inherit");
|
||||
expect(terminalControlRule).toContain("font-family: inherit");
|
||||
expect(terminalControlRule).toContain("font-size: inherit");
|
||||
expect(terminalControlRule).toContain("font-weight: inherit");
|
||||
expect(terminalControlRule).toContain("line-height: inherit");
|
||||
expect(terminalLabelRule).toContain("font-size: inherit");
|
||||
expect(terminalLabelRule).toContain("font-weight: inherit");
|
||||
expect(terminalLabelRule).toContain("line-height: inherit");
|
||||
expect(terminalHoverRule).toContain("color: var(--text)");
|
||||
expect(terminalFocusRule).toContain("box-shadow: var(--focus-ring-strong)");
|
||||
|
||||
[quickChatRule, quickChatHoverRule, quickChatFocusRule, terminalFooterRule, terminalControlRule, terminalLabelRule, terminalHoverRule, terminalFocusRule].forEach(expectNoHardcodedColors);
|
||||
});
|
||||
|
||||
it("omits the Quick Chat footer launcher for floating, off, and mobile modes", () => {
|
||||
|
||||
Reference in New Issue
Block a user