Skip to content

Conversation

@ryujunghy3on
Copy link
Contributor

@ryujunghy3on ryujunghy3on commented Jan 19, 2026

Screenshot 2026-01-19 at 9 01 07 PM

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Alt key modifier support to corner radius adjustments; when all corners are uniform and Alt is not pressed, updates apply to all corners simultaneously.
  • Refactor

    • Improved corner radius geometry calculations and drag handle positioning logic.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Jan 19, 2026

@ryujunghy3on is attempting to deploy a commit to the Grida Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

Walkthrough

This PR adds Alt key modifier support to corner radius gestures in the canvas editor. The altKey state is threaded from the UI drag handler through the action dispatch layer and reducers, enabling conditional logic where pressing Alt maintains uniform corner radius updates versus individual corner adjustments. The corner-radius-handle component is optimized with useMemo for performance.

Changes

Cohort / File(s) Summary
Type Definitions & API
editor/grida-canvas/editor.i.ts, editor/grida-canvas/action.ts, editor/grida-canvas/editor.ts
Added optional altKey?: boolean parameter to surfaceStartCornerRadiusGesture method signature and to GestureCornerRadius type; updated action payload to include altKey in corner-radius gesture variant
Corner Radius Handle Component
editor/grida-canvas-react/viewport/ui/corner-radius-handle.tsx
Introduced useMemo for performance; added currentRadius resolution logic for multiple node types; enhanced drag behavior to pass altKey to gesture start; computed dynamic handle positioning and conditional label rendering based on radius state
State & Reducer Management
editor/grida-canvas/reducers/surface.reducer.ts, editor/grida-canvas/reducers/event-target.reducer.ts
Updated gesture payload destructuring to include altKey; implemented uniform vs. non-uniform corner radius update logic: applies unified corner_radius when Alt is unpressed and all corners are uniform; falls back to individual corner updates otherwise

Sequence Diagram

sequenceDiagram
    participant UI as Corner Radius Handle
    participant API as Editor API
    participant Dispatch as Action Dispatch
    participant Reducer as Reducers
    participant State as Gesture State

    UI->>UI: onDragStart detects Alt key
    UI->>API: surfaceStartCornerRadiusGesture(selection, anchor, altKey)
    API->>Dispatch: dispatch surface/gesture/start
    Dispatch->>Reducer: corner-radius gesture payload with altKey
    Reducer->>Reducer: destructure altKey from gesture
    Reducer->>State: store altKey in draft.gesture.altKey
    Reducer->>Reducer: on drag: check altKey & corner uniformity
    alt altKey unpressed AND corners uniform
        Reducer->>State: update unified corner_radius + all rectangular variants
    else altKey pressed OR non-uniform
        Reducer->>State: update individual corner (nw/ne/se/sw)
    end
    State-->>UI: reflect updated radius values
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Daily RC #486: Modifies corner-radius gesture handling and reducer logic with modifier key state threading.

Suggested reviewers

  • softmarshmallow

Poem

🐰 A hop, skip, and Alt-key press,
Corners dance without distress,
Uniform radii when needed most,
Or individual—each radius host,
Memoized swift, performance blessed! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Improve corner radius handle UX and positioning' accurately reflects the main changes, which involve enhancing corner radius handle positioning logic and gesture behavior (UX improvements through Alt key support and dynamic handle/label positioning).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@vercel
Copy link

vercel bot commented Jan 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
blog Ready Ready Preview, Comment Jan 19, 2026 0:14am
grida Ready Ready Preview, Comment Jan 19, 2026 0:14am

Request Review

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
editor/grida-canvas/reducers/event-target.reducer.ts (1)

735-825: The Alt-key behavior contradicts the stated feature goal.

The commit message says "altKey support for uniform adjustments," but the current implementation forces per-corner adjustments when Alt is held. Either the condition should be flipped to altKey || isUniform (if Alt should enforce uniformity), or the feature needs clarification on the intended UX.

