diff --git a/.gitignore b/.gitignore
index 9410ad399..0a4a02732 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,4 +13,5 @@ test-results
playwright-report
.vercel
.cursor/debug.log
-.cursor
\ No newline at end of file
+.cursor
+meta.json
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
index 5773c4a4d..d9fb39056 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -101,7 +101,7 @@ pnpm typecheck # runs type checking
pnpm format
```
-## Cursor Cloud specific instructions
+## Development instructions
This is a pnpm + Turborepo monorepo (19 packages under `packages/`). No external services (databases, Docker, etc.) are required.
@@ -117,10 +117,6 @@ The root `package.json` has `pnpm.onlyBuiltDependencies` configured for `@parcel
E2E tests (`pnpm test` at root) run Playwright against the `e2e-playground` Vite dev server on port 5175 (auto-started by the Playwright config). Chromium must be installed: `npx --prefix packages/react-grab playwright install chromium --with-deps`.
-### Known flaky test
-
-`e2e/history-items.spec.ts` > "should reposition when toolbar is dragged to top edge" intermittently times out in headless CI environments. This is a pre-existing issue.
-
### Key commands reference
See root `package.json` scripts and `CONTRIBUTING.md` for the full list. Quick reference:
diff --git a/packages/design-system/package.json b/packages/design-system/package.json
index 72f684281..40fbd0c16 100644
--- a/packages/design-system/package.json
+++ b/packages/design-system/package.json
@@ -24,13 +24,14 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
+ "@solidjs/web": "2.0.0-beta.3",
"react-grab": "workspace:*",
- "solid-js": "^1.9.10"
+ "solid-js": "2.0.0-beta.3"
},
"devDependencies": {
"@babel/core": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
- "babel-preset-solid": "^1.9.10",
+ "babel-preset-solid": "2.0.0-beta.3",
"esbuild-plugin-babel": "^0.2.3",
"tsup": "^8.2.4"
}
diff --git a/packages/design-system/src/index.tsx b/packages/design-system/src/index.tsx
index 6594f068b..9564a1a81 100644
--- a/packages/design-system/src/index.tsx
+++ b/packages/design-system/src/index.tsx
@@ -1,7 +1,7 @@
// @ts-expect-error - CSS imported as text via tsup loader
import cssText from "react-grab/dist/styles.css";
-import { render } from "solid-js/web";
-import { createSignal, For, onCleanup, onMount, Show } from "solid-js";
+import { render } from "@solidjs/web";
+import { createSignal, For, onCleanup, onSettled, Show } from "solid-js";
import { SelectionLabel } from "react-grab/src/components/selection-label/index.js";
import { ContextMenu } from "react-grab/src/components/context-menu.js";
import { ToolbarContent } from "react-grab/src/components/toolbar/toolbar-content.js";
@@ -2721,14 +2721,13 @@ const FpsMeter = (props: FpsMeterProps) => {
animationFrameId = requestAnimationFrame(measureFps);
};
- onMount(() => {
+ onSettled(() => {
animationFrameId = requestAnimationFrame(measureFps);
- });
-
- onCleanup(() => {
- if (animationFrameId) {
- cancelAnimationFrame(animationFrameId);
- }
+ return () => {
+ if (animationFrameId) {
+ cancelAnimationFrame(animationFrameId);
+ }
+ };
});
return (
@@ -3110,19 +3109,22 @@ const DesignSystemGrid = () => {
- {(state) => (
- getBoundsForCell(state.id)}
- registerCell={(element) => registerCell(state.id, element)}
- onRefresh={createRefreshHandler(state.id)}
- getTargetDisplayText={() => getTargetDisplayText(state)}
- isStarred={isStarred(state.id)}
- onToggleStar={() => handleToggleStar(state.id)}
- isScrambled={isScrambled()}
- />
- )}
+ {(stateAccessor) => {
+ const state = stateAccessor();
+ return (
+ getBoundsForCell(state.id)}
+ registerCell={(element) => registerCell(state.id, element)}
+ onRefresh={createRefreshHandler(state.id)}
+ getTargetDisplayText={() => getTargetDisplayText(state)}
+ isStarred={isStarred(state.id)}
+ onToggleStar={() => handleToggleStar(state.id)}
+ isScrambled={isScrambled()}
+ />
+ );
+ }}
@@ -3134,19 +3136,22 @@ const DesignSystemGrid = () => {
Flows
- {(state) => (
- getBoundsForCell(state.id)}
- registerCell={(element) => registerCell(state.id, element)}
- onRefresh={createRefreshHandler(state.id)}
- getTargetDisplayText={() => getTargetDisplayText(state)}
- isStarred={isStarred(state.id)}
- onToggleStar={() => handleToggleStar(state.id)}
- isScrambled={isScrambled()}
- />
- )}
+ {(stateAccessor) => {
+ const state = stateAccessor();
+ return (
+ getBoundsForCell(state.id)}
+ registerCell={(element) => registerCell(state.id, element)}
+ onRefresh={createRefreshHandler(state.id)}
+ getTargetDisplayText={() => getTargetDisplayText(state)}
+ isStarred={isStarred(state.id)}
+ onToggleStar={() => handleToggleStar(state.id)}
+ isScrambled={isScrambled()}
+ />
+ );
+ }}
@@ -3158,19 +3163,22 @@ const DesignSystemGrid = () => {
Selection Label
- {(state) => (
- getBoundsForCell(state.id)}
- registerCell={(element) => registerCell(state.id, element)}
- onRefresh={createRefreshHandler(state.id)}
- getTargetDisplayText={() => getTargetDisplayText(state)}
- isStarred={isStarred(state.id)}
- onToggleStar={() => handleToggleStar(state.id)}
- isScrambled={isScrambled()}
- />
- )}
+ {(stateAccessor) => {
+ const state = stateAccessor();
+ return (
+ getBoundsForCell(state.id)}
+ registerCell={(element) => registerCell(state.id, element)}
+ onRefresh={createRefreshHandler(state.id)}
+ getTargetDisplayText={() => getTargetDisplayText(state)}
+ isStarred={isStarred(state.id)}
+ onToggleStar={() => handleToggleStar(state.id)}
+ isScrambled={isScrambled()}
+ />
+ );
+ }}
@@ -3182,19 +3190,22 @@ const DesignSystemGrid = () => {
Context Menu (Right-Click)
- {(state) => (
- getBoundsForCell(state.id)}
- registerCell={(element) => registerCell(state.id, element)}
- onRefresh={createRefreshHandler(state.id)}
- getTargetDisplayText={() => getTargetDisplayText(state)}
- isStarred={isStarred(state.id)}
- onToggleStar={() => handleToggleStar(state.id)}
- isScrambled={isScrambled()}
- />
- )}
+ {(stateAccessor) => {
+ const state = stateAccessor();
+ return (
+ getBoundsForCell(state.id)}
+ registerCell={(element) => registerCell(state.id, element)}
+ onRefresh={createRefreshHandler(state.id)}
+ getTargetDisplayText={() => getTargetDisplayText(state)}
+ isStarred={isStarred(state.id)}
+ onToggleStar={() => handleToggleStar(state.id)}
+ isScrambled={isScrambled()}
+ />
+ );
+ }}
@@ -3206,19 +3217,22 @@ const DesignSystemGrid = () => {
Toolbar
- {(state) => (
- getBoundsForCell(state.id)}
- registerCell={(element) => registerCell(state.id, element)}
- onRefresh={createRefreshHandler(state.id)}
- getTargetDisplayText={() => getTargetDisplayText(state)}
- isStarred={isStarred(state.id)}
- onToggleStar={() => handleToggleStar(state.id)}
- isScrambled={isScrambled()}
- />
- )}
+ {(stateAccessor) => {
+ const state = stateAccessor();
+ return (
+ getBoundsForCell(state.id)}
+ registerCell={(element) => registerCell(state.id, element)}
+ onRefresh={createRefreshHandler(state.id)}
+ getTargetDisplayText={() => getTargetDisplayText(state)}
+ isStarred={isStarred(state.id)}
+ onToggleStar={() => handleToggleStar(state.id)}
+ isScrambled={isScrambled()}
+ />
+ );
+ }}
@@ -3230,19 +3244,22 @@ const DesignSystemGrid = () => {
History Dropdown
- {(state) => (
- getBoundsForCell(state.id)}
- registerCell={(element) => registerCell(state.id, element)}
- onRefresh={createRefreshHandler(state.id)}
- getTargetDisplayText={() => getTargetDisplayText(state)}
- isStarred={isStarred(state.id)}
- onToggleStar={() => handleToggleStar(state.id)}
- isScrambled={isScrambled()}
- />
- )}
+ {(stateAccessor) => {
+ const state = stateAccessor();
+ return (
+ getBoundsForCell(state.id)}
+ registerCell={(element) => registerCell(state.id, element)}
+ onRefresh={createRefreshHandler(state.id)}
+ getTargetDisplayText={() => getTargetDisplayText(state)}
+ isStarred={isStarred(state.id)}
+ onToggleStar={() => handleToggleStar(state.id)}
+ isScrambled={isScrambled()}
+ />
+ );
+ }}
@@ -3254,19 +3271,22 @@ const DesignSystemGrid = () => {
Agent States
- {(state) => (
- getBoundsForCell(state.id)}
- registerCell={(element) => registerCell(state.id, element)}
- onRefresh={createRefreshHandler(state.id)}
- getTargetDisplayText={() => getTargetDisplayText(state)}
- isStarred={isStarred(state.id)}
- onToggleStar={() => handleToggleStar(state.id)}
- isScrambled={isScrambled()}
- />
- )}
+ {(stateAccessor) => {
+ const state = stateAccessor();
+ return (
+ getBoundsForCell(state.id)}
+ registerCell={(element) => registerCell(state.id, element)}
+ onRefresh={createRefreshHandler(state.id)}
+ getTargetDisplayText={() => getTargetDisplayText(state)}
+ isStarred={isStarred(state.id)}
+ onToggleStar={() => handleToggleStar(state.id)}
+ isScrambled={isScrambled()}
+ />
+ );
+ }}
diff --git a/packages/design-system/tsup.config.ts b/packages/design-system/tsup.config.ts
index 61f01b870..97fd2b1fd 100644
--- a/packages/design-system/tsup.config.ts
+++ b/packages/design-system/tsup.config.ts
@@ -13,7 +13,7 @@ const options: Options = {
".css": "text",
},
minify: process.env.NODE_ENV === "production",
- noExternal: ["solid-js", /^react-grab\/src/, "react-grab/dist/styles.css"],
+ noExternal: ["solid-js", "@solidjs/web", /^react-grab\/src/, "react-grab/dist/styles.css"],
outDir: "./dist",
platform: "browser",
sourcemap: false,
diff --git a/packages/react-grab/package.json b/packages/react-grab/package.json
index bb2c0ab89..f0fe555f2 100644
--- a/packages/react-grab/package.json
+++ b/packages/react-grab/package.json
@@ -100,9 +100,10 @@
"dependencies": {
"@medv/finder": "^4.0.2",
"@react-grab/cli": "workspace:*",
+ "@solidjs/web": "2.0.0-beta.3",
"bippy": "^0.5.32",
"element-source": "^0.0.3",
- "solid-js": "^1.9.10"
+ "solid-js": "2.0.0-beta.3"
},
"devDependencies": {
"@babel/core": "^7.28.5",
@@ -111,13 +112,12 @@
"@tailwindcss/cli": "^4.1.17",
"@types/node": "^20.19.23",
"@types/react": "^19.2.11",
- "babel-preset-solid": "^1.9.10",
+ "babel-preset-solid": "2.0.0-beta.3",
"clsx": "^2.1.1",
"concurrently": "^9.1.2",
"esbuild-plugin-babel": "^0.2.3",
"oxlint": "^1.42.0",
"publint": "^0.2.12",
- "tailwind-merge": "^2.5.5",
"tailwindcss": "^4.1.0",
"tsup": "^8.2.4"
},
diff --git a/packages/react-grab/src/components/clear-history-prompt.tsx b/packages/react-grab/src/components/clear-history-prompt.tsx
index 127adda43..659a3aa62 100644
--- a/packages/react-grab/src/components/clear-history-prompt.tsx
+++ b/packages/react-grab/src/components/clear-history-prompt.tsx
@@ -1,4 +1,4 @@
-import { Show, onMount, onCleanup } from "solid-js";
+import { Show, onSettled } from "solid-js";
import type { Component } from "solid-js";
import type { DropdownAnchor } from "../types.js";
import { DROPDOWN_EDGE_TRANSFORM_ORIGIN, Z_INDEX_LABEL } from "../constants.js";
@@ -24,7 +24,7 @@ export const ClearHistoryPrompt: Component = (
() => props.position,
);
- onMount(() => {
+ onSettled(() => {
dropdown.measure();
const unregisterOverlayDismiss = registerOverlayDismiss({
isOpen: () => Boolean(props.position),
@@ -33,10 +33,10 @@ export const ClearHistoryPrompt: Component = (
shouldIgnoreInputEvents: true,
});
- onCleanup(() => {
+ return () => {
dropdown.clearAnimationHandles();
unregisterOverlayDismiss();
- });
+ };
});
return (
diff --git a/packages/react-grab/src/components/context-menu.tsx b/packages/react-grab/src/components/context-menu.tsx
index c10a72af6..1a257d72c 100644
--- a/packages/react-grab/src/components/context-menu.tsx
+++ b/packages/react-grab/src/components/context-menu.tsx
@@ -1,8 +1,7 @@
import {
Show,
For,
- onMount,
- onCleanup,
+ onSettled,
createSignal,
createEffect,
createMemo,
@@ -80,11 +79,14 @@ export const ContextMenu: Component = (props) => {
}
};
- createEffect(() => {
- if (isVisible()) {
- nativeRequestAnimationFrame(measureContainer);
- }
- });
+ createEffect(
+ () => isVisible(),
+ (visible) => {
+ if (visible) {
+ nativeRequestAnimationFrame(measureContainer);
+ }
+ },
+ );
const computedPosition = createMemo(() => {
const bounds = props.selectionBounds;
@@ -165,7 +167,7 @@ export const ContextMenu: Component = (props) => {
}
};
- onMount(() => {
+ onSettled(() => {
measureContainer();
const handleKeyDown = (event: KeyboardEvent) => {
@@ -219,10 +221,10 @@ export const ContextMenu: Component = (props) => {
});
window.addEventListener("keydown", handleKeyDown, { capture: true });
- onCleanup(() => {
+ return () => {
unregisterOverlayDismiss();
window.removeEventListener("keydown", handleKeyDown, { capture: true });
- });
+ };
});
return (
@@ -283,33 +285,36 @@ export const ContextMenu: Component = (props) => {
class="pointer-events-none absolute bg-black/5 opacity-0 transition-[top,left,width,height,opacity] duration-75 ease-out"
/>
- {(item) => (
-
- )}
+ {(itemAccessor) => {
+ const menuItem = () => itemAccessor();
+ return (
+
+ );
+ }}
diff --git a/packages/react-grab/src/components/history-dropdown.tsx b/packages/react-grab/src/components/history-dropdown.tsx
index 5c7763997..6172b2327 100644
--- a/packages/react-grab/src/components/history-dropdown.tsx
+++ b/packages/react-grab/src/components/history-dropdown.tsx
@@ -1,12 +1,4 @@
-import {
- Show,
- For,
- onMount,
- onCleanup,
- createSignal,
- createEffect,
- on,
-} from "solid-js";
+import { Show, For, onSettled, createSignal, createEffect } from "solid-js";
import type { Component } from "solid-js";
import type { HistoryItem, DropdownAnchor } from "../types.js";
import {
@@ -102,15 +94,14 @@ export const HistoryDropdown: Component = (props) => {
// HACK: mouseenter doesn't fire when an element appears under the cursor, so we check :hover after the enter animation commits
createEffect(
- on(
- () => dropdown.isAnimatedIn(),
- (animatedIn) => {
- if (animatedIn && containerRef?.matches(":hover")) {
- props.onDropdownHover?.(true);
- }
- },
- { defer: true },
- ),
+ () => dropdown.isAnimatedIn(),
+ (animatedIn) => {
+ if (animatedIn && containerRef?.matches(":hover")) {
+ props.onDropdownHover?.(true);
+ }
+ },
+ undefined,
+ { defer: true },
);
const clampedMaxWidth = () =>
@@ -129,7 +120,7 @@ export const HistoryDropdown: Component = (props) => {
const panelMinWidth = () =>
Math.max(DROPDOWN_MIN_WIDTH_PX, props.position?.toolbarWidth ?? 0);
- onMount(() => {
+ onSettled(() => {
dropdown.measure();
const handleKeyDown = (event: KeyboardEvent) => {
@@ -143,13 +134,13 @@ export const HistoryDropdown: Component = (props) => {
window.addEventListener("keydown", handleKeyDown, { capture: true });
- onCleanup(() => {
+ return () => {
clearTimeout(copyAllFeedbackTimeout);
clearTimeout(copyItemFeedbackTimeout);
dropdown.clearAnimationHandles();
window.removeEventListener("keydown", handleKeyDown, { capture: true });
safePolygonTracker.stop();
- });
+ };
});
return (
@@ -284,104 +275,114 @@ export const HistoryDropdown: Component = (props) => {
class="pointer-events-none absolute bg-black/5 opacity-0 transition-[top,left,width,height,opacity] duration-75 ease-out"
/>
- {(item) => (
- event.stopPropagation()}
- onClick={(event) => {
- event.stopPropagation();
- props.onSelectItem?.(item);
- setConfirmedCopyItemId(item.id);
- clearTimeout(copyItemFeedbackTimeout);
- copyItemFeedbackTimeout = setTimeout(() => {
- setConfirmedCopyItemId(null);
- }, FEEDBACK_DURATION_MS);
- }}
- onKeyDown={(event) => {
- if (
- event.code === "Space" &&
- event.currentTarget === event.target
- ) {
- event.preventDefault();
+ {(itemAccessor) => {
+ const historyItem = () => itemAccessor();
+ return (
+
event.stopPropagation()}
+ onClick={(event) => {
event.stopPropagation();
- props.onSelectItem?.(item);
- }
- }}
- onMouseEnter={(event) => {
- if (!props.disconnectedItemIds?.has(item.id)) {
- props.onItemHover?.(item.id);
- }
- updateHighlight(event.currentTarget);
- }}
- onMouseLeave={() => {
- props.onItemHover?.(null);
- clearHighlight();
- }}
- onFocus={(event) => updateHighlight(event.currentTarget)}
- onBlur={clearHighlight}
- >
-
-
- {getHistoryItemDisplayName(item)}
-
-
-
- {item.commentText}
+ props.onSelectItem?.(historyItem());
+ setConfirmedCopyItemId(historyItem().id);
+ clearTimeout(copyItemFeedbackTimeout);
+ copyItemFeedbackTimeout = setTimeout(() => {
+ setConfirmedCopyItemId(null);
+ }, FEEDBACK_DURATION_MS);
+ }}
+ onKeyDown={(event) => {
+ if (
+ event.code === "Space" &&
+ event.currentTarget === event.target
+ ) {
+ event.preventDefault();
+ event.stopPropagation();
+ props.onSelectItem?.(historyItem());
+ }
+ }}
+ onMouseEnter={(event) => {
+ if (!props.disconnectedItemIds?.has(historyItem().id)) {
+ props.onItemHover?.(historyItem().id);
+ }
+ updateHighlight(event.currentTarget);
+ }}
+ onMouseLeave={() => {
+ props.onItemHover?.(null);
+ clearHighlight();
+ }}
+ onFocus={(event) => updateHighlight(event.currentTarget)}
+ onBlur={clearHighlight}
+ >
+
+
+ {getHistoryItemDisplayName(historyItem())}
-
-
-
-
- {formatRelativeTime(item.timestamp)}
+
+
+ {historyItem().commentText}
+
+
-
-
-
+ {
+ event.stopPropagation();
+ props.onCopyItem?.(historyItem());
+ setConfirmedCopyItemId(historyItem().id);
+ clearTimeout(copyItemFeedbackTimeout);
+ copyItemFeedbackTimeout = setTimeout(() => {
+ setConfirmedCopyItemId(null);
+ }, FEEDBACK_DURATION_MS);
+ }}
+ >
+
+ }
+ >
+
+
+
+
-
-
- )}
+
+ );
+ }}
diff --git a/packages/react-grab/src/components/overlay-canvas.tsx b/packages/react-grab/src/components/overlay-canvas.tsx
index 462a8f726..c63ab4bd5 100644
--- a/packages/react-grab/src/components/overlay-canvas.tsx
+++ b/packages/react-grab/src/components/overlay-canvas.tsx
@@ -1,4 +1,4 @@
-import { createEffect, onCleanup, onMount, on } from "solid-js";
+import { createEffect, onSettled } from "solid-js";
import type { Component } from "solid-js";
import type {
OverlayBounds,
@@ -492,202 +492,192 @@ export const OverlayCanvas: Component = (props) => {
};
createEffect(
- on(
- () =>
- [
- props.selectionVisible,
- props.selectionBounds,
- props.selectionBoundsMultiple,
- props.selectionIsFading,
- props.selectionShouldSnap,
- ] as const,
- ([isVisible, singleBounds, multipleBounds, , shouldSnap]) => {
- if (
- !isVisible ||
- (!singleBounds && (!multipleBounds || multipleBounds.length === 0))
- ) {
- selectionAnimations = [];
- scheduleAnimationFrame();
- return;
- }
-
- const boundsToRender =
- multipleBounds && multipleBounds.length > 0
- ? multipleBounds
- : singleBounds
- ? [singleBounds]
- : [];
+ () =>
+ [
+ props.selectionVisible,
+ props.selectionBounds,
+ props.selectionBoundsMultiple,
+ props.selectionIsFading,
+ props.selectionShouldSnap,
+ ] as const,
+ ([isVisible, singleBounds, multipleBounds, , shouldSnap]) => {
+ if (
+ !isVisible ||
+ (!singleBounds && (!multipleBounds || multipleBounds.length === 0))
+ ) {
+ selectionAnimations = [];
+ scheduleAnimationFrame();
+ return;
+ }
- selectionAnimations = boundsToRender.map((bounds, index) => {
- const animationId = `selection-${index}`;
- const existingAnimation = selectionAnimations.find(
- (animation) => animation.id === animationId,
- );
+ const boundsToRender =
+ multipleBounds && multipleBounds.length > 0
+ ? multipleBounds
+ : singleBounds
+ ? [singleBounds]
+ : [];
+
+ selectionAnimations = boundsToRender.map((bounds, index) => {
+ const animationId = `selection-${index}`;
+ const existingAnimation = selectionAnimations.find(
+ (animation) => animation.id === animationId,
+ );
- if (existingAnimation) {
- updateAnimationTarget(existingAnimation, bounds);
- if (shouldSnap) {
- existingAnimation.current = { ...existingAnimation.target };
- }
- return existingAnimation;
+ if (existingAnimation) {
+ updateAnimationTarget(existingAnimation, bounds);
+ if (shouldSnap) {
+ existingAnimation.current = { ...existingAnimation.target };
}
+ return existingAnimation;
+ }
- return createAnimatedBounds(animationId, bounds);
- });
+ return createAnimatedBounds(animationId, bounds);
+ });
- scheduleAnimationFrame();
- },
- ),
+ scheduleAnimationFrame();
+ },
);
createEffect(
- on(
- () => [props.dragVisible, props.dragBounds] as const,
- ([isVisible, bounds]) => {
- if (!isVisible || !bounds) {
- dragAnimation = null;
- scheduleAnimationFrame();
- return;
- }
+ () => [props.dragVisible, props.dragBounds] as const,
+ ([isVisible, bounds]) => {
+ if (!isVisible || !bounds) {
+ dragAnimation = null;
+ scheduleAnimationFrame();
+ return;
+ }
- if (dragAnimation) {
- updateAnimationTarget(dragAnimation, bounds);
- } else {
- dragAnimation = createAnimatedBounds("drag", bounds);
- }
+ if (dragAnimation) {
+ updateAnimationTarget(dragAnimation, bounds);
+ } else {
+ dragAnimation = createAnimatedBounds("drag", bounds);
+ }
- scheduleAnimationFrame();
- },
- ),
+ scheduleAnimationFrame();
+ },
);
createEffect(
- on(
- () => props.grabbedBoxes,
- (grabbedBoxes) => {
- const boxesToProcess = grabbedBoxes ?? [];
- const activeBoxIds = new Set(boxesToProcess.map((box) => box.id));
- const existingAnimationIds = new Set(
- grabbedAnimations.map((animation) => animation.id),
- );
+ () => props.grabbedBoxes,
+ (grabbedBoxes) => {
+ const boxesToProcess = grabbedBoxes ?? [];
+ const activeBoxIds = new Set(boxesToProcess.map((box) => box.id));
+ const existingAnimationIds = new Set(
+ grabbedAnimations.map((animation) => animation.id),
+ );
- for (const box of boxesToProcess) {
- if (!existingAnimationIds.has(box.id)) {
- grabbedAnimations.push(
- createAnimatedBounds(box.id, box.bounds, {
- createdAt: box.createdAt,
- }),
- );
- }
+ for (const box of boxesToProcess) {
+ if (!existingAnimationIds.has(box.id)) {
+ grabbedAnimations.push(
+ createAnimatedBounds(box.id, box.bounds, {
+ createdAt: box.createdAt,
+ }),
+ );
}
+ }
- for (const animation of grabbedAnimations) {
- const matchingBox = boxesToProcess.find(
- (box) => box.id === animation.id,
- );
- if (matchingBox) {
- updateAnimationTarget(animation, matchingBox.bounds);
- }
+ for (const animation of grabbedAnimations) {
+ const matchingBox = boxesToProcess.find(
+ (box) => box.id === animation.id,
+ );
+ if (matchingBox) {
+ updateAnimationTarget(animation, matchingBox.bounds);
}
+ }
- grabbedAnimations = grabbedAnimations.filter((animation) => {
- if (animation.id.startsWith("label-")) {
- return true;
- }
- return activeBoxIds.has(animation.id);
- });
+ grabbedAnimations = grabbedAnimations.filter((animation) => {
+ if (animation.id.startsWith("label-")) {
+ return true;
+ }
+ return activeBoxIds.has(animation.id);
+ });
- scheduleAnimationFrame();
- },
- ),
+ scheduleAnimationFrame();
+ },
);
createEffect(
- on(
- () => props.agentSessions,
- (agentSessions) => {
- if (!agentSessions || agentSessions.size === 0) {
- processingAnimations = [];
- scheduleAnimationFrame();
- return;
- }
+ () => props.agentSessions,
+ (agentSessions) => {
+ if (!agentSessions || agentSessions.size === 0) {
+ processingAnimations = [];
+ scheduleAnimationFrame();
+ return;
+ }
- const updatedAnimations: AnimatedBounds[] = [];
+ const updatedAnimations: AnimatedBounds[] = [];
- for (const [sessionId, session] of agentSessions) {
- for (let index = 0; index < session.selectionBounds.length; index++) {
- const bounds = session.selectionBounds[index];
- const animationId = `processing-${sessionId}-${index}`;
- const existingAnimation = processingAnimations.find(
- (animation) => animation.id === animationId,
- );
+ for (const [sessionId, session] of agentSessions) {
+ for (let index = 0; index < session.selectionBounds.length; index++) {
+ const bounds = session.selectionBounds[index];
+ const animationId = `processing-${sessionId}-${index}`;
+ const existingAnimation = processingAnimations.find(
+ (animation) => animation.id === animationId,
+ );
- if (existingAnimation) {
- updateAnimationTarget(existingAnimation, bounds);
- updatedAnimations.push(existingAnimation);
- } else {
- updatedAnimations.push(createAnimatedBounds(animationId, bounds));
- }
+ if (existingAnimation) {
+ updateAnimationTarget(existingAnimation, bounds);
+ updatedAnimations.push(existingAnimation);
+ } else {
+ updatedAnimations.push(createAnimatedBounds(animationId, bounds));
}
}
+ }
- processingAnimations = updatedAnimations;
- scheduleAnimationFrame();
- },
- ),
+ processingAnimations = updatedAnimations;
+ scheduleAnimationFrame();
+ },
);
createEffect(
- on(
- () => props.labelInstances,
- (labelInstances) => {
- const instancesToProcess = labelInstances ?? [];
-
- for (const instance of instancesToProcess) {
- const boundsToRender = resolveBoundsArray(instance);
- const targetOpacity = instance.status === "fading" ? 0 : 1;
-
- for (let index = 0; index < boundsToRender.length; index++) {
- const bounds = boundsToRender[index];
- const animationId = `label-${instance.id}-${index}`;
- const existingAnimation = grabbedAnimations.find(
- (animation) => animation.id === animationId,
- );
+ () => props.labelInstances,
+ (labelInstances) => {
+ const instancesToProcess = labelInstances ?? [];
+
+ for (const instance of instancesToProcess) {
+ const boundsToRender = resolveBoundsArray(instance);
+ const targetOpacity = instance.status === "fading" ? 0 : 1;
+
+ for (let index = 0; index < boundsToRender.length; index++) {
+ const bounds = boundsToRender[index];
+ const animationId = `label-${instance.id}-${index}`;
+ const existingAnimation = grabbedAnimations.find(
+ (animation) => animation.id === animationId,
+ );
- if (existingAnimation) {
- updateAnimationTarget(existingAnimation, bounds, targetOpacity);
- } else {
- grabbedAnimations.push(
- createAnimatedBounds(animationId, bounds, {
- opacity: 1,
- targetOpacity,
- }),
- );
- }
+ if (existingAnimation) {
+ updateAnimationTarget(existingAnimation, bounds, targetOpacity);
+ } else {
+ grabbedAnimations.push(
+ createAnimatedBounds(animationId, bounds, {
+ opacity: 1,
+ targetOpacity,
+ }),
+ );
}
}
+ }
- const activeLabelIds = new Set();
- for (const instance of instancesToProcess) {
- const boundsToRender = resolveBoundsArray(instance);
- for (let index = 0; index < boundsToRender.length; index++) {
- activeLabelIds.add(`label-${instance.id}-${index}`);
- }
+ const activeLabelIds = new Set();
+ for (const instance of instancesToProcess) {
+ const boundsToRender = resolveBoundsArray(instance);
+ for (let index = 0; index < boundsToRender.length; index++) {
+ activeLabelIds.add(`label-${instance.id}-${index}`);
}
+ }
- grabbedAnimations = grabbedAnimations.filter((animation) => {
- if (animation.id.startsWith("label-")) {
- return activeLabelIds.has(animation.id);
- }
- return true;
- });
+ grabbedAnimations = grabbedAnimations.filter((animation) => {
+ if (animation.id.startsWith("label-")) {
+ return activeLabelIds.has(animation.id);
+ }
+ return true;
+ });
- scheduleAnimationFrame();
- },
- ),
+ scheduleAnimationFrame();
+ },
);
- onMount(() => {
+ onSettled(() => {
initializeCanvas();
scheduleAnimationFrame();
@@ -724,7 +714,7 @@ export const OverlayCanvas: Component = (props) => {
setupDprMediaQuery();
- onCleanup(() => {
+ return () => {
window.removeEventListener("resize", handleWindowResize);
if (currentDprMediaQuery) {
currentDprMediaQuery.removeEventListener(
@@ -735,7 +725,7 @@ export const OverlayCanvas: Component = (props) => {
if (animationFrameId !== null) {
nativeCancelAnimationFrame(animationFrameId);
}
- });
+ };
});
return (
diff --git a/packages/react-grab/src/components/renderer.tsx b/packages/react-grab/src/components/renderer.tsx
index 781e3889d..017c7fa49 100644
--- a/packages/react-grab/src/components/renderer.tsx
+++ b/packages/react-grab/src/components/renderer.tsx
@@ -1,4 +1,4 @@
-import { Show, Index } from "solid-js";
+import { Show, For } from "solid-js";
import type { Component } from "solid-js";
import type { AgentSession, ReactGrabRendererProps } from "../types.js";
import {
@@ -64,10 +64,11 @@ export const ReactGrabRenderer: Component = (props) => {
}}
/>
-
{(session) => (
0}>
@@ -116,7 +117,7 @@ export const ReactGrabRenderer: Component = (props) => {
/>
)}
-
+
= (props) => {
/>
-
+
{(instance) => (
= (props) => {
}
/>
)}
-
+
= (
return activeMenuButton ?? undefined;
};
- createEffect(() => {
- void props.items;
- didPointerMove = false;
- });
+ createEffect(
+ () => props.items,
+ () => {
+ didPointerMove = false;
+ },
+ );
- createEffect(() => {
- const activeMenuItem = getMenuItemByIndex(props.activeIndex);
- if (activeMenuItem) {
- updateHighlight(activeMenuItem);
- }
- });
+ createEffect(
+ () => props.activeIndex,
+ (activeIndex) => {
+ const activeMenuItem = getMenuItemByIndex(activeIndex);
+ if (activeMenuItem) {
+ updateHighlight(activeMenuItem);
+ }
+ },
+ );
return (
@@ -62,47 +67,52 @@ export const ArrowNavigationMenu: Component = (
class="pointer-events-none absolute bg-black/5 opacity-0 transition-[top,left,width,height,opacity] duration-75 ease-out"
/>
- {(item, itemIndex) => (
- event.stopPropagation()}
- onPointerEnter={(event) => {
- updateHighlight(event.currentTarget);
- if (didPointerMove) {
+ {(itemAccessor, itemIndex) => {
+ const navItem = () => itemAccessor();
+ return (
+ event.stopPropagation()}
+ onPointerEnter={(event) => {
+ updateHighlight(event.currentTarget);
+ if (didPointerMove) {
+ props.onSelect(itemIndex());
+ }
+ }}
+ onPointerLeave={() => {
+ const activeMenuItem = getMenuItemByIndex(props.activeIndex);
+ if (activeMenuItem) {
+ updateHighlight(activeMenuItem);
+ } else {
+ clearHighlight();
+ }
+ }}
+ onClick={(event) => {
+ event.stopPropagation();
props.onSelect(itemIndex());
- }
- }}
- onPointerLeave={() => {
- const activeMenuItem = getMenuItemByIndex(props.activeIndex);
- if (activeMenuItem) {
- updateHighlight(activeMenuItem);
- } else {
- clearHighlight();
- }
- }}
- onClick={(event) => {
- event.stopPropagation();
- props.onSelect(itemIndex());
- }}
- >
-
-
- {item.componentName}
- .
-
- {item.tagName}
-
-
- )}
+
+
+ {navItem().componentName}
+ .
+
+ {navItem().tagName}
+
+
+ );
+ }}
diff --git a/packages/react-grab/src/components/selection-label/completion-view.tsx b/packages/react-grab/src/components/selection-label/completion-view.tsx
index a1f30a5cb..c2b2e9f55 100644
--- a/packages/react-grab/src/components/selection-label/completion-view.tsx
+++ b/packages/react-grab/src/components/selection-label/completion-view.tsx
@@ -1,4 +1,4 @@
-import { Show, createSignal, onMount, onCleanup } from "solid-js";
+import { Show, createSignal, onSettled } from "solid-js";
import type { Component } from "solid-js";
import type { CompletionViewProps } from "../../types.js";
import {
@@ -142,20 +142,20 @@ export const CompletionView: Component = (props) => {
confirmationFocusManager.claim(instanceId);
};
- onMount(() => {
+ onSettled(() => {
confirmationFocusManager.claim(instanceId);
window.addEventListener("keydown", handleKeyDown, { capture: true });
if (props.supportsFollowUp && props.onFollowUpSubmit && inputRef) {
inputRef.focus();
}
- });
- onCleanup(() => {
- confirmationFocusManager.release(instanceId);
- window.removeEventListener("keydown", handleKeyDown, { capture: true });
- if (fadeTimeoutId !== undefined) window.clearTimeout(fadeTimeoutId);
- if (dismissTimeoutId !== undefined) window.clearTimeout(dismissTimeoutId);
+ return () => {
+ confirmationFocusManager.release(instanceId);
+ window.removeEventListener("keydown", handleKeyDown, { capture: true });
+ if (fadeTimeoutId !== undefined) window.clearTimeout(fadeTimeoutId);
+ if (dismissTimeoutId !== undefined) window.clearTimeout(dismissTimeoutId);
+ };
});
return (
diff --git a/packages/react-grab/src/components/selection-label/discard-prompt.tsx b/packages/react-grab/src/components/selection-label/discard-prompt.tsx
index 43db71203..e94848203 100644
--- a/packages/react-grab/src/components/selection-label/discard-prompt.tsx
+++ b/packages/react-grab/src/components/selection-label/discard-prompt.tsx
@@ -1,4 +1,4 @@
-import { onMount, onCleanup } from "solid-js";
+import { onSettled } from "solid-js";
import type { Component } from "solid-js";
import type { DiscardPromptProps } from "../../types.js";
import { confirmationFocusManager } from "../../utils/confirmation-focus-manager.js";
@@ -30,14 +30,13 @@ export const DiscardPrompt: Component = (props) => {
confirmationFocusManager.claim(instanceId);
};
- onMount(() => {
+ onSettled(() => {
confirmationFocusManager.claim(instanceId);
window.addEventListener("keydown", handleKeyDown, { capture: true });
- });
-
- onCleanup(() => {
- confirmationFocusManager.release(instanceId);
- window.removeEventListener("keydown", handleKeyDown, { capture: true });
+ return () => {
+ confirmationFocusManager.release(instanceId);
+ window.removeEventListener("keydown", handleKeyDown, { capture: true });
+ };
});
return (
diff --git a/packages/react-grab/src/components/selection-label/error-view.tsx b/packages/react-grab/src/components/selection-label/error-view.tsx
index 3dea9d749..1e56467fd 100644
--- a/packages/react-grab/src/components/selection-label/error-view.tsx
+++ b/packages/react-grab/src/components/selection-label/error-view.tsx
@@ -1,4 +1,4 @@
-import { onMount, onCleanup, Show } from "solid-js";
+import { onSettled, Show } from "solid-js";
import type { Component } from "solid-js";
import type { ErrorViewProps } from "../../types.js";
import { confirmationFocusManager } from "../../utils/confirmation-focus-manager.js";
@@ -31,14 +31,13 @@ export const ErrorView: Component = (props) => {
confirmationFocusManager.claim(instanceId);
};
- onMount(() => {
+ onSettled(() => {
confirmationFocusManager.claim(instanceId);
window.addEventListener("keydown", handleKeyDown, { capture: true });
- });
-
- onCleanup(() => {
- confirmationFocusManager.release(instanceId);
- window.removeEventListener("keydown", handleKeyDown, { capture: true });
+ return () => {
+ confirmationFocusManager.release(instanceId);
+ window.removeEventListener("keydown", handleKeyDown, { capture: true });
+ };
});
const hasActions = () => Boolean(props.onRetry || props.onAcknowledge);
@@ -51,8 +50,10 @@ export const ErrorView: Component = (props) => {
onClick={handleFocus}
>
= (props) => {
}
};
- onMount(() => {
+ onSettled(() => {
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const rect = entry.target.getBoundingClientRect();
@@ -157,36 +155,42 @@ export const SelectionLabel: Component = (props) => {
window.visualViewport?.addEventListener("resize", handleViewportChange);
window.visualViewport?.addEventListener("scroll", handleViewportChange);
window.addEventListener("keydown", handleGlobalKeyDown, { capture: true });
- });
-
- onCleanup(() => {
- resizeObserver?.disconnect();
- window.removeEventListener("scroll", handleViewportChange, true);
- window.removeEventListener("resize", handleViewportChange);
- window.visualViewport?.removeEventListener("resize", handleViewportChange);
- window.visualViewport?.removeEventListener("scroll", handleViewportChange);
- window.removeEventListener("keydown", handleGlobalKeyDown, {
- capture: true,
- });
+ return () => {
+ resizeObserver?.disconnect();
+ window.removeEventListener("scroll", handleViewportChange, true);
+ window.removeEventListener("resize", handleViewportChange);
+ window.visualViewport?.removeEventListener(
+ "resize",
+ handleViewportChange,
+ );
+ window.visualViewport?.removeEventListener(
+ "scroll",
+ handleViewportChange,
+ );
+ window.removeEventListener("keydown", handleGlobalKeyDown, {
+ capture: true,
+ });
+ };
});
const elementIdentity = () =>
`${props.tagName ?? ""}:${props.componentName ?? ""}`;
- createEffect(() => {
- if (props.isPromptMode && inputRef && props.onSubmit) {
- // HACK: Defer focus one tick so the textarea is fully mounted.
- const focusTimeout = setTimeout(() => {
- if (inputRef) {
- inputRef.focus();
- autoResizeTextarea(inputRef, TEXTAREA_MAX_HEIGHT_PX);
- }
- }, 0);
- onCleanup(() => {
- clearTimeout(focusTimeout);
- });
- }
- });
+ createEffect(
+ () => [props.isPromptMode, Boolean(props.onSubmit)] as const,
+ ([isPromptMode, hasSubmit]) => {
+ if (isPromptMode && inputRef && hasSubmit) {
+ // HACK: Defer focus one tick so the textarea is fully mounted.
+ const focusTimeout = setTimeout(() => {
+ if (inputRef) {
+ inputRef.focus();
+ autoResizeTextarea(inputRef, TEXTAREA_MAX_HEIGHT_PX);
+ }
+ }, 0);
+ return () => clearTimeout(focusTimeout);
+ }
+ },
+ );
const positionComputation = createMemo(
(previousResult: PositionResult): PositionResult => {
@@ -324,11 +328,12 @@ export const SelectionLabel: Component = (props) => {
const hadValidBounds = () => positionComputation().hadValidBounds;
createEffect(
- on(
- () => props.selectionLabelShakeCount,
- () => setIsShaking(true),
- { defer: true },
- ),
+ () => props.selectionLabelShakeCount,
+ () => {
+ setIsShaking(true);
+ },
+ undefined,
+ { defer: true },
);
const handleKeyDown = (event: KeyboardEvent) => {
@@ -460,10 +465,12 @@ export const SelectionLabel: Component = (props) => {
>
@@ -518,15 +525,19 @@ export const SelectionLabel: Component
= (props) => {
= (props) => {
- {(item, itemIndex) => (
-
-
- {item.label}
-
-
-
- {formatShortcut(item.shortcut!)}
+ {(itemAccessor, itemIndex) => {
+ const cycleItem = () => itemAccessor();
+ return (
+
+
+ {cycleItem().label}
-
-
- )}
+
+
+ {formatShortcut(cycleItem().shortcut!)}
+
+
+
+ );
+ }}
@@ -633,7 +650,7 @@ export const SelectionLabel: Component = (props) => {
onKeyDown={handleKeyDown}
placeholder="Add context"
rows={1}
- readOnly={!props.onSubmit}
+ readonly={!props.onSubmit}
/>
= (props) => {
const hasLearnedSelectionHints = () => (props.clockFlashTrigger ?? 0) > 0;
createEffect(
- on(
- () => [props.isActive, hasLearnedSelectionHints()] as const,
- ([isActive, hasLearned]) => {
- setSelectionHintIndex(0);
- setHasHintCycled(false);
- if (!isActive || hasLearned) return;
- const intervalId = setInterval(() => {
- if (!hasHintCycled()) setHasHintCycled(true);
- setSelectionHintIndex(
- (previousIndex) => (previousIndex + 1) % SELECTION_HINT_COUNT,
- );
- }, SELECTION_HINT_CYCLE_INTERVAL_MS);
- onCleanup(() => clearInterval(intervalId));
- },
- { defer: true },
- ),
+ () => [props.isActive, hasLearnedSelectionHints()] as const,
+ ([isActive, hasLearned]) => {
+ setSelectionHintIndex(0);
+ setHasHintCycled(false);
+ if (!isActive || hasLearned) return;
+ const intervalId = setInterval(() => {
+ if (!hasHintCycled()) setHasHintCycled(true);
+ setSelectionHintIndex(
+ (previousIndex) => (previousIndex + 1) % SELECTION_HINT_COUNT,
+ );
+ }, SELECTION_HINT_CYCLE_INTERVAL_MS);
+ return () => clearInterval(intervalId);
+ },
+ undefined,
+ { defer: true },
);
const hasToolbarActions = () => (props.toolbarActions ?? []).length > 0;
@@ -342,47 +340,39 @@ export const Toolbar: Component = (props) => {
};
createEffect(
- on(
- () => props.shakeCount,
- (count) => {
- if (count && !props.enabled) {
- setIsShaking(true);
- setIsShakeTooltipVisible(true);
-
- clearShakeTooltipTimeout();
- shakeTooltipTimeout = setTimeout(() => {
- setIsShakeTooltipVisible(false);
- }, TOOLBAR_SHAKE_TOOLTIP_DURATION_MS);
- onCleanup(() => {
- clearShakeTooltipTimeout();
- });
- }
- },
- ),
+ () => props.shakeCount,
+ (count) => {
+ if (count && !props.enabled) {
+ setIsShaking(true);
+ setIsShakeTooltipVisible(true);
+
+ clearShakeTooltipTimeout();
+ shakeTooltipTimeout = setTimeout(() => {
+ setIsShakeTooltipVisible(false);
+ }, TOOLBAR_SHAKE_TOOLTIP_DURATION_MS);
+ return () => clearShakeTooltipTimeout();
+ }
+ },
);
createEffect(
- on(
- () => props.enabled,
- (enabled) => {
- if (enabled && isShakeTooltipVisible()) {
- setIsShakeTooltipVisible(false);
- clearShakeTooltipTimeout();
- }
- },
- ),
+ () => props.enabled,
+ (enabled) => {
+ if (enabled && isShakeTooltipVisible()) {
+ setIsShakeTooltipVisible(false);
+ clearShakeTooltipTimeout();
+ }
+ },
);
createEffect(
- on(
- () => [props.isActive, props.isContextMenuOpen] as const,
- ([isActive, isContextMenuOpen]) => {
- if (!isActive && !isContextMenuOpen && unfreezeUpdatesCallback) {
- unfreezeUpdatesCallback();
- unfreezeUpdatesCallback = null;
- }
- },
- ),
+ () => [props.isActive, props.isContextMenuOpen] as const,
+ ([isActive, isContextMenuOpen]) => {
+ if (!isActive && !isContextMenuOpen && unfreezeUpdatesCallback) {
+ unfreezeUpdatesCallback();
+ unfreezeUpdatesCallback = null;
+ }
+ },
);
const reclampToolbarToViewport = () => {
@@ -460,51 +450,49 @@ export const Toolbar: Component = (props) => {
};
createEffect(
- on(
- () => props.clockFlashTrigger ?? 0,
- () => {
- if (props.isHistoryDropdownOpen) return;
- if (clockFlashRef) {
- clockFlashRef.classList.remove("animate-clock-flash");
- // HACK: force reflow between class removal/addition to restart the CSS animation
- void clockFlashRef.offsetHeight;
- clockFlashRef.classList.add("animate-clock-flash");
- }
- setIsHistoryTooltipVisible(true);
- const timerId = setTimeout(() => {
- clockFlashRef?.classList.remove("animate-clock-flash");
- setIsHistoryTooltipVisible(false);
- }, FEEDBACK_DURATION_MS);
- onCleanup(() => {
- clearTimeout(timerId);
- setIsHistoryTooltipVisible(false);
- });
- },
- { defer: true },
- ),
+ () => props.clockFlashTrigger ?? 0,
+ () => {
+ if (props.isHistoryDropdownOpen) return;
+ if (clockFlashRef) {
+ clockFlashRef.classList.remove("animate-clock-flash");
+ // HACK: force reflow between class removal/addition to restart the CSS animation
+ void clockFlashRef.offsetHeight;
+ clockFlashRef.classList.add("animate-clock-flash");
+ }
+ setIsHistoryTooltipVisible(true);
+ const timerId = setTimeout(() => {
+ clockFlashRef?.classList.remove("animate-clock-flash");
+ setIsHistoryTooltipVisible(false);
+ }, FEEDBACK_DURATION_MS);
+ return () => {
+ clearTimeout(timerId);
+ setIsHistoryTooltipVisible(false);
+ };
+ },
+ undefined,
+ { defer: true },
);
createEffect(
- on(
- () => props.historyItemCount ?? 0,
- () => {
- if (isCollapsed()) return;
- // HACK: Wait for grid-cols CSS transition to complete, then re-measure and clamp to viewport
+ () => props.historyItemCount ?? 0,
+ () => {
+ if (isCollapsed()) return;
+ // HACK: Wait for grid-cols CSS transition to complete, then re-measure and clamp to viewport
+ if (historyItemCountTimeout) {
+ clearTimeout(historyItemCountTimeout);
+ }
+ historyItemCountTimeout = setTimeout(() => {
+ measureExpandableDimension();
+ reclampToolbarToViewport();
+ }, TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS);
+ return () => {
if (historyItemCountTimeout) {
clearTimeout(historyItemCountTimeout);
}
- historyItemCountTimeout = setTimeout(() => {
- measureExpandableDimension();
- reclampToolbarToViewport();
- }, TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS);
- onCleanup(() => {
- if (historyItemCountTimeout) {
- clearTimeout(historyItemCountTimeout);
- }
- });
- },
- { defer: true },
- ),
+ };
+ },
+ undefined,
+ { defer: true },
);
let expandedDimensions = {
@@ -849,7 +837,7 @@ export const Toolbar: Component = (props) => {
props.onStateChange?.(state);
};
- onMount(() => {
+ onSettled(() => {
if (containerRef) {
props.onContainerRef?.(containerRef);
}
@@ -908,52 +896,49 @@ export const Toolbar: Component = (props) => {
measureExpandableDimension();
}
+ let unsubscribe: (() => void) | undefined;
if (props.onSubscribeToStateChanges) {
- const unsubscribe = props.onSubscribeToStateChanges(
- (state: ToolbarState) => {
- if (isCollapseAnimating() || isToggleAnimating()) return;
+ unsubscribe = props.onSubscribeToStateChanges((state: ToolbarState) => {
+ if (isCollapseAnimating() || isToggleAnimating()) return;
- const rect = containerRef?.getBoundingClientRect();
- if (!rect) return;
+ const rect = containerRef?.getBoundingClientRect();
+ if (!rect) return;
- const didCollapsedChange = isCollapsed() !== state.collapsed;
+ const didCollapsedChange = isCollapsed() !== state.collapsed;
- setSnapEdge(state.edge);
+ setSnapEdge(state.edge);
- if (didCollapsedChange && !state.collapsed) {
- const collapsedPos = currentPosition();
+ if (didCollapsedChange && !state.collapsed) {
+ const collapsedPos = currentPosition();
+ setIsCollapseAnimating(true);
+ setIsCollapsed(state.collapsed);
+ const { position: newPos, ratio: newRatio } =
+ getExpandedFromCollapsed(collapsedPos, state.edge);
+ setPosition(newPos);
+ setPositionRatio(newRatio);
+ clearTimeout(collapseAnimationTimeout);
+ collapseAnimationTimeout = setTimeout(() => {
+ setIsCollapseAnimating(false);
+ }, TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS);
+ } else {
+ if (didCollapsedChange) {
setIsCollapseAnimating(true);
- setIsCollapsed(state.collapsed);
- const { position: newPos, ratio: newRatio } =
- getExpandedFromCollapsed(collapsedPos, state.edge);
- setPosition(newPos);
- setPositionRatio(newRatio);
clearTimeout(collapseAnimationTimeout);
collapseAnimationTimeout = setTimeout(() => {
setIsCollapseAnimating(false);
}, TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS);
- } else {
- if (didCollapsedChange) {
- setIsCollapseAnimating(true);
- clearTimeout(collapseAnimationTimeout);
- collapseAnimationTimeout = setTimeout(() => {
- setIsCollapseAnimating(false);
- }, TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS);
- }
- setIsCollapsed(state.collapsed);
- const newPosition = getPositionFromEdgeAndRatio(
- state.edge,
- state.ratio,
- expandedDimensions.width,
- expandedDimensions.height,
- );
- setPosition(newPosition);
- setPositionRatio(state.ratio);
}
- },
- );
-
- onCleanup(unsubscribe);
+ setIsCollapsed(state.collapsed);
+ const newPosition = getPositionFromEdgeAndRatio(
+ state.edge,
+ state.ratio,
+ expandedDimensions.width,
+ expandedDimensions.height,
+ );
+ setPosition(newPosition);
+ setPositionRatio(state.ratio);
+ }
+ });
}
window.addEventListener("resize", handleResize);
@@ -964,9 +949,10 @@ export const Toolbar: Component = (props) => {
setIsVisible(true);
}, TOOLBAR_FADE_IN_DELAY_MS);
- onCleanup(() => {
+ return () => {
clearTimeout(fadeInTimeout);
- });
+ unsubscribe?.();
+ };
});
onCleanup(() => {
@@ -1109,7 +1095,7 @@ export const Toolbar: Component = (props) => {
aria-label={
props.isActive ? "Stop selecting element" : "Select element"
}
- aria-pressed={Boolean(props.isActive)}
+ aria-pressed={props.isActive ? "true" : "false"}
class={cn(
"contain-layout flex items-center justify-center cursor-pointer interactive-scale touch-hitbox",
buttonSpacingClass(),
@@ -1148,7 +1134,7 @@ export const Toolbar: Component = (props) => {
: ""
}`}
aria-haspopup="menu"
- aria-expanded={Boolean(props.isHistoryDropdownOpen)}
+ aria-expanded={props.isHistoryDropdownOpen ? "true" : "false"}
class={cn(
"contain-layout flex items-center justify-center cursor-pointer interactive-scale touch-hitbox",
buttonSpacingClass(),
@@ -1250,7 +1236,7 @@ export const Toolbar: Component = (props) => {
: "Open more actions menu"
}
aria-haspopup="menu"
- aria-expanded={Boolean(props.isMenuOpen)}
+ aria-expanded={props.isMenuOpen ? "true" : "false"}
class={cn(
"contain-layout flex items-center justify-center cursor-pointer interactive-scale touch-hitbox",
buttonSpacingClass(),
@@ -1292,7 +1278,7 @@ export const Toolbar: Component = (props) => {
aria-label={
props.enabled ? "Disable React Grab" : "Enable React Grab"
}
- aria-pressed={Boolean(props.enabled)}
+ aria-pressed={props.enabled ? "true" : "false"}
class={cn(
"contain-layout flex items-center justify-center cursor-pointer interactive-scale outline-none",
isVertical() ? "my-0.5" : "mx-0.5",
diff --git a/packages/react-grab/src/components/toolbar/toolbar-content.tsx b/packages/react-grab/src/components/toolbar/toolbar-content.tsx
index cf8f003bf..dac156faa 100644
--- a/packages/react-grab/src/components/toolbar/toolbar-content.tsx
+++ b/packages/react-grab/src/components/toolbar/toolbar-content.tsx
@@ -92,7 +92,7 @@ export const ToolbarContent: Component = (props) => {
data-react-grab-ignore-events
data-react-grab-toolbar-toggle
aria-label={props.isActive ? "Stop selecting element" : "Select element"}
- aria-pressed={Boolean(props.isActive)}
+ aria-pressed={props.isActive ? "true" : "false"}
class={cn(
"contain-layout flex items-center justify-center cursor-pointer interactive-scale touch-hitbox",
buttonSpacingClass(),
@@ -173,7 +173,7 @@ export const ToolbarContent: Component = (props) => {
data-react-grab-ignore-events
data-react-grab-toolbar-enabled
aria-label={props.enabled ? "Disable React Grab" : "Enable React Grab"}
- aria-pressed={Boolean(props.enabled)}
+ aria-pressed={props.enabled ? "true" : "false"}
class={cn(
"contain-layout flex items-center justify-center cursor-pointer interactive-scale outline-none",
isVertical() ? "my-0.5" : "mx-0.5",
diff --git a/packages/react-grab/src/components/toolbar/toolbar-menu.tsx b/packages/react-grab/src/components/toolbar/toolbar-menu.tsx
index 2fa37ba90..0291e5b98 100644
--- a/packages/react-grab/src/components/toolbar/toolbar-menu.tsx
+++ b/packages/react-grab/src/components/toolbar/toolbar-menu.tsx
@@ -1,4 +1,4 @@
-import { Show, For, onMount, onCleanup, createSignal } from "solid-js";
+import { Show, For, onSettled, createSignal } from "solid-js";
import type { Component } from "solid-js";
import type { ToolbarMenuAction, DropdownAnchor } from "../../types.js";
import {
@@ -49,17 +49,17 @@ export const ToolbarMenu: Component = (props) => {
}
};
- onMount(() => {
+ onSettled(() => {
dropdown.measure();
const unregisterOverlayDismiss = registerOverlayDismiss({
isOpen: () => Boolean(props.position),
onDismiss: props.onDismiss,
});
- onCleanup(() => {
+ return () => {
dropdown.clearAnimationHandles();
unregisterOverlayDismiss();
- });
+ };
});
return (
@@ -97,19 +97,20 @@ export const ToolbarMenu: Component = (props) => {
class="pointer-events-none absolute bg-black/5 opacity-0 transition-[top,left,width,height,opacity] duration-75 ease-out"
/>
- {(action) => {
- const isToggleAction = action.isActive !== undefined;
+ {(actionAccessor) => {
+ const action = () => actionAccessor();
+ const isToggleAction = () => action().isActive !== undefined;
const isActionEnabled = () =>
- resolveToolbarActionEnabled(action);
+ resolveToolbarActionEnabled(action());
const isToggleActive = () => {
void toggleRefreshCounter();
- return Boolean(action.isActive?.());
+ return Boolean(action().isActive?.());
};
return (
event.stopPropagation()}
@@ -119,19 +120,19 @@ export const ToolbarMenu: Component = (props) => {
}
}}
onPointerLeave={clearHighlight}
- onClick={(event) => handleActionClick(action, event)}
+ onClick={(event) => handleActionClick(action(), event)}
>
- {action.label}
+ {action().label}
-
+
{(shortcutKey) => (
{formatShortcut(shortcutKey())}
)}
-
+
= (props) => {
let delayTimeoutId: ReturnType
| undefined;
createEffect(
- on(
- () => props.visible,
- (isVisible) => {
- if (delayTimeoutId !== undefined) {
- clearTimeout(delayTimeoutId);
- delayTimeoutId = undefined;
- }
+ () => props.visible,
+ (isVisible) => {
+ if (delayTimeoutId !== undefined) {
+ clearTimeout(delayTimeoutId);
+ delayTimeoutId = undefined;
+ }
- if (isVisible) {
- if (wasTooltipRecentlyVisible()) {
- setShouldAnimate(false);
- setDelayedVisible(true);
- } else {
- setShouldAnimate(true);
- delayTimeoutId = setTimeout(() => {
- setDelayedVisible(true);
- }, TOOLTIP_DELAY_MS);
- }
+ if (isVisible) {
+ if (wasTooltipRecentlyVisible()) {
+ setShouldAnimate(false);
+ setDelayedVisible(true);
} else {
- if (delayedVisible()) {
- lastCloseTimestamp = Date.now();
- }
- setDelayedVisible(false);
+ setShouldAnimate(true);
+ delayTimeoutId = setTimeout(() => {
+ setDelayedVisible(true);
+ }, TOOLTIP_DELAY_MS);
+ }
+ } else {
+ if (delayedVisible()) {
+ lastCloseTimestamp = Date.now();
}
- },
- ),
+ setDelayedVisible(false);
+ }
+ },
);
onCleanup(() => {
diff --git a/packages/react-grab/src/core/index.tsx b/packages/react-grab/src/core/index.tsx
index 8c0f707a2..ff3bfa759 100644
--- a/packages/react-grab/src/core/index.tsx
+++ b/packages/react-grab/src/core/index.tsx
@@ -6,11 +6,8 @@ import {
createSignal,
onCleanup,
createEffect,
- createResource,
- on,
- batch,
} from "solid-js";
-import { render } from "solid-js/web";
+import { render } from "@solidjs/web";
import { createGrabStore } from "./store.js";
import {
isKeyboardEventTriggeredByInput,
@@ -249,7 +246,8 @@ export const init = (rawOptions?: Options): ReactGrabAPI => {
);
createEffect(
- on(isActivated, (activated, previousActivated) => {
+ () => isActivated(),
+ (activated, previousActivated) => {
if (activated && !previousActivated) {
freezePseudoStates();
freezeGlobalAnimations();
@@ -260,7 +258,7 @@ export const init = (rawOptions?: Options): ReactGrabAPI => {
unfreezeGlobalAnimations();
document.body.style.touchAction = "";
}
- }),
+ },
);
const savedToolbarState = loadToolbarState();
@@ -381,45 +379,56 @@ export const init = (rawOptions?: Options): ReactGrabAPI => {
holdState.startTimestamp = null;
};
- createEffect(() => {
- if (store.current.state !== "holding") {
- clearHoldTimer();
- return;
- }
- holdState.startTimestamp = Date.now();
- holdState.timerId = window.setTimeout(() => {
- holdState.timerId = null;
- if (holdState.copyWaiting) {
- holdState.holdTimerFired = true;
+ createEffect(
+ () => store.current.state,
+ (currentState) => {
+ if (currentState !== "holding") {
+ clearHoldTimer();
return;
}
- actions.activate();
- }, store.keyHoldDuration);
- onCleanup(clearHoldTimer);
- });
+ holdState.startTimestamp = Date.now();
+ holdState.timerId = window.setTimeout(() => {
+ holdState.timerId = null;
+ if (holdState.copyWaiting) {
+ holdState.holdTimerFired = true;
+ return;
+ }
+ actions.activate();
+ }, store.keyHoldDuration);
+ return () => clearHoldTimer();
+ },
+ );
- createEffect(() => {
- if (
- store.current.state !== "active" ||
- store.current.phase !== "justDragged"
- )
- return;
- const timerId = setTimeout(() => {
- actions.finishJustDragged();
- }, FEEDBACK_DURATION_MS);
- onCleanup(() => clearTimeout(timerId));
- });
+ createEffect(
+ () => {
+ const currentState = store.current.state;
+ const currentPhase =
+ currentState === "active" ? store.current.phase : null;
+ return [currentState, currentPhase] as const;
+ },
+ ([currentState, currentPhase]) => {
+ if (currentState !== "active" || currentPhase !== "justDragged") return;
+ const timerId = setTimeout(() => {
+ actions.finishJustDragged();
+ }, FEEDBACK_DURATION_MS);
+ return () => clearTimeout(timerId);
+ },
+ );
- createEffect(() => {
- if (store.current.state !== "justCopied") return;
- const timerId = setTimeout(() => {
- actions.finishJustCopied();
- }, FEEDBACK_DURATION_MS);
- onCleanup(() => clearTimeout(timerId));
- });
+ createEffect(
+ () => store.current.state,
+ (currentState) => {
+ if (currentState !== "justCopied") return;
+ const timerId = setTimeout(() => {
+ actions.finishJustCopied();
+ }, FEEDBACK_DURATION_MS);
+ return () => clearTimeout(timerId);
+ },
+ );
createEffect(
- on(isHoldingKeys, (currentlyHolding, previouslyHolding = false) => {
+ () => isHoldingKeys(),
+ (currentlyHolding, previouslyHolding = false) => {
if (!previouslyHolding || currentlyHolding || !isActivated()) {
return;
}
@@ -427,7 +436,7 @@ export const init = (rawOptions?: Options): ReactGrabAPI => {
actions.setWasActivatedByToggle(true);
}
pluginRegistry.hooks.onActivate();
- }),
+ },
);
const preparePromptMode = (
@@ -1004,21 +1013,24 @@ export const init = (rawOptions?: Options): ReactGrabAPI => {
() => store.frozenElement || (isFrozenPhase() ? null : targetElement()),
);
- createEffect(() => {
- const element = store.detectedElement;
- if (!element) return;
+ createEffect(
+ () => store.detectedElement,
+ (element) => {
+ if (!element) return;
- const intervalId = setInterval(() => {
- if (!isElementConnected(element)) {
- actions.setDetectedElement(null);
- }
- }, BOUNDS_RECALC_INTERVAL_MS);
+ const intervalId = setInterval(() => {
+ if (!isElementConnected(element)) {
+ actions.setDetectedElement(null);
+ }
+ }, BOUNDS_RECALC_INTERVAL_MS);
- onCleanup(() => clearInterval(intervalId));
- });
+ return () => clearInterval(intervalId);
+ },
+ );
createEffect(
- on(effectiveElement, (element) => {
+ () => effectiveElement(),
+ (element) => {
if (componentNameDebounceTimerId !== null) {
clearTimeout(componentNameDebounceTimerId);
componentNameDebounceTimerId = null;
@@ -1033,7 +1045,7 @@ export const init = (rawOptions?: Options): ReactGrabAPI => {
componentNameDebounceTimerId = null;
setDebouncedElementForComponentName(element);
}, COMPONENT_NAME_DEBOUNCE_MS);
- }),
+ },
);
onCleanup(() => {
@@ -1043,19 +1055,20 @@ export const init = (rawOptions?: Options): ReactGrabAPI => {
}
});
- createEffect(() => {
- const elements = store.frozenElements;
- const cleanup = freezeAnimations(elements);
- onCleanup(cleanup);
- });
+ createEffect(
+ () => store.frozenElements,
+ (elements) => {
+ return freezeAnimations(elements);
+ },
+ );
createEffect(
- on(isActivated, (activated) => {
+ () => isActivated(),
+ (activated) => {
if (!activated) return;
if (!pluginRegistry.store.options.freezeReactUpdates) return;
- const unfreezeUpdates = freezeUpdates();
- onCleanup(unfreezeUpdates);
- }),
+ return freezeUpdates();
+ },
);
// HACK: In touch mode during drag, effectiveElement() is null so we use detectedElement
@@ -1228,59 +1241,55 @@ export const init = (rawOptions?: Options): ReactGrabAPI => {
});
createEffect(
- on(
- () => [targetElement(), store.lastGrabbedElement] as const,
- ([currentElement, lastElement]) => {
- if (lastElement && currentElement && lastElement !== currentElement) {
- actions.setLastGrabbed(null);
- }
- if (currentElement) {
- pluginRegistry.hooks.onElementHover(currentElement);
- }
- },
- ),
+ () => [targetElement(), store.lastGrabbedElement] as const,
+ ([currentElement, lastElement]) => {
+ if (lastElement && currentElement && lastElement !== currentElement) {
+ actions.setLastGrabbed(null);
+ }
+ if (currentElement) {
+ pluginRegistry.hooks.onElementHover(currentElement);
+ }
+ },
);
createEffect(
- on(
- () => targetElement(),
- (element) => {
- const currentVersion = ++selectionSourceRequestVersion;
+ () => targetElement(),
+ (element) => {
+ const currentVersion = ++selectionSourceRequestVersion;
- const clearSource = () => {
+ const clearSource = () => {
+ if (selectionSourceRequestVersion === currentVersion) {
+ actions.setSelectionSource(null, null);
+ }
+ };
+
+ if (!element) {
+ clearSource();
+ return;
+ }
+
+ resolveSource(element)
+ .then((source) => {
+ if (selectionSourceRequestVersion !== currentVersion) return;
+ if (!source) {
+ clearSource();
+ return;
+ }
+ actions.setSelectionSource(source.filePath, source.lineNumber);
+ })
+ .catch(() => {
if (selectionSourceRequestVersion === currentVersion) {
actions.setSelectionSource(null, null);
}
- };
-
- if (!element) {
- clearSource();
- return;
- }
-
- resolveSource(element)
- .then((source) => {
- if (selectionSourceRequestVersion !== currentVersion) return;
- if (!source) {
- clearSource();
- return;
- }
- actions.setSelectionSource(source.filePath, source.lineNumber);
- })
- .catch(() => {
- if (selectionSourceRequestVersion === currentVersion) {
- actions.setSelectionSource(null, null);
- }
- });
- },
- ),
+ });
+ },
);
createEffect(
- on(
- () => store.viewportVersion,
- () => agentManager._internal.updateBoundsOnViewportChange(),
- ),
+ () => store.viewportVersion,
+ () => {
+ agentManager._internal.updateBoundsOnViewportChange();
+ },
);
const stateChangeGrabbedBoxes = createMemo(() =>
@@ -1352,75 +1361,68 @@ export const init = (rawOptions?: Options): ReactGrabAPI => {
});
createEffect(
- on(derivedStateForHook, (state) => {
+ () => derivedStateForHook(),
+ (state) => {
pluginRegistry.hooks.onStateChange(state);
- }),
+ },
);
createEffect(
- on(
- () =>
- [
- isPromptMode(),
- store.pointer.x,
- store.pointer.y,
- targetElement(),
- ] as const,
- ([inputMode, x, y, target]) => {
- pluginRegistry.hooks.onPromptModeChange(inputMode, {
- x,
- y,
- targetElement: target,
- });
- },
- ),
+ () =>
+ [
+ isPromptMode(),
+ store.pointer.x,
+ store.pointer.y,
+ targetElement(),
+ ] as const,
+ ([inputMode, x, y, target]) => {
+ pluginRegistry.hooks.onPromptModeChange(inputMode, {
+ x,
+ y,
+ targetElement: target,
+ });
+ },
);
createEffect(
- on(
- () => [selectionVisible(), selectionBounds(), targetElement()] as const,
- ([visible, bounds, element]) => {
- pluginRegistry.hooks.onSelectionBox(
- Boolean(visible),
- bounds ?? null,
- element,
- );
- },
- ),
+ () => [selectionVisible(), selectionBounds(), targetElement()] as const,
+ ([visible, bounds, element]) => {
+ pluginRegistry.hooks.onSelectionBox(
+ Boolean(visible),
+ bounds ?? null,
+ element,
+ );
+ },
);
createEffect(
- on(
- () => [dragVisible(), dragBounds()] as const,
- ([visible, bounds]) => {
- pluginRegistry.hooks.onDragBox(Boolean(visible), bounds ?? null);
- },
- ),
+ () => [dragVisible(), dragBounds()] as const,
+ ([visible, bounds]) => {
+ pluginRegistry.hooks.onDragBox(Boolean(visible), bounds ?? null);
+ },
);
createEffect(
- on(
- () =>
- [
- labelVisible(),
- labelVariant(),
- cursorPosition(),
- targetElement(),
- store.selectionFilePath,
- store.selectionLineNumber,
- ] as const,
- ([visible, variant, position, element, filePath, lineNumber]) => {
- pluginRegistry.hooks.onElementLabel(Boolean(visible), variant, {
- x: position.x,
- y: position.y,
- content: "",
- element: element ?? undefined,
- tagName: element ? getTagName(element) || undefined : undefined,
- filePath: filePath ?? undefined,
- lineNumber: lineNumber ?? undefined,
- });
- },
- ),
+ () =>
+ [
+ labelVisible(),
+ labelVariant(),
+ cursorPosition(),
+ targetElement(),
+ store.selectionFilePath,
+ store.selectionLineNumber,
+ ] as const,
+ ([visible, variant, position, element, filePath, lineNumber]) => {
+ pluginRegistry.hooks.onElementLabel(Boolean(visible), variant, {
+ x: position.x,
+ y: position.y,
+ content: "",
+ element: element ?? undefined,
+ tagName: element ? getTagName(element) || undefined : undefined,
+ filePath: filePath ?? undefined,
+ lineNumber: lineNumber ?? undefined,
+ });
+ },
);
let cursorStyleElement: HTMLStyleElement | null = null;
@@ -1440,18 +1442,16 @@ export const init = (rawOptions?: Options): ReactGrabAPI => {
};
createEffect(
- on(
- () => [isActivated(), isCopying(), isPromptMode()] as const,
- ([activated, copying, promptMode]) => {
- if (copying) {
- setCursorOverride("progress");
- } else if (activated && !promptMode) {
- setCursorOverride("crosshair");
- } else {
- setCursorOverride(null);
- }
- },
- ),
+ () => [isActivated(), isCopying(), isPromptMode()] as const,
+ ([activated, copying, promptMode]) => {
+ if (copying) {
+ setCursorOverride("progress");
+ } else if (activated && !promptMode) {
+ setCursorOverride("crosshair");
+ } else {
+ setCursorOverride(null);
+ }
+ },
);
const activateRenderer = () => {
@@ -2292,17 +2292,19 @@ export const init = (rawOptions?: Options): ReactGrabAPI => {
}));
createEffect(
- on(selectionElement, () => {
+ () => selectionElement(),
+ () => {
resetActionCycle();
- }),
+ },
);
createEffect(
- on(canCycleActions, (isEnabled) => {
+ () => canCycleActions(),
+ (isEnabled) => {
if (!isEnabled) {
resetActionCycle();
}
- }),
+ },
);
const getActionById = (actionId: string): ContextMenuAction | undefined =>
@@ -3009,34 +3011,35 @@ export const init = (rawOptions?: Options): ReactGrabAPI => {
});
};
- createEffect(() => {
- const shouldRunInterval =
+ createEffect(
+ () =>
pluginRegistry.store.theme.enabled &&
(isActivated() ||
isCopying() ||
store.labelInstances.length > 0 ||
store.grabbedBoxes.length > 0 ||
- agentManager.sessions().size > 0);
-
- if (shouldRunInterval) {
- if (boundsRecalcIntervalId !== null) return;
-
- boundsRecalcIntervalId = window.setInterval(() => {
- scheduleBoundsSync();
- }, BOUNDS_RECALC_INTERVAL_MS);
- return;
- }
+ agentManager.sessions().size > 0),
+ (shouldRunInterval) => {
+ if (shouldRunInterval) {
+ if (boundsRecalcIntervalId !== null) return;
+
+ boundsRecalcIntervalId = window.setInterval(() => {
+ scheduleBoundsSync();
+ }, BOUNDS_RECALC_INTERVAL_MS);
+ return;
+ }
- if (boundsRecalcIntervalId !== null) {
- window.clearInterval(boundsRecalcIntervalId);
- boundsRecalcIntervalId = null;
- }
+ if (boundsRecalcIntervalId !== null) {
+ window.clearInterval(boundsRecalcIntervalId);
+ boundsRecalcIntervalId = null;
+ }
- if (viewportChangeFrameId !== null) {
- nativeCancelAnimationFrame(viewportChangeFrameId);
- viewportChangeFrameId = null;
- }
- });
+ if (viewportChangeFrameId !== null) {
+ nativeCancelAnimationFrame(viewportChangeFrameId);
+ viewportChangeFrameId = null;
+ }
+ },
+ );
onCleanup(() => {
if (boundsRecalcIntervalId !== null) {
@@ -3121,27 +3124,25 @@ export const init = (rawOptions?: Options): ReactGrabAPI => {
});
createEffect(
- on(
- () => debouncedElementForComponentName(),
- (element) => {
- const currentVersion = ++componentNameRequestVersion;
+ () => debouncedElementForComponentName(),
+ (element) => {
+ const currentVersion = ++componentNameRequestVersion;
- if (!element) {
- setResolvedComponentName(undefined);
- return;
- }
+ if (!element) {
+ setResolvedComponentName(undefined);
+ return;
+ }
- getNearestComponentName(element)
- .then((name) => {
- if (componentNameRequestVersion !== currentVersion) return;
- setResolvedComponentName(name ?? undefined);
- })
- .catch(() => {
- if (componentNameRequestVersion !== currentVersion) return;
- setResolvedComponentName(undefined);
- });
- },
- ),
+ getNearestComponentName(element)
+ .then((name) => {
+ if (componentNameRequestVersion !== currentVersion) return;
+ setResolvedComponentName(name ?? undefined);
+ })
+ .catch(() => {
+ if (componentNameRequestVersion !== currentVersion) return;
+ setResolvedComponentName(undefined);
+ });
+ },
);
const selectionLabelVisible = createMemo(() => {
@@ -3273,24 +3274,57 @@ export const init = (rawOptions?: Options): ReactGrabAPI => {
return getTagName(element) || undefined;
});
- const [contextMenuComponentName] = createResource(
+ const [contextMenuComponentName, setContextMenuComponentName] =
+ createSignal(undefined);
+
+ let contextMenuComponentNameVersion = 0;
+
+ createEffect(
() => ({
element: store.contextMenuElement,
frozenCount: store.frozenElements.length,
}),
- async ({ element, frozenCount }) => {
- if (!element) return undefined;
- if (frozenCount > 1) return undefined;
- const name = await getNearestComponentName(element);
- return name ?? undefined;
+ ({ element, frozenCount }) => {
+ const requestVersion = ++contextMenuComponentNameVersion;
+ if (!element || frozenCount > 1) {
+ setContextMenuComponentName(undefined);
+ return;
+ }
+ getNearestComponentName(element)
+ .then((name) => {
+ if (contextMenuComponentNameVersion !== requestVersion) return;
+ setContextMenuComponentName(name ?? undefined);
+ })
+ .catch(() => {
+ if (contextMenuComponentNameVersion !== requestVersion) return;
+ setContextMenuComponentName(undefined);
+ });
},
);
- const [contextMenuFilePath] = createResource(
+ const [contextMenuFilePath, setContextMenuFilePath] = createSignal
+ > | null>(null);
+
+ let contextMenuFilePathVersion = 0;
+
+ createEffect(
() => store.contextMenuElement,
- async (element) => {
- if (!element) return null;
- return resolveSource(element);
+ (element) => {
+ const requestVersion = ++contextMenuFilePathVersion;
+ if (!element) {
+ setContextMenuFilePath(null);
+ return;
+ }
+ resolveSource(element)
+ .then((source) => {
+ if (contextMenuFilePathVersion !== requestVersion) return;
+ setContextMenuFilePath(source);
+ })
+ .catch(() => {
+ if (contextMenuFilePathVersion !== requestVersion) return;
+ setContextMenuFilePath(null);
+ });
},
);
@@ -3825,27 +3859,25 @@ export const init = (rawOptions?: Options): ReactGrabAPI => {
// HACK: defer to next frame so idle preview labels clear visually before "copied" appears
nativeRequestAnimationFrame(() => {
- batch(() => {
- for (const historyItem of currentHistoryItems) {
- const connectedElements = getConnectedHistoryElements(historyItem);
- for (const element of connectedElements) {
- const bounds = createElementBounds(element);
- const labelId = generateId("label");
-
- actions.addLabelInstance({
- id: labelId,
- bounds,
- tagName: historyItem.tagName,
- componentName: historyItem.componentName,
- status: "copied",
- createdAt: Date.now(),
- element,
- mouseX: bounds.x + bounds.width / 2,
- });
- scheduleLabelFade(labelId);
- }
+ for (const historyItem of currentHistoryItems) {
+ const connectedElements = getConnectedHistoryElements(historyItem);
+ for (const element of connectedElements) {
+ const bounds = createElementBounds(element);
+ const labelId = generateId("label");
+
+ actions.addLabelInstance({
+ id: labelId,
+ bounds,
+ tagName: historyItem.tagName,
+ componentName: historyItem.componentName,
+ status: "copied",
+ createdAt: Date.now(),
+ element,
+ mouseX: bounds.x + bounds.width / 2,
+ });
+ scheduleLabelFade(labelId);
}
- });
+ }
});
};
@@ -3949,14 +3981,12 @@ export const init = (rawOptions?: Options): ReactGrabAPI => {
}, 0);
};
- createEffect(() => {
- const hue = pluginRegistry.store.theme.hue;
- if (hue !== 0) {
- rendererRoot.style.filter = `hue-rotate(${hue}deg)`;
- } else {
- rendererRoot.style.filter = "";
- }
- });
+ createEffect(
+ () => pluginRegistry.store.theme.hue,
+ (hue) => {
+ rendererRoot.style.filter = hue !== 0 ? `hue-rotate(${hue}deg)` : "";
+ },
+ );
if (pluginRegistry.store.theme.enabled) {
render(() => {
diff --git a/packages/react-grab/src/core/plugin-registry.ts b/packages/react-grab/src/core/plugin-registry.ts
index 0e7c8d04c..dd1deb1df 100644
--- a/packages/react-grab/src/core/plugin-registry.ts
+++ b/packages/react-grab/src/core/plugin-registry.ts
@@ -1,4 +1,4 @@
-import { createStore } from "solid-js/store";
+import { createStore, storePath } from "solid-js";
import type {
Position,
Plugin,
@@ -110,10 +110,12 @@ const createPluginRegistry = (initialOptions: SettableOptions = {}) => {
mergedOptions = { ...mergedOptions, ...directOptionOverrides };
- setStore("theme", mergedTheme);
- setStore("options", mergedOptions);
- setStore("actions", allContextMenuActions);
- setStore("toolbarActions", allToolbarActions);
+ setStore((draft) => {
+ draft.theme = mergedTheme;
+ draft.options = mergedOptions;
+ draft.actions = allContextMenuActions;
+ draft.toolbarActions = allToolbarActions;
+ });
};
const setOption = (
@@ -121,7 +123,7 @@ const createPluginRegistry = (initialOptions: SettableOptions = {}) => {
optionValue: OptionsState[OptionKey],
) => {
directOptionOverrides[optionKey] = optionValue;
- setStore("options", optionKey, optionValue);
+ setStore(storePath("options", optionKey, optionValue));
};
const SETTABLE_OPTION_KEYS: Array = [
diff --git a/packages/react-grab/src/core/store.ts b/packages/react-grab/src/core/store.ts
index 2e5df9d86..24fd4fbb4 100644
--- a/packages/react-grab/src/core/store.ts
+++ b/packages/react-grab/src/core/store.ts
@@ -1,4 +1,4 @@
-import { createStore, produce } from "solid-js/store";
+import { createStore, storePath } from "solid-js";
import type {
Position,
Theme,
@@ -213,74 +213,69 @@ const createGrabStore = (input: GrabStoreInput) => {
const [store, setStore] = createStore(createInitialStore(input));
const setActivePhase = (phase: GrabPhase) => {
- setStore(
- "current",
- produce((current) => {
- if (current.state === "active") {
- current.phase = phase;
- }
- }),
- );
+ setStore((draft) => {
+ if (draft.current.state === "active") {
+ draft.current.phase = phase;
+ }
+ });
};
const actions: GrabActions = {
startHold: (duration?: number) => {
- if (duration !== undefined) {
- setStore("keyHoldDuration", duration);
- }
- setStore("current", { state: "holding", startedAt: Date.now() });
+ setStore((draft) => {
+ if (duration !== undefined) {
+ draft.keyHoldDuration = duration;
+ }
+ draft.current = { state: "holding", startedAt: Date.now() };
+ });
},
releaseHold: () => {
if (store.current.state === "holding") {
- setStore("current", { state: "idle" });
+ setStore(storePath("current", { state: "idle" } as GrabState));
}
},
activate: () => {
- setStore(
- produce((draft) => {
- draft.current = {
- state: "active",
- phase: "hovering",
- isPromptMode: false,
- isPendingDismiss: false,
- };
- draft.activationTimestamp = Date.now();
- draft.previouslyFocusedElement = document.activeElement;
- }),
- );
+ setStore((draft) => {
+ draft.current = {
+ state: "active",
+ phase: "hovering",
+ isPromptMode: false,
+ isPendingDismiss: false,
+ };
+ draft.activationTimestamp = Date.now();
+ draft.previouslyFocusedElement = document.activeElement;
+ });
},
deactivate: () => {
- setStore(
- produce((draft) => {
- draft.current = { state: "idle" };
- draft.wasActivatedByToggle = false;
- draft.pendingCommentMode = false;
- draft.inputText = "";
- draft.frozenElement = null;
- draft.frozenElements = [];
- draft.frozenDragRect = null;
- draft.pendingClickData = null;
- draft.replySessionId = null;
- draft.pendingAbortSessionId = null;
- draft.activationTimestamp = null;
- draft.previouslyFocusedElement = null;
- draft.contextMenuPosition = null;
- draft.contextMenuElement = null;
- draft.contextMenuClickOffset = null;
- draft.selectedAgent = null;
- draft.lastCopiedElement = null;
- }),
- );
+ setStore((draft) => {
+ draft.current = { state: "idle" };
+ draft.wasActivatedByToggle = false;
+ draft.pendingCommentMode = false;
+ draft.inputText = "";
+ draft.frozenElement = null;
+ draft.frozenElements = [];
+ draft.frozenDragRect = null;
+ draft.pendingClickData = null;
+ draft.replySessionId = null;
+ draft.pendingAbortSessionId = null;
+ draft.activationTimestamp = null;
+ draft.previouslyFocusedElement = null;
+ draft.contextMenuPosition = null;
+ draft.contextMenuElement = null;
+ draft.contextMenuClickOffset = null;
+ draft.selectedAgent = null;
+ draft.lastCopiedElement = null;
+ });
},
toggle: () => {
if (store.activationTimestamp !== null) {
actions.deactivate();
} else {
- setStore("wasActivatedByToggle", true);
+ setStore(storePath("wasActivatedByToggle", true));
actions.activate();
}
},
@@ -289,7 +284,7 @@ const createGrabStore = (input: GrabStoreInput) => {
if (store.current.state === "active") {
const elementToFreeze = store.frozenElement ?? store.detectedElement;
if (elementToFreeze) {
- setStore("frozenElement", elementToFreeze);
+ setStore(storePath("frozenElement", elementToFreeze));
}
setActivePhase("frozen");
}
@@ -297,13 +292,11 @@ const createGrabStore = (input: GrabStoreInput) => {
unfreeze: () => {
if (store.current.state === "active") {
- setStore(
- produce((draft) => {
- draft.frozenElement = null;
- draft.frozenElements = [];
- draft.frozenDragRect = null;
- }),
- );
+ setStore((draft) => {
+ draft.frozenElement = null;
+ draft.frozenElements = [];
+ draft.frozenDragRect = null;
+ });
setActivePhase("hovering");
}
},
@@ -311,10 +304,12 @@ const createGrabStore = (input: GrabStoreInput) => {
startDrag: (position: Position) => {
if (store.current.state === "active") {
actions.clearFrozenElement();
- setStore("dragStart", {
- x: position.x + window.scrollX,
- y: position.y + window.scrollY,
- });
+ setStore(
+ storePath("dragStart", {
+ x: position.x + window.scrollX,
+ y: position.y + window.scrollY,
+ }),
+ );
setActivePhase("dragging");
}
},
@@ -324,7 +319,12 @@ const createGrabStore = (input: GrabStoreInput) => {
store.current.state === "active" &&
store.current.phase === "dragging"
) {
- setStore("dragStart", { x: OFFSCREEN_POSITION, y: OFFSCREEN_POSITION });
+ setStore(
+ storePath("dragStart", {
+ x: OFFSCREEN_POSITION,
+ y: OFFSCREEN_POSITION,
+ }),
+ );
setActivePhase("justDragged");
}
},
@@ -334,7 +334,12 @@ const createGrabStore = (input: GrabStoreInput) => {
store.current.state === "active" &&
store.current.phase === "dragging"
) {
- setStore("dragStart", { x: OFFSCREEN_POSITION, y: OFFSCREEN_POSITION });
+ setStore(
+ storePath("dragStart", {
+ x: OFFSCREEN_POSITION,
+ y: OFFSCREEN_POSITION,
+ }),
+ );
setActivePhase("hovering");
}
},
@@ -350,24 +355,28 @@ const createGrabStore = (input: GrabStoreInput) => {
startCopy: () => {
const wasActive = store.current.state === "active";
- setStore("current", {
- state: "copying",
- startedAt: Date.now(),
- wasActive,
- });
+ setStore(
+ storePath("current", {
+ state: "copying",
+ startedAt: Date.now(),
+ wasActive,
+ } as GrabState),
+ );
},
completeCopy: (element?: Element) => {
- setStore("pendingClickData", null);
- if (element) {
- setStore("lastCopiedElement", element);
- }
const wasActive =
store.current.state === "copying" ? store.current.wasActive : false;
- setStore("current", {
- state: "justCopied",
- copiedAt: Date.now(),
- wasActive,
+ setStore((draft) => {
+ draft.pendingClickData = null;
+ if (element) {
+ draft.lastCopiedElement = element;
+ }
+ draft.current = {
+ state: "justCopied",
+ copiedAt: Date.now(),
+ wasActive,
+ };
});
},
@@ -377,12 +386,14 @@ const createGrabStore = (input: GrabStoreInput) => {
store.current.wasActive && !store.wasActivatedByToggle;
if (shouldReturnToActive) {
actions.clearFrozenElement();
- setStore("current", {
- state: "active",
- phase: "hovering",
- isPromptMode: false,
- isPendingDismiss: false,
- });
+ setStore(
+ storePath("current", {
+ state: "active",
+ phase: "hovering",
+ isPromptMode: false,
+ isPendingDismiss: false,
+ } as GrabState),
+ );
} else {
actions.deactivate();
}
@@ -393,178 +404,171 @@ const createGrabStore = (input: GrabStoreInput) => {
const bounds = createElementBounds(element);
const selectionCenterX = bounds.x + bounds.width / 2;
- setStore("copyStart", position);
- setStore("copyOffsetFromCenterX", position.x - selectionCenterX);
- setStore("pointer", position);
- setStore("frozenElement", element);
- setStore("wasActivatedByToggle", true);
+ setStore((draft) => {
+ draft.copyStart = position;
+ draft.copyOffsetFromCenterX = position.x - selectionCenterX;
+ draft.pointer = position;
+ draft.frozenElement = element;
+ draft.wasActivatedByToggle = true;
+ });
if (store.current.state !== "active") {
- setStore("current", {
- state: "active",
- phase: "frozen",
- isPromptMode: true,
- isPendingDismiss: false,
+ setStore((draft) => {
+ draft.current = {
+ state: "active",
+ phase: "frozen",
+ isPromptMode: true,
+ isPendingDismiss: false,
+ };
+ draft.activationTimestamp = Date.now();
+ draft.previouslyFocusedElement = document.activeElement;
});
- setStore("activationTimestamp", Date.now());
- setStore("previouslyFocusedElement", document.activeElement);
} else {
- setStore(
- "current",
- produce((current) => {
- if (current.state === "active") {
- current.isPromptMode = true;
- current.phase = "frozen";
- }
- }),
- );
+ setStore((draft) => {
+ if (draft.current.state === "active") {
+ draft.current.isPromptMode = true;
+ draft.current.phase = "frozen";
+ }
+ });
}
},
exitPromptMode: () => {
if (store.current.state === "active") {
- setStore(
- "current",
- produce((current) => {
- if (current.state === "active") {
- current.isPromptMode = false;
- current.isPendingDismiss = false;
- }
- }),
- );
+ setStore((draft) => {
+ if (draft.current.state === "active") {
+ draft.current.isPromptMode = false;
+ draft.current.isPendingDismiss = false;
+ }
+ });
}
},
setInputText: (value: string) => {
- setStore("inputText", value);
+ setStore(storePath("inputText", value));
},
clearInputText: () => {
- setStore("inputText", "");
+ setStore(storePath("inputText", ""));
},
setPendingDismiss: (value: boolean) => {
if (store.current.state === "active") {
- setStore(
- "current",
- produce((current) => {
- if (current.state === "active") {
- current.isPendingDismiss = value;
- }
- }),
- );
+ setStore((draft) => {
+ if (draft.current.state === "active") {
+ draft.current.isPendingDismiss = value;
+ }
+ });
}
},
setPointer: (position: Position) => {
- setStore("pointer", position);
+ setStore(storePath("pointer", position));
},
setDetectedElement: (element: Element | null) => {
- setStore("detectedElement", element);
+ setStore(storePath("detectedElement", element));
},
setFrozenElement: (element: Element) => {
- setStore(
- produce((draft) => {
- draft.frozenElement = element;
- draft.frozenElements = [element];
- draft.frozenDragRect = null;
- }),
- );
+ setStore((draft) => {
+ draft.frozenElement = element;
+ draft.frozenElements = [element];
+ draft.frozenDragRect = null;
+ });
},
setFrozenElements: (elements: Element[]) => {
- setStore(
- produce((draft) => {
- draft.frozenElements = elements;
- draft.frozenElement = elements.length > 0 ? elements[0] : null;
- draft.frozenDragRect = null;
- }),
- );
+ setStore((draft) => {
+ draft.frozenElements = elements;
+ draft.frozenElement = elements.length > 0 ? elements[0] : null;
+ draft.frozenDragRect = null;
+ });
},
setFrozenDragRect: (rect: FrozenDragRect | null) => {
- setStore("frozenDragRect", rect);
+ setStore(storePath("frozenDragRect", rect));
},
clearFrozenElement: () => {
- setStore(
- produce((draft) => {
- draft.frozenElement = null;
- draft.frozenElements = [];
- draft.frozenDragRect = null;
- }),
- );
+ setStore((draft) => {
+ draft.frozenElement = null;
+ draft.frozenElements = [];
+ draft.frozenDragRect = null;
+ });
},
setCopyStart: (position: Position, element: Element) => {
const bounds = createElementBounds(element);
const selectionCenterX = bounds.x + bounds.width / 2;
- setStore("copyStart", position);
- setStore("copyOffsetFromCenterX", position.x - selectionCenterX);
+ setStore((draft) => {
+ draft.copyStart = position;
+ draft.copyOffsetFromCenterX = position.x - selectionCenterX;
+ });
},
setLastGrabbed: (element: Element | null) => {
- setStore("lastGrabbedElement", element);
+ setStore(storePath("lastGrabbedElement", element));
},
clearLastCopied: () => {
- setStore("lastCopiedElement", null);
+ setStore(storePath("lastCopiedElement", null));
},
setWasActivatedByToggle: (value: boolean) => {
- setStore("wasActivatedByToggle", value);
+ setStore(storePath("wasActivatedByToggle", value));
},
setPendingCommentMode: (value: boolean) => {
- setStore("pendingCommentMode", value);
+ setStore(storePath("pendingCommentMode", value));
},
setTouchMode: (value: boolean) => {
- setStore("isTouchMode", value);
+ setStore(storePath("isTouchMode", value));
},
setSelectionSource: (
filePath: string | null,
lineNumber: number | null,
) => {
- setStore(
- produce((draft) => {
- draft.selectionFilePath = filePath;
- draft.selectionLineNumber = lineNumber;
- }),
- );
+ setStore((draft) => {
+ draft.selectionFilePath = filePath;
+ draft.selectionLineNumber = lineNumber;
+ });
},
setPendingClickData: (data: PendingClickData | null) => {
- setStore("pendingClickData", data);
+ setStore(storePath("pendingClickData", data));
},
clearReplySessionId: () => {
- setStore("replySessionId", null);
+ setStore(storePath("replySessionId", null));
},
incrementViewportVersion: () => {
- setStore("viewportVersion", (version) => version + 1);
+ setStore(storePath("viewportVersion", (version) => version + 1));
},
addGrabbedBox: (box: GrabbedBox) => {
- setStore("grabbedBoxes", (boxes) => [...boxes, box]);
+ setStore(storePath("grabbedBoxes", (boxes) => [...boxes, box]));
},
removeGrabbedBox: (boxId: string) => {
- setStore("grabbedBoxes", (boxes) =>
- boxes.filter((box) => box.id !== boxId),
+ setStore(
+ storePath("grabbedBoxes", (boxes) =>
+ boxes.filter((innerBox) => innerBox.id !== boxId),
+ ),
);
},
clearGrabbedBoxes: () => {
- setStore("grabbedBoxes", []);
+ setStore(storePath("grabbedBoxes", []));
},
addLabelInstance: (instance: SelectionLabelInstance) => {
- setStore("labelInstances", (instances) => [...instances, instance]);
+ setStore(
+ storePath("labelInstances", (instances) => [...instances, instance]),
+ );
},
updateLabelInstance: (
@@ -576,72 +580,64 @@ const createGrabStore = (input: GrabStoreInput) => {
(instance) => instance.id === instanceId,
);
if (index !== -1) {
- setStore(
- "labelInstances",
- index,
- produce((instance) => {
- instance.status = status;
- if (errorMessage !== undefined) {
- instance.errorMessage = errorMessage;
- }
- }),
- );
+ setStore(storePath("labelInstances", index, "status", status));
+ if (errorMessage !== undefined) {
+ setStore(
+ storePath("labelInstances", index, "errorMessage", errorMessage),
+ );
+ }
}
},
removeLabelInstance: (instanceId: string) => {
- setStore("labelInstances", (instances) =>
- instances.filter((instance) => instance.id !== instanceId),
+ setStore(
+ storePath("labelInstances", (instances) =>
+ instances.filter((instance) => instance.id !== instanceId),
+ ),
);
},
clearLabelInstances: () => {
- setStore("labelInstances", []);
+ setStore(storePath("labelInstances", []));
},
setHasAgentProvider: (value: boolean) => {
- setStore("hasAgentProvider", value);
+ setStore(storePath("hasAgentProvider", value));
},
setAgentCapabilities: (capabilities) => {
- setStore(
- produce((draft) => {
- draft.supportsUndo = capabilities.supportsUndo;
- draft.supportsFollowUp = capabilities.supportsFollowUp;
- draft.dismissButtonText = capabilities.dismissButtonText;
- draft.isAgentConnected = capabilities.isAgentConnected;
- }),
- );
+ setStore((draft) => {
+ draft.supportsUndo = capabilities.supportsUndo;
+ draft.supportsFollowUp = capabilities.supportsFollowUp;
+ draft.dismissButtonText = capabilities.dismissButtonText;
+ draft.isAgentConnected = capabilities.isAgentConnected;
+ });
},
setPendingAbortSessionId: (sessionId: string | null) => {
- setStore("pendingAbortSessionId", sessionId);
+ setStore(storePath("pendingAbortSessionId", sessionId));
},
showContextMenu: (position: Position, element: Element) => {
const bounds = createElementBounds(element);
const centerX = bounds.x + bounds.width / 2;
const centerY = bounds.y + bounds.height / 2;
- setStore(
- produce((draft) => {
- draft.contextMenuPosition = position;
- draft.contextMenuElement = element;
- draft.contextMenuClickOffset = {
- x: position.x - centerX,
- y: position.y - centerY,
- };
- }),
- );
+ setStore((draft) => {
+ draft.contextMenuPosition = position;
+ draft.contextMenuElement = element;
+ draft.contextMenuClickOffset = {
+ x: position.x - centerX,
+ y: position.y - centerY,
+ };
+ });
},
hideContextMenu: () => {
- setStore(
- produce((draft) => {
- draft.contextMenuPosition = null;
- draft.contextMenuElement = null;
- draft.contextMenuClickOffset = null;
- }),
- );
+ setStore((draft) => {
+ draft.contextMenuPosition = null;
+ draft.contextMenuElement = null;
+ draft.contextMenuClickOffset = null;
+ });
},
updateContextMenuPosition: () => {
@@ -655,14 +651,16 @@ const createGrabStore = (input: GrabStoreInput) => {
const newCenterX = newBounds.x + newBounds.width / 2;
const newCenterY = newBounds.y + newBounds.height / 2;
- setStore("contextMenuPosition", {
- x: newCenterX + clickOffset.x,
- y: newCenterY + clickOffset.y,
- });
+ setStore(
+ storePath("contextMenuPosition", {
+ x: newCenterX + clickOffset.x,
+ y: newCenterY + clickOffset.y,
+ }),
+ );
},
setSelectedAgent: (agent: AgentOptions | null) => {
- setStore("selectedAgent", agent);
+ setStore(storePath("selectedAgent", agent));
},
};
diff --git a/packages/react-grab/src/styles.css b/packages/react-grab/src/styles.css
index 4c4eaca8e..d55bc0ea8 100644
--- a/packages/react-grab/src/styles.css
+++ b/packages/react-grab/src/styles.css
@@ -1,4 +1,4 @@
-@import "tailwindcss";
+@import "tailwindcss" source(".");
@theme {
--font-sans: "Geist", ui-sans-serif, system-ui, sans-serif;
diff --git a/packages/react-grab/src/utils/cn.ts b/packages/react-grab/src/utils/cn.ts
index 8f55b553c..e4fba47da 100644
--- a/packages/react-grab/src/utils/cn.ts
+++ b/packages/react-grab/src/utils/cn.ts
@@ -1,4 +1,3 @@
import { clsx, type ClassValue } from "clsx";
-import { twMerge } from "tailwind-merge";
-export const cn = (...inputs: ClassValue[]): string => twMerge(clsx(inputs));
+export const cn = (...inputs: ClassValue[]): string => clsx(inputs);
diff --git a/packages/react-grab/src/utils/create-anchored-dropdown.ts b/packages/react-grab/src/utils/create-anchored-dropdown.ts
index 10854e01f..af470b314 100644
--- a/packages/react-grab/src/utils/create-anchored-dropdown.ts
+++ b/packages/react-grab/src/utils/create-anchored-dropdown.ts
@@ -1,4 +1,4 @@
-import { createSignal, createEffect, createMemo, onCleanup } from "solid-js";
+import { createSignal, createEffect, createMemo } from "solid-js";
import type { Accessor } from "solid-js";
import type { DropdownAnchor } from "../types.js";
import {
@@ -60,51 +60,55 @@ export const createAnchoredDropdown = (
measure();
};
- createEffect(() => {
- const anchor = anchorAccessor();
- if (anchor) {
- setLastAnchorEdge(anchor.edge);
- clearTimeout(exitAnimationTimeout);
- setShouldMount(true);
- if (enterAnimationFrameId !== undefined)
- nativeCancelAnimationFrame(enterAnimationFrameId);
- // HACK: rAF measures then forces reflow so the browser commits the correct position before transitioning in
- enterAnimationFrameId = nativeRequestAnimationFrame(() => {
- measure();
- void containerRef()?.offsetHeight;
- setIsAnimatedIn(true);
- });
- } else {
- if (enterAnimationFrameId !== undefined)
- nativeCancelAnimationFrame(enterAnimationFrameId);
- setIsAnimatedIn(false);
- exitAnimationTimeout = setTimeout(() => {
- setShouldMount(false);
- }, DROPDOWN_ANIMATION_DURATION_MS);
- }
- onCleanup(clearAnimationHandles);
- });
+ createEffect(
+ () => anchorAccessor(),
+ (anchor) => {
+ if (anchor) {
+ setLastAnchorEdge(anchor.edge);
+ clearTimeout(exitAnimationTimeout);
+ setShouldMount(true);
+ if (enterAnimationFrameId !== undefined)
+ nativeCancelAnimationFrame(enterAnimationFrameId);
+ // HACK: rAF measures then forces reflow so the browser commits the correct position before transitioning in
+ enterAnimationFrameId = nativeRequestAnimationFrame(() => {
+ measure();
+ void containerRef()?.offsetHeight;
+ setIsAnimatedIn(true);
+ });
+ } else {
+ if (enterAnimationFrameId !== undefined)
+ nativeCancelAnimationFrame(enterAnimationFrameId);
+ setIsAnimatedIn(false);
+ exitAnimationTimeout = setTimeout(() => {
+ setShouldMount(false);
+ }, DROPDOWN_ANIMATION_DURATION_MS);
+ }
+ return () => clearAnimationHandles();
+ },
+ );
- createEffect(() => {
- const anchor = anchorAccessor();
- if (!anchor) return;
+ createEffect(
+ () => anchorAccessor(),
+ (anchor) => {
+ if (!anchor) return;
- window.addEventListener("resize", handleViewportChange);
- window.visualViewport?.addEventListener("resize", handleViewportChange);
- window.visualViewport?.addEventListener("scroll", handleViewportChange);
+ window.addEventListener("resize", handleViewportChange);
+ window.visualViewport?.addEventListener("resize", handleViewportChange);
+ window.visualViewport?.addEventListener("scroll", handleViewportChange);
- onCleanup(() => {
- window.removeEventListener("resize", handleViewportChange);
- window.visualViewport?.removeEventListener(
- "resize",
- handleViewportChange,
- );
- window.visualViewport?.removeEventListener(
- "scroll",
- handleViewportChange,
- );
- });
- });
+ return () => {
+ window.removeEventListener("resize", handleViewportChange);
+ window.visualViewport?.removeEventListener(
+ "resize",
+ handleViewportChange,
+ );
+ window.visualViewport?.removeEventListener(
+ "scroll",
+ handleViewportChange,
+ );
+ };
+ },
+ );
const displayPosition = createMemo(
(previousPosition: { left: number; top: number }) => {
diff --git a/packages/react-grab/tsup.config.ts b/packages/react-grab/tsup.config.ts
index 092c985f7..32dad2adf 100644
--- a/packages/react-grab/tsup.config.ts
+++ b/packages/react-grab/tsup.config.ts
@@ -50,7 +50,7 @@ const DEFAULT_OPTIONS: Options = {
".css": "text",
},
minify: process.env.NODE_ENV === "production",
- noExternal: ["clsx", "tailwind-merge", "solid-js", "bippy"],
+ noExternal: ["clsx", "solid-js", "@solidjs/web", "bippy"],
onSuccess: process.env.COPY ? "pbcopy < ./dist/index.global.js" : undefined,
outDir: "./dist",
sourcemap: false,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 243c87bee..7d9dd3dfd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -60,12 +60,15 @@ importers:
packages/design-system:
dependencies:
+ '@solidjs/web':
+ specifier: 2.0.0-beta.3
+ version: 2.0.0-beta.3(@solidjs/signals@0.13.3)(solid-js@2.0.0-beta.3)
react-grab:
specifier: workspace:*
version: link:../react-grab
solid-js:
- specifier: ^1.9.10
- version: 1.9.10
+ specifier: 2.0.0-beta.3
+ version: 2.0.0-beta.3
devDependencies:
'@babel/core':
specifier: ^7.28.5
@@ -74,8 +77,8 @@ importers:
specifier: ^7.28.5
version: 7.28.5(@babel/core@7.28.5)
babel-preset-solid:
- specifier: ^1.9.10
- version: 1.9.10(@babel/core@7.28.5)(solid-js@1.9.10)
+ specifier: 2.0.0-beta.3
+ version: 2.0.0-beta.3(@babel/core@7.28.5)(solid-js@2.0.0-beta.3)
esbuild-plugin-babel:
specifier: ^0.2.3
version: 0.2.3(@babel/core@7.28.5)
@@ -474,6 +477,9 @@ importers:
'@react-grab/cli':
specifier: workspace:*
version: link:../cli
+ '@solidjs/web':
+ specifier: 2.0.0-beta.3
+ version: 2.0.0-beta.3(@solidjs/signals@0.13.3)(solid-js@2.0.0-beta.3)
bippy:
specifier: ^0.5.32
version: 0.5.32(@types/react@19.2.11)(react@19.2.3)
@@ -484,8 +490,8 @@ importers:
specifier: '>=17.0.0'
version: 19.2.3
solid-js:
- specifier: ^1.9.10
- version: 1.9.10
+ specifier: 2.0.0-beta.3
+ version: 2.0.0-beta.3
devDependencies:
'@babel/core':
specifier: ^7.28.5
@@ -506,8 +512,8 @@ importers:
specifier: ^19.2.11
version: 19.2.11
babel-preset-solid:
- specifier: ^1.9.10
- version: 1.9.10(@babel/core@7.28.5)(solid-js@1.9.10)
+ specifier: 2.0.0-beta.3
+ version: 2.0.0-beta.3(@babel/core@7.28.5)(solid-js@2.0.0-beta.3)
clsx:
specifier: ^2.1.1
version: 2.1.1
@@ -523,9 +529,6 @@ importers:
publint:
specifier: ^0.2.12
version: 0.2.12
- tailwind-merge:
- specifier: ^2.5.5
- version: 2.6.0
tailwindcss:
specifier: ^4.1.0
version: 4.1.15
@@ -795,10 +798,6 @@ packages:
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
- '@babel/helper-validator-identifier@7.27.1':
- resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
- engines: {node: '>=6.9.0'}
-
'@babel/helper-validator-identifier@7.28.5':
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
@@ -878,10 +877,6 @@ packages:
resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==}
engines: {node: '>=6.9.0'}
- '@babel/types@7.28.4':
- resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==}
- engines: {node: '>=6.9.0'}
-
'@babel/types@7.28.5':
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
engines: {node: '>=6.9.0'}
@@ -2977,12 +2972,21 @@ packages:
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'}
+ '@solidjs/signals@0.13.3':
+ resolution: {integrity: sha512-Rlq8Kc0WtuhPv8SDNT1ANh45OUIbFKnY7AOiNmCSeOaGKlFaaYPPDxXN8qJtojykwGGwWXV6ZmHwCb1ZeY0b/A==}
+
+ '@solidjs/web@2.0.0-beta.3':
+ resolution: {integrity: sha512-DpTIbba7pLTKIXxUblok4n1rQscBLbjhJKhk+eUoODOnAS4TVDV4OmZ6qhC+VEuyPAbIbqzfoZsectFNEhak3w==}
+ peerDependencies:
+ '@solidjs/signals': ^0.13.3
+ solid-js: ^2.0.0-beta.3
+
'@sourcegraph/amp-sdk@0.1.0-20251210081226-g90e3892':
resolution: {integrity: sha512-y9/ltK2TY+0HD1H2Sz7MvU3zFh4SjER6eQVNQfBx/0gK9N7S0QwHW6cmhHLx3CP25zN190LKHXPieMGqsVvrOQ==}
engines: {node: '>=18'}
- '@sourcegraph/amp@0.0.1773792340-gded184':
- resolution: {integrity: sha512-Kj7jBcX7KbED1fMl+YnrHFOTj89pxIZT8Ipir7sZP2++oQ/IiQnaddv4H3foEYW+UNREWr0sD2tDk7tlB5mBWQ==}
+ '@sourcegraph/amp@0.0.1773950853-g6caa21':
+ resolution: {integrity: sha512-GJ463ale5AMrf6jPSYrtq4ewvSPqMBlLF16YQfTDivLs0mTgZspRW2uNAEq0YDy50GcGTeUakMsuUb6ZBaATfw==}
engines: {node: '>=20'}
hasBin: true
@@ -3783,19 +3787,19 @@ packages:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
- babel-plugin-jsx-dom-expressions@0.40.3:
- resolution: {integrity: sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w==}
+ babel-plugin-jsx-dom-expressions@0.41.0-next.11:
+ resolution: {integrity: sha512-m0yus4+XLNENjhpJNtZtjHXQLPepT3y0bmgAeceoSOgKGKeGfE8A6fOoObUHpz+mRd25dn4wJHa6wqO4JvQMWQ==}
peerDependencies:
'@babel/core': ^7.20.12
babel-plugin-react-compiler@1.0.0:
resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==}
- babel-preset-solid@1.9.10:
- resolution: {integrity: sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ==}
+ babel-preset-solid@2.0.0-beta.3:
+ resolution: {integrity: sha512-hV8Gi0Akolju1ydXNZNmt/SWcvGSWX5mqg+HHQFU2/iz22MDMflRSJrhaqrmN0gxJin9s3A/x3A20HqmcGhkBg==}
peerDependencies:
'@babel/core': ^7.0.0
- solid-js: ^1.9.10
+ solid-js: ^2.0.0-beta.3
peerDependenciesMeta:
solid-js:
optional: true
@@ -6302,14 +6306,14 @@ packages:
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
engines: {node: '>= 18'}
- seroval-plugins@1.3.3:
- resolution: {integrity: sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==}
+ seroval-plugins@1.5.1:
+ resolution: {integrity: sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw==}
engines: {node: '>=10'}
peerDependencies:
seroval: ^1.0
- seroval@1.3.2:
- resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==}
+ seroval@1.5.1:
+ resolution: {integrity: sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA==}
engines: {node: '>=10'}
serve-static@2.2.1:
@@ -6400,8 +6404,8 @@ packages:
resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==}
engines: {node: '>= 18'}
- solid-js@1.9.10:
- resolution: {integrity: sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==}
+ solid-js@2.0.0-beta.3:
+ resolution: {integrity: sha512-s2UT66v1UpmM1QybhNrQlJUiSG1HYl2TM7ah2d0TETy5U4fLxIYbyVECB3YOw7GwOTzaGjjJjlg4A7hkYqCQ+g==}
sonic-boom@4.2.0:
resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
@@ -6948,6 +6952,9 @@ packages:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
+ validate-html-nesting@1.2.4:
+ resolution: {integrity: sha512-doQi7e8EJ2OWneSG1aZpJluS6A49aZM0+EICXWKm1i6WvqTLmq0tpUcImc4KTWG50mORO0C4YDBtOCSYvElftw==}
+
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
@@ -7325,7 +7332,7 @@ snapshots:
'@babel/helper-module-imports@7.18.6':
dependencies:
- '@babel/types': 7.28.5
+ '@babel/types': 7.29.0
'@babel/helper-module-imports@7.27.1':
dependencies:
@@ -7367,8 +7374,6 @@ snapshots:
'@babel/helper-string-parser@7.27.1': {}
- '@babel/helper-validator-identifier@7.27.1': {}
-
'@babel/helper-validator-identifier@7.28.5': {}
'@babel/helper-validator-option@7.27.1': {}
@@ -7456,11 +7461,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@babel/types@7.28.4':
- dependencies:
- '@babel/helper-string-parser': 7.27.1
- '@babel/helper-validator-identifier': 7.27.1
-
'@babel/types@7.28.5':
dependencies:
'@babel/helper-string-parser': 7.27.1
@@ -7470,7 +7470,6 @@ snapshots:
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
- optional: true
'@base-ui/react@1.2.0(@types/react@19.2.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
@@ -9160,12 +9159,21 @@ snapshots:
'@sindresorhus/merge-streams@4.0.0': {}
+ '@solidjs/signals@0.13.3': {}
+
+ '@solidjs/web@2.0.0-beta.3(@solidjs/signals@0.13.3)(solid-js@2.0.0-beta.3)':
+ dependencies:
+ '@solidjs/signals': 0.13.3
+ seroval: 1.5.1
+ seroval-plugins: 1.5.1(seroval@1.5.1)
+ solid-js: 2.0.0-beta.3
+
'@sourcegraph/amp-sdk@0.1.0-20251210081226-g90e3892':
dependencies:
- '@sourcegraph/amp': 0.0.1773792340-gded184
+ '@sourcegraph/amp': 0.0.1773950853-g6caa21
zod: 3.25.76
- '@sourcegraph/amp@0.0.1773792340-gded184':
+ '@sourcegraph/amp@0.0.1773950853-g6caa21':
dependencies:
'@napi-rs/keyring': 1.1.9
@@ -9923,26 +9931,27 @@ snapshots:
axobject-query@4.1.0: {}
- babel-plugin-jsx-dom-expressions@0.40.3(@babel/core@7.28.5):
+ babel-plugin-jsx-dom-expressions@0.41.0-next.11(@babel/core@7.28.5):
dependencies:
'@babel/core': 7.28.5
'@babel/helper-module-imports': 7.18.6
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5)
- '@babel/types': 7.28.4
+ '@babel/types': 7.29.0
html-entities: 2.3.3
parse5: 7.3.0
+ validate-html-nesting: 1.2.4
babel-plugin-react-compiler@1.0.0:
dependencies:
'@babel/types': 7.29.0
optional: true
- babel-preset-solid@1.9.10(@babel/core@7.28.5)(solid-js@1.9.10):
+ babel-preset-solid@2.0.0-beta.3(@babel/core@7.28.5)(solid-js@2.0.0-beta.3):
dependencies:
'@babel/core': 7.28.5
- babel-plugin-jsx-dom-expressions: 0.40.3(@babel/core@7.28.5)
+ babel-plugin-jsx-dom-expressions: 0.41.0-next.11(@babel/core@7.28.5)
optionalDependencies:
- solid-js: 1.9.10
+ solid-js: 2.0.0-beta.3
balanced-match@1.0.2: {}
@@ -12735,11 +12744,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
- seroval-plugins@1.3.3(seroval@1.3.2):
+ seroval-plugins@1.5.1(seroval@1.5.1):
dependencies:
- seroval: 1.3.2
+ seroval: 1.5.1
- seroval@1.3.2: {}
+ seroval@1.5.1: {}
serve-static@2.2.1:
dependencies:
@@ -12874,11 +12883,12 @@ snapshots:
smol-toml@1.6.0: {}
- solid-js@1.9.10:
+ solid-js@2.0.0-beta.3:
dependencies:
+ '@solidjs/signals': 0.13.3
csstype: 3.2.3
- seroval: 1.3.2
- seroval-plugins: 1.3.3(seroval@1.3.2)
+ seroval: 1.5.1
+ seroval-plugins: 1.5.1(seroval@1.5.1)
sonic-boom@4.2.0:
dependencies:
@@ -13456,6 +13466,8 @@ snapshots:
uuid@8.3.2: {}
+ validate-html-nesting@1.2.4: {}
+
vary@1.1.2: {}
vaul@1.1.2(@types/react-dom@19.2.2(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.0.1(react@19.0.1))(react@19.0.1):