Improve chat's input UX

This commit is contained in:
dtonon 2026-06-15 13:54:48 +02:00
parent 30f52af4b1
commit fb74eda8a1
3 changed files with 94 additions and 11 deletions

View file

@ -0,0 +1,48 @@
import type { Action } from "svelte/action";
type AutogrowParams = {
// When false the action is inert, leaving the textarea at its native size.
enabled?: boolean;
// Max height in lines before the textarea starts scrolling.
maxRows?: number;
// A value to watch; resize is re-run whenever it changes (e.g. after a reset).
value?: unknown;
};
// Grow a textarea with its content up to maxRows lines, then scroll.
export const autogrow: Action<
HTMLTextAreaElement,
AutogrowParams | undefined
> = (node, params) => {
let enabled = params?.enabled ?? true;
let maxRows = params?.maxRows ?? 10;
function resize() {
if (!enabled) return;
const cs = getComputedStyle(node);
const lh = parseFloat(cs.lineHeight) || 20;
const vPad = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom);
const vBorder =
parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth);
const max = lh * maxRows + vPad + vBorder;
node.style.height = "auto";
const next = Math.min(node.scrollHeight, max);
node.style.height = `${next}px`;
node.style.overflowY = node.scrollHeight > max ? "auto" : "hidden";
}
node.addEventListener("input", resize);
resize();
return {
update(next) {
enabled = next?.enabled ?? true;
maxRows = next?.maxRows ?? 10;
// Re-run on watched-value changes, including programmatic resets.
resize();
},
destroy() {
node.removeEventListener("input", resize);
},
};
};

View file

@ -207,11 +207,13 @@
sendError = e instanceof Error ? e.message : "Failed to send";
} finally {
sending = false;
// Re-enable runs first, then focus lands on the now-interactive textarea.
tick().then(() => inputEl?.focus({ caretAtEnd: true }));
}
}
function onKeydown(e: KeyboardEvent) {
if (e.key === "Enter" && !e.shiftKey) {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
submit();
} else if (e.key === "Escape" && replyTarget) {
@ -462,16 +464,43 @@
Join to chat
</button>
{:else}
<MentionAutocomplete
bind:this={inputEl}
bind:value={inputValue}
onkeydown={onKeydown}
rows={1}
disabled={sending}
placeholder={auth.user ? "Message..." : "Login to send messages"}
{contextPubkeys}
textareaClass="w-full resize-none rounded border border-neutral-200 dark:border-neutral-700 px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-accent disabled:opacity-50"
/>
<div class="relative">
<MentionAutocomplete
bind:this={inputEl}
bind:value={inputValue}
onkeydown={onKeydown}
rows={1}
autoGrow
maxRows={10}
disabled={sending}
placeholder={auth.user ? "Message..." : "Login to send messages"}
{contextPubkeys}
textareaClass="block w-full resize-none rounded border border-neutral-200 dark:border-neutral-700 py-2 pl-3 pr-11 text-sm focus:outline-none focus:ring-1 focus:ring-accent disabled:opacity-50"
/>
<button
type="button"
onclick={submit}
disabled={sending || !inputValue.trim()}
title="Send (⌘/Ctrl + Enter)"
aria-label="Send message"
class="bg-accent hover:bg-accent-hover absolute right-1.5 bottom-1.5 flex h-6 w-6 items-center justify-center rounded-full text-white transition-colors disabled:cursor-not-allowed disabled:bg-transparent disabled:opacity-40"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 -translate-x-px translate-y-px"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M22 2 11 13" />
<path d="M22 2 15 22l-4-9-9-4 20-7z" />
</svg>
</button>
</div>
{/if}
</div>
</aside>

View file

@ -8,6 +8,7 @@
searchRemoteProfiles,
type ProfileEntry,
} from "$lib/profiles.svelte";
import { autogrow } from "$lib/actions/autogrow";
type Props = {
value: string;
@ -16,6 +17,8 @@
placeholder?: string;
contextPubkeys?: string[];
textareaClass?: string;
autoGrow?: boolean;
maxRows?: number;
onkeydown?: (e: KeyboardEvent) => void;
onfocus?: () => void;
onblur?: () => void;
@ -28,6 +31,8 @@
placeholder = "",
contextPubkeys = [],
textareaClass = "",
autoGrow = false,
maxRows = 10,
onkeydown,
onfocus,
onblur,
@ -298,6 +303,7 @@
<textarea
bind:this={textareaEl}
bind:value
use:autogrow={{ enabled: autoGrow, maxRows, value }}
{disabled}
{rows}
{placeholder}