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:
7
.changeset/fn-8396-task-detail-tablet-icon-size.md
Normal file
7
.changeset/fn-8396-task-detail-tablet-icon-size.md
Normal 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.
|
||||||
@@ -12,6 +12,24 @@ describe("browser layout smoke fixture", () => {
|
|||||||
expect(html).toContain("pr-checks__details-link");
|
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", () => {
|
it("includes localized Quick Add Save fixtures for Board and List composers", () => {
|
||||||
const html = createSmokeHtml();
|
const html = createSmokeHtml();
|
||||||
expect(html).toContain('data-smoke="quick-add-save-fixtures"');
|
expect(html).toContain('data-smoke="quick-add-save-fixtures"');
|
||||||
|
|||||||
@@ -414,6 +414,20 @@ space or icon tokens from making this cluster visually uneven.
|
|||||||
border-radius: var(--detail-control-border-radius);
|
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:
|
FNXC:QuickAddActionRow 2026-07-17-12:00:
|
||||||
FN-8209: Task Detail uses the same compact icon-only priority affordance as
|
FN-8209: Task Detail uses the same compact icon-only priority affordance as
|
||||||
|
|||||||
@@ -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
|
"Oversight actions", which also matches `/actions/i` and made every such
|
||||||
query ambiguous once the trigger stopped being a mobile-only affordance.
|
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 { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
|
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
|
||||||
|
|
||||||
@@ -1411,6 +1413,29 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(fast).not.toHaveTextContent("Fast");
|
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", () => {
|
it("removes bespoke toolbar SVG sizing rules", () => {
|
||||||
const css = readDashboardStylesSource();
|
const css = readDashboardStylesSource();
|
||||||
expect(css).not.toMatch(/\.detail-oversight-menu-trigger svg\s*\{[^}]*width:\s*1em/);
|
expect(css).not.toMatch(/\.detail-oversight-menu-trigger svg\s*\{[^}]*width:\s*1em/);
|
||||||
|
|||||||
@@ -253,6 +253,22 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(css).not.toMatch(/@media \(max-width: 640px\)\s*\{[^}]*\.detail-meta-inline-controls\s*\{[^}]*flex-direction:\s*column;/);
|
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)", () => {
|
it("gives every task-detail inline action the shared square tokenized box across themes (FN-8287)", () => {
|
||||||
const css = readDashboardStylesSource();
|
const css = readDashboardStylesSource();
|
||||||
const sharedSizingSelector = [
|
const sharedSizingSelector = [
|
||||||
|
|||||||
@@ -159,6 +159,29 @@ export function createSmokeHtml() {
|
|||||||
</section>
|
</section>
|
||||||
`)).join("");
|
`)).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>
|
return `<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@@ -238,6 +261,10 @@ export function createSmokeHtml() {
|
|||||||
${quickAddComposerFixtures}
|
${quickAddComposerFixtures}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section data-smoke="task-detail-inline-row-fixtures" aria-label="Task Detail inline action layout fixtures">
|
||||||
|
${taskDetailInlineRowFixtures}
|
||||||
|
</section>
|
||||||
|
|
||||||
<footer class="executor-status-bar">
|
<footer class="executor-status-bar">
|
||||||
<div class="executor-status-bar__segment">
|
<div class="executor-status-bar__segment">
|
||||||
<span class="executor-status-bar__indicator executor-status-bar__indicator--running"></span>
|
<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", {
|
await page.send("Emulation.setDeviceMetricsOverride", {
|
||||||
width: 412,
|
width: 412,
|
||||||
height: 915,
|
height: 915,
|
||||||
|
|||||||
Reference in New Issue
Block a user