kash-shell 0.3.17__py3-none-any.whl → 0.3.18__py3-none-any.whl

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 (37) hide show
  1. kash/actions/core/minify_html.py +41 -0
  2. kash/commands/base/show_command.py +11 -1
  3. kash/config/colors.py +6 -2
  4. kash/docs/markdown/topics/a1_what_is_kash.md +52 -23
  5. kash/docs/markdown/topics/a2_installation.md +17 -30
  6. kash/docs/markdown/topics/a3_getting_started.md +5 -19
  7. kash/exec/action_exec.py +1 -1
  8. kash/exec/fetch_url_metadata.py +9 -0
  9. kash/exec/precondition_registry.py +3 -3
  10. kash/file_storage/file_store.py +18 -1
  11. kash/llm_utils/llm_features.py +5 -1
  12. kash/llm_utils/llms.py +18 -8
  13. kash/media_base/media_cache.py +48 -24
  14. kash/media_base/media_services.py +63 -14
  15. kash/media_base/services/local_file_media.py +9 -1
  16. kash/model/items_model.py +4 -5
  17. kash/model/media_model.py +9 -1
  18. kash/model/params_model.py +9 -3
  19. kash/utils/common/function_inspect.py +97 -1
  20. kash/utils/common/testing.py +58 -0
  21. kash/utils/common/url_slice.py +329 -0
  22. kash/utils/file_utils/file_formats.py +1 -1
  23. kash/utils/text_handling/markdown_utils.py +424 -16
  24. kash/web_gen/templates/base_styles.css.jinja +137 -15
  25. kash/web_gen/templates/base_webpage.html.jinja +13 -17
  26. kash/web_gen/templates/components/toc_scripts.js.jinja +319 -0
  27. kash/web_gen/templates/components/toc_styles.css.jinja +284 -0
  28. kash/web_gen/templates/components/tooltip_scripts.js.jinja +730 -0
  29. kash/web_gen/templates/components/tooltip_styles.css.jinja +482 -0
  30. kash/web_gen/templates/content_styles.css.jinja +13 -8
  31. kash/web_gen/templates/simple_webpage.html.jinja +15 -481
  32. kash/workspaces/workspaces.py +10 -1
  33. {kash_shell-0.3.17.dist-info → kash_shell-0.3.18.dist-info}/METADATA +75 -72
  34. {kash_shell-0.3.17.dist-info → kash_shell-0.3.18.dist-info}/RECORD +37 -30
  35. {kash_shell-0.3.17.dist-info → kash_shell-0.3.18.dist-info}/WHEEL +0 -0
  36. {kash_shell-0.3.17.dist-info → kash_shell-0.3.18.dist-info}/entry_points.txt +0 -0
  37. {kash_shell-0.3.17.dist-info → kash_shell-0.3.18.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,730 @@
1
+ /* -----------------------------------------------------------------------------
2
+ Core tooltip functionality and utilities
3
+ ----------------------------------------------------------------------------- */
4
+
5
+ // Tooltip configuration constants
6
+ const TOOLTIP_CONFIG = {
7
+ // Timing delays (in milliseconds)
8
+ delays: {
9
+ show: 500, // Delay before showing tooltip
10
+ hide: 1500, // Default delay before hiding tooltip
11
+ hideWideRight: 2000, // Delay for wide-right tooltips (farther away)
12
+ hideMovingToward: 500, // Shorter delay when mouse moving toward tooltip
13
+ hideLinkClick: 300, // Delay after clicking a link in tooltip
14
+ },
15
+
16
+ // Spacing and sizing (in pixels)
17
+ spacing: {
18
+ viewportMargin: 10, // Minimum distance from viewport edges
19
+ tooltipGap: 10, // Gap between trigger and tooltip
20
+ wideRightMargin: 16, // Margin for wide-right positioning (1rem)
21
+ mouseTrackMargin: 20, // Margin for mouse movement detection
22
+ mobileTopMargin: 20, // Top margin for mobile positioning
23
+ },
24
+
25
+ // Default dimensions
26
+ dimensions: {
27
+ defaultWidth: 320, // Fallback tooltip width
28
+ defaultHeight: 200, // Fallback tooltip height
29
+ minWideRightWidth: 320, // Minimum space needed for wide-right
30
+ mobileMaxWidth: 768, // Maximum width for mobile tooltips (48rem = 768px at 16px base)
31
+ },
32
+
33
+ // Breakpoints (match CSS breakpoints)
34
+ breakpoints: {
35
+ mobile: 640, // Mobile breakpoint (actual mobile devices)
36
+ tablet: 768, // Tablet breakpoint
37
+ wide: 1200, // Wide screen breakpoint
38
+ },
39
+
40
+ // Content limits
41
+ content: {
42
+ maxFootnoteLength: 400, // Maximum characters for footnote tooltips
43
+ minFootnoteLength: 5, // Minimum characters to show tooltip
44
+ }
45
+ };
46
+
47
+ // Tooltip utility functions
48
+ const TooltipUtils = {
49
+ // Sanitize text content for safe HTML attribute usage
50
+ sanitizeText(text) {
51
+ return text
52
+ .replace(/\s+/g, ' ') // Normalize whitespace
53
+ .trim()
54
+ .replace(/"/g, '"') // Escape quotes
55
+ .replace(/</g, '&lt;') // Escape HTML
56
+ .replace(/>/g, '&gt;');
57
+ },
58
+
59
+ // Extract clean content from an element
60
+ extractContent(element, html = false) {
61
+ // Clone the element to avoid modifying the original
62
+ const clone = element.cloneNode(true);
63
+
64
+ // Remove unwanted elements (like the footnote back-link)
65
+ const backLinks = clone.querySelectorAll('.footnote');
66
+ backLinks.forEach(link => link.remove());
67
+
68
+ // Return HTML or text content
69
+ return html ? (clone.innerHTML || '') : (clone.textContent || clone.innerText || '');
70
+ },
71
+
72
+ // Extract clean text content from an element
73
+ extractTextContent(element) {
74
+ return this.extractContent(element, false);
75
+ },
76
+
77
+ // Extract clean HTML content from an element (preserving links)
78
+ extractHtmlContent(element) {
79
+ return this.extractContent(element, true);
80
+ },
81
+
82
+ // Determine optimal tooltip position based on element location and screen size
83
+ getOptimalPosition(element) {
84
+ const viewportWidth = window.innerWidth;
85
+
86
+ // Check if element is inside a table - if so, always use bottom-right positioning
87
+ // Tables often bleed to the right edge, making wide-right positioning problematic
88
+ if (element.closest('table')) {
89
+ return 'bottom-right';
90
+ }
91
+
92
+ // On wide screens, try to use the sidebar on the right of the main content
93
+ if (viewportWidth >= TOOLTIP_CONFIG.breakpoints.wide) {
94
+ const mainContent = document.getElementById('main-content');
95
+ if (mainContent) {
96
+ const contentRect = mainContent.getBoundingClientRect();
97
+ const available = viewportWidth - contentRect.right - TOOLTIP_CONFIG.spacing.wideRightMargin;
98
+ if (available >= TOOLTIP_CONFIG.dimensions.minWideRightWidth) {
99
+ return 'wide-right';
100
+ }
101
+ }
102
+ }
103
+
104
+ // For medium and small screens, always start with bottom-right
105
+ return 'bottom-right';
106
+ }
107
+ };
108
+
109
+ /* -----------------------------------------------------------------------------
110
+ Generic tooltip creation and management functions
111
+ ----------------------------------------------------------------------------- */
112
+
113
+ // Advanced tooltip manager with real DOM elements
114
+ const TooltipManager = {
115
+ activeTooltips: new Map(), // Track active tooltip states
116
+
117
+ // Add tooltip to any element with HTML content
118
+ addTooltip(element, htmlContent, position = 'auto') {
119
+ if (!element || !htmlContent) return;
120
+
121
+ // Check if tooltip already exists for this element
122
+ if (this.activeTooltips.has(element)) {
123
+ console.debug('Tooltip already exists for element, skipping creation');
124
+ return;
125
+ }
126
+
127
+ const actualPosition = position === 'auto' ?
128
+ TooltipUtils.getOptimalPosition(element) : position;
129
+
130
+ // Create real DOM tooltip element
131
+ const tooltipElement = this.createTooltipElement(htmlContent, actualPosition);
132
+
133
+ // Mark the trigger element
134
+ element.setAttribute('data-tooltip-trigger', 'true');
135
+
136
+ // Determine if we should append to body
137
+ const viewportWidth = window.innerWidth;
138
+ const shouldAppendToBody = actualPosition === 'wide-right' ||
139
+ viewportWidth <= TOOLTIP_CONFIG.breakpoints.mobile;
140
+
141
+ // Append tooltip to appropriate parent
142
+ if (shouldAppendToBody) {
143
+ document.body.appendChild(tooltipElement);
144
+ element._bodyTooltip = tooltipElement; // Store reference for cleanup
145
+ } else {
146
+ element.appendChild(tooltipElement);
147
+ }
148
+
149
+ // Set up enhanced hover behavior
150
+ this.setupAdvancedHover(element, tooltipElement, actualPosition);
151
+ },
152
+
153
+ // Create a real DOM tooltip element
154
+ createTooltipElement(htmlContent, position) {
155
+ const tooltip = document.createElement('div');
156
+ tooltip.className = `tooltip-element tooltip-${position}`;
157
+
158
+ // Add footnote-specific class if content contains footnote or links
159
+ if (htmlContent.includes('footnote') || htmlContent.includes('<a')) {
160
+ tooltip.classList.add('footnote-element');
161
+ }
162
+
163
+ tooltip.innerHTML = htmlContent;
164
+ return tooltip;
165
+ },
166
+
167
+ // Set up advanced hover behavior with delays and mouse tracking
168
+ setupAdvancedHover(triggerElement, tooltipElement, position) {
169
+ const tooltipState = {
170
+ triggerElement,
171
+ tooltipElement,
172
+ position,
173
+ showTimeout: null,
174
+ hideTimeout: null,
175
+ isVisible: false
176
+ };
177
+
178
+ this.activeTooltips.set(triggerElement, tooltipState);
179
+
180
+ // Event handlers
181
+ const handlers = {
182
+ triggerEnter: (e) => this.handleTriggerEnter(tooltipState, e),
183
+ triggerLeave: (e) => this.handleTriggerLeave(tooltipState, e),
184
+ tooltipEnter: () => this.handleTooltipEnter(tooltipState),
185
+ tooltipLeave: () => this.handleTooltipLeave(tooltipState),
186
+ tooltipClick: (e) => this.handleTooltipClick(tooltipState, e)
187
+ };
188
+
189
+ // Add event listeners
190
+ triggerElement.addEventListener('mouseenter', handlers.triggerEnter);
191
+ triggerElement.addEventListener('mouseleave', handlers.triggerLeave);
192
+ tooltipElement.addEventListener('mouseenter', handlers.tooltipEnter);
193
+ tooltipElement.addEventListener('mouseleave', handlers.tooltipLeave);
194
+ tooltipElement.addEventListener('click', handlers.tooltipClick);
195
+
196
+ // Store cleanup function
197
+ tooltipState.cleanupListeners = () => {
198
+ triggerElement.removeEventListener('mouseenter', handlers.triggerEnter);
199
+ triggerElement.removeEventListener('mouseleave', handlers.triggerLeave);
200
+ tooltipElement.removeEventListener('mouseenter', handlers.tooltipEnter);
201
+ tooltipElement.removeEventListener('mouseleave', handlers.tooltipLeave);
202
+ tooltipElement.removeEventListener('click', handlers.tooltipClick);
203
+ };
204
+ },
205
+
206
+ // Handle trigger element mouse enter
207
+ handleTriggerEnter(tooltipState, event) {
208
+ // Cancel any pending hide
209
+ this.clearTimeout(tooltipState, 'hideTimeout');
210
+
211
+ // Start show timer
212
+ if (!tooltipState.isVisible && !tooltipState.showTimeout) {
213
+ tooltipState.showTimeout = setTimeout(() => {
214
+ tooltipState.showTimeout = null;
215
+ this.showTooltip(tooltipState, event);
216
+ }, TOOLTIP_CONFIG.delays.show);
217
+ }
218
+ },
219
+
220
+ // Handle trigger element mouse leave
221
+ handleTriggerLeave(tooltipState, event) {
222
+ // Cancel any pending show
223
+ this.clearTimeout(tooltipState, 'showTimeout');
224
+
225
+ // Start hide timer if tooltip is visible
226
+ if (tooltipState.isVisible) {
227
+ this.startHideTimer(tooltipState, event);
228
+ }
229
+ },
230
+
231
+ // Handle tooltip mouse enter
232
+ handleTooltipEnter(tooltipState) {
233
+ this.clearTimeout(tooltipState, 'hideTimeout');
234
+ },
235
+
236
+ // Handle tooltip mouse leave
237
+ handleTooltipLeave(tooltipState) {
238
+ if (tooltipState.isVisible) {
239
+ this.startHideTimer(tooltipState);
240
+ }
241
+ },
242
+
243
+ // Handle clicks within tooltip
244
+ handleTooltipClick(tooltipState, event) {
245
+ // Allow clicks on links within tooltips
246
+ if (event.target.tagName === 'A') {
247
+ event.stopPropagation();
248
+ // Keep tooltip open briefly after clicking a link
249
+ this.clearTimeout(tooltipState, 'hideTimeout');
250
+ tooltipState.hideTimeout = setTimeout(() => {
251
+ this.hideTooltip(tooltipState);
252
+ }, TOOLTIP_CONFIG.delays.hideLinkClick);
253
+ }
254
+ },
255
+
256
+ // Clear a specific timeout
257
+ clearTimeout(tooltipState, timeoutName) {
258
+ if (tooltipState[timeoutName]) {
259
+ clearTimeout(tooltipState[timeoutName]);
260
+ tooltipState[timeoutName] = null;
261
+ }
262
+ },
263
+
264
+ // Show tooltip with coordination of other tooltips
265
+ showTooltip(tooltipState, event = null) {
266
+ // Close any other open tooltips immediately when showing a new one
267
+ this.closeAllTooltips(tooltipState.triggerElement);
268
+
269
+ // Clear hide timeout and check if already visible
270
+ this.clearTimeout(tooltipState, 'hideTimeout');
271
+ if (tooltipState.isVisible) return;
272
+
273
+ // Prepare tooltip for positioning
274
+ this.prepareTooltipForDisplay(tooltipState.tooltipElement);
275
+
276
+ // Position tooltip based on type
277
+ if (tooltipState.position === 'wide-right') {
278
+ this.positionWideRightTooltip(tooltipState.tooltipElement, tooltipState.triggerElement);
279
+ } else {
280
+ this.ensureTooltipWithinBounds(tooltipState.tooltipElement, tooltipState.triggerElement, tooltipState.position);
281
+ }
282
+
283
+ // Make tooltip visible after positioning
284
+ this.makeTooltipVisible(tooltipState);
285
+ },
286
+
287
+ // Prepare tooltip element for display
288
+ prepareTooltipForDisplay(tooltipElement) {
289
+ tooltipElement.style.opacity = '0';
290
+ tooltipElement.style.visibility = 'hidden';
291
+ tooltipElement.classList.remove('tooltip-visible');
292
+ },
293
+
294
+ // Make tooltip visible with proper timing
295
+ makeTooltipVisible(tooltipState) {
296
+ // Double requestAnimationFrame to ensure layout is complete
297
+ requestAnimationFrame(() => {
298
+ requestAnimationFrame(() => {
299
+ tooltipState.tooltipElement.style.opacity = '';
300
+ tooltipState.tooltipElement.style.visibility = '';
301
+ tooltipState.tooltipElement.classList.add('tooltip-visible');
302
+ tooltipState.isVisible = true;
303
+ });
304
+ });
305
+ },
306
+
307
+ // Position wide-right tooltips
308
+ positionWideRightTooltip(tooltipElement, triggerElement) {
309
+ const triggerRect = triggerElement.getBoundingClientRect();
310
+ const mainContent = document.getElementById('main-content');
311
+ const viewportWidth = window.innerWidth;
312
+ const viewportHeight = window.innerHeight;
313
+
314
+ if (!mainContent) return;
315
+
316
+ const contentRect = mainContent.getBoundingClientRect();
317
+ const tooltipWidth = tooltipElement.offsetWidth || TOOLTIP_CONFIG.dimensions.defaultWidth;
318
+ const tooltipHeight = tooltipElement.offsetHeight || TOOLTIP_CONFIG.dimensions.defaultHeight;
319
+
320
+ // Calculate horizontal position
321
+ let leftPosition = contentRect.right + TOOLTIP_CONFIG.spacing.wideRightMargin;
322
+ const maxWidth = viewportWidth - leftPosition - TOOLTIP_CONFIG.spacing.wideRightMargin;
323
+
324
+ if (tooltipWidth > maxWidth) {
325
+ tooltipElement.style.maxWidth = `${maxWidth}px`;
326
+ }
327
+
328
+ const finalWidth = Math.min(tooltipWidth, maxWidth);
329
+ if (leftPosition + finalWidth + TOOLTIP_CONFIG.spacing.wideRightMargin > viewportWidth) {
330
+ leftPosition = viewportWidth - finalWidth - TOOLTIP_CONFIG.spacing.wideRightMargin;
331
+ }
332
+
333
+ // Calculate vertical position (centered on trigger)
334
+ let topPosition = triggerRect.top + (triggerRect.height / 2) - (tooltipHeight / 2);
335
+ const minTop = TOOLTIP_CONFIG.spacing.mobileTopMargin;
336
+ const maxTop = viewportHeight - tooltipHeight - TOOLTIP_CONFIG.spacing.mobileTopMargin;
337
+ topPosition = Math.max(minTop, Math.min(topPosition, maxTop));
338
+
339
+ // Apply positioning
340
+ tooltipElement.style.left = `${leftPosition}px`;
341
+ tooltipElement.style.top = `${topPosition}px`;
342
+ },
343
+
344
+ // Ensure standard tooltips stay within viewport bounds
345
+ ensureTooltipWithinBounds(tooltipElement, triggerElement, position) {
346
+ const viewportWidth = window.innerWidth;
347
+ const viewportHeight = window.innerHeight;
348
+
349
+ // Skip for wide screens (they use wide-right positioning)
350
+ if (viewportWidth >= TOOLTIP_CONFIG.breakpoints.wide) return;
351
+
352
+ // Get dimensions
353
+ const dimensions = this.measureTooltip(tooltipElement);
354
+ const triggerRect = triggerElement.getBoundingClientRect();
355
+
356
+ // Use bottom-centered positioning for narrow screens
357
+ if (viewportWidth <= TOOLTIP_CONFIG.breakpoints.mobile) {
358
+ this.positionBottomCenteredTooltip(tooltipElement, dimensions.width, viewportWidth);
359
+ } else {
360
+ this.positionMediumTooltip(tooltipElement, triggerRect, dimensions, viewportWidth, viewportHeight);
361
+ }
362
+ },
363
+
364
+ // Measure tooltip dimensions
365
+ measureTooltip(tooltipElement) {
366
+ // Temporarily make visible but transparent to measure
367
+ const originalStyles = {
368
+ opacity: tooltipElement.style.opacity,
369
+ pointerEvents: tooltipElement.style.pointerEvents,
370
+ display: tooltipElement.style.display
371
+ };
372
+
373
+ tooltipElement.style.opacity = '0';
374
+ tooltipElement.style.pointerEvents = 'none';
375
+ tooltipElement.style.display = 'block';
376
+
377
+ const rect = tooltipElement.getBoundingClientRect();
378
+ const dimensions = {
379
+ width: rect.width || tooltipElement.offsetWidth || TOOLTIP_CONFIG.dimensions.defaultWidth,
380
+ height: rect.height || tooltipElement.offsetHeight || TOOLTIP_CONFIG.dimensions.defaultHeight
381
+ };
382
+
383
+ // Restore original styles
384
+ Object.assign(tooltipElement.style, originalStyles);
385
+
386
+ return dimensions;
387
+ },
388
+
389
+ // Position tooltip at bottom center for narrow screens
390
+ positionBottomCenteredTooltip(tooltipElement, tooltipWidth, viewportWidth) {
391
+ // Simple approach: full viewport width minus margins
392
+ const margin = 16; // 1rem margin on all sides
393
+
394
+ // Set tooltip to use viewport width minus margins
395
+ tooltipElement.style.width = `calc(100vw - ${2 * margin}px)`;
396
+ tooltipElement.style.maxWidth = 'none';
397
+
398
+ // Apply positioning - 1rem from left and bottom
399
+ Object.assign(tooltipElement.style, {
400
+ position: 'fixed',
401
+ left: `${margin}px`,
402
+ right: 'auto',
403
+ bottom: `${margin}px`,
404
+ top: 'auto',
405
+ transform: 'none'
406
+ });
407
+
408
+ // Update classes
409
+ this.updatePositionClasses(tooltipElement, 'mobile-bottom');
410
+
411
+ // Remove arrow for mobile
412
+ this.removeArrowStyles(tooltipElement);
413
+ },
414
+
415
+ // Position tooltip for medium screens
416
+ positionMediumTooltip(tooltipElement, triggerRect, dimensions, viewportWidth, viewportHeight) {
417
+ const margin = TOOLTIP_CONFIG.spacing.viewportMargin;
418
+ const gap = TOOLTIP_CONFIG.spacing.tooltipGap;
419
+
420
+ // Try bottom-right first
421
+ let idealLeft = triggerRect.left;
422
+ let idealTop = triggerRect.bottom + gap;
423
+
424
+ // Check if it fits below
425
+ const fitsBelow = (idealTop + dimensions.height + margin) <= viewportHeight;
426
+ if (!fitsBelow) {
427
+ idealTop = triggerRect.top - dimensions.height - gap;
428
+ }
429
+
430
+ // Constrain to viewport
431
+ const finalLeft = Math.max(margin, Math.min(idealLeft, viewportWidth - dimensions.width - margin));
432
+ const finalTop = Math.max(margin, Math.min(idealTop, viewportHeight - dimensions.height - margin));
433
+
434
+ // Apply positioning
435
+ Object.assign(tooltipElement.style, {
436
+ position: 'fixed',
437
+ left: `${finalLeft}px`,
438
+ top: `${finalTop}px`,
439
+ right: 'auto',
440
+ bottom: 'auto',
441
+ transform: 'none'
442
+ });
443
+
444
+ // Update classes and arrow
445
+ const actualPosition = fitsBelow ? 'bottom-right' : 'top-right';
446
+ this.updatePositionClasses(tooltipElement, actualPosition);
447
+ this.updateArrowPosition(tooltipElement, triggerRect, finalLeft, finalTop, dimensions.width, dimensions.height, actualPosition);
448
+ },
449
+
450
+ // Update position classes on tooltip element
451
+ updatePositionClasses(tooltipElement, newPosition) {
452
+ const positionClasses = ['bottom-right', 'top-right', 'bottom-left', 'top-left', 'bottom', 'top', 'left', 'right', 'mobile-bottom'];
453
+ positionClasses.forEach(cls => tooltipElement.classList.remove(`tooltip-${cls}`));
454
+ tooltipElement.classList.add(`tooltip-${newPosition}`);
455
+ },
456
+
457
+ // Remove arrow styles
458
+ removeArrowStyles(tooltipElement) {
459
+ const existingStyle = tooltipElement.querySelector('style');
460
+ if (existingStyle) existingStyle.remove();
461
+ },
462
+
463
+ // Update arrow position to point to trigger
464
+ updateArrowPosition(tooltipElement, triggerRect, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, position) {
465
+ // Create unique ID for this tooltip
466
+ const tooltipId = `tooltip-${Math.random().toString(36).substr(2, 9)}`;
467
+ tooltipElement.setAttribute('data-tooltip-id', tooltipId);
468
+
469
+ // Remove existing arrow style
470
+ this.removeArrowStyles(tooltipElement);
471
+
472
+ // Calculate arrow position
473
+ const triggerCenterX = triggerRect.left + (triggerRect.width / 2);
474
+ const arrowX = triggerCenterX - tooltipLeft;
475
+ const arrowXPercent = (arrowX / tooltipWidth) * 100;
476
+ const clampedPercent = Math.max(10, Math.min(90, arrowXPercent));
477
+
478
+ // Generate arrow style based on position
479
+ const arrowStyle = this.generateArrowStyle(tooltipId, position, clampedPercent);
480
+
481
+ // Apply arrow style
482
+ if (arrowStyle) {
483
+ const styleElement = document.createElement('style');
484
+ styleElement.textContent = arrowStyle;
485
+ tooltipElement.appendChild(styleElement);
486
+ }
487
+ },
488
+
489
+ // Generate CSS for arrow positioning
490
+ generateArrowStyle(tooltipId, position, xPercent) {
491
+ const baseStyle = `
492
+ [data-tooltip-id="${tooltipId}"]::after {
493
+ left: ${xPercent}% !important;
494
+ right: auto !important;
495
+ transform: translateX(-50%) !important;
496
+ `;
497
+
498
+ if (position === 'bottom-right' || position === 'bottom-left') {
499
+ return baseStyle + `
500
+ bottom: 100% !important;
501
+ top: auto !important;
502
+ border-bottom-color: var(--color-border-hint) !important;
503
+ border-top-color: transparent !important;
504
+ border-left-color: transparent !important;
505
+ border-right-color: transparent !important;
506
+ margin-bottom: -5px !important;
507
+ }`;
508
+ } else if (position === 'top-right' || position === 'top-left') {
509
+ return baseStyle + `
510
+ top: 100% !important;
511
+ bottom: auto !important;
512
+ border-top-color: var(--color-border-hint) !important;
513
+ border-bottom-color: transparent !important;
514
+ border-left-color: transparent !important;
515
+ border-right-color: transparent !important;
516
+ margin-top: -5px !important;
517
+ }`;
518
+ }
519
+ return null;
520
+ },
521
+
522
+ // Start the hide timer with appropriate delay
523
+ startHideTimer(tooltipState, event = null) {
524
+ const isWideRight = tooltipState.position === 'wide-right';
525
+ let delay = isWideRight ? TOOLTIP_CONFIG.delays.hideWideRight : TOOLTIP_CONFIG.delays.hide;
526
+
527
+ // Check if mouse is moving toward tooltip (for non-wide-right)
528
+ if (event && !isWideRight) {
529
+ const rect = tooltipState.triggerElement.getBoundingClientRect();
530
+ if (this.isMouseMovingTowardTooltip(rect, event.clientX, event.clientY, tooltipState.position)) {
531
+ delay = TOOLTIP_CONFIG.delays.hideMovingToward;
532
+ }
533
+ }
534
+
535
+ tooltipState.hideTimeout = setTimeout(() => {
536
+ this.hideTooltip(tooltipState);
537
+ }, delay);
538
+ },
539
+
540
+ // Hide tooltip
541
+ hideTooltip(tooltipState) {
542
+ this.clearTimeout(tooltipState, 'showTimeout');
543
+ tooltipState.tooltipElement.classList.remove('tooltip-visible');
544
+ tooltipState.isVisible = false;
545
+ tooltipState.hideTimeout = null;
546
+ },
547
+
548
+ // Close all tooltips immediately (for cleanup)
549
+ closeAllTooltips(exceptElement = null) {
550
+ this.activeTooltips.forEach((tooltipState, element) => {
551
+ if (element !== exceptElement && tooltipState.isVisible) {
552
+ this.clearTimeout(tooltipState, 'hideTimeout');
553
+ this.clearTimeout(tooltipState, 'showTimeout');
554
+ this.hideTooltip(tooltipState);
555
+ }
556
+ });
557
+ },
558
+
559
+ // Close all tooltips with delay when showing a new one
560
+ closeAllTooltipsWithDelay(exceptElement = null) {
561
+ this.activeTooltips.forEach((tooltipState, element) => {
562
+ if (element !== exceptElement) {
563
+ this.clearTimeout(tooltipState, 'showTimeout');
564
+
565
+ if (tooltipState.isVisible && !tooltipState.hideTimeout) {
566
+ tooltipState.hideTimeout = setTimeout(() => {
567
+ this.hideTooltip(tooltipState);
568
+ }, TOOLTIP_CONFIG.delays.show);
569
+ }
570
+ }
571
+ });
572
+ },
573
+
574
+ // Check if mouse is moving toward tooltip area
575
+ isMouseMovingTowardTooltip(elementRect, mouseX, mouseY, position) {
576
+ if (position === 'wide-right' || position === 'mobile-bottom') return false;
577
+
578
+ const margin = TOOLTIP_CONFIG.spacing.mouseTrackMargin;
579
+
580
+ if (position === 'bottom-right' || position === 'bottom-left') {
581
+ return mouseY > elementRect.bottom &&
582
+ mouseX >= elementRect.left - margin &&
583
+ mouseX <= elementRect.right + margin;
584
+ } else if (position === 'top-right' || position === 'top-left') {
585
+ return mouseY < elementRect.top &&
586
+ mouseX >= elementRect.left - margin &&
587
+ mouseX <= elementRect.right + margin;
588
+ }
589
+
590
+ return false;
591
+ },
592
+
593
+ // Remove tooltip from element
594
+ removeTooltip(element) {
595
+ if (!element) return;
596
+
597
+ const tooltipState = this.activeTooltips.get(element);
598
+ if (!tooltipState) return;
599
+
600
+ // Clear all timeouts
601
+ this.clearTimeout(tooltipState, 'hideTimeout');
602
+ this.clearTimeout(tooltipState, 'showTimeout');
603
+
604
+ // Remove tooltip element
605
+ if (tooltipState.tooltipElement?.parentNode) {
606
+ tooltipState.tooltipElement.parentNode.removeChild(tooltipState.tooltipElement);
607
+ }
608
+
609
+ // Remove tooltip reference
610
+ if (element._bodyTooltip?.parentNode) {
611
+ element._bodyTooltip.parentNode.removeChild(element._bodyTooltip);
612
+ delete element._bodyTooltip;
613
+ }
614
+
615
+ // Cleanup listeners
616
+ if (tooltipState.cleanupListeners) {
617
+ tooltipState.cleanupListeners();
618
+ }
619
+
620
+ // Clean up tracking
621
+ this.activeTooltips.delete(element);
622
+ element.removeAttribute('data-tooltip');
623
+ element.removeAttribute('data-tooltip-position');
624
+ element.removeAttribute('data-tooltip-trigger');
625
+ },
626
+
627
+ // Handle window resize to reposition tooltips
628
+ handleResize() {
629
+ this.activeTooltips.forEach((tooltipState, element) => {
630
+ const shouldBeWideRight = TooltipUtils.getOptimalPosition(element) === 'wide-right';
631
+ const isWideRight = tooltipState.position === 'wide-right';
632
+
633
+ if (shouldBeWideRight !== isWideRight) {
634
+ const htmlContent = tooltipState.tooltipElement.innerHTML;
635
+ this.removeTooltip(element);
636
+ this.addTooltip(element, htmlContent, 'auto');
637
+ }
638
+ });
639
+ }
640
+ };
641
+
642
+ // Handle window resize events
643
+ window.addEventListener('resize', () => {
644
+ TooltipManager.handleResize();
645
+ });
646
+
647
+ /* -----------------------------------------------------------------------------
648
+ Automatic footnote detection and tooltip creation
649
+ ----------------------------------------------------------------------------- */
650
+
651
+ // Initialize footnote tooltips
652
+ function initFootnoteTooltips() {
653
+ console.debug('Initializing footnote tooltips...');
654
+
655
+ const footnoteRefs = document.querySelectorAll('.footnote-ref a[href^="#fn-"]');
656
+ let tooltipsAdded = 0;
657
+
658
+ footnoteRefs.forEach(refLink => {
659
+ try {
660
+ const href = refLink.getAttribute('href');
661
+ if (!href || !href.startsWith('#fn-')) return;
662
+
663
+ const footnoteId = href.substring(1);
664
+ const footnoteElement = document.getElementById(footnoteId);
665
+ if (!footnoteElement) {
666
+ console.debug(`Footnote element not found for ID: ${footnoteId}`);
667
+ return;
668
+ }
669
+
670
+ // Extract and validate content
671
+ const footnoteHtml = TooltipUtils.extractHtmlContent(footnoteElement);
672
+ const footnoteText = TooltipUtils.extractTextContent(footnoteElement);
673
+
674
+ if (!footnoteText || footnoteText.length < TOOLTIP_CONFIG.content.minFootnoteLength) {
675
+ console.debug(`Footnote text too short or empty for ID: ${footnoteId}`);
676
+ return;
677
+ }
678
+
679
+ // Truncate if needed
680
+ const displayContent = truncateFootnoteContent(footnoteHtml, footnoteText);
681
+
682
+ // Add tooltip
683
+ TooltipManager.addTooltip(refLink, displayContent, 'auto');
684
+ tooltipsAdded++;
685
+
686
+ console.debug(`Added tooltip for footnote ${footnoteId}: "${footnoteText.substring(0, 50)}..."`);
687
+ } catch (error) {
688
+ console.error('Error processing footnote reference:', refLink, error);
689
+ }
690
+ });
691
+
692
+ console.debug(`Footnote tooltips initialized: ${tooltipsAdded} tooltips added`);
693
+ return tooltipsAdded;
694
+ }
695
+
696
+ // Truncate footnote content if too long
697
+ function truncateFootnoteContent(html, text) {
698
+ if (text.length <= TOOLTIP_CONFIG.content.maxFootnoteLength) {
699
+ return html;
700
+ }
701
+
702
+ const tempDiv = document.createElement('div');
703
+ tempDiv.innerHTML = html;
704
+ const textContent = tempDiv.textContent || tempDiv.innerText || '';
705
+
706
+ if (textContent.length > TOOLTIP_CONFIG.content.maxFootnoteLength) {
707
+ tempDiv.textContent = textContent.substring(0, TOOLTIP_CONFIG.content.maxFootnoteLength) + '...';
708
+ }
709
+
710
+ return tempDiv.innerHTML;
711
+ }
712
+
713
+ // Initialize all tooltips
714
+ function initTooltips() {
715
+ console.debug('Starting tooltip initialization...');
716
+
717
+ try {
718
+ const footnoteCount = initFootnoteTooltips();
719
+ console.debug(`Tooltip initialization complete. Total footnote tooltips: ${footnoteCount}`);
720
+ } catch (error) {
721
+ console.error('Error during tooltip initialization:', error);
722
+ }
723
+ }
724
+
725
+ // Initialize when DOM is ready
726
+ if (document.readyState === 'loading') {
727
+ document.addEventListener('DOMContentLoaded', initTooltips);
728
+ } else {
729
+ initTooltips();
730
+ }