FN-8396: fix task detail inline icon sizing

Normalize Task Detail inline action icons across every responsive breakpoint.

- Apply scoped tokenized SVG sizing that includes wrapped provider icons.
- Add source and browser-layout coverage for optional-control variants at mobile, tablet, and desktop widths.
- Add a patch changeset for the responsive icon-sizing fix.
Files changed:
 .changeset/fn-8396-task-detail-tablet-icon-size.md |  7 +++
 .../__tests__/browser-layout-smoke-fixture.test.ts | 18 ++++++
 .../dashboard/app/components/TaskDetailModal.css   | 14 +++++
 .../__tests__/TaskDetailModal.rendering.test.tsx   | 25 ++++++++
 ...etailModal.responsive-and-dependencies.test.tsx | 16 ++++++
 .../dashboard/scripts/browser-layout-smoke.mjs     | 67 ++++++++++++++++++++++
 6 files changed, 147 insertions(+)

Fusion-Task-Id: FN-8396

Fusion-Task-Lineage: 3cd0e5e6-7edf-4dc7-aa71-0586cca6e6ec

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-19 16:36:42 -07:00
parent efa9580b93
commit e04608d62c
6 changed files with 147 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Task detail inline action icons now render at a consistent size on tablet screens.
category: fix
dev: Scoped the .detail-meta-inline-controls SVG sizing so every inline-row icon (including nested ProviderIcon SVGs) resolves to the shared --icon-size-sm token at desktop, tablet (769–1024px), and mobile. No handler/behavior changes; preserves the icon-only row, square-box sizing, size-prop-less oversight Eye/EyeOff, and the mobile wrap fallback.

View File

