@arraypress/extension-kit 1.3.0 → 1.5.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.
package/README.md CHANGED
@@ -10,8 +10,8 @@ The pieces ArrayPress's Chrome extensions share, so each is written once:
10
10
  service worker shows the matching icon.
11
11
  - **About links**: README, Issues and PRIVACY.md in the extension's GitHub repository, plus
12
12
  package-time warnings for placeholder links.
13
- - **Chrome's page components**: the ⋮ action menu and its row button, the one-field dialog
14
- ("Rename folder"), the toast with Undo and a settings card's site list, built on the
13
+ - **Chrome's page components**: the ⋮ action menu and its row button, the text dialog
14
+ ("Rename folder", or several fields as "Add site search"), the toast with Undo and a settings card's site list, built on the
15
15
  stylesheet's classes; they work in a content script's shadow root too.
16
16
  - **Sites**: what people type into a per-site list, and whether a page is on one.
17
17
  - **Build and test helpers** (Node): icon rendering, vendoring npm packages (with a staleness
@@ -159,13 +159,25 @@ be public before the store listing goes live.
159
159
  ## Page start-up
160
160
 
161
161
  ```js
162
- const t = setUpPage(chrome, { title: 'optionsTitle', substitutions: [name] });
162
+ const t = setUpPage(chrome, { title: 'optionsTitle' });
163
163
  ```
164
164
 
165
165
  Reports the page's colour scheme for the toolbar glyph (`watchColorScheme`; a failure is only
166
166
  logged), fills its `data-i18n` text (`localize`), sets its title from a message, and returns the
167
167
  page's `t`.
168
168
 
169
+ The extension's name lives in one message, `extShortName`. A message that names the extension
170
+ takes it as a placeholder whose content is `$1`, and every lookup without substitutions (`t(key)`,
171
+ `data-i18n`, `data-i18n-*`, the title) fills it in:
172
+
173
+ ```json
174
+ "optionsAbout": { "message": "About $NAME$", "placeholders": { "name": { "content": "$1" } } }
175
+ ```
176
+
177
+ ```html
178
+ <h2 class="section-title" data-i18n="optionsAbout"></h2> <!-- About Upright -->
179
+ ```
180
+
169
181
  ## Sites
170
182
 
171
183
  ```js
@@ -175,11 +187,22 @@ onSite(siteHost(location.href), neverSites); // on a listed site or a subdomain
175
187
  hostPatterns('github.com'); // ['*://github.com/*', '*://*.github.com/*'] (excludeMatches)
176
188
  originPattern('https://bit.ly/x'); // 'https://bit.ly/*' (permissions.request)
177
189
  siteLabel('https://example.com'); // 'example.com', as a list shows it
190
+ withSite(['a.com', 'b.com'], 'a.com'); // ['b.com', 'a.com'], what a list keeps after Add (also withoutSite, withSiteReplaced)
178
191
  ```
179
192
 
180
193
  A site is http or https with a dot in its host (or `localhost`); text without a scheme is https;
181
194
  `localhost:8080` is a host and port. Pure, so a content script can load `sites/sites.js` alone.
182
195
 
