@diabolic/pointy 1.2.0 → 1.3.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.
package/README.md CHANGED
@@ -80,20 +80,35 @@ Pointy supports multiple content formats:
80
80
 
81
81
  // HTML string with custom layout
82
82
  { target: '#el', content: `
83
- <div style="display: flex; gap: 10px; align-items: flex-start; max-width: 260px; margin: 4px 0;">
83
+ <div style="display: flex; gap: 10px; align-items: flex-start; max-width: 260px;">
84
84
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
85
85
  <circle cx="12" cy="12" r="10"/>
86
86
  <path d="M12 16v-4M12 8h.01"/>
87
87
  </svg>
88
- <span style="line-height: 1.4;">Custom tooltip with icon and flexible multi-line text layout!</span>
88
+ <span style="line-height: 1.4;">Custom tooltip with icon!</span>
89
89
  </div>
90
90
  ` }
91
91
 
92
- // Multiple messages (auto-cycles)
92
+ // Multiple messages (auto-cycles with messageInterval)
93
93
  { target: '#el', content: ['First message', 'Second message', 'Third message'] }
94
94
 
95
- // React/JSX element (if using React)
96
- { target: '#el', content: <MyCustomComponent /> }
95
+ // React/JSX element (requires React & ReactDOM)
96
+ { target: '#el', content: (
97
+ <div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
98
+ <svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
99
+ <circle cx={12} cy={12} r={10} />
100
+ <path d="M12 16v-4" />
101
+ </svg>
102
+ <span>React element with SVG!</span>
103
+ </div>
104
+ ) }
105
+
106
+ // Array of React elements
107
+ { target: '#el', content: [
108
+ <span>First React message</span>,
109
+ <strong>Second React message</strong>,
110
+ <em>Third React message</em>
111
+ ] }
97
112
  ```
98
113
 
99
114
  ### Direction Presets
@@ -123,7 +138,7 @@ pointy.setVerticalDirection('down'); // Only vertical
123
138
  pointy.setDirection(null); // Reset to auto
124
139
 
125
140
  // pointTo with direction
126
- pointy.pointTo('#element', 'Message', 'down-left');
141
+ pointy.pointTo('#element', 'Message', { direction: 'down-left' });
127
142
  ```
128
143
 
129
144
  ### Animation
@@ -216,10 +231,34 @@ pointy.goToMessage(index);
216
231
 
217
232
  ### Point to Custom Target
218
233
 
234
+ Temporarily point to any element without changing the current step:
235
+
219
236
  ```javascript
237
+ // Basic usage
220
238
  pointy.pointTo('#element');
221
239
  pointy.pointTo('#element', 'Custom message');
222
- pointy.pointTo('#element', 'Message', 'down');
240
+
241
+ // With options
242
+ pointy.pointTo('#element', 'Message', {
243
+ direction: 'down', // 'up', 'down', 'left', 'right', 'up-left', etc.
244
+ autoplay: true, // Auto-cycle through messages (if array)
245
+ interval: 2000, // Message cycle interval in ms
246
+ cycle: true // Loop messages (true) or stop at last (false)
247
+ });
248
+
249
+ // Array of messages with autoplay
250
+ pointy.pointTo('#element', [
251
+ 'First tip',
252
+ 'Second tip',
253
+ 'Third tip'
254
+ ], { autoplay: true, interval: 2500 });
255
+
256
+ // Stop at last message (no loop)
257
+ pointy.pointTo('#element', ['Step 1', 'Step 2', 'Done!'], {
258
+ autoplay: true,
259
+ interval: 2000,
260
+ cycle: false // Stops at 'Done!'
261
+ });
223
262
  ```
224
263
 
225
264
  ### Autoplay Control