@@ -12,6 +12,24 @@ describe("browser layout smoke fixture", () => {
expect(html).toContain("pr-checks__details-link");
});
it("includes Task Detail inline icon fixtures for all optional-control variants", () => {
const html = createSmokeHtml();
expect(html).toContain('data-smoke="task-detail-inline-row-fixtures"');
for (const variant of ["full", "without-github", "without-oversight", "without-optionals"]) {
expect(html).toContain(`data-smoke="task-detail-inline-row-${variant}"`);
}
for (const testId of [
"detail-inline-attach",
"detail-inline-github-toggle",
"detail-oversight-menu-trigger",
"detail-priority-trigger",
"detail-execution-mode-toggle",
]) {
expect(html).toContain(`data-testid="${testId}"`);
}
expect(html).toContain('<span class="provider-icon"><svg width="16" height="16"');
});
it("includes localized Quick Add Save fixtures for Board and List composers", () => {
const html = createSmokeHtml();
expect(html).toContain('data-smoke="quick-add-save-fixtures"');

View File

@@ -414,6 +414,20 @@ space or icon tokens from making this cluster visually uneven.
border-radius: var(--detail-control-border-radius);
}
/*
FNXC:TaskDetailModalResponsive 2026-07-19-12:00:
FN-8396 requires every inline action icon to resolve to `--icon-size-sm` at
all desktop, tablet, and mobile widths. The global `.btn-icon > svg` rule
cannot reach ProviderIcon's wrapped SVG, so keep this descendant rule scoped
to Task Detail instead of changing global button behavior or adding an
explicit size prop to the Oversight Eye/EyeOff icons.
*/
.detail-meta-inline-controls svg {
width: var(--icon-size-sm);
height: var(--icon-size-sm);
flex-shrink: 0;
}
/*
FNXC:QuickAddActionRow 2026-07-17-12:00:
FN-8209: Task Detail uses the same compact icon-only priority affordance as

View File

@@ -9,6 +9,8 @@ regex. The now-universal Oversight overflow trigger's aria-label is
"Oversight actions", which also matches `/actions/i` and made every such
query ambiguous once the trigger stopped being a mobile-only affordance.
*/
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
@@ -1411,6 +1413,29 @@ describe("TaskDetailModal", () => {
expect(fast).not.toHaveTextContent("Fast");
});
it("keeps every inline action icon-only with the production size-prop contracts", () => {
const source = readFileSync(resolve(__dirname, "../TaskDetailModal.tsx"), "utf8");
const rowStart = source.indexOf('data-testid="detail-meta-inline-controls"');
const rowEnd = source.indexOf('className="detail-hidden-file-input"', rowStart);
const row = source.slice(rowStart, rowEnd);
// FNXC:TaskDetailModalResponsive 2026-07-19-12:00: The row stays ordered
// attach → GitHub → Oversight → priority → Fast; CSS owns icon parity.
expect(row).toMatch(/<Paperclip size=\{12\}[^>]*aria-hidden="true"/);
expect(row).toMatch(/<ProviderIcon provider="github" size="sm"/);
expect(row).toMatch(/<PriorityIcon size=\{14\}[^>]*aria-hidden="true"/);
expect(row).toMatch(/<Zap size=\{14\}[^>]*aria-hidden="true"/);
expect(row).toMatch(/overseerTriggerOn \? <Eye aria-hidden="true"\s*\/> : <EyeOff aria-hidden="true"\s*\/>/);
expect(row).not.toMatch(/<(?:Eye|EyeOff)\s+[^>]*\bsize=/);
expect(row.indexOf("detail-inline-attach")).toBeLessThan(row.indexOf("detail-inline-github-toggle"));
expect(row.indexOf("detail-inline-github-toggle")).toBeLessThan(row.indexOf("detail-oversight-menu-trigger"));
expect(row.indexOf("detail-oversight-menu-trigger")).toBeLessThan(row.indexOf("detail-priority-trigger"));
expect(row.indexOf("detail-priority-trigger")).toBeLessThan(row.indexOf("detail-execution-mode-toggle"));
for (const label of ["aria-label", "title"]) {
expect(row.match(new RegExp(label, "g"))?.length).toBeGreaterThanOrEqual(5);
}
});
it("removes bespoke toolbar SVG sizing rules", () => {
const css = readDashboardStylesSource();
expect(css).not.toMatch(/\.detail-oversight-menu-trigger svg\s*\{[^}]*width:\s*1em/);

View File

@@ -253,6 +253,22 @@ describe("TaskDetailModal", () => {
expect(css).not.toMatch(/@media \(max-width: 640px\)\s*\{[^}]*\.detail-meta-inline-controls\s*\{[^}]*flex-direction:\s*column;/);
});
it("scopes tokenized SVG sizing to every inline-row descendant without changing breakpoint scaffolding", () => {
const css = readDashboardStylesSource();
const tabletBlock = getCssAtRuleBlockContaining(css, "@media (min-width: 769px) and (max-width: 1024px)", ".modal.task-detail-modal");
const mobileBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".detail-meta-inline-controls");
const rowSvgBlock = getExactCssRuleBlock(css, ".detail-meta-inline-controls svg");
// FNXC:TaskDetailModalResponsive 2026-07-19-12:00: Source guards preserve
// the scoped token contract; Blink smoke separately proves computed sizes.
expect(rowSvgBlock).toContain("width: var(--icon-size-sm);");
expect(rowSvgBlock).toContain("height: var(--icon-size-sm);");
expect(rowSvgBlock).toContain("flex-shrink: 0;");
expect(css).not.toMatch(/\.detail-meta-inline-controls svg\s*\{[^}]*width:\s*1em/);
expect(tabletBlock).not.toBe("");
expect(mobileBlock).toMatch(/\.detail-meta-inline-controls\s*\{[^}]*flex-wrap:\s*wrap;/);
});
it("gives every task-detail inline action the shared square tokenized box across themes (FN-8287)", () => {
const css = readDashboardStylesSource();
const sharedSizingSelector = [

View File

@@ -159,6 +159,29 @@ export function createSmokeHtml() {
</section>
`)).join("");
/*
FNXC:TaskDetailModalResponsive 2026-07-19-12:00:
FN-8396 mirrors Task Detail's direct and wrapped SVG structures so Blink can
prove the scoped row rule normalizes ProviderIcon alongside the CSS-only
Oversight Eye/EyeOff contract at every responsive breakpoint.
*/
const taskDetailInlineRowFixtures = [
["full", true, true],
["without-github", false, true],
["without-oversight", true, false],
["without-optionals", false, false],
].map(([variant, includeGithub, includeOversight]) => `
<section data-smoke="task-detail-inline-row-${variant}" aria-label="Task Detail inline action ${variant} fixture">
<div class="detail-meta-inline-controls" data-testid="detail-meta-inline-controls">
<button class="btn btn-icon btn-sm" data-testid="detail-inline-attach" type="button" aria-label="Attach file"><svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><path d="M2 6h8"/></svg></button>
${includeGithub ? '<button class="btn btn-icon btn-sm" data-testid="detail-inline-github-toggle" type="button" aria-label="Toggle GitHub tracking"><span class="provider-icon"><svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M2 8h12"/></svg></span></button>' : ""}
${includeOversight ? '<button class="btn btn-icon btn-sm detail-oversight-menu-trigger" data-testid="detail-oversight-menu-trigger" type="button" aria-label="Oversight actions"><svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true"><path d="M2 12s4-6 10-6 10 6 10 6-4 6-10 6S2 12 2 12Z"/></svg></button>' : ""}
<div class="detail-priority-picker"><button class="btn btn-icon btn-sm" data-testid="detail-priority-trigger" type="button" aria-label="Priority: Normal"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10"/></svg></button></div>
<button class="btn btn-icon btn-sm detail-execution-mode-toggle" data-testid="detail-execution-mode-toggle" type="button" aria-label="Execution mode: fast"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M7 2v10"/></svg></button>
</div>
</section>
`).join("");
return `<!doctype html>
<html lang="en">
<head>
@@ -238,6 +261,10 @@ export function createSmokeHtml() {
${quickAddComposerFixtures}
</section>
<section data-smoke="task-detail-inline-row-fixtures" aria-label="Task Detail inline action layout fixtures">
${taskDetailInlineRowFixtures}
</section>
<footer class="executor-status-bar">
<div class="executor-status-bar__segment">
<span class="executor-status-bar__indicator executor-status-bar__indicator--running"></span>
@@ -1133,6 +1160,46 @@ async function runSmokeChecks(page, pageUrl) {
});
})()`);
const collectTaskDetailInlineIconSizes = () => evaluate(page, `(() => {
return [...document.querySelectorAll('section[data-smoke^="task-detail-inline-row-"]:not([data-smoke="task-detail-inline-row-fixtures"])')].map((fixture) => {
const row = fixture.querySelector('.detail-meta-inline-controls');
const icons = [...row.querySelectorAll('svg')].map((svg) => {
const style = getComputedStyle(svg);
return { width: style.width, height: style.height };
});
return {
fixture: fixture.dataset.smoke,
rowOverflow: row.scrollWidth - row.clientWidth,
icons,
};
});
})()`);
/*
FNXC:TaskDetailModalResponsive 2026-07-19-12:00:
Visible SVG dimensions are a browser-only invariant: every optional-control
variant must measure the compact token at mobile, tablet, and desktop, rather
than relying on CSS-source parsing or a tablet-only regression check.
*/
for (const [name, width, height, deviceScaleFactor, mobile] of [
["mobile", 390, 844, 2, true],
["tablet", 900, 900, 1, false],
["desktop", 1440, 900, 1, false],
]) {
await page.send("Emulation.setDeviceMetricsOverride", { width, height, deviceScaleFactor, mobile });
await evaluate(page, "document.fonts ? document.fonts.ready.then(() => true) : true");
const taskDetailIconSizes = await collectTaskDetailInlineIconSizes();
assertSmokeResult(
`Task Detail inline action icons are uniformly 14px at ${name}`,
taskDetailIconSizes.length === 4
&& taskDetailIconSizes.every((fixture) => fixture.rowOverflow <= 1
&& fixture.icons.length >= 3
&& fixture.icons.every((icon) => icon.width === "14px" && icon.height === "14px")
&& new Set(fixture.icons.map((icon) => `${icon.width}×${icon.height}`)).size === 1),
JSON.stringify(taskDetailIconSizes),
);
}
await page.send("Emulation.setDeviceMetricsOverride", {
width: 412,
height: 915,