From 9a5a8d2b5f5070e576b2a09b43c5a4c5103c9110 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 8 Jul 2026 09:43:19 -0700 Subject: [PATCH] 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) --- .changeset/FN-7683-quick-add-height-parity.md | 7 + ...ick-entry-workflow-trigger-height.test.tsx | 181 +++++++++++++++++- .../app/components/QuickEntryBox.css | 53 +++++ 3 files changed, 234 insertions(+), 7 deletions(-) create mode 100644 .changeset/FN-7683-quick-add-height-parity.md diff --git a/.changeset/FN-7683-quick-add-height-parity.md b/.changeset/FN-7683-quick-add-height-parity.md new file mode 100644 index 0000000000..0317da23b4 --- /dev/null +++ b/.changeset/FN-7683-quick-add-height-parity.md @@ -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. diff --git a/packages/dashboard/app/__tests__/quick-entry-workflow-trigger-height.test.tsx b/packages/dashboard/app/__tests__/quick-entry-workflow-trigger-height.test.tsx index 4c2626b056..93a1d70495 100644 --- a/packages/dashboard/app/__tests__/quick-entry-workflow-trigger-height.test.tsx +++ b/packages/dashboard/app/__tests__/quick-entry-workflow-trigger-height.test.tsx @@ -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 `` 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 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); + }); +}); diff --git a/packages/dashboard/app/components/QuickEntryBox.css b/packages/dashboard/app/components/QuickEntryBox.css index ef5527ef75..a91ee795e6 100644 --- a/packages/dashboard/app/components/QuickEntryBox.css +++ b/packages/dashboard/app/components/QuickEntryBox.css @@ -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 +`` 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 + `` 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 {