@arraypress/extension-kit 1.2.0 → 1.4.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 (43) hide show
  1. package/README.md +147 -17
  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/i18n.d.ts +11 -3
  19. package/src/dom/i18n.js +17 -4
  20. package/src/dom/setup.d.ts +55 -0
  21. package/src/dom/setup.js +47 -0
  22. package/src/index.d.ts +9 -1
  23. package/src/index.js +10 -1
  24. package/src/sites/sites.d.ts +114 -0
  25. package/src/sites/sites.js +169 -0
  26. package/src/styles/chrome-ui.css +13 -6
  27. package/src/testing/browser.d.ts +52 -0
  28. package/src/testing/browser.js +79 -0
  29. package/src/testing/e2e.d.ts +18 -5
  30. package/src/testing/e2e.js +17 -5
  31. package/src/testing/index.d.ts +7 -3
  32. package/src/testing/index.js +9 -3
  33. package/src/testing/pool.d.ts +23 -0
  34. package/src/testing/pool.js +33 -0
  35. package/src/time/time.d.ts +36 -0
  36. package/src/time/time.js +47 -0
  37. package/src/ui/menu.d.ts +13 -6
  38. package/src/ui/menu.js +11 -7
  39. package/src/ui/site-list.d.ts +216 -0
  40. package/src/ui/site-list.js +234 -0
  41. package/src/ui/text-dialog.js +3 -1
  42. package/src/ui/toast.d.ts +25 -9
  43. package/src/ui/toast.js +25 -11
@@ -1,12 +1,18 @@
1
1
  /**
2
2
  * The kit's test helpers: a fake `chrome.storage`, Playwright helpers for
3
- * end-to-end tests and a server for their pages, Chrome's install warnings
4
- * and saved tab groups, and checks for a packaging test. Node only.
3
+ * end-to-end tests (time limits, a pool, a browser that passes for a
4
+ * person's) and a server for their pages, Chrome's install warnings and
5
+ * saved tab groups, and checks for a packaging test. Node only.
5
6
  *
6
7
  * @module testing
7
8
  */
8
9
 
9
- export { checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, until } from './e2e.js';
10
+ export { TEST_BUILDS_ENV, testBuildDir } from '../build/test-build.js';
11
+ export { chromeUserAgent, closeOnSignal, realisticBrowser } from './browser.js';
12
+ export {
13
+ CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, until,
14
+ } from './e2e.js';
15
+ export { eachLimited } from './pool.js';
10
16
  export { serve } from './server.js';
