Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions .github/workflows/test-perf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,13 @@ jobs:
run: |
BASE_SHA="${{ github.event.pull_request.base.sha }}"
git fetch origin "$BASE_SHA" --depth=1
if git cat-file -e "$BASE_SHA:packages/react-grab/src" 2>/dev/null; then
PACKAGE_DEPENDENCY_FIELDS='{ dependencies, devDependencies, optionalDependencies, peerDependencies }'
if ! diff --brief \
<(git show "$BASE_SHA:packages/react-grab/package.json" | jq --sort-keys "$PACKAGE_DEPENDENCY_FIELDS") \
<(jq --sort-keys "$PACKAGE_DEPENDENCY_FIELDS" packages/react-grab/package.json) >/dev/null; then
echo "React Grab's dependencies changed; skipping the incompatible baseline run."
echo "swapped=false" >> "$GITHUB_OUTPUT"
elif git cat-file -e "$BASE_SHA:packages/react-grab/src" 2>/dev/null; then
git checkout "$BASE_SHA" -- packages/react-grab/src
echo "swapped=true" >> "$GITHUB_OUTPUT"
echo "Swapped react-grab/src to base revision $BASE_SHA"
Expand Down Expand Up @@ -150,7 +156,15 @@ jobs:
- name: Compute baseline-vs-current diff
if: github.event_name == 'pull_request' && needs.perf-shard.result == 'success'
run: |
node packages/react-grab/scripts/diff-perf-runs.mjs packages/react-grab/perf/baseline packages/react-grab/perf/current | tee perf-diff.md >> "$GITHUB_STEP_SUMMARY"
if [[ -d packages/react-grab/perf/baseline && -d packages/react-grab/perf/current ]]; then
node packages/react-grab/scripts/diff-perf-runs.mjs packages/react-grab/perf/baseline packages/react-grab/perf/current | tee perf-diff.md >> "$GITHUB_STEP_SUMMARY"
else
{
echo "## Performance comparison skipped"
echo
echo "A compatible base-source run was unavailable, so only current-source performance tests were executed."
} | tee perf-diff.md >> "$GITHUB_STEP_SUMMARY"
fi

