@diabolic/pointy 1.1.1 → 1.2.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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Pointy - A lightweight tooltip library with animated pointer
3
- * @version 1.1.1
3
+ * @version 1.2.0
4
4
  * @license MIT
5
5
  */
6
6
  /**
@@ -54,6 +54,9 @@
54
54
  * - classNames {object} - Full override of class names
55
55
  * - cssVarPrefix {string} - CSS variable prefix (default: classPrefix)
56
56
  * - pointerSvg {string} - Custom SVG for pointer
57
+ * - bubbleBackgroundColor {string} - Custom bubble background color (default: '#0a1551')
58
+ * - bubbleTextColor {string} - Custom bubble text color (default: 'white')
59
+ * - bubbleMaxWidth {string} - Max width for bubble (default: '90vw')
57
60
  * - onStepChange {function} - Callback on step change
58
61
  * - onComplete {function} - Callback on tour complete
59
62
  *
@@ -152,6 +155,7 @@
152
155
  *
153
156
  * Setters (all emit change events):
154
157
  * setEasing(), setAnimationDuration(), setIntroFadeDuration(), setBubbleFadeDuration(),
158
+ * setBubbleBackgroundColor(), setBubbleTextColor(), setBubbleMaxWidth(),
155
159
  * setMessageInterval(), setMessageTransitionDuration(), setOffset(), setZIndex(),
156
160
  * setStayInViewport(enabled, thresholds?), setDirection(direction),
157
161
  * setHorizontalDirection(direction), setVerticalDirection(direction),
@@ -186,7 +190,7 @@ class Pointy {
186
190
  static POINTER_SVG = `
187
191
  <svg xmlns="http://www.w3.org/2000/svg" width="33" height="33" fill="none" viewBox="0 0 33 33">
188
192
  <g filter="url(#pointy-shadow)">
189
- <path fill="#0a1551" d="m18.65 24.262 6.316-14.905c.467-1.103-.645-2.215-1.748-1.747L8.313 13.925c-1.088.461-1.083 2.004.008 2.459l5.049 2.104c.325.135.583.393.718.718l2.104 5.049c.454 1.09 1.997 1.095 2.458.007"/>
193
+ <path fill="currentColor" d="m18.65 24.262 6.316-14.905c.467-1.103-.645-2.215-1.748-1.747L8.313 13.925c-1.088.461-1.083 2.004.008 2.459l5.049 2.104c.325.135.583.393.718.718l2.104 5.049c.454 1.09 1.997 1.095 2.458.007"/>
190
194
  </g>
191
195
  <defs>
192
196
  <filter id="pointy-shadow" width="32.576" height="32.575" x="0" y="0" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
@@ -265,6 +269,10 @@ class Pointy {
265
269
  --${vp}-duration: 1000ms;
266
270
  --${vp}-easing: cubic-bezier(0, 0.55, 0.45, 1);
267
271
  --${vp}-bubble-fade: 500ms;
272
+ --${vp}-bubble-bg: #0a1551;
273
+ --${vp}-bubble-color: white;
274
+ --${vp}-bubble-max-width: min(400px, 90vw);
275
+ --${vp}-pointer-color: #0a1551;
268
276
  transition: left var(--${vp}-duration) var(--${vp}-easing), top var(--${vp}-duration) var(--${vp}-easing), opacity 0.3s ease;
269
277
  animation: ${cn.container}-float 3s ease-in-out infinite;
270
278
  }
@@ -285,7 +293,8 @@ class Pointy {
285
293
  .${cn.pointer} {
286
294
  width: 33px;
287
295
  height: 33px;
288
- transition: transform var(--${vp}-duration) var(--${vp}-easing);
296
+ color: var(--${vp}-pointer-color);
297
+ transition: transform var(--${vp}-duration) var(--${vp}-easing), color 0.3s ease;
289
298
  }
290
299
 
291
300
  .${cn.bubble} {
@@ -293,21 +302,23 @@ class Pointy {
293
302
  right: 26px;
294
303
  left: auto;
295
304
  top: 0;
296
- background: #0a1551;
297
- color: white;
305
+ background: var(--${vp}-bubble-bg);
306
+ color: var(--${vp}-bubble-color);
298
307
  padding: 4px 12px;
299
308
  border-radius: 14px;
300
309
  font-size: 14px;
301
310
  line-height: 20px;
302
311
  font-weight: 400;
303
312
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.25);
304
- white-space: nowrap;
313
+ width: max-content;
314
+ max-width: var(--${vp}-bubble-max-width);
305
315
  overflow: hidden;
306
- transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1), height 0.5s cubic-bezier(0.4, 0, 0.2, 1), transform var(--${vp}-duration) var(--${vp}-easing), opacity var(--${vp}-bubble-fade) ease, left var(--${vp}-duration) var(--${vp}-easing), right var(--${vp}-duration) var(--${vp}-easing);
316
+ transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), height 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform var(--${vp}-duration) var(--${vp}-easing), opacity var(--${vp}-bubble-fade) ease, left var(--${vp}-duration) var(--${vp}-easing), right var(--${vp}-duration) var(--${vp}-easing), background 0.3s ease, color 0.3s ease;
307
317
  }
308
318
 
309
319
  .${cn.bubbleText} {
310
- display: inline-block;
320
+ display: block;
321
+ word-break: break-word;
311
322
  }
312
323
  `;
313
324
  }
@@ -347,16 +358,20 @@ class Pointy {
347
358
  static animateText(element, newContent, duration = 500, bubble = null, onComplete = null) {
348
359
  const hideTime = duration * 0.4;
349
360
  const revealTime = duration * 0.6;
361
+ const resizeTime = 300; // Match CSS transition
350
362
 
351
363
  // Measure new content dimensions using a hidden container
352
364
  let newWidth = null;
353
365
  let newHeight = null;
354
366
  if (bubble) {
367
+ // Get bubble's max-width for proper measurement of multi-line content
368
+ const bubbleStyles = window.getComputedStyle(bubble);
369
+ const maxWidth = bubbleStyles.maxWidth;
370
+
355
371
  const measureDiv = document.createElement('div');
356
- measureDiv.style.cssText = 'visibility: hidden; position: absolute; padding: 4px 12px;';
372
+ measureDiv.style.cssText = `visibility: hidden; position: absolute; padding: 4px 12px; width: max-content; max-width: ${maxWidth};`;
357
373
  Pointy.renderContent(measureDiv, newContent);
358
374
  bubble.appendChild(measureDiv);
359
- // Add horizontal padding (12px left + 12px right = 24px)
360
375
  newWidth = measureDiv.offsetWidth;
361
376
  newHeight = measureDiv.offsetHeight;
362
377
  bubble.removeChild(measureDiv);
@@ -366,22 +381,26 @@ class Pointy {
366
381
  const currentHeight = bubble.offsetHeight;
367
382
  bubble.style.width = currentWidth + 'px';
368
383
  bubble.style.height = currentHeight + 'px';
384
+
385
+ // Force reflow
386
+ bubble.offsetHeight;
387
+
388
+ // Start resizing bubble to new size immediately
389
+ bubble.style.width = newWidth + 'px';
390
+ bubble.style.height = newHeight + 'px';
369
391
  }
370
392
 
371
393
  // Phase 1: Hide old text (clip from left, disappears to right)
372
394
  element.style.transition = `clip-path ${hideTime}ms ease-in`;
373
395
  element.style.clipPath = 'inset(0 0 0 100%)';
374
396
 
397
+ // Wait for bubble resize AND text hide, then change content
398
+ const contentChangeDelay = Math.max(hideTime, resizeTime);
399
+
375
400
  setTimeout(() => {
376
- // Change content while fully clipped
401
+ // Change content while fully clipped AND bubble is at new size
377
402
  Pointy.renderContent(element, newContent);
378
403
 
379
- // Animate bubble to new size
380
- if (bubble && newWidth !== null) {
381
- bubble.style.width = newWidth + 'px';
382
- bubble.style.height = newHeight + 'px';
383
- }
384
-
385
404
  // Prepare for reveal (start fully clipped from right)
386
405
  element.style.transition = 'none';
387
406
  element.style.clipPath = 'inset(0 100% 0 0)';
@@ -393,16 +412,16 @@ class Pointy {
393
412
  element.style.transition = `clip-path ${revealTime}ms ease-out`;
394
413
  element.style.clipPath = 'inset(0 0 0 0)';
395
414
 
396
- // Clear dimensions after transition so it can auto-size
415
+ // Clear explicit dimensions after reveal so bubble can auto-size
397
416
  if (bubble) {
398
417
  setTimeout(() => {
399
418
  bubble.style.width = '';
400
419
  bubble.style.height = '';
401
- }, revealTime + 100);
420
+ }, revealTime + 50);
402
421
  }
403
422
 
404
423
  if (onComplete) onComplete();
405
- }, hideTime);
424
+ }, contentChangeDelay);
406
425
  }
407
426
 
408
427
  /**
@@ -485,6 +504,10 @@ class Pointy {
485
504
  this.autoplayWaitForMessages = options.autoplayWaitForMessages !== undefined ? options.autoplayWaitForMessages : true; // Wait for all messages before advancing
486
505
  this.hideOnComplete = options.hideOnComplete !== undefined ? options.hideOnComplete : true; // Auto-hide after tour completes
487
506
  this.hideOnCompleteDelay = options.hideOnCompleteDelay !== undefined ? options.hideOnCompleteDelay : null; // Delay before hide (null = use animationDuration)
507
+ this.bubbleBackgroundColor = options.bubbleBackgroundColor || null; // Custom bubble background color
508
+ this.bubbleTextColor = options.bubbleTextColor || null; // Custom bubble text color
509
+ this.bubbleMaxWidth = options.bubbleMaxWidth || null; // Max width for bubble (default: min(400px, 90vw))
510
+ this.pointerColor = options.pointerColor || null; // Custom pointer/cursor color
488
511
  this._autoplayTimeoutId = null;
489
512
  this._autoplayPaused = false;
490
513
  this._messagesCompletedForStep = false; // Track if all messages have been shown
@@ -509,6 +532,10 @@ class Pointy {
509
532
  this._lastDirectionChangeTime = 0; // Debounce direction changes
510
533
  this.manualHorizontalDirection = null; // 'left', 'right', or null (auto)
511
534
  this.manualVerticalDirection = null; // 'up', 'down', or null (auto)
535
+ this._autoDirectionLocked = false; // Lock auto-direction after first calculation per target
536
+ this._lastTargetElement = null; // Track target changes to recalculate direction
537
+ this._bubbleConstraintApplied = false; // Track if bubble constraint has been calculated
538
+ this._cachedBubbleNaturalWidth = null; // Cache bubble's natural width before constraint
512
539
  this.moveTimeout = null;
513
540
  this._hasShownBefore = false; // For intro animation
514
541
 
@@ -525,6 +552,20 @@ class Pointy {
525
552
  this.container.style.setProperty(`--${this.cssVarPrefix}-easing`, this._resolveEasing(this.easing));
526
553
  this.container.style.setProperty(`--${this.cssVarPrefix}-bubble-fade`, `${this.bubbleFadeDuration}ms`);
527
554
 
555
+ // Apply custom bubble colors if provided
556
+ if (this.bubbleBackgroundColor) {
557
+ this.container.style.setProperty(`--${this.cssVarPrefix}-bubble-bg`, this.bubbleBackgroundColor);
558
+ }
559
+ if (this.bubbleTextColor) {
560
+ this.container.style.setProperty(`--${this.cssVarPrefix}-bubble-color`, this.bubbleTextColor);
561
+ }
562
+ if (this.bubbleMaxWidth) {
563
+ this.container.style.setProperty(`--${this.cssVarPrefix}-bubble-max-width`, this.bubbleMaxWidth);
564
+ }
565
+ if (this.pointerColor) {
566
+ this.container.style.setProperty(`--${this.cssVarPrefix}-pointer-color`, this.pointerColor);
567
+ }
568
+
528
569
  // Apply floating animation setting
529
570
  if (!this.floatingAnimation) {
530
571
  this.container.style.animationPlayState = 'paused';
@@ -614,7 +655,7 @@ class Pointy {
614
655
  const scrollY = window.scrollY;
615
656
  const viewportWidth = window.innerWidth;
616
657
  const viewportHeight = window.innerHeight;
617
- const bubbleWidth = this.bubble.offsetWidth || 100;
658
+ this.bubble.offsetWidth || 100;
618
659
  const bubbleHeight = this.bubble.offsetHeight || 28;
619
660
 
620
661
  // Manual horizontal direction takes priority
@@ -660,46 +701,47 @@ class Pointy {
660
701
  this.lastTargetY = currentTargetY;
661
702
  }
662
703
 
663
- // Stay in viewport: check if bubble would go off-screen and flip accordingly
664
- // Only auto-flip directions that are not manually set
665
- if (this.stayInViewport) {
704
+ // Check if target changed - if so, unlock auto direction and bubble constraint
705
+ if (this._lastTargetElement !== this.targetElement) {
706
+ this._autoDirectionLocked = false;
707
+ this._bubbleConstraintApplied = false;
708
+ this._cachedBubbleNaturalWidth = null;
709
+ this.bubble.style.maxWidth = ''; // Reset constraint for new target
710
+ this._lastTargetElement = this.targetElement;
711
+ }
712
+
713
+ // Stay in viewport: calculate optimal direction ONCE per target, then lock it
714
+ // This prevents flip-flopping during animations and tracking
715
+ if (this.stayInViewport && !this._autoDirectionLocked) {
666
716
  const prevIsPointingLeft = this.isPointingLeft;
667
717
  const prevIsPointingUp = this.isPointingUp;
668
718
 
669
- // Horizontal flip check (only if not manually set)
719
+ // Horizontal direction (only if not manually set)
670
720
  if (this.manualHorizontalDirection === null) {
671
- const bubbleLeftIfPointingLeft = targetRect.left - bubbleWidth - this.viewportThresholdX;
672
- const bubbleRightIfPointingRight = targetRect.right + bubbleWidth + this.viewportThresholdX;
721
+ // Calculate available space on each side
722
+ const spaceOnLeft = targetRect.left - this.viewportThresholdX;
723
+ const spaceOnRight = viewportWidth - targetRect.right - this.viewportThresholdX;
673
724
 
674
- // Flip to right side if bubble goes off left edge
675
- if (bubbleLeftIfPointingLeft < 0 && this.isPointingLeft) {
676
- this.isPointingLeft = false;
677
- }
678
- // Flip to left side if bubble goes off right edge
679
- else if (bubbleRightIfPointingRight > viewportWidth && !this.isPointingLeft) {
680
- this.isPointingLeft = true;
681
- }
682
- // Return to default (left) if there's room
683
- else if (bubbleLeftIfPointingLeft >= 0 && !this.isPointingLeft) {
684
- this.isPointingLeft = true;
685
- }
725
+ // Pick the side with more space
726
+ // Add a small preference for left (default) when spaces are similar
727
+ const leftPreference = 20;
728
+ this.isPointingLeft = spaceOnLeft + leftPreference >= spaceOnRight;
686
729
  }
687
730
 
688
- // Vertical flip check (only if not manually set)
731
+ // Vertical direction (only if not manually set)
689
732
  if (this.manualVerticalDirection === null) {
690
- const bubbleBottomIfPointingUp = targetRect.bottom + bubbleHeight + this.viewportThresholdY;
691
- const bubbleTopIfPointingDown = targetRect.top - bubbleHeight - this.viewportThresholdY;
733
+ // Calculate available space above and below
734
+ const spaceBelow = viewportHeight - targetRect.bottom - this.viewportThresholdY;
735
+ const spaceAbove = targetRect.top - this.viewportThresholdY;
692
736
 
693
- // Flip to pointing down if bubble goes off bottom edge
694
- if (bubbleBottomIfPointingUp > viewportHeight && this.isPointingUp) {
695
- this.isPointingUp = false;
696
- }
697
- // Flip to pointing up if bubble goes off top edge
698
- else if (bubbleTopIfPointingDown < 0 && !this.isPointingUp) {
699
- this.isPointingUp = true;
700
- }
737
+ // Pick the side with more space, slight preference for below (pointing up)
738
+ const belowPreference = 10;
739
+ this.isPointingUp = spaceBelow + belowPreference >= spaceAbove;
701
740
  }
702
741
 
742
+ // Lock direction - won't recalculate until target changes
743
+ this._autoDirectionLocked = true;
744
+
703
745
  // Emit flip events if direction changed
704
746
  if (prevIsPointingLeft !== this.isPointingLeft) {
705
747
  this._emit('flipHorizontal', {
@@ -761,8 +803,77 @@ class Pointy {
761
803
 
762
804
  this.pointer.style.transform = pointerRotation;
763
805
  this.bubble.style.transform = bubbleTransform;
806
+
807
+ // Clamp container position to keep pointer visible in viewport
808
+ const minLeft = scrollX + 8;
809
+ const maxLeft = scrollX + viewportWidth - 40;
810
+ left = Math.max(minLeft, Math.min(left, maxLeft));
811
+
764
812
  this.container.style.left = `${left}px`;
765
813
  this.container.style.top = `${top}px`;
814
+
815
+ // Ensure bubble doesn't overflow viewport horizontally
816
+ // Pass the viewport-relative position for constraint calculation
817
+ const viewportLeft = left - scrollX;
818
+ this._constrainBubbleToViewport(viewportLeft, viewportWidth);
819
+ }
820
+
821
+ /**
822
+ * Constrain bubble width and position to prevent horizontal viewport overflow
823
+ * @private
824
+ */
825
+ _constrainBubbleToViewport(containerLeft, viewportWidth) {
826
+ // Only calculate constraint once per target to prevent oscillation
827
+ if (this._bubbleConstraintApplied) return;
828
+
829
+ const padding = 8; // Minimum padding from viewport edge
830
+
831
+ // Temporarily remove any existing constraint to measure natural width
832
+ const previousMaxWidth = this.bubble.style.maxWidth;
833
+ this.bubble.style.maxWidth = '';
834
+
835
+ // Force layout reflow to get accurate measurement
836
+ const bubbleNaturalWidth = this.bubble.offsetWidth || 100;
837
+
838
+ // Cache the natural width
839
+ this._cachedBubbleNaturalWidth = bubbleNaturalWidth;
840
+
841
+ let needsConstraint = false;
842
+ let constrainedWidth = 0;
843
+
844
+ if (this.isPointingLeft) {
845
+ // Bubble extends to the left of pointer
846
+ // Bubble's left edge = containerLeft - bubbleWidth + 26 (right offset of bubble)
847
+ const bubbleLeftEdge = containerLeft - bubbleNaturalWidth + 26;
848
+
849
+ if (bubbleLeftEdge < padding) {
850
+ // Bubble would overflow left edge - constrain its width
851
+ const availableWidth = containerLeft + 26 - padding;
852
+ constrainedWidth = Math.max(availableWidth, 80);
853
+ needsConstraint = true;
854
+ }
855
+ } else {
856
+ // Bubble extends to the right of pointer
857
+ // Bubble's right edge = containerLeft + 26 + bubbleWidth
858
+ const bubbleRightEdge = containerLeft + 26 + bubbleNaturalWidth;
859
+
860
+ if (bubbleRightEdge > viewportWidth - padding) {
861
+ // Bubble would overflow right edge - constrain its width
862
+ const availableWidth = viewportWidth - containerLeft - 26 - padding;
863
+ constrainedWidth = Math.max(availableWidth, 80);
864
+ needsConstraint = true;
865
+ }
866
+ }
867
+
868
+ if (needsConstraint) {
869
+ this.bubble.style.maxWidth = `${constrainedWidth}px`;
870
+ } else {
871
+ // Restore previous or let CSS variable handle it
872
+ this.bubble.style.maxWidth = previousMaxWidth || '';
873
+ }
874
+
875
+ // Mark constraint as applied for this target
876
+ this._bubbleConstraintApplied = true;
766
877
  }
