feat: add resizable chat input with top drag handle

- Added custom resize handle above textarea that expands upward
- Fixed send button alignment to stay at bottom of textarea
- Replaced native resize with custom drag-to-resize logic
- Height constraints: min 48px, max 200px
This commit is contained in:
2026-01-23 14:24:53 -08:00
committed by Naomi Carrigan
parent 06810537a9
commit 9abf4b1bdf
+123 -31
View File
@@ -39,6 +39,37 @@
let historyIndex = $state(-1); let historyIndex = $state(-1);
let tempInput = $state(""); let tempInput = $state("");
// Textarea resize state
let textareaHeight = $state(48);
const MIN_HEIGHT = 48;
const MAX_HEIGHT = 200;
let isResizing = $state(false);
let startY = 0;
let startHeight = 0;
function handleResizeStart(event: MouseEvent) {
isResizing = true;
startY = event.clientY;
startHeight = textareaHeight;
document.addEventListener("mousemove", handleResizeMove);
document.addEventListener("mouseup", handleResizeEnd);
event.preventDefault();
}
function handleResizeMove(event: MouseEvent) {
if (!isResizing) return;
// Dragging up (negative deltaY) should increase height
const deltaY = startY - event.clientY;
const newHeight = Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, startHeight + deltaY));
textareaHeight = newHeight;
}
function handleResizeEnd() {
isResizing = false;
document.removeEventListener("mousemove", handleResizeMove);
document.removeEventListener("mouseup", handleResizeEnd);
}
// Load history from localStorage on init // Load history from localStorage on init
function loadHistory(): string[] { function loadHistory(): string[] {
try { try {
@@ -314,13 +345,19 @@ User: ${formattedMessage}`;
<MessageModeSelector /> <MessageModeSelector />
</div> </div>
<div class="input-row flex gap-3 items-end"> <div class="input-row">
<div class="flex-1 relative"> <div class="textarea-wrapper">
<SlashCommandMenu <SlashCommandMenu
commands={matchingCommands} commands={matchingCommands}
selectedIndex={selectedCommandIndex} selectedIndex={selectedCommandIndex}
onSelect={selectCommand} onSelect={selectCommand}
/> />
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="resize-handle"
onmousedown={handleResizeStart}
title="Drag to resize"
></div>
<textarea <textarea
bind:value={inputValue} bind:value={inputValue}
onkeydown={handleKeyDown} onkeydown={handleKeyDown}
@@ -330,41 +367,39 @@ User: ${formattedMessage}`;
: "Connect to Claude first..."} : "Connect to Claude first..."}
disabled={isSubmitting} disabled={isSubmitting}
rows={1} rows={1}
style="height: {textareaHeight}px"
class="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-color)] class="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-color)]
rounded-lg text-[var(--text-primary)] placeholder-gray-500 resize-none rounded-lg text-[var(--text-primary)] placeholder-gray-500 resize-none
focus:outline-none focus:border-[var(--accent-primary)] focus:ring-1 focus:ring-[var(--accent-primary)] focus:outline-none focus:border-[var(--accent-primary)] focus:ring-1 focus:ring-[var(--accent-primary)]
disabled:opacity-50 disabled:cursor-not-allowed disabled:opacity-50 disabled:cursor-not-allowed"
transition-all duration-200"
></textarea> ></textarea>
</div> </div>
{#if isProcessing} <div class="button-wrapper">
<button {#if isProcessing}
type="button" <button
onclick={handleInterrupt} type="button"
class="px-6 py-3 bg-red-600 hover:bg-red-700 onclick={handleInterrupt}
text-white font-medium rounded-lg class="send-button bg-red-600 hover:bg-red-700"
transition-all duration-200 transform hover:scale-105 active:scale-95" title="Interrupt the current response (Ctrl+C)"
title="Interrupt the current response (Ctrl+C)" >
> <span class="font-bold"></span> Stop
<span class="font-bold"></span> Stop </button>
</button> {:else}
{:else} <button
<button type="submit"
type="submit" disabled={!isConnected || isSubmitting || !inputValue.trim()}
disabled={!isConnected || isSubmitting || !inputValue.trim()} class="send-button bg-[var(--accent-primary)] hover:bg-[var(--accent-secondary)]
class="px-6 py-3 bg-[var(--accent-primary)] hover:bg-[var(--accent-secondary)] disabled:opacity-50 disabled:cursor-not-allowed"
text-white font-medium rounded-lg >
disabled:opacity-50 disabled:cursor-not-allowed {#if isSubmitting}
transition-all duration-200 transform hover:scale-105 active:scale-95" <span class="inline-block animate-spin"></span>
> {:else}
{#if isSubmitting} Send
<span class="inline-block animate-spin"></span> {/if}
{:else} </button>
Send {/if}
{/if} </div>
</button>
{/if}
</div> </div>
</form> </form>
@@ -386,4 +421,61 @@ User: ${formattedMessage}`;
gap: 12px; gap: 12px;
align-items: flex-end; align-items: flex-end;
} }
.textarea-wrapper {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
}
.resize-handle {
height: 6px;
cursor: ns-resize;
background: transparent;
border-radius: 3px;
margin-bottom: 2px;
display: flex;
justify-content: center;
align-items: center;
}
.resize-handle::before {
content: "";
width: 40px;
height: 3px;
background: var(--border-color);
border-radius: 2px;
opacity: 0.5;
transition: opacity 0.2s;
}
.resize-handle:hover::before {
opacity: 1;
background: var(--accent-primary);
}
.button-wrapper {
display: flex;
align-items: flex-end;
height: 100%;
}
.send-button {
padding: 0 24px;
height: 48px;
color: white;
font-weight: 500;
border-radius: 8px;
transition: all 0.2s;
white-space: nowrap;
}
.send-button:hover:not(:disabled) {
transform: scale(1.05);
}
.send-button:active:not(:disabled) {
transform: scale(0.95);
}
</style> </style>