FN-6944: reclaim quick entry textarea width

Quick entry textareas now avoid inheriting refine-button padding that caused right-side dead space.

- Override quick-entry textarea right padding with a selector specific enough to beat the shared description refine rule without !important.
- Add CSS cascade regression coverage confirming quick entry reclaims width while global description textareas retain overlay padding.
- Add a patch changeset for the published CLI package.

Files changed:
 .changeset/fn-6944-quick-entry-width.md            |   7 ++
 .../dashboard/app/components/QuickEntryBox.css     |   6 +-
 .../components/__tests__/QuickEntryBox.test.tsx    | 114 +++++++++++++++++++++
 3 files changed, 124 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-6944

Fusion-Task-Lineage: 2a6c7190-ff87-4da6-9437-8e57a113ce56
This commit is contained in:
gsxdsm
2026-06-24 21:34:04 -07:00
parent d3593065e4
commit 214a60c085
3 changed files with 124 additions and 3 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Let quick-entry text use the full entry box width instead of wrapping early.
category: fix
dev: Adds a QuickEntryBox-specific textarea padding override and CSS cascade regression coverage.

View File

@@ -81,10 +81,10 @@ Board/columns omit `.quick-entry--single-line`, keeping the tall 80px + auto-gro
}
/*
FNXC:QuickEntry 2026-06-23-02:45:
The global `.description-with-refine textarea { padding-right: 70px }` (styles.css) reserves space for a refine button overlaid at the textarea's right edge. In QuickEntryBox the refine button lives in the controls panel, NOT overlaid on the textarea, so that 70px is dead space — it left a large empty gap on the right of the quick-add input (computed padding-right: 70px). Reclaim that width down to a tight var(--space-sm). Scope to the wrapping box (.description-with-refine ancestor) so this beats the global rule on specificity, not just source order (the prior `.quick-entry-textarea-wrap textarea` override tied on specificity and lost to styles.css load order). Nothing overlays the input's right edge in this variant, so text now uses the full reclaimed width.
FNXC:QuickEntry 2026-06-24-12:00:
The global `.description-with-refine textarea { padding-right: 70px }` (styles.css) reserves space for a refine button overlaid at the textarea's right edge. QuickEntryBox renders its refine action in the controls panel instead of overlaying the textarea, so inheriting that reserve creates dead right-side text space. Scope the reclaim to the quick-entry input and include the wrapper classes so this rule beats the global description rule by specificity, not source order, while TaskForm/NewTaskModal and fullscreen description textareas keep their overlay padding.
*/
.quick-entry-box .description-with-refine .quick-entry-textarea-wrap textarea {
.quick-entry-box .description-with-refine .quick-entry-textarea-wrap .quick-entry-input {
padding-right: var(--space-sm);
}

View File

@@ -6,6 +6,7 @@ import type { Task } from "@fusion/core";
import { checkDuplicateTasks, fetchSettings, fetchAgents, uploadAttachment } from "../../api";
import { useNodes } from "../../hooks/useNodes";
import { scopedKey } from "../../utils/projectStorage";
import { loadAllAppCss } from "../../test/cssFixture";
const MOCK_MODELS = [
{
@@ -27,6 +28,9 @@ const MOCK_MODELS = [
const TEST_PROJECT_ID = "proj-123";
const QUICK_ENTRY_STORAGE_KEY = scopedKey("kb-quick-entry-text", TEST_PROJECT_ID);
const QUICK_ENTRY_BOX_CSS = readFileSync("app/components/QuickEntryBox.css", "utf8");
const ALL_APP_CSS = loadAllAppCss();
const GLOBAL_DESCRIPTION_TEXTAREA_SELECTOR = ".description-with-refine textarea";
const QUICK_ENTRY_TEXTAREA_RECLAIM_SELECTOR = ".quick-entry-box .description-with-refine .quick-entry-textarea-wrap .quick-entry-input";
const originalWindowInnerWidthDescriptor = Object.getOwnPropertyDescriptor(window, "innerWidth");
const originalWindowMatchMediaDescriptor = Object.getOwnPropertyDescriptor(window, "matchMedia");
@@ -69,6 +73,49 @@ function quickEntryMobileActionsTouchRule() {
);
}
function escapeCssSelectorForRegex(selector: string) {
return selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function cssRuleBody(css: string, selector: string) {
return css.match(new RegExp(`${escapeCssSelectorForRegex(selector)}\\s*\\{([^}]*)\\}`))?.[1] ?? null;
}
function cssDeclarationValue(ruleBody: string, property: string) {
return ruleBody.match(new RegExp(`${property}\\s*:\\s*([^;]+);`))?.[1].trim() ?? null;
}
function classSpecificity(selector: string) {
return selector.match(/\.[A-Za-z0-9_-]+/g)?.length ?? 0;
}
function typeSpecificity(selector: string) {
return selector.split(/\s+/).filter((part) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(part)).length;
}
function expectQuickEntryCssWidthReclaim() {
const globalRule = cssRuleBody(ALL_APP_CSS, GLOBAL_DESCRIPTION_TEXTAREA_SELECTOR);
const quickEntryRule = cssRuleBody(ALL_APP_CSS, QUICK_ENTRY_TEXTAREA_RECLAIM_SELECTOR);
expect(globalRule).not.toBeNull();
expect(quickEntryRule).not.toBeNull();
expect(cssDeclarationValue(globalRule!, "padding-right")).toBe("70px");
expect(cssDeclarationValue(quickEntryRule!, "padding-right")).toBe("var(--space-sm)");
expect(quickEntryRule).not.toContain("!important");
const quickEntrySpecificity = [classSpecificity(QUICK_ENTRY_TEXTAREA_RECLAIM_SELECTOR), typeSpecificity(QUICK_ENTRY_TEXTAREA_RECLAIM_SELECTOR)];
const globalSpecificity = [classSpecificity(GLOBAL_DESCRIPTION_TEXTAREA_SELECTOR), typeSpecificity(GLOBAL_DESCRIPTION_TEXTAREA_SELECTOR)];
expect(quickEntrySpecificity[0]).toBeGreaterThan(globalSpecificity[0]);
}
function expectQuickEntryTextareaWidthReclaim(textarea: HTMLTextAreaElement) {
const box = textarea.closest(".quick-entry-box");
expect(box).toBeTruthy();
expect(textarea.closest(".description-with-refine")).toBeTruthy();
expect(textarea.closest(".quick-entry-textarea-wrap")).toBeTruthy();
expect(textarea.classList.contains("quick-entry-input")).toBe(true);
expectQuickEntryCssWidthReclaim();
}
const CREATED_TASK: Task = {
id: "FN-999",
title: "Created task",
@@ -434,6 +481,73 @@ describe("QuickEntryBox", () => {
});
});
describe("quick-entry textarea width reclaim (FN-6944)", () => {
it("declares a specific non-70px right-padding override while preserving description textarea overlay padding", () => {
expectQuickEntryCssWidthReclaim();
});
it("applies the reclaim hook for empty and populated board quick-entry textareas on desktop", () => {
mockDesktopViewport();
renderQuickEntryBox({ singleLine: false });
const textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement;
expect(textarea.placeholder).toBe("Add a task...");
expectQuickEntryTextareaWidthReclaim(textarea);
fireEvent.change(textarea, {
target: {
value: "This is a long multiline task description that should use the available quick entry width before wrapping.\nIt should not reserve the description refine-button gutter.",
},
});
expect(textarea.value).toContain("available quick entry width");
expectQuickEntryTextareaWidthReclaim(textarea);
});
it("applies the same reclaim hook in list singleLine mode and at the mobile breakpoint", () => {
mockMobileViewport();
renderQuickEntryBox({ singleLine: true, defaultExpanded: false });
const box = screen.getByTestId("quick-entry-box");
const textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement;
expect(window.matchMedia("(max-width: 768px)").matches).toBe(true);
expect(box.className).toContain("quick-entry--single-line");
expect(textarea.rows).toBe(1);
expectQuickEntryTextareaWidthReclaim(textarea);
fireEvent.change(textarea, {
target: { value: "A compact list quick entry should reclaim the same right-side text column on mobile." },
});
expectQuickEntryTextareaWidthReclaim(textarea);
});
it("keeps the quick-entry width reclaim through duplicate warning and disabled submitting states", async () => {
let resolveCreate!: () => void;
const onCreate = vi.fn().mockImplementation(() => new Promise<void>((resolve) => { resolveCreate = resolve; }));
vi.mocked(checkDuplicateTasks).mockResolvedValueOnce([
{ id: "FN-456", title: "Duplicate", description: "desc", column: "todo", score: 0.7 },
]);
renderQuickEntryBox({ onCreate });
const textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement;
fireEvent.change(textarea, { target: { value: "duplicate candidate with enough text to show the padding regression" } });
fireEvent.keyDown(textarea, { key: "Enter" });
expect(await screen.findByText("Possible duplicates")).toBeInTheDocument();
expectQuickEntryTextareaWidthReclaim(textarea);
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
vi.mocked(checkDuplicateTasks).mockResolvedValueOnce([]);
fireEvent.keyDown(textarea, { key: "Enter" });
await waitFor(() => expect(onCreate).toHaveBeenCalledTimes(1));
expect(textarea).toBeDisabled();
expectQuickEntryTextareaWidthReclaim(textarea);
await act(async () => {
resolveCreate();
});
});
});
describe("post-submission focus restoration (FN-6217)", () => {
it("does not auto-focus the quick-entry textarea on empty desktop mount", async () => {
mockDesktopViewport();