generated from nhcarrigan/template
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:
@@ -39,6 +39,37 @@
|
||||
let historyIndex = $state(-1);
|
||||
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
|
||||
function loadHistory(): string[] {
|
||||
try {
|
||||
@@ -314,13 +345,19 @@ User: ${formattedMessage}`;
|
||||
<MessageModeSelector />
|
||||
</div>
|
||||
|
||||
<div class="input-row flex gap-3 items-end">
|
||||
<div class="flex-1 relative">
|
||||
<div class="input-row">
|
||||
<div class="textarea-wrapper">
|
||||
<SlashCommandMenu
|
||||
commands={matchingCommands}
|
||||
selectedIndex={selectedCommandIndex}
|
||||
onSelect={selectCommand}
|
||||
/>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="resize-handle"
|
||||
onmousedown={handleResizeStart}
|
||||
title="Drag to resize"
|
||||
></div>
|
||||
<textarea
|
||||
bind:value={inputValue}
|
||||
onkeydown={handleKeyDown}
|
||||
@@ -330,41 +367,39 @@ User: ${formattedMessage}`;
|
||||
: "Connect to Claude first..."}
|
||||
disabled={isSubmitting}
|
||||
rows={1}
|
||||
style="height: {textareaHeight}px"
|
||||
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
|
||||
focus:outline-none focus:border-[var(--accent-primary)] focus:ring-1 focus:ring-[var(--accent-primary)]
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
transition-all duration-200"
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
{#if isProcessing}
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleInterrupt}
|
||||
class="px-6 py-3 bg-red-600 hover:bg-red-700
|
||||
text-white font-medium rounded-lg
|
||||
transition-all duration-200 transform hover:scale-105 active:scale-95"
|
||||
title="Interrupt the current response (Ctrl+C)"
|
||||
>
|
||||
<span class="font-bold">■</span> Stop
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isConnected || isSubmitting || !inputValue.trim()}
|
||||
class="px-6 py-3 bg-[var(--accent-primary)] hover:bg-[var(--accent-secondary)]
|
||||
text-white font-medium rounded-lg
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
transition-all duration-200 transform hover:scale-105 active:scale-95"
|
||||
>
|
||||
{#if isSubmitting}
|
||||
<span class="inline-block animate-spin">⏳</span>
|
||||
{:else}
|
||||
Send
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<div class="button-wrapper">
|
||||
{#if isProcessing}
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleInterrupt}
|
||||
class="send-button bg-red-600 hover:bg-red-700"
|
||||
title="Interrupt the current response (Ctrl+C)"
|
||||
>
|
||||
<span class="font-bold">■</span> Stop
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isConnected || isSubmitting || !inputValue.trim()}
|
||||
class="send-button bg-[var(--accent-primary)] hover:bg-[var(--accent-secondary)]
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{#if isSubmitting}
|
||||
<span class="inline-block animate-spin">⏳</span>
|
||||
{:else}
|
||||
Send
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -386,4 +421,61 @@ User: ${formattedMessage}`;
|
||||
gap: 12px;
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user