@arraypress/extension-kit 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Chrome's message lookup for an extension's unit tests: its real `_locales`
3
+ * files, a locale's messages over `en`'s, named placeholders ("$SITE$" →
4
+ * `{site: {content: '$1'}}`) filled from substitutions as
5
+ * `chrome.i18n.getMessage` fills them. Node only. From the copies in Better
6
+ * Prices, Better Downloads, Better Saves and Clean Links (`test/helpers/i18n.js`).
7
+ *
8
+ * @module testing/messages
9
+ */
10
+ import { readFileSync } from 'node:fs';
11
+ import path from 'node:path';
12
+
13
+ /**
14
+ * One locale's messages.json.
15
+ *
16
+ * @typedef {Record<string, {message: string, description?: string, placeholders?: Record<string, {content: string}>}>} Messages
17
+ */
18
+
19
+ /**
20
+ * Read a locale's messages.
21
+ *
22
+ * @param {string} extensionDir - The extension's folder (with `_locales/`).
23
+ * @param {string} locale - "en" or "en_GB".
24
+ * @returns {Messages} Its messages.
25
+ * @example
26
+ * readMessages('extension', 'en_GB').optionsColour.message;
27
+ */
28
+ export function readMessages(extensionDir, locale) {
29
+ return JSON.parse(readFileSync(path.join(extensionDir, '_locales', locale, 'messages.json'), 'utf8'));
30
+ }
31
+
32
+ /**
33
+ * `chrome.i18n.getMessage` over a locale's real messages ('' for a missing key, as Chrome gives).
34
+ *
35
+ * @param {string} extensionDir - The extension's folder.
36
+ * @param {string} [locale] - Default "en"; another locale is read over en.
37
+ * @returns {(key: string, substitutions?: string | string[]) => string} The lookup.
38
+ * @example
39
+ * const t = messageLookup('extension');
40
+ * t('optionsMoreActions', ['example.com']); // 'More actions for example.com'
41
+ */
42
+ export function messageLookup(extensionDir, locale = 'en') {
43
+ const messages = { ...readMessages(extensionDir, 'en'), ...(locale === 'en' ? {} : readMessages(extensionDir, locale)) };
44
+ return (key, substitutions = []) => {
45
+ const entry = messages[key];
46
+ if (!entry) return '';
47
+ const subs = [substitutions].flat();
48
+ return entry.message.replace(/\$([A-Za-z_]+)\$/g, (_, name) => (entry.placeholders?.[name.toLowerCase()]?.content ?? '')
49
+ .replace(/\$(\d)/g, (/** @type {string} */ __, /** @type {string} */ n) => subs[Number(n) - 1] ?? ''));
50
+ };
51
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The settings row “Chrome’s built-in AI”: how the on-device model stands,
3
+ * what that means, and Download when it can be had, with Chrome's progress.
4
+ * The download starts from the click (Chrome only downloads from a user
5
+ * gesture): nothing is awaited in the click before `create()`. The words are
6
+ * the page's messages under {@link MODEL_MESSAGES}' keys.
7
+ *
8
+ * Markup it fills (the page's, so it sits in any card):
9
+ *
10
+ * ```html
11
+ * <div class="row two-line">
12
+ * <span class="label">
13
+ * <span data-i18n="optionsModel">Chrome’s built-in AI</span>
14
+ * <span class="secondary"><span id="model-status"></span> <span id="model-help" hidden></span></span>
15
+ * <progress id="model-progress" max="1" value="0" hidden></progress>
16
+ * </span>
17
+ * <button id="model-download" type="button" data-i18n="optionsModelDownload" hidden>Download</button>
18
+ * </div>
19
+ * ```
20
+ *
21
+ * @module ui/model-row
22
+ */
23
+ import type { Availability, LanguageModelOptions, LanguageModelStatic, ModelStatus } from '../ai/model.js';
24
+ export type ModelRowParts = {
25
+ /**
26
+ * The state's words.
27
+ */
28
+ status: HTMLElement;
29
+ /**
30
+ * What it means, or why a download failed.
31
+ */
32
+ help: HTMLElement;
33
+ /**
34
+ * Download.
35
+ */
36
+ download: HTMLButtonElement;
37
+ /**
38
+ * The download's progress, 0–1.
39
+ */
40
+ progress: HTMLProgressElement;
41
+ };
42
+ export type ModelRowOptions = {
43
+ /**
44
+ * What sessions are made with (a download is for these).
45
+ */
46
+ options: LanguageModelOptions;
47
+ /**
48
+ * The model's state as the code that uses it sees it (the
49
+ * worker's, asked by message, when the worker uses it).
50
+ */
51
+ state: () => Promise<Availability>;
52
+ /**
53
+ * The page's messages.
54
+ */
55
+ t: (key: string, substitutions?: string | string[]) => string;
56
+ /**
57
+ * The Prompt API in this page. Default Chrome's.
58
+ */
59
+ model?: () => LanguageModelStatic | undefined;
60
+ /**
61
+ * Hears each state shown
62
+ * (to turn the switches that need the model on or off).
63
+ */
64
+ onShown?: (status: ModelStatus, availability: Availability) => void;
65
+ };
66
+ export type ModelRow = {
67
+ /**
68
+ * Ask the state again and show it.
69
+ */
70
+ refresh: () => Promise<Availability>;
71
+ /**
72
+ * The state last shown (null before the first).
73
+ */
74
+ current: () => Availability | null;
75
+ };
76
+ /** @import { Availability, LanguageModelOptions, LanguageModelStatic, ModelStatus } from '../ai/model.js' */
77
+ /**
78
+ * @typedef {object} ModelRowParts
79
+ * @property {HTMLElement} status The state's words.
80
+ * @property {HTMLElement} help What it means, or why a download failed.
81
+ * @property {HTMLButtonElement} download Download.
82
+ * @property {HTMLProgressElement} progress The download's progress, 0–1.
83
+ */
84
+ /**
85
+ * @typedef {object} ModelRowOptions
86
+ * @property {LanguageModelOptions} options What sessions are made with (a download is for these).
87
+ * @property {() => Promise<Availability>} state The model's state as the code that uses it sees it (the
88
+ * worker's, asked by message, when the worker uses it).
89
+ * @property {(key: string, substitutions?: string | string[]) => string} t The page's messages.
90
+ * @property {() => LanguageModelStatic | undefined} [model] The Prompt API in this page. Default Chrome's.
91
+ * @property {(status: ModelStatus, availability: Availability) => void} [onShown] Hears each state shown
92
+ * (to turn the switches that need the model on or off).
93
+ */
94
+ /**
95
+ * @typedef {object} ModelRow
96
+ * @property {() => Promise<Availability>} refresh Ask the state again and show it.
97
+ * @property {() => Availability | null} current The state last shown (null before the first).
98
+ */
99
+ /**
100
+ * Wire the row up and show the model's state.
101
+ *
102
+ * @param {ModelRowParts} parts - The row's elements.
103
+ * @param {ModelRowOptions} setup - What to ask, and the words.
104
+ * @returns {ModelRow} The row.
105
+ * @example
106
+ * const row = createModelRow({ status: byId('model-status'), help: byId('model-help'),
107
+ * download: byId('model-download', HTMLButtonElement), progress: byId('model-progress', HTMLProgressElement) },
108
+ * { options: SESSION_OPTIONS, state: () => ask(MESSAGES.modelState), t, onShown: ({ state }) => { useAi.disabled = state === 'off'; } });
109
+ * await row.refresh();
110
+ */
111
+ export declare function createModelRow({ status, help, download, progress }: ModelRowParts, { options, state, t, model, onShown }: ModelRowOptions): ModelRow;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The settings row “Chrome’s built-in AI”: how the on-device model stands,
3
+ * what that means, and Download when it can be had, with Chrome's progress.
4
+ * The download starts from the click (Chrome only downloads from a user
5
+ * gesture): nothing is awaited in the click before `create()`. The words are
6
+ * the page's messages under {@link MODEL_MESSAGES}' keys.
7
+ *
8
+ * Markup it fills (the page's, so it sits in any card):
9
+ *
10
+ * ```html
11
+ * <div class="row two-line">
12
+ * <span class="label">
13
+ * <span data-i18n="optionsModel">Chrome’s built-in AI</span>
14
+ * <span class="secondary"><span id="model-status"></span> <span id="model-help" hidden></span></span>
15
+ * <progress id="model-progress" max="1" value="0" hidden></progress>
16
+ * </span>
17
+ * <button id="model-download" type="button" data-i18n="optionsModelDownload" hidden>Download</button>
18
+ * </div>
19
+ * ```
20
+ *
21
+ * @module ui/model-row
22
+ */
23
+
24
+ import { MODEL_MESSAGES, chromeLanguageModel, describeModel, downloadModel } from '../ai/model.js';
25
+
26
+ /** @import { Availability, LanguageModelOptions, LanguageModelStatic, ModelStatus } from '../ai/model.js' */
27
+
28
+ /**
29
+ * @typedef {object} ModelRowParts
30
+ * @property {HTMLElement} status The state's words.
31
+ * @property {HTMLElement} help What it means, or why a download failed.
32
+ * @property {HTMLButtonElement} download Download.
33
+ * @property {HTMLProgressElement} progress The download's progress, 0–1.
34
+ */
35
+
36
+ /**
37
+ * @typedef {object} ModelRowOptions
38
+ * @property {LanguageModelOptions} options What sessions are made with (a download is for these).
39
+ * @property {() => Promise<Availability>} state The model's state as the code that uses it sees it (the
40
+ * worker's, asked by message, when the worker uses it).
41
+ * @property {(key: string, substitutions?: string | string[]) => string} t The page's messages.
42
+ * @property {() => LanguageModelStatic | undefined} [model] The Prompt API in this page. Default Chrome's.
43
+ * @property {(status: ModelStatus, availability: Availability) => void} [onShown] Hears each state shown
44
+ * (to turn the switches that need the model on or off).
45
+ */
46
+
47
+ /**
48
+ * @typedef {object} ModelRow
49
+ * @property {() => Promise<Availability>} refresh Ask the state again and show it.
50
+ * @property {() => Availability | null} current The state last shown (null before the first).
51
+ */
52
+
53
+ /**
54
+ * Wire the row up and show the model's state.
55
+ *
56
+ * @param {ModelRowParts} parts - The row's elements.
57
+ * @param {ModelRowOptions} setup - What to ask, and the words.
58
+ * @returns {ModelRow} The row.
59
+ * @example
60
+ * const row = createModelRow({ status: byId('model-status'), help: byId('model-help'),
61
+ * download: byId('model-download', HTMLButtonElement), progress: byId('model-progress', HTMLProgressElement) },
62
+ * { options: SESSION_OPTIONS, state: () => ask(MESSAGES.modelState), t, onShown: ({ state }) => { useAi.disabled = state === 'off'; } });
63
+ * await row.refresh();
64
+ */
65
+ export function createModelRow({ status, help, download, progress }, { options, state, t, model = chromeLanguageModel, onShown }) {
66
+ /** @type {Availability | null} */
67
+ let shown = null;
68
+
69
+ /**
70
+ * Show a state, or why the last download failed.
71
+ *
72
+ * @param {Availability} availability - The model's.
73
+ * @param {string} [problem] - What went wrong.
74
+ */
75
+ function show(availability, problem = '') {
76
+ const described = describeModel(availability);
77
+ shown = availability;
78
+ status.dataset.state = described.state;
79
+ status.textContent = t(described.status);
80
+ help.textContent = problem || (described.help ? t(described.help) : '');
81
+ help.hidden = !help.textContent;
82
+ download.hidden = !described.canDownload;
83
+ download.disabled = false;
84
+ progress.hidden = true;
85
+ onShown?.(described, availability);
86
+ }
87
+
88
+ download.addEventListener('click', () => {
89
+ download.disabled = true;
90
+ progress.hidden = false;
91
+ progress.value = 0;
92
+ downloadModel(model(), options, {
93
+ onProgress: (loaded) => {
94
+ progress.value = loaded;
95
+ status.textContent = t(MODEL_MESSAGES.downloading, [String(Math.round(loaded * 100))]);
96
+ },
97
+ }).then(async () => show(await state()), () => show('downloadable', t(MODEL_MESSAGES.downloadFailed)));
98
+ });
99
+
100
+ return {
101
+ async refresh() {
102
+ const availability = await state();
103
+ show(availability);
104
+ return availability;
105
+ },
106
+ current: () => shown,
107
+ };
108
+ }
@@ -20,13 +20,33 @@ export type FieldRequest = {
20
20
  */
21
21
  label: string;
22
22
  /**
23
- * The text to start with. Default ''.
23
+ * The text to start with. Default ''. A promise is text
24
+ * still being worked out (an on-device model's suggestion): the field waits, disabled, with
25
+ * `waiting` under it, and takes the text when it comes ('' if it fails).
24
26
  */
25
- value?: string;
27
+ value?: string | Promise<string>;
28
+ /**
29
+ * What the field says while its `value` is worked out ("Finding words…").
30
+ */
31
+ waiting?: string;
26
32
  /**
27
33
  * Shown while it's empty. Default none.
28
34
  */
29
35
  placeholder?: string;
36
+ /**
37
+ * A `<textarea>` (cr-textarea) rather than one line: Enter starts a new
38
+ * line, and only the action button saves. Default false.
39
+ */
40
+ multiline?: boolean;
41
+ /**
42
+ * A multi-line field's height in lines. Default 6.
43
+ */
44
+ rows?: number;
45
+ /**
46
+ * Help under the field (cr-input's
47
+ * description), or a function of its text, kept up as it's typed ("12 words"). Default none.
48
+ */
49
+ hint?: string | ((text: string) => string);
30
50
  /**
31
51
  * Why the (non-blank) text can't be used,
32
52
  * shown under the field; null when it can. Default: anything but blank.
@@ -71,8 +91,16 @@ export type FieldsRequest = DialogRequest & {
71
91
  * @typedef {object} FieldRequest
72
92
  * @property {string} name Its key in the answer ("folder"), and part of its id.
73
93
  * @property {string} label Its label ("Folder").
74
- * @property {string} [value] The text to start with. Default ''.
94
+ * @property {string | Promise<string>} [value] The text to start with. Default ''. A promise is text
95
+ * still being worked out (an on-device model's suggestion): the field waits, disabled, with
96
+ * `waiting` under it, and takes the text when it comes ('' if it fails).
97
+ * @property {string} [waiting] What the field says while its `value` is worked out ("Finding words…").
75
98
  * @property {string} [placeholder] Shown while it's empty. Default none.
99
+ * @property {boolean} [multiline] A `<textarea>` (cr-textarea) rather than one line: Enter starts a new
100
+ * line, and only the action button saves. Default false.
101
+ * @property {number} [rows] A multi-line field's height in lines. Default 6.
102
+ * @property {string | ((text: string) => string)} [hint] Help under the field (cr-input's
103
+ * description), or a function of its text, kept up as it's typed ("12 words"). Default none.
76
104
  * @property {(text: string) => string | null} [validate] Why the (non-blank) text can't be used,
77
105
  * shown under the field; null when it can. Default: anything but blank.
78
106
  * @property {number} [maxLength] Its `maxlength`. Default none.
@@ -120,13 +148,14 @@ export declare function checkText(text: string, validate?: (text: string) => str
120
148
  *
121
149
  * @param {string} name - The field's name.
122
150
  * @param {boolean} only - Whether it's the dialog's only field.
123
- * @returns {{input: string, error: string}} The ids.
151
+ * @returns {{input: string, error: string, hint: string}} The ids.
124
152
  * @example
125
- * fieldIds('folder', false); // → {input: 'text-dialog-folder', error: 'text-dialog-folder-error'}
153
+ * fieldIds('folder', false); // → {input: 'text-dialog-folder', error: 'text-dialog-folder-error', hint: 'text-dialog-folder-hint'}
126
154
  */
127
155
  export declare function fieldIds(name: string, only: boolean): {
128
156
  input: string;
129
157
  error: string;
158
+ hint: string;
130
159
  };
131
160
  export type Parts = {
132
161
  /**
@@ -162,11 +191,19 @@ export type Field = {
162
191
  /**
163
192
  * Its input.
164
193
  */
165
- input: HTMLInputElement;
194
+ input: HTMLInputElement | HTMLTextAreaElement;
166
195
  /**
167
196
  * The reason under it.
168
197
  */
169
198
  error: HTMLElement;
199
+ /**
200
+ * The help under it.
201
+ */
202
+ hint: HTMLElement;
203
+ /**
204
+ * Whether its value is still being worked out.
205
+ */
206
+ waiting: boolean;
170
207
  };
171
208
  /**
172
209
  * Ask for several pieces of text in the page's dialog, one field each.
@@ -199,4 +236,4 @@ export declare function askFields({ title, fields: requests, action, cancel, ret
199
236
  * validate: (text) => (siteFromInput(text) ? null : 'Enter a site, like example.com'),
200
237
  * });
201
238
  */
202
- export declare function askText({ label, value, placeholder, validate, maxLength, ...dialog }: TextRequest): Promise<string | null>;
239
+ export declare function askText({ label, value, placeholder, validate, maxLength, multiline, rows, hint, waiting, ...dialog }: TextRequest): Promise<string | null>;
@@ -19,8 +19,16 @@ import { focusedElement } from '../dom/focus.js';
19
19
  * @typedef {object} FieldRequest
20
20
  * @property {string} name Its key in the answer ("folder"), and part of its id.
21
21
  * @property {string} label Its label ("Folder").
22
- * @property {string} [value] The text to start with. Default ''.
22
+ * @property {string | Promise<string>} [value] The text to start with. Default ''. A promise is text
23
+ * still being worked out (an on-device model's suggestion): the field waits, disabled, with
24
+ * `waiting` under it, and takes the text when it comes ('' if it fails).
25
+ * @property {string} [waiting] What the field says while its `value` is worked out ("Finding words…").
23
26
  * @property {string} [placeholder] Shown while it's empty. Default none.
27
+ * @property {boolean} [multiline] A `<textarea>` (cr-textarea) rather than one line: Enter starts a new
28
+ * line, and only the action button saves. Default false.
29
+ * @property {number} [rows] A multi-line field's height in lines. Default 6.
30
+ * @property {string | ((text: string) => string)} [hint] Help under the field (cr-input's
31
+ * description), or a function of its text, kept up as it's typed ("12 words"). Default none.
24
32
  * @property {(text: string) => string | null} [validate] Why the (non-blank) text can't be used,
25
33
  * shown under the field; null when it can. Default: anything but blank.
26
34
  * @property {number} [maxLength] Its `maxlength`. Default none.
@@ -74,14 +82,14 @@ export function checkText(text, validate = () => null) {
74
82
  *
75
83
  * @param {string} name - The field's name.
76
84
  * @param {boolean} only - Whether it's the dialog's only field.
77
- * @returns {{input: string, error: string}} The ids.
85
+ * @returns {{input: string, error: string, hint: string}} The ids.
78
86
  * @example
79
- * fieldIds('folder', false); // → {input: 'text-dialog-folder', error: 'text-dialog-folder-error'}
87
+ * fieldIds('folder', false); // → {input: 'text-dialog-folder', error: 'text-dialog-folder-error', hint: 'text-dialog-folder-hint'}
80
88
  */
81
89
  export function fieldIds(name, only) {
82
90
  return only
83
- ? { input: 'text-dialog-input', error: 'text-dialog-error' }
84
- : { input: `text-dialog-${name}`, error: `text-dialog-${name}-error` };
91
+ ? { input: 'text-dialog-input', error: 'text-dialog-error', hint: 'text-dialog-hint' }
92
+ : { input: `text-dialog-${name}`, error: `text-dialog-${name}-error`, hint: `text-dialog-${name}-hint` };
85
93
  }
86
94
 
87
95
  /**
@@ -101,8 +109,10 @@ export function fieldIds(name, only) {
101
109
  *
102
110
  * @typedef {object} Field
103
111
  * @property {FieldRequest} request What was asked.
104
- * @property {HTMLInputElement} input Its input.
112
+ * @property {HTMLInputElement | HTMLTextAreaElement} input Its input.
105
113
  * @property {HTMLElement} error The reason under it.
114
+ * @property {HTMLElement} hint The help under it.
115
+ * @property {boolean} waiting Whether its value is still being worked out.
106
116
  */
107
117
 
108
118
  /** Each page's dialog, made on first use. @type {WeakMap<Document, Parts>} */
@@ -161,14 +171,19 @@ function fieldFor(doc, request, only) {
161
171
  const row = element(doc, 'div', { class: 'field' });
162
172
  const label = element(doc, 'label', { for: ids.input });
163
173
  label.textContent = request.label;
164
- const input = element(doc, 'input', { id: ids.input, type: 'text', autocomplete: 'off', 'aria-describedby': ids.error });
165
- input.value = request.value ?? '';
174
+ const described = `${ids.error} ${ids.hint}`;
175
+ const input = request.multiline
176
+ ? element(doc, 'textarea', { id: ids.input, rows: String(request.rows ?? 6), 'aria-describedby': described })
177
+ : element(doc, 'input', { id: ids.input, type: 'text', autocomplete: 'off', 'aria-describedby': described });
178
+ input.value = typeof request.value === 'string' ? request.value : '';
166
179
  if (request.placeholder) input.placeholder = request.placeholder;
167
180
  if (request.maxLength !== undefined) input.maxLength = request.maxLength;
168
181
  const error = element(doc, 'span', { id: ids.error, class: 'hint error' });
169
182
  error.hidden = true;
170
- row.append(label, input, error);
171
- return { row, field: { request, input, error } };
183
+ const hint = element(doc, 'span', { id: ids.hint, class: 'hint secondary' });
184
+ hint.hidden = true;
185
+ row.append(label, input, error, hint);
186
+ return { row, field: { request, input, error, hint, waiting: false } };
172
187
  }
173
188
 
174
189
  /** The open dialog's settle function, per page. @type {WeakMap<Document, (answer: Record<string, string> | null) => void>} */
@@ -205,12 +220,14 @@ export function askFields({
205
220
  parts.confirm.textContent = action;
206
221
  parts.cancel.textContent = cancel;
207
222
 
208
- const allOk = () => fields.every(({ input, request }) => checkText(input.value, request.validate).ok);
223
+ const allOk = () => fields.every(({ input, request, waiting }) => !waiting && checkText(input.value, request.validate).ok);
209
224
  /** @param {Field} field */
210
- const refresh = ({ input, error, request }) => {
211
- const { reason } = checkText(input.value, request.validate);
225
+ const refresh = ({ input, error, hint, request, waiting }) => {
226
+ const { reason } = waiting ? { reason: '' } : checkText(input.value, request.validate);
212
227
  error.textContent = reason;
213
228
  error.hidden = !reason;
229
+ hint.textContent = waiting ? request.waiting ?? '' : typeof request.hint === 'function' ? request.hint(input.value) : request.hint ?? '';
230
+ hint.hidden = !hint.textContent;
214
231
  };
215
232
  for (const field of fields) refresh(field);
216
233
  parts.confirm.disabled = !allOk();
@@ -232,8 +249,29 @@ export function askFields({
232
249
  });
233
250
  };
234
251
  pending.set(doc, settle);
252
+ let typed = false;
253
+ // A value still being worked out: the field waits, then takes it, unless the dialog has moved on.
254
+ for (const field of fields) {
255
+ const { value } = field.request;
256
+ if (typeof value !== 'object' || !value) continue;
257
+ field.waiting = true;
258
+ field.input.disabled = true;
259
+ refresh(field);
260
+ value.catch(() => '').then((text) => {
261
+ if (signal.aborted) return;
262
+ field.waiting = false;
263
+ field.input.disabled = false;
264
+ field.input.value = text;
265
+ refresh(field);
266
+ parts.confirm.disabled = !allOk();
267
+ // The person was waiting for it: the focus moves in, unless they've been typing meanwhile.
268
+ if (!typed) field.input.focus();
269
+ });
270
+ }
271
+ parts.confirm.disabled = !allOk();
235
272
  for (const field of fields) {
236
273
  field.input.addEventListener('input', () => {
274
+ typed = true;
237
275
  refresh(field);
238
276
  parts.confirm.disabled = !allOk();
239
277
  }, { signal });
@@ -256,7 +294,7 @@ export function askFields({
256
294
 
257
295
  parts.dialog.showModal();
258
296
  // The first field still to fill in, or the first, its text selected to type over.
259
- (fields.find(({ input }) => !input.value) ?? fields[0])?.input.select();
297
+ (fields.find(({ input }) => !input.value && !input.disabled) ?? fields[0])?.input.select();
260
298
  });
261
299
  }
262
300
 
@@ -275,8 +313,8 @@ export function askFields({
275
313
  * });
276
314
  */
277
315
  export async function askText({
278
- label, value, placeholder, validate, maxLength, ...dialog
316
+ label, value, placeholder, validate, maxLength, multiline, rows, hint, waiting, ...dialog
279
317
  }) {
280
- const answer = await askFields({ ...dialog, fields: [{ name: 'input', label, value, placeholder, validate, maxLength }] });
318
+ const answer = await askFields({ ...dialog, fields: [{ name: 'input', label, value, placeholder, validate, maxLength, multiline, rows, hint, waiting }] });
281
319
  return answer?.input ?? null;
282
320
  }