@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
package/README.md CHANGED
@@ -11,9 +11,12 @@ The pieces ArrayPress's Chrome extensions share, so each is written once:
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
13
  - **Chrome's page components**: the ⋮ action menu and its row button, the one-field dialog
14
- ("Rename folder"), and the toast with Undo, built on the stylesheet's classes.
15
- - **Build and test helpers** (Node): toolbar icon rendering, zipping for the Chrome Web Store, a
16
- fake `chrome.storage`, Playwright helpers, packaging checks, and a copied-code guard.
14
+ ("Rename folder"), the toast with Undo and a settings card's site list, built on the
15
+ stylesheet's classes; they work in a content script's shadow root too.
16
+ - **Sites**: what people type into a per-site list, and whether a page is on one.
17
+ - **Build and test helpers** (Node): icon rendering, vendoring npm packages (with a staleness
18
+ check), test builds, store screenshots, zipping for the Chrome Web Store, a fake
19
+ `chrome.storage`, Playwright helpers with time limits, packaging checks, and a copied-code guard.
17
20
 
18
21
  Zero dependencies. Every runtime function takes the `chrome` APIs it uses as arguments, so tests run
19
22
  the real logic against fakes.
@@ -52,6 +55,22 @@ import { startToolbarTheme } from '../vendor/extension-kit/index.js';
52
55
  The copy is made from the installed package every `npm install`, so it can't drift from the
53
56
  version in `package.json`. The zip helper leaves its `.d.ts` files out.
54
57
 
58
+ Other libraries are copied the same way, by name (a path ending in `/` copies a folder's contents):
59
+
60
+ ```json
61
+ {
62
+ "scripts": {
63
+ "postinstall": "extension-kit vendor extension/vendor/extension-kit && extension-kit vendor extension/vendor/clean-link @arraypress/clean-link src/",
64
+ "test": "extension-kit vendor-check && eslint . && …"
65
+ }
66
+ }
67
+ ```
68
+
69
+ `npm install some-package` doesn't run the extension's own `postinstall`, so a copy can go stale
70
+ without a word. Each copy is stamped with its package's version (`vendored.json`, left out of the
71
+ zip), and `extension-kit vendor-check` fails, naming the copy, until `npm run postinstall` remakes
72
+ it. From a script: `vendorPackage(name, files, dest)`, `checkVendored(dir)`.
73
+
55
74
  ### An extension with a bundler
56
75
 
57
76
  Import `@arraypress/extension-kit` directly and copy the stylesheet into the build from
@@ -113,14 +132,17 @@ call `createToolbarTheme({storage, action, extensionId})` in the worker and pass
113
132
  the reports, which the content script sends with `reportColorScheme(media, stored,
114
133
  schemeSender(chrome.runtime))`.
115
134
 
116
- Render the glyphs from `icons/toolbar/glyph.svg` (and optional pixel-fitted `glyph-16.svg`):
135
+ Render the glyphs from `icons/toolbar/glyph.svg` (and optional pixel-fitted `glyph-16.svg`), and
136
+ the extension's own icon from `icons/icon.svg` to `icon-16.png` … `icon-128.png`, with the
137
+ extension's Playwright:
117
138
 