196
+ ## Time limits
197
+
198
+ ```js
199
+ const groups = await within(chrome.tabGroups.query({}), 2000, []); // a Chrome call that may never answer
200
+ ```
201
+
202
+ `within(promise, ms, fallback)`: the value in time, or the fallback (late or failed); never
203
+ rejects; its timer cleared once the work settles (the work itself isn't stopped). In the runtime
204
+ half, so a service worker or page can use it; the test helpers use it too.
205
+
183
206
  ## Page components
184
207
 
185
208
  Chrome's own components, made in the page by script (no markup to copy), styled by `chrome-ui.css`.
@@ -215,7 +238,7 @@ opens it below the button instead. `menuPosition` and `isInside` are the pure ge
215
238
  `tooltip` (a `title`) and `focusable: false` (out of the tab order, for a row that takes the focus)
216
239
  cover the rest.
217
240
 
218
- ### One-field dialog (cr-dialog)
241
+ ### Text dialog (cr-dialog)
219
242
 
220
243
  ```js
221
244
  const name = await askText({ title: 'Rename workspace', label: 'Name', value: workspace.name, maxLength: 200 });
@@ -231,6 +254,26 @@ disabled while the text is blank or `validate` gives a reason, which shows under
231
254
  focus goes back to what had it, or to `returnTo` (an element, or a function asked at the end).
232
255
  One `#text-dialog` per page, made on first use.
233
256
 
257
+ ### Dialog with several fields
258
+
259
+ ```js
260
+ const rule = await askFields({
261
+ title: 'Add folder rule', action: 'Add',
262
+ fields: [
263
+ { name: 'from', label: 'Downloads from', placeholder: 'example.com or .pdf', validate: checkFrom },
264
+ { name: 'folder', label: 'Folder', value: 'Receipts' },
265
+ ],
266
+ });
267
+ if (rule) addRule(rule.from, rule.folder);
268
+ ```
269
+
270
+ The same dialog as chrome://settings/searchEngines asks for a site search: a field each, in
271
+ order. Resolves to each field's text by its `name`, trimmed, or `null` when cancelled. The action
272
+ button stays disabled until every field is filled in and valid, each field's reason under it; the
273
+ focus starts in the first empty field. `askText` is this with one field. A one-field dialog's ids
274
+ are `text-dialog-input` and `text-dialog-error`; with several, `text-dialog-<name>` and
275
+ `text-dialog-<name>-error` (`fieldIds`), for a test to find them.
276
+
234
277
  ### Toast with Undo (cr-toast)
235
278
 
236
279
  ```js
@@ -247,25 +290,50 @@ restarts the wait: 10 seconds, as chrome://bookmarks's (`TOAST_MS`), or `duratio
247
290
  ### Site list
248
291
 
249
292
  ```js
250
- // A short list with an add row (chrome://settings), the markup in the page:
251
- // <ul class="list" id="sites"></ul> <p class="row text" id="no-sites">…</p>
252
- // <form class="row add" id="add-site"><input type="text" …><button type="submit">Add</button></form>
293
+ // Memory Saver's way (chrome://settings/performance): an Add button and the dialog, ⋮ Edit/Remove.
294
+ // <div class="row"><span class="label secondary">…</span><button id="add-site" type="button">Add</button></div>
295
+ // <p class="row text" id="no-sites"><span class="label">No sites added</span></p> <ul class="list" id="sites"></ul>
253
296
  const sites = createSiteList({
254
- list: byId('sites'), empty: byId('no-sites'), form: byId('add-site', HTMLFormElement),
255
- parse: siteOrigin,
256
- add: async (origin) => (await chrome.permissions.request({ origins: [originPattern(origin)] })) && save(origin),
257
- remove: (origin) => unsave(origin),
258
- words: { remove: t('optionsRemove'), removeLabel: (site) => t('optionsRemoveLabel', [site]) },
297
+ list: byId('sites'), empty: byId('no-sites'), addButton: byId('add-site'),
298
+ parse: siteHost,
299
+ add: (site) => save({ sites: withSite(settings.sites, site) }),
300
+ remove: (site) => save({ sites: withoutSite(settings.sites, site) }),
301
+ replace: (site, edited) => save({ sites: withSiteReplaced(settings.sites, site, edited) }),
302
+ t, // the page's messages, under the standard keys below
259
303
  });
260
304
  sites.render(settings.sites);
261
305
 
262
- // Memory Saver's way (chrome://settings/performance): an Add button and the dialog, ⋮ Edit/Remove.
263
- createSiteList({ list, empty, addButton: byId('add-site'), parse: siteHost, add, remove, replace: (old, edited) => … });
306
+ // A short list with an add row (chrome://settings): `form` instead of `addButton`.
307
+ // <form class="row add" id="add-site"><input type="text" …><button type="submit">Add</button></form>
264
308
  ```
265
309
 
266
- `add` resolving to false keeps the typed text (access refused); `invalid(text)` hears what isn't
267
- a site in the add row; the dialog keeps its Add off with “Enter a site, like example.com” under
268
- the field. `words` translates `SITE_LIST_WORDS`.
310
+ `withSite` adds a site at the end, once (added again, it moves there); `withoutSite` takes it out;
311
+ `withSiteReplaced` edits it in place, merging it with a duplicate. A list that needs site access
312
+ asks for it in `add`/`replace` (the dialog's click still counts as the person's) and gives it back
313
+ in `remove`. `add` resolving to false keeps the add row's text (access refused); `invalid(text)`
314
+ hears what isn't a site in the add row; the dialog keeps its Add off with “Enter a site, like
315
+ example.com” under the field.
316
+
317
+ The words: Chrome's English (`SITE_LIST_WORDS`) without `t`; with it, the page's messages under
318
+ these keys (`SITE_LIST_MESSAGES`), so every extension's `_locales` names them alike. `words`
319
+ overrides only those that differ.
320
+
321
+ | Word | Key | English |
322
+ |---|---|---|
323
+ | `add` | `optionsAdd` | Add |
324
+ | `addSite` | `optionsAddSite` | Add site |
325
+ | `editSite` | `optionsEditSite` | Edit site |
326
+ | `edit` | `optionsEdit` | Edit |
327
+ | `remove` | `optionsRemove` | Remove |
328
+ | `save` | `optionsSave` | Save |
329
+ | `cancel` | `optionsCancel` | Cancel |
330
+ | `site` | `optionsSiteLabel` | Site |
331
+ | `invalid` | `optionsSiteInvalid` | Enter a site, like example.com |
332
+ | `moreActions` | `optionsMoreActions` | More actions for `$SITE$` (content `$1`; the ⋮ button) |
333
+ | `removeLabel` | `optionsRemoveLabel` | Remove `$SITE$` (content `$1`; the add row's Remove button) |
334
+
335
+ A list with an Add button needs every key but `optionsRemoveLabel`; one with an add row needs
336
+ `optionsRemove` and `optionsRemoveLabel`. A locale test can check them against `SITE_LIST_MESSAGES`.
269
337
 
270
338
  ## Build helpers (`@arraypress/extension-kit/build`)
271
339
 
@@ -300,7 +368,6 @@ except those listed, each with a reason:
300
368
  | `launchExtension(chromium, dir, options)` | Load an unpacked extension into a fresh profile; `{context, worker, extensionId, url(page), profile, close()}`. |
301
369
  | `extensionProfile(chromium, dir, options)` | A profile to start the browser on more than once (a restart, a first start with a changed manifest): `{dir, launch(), remove()}`. |
302
370
  | `checkRunner({timeout})` | Named checks that report and go on: `check(name, fn, {timeout})`, `fail(name)` for an error found outside a check, `finish()`. A check that runs past its limit (`CHECK_MS`, 60 s) fails, and the run goes on. |
303
- | `within(promise, ms, fallback)` | The value in time, or the fallback (late or failed); never rejects; its timer cleared. |
304
371
  | `eachLimited(items, limit, work)` | Work through a list at most `limit` at once, `work(item, slot, index)`. |
305
372
  | `realisticBrowser(options)`, `chromeUserAgent(browser)` | Launch options and a user agent for a browser that doesn't say it's automated, for live checks. |
306
373
  | `closeOnSignal(close)` | On Ctrl-C or a kill, close the run's browsers, then exit. |
@@ -340,7 +407,7 @@ except those listed, each with a reason:
340
407
  | Manager layout | `.manager`/`.layout`, `.sidebar` with `.tree` > `.tree-item`, `.main` with `.list-card` |
341
408
  | Menu (cr-action-menu) | `<dialog class="menu">` of `button[role="menuitem"]`, `hr` (or `createMenu`) |
342
409
  | ⋮ icon | `<span class="icon icon-more-vert" aria-hidden="true">` (or `moreActionsButton`) |
343
- | Dialog (cr-dialog) | `<dialog class="dialog"><form method="dialog">` `.dialog-title`, `.dialog-body`, `.dialog-buttons` (one field: `askText`) |
410
+ | Dialog (cr-dialog) | `<dialog class="dialog"><form method="dialog">` `.dialog-title`, `.dialog-body`, `.dialog-buttons` (`askText`, `askFields`) |
344
411
  | Toast (cr-toast) | `<div class="toast" popover="manual">text <button>Undo</button></div>` (or `createToast`); with `hidden` instead of `popover`, it shows over the page but not the top layer |
345
412
  | Drawer (cr-drawer) | `<dialog class="drawer">` with `.drawer-head` and a `.nav` |
346
413
  | Side panel page | `<body class="side-panel">`: the panel's colours, a column 100vh high |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arraypress/extension-kit",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Shared pieces for Chrome extensions: Chrome's own page styles, a Chrome Sync settings store, the light/dark toolbar glyph, About links, and build and test helpers.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -28,7 +28,7 @@ const SOURCE = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
28
28
  const KIT = '@arraypress/extension-kit';
29
29
 
30
30
  /** What a vendored copy of the kit holds, relative to `src/`: everything an extension page or worker may load. */
31
- export const RUNTIME_ENTRIES = Object.freeze(['index.js', 'index.d.ts', 'about', 'commands', 'dom', 'settings', 'sites', 'styles', 'toolbar', 'ui']);
31
+ export const RUNTIME_ENTRIES = Object.freeze(['index.js', 'index.d.ts', 'about', 'commands', 'dom', 'settings', 'sites', 'styles', 'time', 'toolbar', 'ui']);
32
32
 
33
33
  /** The kit's stylesheet, an absolute path in the installed package. */
34
34
  export const STYLESHEET = path.join(SOURCE, 'styles', 'chrome-ui.css');
package/src/dom/i18n.d.ts CHANGED
@@ -4,6 +4,11 @@
4
4
  * `data-i18n-<attribute>="key"` (sets that attribute, e.g.
5
5
  * `data-i18n-placeholder`, `data-i18n-aria-label`).
6
6
  *
7
+ * The extension's name lives in one message, `extShortName`; a message that
8
+ * names the extension takes it as a placeholder whose content is `$1`
9
+ * (`"About $NAME$"`), and every lookup made without substitutions fills it
10
+ * in, in markup and script alike.
11
+ *
7
12
  * @module dom/i18n
8
13
  */
9
14
  export type I18nApi = {
@@ -19,18 +24,21 @@ export type I18nApi = {
19
24
  */
20
25
  /**
21
26
  * A message lookup that never comes back blank: a missing key shows as
22
- * itself, so a typo is visible on screen instead of an empty label.
27
+ * itself, so a typo is visible on screen instead of an empty label. Without
28
+ * substitutions, `$1` is the extension's name (`extShortName`).
23
29
  *
24
30
  * @param {Pick<I18nApi, 'getMessage'>} i18n - `chrome.i18n`.
25
31
  * @returns {(key: string, substitutions?: string | string[]) => string} Looks a key up.
26
32
  * @example
27
33
  * const t = translator(chrome.i18n);
28
34
  * t('optionsVersion', ['1.0.0']); // 'Version 1.0.0'
35
+ * t('optionsAbout'); // 'About Upright', from "About $NAME$"
29
36
  */
30
37
  export declare function translator(i18n: Pick<I18nApi, 'getMessage'>): (key: string, substitutions?: string | string[]) => string;
31
38
  /**
32
- * Fill every `data-i18n*` attribute under `root`, and, for a whole document,
33
- * set its `lang` to Chrome's UI language.
39
+ * Fill every `data-i18n*` attribute under `root` (a message that names the
40
+ * extension gets its name), and, for a whole document, set its `lang` to
41
+ * Chrome's UI language.
34
42
  *
35
43
  * @param {ParentNode} root - A document or an element.
36
44
  * @param {I18nApi} i18n - `chrome.i18n`.
package/src/dom/i18n.js CHANGED
@@ -4,9 +4,17 @@
4
4
  * `data-i18n-<attribute>="key"` (sets that attribute, e.g.
5
5
  * `data-i18n-placeholder`, `data-i18n-aria-label`).
6
6
  *
7
+ * The extension's name lives in one message, `extShortName`; a message that
8
+ * names the extension takes it as a placeholder whose content is `$1`
9
+ * (`"About $NAME$"`), and every lookup made without substitutions fills it
10
+ * in, in markup and script alike.
11
+ *
7
12
  * @module dom/i18n
8
13
  */
9
14
 
15
+ /** The message that holds the extension's name. */
16
+ const NAME_MESSAGE = 'extShortName';
17
+
10
18
  /**
11
19
  * `chrome.i18n`, as far as pages need it.
12
20
  *
@@ -17,21 +25,26 @@
17
25
 
18
26
  /**
19
27
  * A message lookup that never comes back blank: a missing key shows as
20
- * itself, so a typo is visible on screen instead of an empty label.
28
+ * itself, so a typo is visible on screen instead of an empty label. Without
29
+ * substitutions, `$1` is the extension's name (`extShortName`).
21
30
  *
22
31
  * @param {Pick<I18nApi, 'getMessage'>} i18n - `chrome.i18n`.
23
32
  * @returns {(key: string, substitutions?: string | string[]) => string} Looks a key up.
24
33
  * @example
25
34
  * const t = translator(chrome.i18n);
26
35
  * t('optionsVersion', ['1.0.0']); // 'Version 1.0.0'
36
+ * t('optionsAbout'); // 'About Upright', from "About $NAME$"
27
37
  */
28
38
  export function translator(i18n) {
29
- return (key, substitutions) => i18n.getMessage(key, substitutions) || key;
39
+ const name = i18n.getMessage(NAME_MESSAGE);
40
+ const named = name ? [name] : undefined;
41
+ return (key, substitutions = named) => i18n.getMessage(key, substitutions) || key;
30
42
  }
31
43
 
32
44
  /**
33
- * Fill every `data-i18n*` attribute under `root`, and, for a whole document,
34
- * set its `lang` to Chrome's UI language.
45
+ * Fill every `data-i18n*` attribute under `root` (a message that names the
46
+ * extension gets its name), and, for a whole document, set its `lang` to
47
+ * Chrome's UI language.
35
48
  *
36
49
  * @param {ParentNode} root - A document or an element.
37
50
  * @param {I18nApi} i18n - `chrome.i18n`.
@@ -13,7 +13,7 @@ export type PageSetup = {
13
13
  */
14
14
  title?: string;
15
15
  /**
16
- * The title's placeholders (the extension's name).
16
+ * The title's placeholders. Default: the extension's name (`translator`).
17
17
  */
18
18
  substitutions?: string | string[];
19
19
  /**
@@ -32,7 +32,7 @@ export type PageSetup = {
32
32
  *
33
33
  * @typedef {object} PageSetup
34
34
  * @property {string} [title] The title's message (“optionsTitle”). Default: the title is left as it is.
35
- * @property {string | string[]} [substitutions] The title's placeholders (the extension's name).
35
+ * @property {string | string[]} [substitutions] The title's placeholders. Default: the extension's name (`translator`).
36
36
  * @property {Document} [doc] The page. Default `document`.
37
37
  * @property {SchemeMedia} [media] The scheme to report. Default the page's `matchMedia(DARK_QUERY)`.
38
38
  */
@@ -45,7 +45,8 @@ export type PageSetup = {
45
45
  * @returns {(key: string, substitutions?: string | string[]) => string} The page's message lookup
46
46
  * (`translator(chrome.i18n)`).
47
47
  * @example
48
- * const t = setUpPage(chrome, { title: 'optionsTitle', substitutions: [t('extShortName')] });
48
+ * const t = setUpPage(chrome, { title: 'optionsTitle' }); // "$NAME$ settings": 'Upright settings'
49
+ * t('optionsGetHelp'); // "Get help with $NAME$": 'Get help with Upright'
49
50
  * // The popup, whose title Chrome doesn't show:
50
51
  * const t = setUpPage(chrome);
51
52
  */
package/src/dom/setup.js CHANGED
@@ -17,7 +17,7 @@ import { DARK_QUERY, watchColorScheme } from '../toolbar/scheme.js';
17
17
  *
18
18
  * @typedef {object} PageSetup
19
19
  * @property {string} [title] The title's message (“optionsTitle”). Default: the title is left as it is.
20
- * @property {string | string[]} [substitutions] The title's placeholders (the extension's name).
20
+ * @property {string | string[]} [substitutions] The title's placeholders. Default: the extension's name (`translator`).
21
21
  * @property {Document} [doc] The page. Default `document`.
22
22
  * @property {SchemeMedia} [media] The scheme to report. Default the page's `matchMedia(DARK_QUERY)`.
23
23
  */
@@ -31,7 +31,8 @@ import { DARK_QUERY, watchColorScheme } from '../toolbar/scheme.js';
31
31
  * @returns {(key: string, substitutions?: string | string[]) => string} The page's message lookup
32
32
  * (`translator(chrome.i18n)`).
33
33
  * @example
34
- * const t = setUpPage(chrome, { title: 'optionsTitle', substitutions: [t('extShortName')] });
34
+ * const t = setUpPage(chrome, { title: 'optionsTitle' }); // "$NAME$ settings": 'Upright settings'
35
+ * t('optionsGetHelp'); // "Get help with $NAME$": 'Get help with Upright'
35
36
  * // The popup, whose title Chrome doesn't show:
36
37
  * const t = setUpPage(chrome);
37
38
  */
package/src/index.d.ts CHANGED
@@ -20,19 +20,22 @@ export { setUpPage } from './dom/setup.js';
20
20
  export { createSettingsSaver } from './settings/saver.js';
21
21
  export { MAX_SYNC_ITEM_BYTES } from './settings/storage.js';
22
22
  export { createSettingsStore } from './settings/store.js';
23
- export { MAX_HOST_LENGTH, hostPatterns, onSite, originPattern, siteHost, siteLabel, siteOrigin } from './sites/sites.js';
23
+ export { MAX_HOST_LENGTH, hostPatterns, onSite, originPattern, siteHost, siteLabel, siteOrigin, withSite, withSiteReplaced, withoutSite, } from './sites/sites.js';
24
+ export { within } from './time/time.js';
24
25
  export { POLL_MS, watchSchemeOffscreen } from './toolbar/offscreen.js';
25
26
  export { DARK_QUERY, TOOLBAR_DARK_KEY, TOOLBAR_SCHEME, reportColorScheme, schemeSender, watchColorScheme } from './toolbar/scheme.js';
26
27
  export { OFFSCREEN_PATH, TOOLBAR_ICON_DIR, createToolbarTheme, startToolbarTheme, toolbarIcon, watchFromOffscreen, } from './toolbar/theme.js';
27
28
  export { MENU_EDGE, createMenu, isInside, menuPosition, moreActionsButton } from './ui/menu.js';
28
- export { createSiteList } from './ui/site-list.js';
29
- export { askText, checkText } from './ui/text-dialog.js';
29
+ export { SITE_LIST_MESSAGES, SITE_LIST_WORDS, createSiteList } from './ui/site-list.js';
30
+ export { askFields, askText, checkText, fieldIds } from './ui/text-dialog.js';
30
31
  export { TOAST_MS, createToast } from './ui/toast.js';
31
32
  export type ActionMenu = import('./ui/menu.js').ActionMenu;
32
33
  export type MenuEntry = import('./ui/menu.js').MenuEntry;
33
34
  export type MenuItem = import('./ui/menu.js').MenuItem;
34
35
  export type MenuPlace = import('./ui/menu.js').MenuPlace;
35
36
  export type SiteList = import('./ui/site-list.js').SiteList;
37
+ export type FieldRequest = import('./ui/text-dialog.js').FieldRequest;
38
+ export type FieldsRequest = import('./ui/text-dialog.js').FieldsRequest;
36
39
  export type TextRequest = import('./ui/text-dialog.js').TextRequest;
37
40
  export type Toast = import('./ui/toast.js').Toast;
38
41
  /**
@@ -43,6 +46,8 @@ export type Toast = import('./ui/toast.js').Toast;
43
46
  * @typedef {import('./ui/menu.js').MenuItem} MenuItem
44
47
  * @typedef {import('./ui/menu.js').MenuPlace} MenuPlace
45
48
  * @typedef {import('./ui/site-list.js').SiteList} SiteList
49
+ * @typedef {import('./ui/text-dialog.js').FieldRequest} FieldRequest
50
+ * @typedef {import('./ui/text-dialog.js').FieldsRequest} FieldsRequest
46
51
  * @typedef {import('./ui/text-dialog.js').TextRequest} TextRequest
47
52
  * @typedef {import('./ui/toast.js').Toast} Toast
48
53
  */
package/src/index.js CHANGED
@@ -21,15 +21,18 @@ export { setUpPage } from './dom/setup.js';
21
21
  export { createSettingsSaver } from './settings/saver.js';
22
22
  export { MAX_SYNC_ITEM_BYTES } from './settings/storage.js';
23
23
  export { createSettingsStore } from './settings/store.js';
24
- export { MAX_HOST_LENGTH, hostPatterns, onSite, originPattern, siteHost, siteLabel, siteOrigin } from './sites/sites.js';
24
+ export {
25
+ MAX_HOST_LENGTH, hostPatterns, onSite, originPattern, siteHost, siteLabel, siteOrigin, withSite, withSiteReplaced, withoutSite,
26
+ } from './sites/sites.js';
27
+ export { within } from './time/time.js';
25
28
  export { POLL_MS, watchSchemeOffscreen } from './toolbar/offscreen.js';
26
29
  export { DARK_QUERY, TOOLBAR_DARK_KEY, TOOLBAR_SCHEME, reportColorScheme, schemeSender, watchColorScheme } from './toolbar/scheme.js';
27
30
  export {
28
31
  OFFSCREEN_PATH, TOOLBAR_ICON_DIR, createToolbarTheme, startToolbarTheme, toolbarIcon, watchFromOffscreen,
29
32
  } from './toolbar/theme.js';
30
33
  export { MENU_EDGE, createMenu, isInside, menuPosition, moreActionsButton } from './ui/menu.js';
31
- export { createSiteList } from './ui/site-list.js';
32
- export { askText, checkText } from './ui/text-dialog.js';
34
+ export { SITE_LIST_MESSAGES, SITE_LIST_WORDS, createSiteList } from './ui/site-list.js';
35
+ export { askFields, askText, checkText, fieldIds } from './ui/text-dialog.js';
33
36
  export { TOAST_MS, createToast } from './ui/toast.js';
34
37
 
35
38
  /**
@@ -40,6 +43,8 @@ export { TOAST_MS, createToast } from './ui/toast.js';
40
43
  * @typedef {import('./ui/menu.js').MenuItem} MenuItem
41
44
  * @typedef {import('./ui/menu.js').MenuPlace} MenuPlace
42
45
  * @typedef {import('./ui/site-list.js').SiteList} SiteList
46
+ * @typedef {import('./ui/text-dialog.js').FieldRequest} FieldRequest
47
+ * @typedef {import('./ui/text-dialog.js').FieldsRequest} FieldsRequest
43
48
  * @typedef {import('./ui/text-dialog.js').TextRequest} TextRequest
44
49
  * @typedef {import('./ui/toast.js').Toast} Toast
45
50
  */
@@ -3,7 +3,8 @@
3
3
  * "https://www.Example.com/shop") and as pages report them (`location.href`):
4
4
  * the origin an extension asks Chrome for access to, the host a per-site
5
5
  * on/off list keeps, whether a page is on a listed site (subdomains
6
- * included), and the match patterns for both. Pure.
6
+ * included), the match patterns for both, and a list with a site added,
7
+ * taken out or edited. Pure.
7
8
  *
8
9
  * A site is a web host: http or https, with a dot in its name (or
9
10
  * `localhost`). Text without a scheme is read as https.
@@ -80,3 +81,34 @@ export declare function originPattern(url: string): string;
80
81
  * siteLabel('http://example.com'); // 'http://example.com'
81
82
  */
82
83
  export declare function siteLabel(site: string): string;
84
+ /**
85
+ * A site list with `site` added: at the end, once (a site added again moves
86
+ * there).
87
+ *
88
+ * @param {readonly string[]} sites - The list.
89
+ * @param {string} site - The site to add.
90
+ * @returns {string[]} A new list.
91
+ * @example
92
+ * withSite(['a.com', 'b.com'], 'a.com'); // ['b.com', 'a.com']
93
+ */
94
+ export declare function withSite(sites: readonly string[], site: string): string[];
95
+ /**
96
+ * A site list without `site`.
97
+ *
98
+ * @param {readonly string[]} sites - The list.
99
+ * @param {string} site - The site to take out.
100
+ * @returns {string[]} A new list.
101
+ */
102
+ export declare function withoutSite(sites: readonly string[], site: string): string[];
103
+ /**
104
+ * A site list with `site` edited to `edited`, in its place, once (an edit
105
+ * to a site already listed merges the two).
106
+ *
107
+ * @param {readonly string[]} sites - The list.
108
+ * @param {string} site - The site as it was.
109
+ * @param {string} edited - What it was edited to.
110
+ * @returns {string[]} A new list.
111
+ * @example
112
+ * withSiteReplaced(['a.com', 'b.com'], 'a.com', 'b.com'); // ['b.com']
113
+ */
114
+ export declare function withSiteReplaced(sites: readonly string[], site: string, edited: string): string[];
@@ -3,7 +3,8 @@
3
3
  * "https://www.Example.com/shop") and as pages report them (`location.href`):
4
4
  * the origin an extension asks Chrome for access to, the host a per-site
5
5
  * on/off list keeps, whether a page is on a listed site (subdomains
6
- * included), and the match patterns for both. Pure.
6
+ * included), the match patterns for both, and a list with a site added,
7
+ * taken out or edited. Pure.
7
8
  *
