@replohq/sandbox-runtime 1.18.1 → 1.19.1

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 (40) 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.mjs +5 -5
  15. package/_vendor/schemas/selectModeIframeRpc.mjs +5 -5
  16. package/buildMetadata.js +3 -3
  17. package/{chunk-KRA4LUIR.mjs → chunk-36IPO47A.mjs} +2 -2
  18. package/{chunk-TP4ZXK4R.mjs → chunk-QTGDSYSU.mjs} +1 -1
  19. package/{chunk-FLYEIRFK.mjs → chunk-SKVPFN7M.mjs} +1 -1
  20. package/{chunk-PH3S6LJI.mjs → chunk-THOBCJJA.mjs} +2 -2
  21. package/{chunk-LIPH4O4F.mjs → chunk-ZTT7SNYN.mjs} +5 -2
  22. package/heatmaps/HeatLayerMount.d.ts +1 -0
  23. package/heatmaps/HeatLayerMount.js +12 -0
  24. package/heatmaps/heatBadges.d.ts +25 -0
  25. package/heatmaps/heatBadges.js +98 -0
  26. package/heatmaps/heatCore.d.ts +34 -0
  27. package/heatmaps/heatCore.js +465 -0
  28. package/heatmaps/heatLayer.d.ts +1 -0
  29. package/heatmaps/heatLayer.js +85 -0
  30. package/heatmaps/heatRender.d.ts +36 -0
  31. package/heatmaps/heatRender.js +198 -0
  32. package/package.json +4 -4
  33. package/sandbox-runtime.js +2 -0
  34. package/select-mode/EditorSelection.js +5 -0
  35. package/select-mode/hooks/useSelectionHover.js +7 -1
  36. package/select-mode/hooks/useSelectionInteractions.js +6 -1
  37. package/select-mode/interactions/embeddedFrames.d.ts +17 -0
  38. package/select-mode/interactions/embeddedFrames.js +32 -0
  39. /package/{chunk-TESFJQID.mjs → chunk-4ITJNSXZ.mjs} +0 -0
  40. /package/{chunk-G3S6ZNPF.mjs → chunk-XYS6G2ZC.mjs} +0 -0
@@ -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;