@arraypress/extension-kit 1.9.0 → 1.11.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 (88) hide show
  1. package/README.md +145 -18
  2. package/bin/extension-kit.js +2 -1
  3. package/package.json +1 -1
  4. package/src/ai/host.d.ts +183 -0
  5. package/src/ai/host.js +225 -0
  6. package/src/ai/model.d.ts +2 -1
  7. package/src/ai/model.js +10 -2
  8. package/src/build/index.d.ts +2 -1
  9. package/src/build/index.js +2 -1
  10. package/src/build/store-shots.d.ts +177 -0
  11. package/src/build/store-shots.js +173 -0
  12. package/src/build/test-build.d.ts +13 -1
  13. package/src/build/test-build.js +41 -2
  14. package/src/build/vendor.d.ts +29 -8
  15. package/src/build/vendor.js +136 -40
  16. package/src/content/elements.d.ts +50 -0
  17. package/src/content/elements.js +88 -0
  18. package/src/content/highlight.d.ts +63 -0
  19. package/src/content/highlight.js +117 -0
  20. package/src/content/overlay.d.ts +31 -6
  21. package/src/content/overlay.js +56 -8
  22. package/src/content/page-toast.d.ts +26 -0
  23. package/src/content/page-toast.js +72 -0
  24. package/src/content/slices.d.ts +3 -1
  25. package/src/content/slices.js +29 -14
  26. package/src/content/watch.d.ts +4 -2
  27. package/src/content/watch.js +17 -3
  28. package/src/dom/copy.d.ts +36 -0
  29. package/src/dom/copy.js +84 -0
  30. package/src/dom/focus.d.ts +11 -0
  31. package/src/dom/focus.js +15 -0
  32. package/src/dom/i18n.d.ts +17 -0
  33. package/src/dom/i18n.js +15 -0
  34. package/src/dom/make.d.ts +53 -0
  35. package/src/dom/make.js +71 -0
  36. package/src/frame/window.d.ts +8 -1
  37. package/src/frame/window.js +11 -8
  38. package/src/index.d.ts +31 -6
  39. package/src/index.js +24 -6
  40. package/src/panel/show-on-page.d.ts +82 -0
  41. package/src/panel/show-on-page.js +83 -0
  42. package/src/scripting/call.d.ts +29 -9
  43. package/src/scripting/call.js +35 -11
  44. package/src/scripting/context-menus.d.ts +45 -0
  45. package/src/scripting/context-menus.js +53 -0
  46. package/src/scripting/registration.d.ts +2 -1
  47. package/src/scripting/registration.js +2 -1
  48. package/src/scripting/tab.d.ts +80 -0
  49. package/src/scripting/tab.js +89 -0
  50. package/src/settings/saver.js +16 -6
  51. package/src/settings/storage.d.ts +33 -2
  52. package/src/settings/storage.js +73 -3
  53. package/src/settings/store.d.ts +17 -0
  54. package/src/settings/store.js +87 -20
  55. package/src/sites/sites.d.ts +6 -1
  56. package/src/sites/sites.js +20 -3
  57. package/src/styles/chrome-ui.css +63 -30
  58. package/src/testing/e2e.d.ts +35 -4
  59. package/src/testing/e2e.js +67 -5
  60. package/src/testing/fake-model.d.ts +9 -1
  61. package/src/testing/fake-model.js +44 -23
  62. package/src/testing/fake-storage.d.ts +41 -13
  63. package/src/testing/fake-storage.js +163 -51
  64. package/src/testing/index.d.ts +8 -2
  65. package/src/testing/index.js +8 -2
  66. package/src/testing/live-run.d.ts +151 -0
  67. package/src/testing/live-run.js +206 -0
  68. package/src/testing/messages.d.ts +7 -3
  69. package/src/testing/messages.js +50 -9
  70. package/src/testing/package.d.ts +56 -7
  71. package/src/testing/package.js +57 -18
  72. package/src/testing/pages.d.ts +114 -0
  73. package/src/testing/pages.js +109 -0
  74. package/src/testing/prompt.d.ts +4 -2
  75. package/src/testing/prompt.js +7 -3
  76. package/src/time/pool.d.ts +2 -1
  77. package/src/time/pool.js +4 -2
  78. package/src/toolbar/theme.d.ts +10 -2
  79. package/src/toolbar/theme.js +12 -4
  80. package/src/ui/menu.d.ts +13 -3
  81. package/src/ui/menu.js +31 -11
  82. package/src/ui/model-row.js +5 -1
  83. package/src/ui/notice.d.ts +58 -0
  84. package/src/ui/notice.js +43 -0
  85. package/src/ui/site-list.js +12 -3
  86. package/src/ui/text-dialog.d.ts +3 -2
  87. package/src/ui/text-dialog.js +10 -3
  88. package/src/ui/toast.js +2 -0
