feat(FN-671): add disclosure toggle to QuickEntryBox

- Add disclosure state with expanded/collapsed toggle button
- Implement disclosure pattern CSS styles for visual feedback
- Update QuickEntryBox to support expandable/collapsible interface
- Fix CSS variable reference: use --card-hover instead of non-existent --background-hover
- Set default disclosure state to expanded (true) for backward compatibility
- Add comprehensive tests for disclosure state, toggle behavior, and interactions
This commit is contained in:
gsxdsm
2026-04-01 00:00:46 -07:00
parent 4ef12b36b7
commit 265bcede89
3 changed files with 278 additions and 75 deletions

View File

@@ -8,6 +8,7 @@ import { CustomModelDropdown } from "./CustomModelDropdown";
import { ModelSelectionModal } from "./ModelSelectionModal";
const STORAGE_KEY = "kb-quick-entry-text";
const DISCLOSURE_STORAGE_KEY = "kb-quick-entry-expanded";
interface QuickEntryBoxProps {
onCreate?: (input: TaskCreateInput) => Promise<void>;
@@ -57,7 +58,17 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
return "";
});
const [isSubmitting, setIsSubmitting] = useState(false);
// isExpanded controls textarea height styling (auto-resize)
const [isExpanded, setIsExpanded] = useState(false);
// isDisclosureExpanded controls visibility of the controls panel (Deps, Models, etc.)
const [isDisclosureExpanded, setIsDisclosureExpanded] = useState(() => {
if (typeof window !== "undefined") {
const saved = localStorage.getItem(DISCLOSURE_STORAGE_KEY);
// Default to true (expanded) for backward compatibility - only collapse if explicitly set to "false"
return saved !== "false";
}
return true;
});
const textareaRef = useRef<HTMLTextAreaElement>(null);
const justResetRef = useRef(false);
@@ -139,6 +150,13 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
}
}, [description]);
// Persist disclosure state to localStorage whenever it changes
useEffect(() => {
if (typeof window !== "undefined") {
localStorage.setItem(DISCLOSURE_STORAGE_KEY, isDisclosureExpanded.toString());
}
}, [isDisclosureExpanded]);
// Cleanup on unmount
useEffect(() => {
return () => {
@@ -206,7 +224,8 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
setIsModelModalOpen(false);
setIsRefineMenuOpen(false);
setIsRefining(false);
setIsExpanded(false); // Collapse on reset
setIsExpanded(false); // Collapse textarea height on reset
// Note: isDisclosureExpanded is NOT reset - user preference persists
justResetRef.current = true;
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
@@ -292,8 +311,9 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
localStorage.removeItem(STORAGE_KEY);
}
}
// Collapse on escape
// Collapse textarea and disclosure on escape
setIsExpanded(false);
setIsDisclosureExpanded(false);
textareaRef.current?.blur();
}
},
@@ -304,6 +324,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
showDeps,
isModelModalOpen,
isRefineMenuOpen,
setIsDisclosureExpanded,
],
);
@@ -427,15 +448,16 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
}
}, [availableModels]);
// Show expanded controls only when manually expanded (isExpanded)
const showExpandedControls = isExpanded;
// Show expanded controls based on disclosure state (user preference), not textarea focus
const showExpandedControls = isDisclosureExpanded;
const toggleExpanded = useCallback(() => {
setIsDisclosureExpanded((prev) => !prev);
setIsExpanded((prev) => !prev);
}, []);
return (
<div className={`quick-entry-box ${isExpanded ? "quick-entry-box--expanded" : "quick-entry-box--collapsed"}`} data-testid="quick-entry-box">
<div className={`quick-entry-box ${isDisclosureExpanded ? "quick-entry-box--expanded" : "quick-entry-box--collapsed"}`} data-testid="quick-entry-box">
<div className="quick-entry-main-row">
<textarea
ref={textareaRef}
@@ -455,12 +477,12 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
type="button"
className="btn btn-sm quick-entry-toggle"
onClick={toggleExpanded}
aria-expanded={isExpanded}
aria-expanded={isDisclosureExpanded}
aria-controls="quick-entry-controls"
data-testid="quick-entry-toggle"
title={isExpanded ? "Collapse" : "Expand"}
title={isDisclosureExpanded ? "Collapse" : "Expand"}
>
{isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
{isDisclosureExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</button>
</div>
{showExpandedControls && (

View File

@@ -138,7 +138,11 @@ vi.mock("../ModelSelectionModal", () => ({
},
}));
function renderQuickEntryBox(props = {}) {
function renderQuickEntryBox(props = {}, { startCollapsed = false } = {}) {
// Set disclosure state if needed
if (startCollapsed) {
localStorage.setItem("kb-quick-entry-expanded", "false");
}
const defaultProps = {
onCreate: vi.fn().mockResolvedValue(undefined),
addToast: vi.fn(),
@@ -158,15 +162,18 @@ function expandQuickEntry() {
describe("QuickEntryBox", () => {
beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true });
localStorage.clear();
});
afterEach(() => {
vi.runOnlyPendingTimers();
vi.useRealTimers();
localStorage.clear();
});
it("renders textarea with placeholder", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
expect(textarea).toBeTruthy();
expect(textarea.tagName.toLowerCase()).toBe("textarea");
@@ -174,7 +181,8 @@ describe("QuickEntryBox", () => {
});
it("does NOT expand on focus", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.focus(textarea);
@@ -184,7 +192,8 @@ describe("QuickEntryBox", () => {
});
it("toggle button expands the view", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
// Initially not expanded
@@ -198,7 +207,8 @@ describe("QuickEntryBox", () => {
});
it("toggle button collapses the view when expanded", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
// Expand first
@@ -213,7 +223,8 @@ describe("QuickEntryBox", () => {
});
it("does NOT collapse on blur when empty", async () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
// Expand manually
@@ -232,7 +243,8 @@ describe("QuickEntryBox", () => {
});
it("does NOT collapse on blur when has content", async () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
// Expand manually and add content
@@ -251,7 +263,8 @@ describe("QuickEntryBox", () => {
});
it("creates task on Enter key with TaskCreateInput", async () => {
const { props } = renderQuickEntryBox();
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "New task description" } });
@@ -268,7 +281,8 @@ describe("QuickEntryBox", () => {
});
it("allows Shift+Enter to insert newline when expanded", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -282,7 +296,8 @@ describe("QuickEntryBox", () => {
});
it("submits on Enter even when expanded (without Shift)", async () => {
const { props } = renderQuickEntryBox();
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.focus(textarea);
@@ -301,7 +316,8 @@ describe("QuickEntryBox", () => {
});
it("prevents default on Enter key (without Shift)", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Task" } });
@@ -312,7 +328,8 @@ describe("QuickEntryBox", () => {
});
it("shows loading state during creation", async () => {
const { props } = renderQuickEntryBox();
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
// Slow down the promise to see loading state
props.onCreate.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100)));
@@ -330,7 +347,8 @@ describe("QuickEntryBox", () => {
});
it("clears input after successful creation", async () => {
const { props } = renderQuickEntryBox();
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Task to create" } });
@@ -344,7 +362,8 @@ describe("QuickEntryBox", () => {
});
it("shows error toast on failure and keeps input content", async () => {
const { props } = renderQuickEntryBox();
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
props.onCreate.mockRejectedValue(new Error("Network error"));
const textarea = screen.getByTestId("quick-entry-input");
@@ -360,7 +379,8 @@ describe("QuickEntryBox", () => {
});
it("clears non-empty input on Escape key", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Some text" } });
@@ -371,7 +391,8 @@ describe("QuickEntryBox", () => {
});
it("collapses and blurs on Escape key", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -383,7 +404,8 @@ describe("QuickEntryBox", () => {
});
it("does not clear empty input on Escape key", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.keyDown(textarea, { key: "Escape" });
@@ -391,7 +413,8 @@ describe("QuickEntryBox", () => {
});
it("does not submit on Enter if input is empty", async () => {
const { props } = renderQuickEntryBox();
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.keyDown(textarea, { key: "Enter" });
@@ -403,7 +426,8 @@ describe("QuickEntryBox", () => {
});
it("does not submit on Enter if input is only whitespace", async () => {
const { props } = renderQuickEntryBox();
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: " " } });
@@ -415,7 +439,8 @@ describe("QuickEntryBox", () => {
});
it("updates textarea value on change", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Updated text" } });
@@ -423,7 +448,8 @@ describe("QuickEntryBox", () => {
});
it("trims whitespace when creating task", async () => {
const { props } = renderQuickEntryBox();
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: " Task with spaces " } });
@@ -439,7 +465,8 @@ describe("QuickEntryBox", () => {
});
it("maintains focus after successful creation", async () => {
const { props } = renderQuickEntryBox();
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Task to create" } });
@@ -455,7 +482,7 @@ describe("QuickEntryBox", () => {
describe("Rich creation features", () => {
it("shows dependency button when expanded", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Initially, no controls are visible
expect(screen.queryByTestId("quick-entry-deps-button")).toBeNull();
@@ -470,7 +497,7 @@ describe("QuickEntryBox", () => {
});
it("shows model selector button when expanded", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Initially, no controls are visible
expect(screen.queryByTestId("quick-entry-models-button")).toBeNull();
@@ -485,7 +512,7 @@ describe("QuickEntryBox", () => {
});
it("shows Plan and Subtask buttons when expanded", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Initially, no controls are visible
expect(screen.queryByTestId("plan-button")).toBeNull();
@@ -502,7 +529,7 @@ describe("QuickEntryBox", () => {
});
it("opens dependency dropdown when clicking deps button", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -515,7 +542,8 @@ describe("QuickEntryBox", () => {
});
it("opens model modal when clicking models button", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -532,7 +560,8 @@ describe("QuickEntryBox", () => {
});
it("modal receives correct props (models, loading state, etc.)", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -551,7 +580,8 @@ describe("QuickEntryBox", () => {
});
it("selects dependencies and includes them in submit payload", async () => {
const { props } = renderQuickEntryBox();
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -579,7 +609,6 @@ describe("QuickEntryBox", () => {
it("calls onPlanningMode and clears input when Plan clicked", async () => {
const onPlanningMode = vi.fn();
const { props } = renderQuickEntryBox({ onPlanningMode });
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Plan this task" } });
@@ -596,10 +625,8 @@ describe("QuickEntryBox", () => {
it("calls onSubtaskBreakdown and clears input when Subtask clicked", async () => {
const onSubtaskBreakdown = vi.fn();
const { props } = renderQuickEntryBox({ onSubtaskBreakdown });
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.focus(textarea);
fireEvent.change(textarea, { target: { value: "Break this down" } });
fireEvent.click(screen.getByTestId("subtask-button"));
@@ -612,7 +639,8 @@ describe("QuickEntryBox", () => {
});
it("disables Plan and Subtask buttons when description is empty", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -638,7 +666,8 @@ describe("QuickEntryBox", () => {
});
it("Plan button prevents textarea blur on mousedown", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -656,7 +685,8 @@ describe("QuickEntryBox", () => {
});
it("Subtask button prevents textarea blur on mousedown", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -694,7 +724,8 @@ describe("QuickEntryBox", () => {
});
it("includes selected models in submit payload", async () => {
const { props } = renderQuickEntryBox();
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -725,7 +756,8 @@ describe("QuickEntryBox", () => {
});
it("closes modal on Escape when open", async () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -746,7 +778,8 @@ describe("QuickEntryBox", () => {
});
it("clears all state on second Escape after dropdowns are closed", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -763,8 +796,9 @@ describe("QuickEntryBox", () => {
expect(textarea.classList.contains("quick-entry-input--expanded")).toBe(false);
});
it("resets all state after successful creation", async () => {
const { props } = renderQuickEntryBox();
it("resets all state after successful creation (preserves disclosure preference)", async () => {
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -776,11 +810,12 @@ describe("QuickEntryBox", () => {
expect(props.onCreate).toHaveBeenCalled();
});
// After creation, controls should be collapsed
// After creation, input should be cleared
expect((textarea as HTMLTextAreaElement).value).toBe("");
expect(screen.queryByTestId("quick-entry-deps-button")).toBeNull();
expect(screen.queryByTestId("plan-button")).toBeNull();
expect(screen.queryByTestId("subtask-button")).toBeNull();
// Disclosure preference persists - controls remain visible since we expanded earlier
expect(screen.getByTestId("quick-entry-deps-button")).toBeTruthy();
expect(screen.getByTestId("plan-button")).toBeTruthy();
expect(screen.getByTestId("subtask-button")).toBeTruthy();
});
});
@@ -794,11 +829,84 @@ describe("QuickEntryBox", () => {
localStorage.clear();
});
it("restores disclosure state from localStorage on mount", () => {
// Pre-populate localStorage with expanded state
localStorage.setItem("kb-quick-entry-expanded", "true");
renderQuickEntryBox();
const toggleButton = screen.getByTestId("quick-entry-toggle");
// Should restore the saved disclosure state (expanded)
expect(toggleButton.getAttribute("aria-expanded")).toBe("true");
// Controls should be visible
expect(screen.getByTestId("quick-entry-deps-button")).toBeTruthy();
});
it("defaults to expanded when localStorage is empty", () => {
renderQuickEntryBox();
const toggleButton = screen.getByTestId("quick-entry-toggle");
// Should default to expanded (true) for backward compatibility
expect(toggleButton.getAttribute("aria-expanded")).toBe("true");
// Controls should be visible
expect(screen.getByTestId("quick-entry-deps-button")).toBeTruthy();
});
it("updates localStorage when toggling disclosure", async () => {
renderQuickEntryBox({}, { startCollapsed: true });
const toggleButton = screen.getByTestId("quick-entry-toggle");
// Wait for initial state to be persisted (useEffect runs after mount)
await waitFor(() => {
expect(localStorage.getItem("kb-quick-entry-expanded")).toBe("false");
});
// Initially collapsed
expect(toggleButton.getAttribute("aria-expanded")).toBe("false");
// Click to expand
fireEvent.click(toggleButton);
// Should be expanded
expect(toggleButton.getAttribute("aria-expanded")).toBe("true");
// localStorage should be updated
await waitFor(() => {
expect(localStorage.getItem("kb-quick-entry-expanded")).toBe("true");
});
// Click to collapse
fireEvent.click(toggleButton);
// Should be collapsed
expect(toggleButton.getAttribute("aria-expanded")).toBe("false");
// localStorage should be updated
await waitFor(() => {
expect(localStorage.getItem("kb-quick-entry-expanded")).toBe("false");
});
});
it("aria-expanded attribute updates correctly when toggling", () => {
renderQuickEntryBox({}, { startCollapsed: true });
const toggleButton = screen.getByTestId("quick-entry-toggle");
// Initially collapsed
expect(toggleButton.getAttribute("aria-expanded")).toBe("false");
// Click to expand
fireEvent.click(toggleButton);
expect(toggleButton.getAttribute("aria-expanded")).toBe("true");
// Click to collapse
fireEvent.click(toggleButton);
expect(toggleButton.getAttribute("aria-expanded")).toBe("false");
});
it("restores description from localStorage on mount", () => {
// Pre-populate localStorage
localStorage.setItem("kb-quick-entry-text", "Saved task description");
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
// Should restore the saved description
@@ -806,7 +914,8 @@ describe("QuickEntryBox", () => {
});
it("updates localStorage when typing", async () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Typing this task" } });
@@ -818,7 +927,8 @@ describe("QuickEntryBox", () => {
});
it("clears localStorage after successful task creation", async () => {
const { props } = renderQuickEntryBox();
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
const textarea = screen.getByTestId("quick-entry-input");
// Type something to set localStorage
@@ -839,7 +949,8 @@ describe("QuickEntryBox", () => {
});
it("clears localStorage when Escape clears non-empty input", async () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -858,7 +969,8 @@ describe("QuickEntryBox", () => {
});
it("does not clear localStorage on first Escape when closing dropdowns", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -880,7 +992,8 @@ describe("QuickEntryBox", () => {
describe("AI Refine feature", () => {
it("shows refine button when expanded and text is entered", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
// Initially, refine button is not visible
expect(screen.queryByTestId("refine-button")).toBeNull();
@@ -895,7 +1008,8 @@ describe("QuickEntryBox", () => {
});
it("refine button is hidden when textarea is empty", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -914,7 +1028,8 @@ describe("QuickEntryBox", () => {
});
it("opens refine menu on button click", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -929,7 +1044,8 @@ describe("QuickEntryBox", () => {
});
it("closes refine menu on Escape key", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -951,7 +1067,8 @@ describe("QuickEntryBox", () => {
const { refineText } = await import("../../api");
vi.mocked(refineText).mockResolvedValueOnce("Refined description");
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -974,7 +1091,8 @@ describe("QuickEntryBox", () => {
const { refineText } = await import("../../api");
vi.mocked(refineText).mockResolvedValueOnce("Refined description");
const { props } = renderQuickEntryBox();
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -1003,7 +1121,8 @@ describe("QuickEntryBox", () => {
const { getRefineErrorMessage } = await import("../../api");
const { props } = renderQuickEntryBox();
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -1024,7 +1143,8 @@ describe("QuickEntryBox", () => {
// Slow down the promise to see loading state
vi.mocked(refineText).mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100)));
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -1046,7 +1166,8 @@ describe("QuickEntryBox", () => {
const { refineText } = await import("../../api");
vi.mocked(refineText).mockResolvedValueOnce("Refined description with much more content here");
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -1063,7 +1184,8 @@ describe("QuickEntryBox", () => {
const { refineText } = await import("../../api");
vi.mocked(refineText).mockResolvedValueOnce("Refined text");
const { props } = renderQuickEntryBox();
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -1087,7 +1209,8 @@ describe("QuickEntryBox", () => {
describe("Save button", () => {
it("shows save button when expanded and text is entered", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
// Initially, save button is not visible
expect(screen.queryByTestId("save-button")).toBeNull();
@@ -1102,7 +1225,8 @@ describe("QuickEntryBox", () => {
});
it("save button is disabled when textarea is empty", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -1121,7 +1245,8 @@ describe("QuickEntryBox", () => {
});
it("save button is disabled during submission", async () => {
const { props } = renderQuickEntryBox();
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
// Slow down the promise to see loading state
props.onCreate.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100)));
@@ -1143,7 +1268,8 @@ describe("QuickEntryBox", () => {
});
it("clicking save button persists to localStorage", async () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -1159,7 +1285,8 @@ describe("QuickEntryBox", () => {
});
it("clicking save button creates the task", async () => {
const { props } = renderQuickEntryBox();
const { props } = renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -1180,7 +1307,8 @@ describe("QuickEntryBox", () => {
});
it("save button has correct test id", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -1192,7 +1320,8 @@ describe("QuickEntryBox", () => {
});
it("save button has correct title attribute", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -1203,7 +1332,8 @@ describe("QuickEntryBox", () => {
});
it("save button prevents textarea blur on mousedown", () => {
renderQuickEntryBox();
renderQuickEntryBox({}, { startCollapsed: true });
// Component starts with disclosure expanded by default
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");

View File

@@ -9604,6 +9604,32 @@ html .column.drag-over * {
height: 32px;
width: 32px;
flex-shrink: 0;
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius-sm);
color: var(--text-muted);
cursor: pointer;
transition: all var(--transition-fast);
}
.quick-entry-toggle:hover {
background: var(--card-hover);
border-color: var(--border);
color: var(--text);
}
.quick-entry-toggle:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
}
/* Expanded state - normal padding */
.quick-entry-box--expanded {
padding: 8px 10px;
}
.quick-entry-box--expanded .quick-entry-input {
min-height: 36px;
}
/* Collapsed state - minimal padding */
@@ -9620,6 +9646,31 @@ html .column.drag-over * {
border-bottom-color: var(--triage);
}
/* Responsive adjustments for quick entry */
@media (max-width: 640px) {
.quick-entry-controls {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.quick-entry-controls-left {
width: 100%;
}
.quick-entry-hint {
margin-left: 0;
width: 100%;
text-align: center;
}
.quick-entry-toggle {
height: 28px;
width: 28px;
padding: 4px;
}
}
/* === New Task Modal === */
.new-task-modal .modal-body {
padding: 20px 24px;