From 5c3f47416afdad284a7816b913f997312affa381 Mon Sep 17 00:00:00 2001 From: Peyton-Spencer Date: Sun, 8 Feb 2026 19:33:41 -0500 Subject: [PATCH] feat: configurable activation key and click event blocking Add `key` option to plugin options (default Alt, supports Meta/Control) passed through the virtual module to the runtime. Block click events on the underlying page when the activation key is held so grabbing doesn't trigger element handlers. Co-Authored-By: Claude Opus 4.6 --- src/index.ts | 20 +++++++++++++++++--- src/types.ts | 7 +++++++ src/vite.ts | 3 ++- tests/vite-plugin.test.ts | 30 ++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3f5db42..c3ceaaf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -135,6 +135,15 @@ function onMouseDown(e: MouseEvent) { overlay.clearHighlight(); } +function onClick(e: MouseEvent) { + if (!keyHeld) return; + + // Block clicks on the underlying page while grabbing + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); +} + // ── Clipboard ──────────────────────────────────────────────────────── async function copyToClipboard(text: string) { @@ -200,6 +209,7 @@ function bootstrap() { document.addEventListener("keyup", onKeyUp, true); document.addEventListener("mousemove", onMouseMove, true); document.addEventListener("mousedown", onMouseDown, true); + document.addEventListener("click", onClick, true); window.addEventListener("blur", onBlur); // Connect agent bridge if URL provided @@ -226,6 +236,7 @@ export function destroySolidGrab() { document.removeEventListener("keyup", onKeyUp, true); document.removeEventListener("mousemove", onMouseMove, true); document.removeEventListener("mousedown", onMouseDown, true); + document.removeEventListener("click", onClick, true); window.removeEventListener("blur", onBlur); overlay.unmount(); @@ -235,10 +246,13 @@ export function destroySolidGrab() { } // ── Auto-init on import ────────────────────────────────────────────── -// When imported as a side-effect (`import "solid-grab"`), auto-initialize -// with defaults. Users who want custom options should use initSolidGrab(). +// Deferred to a microtask so that named importers can call +// initSolidGrab({ key: ... }) synchronously before the default init. +// This lets the Vite plugin pass options through the virtual module. -initSolidGrab(); +queueMicrotask(() => { + if (!initialized) initSolidGrab(); +}); // ── Expose global API for extensibility (like React Grab) ──────────── diff --git a/src/types.ts b/src/types.ts index 720b234..f00fae4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -77,6 +77,13 @@ export interface SolidGrabPluginOptions { * @default true */ autoImport?: boolean; + + /** + * Activation key to hold while hovering to grab elements. + * Passed through to the runtime via the virtual module. + * @default "Alt" + */ + key?: "Alt" | "Control" | "Meta"; } // ── Data attribute names ───────────────────────────────────────────── diff --git a/src/vite.ts b/src/vite.ts index d6d130a..ce83372 100644 --- a/src/vite.ts +++ b/src/vite.ts @@ -180,6 +180,7 @@ export default function solidGrab( jsxLocation = true, componentLocation = true, autoImport = true, + key = "Alt", } = options; let projectRoot = ""; @@ -201,7 +202,7 @@ export default function solidGrab( load(id) { if (id === RESOLVED_VIRTUAL_INIT) { - return `import "solid-grab";`; + return `import { initSolidGrab } from "solid-grab";\ninitSolidGrab({ key: "${key}" });`; } }, diff --git a/tests/vite-plugin.test.ts b/tests/vite-plugin.test.ts index 0e2af3e..a42b757 100644 --- a/tests/vite-plugin.test.ts +++ b/tests/vite-plugin.test.ts @@ -207,6 +207,36 @@ describe("transform", () => { }); }); +describe("virtual module (key config)", () => { + test("emits initSolidGrab with default Alt key", () => { + const plugin = createPlugin(); + const result = (plugin as any).load("\0virtual:solid-grab-init"); + + expect(result).toContain('import { initSolidGrab } from "solid-grab"'); + expect(result).toContain('initSolidGrab({ key: "Alt" })'); + }); + + test("emits initSolidGrab with Meta key", () => { + const plugin = createPlugin({ key: "Meta" }); + const result = (plugin as any).load("\0virtual:solid-grab-init"); + + expect(result).toContain('initSolidGrab({ key: "Meta" })'); + }); + + test("emits initSolidGrab with Control key", () => { + const plugin = createPlugin({ key: "Control" }); + const result = (plugin as any).load("\0virtual:solid-grab-init"); + + expect(result).toContain('initSolidGrab({ key: "Control" })'); + }); + + test("returns undefined for non-virtual module ids", () => { + const plugin = createPlugin(); + const result = (plugin as any).load("/project/src/App.tsx"); + expect(result).toBeUndefined(); + }); +}); + describe("transformIndexHtml", () => { test("returns tag descriptors in dev mode", () => { const plugin = createPlugin();