@gem-sdk/clarity-visualize 0.8.94

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 (72) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -0
  3. package/build/clarity.visualize.js +2753 -0
  4. package/build/clarity.visualize.min.js +1 -0
  5. package/build/clarity.visualize.module.js +2748 -0
  6. package/package.json +45 -0
  7. package/rollup.config.ts +79 -0
  8. package/src/clarity.ts +3 -0
  9. package/src/custom/README.md +87 -0
  10. package/src/custom/canvas-layer.ts +355 -0
  11. package/src/custom/dialog.ts +242 -0
  12. package/src/custom/index.ts +7 -0
  13. package/src/data.ts +103 -0
  14. package/src/enrich.ts +80 -0
  15. package/src/global.ts +9 -0
  16. package/src/heatmap.ts +512 -0
  17. package/src/index.ts +4 -0
  18. package/src/interaction.ts +524 -0
  19. package/src/layout.ts +805 -0
  20. package/src/styles/blobUnavailable/chineseSimplified.svg +5 -0
  21. package/src/styles/blobUnavailable/chineseTraditional.svg +5 -0
  22. package/src/styles/blobUnavailable/dutch.svg +5 -0
  23. package/src/styles/blobUnavailable/english.svg +5 -0
  24. package/src/styles/blobUnavailable/french.svg +5 -0
  25. package/src/styles/blobUnavailable/german.svg +5 -0
  26. package/src/styles/blobUnavailable/iconOnly.svg +4 -0
  27. package/src/styles/blobUnavailable/italian.svg +5 -0
  28. package/src/styles/blobUnavailable/japanese.svg +5 -0
  29. package/src/styles/blobUnavailable/korean.svg +5 -0
  30. package/src/styles/blobUnavailable/portuguese.svg +5 -0
  31. package/src/styles/blobUnavailable/russian.svg +5 -0
  32. package/src/styles/blobUnavailable/spanish.svg +5 -0
  33. package/src/styles/blobUnavailable/turkish.svg +5 -0
  34. package/src/styles/iframeUnavailable/chineseSimplified.svg +5 -0
  35. package/src/styles/iframeUnavailable/chineseTraditional.svg +5 -0
  36. package/src/styles/iframeUnavailable/dutch.svg +5 -0
  37. package/src/styles/iframeUnavailable/english.svg +5 -0
  38. package/src/styles/iframeUnavailable/french.svg +5 -0
  39. package/src/styles/iframeUnavailable/german.svg +5 -0
  40. package/src/styles/iframeUnavailable/iconOnly.svg +4 -0
  41. package/src/styles/iframeUnavailable/italian.svg +5 -0
  42. package/src/styles/iframeUnavailable/japanese.svg +5 -0
  43. package/src/styles/iframeUnavailable/korean.svg +5 -0
  44. package/src/styles/iframeUnavailable/portuguese.svg +5 -0
  45. package/src/styles/iframeUnavailable/russian.svg +5 -0
  46. package/src/styles/iframeUnavailable/spanish.svg +5 -0
  47. package/src/styles/iframeUnavailable/turkish.svg +5 -0
  48. package/src/styles/imageMasked/chineseSimplified.svg +5 -0
  49. package/src/styles/imageMasked/chineseTraditional.svg +5 -0
  50. package/src/styles/imageMasked/dutch.svg +5 -0
  51. package/src/styles/imageMasked/english.svg +5 -0
  52. package/src/styles/imageMasked/french.svg +5 -0
  53. package/src/styles/imageMasked/german.svg +5 -0
  54. package/src/styles/imageMasked/iconOnly.svg +4 -0
  55. package/src/styles/imageMasked/italian.svg +5 -0
  56. package/src/styles/imageMasked/japanese.svg +5 -0
  57. package/src/styles/imageMasked/korean.svg +5 -0
  58. package/src/styles/imageMasked/portuguese.svg +5 -0
  59. package/src/styles/imageMasked/russian.svg +5 -0
  60. package/src/styles/imageMasked/spanish.svg +5 -0
  61. package/src/styles/imageMasked/turkish.svg +5 -0
  62. package/src/styles/pointer/click.css +31 -0
  63. package/src/styles/pointer/pointerIcon.svg +18 -0
  64. package/src/styles/shared.css +6 -0
  65. package/src/visualizer.ts +308 -0
  66. package/tsconfig.json +13 -0
  67. package/tslint.json +33 -0
  68. package/types/heatmap.d.ts +18 -0
  69. package/types/index.d.ts +11 -0
  70. package/types/layout.d.ts +12 -0
  71. package/types/string-import.d.ts +9 -0
  72. package/types/visualize.d.ts +253 -0
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@gem-sdk/clarity-visualize",
3
+ "version": "0.8.94",
4
+ "description": "Clarity visualize",
5
+ "author": "Microsoft Corp.",
6
+ "license": "MIT",
7
+ "main": "build/clarity.visualize.js",
8
+ "module": "build/clarity.visualize.module.js",
9
+ "unpkg": "build/clarity.visualize.min.js",
10
+ "types": "types/index.d.ts",
11
+ "dependencies": {
12
+ "@gem-sdk/clarity-decode": "^0.8.94"
13
+ },
14
+ "devDependencies": {
15
+ "@rollup/plugin-commonjs": "^24.0.0",
16
+ "@rollup/plugin-node-resolve": "^15.0.0",
17
+ "@rollup/plugin-terser": "^0.4.0",
18
+ "@rollup/plugin-typescript": "^11.0.0",
19
+ "rollup-plugin-string-import": "^1.2.5",
20
+ "del-cli": "^5.0.0",
21
+ "husky": "^8.0.0",
22
+ "lint-staged": "^13.1.0",
23
+ "rollup": "^3.0.0",
24
+ "tslint": "^6.1.3",
25
+ "typescript": "^4.3.5"
26
+ },
27
+ "scripts": {
28
+ "build": "yarn build:clean && yarn build:main",
29
+ "build:main": "rollup -c rollup.config.ts --configPlugin @rollup/plugin-typescript",
30
+ "build:clean": "del-cli build/*",
31
+ "tslint": "tslint --project ./",
32
+ "tslint:fix": "tslint --fix --project ./ --force"
33
+ },
34
+ "husky": {
35
+ "hooks": {
36
+ "pre-commit": "lint-staged"
37
+ }
38
+ },
39
+ "lint-staged": {
40
+ "*.ts": [
41
+ "tslint --format codeFrame"
42
+ ]
43
+ },
44
+ "source": "src/index.ts"
45
+ }
@@ -0,0 +1,79 @@
1
+ import commonjs from "@rollup/plugin-commonjs";
2
+ import resolve from "@rollup/plugin-node-resolve";
3
+ import terser from "@rollup/plugin-terser";
4
+ import typescript from "@rollup/plugin-typescript";
5
+ import { importAsString } from 'rollup-plugin-string-import';
6
+ import { readFileSync } from "fs";
7
+
8
+ const pkg = JSON.parse(readFileSync("./package.json", "utf-8"));
9
+
10
+ function wrapWithBackground(svg){
11
+ return `background: url("data:image/svg+xml,${svg}") no-repeat center center;`;
12
+ }
13
+
14
+ function regexEncode(svg){
15
+ svg = svg.replace(/>\s{1,}</g, `><`);
16
+ svg = svg.replace(/\s{2,}/g, ` `);
17
+ return svg.replace(/[\r\n%#()<>?[\\\]^`{|}]/g, encodeURIComponent)
18
+ }
19
+
20
+ function doublesToSingles(svg){
21
+ return svg.replace(/"/g, "'")
22
+ }
23
+
24
+ function preprocessFile(content: string, fileName: string): string {
25
+ if (fileName.endsWith(".css")) {
26
+ // remove comments/newlines
27
+ return content.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "").replace(/\r\n/g, "");
28
+ }
29
+ else if (fileName.endsWith(".svg")) {
30
+ // removes comments/newlines while also encoding the svg and adding some scaffolding to import it
31
+ return wrapWithBackground(regexEncode(doublesToSingles(content)));
32
+ }
33
+ return "";
34
+ }
35
+
36
+ export default [
37
+ {
38
+ input: "src/index.ts",
39
+ output: [
40
+ { file: pkg.main, format: "cjs", exports: "named" },
41
+ { file: pkg.module, format: "es", exports: "named" }
42
+ ],
43
+ plugins: [
44
+ resolve(),
45
+ importAsString({
46
+ include: ["**/*.css", "**/*.svg"],
47
+ transform: preprocessFile,
48
+ }),
49
+ typescript(),
50
+ commonjs({ include: ["node_modules/**"] })
51
+ ],
52
+ onwarn(message, warn) {
53
+ if (message.code === 'NON_EXISTENT_EXPORT') { return; }
54
+ if (message.code === 'CIRCULAR_DEPENDENCY') { return; }
55
+ if (message.code === 'SOURCEMAP_ERROR') { return; }
56
+ warn(message);
57
+ }
58
+ },
59
+ {
60
+ input: "src/global.ts",
61
+ output: [ { file: pkg.unpkg, format: "iife", exports: "named" } ],
62
+ plugins: [
63
+ resolve(),
64
+ importAsString({
65
+ include: ["**/*.css", "**/*.svg"],
66
+ transform: preprocessFile,
67
+ }),
68
+ typescript(),
69
+ terser({output: {comments: false}}),
70
+ commonjs({ include: ["node_modules/**"] })
71
+ ],
72
+ onwarn(message, warn) {
73
+ if (message.code === 'NON_EXISTENT_EXPORT') { return; }
74
+ if (message.code === 'CIRCULAR_DEPENDENCY') { return; }
75
+ if (message.code === 'SOURCEMAP_ERROR') { return; }
76
+ warn(message);
77
+ }
78
+ }
79
+ ];
package/src/clarity.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { Visualizer } from "./visualizer";
2
+
3
+ export const { state, dom, get, html, time, clickmap, clearmap, scrollmap, merge, setup, render } = new Visualizer();
@@ -0,0 +1,87 @@
1
+ # Custom Clarity Visualize Modules
2
+
3
+ This directory contains custom rendering modules that extend the base Clarity visualization for GemX.
4
+
5
+ ## Structure
6
+
7
+ All custom modules follow the functional module pattern:
8
+ - Pure functions for testability
9
+ - No side effects in utility functions
10
+ - Clear separation of concerns
11
+ - Type-safe interfaces
12
+
13
+ ## Modules
14
+
15
+ ### Canvas Layer Module (`canvas-layer.ts`)
16
+
17
+ Manages canvas z-index and stacking context with top-layer elements (modal dialogs).
18
+
19
+ **Functions:**
20
+
21
+ - `getOpenDialogs(doc)` - Gets all open modal dialogs
22
+ - `hideDialogsTemporarily(doc)` - Hides modal dialogs and returns restore function
23
+ - `setupCanvasLayer(canvas, options)` - Sets up canvas with proper layer handling
24
+ - `applyCanvasLayerWithDialogAwareness(canvas, onShow, onHide)` - Dialog-aware canvas management
25
+
26
+ **Usage:**
27
+
28
+ ```typescript
29
+ import * as canvasLayer from "./custom/canvas-layer";
30
+
31
+ // Hide dialogs when showing heatmap
32
+ const restoreDialogs = canvasLayer.hideDialogsTemporarily(doc);
33
+
34
+ // Later, restore dialogs
35
+ restoreDialogs();
36
+ ```
37
+
38
+ **Why this is needed:**
39
+
40
+ Modal dialogs use the browser's **top-layer**, which has a higher stacking context than any z-index value (even `z-index: 2147483647`). This module temporarily hides modal dialogs when the heatmap canvas is active, ensuring the canvas is visible.
41
+
42
+ ### Dialog Module (`dialog.ts`)
43
+
44
+ Renders HTML `<dialog>` elements with proper top-layer and backdrop support.
45
+
46
+ **Functions:**
47
+
48
+ - `getDialogRenderOptions(attributes, dialogElement)` - Extracts rendering options from attributes
49
+ - `showDialog(dialogElement, isModal, onError)` - Shows dialog with proper modal/non-modal handling
50
+ - `closeDialog(dialogElement)` - Safely closes a dialog
51
+ - `renderDialog(dialogElement, options, onError)` - Main entry point for dialog rendering
52
+ - `cleanDialogAttributes(attributes)` - Removes internal tracking attributes
53
+
54
+ **Usage:**
55
+
56
+ ```typescript
57
+ import * as dialogCustom from "./custom/dialog";
58
+
59
+ const renderOptions = dialogCustom.getDialogRenderOptions(node.attributes, dialogElement);
60
+ node.attributes = dialogCustom.cleanDialogAttributes(node.attributes);
61
+
62
+ dialogCustom.renderDialog(
63
+ dialogElement,
64
+ renderOptions,
65
+ this.state.options.logerror
66
+ );
67
+ ```
68
+
69
+ **Features:**
70
+
71
+ - Automatic detection of modal vs non-modal dialogs
72
+ - Proper top-layer rendering via `showModal()`
73
+ - Backdrop support for modal dialogs
74
+ - Handles dialogs already open from HTML attributes
75
+ - Error handling and logging
76
+
77
+ ## Adding New Custom Modules
78
+
79
+ 1. Create a new file in this directory (e.g., `my-feature.ts`)
80
+ 2. Follow the functional module pattern
81
+ 3. Export functions with clear type signatures
82
+ 4. Add exports to `index.ts`
83
+ 5. Document the module in this README
84
+
85
+ ## Testing
86
+
87
+ Custom modules should be unit tested separately from the main Clarity codebase.
@@ -0,0 +1,355 @@
1
+ import { Constant } from "@clarity-types/visualize";
2
+
3
+ /**
4
+ * Custom module for managing canvas layer z-index with dialog top-layer
5
+ * Handles stacking context when modal dialogs are present
6
+ *
7
+ * Strategy: Render canvas in parent window (outside iframe) to avoid
8
+ * z-index conflicts with top-layer elements inside the iframe
9
+ */
10
+
11
+ export interface CanvasLayerOptions {
12
+ strategy: 'parent-window' | 'max-z-index' | 'hide-dialogs' | 'popover';
13
+ }
14
+
15
+ export interface ParentCanvasInfo {
16
+ canvas: HTMLCanvasElement;
17
+ iframe: HTMLIFrameElement;
18
+ cleanup: () => void;
19
+ }
20
+
21
+ /**
22
+ * Creates a canvas as sibling to the iframe (same parent container)
23
+ * This allows the canvas to avoid z-index conflicts with top-layer elements inside iframe
24
+ *
25
+ * @param iframeDoc - Document inside the iframe
26
+ * @param canvasId - ID for the canvas element
27
+ * @returns Canvas info with cleanup function
28
+ */
29
+ export function createParentWindowCanvas(
30
+ iframeDoc: Document,
31
+ portalCanvasId: string
32
+ ): ParentCanvasInfo | null {
33
+ // Check if we're inside an iframe
34
+ if (iframeDoc.defaultView === iframeDoc.defaultView?.top) {
35
+ throw new Error('Not in iframe: ' + iframeDoc.defaultView);
36
+ }
37
+
38
+ try {
39
+ const parentWindow = iframeDoc.defaultView?.parent;
40
+ const parentDoc = parentWindow?.document;
41
+ if (!parentDoc) throw new Error('Parent document not found');
42
+
43
+ const iframeId = `clarity-iframe-${portalCanvasId}`;
44
+ const iframe = parentDoc.getElementById(iframeId);
45
+ if (!iframe) throw new Error('Iframe not found');
46
+
47
+ const targetIframe = iframe as HTMLIFrameElement;
48
+ const iframeParent = targetIframe.parentElement;
49
+ if (!iframeParent) throw new Error('Iframe parent not found');
50
+
51
+ // Create canvas as sibling to iframe
52
+ const canvasId = `${Constant.HeatmapCanvas}-${portalCanvasId}`;
53
+ let canvas = parentDoc.getElementById(canvasId) as HTMLCanvasElement;
54
+ if (canvas === null) {
55
+ canvas = parentDoc.createElement('canvas');
56
+ canvas.id = canvasId;
57
+ canvas.classList.add(Constant.HeatmapCanvas);
58
+ canvas.width = 0;
59
+ canvas.height = 0;
60
+ canvas.style.position = Constant.Absolute;
61
+ // canvas.style.zIndex = `${Setting.ZIndex}`;
62
+ canvas.style.pointerEvents = 'none'; // Allow clicks to pass through
63
+ iframeParent.appendChild(canvas);
64
+ }
65
+
66
+ // // Set canvas size to match iframe
67
+ // const updateCanvasSize = () => {
68
+ // canvas.width = targetIframe!.offsetWidth;
69
+ // canvas.height = targetIframe!.offsetHeight;
70
+ // };
71
+
72
+ // updateCanvasSize();
73
+
74
+ // Insert canvas as sibling to iframe (in same parent container)
75
+ // iframeParent.appendChild(canvas);
76
+
77
+ // Update size on resize
78
+ // const handleUpdate = () => updateCanvasSize();
79
+ // parentWindow!.addEventListener('resize', handleUpdate, true);
80
+
81
+ const cleanup = () => {
82
+ // parentWindow!.removeEventListener('resize', handleUpdate, true);
83
+ if (canvas.parentNode) {
84
+ canvas.parentNode.removeChild(canvas);
85
+ }
86
+ };
87
+
88
+ return {
89
+ canvas,
90
+ iframe: targetIframe,
91
+ cleanup
92
+ };
93
+ } catch (e) {
94
+ throw new Error(e);
95
+ }
96
+ }
97
+
98
+ // ======================================================== //
99
+ // ======================================================== //
100
+
101
+
102
+ /**
103
+ * Gets all open modal dialogs in the document
104
+ */
105
+ export function getOpenDialogs(doc: Document): HTMLDialogElement[] {
106
+ const dialogs = Array.from(doc.querySelectorAll('dialog[open]'));
107
+ return dialogs.filter(d => {
108
+ const dialog = d as HTMLDialogElement;
109
+ // Check if it's a modal dialog (in top-layer)
110
+ try {
111
+ const backdropStyle = window.getComputedStyle(dialog, '::backdrop');
112
+ return backdropStyle.getPropertyValue('display') !== 'none';
113
+ } catch {
114
+ return false;
115
+ }
116
+ }) as HTMLDialogElement[];
117
+ }
118
+
119
+ /**
120
+ * Temporarily hides modal dialogs to allow canvas to be visible
121
+ * Returns a restore function to bring dialogs back
122
+ */
123
+ export function hideDialogsTemporarily(doc: Document): () => void {
124
+ const dialogs = getOpenDialogs(doc);
125
+ const dialogStates: Array<{ dialog: HTMLDialogElement; wasOpen: boolean }> = [];
126
+
127
+ dialogs.forEach(dialog => {
128
+ if (dialog.open) {
129
+ dialogStates.push({ dialog, wasOpen: true });
130
+ dialog.close();
131
+ }
132
+ });
133
+
134
+ // Return restore function
135
+ return () => {
136
+ dialogStates.forEach(({ dialog, wasOpen }) => {
137
+ if (wasOpen && !dialog.open) {
138
+ try {
139
+ dialog.showModal();
140
+ } catch (e) {
141
+ console.warn('Failed to restore dialog:', e);
142
+ }
143
+ }
144
+ });
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Sets up canvas with proper z-index handling
150
+ *
151
+ * @param canvas - The canvas element
152
+ * @param options - Layer options
153
+ * @returns Cleanup function
154
+ */
155
+ export function setupCanvasLayer(
156
+ canvas: HTMLCanvasElement,
157
+ options: CanvasLayerOptions = { strategy: 'max-z-index' }
158
+ ): () => void {
159
+ switch (options.strategy) {
160
+ case 'hide-dialogs':
161
+ // Hide dialogs when canvas is active
162
+ return hideDialogsTemporarily(canvas.ownerDocument);
163
+
164
+ case 'popover':
165
+ // Use Popover API if available (also uses top-layer)
166
+ if ('popover' in canvas) {
167
+ try {
168
+ (canvas as any).popover = 'manual';
169
+ (canvas as any).showPopover();
170
+ return () => {
171
+ try {
172
+ (canvas as any).hidePopover();
173
+ } catch (e) {
174
+ // Ignore
175
+ }
176
+ };
177
+ } catch (e) {
178
+ console.warn('Popover API not available, falling back to max z-index');
179
+ }
180
+ }
181
+ // Fallback to max-z-index
182
+ return () => {};
183
+
184
+ case 'max-z-index':
185
+ default:
186
+ // Use maximum z-index (works for non-modal content)
187
+ // Modal dialogs will still be on top due to top-layer
188
+ return () => {};
189
+ }
190
+ }
191
+
192
+
193
+
194
+ /**
195
+ * Synchronizes canvas content from iframe to parent window canvas
196
+ *
197
+ * @param iframeCanvas - Canvas inside iframe
198
+ * @param parentCanvas - Canvas in parent window
199
+ */
200
+ export function syncCanvasContent(
201
+ iframeCanvas: HTMLCanvasElement,
202
+ parentCanvas: HTMLCanvasElement
203
+ ): void {
204
+ const ctx = parentCanvas.getContext('2d');
205
+ if (ctx) {
206
+ ctx.clearRect(0, 0, parentCanvas.width, parentCanvas.height);
207
+ ctx.drawImage(iframeCanvas, 0, 0);
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Alternative approach: Make dialogs "transparent" for rendering
213
+ * Sets pointer-events: none on dialogs so canvas can render on top
214
+ */
215
+ export function makeDialogsTransparent(doc: Document): () => void {
216
+ const dialogs = Array.from(doc.querySelectorAll('dialog[open]')) as HTMLDialogElement[];
217
+ const originalStyles: Map<HTMLDialogElement, string> = new Map();
218
+
219
+ dialogs.forEach((dialog) => {
220
+ originalStyles.set(dialog, dialog.style.pointerEvents || '');
221
+ dialog.style.pointerEvents = 'none';
222
+ dialog.style.zIndex = '2147483646'; // Just below canvas
223
+ });
224
+
225
+ // Return restore function
226
+ return () => {
227
+ dialogs.forEach((dialog) => {
228
+ const original = originalStyles.get(dialog);
229
+ if (original !== undefined) {
230
+ dialog.style.pointerEvents = original;
231
+ dialog.style.zIndex = '';
232
+ }
233
+ });
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Solution 3: Lower dialog z-index temporarily
239
+ * Moves dialog out of top-layer by closing and re-showing as non-modal
240
+ */
241
+ export function lowerDialogZIndex(doc: Document): () => void {
242
+ const dialogs = getOpenDialogs(doc);
243
+ const dialogStates: Array<{ dialog: HTMLDialogElement; wasModal: boolean }> = [];
244
+
245
+ dialogs.forEach((dialog) => {
246
+ const wasModal = true; // If it's in getOpenDialogs, it's modal
247
+ dialogStates.push({ dialog, wasModal });
248
+
249
+ // Close modal and reopen as non-modal (removes from top-layer)
250
+ dialog.close();
251
+ dialog.show(); // Non-modal, can be controlled by z-index
252
+ dialog.style.zIndex = '2147483646'; // Below canvas
253
+ });
254
+
255
+ // Return restore function
256
+ return () => {
257
+ dialogStates.forEach(({ dialog, wasModal }) => {
258
+ if (wasModal) {
259
+ dialog.close();
260
+ try {
261
+ dialog.showModal(); // Restore to top-layer
262
+ dialog.style.zIndex = '';
263
+ } catch (e) {
264
+ console.warn('Failed to restore modal dialog:', e);
265
+ }
266
+ }
267
+ });
268
+ };
269
+ }
270
+
271
+ /**
272
+ * Solution 4: Use mix-blend-mode to render canvas "through" dialog
273
+ * This is experimental and may not work in all browsers
274
+ */
275
+ export function setupCanvasBlendMode(canvas: HTMLCanvasElement): void {
276
+ canvas.style.mixBlendMode = 'multiply';
277
+ canvas.style.isolation = 'isolate';
278
+ }
279
+
280
+ /**
281
+ * Master function: Apply best strategy based on context
282
+ * Tries multiple approaches in order of preference
283
+ */
284
+ export function applyBestCanvasStrategy(
285
+ doc: Document,
286
+ canvasId: string,
287
+ strategy?: 'parent-window' | 'lower-dialog' | 'transparent' | 'auto'
288
+ ): { canvas: HTMLCanvasElement | null; cleanup: () => void } | null {
289
+ const selectedStrategy = strategy || 'auto';
290
+
291
+ // Strategy 1: Parent window canvas (best for iframe contexts)
292
+ if (selectedStrategy === 'auto' || selectedStrategy === 'parent-window') {
293
+ const parentCanvas = createParentWindowCanvas(doc, canvasId);
294
+ if (parentCanvas) {
295
+ return {
296
+ canvas: parentCanvas.canvas,
297
+ cleanup: parentCanvas.cleanup
298
+ };
299
+ }
300
+ }
301
+
302
+ // Strategy 2: Lower dialog z-index (converts modal to non-modal temporarily)
303
+ if (selectedStrategy === 'auto' || selectedStrategy === 'lower-dialog') {
304
+ const canvas = doc.getElementById(canvasId) as HTMLCanvasElement;
305
+ if (canvas) {
306
+ const restore = lowerDialogZIndex(doc);
307
+ return {
308
+ canvas,
309
+ cleanup: restore
310
+ };
311
+ }
312
+ }
313
+
314
+ // Strategy 3: Make dialogs transparent (least intrusive)
315
+ if (selectedStrategy === 'auto' || selectedStrategy === 'transparent') {
316
+ const canvas = doc.getElementById(canvasId) as HTMLCanvasElement;
317
+ if (canvas) {
318
+ const restore = makeDialogsTransparent(doc);
319
+ return {
320
+ canvas,
321
+ cleanup: restore
322
+ };
323
+ }
324
+ }
325
+
326
+ return null;
327
+ }
328
+
329
+ /**
330
+ * Applies canvas layer with dialog awareness
331
+ * This is the recommended approach: hide dialogs temporarily while heatmap is active
332
+ */
333
+ export function applyCanvasLayerWithDialogAwareness(
334
+ canvas: HTMLCanvasElement,
335
+ onShow?: () => void,
336
+ onHide?: () => void
337
+ ): { show: () => void; hide: () => void } {
338
+ let restoreDialogs: (() => void) | null = null;
339
+
340
+ return {
341
+ show: () => {
342
+ // Hide dialogs when showing canvas
343
+ restoreDialogs = hideDialogsTemporarily(canvas.ownerDocument);
344
+ if (onShow) onShow();
345
+ },
346
+ hide: () => {
347
+ // Restore dialogs when hiding canvas
348
+ if (restoreDialogs) {
349
+ restoreDialogs();
350
+ restoreDialogs = null;
351
+ }
352
+ if (onHide) onHide();
353
+ }
354
+ };
355
+ }