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
1 change: 1 addition & 0 deletions packages/react-grab/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"css:watch": "tailwindcss -i ./src/styles.css -o ./dist/styles.css -w",
"prebuild": "mkdir -p dist && tailwindcss -i ./src/styles.css -o ./dist/styles.css -m && tsx scripts/css-rem-to-px.ts",
"build": "NODE_ENV=production vp pack",
"build:demo": "IS_DEMO=true pnpm build",
"build:profiling": "pnpm run prebuild && NODE_ENV=profiling REACT_GRAB_NO_MINIFY=true REACT_GRAB_SOURCEMAP=true vp pack",
"dev": "concurrently \"pnpm:css:watch\" \"vp pack --watch\"",
"test": "vp test run tests && playwright test",
Expand Down
18 changes: 18 additions & 0 deletions packages/react-grab/src/components/toolbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { freezeGlobalAnimations, unfreezeGlobalAnimations } from "../../utils/fr
import { freezePseudoStates, unfreezePseudoStates } from "../../utils/freeze-pseudo-states.js";
import { ToolbarContent } from "./toolbar-content.js";
import { getVisualViewport } from "../../utils/get-visual-viewport.js";
import { getScopeContainer, IS_DEMO } from "../../utils/runtime-mode.js";
import {
calculateExpandedPositionFromCollapsed,
getCollapsedDimsForEdge,
Expand Down Expand Up @@ -382,6 +383,15 @@ export const Toolbar: Component<ToolbarProps> = (props) => {
setPosition(getPositionFromEdgeAndRatio(snapEdge(), positionRatio(), newWidth, newHeight));
};

// In scoped mode the toolbar is anchored to a container whose viewport box
// moves as the page scrolls, so it must re-anchor on scroll. Repositioning
// only (no ratio/resize dance) keeps it glued to the container edge.
const handleScopedScroll = () => {
if (!getScopeContainer()) return;
if (drag.isDragging() || drag.isSnapping()) return;
recalculatePosition();
};

const handleResize = () => {
if (drag.isDragging()) return;

Expand Down Expand Up @@ -520,6 +530,11 @@ export const Toolbar: Component<ToolbarProps> = (props) => {
window.addEventListener("resize", handleResize);
window.visualViewport?.addEventListener("resize", handleResize);
window.visualViewport?.addEventListener("scroll", handleResize);
// Scoped re-anchoring only matters for the (demo-only) container mode; keep
// it out of normal builds so real users pay nothing on scroll.
if (IS_DEMO) {
window.addEventListener("scroll", handleScopedScroll, { passive: true, capture: true });
}
Comment thread
aidenybai marked this conversation as resolved.

if (typeof ResizeObserver !== "undefined" && containerRef) {
const observer = new ResizeObserver((entries) => {
Expand Down Expand Up @@ -561,6 +576,9 @@ export const Toolbar: Component<ToolbarProps> = (props) => {
window.removeEventListener("resize", handleResize);
window.visualViewport?.removeEventListener("resize", handleResize);
window.visualViewport?.removeEventListener("scroll", handleResize);
if (IS_DEMO) {
window.removeEventListener("scroll", handleScopedScroll, { capture: true });
}
clearTimeout(resizeTimeout);
clearTimeout(collapseAnimationTimeout);

Expand Down
7 changes: 7 additions & 0 deletions packages/react-grab/src/components/toolbar/state.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import type { ToolbarState } from "../../types.js";
import { DEFAULT_ACTION_ID, TOOLBAR_DEFAULT_POSITION_RATIO } from "../../constants.js";
import { IS_DEMO } from "../../utils/runtime-mode.js";

export type { ToolbarState };
export type SnapEdge = "top" | "bottom" | "left" | "right";

const STORAGE_KEY = "react-grab-toolbar-state";

export const loadToolbarState = (): ToolbarState | null => {
// Demo mode is display-only and must stay deterministic, so it never reads the
// visitor's persisted toolbar prefs - it always starts from the defaults.
if (IS_DEMO) return null;
Comment thread
aidenybai marked this conversation as resolved.
try {
const serializedToolbarState = localStorage.getItem(STORAGE_KEY);
if (!serializedToolbarState) return null;
Expand Down Expand Up @@ -36,6 +40,9 @@ export const loadToolbarState = (): ToolbarState | null => {
};

export const saveToolbarState = (state: ToolbarState): void => {
// Demo mode is display-only; persisting would clobber the real toolbar prefs
// of anyone running React Grab on the same origin.
if (IS_DEMO) return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
} catch (error) {
Expand Down
10 changes: 10 additions & 0 deletions packages/react-grab/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,16 @@ export const SHIFT_SELECTION_LABEL_MIN_ANCHOR_RATIO = 0;
export const SHIFT_SELECTION_LABEL_MAX_ANCHOR_RATIO = 1;
export const SHIFT_SELECTION_LABEL_FALLBACK_ANCHOR_RATIO = 0;

// Demo driver (react-grab/demo). The pointer artwork is 19×26 with its tip near
// the top-left, so the cursor element is offset by these so the tip — not the
// bounding box — lands on the animation target.
export const DEMO_CURSOR_TIP_X_PX = 5;
export const DEMO_CURSOR_TIP_Y_PX = 4;
export const DEMO_CURSOR_FADE_MS = 300;
export const DEMO_CLICK_PULSE_MS = 220;
export const DEMO_CLICK_PULSE_MIN_SCALE = 0.8;
export const DEMO_TYPE_CHAR_MS = 55;

export const RELEVANT_CSS_PROPERTIES = new Set([
"display",
"position",
Expand Down
Loading
Loading