767
878
 
768
879
  show() {
@@ -1049,6 +1160,10 @@ class Pointy {
1049
1160
  this.currentMessages = Array.isArray(firstStep.content) ? firstStep.content : [firstStep.content];
1050
1161
  this.currentMessageIndex = 0;
1051
1162
  Pointy.renderContent(this.bubbleText, this.currentMessages[0]);
1163
+ this._autoDirectionLocked = false; // Unlock direction for recalculation
1164
+ this._bubbleConstraintApplied = false;
1165
+ this._cachedBubbleNaturalWidth = null;
1166
+ this.bubble.style.maxWidth = '';
1052
1167
  }
1053
1168
 
1054
1169
  // After animation completes
@@ -1690,6 +1805,74 @@ class Pointy {
1690
1805
  this._emit('bubbleFadeDurationChange', { from: oldDuration, to: duration });
1691
1806
  }
1692
1807
 
1808
+ /**
1809
+ * Set the bubble background color
1810
+ * @param {string} color - CSS color value (hex, rgb, etc.)
1811
+ */
1812
+ setBubbleBackgroundColor(color) {
1813
+ const oldColor = this.bubbleBackgroundColor;
1814
+ if (oldColor === color) return;
1815
+
1816
+ this.bubbleBackgroundColor = color;
1817
+ if (color) {
1818
+ this.container.style.setProperty(`--${this.cssVarPrefix}-bubble-bg`, color);
1819
+ } else {
1820
+ this.container.style.removeProperty(`--${this.cssVarPrefix}-bubble-bg`);
1821
+ }
1822
+ this._emit('bubbleBackgroundColorChange', { from: oldColor, to: color });
1823
+ }
1824
+
1825
+ /**
1826
+ * Set the bubble text color
1827
+ * @param {string} color - CSS color value (hex, rgb, etc.)
1828
+ */
1829
+ setBubbleTextColor(color) {
1830
+ const oldColor = this.bubbleTextColor;
1831
+ if (oldColor === color) return;
1832
+
1833
+ this.bubbleTextColor = color;
1834
+ if (color) {
1835
+ this.container.style.setProperty(`--${this.cssVarPrefix}-bubble-color`, color);
1836
+ } else {
1837
+ this.container.style.removeProperty(`--${this.cssVarPrefix}-bubble-color`);
1838
+ }
1839
+ this._emit('bubbleTextColorChange', { from: oldColor, to: color });
1840
+ }
1841
+
1842
+ /**
1843
+ * Set the bubble max width
1844
+ * @param {string} width - CSS width value (e.g., '90vw', '300px')
1845
+ */
1846
+ setBubbleMaxWidth(width) {
1847
+ const oldWidth = this.bubbleMaxWidth;
1848
+ if (oldWidth === width) return;
1849
+
1850
+ this.bubbleMaxWidth = width;
1851
+ if (width) {
1852
+ this.container.style.setProperty(`--${this.cssVarPrefix}-bubble-max-width`, width);
1853
+ } else {
1854
+ this.container.style.removeProperty(`--${this.cssVarPrefix}-bubble-max-width`);
1855
+ }
1856
+ this._emit('bubbleMaxWidthChange', { from: oldWidth, to: width });
1857
+ }
1858
+
1859
+ /**
1860
+ * Set the pointer/cursor color
1861
+ * @param {string} color - CSS color value (hex, rgb, etc.)
1862
+ */
1863
+ setPointerColor(color) {
1864
+ const oldColor = this.pointerColor;
1865
+ if (oldColor === color) return;
1866
+
1867
+ this.pointerColor = color;
1868
+ if (color) {
1869
+ this.container.style.setProperty(`--${this.cssVarPrefix}-pointer-color`, color);
1870
+ } else {
1871
+ this.container.style.removeProperty(`--${this.cssVarPrefix}-pointer-color`);
1872
+ }
1873
+ this._emit('pointerColorChange', { from: oldColor, to: color });
1874
+ }
1875
+
1693
1876
  /**
1694
1877
  * Get the initial position coordinates based on initialPosition setting
1695
1878
  * @returns {{x: number, y: number, isPointingUp?: boolean}} - Position coordinates and optional direction
@@ -1967,9 +2150,13 @@ class Pointy {
1967
2150
  // Parse direction: can be 'up', 'down', 'left', 'right', 'up-left', 'down-right', etc.
1968
2151
  this._parseDirection(step.direction);
1969
2152
 
1970
- // Reset velocity tracking for new target
2153
+ // Reset velocity tracking and direction lock for new target
1971
2154
  this._targetYHistory = [];
1972
2155
  this.lastTargetY = null;
2156
+ this._autoDirectionLocked = false;
2157
+ this._bubbleConstraintApplied = false;
2158
+ this._cachedBubbleNaturalWidth = null;
2159
+ this.bubble.style.maxWidth = '';
1973
2160
 
1974
2161
  // Pause floating animation during movement
1975
2162
  this.container.classList.add(this.classNames.moving);
@@ -2291,9 +2478,13 @@ class Pointy {
2291
2478
  // Parse direction (null means auto)
2292
2479
  this._parseDirection(direction);
2293
2480
 
2294
- // Reset velocity tracking for new target
2481
+ // Reset velocity tracking and direction lock for new target
2295
2482
  this._targetYHistory = [];
2296
2483
  this.lastTargetY = null;
2484
+ this._autoDirectionLocked = false;
2485
+ this._bubbleConstraintApplied = false;
2486
+ this._cachedBubbleNaturalWidth = null;
2487
+ this.bubble.style.maxWidth = '';
2297
2488
 
2298
2489
  // Pause floating animation during movement
2299
2490
  this.container.classList.add(this.classNames.moving);