118
- ```js
119
- import { chromium } from 'playwright';
120
- import { renderToolbarIcons } from '@arraypress/extension-kit/build';
121
- await renderToolbarIcons({ chromium, dir: 'extension/icons/toolbar' });
139
+ ```json
140
+ { "scripts": { "icons": "extension-kit icons", "toolbar-icons": "extension-kit toolbar-icons" } }
122
141
  ```
123
142
 
143
+ (`extension-kit toolbar-icons src/icons/toolbar` for another folder; from a script,
144
+ `renderIcons` and `renderToolbarIcons({chromium, dir})`.)
145
+
124
146
  Colours are Chrome 157's toolbar icon colours, #474747 / #C7C7C7 (sources in the file).
125
147
 
126
148
  ## About links
@@ -134,11 +156,59 @@ At the end of `npm run package`, `printReleaseChecks(ABOUT_LINKS, REPOSITORY)` w
134
156
  link is a placeholder (`isPlaceholder`: example.com/.org/.net) and reminds you the repository must
135
157
  be public before the store listing goes live.
136
158
 
159
+ ## Page start-up
160
+
161
+ ```js
162
+ const t = setUpPage(chrome, { title: 'optionsTitle' });
163
+ ```
164
+
165
+ Reports the page's colour scheme for the toolbar glyph (`watchColorScheme`; a failure is only
166
+ logged), fills its `data-i18n` text (`localize`), sets its title from a message, and returns the
167
+ page's `t`.
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
+
181
+ ## Sites
182
+
183
+ ```js
184
+ siteOrigin('example.com'); // 'https://example.com', what Chrome grants access to
185
+ siteHost(' https://www.GitHub.com/a '); // 'github.com', what a per-site list keeps
186
+ onSite(siteHost(location.href), neverSites); // on a listed site or a subdomain of one
187
+ hostPatterns('github.com'); // ['*://github.com/*', '*://*.github.com/*'] (excludeMatches)
188
+ originPattern('https://bit.ly/x'); // 'https://bit.ly/*' (permissions.request)
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)
191
+ ```
192
+
193
+ A site is http or https with a dot in its host (or `localhost`); text without a scheme is https;
194
+ `localhost:8080` is a host and port. Pure, so a content script can load `sites/sites.js` alone.
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
+
137
206
  ## Page components
138
207
 
139
208
  Chrome's own components, made in the page by script (no markup to copy), styled by `chrome-ui.css`.
140
- Each takes `doc` (default `document`); the wording defaults to Chrome's English and takes a
141
- translation.
209
+ Each takes `doc` (default `document`); the menu and the toast also take `root`, an element or a
210
+ shadow root to go in (a content script's, so the page's styles never reach them), and follow the
211
+ focus through shadow roots. The wording defaults to Chrome's English and takes a translation.
142
212
 
143
213
  ### ⋮ menu (cr-action-menu)
144
214
 
@@ -192,9 +262,58 @@ toast.show(`Deleted “${workspace.name}”`, () => restore(workspace));
192
262
  toast.show('Copied'); // no Undo
193
263
  ```
194
264
 
