@replohq/sandbox-runtime 1.18.0 → 1.19.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 (44) hide show
  1. package/_vendor/replo-utils/lib/crypto.d.ts +1 -0
  2. package/_vendor/replo-utils/lib/crypto.mjs +1 -1
  3. package/_vendor/replo-utils/lib/fonts.mjs +2 -2
  4. package/_vendor/replo-utils/lib/math.mjs +3 -3
  5. package/_vendor/replo-utils/lib/misc.mjs +4 -4
  6. package/_vendor/replo-utils/lib/pageCapture.mjs +1 -1
  7. package/_vendor/schemas/errors.d.ts +0 -1
  8. package/_vendor/schemas/generated/pageHeatmap.d.ts +57 -0
  9. package/_vendor/schemas/generated/pageHeatmap.mjs +1 -0
  10. package/_vendor/schemas/iframeRpcChannel.mjs +6 -6
  11. package/_vendor/schemas/pageHeatmap.d.ts +132 -0
  12. package/_vendor/schemas/pageHeatmap.mjs +72 -0
  13. package/_vendor/schemas/previewHealthIframeRpc.mjs +6 -6
  14. package/_vendor/schemas/selectMode.d.ts +1 -1
  15. package/_vendor/schemas/selectMode.mjs +5 -5
  16. package/_vendor/schemas/selectModeIframeRpc.d.ts +2 -2
  17. package/_vendor/schemas/selectModeIframeRpc.mjs +5 -5
  18. package/buildMetadata.js +3 -3
  19. package/capturePage.js +5 -1
  20. package/{chunk-KRA4LUIR.mjs → chunk-36IPO47A.mjs} +2 -2
  21. package/{chunk-TP4ZXK4R.mjs → chunk-QTGDSYSU.mjs} +1 -1
  22. package/{chunk-FLYEIRFK.mjs → chunk-SKVPFN7M.mjs} +1 -1
  23. package/{chunk-PH3S6LJI.mjs → chunk-THOBCJJA.mjs} +2 -2
  24. package/{chunk-LIPH4O4F.mjs → chunk-ZTT7SNYN.mjs} +5 -2
  25. package/heatmaps/HeatLayerMount.d.ts +1 -0
  26. package/heatmaps/HeatLayerMount.js +12 -0
  27. package/heatmaps/heatBadges.d.ts +25 -0
  28. package/heatmaps/heatBadges.js +98 -0
  29. package/heatmaps/heatCore.d.ts +34 -0
  30. package/heatmaps/heatCore.js +465 -0
  31. package/heatmaps/heatLayer.d.ts +1 -0
  32. package/heatmaps/heatLayer.js +85 -0
  33. package/heatmaps/heatRender.d.ts +36 -0
  34. package/heatmaps/heatRender.js +198 -0
  35. package/package.json +4 -4
  36. package/sandbox-runtime.js +2 -0
  37. package/select-mode/hooks/useSelectionHover.js +5 -0
  38. package/select-mode/section-canvas/SectionCanvas.js +80 -124
  39. package/select-mode/section-canvas/sectionCanvasStore.d.ts +0 -2
  40. package/select-mode/section-canvas/sectionCanvasStore.js +0 -15
  41. package/select-mode/section-canvas/sectionGhost.d.ts +0 -5
  42. package/select-mode/section-canvas/sectionGhost.js +0 -23
  43. /package/{chunk-TESFJQID.mjs → chunk-4ITJNSXZ.mjs} +0 -0
  44. /package/{chunk-G3S6ZNPF.mjs → chunk-XYS6G2ZC.mjs} +0 -0
