@arraypress/extension-kit 1.6.0 → 1.8.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The kit's test helpers: a fake `chrome.storage`, Playwright helpers for
2
+ * The kit's test helpers: a fake `chrome.storage`, a fake on-device model, Playwright helpers for
3
3
  * end-to-end tests (time limits, a pool, a browser that passes for a
4
4
  * person's) and a server for their pages, Chrome's install warnings and
5
5
  * saved tab groups, and checks for a packaging test. Node only.
@@ -8,19 +8,26 @@
8
8
  */
9
9
  export { TEST_BUILDS_ENV, testBuildDir } from '../build/test-build.js';
10
10
  export { chromeUserAgent, closeOnSignal, realisticBrowser } from './browser.js';
11
- export { CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, until, } from './e2e.js';
11
+ export { CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, runChecks, until, } from './e2e.js';
12
12
  export { exitPoint, parseLiveOptions, parseProxy } from './live.js';
13
13
  export { eachLimited } from '../time/pool.js';
14
14
  export { filesIn, launchDownloading } from './downloads.js';
15
+ export { fakeLanguageModel } from './fake-model.js';
16
+ export { messageLookup, readMessages } from './messages.js';
17
+ export { heartbeat } from './heartbeat.js';
18
+ export { playPermissionPrompt } from './prompt.js';
15
19
  export { serve, testCertificate } from './server.js';
16
- export { SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
20
+ export { SYNC_QUOTA_BYTES, SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
17
21
  export { filesUnder, manifestReferences, missingReferences, referencedPath, zipEntries } from './package.js';
18
22
  export { savedTabGroups } from './saved-groups.js';
19
23
  export { installWarnings, warningsAddedBy, withoutPermissions } from './warnings.js';
20
24
  export type CheckRunner = import('./e2e.js').CheckRunner;
25
+ export type FakeLanguageModel = import('./fake-model.js').FakeLanguageModel;
26
+ export type FakeModelState = import('./fake-model.js').FakeModelState;
21
27
  export type ExtensionProfile = import('./e2e.js').ExtensionProfile;
22
28
  export type LaunchedExtension = import('./e2e.js').LaunchedExtension;
23
29
  export type LaunchOptions = import('./e2e.js').LaunchOptions;
30
+ export type Messages = import('./messages.js').Messages;
24
31
  export type Exit = import('./live.js').Exit;
25
32
  export type LiveOptions = import('./live.js').LiveOptions;
26
33
  export type SavedTabGroup = import('./saved-groups.js').SavedTabGroup;
@@ -32,9 +39,12 @@ export type TestServer = import('./server.js').TestServer;
32
39
  * The helpers' types, for a test's JSDoc.
33
40
  *
34
41
  * @typedef {import('./e2e.js').CheckRunner} CheckRunner
42
+ * @typedef {import('./fake-model.js').FakeLanguageModel} FakeLanguageModel
43
+ * @typedef {import('./fake-model.js').FakeModelState} FakeModelState
35
44
  * @typedef {import('./e2e.js').ExtensionProfile} ExtensionProfile
36
45
  * @typedef {import('./e2e.js').LaunchedExtension} LaunchedExtension
37
46
  * @typedef {import('./e2e.js').LaunchOptions} LaunchOptions
47
+ * @typedef {import('./messages.js').Messages} Messages
38
48
  * @typedef {import('./live.js').Exit} Exit
39
49
  * @typedef {import('./live.js').LiveOptions} LiveOptions
40
50
  * @typedef {import('./saved-groups.js').SavedTabGroup} SavedTabGroup
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The kit's test helpers: a fake `chrome.storage`, Playwright helpers for
2
+ * The kit's test helpers: a fake `chrome.storage`, a fake on-device model, Playwright helpers for
3
3
  * end-to-end tests (time limits, a pool, a browser that passes for a
4
4
  * person's) and a server for their pages, Chrome's install warnings and
5
5
  * saved tab groups, and checks for a packaging test. Node only.
@@ -10,13 +10,17 @@
10
10
  export { TEST_BUILDS_ENV, testBuildDir } from '../build/test-build.js';
11
11
  export { chromeUserAgent, closeOnSignal, realisticBrowser } from './browser.js';
12
12
  export {
13
- CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, until,
13
+ CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, runChecks, until,
14
14
  } from './e2e.js';
15
15
  export { exitPoint, parseLiveOptions, parseProxy } from './live.js';
16
16
  export { eachLimited } from '../time/pool.js';
17
17
  export { filesIn, launchDownloading } from './downloads.js';
18
+ export { fakeLanguageModel } from './fake-model.js';
19
+ export { messageLookup, readMessages } from './messages.js';
20
+ export { heartbeat } from './heartbeat.js';
21
+ export { playPermissionPrompt } from './prompt.js';
18
22
  export { serve, testCertificate } from './server.js';
19
- export { SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
23
+ export { SYNC_QUOTA_BYTES, SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
20
24
  export { filesUnder, manifestReferences, missingReferences, referencedPath, zipEntries } from './package.js';
21
25
  export { savedTabGroups } from './saved-groups.js';
22
26
  export { installWarnings, warningsAddedBy, withoutPermissions } from './warnings.js';
@@ -25,9 +29,12 @@ export { installWarnings, warningsAddedBy, withoutPermissions } from './warnings
25
29
  * The helpers' types, for a test's JSDoc.
26
30
  *
27
31
  * @typedef {import('./e2e.js').CheckRunner} CheckRunner
32
+ * @typedef {import('./fake-model.js').FakeLanguageModel} FakeLanguageModel
33
+ * @typedef {import('./fake-model.js').FakeModelState} FakeModelState
28
34
  * @typedef {import('./e2e.js').ExtensionProfile} ExtensionProfile
29
35
  * @typedef {import('./e2e.js').LaunchedExtension} LaunchedExtension
30
36
  * @typedef {import('./e2e.js').LaunchOptions} LaunchOptions
37
+ * @typedef {import('./messages.js').Messages} Messages
31
38
  * @typedef {import('./live.js').Exit} Exit
32
39
  * @typedef {import('./live.js').LiveOptions} LiveOptions
33
40
  * @typedef {import('./saved-groups.js').SavedTabGroup} SavedTabGroup
@@ -0,0 +1,33 @@
1
+ export type Messages = Record<string, {
2
+ message: string;
3
+ description?: string;
4
+ placeholders?: Record<string, {
5
+ content: string;
6
+ }>;
7
+ }>;
8
+ /**
9
+ * One locale's messages.json.
10
+ *
11
+ * @typedef {Record<string, {message: string, description?: string, placeholders?: Record<string, {content: string}>}>} Messages
12
+ */
13
+ /**
14
+ * Read a locale's messages.
15
+ *
16
+ * @param {string} extensionDir - The extension's folder (with `_locales/`).
17
+ * @param {string} locale - "en" or "en_GB".
18
+ * @returns {Messages} Its messages.
19
+ * @example
20
+ * readMessages('extension', 'en_GB').optionsColour.message;
21
+ */
22
+ export declare function readMessages(extensionDir: string, locale: string): Messages;
23
+ /**
24
+ * `chrome.i18n.getMessage` over a locale's real messages ('' for a missing key, as Chrome gives).
25
+ *
26
+ * @param {string} extensionDir - The extension's folder.
27
+ * @param {string} [locale] - Default "en"; another locale is read over en.
28
+ * @returns {(key: string, substitutions?: string | string[]) => string} The lookup.
29
+ * @example
30
+ * const t = messageLookup('extension');
31
+ * t('optionsMoreActions', ['example.com']); // 'More actions for example.com'
32
+ */
33
+ export declare function messageLookup(extensionDir: string, locale?: string): (key: string, substitutions?: string | string[]) => string;
@@ -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,20 @@
1
+ /**
2
+ * Chrome's permission prompt, played in an extension page for an e2e test: Playwright can't
3
+ * click Chrome's own prompt. From Topicless' and Better Screenshots' e2e helpers.
4
+ *
5
+ * @module testing/prompt
6
+ */
7
+ /**
8
+ * Stand in for Chrome's permission prompt in a page: each `chrome.permissions.request` is
9
+ * recorded on `window.asked` (what was asked, and whether a click was behind it,
10
+ * `navigator.userActivation.isActive`) and answered from `answers` (the next one; the last is
11
+ * kept), as a person clicking “Allow” or “Deny” would. Runs in the page: give it to
12
+ * `page.evaluate` or `page.addInitScript`, so it takes no closures.
13
+ *
14
+ * @param {boolean[]} answers - The answers, in order.
15
+ * @example
16
+ * await page.evaluate(playPermissionPrompt, [true]);
17
+ * await page.click('#download');
18
+ * const asked = await page.evaluate(() => window.asked); // [{ permissions: ['downloads'], origins: undefined, gesture: true }]
19
+ */
20
+ export declare function playPermissionPrompt(answers: boolean[]): void;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Chrome's permission prompt, played in an extension page for an e2e test: Playwright can't
3
+ * click Chrome's own prompt. From Topicless' and Better Screenshots' e2e helpers.
4
+ *
5
+ * @module testing/prompt
6
+ */
7
+
8
+ /**
9
+ * Stand in for Chrome's permission prompt in a page: each `chrome.permissions.request` is
10
+ * recorded on `window.asked` (what was asked, and whether a click was behind it,
11
+ * `navigator.userActivation.isActive`) and answered from `answers` (the next one; the last is
12
+ * kept), as a person clicking “Allow” or “Deny” would. Runs in the page: give it to
13
+ * `page.evaluate` or `page.addInitScript`, so it takes no closures.
14
+ *
15
+ * @param {boolean[]} answers - The answers, in order.
16
+ * @example
17
+ * await page.evaluate(playPermissionPrompt, [true]);
18
+ * await page.click('#download');
19
+ * const asked = await page.evaluate(() => window.asked); // [{ permissions: ['downloads'], origins: undefined, gesture: true }]
20
+ */
21
+ export function playPermissionPrompt(answers) {
22
+ /** @type {{permissions?: string[], origins?: string[], gesture: boolean}[]} */
23
+ const asked = [];
24
+ Object.assign(window, { asked });
25
+ chrome.permissions.request = /** @type {typeof chrome.permissions.request} */ (/** @type {unknown} */ (async (/** @type {chrome.permissions.Permissions} */ permissions) => {
26
+ asked.push({ permissions: permissions.permissions, origins: permissions.origins, gesture: navigator.userActivation.isActive });
27
+ return answers.length > 1 ? answers.shift() : answers[0];
28
+ }));
29
+ }
@@ -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>;