@devaccel-labs/app-comments-toolbar 1.0.0 → 1.1.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 +25 -9
- package/dist/react.js +146 -44
- package/dist/types/react.d.ts +1 -1
- package/dist/types/runtime/build-id.d.ts +6 -0
- package/dist/types/runtime/index.d.ts +1 -1
- package/dist/types/runtime/loader.d.ts +1 -9
- package/dist/types/runtime/page-image.d.ts +70 -0
- package/dist/types/runtime/runtime.d.ts +6 -0
- package/dist/types/runtime/screenshot.d.ts +2 -0
- package/dist/types/runtime/update.d.ts +13 -0
- package/package.json +3 -3
- package/dist/chunks/chunk-CSKXLF33.js +0 -153
- package/dist/chunks/chunk-HUM5FQII.js +0 -160
- package/dist/chunks/render-page-76MLIT6M.js +0 -1421
- package/dist/chunks/runtime-JX75TKER.js +0 -9267
- package/dist/chunks/screenshot-66ARTBGR.js +0 -12
package/README.md
CHANGED
|
@@ -2,19 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
The in-app comment toolbar that host apps (Officers Assist, JebelAI) load at runtime from the App comments
|
|
4
4
|
worker as `/v1/toolbar/1.js`. Ported from the comment-service repo (`packages/core` and `packages/react`) on
|
|
5
|
-
2026-10-12; this is
|
|
5
|
+
2026-10-12; this is the source of record for the runtime.
|
|
6
6
|
|
|
7
7
|
- `src/core`: anchor model and resolution (`resolveAnchor`), DOM-free.
|
|
8
8
|
- `src/runtime`: the React runtime (`runtime.tsx` is the bundle entry), element capture and placement, sync,
|
|
9
|
-
attachments, screenshots
|
|
9
|
+
attachments, page screenshots (`page-image.ts`), styles, and the live update (`update.ts`, `build-id.ts`).
|
|
10
|
+
`loader.tsx` is the host-side `<CommentToolbar>`.
|
|
10
11
|
- `src/compiler`, `src/vite`, `src/server`: the Vite plugin that stamps components with `data-cs-cmp` /
|
|
11
12
|
`data-cs-file`, and the same-origin session route. Entry points `src/react.ts`, `src/vite.ts`, `src/server.ts`.
|
|
12
|
-
- Worker bundle: `bun scripts/app-comments/build-toolbar.mjs` writes `workers/app-comments/toolbar-dist/v1/toolbar/`
|
|
13
|
-
(checked in; see `docs/policies/generated-files-policy.md`). `bun scripts/dev/dev-app-comments.mjs` builds it too.
|
|
14
|
-
- npm package: `bun run build` here writes `dist/` (vite, react with the runtime bundled as fallback, server, and
|
|
15
|
-
declarations); publish with `npm publish` from this directory. Host apps install it and use
|
|
16
|
-
`@devaccel-labs/app-comments-toolbar/vite`, `/react` and `/server`; the `COMMENT_SERVICE_*` env names are unchanged.
|
|
17
13
|
- Tests: `tests/unit/app-comments-toolbar-*.spec.ts`.
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
## How the toolbar ships
|
|
16
|
+
|
|
17
|
+
The toolbar is released by deploying MyStatusFlow, nothing else (ADR-APPC-008):
|
|
18
|
+
|
|
19
|
+
- `bun scripts/app-comments/build-toolbar.mjs` writes `workers/app-comments/toolbar-dist/v1/toolbar/` (checked in;
|
|
20
|
+
see `docs/policies/generated-files-policy.md`) with a build id hashed from the runtime's source, stamped into
|
|
21
|
+
the bundle and written to `build.json`. `bun scripts/dev/dev-app-comments.mjs` builds it too.
|
|
22
|
+
- `bun run deploy:staging` (and the CI deploy) rebuilds it as its own phase and deploys the worker; a worker's
|
|
23
|
+
served assets count toward the deploy's change detection.
|
|
24
|
+
- A toolbar that is open reads `/v1/toolbar/build.json` when its live socket reconnects (a deploy restarts it),
|
|
25
|
+
when its tab becomes visible, and every five minutes. A different id makes it import the new bundle and remount
|
|
26
|
+
itself in place, once nobody is mid-draft, mid-reply, recording or editing.
|
|
27
|
+
|
|
28
|
+
## The npm package
|
|
29
|
+
|
|
30
|
+
Hosts install the package for the three things that are host-side by nature: the Vite plugin (build time), the
|
|
31
|
+
session route (server side) and the `<CommentToolbar>` loader. The package holds no runtime (since 1.1.0), so it
|
|
32
|
+
only changes when the loader contract (`runtimeVersion`) does. `bun run build` here writes `dist/`; publish with
|
|
33
|
+
`npm publish` from this directory. The `COMMENT_SERVICE_*` env names in hosts are unchanged.
|
|
34
|
+
|
|
35
|
+
`/v1` responses and the runtime's storage keys must stay compatible with whatever package version the hosts have
|
|
36
|
+
installed; the runtime itself is always the worker's current build.
|
package/dist/react.js
CHANGED
|
@@ -1,36 +1,139 @@
|
|
|
1
|
-
import {
|
|
2
|
-
captureHandoff,
|
|
3
|
-
runtimeVersion,
|
|
4
|
-
sessionFor,
|
|
5
|
-
whenIdle
|
|
6
|
-
} from "./chunks/chunk-CSKXLF33.js";
|
|
7
|
-
|
|
8
1
|
// src/runtime/loader.tsx
|
|
9
2
|
import { useEffect, useRef } from "react";
|
|
3
|
+
|
|
4
|
+
// src/runtime/handoff.ts
|
|
5
|
+
var key = "devaccel.comment.handoff";
|
|
6
|
+
var ttl = 30 * 60 * 1e3;
|
|
7
|
+
function readHandoff() {
|
|
8
|
+
try {
|
|
9
|
+
const value = JSON.parse(sessionStorage.getItem(key) ?? "null");
|
|
10
|
+
return value && Date.now() - value.at < ttl ? value : null;
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function write(value) {
|
|
16
|
+
try {
|
|
17
|
+
if (value.invite || value.thread) sessionStorage.setItem(key, JSON.stringify(value));
|
|
18
|
+
else sessionStorage.removeItem(key);
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function linkParams() {
|
|
23
|
+
const url = new URL(window.location.href);
|
|
24
|
+
const hash = new URLSearchParams(url.hash.slice(1));
|
|
25
|
+
return {
|
|
26
|
+
url,
|
|
27
|
+
hash,
|
|
28
|
+
invite: url.searchParams.get("cs-invite") ?? hash.get("cs-invite"),
|
|
29
|
+
thread: url.searchParams.get("cs-thread") ?? hash.get("cs-thread")
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function captureHandoff() {
|
|
33
|
+
if (typeof window === "undefined") return;
|
|
34
|
+
const { invite, thread } = linkParams();
|
|
35
|
+
if (!invite && !thread) return;
|
|
36
|
+
write({
|
|
37
|
+
...readHandoff(),
|
|
38
|
+
...invite ? { invite } : {},
|
|
39
|
+
...thread ? { thread } : {},
|
|
40
|
+
at: Date.now()
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/runtime/idle.ts
|
|
45
|
+
var FALLBACK_SLICE_MS = 8;
|
|
46
|
+
function whenIdle(task, timeout) {
|
|
47
|
+
if (typeof requestIdleCallback === "function") {
|
|
48
|
+
const id2 = requestIdleCallback(task, { timeout });
|
|
49
|
+
return () => cancelIdleCallback(id2);
|
|
50
|
+
}
|
|
51
|
+
const id = setTimeout(
|
|
52
|
+
() => {
|
|
53
|
+
const end = performance.now() + FALLBACK_SLICE_MS;
|
|
54
|
+
task({ didTimeout: false, timeRemaining: () => Math.max(0, end - performance.now()) });
|
|
55
|
+
},
|
|
56
|
+
Math.min(timeout, 200)
|
|
57
|
+
);
|
|
58
|
+
return () => clearTimeout(id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/runtime/runtime-version.ts
|
|
62
|
+
var runtimeVersion = 1;
|
|
63
|
+
|
|
64
|
+
// src/runtime/session.ts
|
|
65
|
+
var EXPIRY_MARGIN_MS = 6e4;
|
|
66
|
+
var UNKNOWN_EXPIRY_MS = 5 * 6e4;
|
|
67
|
+
function readJwt(token) {
|
|
68
|
+
try {
|
|
69
|
+
const part = token.split(".")[1];
|
|
70
|
+
if (!part) return null;
|
|
71
|
+
const pad = part.length % 4 === 0 ? "" : "=".repeat(4 - part.length % 4);
|
|
72
|
+
return JSON.parse(atob(part.replaceAll("-", "+").replaceAll("_", "/") + pad));
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function sessionFrom(payload) {
|
|
78
|
+
const value = payload;
|
|
79
|
+
if (!value?.enabled || !value.apiUrl || !value.project || !value.environment || !value.hostToken)
|
|
80
|
+
return null;
|
|
81
|
+
return {
|
|
82
|
+
apiUrl: value.apiUrl,
|
|
83
|
+
project: value.project,
|
|
84
|
+
environment: value.environment,
|
|
85
|
+
hostToken: value.hostToken
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async function fetchSession(path) {
|
|
89
|
+
const response = await fetch(path, {
|
|
90
|
+
headers: { accept: "application/json", "x-comment-service-origin": window.location.origin },
|
|
91
|
+
cache: "no-store"
|
|
92
|
+
});
|
|
93
|
+
return response.json();
|
|
94
|
+
}
|
|
95
|
+
function sessionSource(path, load = fetchSession, now = Date.now) {
|
|
96
|
+
let kept = null;
|
|
97
|
+
let pending = null;
|
|
98
|
+
return ({ refresh = false } = {}) => {
|
|
99
|
+
if (!refresh && kept && now() < kept.until) return Promise.resolve(kept.session);
|
|
100
|
+
if (pending) return pending;
|
|
101
|
+
pending = load(path).then((payload) => {
|
|
102
|
+
const session = sessionFrom(payload);
|
|
103
|
+
const expires = session ? readJwt(session.hostToken)?.exp : void 0;
|
|
104
|
+
const until = !session ? Infinity : expires ? expires * 1e3 - EXPIRY_MARGIN_MS : now() + UNKNOWN_EXPIRY_MS;
|
|
105
|
+
kept = { session, until };
|
|
106
|
+
return session;
|
|
107
|
+
}).finally(() => {
|
|
108
|
+
pending = null;
|
|
109
|
+
});
|
|
110
|
+
return pending;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
var sources = /* @__PURE__ */ new Map();
|
|
114
|
+
function sessionFor(path) {
|
|
115
|
+
let source = sources.get(path);
|
|
116
|
+
if (!source) {
|
|
117
|
+
source = sessionSource(path);
|
|
118
|
+
sources.set(path, source);
|
|
119
|
+
}
|
|
120
|
+
return source;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/runtime/loader.tsx
|
|
10
124
|
var IDLE_TIMEOUT_MS = 3e3;
|
|
11
125
|
captureHandoff();
|
|
12
|
-
|
|
13
|
-
async function loadRuntime(apiUrl, source) {
|
|
14
|
-
if (source === "bundled") return bundled();
|
|
126
|
+
async function loadRuntime(apiUrl) {
|
|
15
127
|
const url = `${apiUrl.replace(/\/+$/, "")}/v1/toolbar/${runtimeVersion}.js`;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
throw new Error(`${url} has no mount export`);
|
|
24
|
-
} catch (error) {
|
|
25
|
-
console.warn(
|
|
26
|
-
"[comment-service] Could not load the latest toolbar, using the copy bundled with the package.",
|
|
27
|
-
error
|
|
28
|
-
);
|
|
29
|
-
return bundled();
|
|
30
|
-
}
|
|
128
|
+
const remote = await import(
|
|
129
|
+
/* @vite-ignore */
|
|
130
|
+
/* webpackIgnore: true */
|
|
131
|
+
url
|
|
132
|
+
);
|
|
133
|
+
if (typeof remote.mount === "function") return remote;
|
|
134
|
+
throw new Error(`${url} has no mount export`);
|
|
31
135
|
}
|
|
32
136
|
function CommentToolbar({
|
|
33
|
-
source = "latest",
|
|
34
137
|
sessionPath = "/api/comment-service/session",
|
|
35
138
|
pollMs,
|
|
36
139
|
position,
|
|
@@ -39,17 +142,19 @@ function CommentToolbar({
|
|
|
39
142
|
}) {
|
|
40
143
|
const handle = useRef(null);
|
|
41
144
|
const navigateRef = useRef(navigate);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
145
|
+
const propsRef = useRef({});
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
navigateRef.current = navigate;
|
|
148
|
+
propsRef.current = {
|
|
149
|
+
sessionPath,
|
|
150
|
+
pollMs,
|
|
151
|
+
position,
|
|
152
|
+
theme,
|
|
153
|
+
navigate: navigate ? (href) => navigateRef.current?.(href) : void 0,
|
|
154
|
+
getSession: sessionFor(sessionPath)
|
|
155
|
+
};
|
|
156
|
+
handle.current?.update(propsRef.current);
|
|
157
|
+
}, [navigate, pollMs, position, sessionPath, theme]);
|
|
53
158
|
useEffect(() => {
|
|
54
159
|
let cancelled = false;
|
|
55
160
|
let mounted = null;
|
|
@@ -57,13 +162,13 @@ function CommentToolbar({
|
|
|
57
162
|
const start = async () => {
|
|
58
163
|
const session = await sessionFor(sessionPath)().catch(() => null);
|
|
59
164
|
if (!session || cancelled) return;
|
|
60
|
-
const runtime = await loadRuntime(session.apiUrl
|
|
165
|
+
const runtime = await loadRuntime(session.apiUrl);
|
|
61
166
|
if (cancelled) return;
|
|
62
167
|
container = document.createElement("div");
|
|
63
|
-
container.setAttribute("data-cs-runtime",
|
|
168
|
+
container.setAttribute("data-cs-runtime", "latest");
|
|
64
169
|
container.style.display = "contents";
|
|
65
170
|
document.body.append(container);
|
|
66
|
-
mounted = runtime.mount(container,
|
|
171
|
+
mounted = runtime.mount(container, propsRef.current);
|
|
67
172
|
handle.current = mounted;
|
|
68
173
|
};
|
|
69
174
|
const cancelIdle = whenIdle(() => {
|
|
@@ -80,10 +185,7 @@ function CommentToolbar({
|
|
|
80
185
|
container?.remove();
|
|
81
186
|
});
|
|
82
187
|
};
|
|
83
|
-
}, [sessionPath
|
|
84
|
-
useEffect(() => {
|
|
85
|
-
handle.current?.update(props.current);
|
|
86
|
-
}, [pollMs, position, theme, hasNavigate]);
|
|
188
|
+
}, [sessionPath]);
|
|
87
189
|
return null;
|
|
88
190
|
}
|
|
89
191
|
export {
|
package/dist/types/react.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { CommentToolbar
|
|
1
|
+
export { CommentToolbar } from "./runtime/loader.js";
|
|
2
2
|
export type { ToolbarPosition, ToolbarProps, ToolbarTheme } from "./runtime/toolbar.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identifies this build of the runtime. `scripts/app-comments/build-toolbar.mjs` defines it from a hash of the
|
|
3
|
+
* runtime's source and writes the same value to `/v1/toolbar/build.json` next to the bundle, so a running toolbar
|
|
4
|
+
* can tell when the worker started serving a newer build. "dev" when the bundle was built without it (tests).
|
|
5
|
+
*/
|
|
6
|
+
export declare const runtimeBuild: string;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { CommentToolbar
|
|
1
|
+
export { CommentToolbar } from "./loader.js";
|
|
2
2
|
export type { ToolbarPosition, ToolbarProps, ToolbarTheme } from "./toolbar.js";
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import type { ToolbarProps } from "./toolbar.js";
|
|
2
|
-
export type ToolbarSource = "latest" | "bundled";
|
|
3
2
|
/**
|
|
4
3
|
* Renders nothing and does nothing during render. Once the page is idle it asks the session route, once per page
|
|
5
4
|
* load, whether comments are on, and only then loads and mounts the toolbar.
|
|
6
5
|
*/
|
|
7
|
-
export declare function CommentToolbar({
|
|
8
|
-
/**
|
|
9
|
-
* "latest" (default) loads the newest compatible toolbar from the comment service, so fixes ship
|
|
10
|
-
* without a dependency upgrade, and falls back to the copy in this package if that fails.
|
|
11
|
-
* "bundled" always uses the copy in this package.
|
|
12
|
-
*/
|
|
13
|
-
source?: ToolbarSource;
|
|
14
|
-
}): null;
|
|
6
|
+
export declare function CommentToolbar({ sessionPath, pollMs, position, theme, navigate, }: ToolbarProps): null;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Draws the visible page onto a canvas without reading a computed style per element.
|
|
3
|
+
*
|
|
4
|
+
* The page is cloned natively (`cloneNode(true)`, a few milliseconds for thousands of nodes), the page's own
|
|
5
|
+
* stylesheets are inlined once (cached per page, with fonts and `url()` assets embedded as data URLs), and the
|
|
6
|
+
* result is rasterized through an SVG `<foreignObject>`. The previous renderer (modern-screenshot) copied the
|
|
7
|
+
* diff of ~400 computed properties for every element and its pseudo-elements, which cost about a second of
|
|
8
|
+
* main-thread time per capture on an ordinary page and grew with the page. This one spends the main thread on
|
|
9
|
+
* the clone walk and on the browser's own SVG layout, and nothing on the network once warmed.
|
|
10
|
+
*
|
|
11
|
+
* What is not carried over: cross-origin iframes (drawn blank), `<video>` frames (blank), styles that depend on
|
|
12
|
+
* `:hover`/`:focus` (dropped, which is what a screenshot should show anyway).
|
|
13
|
+
*/
|
|
14
|
+
export type PageImageOptions = {
|
|
15
|
+
/** Device pixels per CSS pixel in the output. */
|
|
16
|
+
scale: number;
|
|
17
|
+
/** Upper bound on the whole capture. */
|
|
18
|
+
timeout?: number;
|
|
19
|
+
/** Subtrees to leave out (the toolbar). */
|
|
20
|
+
exclude?: (element: Element) => boolean;
|
|
21
|
+
/** Painted behind the page. */
|
|
22
|
+
backgroundColor?: string;
|
|
23
|
+
/** CSS width/height of the captured area; defaults to the viewport. */
|
|
24
|
+
width?: number;
|
|
25
|
+
height?: number;
|
|
26
|
+
/** Window scroll offsets the page is shifted by. */
|
|
27
|
+
scrollX?: number;
|
|
28
|
+
scrollY?: number;
|
|
29
|
+
};
|
|
30
|
+
type Viewport = {
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
};
|
|
34
|
+
/** A data URL for `url`, or null when it can't be fetched in time. Never rejects. */
|
|
35
|
+
export declare function assetDataUrl(url: string, timeout?: number): Promise<string | null>;
|
|
36
|
+
/** Replaces every `url()` in `cssText` with a data URL. Unfetchable ones are left as they are. */
|
|
37
|
+
export declare function inlineCssUrls(cssText: string, baseHref: string): Promise<string>;
|
|
38
|
+
type StyleSource = {
|
|
39
|
+
key: string;
|
|
40
|
+
ownHref: string;
|
|
41
|
+
text: string | null;
|
|
42
|
+
href: string | null;
|
|
43
|
+
};
|
|
44
|
+
/** The sheets that style the page, read in document order. */
|
|
45
|
+
export declare function collectStyleSources(doc: Document): StyleSource[];
|
|
46
|
+
/** Every page style as one block of CSS with assets embedded. Cached; refreshed when a sheet is added or grows. */
|
|
47
|
+
export declare function pageStyleText(doc?: Document): Promise<string>;
|
|
48
|
+
/** Fetches fonts and CSS assets now, so the first capture pays nothing on the network. */
|
|
49
|
+
export declare function prewarmPageImage(doc?: Document): void;
|
|
50
|
+
/**
|
|
51
|
+
* The elements that paint inside the viewport, plus every ancestor of one (so the tree above them survives).
|
|
52
|
+
* Elements without a box (`display: none`, `<option>`, SVG `<defs>`, `display: contents` wrappers) are not
|
|
53
|
+
* listed: they neither show nor hide anything by themselves, so the clone keeps them under a kept parent.
|
|
54
|
+
*/
|
|
55
|
+
export declare function visibleSet(root: Element, viewport: Viewport): Set<Element>;
|
|
56
|
+
export type ClonedPage = {
|
|
57
|
+
root: Element;
|
|
58
|
+
waits: Promise<unknown>[];
|
|
59
|
+
elements: number;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* A copy of `root` ready to serialize: offscreen and non-painting subtrees removed, live form values, scroll
|
|
63
|
+
* offsets, canvases and images carried over. `waits` resolve when every image has its pixels.
|
|
64
|
+
*/
|
|
65
|
+
export declare function clonePage(root: Element, viewport: Viewport, exclude: (element: Element) => boolean, view?: Window): ClonedPage;
|
|
66
|
+
/** The SVG document that draws `clone` with `css` applied, `width`×`height` CSS pixels. */
|
|
67
|
+
export declare function pageSvg(clone: Element, css: string, width: number, height: number): string;
|
|
68
|
+
/** The visible page as a canvas. Throws when the page can't be drawn in time. */
|
|
69
|
+
export declare function drawPage(options: PageImageOptions): Promise<HTMLCanvasElement>;
|
|
70
|
+
export {};
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { type RuntimeProps } from "./toolbar.js";
|
|
2
|
+
export { runtimeBuild } from "./build-id.js";
|
|
2
3
|
export { runtimeVersion } from "./runtime-version.js";
|
|
3
4
|
export type { RuntimeProps } from "./toolbar.js";
|
|
4
5
|
export type ToolbarHandle = {
|
|
5
6
|
update(props: RuntimeProps): void;
|
|
6
7
|
unmount(): void;
|
|
7
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* Mounts the toolbar. The handle stays valid across a live update: when the worker starts serving a newer build,
|
|
11
|
+
* the toolbar loads it, unmounts this one and mounts the new one in the same container with the same props, and
|
|
12
|
+
* the handle the host holds forwards to it.
|
|
13
|
+
*/
|
|
8
14
|
export declare function mount(container: HTMLElement, props: RuntimeProps): ToolbarHandle;
|
|
@@ -3,6 +3,8 @@ import type { Capture } from "./client.js";
|
|
|
3
3
|
export declare function describeCapture(url: string): Capture;
|
|
4
4
|
/** The visible page as a canvas, toolbar left out. `timeout` bounds a slow frame. */
|
|
5
5
|
export declare function renderViewport(scale: number, timeout?: number): Promise<HTMLCanvasElement>;
|
|
6
|
+
/** Drops the reused render, for example after the page changed in a way the view key can't see. */
|
|
7
|
+
export declare function forgetDrawnPage(): void;
|
|
6
8
|
/** The whole visible page as a JPEG, toolbar left out. If `highlight` is on screen it's outlined. */
|
|
7
9
|
export declare function capturePage(highlight?: Element | null): Promise<Blob | null>;
|
|
8
10
|
/**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Between checks of the served build while the page stays open. Reconnects and tab switches also check. */
|
|
2
|
+
export declare const CHECK_EVERY_MS: number;
|
|
3
|
+
/** Loads a newer runtime at `url` and remounts the toolbar from it. Registered by the running runtime's `mount`. */
|
|
4
|
+
export type Swapper = (url: string) => Promise<void>;
|
|
5
|
+
export declare function registerSwapper(next: Swapper | null): void;
|
|
6
|
+
/** The bundle URL for `build`. The query defeats the module cache; the chunks it imports carry their own hashes. */
|
|
7
|
+
export declare function runtimeUrl(apiUrl: string, build: string): string;
|
|
8
|
+
/** The build the worker serves now, or null when it can't be read. Never throws. */
|
|
9
|
+
export declare function servedBuild(apiUrl: string, fetchFn?: typeof fetch): Promise<string | null>;
|
|
10
|
+
/** True when the worker serves a different build than the one running here. A "dev" build never swaps. */
|
|
11
|
+
export declare function needsSwap(served: string | null, current?: string): boolean;
|
|
12
|
+
/** Swaps to the build at `url` once. Resolves false when no runtime registered or a swap is under way. */
|
|
13
|
+
export declare function swapRuntime(url: string): Promise<boolean>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devaccel-labs/app-comments-toolbar",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "In-app comment toolbar for host apps on MyStatusFlow App comments: Vite plugin, <CommentToolbar>, and the same-origin session route.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"files": [
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"@babel/generator": "^7.28.5",
|
|
42
42
|
"@babel/parser": "^7.28.5",
|
|
43
43
|
"@babel/traverse": "^7.28.5",
|
|
44
|
-
"@babel/types": "^7.28.5"
|
|
45
|
-
"modern-screenshot": "4.7.0"
|
|
44
|
+
"@babel/types": "^7.28.5"
|
|
46
45
|
},
|
|
47
46
|
"devDependencies": {
|
|
47
|
+
"modern-screenshot": "4.7.0",
|
|
48
48
|
"esbuild": "0.28.1"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
// src/runtime/runtime-version.ts
|
|
2
|
-
var runtimeVersion = 1;
|
|
3
|
-
|
|
4
|
-
// src/runtime/handoff.ts
|
|
5
|
-
var key = "devaccel.comment.handoff";
|
|
6
|
-
var ttl = 30 * 60 * 1e3;
|
|
7
|
-
function readHandoff() {
|
|
8
|
-
try {
|
|
9
|
-
const value = JSON.parse(sessionStorage.getItem(key) ?? "null");
|
|
10
|
-
return value && Date.now() - value.at < ttl ? value : null;
|
|
11
|
-
} catch {
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
function write(value) {
|
|
16
|
-
try {
|
|
17
|
-
if (value.invite || value.thread) sessionStorage.setItem(key, JSON.stringify(value));
|
|
18
|
-
else sessionStorage.removeItem(key);
|
|
19
|
-
} catch {
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
function linkParams() {
|
|
23
|
-
const url = new URL(window.location.href);
|
|
24
|
-
const hash = new URLSearchParams(url.hash.slice(1));
|
|
25
|
-
return {
|
|
26
|
-
url,
|
|
27
|
-
hash,
|
|
28
|
-
invite: url.searchParams.get("cs-invite") ?? hash.get("cs-invite"),
|
|
29
|
-
thread: url.searchParams.get("cs-thread") ?? hash.get("cs-thread")
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
function captureHandoff() {
|
|
33
|
-
if (typeof window === "undefined") return;
|
|
34
|
-
const { invite, thread } = linkParams();
|
|
35
|
-
if (!invite && !thread) return;
|
|
36
|
-
write({
|
|
37
|
-
...readHandoff(),
|
|
38
|
-
...invite ? { invite } : {},
|
|
39
|
-
...thread ? { thread } : {},
|
|
40
|
-
at: Date.now()
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
function dropHandoff(part) {
|
|
44
|
-
const current = readHandoff();
|
|
45
|
-
if (!current) return;
|
|
46
|
-
const { [part]: _dropped, ...rest } = current;
|
|
47
|
-
write(rest);
|
|
48
|
-
}
|
|
49
|
-
function stripHandoffFromUrl() {
|
|
50
|
-
const { url, hash } = linkParams();
|
|
51
|
-
const inQuery = url.searchParams.has("cs-invite") || url.searchParams.has("cs-thread");
|
|
52
|
-
const inHash = hash.has("cs-invite") || hash.has("cs-thread");
|
|
53
|
-
if (!inQuery && !inHash) return;
|
|
54
|
-
url.searchParams.delete("cs-invite");
|
|
55
|
-
url.searchParams.delete("cs-thread");
|
|
56
|
-
if (inHash) url.hash = "";
|
|
57
|
-
history.replaceState(history.state, "", url);
|
|
58
|
-
}
|
|
59
|
-
function linkWith(href, name, value) {
|
|
60
|
-
const url = new URL(href, window.location.origin);
|
|
61
|
-
url.searchParams.delete("cs-invite");
|
|
62
|
-
url.searchParams.delete("cs-thread");
|
|
63
|
-
url.hash = `${name}=${encodeURIComponent(value)}`;
|
|
64
|
-
return url.toString();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// src/runtime/idle.ts
|
|
68
|
-
var FALLBACK_SLICE_MS = 8;
|
|
69
|
-
function whenIdle(task, timeout) {
|
|
70
|
-
if (typeof requestIdleCallback === "function") {
|
|
71
|
-
const id2 = requestIdleCallback(task, { timeout });
|
|
72
|
-
return () => cancelIdleCallback(id2);
|
|
73
|
-
}
|
|
74
|
-
const id = setTimeout(
|
|
75
|
-
() => {
|
|
76
|
-
const end = performance.now() + FALLBACK_SLICE_MS;
|
|
77
|
-
task({ didTimeout: false, timeRemaining: () => Math.max(0, end - performance.now()) });
|
|
78
|
-
},
|
|
79
|
-
Math.min(timeout, 200)
|
|
80
|
-
);
|
|
81
|
-
return () => clearTimeout(id);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// src/runtime/session.ts
|
|
85
|
-
var EXPIRY_MARGIN_MS = 6e4;
|
|
86
|
-
var UNKNOWN_EXPIRY_MS = 5 * 6e4;
|
|
87
|
-
function readJwt(token) {
|
|
88
|
-
try {
|
|
89
|
-
const part = token.split(".")[1];
|
|
90
|
-
if (!part) return null;
|
|
91
|
-
const pad = part.length % 4 === 0 ? "" : "=".repeat(4 - part.length % 4);
|
|
92
|
-
return JSON.parse(atob(part.replaceAll("-", "+").replaceAll("_", "/") + pad));
|
|
93
|
-
} catch {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
function sessionFrom(payload) {
|
|
98
|
-
const value = payload;
|
|
99
|
-
if (!value?.enabled || !value.apiUrl || !value.project || !value.environment || !value.hostToken)
|
|
100
|
-
return null;
|
|
101
|
-
return {
|
|
102
|
-
apiUrl: value.apiUrl,
|
|
103
|
-
project: value.project,
|
|
104
|
-
environment: value.environment,
|
|
105
|
-
hostToken: value.hostToken
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
async function fetchSession(path) {
|
|
109
|
-
const response = await fetch(path, {
|
|
110
|
-
headers: { accept: "application/json", "x-comment-service-origin": window.location.origin },
|
|
111
|
-
cache: "no-store"
|
|
112
|
-
});
|
|
113
|
-
return response.json();
|
|
114
|
-
}
|
|
115
|
-
function sessionSource(path, load = fetchSession, now = Date.now) {
|
|
116
|
-
let kept = null;
|
|
117
|
-
let pending = null;
|
|
118
|
-
return ({ refresh = false } = {}) => {
|
|
119
|
-
if (!refresh && kept && now() < kept.until) return Promise.resolve(kept.session);
|
|
120
|
-
if (pending) return pending;
|
|
121
|
-
pending = load(path).then((payload) => {
|
|
122
|
-
const session = sessionFrom(payload);
|
|
123
|
-
const expires = session ? readJwt(session.hostToken)?.exp : void 0;
|
|
124
|
-
const until = !session ? Infinity : expires ? expires * 1e3 - EXPIRY_MARGIN_MS : now() + UNKNOWN_EXPIRY_MS;
|
|
125
|
-
kept = { session, until };
|
|
126
|
-
return session;
|
|
127
|
-
}).finally(() => {
|
|
128
|
-
pending = null;
|
|
129
|
-
});
|
|
130
|
-
return pending;
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
var sources = /* @__PURE__ */ new Map();
|
|
134
|
-
function sessionFor(path) {
|
|
135
|
-
let source = sources.get(path);
|
|
136
|
-
if (!source) {
|
|
137
|
-
source = sessionSource(path);
|
|
138
|
-
sources.set(path, source);
|
|
139
|
-
}
|
|
140
|
-
return source;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export {
|
|
144
|
-
readHandoff,
|
|
145
|
-
captureHandoff,
|
|
146
|
-
dropHandoff,
|
|
147
|
-
stripHandoffFromUrl,
|
|
148
|
-
linkWith,
|
|
149
|
-
whenIdle,
|
|
150
|
-
runtimeVersion,
|
|
151
|
-
readJwt,
|
|
152
|
-
sessionFor
|
|
153
|
-
};
|