From 71f58e7c5f4c9cae95b8a6f42c85c11a03153f6f Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 13 Jan 2026 06:45:56 +0800 Subject: [PATCH] refactor: simplify expand chat input to 2-state with optimized button layout - Remove 3-state logic (normal/50%/80%) - now only normal/expanded - Remove double-click detection and tooltips for simplicity - Remove platform-specific behavior (same UX for Electron and web) - Optimize button layout: reduce from 36px to 28px to fit 3 buttons - Position expand button above history buttons in vertical stack - Keep 15-line expanded height (360px, capped to available space) Per upstream dev feedback to keep it simple with one approach --- packages/ui/src/components/expand-button.tsx | 96 ++------------ packages/ui/src/components/prompt-input.tsx | 44 +++---- .../ui/src/styles/messaging/prompt-input.css | 117 ++++-------------- 3 files changed, 49 insertions(+), 208 deletions(-) diff --git a/packages/ui/src/components/expand-button.tsx b/packages/ui/src/components/expand-button.tsx index 2dfbfffc..0b6e4fb0 100644 --- a/packages/ui/src/components/expand-button.tsx +++ b/packages/ui/src/components/expand-button.tsx @@ -1,109 +1,29 @@ -import { createSignal, Show } from "solid-js" +import { Show } from "solid-js" import { Maximize2, Minimize2 } from "lucide-solid" -import { isElectronHost } from "../lib/runtime-env" interface ExpandButtonProps { - expandState: () => "normal" | "fifty" | "eighty" | "expanded" - onToggleExpand: (nextState: "normal" | "fifty" | "eighty" | "expanded") => void + expandState: () => "normal" | "expanded" + onToggleExpand: (nextState: "normal" | "expanded") => void } export default function ExpandButton(props: ExpandButtonProps) { - const [clickTime, setClickTime] = createSignal(0) - const [clickTimer, setClickTimer] = createSignal(null) - const DOUBLE_CLICK_THRESHOLD = 300 - - // Check if we're in Electron (desktop app with 3-state support) - const isDesktopApp = isElectronHost() - function handleClick() { const current = props.expandState() - - if (!isDesktopApp) { - // Web/Mobile: Simple 2-state toggle (instant, no delay) - if (current === "normal") { - props.onToggleExpand("expanded") - } else { - props.onToggleExpand("normal") - } - return - } - - // Electron: 3-state with double-click detection - const now = Date.now() - const lastClick = clickTime() - const isDoubleClick = now - lastClick < DOUBLE_CLICK_THRESHOLD - - // Clear any pending single-click timer - const timer = clickTimer() - if (timer !== null) { - clearTimeout(timer) - setClickTimer(null) - } - - if (isDoubleClick) { - // Double click behavior - execute immediately - if (current === "normal") { - props.onToggleExpand("eighty") - } else if (current === "fifty") { - props.onToggleExpand("eighty") - } else { - props.onToggleExpand("fifty") - } - // Reset click time to prevent triple-click issues - setClickTime(0) - } else { - // Single click behavior - delay to wait for potential double-click - setClickTime(now) - - 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 = () => { - // No tooltip for web/mobile - only Electron gets tooltips - if (!isDesktopApp) { - return undefined - } - - const current = props.expandState() - if (current === "normal") { - return "Click to expand (50%) • Double-click to expand further (80%)" - } else if (current === "fifty") { - return "Double-click to expand to 80% • Click to minimize" - } else { - return "Click to minimize • Double-click to reduce to 50%" - } - } - - const isExpanded = () => { - const state = props.expandState() - return state !== "normal" + props.onToggleExpand(current === "normal" ? "expanded" : "normal") } return ( ) diff --git a/packages/ui/src/components/prompt-input.tsx b/packages/ui/src/components/prompt-input.tsx index 89e17dc0..f7f9b036 100644 --- a/packages/ui/src/components/prompt-input.tsx +++ b/packages/ui/src/components/prompt-input.tsx @@ -2,7 +2,6 @@ import { createSignal, Show, onMount, For, onCleanup, createEffect, on, untrack, import { ArrowBigUp, ArrowBigDown } from "lucide-solid" import UnifiedPicker from "./unified-picker" import ExpandButton from "./expand-button" -import { isElectronHost } from "../lib/runtime-env" import { addToHistory, getHistory } from "../stores/message-history" import { getAttachments, addAttachment, clearAttachments, removeAttachment } from "../stores/attachments" import { resolvePastedPlaceholders } from "../lib/prompt-placeholders" @@ -48,13 +47,12 @@ export default function PromptInput(props: PromptInputProps) { const [pasteCount, setPasteCount] = createSignal(0) const [imageCount, setImageCount] = createSignal(0) const [mode, setMode] = createSignal<"normal" | "shell">("normal") - const [expandState, setExpandState] = createSignal<"normal" | "fifty" | "eighty" | "expanded">("normal") + const [expandState, setExpandState] = createSignal<"normal" | "expanded">("normal") const SELECTION_INSERT_MAX_LENGTH = 2000 let textareaRef: HTMLTextAreaElement | undefined let containerRef: HTMLDivElement | undefined - // Check if we're in Electron (desktop app with 3-state support) - const isDesktopApp = isElectronHost() + // Fixed line height for expanded state (15 lines as suggested by dev) // Fixed line height for web/mobile expanded state (15 lines as suggested) const EXPANDED_LINES = 15 @@ -85,21 +83,11 @@ export default function PromptInput(props: PromptInputProps) { const state = expandState() if (state === "normal") return "auto" + // Use fixed height, but cap at available space + // This prevents overflow in landscape or small screens const availableHeight = calculateExpandedHeight() - - if (isDesktopApp) { - // Electron: Use percentage-based heights (50% / 80%) - if (state === "fifty") { - return `${availableHeight * 0.5}px` - } - // state === "eighty" - return `${availableHeight * 0.8}px` - } else { - // Web/Mobile: Use fixed height, but cap at available space - // This prevents overflow in landscape or small screens - const maxHeight = Math.min(FIXED_EXPANDED_HEIGHT, availableHeight * 0.6) - return `${Math.max(maxHeight, 150)}px` // Minimum 150px to be useful - } + const maxHeight = Math.min(FIXED_EXPANDED_HEIGHT, availableHeight * 0.6) + return `${Math.max(maxHeight, 150)}px` // Minimum 150px to be useful }) // Responsive placeholder text - shorter on mobile to avoid overlap with expand button @@ -782,7 +770,7 @@ export default function PromptInput(props: PromptInputProps) { void props.onAbortSession() } - function handleExpandToggle(nextState: "normal" | "fifty" | "eighty" | "expanded") { + function handleExpandToggle(nextState: "normal" | "expanded") { setExpandState(nextState) // Keep focus on textarea textareaRef?.focus() @@ -1276,8 +1264,12 @@ export default function PromptInput(props: PromptInputProps) { autoCapitalize="off" autocomplete="off" /> - -
+
+ + -
-
-
- -
- +
diff --git a/packages/ui/src/styles/messaging/prompt-input.css b/packages/ui/src/styles/messaging/prompt-input.css index eafe0238..8da5b2c7 100644 --- a/packages/ui/src/styles/messaging/prompt-input.css +++ b/packages/ui/src/styles/messaging/prompt-input.css @@ -37,18 +37,18 @@ height: 100%; } - .prompt-input { - @apply w-full pl-3 pr-10 pt-2.5 border text-sm resize-none outline-none transition-colors; - font-family: inherit; - background-color: var(--surface-base); - color: inherit; - border-color: var(--border-base); - line-height: var(--line-height-normal); - border-radius: 0; - padding-bottom: 0; - height: 100%; - min-height: 100%; - } +.prompt-input { + @apply w-full pl-3 pr-10 pt-2.5 border text-sm resize-none outline-none transition-colors; + font-family: inherit; + background-color: var(--surface-base); + color: inherit; + border-color: var(--border-base); + line-height: var(--line-height-normal); + border-radius: 0; + padding-bottom: 0; + height: 100%; + min-height: 100%; +} .prompt-input-overlay { @@ -70,110 +70,47 @@ color: var(--text-primary); } -.prompt-history-top, -.prompt-history-bottom { +/* Navigation buttons container (expand, prev, next) */ +.prompt-nav-buttons { position: absolute; - right: 0.35rem; + top: 0.25rem; + right: 0.25rem; + bottom: 0.25rem; display: flex; - align-items: center; - justify-content: center; + flex-direction: column; + justify-content: flex-start; + gap: 0.125rem; z-index: 2; } -.prompt-history-top { - top: 0.3rem; -} - -.prompt-history-bottom { - bottom: 0.6rem; -} - +.prompt-expand-button, .prompt-history-button { - @apply w-9 h-9 flex items-center justify-center rounded-md; + @apply w-7 h-7 flex items-center justify-center rounded-md; color: var(--text-muted); background-color: rgba(15, 23, 42, 0.04); transition: background-color 0.15s ease, color 0.15s ease; padding: 0; + flex-shrink: 0; } +.prompt-expand-button:hover:not(:disabled), .prompt-history-button:hover:not(:disabled) { background-color: var(--surface-secondary); color: var(--text-primary); } -.prompt-history-button:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -.prompt-expand-top { - position: absolute; - top: 0.3rem; - right: 3.5rem; - display: flex; - align-items: center; - justify-content: center; - z-index: 2; -} - -.prompt-expand-button { - @apply w-9 h-9 flex items-center justify-center rounded-md; - color: var(--text-muted); - background-color: rgba(15, 23, 42, 0.04); - transition: background-color 0.15s ease, color 0.15s ease; - padding: 0; - position: relative; -} - -.prompt-expand-button:hover:not(:disabled) { - background-color: var(--surface-secondary); - color: var(--text-primary); -} - .prompt-expand-button:active:not(:disabled) { background-color: var(--accent-primary); color: var(--text-inverted); transform: scale(0.95); } -.prompt-expand-button:disabled { +.prompt-expand-button:disabled, +.prompt-history-button:disabled { opacity: 0.4; cursor: not-allowed; } -.prompt-expand-button::after { - content: attr(data-tooltip); - position: absolute; - top: calc(100% + 8px); - right: 0; - background: linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.98) 100%); - color: var(--text-primary); - padding: 0.5rem 0.75rem; - border-radius: 6px; - font-size: 0.75rem; - line-height: 1.4; - white-space: nowrap; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.1); - border: 1px solid var(--border-base); - pointer-events: none; - opacity: 0; - transform: translateY(-4px); - transition: opacity 0.2s ease, transform 0.2s ease; - z-index: 1000; - backdrop-filter: blur(8px); -} - -/* Only show tooltip on hover for Electron (desktop-mode) */ -.prompt-expand-button.desktop-mode:hover::after { - opacity: 1; - transform: translateY(0); -} - -/* Web/Mobile: No tooltip (data-tooltip will be undefined anyway) */ -.prompt-expand-button.web-mode::after { - display: none; -} - .prompt-overlay-text { display: inline-flex; align-items: center; @@ -369,4 +306,4 @@ gap: 0; padding: 0; } -} +} \ No newline at end of file