diff options
| author | Adam Soltys <[email protected]> | 2026-08-29 07:29:49 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-29 07:29:49 -0700 |
| commit | b61198fc49e55213ee29a545f7bab3691f43d6b5 (patch) | |
| tree | d74ca8c23da05125d2bf1ba156e72924f5868ca1 | |
| parent | afff4e068fa327b3420255b5c21c92e5d4683c4a (diff) | |
| download | coinosv3-b61198fc49e55213ee29a545f7bab3691f43d6b5.tar.gz coinosv3-b61198fc49e55213ee29a545f7bab3691f43d6b5.zip | |
The composer learns the second dimension: a textarea that grows with the text (capped at five lines), Enter still sends, and a newline is one Shift+Enter or Ctrl+J away — bubbles already knew how to show them
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019WboeoetMF3MTmgT7Ew8yi
| -rw-r--r-- | src/features/messages.js | 26 | ||||
| -rw-r--r-- | src/style.css | 5 |
2 files changed, 25 insertions, 6 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index ea368ee..8b0f6c9 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -2007,15 +2007,33 @@ export function messagesFeature(ctx) { }, ui.msgReconnecting ? h('span', { class: 'spinner sm' }) : t('msgReconnect'))); } + // Enter sends; Shift+Enter (the textarea's native edit) and Ctrl+J insert + // a newline. Height follows the text as it's typed — imperatively, since + // typing doesn't re-render — with rows as the re-render fallback. + const growComposer = (el) => { + el.style.height = 'auto'; + el.style.height = Math.min(el.scrollHeight, 120) + 'px'; + }; const composer = (placeholder, onSend, onType, draftKey) => h('div', { class: 'col', style: 'gap:6px' }, signerNotice(), h('div', { class: 'chat-compose' }, - h('input', { - class: 'grow', type: 'text', id: 'msg-draft', placeholder, + h('textarea', { + class: 'grow', id: 'msg-draft', placeholder, rows: String(Math.min(5, draftFor(draftKey).split('\n').length)), value: draftFor(draftKey), maxlength: '2000', - onInput: (e) => { setDraft(draftKey, e.target.value); if (onType && e.target.value) onType(); }, - onKeydown: (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); onSend(); } }, + onInput: (e) => { setDraft(draftKey, e.target.value); growComposer(e.target); if (onType && e.target.value) onType(); }, + onKeydown: (e) => { + if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); onSend(); return; } + if (e.ctrlKey && (e.key === 'j' || e.key === 'J')) { + e.preventDefault(); + const el = e.target; + const { selectionStart: s0, selectionEnd: s1, value } = el; + el.value = value.slice(0, s0) + '\n' + value.slice(s1); + el.selectionStart = el.selectionEnd = s0 + 1; + setDraft(draftKey, el.value); + growComposer(el); + } + }, // Focus summons the keyboard; some browsers resize only after its // animation, so re-stick once now and once when it has settled. onFocus: () => { stickToBottom(); setTimeout(stickToBottom, 350); }, diff --git a/src/style.css b/src/style.css index db9cd9c..412e3b9 100644 --- a/src/style.css +++ b/src/style.css @@ -402,11 +402,12 @@ hr.divider { margin: 16px 0; } display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--line); align-items: center; flex: 0 0 auto; } -.chat-compose input { +.chat-compose input, .chat-compose textarea { flex: 1; min-width: 0; font: inherit; color: var(--ink); background: var(--paper); border: 1px solid var(--line-strong); border-radius: 11px; padding: 10px 13px; } -.chat-compose input:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; } +.chat-compose textarea { resize: none; max-height: 120px; overflow-y: auto; line-height: 1.4; } +.chat-compose input:focus-visible, .chat-compose textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; } /* Messages v2 — home lists, DM threads, invite panel. */ .chat-back { font-size: 22px; line-height: 1; padding: 2px 10px 4px; flex-shrink: 0; } |
