react-grab 0.1.24 → 0.1.26

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
@@ -201,15 +201,117 @@ actions: [
201
201
  ];
202
202
  ```
203
203
 
204
- A plugin can provide any combination of:
204
+ See [`packages/react-grab/src/types.ts`](https://github.com/aidenybai/react-grab/blob/main/packages/react-grab/src/types.ts) for the full `Plugin`, `PluginHooks`, and `PluginConfig` interfaces.
205
205
 
206
- - **`actions`** — context menu and/or toolbar items in a single array (use `target: "toolbar"` for toolbar items)
207
- - **`hooks`** — lifecycle callbacks like `onActivate`, `onElementSelect`, `onCopySuccess`, `transformCopyContent`, etc. (see `PluginHooks`)
208
- - **`theme`** — partial theme overrides (see `Theme`)
209
- - **`options`** — override default options like `activationMode` or `keyHoldDuration`
210
- - **`setup(api)`** — a function that receives the full `ReactGrabAPI` and can return additional config or a `cleanup` function
206
+ ## Primitives
211
207
 
212
- See [`packages/react-grab/src/types.ts`](https://github.com/aidenybai/react-grab/blob/main/packages/react-grab/src/types.ts) for the full `Plugin`, `PluginHooks`, and `PluginConfig` interfaces.
208
+ React Grab provides a set of primitives for building your own mini React Grab.
209
+
210
+ Here's a simple example of how to build your own element selector with hover highlight and one-click inspection:
211
+
212
+ ```bash
213
+ npm install react-grab@latest
214
+ ```
215
+
216
+ Then, put this in your React app:
217
+
218
+ ```tsx
219
+ import { useState } from "react";
220
+ import {
221
+ getElementContext,
222
+ freeze,
223
+ unfreeze,
224
+ openFile,
225
+ type ReactGrabElementContext,
226
+ } from "react-grab/primitives";
227
+
228
+ const useElementSelector = (
229
+ onSelect: (context: ReactGrabElementContext) => void,
230
+ ) => {
231
+ const [isActive, setIsActive] = useState(false);
232
+
233
+ const startSelecting = () => {
234
+ setIsActive(true);
235
+
236
+ const highlightOverlay = document.createElement("div");
237
+ Object.assign(highlightOverlay.style, {
238
+ position: "fixed",
239
+ pointerEvents: "none",
240
+ zIndex: "999999",
241
+ border: "2px solid #3b82f6",
242
+ transition: "all 75ms ease-out",
243
+ display: "none",
244
+ });
245
+ document.body.appendChild(highlightOverlay);
246
+
247
+ const handleMouseMove = ({ clientX, clientY }: MouseEvent) => {
248
+ highlightOverlay.style.display = "none";
249
+ const target = document.elementFromPoint(clientX, clientY);
250
+ if (!target) return;
251
+ const { top, left, width, height } = target.getBoundingClientRect();
252
+ Object.assign(highlightOverlay.style, {
253
+ top: `${top}px`,
254
+ left: `${left}px`,
255
+ width: `${width}px`,
256
+ height: `${height}px`,
257
+ display: "block",
258
+ });
259
+ };
260
+
261
+ const handleClick = async ({ clientX, clientY }: MouseEvent) => {
262
+ highlightOverlay.style.display = "none";
263
+ const target = document.elementFromPoint(clientX, clientY);
264
+ teardown();
265
+ if (!target) return;
266
+ freeze();
267
+ onSelect(await getElementContext(target));
268
+ unfreeze();
269
+ };
270
+
271
+ const teardown = () => {
272
+ document.removeEventListener("mousemove", handleMouseMove);
273
+ document.removeEventListener("click", handleClick, true);
274
+ highlightOverlay.remove();
275
+ setIsActive(false);
276
+ };
277
+
278
+ document.addEventListener("mousemove", handleMouseMove);
279
+ document.addEventListener("click", handleClick, true);
280
+ };
281
+
282
+ return { isActive, startSelecting };
283
+ };
284
+
285
+ const ElementSelector = () => {
286
+ const [context, setContext] = useState<ReactGrabElementContext | null>(null);
287
+ const selector = useElementSelector(setContext);
288
+
289
+ return (
290
+ <div>
291
+ <button onClick={selector.startSelecting} disabled={selector.isActive}>
292
+ {selector.isActive ? "Selecting…" : "Select Element"}
293
+ </button>
294
+ {context && (
295
+ <div>
296
+ <p>Component: {context.componentName}</p>
297
+ <p>Selector: {context.selector}</p>
298
+ <pre>{context.stackString}</pre>
299
+ <button
300
+ onClick={() => {
301
+ const frame = context.stack[0];
302
+ if (frame?.fileName) openFile(frame.fileName, frame.lineNumber);
303
+ }}
304
+ >
305
+ Open in Editor
306
+ </button>
307
+ </div>
308
+ )}
309
+ </div>
310
+ );
311
+ };
312
+ ```
313
+
314
+ See [`packages/react-grab/src/primitives.ts`](https://github.com/aidenybai/react-grab/blob/main/packages/react-grab/src/primitives.ts) for the full `ReactGrabElementContext`, `getElementContext`, `freeze`, `unfreeze`, and `openFile` primitives.
213
315
 
214
316
  ## Resources & Contributing Back
215
317