- name: Upload perf diff
if: always() && github.event_name == 'pull_request'
Expand Down
6 changes: 4 additions & 2 deletions apps/openstory/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@
"openstory": "^0.1.0"
},
"devDependencies": {
"@solidjs/vite-plugin": "3.0.0-next.28",
"@solidjs/web": "2.0.0-rc.0",
"@types/node": "^25.6.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"react-grab": "workspace:*",
"solid-js": "^1.9.12",
"solid-js": "2.0.0-rc.0",
"typescript": "^6.0.3",
"vite": "npm:@voidzero-dev/vite-plus-core@^0.1.20",
"vite-plugin-solid": "^2.11.12"
"vite-plugin-solid": "3.0.0-next.27"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: vite-plugin-solid@3.0.0-next.27 is now an unused dependency. The Solid Vite plugin was moved to @solidjs/vite-plugin (imported in vite.config.ts), and nothing else references vite-plugin-solid. Remove the vite-plugin-solid devDependency instead of bumping it to keep package.json free of dead dependencies.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/openstory/package.json, line 25:

<comment>vite-plugin-solid@3.0.0-next.27 is now an unused dependency. The Solid Vite plugin was moved to @solidjs/vite-plugin (imported in vite.config.ts), and nothing else references vite-plugin-solid. Remove the vite-plugin-solid devDependency instead of bumping it to keep package.json free of dead dependencies.</comment>

<file context>
@@ -13,14 +13,16 @@
     "typescript": "^6.0.3",
     "vite": "npm:@voidzero-dev/vite-plus-core@^0.1.20",
-    "vite-plugin-solid": "^2.11.12"
+    "vite-plugin-solid": "3.0.0-next.27"
   },
   "optionalDependencies": {
</file context>

},
"optionalDependencies": {
"react": "19.2.6",
Expand Down
2 changes: 1 addition & 1 deletion apps/openstory/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { JSX } from "solid-js";
import type { JSX } from "@solidjs/web";
import "react-grab/dist/styles.css";
import type { Preview } from "openstory/solid";

Expand Down
6 changes: 3 additions & 3 deletions apps/openstory/stories/playground/freeze-demo.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "openstory/solid";
import { onCleanup, onMount } from "solid-js";
import { onSettled } from "solid-js";
import { createElement } from "react";
import { createRoot, type Root } from "react-dom/client";

Expand All @@ -9,11 +9,11 @@ import { BouncingTimer } from "./bouncing-timer.react.js";
const ReactHost = () => {
let hostElement: HTMLDivElement | undefined;

onMount(() => {
onSettled(() => {
if (!hostElement) return;
const reactRoot: Root = createRoot(hostElement);
reactRoot.render(createElement(BouncingTimer));
onCleanup(() => reactRoot.unmount());
return () => reactRoot.unmount();
});

return <div ref={hostElement} style={{ height: "100vh", width: "100vw" }} />;
Expand Down
6 changes: 3 additions & 3 deletions apps/openstory/stories/playground/live-updates.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "openstory/solid";
import { onCleanup, onMount } from "solid-js";
import { onSettled } from "solid-js";
import { createElement } from "react";
import { createRoot, type Root } from "react-dom/client";

Expand All @@ -9,11 +9,11 @@ import { LiveCounter } from "./live-counter.react.js";
const ReactHost = () => {
let hostElement: HTMLDivElement | undefined;

onMount(() => {
onSettled(() => {
if (!hostElement) return;
const reactRoot: Root = createRoot(hostElement);
reactRoot.render(createElement(LiveCounter));
onCleanup(() => reactRoot.unmount());
return () => reactRoot.unmount();
});

return <div ref={hostElement} style={{ "min-height": "100vh" }} />;
Expand Down
10 changes: 5 additions & 5 deletions apps/openstory/stories/renderer.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createEffect, createSignal, on, onCleanup, onMount } from "solid-js";
import { createEffect, createSignal, onSettled } from "solid-js";
import type { Meta, StoryContext, StoryObj } from "openstory/solid";
import { expect, waitFor } from "openstory/test";
import { ReactGrabRenderer } from "@react-grab-source/components/renderer.js";
Expand Down Expand Up @@ -89,16 +89,16 @@ const Scene = (props: SceneProps) => {
});
};

onMount(() => {
onSettled(() => {
recompute();
window.addEventListener("resize", recompute);
onCleanup(() => {
return () => {
window.removeEventListener("resize", recompute);
if (pendingFrameHandle !== null) cancelAnimationFrame(pendingFrameHandle);
});
};
});

createEffect(on(() => props.selectedElement, scheduleRecompute, { defer: true }));
createEffect(() => props.selectedElement, scheduleRecompute, { defer: true });

const elementMeta = (): ElementMeta => {
const element = elementRefs[props.selectedElement];
Expand Down
3 changes: 2 additions & 1 deletion apps/openstory/stories/target-box.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Component, JSX } from "solid-js";
import type { Component } from "solid-js";
import type { JSX } from "@solidjs/web";
import { DEMO_BOUNDS } from "./demo-bounds.js";

export const TargetBox: Component = () => (
Expand Down
2 changes: 1 addition & 1 deletion apps/openstory/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "preserve",
"jsxImportSource": "solid-js",
"jsxImportSource": "@solidjs/web",
"strict": true,
"noEmit": true,
"esModuleInterop": true,
Expand Down
7 changes: 4 additions & 3 deletions apps/openstory/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { fileURLToPath } from "node:url";
import { defineConfig } from "vite";
import solid from "vite-plugin-solid";
import solid from "@solidjs/vite-plugin";
import { openstory } from "openstory/plugin";
import { solidSourceLocationBabelPlugin } from "../../packages/react-grab/solid-source-location-babel-plugin.js";

const REACT_FILE_PATTERN = /\.react\.tsx$/;
const REACT_FILE_PATTERN = "**/*.react.tsx";

export default defineConfig(({ command }) => ({
plugins: [
Expand All @@ -21,11 +21,12 @@ export default defineConfig(({ command }) => ({
],
resolve: {
alias: {
"solid-js/web": "@solidjs/web",
"@react-grab-source": fileURLToPath(
new URL("../../packages/react-grab/src", import.meta.url),
),
},
dedupe: ["solid-js", "solid-js/web"],
dedupe: ["solid-js", "@solidjs/web"],
},
define: {
"process.env.VERSION": JSON.stringify("[DEV]"),
Expand Down
4 changes: 2 additions & 2 deletions packages/react-grab/e2e/solid-source-location.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { SOLID_SOURCE_LOCATION_ATTRIBUTE } from "../src/utils/resolve-solid-sour
const TOOLBAR_ACTION_SELECTOR = "[data-react-grab-toolbar-action]";
const TOOLBAR_ACTION_SOURCE_PATH =
"packages/react-grab/src/components/toolbar/toolbar-action-button.tsx";
const TOOLBAR_ACTION_SOURCE_LINE_NUMBER = 24;
const TOOLBAR_ACTION_SOURCE_COLUMN_NUMBER = 5;
const TOOLBAR_ACTION_SOURCE_LINE_NUMBER = 39;
const TOOLBAR_ACTION_SOURCE_COLUMN_NUMBER = 7;

test("exposes exact Solid sources only in development", async ({ reactGrab }, testInfo) => {
await reactGrab.activate();
Expand Down
5 changes: 3 additions & 2 deletions packages/react-grab/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,15 @@
"@jridgewell/trace-mapping": "^0.3.31",
"@playwright/test": "^1.59.1",
"@react-grab/playwright-coverage": "workspace:*",
"@solidjs/web": "2.0.0-rc.0",
"@tailwindcss/cli": "^4.3.0",
"@types/babel__core": "^7.20.5",
"@types/node": "^25.6.2",
"@types/react": "^19.2.14",
"babel-preset-solid": "^1.9.12",
"babel-preset-solid": "2.0.0-rc.0",
"concurrently": "^9.2.1",
"expect-sdk": "^0.1.2",
"solid-js": "^1.9.12",
"solid-js": "2.0.0-rc.0",
"tailwindcss": "^4.3.0",
"tsx": "^4.21.0",
"vite-plus": "^0.1.20"
Expand Down
4 changes: 2 additions & 2 deletions packages/react-grab/solid-babel-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ export const cssTextPlugin = () => {

export const solidWebBrowserPlugin = () => {
const require = createRequire(import.meta.url);
const serverPath = require.resolve("solid-js/web");
const serverPath = require.resolve("@solidjs/web");
const distDir = dirname(serverPath);
const browserPath = resolve(distDir, "web.js");
return {
name: "solid-web-browser",
enforce: "pre" as const,
resolveId(source: string) {
if (source === "solid-js/web") return browserPath;
if (source === "@solidjs/web") return browserPath;
},
};
};
Expand Down
82 changes: 39 additions & 43 deletions packages/react-grab/src/components/context-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import {
createMemo,
createSignal,
For,
on,
onCleanup,
onMount,
onSettled,
Show,
type Component,
} from "solid-js";
Expand Down Expand Up @@ -98,8 +96,8 @@ export const ContextMenu: Component<ContextMenuProps> = (props) => {
// Elements that were just mounted may not have been laid out yet, so without
// deferring to the next frame the measured dimensions are zero and the menu
// would flash at the wrong position before jumping to its correct spot.
createEffect(() => {
if (isVisible()) {
createEffect(isVisible, (visible) => {
if (visible) {
nativeRequestAnimationFrame(measureContainer);
}
});
Expand Down Expand Up @@ -174,43 +172,41 @@ export const ContextMenu: Component<ContextMenuProps> = (props) => {
// restore focus to whatever the host page had focused — but only if
// nothing else has already claimed focus (e.g. the prompt-mode textarea
// that an action opened), so we do not yank it back.
createEffect(
on(isVisible, (visible) => {
if (visible) {
// document.activeElement returns the shadow host when focus is
// inside the shadow root, so use the host's shadowRoot.activeElement
// to detect a focused element that already lives in our own DOM
// tree; we should not capture our own host as "previous".
const hostShadowRoot = containerRef?.getRootNode();
const focusInsideHost =
hostShadowRoot instanceof ShadowRoot ? hostShadowRoot.activeElement : null;
const pageActiveElement = document.activeElement;
const wasFocusedOnPage =
pageActiveElement instanceof HTMLElement &&
focusInsideHost === null &&
!(containerRef instanceof Element && containerRef.contains(pageActiveElement));
previouslyFocusedElement = wasFocusedOnPage ? pageActiveElement : null;
menuContainerRef?.focus({ preventScroll: true });
return;
}
menuStore.setActiveItem(null);
const restoreTarget = previouslyFocusedElement;
previouslyFocusedElement = null;
if (!(restoreTarget instanceof HTMLElement) || !document.contains(restoreTarget)) return;
// Defer to the next frame so an action-triggered focus move (e.g.
// the prompt textarea focusing itself via queueMicrotask) lands
// first. If something other than the body has focus by then, the
// action's side effect deserves to keep it.
nativeRequestAnimationFrame(() => {
const currentActive = document.activeElement;
const isOrphanedFocus = currentActive === null || currentActive === document.body;
if (!isOrphanedFocus) return;
restoreTarget.focus({ preventScroll: true });
});
}),
);
createEffect(isVisible, (visible) => {
if (visible) {
// document.activeElement returns the shadow host when focus is
// inside the shadow root, so use the host's shadowRoot.activeElement
// to detect a focused element that already lives in our own DOM
// tree; we should not capture our own host as "previous".
const hostShadowRoot = containerRef?.getRootNode();
const focusInsideHost =
hostShadowRoot instanceof ShadowRoot ? hostShadowRoot.activeElement : null;
const pageActiveElement = document.activeElement;
const wasFocusedOnPage =
pageActiveElement instanceof HTMLElement &&
focusInsideHost === null &&
!(containerRef instanceof Element && containerRef.contains(pageActiveElement));
previouslyFocusedElement = wasFocusedOnPage ? pageActiveElement : null;
menuContainerRef?.focus({ preventScroll: true });
return;
}
menuStore.setActiveItem(null);
const restoreTarget = previouslyFocusedElement;
previouslyFocusedElement = null;
if (!(restoreTarget instanceof HTMLElement) || !document.contains(restoreTarget)) return;
// Defer to the next frame so an action-triggered focus move (e.g.
// the prompt textarea focusing itself via queueMicrotask) lands
// first. If something other than the body has focus by then, the
// action's side effect deserves to keep it.
nativeRequestAnimationFrame(() => {
const currentActive = document.activeElement;
const isOrphanedFocus = currentActive === null || currentActive === document.body;
if (!isOrphanedFocus) return;
restoreTarget.focus({ preventScroll: true });
});
});

onMount(() => {
onSettled(() => {
measureContainer();

const handleKeyDown = (event: KeyboardEvent) => {
Expand Down Expand Up @@ -278,10 +274,10 @@ export const ContextMenu: Component<ContextMenuProps> = (props) => {
const gatedHandleKeyDown = ignoreRealInput(handleKeyDown);
window.addEventListener("keydown", gatedHandleKeyDown, { capture: true });

onCleanup(() => {
return () => {
unregisterOverlayDismiss();
window.removeEventListener("keydown", gatedHandleKeyDown, { capture: true });
});
};
});

const accessibleMenuLabel = createMemo(() => {
Expand Down
17 changes: 10 additions & 7 deletions packages/react-grab/src/components/frozen-glow.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createEffect, createSignal, onCleanup, onMount, type Component } from "solid-js";
import { createEffect, createSignal, onSettled, type Component } from "solid-js";
import {
FADE_DURATION_MS,
FROZEN_GLOW_COLOR,
Expand Down Expand Up @@ -27,24 +27,27 @@ export const FrozenGlow: Component<FrozenGlowProps> = (props) => {
if (scopeContainer) {
const handleViewportChange = () => setScopeRect(measureRect());

onMount(() => {
onSettled(() => {
const resizeObserver = new ResizeObserver(handleViewportChange);
resizeObserver.observe(scopeContainer);
window.addEventListener("scroll", handleViewportChange, { capture: true, passive: true });
window.addEventListener("resize", handleViewportChange);
onCleanup(() => {
return () => {
resizeObserver.disconnect();
window.removeEventListener("scroll", handleViewportChange, { capture: true });
window.removeEventListener("resize", handleViewportChange);
});
};
});

// ResizeObserver misses position-only layout shifts (e.g. content above
// the container expanding while the showcase is idle), so re-measure at
// the moment the glow fades in — the only time a stale rect would show.
createEffect(() => {
if (props.visible) handleViewportChange();
});
createEffect(
() => props.visible,
(visible) => {
if (visible) handleViewportChange();
},
);
}

const top = () => {
Expand Down
9 changes: 5 additions & 4 deletions packages/react-grab/src/components/menu/menu-item.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { onCleanup, onMount, type Component, type JSX } from "solid-js";
import { onCleanup, onSettled, type Component } from "solid-js";
import type { JSX } from "@solidjs/web";
import { cn } from "../../utils/cn.js";
import { useMenuStore } from "./menu-context.js";

Expand Down Expand Up @@ -30,7 +31,7 @@ export const MenuItem: Component<MenuItemProps> = (props) => {

let buttonElement: HTMLButtonElement | undefined;

onMount(() => {
onSettled(() => {
if (!buttonElement) return;
store.registerItem({
value: registeredValue,
Expand All @@ -50,8 +51,8 @@ export const MenuItem: Component<MenuItemProps> = (props) => {
data-react-grab-menu-item={props.dataId ?? registeredValue}
type="button"
role={role()}
aria-checked={role() === "menuitemradio" ? Boolean(props.checked) : undefined}
aria-disabled={Boolean(props.disabled)}
aria-checked={role() === "menuitemradio" ? (props.checked ? "true" : "false") : undefined}
aria-disabled={props.disabled ? "true" : "false"}
tabindex={store.keyboardNavigation ? (isActive() ? 0 : -1) : undefined}
disabled={props.disabled}
class={cn(
Expand Down
Loading
Loading