FN-8421: align tablet task modal control heights

Align Task Detail action controls and Planner Chat composer heights across tablet and desktop.

- Apply shared compact-height classes to inline Task Detail actions
- Use Quick Add control-height tokens and remove conflicting button padding
- Size Planner Chat Send and Stop controls to match the composer
- Add dashboard regression coverage and a patch changeset

Files changed:
 .../fn-8421-task-detail-tablet-control-heights.md  |  7 ++++++
 .../dashboard/app/components/TaskDetailModal.css   | 18 ++++++++++++---
 .../dashboard/app/components/TaskDetailModal.tsx   | 11 ++++++---
 .../app/components/TaskPlannerChatTab.css          | 10 ++++++++
 .../__tests__/TaskDetailModal.rendering.test.tsx   |  8 +++++++
 ...etailModal.responsive-and-dependencies.test.tsx | 13 +++++++++++
 .../__tests__/TaskPlannerChatTab.test.tsx          | 27 ++++++++++++++++++++++
 7 files changed, 88 insertions(+), 6 deletions(-)

Fusion-Task-Id: FN-8421

Fusion-Task-Lineage: 151795c8-4c00-4976-8901-9446557fc1a3

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-20 01:00:37 -07:00
parent 2c1567c6b8
commit 57636226b1
7 changed files with 88 additions and 6 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Task detail Oversight/Fast and chat send heights match sibling controls on tablet.
category: fix
dev: Completes FN-8287 class wiring on attach/github/priority; aliases detail shared square to Quick Add compact height tokens; equalizes Planner Chat input/send block size for tablet/desktop.

View File

