agex 0.2.7 → 0.2.10

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 ADDED
@@ -0,0 +1,206 @@
1
+ # agex
2
+
3
+ **Let AI agents prove their work — with video.**
4
+
5
+ AI agents work in the background. They write code, fix bugs, ship features. But how do they prove their work?
6
+
7
+ No screenshots. No recordings. No proof. You just have to trust the logs — or worse, manually test it yourself. A bottleneck.
8
+
9
+ agex lets any AI agent (Cursor, Claude, Codex) open a browser, take screenshots, record video, and deliver visual evidence that the work is done. Run it locally. Run it in CI. Ship with proof.
10
+
11
+ Built on [agent-browser](https://github.com/vercel-labs/agent-browser) by Vercel.
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ npx agex prove "the homepage has a search bar and a sign up button" --url https://github.com
17
+ ```
18
+
19
+ On your PR CI
20
+ ```
21
+ npx agex prove-pr --url http://localhost:3000 # your local dev URL, start your app in the CI
22
+ ```
23
+
24
+ ## Install
25
+
26
+
27
+ ```bash
28
+ npm install -g agex
29
+ ```
30
+
31
+ ## Commands
32
+
33
+ ### `agex prove` - Visual assertion testing
34
+
35
+ Prove visual assertions on any URL with screenshots and video evidence.
36
+
37
+ ```bash
38
+ # Is there a dog in Google's logo? (spoiler: no — and the agent catches it)
39
+ agex prove "google home page has a dog in the logo" \
40
+ --url "https://www.google.com/?hl=en&gl=us" --agent codex
41
+
42
+ # Verify GitHub's Code dropdown has HTTPS/SSH/GitHub CLI tabs
43
+ agex prove "clicking the Code dropdown opens a panel with HTTPS/SSH/GitHub CLI tabs" \
44
+ --url https://github.com/facebook/react
45
+
46
+ # Multi-element verification
47
+ agex prove "github.com homepage has a search bar, a sign up button, and at least 3 navigation links" \
48
+ --url https://github.com --agent claude
49
+
50
+ # Complex page structure
51
+ agex prove "the wikipedia page for JavaScript has a History section, a Syntax section, and an infobox sidebar" \
52
+ --url https://en.wikipedia.org/wiki/JavaScript --agent claude
53
+
54
+ # E-commerce flow
55
+ agex prove "search for 'mechanical keyboard', filter by 4+ stars, verify the first result shows product image, price, and rating" \
56
+ --url https://amazon.com --agent claude
57
+
58
+ # Video content
59
+ agex prove "youtube homepage shows at least 6 video thumbnails with titles and view counts" \
60
+ --url https://www.youtube.com --agent codex
61
+ ```
62
+
63
+ | Flag | Description | Default |
64
+ |------|-------------|---------|
65
+ | `--agent, -a` | Agent to use: `cursor`, `claude`, `codex` | `cursor` |
66
+ | `--url, -u` | URL to navigate to | - |
67
+ | `--output, -o` | Output directory | `./prove-results` |
68
+ | `--video` | Enable video recording | `true` |
69
+ | `--screenshots` | Enable screenshots | `true` |
70
+ | `--model, -m` | Model to use | - |
71
+ | `--timeout, -t` | Timeout in ms | `300000` |
72
+ | `--viewport` | Viewport size (WxH) | `1920x1080` |
73
+ | `--headless` | Run headless | `true` |
74
+ | `--browser, -b` | Browser mode: `mcp` or `cli` | `mcp` |
75
+
76
+ ---
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
+
108
+ ### `agex prove-pr` - PR verification in CI
109
+
110
+ Run it in your CI. It reads the diff, thinks, acts, and reports autonomously.
111
+
112
+ ```bash
113
+ agex prove-pr --base main --url http://localhost:3000
114
+ agex prove-pr --base HEAD~3 --agent claude --hypotheses 10 --hint "focus on mobile layout"
115
+ ```
116
+
117
+ | Flag | Description | Default |
118
+ |------|-------------|---------|
119
+ | `--base` | Base branch/commit to compare against | auto-detected |
120
+ | `--agent, -a` | Agent to use: `cursor`, `claude`, `codex` | `cursor` |
121
+ | `--url, -u` | Dev server URL for visual testing | - |
122
+ | `--output, -o` | Output directory | `./prove-pr-results` |
123
+ | `--hypotheses` | Number of hypotheses to generate | `5` |
124
+ | `--model, -m` | Model to use | - |
125
+ | `--viewport` | Viewport size (WxH) | `1920x1080` |
126
+ | `--hint` | Additional prompt hint for hypothesis generation | - |
127
+ | `--timeout, -t` | Timeout in ms | `300000` |
128
+
129
+ ---
130
+
131
+ ### `agex review` - AI code review
132
+
133
+ Review code changes using an AI agent.
134
+
135
+ ```bash
136
+ agex review --base main --agent cursor
137
+ agex review --base HEAD~3 --agent claude --hint "focus on security"
138
+ agex review --agent codex --hypotheses 3 --no-worktree
139
+ ```
140
+
141
+ | Flag | Description | Default |
142
+ |------|-------------|---------|
143
+ | `--base` | Base branch/commit to compare against | auto-detected |
144
+ | `--agent, -a` | Agent to use: `cursor`, `claude`, `codex` | required |
145
+ | `--model, -m` | Model to use | - |
146
+ | `--worktree` | Include worktree changes | `true` |
147
+ | `--hypotheses` | Number of hypotheses to generate | - |
148
+ | `--hint` | Additional prompt hint | - |
149
+ | `--timeout, -t` | Timeout in ms | `300000` |
150
+
151
+ ---
152
+
153
+ ### `agex run` - Execute AI agent tasks
154
+
155
+ Run a prompt through any supported AI agent.
156
+
157
+ ```bash
158
+ agex run "build a landing page" --agent cursor
159
+ agex run "refactor this function" --agent claude --mode json
160
+ agex run "fix the bug" --approval on-request --timeout 600000
161
+ ```
162
+
163
+ | Flag | Description | Default |
164
+ |------|-------------|---------|
165
+ | `--agent, -a` | Agent to use: `cursor`, `claude`, `codex` | auto-detected |
166
+ | `--model, -m` | Model to use | - |
167
+ | `--mode` | Output mode: `text`, `json`, `debug` | `text` |
168
+ | `--approval` | Approval policy: `never`, `on-request`, `on-failure`, `untrusted` | - |
169
+ | `--timeout` | Timeout in ms | `300000` |
170
+ | `--install` | Run install command before agent | `false` |
171
+ | `--install-command` | Custom install command | - |
172
+ | `--stream` | Enable streaming output | `true` |
173
+ | `--browser` | Enable browser | `true` |
174
+
175
+ ---
176
+
177
+ ### `agex browse` - Browser automation
178
+
179
+ Low-level browser automation via agent-browser.
180
+
181
+ ```bash
182
+ agex browse open https://example.com
183
+ agex browse snapshot -i
184
+ agex browse click @e1
185
+ agex browse fill @e2 "hello@example.com"
186
+ agex browse screenshot page.png
187
+ agex browse close
188
+ ```
189
+
190
+ | Flag | Description | Default |
191
+ |------|-------------|---------|
192
+ | `--install` | Force reinstall browser | `false` |
193
+
194
+ ---
195
+
196
+ ## Supported Agents
197
+
198
+ | Agent | Description |
199
+ |-------|-------------|
200
+ | `cursor` | Cursor IDE agent |
201
+ | `claude` | Anthropic Claude CLI |
202
+ | `codex` | OpenAI Codex CLI |
203
+
204
+ ## Ship with proof.
205
+
206
+ MIT License
@@ -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,22 @@
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;
164
181
  }
165
182
  .${EFFECTS_ID}-drawing-canvas {
166
183
  position: fixed;
@@ -182,6 +199,7 @@
182
199
  // HIGHLIGHT
183
200
  highlight: {
184
201
  add(selector, { color = '#ff4444', style = 'border', duration } = {}) {
202
+ injectStyles();
185
203
  const el = utils.getElement(selector);
186
204
  if (!el) return false;
187
205
 
@@ -213,6 +231,7 @@
213
231
  // ANNOTATION
214
232
  annotation: {
215
233
  add(selector, { text, position = 'top', color = '#333', arrow = true } = {}) {
234
+ injectStyles();
216
235
  const el = utils.getElement(selector);
217
236
  if (!el) return false;
218
237
 
@@ -247,6 +266,7 @@
247
266
  // SPOTLIGHT
248
267
  spotlight: {
249
268
  show(selector, { opacity = 0.7, label = '' } = {}) {
269
+ injectStyles();
250
270
  const el = utils.getElement(selector);
251
271
  if (!el) return false;
252
272
 
@@ -355,10 +375,9 @@
355
375
  const cursor = document.getElementById('agex-cursor');
356
376
  if (!cursor) return;
357
377
 
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`;
378
+ const scales = { normal: 0.75, large: 1, 'extra-large': 1.3 };
379
+ const s = scales[size] || 1;
380
+ cursor.style.transform = `scale(${s})`;
362
381
 
363
382
  cursor.dataset.clickRipple = clickRipple;
364
383
  cursor.dataset.trail = trail;
@@ -390,7 +409,7 @@
390
409
  return true;
391
410
  },
