feat(KB-657): add expand/collapse toggle to quick add components

- Add toggle button (ChevronDown/ChevronUp) to QuickEntryBox (list view) and InlineCreateCard (board view)

- Replace auto-expand behavior with manual toggle control

- Add CSS styles for collapsed/expanded states and toggle button positioning

- Update tests to verify toggle behavior

- Provide cleaner default UI while keeping advanced options discoverable
This commit is contained in:
gsxdsm
2026-04-01 07:13:14 -07:00
parent 005eb6fca6
commit a8dbd95d8c
7 changed files with 439 additions and 369 deletions

View File

@@ -3,7 +3,7 @@ import type { ToastType } from "../hooks/useToast";
import type { Task, TaskCreateInput } from "@fusion/core";
import type { ModelInfo, RefinementType } from "../api";
import { fetchModels, refineText, getRefineErrorMessage } from "../api";
import { Link, Brain, Lightbulb, ListTree, Sparkles, Save, ChevronUp, ChevronDown } from "lucide-react";
import { Link, Brain, Lightbulb, ListTree, Sparkles, Save, ChevronDown, ChevronUp } from "lucide-react";
import { CustomModelDropdown } from "./CustomModelDropdown";
import { ModelSelectionModal } from "./ModelSelectionModal";
@@ -70,7 +70,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
return true;
});
const textareaRef = useRef<HTMLTextAreaElement>(null);
const blurTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const justResetRef = useRef(false);
// Rich creation state (mirrors InlineCreateCard)
@@ -151,19 +150,10 @@ 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 () => {
if (blurTimeoutRef.current) {
clearTimeout(blurTimeoutRef.current);
}
// No blur timeout to clean up
};
}, []);
@@ -227,8 +217,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
setIsModelModalOpen(false);
setIsRefineMenuOpen(false);
setIsRefining(false);
setIsExpanded(false); // Collapse textarea height on reset
// Note: isDisclosureExpanded is NOT reset - user preference persists
setIsExpanded(false); // Collapse on reset
justResetRef.current = true;
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
@@ -314,9 +303,8 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
localStorage.removeItem(STORAGE_KEY);
}
}
// Collapse textarea and disclosure on escape
// Collapse on escape
setIsExpanded(false);
setIsDisclosureExpanded(false);
textareaRef.current?.blur();
}
},
@@ -327,7 +315,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
showDeps,
isModelModalOpen,
isRefineMenuOpen,
setIsDisclosureExpanded,
],
);
@@ -337,31 +324,16 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
justResetRef.current = false;
return;
}
// Only auto-expand if autoExpand prop is true (defaults to true for backward compatibility)
if (autoExpand) {
setIsExpanded(true);
}
}, [autoExpand]);
// No auto-expand on focus — manual toggle only
}, []);
const handleBlur = useCallback(() => {
// Clear any existing timeout
if (blurTimeoutRef.current) {
clearTimeout(blurTimeoutRef.current);
// No auto-collapse on blur — state persists until manually toggled or task is submitted/cancelled
// Only clear the justResetRef flag if needed
if (justResetRef.current) {
justResetRef.current = false;
}
// Collapse after a short delay to allow click events on dropdowns
// Collapse regardless of content - only check if dropdowns are open
blurTimeoutRef.current = setTimeout(() => {
if (!showDeps && !isModelModalOpen && !isRefineMenuOpen) {
setIsExpanded(false);
// Reset height when collapsing
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
}
}
blurTimeoutRef.current = null;
}, 200);
}, [showDeps, isModelModalOpen, isRefineMenuOpen]);
}, []);
const toggleDep = useCallback((id: string) => {
setDependencies((prev) =>
@@ -466,16 +438,15 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
}
}, [availableModels]);
// Show expanded controls based on disclosure state (user preference), not textarea focus
const showExpandedControls = isDisclosureExpanded;
// Show expanded controls only when manually expanded (isExpanded)
const showExpandedControls = isExpanded;
const toggleExpanded = useCallback(() => {
setIsDisclosureExpanded((prev) => !prev);
setIsExpanded((prev) => !prev);
}, []);
return (
<div className={`quick-entry-box ${isDisclosureExpanded ? "quick-entry-box--expanded" : "quick-entry-box--collapsed"}`} data-testid="quick-entry-box">
<div className={`quick-entry-box ${isExpanded ? "quick-entry-box--expanded" : "quick-entry-box--collapsed"}`} data-testid="quick-entry-box">
<div className="quick-entry-main-row">
<textarea
ref={textareaRef}
@@ -495,16 +466,16 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
type="button"
className="btn btn-sm quick-entry-toggle"
onClick={toggleExpanded}
aria-expanded={isDisclosureExpanded}
aria-expanded={isExpanded}
aria-controls="quick-entry-controls"
data-testid="quick-entry-toggle"
title={isDisclosureExpanded ? "Collapse" : "Expand"}
title={isExpanded ? "Collapse" : "Expand"}
>
{isDisclosureExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
{isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</button>
</div>
{showExpandedControls && (
<div className="quick-entry-controls">
<div id="quick-entry-controls" className="quick-entry-controls">
<div className="quick-entry-controls-left">
<div className="dep-trigger-wrap">
<button