package/src/dom/i18n.d.ts CHANGED
@@ -45,3 +45,20 @@ export declare function translator(i18n: Pick<I18nApi, 'getMessage'>): (key: str
45
45
  * @returns {void}
46
46
  */
47
47
  export declare function localize(root: ParentNode, i18n: I18nApi): void;
48
+ /**
49
+ * A count in words, its number written as the person's language writes numbers: one message for
50
+ * one, another (with the number as `$1`) for any other count.
51
+ *
52
+ * @param {number} count - How many.
53
+ * @param {string} one - The message for one (`"1 colour"`).
54
+ * @param {string} many - The message for others (`"$COUNT$ colours"`, its placeholder `$1`).
55
+ * @param {{t: (key: string, substitutions?: string | string[]) => string, lang: string}} words - The
56
+ * page's message lookup and the UI language.
57
+ * @returns {string} The words.
58
+ * @example
59
+ * counted(1200, 'panelColoursOne', 'panelColours', { t, lang: 'en-GB' }); // '1,200 colours'
60
+ */
61
+ export declare const counted: (count: number, one: string, many: string, { t, lang }: {
62
+ t: (key: string, substitutions?: string | string[]) => string;
63
+ lang: string;
64
+ }) => string;
package/src/dom/i18n.js CHANGED
@@ -60,3 +60,18 @@ export function localize(root, i18n) {
60
60
  }
61
61
  if ('documentElement' in root) /** @type {Document} */ (root).documentElement.lang = i18n.getUILanguage();
62
62
  }
63
+
64
+ /**
65
+ * A count in words, its number written as the person's language writes numbers: one message for
66
+ * one, another (with the number as `$1`) for any other count.
67
+ *
68
+ * @param {number} count - How many.
69
+ * @param {string} one - The message for one (`"1 colour"`).
70
+ * @param {string} many - The message for others (`"$COUNT$ colours"`, its placeholder `$1`).
71
+ * @param {{t: (key: string, substitutions?: string | string[]) => string, lang: string}} words - The
72
+ * page's message lookup and the UI language.
73
+ * @returns {string} The words.
74
+ * @example
75
+ * counted(1200, 'panelColoursOne', 'panelColours', { t, lang: 'en-GB' }); // '1,200 colours'
76
+ */
77
+ export const counted = (count, one, many, { t, lang }) => (count === 1 ? t(one) : t(many, [count.toLocaleString(lang)]));
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Making a page's elements in a line: an element with its class and text, an
3
+ * icon from the page's SVG sprite, and Chrome's 24px icon button with its
4
+ * tooltip. For extension pages and content-script overlays alike (pass the
5
+ * document the element belongs to).
6
+ *
7
+ * @module dom/make
8
+ */
9
+ /**
10
+ * An element with a class and text.
11
+ *
12
+ * @template {keyof HTMLElementTagNameMap} K
13
+ * @param {K} tag - Its tag.
14
+ * @param {string} [className] - Its class.
15
+ * @param {string} [text] - Its text.
16
+ * @param {Document} [doc] - Its document. Default `document`.
17
+ * @returns {HTMLElementTagNameMap[K]} It.
18
+ * @example
19
+ * make('span', 'meta', '12 colours');
20
+ */
21
+ export declare function make<K extends keyof HTMLElementTagNameMap>(tag: K, className?: string, text?: string, doc?: Document): HTMLElementTagNameMap[K];
22
+ /**
23
+ * An icon from the page's SVG sprite (`<symbol id="i-copy">`), hidden from screen readers.
24
+ *
25
+ * @param {string} id - The symbol's id, without `#`.
26
+ * @param {string} [className] - More classes beside `icon`.
27
+ * @param {Document} [doc] - Its document. Default `document`.
28
+ * @returns {SVGSVGElement} The icon.
29
+ * @example
30
+ * button.append(spriteIcon('i-copy'));
31
+ */
32
+ export declare function spriteIcon(id: string, className?: string, doc?: Document): SVGSVGElement;
33
+ /**
34
+ * Chrome's icon button (chrome-ui.css `.icon-button`): an icon, its label as tooltip and for
35
+ * screen readers.
36
+ *
37
+ * @param {object} options
38
+ * @param {string} options.icon - The sprite symbol's id.
39
+ * @param {string} options.label - What it does.
40
+ * @param {() => void} options.onClick - Doing it.
41
+ * @param {string} [options.className] - More classes.
42
+ * @param {Document} [options.doc] - Its document. Default `document`.
43
+ * @returns {HTMLButtonElement} The button.
44
+ * @example
45
+ * iconButton({ icon: 'i-copy', label: 'Copy', onClick: () => copy(hex) });
46
+ */
47
+ export declare function iconButton({ icon, label, onClick, className, doc }: {
48
+ icon: string;
49
+ label: string;
50
+ onClick: () => void;
51
+ className?: string;
52
+ doc?: Document;
53
+ }): HTMLButtonElement;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Making a page's elements in a line: an element with its class and text, an
3
+ * icon from the page's SVG sprite, and Chrome's 24px icon button with its
4
+ * tooltip. For extension pages and content-script overlays alike (pass the
5
+ * document the element belongs to).
6
+ *
7
+ * @module dom/make
8
+ */
9
+
10
+ /**
11
+ * An element with a class and text.
12
+ *
13
+ * @template {keyof HTMLElementTagNameMap} K
14
+ * @param {K} tag - Its tag.
15
+ * @param {string} [className] - Its class.
16
+ * @param {string} [text] - Its text.
17
+ * @param {Document} [doc] - Its document. Default `document`.
18
+ * @returns {HTMLElementTagNameMap[K]} It.
19
+ * @example
20
+ * make('span', 'meta', '12 colours');
21
+ */
22
+ export function make(tag, className = '', text = '', doc = document) {
23
+ const element = doc.createElement(tag);
24
+ if (className) element.className = className;
25
+ if (text) element.textContent = text;
26
+ return element;
27
+ }
28
+
29
+ /**
30
+ * An icon from the page's SVG sprite (`<symbol id="i-copy">`), hidden from screen readers.
31
+ *
32
+ * @param {string} id - The symbol's id, without `#`.
33
+ * @param {string} [className] - More classes beside `icon`.
34
+ * @param {Document} [doc] - Its document. Default `document`.
35
+ * @returns {SVGSVGElement} The icon.
36
+ * @example
37
+ * button.append(spriteIcon('i-copy'));
38
+ */
39
+ export function spriteIcon(id, className = '', doc = document) {
40
+ const svg = doc.createElementNS('http://www.w3.org/2000/svg', 'svg');
41
+ svg.setAttribute('class', `icon ${className}`.trim());
42
+ svg.setAttribute('aria-hidden', 'true');
43
+ const use = doc.createElementNS('http://www.w3.org/2000/svg', 'use');
44
+ use.setAttribute('href', `#${id}`);
45
+ svg.append(use);
46
+ return svg;
47
+ }
48
+
49
+ /**
50
+ * Chrome's icon button (chrome-ui.css `.icon-button`): an icon, its label as tooltip and for
51
+ * screen readers.
52
+ *
53
+ * @param {object} options
54
+ * @param {string} options.icon - The sprite symbol's id.
55
+ * @param {string} options.label - What it does.
56
+ * @param {() => void} options.onClick - Doing it.
57
+ * @param {string} [options.className] - More classes.
58
+ * @param {Document} [options.doc] - Its document. Default `document`.
59
+ * @returns {HTMLButtonElement} The button.
60
+ * @example
61
+ * iconButton({ icon: 'i-copy', label: 'Copy', onClick: () => copy(hex) });
62
+ */
63
+ export function iconButton({ icon, label, onClick, className = '', doc = document }) {
64
+ const button = make('button', `icon-button ${className}`.trim(), '', doc);
65
+ button.type = 'button';
66
+ button.title = label;
67
+ button.setAttribute('aria-label', label);
68
+ button.append(spriteIcon(icon, '', doc));
69
+ button.addEventListener('click', onClick);
70
+ return button;
71
+ }
@@ -27,6 +27,13 @@ export declare const MAC_WINDOW: Readonly<{
27
27
  }>;