@@ -393,11 +432,11 @@ pointy.on('all', (data) => {
393
432
  #### Message Cycle
394
433
  | Event | Data |
395
434
  |-------|------|
396
- | `messageCycleStart` | `{ interval, totalMessages }` |
435
+ | `messageCycleStart` | `{ interval, totalMessages, cycle }` |
397
436
  | `messageCycleStop` | `{ currentIndex }` |
398
437
  | `messageCyclePause` | `{ currentIndex }` |
399
438
  | `messageCycleResume` | `{ currentIndex }` |
400
- | `messageCycleComplete` | `{ stepIndex, totalMessages }` |
439
+ | `messageCycleComplete` | `{ stepIndex?, totalMessages }` |
401
440
 
402
441
  #### Pointing
403
442
  | Event | Data |
@@ -566,6 +605,42 @@ tour.setPointerColor('#00ff88');
566
605
  tour.setBubbleBackgroundColor('#2d2d44');
567
606
  ```
568
607
 
608
+ ### React/JSX Content
609
+
610
+ ```jsx
611
+ // With React loaded via CDN or bundler
612
+ const tour = new Pointy({
613
+ steps: [{
614
+ target: '#feature',
615
+ content: (
616
+ <div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
617
+ <svg
618
+ width={20} height={20} viewBox="0 0 24 24"
619
+ fill="none" stroke="currentColor" strokeWidth={2}
620
+ strokeLinecap="round" strokeLinejoin="round"
621
+ >
622
+ <path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" />
623
+ <path d="M12 16v-4" />
624
+ <path d="M12 8h.01" />
625
+ </svg>
626
+ <span>React element with info icon!</span>
627
+ </div>
628
+ )
629
+ }]
630
+ });
631
+
632
+ tour.show();
633
+
634
+ // Or with pointTo and autoplay
635
+ const messages = [
636
+ <span>First tip</span>,
637
+ <strong>Important tip!</strong>,
638
+ <em>Final tip</em>
639
+ ];
640
+
641
+ tour.pointTo('#element', messages, { autoplay: true, interval: 2000 });
642
+ ```
643
+
569
644
  ### Autoplay Tour
570
645
 
571
646
  ```javascript
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Pointy - A lightweight tooltip library with animated pointer
3
- * @version 1.2.0
3
+ * @version 1.3.0
4
4
  * @license MIT
5
5
  */
6
6
  /**
@@ -434,19 +434,32 @@ class Pointy {
434
434
  if (content && typeof content === 'object' && content.$$typeof) {
435
435
  // React element - use ReactDOM if available
436
436
  if (typeof ReactDOM !== 'undefined' && ReactDOM.createRoot) {
437
- // React 18+
438
- const root = ReactDOM.createRoot(element);
439
- root.render(content);
440
- element._reactRoot = root;
437
+ // React 18+ - reuse existing root or create new one
438
+ let root = element._reactRoot;
439
+ if (!root) {
440
+ root = ReactDOM.createRoot(element);
441
+ element._reactRoot = root;
442
+ }
443
+ if (typeof ReactDOM.flushSync === 'function') {
444
+ ReactDOM.flushSync(() => {
445
+ root.render(content);
446
+ });
447
+ } else {
448
+ root.render(content);
449
+ }
441
450
  } else if (typeof ReactDOM !== 'undefined' && ReactDOM.render) {
442
- // React 17 and below
451
+ // React 17 and below (already synchronous)
443
452
  ReactDOM.render(content, element);
444
453
  } else {
445
454
  console.warn('Pointy: React element passed but ReactDOM not found');
446
455
  element.innerHTML = String(content);
447
456
  }
448
457
  } else {
449
- // String content - render as HTML
458
+ // String content - unmount React root if exists, then render as HTML
459
+ if (element._reactRoot) {
460
+ element._reactRoot.unmount();
461
+ element._reactRoot = null;
462
+ }
450
463
  element.innerHTML = content;
451
464
  }
452
465
  }
@@ -1310,6 +1323,45 @@ class Pointy {
1310
1323
  }
1311
1324
  }
1312
1325
 
1326
+ /**
1327
+ * Generate a stable key from React element content
1328
+ * @param {object} element - React element
1329
+ * @returns {string} - Stable key
1330
+ * @private
1331
+ */
1332
+ static _generateReactKey(element) {
1333
+ // Create a deterministic key from element type and props
1334
+ const type = typeof element.type === 'function'
1335
+ ? element.type.name || 'Component'
1336
+ : element.type || 'unknown';
1337
+
1338
+ // Hash the props to create stable key (with fallback for circular refs)
1339
+ let propsStr = '';
1340
+ try {
1341
+ propsStr = JSON.stringify(element.props, (key, value) => {
1342
+ // Skip children, functions, and symbols for hashing
1343
+ if (key === 'children' || typeof value === 'function' || typeof value === 'symbol') return undefined;
1344
+ // Skip React elements in props (they have $$typeof symbol)
1345
+ if (value && typeof value === 'object' && value.$$typeof) return '[ReactElement]';
1346
+ return value;
1347
+ }) || '';
1348
+ } catch (e) {
1349
+ // Fallback for circular references or other stringify errors
1350
+ propsStr = String(Object.keys(element.props || {}).length);
1351
+ }
1352
+
1353
+ // Simple hash function
1354
+ let hash = 0;
1355
+ const str = type + propsStr;
1356
+ for (let i = 0; i < str.length; i++) {
1357
+ const char = str.charCodeAt(i);
1358
+ hash = ((hash << 5) - hash) + char;
1359
+ hash = hash & hash;
1360
+ }
1361
+
1362
+ return `pointy-${type}-${Math.abs(hash).toString(36)}`;
1363
+ }
1364
+
1313
1365
  /**
1314
1366
  * Set messages for current step (internal)
1315
1367
  * @param {string|string[]} content - Single message or array of messages
@@ -1323,8 +1375,27 @@ class Pointy {
1323
1375
  // Stop any existing auto-cycle
1324
1376
  this._stopMessageCycle();
1325
1377
 
1326
- // Normalize to array
1327
- this.currentMessages = Array.isArray(content) ? content : [content];
1378
+ // Normalize to array and add keys to React elements if needed
1379
+ let messages = Array.isArray(content) ? content : [content];
1380
+
1381
+ // Auto-add keys to React elements in array (with collision handling)
1382
+ if (typeof React !== 'undefined' && React.cloneElement && messages.length > 1) {
1383
+ const keyCount = new Map();
1384
+ messages = messages.map((msg) => {
1385
+ // Check if it's a React element without a key
1386
+ if (msg && typeof msg === 'object' && msg.$$typeof && msg.key == null) {
1387
+ const baseKey = Pointy._generateReactKey(msg);
1388
+ const count = keyCount.get(baseKey) || 0;
1389
+ keyCount.set(baseKey, count + 1);
1390
+ // Add suffix for duplicate keys
1391
+ const finalKey = count > 0 ? `${baseKey}-${count}` : baseKey;
1392
+ return React.cloneElement(msg, { key: finalKey });
1393
+ }
1394
+ return msg;
1395
+ });
1396
+ }
1397
+
1398
+ this.currentMessages = messages;
1328
1399
  this.currentMessageIndex = 0;
1329
1400
 
1330
1401
  // Show first message
@@ -1348,27 +1419,40 @@ class Pointy {
1348
1419
 
1349
1420
  /**
1350
1421
  * Start auto-cycling through messages
1422
+ * @param {number} customInterval - Optional custom interval (uses messageInterval if not provided)
1423
+ * @param {boolean} shouldCycle - Whether to cycle back to first message (default: true)
1351
1424
  * @private
1352
1425
  */
1353
- _startMessageCycle() {
1426
+ _startMessageCycle(customInterval, shouldCycle = true) {
1427
+ const intervalToUse = customInterval || this.messageInterval;
1354
1428
  this._messagesCompletedForStep = false;
1429
+ this._shouldCycleMessages = shouldCycle;
1355
1430
  this._messageIntervalId = setInterval(() => {
1356
- // Check if we're at the last message and autoplay is waiting for messages
1431
+ // Check if we're at the last message
1357
1432
  const isLastMessage = this.currentMessageIndex === this.currentMessages.length - 1;
1358
1433
 
1359
- if (isLastMessage && this.autoplay && this.autoplayWaitForMessages) {
1360
- // Don't cycle back to first message - stop here and advance to next step
1361
- this._stopMessageCycle();
1362
- this._messagesCompletedForStep = true;
1363
- this._emit('messageCycleComplete', { stepIndex: this.currentStepIndex, totalMessages: this.currentMessages.length });
1364
- // Trigger autoplay advance after a brief pause
1365
- this._scheduleAutoplayAfterMessages();
1434
+ if (isLastMessage) {
1435
+ if (this.autoplay && this.autoplayWaitForMessages) {
1436
+ // Don't cycle back to first message - stop here and advance to next step
1437
+ this._stopMessageCycle();
1438
+ this._messagesCompletedForStep = true;
1439
+ this._emit('messageCycleComplete', { stepIndex: this.currentStepIndex, totalMessages: this.currentMessages.length });
1440
+ // Trigger autoplay advance after a brief pause
1441
+ this._scheduleAutoplayAfterMessages();
1442
+ } else if (!this._shouldCycleMessages) {
1443
+ // cycle: false - stop at last message
1444
+ this._stopMessageCycle();
1445
+ this._emit('messageCycleComplete', { totalMessages: this.currentMessages.length });
1446
+ } else {
1447
+ // Normal cycling - go back to first
1448
+ this.nextMessage(true); // true = isAuto
1449
+ }
1366
1450
  } else {
1367
1451
  // Normal message cycling
1368
1452
  this.nextMessage(true); // true = isAuto
1369
1453
  }
1370
- }, this.messageInterval);
1371
- this._emit('messageCycleStart', { interval: this.messageInterval, totalMessages: this.currentMessages.length });
1454
+ }, intervalToUse);
1455
+ this._emit('messageCycleStart', { interval: intervalToUse, totalMessages: this.currentMessages.length, cycle: shouldCycle });
1372
1456
  }
1373
1457
 
1374
1458
  /**
@@ -2459,10 +2543,19 @@ class Pointy {
2459
2543
  * Temporarily point to a target without changing the current step.
2460
2544
  * When next() is called, it will continue from where it left off.
2461
2545
  * @param {string|HTMLElement} target - The target element or selector
2462
- * @param {string} content - Optional content to show
2463
- * @param {string} direction - Optional direction: 'up', 'down', 'left', 'right', 'up-left', 'down-right', etc. or null for auto
2464
- */
2465
- pointTo(target, content, direction) {
2546
+ * @param {string|string[]|ReactElement|ReactElement[]} content - Optional content to show
2547
+ * @param {Object} options - Options object
2548
+ * @param {string} options.direction - Optional direction: 'up', 'down', 'left', 'right', 'up-left', 'down-right', etc. or null for auto
2549
+ * @param {boolean} options.autoplay - Auto-cycle through messages if content is array (default: false)
2550
+ * @param {number} options.interval - Message cycle interval in ms (uses default messageInterval if not specified)
2551
+ * @param {boolean} options.cycle - Whether to cycle back to first message or stop at last (default: true)
2552
+ */
2553
+ pointTo(target, content, options = {}) {
2554
+ const direction = options.direction || null;
2555
+ const autoplay = options.autoplay === true;
2556
+ const interval = options.interval || null;
2557
+ const cycle = options.cycle !== false; // default true unless explicitly false
2558
+
2466
2559
  const previousTarget = this.targetElement;
2467
2560
 
2468
2561
  // Determine actual direction ('auto' means will be calculated in updatePosition)
@@ -2503,6 +2596,11 @@ class Pointy {
2503
2596
 
2504
2597
  if (content !== undefined) {
2505
2598
  this._applyMessages(content, false); // false = not from step change, don't auto-start cycle
2599
+
2600
+ // If autoplay is enabled and we have multiple messages, start the cycle
2601
+ if (autoplay && Array.isArray(content) && content.length > 1) {
2602
+ this._startMessageCycle(interval, cycle);
2603
+ }
2506
2604
  }
2507
2605
 
2508
2606
  this.updatePosition();
package/dist/pointy.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Pointy - A lightweight tooltip library with animated pointer
3
- * @version 1.2.0
3
+ * @version 1.3.0
4
4
  * @license MIT
5
5
  */
6
6
  (function (global, factory) {
@@ -440,19 +440,32 @@
440
440
  if (content && typeof content === 'object' && content.$$typeof) {
441
441
  // React element - use ReactDOM if available
442
442
  if (typeof ReactDOM !== 'undefined' && ReactDOM.createRoot) {
443
- // React 18+
444
- const root = ReactDOM.createRoot(element);
445
- root.render(content);
446
- element._reactRoot = root;
443
+ // React 18+ - reuse existing root or create new one
444
+ let root = element._reactRoot;
445
+ if (!root) {
446
+ root = ReactDOM.createRoot(element);
447
+ element._reactRoot = root;
448
+ }
449
+ if (typeof ReactDOM.flushSync === 'function') {
450
+ ReactDOM.flushSync(() => {
451
+ root.render(content);
452
+ });
453
+ } else {
454
+ root.render(content);
455
+ }
447
456
  } else if (typeof ReactDOM !== 'undefined' && ReactDOM.render) {
448
- // React 17 and below
457
+ // React 17 and below (already synchronous)
449
458
  ReactDOM.render(content, element);
450
459
  } else {
451
460
  console.warn('Pointy: React element passed but ReactDOM not found');
452
461
  element.innerHTML = String(content);
453
462
  }
454
463
  } else {
455
- // String content - render as HTML
464
+ // String content - unmount React root if exists, then render as HTML
465
+ if (element._reactRoot) {
466
+ element._reactRoot.unmount();
467
+ element._reactRoot = null;
468
+ }
456
469
  element.innerHTML = content;
457
470
  }
458
471
  }
@@ -1316,6 +1329,45 @@
1316
1329
  }
1317
1330
  }
1318
1331
 
1332
+ /**
1333
+ * Generate a stable key from React element content
1334
+ * @param {object} element - React element
1335
+ * @returns {string} - Stable key
1336
+ * @private
1337
+ */
1338
+ static _generateReactKey(element) {
1339
+ // Create a deterministic key from element type and props
1340
+ const type = typeof element.type === 'function'
1341
+ ? element.type.name || 'Component'
1342
+ : element.type || 'unknown';
1343
+
1344
+ // Hash the props to create stable key (with fallback for circular refs)
1345
+ let propsStr = '';
1346
+ try {
1347
+ propsStr = JSON.stringify(element.props, (key, value) => {
1348
+ // Skip children, functions, and symbols for hashing
1349
+ if (key === 'children' || typeof value === 'function' || typeof value === 'symbol') return undefined;
1350
+ // Skip React elements in props (they have $$typeof symbol)
1351
+ if (value && typeof value === 'object' && value.$$typeof) return '[ReactElement]';
1352
+ return value;
1353
+ }) || '';
1354
+ } catch (e) {
1355
+ // Fallback for circular references or other stringify errors
1356
+ propsStr = String(Object.keys(element.props || {}).length);
1357
+ }
1358
+
1359
+ // Simple hash function
1360
+ let hash = 0;
1361
+ const str = type + propsStr;
1362
+ for (let i = 0; i < str.length; i++) {
1363
+ const char = str.charCodeAt(i);
1364
+ hash = ((hash << 5) - hash) + char;
1365
+ hash = hash & hash;
1366
+ }
1367
+
1368
+ return `pointy-${type}-${Math.abs(hash).toString(36)}`;
1369
+ }
1370
+
1319
1371
  /**
1320
1372
  * Set messages for current step (internal)
1321
1373
  * @param {string|string[]} content - Single message or array of messages
@@ -1329,8 +1381,27 @@
1329
1381
  // Stop any existing auto-cycle
1330
1382
  this._stopMessageCycle();
1331
1383
 
1332
- // Normalize to array
1333
- this.currentMessages = Array.isArray(content) ? content : [content];
1384
+ // Normalize to array and add keys to React elements if needed
1385
+ let messages = Array.isArray(content) ? content : [content];
1386
+
1387
+ // Auto-add keys to React elements in array (with collision handling)
1388
+ if (typeof React !== 'undefined' && React.cloneElement && messages.length > 1) {
1389
+ const keyCount = new Map();
1390
+ messages = messages.map((msg) => {
1391
+ // Check if it's a React element without a key
1392
+ if (msg && typeof msg === 'object' && msg.$$typeof && msg.key == null) {
1393
+ const baseKey = Pointy._generateReactKey(msg);
1394
+ const count = keyCount.get(baseKey) || 0;
1395
+ keyCount.set(baseKey, count + 1);
1396
+ // Add suffix for duplicate keys
1397
+ const finalKey = count > 0 ? `${baseKey}-${count}` : baseKey;
1398
+ return React.cloneElement(msg, { key: finalKey });
1399
+ }
1400
+ return msg;
1401
+ });
1402
+ }
1403
+
1404
+ this.currentMessages = messages;
1334
1405
  this.currentMessageIndex = 0;
1335
1406
 
1336
1407
  // Show first message
@@ -1354,27 +1425,40 @@
1354
1425
 
1355
1426
  /**
1356
1427
  * Start auto-cycling through messages
1428
+ * @param {number} customInterval - Optional custom interval (uses messageInterval if not provided)
1429
+ * @param {boolean} shouldCycle - Whether to cycle back to first message (default: true)
1357
1430
  * @private
1358
1431
  */
1359
- _startMessageCycle() {
1432
+ _startMessageCycle(customInterval, shouldCycle = true) {
1433
+ const intervalToUse = customInterval || this.messageInterval;
1360
1434
  this._messagesCompletedForStep = false;
1435
+ this._shouldCycleMessages = shouldCycle;
1361
1436
  this._messageIntervalId = setInterval(() => {
1362
- // Check if we're at the last message and autoplay is waiting for messages
1437
+ // Check if we're at the last message
1363
1438
  const isLastMessage = this.currentMessageIndex === this.currentMessages.length - 1;
1364
1439
 
1365
- if (isLastMessage && this.autoplay && this.autoplayWaitForMessages) {
1366
- // Don't cycle back to first message - stop here and advance to next step
1367
- this._stopMessageCycle();
1368
- this._messagesCompletedForStep = true;
1369
- this._emit('messageCycleComplete', { stepIndex: this.currentStepIndex, totalMessages: this.currentMessages.length });
1370
- // Trigger autoplay advance after a brief pause
1371
- this._scheduleAutoplayAfterMessages();
1440
+ if (isLastMessage) {
1441
+ if (this.autoplay && this.autoplayWaitForMessages) {
1442
+ // Don't cycle back to first message - stop here and advance to next step
1443
+ this._stopMessageCycle();
1444
+ this._messagesCompletedForStep = true;
1445
+ this._emit('messageCycleComplete', { stepIndex: this.currentStepIndex, totalMessages: this.currentMessages.length });
1446
+ // Trigger autoplay advance after a brief pause
1447
+ this._scheduleAutoplayAfterMessages();
1448
+ } else if (!this._shouldCycleMessages) {
1449
+ // cycle: false - stop at last message
1450
+ this._stopMessageCycle();
1451
+ this._emit('messageCycleComplete', { totalMessages: this.currentMessages.length });
1452
+ } else {
1453
+ // Normal cycling - go back to first
1454
+ this.nextMessage(true); // true = isAuto
1455
+ }
1372
1456
  } else {
1373
1457
  // Normal message cycling
1374
1458
  this.nextMessage(true); // true = isAuto
1375
1459
  }
1376
- }, this.messageInterval);
1377
- this._emit('messageCycleStart', { interval: this.messageInterval, totalMessages: this.currentMessages.length });
1460
+ }, intervalToUse);
1461
+ this._emit('messageCycleStart', { interval: intervalToUse, totalMessages: this.currentMessages.length, cycle: shouldCycle });
1378
1462
  }
1379
1463
 
1380
1464
  /**
@@ -2465,10 +2549,19 @@
2465
2549
  * Temporarily point to a target without changing the current step.
2466
2550
  * When next() is called, it will continue from where it left off.
2467
2551
  * @param {string|HTMLElement} target - The target element or selector
2468
- * @param {string} content - Optional content to show
2469
- * @param {string} direction - Optional direction: 'up', 'down', 'left', 'right', 'up-left', 'down-right', etc. or null for auto
2470
- */
2471
- pointTo(target, content, direction) {
2552
+ * @param {string|string[]|ReactElement|ReactElement[]} content - Optional content to show
2553
+ * @param {Object} options - Options object
2554
+ * @param {string} options.direction - Optional direction: 'up', 'down', 'left', 'right', 'up-left', 'down-right', etc. or null for auto
2555
+ * @param {boolean} options.autoplay - Auto-cycle through messages if content is array (default: false)
2556
+ * @param {number} options.interval - Message cycle interval in ms (uses default messageInterval if not specified)
2557
+ * @param {boolean} options.cycle - Whether to cycle back to first message or stop at last (default: true)
2558
+ */
2559
+ pointTo(target, content, options = {}) {
2560
+ const direction = options.direction || null;
2561
+ const autoplay = options.autoplay === true;
2562
+ const interval = options.interval || null;
2563
+ const cycle = options.cycle !== false; // default true unless explicitly false
2564
+
2472
2565
  const previousTarget = this.targetElement;
2473
2566
 
2474
2567
  // Determine actual direction ('auto' means will be calculated in updatePosition)
@@ -2509,6 +2602,11 @@
2509
2602
 
2510
2603
  if (content !== undefined) {
2511
2604
  this._applyMessages(content, false); // false = not from step change, don't auto-start cycle
2605
+
2606
+ // If autoplay is enabled and we have multiple messages, start the cycle
2607
+ if (autoplay && Array.isArray(content) && content.length > 1) {
2608
+ this._startMessageCycle(interval, cycle);
2609
+ }
2512
2610
  }
2513
2611
 
2514
2612
  this.updatePosition();