Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
8 changes: 8 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,10 @@ export const loadToolbarState = (): ToolbarState | null => {
};

export const saveToolbarState = (state: ToolbarState): void => {
// Demo mode never writes to the visitor's clipboard or storage; persisting
// here 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