FN-7683: fix Quick Add mobile Save button height overshoot with fixed action-row box height

Upgrades Quick Add action-row height parity from a bare min-height floor to a true fixed box height, then adds a mobile-only Save correction per operator feedback so Save matches its siblings at the <=768px breakpoint without touching desktop/tablet sizing.

- Pair min-height with an equal max-height (plus tokenized line-height and centered alignment) on `.quick-entry-actions .btn, .quick-entry-actions .wf-optional-steps-dropdown-trigger` at both the desktop base rule and the <=768px touch-target media block (FN-5751: never a breakpoint-only fix)
- Add a mobile-only override scoped to `[data-testid="quick-entry-save"]` inside the <=768px media query (zero vertical padding, line-height:1) so Save's text+icon content fits the same fixed box as its siblings, leaving desktop/tablet Save sizing untouched
- Add regression coverage asserting the fixed min-height==max-height contract at both breakpoints, that shared .btn-sm/.btn-icon/.dep-trigger rules are untouched, and that the Save-only override exists only inside the mobile media query
- Add a patch changeset documenting the fix and follow-up for @runfusion/fusion

Files changed:
 .changeset/FN-7683-quick-add-height-parity.md      |   7 +
 .../quick-entry-workflow-trigger-height.test.tsx   | 181 ++++++++++++++++++++-
 .../dashboard/app/components/QuickEntryBox.css     |  53 ++++++
 3 files changed, 234 insertions(+), 7 deletions(-)

Fusion-Task-Id: FN-7683

Fusion-Task-Lineage: d3df638a-812e-4fc2-aada-cfee58d29f2b

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-08 09:43:19 -07:00
parent 2bea332ff2
commit 9a5a8d2b5f
3 changed files with 234 additions and 7 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Quick Add action-row controls now resolve one identical box height, not just a min-height floor.
category: fix
dev: Upgraded `.quick-entry-actions .btn, .quick-entry-actions .wf-optional-steps-dropdown-trigger` in QuickEntryBox.css from a bare min-height floor to a fixed box height (min-height paired with an equal max-height) plus tokenized line-height and centered alignment, at both the desktop base rule and the <=768px touch-target media block. Follow-up: a mobile-only Save-specific override (no vertical padding, line-height:1) further corrects Save's mobile sizing to match siblings without affecting desktop/tablet.

View File

