From 0d8a844af886b7ac94bab83916860cffe40a665b Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Sun, 11 Jan 2026 21:59:28 +0800 Subject: [PATCH] feat: implement expandable chat input with double-click detection and gradient tooltip - Add expand button with Maximize2/Minimize2 icons - Implement 3-state height management (normal/50%/80%) - Smart double-click detection with 300ms delay - Height calculation based on session-view - 200px message space - Custom CSS tooltip with dark gradient background and backdrop blur - Send button anchored at bottom via margin-top: auto - Smooth CSS transitions throughout - Double-click at 80% now reduces to 50% (not normal) - Removed all debug console.log statements --- packages/ui/src/components/expand-button.tsx | 43 +++++++---- packages/ui/src/components/prompt-input.tsx | 73 ++++++++++++------- .../ui/src/styles/messaging/prompt-input.css | 31 +++++++- 3 files changed, 103 insertions(+), 44 deletions(-) diff --git a/packages/ui/src/components/expand-button.tsx b/packages/ui/src/components/expand-button.tsx index 3efe8cfb..3c3d92b6 100644 --- a/packages/ui/src/components/expand-button.tsx +++ b/packages/ui/src/components/expand-button.tsx @@ -8,6 +8,7 @@ interface ExpandButtonProps { export default function ExpandButton(props: ExpandButtonProps) { const [clickTime, setClickTime] = createSignal(0) + const [clickTimer, setClickTimer] = createSignal(null) const DOUBLE_CLICK_THRESHOLD = 300 function handleClick() { @@ -15,30 +16,42 @@ export default function ExpandButton(props: ExpandButtonProps) { const lastClick = clickTime() const isDoubleClick = now - lastClick < DOUBLE_CLICK_THRESHOLD - setClickTime(now) + // Clear any pending single-click timer + const timer = clickTimer() + if (timer !== null) { + clearTimeout(timer) + setClickTimer(null) + } const current = props.expandState() if (isDoubleClick) { - // Double click behavior + // Double click behavior - execute immediately if (current === "normal") { - props.onToggleExpand("fifty") + props.onToggleExpand("eighty") } else if (current === "fifty") { props.onToggleExpand("eighty") } else { - props.onToggleExpand("normal") - } - } else { - // Single click behavior - if (current === "normal") { props.onToggleExpand("fifty") - } else { - props.onToggleExpand("normal") } - } + // Reset click time to prevent triple-click issues + setClickTime(0) + } else { + // Single click behavior - delay to wait for potential double-click + setClickTime(now) - // Reset click timer after threshold - setTimeout(() => setClickTime(0), DOUBLE_CLICK_THRESHOLD) + const newTimer = window.setTimeout(() => { + const currentState = props.expandState() + if (currentState === "normal") { + props.onToggleExpand("fifty") + } else { + props.onToggleExpand("normal") + } + setClickTimer(null) + }, DOUBLE_CLICK_THRESHOLD) + + setClickTimer(newTimer) + } } const getTooltip = () => { @@ -48,7 +61,7 @@ export default function ExpandButton(props: ExpandButtonProps) { } else if (current === "fifty") { return "Double-click to expand to 80% • Click to minimize" } else { - return "Click to minimize • Double-click to expand to 50%" + return "Click to minimize • Double-click to reduce to 50%" } } @@ -59,7 +72,7 @@ export default function ExpandButton(props: ExpandButtonProps) { onClick={handleClick} disabled={false} aria-label="Toggle chat input height" - title={getTooltip()} + data-tooltip={getTooltip()} > { - if (!containerRef) return 0 - const rect = containerRef.getBoundingClientRect() - const root = containerRef.closest(".session-view") - if (!root) return 0 - const rootRect = root.getBoundingClientRect() - return rootRect.height - rect.top - } + const calculateExpandedHeight = () => { + if (!containerRef) { + return 0 + } - const getExpandedHeight = (): string => { - const state = expandState() - if (state === "normal") return "auto" - const containerHeight = calculateContainerHeight() - if (state === "fifty") return `${containerHeight * 0.5}px` - return `${containerHeight * 0.8}px` - } + const root = containerRef.closest(".session-view") + if (!root) { + return 0 + } + const rootRect = root.getBoundingClientRect() + + // Reserve minimum space for message section (200px minimum) + const MIN_MESSAGE_SPACE = 200 + const availableForInput = rootRect.height - MIN_MESSAGE_SPACE + + return availableForInput + } + + const expandedHeight = createMemo(() => { + const state = expandState() + if (state === "normal") return "auto" + + const availableHeight = calculateExpandedHeight() + + if (state === "fifty") { + return `${availableHeight * 0.5}px` + } + return `${availableHeight * 0.8}px` + }) @@ -721,11 +734,11 @@ export default function PromptInput(props: PromptInputProps) { void props.onAbortSession() } - function handleExpandToggle(nextState: "normal" | "fifty" | "eighty") { - setExpandState(nextState) - // Keep focus on textarea - textareaRef?.focus() - } + function handleExpandToggle(nextState: "normal" | "fifty" | "eighty") { + setExpandState(nextState) + // Keep focus on textarea + textareaRef?.focus() + } function handleInput(e: Event) { @@ -1186,7 +1199,13 @@ export default function PromptInput(props: PromptInputProps) { -
+