11
17
  export { SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
12
18
  export { filesUnder, manifestReferences, missingReferences, referencedPath, zipEntries } from './package.js';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Working through a list a few at a time: a live check of many sites, each
3
+ * in one of a few browsers.
4
+ *
5
+ * @module testing/pool
6
+ */
7
+ /**
8
+ * Run `work` on every item, at most `limit` at once, each slot taking the
9
+ * next item as it finishes one. Errors are `work`'s to handle: one that
10
+ * throws stops its slot, and the returned promise rejects once the others
11
+ * are done.
12
+ *
13
+ * @template T
14
+ * @param {readonly T[]} items - The items.
15
+ * @param {number} limit - At most this many at once.
16
+ * @param {(item: T, slot: number, index: number) => Promise<void>} work - Deals with one item;
17
+ * `slot` (0 to limit − 1) is the same for everything one slot does, for per-slot resources
18
+ * (a browser each).
19
+ * @returns {Promise<void>} Once every item is done.
20
+ * @example
21
+ * await eachLimited(sites, 3, async (site, slot) => check(site, browsers[slot]));
22
+ */
23
+ export declare function eachLimited<T>(items: readonly T[], limit: number, work: (item: T, slot: number, index: number) => Promise<void>): Promise<void>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Working through a list a few at a time: a live check of many sites, each
3
+ * in one of a few browsers.
4
+ *
5
+ * @module testing/pool
6
+ */
7
+
8
+ /**
9
+ * Run `work` on every item, at most `limit` at once, each slot taking the
10
+ * next item as it finishes one. Errors are `work`'s to handle: one that
11
+ * throws stops its slot, and the returned promise rejects once the others
12
+ * are done.
13
+ *
14
+ * @template T
15
+ * @param {readonly T[]} items - The items.
16
+ * @param {number} limit - At most this many at once.
17
+ * @param {(item: T, slot: number, index: number) => Promise<void>} work - Deals with one item;
18
+ * `slot` (0 to limit − 1) is the same for everything one slot does, for per-slot resources
19
+ * (a browser each).
20
+ * @returns {Promise<void>} Once every item is done.
21
+ * @example
22
+ * await eachLimited(sites, 3, async (site, slot) => check(site, browsers[slot]));
23
+ */
24
+ export async function eachLimited(items, limit, work) {
25
+ let next = 0;
26
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async (_, slot) => {
27
+ while (next < items.length) {
28
+ const index = next;
29
+ next += 1;
30
+ await work(items[index], slot, index);
31
+ }
32
+ }));
33
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Time limits for work that may never finish: a Chrome call a service worker
3
+ * shouldn't wait on forever, or, in tests that drive a real browser, a page
4
+ * that stops responding. Runs anywhere (worker, page, Node).
5
+ *
6
+ * @module time/time
7
+ */
8
+ /**
9
+ * Settle `promise` within `ms`, or settle the way `late` says. The timer is
10
+ * cleared as soon as either happens, so a finished run isn't kept waiting
11
+ * for it. The work itself isn't stopped.
12
+ *
13
+ * @template T
14
+ * @param {Promise<T>} promise - The work.
15
+ * @param {number} ms - The limit.
16
+ * @param {(resolve: (value: T) => void, reject: (error: Error) => void) => void} late - Runs when the
17
+ * limit passes first.
18
+ * @returns {Promise<T>} The work's outcome, or `late`'s.
19
+ */
20
+ export declare function deadline<T>(promise: Promise<T>, ms: number, late: (resolve: (value: T) => void, reject: (error: Error) => void) => void): Promise<T>;
21
+ /**
22
+ * A promise's value within a time limit, or `fallback` when it's late or
23
+ * fails: for a step to give up on and go past (a Chrome call that never
24
+ * answers, a renderer that stopped responding, a close that never returns).
25
+ * The work itself isn't stopped; the timer is cleared once it settles.
26
+ *
27
+ * @template T
28
+ * @param {Promise<T>} promise - The work.
29
+ * @param {number} ms - The limit.
30
+ * @param {T} fallback - What to give if it's late or fails.
31
+ * @returns {Promise<T>} Its value, or the fallback. Never rejects.
32
+ * @example
33
+ * const list = await within(handled(worker, page), 8000, []);
34
+ * await within(page.close(), 15000, undefined);
35
+ */
36
+ export declare function within<T>(promise: Promise<T>, ms: number, fallback: T): Promise<T>;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Time limits for work that may never finish: a Chrome call a service worker
3
+ * shouldn't wait on forever, or, in tests that drive a real browser, a page
4
+ * that stops responding. Runs anywhere (worker, page, Node).
5
+ *
6
+ * @module time/time
7
+ */
8
+
9
+ /**
10
+ * Settle `promise` within `ms`, or settle the way `late` says. The timer is
11
+ * cleared as soon as either happens, so a finished run isn't kept waiting
12
+ * for it. The work itself isn't stopped.
13
+ *
14
+ * @template T
15
+ * @param {Promise<T>} promise - The work.
16
+ * @param {number} ms - The limit.
17
+ * @param {(resolve: (value: T) => void, reject: (error: Error) => void) => void} late - Runs when the
18
+ * limit passes first.
19
+ * @returns {Promise<T>} The work's outcome, or `late`'s.
20
+ */
21
+ export function deadline(promise, ms, late) {
22
+ /** @type {ReturnType<typeof setTimeout> | undefined} */
23
+ let timer;
24
+ const limit = new Promise((resolve, reject) => {
25
+ timer = setTimeout(() => late(resolve, reject), ms);
26
+ });
27
+ return /** @type {Promise<T>} */ (Promise.race([promise, limit])).finally(() => clearTimeout(timer));
28
+ }
29
+
30
+ /**
31
+ * A promise's value within a time limit, or `fallback` when it's late or
32
+ * fails: for a step to give up on and go past (a Chrome call that never
33
+ * answers, a renderer that stopped responding, a close that never returns).
34
+ * The work itself isn't stopped; the timer is cleared once it settles.
35
+ *
36
+ * @template T
37
+ * @param {Promise<T>} promise - The work.
38
+ * @param {number} ms - The limit.
39
+ * @param {T} fallback - What to give if it's late or fails.
40
+ * @returns {Promise<T>} Its value, or the fallback. Never rejects.
41
+ * @example
42
+ * const list = await within(handled(worker, page), 8000, []);
43
+ * await within(page.close(), 15000, undefined);
44
+ */
45
+ export function within(promise, ms, fallback) {
46
+ return deadline(promise.catch(() => fallback), ms, (resolve) => resolve(fallback));
47
+ }
package/src/ui/menu.d.ts CHANGED
@@ -140,13 +140,18 @@ export type MenuOptions = {
140
140
  */
141
141
  placement?: 'over' | 'below';
142
142
  /**
143
- * The page. Default `document`.
143
+ * Where the menu goes: an element, or a shadow root (a content
144
+ * script's, so the page's styles never reach it). Default the page's `body`.
145
+ */
146
+ root?: ParentNode;
147
+ /**
148
+ * The page. Default `root`'s document, else `document`.
144
149
  */
145
150
  doc?: Document;
146
151
  };