28
28
  /** The window's default width (px): the store screenshot's. */
29
29
  export declare const WINDOW_W = 1152;
30
+ /**
31
+ * Text made safe for the drawn windows' HTML (and the store screenshots').
32
+ *
33
+ * @param {unknown} s - Text.
34
+ * @returns {string} It, escaped for HTML text and attributes.
35
+ */
36
+ export declare const esc: (s: unknown) => string;
30
37
  /**
31
38
  * A letter favicon for made-up sites (no real brands).
32
39
  *
@@ -104,7 +111,7 @@ export type Tab = {
104
111
  */
105
112
  title: string;
106
113
  /**
107
- * Its favicon's URL.
114
+ * Its favicon's URL (escaped into the markup: an `&` in it is fine).
108
115
  */
109
116
  icon: string;
110
117
  /**
@@ -26,10 +26,12 @@ export const MAC_WINDOW = Object.freeze({ titleBar: 32, radius: 16 });
26
26
  export const WINDOW_W = 1152;
27
27
 
28
28
  /**
29
+ * Text made safe for the drawn windows' HTML (and the store screenshots').
30
+ *
29
31
  * @param {unknown} s - Text.
30
32
  * @returns {string} It, escaped for HTML text and attributes.
31
33
  */
32
- const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
34
+ export const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
33
35
 
34
36
  /** Material icon paths (24×24). */
35
37
  const P = {
@@ -66,7 +68,7 @@ const icon = (name, size = 20) =>
66
68
  * @returns {string} A data: URL of the SVG.
67
69
  */
68
70
  export const letterIcon = (letter, bg, fg = '#fff') =>
69
- `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="4" fill="${bg}"/><text x="8" y="12" font-family="-apple-system,Helvetica,Arial" font-size="11" font-weight="700" text-anchor="middle" fill="${fg}">${letter}</text></svg>`)}`;
71
+ `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="4" fill="${esc(bg)}"/><text x="8" y="12" font-family="-apple-system,Helvetica,Arial" font-size="11" font-weight="700" text-anchor="middle" fill="${esc(fg)}">${esc(letter)}</text></svg>`)}`;
70
72
 