@@ -0,0 +1,34 @@
1
+ /** Heat drawn over one document: heat canvas or scroll bands, revenue badges, focus ring and revenue clicks. */
2
+ import type { PageHeatmapElementSection, PageHeatmapSetMessage } from "../_vendor/schemas/generated/pageHeatmap";
3
+ export declare const HEAT_LAYER_HOST_ID = "replo-heat-layer";
4
+ export type HeatLayerSet = Omit<PageHeatmapSetMessage, "type" | "protocolVersion">;
5
+ export interface HeatLayerDrawn {
6
+ setId: string;
7
+ placedWeight: number;
8
+ unplacedWeight: number;
9
+ sections: PageHeatmapElementSection[];
10
+ }
11
+ export interface HeatLayer {
12
+ /** Draws `set` (null clears the layer and stops observing the page). */
13
+ show: (set: HeatLayerSet | null) => void;
14
+ /** Outlines one element and scrolls it into view when it's off screen; null hides the outline. */
15
+ focus: (selector: string | null) => void;
16
+ destroy: () => void;
17
+ }
18
+ /**
19
+ * Draws heat sets over `document`, which can be this page or a same-origin
20
+ * iframe's. Inert until the first `show`.
21
+ */
22
+ export declare function createHeatLayer({ document: doc, onDrawn, onElementClicked, shouldIgnoreClick, paintAsImage, }: {
23
+ document: Document;
24
+ /** Fires when what was placed changes, not on every re-measure. */
25
+ onDrawn: (drawn: HeatLayerDrawn) => void;
26
+ /** Revenue mode: the innermost badged element under a page click, or null. */
27
+ onElementClicked: (selector: string | null) => void;
28
+ shouldIgnoreClick?: (target: Node) => boolean;
29
+ /**
30
+ * A document without scripting (a sandboxed site snapshot) renders a canvas's fallback content, not
31
+ * its bitmap, so the heat is drawn off-page and shown as an image.
32
+ */
33
+ paintAsImage?: boolean;
34
+ }): HeatLayer;
@@ -0,0 +1,465 @@
1
+ import { exhaustiveSwitch } from "../_vendor/replo-utils/lib/misc.mjs";
2
+ import { findClickedSelector, findSection, renderBadges } from "./heatBadges";
3
+ import {
4
+ drawHeatPoints,
5
+ drawMarkLine,
6
+ drawScrollBands,
7
+ getReachMarks,
8
+ getScrollReach
9
+ } from "./heatRender";
10
+ const HEAT_LAYER_HOST_ID = "replo-heat-layer";
11
+ const HEAT_LAYER_Z_INDEX = 2147483644;
12
+ const REMEASURE_DEBOUNCE_MS = 250;
13
+ const MAX_CANVAS_PIXELS = 16e6;
14
+ const CLICK_RADIUS_PX = 16;
15
+ const FOCUS_RING_PADDING_PX = 6;
16
+ const REVENUE_HEAT_OPACITY = "0.4";
17
+ function isNode(value) {
18
+ return typeof value === "object" && value !== null && "nodeType" in value;
19
+ }
20
+ function createHeatLayer({
21
+ document: doc,
22
+ onDrawn,
23
+ onElementClicked,
24
+ shouldIgnoreClick = () => false,
25
+ paintAsImage = false
26
+ }) {
27
+ let heatSet = null;
28
+ let focusSelector = null;
29
+ let lastDrawnReport = null;
30
+ let nodes = null;
31
+ let remeasureFrameId = null;
32
+ let remeasureDebounceId = null;
33
+ let resizeObserver = null;
34
+ let mutationObserver = null;
35
+ let observedView = null;
36
+ let clickView = null;
37
+ let badgedElements = [];
38
+ let imageUrl = null;
39
+ let imagePaint = 0;
40
+ const releaseImage = () => {
41
+ imagePaint += 1;
42
+ if (imageUrl) {
43
+ URL.revokeObjectURL(imageUrl);
44
+ imageUrl = null;
45
+ }
46
+ };
47
+ const paintImage = ({
48
+ canvas,
49
+ image
50
+ }) => {
51
+ image.style.cssText = `${canvas.style.cssText}; max-width: none; max-height: none`;
52
+ const paint = ++imagePaint;
53
+ canvas.toBlob((blob) => {
54
+ if (!blob || paint !== imagePaint || !image.isConnected) {
55
+ return;
56
+ }
57
+ const previous = imageUrl;
58
+ imageUrl = URL.createObjectURL(blob);
59
+ image.src = imageUrl;
60
+ if (previous) {
61
+ URL.revokeObjectURL(previous);
62
+ }
63
+ });
64
+ };
65
+ const ensureNodes = () => {
66
+ if (nodes?.host.isConnected) {
67
+ return nodes;
68
+ }
69
+ if (!doc.body) {
70
+ return null;
71
+ }
72
+ const host = doc.createElement("div");
73
+ host.id = HEAT_LAYER_HOST_ID;
74
+ host.setAttribute("aria-hidden", "true");
75
+ host.style.cssText = [
76
+ "position: absolute",
77
+ "top: 0",
78
+ "left: 0",
79
+ "width: 0",
80
+ "height: 0",
81
+ "margin: 0",
82
+ "padding: 0",
83
+ "pointer-events: none",
84
+ `z-index: ${HEAT_LAYER_Z_INDEX}`
85
+ ].join(";");
86
+ const canvas = doc.createElement("canvas");
87
+ canvas.style.cssText = [
88
+ "position: absolute",
89
+ "top: 0",
90
+ "left: 0",
91
+ "pointer-events: none"
92
+ ].join(";");
93
+ const focusRing = doc.createElement("div");
94
+ focusRing.style.cssText = [
95
+ "display: none",
96
+ "box-sizing: border-box",
97
+ "border-radius: 8px",
98
+ "pointer-events: none",
99
+ "box-shadow: 0 0 0 3px #fff, 0 0 0 6px rgba(17, 17, 17, 0.85)"
100
+ ].join(";");
101
+ const badges = doc.createElement("div");
102
+ badges.style.cssText = "position: absolute; top: 0; left: 0; pointer-events: none";
103
+ const image = paintAsImage ? doc.createElement("img") : null;
104
+ if (image) {
105
+ image.alt = "";
106
+ }
107
+ host.append(image ?? canvas, focusRing, badges);
108
+ doc.body.append(host);
109
+ nodes = { host, canvas, image, focusRing, badges };
110
+ return nodes;
111
+ };
112
+ const removeNodes = () => {
113
+ releaseImage();
114
+ nodes?.host.remove();
115
+ nodes = null;
116
+ };
117
+ const getPageSize = (host) => {
118
+ host.style.display = "none";
119
+ const root = doc.documentElement;
120
+ const width = Math.max(root.scrollWidth, doc.body.scrollWidth);
121
+ const height = Math.max(root.scrollHeight, doc.body.scrollHeight);
122
+ host.style.display = "";
123
+ return { width, height };
124
+ };
125
+ const hasFixedAncestor = (element) => {
126
+ const view = doc.defaultView;
127
+ let current = element;
128
+ while (current && current !== doc.documentElement) {
129
+ if (view?.getComputedStyle(current).position === "fixed") {
130
+ return true;
131
+ }
132
+ current = current.parentElement;
133
+ }
134
+ return false;
135
+ };
136
+ const findElement = (selector) => {
137
+ try {
138
+ return doc.querySelector(selector);
139
+ } catch {
140
+ return null;
141
+ }
142
+ };
143
+ const getScrollOffset = () => {
144
+ const scrollingElement = doc.scrollingElement;
145
+ return {
146
+ left: scrollingElement?.scrollLeft ?? doc.defaultView?.scrollX ?? 0,
147
+ top: scrollingElement?.scrollTop ?? doc.defaultView?.scrollY ?? 0
148
+ };
149
+ };
150
+ const getPageRect = (element) => {
151
+ const domRect = element.getBoundingClientRect();
152
+ if (domRect.width === 0 && domRect.height === 0) {
153
+ return null;
154
+ }
155
+ const isFixed = hasFixedAncestor(element);
156
+ const scroll = isFixed ? { left: 0, top: 0 } : getScrollOffset();
157
+ return {
158
+ x: domRect.x + scroll.left,
159
+ y: domRect.y + scroll.top,
160
+ width: domRect.width,
161
+ height: domRect.height,
162
+ isFixed
163
+ };
164
+ };
165
+ const createElementLookup = () => {
166
+ const elements = /* @__PURE__ */ new Map();
167
+ return (selector) => {
168
+ let element = elements.get(selector);
169
+ if (element === void 0) {
170
+ element = findElement(selector);
171
+ elements.set(selector, element);
172
+ }
173
+ return element;
174
+ };
175
+ };
176
+ const getClickPoints = ({
177
+ set,
178
+ lookup
179
+ }) => {
180
+ const rectsBySelector = /* @__PURE__ */ new Map();
181
+ const points = [];
182
+ let placedWeight = 0;
183
+ let unplacedWeight = 0;
184
+ for (const click of set.clicks) {
185
+ let rect = rectsBySelector.get(click.selector);
186
+ if (rect === void 0) {
187
+ const element = lookup(click.selector);
188
+ rect = element ? getPageRect(element) : null;
189
+ rectsBySelector.set(click.selector, rect);
190
+ }
191
+ if (!rect) {
192
+ unplacedWeight += click.weight;
193
+ continue;
194
+ }
195
+ placedWeight += click.weight;
196
+ points.push({
197
+ x: rect.x + click.rx * rect.width,
198
+ y: rect.y + click.ry * rect.height,
199
+ weight: click.weight
200
+ });
201
+ }
202
+ return { points, placedWeight, unplacedWeight };
203
+ };
204
+ const drawSet = ({
205
+ set,
206
+ canvas,
207
+ page,
208
+ lookup
209
+ }) => {
210
+ const resolution = Math.min(
211
+ 1,
212
+ Math.sqrt(MAX_CANVAS_PIXELS / Math.max(1, page.width * page.height))
213
+ );
214
+ canvas.width = Math.max(1, Math.round(page.width * resolution));
215
+ canvas.height = Math.max(1, Math.round(page.height * resolution));
216
+ canvas.style.width = `${page.width}px`;
217
+ canvas.style.height = `${page.height}px`;
218
+ const context = canvas.getContext("2d", { willReadFrequently: true });
219
+ const toCanvas = (point) => ({
220
+ x: point.x * resolution,
221
+ y: point.y * resolution,
222
+ weight: point.weight
223
+ });
224
+ canvas.style.opacity = set.mode === "revenue" ? REVENUE_HEAT_OPACITY : "1";
225
+ const drawClicks = () => {
226
+ const resolved = getClickPoints({ set, lookup });
227
+ if (context) {
228
+ drawHeatPoints({
229
+ context,
230
+ points: resolved.points.map(toCanvas),
231
+ radius: CLICK_RADIUS_PX * resolution
232
+ });
233
+ }
234
+ return {
235
+ placedWeight: resolved.placedWeight,
236
+ unplacedWeight: resolved.unplacedWeight
237
+ };
238
+ };
239
+ const result = exhaustiveSwitch({ type: set.mode })({
240
+ revenue: drawClicks,
241
+ clicks: drawClicks,
242
+ scroll: () => {
243
+ if (context) {
244
+ const reach = getScrollReach(set.scroll);
245
+ drawScrollBands({ context, reach });
246
+ for (const mark of getReachMarks({ reach, height: canvas.height })) {
247
+ drawMarkLine({
248
+ context,
249
+ y: mark.y,
250
+ label: `${Math.round(mark.share * 100)}% of visitors reach here`,
251
+ dashed: true,
252
+ scale: resolution
253
+ });
254
+ }
255
+ }
256
+ return { placedWeight: 0, unplacedWeight: 0 };
257
+ }
258
+ });
259
+ if (context && set.fold !== null && set.fold < page.height) {
260
+ drawMarkLine({
261
+ context,
262
+ y: set.fold * resolution,
263
+ label: "End of the first screen",
264
+ dashed: false,
265
+ scale: resolution
266
+ });
267
+ }
268
+ return result;
269
+ };
270
+ const positionFocusRing = (focusRing) => {
271
+ const element = focusSelector ? findElement(focusSelector) : null;
272
+ const rect = element ? getPageRect(element) : null;
273
+ if (!rect) {
274
+ focusRing.style.display = "none";
275
+ return null;
276
+ }
277
+ focusRing.style.display = "block";
278
+ focusRing.style.position = rect.isFixed ? "fixed" : "absolute";
279
+ focusRing.style.left = `${rect.x - FOCUS_RING_PADDING_PX}px`;
280
+ focusRing.style.top = `${rect.y - FOCUS_RING_PADDING_PX}px`;
281
+ focusRing.style.width = `${rect.width + FOCUS_RING_PADDING_PX * 2}px`;
282
+ focusRing.style.height = `${rect.height + FOCUS_RING_PADDING_PX * 2}px`;
283
+ return element;
284
+ };
285
+ const drawBadges = ({
286
+ set,
287
+ container,
288
+ lookup
289
+ }) => {
290
+ if (set.mode !== "revenue") {
291
+ badgedElements = [];
292
+ container.replaceChildren();
293
+ return;
294
+ }
295
+ badgedElements = set.badges.flatMap((badge) => {
296
+ const element = lookup(badge.selector);
297
+ return element ? [{ selector: badge.selector, element }] : [];
298
+ });
299
+ renderBadges({
300
+ container,
301
+ badges: set.badges,
302
+ rectFor: (selector) => {
303
+ const element = lookup(selector);
304
+ return element ? getPageRect(element) : null;
305
+ }
306
+ });
307
+ };
308
+ const findSections = ({
309
+ set,
310
+ lookup
311
+ }) => {
312
+ const selectors = /* @__PURE__ */ new Set([
313
+ ...set.clicks.map((click) => click.selector),
314
+ ...set.badges.map((badge) => badge.selector)
315
+ ]);
316
+ return [...selectors].flatMap((selector) => {
317
+ const element = lookup(selector);
318
+ const section = element ? findSection(element) : null;
319
+ return section ? [{ selector, section }] : [];
320
+ });
321
+ };
322
+ const handleRevenueClick = (event) => {
323
+ const target = event.target;
324
+ if (!isNode(target) || shouldIgnoreClick(target)) {
325
+ return;
326
+ }
327
+ event.preventDefault();
328
+ event.stopPropagation();
329
+ const selector = findClickedSelector({ target, badged: badgedElements });
330
+ focusSelector = selector;
331
+ if (nodes) {
332
+ positionFocusRing(nodes.focusRing);
333
+ }
334
+ onElementClicked(selector);
335
+ };
336
+ const syncClickInterception = () => {
337
+ const view = heatSet?.mode === "revenue" ? doc.defaultView : null;
338
+ if (view === clickView) {
339
+ return;
340
+ }
341
+ clickView?.removeEventListener("click", handleRevenueClick, true);
342
+ view?.addEventListener("click", handleRevenueClick, true);
343
+ clickView = view;
344
+ };
345
+ const scheduleRemeasure = () => {
346
+ remeasureFrameId ??= requestAnimationFrame(() => {
347
+ remeasureFrameId = null;
348
+ render();
349
+ });
350
+ };
351
+ const scheduleDebouncedRemeasure = () => {
352
+ if (remeasureDebounceId !== null) {
353
+ clearTimeout(remeasureDebounceId);
354
+ }
355
+ remeasureDebounceId = setTimeout(scheduleRemeasure, REMEASURE_DEBOUNCE_MS);
356
+ };
357
+ const stopObserving = () => {
358
+ observedView?.removeEventListener("resize", scheduleDebouncedRemeasure);
359
+ observedView = null;
360
+ resizeObserver?.disconnect();
361
+ resizeObserver = null;
362
+ mutationObserver?.disconnect();
363
+ mutationObserver = null;
364
+ if (remeasureDebounceId !== null) {
365
+ clearTimeout(remeasureDebounceId);
366
+ remeasureDebounceId = null;
367
+ }
368
+ if (remeasureFrameId !== null) {
369
+ cancelAnimationFrame(remeasureFrameId);
370
+ remeasureFrameId = null;
371
+ }
372
+ };
373
+ const syncObservers = () => {
374
+ const shouldObserve = heatSet !== null;
375
+ if (shouldObserve === (mutationObserver !== null)) {
376
+ return;
377
+ }
378
+ if (!shouldObserve) {
379
+ stopObserving();
380
+ return;
381
+ }
382
+ observedView = doc.defaultView;
383
+ observedView?.addEventListener("resize", scheduleDebouncedRemeasure);
384
+ if (typeof ResizeObserver !== "undefined") {
385
+ resizeObserver = new ResizeObserver(scheduleDebouncedRemeasure);
386
+ resizeObserver.observe(doc.body);
387
+ }
388
+ mutationObserver = new MutationObserver((records) => {
389
+ const host = nodes?.host;
390
+ const hasForeignMutation = records.some((record) => {
391
+ return !host || !host.contains(record.target);
392
+ });
393
+ if (hasForeignMutation) {
394
+ scheduleDebouncedRemeasure();
395
+ }
396
+ });
397
+ mutationObserver.observe(doc.body, { childList: true, subtree: true });
398
+ };
399
+ function render() {
400
+ if (!heatSet) {
401
+ removeNodes();
402
+ badgedElements = [];
403
+ syncObservers();
404
+ syncClickInterception();
405
+ return;
406
+ }
407
+ const layer = ensureNodes();
408
+ if (!layer) {
409
+ return;
410
+ }
411
+ const lookup = createElementLookup();
412
+ const page = getPageSize(layer.host);
413
+ const weights = drawSet({
414
+ set: heatSet,
415
+ canvas: layer.canvas,
416
+ page,
417
+ lookup
418
+ });
419
+ if (layer.image) {
420
+ paintImage({ canvas: layer.canvas, image: layer.image });
421
+ }
422
+ drawBadges({ set: heatSet, container: layer.badges, lookup });
423
+ positionFocusRing(layer.focusRing);
424
+ const sections = findSections({ set: heatSet, lookup });
425
+ const report = JSON.stringify([heatSet.setId, weights, sections]);
426
+ if (report !== lastDrawnReport) {
427
+ lastDrawnReport = report;
428
+ onDrawn({ setId: heatSet.setId, ...weights, sections });
429
+ }
430
+ syncObservers();
431
+ syncClickInterception();
432
+ }
433
+ const show = (set) => {
434
+ heatSet = set;
435
+ if (!set) {
436
+ focusSelector = null;
437
+ }
438
+ lastDrawnReport = null;
439
+ render();
440
+ };
441
+ return {
442
+ show,
443
+ focus(selector) {
444
+ focusSelector = selector;
445
+ if (!heatSet || !nodes) {
446
+ return;
447
+ }
448
+ const element = positionFocusRing(nodes.focusRing);
449
+ if (!element) {
450
+ return;
451
+ }
452
+ const viewportRect = element.getBoundingClientRect();
453
+ const viewportHeight = doc.defaultView?.innerHeight ?? 0;
454
+ const isInView = viewportRect.top >= 0 && viewportRect.bottom <= viewportHeight;
455
+ if (!isInView) {
456
+ element.scrollIntoView({ block: "center", behavior: "smooth" });
457
+ }
458
+ },
459
+ destroy: () => show(null)
460
+ };
461
+ }
462
+ export {
463
+ HEAT_LAYER_HOST_ID,
464
+ createHeatLayer
465
+ };
@@ -0,0 +1 @@
1
+ export declare function initHeatLayer(): void;
@@ -0,0 +1,85 @@
1
+ import { exhaustiveSwitch } from "../_vendor/replo-utils/lib/misc.mjs";
2
+ import {
3
+ PAGE_HEATMAP_PROTOCOL_VERSION,
4
+ pageHeatmapParentMessageSchema
5
+ } from "../_vendor/schemas/pageHeatmap.mjs";
6
+ import {
7
+ isAllowedEditorMessage,
8
+ isAllowedEditorOrigin
9
+ } from "../annotations/bridgeGate";
10
+ import { ANNOTATION_CONTAINER_ID } from "../select-mode/annotationFrames";
11
+ import { displayModeStore } from "../select-mode/displayModeStore";
12
+ import { createHeatLayer } from "./heatCore";
13
+ let isInitialized = false;
14
+ let editorOrigin = null;
15
+ let layer = null;
16
+ function postToEditor(message) {
17
+ if (!editorOrigin) {
18
+ return;
19
+ }
20
+ window.parent.postMessage(message, editorOrigin);
21
+ }
22
+ function getLayer() {
23
+ layer ??= createHeatLayer({
24
+ document,
25
+ onDrawn: (drawn) => {
26
+ postToEditor({ type: "replo:heatmap:drawn", ...drawn });
27
+ },
28
+ onElementClicked: (selector) => {
29
+ postToEditor({ type: "replo:heatmap:elementClicked", selector });
30
+ },
31
+ shouldIgnoreClick: (target) => {
32
+ return displayModeStore.getSnapshot() === "selection" || Boolean(
33
+ document.getElementById(ANNOTATION_CONTAINER_ID)?.contains(target)
34
+ );
35
+ }
36
+ });
37
+ return layer;
38
+ }
39
+ function handleMessage(event) {
40
+ if (!isAllowedEditorMessage(event)) {
41
+ return;
42
+ }
43
+ const parsed = pageHeatmapParentMessageSchema.safeParse(event.data);
44
+ if (!parsed.success) {
45
+ return;
46
+ }
47
+ editorOrigin = event.origin;
48
+ exhaustiveSwitch(parsed.data)({
49
+ "replo:heatmap:set": (message) => {
50
+ getLayer().show(message);
51
+ },
52
+ "replo:heatmap:focus": (message) => {
53
+ getLayer().focus(message.selector);
54
+ },
55
+ "replo:heatmap:clear": () => {
56
+ getLayer().show(null);
57
+ },
58
+ "replo:heatmap:requestReady": () => {
59
+ postToEditor({
60
+ type: "replo:heatmap:ready",
61
+ protocolVersion: PAGE_HEATMAP_PROTOCOL_VERSION
62
+ });
63
+ }
64
+ });
65
+ }
66
+ function initHeatLayer() {
67
+ if (isInitialized || window.self === window.top) {
68
+ return;
69
+ }
70
+ isInitialized = true;
71
+ window.addEventListener("message", handleMessage);
72
+ const referrerOrigin = URL.canParse(document.referrer) ? new URL(document.referrer).origin : null;
73
+ if (referrerOrigin && isAllowedEditorOrigin(referrerOrigin)) {
74
+ window.parent.postMessage(
75
+ {
76
+ type: "replo:heatmap:ready",
77
+ protocolVersion: PAGE_HEATMAP_PROTOCOL_VERSION
78
+ },
79
+ referrerOrigin
80
+ );
81
+ }
82
+ }
83
+ export {
84
+ initHeatLayer
85
+ };
@@ -0,0 +1,36 @@
1
+ /** Heat drawing: alpha-summed discs recoloured through a thermal palette, or scroll-reach bands. */
2
+ export interface HeatPoint {
3
+ x: number;
4
+ y: number;
5
+ weight: number;
6
+ }
7
+ /** Points are in canvas pixels; `radius` too. */
8
+ export declare function drawHeatPoints({ context, points, radius, }: {
9
+ context: CanvasRenderingContext2D;
10
+ points: HeatPoint[];
11
+ radius: number;
12
+ }): void;
13
+ /** `reach[p]` is the share of page views that saw p% of the page, from each view's deepest point. */
14
+ export declare function getScrollReach(buckets: {
15
+ pct: number;
16
+ views: number;
17
+ }[]): number[];
18
+ export declare function drawScrollBands({ context, reach, }: {
19
+ context: CanvasRenderingContext2D;
20
+ reach: number[];
21
+ }): void;
22
+ /** Page y (canvas px) where reach first drops below each mark. */
23
+ export declare function getReachMarks({ reach, height, }: {
24
+ reach: number[];
25
+ height: number;
26
+ }): {
27
+ share: number;
28
+ y: number;
29
+ }[];
30
+ export declare function drawMarkLine({ context, y, label, dashed, scale, }: {
31
+ context: CanvasRenderingContext2D;
32
+ y: number;
33
+ label: string;
34
+ dashed: boolean;
35
+ scale: number;
36
+ }): void;