147
152
  export type ActionMenu = {
148
153
  /**
149
- * The `<dialog class="menu" role="menu">`, in the page's body.
154
+ * The `<dialog class="menu" role="menu">`, in `root`.
150
155
  */
151
156
  element: HTMLDialogElement;
152
157
  /**
@@ -165,13 +170,15 @@ export type ActionMenu = {
165
170
  * @typedef {object} MenuOptions
166
171
  * @property {string} [id] The menu element's id.
167
172
  * @property {'over' | 'below'} [placement] Where it opens from a button. Default `'over'`, as Chrome's.
168
- * @property {Document} [doc] The page. Default `document`.
173
+ * @property {ParentNode} [root] Where the menu goes: an element, or a shadow root (a content
174
+ * script's, so the page's styles never reach it). Default the page's `body`.
175
+ * @property {Document} [doc] The page. Default `root`'s document, else `document`.
169
176
  */
170
177
  /**
171
178
  * A page's action menu.
172
179
  *
173
180
  * @typedef {object} ActionMenu
174
- * @property {HTMLDialogElement} element The `<dialog class="menu" role="menu">`, in the page's body.
181
+ * @property {HTMLDialogElement} element The `<dialog class="menu" role="menu">`, in `root`.
175
182
  * @property {(entries: readonly MenuEntry[], where: MenuPlace) => void} open Fill it with `entries`
176
183
  * and open it (closing it first if it's open).
177
184
  * @property {() => void} close Close it, if open, and return the focus.
@@ -180,7 +187,7 @@ export type ActionMenu = {
180
187
  * Make a page's action menu: one `<dialog class="menu">` that any number of
181
188
  * ⋮ buttons (or right-clicks) open with their own items.
182
189
  *
183
- * @param {MenuOptions} [options] - Its id, placement and page.
190
+ * @param {MenuOptions} [options] - Its id, placement, and where it goes.
184
191
  * @returns {ActionMenu} The menu.
185
192
  * @example
186
193
  * const menu = createMenu();
@@ -192,7 +199,7 @@ export type ActionMenu = {
192
199
  * ], {anchor: button}),
193
200
  * }));
194
201
  */
195
- export declare function createMenu({ id, placement, doc }?: MenuOptions): ActionMenu;
202
+ export declare function createMenu({ id, placement, root, doc }?: MenuOptions): ActionMenu;
196
203
  export type MoreActionsOptions = {
197
204
  /**
198
205
  * The whole label, for a page that translates it. Default
package/src/ui/menu.js CHANGED
@@ -14,6 +14,8 @@
14
14
  * @module ui/menu
15
15
  */
16
16
 
17
+ import { focusedElement, rootDocument } from '../dom/focus.js';
18
+
17
19
  /** The gap kept between a menu and the window's edges, in px. */
18
20
  export const MENU_EDGE = 8;
19
21
 
@@ -103,14 +105,16 @@ export function isInside(box, x, y) {
103
105
  * @typedef {object} MenuOptions
104
106
  * @property {string} [id] The menu element's id.
105
107
  * @property {'over' | 'below'} [placement] Where it opens from a button. Default `'over'`, as Chrome's.
106
- * @property {Document} [doc] The page. Default `document`.
108
+ * @property {ParentNode} [root] Where the menu goes: an element, or a shadow root (a content
109
+ * script's, so the page's styles never reach it). Default the page's `body`.
110
+ * @property {Document} [doc] The page. Default `root`'s document, else `document`.
107
111
  */
108
112
 
109
113
  /**
110
114
  * A page's action menu.
111
115
  *
112
116
  * @typedef {object} ActionMenu
113
- * @property {HTMLDialogElement} element The `<dialog class="menu" role="menu">`, in the page's body.
117
+ * @property {HTMLDialogElement} element The `<dialog class="menu" role="menu">`, in `root`.
114
118
  * @property {(entries: readonly MenuEntry[], where: MenuPlace) => void} open Fill it with `entries`
115
119
  * and open it (closing it first if it's open).
116
120
  * @property {() => void} close Close it, if open, and return the focus.
@@ -120,7 +124,7 @@ export function isInside(box, x, y) {
120
124
  * Make a page's action menu: one `<dialog class="menu">` that any number of
121
125
  * ⋮ buttons (or right-clicks) open with their own items.
122
126
  *
123
- * @param {MenuOptions} [options] - Its id, placement and page.
127
+ * @param {MenuOptions} [options] - Its id, placement, and where it goes.
124
128
  * @returns {ActionMenu} The menu.
125
129
  * @example
126
130
  * const menu = createMenu();
@@ -132,13 +136,13 @@ export function isInside(box, x, y) {
132
136
  * ], {anchor: button}),
133
137
  * }));
134
138
  */
135
- export function createMenu({ id, placement = 'over', doc = document } = {}) {
139
+ export function createMenu({ id, placement = 'over', root, doc = rootDocument(root) } = {}) {
136
140
  const view = /** @type {Window & typeof globalThis} */ (doc.defaultView);
137
141
  const menu = doc.createElement('dialog');
138
142
  menu.className = 'menu';
139
143
  menu.setAttribute('role', 'menu');
140
144
  if (id) menu.id = id;
141
- doc.body.append(menu);
145
+ (root ?? doc.body).append(menu);
142
146
 
143
147
  /** @type {HTMLElement | null} */
144
148
  let returnFocus = null;
@@ -164,7 +168,7 @@ export function createMenu({ id, placement = 'over', doc = document } = {}) {
164
168
 
165
169
  menu.addEventListener('keydown', (event) => {
166
170
  const list = items();
167
- const at = list.indexOf(/** @type {HTMLButtonElement} */ (doc.activeElement));
171
+ const at = list.indexOf(/** @type {HTMLButtonElement} */ (focusedElement(doc)));
168
172
  /** @param {number} index */
169
173
  const go = (index) => {
170
174
  event.preventDefault();
@@ -215,7 +219,7 @@ export function createMenu({ id, placement = 'over', doc = document } = {}) {
215
219
  if (label) menu.setAttribute('aria-label', label);
216
220
  else menu.removeAttribute('aria-label');
217
221
 
218
- const active = doc.activeElement;
222
+ const active = focusedElement(doc);
219
223
  returnFocus = focusTo ?? anchor ?? (active instanceof view.HTMLElement ? active : null);
220
224
  if (anchor?.hasAttribute('aria-haspopup')) {
221
225
  anchor.setAttribute('aria-expanded', 'true');
@@ -0,0 +1,216 @@
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
+ /**
94
+ * The message key each word is read from when a page passes its `t`, so
95
+ * every extension's `_locales` names them alike. `removeLabel` and
96
+ * `moreActions` take the site as `$1` (`"More actions for $SITE$"`).
97
+ *
98
+ * @type {Readonly<Record<keyof SiteListWords, string>>}
99
+ */
100
+ export declare const SITE_LIST_MESSAGES: Readonly<Record<keyof SiteListWords, string>>;
101
+ export type SiteListOptions = {
102
+ /**
103
+ * The `<ul class="list">` to fill.
104
+ */
105
+ list: HTMLElement;
106
+ /**
107
+ * Shown while the list is empty.
108
+ */
109
+ empty?: HTMLElement;
110
+ /**
111
+ * An add row: a form with one text field and a submit button.
112
+ */
113
+ form?: HTMLFormElement;
114
+ /**
115
+ * Or an Add button that asks in the dialog; each row then has a
116
+ * ⋮ menu (Edit, Remove) instead of a Remove button.
117
+ */
118
+ addButton?: HTMLElement;
119
+ /**
120
+ * What was typed as the site the list keeps
121
+ * (`siteHost`, `siteOrigin`), or null when it isn't one.
122
+ */
123
+ parse: (text: string) => string | null;
124
+ /**
125
+ * Adds a parsed site (asking Chrome for access first, if
126
+ * the list needs it). Resolve to false to keep the text in the add row's field (access refused).
127
+ */
128
+ add: (site: string) => unknown;
129
+ /**
130
+ * Removes a site.
131
+ */
132
+ remove: (site: string) => unknown;
133
+ /**
134
+ * Puts an edited site in place of
135
+ * the old one (the ⋮ menu's Edit; without it, the menu has only Remove).
136
+ */
137
+ replace?: (site: string, edited: string) => unknown;
138
+ /**
139
+ * The add row: called with what was typed when it isn't a site.
140
+ */
141
+ invalid?: (text: string) => void;
142
+ /**
143
+ * How a site reads. Default {@link siteLabel}.
144
+ */
145
+ label?: (site: string) => string;
146
+ /**
147
+ * The dialog field's `maxlength`. Default {@link MAX_HOST_LENGTH}.
148
+ */
149
+ maxLength?: number;
150
+ /**
151
+ * The page's message lookup
152
+ * (`setUpPage`'s): the words are then its messages under {@link SITE_LIST_MESSAGES}. Without it,
153
+ * Chrome's English ({@link SITE_LIST_WORDS}).
154
+ */
155
+ t?: (key: string, substitutions?: string | string[]) => string;
156
+ /**
157
+ * Words that differ from those, by name.
158
+ */
159
+ words?: Partial<SiteListWords>;
160
+ };
161
+ export type SiteList = {
162
+ /**
163
+ * Show these sites (call it whenever they change).
164
+ */
165
+ render: (sites: readonly string[]) => void;
166
+ };
167
+ /**
168
+ * What {@link createSiteList} takes: the list, one way to add to it, and
169
+ * what adding, removing and editing do.
170
+ *
171
+ * @typedef {object} SiteListOptions
172
+ * @property {HTMLElement} list The `<ul class="list">` to fill.
173
+ * @property {HTMLElement} [empty] Shown while the list is empty.
174
+ * @property {HTMLFormElement} [form] An add row: a form with one text field and a submit button.
175
+ * @property {HTMLElement} [addButton] Or an Add button that asks in the dialog; each row then has a
176
+ * ⋮ menu (Edit, Remove) instead of a Remove button.
177
+ * @property {(text: string) => string | null} parse What was typed as the site the list keeps
178
+ * (`siteHost`, `siteOrigin`), or null when it isn't one.
179
+ * @property {(site: string) => unknown} add Adds a parsed site (asking Chrome for access first, if
180
+ * the list needs it). Resolve to false to keep the text in the add row's field (access refused).
181
+ * @property {(site: string) => unknown} remove Removes a site.
182
+ * @property {(site: string, edited: string) => unknown} [replace] Puts an edited site in place of
183
+ * the old one (the ⋮ menu's Edit; without it, the menu has only Remove).
184
+ * @property {(text: string) => void} [invalid] The add row: called with what was typed when it isn't a site.
185
+ * @property {(site: string) => string} [label] How a site reads. Default {@link siteLabel}.
186
+ * @property {number} [maxLength] The dialog field's `maxlength`. Default {@link MAX_HOST_LENGTH}.
187
+ * @property {(key: string, substitutions?: string | string[]) => string} [t] The page's message lookup
188
+ * (`setUpPage`'s): the words are then its messages under {@link SITE_LIST_MESSAGES}. Without it,
189
+ * Chrome's English ({@link SITE_LIST_WORDS}).
190
+ * @property {Partial<SiteListWords>} [words] Words that differ from those, by name.
191
+ */
192
+ /**
193
+ * A site list.
194
+ *
195
+ * @typedef {object} SiteList
196
+ * @property {(sites: readonly string[]) => void} render Show these sites (call it whenever they change).
197
+ */
198
+ /**
199
+ * Fill a site list and wire its way of adding: an add row (typing a site and
200
+ * pressing Enter or Add) or an Add button (the dialog, which keeps its Add off
201
+ * until what's typed is a site). A row's Remove (or its ⋮ menu's Remove and
202
+ * Edit) hands its site to `remove` (or `replace`).
203
+ *
204
+ * @param {SiteListOptions} options - The markup, the parser and what adding and removing do.
205
+ * @returns {SiteList} The list.
206
+ * @example
207
+ * const never = createSiteList({
208
+ * list: byId('never-sites'), form: byId('add-never', HTMLFormElement), empty: byId('no-sites'),
209
+ * parse: siteHost,
210
+ * add: (host) => save({ neverSites: withSite(settings.neverSites, host) }),
211
+ * remove: (host) => save({ neverSites: withoutSite(settings.neverSites, host) }),
212
+ * t, // its words from optionsAdd, optionsRemove, …
213
+ * });
214
+ * never.render(settings.neverSites);
215
+ */
216
+ export declare function createSiteList({ list, empty, form, addButton, parse, add, remove, replace, invalid, label, maxLength, t, words, }: SiteListOptions): SiteList;