71
73
  /**
72
74
  * A window's shadow: soft and dropped below it, as macOS draws one (px; `blur` as CSS's and a
@@ -138,7 +140,7 @@ const trafficLights = ({ left, top, size, gap }) =>
138
140
  *
139
141
  * @typedef {object} Tab
140
142
  * @property {string} title Its title.
141
- * @property {string} icon Its favicon's URL.
143
+ * @property {string} icon Its favicon's URL (escaped into the markup: an `&` in it is fine).
142
144
  * @property {boolean} [active] The selected tab.
143
145
  * @property {boolean} [audible] Playing sound.
144
146
  * @property {boolean} [muted] Muted.
@@ -158,7 +160,7 @@ function tabLayout(tabs, w) {
158
160
  const right = 60;
159
161
  const max = 240;
160
162
  const avail = w - left - right - 36;
161
- const width = Math.min(max, Math.floor(avail / tabs.length));
163
+ const width = Math.min(max, Math.floor(avail / Math.max(1, tabs.length)));
162
164
  return tabs.map((_, i) => ({ x: left + i * width, width }));
163
165
  }
164
166
 
@@ -183,15 +185,16 @@ export function windowChrome({ tabs, url, urlHost, theme = 'light', w = WINDOW_W
183
185
  const alert = tab.muted ? icon('muted', 16) : tab.audible ? icon('sound', 16) : '';
184
186
  const showClose = tab.active || width >= 120;
185
187
  return `<div class="tab${tab.active ? ' active' : ''}" style="left:${x}px;width:${width}px">
186
- <img class="fav" src="${tab.icon}" width="16" height="16" alt=""/>
188
+ <img class="fav" src="${esc(tab.icon)}" width="16" height="16" alt=""/>
187
189
  <span class="title">${esc(tab.title)}</span>
188
190
  ${alert ? `<span class="alert">${alert}</span>` : ''}
189
191
  ${showClose ? `<span class="x">${icon('close', 14)}</span>` : ''}
190
192
  </div>${sepAfter ? `<span class="sep" style="left:${x + width - 1}px"></span>` : ''}`;
191
193
  }).join('');
192
- const last = layout[layout.length - 1];
193
- const plusX = last.x + last.width + 4;
194
- const extIcons = ext.map((src) => `<img src="${src}" width="16" height="16" alt="" style="margin:0 9px"/>`).join('');
194
+ // After the last tab, or where the first would be in a window drawn without any.
195
+ const last = layout.at(-1);
196
+ const plusX = last ? last.x + last.width + 4 : 84;
197
+ const extIcons = ext.map((src) => `<img src="${esc(src)}" width="16" height="16" alt="" style="margin:0 9px"/>`).join('');
195
198
  return `
196
199
  <div class="strip">
197
200
  ${trafficLights({ left: 16, top: 15, size: 12, gap: 8 })}
package/src/index.d.ts CHANGED
@@ -3,8 +3,9 @@
3
3
  * service worker and content scripts load, Chrome's page components (the ⋮
4
4
  * menu, the text dialog, the toast with Undo, a settings card's site list,
5
5
  * the built-in AI row), Chrome's window drawn around a capture, per-site lists' sites, Chrome's on-device model,
6
- * a content script's slices, change watcher and overlay, a module's function called in a page,
7
- * and a tab's side panel opened from a click. Every function takes the `chrome`
6
+ * a content script's slices, change watcher, overlay and “Show on page” boxes, a module's
7
+ * function called in a page or every frame of a tab, a tab's side panel opened from a click,
8
+ * copying (plain text and HTML), and making elements and icon buttons in a line. Every function takes the `chrome`
8
9
  * APIs it uses as arguments, so tests run the real logic against fakes.
9
10
  *
10
11
  * Build-time helpers are in `@arraypress/extension-kit/build`, test helpers
@@ -15,20 +16,29 @@
15
16
  */
16
17
  export { githubAboutLinks, isPlaceholder } from './about/links.js';
17
18
  export { MODEL_MESSAGES, chromeLanguageModel, createModelSession, describeModel, downloadModel, modelAvailability, } from './ai/model.js';
19
+ export { HOST_GRACE_MS, MODEL_HOST, createHostedModelSession, serveModelOffscreen } from './ai/host.js';
18
20
  export { setShortcuts } from './commands/shortcuts.js';
19
21
  export { BLOCK_CHARS, SLICE_MS, blocksOf, eachInSlices, inSlices } from './content/slices.js';
22
+ export { MAX_ELEMENTS, otherSiteFrames, renderedElements, shadowRootOf } from './content/elements.js';
23
+ export { HIGHLIGHT_CSS, MAX_BOXES, createHighlighter } from './content/highlight.js';
20
24
  export { pageOverlay } from './content/overlay.js';
25
+ export { PAGE_TOAST_MS, showPageToast } from './content/page-toast.js';
21
26
  export { MAX_WAIT_MS, SETTLE_MS, watchChanges } from './content/watch.js';
22
- export { focusedElement } from './dom/focus.js';
27
+ export { copyFormats } from './dom/copy.js';
28
+ export { focusedElement, focusedIn } from './dom/focus.js';
29
+ export { iconButton, make, spriteIcon } from './dom/make.js';
23
30
  export { MAC_WINDOW, THEMES, WINDOW_BARS, WINDOW_W, letterIcon, macWindowSvg, windowChrome, windowChromeSvg } from './frame/window.js';
24
- export { localize, translator } from './dom/i18n.js';
31
+ export { counted, localize, translator } from './dom/i18n.js';
25
32
  export { byId, setAboutLinks, statusFlasher, valueRow } from './dom/page.js';
26
33
  export { setUpPage } from './dom/setup.js';
34
+ export { createShowOnPage } from './panel/show-on-page.js';
27
35
  export { openSidePanel, sidePanelPath } from './panel/side-panel.js';
28
- export { callInPage, importAndCall } from './scripting/call.js';
36
+ export { CALL_FAILED, callInPage, importAndCall } from './scripting/call.js';
37
+ export { contextMenuBuilder } from './scripting/context-menus.js';
29
38
  export { contentScriptSync } from './scripting/registration.js';
