@arraypress/extension-kit 1.10.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.
- package/README.md +97 -20
- package/bin/extension-kit.js +2 -1
- package/package.json +1 -1
- package/src/ai/host.d.ts +183 -0
- package/src/ai/host.js +225 -0
- package/src/ai/model.d.ts +2 -1
- package/src/ai/model.js +10 -2
- package/src/build/vendor.d.ts +29 -8
- package/src/build/vendor.js +136 -40
- package/src/content/highlight.d.ts +15 -6
- package/src/content/highlight.js +42 -17
- package/src/content/overlay.d.ts +19 -4
- package/src/content/overlay.js +44 -6
- package/src/content/slices.d.ts +3 -1
- package/src/content/slices.js +29 -14
- package/src/content/watch.d.ts +4 -2
- package/src/content/watch.js +17 -3
- package/src/dom/focus.d.ts +11 -0
- package/src/dom/focus.js +15 -0
- package/src/frame/window.d.ts +1 -1
- package/src/frame/window.js +8 -7
- package/src/index.d.ts +10 -3
- package/src/index.js +8 -3
- package/src/panel/show-on-page.d.ts +82 -0
- package/src/panel/show-on-page.js +83 -0
- package/src/scripting/call.d.ts +29 -9
- package/src/scripting/call.js +35 -11
- package/src/scripting/context-menus.d.ts +45 -0
- package/src/scripting/context-menus.js +53 -0
- package/src/scripting/registration.d.ts +2 -1
- package/src/scripting/registration.js +2 -1
- package/src/scripting/tab.d.ts +17 -5
- package/src/scripting/tab.js +15 -7
- package/src/settings/saver.js +16 -6
- package/src/settings/storage.d.ts +33 -2
- package/src/settings/storage.js +73 -3
- package/src/settings/store.d.ts +17 -0
- package/src/settings/store.js +87 -20
- package/src/sites/sites.d.ts +6 -1
- package/src/sites/sites.js +20 -3
- package/src/styles/chrome-ui.css +56 -30
- package/src/testing/e2e.d.ts +35 -4
- package/src/testing/e2e.js +67 -5
- package/src/testing/fake-model.d.ts +9 -1
- package/src/testing/fake-model.js +44 -23
- package/src/testing/fake-storage.d.ts +41 -13
- package/src/testing/fake-storage.js +163 -51
- package/src/testing/index.d.ts +3 -3
- package/src/testing/index.js +5 -3
- package/src/testing/live-run.d.ts +23 -2
- package/src/testing/live-run.js +64 -18
- package/src/testing/messages.d.ts +7 -3
- package/src/testing/messages.js +50 -9
- package/src/testing/package.d.ts +56 -7
- package/src/testing/package.js +57 -18
- package/src/testing/prompt.d.ts +4 -2
- package/src/testing/prompt.js +7 -3
- package/src/time/pool.d.ts +2 -1
- package/src/time/pool.js +4 -2
- package/src/toolbar/theme.d.ts +10 -2
- package/src/toolbar/theme.js +12 -4
- package/src/ui/menu.d.ts +13 -3
- package/src/ui/menu.js +31 -11
- package/src/ui/model-row.js +5 -1
- package/src/ui/site-list.js +12 -3
- package/src/ui/text-dialog.d.ts +3 -2
- package/src/ui/text-dialog.js +10 -3
- package/src/ui/toast.js +2 -0
package/src/content/overlay.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* @property {HTMLElement} host The host element, on the page's root element.
|
|
15
15
|
* @property {ShadowRoot} root Its closed shadow root, the styles first: append to it.
|
|
16
16
|
* @property {() => void} remove Take it off the page.
|
|
17
|
+
* @property {() => void} raise With `topLayer`: put it over whatever entered the top layer since.
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -25,10 +26,19 @@
|
|
|
25
26
|
* with `!important` after the reset. `css` styles what's inside (and `:host` custom properties,
|
|
26
27
|
* which the reset doesn't touch).
|
|
27
28
|
*
|
|
29
|
+
* With `topLayer` the host is a manual popover, shown: it draws in the top layer, over the page's
|
|
30
|
+
* modal dialogs, popovers and fullscreen element, which no `z-index` reaches. The top layer
|
|
31
|
+
* paints in the order things entered it, so the overlay goes back on top (`raise`) whenever the
|
|
32
|
+
* page opens a dialog or a popover or goes fullscreen after it. What a modal dialog makes inert
|
|
33
|
+
* stays inert, the overlay with it: it shows over the dialog, but can't be clicked while the
|
|
34
|
+
* dialog is open. The popover's own backdrop is taken away (first in the overlay's styles), so a
|
|
35
|
+
* page's `::backdrop` rule can't tint the window through it.
|
|
36
|
+
*
|
|
28
37
|
* @param {Document} doc - The page.
|
|
29
|
-
* @param {{name: string, css: string, hostStyle?: Record<string, string
|
|
30
|
-
* host's element name, with a hyphen (never registered: a plain element); `css`: the
|
|
31
|
-
* styles; `hostStyle`: the host's own styles by CSS property name (`'z-index'`), each
|
|
38
|
+
* @param {{name: string, css: string, hostStyle?: Record<string, string>, topLayer?: boolean}} options -
|
|
39
|
+
* `name`: the host's element name, with a hyphen (never registered: a plain element); `css`: the
|
|
40
|
+
* overlay's styles; `hostStyle`: the host's own styles by CSS property name (`'z-index'`), each
|
|
41
|
+
* `!important`; `topLayer`: draw over the page's dialogs and fullscreen element. Default false.
|
|
32
42
|
* @returns {PageOverlay} The overlay.
|
|
33
43
|
* @example
|
|
34
44
|
* const { root, remove } = pageOverlay(document, {
|
|
@@ -38,15 +48,43 @@
|
|
|
38
48
|
* });
|
|
39
49
|
* root.append(card);
|
|
40
50
|
*/
|
|
41
|
-
export function pageOverlay(doc, { name, css, hostStyle = {} }) {
|
|
51
|
+
export function pageOverlay(doc, { name, css, hostStyle = {}, topLayer = false }) {
|
|
42
52
|
for (const old of doc.querySelectorAll(name)) old.remove();
|
|
43
53
|
const host = doc.createElement(name);
|
|
44
54
|
host.style.setProperty('all', 'initial', 'important');
|
|
45
55
|
for (const [property, value] of Object.entries(hostStyle)) host.style.setProperty(property, value, 'important');
|
|
46
56
|
const root = host.attachShadow({ mode: 'closed' });
|
|
47
57
|
const style = doc.createElement('style');
|
|
48
|
-
|
|
58
|
+
// A shown popover has a backdrop over the whole window, which a page's bare `::backdrop { … }` rule would tint.
|
|
59
|
+
style.textContent = topLayer ? `:host::backdrop { display: none !important; }\n${css}` : css;
|
|
49
60
|
root.append(style);
|
|
50
61
|
doc.documentElement.append(host);
|
|
51
|
-
return { host, root, remove: () => host.remove() };
|
|
62
|
+
if (!topLayer || typeof host.showPopover !== 'function') return { host, root, remove: () => host.remove(), raise: () => {} };
|
|
63
|
+
|
|
64
|
+
host.popover = 'manual';
|
|
65
|
+
const raise = () => {
|
|
66
|
+
if (!host.isConnected) return;
|
|
67
|
+
host.hidePopover();
|
|
68
|
+
host.showPopover();
|
|
69
|
+
};
|
|
70
|
+
/** Something of the page's entered the top layer: a dialog or popover opened, or the page went fullscreen. */
|
|
71
|
+
const entered = (/** @type {Event} */ event) => {
|
|
72
|
+
if (event.target === host) return;
|
|
73
|
+
if (event.type === 'toggle' && /** @type {ToggleEvent} */ (event).newState !== 'open') return;
|
|
74
|
+
raise();
|
|
75
|
+
};
|
|
76
|
+
const view = doc.defaultView;
|
|
77
|
+
view?.addEventListener('toggle', entered, true);
|
|
78
|
+
view?.addEventListener('fullscreenchange', entered, true);
|
|
79
|
+
host.showPopover();
|
|
80
|
+
return {
|
|
81
|
+
host,
|
|
82
|
+
root,
|
|
83
|
+
raise,
|
|
84
|
+
remove() {
|
|
85
|
+
view?.removeEventListener('toggle', entered, true);
|
|
86
|
+
view?.removeEventListener('fullscreenchange', entered, true);
|
|
87
|
+
host.remove();
|
|
88
|
+
},
|
|
89
|
+
};
|
|
52
90
|
}
|
package/src/content/slices.d.ts
CHANGED
|
@@ -14,7 +14,9 @@ export declare const BLOCK_CHARS = 4000;
|
|
|
14
14
|
export declare const SLICE_MS = 40;
|
|
15
15
|
/**
|
|
16
16
|
* The blocks of a part of the page: itself if small enough, else its
|
|
17
|
-
* children's blocks (its own text nodes as blocks of their own).
|
|
17
|
+
* children's blocks (its own text nodes as blocks of their own). Scripts,
|
|
18
|
+
* styles and templates inside a part that's cut are left out: their text
|
|
19
|
+
* isn't the page's.
|
|
18
20
|
*
|
|
19
21
|
* @param {Node} root - The part.
|
|
20
22
|
* @param {number} [blockChars] - The most text a block holds. Default {@link BLOCK_CHARS}.
|
package/src/content/slices.js
CHANGED
|
@@ -15,9 +15,35 @@ export const BLOCK_CHARS = 4000;
|
|
|
15
15
|
/** How long one slice may run before the page gets the main thread back (ms), by default. Shorter slices cost more layout in all. */
|
|
16
16
|
export const SLICE_MS = 40;
|
|
17
17
|
|
|
18
|
+
/** Elements whose text is never the page's content: not cut into, nor handed over as blocks. */
|
|
19
|
+
const NOT_CONTENT = new Set(['script', 'style', 'noscript', 'template']);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The blocks of a part of the page, one at a time as they're asked for: the cutting (each
|
|
23
|
+
* level's text measured) is spread over the slices with the work, not done whole before the
|
|
24
|
+
* first. A level's children are listed when it's reached, so what the work adds beside a block
|
|
25
|
+
* isn't handed back to it.
|
|
26
|
+
*
|
|
27
|
+
* @param {Node} node - The part.
|
|
28
|
+
* @param {number} blockChars - The most text a block holds.
|
|
29
|
+
* @returns {Generator<Node, void, void>} The blocks, in page order.
|
|
30
|
+
*/
|
|
31
|
+
function* cut(node, blockChars) {
|
|
32
|
+
if (node.nodeType !== 1 || (node.textContent ?? '').length <= blockChars || !node.firstChild) {
|
|
33
|
+
yield node;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const child of [...node.childNodes]) {
|
|
37
|
+
if (child.nodeType === 1 && NOT_CONTENT.has(/** @type {Element} */ (child).localName)) continue;
|
|
38
|
+
yield* cut(child, blockChars);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
18
42
|
/**
|
|
19
43
|
* The blocks of a part of the page: itself if small enough, else its
|
|
20
|
-
* children's blocks (its own text nodes as blocks of their own).
|
|
44
|
+
* children's blocks (its own text nodes as blocks of their own). Scripts,
|
|
45
|
+
* styles and templates inside a part that's cut are left out: their text
|
|
46
|
+
* isn't the page's.
|
|
21
47
|
*
|
|
22
48
|
* @param {Node} root - The part.
|
|
23
49
|
* @param {number} [blockChars] - The most text a block holds. Default {@link BLOCK_CHARS}.
|
|
@@ -26,18 +52,7 @@ export const SLICE_MS = 40;
|
|
|
26
52
|
* blocksOf(document.body).length; // 1 for a short page, one per card for a long listing
|
|
27
53
|
*/
|
|
28
54
|
export function blocksOf(root, blockChars = BLOCK_CHARS) {
|
|
29
|
-
|
|
30
|
-
const blocks = [];
|
|
31
|
-
/** @param {Node} node */
|
|
32
|
-
const cut = (node) => {
|
|
33
|
-
if (node.nodeType !== 1 || (node.textContent ?? '').length <= blockChars || !node.firstChild) {
|
|
34
|
-
blocks.push(node);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
for (const child of node.childNodes) cut(child);
|
|
38
|
-
};
|
|
39
|
-
cut(root);
|
|
40
|
-
return blocks;
|
|
55
|
+
return [...cut(root, blockChars)];
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
/**
|
|
@@ -95,5 +110,5 @@ export async function eachInSlices(items, work, { sliceMs = SLICE_MS, pause = ne
|
|
|
95
110
|
*/
|
|
96
111
|
export function inSlices(root, work, { blockChars = BLOCK_CHARS, ...options } = {}) {
|
|
97
112
|
// A block the work before took out of the page is skipped.
|
|
98
|
-
return eachInSlices(
|
|
113
|
+
return eachInSlices(cut(root, blockChars), (block) => { if (block.isConnected) work(block); }, options);
|
|
99
114
|
}
|
package/src/content/watch.d.ts
CHANGED
|
@@ -24,7 +24,8 @@ export type WatchHandlers = {
|
|
|
24
24
|
*/
|
|
25
25
|
removed?: (node: Node) => void;
|
|
26
26
|
/**
|
|
27
|
-
* Whether a node
|
|
27
|
+
* Whether a node is the extension's own: one added or removed, and
|
|
28
|
+
* anything that changes inside one (its text set again), is left out.
|
|
28
29
|
*/
|
|
29
30
|
ignore?: (node: Node) => boolean;
|
|
30
31
|
/**
|
|
@@ -48,7 +49,8 @@ export type WatchHandlers = {
|
|
|
48
49
|
* none inside another.
|
|
49
50
|
* @property {(node: Node) => void} [removed] A node that left (at once, so what the extension added for it
|
|
50
51
|
* can go with it).
|
|
51
|
-
* @property {(node: Node) => boolean} [ignore] Whether a node
|
|
52
|
+
* @property {(node: Node) => boolean} [ignore] Whether a node is the extension's own: one added or removed, and
|
|
53
|
+
* anything that changes inside one (its text set again), is left out.
|
|
52
54
|
* @property {number} [settleMs] How long a batch waits for more. Default {@link SETTLE_MS}.
|
|
53
55
|
* @property {number} [maxWaitMs] The longest a batch waits. Default {@link MAX_WAIT_MS}.
|
|
54
56
|
* @property {number} [regroupChildren] An element whose children changed is handed over again whole
|
package/src/content/watch.js
CHANGED
|
@@ -21,7 +21,8 @@ export const MAX_WAIT_MS = 600;
|
|
|
21
21
|
* none inside another.
|
|
22
22
|
* @property {(node: Node) => void} [removed] A node that left (at once, so what the extension added for it
|
|
23
23
|
* can go with it).
|
|
24
|
-
* @property {(node: Node) => boolean} [ignore] Whether a node
|
|
24
|
+
* @property {(node: Node) => boolean} [ignore] Whether a node is the extension's own: one added or removed, and
|
|
25
|
+
* anything that changes inside one (its text set again), is left out.
|
|
25
26
|
* @property {number} [settleMs] How long a batch waits for more. Default {@link SETTLE_MS}.
|
|
26
27
|
* @property {number} [maxWaitMs] The longest a batch waits. Default {@link MAX_WAIT_MS}.
|
|
27
28
|
* @property {number} [regroupChildren] An element whose children changed is handed over again whole
|
|
@@ -56,14 +57,27 @@ export function watchChanges(root, {
|
|
|
56
57
|
firstAt = 0;
|
|
57
58
|
const nodes = [...pending].filter((node) => node.isConnected);
|
|
58
59
|
pending = new Set();
|
|
59
|
-
// Each subtree once: a node inside another changed node goes with it.
|
|
60
|
-
|
|
60
|
+
// Each subtree once: a node inside another changed node goes with it. Found by walking up from
|
|
61
|
+
// each (its depth), not by asking every pair: a feed adding thousands at once would stall the page.
|
|
62
|
+
const batch = new Set(nodes);
|
|
63
|
+
const tops = nodes.filter((node) => {
|
|
64
|
+
for (let above = node.parentNode; above; above = above.parentNode) if (batch.has(above)) return false;
|
|
65
|
+
return true;
|
|
66
|
+
});
|
|
61
67
|
if (tops.length) changed(tops);
|
|
62
68
|
};
|
|
63
69
|
|
|
70
|
+
/** Whether a node is the extension's own, or inside one that is. @param {Node | null} node */
|
|
71
|
+
const within = (node) => {
|
|
72
|
+
for (let at = node; at; at = at.parentNode) if (ignore(at)) return true;
|
|
73
|
+
return false;
|
|
74
|
+
};
|
|
75
|
+
|
|
64
76
|
const observer = new MutationObserver((mutations) => {
|
|
65
77
|
for (const mutation of mutations) {
|
|
66
78
|
const target = mutation.target;
|
|
79
|
+
// What happens inside the extension's own elements (its note's text set again) isn't the page changing.
|
|
80
|
+
if (within(target)) continue;
|
|
67
81
|
if (mutation.type === 'characterData') {
|
|
68
82
|
pending.add(target);
|
|
69
83
|
continue;
|
package/src/dom/focus.d.ts
CHANGED
|
@@ -19,6 +19,17 @@
|
|
|
19
19
|
* focusedElement(); // → the button
|
|
20
20
|
*/
|
|
21
21
|
export declare function focusedElement(root?: Document | ShadowRoot): Element | null;
|
|
22
|
+
/**
|
|
23
|
+
* The focused element as seen from where `node` is: its shadow root's (a
|
|
24
|
+
* closed one too, which the document's `activeElement` stops at) or its
|
|
25
|
+
* document's, followed into open shadow roots below.
|
|
26
|
+
*
|
|
27
|
+
* @param {Node} node - A node in the page or in a shadow root.
|
|
28
|
+
* @returns {Element | null} The focused element, or null when nothing there has it.
|
|
29
|
+
* @example
|
|
30
|
+
* focusedIn(menu); // the menu's focused item, in a content script's closed shadow root too
|
|
31
|
+
*/
|
|
32
|
+
export declare function focusedIn(node: Node): Element | null;
|
|
22
33
|
/**
|
|
23
34
|
* The document a component is made in: `root`'s, or the page's.
|
|
24
35
|
*
|
package/src/dom/focus.js
CHANGED
|
@@ -25,6 +25,21 @@ export function focusedElement(root = document) {
|
|
|
25
25
|
return active;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The focused element as seen from where `node` is: its shadow root's (a
|
|
30
|
+
* closed one too, which the document's `activeElement` stops at) or its
|
|
31
|
+
* document's, followed into open shadow roots below.
|
|
32
|
+
*
|
|
33
|
+
* @param {Node} node - A node in the page or in a shadow root.
|
|
34
|
+
* @returns {Element | null} The focused element, or null when nothing there has it.
|
|
35
|
+
* @example
|
|
36
|
+
* focusedIn(menu); // the menu's focused item, in a content script's closed shadow root too
|
|
37
|
+
*/
|
|
38
|
+
export function focusedIn(node) {
|
|
39
|
+
const root = /** @type {Document | ShadowRoot} */ (node.getRootNode());
|
|
40
|
+
return 'activeElement' in root ? focusedElement(root) : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
/**
|
|
29
44
|
* The document a component is made in: `root`'s, or the page's.
|
|
30
45
|
*
|
package/src/frame/window.d.ts
CHANGED
package/src/frame/window.js
CHANGED
|
@@ -68,7 +68,7 @@ const icon = (name, size = 20) =>
|
|
|
68
68
|
* @returns {string} A data: URL of the SVG.
|
|
69
69
|
*/
|
|
70
70
|
export const letterIcon = (letter, bg, fg = '#fff') =>
|
|
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="${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>`)}`;
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* A window's shadow: soft and dropped below it, as macOS draws one (px; `blur` as CSS's and a
|
|
@@ -140,7 +140,7 @@ const trafficLights = ({ left, top, size, gap }) =>
|
|
|
140
140
|
*
|
|
141
141
|
* @typedef {object} Tab
|
|
142
142
|
* @property {string} title Its title.
|
|
143
|
-
* @property {string} icon Its favicon's URL.
|
|
143
|
+
* @property {string} icon Its favicon's URL (escaped into the markup: an `&` in it is fine).
|
|
144
144
|
* @property {boolean} [active] The selected tab.
|
|
145
145
|
* @property {boolean} [audible] Playing sound.
|
|
146
146
|
* @property {boolean} [muted] Muted.
|
|
@@ -160,7 +160,7 @@ function tabLayout(tabs, w) {
|
|
|
160
160
|
const right = 60;
|
|
161
161
|
const max = 240;
|
|
162
162
|
const avail = w - left - right - 36;
|
|
163
|
-
const width = Math.min(max, Math.floor(avail / tabs.length));
|
|
163
|
+
const width = Math.min(max, Math.floor(avail / Math.max(1, tabs.length)));
|
|
164
164
|
return tabs.map((_, i) => ({ x: left + i * width, width }));
|
|
165
165
|
}
|
|
166
166
|
|
|
@@ -185,15 +185,16 @@ export function windowChrome({ tabs, url, urlHost, theme = 'light', w = WINDOW_W
|
|
|
185
185
|
const alert = tab.muted ? icon('muted', 16) : tab.audible ? icon('sound', 16) : '';
|
|
186
186
|
const showClose = tab.active || width >= 120;
|
|
187
187
|
return `<div class="tab${tab.active ? ' active' : ''}" style="left:${x}px;width:${width}px">
|
|
188
|
-
<img class="fav" src="${tab.icon}" width="16" height="16" alt=""/>
|
|
188
|
+
<img class="fav" src="${esc(tab.icon)}" width="16" height="16" alt=""/>
|
|
189
189
|
<span class="title">${esc(tab.title)}</span>
|
|
190
190
|
${alert ? `<span class="alert">${alert}</span>` : ''}
|
|
191
191
|
${showClose ? `<span class="x">${icon('close', 14)}</span>` : ''}
|
|
192
192
|
</div>${sepAfter ? `<span class="sep" style="left:${x + width - 1}px"></span>` : ''}`;
|
|
193
193
|
}).join('');
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
const
|
|
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('');
|
|
197
198
|
return `
|
|
198
199
|
<div class="strip">
|
|
199
200
|
${trafficLights({ left: 16, top: 15, size: 12, gap: 8 })}
|
package/src/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export { githubAboutLinks, isPlaceholder } from './about/links.js';
|
|
18
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';
|
|
19
20
|
export { setShortcuts } from './commands/shortcuts.js';
|
|
20
21
|
export { BLOCK_CHARS, SLICE_MS, blocksOf, eachInSlices, inSlices } from './content/slices.js';
|
|
21
22
|
export { MAX_ELEMENTS, otherSiteFrames, renderedElements, shadowRootOf } from './content/elements.js';
|
|
@@ -24,18 +25,20 @@ export { pageOverlay } from './content/overlay.js';
|
|
|
24
25
|
export { PAGE_TOAST_MS, showPageToast } from './content/page-toast.js';
|
|
25
26
|
export { MAX_WAIT_MS, SETTLE_MS, watchChanges } from './content/watch.js';
|
|
26
27
|
export { copyFormats } from './dom/copy.js';
|
|
27
|
-
export { focusedElement } from './dom/focus.js';
|
|
28
|
+
export { focusedElement, focusedIn } from './dom/focus.js';
|
|
28
29
|
export { iconButton, make, spriteIcon } from './dom/make.js';
|
|
29
30
|
export { MAC_WINDOW, THEMES, WINDOW_BARS, WINDOW_W, letterIcon, macWindowSvg, windowChrome, windowChromeSvg } from './frame/window.js';
|
|
30
31
|
export { counted, localize, translator } from './dom/i18n.js';
|
|
31
32
|
export { byId, setAboutLinks, statusFlasher, valueRow } from './dom/page.js';
|
|
32
33
|
export { setUpPage } from './dom/setup.js';
|
|
34
|
+
export { createShowOnPage } from './panel/show-on-page.js';
|
|
33
35
|
export { openSidePanel, sidePanelPath } from './panel/side-panel.js';
|
|
34
|
-
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';
|
|
35
38
|
export { contentScriptSync } from './scripting/registration.js';
|
|
36
39
|
export { callInTab, isBrowserPage } from './scripting/tab.js';
|
|
37
40
|
export { createSettingsSaver } from './settings/saver.js';
|
|
38
|
-
export { MAX_SYNC_ITEM_BYTES } from './settings/storage.js';
|
|
41
|
+
export { MAX_SYNC_ITEM_BYTES, syncItemBytes } from './settings/storage.js';
|
|
39
42
|
export { createSettingsStore } from './settings/store.js';
|
|
40
43
|
export { MAX_HOST_LENGTH, hostPatterns, onSite, originPattern, siteHost, siteLabel, siteOrigin, withSite, withSiteReplaced, withoutSite, } from './sites/sites.js';
|
|
41
44
|
export { eachLimited } from './time/pool.js';
|
|
@@ -59,6 +62,8 @@ export type ModelSession = import('./ai/model.js').ModelSession;
|
|
|
59
62
|
export type ModelStatus = import('./ai/model.js').ModelStatus;
|
|
60
63
|
export type SliceOptions = import('./content/slices.js').SliceOptions;
|
|
61
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;
|
|
62
67
|
export type PageOverlay = import('./content/overlay.js').PageOverlay;
|
|
63
68
|
export type ClipboardFormats = import('./dom/copy.js').ClipboardFormats;
|
|
64
69
|
export type WatchHandlers = import('./content/watch.js').WatchHandlers;
|
|
@@ -93,6 +98,8 @@ export type Toast = import('./ui/toast.js').Toast;
|
|
|
93
98
|
* @typedef {import('./ai/model.js').ModelStatus} ModelStatus
|
|
94
99
|
* @typedef {import('./content/slices.js').SliceOptions} SliceOptions
|
|
95
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
|
|
96
103
|
* @typedef {import('./content/overlay.js').PageOverlay} PageOverlay
|
|
97
104
|
* @typedef {import('./dom/copy.js').ClipboardFormats} ClipboardFormats
|
|
98
105
|
* @typedef {import('./content/watch.js').WatchHandlers} WatchHandlers
|
package/src/index.js
CHANGED
|
@@ -19,6 +19,7 @@ export { githubAboutLinks, isPlaceholder } from './about/links.js';
|
|
|
19
19
|
export {
|
|
20
20
|
MODEL_MESSAGES, chromeLanguageModel, createModelSession, describeModel, downloadModel, modelAvailability,
|
|
21
21
|
} from './ai/model.js';
|
|
22
|
+
export { HOST_GRACE_MS, MODEL_HOST, createHostedModelSession, serveModelOffscreen } from './ai/host.js';
|
|
22
23
|
export { setShortcuts } from './commands/shortcuts.js';
|
|
23
24
|
export { BLOCK_CHARS, SLICE_MS, blocksOf, eachInSlices, inSlices } from './content/slices.js';
|
|
24
25
|
export { MAX_ELEMENTS, otherSiteFrames, renderedElements, shadowRootOf } from './content/elements.js';
|
|
@@ -27,18 +28,20 @@ export { pageOverlay } from './content/overlay.js';
|
|
|
27
28
|
export { PAGE_TOAST_MS, showPageToast } from './content/page-toast.js';
|
|
28
29
|
export { MAX_WAIT_MS, SETTLE_MS, watchChanges } from './content/watch.js';
|
|
29
30
|
export { copyFormats } from './dom/copy.js';
|
|
30
|
-
export { focusedElement } from './dom/focus.js';
|
|
31
|
+
export { focusedElement, focusedIn } from './dom/focus.js';
|
|
31
32
|
export { iconButton, make, spriteIcon } from './dom/make.js';
|
|
32
33
|
export { MAC_WINDOW, THEMES, WINDOW_BARS, WINDOW_W, letterIcon, macWindowSvg, windowChrome, windowChromeSvg } from './frame/window.js';
|
|
33
34
|
export { counted, localize, translator } from './dom/i18n.js';
|
|
34
35
|
export { byId, setAboutLinks, statusFlasher, valueRow } from './dom/page.js';
|
|
35
36
|
export { setUpPage } from './dom/setup.js';
|
|
37
|
+
export { createShowOnPage } from './panel/show-on-page.js';
|
|
36
38
|
export { openSidePanel, sidePanelPath } from './panel/side-panel.js';
|
|
37
|
-
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';
|
|
38
41
|
export { contentScriptSync } from './scripting/registration.js';
|
|
39
42
|
export { callInTab, isBrowserPage } from './scripting/tab.js';
|
|
40
43
|
export { createSettingsSaver } from './settings/saver.js';
|
|
41
|
-
export { MAX_SYNC_ITEM_BYTES } from './settings/storage.js';
|
|
44
|
+
export { MAX_SYNC_ITEM_BYTES, syncItemBytes } from './settings/storage.js';
|
|
42
45
|
export { createSettingsStore } from './settings/store.js';
|
|
43
46
|
export {
|
|
44
47
|
MAX_HOST_LENGTH, hostPatterns, onSite, originPattern, siteHost, siteLabel, siteOrigin, withSite, withSiteReplaced, withoutSite,
|
|
@@ -70,6 +73,8 @@ export { TOAST_MS, createToast } from './ui/toast.js';
|
|
|
70
73
|
* @typedef {import('./ai/model.js').ModelStatus} ModelStatus
|
|
71
74
|
* @typedef {import('./content/slices.js').SliceOptions} SliceOptions
|
|
72
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
|
|
73
78
|
* @typedef {import('./content/overlay.js').PageOverlay} PageOverlay
|
|
74
79
|
* @typedef {import('./dom/copy.js').ClipboardFormats} ClipboardFormats
|
|
75
80
|
* @typedef {import('./content/watch.js').WatchHandlers} WatchHandlers
|
|
@@ -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
|
+
}
|