diff --git a/src/lib/actions/autogrow.ts b/src/lib/actions/autogrow.ts new file mode 100644 index 0000000..b3db3c8 --- /dev/null +++ b/src/lib/actions/autogrow.ts @@ -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); + }, + }; +}; diff --git a/src/lib/components/ChatSidebar.svelte b/src/lib/components/ChatSidebar.svelte index 67f3d37..4639113 100644 --- a/src/lib/components/ChatSidebar.svelte +++ b/src/lib/components/ChatSidebar.svelte @@ -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 {:else} - +
+ + +
{/if} diff --git a/src/lib/components/MentionAutocomplete.svelte b/src/lib/components/MentionAutocomplete.svelte index c15b5fa..f1eecfd 100644 --- a/src/lib/components/MentionAutocomplete.svelte +++ b/src/lib/components/MentionAutocomplete.svelte @@ -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 @@