392
411
 
393
- showClick(x, y, color = '#ff4444') {
412
+ showClick(x, y, color = 'rgba(0, 0, 0, 0.15)') {
394
413
  const ripple = utils.createElement('div', {
395
414
  position: 'fixed',
396
415
  left: `${x}px`,
@@ -457,6 +476,117 @@
457
476
  await this.moveTo(points[i + 1].x, points[i + 1].y, segmentDuration);
458
477
  }
459
478
  return true;
479
+ },
480
+
481
+ _lastPos: null,
482
+
483
+ animateToPosition(targetX, targetY) {
484
+ const c = document.getElementById('agex-cursor');
485
+ if (!c) return 0;
486
+
487
+ // Lock demo cursor so native mousemove won't fight the animation
488
+ if (window.__agexCursor) window.__agexCursor.setAnimationLock(true);
489
+
490
+ const vh = window.innerHeight;
491
+ const wasHidden = c.style.opacity === '0' || c.style.opacity === '' || c.style.left === '-100px';
492
+ const startX = wasHidden ? (this._lastPos ? this._lastPos.x : 0) : parseFloat(c.style.left) || 0;
493
+ const startY = wasHidden ? (this._lastPos ? this._lastPos.y : vh / 2) : parseFloat(c.style.top) || vh / 2;
494
+
495
+ c.style.transition = 'none';
496
+ c.style.left = startX + 'px';
497
+ c.style.top = startY + 'px';
498
+ c.style.opacity = '1';
499
+ c.offsetHeight;
500
+
501
+ if (isNaN(targetX) || isNaN(targetY)) {
502
+ if (window.__agexCursor) window.__agexCursor.setAnimationLock(false);
503
+ return 0;
504
+ }
505
+
506
+ this._lastPos = { x: targetX, y: targetY };
507
+
508
+ const dist = Math.sqrt((targetX - startX) ** 2 + (targetY - startY) ** 2);
509
+ const duration = Math.max(1200, Math.round(dist / 0.3));
510
+ const start = performance.now();
511
+ const self = this;
512
+
513
+ const step = (now) => {
514
+ const elapsed = now - start;
515
+ const t = Math.min(elapsed / duration, 1);
516
+ const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
517
+ c.style.left = (startX + (targetX - startX) * ease) + 'px';
518
+ c.style.top = (startY + (targetY - startY) * ease) + 'px';
519
+ if (t < 1) {
520
+ requestAnimationFrame(step);
521
+ } else {
522
+ // Animation done — release lock
523
+ if (window.__agexCursor) window.__agexCursor.setAnimationLock(false);
524
+ }
525
+ };
526
+ requestAnimationFrame(step);
527
+ return duration;
528
+ },
529
+
530
+ showInputFocus(selector) {
531
+ this.clearInputFocus();
532
+ var el = utils.getElement(selector);
533
+ if (!el) return;
534
+ var rect = el.getBoundingClientRect();
535
+ var overlay = document.createElement('div');
536
+ overlay.id = EFFECTS_ID + '-input-focus';
537
+ overlay.className = EFFECTS_ID + '-input-focus';
538
+ overlay.style.left = (rect.left + window.scrollX - 2) + 'px';
539
+ overlay.style.top = (rect.top + window.scrollY - 2) + 'px';
540
+ overlay.style.width = (rect.width + 4) + 'px';
541
+ overlay.style.height = (rect.height + 4) + 'px';
542
+ document.body.appendChild(overlay);
543
+ },
544
+
545
+ clearInputFocus() {
546
+ var el = document.getElementById(EFFECTS_ID + '-input-focus');
547
+ if (el) el.remove();
548
+ },
549
+
550
+ typeText(selector, text, charDelay) {
551
+ charDelay = charDelay || 80;
552
+ var el = utils.getElement(selector);
553
+ if (!el) return 0;
554
+ el.focus();
555
+ var i = 0;
556
+ var len = text.length;
557
+ function typeNext() {
558
+ if (i >= len) return;
559
+ var ch = text[i];
560
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: ch, bubbles: true }));
561
+ el.dispatchEvent(new KeyboardEvent('keypress', { key: ch, bubbles: true }));
562
+ // For contenteditable elements, insert text differently
563
+ if (el.isContentEditable) {
564
+ document.execCommand('insertText', false, ch);
565
+ } else {
566
+ // Use InputEvent for proper React/framework compatibility
567
+ var nativeInputValueSetter = Object.getOwnPropertyDescriptor(
568
+ Object.getPrototypeOf(el), 'value'
569
+ );
570
+ if (nativeInputValueSetter && nativeInputValueSetter.set) {
571
+ nativeInputValueSetter.set.call(el, el.value + ch);
572
+ } else {
573
+ el.value = el.value + ch;
574
+ }
575
+ el.dispatchEvent(new Event('input', { bubbles: true }));
576
+ }
577
+ el.dispatchEvent(new KeyboardEvent('keyup', { key: ch, bubbles: true }));
578
+ i++;
579
+ if (i < len) setTimeout(typeNext, charDelay);
580
+ }
581
+ typeNext();
582
+ return len * charDelay;
583
+ },
584
+
585
+ fadeOut() {
586
+ const c = document.getElementById('agex-cursor');
587
+ if (!c) return;
588
+ c.style.transition = 'opacity 0.4s ease-out';
589
+ c.style.opacity = '0';
460
590
  }