@@ -336,6 +336,13 @@ The task-detail modal metadata must keep priority, execution mode, provenance, P
and applies at every width, so no mobile-only override is needed here. and applies at every width, so no mobile-only override is needed here.
*/ */
.detail-meta-inline-controls { .detail-meta-inline-controls {
/*
FNXC:QuickAddActionRow 2026-07-20-12:00:
FN-8421 keeps the Task Detail action row at the mobile touch floor while
the default compact Quick Add alias serves desktop and tablet widths.
*/
--detail-priority-control-min-height: var(--quick-entry-action-row-height-mobile);
flex-wrap: wrap; flex-wrap: wrap;
} }
@@ -382,7 +389,14 @@ The task-detail modal metadata must keep priority, execution mode, provenance, P
low/high/urgent, oversight observe/steer/autonomous/off, execution-mode low/high/urgent, oversight observe/steer/autonomous/off, execution-mode
fast) are untouched. fast) are untouched.
*/ */
--detail-priority-control-min-height: calc(var(--space-lg) + var(--space-lg) + var(--space-xs)); /*
FNXC:QuickAddActionRow 2026-07-20-12:00:
FN-8421 completes FN-8287's shared-square contract. Desktop and tablet must
alias Quick Add's compact control token, not make every action equally tall
with the former space-calc box; the mobile rule above raises the same alias
to the touch-floor token.
*/
--detail-priority-control-min-height: var(--quick-entry-action-row-height-desktop);
--detail-control-border-radius: var(--radius-md); --detail-control-border-radius: var(--radius-md);
display: flex; display: flex;
@@ -512,7 +526,6 @@ no raw colors so the tokenized-focus assertion stays green.
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: var(--space-xs); gap: var(--space-xs);
padding-block: var(--space-xs);
} }
.detail-execution-mode-toggle--fast { .detail-execution-mode-toggle--fast {
@@ -698,7 +711,6 @@ affect its size or anchoring — only the trigger's box height changes.
} }
.detail-oversight-menu-trigger { .detail-oversight-menu-trigger {
padding-block: var(--space-xs);
/* /*
FNXC:PlannerOversight 2026-07-05-00:00: FNXC:PlannerOversight 2026-07-05-00:00:
FN-7604 — this trigger is now the single universal surface at every FN-7604 — this trigger is now the single universal surface at every

View File

@@ -4287,11 +4287,16 @@ export function TaskDetailContent({
attach, GitHub tracking, Oversight, Priority, then Fast. The compact attach, GitHub tracking, Oversight, Priority, then Fast. The compact
controls delegate to the existing single file-input upload and controls delegate to the existing single file-input upload and
GitHub-tracking handlers so this row never forks persistence paths. GitHub-tracking handlers so this row never forks persistence paths.
FNXC:QuickAddActionRow 2026-07-20-12:00:
FN-8421 completes FN-8287 sizing-class wiring: every mounted inline
action carries its shared-square class, preventing Oversight/Fast
from using a different tablet-height contract than its siblings.
*/} */}
<div className="detail-meta-inline-controls" data-testid="detail-meta-inline-controls"> <div className="detail-meta-inline-controls" data-testid="detail-meta-inline-controls">
<button <button
type="button" type="button"
className="btn btn-icon btn-sm" className="btn btn-icon btn-sm detail-inline-attach"
data-testid="detail-inline-attach" data-testid="detail-inline-attach"
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
disabled={uploading} disabled={uploading}
@@ -4303,7 +4308,7 @@ export function TaskDetailContent({
{canEditGithubTracking && !gitlabTrackedItem && ( {canEditGithubTracking && !gitlabTrackedItem && (
<button <button
type="button" type="button"
className={`btn btn-icon btn-sm ${githubTrackingEnabled ? "btn-primary" : ""}`} className={`btn btn-icon btn-sm detail-inline-github-toggle ${githubTrackingEnabled ? "btn-primary" : ""}`}
data-testid="detail-inline-github-toggle" data-testid="detail-inline-github-toggle"
onClick={() => void handleToggleGithubTracking()} onClick={() => void handleToggleGithubTracking()}
disabled={isSavingGithubTracking} disabled={isSavingGithubTracking}
@@ -4530,7 +4535,7 @@ export function TaskDetailContent({
*/} */}
<button <button
type="button" type="button"
className="btn btn-icon btn-sm" className="btn btn-icon btn-sm detail-priority-trigger"
data-testid="detail-priority-trigger" data-testid="detail-priority-trigger"
onClick={() => setShowInlinePriorityPicker((isOpen) => !isOpen)} onClick={() => setShowInlinePriorityPicker((isOpen) => !isOpen)}
disabled={isSavingInlinePriority} disabled={isSavingInlinePriority}

View File

@@ -218,9 +218,19 @@ The Planner Chat streaming Stop button must occupy the same width footprint as t
resize: vertical; resize: vertical;
} }
/*
FNXC:TaskDetailPlannerChat 2026-07-20-12:00:
FN-8421 extends the existing desktop/mobile composer parity to tablet, which
inherits these base rules. Send and streaming Stop share this class, so pin
its border-box block size and clear button padding to the textarea's compact
height rather than relying on the global button minimum.
*/
.task-planner-chat-send { .task-planner-chat-send {
flex: 0 0 auto; flex: 0 0 auto;
block-size: calc(var(--space-2xl) + var(--space-sm));
min-block-size: calc(var(--space-2xl) + var(--space-sm)); min-block-size: calc(var(--space-2xl) + var(--space-sm));
box-sizing: border-box;
padding: 0;
/* FN-7634: floor both the Send and Stop button variants (both carry this class) at the same width so the streaming swap never narrows or widens the control. */ /* FN-7634: floor both the Send and Stop button variants (both carry this class) at the same width so the streaming swap never narrows or widens the control. */
min-inline-size: var(--chat-input-control-size); min-inline-size: var(--chat-input-control-size);
} }

View File

@@ -1431,6 +1431,14 @@ describe("TaskDetailModal", () => {
expect(row.indexOf("detail-inline-github-toggle")).toBeLessThan(row.indexOf("detail-oversight-menu-trigger")); 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-oversight-menu-trigger")).toBeLessThan(row.indexOf("detail-priority-trigger"));
expect(row.indexOf("detail-priority-trigger")).toBeLessThan(row.indexOf("detail-execution-mode-toggle")); expect(row.indexOf("detail-priority-trigger")).toBeLessThan(row.indexOf("detail-execution-mode-toggle"));
// FNXC:QuickAddActionRow 2026-07-20-12:00: Every test-id affordance must
// also carry its FN-8287 sizing class, including optional GitHub and
// Oversight surfaces, so mounted tablet controls share one compact box.
expect(row).toMatch(/className="btn btn-icon btn-sm detail-inline-attach"/);
expect(row).toMatch(/className=\{`btn btn-icon btn-sm detail-inline-github-toggle/);
expect(row).toMatch(/className="btn btn-icon btn-sm detail-oversight-menu-trigger"/);
expect(row).toMatch(/className="btn btn-icon btn-sm detail-priority-trigger"/);
expect(row).toMatch(/className=\{`btn btn-icon btn-sm detail-execution-mode-toggle/);
for (const label of ["aria-label", "title"]) { for (const label of ["aria-label", "title"]) {
expect(row.match(new RegExp(label, "g"))?.length).toBeGreaterThanOrEqual(5); expect(row.match(new RegExp(label, "g"))?.length).toBeGreaterThanOrEqual(5);
} }

View File

@@ -291,6 +291,19 @@ describe("TaskDetailModal", () => {
expect(sharedSizingBlock).toContain("border-width: var(--btn-border-width);"); expect(sharedSizingBlock).toContain("border-width: var(--btn-border-width);");
expect(sharedSizingBlock).toContain("border-color: var(--border);"); expect(sharedSizingBlock).toContain("border-color: var(--border);");
expect(sharedSizingBlock).toContain("border-radius: var(--detail-control-border-radius);"); expect(sharedSizingBlock).toContain("border-radius: var(--detail-control-border-radius);");
const inlineControlsBlock = getStandaloneCssRuleBlock(css, ".detail-meta-inline-controls");
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 mobileInlineControlsBlock = getCssRuleBlock(mobileBlock, ".detail-meta-inline-controls");
// FNXC:QuickAddActionRow 2026-07-20-12:00: Equal boxes are insufficient:
// desktop and tablet must use Quick Add's compact token, while mobile
// deliberately upgrades the same shared alias to its touch-floor token.
expect(inlineControlsBlock).toContain("--detail-priority-control-min-height: var(--quick-entry-action-row-height-desktop);");
expect(inlineControlsBlock).not.toContain("calc(var(--space-lg) + var(--space-lg) + var(--space-xs))");
expect(tabletBlock).not.toMatch(/\.detail-(?:oversight-menu-trigger|execution-mode-toggle)\s*\{[^}]*?(?:height|min-height|width|min-width):/);
expect(mobileInlineControlsBlock).toContain("--detail-priority-control-min-height: var(--quick-entry-action-row-height-mobile);");
}); });
it.skip("unifies border/radius/height across the Priority, Execution-mode, and Oversight quick controls (FN-7585)", () => { it.skip("unifies border/radius/height across the Priority, Execution-mode, and Oversight quick controls (FN-7585)", () => {

View File

@@ -937,6 +937,33 @@ describe("TaskPlannerChatTab", () => {
expect(mobileTextHideRule).toMatch(/span:not\(\.chat-input-stop-icon\)/); expect(mobileTextHideRule).toMatch(/span:not\(\.chat-input-stop-icon\)/);
}); });
it("keeps Planner Chat input and send/stop height-parity across desktop, tablet, and mobile (FN-8421)", () => {
const mobileQueryStart = taskPlannerChatCss.indexOf("@media (max-width: 768px)");
const desktopAndTabletCss = mobileQueryStart >= 0 ? taskPlannerChatCss.slice(0, mobileQueryStart) : taskPlannerChatCss;
const desktopInputRule = desktopAndTabletCss.match(/\.task-planner-chat-input\s*\{[^}]*\}/)?.[0] ?? "";
const desktopSendRule = desktopAndTabletCss.match(/\.task-planner-chat-send\s*\{[^}]*\}/)?.[0] ?? "";
const mobileInputRule = taskPlannerChatCss.slice(mobileQueryStart).match(/\.task-planner-chat-input\s*\{[^}]*\}/)?.[0] ?? "";
const mobileSendRule = taskPlannerChatCss.slice(mobileQueryStart).match(/\.task-planner-chat-send\s*\{[^}]*\}/)?.[0] ?? "";
// FNXC:TaskDetailPlannerChat 2026-07-20-12:00: The base contract serves
// desktop and the 769–1024 tablet band; explicit border-box sizing and
// zero button padding prevent inherited `.btn` padding from re-inflating
// the Send or Stop variant above the textarea.
const desktopTabletHeight = "calc(var(--space-2xl) + var(--space-sm))";
const mobileHeight = "calc(var(--space-2xl) + var(--space-lg))";
expect(desktopInputRule).toContain(`height: ${desktopTabletHeight};`);
expect(desktopInputRule).toContain(`min-height: ${desktopTabletHeight};`);
expect(desktopSendRule).toContain(`block-size: ${desktopTabletHeight};`);
expect(desktopSendRule).toContain(`min-block-size: ${desktopTabletHeight};`);
expect(desktopSendRule).toContain("box-sizing: border-box;");
expect(desktopSendRule).toContain("padding: 0;");
expect(mobileInputRule).toContain(`height: ${mobileHeight};`);
expect(mobileInputRule).toContain(`min-height: ${mobileHeight};`);
expect(mobileSendRule).toContain(`block-size: ${mobileHeight};`);
expect(mobileSendRule).toContain(`min-block-size: ${mobileHeight};`);
expect(mobileSendRule).toContain("padding: 0;");
});
it("renders live and stored thinking output through the standard chat surface", async () => { it("renders live and stored thinking output through the standard chat surface", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
mockFetchChatMessages.mockResolvedValueOnce({ mockFetchChatMessages.mockResolvedValueOnce({