@arraypress/extension-kit 1.1.0 → 1.3.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.
Files changed (48) hide show
  1. package/README.md +166 -14
  2. package/bin/extension-kit.js +46 -10
  3. package/package.json +1 -1
  4. package/src/build/index.d.ts +4 -2
  5. package/src/build/index.js +6 -2
  6. package/src/build/store-frame.d.ts +176 -0
  7. package/src/build/store-frame.js +241 -0
  8. package/src/build/test-build.d.ts +85 -0
  9. package/src/build/test-build.js +82 -0
  10. package/src/build/toolbar-icons.d.ts +36 -18
  11. package/src/build/toolbar-icons.js +91 -24
  12. package/src/build/vendor.d.ts +78 -8
  13. package/src/build/vendor.js +133 -10
  14. package/src/build/zip.d.ts +3 -2
  15. package/src/build/zip.js +6 -4
  16. package/src/dom/focus.d.ts +28 -0
  17. package/src/dom/focus.js +36 -0
  18. package/src/dom/setup.d.ts +54 -0
  19. package/src/dom/setup.js +46 -0
  20. package/src/index.d.ts +29 -2
  21. package/src/index.js +23 -2
  22. package/src/sites/sites.d.ts +82 -0
  23. package/src/sites/sites.js +128 -0
  24. package/src/styles/chrome-ui.css +18 -7
  25. package/src/testing/browser.d.ts +52 -0
  26. package/src/testing/browser.js +79 -0
  27. package/src/testing/e2e.d.ts +18 -5
  28. package/src/testing/e2e.js +17 -5
  29. package/src/testing/index.d.ts +15 -4
  30. package/src/testing/index.js +15 -4
  31. package/src/testing/package.d.ts +62 -4
  32. package/src/testing/package.js +39 -8
  33. package/src/testing/pool.d.ts +23 -0
  34. package/src/testing/pool.js +33 -0
  35. package/src/testing/saved-groups.d.ts +33 -0
  36. package/src/testing/saved-groups.js +222 -0
  37. package/src/testing/time.d.ts +35 -0
  38. package/src/testing/time.js +46 -0
  39. package/src/testing/warnings.d.ts +57 -0
  40. package/src/testing/warnings.js +69 -0
  41. package/src/ui/menu.d.ts +257 -0
  42. package/src/ui/menu.js +283 -0
  43. package/src/ui/site-list.d.ts +199 -0
  44. package/src/ui/site-list.js +194 -0
  45. package/src/ui/text-dialog.d.ts +132 -0
  46. package/src/ui/text-dialog.js +187 -0
  47. package/src/ui/toast.d.ts +89 -0
  48. package/src/ui/toast.js +103 -0