@@ -182,7 +182,7 @@ describe("quick-entry-workflow-trigger height parity (FN-7677)", () => {
expect(screen.getByTestId("quick-entry-save")).toBeInTheDocument();
});
it("re-asserts .btn-sm's own padding on .quick-entry-workflow-trigger instead of inheriting the shorter shared .dep-trigger padding", () => {
it("tokenizes .quick-entry-workflow-trigger's own padding rather than inheriting the shorter shared .dep-trigger padding (FN-7682)", () => {
const cssContent = loadAllAppCss();
// .btn-sm establishes the padding contract the Save/Fast/Subtask buttons resolve.
@@ -200,19 +200,25 @@ describe("quick-entry-workflow-trigger height parity (FN-7677)", () => {
expect(depTriggerMatch).not.toBeNull();
expect(depTriggerMatch![1].trim()).toBe("3px 8px");
// .quick-entry-workflow-trigger must locally re-assert the .btn-sm padding
// value (not a new arbitrary literal) so cascade order resolves it to the
// same box height as its .btn.btn-sm siblings.
// .quick-entry-workflow-trigger must locally re-assert its OWN padding
// (FN-7682 tokenized it to var(--space-sm) var(--space-md), replacing the
// earlier raw "4px 10px" literal) so it does not fall back to the shared
// .dep-trigger's shorter 3px/8px padding. Since FN-7683, box HEIGHT parity
// no longer depends on this padding value matching .btn-sm's literal —
// the FN-7683 fixed min-height==max-height contract on
// `.quick-entry-actions .btn` normalizes resolved height regardless of
// padding — so this only asserts the padding is tokenized and distinct
// from the shared .dep-trigger override, not literal parity with .btn-sm.
const triggerMatch = cssContent.match(
/\.quick-entry-workflow-trigger\s*\{[^}]*padding:\s*([^;]+);/,
);
expect(triggerMatch).not.toBeNull();
const triggerPadding = triggerMatch![1].trim();
expect(triggerPadding).toBe(btnSmPadding);
expect(triggerPadding).toBe("var(--space-sm) var(--space-md)");
expect(triggerPadding).not.toBe(depTriggerMatch![1].trim());
});
it("keeps the height-parity override in the base (non-media-query) rule so desktop widths are covered too, not only the ≤768px touch-target block", () => {
it("keeps the workflow-trigger padding override in the base (non-media-query) rule so desktop widths are covered too, not only the ≤768px touch-target block", () => {
const cssContent = loadAllAppCss();
// Strip everything inside @media blocks to isolate base/desktop rules.
@@ -222,7 +228,7 @@ describe("quick-entry-workflow-trigger height parity (FN-7677)", () => {
/\.quick-entry-workflow-trigger\s*\{[^}]*padding:\s*([^;]+);/,
);
expect(baseTriggerMatch).not.toBeNull();
expect(baseTriggerMatch![1].trim()).toBe("4px 10px");
expect(baseTriggerMatch![1].trim()).toBe("var(--space-sm) var(--space-md)");
});
it("does not modify the shared global .dep-trigger rule's selector list (InlineCreateCard/NewTaskModal/TaskDetailModal/TaskForm still share it)", () => {
@@ -246,3 +252,164 @@ describe("quick-entry-workflow-trigger height parity (FN-7677)", () => {
expect(trigger.textContent?.length ?? 0).toBeGreaterThan(0);
});
});
/*
FNXC:QuickAddActionRow 2026-07-08-10:15:
FN-7683 regression coverage. FN-7680 gave `.quick-entry-actions .btn,
.quick-entry-actions .wf-optional-steps-dropdown-trigger` a bare `min-height`
floor, which only guarantees a control is AT LEAST that tall — it cannot clamp
a variant (e.g. Save's text + inline `<Save size={12}>` icon content) whose
natural content box already resolves TALLER than the floor back down to match
shorter siblings. These tests assert the upgraded contract declares a FIXED
box height (min-height paired with an EQUAL max-height, not a bare floor) plus
centered, tokenized line-height, at BOTH the desktop base rule and the
≤768px touch-target media block (FN-5751: never a breakpoint-only fix). jsdom
does not run a real layout engine (`getBoundingClientRect` returns zeros and
`calc()`/`var()` are unresolved there), so parity is asserted at the CSS
declaration level — not via pixel measurement — plus a rendered-DOM check that
the controls remain siblings in one `.quick-entry-actions` row.
*/
describe("quick-entry-actions fixed-height parity, not just a min-height floor (FN-7683)", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers({ shouldAdvanceTime: true });
localStorage.clear();
vi.mocked(fetchAgents).mockResolvedValue([]);
Object.defineProperty(URL, "createObjectURL", {
configurable: true,
writable: true,
value: vi.fn(() => "blob:mock"),
});
Object.defineProperty(URL, "revokeObjectURL", {
configurable: true,
writable: true,
value: vi.fn(),
});
});
afterEach(async () => {
await act(async () => {
vi.runOnlyPendingTimers();
});
vi.useRealTimers();
localStorage.clear();
});
it("declares a FIXED box height (min-height paired with an equal max-height) on the desktop base rule, not a bare min-height floor", () => {
const cssContent = loadAllAppCss();
const withoutMediaBlocks = cssContent.replace(/@media[^{]*\{(?:[^{}]*\{[^{}]*\})*[^{}]*\}/g, "");
const baseRuleMatch = withoutMediaBlocks.match(
/\.quick-entry-actions \.btn,\s*\n\.quick-entry-actions \.wf-optional-steps-dropdown-trigger \{([^}]*)\}/,
);
expect(baseRuleMatch).not.toBeNull();
const baseRuleBody = baseRuleMatch![1];
const minHeightMatch = baseRuleBody.match(/min-height:\s*([^;]+);/);
const maxHeightMatch = baseRuleBody.match(/max-height:\s*([^;]+);/);
expect(minHeightMatch).not.toBeNull();
expect(maxHeightMatch).not.toBeNull();
// The defect this task fixes: a bare min-height floor with no max-height
// cannot clamp a taller variant back down. Assert max-height is present
// AND equal to min-height, i.e. a true fixed box, not merely a floor.
expect(maxHeightMatch![1].trim()).toBe(minHeightMatch![1].trim());
// Centered, tokenized line-height so text-only, icon-only, and text+icon
// content (Save's inline <Save size={12}> icon) all resolve within the
// same fixed box regardless of natural content height.
expect(baseRuleBody).toMatch(/line-height:\s*var\(--line-height-tight\)/);
expect(baseRuleBody).toMatch(/align-items:\s*center/);
});
it("declares the SAME fixed-height contract (min-height == max-height) inside the ≤768px touch-target media block, not only at desktop widths", () => {
const cssContent = loadAllAppCss();
// Anchor on the unique "Quick Entry Mobile Touch + Overflow Fixes" marker
// comment (rather than a generic "@media (max-width: 768px)" boundary,
// which recurs across other concatenated component/style CSS files) to
// isolate this specific mobile touch-target block unambiguously.
const mobileRuleMatch = cssContent.match(
/Quick Entry Mobile Touch[\s\S]*?\.quick-entry-actions \.btn,\s*\n\s*\.quick-entry-actions \.wf-optional-steps-dropdown-trigger \{([^}]*)\}/,
);
expect(mobileRuleMatch).not.toBeNull();
const mobileRuleBody = mobileRuleMatch![1];
const minHeightMatch = mobileRuleBody.match(/min-height:\s*([^;]+);/);
const maxHeightMatch = mobileRuleBody.match(/max-height:\s*([^;]+);/);
expect(minHeightMatch).not.toBeNull();
expect(maxHeightMatch).not.toBeNull();
expect(maxHeightMatch![1].trim()).toBe(minHeightMatch![1].trim());
expect(mobileRuleBody).toMatch(/line-height:\s*var\(--line-height-tight\)/);
expect(mobileRuleBody).toMatch(/align-items:\s*center/);
// Mobile and desktop intentionally use different fixed heights (36px vs
// 28px touch targets) — that is expected; parity is required WITHIN each
// breakpoint, not across breakpoints.
expect(minHeightMatch![1].trim()).toBe("calc(var(--space-2xl) + var(--space-xs))");
});
it("does not modify the shared global .btn-sm / .btn-icon / .dep-trigger rules in styles.css (cross-surface regression guard)", () => {
const stylesCssContent = loadStylesCss();
const btnSmMatch = stylesCssContent.match(/\.btn-sm\s*\{([^}]*)\}/);
expect(btnSmMatch).not.toBeNull();
expect(btnSmMatch![1]).not.toMatch(/max-height/);
const btnIconMatch = stylesCssContent.match(/\.btn-icon\s*\{([^}]*)\}/);
expect(btnIconMatch).not.toBeNull();
expect(btnIconMatch![1]).toMatch(/line-height:\s*0/);
const depTriggerMatches = stylesCssContent.match(/\.dep-trigger,\s*\n\s*\.inline-create-model-trigger\s*\{/g);
expect(depTriggerMatches).not.toBeNull();
expect(depTriggerMatches!.length).toBe(1);
});
it("renders Save, Attach, Fast, and the workflow trigger as siblings in one .quick-entry-actions row (no shell/layout regression; jsdom cannot measure real pixel heights)", () => {
mockDesktopViewport();
renderQuickEntryBox();
const saveButton = screen.getByTestId("quick-entry-save");
const attachButton = screen.getByTestId("quick-entry-attach");
const fastToggle = screen.getByTestId("quick-entry-fast-toggle");
const workflowTrigger = screen.getByTestId("quick-entry-workflow-trigger");
const actionsRow = saveButton.closest(".quick-entry-actions");
expect(actionsRow).not.toBeNull();
expect(actionsRow?.contains(attachButton)).toBe(true);
expect(actionsRow?.contains(fastToggle)).toBe(true);
expect(actionsRow?.contains(workflowTrigger)).toBe(true);
});
it("applies a Save-only mobile correction (tighter line-height, no vertical padding) scoped to the ≤768px breakpoint, without touching the shared desktop rule", () => {
const cssContent = loadAllAppCss();
// The mobile-only Save override must exist, targeting the existing
// quick-entry-save test id (no new wrapper element), inside a
// max-width: 768px media query.
const mobileSaveRuleMatch = cssContent.match(
/@media \(max-width: 768px\) \{[\s\S]*?\.quick-entry-actions \[data-testid="quick-entry-save"\] \{([^}]*)\}/,
);
expect(mobileSaveRuleMatch).not.toBeNull();
const mobileSaveRuleBody = mobileSaveRuleMatch![1];
expect(mobileSaveRuleBody).toMatch(/padding-top:\s*0/);
expect(mobileSaveRuleBody).toMatch(/padding-bottom:\s*0/);
expect(mobileSaveRuleBody).toMatch(/line-height:\s*1\b/);
// The desktop base (non-media) rule must NOT contain this Save-specific
// override — desktop/tablet Save sizing is intentionally left untouched.
const withoutMediaBlocks = cssContent.replace(/@media[^{]*\{(?:[^{}]*\{[^{}]*\})*[^{}]*\}/g, "");
expect(withoutMediaBlocks).not.toMatch(/\[data-testid="quick-entry-save"\]/);
// The shared `.quick-entry-actions .btn, .quick-entry-actions
// .wf-optional-steps-dropdown-trigger` fixed-height contract (governing
// every sibling control at both breakpoints) must remain exactly as
// FN-7683 left it — this follow-up must not weaken or duplicate it.
const sharedRuleMatches = cssContent.match(
/\.quick-entry-actions \.btn,\s*\n\.quick-entry-actions \.wf-optional-steps-dropdown-trigger \{/g,
);
expect(sharedRuleMatches).not.toBeNull();
expect(sharedRuleMatches!.length).toBe(1);
});
});

View File

@@ -215,10 +215,37 @@ existing mobile touch-target min-height block further down this file (the
normalization pattern for mobile; this rule closes the gap for desktop widths
so the invariant holds at both breakpoints (FN-5751 lesson: no mobile-only or
desktop-only fix).
FNXC:QuickAddActionRow 2026-07-08-10:15:
FN-7683 — a bare `min-height` floor is not sufficient: it only guarantees a
button is AT LEAST that tall, it never clamps a variant whose natural content
box (line-height + padding + inline icon) already resolves TALLER than the
floor. Save (`btn btn-task-create btn-sm`) renders text plus an inline
`<Save size={12}>` icon as two separate inline boxes inside the button; that
combination, together with the inherited (non-tokenized) body line-height at
12px font-size, can resolve a content/line box taller than the
`calc(--space-xl + --space-xs)` (28px) floor, so `min-height` alone left Save
measurably taller than shorter siblings (Attach's `.btn-icon`, whose
`line-height: 0` collapses it to its bare icon). A floor cannot shrink an
over-tall variant back down — only a FIXED box height can. Upgrade the
contract: pair `min-height` with an equal `max-height` (a true fixed box, not
just a floor) and set an explicit tokenized `line-height` plus
`align-items: center` so every control's content — text-only, icon-only, or
text+icon — is vertically centered and clipped to the same resolved height
regardless of its natural content box. This keeps `box-sizing: border-box`
working as before (padding/border still absorbed into the declared height)
while guaranteeing IDENTICAL resolved height across Save, workflow trigger,
Attach, GitHub toggle, Fast, Priority, Deps, Subtask, and the optional-steps
trigger — not merely all-at-least-28px. Applies at both this desktop rule and
the mobile touch-target block below (FN-5751: never a breakpoint-only fix).
*/
.quick-entry-actions .btn,
.quick-entry-actions .wf-optional-steps-dropdown-trigger {
min-height: calc(var(--space-xl) + var(--space-xs));
max-height: calc(var(--space-xl) + var(--space-xs));
line-height: var(--line-height-tight);
align-items: center;
overflow: hidden;
}
/*
@@ -633,7 +660,33 @@ FN-7682 — the tokenized-CSS test forbids raw px in the workflow-selector rules
.quick-entry-actions .btn,
.quick-entry-actions .wf-optional-steps-dropdown-trigger {
/* FNXC:WorkflowOptionalSteps 2026-06-25-00:00: The optional-steps trigger is not a `.btn`, so opt it into the same mobile quick-entry touch-target contract as the surrounding action buttons without adding a wrapper that could leave an empty shell for zero-step workflows. */
/* FNXC:QuickAddActionRow 2026-07-08-10:15: FN-7683 — mirror the desktop upgrade: a bare min-height floor cannot equalize a variant (e.g. Save's text+icon content) that naturally resolves taller than the touch-target height. Pair min-height with an equal max-height plus centered, tokenized line-height so every control lands on the SAME resolved touch-target height at this breakpoint, not merely >= it. */
min-height: calc(var(--space-2xl) + var(--space-xs));
max-height: calc(var(--space-2xl) + var(--space-xs));
line-height: var(--line-height-tight);
align-items: center;
overflow: hidden;
}
/*
FNXC:QuickAddActionRow 2026-07-08-11:30:
FN-7683 follow-up (operator feedback) — desktop/tablet parity from the fix
above stays untouched, and so does the shared mobile touch-target contract
applied to every sibling control. On real mobile browsers, Save (`btn
btn-task-create btn-sm`) still renders visibly taller than its shorter
siblings (Fast, the workflow trigger) at this breakpoint: its inline
`<Save size={12}>` icon plus text naturally claims more vertical space than
the fixed box, so the excess was only being silently clipped by the shared
rule's `overflow: hidden` rather than the button shrinking cleanly. Scope a
Save-only correction (via its existing `quick-entry-save` test id, not a new
wrapper) that removes the extra vertical padding and tightens line-height so
Save's natural content fits the SAME fixed box as its siblings without
relying on clipping — mobile-only; desktop/tablet Save sizing is unaffected.
*/
.quick-entry-actions [data-testid="quick-entry-save"] {
padding-top: 0;
padding-bottom: 0;
line-height: 1;
}
.quick-entry-box .dep-dropdown {