ui-sniper 3.0.2

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/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ PolyForm Shield License 1.0.0
2
+
3
+ Copyright (c) 2026 Benji Taylor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to use,
7
+ copy, modify, and distribute the Software, subject to the following conditions:
8
+
9
+ 1. You may not use the Software to provide a product or service that competes
10
+ with the Software or any product or service offered by the Licensor that
11
+ includes the Software.
12
+
13
+ 2. You may not remove or obscure any licensing, copyright, or other notices
14
+ included in the Software.
15
+
16
+ 3. If you distribute the Software or any derivative works, you must include a
17
+ copy of this license.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ For more information, see https://polyformproject.org/licenses/shield/1.0.0
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/benjitaylor/ui-sniper/main/package/logo-dark.svg">
3
+ <img src="https://raw.githubusercontent.com/benjitaylor/ui-sniper/main/package/logo.svg" alt="UI Sniper" width="200">
4
+ </picture>
5
+
6
+ <br>
7
+
8
+ [![npm version](https://img.shields.io/npm/v/ui-sniper)](https://www.npmjs.com/package/ui-sniper)
9
+ [![downloads](https://img.shields.io/npm/dm/ui-sniper)](https://www.npmjs.com/package/ui-sniper)
10
+
11
+ **[UI Sniper](https://hara-xy.com)** is an agent-agnostic visual feedback tool. Click elements on your page, add notes, and copy structured output that helps AI coding agents find the exact code you're referring to.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install ui-sniper -D
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```tsx
22
+ import { UI Sniper } from 'ui-sniper';
23
+
24
+ function App() {
25
+ return (
26
+ <>
27
+ <YourApp />
28
+ <UI Sniper />
29
+ </>
30
+ );
31
+ }
32
+ ```
33
+
34
+ The toolbar appears in the bottom-right corner. Click to activate, then click any element to annotate it.
35
+
36
+ ## Features
37
+
38
+ - **Click to annotate** – Click any element with automatic selector identification
39
+ - **Text selection** – Select text to annotate specific content
40
+ - **Multi-select** – Drag to select multiple elements at once
41
+ - **Area selection** – Drag to annotate any region, even empty space
42
+ - **Animation pause** – Freeze all animations (CSS, JS, videos) to capture specific states
43
+ - **Structured output** – Copy markdown with selectors, positions, and context
44
+ - **Programmatic access** – Callback prop for direct integration with tools
45
+ - **Dark/light mode** – Toggle in settings, persists to localStorage
46
+ - **Zero dependencies** – Pure CSS animations, no runtime libraries
47
+
48
+ ## Props
49
+
50
+ | Prop | Type | Default | Description |
51
+ |------|------|---------|-------------|
52
+ | `onAnnotationAdd` | `(annotation: Annotation) => void` | - | Called when an annotation is created |
53
+ | `onAnnotationDelete` | `(annotation: Annotation) => void` | - | Called when an annotation is deleted |
54
+ | `onAnnotationUpdate` | `(annotation: Annotation) => void` | - | Called when an annotation is edited |
55
+ | `onAnnotationsClear` | `(annotations: Annotation[]) => void` | - | Called when all annotations are cleared |
56
+ | `onCopy` | `(markdown: string) => void` | - | Callback with markdown output when copy is clicked |
57
+ | `onSubmit` | `(output: string, annotations: Annotation[]) => void` | - | Called when "Send Annotations" is clicked |
58
+ | `copyToClipboard` | `boolean` | `true` | Set to false to prevent writing to clipboard |
59
+ | `endpoint` | `string` | - | Server URL for Agent Sync (e.g., `"http://localhost:4747"`) |
60
+ | `sessionId` | `string` | - | Pre-existing session ID to join |
61
+ | `onSessionCreated` | `(sessionId: string) => void` | - | Called when a new session is created |
62
+ | `webhookUrl` | `string` | - | Webhook URL to receive annotation events |
63
+
64
+ ### Programmatic Integration
65
+
66
+ Use callbacks to receive annotation data directly:
67
+
68
+ ```tsx
69
+ import { UI Sniper, type Annotation } from 'ui-sniper';
70
+
71
+ function App() {
72
+ const handleAnnotation = (annotation: Annotation) => {
73
+ // Structured data - no parsing needed
74
+ console.log(annotation.element); // "Button"
75
+ console.log(annotation.elementPath); // "body > div > button"
76
+ console.log(annotation.boundingBox); // { x, y, width, height }
77
+ console.log(annotation.cssClasses); // "btn btn-primary"
78
+
79
+ // Send to your agent, API, etc.
80
+ sendToAgent(annotation);
81
+ };
82
+
83
+ return (
84
+ <>
85
+ <YourApp />
86
+ <UI Sniper
87
+ onAnnotationAdd={handleAnnotation}
88
+ copyToClipboard={false} // Don't write to clipboard
89
+ />
90
+ </>
91
+ );
92
+ }
93
+ ```
94
+
95
+ ### Annotation Type
96
+
97
+ ```typescript
98
+ type Annotation = {
99
+ id: string;
100
+ x: number; // % of viewport width
101
+ y: number; // px from top of document (absolute) OR viewport (if isFixed)
102
+ comment: string; // User's note
103
+ element: string; // e.g., "Button"
104
+ elementPath: string; // e.g., "body > div > button"
105
+ timestamp: number;
106
+
107
+ // Optional metadata (when available)
108
+ selectedText?: string;
109
+ boundingBox?: { x: number; y: number; width: number; height: number };
110
+ nearbyText?: string;
111
+ cssClasses?: string;
112
+ nearbyElements?: string;
113
+ computedStyles?: string;
114
+ fullPath?: string;
115
+ accessibility?: string;
116
+ isMultiSelect?: boolean;
117
+ isFixed?: boolean;
118
+ };
119
+ ```
120
+
121
+ > **Note:** This is a simplified type. The full type includes additional fields for Agent Sync (`url`, `status`, `thread`, `reactComponents`, etc.). See [hara-xy.com/schema](https://hara-xy.com/schema) for the complete schema.
122
+
123
+ ## How it works
124
+
125
+ UI Sniper captures class names, selectors, and element positions so AI agents can `grep` for the exact code you're referring to. Instead of describing "the blue button in the sidebar," you give the agent `.sidebar > button.primary` and your feedback.
126
+
127
+ ## Requirements
128
+
129
+ - React 18+
130
+ - Desktop browser (mobile not supported)
131
+
132
+ ## Docs
133
+
134
+ Full documentation at [hara-xy.com](https://hara-xy.com)
135
+
136
+ ## License
137
+
138
+ © 2026 Benji Taylor
139
+
140
+ Licensed under PolyForm Shield 1.0.0
@@ -0,0 +1,317 @@
1
+ import * as react from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+
4
+ type Annotation = {
5
+ id: string;
6
+ x: number;
7
+ y: number;
8
+ comment: string;
9
+ element: string;
10
+ elementPath: string;
11
+ timestamp: number;
12
+ selectedText?: string;
13
+ boundingBox?: {
14
+ x: number;
15
+ y: number;
16
+ width: number;
17
+ height: number;
18
+ };
19
+ nearbyText?: string;
20
+ cssClasses?: string;
21
+ nearbyElements?: string;
22
+ computedStyles?: string;
23
+ fullPath?: string;
24
+ accessibility?: string;
25
+ isMultiSelect?: boolean;
26
+ isFixed?: boolean;
27
+ reactComponents?: string;
28
+ sourceFile?: string;
29
+ drawingIndex?: number;
30
+ elementBoundingBoxes?: Array<{
31
+ x: number;
32
+ y: number;
33
+ width: number;
34
+ height: number;
35
+ }>;
36
+ kind?: "feedback" | "placement" | "rearrange";
37
+ placement?: {
38
+ componentType: string;
39
+ width: number;
40
+ height: number;
41
+ scrollY: number;
42
+ text?: string;
43
+ };
44
+ rearrange?: {
45
+ selector: string;
46
+ label: string;
47
+ tagName: string;
48
+ originalRect: {
49
+ x: number;
50
+ y: number;
51
+ width: number;
52
+ height: number;
53
+ };
54
+ currentRect: {
55
+ x: number;
56
+ y: number;
57
+ width: number;
58
+ height: number;
59
+ };
60
+ };
61
+ sessionId?: string;
62
+ url?: string;
63
+ intent?: AnnotationIntent;
64
+ severity?: AnnotationSeverity;
65
+ status?: AnnotationStatus;
66
+ thread?: ThreadMessage[];
67
+ createdAt?: string;
68
+ updatedAt?: string;
69
+ resolvedAt?: string;
70
+ resolvedBy?: "human" | "agent";
71
+ authorId?: string;
72
+ _syncedTo?: string;
73
+ };
74
+ type AnnotationIntent = "fix" | "change" | "question" | "approve";
75
+ type AnnotationSeverity = "blocking" | "important" | "suggestion";
76
+ type AnnotationStatus = "pending" | "acknowledged" | "resolved" | "dismissed";
77
+ type ThreadMessage = {
78
+ id: string;
79
+ role: "human" | "agent";
80
+ content: string;
81
+ timestamp: number;
82
+ };
83
+
84
+ type DemoAnnotation = {
85
+ selector: string;
86
+ comment: string;
87
+ selectedText?: string;
88
+ };
89
+ type PageFeedbackToolbarCSSProps = {
90
+ demoAnnotations?: DemoAnnotation[];
91
+ demoDelay?: number;
92
+ enableDemoMode?: boolean;
93
+ /** Callback fired when an annotation is added. */
94
+ onAnnotationAdd?: (annotation: Annotation) => void;
95
+ /** Callback fired when an annotation is deleted. */
96
+ onAnnotationDelete?: (annotation: Annotation) => void;
97
+ /** Callback fired when an annotation comment is edited. */
98
+ onAnnotationUpdate?: (annotation: Annotation) => void;
99
+ /** Callback fired when all annotations are cleared. Receives the annotations that were cleared. */
100
+ onAnnotationsClear?: (annotations: Annotation[]) => void;
101
+ /** Callback fired when the copy button is clicked. Receives the markdown output. */
102
+ onCopy?: (markdown: string) => void;
103
+ /** Callback fired when "Send to Agent" is clicked. Receives the markdown output and annotations. */
104
+ onSubmit?: (output: string, annotations: Annotation[]) => void;
105
+ /** Whether to copy to clipboard when the copy button is clicked. Defaults to true. */
106
+ copyToClipboard?: boolean;
107
+ /** Server URL for sync (e.g., "http://localhost:4747"). If not provided, uses localStorage only. */
108
+ endpoint?: string;
109
+ /** Pre-existing session ID to join. If not provided with endpoint, creates a new session. */
110
+ sessionId?: string;
111
+ /** Called when a new session is created (only when endpoint is provided without sessionId). */
112
+ onSessionCreated?: (sessionId: string) => void;
113
+ /** Webhook URL to receive annotation events. */
114
+ webhookUrl?: string;
115
+ /** Custom class name applied to the toolbar container. Use to adjust positioning or z-index. */
116
+ className?: string;
117
+ };
118
+ /** Alias for PageFeedbackToolbarCSSProps */
119
+ type UISniperProps = PageFeedbackToolbarCSSProps;
120
+ declare function PageFeedbackToolbarCSS({ demoAnnotations, demoDelay, enableDemoMode, onAnnotationAdd, onAnnotationDelete, onAnnotationUpdate, onAnnotationsClear, onCopy, onSubmit, copyToClipboard, endpoint, sessionId: initialSessionId, onSessionCreated, webhookUrl, className: userClassName, }?: PageFeedbackToolbarCSSProps): react.ReactPortal | null;
121
+
122
+ interface AnnotationPopupCSSProps {
123
+ /** Element name to display in header */
124
+ element: string;
125
+ /** Optional timestamp display (e.g., "@ 1.23s" for animation feedback) */
126
+ timestamp?: string;
127
+ /** Optional selected/highlighted text */
128
+ selectedText?: string;
129
+ /** Placeholder text for the textarea */
130
+ placeholder?: string;
131
+ /** Initial value for textarea (for edit mode) */
132
+ initialValue?: string;
133
+ /** Label for submit button (default: "Add") */
134
+ submitLabel?: string;
135
+ /** Called when annotation is submitted with text */
136
+ onSubmit: (text: string) => void;
137
+ /** Called when popup is cancelled/dismissed */
138
+ onCancel: () => void;
139
+ /** Called when delete button is clicked (only shown if provided) */
140
+ onDelete?: () => void;
141
+ /** Position styles (left, top) */
142
+ style?: React.CSSProperties;
143
+ /** Custom color for submit button and textarea focus (hex) */
144
+ accentColor?: string;
145
+ /** External exit state (parent controls exit animation) */
146
+ isExiting?: boolean;
147
+ /** Light mode styling */
148
+ lightMode?: boolean;
149
+ /** Computed styles for the selected element */
150
+ computedStyles?: Record<string, string>;
151
+ }
152
+ interface AnnotationPopupCSSHandle {
153
+ /** Shake the popup (e.g., when user clicks outside) */
154
+ shake: () => void;
155
+ }
156
+ declare const AnnotationPopupCSS: react.ForwardRefExoticComponent<AnnotationPopupCSSProps & react.RefAttributes<AnnotationPopupCSSHandle>>;
157
+
158
+ declare const IconClose: ({ size }: {
159
+ size?: number;
160
+ }) => react_jsx_runtime.JSX.Element;
161
+ declare const IconPlus: ({ size }: {
162
+ size?: number;
163
+ }) => react_jsx_runtime.JSX.Element;
164
+ declare const IconCheck: ({ size }: {
165
+ size?: number;
166
+ }) => react_jsx_runtime.JSX.Element;
167
+ declare const IconCheckSmall: ({ size }: {
168
+ size?: number;
169
+ }) => react_jsx_runtime.JSX.Element;
170
+ declare const IconListSparkle: ({ size, style, }: {
171
+ size?: number;
172
+ style?: React.CSSProperties;
173
+ }) => react_jsx_runtime.JSX.Element;
174
+ declare const IconHelp: ({ size, ...props }: {
175
+ size?: number;
176
+ } & React.SVGProps<SVGSVGElement>) => react_jsx_runtime.JSX.Element;
177
+ declare const IconCheckSmallAnimated: ({ size }: {
178
+ size?: number;
179
+ }) => react_jsx_runtime.JSX.Element;
180
+ declare const IconCopyAlt: ({ size }: {
181
+ size?: number;
182
+ }) => react_jsx_runtime.JSX.Element;
183
+ declare const IconCopyAnimated: ({ size, copied, tint, }: {
184
+ size?: number;
185
+ copied?: boolean;
186
+ tint?: string;
187
+ }) => react_jsx_runtime.JSX.Element;
188
+ declare const IconSendArrow: ({ size, state, }: {
189
+ size?: number;
190
+ state?: "idle" | "sending" | "sent" | "failed";
191
+ }) => react_jsx_runtime.JSX.Element;
192
+ declare const IconSendAnimated: ({ size, sent, }: {
193
+ size?: number;
194
+ sent?: boolean;
195
+ }) => react_jsx_runtime.JSX.Element;
196
+ declare const IconEye: ({ size }: {
197
+ size?: number;
198
+ }) => react_jsx_runtime.JSX.Element;
199
+ declare const IconEyeAlt: ({ size }: {
200
+ size?: number;
201
+ }) => react_jsx_runtime.JSX.Element;
202
+ declare const IconEyeClosed: ({ size }: {
203
+ size?: number;
204
+ }) => react_jsx_runtime.JSX.Element;
205
+ declare const IconEyeAnimated: ({ size, isOpen, }: {
206
+ size?: number;
207
+ isOpen?: boolean;
208
+ }) => react_jsx_runtime.JSX.Element;
209
+ declare const IconPausePlayAnimated: ({ size, isPaused, }: {
210
+ size?: number;
211
+ isPaused?: boolean;
212
+ }) => react_jsx_runtime.JSX.Element;
213
+ declare const IconEyeMinus: ({ size }: {
214
+ size?: number;
215
+ }) => react_jsx_runtime.JSX.Element;
216
+ declare const IconGear: ({ size }: {
217
+ size?: number;
218
+ }) => react_jsx_runtime.JSX.Element;
219
+ declare const IconPauseAlt: ({ size }: {
220
+ size?: number;
221
+ }) => react_jsx_runtime.JSX.Element;
222
+ declare const IconPause: ({ size }: {
223
+ size?: number;
224
+ }) => react_jsx_runtime.JSX.Element;
225
+ declare const IconPlayAlt: ({ size }: {
226
+ size?: number;
227
+ }) => react_jsx_runtime.JSX.Element;
228
+ declare const IconTrashAlt: ({ size }: {
229
+ size?: number;
230
+ }) => react_jsx_runtime.JSX.Element;
231
+ declare const IconChatEllipsis: ({ size, style, }: {
232
+ size?: number;
233
+ style?: React.CSSProperties;
234
+ }) => react_jsx_runtime.JSX.Element;
235
+ declare const IconCheckmark: ({ size }: {
236
+ size?: number;
237
+ }) => react_jsx_runtime.JSX.Element;
238
+ declare const IconCheckmarkLarge: ({ size }: {
239
+ size?: number;
240
+ }) => react_jsx_runtime.JSX.Element;
241
+ declare const IconCheckmarkCircle: ({ size }: {
242
+ size?: number;
243
+ }) => react_jsx_runtime.JSX.Element;
244
+ declare const IconXmark: ({ size }: {
245
+ size?: number;
246
+ }) => react_jsx_runtime.JSX.Element;
247
+ declare const IconXmarkLarge: ({ size }: {
248
+ size?: number;
249
+ }) => react_jsx_runtime.JSX.Element;
250
+ declare const IconSun: ({ size }: {
251
+ size?: number;
252
+ }) => react_jsx_runtime.JSX.Element;
253
+ declare const IconMoon: ({ size }: {
254
+ size?: number;
255
+ }) => react_jsx_runtime.JSX.Element;
256
+ declare const IconEdit: ({ size }: {
257
+ size?: number;
258
+ }) => react_jsx_runtime.JSX.Element;
259
+ declare const IconTrash: ({ size }: {
260
+ size?: number;
261
+ }) => react_jsx_runtime.JSX.Element;
262
+ declare const IconChevronLeft: ({ size }: {
263
+ size?: number;
264
+ }) => react_jsx_runtime.JSX.Element;
265
+ declare const IconChevronRight: ({ size }: {
266
+ size?: number;
267
+ }) => react_jsx_runtime.JSX.Element;
268
+ declare const AnimatedBunny: ({ size, color, }: {
269
+ size?: number;
270
+ color?: string;
271
+ }) => react_jsx_runtime.JSX.Element;
272
+ declare const IconLayout: ({ size }: {
273
+ size?: number;
274
+ }) => react_jsx_runtime.JSX.Element;
275
+
276
+ /**
277
+ * Finds the closest ancestor matching a selector, crossing shadow DOM boundaries.
278
+ */
279
+ declare function closestCrossingShadow(element: Element, selector: string): Element | null;
280
+ /**
281
+ * Checks if an element is inside a shadow DOM
282
+ */
283
+ declare function isInShadowDOM(element: Element): boolean;
284
+ /**
285
+ * Gets the shadow host for an element, or null if not in shadow DOM
286
+ */
287
+ declare function getShadowHost(element: Element): Element | null;
288
+ /**
289
+ * Gets a readable path for an element (e.g., "article > section > p")
290
+ * Supports elements inside shadow DOM by crossing shadow boundaries.
291
+ */
292
+ declare function getElementPath(target: HTMLElement, maxDepth?: number): string;
293
+ /**
294
+ * Identifies an element and returns a human-readable name + path
295
+ */
296
+ declare function identifyElement(target: HTMLElement): {
297
+ name: string;
298
+ path: string;
299
+ };
300
+ /**
301
+ * Gets text content from element and siblings for context
302
+ */
303
+ declare function getNearbyText(element: HTMLElement): string;
304
+ /**
305
+ * Simplified element identifier for animation feedback (less verbose)
306
+ */
307
+ declare function identifyAnimationElement(target: HTMLElement): string;
308
+ /**
309
+ * Gets CSS class names from an element (cleaned of module hashes)
310
+ */
311
+ declare function getElementClasses(target: HTMLElement): string;
312
+
313
+ declare function getStorageKey(pathname: string): string;
314
+ declare function loadAnnotations<T = Annotation>(pathname: string): T[];
315
+ declare function saveAnnotations<T = Annotation>(pathname: string, annotations: T[]): void;
316
+
317
+ export { AnimatedBunny, type Annotation, AnnotationPopupCSS, type AnnotationPopupCSSHandle, type AnnotationPopupCSSProps, type DemoAnnotation, IconChatEllipsis, IconCheck, IconCheckSmall, IconCheckSmallAnimated, IconCheckmark, IconCheckmarkCircle, IconCheckmarkLarge, IconChevronLeft, IconChevronRight, IconClose, IconCopyAlt, IconCopyAnimated, IconEdit, IconEye, IconEyeAlt, IconEyeAnimated, IconEyeClosed, IconEyeMinus, IconGear, IconHelp, IconLayout, IconListSparkle, IconMoon, IconPause, IconPauseAlt, IconPausePlayAnimated, IconPlayAlt, IconPlus, IconSendAnimated, IconSendArrow, IconSun, IconTrash, IconTrashAlt, IconXmark, IconXmarkLarge, PageFeedbackToolbarCSS, PageFeedbackToolbarCSS as UISniper, type UISniperProps, closestCrossingShadow, getElementClasses, getElementPath, getNearbyText, getShadowHost, getStorageKey, identifyAnimationElement, identifyElement, isInShadowDOM, loadAnnotations, saveAnnotations };