461
591
  },
462
592
 
@@ -1289,12 +1419,15 @@
1289
1419
  });
1290
1420
  },
1291
1421
 
1292
- showSubtitle(text, duration = 3000, position = 'bottom') {
1422
+ showSubtitle(text, duration = 3000, position = 'bottom', fg = '', bg = '') {
1423
+ injectStyles();
1293
1424
  this.clear();
1294
1425
  const subtitle = utils.createElement('div', {
1295
1426
  [position]: '40px'
1296
1427
  }, `${EFFECTS_ID}-subtitle`);
1297
1428
  subtitle.classList.add(`${EFFECTS_ID}-subtitle`);
1429
+ if (fg) subtitle.style.setProperty('--subtitle-fg', fg);
1430
+ if (bg) subtitle.style.setProperty('--subtitle-bg', bg);
1298
1431
  subtitle.textContent = text;
1299
1432
  document.body.appendChild(subtitle);
1300
1433
 
@@ -1305,6 +1438,7 @@
1305
1438
 
1306
1439
  clear() {
1307
1440
  utils.removeElements(`#${EFFECTS_ID}-subtitle`);
1441
+ utils.removeElements(`.${EFFECTS_ID}-subtitle`);
1308
1442
  if ('speechSynthesis' in window) {
1309
1443
  speechSynthesis.cancel();
1310
1444
  }
@@ -1499,6 +1633,14 @@
1499
1633
  if (el) el.scrollIntoView({ block: 'center', behavior: 'instant' });
1500
1634
  },
1501
1635
 
1636
+ scrollElementToCenter(selector) {
1637
+ var el = typeof selector === 'string' ? document.querySelector(selector) : selector;
1638
+ if (!el) return false;
1639
+ // scrollIntoView with block:'center' handles nested scrollable containers
1640
+ el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
1641
+ return true;
1642
+ },
1643
+
1502
1644
  scrollToCoords(y, viewportHeight, elHeight) {
1503
1645
  window.scrollTo({
1504
1646
  top: window.scrollY + y - (viewportHeight / 2) + (elHeight / 2),