Skip to content

Commit af140c7

Browse files
committed
feat: replace inspect stack with element properties panel
Replace the ancestor stack list shown during inspect mode (Shift) with a Chrome DevTools-style element properties panel showing computed styles, className, React props, accessibility info, and box model overlay. - Show size, color/background with swatches, font, margin, padding, display, position, overflow, and flex/grid properties - Show className with line-clamp, React component props (filtered) - Accessibility section with contrast ratio (AA/AAA badges), accessible name, role, and keyboard-focusable icons - Canvas box model overlay draws padding and margin zones in pink - Freeze element detection and block selection during inspect mode - Allow text selection within the properties panel - Remove ancestor overlay boxes and dead inspect canvas layer code Made-with: Cursor
1 parent 82c9544 commit af140c7

File tree

8 files changed

+733
-127
lines changed

8 files changed

+733
-127
lines changed

packages/react-grab/src/components/overlay-canvas.tsx

Lines changed: 83 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
OverlayBounds,
55
SelectionLabelInstance,
66
AgentSession,
7+
InspectBoxModel,
78
} from "../types.js";
89
import { lerp } from "../utils/lerp.js";
910
import {
@@ -19,8 +20,8 @@ import {
1920
OPACITY_CONVERGENCE_THRESHOLD,
2021
OVERLAY_BORDER_COLOR_DEFAULT,
2122
OVERLAY_FILL_COLOR_DEFAULT,
22-
OVERLAY_BORDER_COLOR_INSPECT,
23-
OVERLAY_FILL_COLOR_INSPECT,
23+
OVERLAY_FILL_COLOR_MARGIN,
24+
OVERLAY_FILL_COLOR_PADDING,
2425
} from "../constants.js";
2526
import {
2627
nativeCancelAnimationFrame,
@@ -34,12 +35,6 @@ const DEFAULT_LAYER_STYLE = {
3435
lerpFactor: SELECTION_LERP_FACTOR,
3536
} as const;
3637

37-
const INSPECT_LAYER_STYLE = {
38-
borderColor: OVERLAY_BORDER_COLOR_INSPECT,
39-
fillColor: OVERLAY_FILL_COLOR_INSPECT,
40-
lerpFactor: SELECTION_LERP_FACTOR,
41-
} as const;
42-
4338
const LAYER_STYLES = {
4439
drag: {
4540
borderColor: OVERLAY_BORDER_COLOR_DRAG,
@@ -49,10 +44,9 @@ const LAYER_STYLES = {
4944
selection: DEFAULT_LAYER_STYLE,
5045
grabbed: DEFAULT_LAYER_STYLE,
5146
processing: DEFAULT_LAYER_STYLE,
52-
inspect: INSPECT_LAYER_STYLE,
5347
} as const;
5448

55-
type LayerName = "drag" | "selection" | "grabbed" | "processing" | "inspect";
49+
type LayerName = "drag" | "selection" | "grabbed" | "processing";
5650

5751
interface OffscreenLayer {
5852
canvas: OffscreenCanvas | null;
@@ -77,9 +71,6 @@ export interface OverlayCanvasProps {
7771
selectionIsFading?: boolean;
7872
selectionShouldSnap?: boolean;
7973

80-
inspectVisible?: boolean;
81-
inspectBounds?: OverlayBounds[];
82-
8374
dragVisible?: boolean;
8475
dragBounds?: OverlayBounds;
8576

@@ -89,6 +80,8 @@ export interface OverlayCanvasProps {
8980
createdAt: number;
9081
}>;
9182

83+
inspectBoxModel?: InspectBoxModel;
84+
9285
agentSessions?: Map<string, AgentSession>;
9386

9487
labelInstances?: SelectionLabelInstance[];
@@ -107,14 +100,12 @@ export const OverlayCanvas: Component<OverlayCanvasProps> = (props) => {
107100
selection: { canvas: null, context: null },
108101
grabbed: { canvas: null, context: null },
109102
processing: { canvas: null, context: null },
110-
inspect: { canvas: null, context: null },
111103
};
112104

113105
let selectionAnimations: AnimatedBounds[] = [];
114106
let dragAnimation: AnimatedBounds | null = null;
115107
let grabbedAnimations: AnimatedBounds[] = [];
116108
let processingAnimations: AnimatedBounds[] = [];
117-
let inspectAnimations: AnimatedBounds[] = [];
118109

119110
const canvasColorSpace: PredefinedColorSpace = supportsDisplayP3()
120111
? "display-p3"
@@ -276,6 +267,30 @@ export const OverlayCanvas: Component<OverlayCanvasProps> = (props) => {
276267
);
277268
};
278269

270+
const drawBoxModelZone = (
271+
context: OffscreenCanvasRenderingContext2D,
272+
outerX: number,
273+
outerY: number,
274+
outerWidth: number,
275+
outerHeight: number,
276+
innerX: number,
277+
innerY: number,
278+
innerWidth: number,
279+
innerHeight: number,
280+
fillColor: string,
281+
opacity: number,
282+
) => {
283+
if (outerWidth <= 0 || outerHeight <= 0) return;
284+
285+
context.globalAlpha = opacity;
286+
context.beginPath();
287+
context.rect(outerX, outerY, outerWidth, outerHeight);
288+
context.rect(innerX, innerY, innerWidth, innerHeight);
289+
context.fillStyle = fillColor;
290+
context.fill("evenodd");
291+
context.globalAlpha = 1;
292+
};
293+
279294
const renderSelectionLayer = () => {
280295
const layer = layers.selection;
281296
if (!layer.context) return;
@@ -286,15 +301,64 @@ export const OverlayCanvas: Component<OverlayCanvasProps> = (props) => {
286301
if (!props.selectionVisible) return;
287302

288303
const style = LAYER_STYLES.selection;
304+
const boxModel = props.inspectBoxModel;
289305

290306
for (const animation of selectionAnimations) {
291307
const effectiveOpacity = props.selectionIsFading ? 0 : animation.opacity;
308+
const { x, y, width, height } = animation.current;
309+
310+
if (boxModel) {
311+
const hasMargin =
312+
boxModel.marginTop > 0 ||
313+
boxModel.marginRight > 0 ||
314+
boxModel.marginBottom > 0 ||
315+
boxModel.marginLeft > 0;
316+
317+
if (hasMargin) {
318+
drawBoxModelZone(
319+
context,
320+
x - boxModel.marginLeft,
321+
y - boxModel.marginTop,
322+
width + boxModel.marginLeft + boxModel.marginRight,
323+
height + boxModel.marginTop + boxModel.marginBottom,
324+
x,
325+
y,
326+
width,
327+
height,
328+
OVERLAY_FILL_COLOR_MARGIN,
329+
effectiveOpacity,
330+
);
331+
}
332+
333+
const hasPadding =
334+
boxModel.paddingTop > 0 ||
335+
boxModel.paddingRight > 0 ||
336+
boxModel.paddingBottom > 0 ||
337+
boxModel.paddingLeft > 0;
338+
339+
if (hasPadding) {
340+
drawBoxModelZone(
341+
context,
342+
x,
343+
y,
344+
width,
345+
height,
346+
x + boxModel.paddingLeft,
347+
y + boxModel.paddingTop,
348+
width - boxModel.paddingLeft - boxModel.paddingRight,
349+
height - boxModel.paddingTop - boxModel.paddingBottom,
350+
OVERLAY_FILL_COLOR_PADDING,
351+
effectiveOpacity,
352+
);
353+
}
354+
}
355+
292356
drawRoundedRectangle(
293357
context,
294-
animation.current.x,
295-
animation.current.y,
296-
animation.current.width,
297-
animation.current.height,
358+
x,
359+
y,
360+
width,
361+
height,
298362
animation.borderRadius,
299363
style.fillColor,
300364
style.borderColor,
@@ -341,10 +405,8 @@ export const OverlayCanvas: Component<OverlayCanvasProps> = (props) => {
341405
renderSelectionLayer();
342406
renderBoundsLayer("grabbed", grabbedAnimations);
343407
renderBoundsLayer("processing", processingAnimations);
344-
renderBoundsLayer("inspect", inspectAnimations);
345408

346409
const layerRenderOrder: LayerName[] = [
347-
"inspect",
348410
"drag",
349411
"selection",
350412
"grabbed",
@@ -482,14 +544,6 @@ export const OverlayCanvas: Component<OverlayCanvasProps> = (props) => {
482544
}
483545
}
484546

485-
for (const animation of inspectAnimations) {
486-
if (animation.isInitialized) {
487-
if (interpolateBounds(animation, LAYER_STYLES.inspect.lerpFactor)) {
488-
shouldContinueAnimating = true;
489-
}
490-
}
491-
}
492-
493547
compositeAllLayers();
494548

495549
if (shouldContinueAnimating) {
@@ -691,35 +745,6 @@ export const OverlayCanvas: Component<OverlayCanvasProps> = (props) => {
691745
),
692746
);
693747

694-
createEffect(
695-
on(
696-
() => [props.inspectVisible, props.inspectBounds] as const,
697-
([isVisible, bounds]) => {
698-
if (!isVisible || !bounds || bounds.length === 0) {
699-
inspectAnimations = [];
700-
scheduleAnimationFrame();
701-
return;
702-
}
703-
704-
inspectAnimations = bounds.map((ancestorBounds, index) => {
705-
const animationId = `inspect-${index}`;
706-
const existingAnimation = inspectAnimations.find(
707-
(animation) => animation.id === animationId,
708-
);
709-
710-
if (existingAnimation) {
711-
updateAnimationTarget(existingAnimation, ancestorBounds);
712-
return existingAnimation;
713-
}
714-
715-
return createAnimatedBounds(animationId, ancestorBounds);
716-
});
717-
718-
scheduleAnimationFrame();
719-
},
720-
),
721-
);
722-
723748
onMount(() => {
724749
initializeCanvas();
725750
scheduleAnimationFrame();

packages/react-grab/src/components/renderer.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,10 @@ export const ReactGrabRenderer: Component<ReactGrabRendererProps> = (props) => {
4141
selectionBoundsMultiple={props.selectionBoundsMultiple}
4242
selectionShouldSnap={props.selectionShouldSnap}
4343
selectionIsFading={props.selectionLabelStatus === "fading"}
44-
inspectVisible={props.inspectVisible}
45-
inspectBounds={props.inspectBounds}
4644
dragVisible={props.dragVisible}
4745
dragBounds={props.dragBounds}
4846
grabbedBoxes={props.grabbedBoxes}
47+
inspectBoxModel={props.inspectPropertiesState?.boxModel}
4948
agentSessions={props.agentSessions}
5049
labelInstances={props.labelInstances}
5150
/>
@@ -138,8 +137,7 @@ export const ReactGrabRenderer: Component<ReactGrabRendererProps> = (props) => {
138137
actionCycleState={props.selectionActionCycleState}
139138
arrowNavigationState={props.selectionArrowNavigationState}
140139
onArrowNavigationSelect={props.onArrowNavigationSelect}
141-
inspectNavigationState={props.inspectNavigationState}
142-
onInspectSelect={props.onInspectSelect}
140+
inspectPropertiesState={props.inspectPropertiesState}
143141
filePath={props.selectionFilePath}
144142
onInputChange={props.onInputChange}
145143
onSubmit={props.onInputSubmit}

0 commit comments

Comments
 (0)