Skip to content

feat: add mouse wheel input support for slider controls#38

Open
unsafe0x0 wants to merge 2 commits intoKartikLabhshetwar:mainfrom
unsafe0x0:unsafe0x0
Open

feat: add mouse wheel input support for slider controls#38
unsafe0x0 wants to merge 2 commits intoKartikLabhshetwar:mainfrom
unsafe0x0:unsafe0x0

Conversation

@unsafe0x0
Copy link
Copy Markdown

added mouse wheel input to editor slider controls

  • added a useWheelInput hook that enables mouse-wheel scrolling to adjust slider values on sliders throughout the editor

Copilot AI review requested due to automatic review settings December 10, 2025 14:18
@vercel
Copy link
Copy Markdown

vercel Bot commented Dec 10, 2025

@unsafe0x0 is attempting to deploy a commit to the Kartik Labhshetwar's projects Team on Vercel.

A member of the Team first needs to authorize it.

@unsafe0x0
Copy link
Copy Markdown
Author

preview:

2025-12-10.19-51-05.mp4

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces mouse wheel input support for slider controls throughout the editor by implementing a reusable useWheelInput hook. The hook enables users to adjust numeric values by scrolling the mouse wheel over slider controls, with support for modifier keys (Shift for 10x multiplier, Ctrl/Cmd for 0.1x multiplier). The implementation is systematically applied across 9 component files, covering text overlays, image overlays, mockups, borders, shadows, 3D perspective transforms, and background effects.

Key Changes:

  • Created a new useWheelInput custom hook that attaches wheel event listeners to container elements and calculates adjusted values based on wheel direction and modifier keys
  • Applied the hook to 37 slider controls across the editor, attaching refs to slider container divs to enable wheel input
  • Maintained existing slider functionality while adding wheel input as an enhancement

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
hooks/useWheelInput.ts New custom hook implementing wheel event handling with step calculation, precision rounding, and min/max clamping
components/text-overlay/text-overlay-controls.tsx Added wheel input support for 7 text overlay sliders (font size, opacity, shadow properties, position)
components/overlays/overlay-controls.tsx Added wheel input support for 5 image overlay sliders (size, rotation, opacity, X/Y position)
components/mockups/MockupControls.tsx Added wheel input support for 5 mockup sliders (size, rotation, opacity, X/Y position)
components/editor/style-tabs.tsx Added wheel input support for 3 image style sliders (border radius, scale, opacity)
components/editor/editor-right-panel.tsx Added wheel input support for 2 background sliders (opacity, border radius)
components/controls/ShadowControls.tsx Added wheel input support for 4 shadow sliders (blur, offsetX, offsetY, spread)
components/controls/Perspective3DControls.tsx Added wheel input support for 6 perspective transform sliders (perspective, rotations, translations)
components/controls/BorderControls.tsx Added wheel input support for 2 border sliders (width, padding)
components/controls/BackgroundEffects.tsx Added wheel input support for 2 background effect sliders (blur, noise)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +157 to +211
const { ref: fontSizeRef } = useWheelInput({
value: selectedOverlay?.fontSize ?? 24,
onChange: (val) => handleUpdateFontSize([val]),
min: 12,
max: 150,
step: 1,
});

const { ref: opacityRef } = useWheelInput({
value: selectedOverlay?.opacity ?? 1,
onChange: (val) => handleUpdateOpacity([val]),
min: 0,
max: 1,
step: 0.01,
});

const { ref: shadowBlurRef } = useWheelInput({
value: selectedOverlay?.textShadow.blur ?? 0,
onChange: (val) => handleUpdateTextShadow({ blur: val }),
min: 0,
max: 20,
step: 1,
});

const { ref: shadowOffsetXRef } = useWheelInput({
value: selectedOverlay?.textShadow.offsetX ?? 0,
onChange: (val) => handleUpdateTextShadow({ offsetX: val }),
min: -20,
max: 20,
step: 1,
});

const { ref: shadowOffsetYRef } = useWheelInput({
value: selectedOverlay?.textShadow.offsetY ?? 0,
onChange: (val) => handleUpdateTextShadow({ offsetY: val }),
min: -20,
max: 20,
step: 1,
});

const { ref: positionXRef } = useWheelInput({
value: selectedOverlay?.position.x ?? 50,
onChange: (val) => handleUpdatePosition('x', [val]),
min: 0,
max: 100,
step: 1,
});

const { ref: positionYRef } = useWheelInput({
value: selectedOverlay?.position.y ?? 50,
onChange: (val) => handleUpdatePosition('y', [val]),
min: 0,
max: 100,
step: 1,
});
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

The useWheelInput hooks are initialized outside of any conditional check, but they use selectedOverlay values which can be undefined when no overlay is selected. This could lead to the hooks being called with undefined values. While the default values (using ??) handle this, the hooks will still trigger onChange callbacks with potentially incorrect values when the wheel is scrolled without a selection.

Consider wrapping these hook calls in a conditional or moving them inside the conditional render block that checks for selectedOverlay, or add early returns in the onChange handlers to prevent updates when no overlay is selected.

Copilot uses AI. Check for mistakes.
Comment on lines +81 to 114

const { ref: rotationRef } = useWheelInput({
value: selectedOverlay?.rotation ?? 0,
onChange: (val) => handleUpdateRotation([val]),
min: 0,
max: 360,
step: 1,
});

const { ref: opacityRef } = useWheelInput({
value: selectedOverlay?.opacity ?? 1,
onChange: (val) => handleUpdateOpacity([val]),
min: 0,
max: 1,
step: 0.01,
});

const { ref: posXRef } = useWheelInput({
value: selectedOverlay?.position.x ?? 0,
onChange: (val) => handleUpdatePosition('x', [val]),
min: 0,
max: 800,
step: 1,
});

const { ref: posYRef } = useWheelInput({
value: selectedOverlay?.position.y ?? 0,
onChange: (val) => handleUpdatePosition('y', [val]),
min: 0,
max: 600,
step: 1,
});


Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

The useWheelInput hooks are initialized with selectedOverlay values which can be undefined when no overlay is selected. While the default values (using ??) prevent crashes, if a user scrolls the wheel over these controls when no overlay is selected, the onChange handlers will be called but won't update anything due to the guards in handleUpdateSize, etc. This is fine functionally, but it's processing unnecessary events.

The handlers already check for selectedOverlay, so this is safe but not optimal. Consider documenting this behavior or conditionally rendering the refs only when an overlay is selected.

Suggested change
const { ref: rotationRef } = useWheelInput({
value: selectedOverlay?.rotation ?? 0,
onChange: (val) => handleUpdateRotation([val]),
min: 0,
max: 360,
step: 1,
});
const { ref: opacityRef } = useWheelInput({
value: selectedOverlay?.opacity ?? 1,
onChange: (val) => handleUpdateOpacity([val]),
min: 0,
max: 1,
step: 0.01,
});
const { ref: posXRef } = useWheelInput({
value: selectedOverlay?.position.x ?? 0,
onChange: (val) => handleUpdatePosition('x', [val]),
min: 0,
max: 800,
step: 1,
});
const { ref: posYRef } = useWheelInput({
value: selectedOverlay?.position.y ?? 0,
onChange: (val) => handleUpdatePosition('y', [val]),
min: 0,
max: 600,
step: 1,
});
// Only initialize wheel input hooks if an overlay is selected
let rotationRef, opacityRef, posXRef, posYRef;
if (selectedOverlay) {
({ ref: rotationRef } = useWheelInput({
value: selectedOverlay.rotation,
onChange: (val) => handleUpdateRotation([val]),
min: 0,
max: 360,
step: 1,
}));
({ ref: opacityRef } = useWheelInput({
value: selectedOverlay.opacity,
onChange: (val) => handleUpdateOpacity([val]),
min: 0,
max: 1,
step: 0.01,
}));
({ ref: posXRef } = useWheelInput({
value: selectedOverlay.position.x,
onChange: (val) => handleUpdatePosition('x', [val]),
min: 0,
max: 800,
step: 1,
}));
({ ref: posYRef } = useWheelInput({
value: selectedOverlay.position.y,
onChange: (val) => handleUpdatePosition('y', [val]),
min: 0,
max: 600,
step: 1,
}));
}

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +104
const { ref: sizeRef } = useWheelInput({
value: selectedMockup?.size ?? 600,
onChange: (val) => handleUpdateSize([val]),
min: 200,
max: 1200,
step: 10,
});

const { ref: rotationRef } = useWheelInput({
value: selectedMockup?.rotation ?? 0,
onChange: (val) => handleUpdateRotation([val]),
min: 0,
max: 360,
step: 1,
});

const { ref: opacityRef } = useWheelInput({
value: selectedMockup?.opacity ?? 1,
onChange: (val) => handleUpdateOpacity([val]),
min: 0,
max: 1,
step: 0.01,
});

const { ref: posXRef } = useWheelInput({
value: selectedMockup?.position.x ?? 0,
onChange: (val) => handleUpdatePosition('x', [val]),
min: 0,
max: 1600,
step: 1,
});

const { ref: posYRef } = useWheelInput({
value: selectedMockup?.position.y ?? 0,
onChange: (val) => handleUpdatePosition('y', [val]),
min: 0,
max: 1000,
step: 1,
});
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

The useWheelInput hooks are initialized with selectedMockup values which can be undefined when no mockup is selected. While the default values (using ??) prevent crashes, if a user scrolls the wheel over these controls when no mockup is selected, the onChange handlers will be called but won't update anything due to the guards in handleUpdateSize, etc. This is safe functionally but processes unnecessary events.

