diff --git a/.changeset/fn-6959-footer-launcher-style.md b/.changeset/fn-6959-footer-launcher-style.md new file mode 100644 index 0000000000..84d338ae00 --- /dev/null +++ b/.changeset/fn-6959-footer-launcher-style.md @@ -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. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index f0e6a2a93f..cf966f68fe 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -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 diff --git a/packages/dashboard/app/components/ExecutorStatusBar.css b/packages/dashboard/app/components/ExecutorStatusBar.css index a55cbd9f85..d30c55fc6b 100644 --- a/packages/dashboard/app/components/ExecutorStatusBar.css +++ b/packages/dashboard/app/components/ExecutorStatusBar.css @@ -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; diff --git a/packages/dashboard/app/components/TerminalLauncher.css b/packages/dashboard/app/components/TerminalLauncher.css index 50682da688..733d964c5d 100644 --- a/packages/dashboard/app/components/TerminalLauncher.css +++ b/packages/dashboard/app/components/TerminalLauncher.css @@ -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); diff --git a/packages/dashboard/app/components/__tests__/ExecutorStatusBar.test.tsx b/packages/dashboard/app/components/__tests__/ExecutorStatusBar.test.tsx index bac2c612b3..9d1c24808b 100644 --- a/packages/dashboard/app/components/__tests__/ExecutorStatusBar.test.tsx +++ b/packages/dashboard/app/components/__tests__/ExecutorStatusBar.test.tsx @@ -46,13 +46,23 @@ import type { ExecutorStats } from "../../api"; const mockUseExecutorStats = useExecutorStats as ReturnType; 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(); + 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( + , + ); + + 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( + , + ); - 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", () => {