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
This commit is contained in:
@@ -1,109 +1,29 @@
|
|||||||
import { createSignal, Show } from "solid-js"
|
import { Show } from "solid-js"
|
||||||
import { Maximize2, Minimize2 } from "lucide-solid"
|
import { Maximize2, Minimize2 } from "lucide-solid"
|
||||||
import { isElectronHost } from "../lib/runtime-env"
|
|
||||||
|
|
||||||
interface ExpandButtonProps {
|
interface ExpandButtonProps {
|
||||||
expandState: () => "normal" | "fifty" | "eighty" | "expanded"
|
expandState: () => "normal" | "expanded"
|
||||||
onToggleExpand: (nextState: "normal" | "fifty" | "eighty" | "expanded") => void
|
onToggleExpand: (nextState: "normal" | "expanded") => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExpandButton(props: ExpandButtonProps) {
|
export default function ExpandButton(props: ExpandButtonProps) {
|
||||||
const [clickTime, setClickTime] = createSignal<number>(0)
|
|
||||||
const [clickTimer, setClickTimer] = createSignal<number | null>(null)
|
|
||||||
const DOUBLE_CLICK_THRESHOLD = 300
|
|
||||||
|
|
||||||
// Check if we're in Electron (desktop app with 3-state support)
|
|
||||||
const isDesktopApp = isElectronHost()
|
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
const current = props.expandState()
|
const current = props.expandState()
|
||||||
|
props.onToggleExpand(current === "normal" ? "expanded" : "normal")
|
||||||
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"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`prompt-expand-button ${isDesktopApp ? "desktop-mode" : "web-mode"}`}
|
class="prompt-expand-button"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
disabled={false}
|
|
||||||
aria-label="Toggle chat input height"
|
aria-label="Toggle chat input height"
|
||||||
data-tooltip={getTooltip()}
|
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={!isExpanded()}
|
when={props.expandState() === "normal"}
|
||||||
fallback={<Minimize2 class="h-5 w-5" aria-hidden="true" />}
|
fallback={<Minimize2 class="h-4 w-4" aria-hidden="true" />}
|
||||||
>
|
>
|
||||||
<Maximize2 class="h-5 w-5" aria-hidden="true" />
|
<Maximize2 class="h-4 w-4" aria-hidden="true" />
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { createSignal, Show, onMount, For, onCleanup, createEffect, on, untrack,
|
|||||||
import { ArrowBigUp, ArrowBigDown } from "lucide-solid"
|
import { ArrowBigUp, ArrowBigDown } from "lucide-solid"
|
||||||
import UnifiedPicker from "./unified-picker"
|
import UnifiedPicker from "./unified-picker"
|
||||||
import ExpandButton from "./expand-button"
|
import ExpandButton from "./expand-button"
|
||||||
import { isElectronHost } from "../lib/runtime-env"
|
|
||||||
import { addToHistory, getHistory } from "../stores/message-history"
|
import { addToHistory, getHistory } from "../stores/message-history"
|
||||||
import { getAttachments, addAttachment, clearAttachments, removeAttachment } from "../stores/attachments"
|
import { getAttachments, addAttachment, clearAttachments, removeAttachment } from "../stores/attachments"
|
||||||
import { resolvePastedPlaceholders } from "../lib/prompt-placeholders"
|
import { resolvePastedPlaceholders } from "../lib/prompt-placeholders"
|
||||||
@@ -48,13 +47,12 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
const [pasteCount, setPasteCount] = createSignal(0)
|
const [pasteCount, setPasteCount] = createSignal(0)
|
||||||
const [imageCount, setImageCount] = createSignal(0)
|
const [imageCount, setImageCount] = createSignal(0)
|
||||||
const [mode, setMode] = createSignal<"normal" | "shell">("normal")
|
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
|
const SELECTION_INSERT_MAX_LENGTH = 2000
|
||||||
let textareaRef: HTMLTextAreaElement | undefined
|
let textareaRef: HTMLTextAreaElement | undefined
|
||||||
let containerRef: HTMLDivElement | undefined
|
let containerRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
// Check if we're in Electron (desktop app with 3-state support)
|
// Fixed line height for expanded state (15 lines as suggested by dev)
|
||||||
const isDesktopApp = isElectronHost()
|
|
||||||
|
|
||||||
// Fixed line height for web/mobile expanded state (15 lines as suggested)
|
// Fixed line height for web/mobile expanded state (15 lines as suggested)
|
||||||
const EXPANDED_LINES = 15
|
const EXPANDED_LINES = 15
|
||||||
@@ -85,21 +83,11 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
const state = expandState()
|
const state = expandState()
|
||||||
if (state === "normal") return "auto"
|
if (state === "normal") return "auto"
|
||||||
|
|
||||||
|
// Use fixed height, but cap at available space
|
||||||
|
// This prevents overflow in landscape or small screens
|
||||||
const availableHeight = calculateExpandedHeight()
|
const availableHeight = calculateExpandedHeight()
|
||||||
|
const maxHeight = Math.min(FIXED_EXPANDED_HEIGHT, availableHeight * 0.6)
|
||||||
if (isDesktopApp) {
|
return `${Math.max(maxHeight, 150)}px` // Minimum 150px to be useful
|
||||||
// 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
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Responsive placeholder text - shorter on mobile to avoid overlap with expand button
|
// 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()
|
void props.onAbortSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleExpandToggle(nextState: "normal" | "fifty" | "eighty" | "expanded") {
|
function handleExpandToggle(nextState: "normal" | "expanded") {
|
||||||
setExpandState(nextState)
|
setExpandState(nextState)
|
||||||
// Keep focus on textarea
|
// Keep focus on textarea
|
||||||
textareaRef?.focus()
|
textareaRef?.focus()
|
||||||
@@ -1276,8 +1264,12 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<Show when={hasHistory()}>
|
<div class="prompt-nav-buttons">
|
||||||
<div class="prompt-history-top">
|
<ExpandButton
|
||||||
|
expandState={expandState}
|
||||||
|
onToggleExpand={handleExpandToggle}
|
||||||
|
/>
|
||||||
|
<Show when={hasHistory()}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="prompt-history-button"
|
class="prompt-history-button"
|
||||||
@@ -1287,8 +1279,6 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
>
|
>
|
||||||
<ArrowBigUp class="h-5 w-5" aria-hidden="true" />
|
<ArrowBigUp class="h-5 w-5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
<div class="prompt-history-bottom">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="prompt-history-button"
|
class="prompt-history-button"
|
||||||
@@ -1298,13 +1288,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
>
|
>
|
||||||
<ArrowBigDown class="h-5 w-5" aria-hidden="true" />
|
<ArrowBigDown class="h-5 w-5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</Show>
|
||||||
</Show>
|
|
||||||
<div class="prompt-expand-top">
|
|
||||||
<ExpandButton
|
|
||||||
expandState={expandState}
|
|
||||||
onToggleExpand={handleExpandToggle}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Show when={shouldShowOverlay()}>
|
<Show when={shouldShowOverlay()}>
|
||||||
<div class={`prompt-input-overlay ${mode() === "shell" ? "shell-mode" : ""}`}>
|
<div class={`prompt-input-overlay ${mode() === "shell" ? "shell-mode" : ""}`}>
|
||||||
|
|||||||
@@ -37,18 +37,18 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-input {
|
.prompt-input {
|
||||||
@apply w-full pl-3 pr-10 pt-2.5 border text-sm resize-none outline-none transition-colors;
|
@apply w-full pl-3 pr-10 pt-2.5 border text-sm resize-none outline-none transition-colors;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
background-color: var(--surface-base);
|
background-color: var(--surface-base);
|
||||||
color: inherit;
|
color: inherit;
|
||||||
border-color: var(--border-base);
|
border-color: var(--border-base);
|
||||||
line-height: var(--line-height-normal);
|
line-height: var(--line-height-normal);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.prompt-input-overlay {
|
.prompt-input-overlay {
|
||||||
@@ -70,110 +70,47 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-history-top,
|
/* Navigation buttons container (expand, prev, next) */
|
||||||
.prompt-history-bottom {
|
.prompt-nav-buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0.35rem;
|
top: 0.25rem;
|
||||||
|
right: 0.25rem;
|
||||||
|
bottom: 0.25rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: flex-start;
|
||||||
|
gap: 0.125rem;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-history-top {
|
.prompt-expand-button,
|
||||||
top: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-history-bottom {
|
|
||||||
bottom: 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-history-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);
|
color: var(--text-muted);
|
||||||
background-color: rgba(15, 23, 42, 0.04);
|
background-color: rgba(15, 23, 42, 0.04);
|
||||||
transition: background-color 0.15s ease, color 0.15s ease;
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prompt-expand-button:hover:not(:disabled),
|
||||||
.prompt-history-button:hover:not(:disabled) {
|
.prompt-history-button:hover:not(:disabled) {
|
||||||
background-color: var(--surface-secondary);
|
background-color: var(--surface-secondary);
|
||||||
color: var(--text-primary);
|
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) {
|
.prompt-expand-button:active:not(:disabled) {
|
||||||
background-color: var(--accent-primary);
|
background-color: var(--accent-primary);
|
||||||
color: var(--text-inverted);
|
color: var(--text-inverted);
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-expand-button:disabled {
|
.prompt-expand-button:disabled,
|
||||||
|
.prompt-history-button:disabled {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
cursor: not-allowed;
|
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 {
|
.prompt-overlay-text {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -369,4 +306,4 @@
|
|||||||
gap: 0;
|
gap: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user