@np-dev/ui-ai-anotation 0.1.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 +245 -0
- package/dist/cjs/index.cjs +1550 -0
- package/dist/cjs/index.cjs.map +7 -0
- package/dist/cjs/index.native.cjs +1004 -0
- package/dist/cjs/index.native.cjs.map +7 -0
- package/dist/cjs/index.web.cjs +83 -0
- package/dist/cjs/index.web.cjs.map +7 -0
- package/dist/esm/index.js +1524 -0
- package/dist/esm/index.js.map +7 -0
- package/dist/esm/index.native.js +1012 -0
- package/dist/esm/index.native.js.map +7 -0
- package/dist/esm/index.web.js +62 -0
- package/dist/esm/index.web.js.map +7 -0
- package/dist/types/components/AnnotationInput.d.ts +8 -0
- package/dist/types/components/AnnotationList.d.ts +1 -0
- package/dist/types/components/Draggable.d.ts +10 -0
- package/dist/types/components/Highlighter.d.ts +1 -0
- package/dist/types/components/Toolbar.d.ts +1 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/types/index.web.d.ts +69 -0
- package/dist/types/store.d.ts +66 -0
- package/dist/types/utils/fiber.d.ts +51 -0
- package/dist/types/utils/platform.d.ts +8 -0
- package/dist/types/utils/screenshot.d.ts +28 -0
- package/package.json +115 -0
- package/src/components/AnnotationInput.tsx +269 -0
- package/src/components/AnnotationList.tsx +248 -0
- package/src/components/Draggable.tsx +73 -0
- package/src/components/Highlighter.tsx +497 -0
- package/src/components/Toolbar.tsx +213 -0
- package/src/components/native/AnnotationInput.tsx +227 -0
- package/src/components/native/AnnotationList.tsx +157 -0
- package/src/components/native/Draggable.tsx +65 -0
- package/src/components/native/Highlighter.tsx +239 -0
- package/src/components/native/Toolbar.tsx +192 -0
- package/src/components/native/index.ts +6 -0
- package/src/components/web/AnnotationInput.tsx +150 -0
- package/src/components/web/AnnotationList.tsx +117 -0
- package/src/components/web/Draggable.tsx +74 -0
- package/src/components/web/Highlighter.tsx +329 -0
- package/src/components/web/Toolbar.tsx +198 -0
- package/src/components/web/index.ts +6 -0
- package/src/extension.tsx +15 -0
- package/src/index.native.tsx +50 -0
- package/src/index.tsx +41 -0
- package/src/index.web.tsx +124 -0
- package/src/store.tsx +120 -0
- package/src/utils/fiber.native.ts +90 -0
- package/src/utils/fiber.ts +255 -0
- package/src/utils/platform.ts +33 -0
- package/src/utils/screenshot.native.ts +139 -0
- package/src/utils/screenshot.ts +162 -0
package/src/index.tsx
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AiAnnotationProvider as Provider } from './store';
|
|
3
|
+
import { Toolbar } from './components/Toolbar';
|
|
4
|
+
|
|
5
|
+
export interface AiAnnotationProviderProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Web AI Annotation Provider
|
|
11
|
+
* Wraps your app to provide annotation functionality
|
|
12
|
+
*/
|
|
13
|
+
export function AiAnnotationProvider({ children }: AiAnnotationProviderProps) {
|
|
14
|
+
return (
|
|
15
|
+
<Provider>
|
|
16
|
+
{children}
|
|
17
|
+
<Toolbar />
|
|
18
|
+
</Provider>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Re-export store
|
|
23
|
+
export * from './store';
|
|
24
|
+
|
|
25
|
+
// Export web components
|
|
26
|
+
export { Toolbar } from './components/Toolbar';
|
|
27
|
+
export { Highlighter } from './components/Highlighter';
|
|
28
|
+
export { AnnotationInput } from './components/AnnotationInput';
|
|
29
|
+
export { AnnotationList } from './components/AnnotationList';
|
|
30
|
+
export { Draggable } from './components/Draggable';
|
|
31
|
+
|
|
32
|
+
// Export screenshot utility
|
|
33
|
+
export { captureScreenshot } from './utils/screenshot';
|
|
34
|
+
export type { ScreenshotOptions, ScreenshotResult } from './utils/screenshot';
|
|
35
|
+
|
|
36
|
+
// Export fiber utilities
|
|
37
|
+
export { getReactFiber, getComponentDisplayName, getElementFromFiber } from './utils/fiber';
|
|
38
|
+
|
|
39
|
+
// Export platform utilities
|
|
40
|
+
export { isWeb, isNative, getPlatform, isTauriEnv } from './utils/platform';
|
|
41
|
+
export type { PlatformType } from './utils/platform';
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web-only entry point for Chrome Extension
|
|
3
|
+
* This avoids any React Native imports
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
|
|
7
|
+
import type { ComponentDetails, ChildComponentInfo, ElementInfo } from './utils/fiber';
|
|
8
|
+
|
|
9
|
+
// Re-export types for external use
|
|
10
|
+
export type { ComponentDetails, ChildComponentInfo, ElementInfo };
|
|
11
|
+
|
|
12
|
+
export type Annotation = {
|
|
13
|
+
id: string;
|
|
14
|
+
componentName: string;
|
|
15
|
+
note: string;
|
|
16
|
+
timestamp: number;
|
|
17
|
+
/** Enhanced component details (optional for backward compatibility) */
|
|
18
|
+
details?: ComponentDetails;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type Mode = 'disabled' | 'inspecting';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Platform-agnostic element type
|
|
25
|
+
* - On web: HTMLElement
|
|
26
|
+
*/
|
|
27
|
+
export type HoveredElement = HTMLElement | null;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Enhanced component info with full details
|
|
31
|
+
*/
|
|
32
|
+
export interface HoveredComponentInfo {
|
|
33
|
+
name: string;
|
|
34
|
+
/** Full component details including hierarchy, children, and element info */
|
|
35
|
+
details?: ComponentDetails;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface State {
|
|
39
|
+
mode: Mode;
|
|
40
|
+
annotations: Annotation[];
|
|
41
|
+
hoveredElement: HoveredElement;
|
|
42
|
+
hoveredComponentInfo: HoveredComponentInfo | null;
|
|
43
|
+
isMinimized: boolean;
|
|
44
|
+
showList: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type Action =
|
|
48
|
+
| { type: 'SET_MODE'; payload: Mode }
|
|
49
|
+
| { type: 'ADD_ANNOTATION'; payload: Annotation }
|
|
50
|
+
| { type: 'REMOVE_ANNOTATION'; payload: string }
|
|
51
|
+
| { type: 'CLEAR_ALL_ANNOTATIONS' }
|
|
52
|
+
| { type: 'SET_HOVERED'; payload: { element: HoveredElement; name: string | null; details?: ComponentDetails } }
|
|
53
|
+
| { type: 'TOGGLE_MINIMIZED' }
|
|
54
|
+
| { type: 'TOGGLE_LIST' }
|
|
55
|
+
| { type: 'RESET_HOVER' };
|
|
56
|
+
|
|
57
|
+
const initialState: State = {
|
|
58
|
+
mode: 'inspecting', // Start enabled for extension
|
|
59
|
+
annotations: [],
|
|
60
|
+
hoveredElement: null,
|
|
61
|
+
hoveredComponentInfo: null,
|
|
62
|
+
isMinimized: false,
|
|
63
|
+
showList: false,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const AnnotationContext = createContext<{
|
|
67
|
+
state: State;
|
|
68
|
+
dispatch: React.Dispatch<Action>;
|
|
69
|
+
} | undefined>(undefined);
|
|
70
|
+
|
|
71
|
+
function reducer(state: State, action: Action): State {
|
|
72
|
+
switch (action.type) {
|
|
73
|
+
case 'SET_MODE':
|
|
74
|
+
return { ...state, mode: action.payload };
|
|
75
|
+
case 'ADD_ANNOTATION':
|
|
76
|
+
return { ...state, annotations: [...state.annotations, action.payload] };
|
|
77
|
+
case 'REMOVE_ANNOTATION':
|
|
78
|
+
return {
|
|
79
|
+
...state,
|
|
80
|
+
annotations: state.annotations.filter((a) => a.id !== action.payload),
|
|
81
|
+
};
|
|
82
|
+
case 'CLEAR_ALL_ANNOTATIONS':
|
|
83
|
+
return {
|
|
84
|
+
...state,
|
|
85
|
+
annotations: [],
|
|
86
|
+
};
|
|
87
|
+
case 'SET_HOVERED':
|
|
88
|
+
// Avoid updates if same
|
|
89
|
+
if (state.hoveredElement === action.payload.element) return state;
|
|
90
|
+
return {
|
|
91
|
+
...state,
|
|
92
|
+
hoveredElement: action.payload.element,
|
|
93
|
+
hoveredComponentInfo: action.payload.name
|
|
94
|
+
? { name: action.payload.name, details: action.payload.details }
|
|
95
|
+
: null
|
|
96
|
+
};
|
|
97
|
+
case 'RESET_HOVER':
|
|
98
|
+
return { ...state, hoveredElement: null, hoveredComponentInfo: null };
|
|
99
|
+
case 'TOGGLE_MINIMIZED':
|
|
100
|
+
return { ...state, isMinimized: !state.isMinimized };
|
|
101
|
+
case 'TOGGLE_LIST':
|
|
102
|
+
return { ...state, showList: !state.showList };
|
|
103
|
+
default:
|
|
104
|
+
return state;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function AiAnnotationProvider({ children }: { children: ReactNode }) {
|
|
109
|
+
const [state, dispatch] = useReducer(reducer, initialState);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<AnnotationContext.Provider value={{ state, dispatch }}>
|
|
113
|
+
{children}
|
|
114
|
+
</AnnotationContext.Provider>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function useAiAnnotation() {
|
|
119
|
+
const context = useContext(AnnotationContext);
|
|
120
|
+
if (!context) {
|
|
121
|
+
throw new Error('useAiAnnotation must be used within an AiAnnotationProvider');
|
|
122
|
+
}
|
|
123
|
+
return context;
|
|
124
|
+
}
|
package/src/store.tsx
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
|
|
2
|
+
import type { ComponentDetails, ChildComponentInfo, ElementInfo } from './utils/fiber';
|
|
3
|
+
|
|
4
|
+
// Re-export types for external use
|
|
5
|
+
export type { ComponentDetails, ChildComponentInfo, ElementInfo };
|
|
6
|
+
|
|
7
|
+
export type Annotation = {
|
|
8
|
+
id: string;
|
|
9
|
+
componentName: string;
|
|
10
|
+
note: string;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
/** Enhanced component details (optional for backward compatibility) */
|
|
13
|
+
details?: ComponentDetails;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type Mode = 'disabled' | 'inspecting';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Platform-agnostic element type
|
|
20
|
+
* - On web: HTMLElement
|
|
21
|
+
* - On native: View ref or null
|
|
22
|
+
*/
|
|
23
|
+
export type HoveredElement = any;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Enhanced component info with full details
|
|
27
|
+
*/
|
|
28
|
+
export interface HoveredComponentInfo {
|
|
29
|
+
name: string;
|
|
30
|
+
/** Full component details including hierarchy, children, and element info */
|
|
31
|
+
details?: ComponentDetails;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface State {
|
|
35
|
+
mode: Mode;
|
|
36
|
+
annotations: Annotation[];
|
|
37
|
+
hoveredElement: HoveredElement;
|
|
38
|
+
hoveredComponentInfo: HoveredComponentInfo | null;
|
|
39
|
+
isMinimized: boolean;
|
|
40
|
+
showList: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type Action =
|
|
44
|
+
| { type: 'SET_MODE'; payload: Mode }
|
|
45
|
+
| { type: 'ADD_ANNOTATION'; payload: Annotation }
|
|
46
|
+
| { type: 'REMOVE_ANNOTATION'; payload: string }
|
|
47
|
+
| { type: 'CLEAR_ALL_ANNOTATIONS' }
|
|
48
|
+
| { type: 'SET_HOVERED'; payload: { element: HoveredElement; name: string | null; details?: ComponentDetails } }
|
|
49
|
+
| { type: 'TOGGLE_MINIMIZED' }
|
|
50
|
+
| { type: 'TOGGLE_LIST' }
|
|
51
|
+
| { type: 'RESET_HOVER' };
|
|
52
|
+
|
|
53
|
+
const initialState: State = {
|
|
54
|
+
mode: 'disabled',
|
|
55
|
+
annotations: [],
|
|
56
|
+
hoveredElement: null,
|
|
57
|
+
hoveredComponentInfo: null,
|
|
58
|
+
isMinimized: false,
|
|
59
|
+
showList: false,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const AnnotationContext = createContext<{
|
|
63
|
+
state: State;
|
|
64
|
+
dispatch: React.Dispatch<Action>;
|
|
65
|
+
} | undefined>(undefined);
|
|
66
|
+
|
|
67
|
+
function reducer(state: State, action: Action): State {
|
|
68
|
+
switch (action.type) {
|
|
69
|
+
case 'SET_MODE':
|
|
70
|
+
return { ...state, mode: action.payload };
|
|
71
|
+
case 'ADD_ANNOTATION':
|
|
72
|
+
return { ...state, annotations: [...state.annotations, action.payload] };
|
|
73
|
+
case 'REMOVE_ANNOTATION':
|
|
74
|
+
return {
|
|
75
|
+
...state,
|
|
76
|
+
annotations: state.annotations.filter((a) => a.id !== action.payload),
|
|
77
|
+
};
|
|
78
|
+
case 'CLEAR_ALL_ANNOTATIONS':
|
|
79
|
+
return {
|
|
80
|
+
...state,
|
|
81
|
+
annotations: [],
|
|
82
|
+
};
|
|
83
|
+
case 'SET_HOVERED':
|
|
84
|
+
// Avoid updates if same
|
|
85
|
+
if (state.hoveredElement === action.payload.element) return state;
|
|
86
|
+
return {
|
|
87
|
+
...state,
|
|
88
|
+
hoveredElement: action.payload.element,
|
|
89
|
+
hoveredComponentInfo: action.payload.name
|
|
90
|
+
? { name: action.payload.name, details: action.payload.details }
|
|
91
|
+
: null
|
|
92
|
+
};
|
|
93
|
+
case 'RESET_HOVER':
|
|
94
|
+
return { ...state, hoveredElement: null, hoveredComponentInfo: null };
|
|
95
|
+
case 'TOGGLE_MINIMIZED':
|
|
96
|
+
return { ...state, isMinimized: !state.isMinimized };
|
|
97
|
+
case 'TOGGLE_LIST':
|
|
98
|
+
return { ...state, showList: !state.showList };
|
|
99
|
+
default:
|
|
100
|
+
return state;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function AiAnnotationProvider({ children }: { children: ReactNode }) {
|
|
105
|
+
const [state, dispatch] = useReducer(reducer, initialState);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<AnnotationContext.Provider value={{ state, dispatch }}>
|
|
109
|
+
{children}
|
|
110
|
+
</AnnotationContext.Provider>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function useAiAnnotation() {
|
|
115
|
+
const context = useContext(AnnotationContext);
|
|
116
|
+
if (!context) {
|
|
117
|
+
throw new Error('useAiAnnotation must be used within an AiAnnotationProvider');
|
|
118
|
+
}
|
|
119
|
+
return context;
|
|
120
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Fiber utilities for React Native
|
|
3
|
+
*
|
|
4
|
+
* Note: React Native has different fiber access compared to web.
|
|
5
|
+
* These utilities provide a similar interface but with native-specific logic.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Attempts to get the React Fiber from a native view instance
|
|
10
|
+
* This is less reliable in React Native than on web
|
|
11
|
+
*/
|
|
12
|
+
export function getReactFiber(instance: any): any {
|
|
13
|
+
if (!instance) return null;
|
|
14
|
+
|
|
15
|
+
// React Native stores fiber differently
|
|
16
|
+
// Try to find the fiber via internal props
|
|
17
|
+
const key = Object.keys(instance).find((k) =>
|
|
18
|
+
k.startsWith('__reactFiber$') ||
|
|
19
|
+
k.startsWith('__reactInternalInstance$') ||
|
|
20
|
+
k.startsWith('_reactInternals')
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
return key ? instance[key] : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Gets the display name of a React component from its fiber
|
|
28
|
+
*/
|
|
29
|
+
export function getComponentDisplayName(fiber: any): string {
|
|
30
|
+
if (!fiber) return 'Unknown';
|
|
31
|
+
|
|
32
|
+
let curr = fiber;
|
|
33
|
+
while (curr) {
|
|
34
|
+
const type = curr.type;
|
|
35
|
+
if (type) {
|
|
36
|
+
// Check for displayName or name
|
|
37
|
+
const name = type.displayName || type.name;
|
|
38
|
+
if (name && typeof name === 'string') {
|
|
39
|
+
// Skip internal React Native components
|
|
40
|
+
if (!name.startsWith('RCT') && !name.startsWith('_')) {
|
|
41
|
+
return name;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
curr = curr.return;
|
|
46
|
+
}
|
|
47
|
+
return 'Unknown';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* For React Native, we can't easily get DOM elements from fibers
|
|
52
|
+
* This is a no-op that returns null
|
|
53
|
+
*/
|
|
54
|
+
export function getElementFromFiber(_fiber: any): null {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Interface for component inspection result
|
|
60
|
+
*/
|
|
61
|
+
export interface ComponentInfo {
|
|
62
|
+
name: string;
|
|
63
|
+
props?: Record<string, any>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Inspects a React Native component instance to get info
|
|
68
|
+
* This works with refs passed to components
|
|
69
|
+
*/
|
|
70
|
+
export function inspectComponent(ref: any): ComponentInfo | null {
|
|
71
|
+
if (!ref) return null;
|
|
72
|
+
|
|
73
|
+
// Try to get fiber from the ref
|
|
74
|
+
const fiber = getReactFiber(ref);
|
|
75
|
+
if (fiber) {
|
|
76
|
+
return {
|
|
77
|
+
name: getComponentDisplayName(fiber),
|
|
78
|
+
props: fiber.memoizedProps,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Fallback: try to infer from the ref itself
|
|
83
|
+
if (ref.constructor && ref.constructor.name) {
|
|
84
|
+
return {
|
|
85
|
+
name: ref.constructor.name,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
|
|
2
|
+
export function getReactFiber(dom: HTMLElement): any {
|
|
3
|
+
const key = Object.keys(dom).find((key) =>
|
|
4
|
+
key.startsWith("__reactFiber$")
|
|
5
|
+
);
|
|
6
|
+
// @ts-ignore
|
|
7
|
+
return key ? dom[key] : null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getComponentDisplayName(fiber: any): string {
|
|
11
|
+
let curr = fiber;
|
|
12
|
+
while (curr) {
|
|
13
|
+
const name = curr.type?.displayName || curr.type?.name;
|
|
14
|
+
// Filter out internal components if needed, but for now take the first named one
|
|
15
|
+
// Also skip styled-components or basic html tags if we want "Component" names
|
|
16
|
+
if (name && typeof curr.type === 'function') return name;
|
|
17
|
+
curr = curr.return;
|
|
18
|
+
}
|
|
19
|
+
return "Unknown";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getElementFromFiber(fiber: any): HTMLElement | null {
|
|
23
|
+
let curr = fiber;
|
|
24
|
+
while (curr) {
|
|
25
|
+
if (curr.stateNode instanceof HTMLElement) {
|
|
26
|
+
return curr.stateNode;
|
|
27
|
+
}
|
|
28
|
+
curr = curr.child;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Component info with hierarchy details
|
|
35
|
+
*/
|
|
36
|
+
export interface ComponentDetails {
|
|
37
|
+
name: string;
|
|
38
|
+
parentHierarchy: string[];
|
|
39
|
+
childComponents: ChildComponentInfo[];
|
|
40
|
+
elementInfo: ElementInfo;
|
|
41
|
+
props?: Record<string, any>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ChildComponentInfo {
|
|
45
|
+
name: string;
|
|
46
|
+
depth: number;
|
|
47
|
+
count: number;
|
|
48
|
+
hasChildren: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ElementInfo {
|
|
52
|
+
tagName: string;
|
|
53
|
+
id?: string;
|
|
54
|
+
className?: string;
|
|
55
|
+
attributes: Record<string, string>;
|
|
56
|
+
childElementCount: number;
|
|
57
|
+
textContent?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get parent component hierarchy (from current component up to root)
|
|
62
|
+
*/
|
|
63
|
+
export function getParentHierarchy(fiber: any, maxDepth: number = 10): string[] {
|
|
64
|
+
const hierarchy: string[] = [];
|
|
65
|
+
let curr = fiber?.return;
|
|
66
|
+
let depth = 0;
|
|
67
|
+
|
|
68
|
+
while (curr && depth < maxDepth) {
|
|
69
|
+
const name = curr.type?.displayName || curr.type?.name;
|
|
70
|
+
if (name && typeof curr.type === 'function') {
|
|
71
|
+
// Skip internal React components (starting with uppercase but containing $)
|
|
72
|
+
if (!name.includes('$') && !name.startsWith('_')) {
|
|
73
|
+
hierarchy.push(name);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
curr = curr.return;
|
|
77
|
+
depth++;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return hierarchy;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get child components from a fiber node
|
|
85
|
+
*/
|
|
86
|
+
export function getChildComponents(fiber: any, maxDepth: number = 5): ChildComponentInfo[] {
|
|
87
|
+
const children: Map<string, { count: number; depth: number; hasChildren: boolean }> = new Map();
|
|
88
|
+
|
|
89
|
+
function traverse(node: any, depth: number) {
|
|
90
|
+
if (!node || depth > maxDepth) return;
|
|
91
|
+
|
|
92
|
+
const name = node.type?.displayName || node.type?.name;
|
|
93
|
+
if (name && typeof node.type === 'function' && !name.includes('$') && !name.startsWith('_')) {
|
|
94
|
+
const existing = children.get(name);
|
|
95
|
+
const hasChildComponents = checkHasChildComponents(node.child);
|
|
96
|
+
|
|
97
|
+
if (existing) {
|
|
98
|
+
existing.count++;
|
|
99
|
+
// Keep the shallowest depth
|
|
100
|
+
if (depth < existing.depth) {
|
|
101
|
+
existing.depth = depth;
|
|
102
|
+
}
|
|
103
|
+
existing.hasChildren = existing.hasChildren || hasChildComponents;
|
|
104
|
+
} else {
|
|
105
|
+
children.set(name, { count: 1, depth, hasChildren: hasChildComponents });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Traverse siblings and children
|
|
110
|
+
if (node.child) traverse(node.child, depth + 1);
|
|
111
|
+
if (node.sibling) traverse(node.sibling, depth);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Start from the fiber's child
|
|
115
|
+
if (fiber?.child) {
|
|
116
|
+
traverse(fiber.child, 1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return Array.from(children.entries()).map(([name, info]) => ({
|
|
120
|
+
name,
|
|
121
|
+
depth: info.depth,
|
|
122
|
+
count: info.count,
|
|
123
|
+
hasChildren: info.hasChildren,
|
|
124
|
+
})).sort((a, b) => a.depth - b.depth);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if a fiber node has child components
|
|
129
|
+
*/
|
|
130
|
+
function checkHasChildComponents(fiber: any, maxDepth: number = 3): boolean {
|
|
131
|
+
if (!fiber || maxDepth <= 0) return false;
|
|
132
|
+
|
|
133
|
+
const name = fiber.type?.displayName || fiber.type?.name;
|
|
134
|
+
if (name && typeof fiber.type === 'function' && !name.includes('$')) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (fiber.child && checkHasChildComponents(fiber.child, maxDepth - 1)) return true;
|
|
139
|
+
if (fiber.sibling && checkHasChildComponents(fiber.sibling, maxDepth - 1)) return true;
|
|
140
|
+
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get DOM element information
|
|
146
|
+
*/
|
|
147
|
+
export function getElementInfo(element: HTMLElement): ElementInfo {
|
|
148
|
+
const importantAttributes = ['data-testid', 'role', 'aria-label', 'type', 'name', 'href', 'src'];
|
|
149
|
+
const attributes: Record<string, string> = {};
|
|
150
|
+
|
|
151
|
+
importantAttributes.forEach(attr => {
|
|
152
|
+
const value = element.getAttribute(attr);
|
|
153
|
+
if (value) {
|
|
154
|
+
attributes[attr] = value;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Get text content (truncated)
|
|
159
|
+
let textContent: string | undefined;
|
|
160
|
+
const directText = Array.from(element.childNodes)
|
|
161
|
+
.filter(node => node.nodeType === Node.TEXT_NODE)
|
|
162
|
+
.map(node => node.textContent?.trim())
|
|
163
|
+
.filter(Boolean)
|
|
164
|
+
.join(' ');
|
|
165
|
+
|
|
166
|
+
if (directText) {
|
|
167
|
+
textContent = directText.length > 100 ? directText.substring(0, 100) + '...' : directText;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
tagName: element.tagName.toLowerCase(),
|
|
172
|
+
id: element.id || undefined,
|
|
173
|
+
className: element.className && typeof element.className === 'string'
|
|
174
|
+
? element.className.split(' ').slice(0, 5).join(' ') + (element.className.split(' ').length > 5 ? '...' : '')
|
|
175
|
+
: undefined,
|
|
176
|
+
attributes,
|
|
177
|
+
childElementCount: element.childElementCount,
|
|
178
|
+
textContent,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get component props (filtered for safety - removes functions and large objects)
|
|
184
|
+
*/
|
|
185
|
+
export function getComponentProps(fiber: any, maxProps: number = 10): Record<string, any> | undefined {
|
|
186
|
+
if (!fiber?.memoizedProps) return undefined;
|
|
187
|
+
|
|
188
|
+
const props = fiber.memoizedProps;
|
|
189
|
+
const safeProps: Record<string, any> = {};
|
|
190
|
+
let count = 0;
|
|
191
|
+
|
|
192
|
+
for (const key of Object.keys(props)) {
|
|
193
|
+
if (count >= maxProps) {
|
|
194
|
+
safeProps['...'] = `${Object.keys(props).length - maxProps} more props`;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Skip children and function props
|
|
199
|
+
if (key === 'children') continue;
|
|
200
|
+
|
|
201
|
+
const value = props[key];
|
|
202
|
+
const type = typeof value;
|
|
203
|
+
|
|
204
|
+
if (type === 'function') {
|
|
205
|
+
safeProps[key] = '[Function]';
|
|
206
|
+
} else if (type === 'object' && value !== null) {
|
|
207
|
+
if (Array.isArray(value)) {
|
|
208
|
+
safeProps[key] = `[Array(${value.length})]`;
|
|
209
|
+
} else if (value.$$typeof) {
|
|
210
|
+
// React element
|
|
211
|
+
safeProps[key] = '[ReactElement]';
|
|
212
|
+
} else {
|
|
213
|
+
try {
|
|
214
|
+
const json = JSON.stringify(value);
|
|
215
|
+
safeProps[key] = json.length > 100 ? '[Object]' : value;
|
|
216
|
+
} catch {
|
|
217
|
+
safeProps[key] = '[Object]';
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
safeProps[key] = value;
|
|
222
|
+
}
|
|
223
|
+
count++;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return Object.keys(safeProps).length > 0 ? safeProps : undefined;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get comprehensive component details for the selected element
|
|
231
|
+
*/
|
|
232
|
+
export function getComponentDetails(element: HTMLElement, options?: {
|
|
233
|
+
includeProps?: boolean;
|
|
234
|
+
maxParentDepth?: number;
|
|
235
|
+
maxChildDepth?: number;
|
|
236
|
+
}): ComponentDetails {
|
|
237
|
+
const { includeProps = false, maxParentDepth = 10, maxChildDepth = 5 } = options || {};
|
|
238
|
+
|
|
239
|
+
const fiber = getReactFiber(element);
|
|
240
|
+
const name = fiber ? getComponentDisplayName(fiber) : 'Unknown';
|
|
241
|
+
|
|
242
|
+
// Find the actual component fiber (not DOM node fiber)
|
|
243
|
+
let componentFiber = fiber;
|
|
244
|
+
while (componentFiber && typeof componentFiber.type !== 'function') {
|
|
245
|
+
componentFiber = componentFiber.return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
name,
|
|
250
|
+
parentHierarchy: getParentHierarchy(componentFiber, maxParentDepth),
|
|
251
|
+
childComponents: getChildComponents(componentFiber, maxChildDepth),
|
|
252
|
+
elementInfo: getElementInfo(element),
|
|
253
|
+
props: includeProps ? getComponentProps(componentFiber) : undefined,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection utilities for cross-platform support
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Check if we're running in a web environment
|
|
6
|
+
export const isWeb = typeof document !== 'undefined' && typeof window !== 'undefined';
|
|
7
|
+
|
|
8
|
+
// Check if we're running in React Native
|
|
9
|
+
// We check for the absence of DOM and presence of native-specific globals
|
|
10
|
+
export const isNative = !isWeb && typeof global !== 'undefined';
|
|
11
|
+
|
|
12
|
+
// Get the current platform
|
|
13
|
+
export type PlatformType = 'web' | 'ios' | 'android' | 'native';
|
|
14
|
+
|
|
15
|
+
export function getPlatform(): PlatformType {
|
|
16
|
+
if (isWeb) return 'web';
|
|
17
|
+
|
|
18
|
+
// In React Native, we need to dynamically check Platform
|
|
19
|
+
try {
|
|
20
|
+
// This will only work in React Native environment
|
|
21
|
+
const { Platform } = require('react-native');
|
|
22
|
+
if (Platform.OS === 'ios') return 'ios';
|
|
23
|
+
if (Platform.OS === 'android') return 'android';
|
|
24
|
+
return 'native';
|
|
25
|
+
} catch {
|
|
26
|
+
return 'native';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check if running in Tauri environment (web only)
|
|
31
|
+
export function isTauriEnv(): boolean {
|
|
32
|
+
return isWeb && typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;
|
|
33
|
+
}
|