🔧 Suggested fix (if Alt should enforce uniform updates)
-              if (!altKey && isUniform) {
+              if (altKey || isUniform) {

Please verify the intended behavior and update accordingly.

🤖 Fix all issues with AI agents
In `@editor/grida-canvas-react/viewport/ui/corner-radius-handle.tsx`:
- Around line 181-189: The labelPositions object uses labelOffsets.X for bottom
vertical offsets in the 'se' and 'sw' entries causing bottom labels to use the X
constant instead of the vertical Y offset; update the 'se' and 'sw' entries to
use labelOffsets.Y_BOTTOM for their bottom values (keeping left/right as
labelOffsets.X and top entries using labelOffsets.Y_TOP), so the returned value
from labelPositions[anchor] places bottom labels with the correct vertical
spacing (references: labelPositions, anchor, labelOffsets.Y_BOTTOM,
labelOffsets.Y_TOP, minmargin).
- Around line 60-138: The geometry calculation currently compares an unscaled
currentRadius against pixel margins, which breaks under zoom/anisotropic
transforms; update geometry's radius math to compute a scaled radius in pixels
(e.g., scaledRadiusX = currentRadius * scaleX, scaledRadiusY = currentRadius *
scaleY, scaledRadiusPx = Math.max(|scaledRadiusX|, |scaledRadiusY|)), then use
scaledRadiusPx when computing minmargin and useMarginBased (minmargin =
Math.max(scaledRadiusPx + size, margin); useMarginBased = scaledRadiusPx <
margin). Keep the existing offsets (they already use currentRadius *
scaleX/scaleY) and ensure these renamed/added scaled values are used in the same
useMemo that defines minmargin/useMarginBased and referenced by handleStyle.

Comment on lines +60 to +138
const geometry = useMemo(() => {
const br = editor.geometryProvider.getNodeAbsoluteBoundingRect(node_id);
if (!br) return null;

const boundingSurfaceRect = cmath.rect.transform(br, transform);
const [scaleX, scaleY] = cmath.transform.getScale(transform);
const w = boundingSurfaceRect.width;
const h = boundingSurfaceRect.height;
const minmargin = Math.max(currentRadius + size, margin);
const useMarginBased = currentRadius < margin;

// Corner coordinates: C = (C_x, C_y)
const corners = {
nw: [0, 0],
ne: [w, 0],
se: [w, h],
sw: [0, h],
} as const;
const [C_x, C_y] = corners[anchor];

// Arc center offset: O = (O_x, O_y) = (r * s_x * sign_x, r * s_y * sign_y)
const offsets = {
nw: [currentRadius * scaleX, currentRadius * scaleY],
ne: [-currentRadius * scaleX, currentRadius * scaleY],
se: [-currentRadius * scaleX, -currentRadius * scaleY],
sw: [currentRadius * scaleX, -currentRadius * scaleY],
} as const;
const [O_x, O_y] = offsets[anchor];

// Center coordinates: M = (M_x, M_y) = (w/2, h/2)
const M_x = w / 2;
const M_y = h / 2;

// Handle position relative to center: H = (H_x, H_y) = (C + O - M)
const H_x = C_x + O_x - M_x;
const H_y = C_y + O_y - M_y;

return {
w,
h,
scaleX,
scaleY,
minmargin,
useMarginBased,
H_x,
H_y,
M_x,
M_y,
};
}, [editor.geometryProvider, node_id, anchor, currentRadius, transform, size, margin]);

// Calculate handle position: at arc center O when radius >= margin, otherwise at corner with margin
const handleStyle = useMemo(() => {
if (!geometry) return null;

const { useMarginBased, H_x, H_y, minmargin } = geometry;

if (!useMarginBased && currentRadius > 0) {
// Handle at arc center: H = (C + O - M) relative to center
return {
left: `calc(50% + ${H_x}px)`,
top: `calc(50% + ${H_y}px)`,
transform: "translate(-50%, -50%)",
};
}

// Handle at corner with margin: position = minmargin from edge
const positions = {
nw: { top: `${minmargin}px`, left: `${minmargin}px` },
ne: { top: `${minmargin}px`, right: `${minmargin}px` },
se: { bottom: `${minmargin}px`, right: `${minmargin}px` },
sw: { bottom: `${minmargin}px`, left: `${minmargin}px` },
} as const;

return {
...positions[anchor],
transform: `translate(${anchor[1] === "w" ? "-50%" : "50%"}, ${anchor[0] === "n" ? "-50%" : "50%"})`,
};
}, [geometry, anchor, currentRadius]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Scale-aware margin logic needed for zoomed/anisotropic transforms.
useMarginBased / minmargin compare an unscaled radius against pixel margins, so at zoom ≠ 1 the handle can jump to the wrong mode.

🧩 Suggested fix (use scaled radius in px)
-    const minmargin = Math.max(currentRadius + size, margin);
-    const useMarginBased = currentRadius < margin;
+    const radiusPx = Math.min(
+      Math.abs(currentRadius * scaleX),
+      Math.abs(currentRadius * scaleY)
+    );
+    const minmargin = Math.max(radiusPx + size, margin);
+    const useMarginBased = radiusPx < margin;
@@
-    return {
+    return {
       w,
       h,
       scaleX,
       scaleY,
+      radiusPx,
       minmargin,
       useMarginBased,
       H_x,
       H_y,
       M_x,
       M_y,
     };
@@
-    const { useMarginBased, H_x, H_y, minmargin } = geometry;
+    const { useMarginBased, H_x, H_y, minmargin, radiusPx } = geometry;
@@
-    if (!useMarginBased && currentRadius > 0) {
+    if (!useMarginBased && radiusPx > 0) {
🤖 Prompt for AI Agents
In `@editor/grida-canvas-react/viewport/ui/corner-radius-handle.tsx` around lines
60 - 138, The geometry calculation currently compares an unscaled currentRadius
against pixel margins, which breaks under zoom/anisotropic transforms; update
geometry's radius math to compute a scaled radius in pixels (e.g., scaledRadiusX
= currentRadius * scaleX, scaledRadiusY = currentRadius * scaleY, scaledRadiusPx
= Math.max(|scaledRadiusX|, |scaledRadiusY|)), then use scaledRadiusPx when
computing minmargin and useMarginBased (minmargin = Math.max(scaledRadiusPx +
size, margin); useMarginBased = scaledRadiusPx < margin). Keep the existing
offsets (they already use currentRadius * scaleX/scaleY) and ensure these
renamed/added scaled values are used in the same useMemo that defines
minmargin/useMarginBased and referenced by handleStyle.

Comment on lines +181 to +189
// Margin-based: label inside direction from handle
const labelPositions = {
nw: { top: `${minmargin + labelOffsets.Y_TOP}px`, left: `${minmargin + labelOffsets.X}px` },
ne: { top: `${minmargin + labelOffsets.Y_TOP}px`, right: `${minmargin + labelOffsets.X}px` },
se: { bottom: `${minmargin + labelOffsets.X}px`, right: `${minmargin + labelOffsets.X}px` },
sw: { bottom: `${minmargin + labelOffsets.X}px`, left: `${minmargin + labelOffsets.X}px` },
} as const;

return labelPositions[anchor];
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Bottom label offsets use the X constant (likely typo).
For se/sw, the vertical offset should probably use Y_BOTTOM to match the top-corner spacing and avoid the label sitting too close.

🔧 Suggested fix
-      se: { bottom: `${minmargin + labelOffsets.X}px`, right: `${minmargin + labelOffsets.X}px` },
-      sw: { bottom: `${minmargin + labelOffsets.X}px`, left: `${minmargin + labelOffsets.X}px` },
+      se: { bottom: `${minmargin + labelOffsets.Y_BOTTOM}px`, right: `${minmargin + labelOffsets.X}px` },
+      sw: { bottom: `${minmargin + labelOffsets.Y_BOTTOM}px`, left: `${minmargin + labelOffsets.X}px` },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Margin-based: label inside direction from handle
const labelPositions = {
nw: { top: `${minmargin + labelOffsets.Y_TOP}px`, left: `${minmargin + labelOffsets.X}px` },
ne: { top: `${minmargin + labelOffsets.Y_TOP}px`, right: `${minmargin + labelOffsets.X}px` },
se: { bottom: `${minmargin + labelOffsets.X}px`, right: `${minmargin + labelOffsets.X}px` },
sw: { bottom: `${minmargin + labelOffsets.X}px`, left: `${minmargin + labelOffsets.X}px` },
} as const;
return labelPositions[anchor];
// Margin-based: label inside direction from handle
const labelPositions = {
nw: { top: `${minmargin + labelOffsets.Y_TOP}px`, left: `${minmargin + labelOffsets.X}px` },
ne: { top: `${minmargin + labelOffsets.Y_TOP}px`, right: `${minmargin + labelOffsets.X}px` },
se: { bottom: `${minmargin + labelOffsets.Y_BOTTOM}px`, right: `${minmargin + labelOffsets.X}px` },
sw: { bottom: `${minmargin + labelOffsets.Y_BOTTOM}px`, left: `${minmargin + labelOffsets.X}px` },
} as const;
return labelPositions[anchor];
🤖 Prompt for AI Agents
In `@editor/grida-canvas-react/viewport/ui/corner-radius-handle.tsx` around lines
181 - 189, The labelPositions object uses labelOffsets.X for bottom vertical
offsets in the 'se' and 'sw' entries causing bottom labels to use the X constant
instead of the vertical Y offset; update the 'se' and 'sw' entries to use
labelOffsets.Y_BOTTOM for their bottom values (keeping left/right as
labelOffsets.X and top entries using labelOffsets.Y_TOP), so the returned value
from labelPositions[anchor] places bottom labels with the correct vertical
spacing (references: labelPositions, anchor, labelOffsets.Y_BOTTOM,
labelOffsets.Y_TOP, minmargin).

@softmarshmallow softmarshmallow merged commit 29afd3b into gridaco:main Jan 19, 2026
4 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants