agex 0.2.8 → 0.2.11

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
@@ -75,6 +75,36 @@ agex prove "youtube homepage shows at least 6 video thumbnails with titles and v
75
75
 
76
76
  ---
77
77
 
78
+ ### `agex demo` - Record demo videos
79
+
80
+ Record a narrated browser walkthrough as a video. The agent opens a URL, performs the task, and captures the session.
81
+
82
+ ```bash
83
+ # Record a demo of selecting a brand design from a menu
84
+ agex demo "how to select a brand design from the menu" --url https://example.com
85
+
86
+ # With subtitles and custom colors (via task text)
87
+ agex demo "how to create a new project. use subtitles with white text on red background" \
88
+ --url https://app.example.com --agent cursor
89
+
90
+ # Non-headless for debugging
91
+ agex demo "sign up for a free trial" --url https://example.com --no-headless
92
+ ```
93
+
94
+ | Flag | Description | Default |
95
+ |------|-------------|---------|
96
+ | `--agent, -a` | Agent to use: `cursor`, `claude`, `codex` | auto-detected |
97
+ | `--url, -u` | URL to navigate to | - |
98
+ | `--output, -o` | Output directory | `./demo-results` |
99
+ | `--video` | Enable video recording | `true` |
100
+ | `--screenshots` | Enable screenshots | `true` |
101
+ | `--model, -m` | Model to use | - |
102
+ | `--timeout, -t` | Timeout in ms | `300000` |
103
+ | `--viewport` | Viewport size (WxH) | `1920x1080` |
104
+ | `--headless` | Run headless | `true` |
105
+
106
+ ---
107
+
78
108
  ### `agex prove-pr` - PR verification in CI
79
109
 
80
110
  Run it in your CI. It reads the diff, thinks, acts, and reports autonomously.
@@ -3,6 +3,19 @@
3
3
  let clickRippleEnabled = true;
4
4
  let trailEnabled = false;
5
5
  let trailPoints = [];
6
+ let currentMode = 'default'; // 'default' or 'pointer'
7
+ let animationLock = false; // when true, mousemove won't update demo cursor position
8
+
9
+ const ARROW_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
10
+ <path d="M8.5 3L8.5 24.2l5-5.6 3.6 7.7 3.5-1.5-3.7-7.6 7.1-.5L8.5 3z" fill="#000" stroke="#fff" stroke-width="1.8" stroke-linejoin="round"></path>
11
+ </svg>`;
12
+
13
+ const POINTER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
14
+ <g transform="rotate(-20, 16, 16)">
15
+ <path d="M14 2 C14 2 11.5 2 11.5 4.5 L11.5 15 L10 13.5 C10 13.5 8.5 12 7 12.5 C5.5 13 5.8 14.5 6.5 15.8 L10.5 22.5 C12 25.5 14.5 27.5 18 27.5 L19.5 27.5 C23.5 27.5 26.5 24 26.5 20 L26.5 15.5 C26.5 14 25 13 23.5 13.5 C23 13.7 22.5 14.2 22.5 15 L22.5 14 C22.5 12.5 21 11.5 19.5 12 C19 12.2 18.5 12.7 18.5 13.5 L18.5 12.5 C18.5 11 17 10 15.5 10.5 C15 10.7 14.5 11.3 14.5 12 L14.5 4.5 C14.5 4.5 14.5 2 14 2 Z" fill="#000" stroke="#fff" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>
16
+ <line x1="18.5" y1="13" x2="18.5" y2="17" stroke="#fff" stroke-width="0.6" opacity="0.3"/><line x1="22.5" y1="14.5" x2="22.5" y2="17" stroke="#fff" stroke-width="0.6" opacity="0.3"/>
17
+ </g>
18
+ </svg>`;
6
19
 