39
+ export { callInTab, isBrowserPage } from './scripting/tab.js';
30
40
  export { createSettingsSaver } from './settings/saver.js';
31
- export { MAX_SYNC_ITEM_BYTES } from './settings/storage.js';
41
+ export { MAX_SYNC_ITEM_BYTES, syncItemBytes } from './settings/storage.js';
32
42
  export { createSettingsStore } from './settings/store.js';
33
43
  export { MAX_HOST_LENGTH, hostPatterns, onSite, originPattern, siteHost, siteLabel, siteOrigin, withSite, withSiteReplaced, withoutSite, } from './sites/sites.js';
34
44
  export { eachLimited } from './time/pool.js';
@@ -40,6 +50,7 @@ export { MENU_EDGE, createMenu, isInside, menuPosition, moreActionsButton } from
40
50
  export { createModelRow } from './ui/model-row.js';
41
51
  export { SITE_LIST_MESSAGES, SITE_LIST_WORDS, createSiteList } from './ui/site-list.js';
42
52
  export { askFields, askText, checkText, fieldIds } from './ui/text-dialog.js';
53
+ export { noticeCard } from './ui/notice.js';
43
54
  export { TOAST_MS, createToast } from './ui/toast.js';
44
55
  export type Availability = import('./ai/model.js').Availability;
45
56
  export type InitialPrompt = import('./ai/model.js').InitialPrompt;
@@ -50,7 +61,11 @@ export type ModelAnswer = import('./ai/model.js').ModelAnswer;
50
61
  export type ModelSession = import('./ai/model.js').ModelSession;
51
62
  export type ModelStatus = import('./ai/model.js').ModelStatus;
52
63
  export type SliceOptions = import('./content/slices.js').SliceOptions;
64
+ export type Highlighter = import('./content/highlight.js').Highlighter;
65
+ export type ClearReason = import('./content/highlight.js').ClearReason;
66
+ export type ShowOnPage = import('./panel/show-on-page.js').ShowOnPage;
53
67
  export type PageOverlay = import('./content/overlay.js').PageOverlay;
68
+ export type ClipboardFormats = import('./dom/copy.js').ClipboardFormats;
54
69
  export type WatchHandlers = import('./content/watch.js').WatchHandlers;
55
70
  export type WindowTab = import('./frame/window.js').Tab;
56
71
  export type WindowTheme = import('./frame/window.js').Theme;
@@ -58,6 +73,8 @@ export type WindowShadow = import('./frame/window.js').WindowShadow;
58
73
  export type ModelRow = import('./ui/model-row.js').ModelRow;
59
74
  export type ContentScript = import('./scripting/registration.js').ContentScript;
60
75
  export type FrameResult = import('./scripting/call.js').FrameResult<unknown>;
76
+ export type TabReading = import('./scripting/tab.js').TabReading<unknown>;
77
+ export type TabEdges = import('./scripting/tab.js').TabEdges;
61
78
  export type ActionMenu = import('./ui/menu.js').ActionMenu;
62
79
  export type MenuEntry = import('./ui/menu.js').MenuEntry;
63
80
  export type MenuItem = import('./ui/menu.js').MenuItem;
@@ -66,6 +83,7 @@ export type SiteList = import('./ui/site-list.js').SiteList;
66
83
  export type FieldRequest = import('./ui/text-dialog.js').FieldRequest;
67
84
  export type FieldsRequest = import('./ui/text-dialog.js').FieldsRequest;
68
85
  export type TextRequest = import('./ui/text-dialog.js').TextRequest;
86
+ export type Notice = import('./ui/notice.js').Notice;
69
87
  export type Toast = import('./ui/toast.js').Toast;
70
88
  /**
71
89
  * The page helpers' types, for a page's JSDoc.
@@ -79,7 +97,11 @@ export type Toast = import('./ui/toast.js').Toast;
79
97
  * @typedef {import('./ai/model.js').ModelSession} ModelSession
80
98
  * @typedef {import('./ai/model.js').ModelStatus} ModelStatus
81
99
  * @typedef {import('./content/slices.js').SliceOptions} SliceOptions
100
+ * @typedef {import('./content/highlight.js').Highlighter} Highlighter
101
+ * @typedef {import('./content/highlight.js').ClearReason} ClearReason
102
+ * @typedef {import('./panel/show-on-page.js').ShowOnPage} ShowOnPage
82
103
  * @typedef {import('./content/overlay.js').PageOverlay} PageOverlay
104
+ * @typedef {import('./dom/copy.js').ClipboardFormats} ClipboardFormats
83
105
  * @typedef {import('./content/watch.js').WatchHandlers} WatchHandlers
84
106
  * @typedef {import('./frame/window.js').Tab} WindowTab
85
107
  * @typedef {import('./frame/window.js').Theme} WindowTheme
@@ -87,6 +109,8 @@ export type Toast = import('./ui/toast.js').Toast;
87
109
  * @typedef {import('./ui/model-row.js').ModelRow} ModelRow
88
110
  * @typedef {import('./scripting/registration.js').ContentScript} ContentScript
89
111
  * @typedef {import('./scripting/call.js').FrameResult<unknown>} FrameResult
112
+ * @typedef {import('./scripting/tab.js').TabReading<unknown>} TabReading
113
+ * @typedef {import('./scripting/tab.js').TabEdges} TabEdges
90
114
  * @typedef {import('./ui/menu.js').ActionMenu} ActionMenu
91
115
  * @typedef {import('./ui/menu.js').MenuEntry} MenuEntry
92
116
  * @typedef {import('./ui/menu.js').MenuItem} MenuItem
@@ -95,5 +119,6 @@ export type Toast = import('./ui/toast.js').Toast;
95
119
  * @typedef {import('./ui/text-dialog.js').FieldRequest} FieldRequest
96
120
  * @typedef {import('./ui/text-dialog.js').FieldsRequest} FieldsRequest
97
121
  * @typedef {import('./ui/text-dialog.js').TextRequest} TextRequest
122
+ * @typedef {import('./ui/notice.js').Notice} Notice
98
123
  * @typedef {import('./ui/toast.js').Toast} Toast
99
124
  */