@@ -0,0 +1,199 @@
1
+ /**
2
+ * A settings card's list of sites (“Never on”, “Allowed sites”), in one of
3
+ * Chrome's two ways:
4
+ *
5
+ * - **With an add row** (`form`), as a short list in a chrome://settings
6
+ * card: a row per site with a Remove button, and below them a field and an
7
+ * Add button.
8
+ * - **With an Add button and a dialog** (`addButton`), as
9
+ * chrome://settings/performance lists the sites Memory Saver keeps active:
10
+ * the button asks for a site in the one-field dialog (“Add site”), and each
11
+ * row has a ⋮ menu with Edit and Remove.
12
+ *
13
+ * Either way, an optional “No sites added” row shows while there are none.
14
+ * The page's markup holds the list, the empty row and the form or button (so
15
+ * they're localised with the rest of the page); this fills the list and wires
16
+ * them up.
17
+ *
18
+ * ```html
19
+ * <ul class="list" id="never-sites"></ul>
20
+ * <p class="row text" id="no-sites"><span class="label secondary">No sites added</span></p>
21
+ * <form class="row add" id="add-never">
22
+ * <input type="text" placeholder="example.com" aria-label="Site" autocomplete="off" spellcheck="false">
23
+ * <button type="submit">Add</button>
24
+ * </form>
25
+ * ```
26
+ *
27
+ * @module ui/site-list
28
+ */
29
+ export type SiteListWords = {
30
+ /**
31
+ * The Add button in the dialog.
32
+ */
33
+ add: string;
34
+ /**
35
+ * The dialog's title when adding.
36
+ */
37
+ addSite: string;
38
+ /**
39
+ * The dialog's title when editing.
40
+ */
41
+ editSite: string;
42
+ /**
43
+ * The ⋮ menu's Edit item.
44
+ */
45
+ edit: string;
46
+ /**
47
+ * The Remove button, or the ⋮ menu's Remove item.
48
+ */
49
+ remove: string;
50
+ /**
51
+ * The dialog's action when editing.
52
+ */
53
+ save: string;
54
+ /**
55
+ * The dialog's Cancel button.
56
+ */
57
+ cancel: string;
58
+ /**
59
+ * The dialog's field label.
60
+ */
61
+ site: string;
62
+ /**
63
+ * Under the dialog's field while what's typed isn't a site.
64
+ */
65
+ invalid: string;
66
+ /**
67
+ * The Remove button's name for screen readers.
68
+ */
69
+ removeLabel: (label: string) => string;
70
+ /**
71
+ * The ⋮ button's name for screen readers.
72
+ */
73
+ moreActions: (label: string) => string;
74
+ };
75
+ /**
76
+ * The list's words, Chrome's English by default; a page passes its translations.
77
+ *
78
+ * @typedef {object} SiteListWords
79
+ * @property {string} add The Add button in the dialog.
80
+ * @property {string} addSite The dialog's title when adding.
81
+ * @property {string} editSite The dialog's title when editing.
82
+ * @property {string} edit The ⋮ menu's Edit item.
83
+ * @property {string} remove The Remove button, or the ⋮ menu's Remove item.
84
+ * @property {string} save The dialog's action when editing.
85
+ * @property {string} cancel The dialog's Cancel button.
86
+ * @property {string} site The dialog's field label.
87
+ * @property {string} invalid Under the dialog's field while what's typed isn't a site.
88
+ * @property {(label: string) => string} removeLabel The Remove button's name for screen readers.
89
+ * @property {(label: string) => string} moreActions The ⋮ button's name for screen readers.
90
+ */
91
+ /** @type {Readonly<SiteListWords>} */
92
+ export declare const SITE_LIST_WORDS: Readonly<SiteListWords>;
93
+ export type SiteListOptions = {
94
+ /**
95
+ * The `<ul class="list">` to fill.
96
+ */
97
+ list: HTMLElement;
98
+ /**
99
+ * Shown while the list is empty.
100
+ */
101
+ empty?: HTMLElement;
102
+ /**
103
+ * An add row: a form with one text field and a submit button.
104
+ */
105
+ form?: HTMLFormElement;
106
+ /**
107
+ * Or an Add button that asks in the dialog; each row then has a
108
+ * ⋮ menu (Edit, Remove) instead of a Remove button.
109
+ */
110
+ addButton?: HTMLElement;
111
+ /**
112
+ * What was typed as the site the list keeps
113
+ * (`siteHost`, `siteOrigin`), or null when it isn't one.
114
+ */
115
+ parse: (text: string) => string | null;
116
+ /**
117
+ * Adds a parsed site (asking Chrome for access first, if
118
+ * the list needs it). Resolve to false to keep the text in the add row's field (access refused).
119
+ */
120
+ add: (site: string) => unknown;
121
+ /**
122
+ * Removes a site.
123
+ */
124
+ remove: (site: string) => unknown;
125
+ /**
126
+ * Puts an edited site in place of
127
+ * the old one (the ⋮ menu's Edit; without it, the menu has only Remove).
128
+ */
129
+ replace?: (site: string, edited: string) => unknown;
130
+ /**
131
+ * The add row: called with what was typed when it isn't a site.
132
+ */
133
+ invalid?: (text: string) => void;
134
+ /**
135
+ * How a site reads. Default {@link siteLabel}.
136
+ */
137
+ label?: (site: string) => string;
138
+ /**
139
+ * The dialog field's `maxlength`. Default {@link MAX_HOST_LENGTH}.
140
+ */
141
+ maxLength?: number;
142
+ /**
143
+ * Translations of {@link SITE_LIST_WORDS}.
144
+ */
145
+ words?: Partial<SiteListWords>;
146
+ };
147
+ export type SiteList = {
148
+ /**
149
+ * Show these sites (call it whenever they change).
150
+ */
151
+ render: (sites: readonly string[]) => void;
152
+ };
153
+ /**
154
+ * What {@link createSiteList} takes: the list, one way to add to it, and
155
+ * what adding, removing and editing do.
156
+ *
157
+ * @typedef {object} SiteListOptions
158
+ * @property {HTMLElement} list The `<ul class="list">` to fill.
159
+ * @property {HTMLElement} [empty] Shown while the list is empty.
160
+ * @property {HTMLFormElement} [form] An add row: a form with one text field and a submit button.
161
+ * @property {HTMLElement} [addButton] Or an Add button that asks in the dialog; each row then has a
162
+ * ⋮ menu (Edit, Remove) instead of a Remove button.
163
+ * @property {(text: string) => string | null} parse What was typed as the site the list keeps
164
+ * (`siteHost`, `siteOrigin`), or null when it isn't one.
165
+ * @property {(site: string) => unknown} add Adds a parsed site (asking Chrome for access first, if
166
+ * the list needs it). Resolve to false to keep the text in the add row's field (access refused).
167
+ * @property {(site: string) => unknown} remove Removes a site.
168
+ * @property {(site: string, edited: string) => unknown} [replace] Puts an edited site in place of
169
+ * the old one (the ⋮ menu's Edit; without it, the menu has only Remove).
170
+ * @property {(text: string) => void} [invalid] The add row: called with what was typed when it isn't a site.
171
+ * @property {(site: string) => string} [label] How a site reads. Default {@link siteLabel}.
172
+ * @property {number} [maxLength] The dialog field's `maxlength`. Default {@link MAX_HOST_LENGTH}.
173
+ * @property {Partial<SiteListWords>} [words] Translations of {@link SITE_LIST_WORDS}.
174
+ */
175
+ /**
176
+ * A site list.
177
+ *
178
+ * @typedef {object} SiteList
179
+ * @property {(sites: readonly string[]) => void} render Show these sites (call it whenever they change).
180
+ */
181
+ /**
182
+ * Fill a site list and wire its way of adding: an add row (typing a site and
183
+ * pressing Enter or Add) or an Add button (the dialog, which keeps its Add off
184
+ * until what's typed is a site). A row's Remove (or its ⋮ menu's Remove and
185
+ * Edit) hands its site to `remove` (or `replace`).
186
+ *
187
+ * @param {SiteListOptions} options - The markup, the parser and what adding and removing do.
188
+ * @returns {SiteList} The list.
189
+ * @example
190
+ * const never = createSiteList({
191
+ * list: byId('never-sites'), form: byId('add-never', HTMLFormElement), empty: byId('no-sites'),
192
+ * parse: siteHost,
193
+ * add: (host) => save({ neverSites: [...settings.neverSites, host] }),
194
+ * remove: (host) => save({ neverSites: settings.neverSites.filter((h) => h !== host) }),
195
+ * words: { remove: t('optionsRemove'), removeLabel: (site) => t('optionsRemoveLabel', [site]) },
196
+ * });
197
+ * never.render(settings.neverSites);
198
+ */
199
+ export declare function createSiteList({ list, empty, form, addButton, parse, add, remove, replace, invalid, label, maxLength, words, }: SiteListOptions): SiteList;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * A settings card's list of sites (“Never on”, “Allowed sites”), in one of
3
+ * Chrome's two ways:
4
+ *
5
+ * - **With an add row** (`form`), as a short list in a chrome://settings
6
+ * card: a row per site with a Remove button, and below them a field and an
7
+ * Add button.
8
+ * - **With an Add button and a dialog** (`addButton`), as
9
+ * chrome://settings/performance lists the sites Memory Saver keeps active:
10
+ * the button asks for a site in the one-field dialog (“Add site”), and each
11
+ * row has a ⋮ menu with Edit and Remove.
12
+ *
13
+ * Either way, an optional “No sites added” row shows while there are none.
14
+ * The page's markup holds the list, the empty row and the form or button (so
15
+ * they're localised with the rest of the page); this fills the list and wires
16
+ * them up.
17
+ *
18
+ * ```html
19
+ * <ul class="list" id="never-sites"></ul>
20
+ * <p class="row text" id="no-sites"><span class="label secondary">No sites added</span></p>
21
+ * <form class="row add" id="add-never">
22
+ * <input type="text" placeholder="example.com" aria-label="Site" autocomplete="off" spellcheck="false">
23
+ * <button type="submit">Add</button>
24
+ * </form>
25
+ * ```
26
+ *
27
+ * @module ui/site-list
28
+ */
29
+
30
+ import { createMenu, moreActionsButton } from './menu.js';
31
+ import { askText } from './text-dialog.js';
32
+ import { MAX_HOST_LENGTH, siteLabel } from '../sites/sites.js';
33
+
34
+ /**
35
+ * The list's words, Chrome's English by default; a page passes its translations.
36
+ *
37
+ * @typedef {object} SiteListWords
38
+ * @property {string} add The Add button in the dialog.
39
+ * @property {string} addSite The dialog's title when adding.
40
+ * @property {string} editSite The dialog's title when editing.
41
+ * @property {string} edit The ⋮ menu's Edit item.
42
+ * @property {string} remove The Remove button, or the ⋮ menu's Remove item.
43
+ * @property {string} save The dialog's action when editing.
44
+ * @property {string} cancel The dialog's Cancel button.
45
+ * @property {string} site The dialog's field label.
46
+ * @property {string} invalid Under the dialog's field while what's typed isn't a site.
47
+ * @property {(label: string) => string} removeLabel The Remove button's name for screen readers.
48
+ * @property {(label: string) => string} moreActions The ⋮ button's name for screen readers.
49
+ */
50
+
51
+ /** @type {Readonly<SiteListWords>} */
52
+ export const SITE_LIST_WORDS = Object.freeze({
53
+ add: 'Add',
54
+ addSite: 'Add site',
55
+ editSite: 'Edit site',
56
+ edit: 'Edit',
57
+ remove: 'Remove',
58
+ save: 'Save',
59
+ cancel: 'Cancel',
60
+ site: 'Site',
61
+ invalid: 'Enter a site, like example.com',
62
+ removeLabel: (/** @type {string} */ label) => `Remove “${label}”`,
63
+ moreActions: (/** @type {string} */ label) => `More actions for “${label}”`,
64
+ });
65
+
66
+ /**
67
+ * What {@link createSiteList} takes: the list, one way to add to it, and
68
+ * what adding, removing and editing do.
69
+ *
70
+ * @typedef {object} SiteListOptions
71
+ * @property {HTMLElement} list The `<ul class="list">` to fill.
72
+ * @property {HTMLElement} [empty] Shown while the list is empty.
73
+ * @property {HTMLFormElement} [form] An add row: a form with one text field and a submit button.
74
+ * @property {HTMLElement} [addButton] Or an Add button that asks in the dialog; each row then has a
75
+ * ⋮ menu (Edit, Remove) instead of a Remove button.
76
+ * @property {(text: string) => string | null} parse What was typed as the site the list keeps
77
+ * (`siteHost`, `siteOrigin`), or null when it isn't one.
78
+ * @property {(site: string) => unknown} add Adds a parsed site (asking Chrome for access first, if
79
+ * the list needs it). Resolve to false to keep the text in the add row's field (access refused).
80
+ * @property {(site: string) => unknown} remove Removes a site.
81
+ * @property {(site: string, edited: string) => unknown} [replace] Puts an edited site in place of
82
+ * the old one (the ⋮ menu's Edit; without it, the menu has only Remove).
83
+ * @property {(text: string) => void} [invalid] The add row: called with what was typed when it isn't a site.
84
+ * @property {(site: string) => string} [label] How a site reads. Default {@link siteLabel}.
85
+ * @property {number} [maxLength] The dialog field's `maxlength`. Default {@link MAX_HOST_LENGTH}.
86
+ * @property {Partial<SiteListWords>} [words] Translations of {@link SITE_LIST_WORDS}.
87
+ */
88
+
89
+ /**
90
+ * A site list.
91
+ *
92
+ * @typedef {object} SiteList
93
+ * @property {(sites: readonly string[]) => void} render Show these sites (call it whenever they change).
94
+ */
95
+
96
+ /**
97
+ * Fill a site list and wire its way of adding: an add row (typing a site and
98
+ * pressing Enter or Add) or an Add button (the dialog, which keeps its Add off
99
+ * until what's typed is a site). A row's Remove (or its ⋮ menu's Remove and
100
+ * Edit) hands its site to `remove` (or `replace`).
101
+ *
102
+ * @param {SiteListOptions} options - The markup, the parser and what adding and removing do.
103
+ * @returns {SiteList} The list.
104
+ * @example
105
+ * const never = createSiteList({
106
+ * list: byId('never-sites'), form: byId('add-never', HTMLFormElement), empty: byId('no-sites'),
107
+ * parse: siteHost,
108
+ * add: (host) => save({ neverSites: [...settings.neverSites, host] }),
109
+ * remove: (host) => save({ neverSites: settings.neverSites.filter((h) => h !== host) }),
110
+ * words: { remove: t('optionsRemove'), removeLabel: (site) => t('optionsRemoveLabel', [site]) },
111
+ * });
112
+ * never.render(settings.neverSites);
113
+ */
114
+ export function createSiteList({
115
+ list, empty, form, addButton, parse, add, remove, replace, invalid, label = siteLabel, maxLength = MAX_HOST_LENGTH, words,
116
+ }) {
117
+ const doc = list.ownerDocument;
118
+ const w = { ...SITE_LIST_WORDS, ...words };
119
+ const menu = addButton ? createMenu({ doc }) : null;
120
+
121
+ /**
122
+ * Ask for a site in the dialog.
123
+ *
124
+ * @param {string} title - Its title.
125
+ * @param {string} action - Its action button.
126
+ * @param {string} [value] - The site to start with.
127
+ * @returns {Promise<string | null>} The site, parsed, or null when cancelled.
128
+ */
129
+ const ask = async (title, action, value) => {
130
+ const text = await askText({
131
+ title, label: w.site, value, action, cancel: w.cancel, maxLength, doc,
132
+ validate: (typed) => (parse(typed) ? null : w.invalid),
133
+ });
134
+ return text === null ? null : parse(text);
135
+ };
136
+
137
+ form?.addEventListener('submit', async (event) => {
138
+ event.preventDefault();
139
+ const input = /** @type {HTMLInputElement} */ (form.querySelector('input'));
140
+ const site = parse(input.value);
141
+ if (!site) invalid?.(input.value);
142
+ else if (await add(site) !== false) input.value = '';
143
+ });
144
+ addButton?.addEventListener('click', async () => {
145
+ const site = await ask(w.addSite, w.add);
146
+ if (site) await add(site);
147
+ });
148
+
149
+ /**
150
+ * The end of a site's row: its ⋮ button, or its Remove button.
151
+ *
152
+ * @param {string} site - The site.
153
+ * @param {string} name - How it reads.
154
+ * @returns {HTMLButtonElement} The button.
155
+ */
156
+ const rowEnd = (site, name) => {
157
+ if (menu) {
158
+ return moreActionsButton(name, {
159
+ label: w.moreActions(name), doc,
160
+ onClick: (anchor) => menu.open([
161
+ ...replace ? [{
162
+ label: w.edit,
163
+ run: async () => {
164
+ const edited = await ask(w.editSite, w.save, name);
165
+ if (edited && edited !== site) await replace(site, edited);
166
+ },
167
+ }] : [],
168
+ { label: w.remove, run: () => remove(site) },
169
+ ], { anchor }),
170
+ });
171
+ }
172
+ const button = doc.createElement('button');
173
+ button.type = 'button';
174
+ button.textContent = w.remove;
175
+ button.setAttribute('aria-label', w.removeLabel(name));
176
+ button.addEventListener('click', () => remove(site));
177
+ return button;
178
+ };
179
+
180
+ return {
181
+ render(sites) {
182
+ list.replaceChildren(...sites.map((site) => {
183
+ const row = doc.createElement('li');
184
+ row.className = 'row';
185
+ const name = doc.createElement('span');
186
+ name.className = 'label';
187
+ name.textContent = label(site);
188
+ row.append(name, rowEnd(site, name.textContent));
189
+ return row;
190
+ }));
191
+ if (empty) empty.hidden = sites.length > 0;
192
+ },
193
+ };
194
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Chrome's one-field dialog (cr-dialog with a cr-input), as chrome://bookmarks
3
+ * asks for a folder's name: a title, a labelled field, Cancel and an action
4
+ * button. The action button stays disabled while the text is blank or isn't
5
+ * valid, with the reason under the field. Enter means the action button, Esc
6
+ * and Cancel cancel, and the focus goes back to what opened it. The page
7
+ * needs no markup: the dialog (`#text-dialog`) is made the first time.
8
+ *
9
+ * @module ui/text-dialog
10
+ */
11
+ export type TextRequest = {
12
+ /**
13
+ * The dialog's title ("Rename folder").
14
+ */
15
+ title: string;
16
+ /**
17
+ * The field's label ("Name").
18
+ */
19
+ label: string;
20
+ /**
21
+ * The text to start with (selected). Default ''.
22
+ */
23
+ value?: string;
24
+ /**
25
+ * The action button's text. Default "Save".
26
+ */
27
+ action?: string;
28
+ /**
29
+ * The cancel button's text. Default "Cancel".
30
+ */
31
+ cancel?: string;
32
+ /**
33
+ * Why the (non-blank) text can't be used,
34
+ * shown under the field; null when it can. Default: anything but blank.
35
+ */
36
+ validate?: (text: string) => string | null;
37
+ /**
38
+ * The field's `maxlength`. Default none.
39
+ */
40
+ maxLength?: number;
41
+ /**
42
+ * Where the focus
43
+ * goes when it closes, or a function that says (asked then, so it can pick a row drawn since).
44
+ * Default what had the focus when it opened.
45
+ */
46
+ returnTo?: HTMLElement | (() => HTMLElement | null | undefined) | null;
47
+ /**
48
+ * The page. Default `document`.
49
+ */
50
+ doc?: Document;
51
+ };
52
+ /**
53
+ * What the dialog asks.
54
+ *
55
+ * @typedef {object} TextRequest
56
+ * @property {string} title The dialog's title ("Rename folder").
57
+ * @property {string} label The field's label ("Name").
58
+ * @property {string} [value] The text to start with (selected). Default ''.
59
+ * @property {string} [action] The action button's text. Default "Save".
60
+ * @property {string} [cancel] The cancel button's text. Default "Cancel".
61
+ * @property {(text: string) => string | null} [validate] Why the (non-blank) text can't be used,
62
+ * shown under the field; null when it can. Default: anything but blank.
63
+ * @property {number} [maxLength] The field's `maxlength`. Default none.
64
+ * @property {HTMLElement | (() => HTMLElement | null | undefined) | null} [returnTo] Where the focus
65
+ * goes when it closes, or a function that says (asked then, so it can pick a row drawn since).
66
+ * Default what had the focus when it opened.
67
+ * @property {Document} [doc] The page. Default `document`.
68
+ */
69
+ /**
70
+ * Whether a text can be used, and the reason to show when it can't. Pure.
71
+ *
72
+ * @param {string} text - What's typed.
73
+ * @param {(text: string) => string | null} [validate] - Why a non-blank text can't be used, or null.
74
+ * @returns {{ok: boolean, reason: string}} `ok` when it can be used; `reason` is '' for blank text
75
+ * (nothing to say yet) and for a usable one.
76
+ * @example
77
+ * checkText(' '); // → {ok: false, reason: ''}
78
+ * checkText('x', () => 'Enter a site'); // → {ok: false, reason: 'Enter a site'}
79
+ */
80
+ export declare function checkText(text: string, validate?: (text: string) => string | null): {
81
+ ok: boolean;
82
+ reason: string;
83
+ };
84
+ export type Parts = {
85
+ /**
86
+ * The `<dialog>`.
87
+ */
88
+ dialog: HTMLDialogElement;
89
+ /**
90
+ * Its form.
91
+ */
92
+ form: HTMLFormElement;
93
+ /**
94
+ * The title.
95
+ */
96
+ title: HTMLElement;
97
+ /**
98
+ * The field's label.
99
+ */
100
+ label: HTMLLabelElement;
101
+ /**
102
+ * The field.
103
+ */
104
+ input: HTMLInputElement;
105
+ /**
106
+ * The reason under the field.
107
+ */
108
+ error: HTMLElement;
109
+ /**
110
+ * The cancel button.
111
+ */
112
+ cancel: HTMLButtonElement;
113
+ /**
114
+ * The action button.
115
+ */
116
+ confirm: HTMLButtonElement;
117
+ };
118
+ /**
119
+ * Ask for a piece of text in the page's one-field dialog.
120
+ *
121
+ * @param {TextRequest} request - What to ask.
122
+ * @returns {Promise<string | null>} The text, trimmed; null when cancelled.
123
+ * @example
124
+ * const name = await askText({title: 'Rename workspace', label: 'Name', value: workspace.name, maxLength: 200});
125
+ * if (name) rename(workspace, name);
126
+ *
127
+ * const site = await askText({
128
+ * title: 'Add site', label: 'Site', action: 'Add',
129
+ * validate: (text) => (siteFromInput(text) ? null : 'Enter a site, like example.com'),
130
+ * });
131
+ */
132
+ export declare function askText({ title, label, value, action, cancel, validate, maxLength, returnTo, doc, }: TextRequest): Promise<string | null>;