7
20
  function createCursor() {
8
21
  if (document.getElementById('agex-cursor')) return;
@@ -10,21 +23,36 @@
10
23
  const cursor = document.createElement('div');
11
24
  cursor.id = 'agex-cursor';
12
25
  cursor.style.cssText = `
13
- width: 24px;
14
- height: 24px;
15
- background: rgba(255, 50, 50, 0.85);
16
- border: 2px solid white;
17
- border-radius: 50%;
18
26
  position: fixed;
19
27
  pointer-events: none;
20
28
  z-index: 2147483647;
21
- transform: translate(-50%, -50%);
22
- box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
23
29
  left: -100px;
24
30
  top: -100px;
25
31
  opacity: 0;
26
- transition: left 0.08s ease-out, top 0.08s ease-out, opacity 0.15s, transform 0.1s, background 0.1s;
32
+ transition: opacity 0.15s, transform 0.1s ease-out;
33
+ transform-origin: 2px 2px;
34
+ will-change: left, top;
35
+ `;
36
+
37
+ const dot = document.createElement('div');
38
+ dot.className = 'agex-cursor-dot';
39
+ dot.style.cssText = `
40
+ position: absolute;
41
+ width: 38px;
42
+ height: 38px;
43
+ border-radius: 50%;
44
+ background: rgb(251, 255, 0, 0.45);
45
+ top: -16px;
46
+ left: -10px;
47
+ pointer-events: none;
27
48
  `;
49
+
50
+ const svg = document.createElement('div');
51
+ svg.className = 'agex-cursor-svg';
52
+ svg.innerHTML = ARROW_SVG;
53
+
54
+ cursor.appendChild(dot);
55
+ cursor.appendChild(svg);
28
56
  document.body.appendChild(cursor);
29
57
 
30
58
  const style = document.createElement('style');
@@ -33,20 +61,28 @@
33
61
  .agex-cursor-ripple {
34
62
  position: fixed;
35
63
  border-radius: 50%;
36
- background: rgba(255, 50, 50, 0.4);
37
64
  pointer-events: none;
38
65
  z-index: 2147483646;
39
66
  transform: translate(-50%, -50%) scale(0);
40
- animation: agex-ripple 0.4s ease-out forwards;
67
+ animation: agex-ripple 0.5s ease-out forwards;
68
+ }
69
+ .agex-cursor-ripple-ring {
70
+ position: fixed;
71
+ border-radius: 50%;
72
+ border: 3px solid rgba(0, 0, 0, 0.4);
73
+ pointer-events: none;
74
+ z-index: 2147483646;
75
+ transform: translate(-50%, -50%) scale(0);
76
+ animation: agex-ripple 0.6s ease-out forwards;
41
77
  }
42
78
  @keyframes agex-ripple {
43
79
  to { transform: translate(-50%, -50%) scale(1); opacity: 0; }
44
80
  }
45
81
  .agex-cursor-trail {
46
82
  position: fixed;
47
- width: 8px;
48
- height: 8px;
49
- background: rgba(255, 50, 50, 0.3);
83
+ width: 6px;
84
+ height: 6px;
85
+ background: rgba(0, 0, 0, 0.12);
50
86
  border-radius: 50%;
51
87
  pointer-events: none;
52
88
  z-index: 2147483645;
@@ -55,16 +91,29 @@
55
91
  `;
56
92
  document.head.appendChild(style);
57
93
 
94
+ const CLICKABLE = 'a, button, [role="button"], input[type="submit"], input[type="button"], select, label[for], [onclick], [data-action], summary, [tabindex]';
95
+
58
96
  document.addEventListener('mousemove', (e) => {
59
97
  const c = document.getElementById('agex-cursor');
60
98
  if (c) {
61
- c.style.left = `${e.clientX}px`;
62
- c.style.top = `${e.clientY}px`;
63
- if (!hasMoved) {
64
- hasMoved = true;
65
- c.style.opacity = '1';
99
+ if (!animationLock) {
100
+ c.style.left = `${e.clientX}px`;
101
+ c.style.top = `${e.clientY}px`;
102
+ if (!hasMoved) {
103
+ hasMoved = true;
104
+ c.style.opacity = '1';
105
+ }
106
+ }
107
+
108
+ const hovered = document.elementFromPoint(e.clientX, e.clientY);
109
+ const isClickable = hovered && hovered.closest(CLICKABLE);
110
+ const newMode = isClickable ? 'pointer' : 'default';
111
+ if (newMode !== currentMode) {
112
+ currentMode = newMode;
113
+ const svgEl = c.querySelector('.agex-cursor-svg');
114
+ if (svgEl) svgEl.innerHTML = currentMode === 'pointer' ? POINTER_SVG : ARROW_SVG;
66
115
  }
67
-
116
+
68
117
  if (trailEnabled) {
69
118
  createTrailPoint(e.clientX, e.clientY);
70
119
  }
@@ -74,9 +123,8 @@
74
123
  document.addEventListener('mousedown', (e) => {
75
124
  const c = document.getElementById('agex-cursor');
76
125
  if (c) {
77
- c.style.transform = 'translate(-50%, -50%) scale(0.8)';
78
- c.style.background = 'rgba(255, 200, 50, 1)';
79
-
126
+ c.style.transform = 'scale(0.85)';
127
+
80
128
  if (clickRippleEnabled) {
81
129
  createClickRipple(e.clientX, e.clientY);
82
130
  }
@@ -86,21 +134,30 @@
86
134
  document.addEventListener('mouseup', () => {
87
135
  const c = document.getElementById('agex-cursor');
88
136
  if (c) {
89
- c.style.transform = 'translate(-50%, -50%) scale(1)';
90
- c.style.background = 'rgba(255, 50, 50, 0.85)';
137
+ c.style.transform = 'scale(1)';
91
138
  }
92
139
  }, true);
93
140
  }
94
141
 
95
142
  function createClickRipple(x, y) {
96
- const ripple = document.createElement('div');
97
- ripple.className = 'agex-cursor-ripple';
98
- ripple.style.left = `${x}px`;
99
- ripple.style.top = `${y}px`;
100
- ripple.style.width = '60px';
101
- ripple.style.height = '60px';
102
- document.body.appendChild(ripple);
103
- setTimeout(() => ripple.remove(), 400);
143
+ const fill = document.createElement('div');
144
+ fill.className = 'agex-cursor-ripple';
145
+ fill.style.left = `${x}px`;
146
+ fill.style.top = `${y}px`;
147
+ fill.style.width = '50px';
148
+ fill.style.height = '50px';
149
+ fill.style.background = 'rgba(251, 255, 0, 0.35)';
150
+ document.body.appendChild(fill);
151
+ setTimeout(() => fill.remove(), 500);
152
+
153
+ const ring = document.createElement('div');
154
+ ring.className = 'agex-cursor-ripple-ring';
155
+ ring.style.left = `${x}px`;
156
+ ring.style.top = `${y}px`;
157
+ ring.style.width = '70px';
158
+ ring.style.height = '70px';
159
+ document.body.appendChild(ring);
160
+ setTimeout(() => ring.remove(), 600);
104
161
  }
105
162
 
106
163
  function createTrailPoint(x, y) {
@@ -122,15 +179,16 @@
122
179
  }, 400);
123
180
  }
124
181
 
125
- // Expose cursor configuration API
126
182
  window.__agexCursor = {
183
+ setAnimationLock(locked) {
184
+ animationLock = locked;
185
+ },
127
186
  setSize(size) {
128
187
  const cursor = document.getElementById('agex-cursor');
129
188
  if (!cursor) return;
130
- const sizes = { normal: 16, large: 24, 'extra-large': 32 };
131
- const s = sizes[size] || sizes.large;
132
- cursor.style.width = `${s}px`;
133
- cursor.style.height = `${s}px`;
189
+ const scales = { normal: 0.75, large: 1, 'extra-large': 1.3 };
190
+ const s = scales[size] || scales.large;
191
+ cursor.style.transform = `scale(${s})`;
134
192
  },
135
193
  setClickRipple(enabled) {
136
194
  clickRippleEnabled = enabled;
@@ -143,6 +201,26 @@
143
201
  if (options.clickRipple !== undefined) this.setClickRipple(options.clickRipple);
144
202
  if (options.trail !== undefined) this.setTrail(options.trail);
145
203
  },
204
+ setMode(mode) {
205
+ const cursor = document.getElementById('agex-cursor');
206
+ if (!cursor) return;
207
+ const svgEl = cursor.querySelector('.agex-cursor-svg');
208
+ if (!svgEl) return;
209
+ if (mode === 'pointer') {
210
+ currentMode = 'pointer';
211
+ svgEl.innerHTML = POINTER_SVG;
212
+ } else {
213
+ currentMode = 'default';
214
+ svgEl.innerHTML = ARROW_SVG;
215
+ }
216
+ },
217
+ showClickRipple() {
218
+ const cursor = document.getElementById('agex-cursor');
219
+ if (!cursor) return;
220
+ const x = parseFloat(cursor.style.left) || 0;
221
+ const y = parseFloat(cursor.style.top) || 0;
222
+ createClickRipple(x, y);
223
+ },
146
224
  sync() {
147
225
  const cursor = document.getElementById('agex-cursor');
148
226
  if (!cursor) return Promise.resolve('no cursor');
@@ -95,6 +95,18 @@
95
95
  0%, 100% { box-shadow: 0 0 20px var(--color, #ff4444), inset 0 0 20px var(--color, #ff4444); }
96
96
  50% { box-shadow: 0 0 35px var(--color, #ff4444), inset 0 0 35px var(--color, #ff4444); }
97
97
  }
98
+ .${EFFECTS_ID}-input-focus {
99
+ position: absolute;
100
+ pointer-events: none;
101
+ z-index: 2147483639;
102
+ border-radius: 6px;
103
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5), 0 0 16px rgba(59, 130, 246, 0.3);
104
+ animation: ${EFFECTS_ID}-input-focus-pulse 1.5s ease-in-out infinite;
105
+ }
106
+ @keyframes ${EFFECTS_ID}-input-focus-pulse {
107
+ 0%, 100% { box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5), 0 0 16px rgba(59, 130, 246, 0.3); }
108
+ 50% { box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.7), 0 0 24px rgba(59, 130, 246, 0.5); }
109
+ }
98
110
  .${EFFECTS_ID}-highlight.pulse {
99
111
  animation: ${EFFECTS_ID}-pulse 1s ease-in-out infinite;
100
112
  border: 3px solid var(--color, #ff4444);
@@ -150,17 +162,33 @@
150
162
  font-family: system-ui, sans-serif;
151
163
  transition: all 0.3s ease;
152
164
  }
165
+ @keyframes ${EFFECTS_ID}-subtitle-slide-down {
166
+ from { opacity: 0; transform: translateX(-50%) translateY(-20px); }
167
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
168
+ }
153
169
  .${EFFECTS_ID}-subtitle {
154
170
  position: fixed;
155
171
  left: 50%;
156
172
  transform: translateX(-50%);
157
- background: rgba(0, 0, 0, 0.8);
158
- color: white;
173
+ background: var(--subtitle-bg, rgba(0, 0, 0, 0.8));
174
+ color: var(--subtitle-fg, white);
159
175
  padding: 12px 24px;
160
176
  border-radius: 8px;
161
177
  font-size: 18px;
162
178
  max-width: 80%;
163
179
  text-align: center;
180
+ animation: ${EFFECTS_ID}-subtitle-slide-down 0.35s ease-out;
181
+ overflow: hidden;
182
+ }
183
+ .${EFFECTS_ID}-subtitle-progress {
184
+ position: absolute;
185
+ bottom: 0;
186
+ left: 0;
187
+ height: 2px;
188
+ background: color-mix(in srgb, var(--subtitle-fg, white) 15%, transparent);
189
+ border-radius: 0 0 8px 8px;
190
+ width: 0%;
191
+ transition: none;
164
192
  }
165
193
  .${EFFECTS_ID}-drawing-canvas {
166
194
  position: fixed;
@@ -182,6 +210,7 @@
182
210
  // HIGHLIGHT
183
211
  highlight: {
184
212
  add(selector, { color = '#ff4444', style = 'border', duration } = {}) {
213
+ injectStyles();
185
214
  const el = utils.getElement(selector);
186
215
  if (!el) return false;
187
216
 
@@ -213,6 +242,7 @@
213
242
  // ANNOTATION
214
243
  annotation: {
215
244
  add(selector, { text, position = 'top', color = '#333', arrow = true } = {}) {
245
+ injectStyles();
216
246
  const el = utils.getElement(selector);
217
247
  if (!el) return false;
218
248
 
@@ -247,6 +277,7 @@
247
277
  // SPOTLIGHT
248
278
  spotlight: {
249
279
  show(selector, { opacity = 0.7, label = '' } = {}) {
280
+ injectStyles();
250
281
  const el = utils.getElement(selector);
251
282
  if (!el) return false;
252
283
 
@@ -355,10 +386,9 @@
355
386
  const cursor = document.getElementById('agex-cursor');
356
387
  if (!cursor) return;
357
388
 
358
- const sizes = { normal: 16, large: 24, 'extra-large': 32 };
359
- const s = sizes[size] || 24;
360
- cursor.style.width = `${s}px`;
361
- cursor.style.height = `${s}px`;
389
+ const scales = { normal: 0.75, large: 1, 'extra-large': 1.3 };
390
+ const s = scales[size] || 1;
391
+ cursor.style.transform = `scale(${s})`;
362
392
 
363
393
  cursor.dataset.clickRipple = clickRipple;
364
394
  cursor.dataset.trail = trail;
@@ -390,7 +420,7 @@
390
420
  return true;
391
421
  },
392
422
 
393
- showClick(x, y, color = '#ff4444') {
423
+ showClick(x, y, color = 'rgba(0, 0, 0, 0.15)') {
394
424
  const ripple = utils.createElement('div', {
395
425
  position: 'fixed',
396
426
  left: `${x}px`,
@@ -457,6 +487,91 @@
457
487
  await this.moveTo(points[i + 1].x, points[i + 1].y, segmentDuration);
458
488
  }
459
489
  return true;
490
+ },
491
+
492
+ _lastPos: null,
493
+
494
+ animateToPosition(targetX, targetY) {
495
+ const c = document.getElementById('agex-cursor');
496
+ if (!c) return 0;
497
+
498
+ // Lock demo cursor so native mousemove won't fight the animation
499
+ if (window.__agexCursor) window.__agexCursor.setAnimationLock(true);
500
+
501
+ const vh = window.innerHeight;
502
+ const wasHidden = c.style.opacity === '0' || c.style.opacity === '' || c.style.left === '-100px';
503
+ const startX = wasHidden ? (this._lastPos ? this._lastPos.x : 0) : parseFloat(c.style.left) || 0;
504
+ const startY = wasHidden ? (this._lastPos ? this._lastPos.y : vh / 2) : parseFloat(c.style.top) || vh / 2;
505
+
506
+ c.style.transition = 'none';
507
+ c.style.left = startX + 'px';
508
+ c.style.top = startY + 'px';
509
+ c.style.opacity = '1';
510
+ c.offsetHeight;
511
+
512
+ if (isNaN(targetX) || isNaN(targetY)) {
513
+ if (window.__agexCursor) window.__agexCursor.setAnimationLock(false);
514
+ return 0;
515
+ }
516
+
517
+ this._lastPos = { x: targetX, y: targetY };
518
+
519
+ const dist = Math.sqrt((targetX - startX) ** 2 + (targetY - startY) ** 2);
520
+ const duration = Math.max(1200, Math.round(dist / 0.3));
521
+ const start = performance.now();
522
+ const self = this;
523
+
524
+ const step = (now) => {
525
+ const elapsed = now - start;
526
+ const t = Math.min(elapsed / duration, 1);
527
+ const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
528
+ c.style.left = (startX + (targetX - startX) * ease) + 'px';
529
+ c.style.top = (startY + (targetY - startY) * ease) + 'px';
530
+ if (t < 1) {
531
+ requestAnimationFrame(step);
532
+ } else {
533
+ // Animation done — release lock
534
+ if (window.__agexCursor) window.__agexCursor.setAnimationLock(false);
535
+ }
536
+ };
537
+ requestAnimationFrame(step);
538
+ return duration;
539
+ },
540
+
541
+ showInputFocus(selector) {
542
+ this.clearInputFocus();
543
+ var el = utils.getElement(selector);
544
+ if (!el) return;
545
+ var rect = el.getBoundingClientRect();
546
+ var overlay = document.createElement('div');
547
+ overlay.id = EFFECTS_ID + '-input-focus';
548
+ overlay.className = EFFECTS_ID + '-input-focus';
549
+ overlay.style.left = (rect.left + window.scrollX - 2) + 'px';
550
+ overlay.style.top = (rect.top + window.scrollY - 2) + 'px';
551
+ overlay.style.width = (rect.width + 4) + 'px';
552
+ overlay.style.height = (rect.height + 4) + 'px';
553
+ document.body.appendChild(overlay);
554
+ },
555
+
556
+ clearInputFocus() {
557
+ var el = document.getElementById(EFFECTS_ID + '-input-focus');
558
+ if (el) el.remove();
559
+ },
560
+
561
+ setPosition(x, y) {
562
+ var c = document.getElementById('agex-cursor');
563
+ if (!c) return;
564
+ c.style.transition = 'none';
565
+ c.style.left = x + 'px';
566
+ c.style.top = y + 'px';
567
+ this._lastPos = { x: x, y: y };
568
+ },
569
+
570
+ fadeOut() {
571
+ const c = document.getElementById('agex-cursor');
572
+ if (!c) return;
573
+ c.style.transition = 'opacity 0.4s ease-out';
574
+ c.style.opacity = '0';
460
575
  }
461
576
  },
462
577
 
@@ -1289,22 +1404,45 @@
1289
1404
  });
1290
1405
  },
1291
1406
 
1292
- showSubtitle(text, duration = 3000, position = 'bottom') {
1407
+ showSubtitle(text, duration = 3000, position = 'bottom', fg = '', bg = '') {
1408
+ injectStyles();
1293
1409
  this.clear();
1294
1410
  const subtitle = utils.createElement('div', {
1295
1411
  [position]: '40px'
1296
1412
  }, `${EFFECTS_ID}-subtitle`);
1297
1413
  subtitle.classList.add(`${EFFECTS_ID}-subtitle`);
1414
+ if (fg) subtitle.style.setProperty('--subtitle-fg', fg);
1415
+ if (bg) subtitle.style.setProperty('--subtitle-bg', bg);
1298
1416
  subtitle.textContent = text;
1417
+
1418
+ var bar = document.createElement('div');
1419
+ bar.className = `${EFFECTS_ID}-subtitle-progress`;
1420
+ subtitle.appendChild(bar);
1421
+
1422
+ var startTime = performance.now();
1423
+ var barDuration = duration > 0 ? duration : 30000;
1424
+ var rafId = 0;
1425
+ function tick() {
1426
+ var elapsed = performance.now() - startTime;
1427
+ var pct = Math.min(elapsed / barDuration * 100, 100);
1428
+ bar.style.width = pct + '%';
1429
+ if (pct < 100) rafId = requestAnimationFrame(tick);
1430
+ }
1431
+ rafId = requestAnimationFrame(tick);
1432
+
1299
1433
  document.body.appendChild(subtitle);
1300
1434
 
1301
1435
  if (duration > 0) {
1302
- setTimeout(() => subtitle.remove(), duration);
1436
+ setTimeout(function() {
1437
+ cancelAnimationFrame(rafId);
1438
+ subtitle.remove();
1439
+ }, duration);
1303
1440
  }
1304
1441
  },
1305
1442
 
1306
1443
  clear() {
1307
1444
  utils.removeElements(`#${EFFECTS_ID}-subtitle`);
1445
+ utils.removeElements(`.${EFFECTS_ID}-subtitle`);
1308
1446
  if ('speechSynthesis' in window) {
1309
1447
  speechSynthesis.cancel();
1310
1448
  }
@@ -1499,6 +1637,26 @@
1499
1637
  if (el) el.scrollIntoView({ block: 'center', behavior: 'instant' });
1500
1638
  },
1501
1639
 
1640
+ scrollElementToCenter(selector) {
1641
+ var el = typeof selector === 'string' ? document.querySelector(selector) : selector;
1642
+ if (!el) return false;
1643
+ el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
1644
+ return true;
1645
+ },
1646
+
1647
+ scrollElementToCenterSmooth(selector) {
1648
+ var el = typeof selector === 'string' ? document.querySelector(selector) : selector;
1649
+ if (!el) return 0;
1650
+ var rect = el.getBoundingClientRect();
1651
+ var viewH = window.innerHeight;
1652
+ var centerY = rect.top + rect.height / 2;
1653
+ if (centerY > viewH * 0.2 && centerY < viewH * 0.8) return 0;
1654
+ var dist = Math.abs(centerY - viewH / 2);
1655
+ var duration = Math.max(600, Math.min(2000, Math.round(dist * 1.5)));
1656
+ el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'smooth' });
1657
+ return duration;
1658
+ },
1659
+
1502
1660
  scrollToCoords(y, viewportHeight, elHeight) {
1503
1661
  window.scrollTo({
1504
1662
  top: window.scrollY + y - (viewportHeight / 2) + (elHeight / 2),
@@ -1543,6 +1701,31 @@
1543
1701
  }
1544
1702
  };
1545
1703
 
1704
+ // Enable or disable page CSS animations & transitions (preserves agex effects)
1705
+ function setPageAnimations(enabled) {
1706
+ var id = EFFECTS_ID + '-disable-animations';
1707
+ var existing = document.getElementById(id);
1708
+ if (enabled) {
1709
+ if (existing) existing.remove();
1710
+ return;
1711
+ }
1712
+ if (existing) return;
1713
+ var style = document.createElement('style');
1714
+ style.id = id;
1715
+ style.textContent = `
1716
+ *:not(.${EFFECTS_ID}):not(.${EFFECTS_ID} *) {
1717
+ animation: none !important;
1718
+ animation-delay: 0s !important;
1719
+ animation-duration: 0s !important;
1720
+ transition: none !important;
1721
+ transition-delay: 0s !important;
1722
+ transition-duration: 0s !important;
1723
+ scroll-behavior: auto !important;
1724
+ }
1725
+ `;
1726
+ document.head.appendChild(style);
1727
+ }
1728
+
1546
1729
  // Clear all effects
1547
1730
  function clearAll() {
1548
1731
  effects.highlight.clear();
@@ -1578,6 +1761,7 @@
1578
1761
  window.__agexEffects = {
1579
1762
  apply,
1580
1763
  clearAll,
1764
+ setPageAnimations,
1581
1765
  banner,
1582
1766
  proof,
1583
1767
  ...effects,