Consider documenting this behavior or conditionally rendering the refs only when a mockup is selected.

Copilot uses AI. Check for mistakes.
Comment thread hooks/useWheelInput.ts
Comment on lines +5 to +11
interface UseWheelInputOptions {
value: number;
onChange: (value: number) => void;
min?: number;
max?: number;
step?: number;
}
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

The hook and its interface lack JSDoc comments explaining its purpose, parameters, and return value. Consider adding documentation, especially to describe the modifier key behavior (Shift for 10x, Ctrl/Cmd for 0.1x) which is important for users to know:

/**
 * Hook that enables mouse wheel input for numeric value adjustments.
 * 
 * @param value - Current numeric value
 * @param onChange - Callback function when value changes
 * @param min - Minimum allowed value (default: -Infinity)
 * @param max - Maximum allowed value (default: Infinity)
 * @param step - Base increment/decrement step (default: 1)
 * 
 * @returns Object with a ref to attach to the container element
 * 
 * @remarks
 * Supports modifier keys:
 * - Shift: 10x step multiplier
 * - Ctrl/Cmd: 0.1x step multiplier
 */

Copilot uses AI. Check for mistakes.
Comment thread hooks/useWheelInput.ts

const handleWheel = useCallback(
(e: WheelEvent) => {
e.preventDefault();
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

Calling e.preventDefault() unconditionally blocks the browser's native scroll behavior when the user scrolls over slider controls. This can be disruptive to user experience, especially when users are trying to scroll the page and their cursor happens to be over a slider.

Consider adding a condition to only prevent default when the element is focused or when a modifier key is held, allowing normal scrolling otherwise. For example:

if (document.activeElement === e.target || e.shiftKey || e.ctrlKey || e.metaKey) {
  e.preventDefault();
}
Suggested change
e.preventDefault();
if (
document.activeElement === e.currentTarget ||
e.shiftKey ||
e.ctrlKey ||
e.metaKey
) {
e.preventDefault();
}

Copilot uses AI. Check for mistakes.
Comment thread hooks/useWheelInput.ts
Comment on lines +22 to +36
const handleWheel = useCallback(
(e: WheelEvent) => {
e.preventDefault();
const delta = e.deltaY > 0 ? -step : step;
const multiplier = e.shiftKey ? 10 : e.ctrlKey || e.metaKey ? 0.1 : 1;
const rawValue = value + delta * multiplier;
const precision = step < 1 ? Math.ceil(-Math.log10(step)) : 0;
const newValue = Math.min(
max,
Math.max(min, parseFloat(rawValue.toFixed(precision))),
);
onChange(newValue);
},
[value, onChange, min, max, step],
);
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

The handleWheel callback depends on the value prop, causing it to be recreated on every value change. This means the event listener is removed and re-added on every slider adjustment, which is inefficient. Consider using a ref to store the latest value instead:

const valueRef = useRef(value);
useEffect(() => {
  valueRef.current = value;
}, [value]);

const handleWheel = useCallback(
  (e: WheelEvent) => {
    e.preventDefault();
    const delta = e.deltaY > 0 ? -step : step;
    const multiplier = e.shiftKey ? 10 : e.ctrlKey || e.metaKey ? 0.1 : 1;
    const rawValue = valueRef.current + delta * multiplier;
    // ... rest of the logic
  },
  [onChange, min, max, step], // value removed from dependencies
);

Copilot uses AI. Check for mistakes.
Comment thread hooks/useWheelInput.ts Outdated
const delta = e.deltaY > 0 ? -step : step;
const multiplier = e.shiftKey ? 10 : e.ctrlKey || e.metaKey ? 0.1 : 1;
const rawValue = value + delta * multiplier;
const precision = step < 1 ? Math.ceil(-Math.log10(step)) : 0;
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

The precision calculation using Math.ceil(-Math.log10(step)) will produce incorrect results for steps like 0.01 when the step is represented imprecisely in floating point (e.g., Math.log10(0.01) might not be exactly -2). Additionally, for steps like 10, this produces precision of -1, which is incorrect. Consider a more robust approach:

const precision = step < 1 ? Math.abs(step.toString().split('.')[1]?.length ?? 0) : 0;

Or use a safer calculation:

const precision = step < 1 ? Math.max(0, -Math.floor(Math.log10(step))) : 0;
Suggested change
const precision = step < 1 ? Math.ceil(-Math.log10(step)) : 0;
const precision = step < 1 ? Math.abs(step.toString().split('.')[1]?.length ?? 0) : 0;

Copilot uses AI. Check for mistakes.
@KartikLabhshetwar
Copy link
Copy Markdown
Owner

hi @unsafe0x0 can you please fix the suggestion from copilot.

@unsafe0x0
Copy link
Copy Markdown
Author

@KartikLabhshetwar fixed the suggestion from copilot.

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.

3 participants