@promakeai/inspector 0.1.2 → 0.2.1

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
@@ -1,6 +1,7 @@
1
1
  # @promakeai/inspector
2
2
 
3
3
  [![Build Status](https://github.com/promakeai/inspector/actions/workflows/build.yml/badge.svg)](https://github.com/promakeai/inspector/actions/workflows/build.yml)
4
+ [![Tests](https://github.com/promakeai/inspector/actions/workflows/test.yml/badge.svg)](https://github.com/promakeai/inspector/actions/workflows/test.yml)
4
5
  [![npm version](https://badge.fury.io/js/@promakeai%2Finspector.svg)](https://www.npmjs.com/package/@promakeai/inspector)
5
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
7
 
@@ -24,7 +25,12 @@ Visual element inspector for React apps in iframe with AI prompt support. Perfec
24
25
  - 🔍 **Text Node Detection** - Automatically detect if selected element is a text node
25
26
  - 🖼️ **Image Node Detection** - Automatically detect if selected element is an image
26
27
  - 🏷️ **Promake Badge** - Optional "Built by Promake" badge with smooth animations
28
+ - 🔍 **Element Highlighting** - Revisit and highlight previously inspected elements
29
+ - 🆔 **Persistent Element IDs** - Structural signature-based IDs that survive page reloads
30
+ - 🔄 **Reload-Safe Highlighting** - Same element gets same ID even after refresh
31
+ - 🎯 **Smart Fallback** - Position-based matching when DOM structure changes
27
32
  - 📦 **TypeScript** - Full type safety
33
+ - ✅ **Well Tested** - 42 unit tests + 35 e2e tests with 90%+ coverage
28
34
 
29
35
  ## Installation
30
36
 
@@ -211,6 +217,8 @@ function App() {
211
217
  - **showContentInput**: `(show: boolean, updateImmediately?: boolean) => void` - Show or hide text content input dynamically. If `updateImmediately` is true, DOM is updated immediately on submit (useful for slow backend responses)
212
218
  - **showImageInput**: `(show: boolean, updateImmediately?: boolean) => void` - Show or hide image input dynamically. If `updateImmediately` is true, DOM is updated immediately on submit (useful for slow backend responses)
213
219
  - **setBadgeVisible**: `(visible: boolean) => void` - Show or hide "Built by Promake" badge in bottom-right corner
220
+ - **highlightElement**: `(identifier: string | ElementIdentifier, options?: HighlightOptions) => void` - Highlight an element in the iframe by ID or selector
221
+ - **getElementByInspectorId**: `(inspectorId: string) => void` - Request element info by unique inspector ID
214
222
 
215
223
  ## Types
216
224
 
@@ -227,9 +235,115 @@ interface SelectedElementData {
227
235
  textContent?: string; // Text content of the element (if text node)
228
236
  isImageNode?: boolean; // Whether the element is an image node
229
237
  imageUrl?: string; // Image URL (if image node)
238
+ inspectorId?: string; // Unique inspector ID for element tracking
239
+ selector?: string; // CSS selector for element identification
230
240
  }
231
241
  ```
232
242
 
243
+ ### `ElementIdentifier`
244
+
245
+ ```typescript
246
+ interface ElementIdentifier {
247
+ inspectorId: string; // Unique ID (primary)
248
+ selector: string; // CSS selector (fallback)
249
+ position?: {
250
+ // Position (for disambiguation)
251
+ top: number;
252
+ left: number;
253
+ };
254
+ component?: {
255
+ // Component info (metadata)
256
+ name: string;
257
+ fileName: string;
258
+ lineNumber?: number;
259
+ };
260
+ }
261
+ ```
262
+
263
+ ### `HighlightOptions`
264
+
265
+ ```typescript
266
+ interface HighlightOptions {
267
+ duration?: number; // Highlight duration in ms (default: 3000)
268
+ scrollIntoView?: boolean; // Scroll to element (default: true)
269
+ color?: string; // Highlight color (default: '#4417db')
270
+ animation?: "pulse" | "fade" | "none"; // Animation type (default: 'pulse')
271
+ }
272
+ ```
273
+
274
+ ## Element Identification & Persistence
275
+
276
+ Inspector uses a **structural signature-based ID system** to ensure the same element gets the same ID across page reloads, making it perfect for features like chat history highlighting.
277
+
278
+ ### How Persistent IDs Work
279
+
280
+ #### ID Format
281
+
282
+ ```
283
+ pi-{hash}
284
+ ```
285
+
286
+ Example: `pi-1f2a3b4c`
287
+
288
+ #### ID Generation
289
+
290
+ IDs are generated based on element's structural properties:
291
+
292
+ 1. **XPath** - DOM tree position (primary identifier)
293
+ 2. **Static Identifiers** - `id`, `data-id`, `data-testid`, `data-component`, `data-cy`
294
+ 3. **Semantic Properties** - `tag`, `role`, `type`, `name`, `href`, `src`
295
+ 4. **Structural Context** - sibling index, parent tag (if no static ID)
296
+
297
+ **Excluded for Stability:**
298
+
299
+ - ❌ Class names (frequently change)
300
+ - ❌ Text content (dynamic)
301
+ - ❌ Style attributes (dynamic)
302
+
303
+ #### Consistency Guarantees
304
+
305
+ ✅ **Same element → Same ID** (across page reloads)
306
+ ✅ **Class changes → ID unchanged**
307
+ ✅ **Text changes → ID unchanged**
308
+ ✅ **Style changes → ID unchanged**
309
+ ❌ **DOM position changes → Different ID** (new structure = new ID)
310
+
311
+ #### Best Practices for Maximum Consistency
312
+
313
+ To ensure the most reliable element identification:
314
+
315
+ ```html
316
+ <!-- ✅ Best: Use data-id for elements you want to reliably track -->
317
+ <div data-id="hero-section">...</div>
318
+ <button data-id="cta-button">...</button>
319
+
320
+ <!-- ✅ Good: Use element.id -->
321
+ <div id="main-content">...</div>
322
+
323
+ <!-- ✅ Good: Use data-testid -->
324
+ <button data-testid="submit-form">...</button>
325
+
326
+ <!-- ⚠️ Okay: Relies on DOM position (changes if DOM structure changes) -->
327
+ <div class="card">...</div>
328
+ ```
329
+
330
+ #### Fallback System
331
+
332
+ If an element's ID changes (e.g., DOM restructure), the system uses smart fallbacks:
333
+
334
+ 1. **Primary**: Try to find by `inspectorId`
335
+ 2. **Fallback 1**: Use CSS selector
336
+ 3. **Fallback 2**: Use position matching (if multiple matches)
337
+
338
+ ```typescript
339
+ // Full identifier with fallback support
340
+ highlightElement({
341
+ inspectorId: "pi-1f2a3b4c", // Primary
342
+ selector: "button.cta-primary", // Fallback 1
343
+ position: { top: 500, left: 100 }, // Fallback 2
344
+ });
345
+ ```
346
+
233
347
  ### `ComponentInfo`
234
348
 
235
349
  ```typescript
@@ -556,6 +670,57 @@ Perfect for integrating with error tracking services like Sentry, LogRocket, or
556
670
 
557
671
  ## Examples
558
672
 
673
+ ### Element Highlighting from Chat History
674
+
675
+ ```typescript
676
+ const { highlightElement, getElementByInspectorId } = useInspector(iframeRef, {
677
+ onElementSelected: (data) => {
678
+ // Store inspectorId in your chat message
679
+ const message = {
680
+ text: `Selected ${data.tagName} element`,
681
+ inspectorId: data.inspectorId,
682
+ selector: data.selector,
683
+ position: data.position,
684
+ };
685
+ saveToChatHistory(message);
686
+ },
687
+ onElementInfoReceived: (data) => {
688
+ if (data.found) {
689
+ console.log("Element found:", data.element);
690
+ } else {
691
+ console.log("Element not found:", data.error);
692
+ }
693
+ },
694
+ });
695
+
696
+ // Later: Click on a chat message to re-highlight the element
697
+ function handleChatMessageClick(message) {
698
+ // Highlight with full identifier (with fallback support)
699
+ highlightElement(
700
+ {
701
+ inspectorId: message.inspectorId,
702
+ selector: message.selector,
703
+ position: message.position,
704
+ },
705
+ {
706
+ duration: 4000,
707
+ color: "#10b981",
708
+ animation: "pulse",
709
+ }
710
+ );
711
+ }
712
+
713
+ // Or simply by ID
714
+ function quickHighlight(inspectorId) {
715
+ highlightElement(inspectorId);
716
+ }
717
+
718
+ // Request element info
719
+ function checkElementStatus(inspectorId) {
720
+ getElementByInspectorId(inspectorId);
721
+ }
722
+ ```
723
+
559
724
  ### Dynamic Content Input Based on AI Response
560
725
 
561
726
  ```typescript
@@ -1 +1 @@
1
- {"version":3,"file":"hook.d.ts","sourceRoot":"","sources":["../src/hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6GG;AAEH,OAAO,EAAoC,SAAS,EAAE,MAAM,OAAO,CAAC;AACpE,OAAO,KAAK,EACV,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,kBAAkB,EAOnB,MAAM,SAAS,CAAC;AAsDjB,wBAAgB,YAAY,CAC1B,SAAS,EAAE,SAAS,CAAC,iBAAiB,CAAC,EACvC,SAAS,CAAC,EAAE,kBAAkB,EAC9B,MAAM,CAAC,EAAE,eAAe,EACxB,KAAK,CAAC,EAAE,cAAc,GACrB,kBAAkB,CA4LpB;AAGD,mBAAmB,SAAS,CAAC"}
1
+ {"version":3,"file":"hook.d.ts","sourceRoot":"","sources":["../src/hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6GG;AAEH,OAAO,EAAoC,SAAS,EAAE,MAAM,OAAO,CAAC;AACpE,OAAO,KAAK,EACV,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,kBAAkB,EAUnB,MAAM,SAAS,CAAC;AAmEjB,wBAAgB,YAAY,CAC1B,SAAS,EAAE,SAAS,CAAC,iBAAiB,CAAC,EACvC,SAAS,CAAC,EAAE,kBAAkB,EAC9B,MAAM,CAAC,EAAE,eAAe,EACxB,KAAK,CAAC,EAAE,cAAc,GACrB,kBAAkB,CA6NpB;AAGD,mBAAmB,SAAS,CAAC"}
package/dist/hook.js CHANGED
@@ -182,6 +182,25 @@ export function useInspector(iframeRef, callbacks, labels, theme) {
182
182
  badgeText: labels?.badgeText,
183
183
  });
184
184
  }, [sendMessage, labels]);
185
+ /**
186
+ * Highlight an element in the iframe
187
+ */
188
+ const highlightElement = useCallback((identifier, options) => {
189
+ sendMessage({
190
+ type: "HIGHLIGHT_ELEMENT",
191
+ identifier: identifier,
192
+ options: options,
193
+ });
194
+ }, [sendMessage]);
195
+ /**
196
+ * Request element info by inspector ID
197
+ */
198
+ const getElementByInspectorId = useCallback((inspectorId) => {
199
+ sendMessage({
200
+ type: "GET_ELEMENT_BY_ID",
201
+ inspectorId: inspectorId,
202
+ });
203
+ }, [sendMessage]);
185
204
  /**
186
205
  * Listen for messages from iframe
187
206
  */
@@ -227,6 +246,9 @@ export function useInspector(iframeRef, callbacks, labels, theme) {
227
246
  case "INSPECTOR_ERROR":
228
247
  callbacks?.onError?.(messageData.data);
229
248
  break;
249
+ case "ELEMENT_INFO_RESPONSE":
250
+ callbacks?.onElementInfoReceived?.(messageData.data);
251
+ break;
230
252
  default:
231
253
  // Unknown message type - ignore
232
254
  break;
@@ -260,5 +282,7 @@ export function useInspector(iframeRef, callbacks, labels, theme) {
260
282
  showContentInput,
261
283
  showImageInput,
262
284
  setBadgeVisible,
285
+ highlightElement,
286
+ getElementByInspectorId,
263
287
  };
264
288
  }
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAE9B,wBAAgB,eAAe,IAAI,MAAM,CA4vCxC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAE9B,wBAAgB,eAAe,IAAI,MAAM,CA2uDxC"}
package/dist/plugin.js CHANGED
@@ -54,6 +54,347 @@ export function inspectorPlugin() {
54
54
  },
55
55
  children: `
56
56
  (function() {
57
+ // ElementTracker class - inline implementation
58
+ class ElementTracker {
59
+ constructor() {
60
+ this.ID_ATTRIBUTE = 'data-inspector-id';
61
+ }
62
+
63
+ ensureElementId(element) {
64
+ const existingId = element.getAttribute(this.ID_ATTRIBUTE);
65
+ if (existingId) return existingId;
66
+
67
+ const persistentId = this.generatePersistentId(element);
68
+ element.setAttribute(this.ID_ATTRIBUTE, persistentId);
69
+ return persistentId;
70
+ }
71
+
72
+ generatePersistentId(element) {
73
+ const hasUniqueId = element.id || element.getAttribute('data-id');
74
+
75
+ const signatureParts = [];
76
+
77
+ signatureParts.push(\`xpath:\${this.getXPath(element)}\`);
78
+
79
+ if (element.id) signatureParts.push(\`id:\${element.id}\`);
80
+
81
+ const dataId = element.getAttribute('data-id');
82
+ if (dataId) signatureParts.push(\`data-id:\${dataId}\`);
83
+
84
+ const dataTestId = element.getAttribute('data-testid');
85
+ if (dataTestId) signatureParts.push(\`data-testid:\${dataTestId}\`);
86
+
87
+ const dataComponent = element.getAttribute('data-component');
88
+ if (dataComponent) signatureParts.push(\`data-component:\${dataComponent}\`);
89
+
90
+ const dataCy = element.getAttribute('data-cy');
91
+ if (dataCy) signatureParts.push(\`data-cy:\${dataCy}\`);
92
+
93
+ signatureParts.push(\`tag:\${element.tagName.toLowerCase()}\`);
94
+
95
+ const role = element.getAttribute('role');
96
+ if (role) signatureParts.push(\`role:\${role}\`);
97
+
98
+ const type = element.getAttribute('type');
99
+ if (type) signatureParts.push(\`type:\${type}\`);
100
+
101
+ const name = element.getAttribute('name');
102
+ if (name) signatureParts.push(\`name:\${name}\`);
103
+
104
+ const href = element.getAttribute('href');
105
+ if (href) signatureParts.push(\`href:\${href}\`);
106
+
107
+ const src = element.getAttribute('src');
108
+ if (src) signatureParts.push(\`src:\${src}\`);
109
+
110
+ if (!hasUniqueId) {
111
+ signatureParts.push(\`sibling:\${this.getTypedSiblingIndex(element)}\`);
112
+ const parentTag = element.parentElement?.tagName.toLowerCase();
113
+ if (parentTag) signatureParts.push(\`parent:\${parentTag}\`);
114
+ }
115
+
116
+ const signature = signatureParts.join('|');
117
+ const hash = this.createStableHash(signature);
118
+ return \`pi-\${hash}\`;
119
+ }
120
+
121
+ getXPath(element) {
122
+ if (element.id) {
123
+ return \`//*[@id="\${element.id}"]\`;
124
+ }
125
+
126
+ const dataId = element.getAttribute('data-id');
127
+ if (dataId) {
128
+ return \`//*[@data-id="\${dataId}"]\`;
129
+ }
130
+
131
+ const parts = [];
132
+ let current = element;
133
+
134
+ while (current && current.nodeType === Node.ELEMENT_NODE) {
135
+ const tagName = current.tagName.toLowerCase();
136
+ const index = this.getTypedSiblingIndex(current);
137
+ parts.unshift(\`\${tagName}[\${index}]\`);
138
+ current = current.parentElement;
139
+ }
140
+
141
+ return '/' + parts.join('/');
142
+ }
143
+
144
+ getTypedSiblingIndex(element) {
145
+ let index = 1;
146
+ let sibling = element.previousElementSibling;
147
+
148
+ while (sibling) {
149
+ if (sibling.tagName === element.tagName) {
150
+ index++;
151
+ }
152
+ sibling = sibling.previousElementSibling;
153
+ }
154
+
155
+ return index;
156
+ }
157
+
158
+ createStableHash(str) {
159
+ let hash = 5381;
160
+ for (let i = 0; i < str.length; i++) {
161
+ const char = str.charCodeAt(i);
162
+ hash = ((hash << 5) + hash) + char;
163
+ hash = hash | 0;
164
+ }
165
+ return (hash >>> 0).toString(36);
166
+ }
167
+
168
+ findElementById(doc, id) {
169
+ return doc.querySelector(\`[\${this.ID_ATTRIBUTE}="\${id}"]\`);
170
+ }
171
+
172
+ findElementWithFallback(doc, identifier) {
173
+ const byId = this.findElementById(doc, identifier.inspectorId);
174
+ if (byId) return byId;
175
+
176
+ if (identifier.selector && identifier.position) {
177
+ return this.findByPositionMatch(doc, identifier.selector, identifier.position);
178
+ }
179
+
180
+ if (identifier.selector) {
181
+ return doc.querySelector(identifier.selector);
182
+ }
183
+
184
+ return null;
185
+ }
186
+
187
+ findByPositionMatch(doc, selector, expectedPosition) {
188
+ const elements = Array.from(doc.querySelectorAll(selector));
189
+
190
+ if (elements.length === 0) return null;
191
+ if (elements.length === 1) return elements[0];
192
+
193
+ return elements.reduce((closest, element) => {
194
+ const rect = element.getBoundingClientRect();
195
+ const distance = Math.sqrt(
196
+ Math.pow(rect.top - expectedPosition.top, 2) +
197
+ Math.pow(rect.left - expectedPosition.left, 2)
198
+ );
199
+
200
+ const closestRect = closest.getBoundingClientRect();
201
+ const closestDistance = Math.sqrt(
202
+ Math.pow(closestRect.top - expectedPosition.top, 2) +
203
+ Math.pow(closestRect.left - expectedPosition.left, 2)
204
+ );
205
+
206
+ return distance < closestDistance ? element : closest;
207
+ });
208
+ }
209
+ }
210
+
211
+ // ElementHighlighter class - inline implementation
212
+ class ElementHighlighter {
213
+ constructor() {
214
+ this.activeOverlay = null;
215
+ this.scrollListener = null;
216
+ this.resizeListener = null;
217
+ this.cleanupTimeout = null;
218
+ }
219
+
220
+ highlight(element, options = {}) {
221
+ this.clearHighlight();
222
+
223
+ const duration = options.duration || 3000;
224
+ const scrollIntoView = options.scrollIntoView !== false;
225
+ const color = options.color || '#4417db';
226
+ const animation = options.animation || 'pulse';
227
+
228
+ if (scrollIntoView) {
229
+ element.scrollIntoView({
230
+ behavior: 'smooth',
231
+ block: 'nearest',
232
+ inline: 'center'
233
+ });
234
+ }
235
+
236
+ const rect = element.getBoundingClientRect();
237
+ const overlay = document.createElement('div');
238
+ overlay.id = 'inspector-highlight-overlay';
239
+
240
+ overlay.style.cssText = \`
241
+ position: fixed;
242
+ top: \${rect.top}px;
243
+ left: \${rect.left}px;
244
+ width: \${rect.width}px;
245
+ height: \${rect.height}px;
246
+ border: 3px solid \${color};
247
+ background: \${color}22;
248
+ border-radius: 8px;
249
+ pointer-events: none;
250
+ z-index: 999999;
251
+ box-shadow: 0 0 20px \${color}66;
252
+ \`;
253
+
254
+ if (animation === 'pulse') {
255
+ overlay.style.animation = 'inspectorPulse 0.6s ease-in-out infinite';
256
+ } else if (animation === 'fade') {
257
+ overlay.style.animation = 'inspectorFade 0.5s ease-in-out';
258
+ }
259
+
260
+ this.injectStyles();
261
+
262
+ document.body.appendChild(overlay);
263
+ this.activeOverlay = overlay;
264
+
265
+ const updatePosition = () => {
266
+ const newRect = element.getBoundingClientRect();
267
+ overlay.style.top = \`\${newRect.top}px\`;
268
+ overlay.style.left = \`\${newRect.left}px\`;
269
+ };
270
+
271
+ this.scrollListener = updatePosition;
272
+ this.resizeListener = updatePosition;
273
+
274
+ window.addEventListener('scroll', this.scrollListener, true);
275
+ window.addEventListener('resize', this.resizeListener);
276
+
277
+ this.cleanupTimeout = setTimeout(() => {
278
+ this.clearHighlight();
279
+ }, duration);
280
+ }
281
+
282
+ clearHighlight() {
283
+ if (this.activeOverlay) {
284
+ this.activeOverlay.remove();
285
+ this.activeOverlay = null;
286
+ }
287
+
288
+ if (this.scrollListener) {
289
+ window.removeEventListener('scroll', this.scrollListener, true);
290
+ this.scrollListener = null;
291
+ }
292
+
293
+ if (this.resizeListener) {
294
+ window.removeEventListener('resize', this.resizeListener);
295
+ this.resizeListener = null;
296
+ }
297
+
298
+ if (this.cleanupTimeout !== null) {
299
+ clearTimeout(this.cleanupTimeout);
300
+ this.cleanupTimeout = null;
301
+ }
302
+ }
303
+
304
+ injectStyles() {
305
+ const styleId = 'inspector-highlight-styles';
306
+
307
+ if (document.getElementById(styleId)) return;
308
+
309
+ const style = document.createElement('style');
310
+ style.id = styleId;
311
+ style.textContent = \`
312
+ @keyframes inspectorPulse {
313
+ 0%, 100% { opacity: 0.7; transform: scale(1); }
314
+ 50% { opacity: 1; transform: scale(1.005); }
315
+ }
316
+
317
+ @keyframes inspectorFade {
318
+ 0% { opacity: 0; transform: scale(0.98); }
319
+ 100% { opacity: 0.7; transform: scale(1); }
320
+ }
321
+ \`;
322
+
323
+ document.head.appendChild(style);
324
+ }
325
+ }
326
+
327
+ // Initialize tracker and highlighter
328
+ const elementTracker = new ElementTracker();
329
+ const elementHighlighter = new ElementHighlighter();
330
+
331
+ // Assign IDs to all elements on page load
332
+ function assignIdsToAllElements() {
333
+ // Wait for DOM to be ready
334
+ if (document.readyState === 'loading') {
335
+ document.addEventListener('DOMContentLoaded', () => {
336
+ assignIdsToAllElements();
337
+ });
338
+ return;
339
+ }
340
+
341
+ // Get all elements in the document
342
+ const allElements = document.querySelectorAll('body *');
343
+ allElements.forEach(element => {
344
+ // Skip inspector's own elements
345
+ if (element.hasAttribute('data-inspector-ignore') ||
346
+ element.closest('[data-inspector-ignore]')) {
347
+ return;
348
+ }
349
+ elementTracker.ensureElementId(element);
350
+ });
351
+ console.log(\`🆔 Inspector: Assigned IDs to \${allElements.length} elements\`);
352
+ }
353
+
354
+ // Watch for dynamically added elements
355
+ function observeNewElements() {
356
+ if (!document.body) return;
357
+
358
+ const observer = new MutationObserver((mutations) => {
359
+ mutations.forEach(mutation => {
360
+ mutation.addedNodes.forEach(node => {
361
+ // Only process element nodes
362
+ if (node.nodeType === 1) {
363
+ const element = node;
364
+
365
+ // Skip inspector's own elements
366
+ if (element.hasAttribute('data-inspector-ignore') ||
367
+ element.closest('[data-inspector-ignore]')) {
368
+ return;
369
+ }
370
+
371
+ // Assign ID to the new element
372
+ elementTracker.ensureElementId(element);
373
+
374
+ // Also assign IDs to all children
375
+ const children = element.querySelectorAll('*');
376
+ children.forEach(child => {
377
+ if (!child.hasAttribute('data-inspector-ignore') &&
378
+ !child.closest('[data-inspector-ignore]')) {
379
+ elementTracker.ensureElementId(child);
380
+ }
381
+ });
382
+ }
383
+ });
384
+ });
385
+ });
386
+
387
+ // Start observing
388
+ observer.observe(document.body, {
389
+ childList: true,
390
+ subtree: true
391
+ });
392
+ }
393
+
394
+ // Initialize on page load
395
+ assignIdsToAllElements();
396
+ observeNewElements();
397
+
57
398
  let inspectMode = false;
58
399
  let isPaused = false;
59
400
  let hoveredElement = null;
@@ -137,53 +478,13 @@ export function inspectorPlugin() {
137
478
  controlBox.setAttribute('data-is-image-element', isImageElement);
138
479
  controlBox.setAttribute('data-current-image-url', currentImageUrl);
139
480
 
140
- // Calculate position with viewport bounds
141
- const viewportWidth = window.innerWidth;
142
- const viewportHeight = window.innerHeight;
143
- const padding = 10; // Minimum padding from viewport edges
144
-
145
- // Calculate box width (responsive, between 320 and 600px, but never wider than viewport)
146
- const maxWidth = Math.min(600, viewportWidth - (padding * 2));
147
- const minWidth = Math.min(320, maxWidth);
148
- const boxWidth = Math.max(minWidth, Math.min(rect.width, maxWidth));
149
-
150
- // Calculate horizontal position (centered on element, but within viewport)
151
- let centerLeft = rect.left + (rect.width / 2) - (boxWidth / 2);
152
- // Ensure box doesn't overflow left or right
153
- centerLeft = Math.max(padding, Math.min(centerLeft, viewportWidth - boxWidth - padding));
154
-
155
- // Calculate vertical position
156
- const spaceBelow = viewportHeight - rect.bottom;
157
- const spaceAbove = rect.top;
158
-
159
- // Estimate box height (only prompt input now, smaller)
160
- const estimatedBoxHeight = 100;
161
-
162
- // Calculate available space and max height
163
- const maxBoxHeight = Math.min(400, viewportHeight - (padding * 4)); // Max 400px or viewport - padding
164
-
165
- // Determine if box should be above or below element
166
- let topPosition;
167
- if (spaceBelow < estimatedBoxHeight + padding && spaceAbove > spaceBelow) {
168
- // Show above element
169
- topPosition = Math.max(padding, rect.top - maxBoxHeight - 10);
170
- controlBox.setAttribute('data-position', 'above');
171
- } else {
172
- // Show below element
173
- topPosition = rect.bottom + 10;
174
- // Ensure box fits in viewport
175
- if (topPosition + maxBoxHeight > viewportHeight - padding) {
176
- topPosition = Math.max(padding, viewportHeight - maxBoxHeight - padding);
177
- }
178
- controlBox.setAttribute('data-position', 'below');
179
- }
180
-
481
+ // Initial CSS (position will be calculated after render)
181
482
  controlBox.style.cssText = \`
182
483
  position: fixed;
183
- top: \${topPosition}px;
184
- left: \${centerLeft}px;
185
- width: \${boxWidth}px;
186
- max-height: \${maxBoxHeight}px;
484
+ top: 0px;
485
+ left: 0px;
486
+ width: 320px;
487
+ max-height: 400px;
187
488
  background: \${theme.backgroundColor};
188
489
  border: 1px solid #e5e7eb;
189
490
  border-radius: 14px;
@@ -196,6 +497,8 @@ export function inspectorPlugin() {
196
497
  flex-direction: column;
197
498
  gap: 0;
198
499
  overflow-y: auto;
500
+ opacity: 0;
501
+ pointer-events: none;
199
502
  \`;
200
503
 
201
504
  // Only prompt input and buttons (always shown)
@@ -281,8 +584,13 @@ export function inspectorPlugin() {
281
584
  unpauseInspection();
282
585
  });
283
586
 
284
- // Focus prompt input
285
- setTimeout(() => promptInput.focus(), 100);
587
+ // Calculate and set proper position after DOM render
588
+ setTimeout(() => {
589
+ adjustControlBoxPosition();
590
+ controlBox.style.opacity = '1';
591
+ controlBox.style.pointerEvents = 'auto';
592
+ promptInput.focus();
593
+ }, 10);
286
594
  }
287
595
 
288
596
  // Show or hide content input section
@@ -388,23 +696,23 @@ export function inspectorPlugin() {
388
696
  // Prevent input events from bubbling
389
697
  textInput.addEventListener('click', (e) => e.stopPropagation());
390
698
 
391
- // Focus text input
392
- setTimeout(() => textInput.focus(), 100);
393
-
394
- // Adjust box height
395
- adjustControlBoxPosition();
699
+ // Adjust box height and focus text input after DOM update
700
+ setTimeout(() => {
701
+ adjustControlBoxPosition();
702
+ textInput.focus();
703
+ }, 10);
396
704
  } else if (!show && contentSection) {
397
705
  // Remove content input section
398
706
  contentSection.remove();
399
707
 
400
- // Refocus prompt input
401
- const promptInput = controlBox.querySelector('#inspector-prompt-input');
402
- if (promptInput) {
403
- setTimeout(() => promptInput.focus(), 100);
404
- }
405
-
406
- // Adjust box height
407
- adjustControlBoxPosition();
708
+ // Adjust box height and refocus prompt input after DOM update
709
+ setTimeout(() => {
710
+ adjustControlBoxPosition();
711
+ const promptInput = controlBox.querySelector('#inspector-prompt-input');
712
+ if (promptInput) {
713
+ promptInput.focus();
714
+ }
715
+ }, 10);
408
716
  }
409
717
  }
410
718
 
@@ -615,20 +923,22 @@ export function inspectorPlugin() {
615
923
  toggleInspectMode(false);
616
924
  });
617
925
 
618
- // Adjust box height
619
- adjustControlBoxPosition();
926
+ // Adjust box height after DOM update
927
+ setTimeout(() => {
928
+ adjustControlBoxPosition();
929
+ }, 10);
620
930
  } else if (!show && imageSection) {
621
931
  // Remove image input section
622
932
  imageSection.remove();
623
933
 
624
- // Refocus prompt input
625
- const promptInput = controlBox.querySelector('#inspector-prompt-input');
626
- if (promptInput) {
627
- setTimeout(() => promptInput.focus(), 100);
628
- }
629
-
630
- // Adjust box height
631
- adjustControlBoxPosition();
934
+ // Adjust box height and refocus prompt input after DOM update
935
+ setTimeout(() => {
936
+ adjustControlBoxPosition();
937
+ const promptInput = controlBox.querySelector('#inspector-prompt-input');
938
+ if (promptInput) {
939
+ promptInput.focus();
940
+ }
941
+ }, 10);
632
942
  }
633
943
  }
634
944
 
@@ -654,22 +964,30 @@ export function inspectorPlugin() {
654
964
  const spaceBelow = viewportHeight - rect.bottom;
655
965
  const spaceAbove = rect.top;
656
966
 
657
- // Get actual box height and apply max height
967
+ // Get actual box height and calculate max height
658
968
  const boxHeight = controlBox.offsetHeight;
659
969
  const maxBoxHeight = Math.min(400, viewportHeight - (padding * 4));
660
970
 
971
+ // Use the smaller of actual height or max height
972
+ const effectiveHeight = Math.min(boxHeight, maxBoxHeight);
973
+
661
974
  // Determine position with viewport bounds
662
975
  let topPosition;
663
- if (spaceBelow < boxHeight + padding && spaceAbove > spaceBelow) {
664
- // Show above element
665
- topPosition = Math.max(padding, rect.top - maxBoxHeight - 10);
976
+ if (spaceBelow < effectiveHeight + padding && spaceAbove > spaceBelow) {
977
+ // Show above element - use actual box height, not max
978
+ topPosition = rect.top - effectiveHeight - 10;
979
+ // Ensure it doesn't go above viewport
980
+ if (topPosition < padding) {
981
+ topPosition = padding;
982
+ }
666
983
  controlBox.setAttribute('data-position', 'above');
667
984
  } else {
668
985
  // Show below element
669
986
  topPosition = rect.bottom + 10;
670
987
  // Ensure box fits in viewport
671
- if (topPosition + maxBoxHeight > viewportHeight - padding) {
672
- topPosition = Math.max(padding, viewportHeight - maxBoxHeight - padding);
988
+ if (topPosition + effectiveHeight > viewportHeight - padding) {
989
+ // If it doesn't fit below, try to fit it as much as possible
990
+ topPosition = Math.max(padding, viewportHeight - effectiveHeight - padding);
673
991
  }
674
992
  controlBox.setAttribute('data-position', 'below');
675
993
  }
@@ -728,6 +1046,24 @@ export function inspectorPlugin() {
728
1046
  }
729
1047
  }
730
1048
 
1049
+ // Generate CSS selector for an element
1050
+ function generateSelector(element) {
1051
+ if (element.id) {
1052
+ return '#' + element.id;
1053
+ }
1054
+
1055
+ let selector = element.tagName.toLowerCase();
1056
+
1057
+ if (element.className && typeof element.className === 'string') {
1058
+ const classes = element.className.trim().split(/\\s+/).filter(c => c);
1059
+ if (classes.length > 0) {
1060
+ selector += '.' + classes.join('.');
1061
+ }
1062
+ }
1063
+
1064
+ return selector;
1065
+ }
1066
+
731
1067
  // Get React Fiber node from DOM element
732
1068
  function getReactFiber(element) {
733
1069
  const key = Object.keys(element).find(k =>
@@ -865,6 +1201,11 @@ export function inspectorPlugin() {
865
1201
  e.stopPropagation();
866
1202
 
867
1203
  const element = e.target;
1204
+
1205
+ // Assign unique inspector ID
1206
+ const inspectorId = elementTracker.ensureElementId(element);
1207
+ const selector = generateSelector(element);
1208
+
868
1209
  const fiber = getReactFiber(element);
869
1210
  const componentInfo = fiber ? getComponentInfo(fiber) : null;
870
1211
 
@@ -890,7 +1231,10 @@ export function inspectorPlugin() {
890
1231
  isTextNode: isTextNode,
891
1232
  textContent: textContent,
892
1233
  isImageNode: isImageNode,
893
- imageUrl: imageUrl
1234
+ imageUrl: imageUrl,
1235
+ inspectorId: inspectorId,
1236
+ selector: selector,
1237
+ currentRoute: window.location.pathname
894
1238
  };
895
1239
 
896
1240
  // Send info to parent window
@@ -969,6 +1313,95 @@ export function inspectorPlugin() {
969
1313
  labels.badgeText = event.data.badgeText;
970
1314
  }
971
1315
  setBadgeVisible(event.data.visible);
1316
+ } else if (event.data.type === 'HIGHLIGHT_ELEMENT') {
1317
+ // Handle highlight element request
1318
+ const identifier = event.data.identifier;
1319
+ const options = event.data.options || {};
1320
+
1321
+ // Handle route navigation if needed
1322
+ (async () => {
1323
+ if (options.targetRoute && window.location.pathname !== options.targetRoute) {
1324
+ window.history.pushState(null, '', options.targetRoute);
1325
+ // Wait for route change to process
1326
+ await new Promise(resolve => setTimeout(resolve, 100));
1327
+ }
1328
+
1329
+ let element = null;
1330
+
1331
+ if (typeof identifier === 'string') {
1332
+ // Try as inspector ID first
1333
+ element = elementTracker.findElementById(document, identifier);
1334
+
1335
+ // Fallback to selector
1336
+ if (!element) {
1337
+ element = document.querySelector(identifier);
1338
+ }
1339
+ } else {
1340
+ // Full identifier object with fallback
1341
+ element = elementTracker.findElementWithFallback(document, identifier);
1342
+ }
1343
+
1344
+ if (element) {
1345
+ elementHighlighter.highlight(element, options);
1346
+ } else {
1347
+ console.warn('[Inspector] Element not found for highlighting:', identifier);
1348
+ }
1349
+ })();
1350
+ } else if (event.data.type === 'GET_ELEMENT_BY_ID') {
1351
+ // Handle get element by ID request
1352
+ const inspectorId = event.data.inspectorId;
1353
+ const element = elementTracker.findElementById(document, inspectorId);
1354
+
1355
+ if (element) {
1356
+ const fiber = getReactFiber(element);
1357
+ const componentInfo = fiber ? getComponentInfo(fiber) : null;
1358
+ const isTextNode = element.textContent && element.children.length === 0;
1359
+ const textContent = isTextNode ? element.textContent.trim() : '';
1360
+ const isImageNode = element.tagName === 'IMG';
1361
+ const imageUrl = isImageNode ? element.src : '';
1362
+ const selector = generateSelector(element);
1363
+
1364
+ const elementData = {
1365
+ tagName: element.tagName,
1366
+ className: element.className,
1367
+ id: element.id,
1368
+ component: componentInfo,
1369
+ position: {
1370
+ top: element.getBoundingClientRect().top,
1371
+ left: element.getBoundingClientRect().left,
1372
+ width: element.getBoundingClientRect().width,
1373
+ height: element.getBoundingClientRect().height
1374
+ },
1375
+ isTextNode: isTextNode,
1376
+ textContent: textContent,
1377
+ isImageNode: isImageNode,
1378
+ imageUrl: imageUrl,
1379
+ inspectorId: inspectorId,
1380
+ selector: selector
1381
+ };
1382
+
1383
+ if (window.parent !== window) {
1384
+ const message = JSON.stringify({
1385
+ type: 'ELEMENT_INFO_RESPONSE',
1386
+ data: {
1387
+ found: true,
1388
+ element: elementData
1389
+ }
1390
+ });
1391
+ window.parent.postMessage(message, '*');
1392
+ }
1393
+ } else {
1394
+ if (window.parent !== window) {
1395
+ const message = JSON.stringify({
1396
+ type: 'ELEMENT_INFO_RESPONSE',
1397
+ data: {
1398
+ found: false,
1399
+ error: 'Element not found'
1400
+ }
1401
+ });
1402
+ window.parent.postMessage(message, '*');
1403
+ }
1404
+ }
972
1405
  }
973
1406
  });
974
1407
 
@@ -1245,52 +1678,76 @@ export function inspectorPlugin() {
1245
1678
  console.log('🏷️ Badge auto-enabled for preview.promake.ai');
1246
1679
  }
1247
1680
 
1248
- // 🖼️ Auto cache-bust all images on page load
1681
+ // 🖼️ Auto cache-bust all images on page load and for dynamically added images
1249
1682
  // This ensures images are always fresh when page is refreshed/reloaded
1250
- function cacheBustAllImages() {
1251
- const timestamp = Date.now();
1252
- let imageCount = 0;
1683
+ const cacheBustTimestamp = Date.now();
1684
+
1685
+ function cacheBustImage(img) {
1686
+ if (img.src && !img.src.startsWith('data:') && !img.hasAttribute('data-cache-busted')) {
1687
+ try {
1688
+ const url = new URL(img.src);
1689
+ // Remove old cache busting params
1690
+ url.searchParams.delete('_t');
1691
+ url.searchParams.delete('_cache_bust');
1692
+ url.searchParams.delete('v');
1693
+ // Add new timestamp
1694
+ url.searchParams.set('_t', cacheBustTimestamp.toString());
1695
+
1696
+ const newSrc = url.toString();
1697
+
1698
+ // Force reload by clearing and resetting src
1699
+ img.removeAttribute('src');
1700
+ // Use setTimeout to ensure the browser registers the change
1701
+ setTimeout(() => {
1702
+ img.src = newSrc;
1703
+ }, 0);
1704
+
1705
+ img.setAttribute('data-cache-busted', 'true');
1706
+ return true;
1707
+ } catch (e) {
1708
+ // Ignore invalid URLs
1709
+ }
1710
+ }
1711
+ return false;
1712
+ }
1713
+
1714
+ function cacheBustBackgroundImage(el) {
1715
+ if (el.hasAttribute('data-cache-busted')) return false;
1253
1716
 
1254
- // Cache-bust all <img> tags
1255
- const images = document.querySelectorAll('img');
1256
- images.forEach((img) => {
1257
- if (img.src && !img.src.startsWith('data:')) {
1717
+ const bgImage = window.getComputedStyle(el).backgroundImage;
1718
+ if (bgImage && bgImage !== 'none' && bgImage.includes('url(')) {
1719
+ const urlMatch = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/);
1720
+ if (urlMatch && urlMatch[1] && !urlMatch[1].startsWith('data:')) {
1258
1721
  try {
1259
- const url = new URL(img.src);
1260
- // Remove old cache busting params
1722
+ const url = new URL(urlMatch[1], window.location.origin);
1261
1723
  url.searchParams.delete('_t');
1262
1724
  url.searchParams.delete('_cache_bust');
1263
1725
  url.searchParams.delete('v');
1264
- // Add new timestamp
1265
- url.searchParams.set('_t', timestamp.toString());
1266
- img.src = url.toString();
1267
- imageCount++;
1726
+ url.searchParams.set('_t', cacheBustTimestamp.toString());
1727
+ el.style.backgroundImage = 'url("' + url.toString() + '")';
1728
+ el.setAttribute('data-cache-busted', 'true');
1729
+ return true;
1268
1730
  } catch (e) {
1269
1731
  // Ignore invalid URLs
1270
1732
  }
1271
1733
  }
1734
+ }
1735
+ return false;
1736
+ }
1737
+
1738
+ function cacheBustAllImages() {
1739
+ let imageCount = 0;
1740
+
1741
+ // Cache-bust all <img> tags
1742
+ const images = document.querySelectorAll('img');
1743
+ images.forEach((img) => {
1744
+ if (cacheBustImage(img)) imageCount++;
1272
1745
  });
1273
1746
 
1274
1747
  // Cache-bust background images
1275
1748
  const allElements = document.querySelectorAll('*');
1276
1749
  allElements.forEach((el) => {
1277
- const bgImage = window.getComputedStyle(el).backgroundImage;
1278
- if (bgImage && bgImage !== 'none' && bgImage.includes('url(')) {
1279
- const urlMatch = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/);
1280
- if (urlMatch && urlMatch[1] && !urlMatch[1].startsWith('data:')) {
1281
- try {
1282
- const url = new URL(urlMatch[1], window.location.origin);
1283
- url.searchParams.delete('_t');
1284
- url.searchParams.delete('_cache_bust');
1285
- url.searchParams.delete('v');
1286
- url.searchParams.set('_t', timestamp.toString());
1287
- el.style.backgroundImage = 'url("' + url.toString() + '")';
1288
- imageCount++;
1289
- } catch (e) {
1290
- // Ignore invalid URLs
1291
- }
1292
- }
1293
- }
1750
+ if (cacheBustBackgroundImage(el)) imageCount++;
1294
1751
  });
1295
1752
 
1296
1753
  if (imageCount > 0) {
@@ -1298,12 +1755,50 @@ export function inspectorPlugin() {
1298
1755
  }
1299
1756
  }
1300
1757
 
1758
+ // Watch for dynamically added images
1759
+ function observeNewImages() {
1760
+ if (!document.body) return;
1761
+
1762
+ const imageObserver = new MutationObserver((mutations) => {
1763
+ mutations.forEach(mutation => {
1764
+ mutation.addedNodes.forEach(node => {
1765
+ if (node.nodeType === 1) {
1766
+ const element = node;
1767
+
1768
+ // If it's an img element, cache-bust it
1769
+ if (element.tagName === 'IMG') {
1770
+ cacheBustImage(element);
1771
+ }
1772
+
1773
+ // Check for img children
1774
+ const imgChildren = element.querySelectorAll('img');
1775
+ imgChildren.forEach(img => cacheBustImage(img));
1776
+
1777
+ // Check for background images
1778
+ if (cacheBustBackgroundImage(element)) {
1779
+ // Successfully cache-busted
1780
+ }
1781
+ }
1782
+ });
1783
+ });
1784
+ });
1785
+
1786
+ imageObserver.observe(document.body, {
1787
+ childList: true,
1788
+ subtree: true
1789
+ });
1790
+ }
1791
+
1301
1792
  // Run cache-bust after DOM is fully loaded
1302
1793
  if (document.readyState === 'loading') {
1303
- document.addEventListener('DOMContentLoaded', cacheBustAllImages);
1794
+ document.addEventListener('DOMContentLoaded', () => {
1795
+ cacheBustAllImages();
1796
+ observeNewImages();
1797
+ });
1304
1798
  } else {
1305
1799
  // DOM already loaded, run immediately
1306
1800
  cacheBustAllImages();
1801
+ observeNewImages();
1307
1802
  }
1308
1803
 
1309
1804
  console.log('🔍 Inspector plugin loaded');
package/dist/types.d.ts CHANGED
@@ -26,6 +26,9 @@ export interface SelectedElementData {
26
26
  textContent?: string;
27
27
  isImageNode?: boolean;
28
28
  imageUrl?: string;
29
+ inspectorId?: string;
30
+ selector?: string;
31
+ currentRoute?: string;
29
32
  }
30
33
  export interface UrlChangeData {
31
34
  url: string;
@@ -61,6 +64,31 @@ export interface ErrorData {
61
64
  columnNumber?: number;
62
65
  timestamp: number;
63
66
  }
67
+ export interface ElementIdentifier {
68
+ inspectorId: string;
69
+ selector: string;
70
+ position?: {
71
+ top: number;
72
+ left: number;
73
+ };
74
+ component?: {
75
+ name: string;
76
+ fileName: string;
77
+ lineNumber?: number;
78
+ };
79
+ }
80
+ export interface HighlightOptions {
81
+ duration?: number;
82
+ scrollIntoView?: boolean;
83
+ color?: string;
84
+ animation?: 'pulse' | 'fade' | 'none';
85
+ targetRoute?: string;
86
+ }
87
+ export interface ElementInfoData {
88
+ found: boolean;
89
+ element?: SelectedElementData;
90
+ error?: string;
91
+ }
64
92
  export interface InspectorLabels {
65
93
  editText?: string;
66
94
  textPlaceholder?: string;
@@ -95,6 +123,7 @@ export interface InspectorCallbacks {
95
123
  onImageUpdated?: (data: ImageUpdatedData) => void;
96
124
  onInspectorClosed?: () => void;
97
125
  onError?: (data: ErrorData) => void;
126
+ onElementInfoReceived?: (data: ElementInfoData) => void;
98
127
  }
99
128
  export interface UseInspectorReturn {
100
129
  isInspecting: boolean;
@@ -104,5 +133,7 @@ export interface UseInspectorReturn {
104
133
  showContentInput: (show: boolean, updateImmediately?: boolean) => void;
105
134
  showImageInput: (show: boolean, updateImmediately?: boolean) => void;
106
135
  setBadgeVisible: (visible: boolean) => void;
136
+ highlightElement: (identifier: string | ElementIdentifier, options?: HighlightOptions) => void;
137
+ getElementByInspectorId: (inspectorId: string) => void;
107
138
  }
108
139
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promakeai/inspector",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "Visual element inspector for React apps in iframe with AI prompt support",
5
5
  "author": "Promake",
6
6
  "type": "module",
@@ -33,6 +33,14 @@
33
33
  "scripts": {
34
34
  "build": "tsc && tsc -p tsconfig.plugin.json",
35
35
  "dev": "concurrently -n \"HOOK,PLUGIN\" -c \"blue,magenta\" \"tsc --watch\" \"tsc -p tsconfig.plugin.json --watch\"",
36
+ "test": "vitest run",
37
+ "test:watch": "vitest",
38
+ "test:ui": "vitest --ui",
39
+ "test:coverage": "vitest run --coverage",
40
+ "test:e2e": "cd test && npm test",
41
+ "test:e2e:ui": "cd test && npm run test:ui",
42
+ "test:e2e:headed": "cd test && npm run test:headed",
43
+ "test:all": "npm test && npm run test:e2e",
36
44
  "prepublishOnly": "npm run build",
37
45
  "release": "npm run build && npm publish --access public"
38
46
  },
@@ -48,13 +56,21 @@
48
56
  "prompt"
49
57
  ],
50
58
  "peerDependencies": {
51
- "react": ">18.0.0",
52
- "vite": ">5.0.0"
59
+ "vite": ">=5.0.0"
53
60
  },
54
61
  "devDependencies": {
62
+ "@testing-library/jest-dom": "^6.9.1",
63
+ "@testing-library/react": "^16.3.0",
55
64
  "@types/react": "^18.2.0",
65
+ "@vitest/coverage-v8": "^4.0.8",
66
+ "@vitest/ui": "^4.0.8",
56
67
  "concurrently": "^9.1.0",
68
+ "happy-dom": "^20.0.10",
69
+ "jsdom": "^27.2.0",
70
+ "react": "^18.3.1",
71
+ "react-dom": "^18.3.1",
57
72
  "typescript": "^5.3.0",
58
- "vite": "^5.4.0"
73
+ "vite": "^5.4.0",
74
+ "vitest": "^4.0.8"
59
75
  }
60
76
  }