8
9
  * A site is a web host: http or https, with a dot in its name (or
9
10
  * `localhost`). Text without a scheme is read as https.
@@ -126,3 +127,43 @@ export function originPattern(url) {
126
127
  export function siteLabel(site) {
127
128
  return site.replace(/^https:\/\//, '');
128
129
  }
130
+
131
+ /**
132
+ * A site list with `site` added: at the end, once (a site added again moves
133
+ * there).
134
+ *
135
+ * @param {readonly string[]} sites - The list.
136
+ * @param {string} site - The site to add.
137
+ * @returns {string[]} A new list.
138
+ * @example
139
+ * withSite(['a.com', 'b.com'], 'a.com'); // ['b.com', 'a.com']
140
+ */
141
+ export function withSite(sites, site) {
142
+ return [...withoutSite(sites, site), site];
143
+ }
144
+
145
+ /**
146
+ * A site list without `site`.
147
+ *
148
+ * @param {readonly string[]} sites - The list.
149
+ * @param {string} site - The site to take out.
150
+ * @returns {string[]} A new list.
151
+ */
152
+ export function withoutSite(sites, site) {
153
+ return sites.filter((s) => s !== site);
154
+ }
155
+
156
+ /**
157
+ * A site list with `site` edited to `edited`, in its place, once (an edit
158
+ * to a site already listed merges the two).
159
+ *
160
+ * @param {readonly string[]} sites - The list.
161
+ * @param {string} site - The site as it was.
162
+ * @param {string} edited - What it was edited to.
163
+ * @returns {string[]} A new list.
164
+ * @example
165
+ * withSiteReplaced(['a.com', 'b.com'], 'a.com', 'b.com'); // ['b.com']
166
+ */
167
+ export function withSiteReplaced(sites, site, edited) {
168
+ return [...new Set(sites.map((s) => (s === site ? edited : s)))];
169
+ }
@@ -8,7 +8,7 @@
8
8
  * @module testing/browser
9
9
  */
10
10
 
11
- import { within } from './time.js';
11
+ import { within } from '../time/time.js';
12
12
 
13
13
  /** @import { Browser } from 'playwright' */
14
14
 
@@ -12,7 +12,7 @@ import assert from 'node:assert/strict';
12
12
  import { mkdtemp, rm } from 'node:fs/promises';
13
13
  import { tmpdir } from 'node:os';
14
14
  import path from 'node:path';
15
- import { deadline } from './time.js';
15
+ import { deadline } from '../time/time.js';
16
16
 
17
17
  /** @import { BrowserContext, BrowserType, CDPSession, Page, Worker } from 'playwright' */
18
18
 
@@ -10,7 +10,6 @@ export { TEST_BUILDS_ENV, testBuildDir } from '../build/test-build.js';
10
10
  export { chromeUserAgent, closeOnSignal, realisticBrowser } from './browser.js';
11
11
  export { CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, until, } from './e2e.js';
12
12
  export { eachLimited } from './pool.js';
13
- export { within } from './time.js';
14
13
  export { serve } from './server.js';
15
14
  export { SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
16
15
  export { filesUnder, manifestReferences, missingReferences, referencedPath, zipEntries } from './package.js';
@@ -13,7 +13,6 @@ export {
13
13
  CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, until,
14
14
  } from './e2e.js';
15
15
  export { eachLimited } from './pool.js';
16
- export { within } from './time.js';
17
16
  export { serve } from './server.js';
18
17
  export { SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
19
18
  export { filesUnder, manifestReferences, missingReferences, referencedPath, zipEntries } from './package.js';
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Time limits for tests that drive a real browser, where a page that stops
3
- * responding would otherwise hold a run up forever.
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).
4
5
  *
5
- * @module testing/time
6
+ * @module time/time
6
7
  */
7
8
  /**
8
9
  * Settle `promise` within `ms`, or settle the way `late` says. The timer is
@@ -19,9 +20,9 @@
19
20
  export declare function deadline<T>(promise: Promise<T>, ms: number, late: (resolve: (value: T) => void, reject: (error: Error) => void) => void): Promise<T>;
20
21
  /**
21
22
  * A promise's value within a time limit, or `fallback` when it's late or
22
- * fails: for a step a live run should give up on and go past (a renderer
23
- * that stopped responding, a close that never returns). The work itself
24
- * isn't stopped; the timer is cleared once it settles.
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.
25
26
  *
26
27
  * @template T
27
28
  * @param {Promise<T>} promise - The work.
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Time limits for tests that drive a real browser, where a page that stops
3
- * responding would otherwise hold a run up forever.
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).
4
5
  *
5
- * @module testing/time
6
+ * @module time/time
6
7
  */
7
8
 
8
9
  /**
@@ -28,9 +29,9 @@ export function deadline(promise, ms, late) {
28
29
 
29
30
  /**
30
31
  * A promise's value within a time limit, or `fallback` when it's late or
31
- * fails: for a step a live run should give up on and go past (a renderer
32
- * that stopped responding, a close that never returns). The work itself
33
- * isn't stopped; the timer is cleared once it settles.
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.
34
35
  *
35
36
  * @template T
36
37
  * @param {Promise<T>} promise - The work.