dashboard: themedPrompt is a textarea — Enter submits, Shift+Enter newline

The approval deny-reason prompt (and every themedPrompt dialog) used a
single-line input. Make themedPrompt always a resizable <textarea> with
chat-box keys: Enter submits (clicks the confirm button), Shift+Enter inserts
a newline. Short answers stay one keystroke; multi-line reasons (e.g. a deny
note) are now possible. No single-line variant — themedPrompt is only used by
the data-async data-prompt path, so all those dialogs get the textarea. Closes #1840.
This commit is contained in:
iris 2026-06-21 23:38:56 +02:00
commit 08afa49c96
2 changed files with 23 additions and 3 deletions

View file

@ -481,6 +481,14 @@ body.dashboard-shell {
padding: 0.4em 0.6em;
}
.tc-input:focus { outline: 1px solid var(--green); }
/* themedPrompt's field is always a resizable textarea (Enter submits,
Shift+Enter inserts a newline) sensible min-height, wrapped output. */
.tc-textarea {
resize: vertical;
min-height: 4.5em;
line-height: 1.4;
white-space: pre-wrap;
}
/* themedToast — non-blocking transient notifications (errors/info/ok) */
.tc-toasts {

View file

@ -125,15 +125,27 @@ export function themedConfirm(opts = {}) {
}
// themedPrompt({ title, message, label, placeholder, value, confirmLabel, cancelLabel })
// → Promise<string | null>. Themed replacement for window.prompt(): an input
// dialog that resolves to the entered string on confirm, or null on cancel.
// → Promise<string | null>. Themed replacement for window.prompt(): a
// resizable <textarea> dialog that resolves to the entered string on confirm,
// or null on cancel. Chat-box key behaviour: **Enter submits**, **Shift+Enter
// inserts a newline** — so short answers are one keystroke while multi-line
// reasons (e.g. an approval deny note) are still possible. Escape cancels
// (via openDialog).
export function themedPrompt(opts = {}) {
const {
title = '', message = '', label = '', placeholder = '', value = '',
confirmLabel = 'ok', cancelLabel = 'cancel',
} = opts;
const input = el('input', { type: 'text', class: 'tc-input', placeholder });
const input = el('textarea', { class: 'tc-input tc-textarea', rows: '3', placeholder });
if (value) input.value = value;
// Enter submits (clicks the confirm button mounted by openDialog);
// Shift+Enter falls through to the textarea's default newline insert.
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
input.closest('.tc-box')?.querySelector('.tc-confirm')?.click();
}
});
const content = el('div', { class: 'tc-promptfield' },
label ? el('label', { class: 'tc-promptlabel' }, label) : null,
input);