package/src/index.js CHANGED
@@ -3,8 +3,9 @@
3
3
  * service worker and content scripts load, Chrome's page components (the ⋮
4
4
  * menu, the text dialog, the toast with Undo, a settings card's site list,
5
5
  * the built-in AI row), Chrome's window drawn around a capture, per-site lists' sites, Chrome's on-device model,
6
- * a content script's slices, change watcher and overlay, a module's function called in a page,
7
- * and a tab's side panel opened from a click. Every function takes the `chrome`
6
+ * a content script's slices, change watcher, overlay and “Show on page” boxes, a module's
7
+ * function called in a page or every frame of a tab, a tab's side panel opened from a click,
8
+ * copying (plain text and HTML), and making elements and icon buttons in a line. Every function takes the `chrome`
8
9
  * APIs it uses as arguments, so tests run the real logic against fakes.
9
10
  *
10
11
  * Build-time helpers are in `@arraypress/extension-kit/build`, test helpers
@@ -18,20 +19,29 @@ export { githubAboutLinks, isPlaceholder } from './about/links.js';
18
19
  export {
19
20
  MODEL_MESSAGES, chromeLanguageModel, createModelSession, describeModel, downloadModel, modelAvailability,
20
21
  } from './ai/model.js';
22
+ export { HOST_GRACE_MS, MODEL_HOST, createHostedModelSession, serveModelOffscreen } from './ai/host.js';
21
23
  export { setShortcuts } from './commands/shortcuts.js';
22
24
  export { BLOCK_CHARS, SLICE_MS, blocksOf, eachInSlices, inSlices } from './content/slices.js';
25
+ export { MAX_ELEMENTS, otherSiteFrames, renderedElements, shadowRootOf } from './content/elements.js';
26
+ export { HIGHLIGHT_CSS, MAX_BOXES, createHighlighter } from './content/highlight.js';
23
27
  export { pageOverlay } from './content/overlay.js';
28
+ export { PAGE_TOAST_MS, showPageToast } from './content/page-toast.js';
24
29
  export { MAX_WAIT_MS, SETTLE_MS, watchChanges } from './content/watch.js';
25
- export { focusedElement } from './dom/focus.js';
30
+ export { copyFormats } from './dom/copy.js';
31
+ export { focusedElement, focusedIn } from './dom/focus.js';
32
+ export { iconButton, make, spriteIcon } from './dom/make.js';
26
33
  export { MAC_WINDOW, THEMES, WINDOW_BARS, WINDOW_W, letterIcon, macWindowSvg, windowChrome, windowChromeSvg } from './frame/window.js';
27
- export { localize, translator } from './dom/i18n.js';
34
+ export { counted, localize, translator } from './dom/i18n.js';
28
35
  export { byId, setAboutLinks, statusFlasher, valueRow } from './dom/page.js';
29
36
  export { setUpPage } from './dom/setup.js';
37
+ export { createShowOnPage } from './panel/show-on-page.js';
30
38
  export { openSidePanel, sidePanelPath } from './panel/side-panel.js';
31
- export { callInPage, importAndCall } from './scripting/call.js';
39
+ export { CALL_FAILED, callInPage, importAndCall } from './scripting/call.js';
40
+ export { contextMenuBuilder } from './scripting/context-menus.js';
32
41
  export { contentScriptSync } from './scripting/registration.js';
42
+ export { callInTab, isBrowserPage } from './scripting/tab.js';
33
43
  export { createSettingsSaver } from './settings/saver.js';
34
- export { MAX_SYNC_ITEM_BYTES } from './settings/storage.js';
44
+ export { MAX_SYNC_ITEM_BYTES, syncItemBytes } from './settings/storage.js';
35
45
  export { createSettingsStore } from './settings/store.js';