195
- `#toast` (`role="status"`), bottom left. A new message replaces the last and restarts the wait:
196
- 10 seconds, as chrome://bookmarks's (`TOAST_MS`), or `duration` / `undoDuration`. Undo hides it
197
- and runs once. `undoLabel` translates the button.
265
+ `#toast` (`role="status"`), bottom left, a manual `popover` so it's in the top layer, over
266
+ anything the page stacks (`:popover-open` while it shows). A new message replaces the last and
267
+ restarts the wait: 10 seconds, as chrome://bookmarks's (`TOAST_MS`), or `duration` /
268
+ `undoDuration`. Undo hides it and runs once. `undoLabel` translates the button.
269
+
270
+ ### Site list
271
+
272
+ ```js
273
+ // Memory Saver's way (chrome://settings/performance): an Add button and the dialog, ⋮ Edit/Remove.
274
+ // <div class="row"><span class="label secondary">…</span><button id="add-site" type="button">Add</button></div>
275
+ // <p class="row text" id="no-sites"><span class="label">No sites added</span></p> <ul class="list" id="sites"></ul>
276
+ const sites = createSiteList({
277
+ list: byId('sites'), empty: byId('no-sites'), addButton: byId('add-site'),
278
+ parse: siteHost,
279
+ add: (site) => save({ sites: withSite(settings.sites, site) }),
280
+ remove: (site) => save({ sites: withoutSite(settings.sites, site) }),
281
+ replace: (site, edited) => save({ sites: withSiteReplaced(settings.sites, site, edited) }),
282
+ t, // the page's messages, under the standard keys below
283
+ });
284
+ sites.render(settings.sites);
285
+
286
+ // A short list with an add row (chrome://settings): `form` instead of `addButton`.
287
+ // <form class="row add" id="add-site"><input type="text" …><button type="submit">Add</button></form>
288
+ ```
289
+
290
+ `withSite` adds a site at the end, once (added again, it moves there); `withoutSite` takes it out;
291
+ `withSiteReplaced` edits it in place, merging it with a duplicate. A list that needs site access
292
+ asks for it in `add`/`replace` (the dialog's click still counts as the person's) and gives it back
293
+ in `remove`. `add` resolving to false keeps the add row's text (access refused); `invalid(text)`
294
+ hears what isn't a site in the add row; the dialog keeps its Add off with “Enter a site, like
295
+ example.com” under the field.
296
+
297
+ The words: Chrome's English (`SITE_LIST_WORDS`) without `t`; with it, the page's messages under
298
+ these keys (`SITE_LIST_MESSAGES`), so every extension's `_locales` names them alike. `words`
299
+ overrides only those that differ.
300
+
301
+ | Word | Key | English |
302
+ |---|---|---|
303
+ | `add` | `optionsAdd` | Add |
304
+ | `addSite` | `optionsAddSite` | Add site |
305
+ | `editSite` | `optionsEditSite` | Edit site |
306
+ | `edit` | `optionsEdit` | Edit |
307
+ | `remove` | `optionsRemove` | Remove |
308
+ | `save` | `optionsSave` | Save |
309
+ | `cancel` | `optionsCancel` | Cancel |
310
+ | `site` | `optionsSiteLabel` | Site |
311
+ | `invalid` | `optionsSiteInvalid` | Enter a site, like example.com |
312
+ | `moreActions` | `optionsMoreActions` | More actions for `$SITE$` (content `$1`; the ⋮ button) |
313
+ | `removeLabel` | `optionsRemoveLabel` | Remove `$SITE$` (content `$1`; the add row's Remove button) |
314
+
315
+ A list with an Add button needs every key but `optionsRemoveLabel`; one with an add row needs
316
+ `optionsRemove` and `optionsRemoveLabel`. A locale test can check them against `SITE_LIST_MESSAGES`.
198
317
 
199
318
  ## Build helpers (`@arraypress/extension-kit/build`)
200
319
 
@@ -203,7 +322,12 @@ and runs once. `undoLabel` translates the button.
203
322
  | `zipExtension(from, to, exclude?)` | Zip a folder for the Chrome Web Store, files only, less `ZIP_EXCLUDE` (icon SVG sources, `.d.ts`, `.DS_Store`). |
204
323
  | `shipsFile(file)` | Whether a file ships, by the same rules. |
205
324
  | `vendorKit(dest)` | Copy the browser half into an extension (the `extension-kit vendor` command). |
206
- | `renderToolbarIcons({chromium, dir})` | See above. |
325
+ | `vendorPackage(name, files, dest)`, `packageDir(name)` | Copy files of an installed package, stamped (`extension-kit vendor <dir> <package> <path>...`). |
326
+ | `checkVendored(dir)` | The stale copies under `dir` (`extension-kit vendor-check`). |
327
+ | `renderIcons`, `renderToolbarIcons({chromium, dir})` | See above. |
328
+ | `buildTestCopy({from, to, hook, manifest})` | A test build: the extension, `hook` as its service worker (`e2e-hook.js`), the manifest changed. |
329
+ | `testBuildDir(root, name)` | `.test-builds/<name>`, or under `$EXTENSION_TEST_BUILDS` (to test another build, or run two side by side). |
330
+ | `storeFrame` | Store screenshots: `canvas`, `windowChrome`, `geometry`, `letterIcon`, `cursor`, `THEMES`, `W`×`H`. |
207
331
  | `placeholderWarning`, `repositoryReminder`, `printReleaseChecks` | See About links. |
208
332
  | `findDisallowedClones({runJscpd})` | Run jscpd and return clones `.jscpd-allowlist.json` doesn't cover (the `extension-kit duplication` command). |
209
333
 
@@ -223,7 +347,11 @@ except those listed, each with a reason:
223
347
  | `fakeChromeStorage({local, sync, session, now})` | In-memory `chrome.storage`: JSON copies, `onChanged` for real changes only, sync's 8,192-byte item quota and 120 writes a minute. `settled()` waits for listeners. |
224
348
  | `launchExtension(chromium, dir, options)` | Load an unpacked extension into a fresh profile; `{context, worker, extensionId, url(page), profile, close()}`. |
225
349
  | `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()}`. |
226
- | `checkRunner()` | Named checks that report and go on: `check(name, fn)`, `fail(name)` for an error found outside a check, `finish()`. |
350
+ | `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. |
351
+ | `eachLimited(items, limit, work)` | Work through a list at most `limit` at once, `work(item, slot, index)`. |
352
+ | `realisticBrowser(options)`, `chromeUserAgent(browser)` | Launch options and a user agent for a browser that doesn't say it's automated, for live checks. |
353
+ | `closeOnSignal(close)` | On Ctrl-C or a kill, close the run's browsers, then exit. |
354
+ | `testBuildDir(root, name)` | As in `/build`, for the tests that load the build. |
227
355
  | `serve(routes, {port})` | Serve test pages on 127.0.0.1 from a table, a `Map` or a function of the path; `{origin, port, server, close()}`. |
228
356
  | `until(fn, what, timeout?)` | Poll until truthy. |
229
357
  | `offscreenEmulator(cdp)` | Emulate light/dark inside the offscreen document. |
@@ -253,13 +381,14 @@ except those listed, each with a reason:
253
381
  | Toggle (cr-toggle) | `<input type="checkbox" class="switch" role="switch">` |
254
382
  | Select (md-select) | plain `<select>` |
255
383
  | Text field (cr-input) | plain `<input>`, `<textarea>`; `.field` > `label` + input for a labelled one |
384
+ | Add row under a list | `<form class="row add">` with an `<input>` and a submit `<button>` (or `createSiteList`) |
256
385
  | Buttons (cr-button) | plain `<button>`; `.action` filled; `.link` as a link; `.icon-button` 32px round |
257
386
  | Settings menu | `.nav` > `ul.nav-list` > `a.nav-item[aria-current="page"]`, `.nav-separator` |
258
387
  | Manager layout | `.manager`/`.layout`, `.sidebar` with `.tree` > `.tree-item`, `.main` with `.list-card` |
259
388
  | Menu (cr-action-menu) | `<dialog class="menu">` of `button[role="menuitem"]`, `hr` (or `createMenu`) |
260
389
  | ⋮ icon | `<span class="icon icon-more-vert" aria-hidden="true">` (or `moreActionsButton`) |
261
390
  | Dialog (cr-dialog) | `<dialog class="dialog"><form method="dialog">` `.dialog-title`, `.dialog-body`, `.dialog-buttons` (one field: `askText`) |
262
- | Toast (cr-toast) | `<div class="toast">text <button>Undo</button></div>` (or `createToast`) |
391
+ | 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 |
263
392
  | Drawer (cr-drawer) | `<dialog class="drawer">` with `.drawer-head` and a `.nav` |
264
393
  | Side panel page | `<body class="side-panel">`: the panel's colours, a column 100vh high |
265
394
  | Panel tabs (cr-tabs) | `<div class="tabs" role="tablist">` of `button.tab[role="tab"][aria-selected]` |
@@ -278,7 +407,8 @@ chrome://bookmarks in Chromium 156 / Chrome 157; the sources are listed at the t
278
407
  ## Development
279
408
 
280
409
  `npm test` runs ESLint, a strict TypeScript check of the JSDoc (`checkJs`), the tests (the menu,
281
- the dialog and the Chrome readers in Playwright's Chromium, the rest in Node and jsdom), and the
410
+ the dialog, the toast, the dialog site list, icon rendering and the Chrome readers in Playwright's
411
+ Chromium, the rest in Node and jsdom), and the
282
412
  copied-code guard. The `.d.ts` files are generated from the JSDoc: `npm run types` writes them, and
283
413
  the type check fails while one is stale (it regenerates them into `.types/` and compares).
284
414
 
@@ -2,8 +2,16 @@
2
2
  /**
3
3
  * The kit's command line, run from an extension's npm scripts:
4
4
  *
5
- * extension-kit vendor <dir> copy the browser half into <dir> (npm install runs it)
6
- * extension-kit duplication fail on copied code jscpd finds, less .jscpd-allowlist.json
5
+ * extension-kit vendor <dir> copy the kit's browser half into <dir> (postinstall)
6
+ * extension-kit vendor <dir> <package> <path>... copy files of an installed package into <dir>
7
+ * (a path ending in / copies its contents)
8
+ * extension-kit vendor-check [dir] fail when a copy in [dir] (default extension/vendor)
9
+ * no longer matches the installed package
10
+ * extension-kit icons [dir] render [dir]/icon.svg (default extension/icons)
11
+ * extension-kit toolbar-icons [dir] render the toolbar glyph (default extension/icons/toolbar)
12
+ * extension-kit duplication fail on copied code jscpd finds, less .jscpd-allowlist.json
13
+ *
14
+ * Playwright and jscpd are the extension's devDependencies, so they're resolved from there.
7
15
  *
8
16
  * @module bin/extension-kit
9
17
  */
@@ -11,16 +19,44 @@
11
19
  import { createRequire } from 'node:module';
12
20
  import path from 'node:path';
13
21
  import { findDisallowedClones } from '../src/build/duplication.js';
14
- import { vendorKit } from '../src/build/vendor.js';
22
+ import { renderIcons, renderToolbarIcons } from '../src/build/toolbar-icons.js';
23
+ import { checkVendored, vendorKit, vendorPackage } from '../src/build/vendor.js';
24
+
25
+ const USAGE = `Usage:
26
+ extension-kit vendor <dir> [<package> <path>...]
27
+ extension-kit vendor-check [dir]
28
+ extension-kit icons [dir]
29
+ extension-kit toolbar-icons [dir]
30
+ extension-kit duplication`;
15
31
 
16
- const [command, arg] = process.argv.slice(2);
32
+ const [command, ...args] = process.argv.slice(2);
33
+ /** The extension's own `require`, for its devDependencies. */
34
+ const extensionRequire = createRequire(path.join(process.cwd(), 'package.json'));
17
35
 
18
- if (command === 'vendor' && arg) {
19
- await vendorKit(path.resolve(arg));
20
- console.log(`extension-kit: copied into ${arg}`);
36
+ if (command === 'vendor' && args.length === 1) {
37
+ await vendorKit(path.resolve(args[0]));
38
+ console.log(`extension-kit: copied into ${args[0]}`);
39
+ } else if (command === 'vendor' && args.length >= 3) {
40
+ const [dir, name, ...files] = args;
41
+ await vendorPackage(name, files, path.resolve(dir));
42
+ console.log(`extension-kit: copied ${name} into ${dir}`);
43
+ } else if (command === 'vendor-check' && args.length <= 1) {
44
+ const dir = args[0] ?? 'extension/vendor';
45
+ const stale = await checkVendored(dir);
46
+ for (const line of stale) console.error(line);
47
+ if (stale.length) {
48
+ console.error('\nThe vendored copies are out of date: run `npm run postinstall` (installing a package doesn\'t rerun it).');
49
+ process.exitCode = 1;
50
+ } else {
51
+ console.log(`${dir} matches the installed packages.`);
52
+ }
53
+ } else if ((command === 'icons' || command === 'toolbar-icons') && args.length <= 1) {
54
+ const { chromium } = /** @type {typeof import('playwright')} */ (extensionRequire('playwright'));
55
+ const render = command === 'icons' ? renderIcons : renderToolbarIcons;
56
+ const dir = args[0] ?? (command === 'icons' ? 'extension/icons' : 'extension/icons/toolbar');
57
+ for (const file of await render({ chromium, dir })) console.log(`wrote ${file}`);
21
58
  } else if (command === 'duplication') {
22
- // jscpd is the extension's devDependency, so resolve it from there.
23
- const { runJscpd } = createRequire(path.join(process.cwd(), 'package.json'))('jscpd');
59
+ const { runJscpd } = extensionRequire('jscpd');
24
60
  const clones = await findDisallowedClones({ runJscpd });
25
61
  for (const clone of clones) console.error(`Copied code (${clone.lines} lines): ${clone.where}`);
26
62
  if (clones.length) {
@@ -30,6 +66,6 @@ if (command === 'vendor' && arg) {
30
66
  console.log('No copied code.');
31
67
  }
32
68
  } else {
33
- console.error('Usage: extension-kit vendor <dir> | extension-kit duplication');
69
+ console.error(USAGE);
34
70
  process.exitCode = 2;
35
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arraypress/extension-kit",
3
- "version": "1.2.0",
3
+ "version": "1.4.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",
@@ -6,6 +6,8 @@
6
6
  */
7
7
  export { clonesFromReport, disallowedClones, findDisallowedClones } from './duplication.js';
8
8
  export { placeholderWarning, printReleaseChecks, repositoryReminder } from './release-checks.js';
9
- export { TOOLBAR_COLORS, TOOLBAR_ICON_COLORS, TOOLBAR_ICON_SIZES, renderToolbarIcons, toolbarIconFile } from './toolbar-icons.js';
10
- export { RUNTIME_ENTRIES, STYLESHEET, vendorKit } from './vendor.js';
9
+ export * as storeFrame from './store-frame.js';
10
+ export { HOOK_FILE, TEST_BUILDS_ENV, buildTestCopy, testBuildDir } from './test-build.js';
11
+ export { ICON_SIZES, TOOLBAR_COLORS, TOOLBAR_ICON_COLORS, TOOLBAR_ICON_SIZES, renderIcons, renderToolbarIcons, toolbarIconFile, } from './toolbar-icons.js';
12
+ export { RUNTIME_ENTRIES, STYLESHEET, VENDOR_STAMP, checkVendored, packageDir, vendorKit, vendorPackage } from './vendor.js';
11
13
  export { ZIP_EXCLUDE, shipsFile, zipExtension } from './zip.js';
@@ -7,6 +7,10 @@
7
7
 
8
8
  export { clonesFromReport, disallowedClones, findDisallowedClones } from './duplication.js';
9
9
  export { placeholderWarning, printReleaseChecks, repositoryReminder } from './release-checks.js';
10
- export { TOOLBAR_COLORS, TOOLBAR_ICON_COLORS, TOOLBAR_ICON_SIZES, renderToolbarIcons, toolbarIconFile } from './toolbar-icons.js';
11
- export { RUNTIME_ENTRIES, STYLESHEET, vendorKit } from './vendor.js';
10
+ export * as storeFrame from './store-frame.js';
11
+ export { HOOK_FILE, TEST_BUILDS_ENV, buildTestCopy, testBuildDir } from './test-build.js';
12
+ export {
13
+ ICON_SIZES, TOOLBAR_COLORS, TOOLBAR_ICON_COLORS, TOOLBAR_ICON_SIZES, renderIcons, renderToolbarIcons, toolbarIconFile,
14
+ } from './toolbar-icons.js';
15
+ export { RUNTIME_ENTRIES, STYLESHEET, VENDOR_STAMP, checkVendored, packageDir, vendorKit, vendorPackage } from './vendor.js';
12
16
  export { ZIP_EXCLUDE, shipsFile, zipExtension } from './zip.js';
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Drawing kit for store screenshots: a 1280×800 canvas with a caption band and a Chrome window
3
+ * (macOS, light or dark, default theme). Everything here is presentation around real captures;
4
+ * nothing is a feature of the extension.
5
+ *
6
+ * Window metrics follow Chrome's 2023 refresh on macOS (tabs 34px high in a 42px strip, a 46px
7
+ * toolbar with a pill omnibox).
8
+ *
9
+ * Use it from an extension's `store/tools/make-screenshots.mjs`: capture the page content with
10
+ * Playwright at `geometry(zoom).content`, then screenshot `canvas({caption, chrome:
11
+ * windowChrome({tabs, url}), content})` at {@link W}×{@link H}.
12
+ *
13
+ * @module build/store-frame
14
+ */
15
+ /** The store screenshot's width. */
16
+ export declare const W = 1280;
17
+ /** The store screenshot's height. */
18
+ export declare const H = 800;
19
+ /** Where the window sits: its left edge, top edge, full width, and tab strip and toolbar heights. */
20
+ export declare const WIN: {
21
+ x: number;
22
+ y: number;
23
+ w: number;
24
+ strip: number;
25
+ toolbar: number;
26
+ };
27
+ export type Geometry = {
28
+ /**
29
+ * The window's scale.
30
+ */
31
+ zoom: number;
32
+ /**
33
+ * The window's unscaled width.
34
+ */
35
+ w: number;
36
+ /**
37
+ * The size to capture page content at.
38
+ */
39
+ content: {
40
+ width: number;
41
+ height: number;
42
+ };
43
+ };
44
+ /**
45
+ * How a shot's window is laid out.
46
+ *
47
+ * @typedef {object} Geometry
48
+ * @property {number} zoom The window's scale.
49
+ * @property {number} w The window's unscaled width.
50
+ * @property {{ width: number, height: number }} content The size to capture page content at.
51
+ */
52
+ /**
53
+ * A shot's window is drawn at `zoom` (scaled about its top-left corner) so text stays legible
54
+ * when the store shows screenshots small. Content is captured at 2× so the scaling stays sharp.
55
+ *
56
+ * @param {number} [zoom] - The window's scale. Default 1.
57
+ * @returns {Geometry} The window's unscaled width and the size to capture page content at.
58
+ */
59
+ export declare function geometry(zoom?: number): Geometry;
60
+ /**
61
+ * A letter favicon for made-up sites (no real brands).
62
+ *
63
+ * @param {string} letter - The letter.
64
+ * @param {string} bg - Background colour.
65
+ * @param {string} [fg] - Letter colour. Default white.
66
+ * @returns {string} A data: URL of the SVG.
67
+ */
68
+ export declare const letterIcon: (letter: string, bg: string, fg?: string) => string;
69
+ /** Chrome's light and dark window colours, and the canvas behind. */
70
+ export declare const THEMES: {
71
+ light: {
72
+ bg: string;
73
+ caption: string;
74
+ sub: string;
75
+ frame: string;
76
+ tabText: string;
77
+ tabTextInactive: string;
78
+ tabSep: string;
79
+ activeTab: string;
80
+ toolbar: string;
81
+ toolbarIcon: string;
82
+ omnibox: string;
83
+ url: string;
84
+ urlDim: string;
85
+ contentLine: string;
86
+ shadow: string;
87
+ };
88
+ dark: {
89
+ bg: string;
90
+ caption: string;
91
+ sub: string;
92
+ frame: string;
93
+ tabText: string;
94
+ tabTextInactive: string;
95
+ tabSep: string;
96
+ activeTab: string;
97
+ toolbar: string;
98
+ toolbarIcon: string;
99
+ omnibox: string;
100
+ url: string;
101
+ urlDim: string;
102
+ contentLine: string;
103
+ shadow: string;
104
+ };
105
+ };
106
+ export type Theme = keyof typeof THEMES;
107
+ export type Tab = {
108
+ /**
109
+ * Its title.
110
+ */
111
+ title: string;
112
+ /**
113
+ * Its favicon's URL.
114
+ */
115
+ icon: string;
116
+ /**
117
+ * The selected tab.
118
+ */
119
+ active?: boolean;
120
+ /**
121
+ * Playing sound.
122
+ */
123
+ audible?: boolean;
124
+ /**
125
+ * Muted.
126
+ */
127
+ muted?: boolean;
128
+ };
129
+ /**
130
+ * The window's tab strip and toolbar, as HTML. Positions are window-relative.
131
+ *
132
+ * @param {object} options - What to draw.
133
+ * @param {readonly Tab[]} options.tabs - The tabs, left to right.
134
+ * @param {string} options.url - The address bar's text.
135
+ * @param {string} [options.urlHost] - The start of `url` to show in full; the rest is dimmed.
136
+ * @param {Theme} [options.theme] - Default light.
137
+ * @param {number} [options.w] - The window's width. Default the full window.
138
+ * @param {readonly string[]} [options.ext] - Toolbar extension icons' URLs.
139
+ * @returns {string} The HTML, with its own styles.
140
+ */
141
+ export declare function windowChrome({ tabs, url, urlHost, theme, w, ext }: {
142
+ tabs: readonly Tab[];
143
+ url: string;
144
+ urlHost?: string;
145
+ theme?: Theme;
146
+ w?: number;
147
+ ext?: readonly string[];
148
+ }): string;
149
+ /**
150
+ * The macOS arrow pointer.
151
+ *
152
+ * @param {number} x - Its tip, across the canvas.
153
+ * @param {number} y - Its tip, down the canvas.
154
+ * @returns {string} Absolutely positioned SVG.
155
+ */
156
+ export declare const cursor: (x: number, y: number) => string;
157
+ /**
158
+ * The full page: caption band, window, content and overlays.
159
+ *
160
+ * @param {object} options - What to draw.
161
+ * @param {string} options.caption - The headline above the window.
162
+ * @param {Theme} [options.theme] - Default light.
163
+ * @param {string} options.chrome - From windowChrome.
164
+ * @param {string} options.content - An <img> src (a capture at `geo.content` size) or raw HTML.
165
+ * @param {string} [options.overlays] - HTML drawn over the window.
166
+ * @param {Geometry} [options.geo] - Default geometry(1).
167
+ * @returns {string} A whole HTML document, W×H.
168
+ */
169
+ export declare function canvas({ caption, theme, chrome, content, overlays, geo }: {
170
+ caption: string;
171
+ theme?: Theme;
172
+ chrome: string;
173
+ content: string;
174
+ overlays?: string;
175
+ geo?: Geometry;
176
+ }): string;