36
46
  export {
37
47
  MAX_HOST_LENGTH, hostPatterns, onSite, originPattern, siteHost, siteLabel, siteOrigin, withSite, withSiteReplaced, withoutSite,
@@ -47,6 +57,7 @@ export { MENU_EDGE, createMenu, isInside, menuPosition, moreActionsButton } from
47
57
  export { createModelRow } from './ui/model-row.js';
48
58
  export { SITE_LIST_MESSAGES, SITE_LIST_WORDS, createSiteList } from './ui/site-list.js';
49
59
  export { askFields, askText, checkText, fieldIds } from './ui/text-dialog.js';
60
+ export { noticeCard } from './ui/notice.js';
50
61
  export { TOAST_MS, createToast } from './ui/toast.js';
51
62
 
52
63
  /**
@@ -61,7 +72,11 @@ export { TOAST_MS, createToast } from './ui/toast.js';
61
72
  * @typedef {import('./ai/model.js').ModelSession} ModelSession
62
73
  * @typedef {import('./ai/model.js').ModelStatus} ModelStatus
63
74
  * @typedef {import('./content/slices.js').SliceOptions} SliceOptions
75
+ * @typedef {import('./content/highlight.js').Highlighter} Highlighter
76
+ * @typedef {import('./content/highlight.js').ClearReason} ClearReason
77
+ * @typedef {import('./panel/show-on-page.js').ShowOnPage} ShowOnPage
64
78
  * @typedef {import('./content/overlay.js').PageOverlay} PageOverlay
79
+ * @typedef {import('./dom/copy.js').ClipboardFormats} ClipboardFormats
65
80
  * @typedef {import('./content/watch.js').WatchHandlers} WatchHandlers
66
81
  * @typedef {import('./frame/window.js').Tab} WindowTab
67
82
  * @typedef {import('./frame/window.js').Theme} WindowTheme
@@ -69,6 +84,8 @@ export { TOAST_MS, createToast } from './ui/toast.js';
69
84
  * @typedef {import('./ui/model-row.js').ModelRow} ModelRow
70
85
  * @typedef {import('./scripting/registration.js').ContentScript} ContentScript
71
86
  * @typedef {import('./scripting/call.js').FrameResult<unknown>} FrameResult
87
+ * @typedef {import('./scripting/tab.js').TabReading<unknown>} TabReading
88
+ * @typedef {import('./scripting/tab.js').TabEdges} TabEdges
72
89
  * @typedef {import('./ui/menu.js').ActionMenu} ActionMenu
73
90
  * @typedef {import('./ui/menu.js').MenuEntry} MenuEntry
74
91
  * @typedef {import('./ui/menu.js').MenuItem} MenuItem
@@ -77,5 +94,6 @@ export { TOAST_MS, createToast } from './ui/toast.js';
77
94
  * @typedef {import('./ui/text-dialog.js').FieldRequest} FieldRequest
78
95
  * @typedef {import('./ui/text-dialog.js').FieldsRequest} FieldsRequest
79
96
  * @typedef {import('./ui/text-dialog.js').TextRequest} TextRequest
97
+ * @typedef {import('./ui/notice.js').Notice} Notice
80
98
  * @typedef {import('./ui/toast.js').Toast} Toast
81
99
  */
@@ -0,0 +1,82 @@
1
+ /**
2
+ * A side panel's “Show on page”: which of the panel's items (a font, a
3
+ * colour) is boxed on the page, kept true whatever ends it. A click on the
4
+ * item's button shows it, a second click takes it away, another item's
5
+ * replaces it; a rescan or the panel closing clears the page's boxes
6
+ * (`reset`); and when the page clears them itself (Esc there, or the page
7
+ * left: the highlighter's `onClear` reasons `'escape'` and `'pagehide'`,
8
+ * sent to the panel as a message) the panel's button is released without
9
+ * calling the page back (`cleared`).
10
+ *
11
+ * @module panel/show-on-page
12
+ */
13
+ export type ShowOnPageSetup = {
14
+ /**
15
+ * Boxes these on
16
+ * the page (the panel's own call into the tab's frames), scrolling the first into view in
17
+ * `revealFrame`; null takes the boxes away.
18
+ */
19
+ show: (ids: string[] | null, revealFrame?: number) => Promise<unknown>;
20
+ /**
21
+ * Hears which item is shown now (press its
22
+ * button, release the others).
23
+ */
24
+ onChange: (shown: string | null) => void;
25
+ };
26
+ export type ShowOnPage = {
27
+ /**
28
+ * The item shown, or null.
29
+ */
30
+ shown: () => string | null;
31
+ /**
32
+ * Show the item
33
+ * `id` (the page's `ids`), or take it away if it's the one shown. Rejects when the page can't be
34
+ * reached, with nothing shown.
35
+ */
36
+ toggle: (id: string, ids: string[], revealFrame?: number) => Promise<void>;
37
+ /**
38
+ * A rescan started, or the panel is closing: take the page's boxes
39
+ * away if any are shown. The call to the page starts at once (a closing panel still gets it out)
40
+ * and a failure is ignored (the tab is gone).
41
+ */
42
+ reset: () => Promise<void>;
43
+ /**
44
+ * The page says its boxes went: nothing is shown, and the page isn't called.
45
+ */
46
+ cleared: () => void;
47
+ };
48
+ /**
49
+ * @typedef {object} ShowOnPageSetup
50
+ * @property {(ids: string[] | null, revealFrame?: number) => Promise<unknown>} show Boxes these on
51
+ * the page (the panel's own call into the tab's frames), scrolling the first into view in
52
+ * `revealFrame`; null takes the boxes away.
53
+ * @property {(shown: string | null) => void} onChange Hears which item is shown now (press its
54
+ * button, release the others).
55
+ */
56
+ /**
57
+ * @typedef {object} ShowOnPage
58
+ * @property {() => string | null} shown The item shown, or null.
59
+ * @property {(id: string, ids: string[], revealFrame?: number) => Promise<void>} toggle Show the item
60
+ * `id` (the page's `ids`), or take it away if it's the one shown. Rejects when the page can't be
61
+ * reached, with nothing shown.
62
+ * @property {() => Promise<void>} reset A rescan started, or the panel is closing: take the page's boxes
63
+ * away if any are shown. The call to the page starts at once (a closing panel still gets it out)
64
+ * and a failure is ignored (the tab is gone).
65
+ * @property {() => void} cleared The page says its boxes went: nothing is shown, and the page isn't called.
66
+ */
67
+ /**
68
+ * A panel's “Show on page” state.
69
+ *
70
+ * @param {ShowOnPageSetup} setup - How to box items on the page, and who hears what's shown.
71
+ * @returns {ShowOnPage} The controller.
72
+ * @example
73
+ * const onPage = createShowOnPage({
74
+ * show: (ids, frame) => callPage('highlight', [ids, frame]),
75
+ * onChange: (shown) => { for (const button of buttons) button.setAttribute('aria-pressed', String(button.dataset.id === shown)); },
76
+ * });
77
+ * button.addEventListener('click', () => onPage.toggle(font.id, [font.id], font.frameId));
78
+ * chrome.runtime.onMessage.addListener((message) => { if (message.type === 'boxes-cleared') onPage.cleared(); });
79
+ * addEventListener('pagehide', () => onPage.reset());
80
+ * await onPage.reset(); // before a rescan
81
+ */
82
+ export declare function createShowOnPage({ show, onChange }: ShowOnPageSetup): ShowOnPage;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * A side panel's “Show on page”: which of the panel's items (a font, a
3
+ * colour) is boxed on the page, kept true whatever ends it. A click on the
4
+ * item's button shows it, a second click takes it away, another item's
5
+ * replaces it; a rescan or the panel closing clears the page's boxes
6
+ * (`reset`); and when the page clears them itself (Esc there, or the page
7
+ * left: the highlighter's `onClear` reasons `'escape'` and `'pagehide'`,
8
+ * sent to the panel as a message) the panel's button is released without
9
+ * calling the page back (`cleared`).
10
+ *
11
+ * @module panel/show-on-page
12
+ */
13
+
14
+ /**
15
+ * @typedef {object} ShowOnPageSetup
16
+ * @property {(ids: string[] | null, revealFrame?: number) => Promise<unknown>} show Boxes these on
17
+ * the page (the panel's own call into the tab's frames), scrolling the first into view in
18
+ * `revealFrame`; null takes the boxes away.
19
+ * @property {(shown: string | null) => void} onChange Hears which item is shown now (press its
20
+ * button, release the others).
21
+ */
22
+
23
+ /**
24
+ * @typedef {object} ShowOnPage
25
+ * @property {() => string | null} shown The item shown, or null.
26
+ * @property {(id: string, ids: string[], revealFrame?: number) => Promise<void>} toggle Show the item
27
+ * `id` (the page's `ids`), or take it away if it's the one shown. Rejects when the page can't be
28
+ * reached, with nothing shown.
29
+ * @property {() => Promise<void>} reset A rescan started, or the panel is closing: take the page's boxes
30
+ * away if any are shown. The call to the page starts at once (a closing panel still gets it out)
31
+ * and a failure is ignored (the tab is gone).
32
+ * @property {() => void} cleared The page says its boxes went: nothing is shown, and the page isn't called.
33
+ */
34
+
35
+ /**
36
+ * A panel's “Show on page” state.
37
+ *
38
+ * @param {ShowOnPageSetup} setup - How to box items on the page, and who hears what's shown.
39
+ * @returns {ShowOnPage} The controller.
40
+ * @example
41
+ * const onPage = createShowOnPage({
42
+ * show: (ids, frame) => callPage('highlight', [ids, frame]),
43
+ * onChange: (shown) => { for (const button of buttons) button.setAttribute('aria-pressed', String(button.dataset.id === shown)); },
44
+ * });
45
+ * button.addEventListener('click', () => onPage.toggle(font.id, [font.id], font.frameId));
46
+ * chrome.runtime.onMessage.addListener((message) => { if (message.type === 'boxes-cleared') onPage.cleared(); });
47
+ * addEventListener('pagehide', () => onPage.reset());
48
+ * await onPage.reset(); // before a rescan
49
+ */
50
+ export function createShowOnPage({ show, onChange }) {
51
+ /** @type {string | null} */
52
+ let shown = null;
53
+ /** @param {string | null} id */
54
+ const set = (id) => {
55
+ if (shown === id) return;
56
+ shown = id;
57
+ onChange(id);
58
+ };
59
+
60
+ return {
61
+ shown: () => shown,
62
+ async toggle(id, ids, revealFrame) {
63
+ if (shown === id) {
64
+ set(null);
65
+ await show(null);
66
+ return;
67
+ }
68
+ set(id);
69
+ try {
70
+ await show(ids, revealFrame);
71
+ } catch (error) {
72
+ if (shown === id) set(null);
73
+ throw error;
74
+ }
75
+ },
76
+ async reset() {
77
+ if (shown === null) return;
78
+ set(null);
79
+ await show(null).catch(() => {});
80
+ },
81
+ cleared: () => set(null),
82
+ };
83
+ }